From c8dc27ebb386ef70df4749e3ec0042ec45aa76d3 Mon Sep 17 00:00:00 2001 From: fromhsc Date: Tue, 2 Apr 2024 11:43:36 +0800 Subject: [PATCH 01/32] =?UTF-8?q?EulerCopilot=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E4=BB=A3=E7=A0=81=20Signed-off-by:=20He=20Sh?= =?UTF-8?q?oucheng=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 59 ++++++++++++++++++++++++++++++++++++++++++ src/cmd_generate.py | 36 ++++++++++++++++++++++++++ src/euler_copilot.spec | 55 +++++++++++++++++++++++++++++++++++++++ src/eulercopilot.py | 23 ++++++++++++++++ src/interact.py | 17 ++++++++++++ src/setup.py | 13 ++++++++++ 6 files changed, 203 insertions(+) create mode 100644 .idea/workspace.xml create mode 100644 src/cmd_generate.py create mode 100644 src/euler_copilot.spec create mode 100644 src/eulercopilot.py create mode 100644 src/interact.py create mode 100644 src/setup.py diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..e25a264 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 1712028748459 + + + + + + + \ No newline at end of file diff --git a/src/cmd_generate.py b/src/cmd_generate.py new file mode 100644 index 0000000..d164703 --- /dev/null +++ b/src/cmd_generate.py @@ -0,0 +1,36 @@ +import json +import shlex +import interact +import requests +import os + + +def cmd_generate(question): + endpoint = "https://rag.test.osinfra.cn/kb/shell" + + data = {"question": question} + try: + res = requests.post( + endpoint, + headers={"Content-Type": "application/json"}, + json=data, + stream=False + ) + result = res.json() + except Exception as _: + return None + + shell = os.environ.get("SHELL", "/bin/sh") + ans = result.get("answer", None) + try: + ans = json.loads(ans) + except Exception as _: + return None + for cmd in list(ans.values()): + print(cmd) + + if interact.query_yes_or_no("\033[33mEulerCopilot:\033[0m 是否执行以上命令?"): + for cmd in list(ans.values()): + full_command = f"{shell} -c {shlex.quote(cmd)}" + os.system(full_command) + diff --git a/src/euler_copilot.spec b/src/euler_copilot.spec new file mode 100644 index 0000000..660383b --- /dev/null +++ b/src/euler_copilot.spec @@ -0,0 +1,55 @@ +Name: euler_copilot +Version: 1.0 +Release: 1 +BuildArch: x86_64 +Summary: euler_copilot +SOURCE: copilot.tar.gz +SOURCE1: setup.py +License: GPL +URL: https://www.openeuler.org/zh/ + +BuildRequires: python3-devel python3-Cython gcc + +%description +test examples for xats, create xats rpm package + + +%prep +rm -rf %{name}-%{version} +tar -xf %{SOURCE0} +mv copilot %{name}-%{version} +cp %{SOURCE1} %{name}-%{version} + +%post +cat << EOF >> /root/.bashrc +# eulercopilot +function commandline { + stty sane && python3 /eulercopilot/eulercopilot.py \$READLINE_LINE + READLINE_LINE= + stty erase ^H +} +bind -x '"\C-l":commandline' +EOF +source /root/.bashrc + +%postun +sed -i '/^# eulercopilot/,+6d' /root/.bashrc +source /root/.bashrc + +%build +pushd %{name}-%{version} +python3 setup.py build_ext + +%install +%define _unpackaged_files_terminate_build 0 +mkdir -p -m 700 %{buildroot}/%{python3_sitelib} +mkdir -p -m 700 %{buildroot}/eulercopilot + +install -c -m 0700 %{_builddir}/%{name}-%{version}/build/lib.linux-x86_64-3.9/cmd_generate.cpython-39-x86_64-linux-gnu.so %{buildroot}/%{python3_sitelib} +install -c -m 0700 %{_builddir}/%{name}-%{version}/build/lib.linux-x86_64-3.9/interact.cpython-39-x86_64-linux-gnu.so %{buildroot}/%{python3_sitelib} +install -c -m 0700 %{_builddir}/%{name}-%{version}/eulercopilot.py %{buildroot}/eulercopilot + +%files +%{python3_sitelib}/cmd_generate.cpython-39-x86_64-linux-gnu.so +%{python3_sitelib}/interact.cpython-39-x86_64-linux-gnu.so +/eulercopilot/eulercopilot.py \ No newline at end of file diff --git a/src/eulercopilot.py b/src/eulercopilot.py new file mode 100644 index 0000000..f1319c9 --- /dev/null +++ b/src/eulercopilot.py @@ -0,0 +1,23 @@ +import sys +import cmd_generate +import interact + +args = sys.argv +user_input = args[1] + + +if __name__ == "__main__": + while True: + print(f"\033[35m用户请求:\033[0m {user_input}") + print("\033[33mEulerCopilot:\033[0m 已经收到您的请求,正在思考答案中") + + satisfaction_query = "\033[33mEulerCopilot:\033[0m 是否继续本次服务?" + cmd_generate.cmd_generate(user_input) + if not interact.query_yes_or_no(satisfaction_query): + print("\033[33mEulerCopilot:\033[0m 很高兴为您服务,下次再见~") + sys.exit(0) + else: + # 用户继续提出需求: + print("\033[33mEulerCopilot:\033[0m请继续提出您的需求:") + user_input = sys.stdin.readline() + diff --git a/src/interact.py b/src/interact.py new file mode 100644 index 0000000..b015504 --- /dev/null +++ b/src/interact.py @@ -0,0 +1,17 @@ +import sys + + +def query_yes_or_no(question): + valid = {"yes": True, "y": True, "no": False, "n": False} + prompt = " [y/n] " + + while True: + sys.stdout.write(question + prompt) + choice = input().lower() + if choice == "": + return valid["y"] + elif choice in valid: + return valid[choice] + else: + sys.stdout.write("请用 'yes' or 'no' 回答" "(or 'y' or 'n').\n") + diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..a465a00 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup +from Cython.Build import cythonize + +setup(ext_modules=cythonize(["interact.py", "cmd_generate.py"])) + + +function commandline { + stty sane && python3 /eulercopilot/eulercopilot.py $READLINE_LINE + READLINE_LINE= + stty erase ^H +} +bind -x '"\C-l":commandline' + -- Gitee From d0e8dfe685651e4835330fe828dab125ff6bddf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Mon, 15 Apr 2024 16:35:34 +0800 Subject: [PATCH 02/32] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=98=9F=E7=81=AB?= =?UTF-8?q?=E5=A4=A7=E6=A8=A1=E5=9E=8B=E5=90=8E=E7=AB=AF=EF=BC=9B=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E7=BB=84=E7=BB=87=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 星火chat模式支持上下文 * v1.1 * 尝试修复升级 copilot 时,快捷键配置被异常删除的问题 * 重构 backends,修复星火上下文支持 * 代码高亮;星火chat模式支持上下文 Signed-off-by: 史鸿宇 --- .gitignore | 19 ++++ .idea/workspace.xml | 59 ------------ distribution/build_rpm.sh | 27 ++++++ distribution/create_tarball.py | 70 ++++++++++++++ distribution/eulercopilot.spec | 36 +++++++ src/app/__init__.py | 0 src/app/copilot_app.py | 76 +++++++++++++++ src/app/copilot_init.py | 36 +++++++ src/backends/__init__.py | 0 src/backends/framework_api.py | 98 +++++++++++++++++++ src/backends/llm_service.py | 13 +++ src/backends/spark_api.py | 165 ++++++++++++++++++++++++++++++++ src/cmd_generate.py | 36 ------- src/copilot.py | 46 +++++++++ src/euler_copilot.spec | 55 ----------- src/eulercopilot.py | 23 ----- src/eulercopilot_shortcut.sh | 11 +++ src/setup.py | 70 ++++++++++++-- src/utilities/__init__.py | 0 src/utilities/config_manager.py | 103 ++++++++++++++++++++ src/{ => utilities}/interact.py | 13 +-- src/utilities/os_info.py | 47 +++++++++ 22 files changed, 815 insertions(+), 188 deletions(-) create mode 100644 .gitignore delete mode 100644 .idea/workspace.xml create mode 100644 distribution/build_rpm.sh create mode 100644 distribution/create_tarball.py create mode 100644 distribution/eulercopilot.spec create mode 100644 src/app/__init__.py create mode 100644 src/app/copilot_app.py create mode 100644 src/app/copilot_init.py create mode 100644 src/backends/__init__.py create mode 100644 src/backends/framework_api.py create mode 100644 src/backends/llm_service.py create mode 100644 src/backends/spark_api.py delete mode 100644 src/cmd_generate.py create mode 100755 src/copilot.py delete mode 100644 src/euler_copilot.spec delete mode 100644 src/eulercopilot.py create mode 100644 src/eulercopilot_shortcut.sh create mode 100644 src/utilities/__init__.py create mode 100644 src/utilities/config_manager.py rename src/{ => utilities}/interact.py (45%) create mode 100644 src/utilities/os_info.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4649808 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*venv/ +**/__pycache__/ +**/*.pyc + +# build +**/build/ +**/dist/ +**/*.egg-info/ +**/*.so +**/*.o + +.env + +# ide +.idea +.vscode + +# Cython +*.c diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index e25a264..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - 1712028748459 - - - - - - - \ No newline at end of file diff --git a/distribution/build_rpm.sh b/distribution/build_rpm.sh new file mode 100644 index 0000000..22d439d --- /dev/null +++ b/distribution/build_rpm.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Check if ~/rpmbuild directory exists; if not, run rpmdev-setuptree +if [ ! -d ~/rpmbuild ]; then + rpmdev-setuptree +fi + +# Run the Python script +python3 create_tarball.py + +# Find the generated tarball file and move it to ~/rpmbuild/SOURCES +generated_tarball=$(find . -maxdepth 1 -type f -name "*.tar.gz" -printf "%f\n") +mv "./$generated_tarball" ~/rpmbuild/SOURCES/ + +# Locate the spec file in the parent directory +spec_file="eulercopilot.spec" + +if [[ ! -f "$spec_file" ]]; then + echo "Error: Could not find the spec file ($spec_file) in the parent directory." + exit 1 +fi + +# Remove old builds +rm -f ~/rpmbuild/RPMS/$(uname -m)/eulercopilot-* + +# Build the RPM package using rpmbuild +rpmbuild --define "_timestamp $(date +%s)" -bb "$spec_file" --nodebuginfo diff --git a/distribution/create_tarball.py b/distribution/create_tarball.py new file mode 100644 index 0000000..ec4b6bf --- /dev/null +++ b/distribution/create_tarball.py @@ -0,0 +1,70 @@ +import os +import re +import shutil +import tarfile + + +def extract_spec_fields(spec_file): + with open(spec_file, 'r', encoding='utf-8') as f: + content = f.read() + + name_pattern = re.compile(r'^Name:\s*(.+)$', re.MULTILINE) + version_pattern = re.compile(r'^Version:\s*(.+)$', re.MULTILINE) + + name_match = name_pattern.search(content) + version_match = version_pattern.search(content) + + if name_match and version_match: + return { + 'name': name_match.group(1).strip(), + 'version': version_match.group(1).strip() + } + else: + raise ValueError("Could not find Name or Version fields in the spec file") + + +def create_cache_folder(spec_info, src_dir): + name = spec_info['name'] + version = spec_info['version'] + + cache_folder_name = f"{name}-{version}" + cache_folder_path = os.path.join(os.path.dirname(src_dir), cache_folder_name) + + if not os.path.exists(cache_folder_path): + os.makedirs(cache_folder_path) + + copy_files(src_dir, cache_folder_path) + create_tarball(cache_folder_path, f"{cache_folder_name}.tar.gz") + delete_cache_folder(cache_folder_path) + + +def copy_files(src_dir, dst_dir): + for dirpath, _, files in os.walk(src_dir): + relative_path = os.path.relpath(dirpath, src_dir) + target_path = os.path.join(dst_dir, relative_path) + + if not os.path.exists(target_path): + os.makedirs(target_path) + + for file in files: + if file.endswith('.py') or file.endswith('.sh'): + src_file = os.path.join(dirpath, file) + dst_file = os.path.join(target_path, file) + os.link(src_file, dst_file) # 使用硬链接以节省空间和时间 + + +def create_tarball(folder_path, tarball_name): + with tarfile.open(tarball_name, "w:gz") as tar: + tar.add(folder_path, arcname=os.path.basename(folder_path)) + + +def delete_cache_folder(folder_path): + shutil.rmtree(folder_path) + + +if __name__ == "__main__": + SPEC_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "eulercopilot.spec")) + SRC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) + + info = extract_spec_fields(SPEC_FILE) + create_cache_folder(info, SRC_DIR) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec new file mode 100644 index 0000000..182f70c --- /dev/null +++ b/distribution/eulercopilot.spec @@ -0,0 +1,36 @@ +Name: eulercopilot +Version: 1.1 +Release: 1%{?dist}%{?_timestamp} +Group: Applications/Utilities +Summary: EulerCopilot CLI Tool +Source: %{name}-%{version}.tar.gz +License: MulanPSL-2.0 +URL: https://www.openeuler.org/zh/ + +BuildRequires: python3-devel python3-setuptools python3-Cython gcc + +Requires: python3 python3-pip + +%description +EulerCopilot CLI Tool + +%prep +%setup -q + +%build +python3 setup.py build_ext + +%install +%define _unpackaged_files_terminate_build 0 +python3 setup.py install --root=%{buildroot} --single-version-externally-managed --record=INSTALLED_FILES +install -d %{buildroot}/etc/profile.d +install -c -m 0755 %{_builddir}/%{name}-%{version}/eulercopilot_shortcut.sh %{buildroot}/etc/profile.d + +%files -f INSTALLED_FILES +%defattr(-,root,root,-) +/etc/profile.d/eulercopilot_shortcut.sh + +%post +/usr/bin/python3 -m pip install --upgrade websockets >/dev/null +/usr/bin/python3 -m pip install --upgrade requests >/dev/null +/usr/bin/python3 -m pip install --upgrade rich >/dev/null diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/copilot_app.py b/src/app/copilot_app.py new file mode 100644 index 0000000..746cc61 --- /dev/null +++ b/src/app/copilot_app.py @@ -0,0 +1,76 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +# pylint: disable=W0611 + +import os +import readline # noqa: F401 +import shlex +import sys +import uuid +from typing import Union + +from backends import framework_api, llm_service, spark_api +from utilities import config_manager, interact + +EXIT_MESSAGE = "\033[33m>>>\033[0m 很高兴为您服务,下次再见~" + + +def execute_shell_command(cmd: str) -> None: + """Execute a shell command and exit.""" + shell = os.environ.get("SHELL", "/bin/sh") + full_command = f"{shell} -c {shlex.quote(cmd)}" + os.system(full_command) + + +def handle_user_input(service: llm_service.LLMService, + user_input: str, mode: str) -> None: + """Process user input based on the given flag and backend configuration.""" + if mode == 'shell': + cmd = service.get_shell_answer(user_input) + if cmd and interact.query_yes_or_no("\033[33m是否执行命令?\033[0m "): + execute_shell_command(cmd) + sys.exit(0) + elif mode == 'chat': + service.get_general_answer(user_input) + + +def main(user_input: Union[str, None]): + config = config_manager.load_config() + backend = config.get('backend') + mode = config.get('query_mode') + service: llm_service.LLMService = None + if backend == 'framework': + service = framework_api.Framework( + url=config.get('framework_url'), + api_key=config.get('framework_api_key'), + session_id=str(uuid.uuid4().hex) + ) + elif backend == 'spark': + service = spark_api.Spark( + app_id=config.get('spark_app_id'), + api_key=config.get('spark_api_key'), + api_secret=config.get('spark_api_secret'), + spark_url=config.get('spark_url'), + domain=config.get('spark_domain') + ) + + if service is None: + sys.stderr.write("\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m") + sys.exit(1) + + if mode == 'shell': + print("\033[33m当前模式:Shell 命令生成\033[0m") + if mode == 'chat': + print("\033[33m当前模式:智能问答\033[0m 输入 \"exit\" 或按下 Ctrl+C 退出服务") + + try: + while True: + if user_input is None: + user_input = input("\033[35m>>>\033[0m ") + if user_input.lower().startswith('exit'): + print(EXIT_MESSAGE) + sys.exit(0) + handle_user_input(service, user_input, mode) + user_input = None # Reset user_input for next iteration (only if continuing service) + except KeyboardInterrupt: + print() + sys.exit(0) diff --git a/src/app/copilot_init.py b/src/app/copilot_init.py new file mode 100644 index 0000000..9fb519c --- /dev/null +++ b/src/app/copilot_init.py @@ -0,0 +1,36 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +# pylint: disable=W0611 + +import os +import readline # noqa: F401 + +from utilities import config_manager + + +def setup_copilot(): + def _init_config(): + if not os.path.exists(config_manager.CONFIG_DIR): + os.makedirs(config_manager.CONFIG_DIR) + if not os.path.exists(config_manager.CONFIG_PATH): + config_manager.init_config() + + def _prompt_for_config(config_key: str, prompt_text: str): + config_value = input(prompt_text) + config_manager.update_config(config_key, config_value) + + if not os.path.exists(config_manager.CONFIG_PATH): + _init_config() + + config = config_manager.load_config() + if config.get('backend') == 'spark': + if config.get('spark_app_id') == '': + _prompt_for_config('spark_app_id', '请输入你的星火大模型 App ID:') + if config.get('spark_api_key') == '': + _prompt_for_config('spark_api_key', '请输入你的星火大模型 API Key:') + if config.get('spark_api_secret') == '': + _prompt_for_config('spark_api_secret', '请输入你的星火大模型 App Secret:') + if config.get('backend') == 'framework': + if config.get('framework_url') == '': + _prompt_for_config('framework_url', '请输入你的 EulerCopilot URL:') + if config.get('framework_api_key') == '': + _prompt_for_config('framework_api_key', '请输入你的 EulerCopilot API Key:') diff --git a/src/backends/__init__.py b/src/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backends/framework_api.py b/src/backends/framework_api.py new file mode 100644 index 0000000..1b3f0c2 --- /dev/null +++ b/src/backends/framework_api.py @@ -0,0 +1,98 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import json +import os +import sys + +import requests +from rich.console import Console +from rich.live import Live +from rich.markdown import Markdown +from rich.spinner import Spinner + +from backends.llm_service import LLMService + +CONFIG_PATH = "/eulercopilot" +COOKIE_CONFIG = os.path.join(CONFIG_PATH, "cookie") +CSRF_CONFIG = os.path.join(CONFIG_PATH, "csrf") + +BASE_DOMAIN = "qa-robot-openeuler.test.osinfra.cn" +BASE_URL = f"https://{BASE_DOMAIN}" + + +class Framework(LLMService): + def __init__(self, url, api_key, session_id): + self.endpoint: str = url + self.api_key: str = api_key + self.session_id: str = session_id + self.content: str = "" + # 富文本显示 + self.console = Console() + + def get_general_answer(self, question: str) -> str: + self.endpoint = f"{BASE_URL}/stream/get_stream_answer" + cookie = self._read_config(COOKIE_CONFIG) + csrf_token = self._read_config(CSRF_CONFIG) + + headers = self._get_headers(cookie, csrf_token) + + user_url = f"{BASE_URL}/rag/authorize/user" + r = requests.request("GET", user_url, data="", headers=headers, timeout=10) + if r.status_code != 200: + sys.stderr.write(f"{r.status_code} 登录凭证已过期,请重新登录\n") + + data = {"question": question, "session_id": self.session_id} + self._stream_response(headers, data) + + return self.content + + def get_shell_answer(self, question: str) -> str: + question = "请用单行shell命令回答以下问题:\n" + question + \ + "\n\n请直接以纯文本形式回复shell命令,不要添加任何多余内容。\n" + \ + "请注意你是 openEuler 的小助手,你所回答的命令必须被 openEuler 系统支持" + return self.get_general_answer(question).replace("`", "") + + def _stream_response(self, headers, data): + spinner = Spinner('material') + with Live(console=self.console) as live: + live.update(spinner, refresh=True) + response = requests.post( + self.endpoint, + headers=headers, + json=data, + stream=True, + timeout=60 + ) + if response.status_code != 200: + sys.stderr.write(f"{response.status_code} 请求失败\n") + return + for line in response.iter_lines(): + if line is None: + continue + content = line.decode('utf-8').strip("data: ") + try: + jcontent = json.loads(content) + except json.JSONDecodeError: + continue + else: + chunk = jcontent.get("content", "") + self.content += chunk + live.update(Markdown(self.content, code_theme='github-dark'), refresh=True) + + def _read_config(self, config_name: str) -> str: + try: + with open(config_name, "r", encoding="utf-8") as c: + return c.read().strip() + except FileNotFoundError: + with open(config_name, "w", encoding="utf-8") as c: + return "" + + def _get_headers(self, cookie: str, csrf_token: str) -> dict: + return { + "Accept": "application/json", + "Accept-Language": "zh-CN,zh-Hans,en-US;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json", + "Cookie": cookie, + "X-CSRF-Token": csrf_token + } diff --git a/src/backends/llm_service.py b/src/backends/llm_service.py new file mode 100644 index 0000000..69f66ae --- /dev/null +++ b/src/backends/llm_service.py @@ -0,0 +1,13 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +from abc import ABC, abstractmethod + + +class LLMService(ABC): + @abstractmethod + def get_general_answer(self, question: str) -> str: + pass + + @abstractmethod + def get_shell_answer(self, question: str) -> str: + pass diff --git a/src/backends/spark_api.py b/src/backends/spark_api.py new file mode 100644 index 0000000..40868cf --- /dev/null +++ b/src/backends/spark_api.py @@ -0,0 +1,165 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import asyncio +import base64 +import hashlib +import hmac +import json +import re +import sys +from datetime import datetime +from time import mktime +from urllib.parse import urlencode, urlparse +from wsgiref.handlers import format_date_time + +import websockets +from rich.console import Console +from rich.live import Live +from rich.markdown import Markdown +from rich.spinner import Spinner +from utilities.os_info import get_os_info + +from backends.llm_service import LLMService + + +class Spark(LLMService): + def __init__(self, app_id, api_key, api_secret, spark_url, domain, max_tokens=4096): + self.app_id: str = app_id + self.api_key: str = api_key + self.api_secret: str = api_secret + self.spark_url: str = spark_url + self.host = urlparse(spark_url).netloc + self.path = urlparse(spark_url).path + self.domain: str = domain + self.max_tokens: int = max_tokens + self.answer: str = '' + self.history: list = [] + # 富文本显示 + self.console = Console() + + def get_general_answer(self, question: str) -> str: + asyncio.get_event_loop().run_until_complete( + self._query_spark_ai(question) + ) + return self.answer + + def get_shell_answer(self, question: str) -> str: + query = f'请用单行shell命令回答以下问题:\n{question}\n\ + \n要求:\n请直接回复命令,不要添加任何多余内容;\n\ + 当前操作系统是:{get_os_info()},请返回符合当前系统要求的命令。' + return self._extract_shell_code_blocks(self.get_general_answer(query)) + + async def _query_spark_ai(self, query: str): + url = self._create_url() + self.answer = '' + spinner = Spinner('material') + try: + with Live(console=self.console) as live: + live.update(spinner, refresh=True) + async with websockets.connect(url) as websocket: + data = json.dumps(self._gen_params(query)) + await websocket.send(data) + + while True: + try: + message = await websocket.recv() + data = json.loads(message) + code = data['header']['code'] + if code != 0: + message = data['header']['message'] + live.update(f'请求错误: {code}\n{message}', refresh=True) + await websocket.close() + else: + choices = data["payload"]["choices"] + status = choices["status"] + content = choices["text"][0]["content"] + self.answer += content + live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) + if status == 2: + self.history.append({"role": "assistant", "content": self.answer}) + break + except websockets.exceptions.ConnectionClosed: + break + + except websockets.exceptions.InvalidStatusCode: + sys.stderr.write('\033[1;31m请求错误!\033[0m\n请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。') + print('输入 "copilot --settings" 来查看和编辑配置') + + def _create_url(self): + now = datetime.now() # 生成RFC1123格式的时间戳 + date = format_date_time(mktime(now.timetuple())) + + signature_origin = f'host: {self.host}\ndate: {date}\nGET {self.path} HTTP/1.1' + + # 进行hmac-sha256进行加密 + signature_sha = hmac.new(self.api_secret.encode('utf-8'), + signature_origin.encode('utf-8'), + digestmod=hashlib.sha256).digest() + + signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8') + + authorization_origin = f'api_key="{self.api_key}", algorithm="hmac-sha256", ' + \ + f'headers="host date request-line", signature="{signature_sha_base64}"' + + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + + # 将请求的鉴权参数组合为字典 + v = { + "authorization": authorization, + "date": date, + "host": self.host + } + # 拼接鉴权参数,生成url + url = self.spark_url + '?' + urlencode(v) + # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致 + return url + + def _get_length(self, context: list) -> int: + length = 0 + for content in context: + temp = content["content"] + leng = len(temp) + length += leng + return length + + def _check_len(self, context: list) -> list: + while self._get_length(context) > self.max_tokens / 2: + del context[0] + return context + + def _gen_params(self, query: str): + """ + 通过appid和用户的提问来生成请参数 + """ + self.history.append({"role": "user", "content": query}) + history = self._check_len( + self.history if len(self.history) < 5 else self.history[-5:] + ) + data = { + "header": { + "app_id": self.app_id, + "uid": "1234", + }, + "parameter": { + "chat": { + "domain": self.domain, + "temperature": 0.5, + "max_tokens": self.max_tokens, + "auditing": "default", + } + }, + "payload": { + "message": { + "text": history + } + } + } + return data + + def _extract_shell_code_blocks(self, markdown_text): + shell_code_pattern = re.compile(r'```shell\n(?P(?:\n|.)*?)\n```', re.DOTALL) + matches = shell_code_pattern.finditer(markdown_text) + cmds: list = [match.group('code') for match in matches] + if len(cmds) > 0: + return cmds[0] + return markdown_text diff --git a/src/cmd_generate.py b/src/cmd_generate.py deleted file mode 100644 index d164703..0000000 --- a/src/cmd_generate.py +++ /dev/null @@ -1,36 +0,0 @@ -import json -import shlex -import interact -import requests -import os - - -def cmd_generate(question): - endpoint = "https://rag.test.osinfra.cn/kb/shell" - - data = {"question": question} - try: - res = requests.post( - endpoint, - headers={"Content-Type": "application/json"}, - json=data, - stream=False - ) - result = res.json() - except Exception as _: - return None - - shell = os.environ.get("SHELL", "/bin/sh") - ans = result.get("answer", None) - try: - ans = json.loads(ans) - except Exception as _: - return None - for cmd in list(ans.values()): - print(cmd) - - if interact.query_yes_or_no("\033[33mEulerCopilot:\033[0m 是否执行以上命令?"): - for cmd in list(ans.values()): - full_command = f"{shell} -c {shlex.quote(cmd)}" - os.system(full_command) - diff --git a/src/copilot.py b/src/copilot.py new file mode 100755 index 0000000..edd7bc5 --- /dev/null +++ b/src/copilot.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import argparse + +from app.copilot_app import main +from app.copilot_init import setup_copilot +from utilities.config_manager import edit_config, select_backend, select_query_mode + + +def parse_arguments(): + parser = argparse.ArgumentParser(prog="copilot", description="EulerCopilot 命令行工具") + parser.add_argument("user_input", nargs="?", default=None, help="用自然语言提问") + parser.add_argument("--init", action="store_true", help="初始化 copilot 设置") + parser.add_argument("--shell", "-s", action="store_true", help="选择 Shell 命令模式") + parser.add_argument("--chat", "-c", action="store_true", help="选择智能问答模式") + parser.add_argument("--diagnose", "-d", action="store_true", help="选择智能诊断模式") + parser.add_argument("--tuning", "-t", action="store_true", help="选择智能调优模式") + parser.add_argument("--backend", action="store_true", help="选择大语言模型后端") + parser.add_argument("--settings", action="store_true", help="编辑 copilot 设置") + return parser.parse_args() + + +def run_command_line(): + args = parse_arguments() + + if args.init: + setup_copilot() + elif args.shell: + select_query_mode(0) + elif args.chat: + select_query_mode(1) + elif args.diagnose: + select_query_mode(2) + elif args.tuning: + select_query_mode(3) + elif args.backend: + select_backend() + elif args.settings: + edit_config() + else: + main(args.user_input) + + +if __name__ == "__main__": + run_command_line() diff --git a/src/euler_copilot.spec b/src/euler_copilot.spec deleted file mode 100644 index 660383b..0000000 --- a/src/euler_copilot.spec +++ /dev/null @@ -1,55 +0,0 @@ -Name: euler_copilot -Version: 1.0 -Release: 1 -BuildArch: x86_64 -Summary: euler_copilot -SOURCE: copilot.tar.gz -SOURCE1: setup.py -License: GPL -URL: https://www.openeuler.org/zh/ - -BuildRequires: python3-devel python3-Cython gcc - -%description -test examples for xats, create xats rpm package - - -%prep -rm -rf %{name}-%{version} -tar -xf %{SOURCE0} -mv copilot %{name}-%{version} -cp %{SOURCE1} %{name}-%{version} - -%post -cat << EOF >> /root/.bashrc -# eulercopilot -function commandline { - stty sane && python3 /eulercopilot/eulercopilot.py \$READLINE_LINE - READLINE_LINE= - stty erase ^H -} -bind -x '"\C-l":commandline' -EOF -source /root/.bashrc - -%postun -sed -i '/^# eulercopilot/,+6d' /root/.bashrc -source /root/.bashrc - -%build -pushd %{name}-%{version} -python3 setup.py build_ext - -%install -%define _unpackaged_files_terminate_build 0 -mkdir -p -m 700 %{buildroot}/%{python3_sitelib} -mkdir -p -m 700 %{buildroot}/eulercopilot - -install -c -m 0700 %{_builddir}/%{name}-%{version}/build/lib.linux-x86_64-3.9/cmd_generate.cpython-39-x86_64-linux-gnu.so %{buildroot}/%{python3_sitelib} -install -c -m 0700 %{_builddir}/%{name}-%{version}/build/lib.linux-x86_64-3.9/interact.cpython-39-x86_64-linux-gnu.so %{buildroot}/%{python3_sitelib} -install -c -m 0700 %{_builddir}/%{name}-%{version}/eulercopilot.py %{buildroot}/eulercopilot - -%files -%{python3_sitelib}/cmd_generate.cpython-39-x86_64-linux-gnu.so -%{python3_sitelib}/interact.cpython-39-x86_64-linux-gnu.so -/eulercopilot/eulercopilot.py \ No newline at end of file diff --git a/src/eulercopilot.py b/src/eulercopilot.py deleted file mode 100644 index f1319c9..0000000 --- a/src/eulercopilot.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -import cmd_generate -import interact - -args = sys.argv -user_input = args[1] - - -if __name__ == "__main__": - while True: - print(f"\033[35m用户请求:\033[0m {user_input}") - print("\033[33mEulerCopilot:\033[0m 已经收到您的请求,正在思考答案中") - - satisfaction_query = "\033[33mEulerCopilot:\033[0m 是否继续本次服务?" - cmd_generate.cmd_generate(user_input) - if not interact.query_yes_or_no(satisfaction_query): - print("\033[33mEulerCopilot:\033[0m 很高兴为您服务,下次再见~") - sys.exit(0) - else: - # 用户继续提出需求: - print("\033[33mEulerCopilot:\033[0m请继续提出您的需求:") - user_input = sys.stdin.readline() - diff --git a/src/eulercopilot_shortcut.sh b/src/eulercopilot_shortcut.sh new file mode 100644 index 0000000..2f83ba9 --- /dev/null +++ b/src/eulercopilot_shortcut.sh @@ -0,0 +1,11 @@ +run_copilot() { + local readline="${READLINE_LINE}" + if [[ -z "${readline}" ]]; then + READLINE_LINE="copilot " + READLINE_POINT=${#READLINE_LINE} + elif [[ ! "${readline}" =~ ^copilot ]]; then + READLINE_LINE="copilot '${readline}'" + READLINE_POINT=${#READLINE_LINE} + fi +} +bind -x '"\C-h": run_copilot' diff --git a/src/setup.py b/src/setup.py index a465a00..ca62d2e 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,13 +1,69 @@ -from distutils.core import setup +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import os + from Cython.Build import cythonize +from Cython.Distutils import build_ext +from setuptools import setup +from setuptools.extension import Extension + -setup(ext_modules=cythonize(["interact.py", "cmd_generate.py"])) +def add_py_files(module_name): + return [ + os.path.join(module_name, f) + for f in os.listdir(module_name) + if f.endswith('.py') + ] -function commandline { - stty sane && python3 /eulercopilot/eulercopilot.py $READLINE_LINE - READLINE_LINE= - stty erase ^H +# 定义编译选项 +cython_compile_options = { + 'language_level': '3', + 'annotate': False, # 生成 HTML 注解文件 + 'compiler_directives': {}, } -bind -x '"\C-l":commandline' +# 定义 Cython 编译规则 +cython_files = [] +cython_files += add_py_files('app') +cython_files += add_py_files('backends') +cython_files += add_py_files('utilities') + +extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] + +# 定义 setup() 参数 +setup( + name='copilot', + version='1.0', + description='EulerCopilot CLI Tool', + author='Hongyu Shi', + author_email='shihongyu15@huawei.com', + url='https://gitee.com/openeuler-customization/euler-copilot-shell', + py_modules=['copilot'], + ext_modules=cythonize( + extensions, + compiler_directives=cython_compile_options['compiler_directives'], + annotate=cython_compile_options['annotate'], + language_level=cython_compile_options['language_level'] + ), + cmdclass={'build_ext': build_ext}, + include_package_data=True, + zip_safe=False, + classifiers=[ + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'License :: OSI Approved :: Mulan Permissive Software License, Version 2', # 木兰许可证 v2 + 'Operating System :: POSIX :: Linux', + 'Operating System :: MacOS :: MacOS X' + ], + python_requires='>=3.9', # Python 版本要求为 3.9 及以上 + install_requires=[ # 添加项目依赖的库:websockets 和 requests + 'websockets', + 'requests', + ], + entry_points={ + 'console_scripts': ['copilot=copilot:run_command_line'] + } +) diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utilities/config_manager.py b/src/utilities/config_manager.py new file mode 100644 index 0000000..360db68 --- /dev/null +++ b/src/utilities/config_manager.py @@ -0,0 +1,103 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import json +import os + +CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') +CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') + +EMPTY_CONFIG = { + "backend": "spark", + "query_mode": "shell", + "spark_app_id": "", + "spark_api_key": "", + "spark_api_secret": "", + "spark_url": "wss://spark-api.xf-yun.com/v3.5/chat", + "spark_domain": "generalv3.5", + "framework_api_key": "", + "framework_url": "" +} + + +def load_config() -> dict: + try: + with open(CONFIG_PATH, 'r', encoding='utf-8') as file: + config = json.load(file) + except FileNotFoundError: + init_config() + config = load_config() + return config + + +def write_config(config): + with open(CONFIG_PATH, 'w', encoding='utf-8') as json_file: + json.dump(config, json_file, indent=4) + + +def init_config(): + write_config(EMPTY_CONFIG) + + +def update_config(key: str, value): + if key not in EMPTY_CONFIG: + return + config = load_config() + config.update({key: value}) + write_config(config) + + +def select_query_mode(mode: int): + modes = ['shell', 'chat'] + if mode < len(modes): + update_config('query_mode', modes[mode]) + + +def select_backend(): + backends = ['spark', 'framework'] + print('\n\033[1;33m请选择大模型后端:\033[0m\n') + print('\t<1> 讯飞星火大模型 3.5') + print('\t<2> EulerCopilot') + print() + try: + while True: + backend_input = input('\033[33m>>>\033[0m ').strip() + try: + backend_index = int(backend_input) - 1 + backend = backends[backend_index] + except (ValueError, TypeError): + print('请输入正确的序号') + continue + else: + update_config('backend', backend) + break + except KeyboardInterrupt: + print('\n\033[1;31m用户已取消选择\033[0m\n') + + +def edit_config(): + config = load_config() + print('\n\033[1;33m当前设置:\033[0m') + format_string = "{:<32} {}".format + for key, value in config.items(): + print(f'- {format_string(key, value)}') + + print('\n\033[33m输入要修改的设置项以修改设置:\033[0m') + print('示例:') + print('>>> spark_api_key(按下回车)') + print('<<< (在此处输入你的星火大模型 API Key)') + print('* 输入空白值以退出程序') + print('* 建议在管理员指导下操作') + try: + while True: + key = input('\033[35m>>>\033[0m ') + if key in config: + value = input('\033[33m<<<\033[0m ') + if value == '': + break + config[key] = value + elif key == '': + break + else: + print('输入有误,请重试') + except KeyboardInterrupt: + print('\n\033[1;31m用户已取消编辑\033[0m\n') diff --git a/src/interact.py b/src/utilities/interact.py similarity index 45% rename from src/interact.py rename to src/utilities/interact.py index b015504..6d34afe 100644 --- a/src/interact.py +++ b/src/utilities/interact.py @@ -1,17 +1,14 @@ -import sys +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. - -def query_yes_or_no(question): +def query_yes_or_no(question: str) -> bool: valid = {"yes": True, "y": True, "no": False, "n": False} - prompt = " [y/n] " + prompt = " [Y/n] " while True: - sys.stdout.write(question + prompt) - choice = input().lower() + choice = input(question + prompt).lower() if choice == "": return valid["y"] elif choice in valid: return valid[choice] else: - sys.stdout.write("请用 'yes' or 'no' 回答" "(or 'y' or 'n').\n") - + print('请用 "yes (y)" 或 "no (n)" 回答') diff --git a/src/utilities/os_info.py b/src/utilities/os_info.py new file mode 100644 index 0000000..56a1576 --- /dev/null +++ b/src/utilities/os_info.py @@ -0,0 +1,47 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import platform +import re +import subprocess + + +def _exec_shell_cmd(cmd: list) -> subprocess.CompletedProcess: + return subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + + +def _porc_linux_info(shell_result: subprocess.CompletedProcess): + pattern = r'PRETTY_NAME="(.+?)"' + match = re.search(pattern, shell_result.stdout) + if match: + return match.group(1) # 返回括号内匹配的内容,即PRETTY_NAME的值 + return 'Unknown' + + +def _porc_macos_info(shell_result: subprocess.CompletedProcess): + macos_info = {} + if shell_result.returncode == 0: + lines = shell_result.stdout.splitlines() + for line in lines: + key, value = line.split(':\t\t', maxsplit=1) + macos_info[key.strip()] = value.strip() + product_name = macos_info.get('ProductName') + product_version = macos_info.get('ProductVersion') + if product_name is not None and product_version is not None: + return f'{product_name} {product_version}' + return 'Unknown' + + +def get_os_info() -> str: + system = platform.system() + if system == 'Linux': + return _porc_linux_info(_exec_shell_cmd(['cat', '/etc/os-release'])) + elif system == 'Darwin': + return _porc_macos_info(_exec_shell_cmd(['sw_vers'])) + else: + return system -- Gitee From ee4b92e8c518639e75b6ca38a65470889a2ef7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Mon, 15 Apr 2024 11:51:59 +0800 Subject: [PATCH 03/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/eulercopilot_shortcut.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/eulercopilot_shortcut.sh b/src/eulercopilot_shortcut.sh index 2f83ba9..2bb1a96 100644 --- a/src/eulercopilot_shortcut.sh +++ b/src/eulercopilot_shortcut.sh @@ -1,11 +1,13 @@ run_copilot() { + local terminal_settings=$(stty -g) local readline="${READLINE_LINE}" if [[ -z "${readline}" ]]; then READLINE_LINE="copilot " READLINE_POINT=${#READLINE_LINE} elif [[ ! "${readline}" =~ ^copilot ]]; then - READLINE_LINE="copilot '${readline}'" - READLINE_POINT=${#READLINE_LINE} + READLINE_LINE="" + stty sane && (copilot "${readline}") fi + stty "${terminal_settings}" } bind -x '"\C-h": run_copilot' -- Gitee From f416cfa97409e7c8e9495563dd69189e3683a952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Mon, 15 Apr 2024 19:11:30 +0800 Subject: [PATCH 04/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=98=9F=E7=81=AB?= =?UTF-8?q?=E5=A4=A7=E6=A8=A1=E5=9E=8B=E5=90=8E=E7=AB=AFshell=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/backends/spark_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/spark_api.py b/src/backends/spark_api.py index 40868cf..e3cfa3b 100644 --- a/src/backends/spark_api.py +++ b/src/backends/spark_api.py @@ -162,4 +162,4 @@ class Spark(LLMService): cmds: list = [match.group('code') for match in matches] if len(cmds) > 0: return cmds[0] - return markdown_text + return markdown_text.replace('`', '') -- Gitee From 7dc281a8cca49600c26645118fc69cda47523c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Mon, 15 Apr 2024 20:11:07 +0800 Subject: [PATCH 05/32] =?UTF-8?q?=E7=A6=81=E6=AD=A2=E2=80=9C=E6=9C=AA?= =?UTF-8?q?=E5=90=AF=E7=94=A8=E8=A1=8C=E7=BC=96=E8=BE=91=E2=80=9D=E8=AD=A6?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/eulercopilot_shortcut.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/eulercopilot_shortcut.sh b/src/eulercopilot_shortcut.sh index 2bb1a96..b6309da 100644 --- a/src/eulercopilot_shortcut.sh +++ b/src/eulercopilot_shortcut.sh @@ -10,4 +10,5 @@ run_copilot() { fi stty "${terminal_settings}" } -bind -x '"\C-h": run_copilot' + +bind -x '"\C-h": run_copilot' 2>/dev/null -- Gitee From 11be0d9554031c4bbede0d7bd040c881557a5e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 16 Apr 2024 09:37:44 +0800 Subject: [PATCH 06/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=9C=AA?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E7=9A=84=E6=83=85=E5=86=B5=E4=B8=8B?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E4=BD=BF=E7=94=A8=E4=BC=9A=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/utilities/config_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utilities/config_manager.py b/src/utilities/config_manager.py index 360db68..9932a20 100644 --- a/src/utilities/config_manager.py +++ b/src/utilities/config_manager.py @@ -35,6 +35,8 @@ def write_config(config): def init_config(): + if not os.path.exists(CONFIG_DIR): + os.makedirs(CONFIG_DIR) write_config(EMPTY_CONFIG) -- Gitee From 4472db3c900d7e6501696cc141b7dbc5a8a381c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 30 Apr 2024 18:30:49 +0800 Subject: [PATCH 07/32] =?UTF-8?q?=E5=88=A0=E9=99=A4=20framework=20?= =?UTF-8?q?=E6=97=A0=E7=94=A8=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/backends/framework_api.py | 36 +++-------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/backends/framework_api.py b/src/backends/framework_api.py index 1b3f0c2..985425d 100644 --- a/src/backends/framework_api.py +++ b/src/backends/framework_api.py @@ -1,7 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. import json -import os import sys import requests @@ -12,13 +11,6 @@ from rich.spinner import Spinner from backends.llm_service import LLMService -CONFIG_PATH = "/eulercopilot" -COOKIE_CONFIG = os.path.join(CONFIG_PATH, "cookie") -CSRF_CONFIG = os.path.join(CONFIG_PATH, "csrf") - -BASE_DOMAIN = "qa-robot-openeuler.test.osinfra.cn" -BASE_URL = f"https://{BASE_DOMAIN}" - class Framework(LLMService): def __init__(self, url, api_key, session_id): @@ -30,20 +22,9 @@ class Framework(LLMService): self.console = Console() def get_general_answer(self, question: str) -> str: - self.endpoint = f"{BASE_URL}/stream/get_stream_answer" - cookie = self._read_config(COOKIE_CONFIG) - csrf_token = self._read_config(CSRF_CONFIG) - - headers = self._get_headers(cookie, csrf_token) - - user_url = f"{BASE_URL}/rag/authorize/user" - r = requests.request("GET", user_url, data="", headers=headers, timeout=10) - if r.status_code != 200: - sys.stderr.write(f"{r.status_code} 登录凭证已过期,请重新登录\n") - + headers = self._get_headers() data = {"question": question, "session_id": self.session_id} self._stream_response(headers, data) - return self.content def get_shell_answer(self, question: str) -> str: @@ -79,20 +60,9 @@ class Framework(LLMService): self.content += chunk live.update(Markdown(self.content, code_theme='github-dark'), refresh=True) - def _read_config(self, config_name: str) -> str: - try: - with open(config_name, "r", encoding="utf-8") as c: - return c.read().strip() - except FileNotFoundError: - with open(config_name, "w", encoding="utf-8") as c: - return "" - - def _get_headers(self, cookie: str, csrf_token: str) -> dict: + def _get_headers(self) -> dict: return { "Accept": "application/json", - "Accept-Language": "zh-CN,zh-Hans,en-US;q=0.9", - "Accept-Encoding": "gzip, deflate, br", "Content-Type": "application/json", - "Cookie": cookie, - "X-CSRF-Token": csrf_token + 'Authorization': f'Bearer {self.api_key}' } -- Gitee From c091d2641ca5e5caf95108b7ec5e18f64bfbf26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 30 Apr 2024 18:31:19 +0800 Subject: [PATCH 08/32] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20OpenAI=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/app/copilot_app.py | 8 ++- src/app/copilot_init.py | 7 ++ src/backends/openai_api.py | 109 ++++++++++++++++++++++++++++++++ src/utilities/config_manager.py | 7 +- 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/backends/openai_api.py diff --git a/src/app/copilot_app.py b/src/app/copilot_app.py index 746cc61..384993b 100644 --- a/src/app/copilot_app.py +++ b/src/app/copilot_app.py @@ -8,7 +8,7 @@ import sys import uuid from typing import Union -from backends import framework_api, llm_service, spark_api +from backends import framework_api, llm_service, spark_api, openai_api from utilities import config_manager, interact EXIT_MESSAGE = "\033[33m>>>\033[0m 很高兴为您服务,下次再见~" @@ -52,6 +52,12 @@ def main(user_input: Union[str, None]): spark_url=config.get('spark_url'), domain=config.get('spark_domain') ) + elif backend == 'openai': + service = openai_api.OpenAI( + url=config.get('model_url'), + api_key=config.get('model_api_key'), + model=config.get('model_name') + ) if service is None: sys.stderr.write("\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m") diff --git a/src/app/copilot_init.py b/src/app/copilot_init.py index 9fb519c..4dff2d5 100644 --- a/src/app/copilot_init.py +++ b/src/app/copilot_init.py @@ -34,3 +34,10 @@ def setup_copilot(): _prompt_for_config('framework_url', '请输入你的 EulerCopilot URL:') if config.get('framework_api_key') == '': _prompt_for_config('framework_api_key', '请输入你的 EulerCopilot API Key:') + if config.get('backend') == 'openai': + if config.get('model_url') == '': + _prompt_for_config('model_url', '请输入你的大模型 URL:') + if config.get('model_api_key') == '': + _prompt_for_config('model_api_key', '请输入你的大模型 API Key:') + if config.get('model_name') == '': + _prompt_for_config('model_name', '请输入你的大模型名称:') diff --git a/src/backends/openai_api.py b/src/backends/openai_api.py new file mode 100644 index 0000000..7b77216 --- /dev/null +++ b/src/backends/openai_api.py @@ -0,0 +1,109 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import json +import sys +import re + +import requests +from rich.console import Console +from rich.live import Live +from rich.markdown import Markdown +from rich.spinner import Spinner +from utilities.os_info import get_os_info + +from backends.llm_service import LLMService + + +class OpenAI(LLMService): + def __init__(self, url, api_key, model = 'qwen-7b', max_tokens = 2048): + self.url: str = url + self.api_key: str = api_key + self.model: str = model + self.max_tokens: int = max_tokens + self.answer: str = '' + self.history: list = [] + # 富文本显示 + self.console = Console() + + def get_general_answer(self, question: str) -> str: + self._stream_response(question) + return self.answer + + def get_shell_answer(self, question: str) -> str: + query = f'请用单行shell命令回答以下问题:\n{question}\n\ + \n要求:\n请直接回复命令,不要添加任何多余内容;\n\ + 当前操作系统是:{get_os_info()},请返回符合当前系统要求的命令。' + return self._extract_shell_code_blocks(self.get_general_answer(query)) + + def _get_length(self, context: list) -> int: + length = 0 + for content in context: + temp = content['content'] + leng = len(temp) + length += leng + return length + + def _check_len(self, context: list) -> list: + while self._get_length(context) > self.max_tokens / 2: + del context[0] + return context + + def _gen_params(self, query: str, stream: bool = True): + self.history.append({'content': query, 'role': 'user'}) + history = self._check_len( + self.history if len(self.history) < 5 else self.history[-5:] + ) + return { + 'messages': history, + 'model': self.model, + 'stream': stream, + 'max_tokens': self.max_tokens, + 'temperature': 0.7, + 'top_p': 0.95 + } + + def _gen_headers(self): + return { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.api_key}' + } + + def _stream_response(self, query: str): + spinner = Spinner('material') + self.answer = '' + with Live(console=self.console) as live: + live.update(spinner, refresh=True) + response = requests.post( + self.url, + headers=self._gen_headers(), + data=json.dumps(self._gen_params(query)), + stream=True, + timeout=60 + ) + if response.status_code != 200: + sys.stderr.write(f'{response.status_code} 请求失败\n') + return + for line in response.iter_lines(): + if line is None: + continue + content = line.decode('utf-8').strip('data: ') + try: + jcontent = json.loads(content) + except json.JSONDecodeError: + continue + else: + chunk = jcontent['choices'][0]['delta']['content'] + finish_reason = jcontent['choices'][0]['finish_reason'] + self.answer += chunk + live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) + if finish_reason == 'stop': + self.history.append({'content': self.answer, 'role': 'assistant'}) + break + + def _extract_shell_code_blocks(self, markdown_text): + shell_code_pattern = re.compile(r'```shell\n(?P(?:\n|.)*?)\n```', re.DOTALL) + matches = shell_code_pattern.finditer(markdown_text) + cmds: list = [match.group('code') for match in matches] + if len(cmds) > 0: + return cmds[0] + return markdown_text.replace('`', '') diff --git a/src/utilities/config_manager.py b/src/utilities/config_manager.py index 9932a20..a6ddee6 100644 --- a/src/utilities/config_manager.py +++ b/src/utilities/config_manager.py @@ -7,7 +7,7 @@ CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') EMPTY_CONFIG = { - "backend": "spark", + "backend": "openai", "query_mode": "shell", "spark_app_id": "", "spark_api_key": "", @@ -15,7 +15,10 @@ EMPTY_CONFIG = { "spark_url": "wss://spark-api.xf-yun.com/v3.5/chat", "spark_domain": "generalv3.5", "framework_api_key": "", - "framework_url": "" + "framework_url": "", + "model_url": "http://localhost:1337/v1/chat/completions", + "model_api_key": "", + "model_name": "_" } -- Gitee From b9f5a22ebe9104498f5c56314b7b1920951243a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 7 May 2024 10:28:39 +0800 Subject: [PATCH 09/32] =?UTF-8?q?=E6=98=9F=E7=81=AB=E5=A4=A7=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=20&=20OpenAI-like=20=E6=94=AF=E6=8C=81=20system=20pro?= =?UTF-8?q?mpt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 OpenAI 接口模式添加默认的系统 Prompt - 更改快捷键为 Ctrl + O - system prompt中增加当前用户是否为 root; - 星火大模型支持 system prompt Signed-off-by: 史鸿宇 --- src/app/copilot_app.py | 66 +++++++++++++++---- src/app/copilot_cli.py | 46 ++++++++++++++ src/backends/framework_api.py | 4 +- src/backends/llm_service.py | 43 +++++++++++++ src/backends/openai_api.py | 77 +++++++++++------------ src/backends/spark_api.py | 67 ++++++++------------ src/copilot.py | 41 +----------- src/eulercopilot_shortcut.sh | 19 +++++- src/setup.py | 3 +- src/utilities/config_manager.py | 13 ++-- src/utilities/{os_info.py => env_info.py} | 5 ++ 11 files changed, 240 insertions(+), 144 deletions(-) create mode 100644 src/app/copilot_cli.py rename src/utilities/{os_info.py => env_info.py} (95%) diff --git a/src/app/copilot_app.py b/src/app/copilot_app.py index 384993b..08854b3 100644 --- a/src/app/copilot_app.py +++ b/src/app/copilot_app.py @@ -1,8 +1,9 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. # pylint: disable=W0611 -import os +import re import readline # noqa: F401 +import subprocess import shlex import sys import uuid @@ -14,11 +15,50 @@ from utilities import config_manager, interact EXIT_MESSAGE = "\033[33m>>>\033[0m 很高兴为您服务,下次再见~" -def execute_shell_command(cmd: str) -> None: +def check_shell_features(cmd: str) -> bool: + patterns = [ + # 重定向 + r'\>|\<|\>\>|\<<', + # 管道 + r'\|', + # 通配符 + r'\*|\?', + # 美元符号开头的环境变量 + r'\$[\w_]+', + # 历史展开 + r'!', + # 后台运行符号 + r'&', + # 括号命令分组 + r'\(|\)|\{|\}', + # 逻辑操作符 + r'&&|\|\|', + # Shell函数或变量赋值 + r'\b\w+\s*=\s*[^=\s]+' + ] + + for pattern in patterns: + if re.search(pattern, cmd): + return True + return False + + +def execute_shell_command(cmd: str) -> int: """Execute a shell command and exit.""" - shell = os.environ.get("SHELL", "/bin/sh") - full_command = f"{shell} -c {shlex.quote(cmd)}" - os.system(full_command) + if check_shell_features(cmd): + try: + process = subprocess.Popen(cmd, shell=True) + except ValueError as e: + print(f'执行命令时出错:{e}') + return 1 + else: + try: + process = subprocess.Popen(shlex.split(cmd)) + except FileNotFoundError as e: + print(f'命令不存在:{e}') + return 1 + exit_code = process.wait() + return exit_code def handle_user_input(service: llm_service.LLMService, @@ -26,18 +66,18 @@ def handle_user_input(service: llm_service.LLMService, """Process user input based on the given flag and backend configuration.""" if mode == 'shell': cmd = service.get_shell_answer(user_input) + exit_code: int = 0 if cmd and interact.query_yes_or_no("\033[33m是否执行命令?\033[0m "): - execute_shell_command(cmd) - sys.exit(0) + exit_code = execute_shell_command(cmd) + sys.exit(exit_code) elif mode == 'chat': service.get_general_answer(user_input) -def main(user_input: Union[str, None]): - config = config_manager.load_config() +def main(user_input: Union[str, None], config: dict): backend = config.get('backend') - mode = config.get('query_mode') - service: llm_service.LLMService = None + mode = str(config.get('query_mode')) + service: Union[llm_service.LLMService, None] = None if backend == 'framework': service = framework_api.Framework( url=config.get('framework_url'), @@ -53,8 +93,8 @@ def main(user_input: Union[str, None]): domain=config.get('spark_domain') ) elif backend == 'openai': - service = openai_api.OpenAI( - url=config.get('model_url'), + service = openai_api.ChatOpenAI( + url=str(config.get('model_url')), api_key=config.get('model_api_key'), model=config.get('model_name') ) diff --git a/src/app/copilot_cli.py b/src/app/copilot_cli.py new file mode 100644 index 0000000..25f35e8 --- /dev/null +++ b/src/app/copilot_cli.py @@ -0,0 +1,46 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import argparse + +from app.copilot_app import main +from app.copilot_init import setup_copilot +from utilities.config_manager import edit_config, load_config, select_backend, select_query_mode + +config = load_config() +backend = config.get('backend') + + +def _parse_arguments(): + parser = argparse.ArgumentParser(prog="copilot", description="EulerCopilot 命令行工具") + parser.add_argument("user_input", nargs="?", default=None, help="用自然语言提问") + parser.add_argument("--init", action="store_true", help="初始化 copilot 设置") + parser.add_argument("--shell", "-s", action="store_true", help="选择 Shell 命令模式") + parser.add_argument("--chat", "-c", action="store_true", help="选择智能问答模式") + if backend == 'framework': + parser.add_argument("--diagnose", "-d", action="store_true", help="选择智能诊断模式") + parser.add_argument("--tuning", "-t", action="store_true", help="选择智能调优模式") + parser.add_argument("--backend", action="store_true", help="选择大语言模型后端") + parser.add_argument("--settings", action="store_true", help="编辑 copilot 设置") + return parser.parse_args() + + +def run_command_line(): + args = _parse_arguments() + + if args.init: + setup_copilot() + elif args.shell: + select_query_mode(0) + elif args.chat: + select_query_mode(1) + elif backend == 'framework': + if args.diagnose: + select_query_mode(2) + elif args.tuning: + select_query_mode(3) + elif args.backend: + select_backend() + elif args.settings: + edit_config() + else: + main(args.user_input, config) diff --git a/src/backends/framework_api.py b/src/backends/framework_api.py index 985425d..e9ad794 100644 --- a/src/backends/framework_api.py +++ b/src/backends/framework_api.py @@ -28,10 +28,10 @@ class Framework(LLMService): return self.content def get_shell_answer(self, question: str) -> str: - question = "请用单行shell命令回答以下问题:\n" + question + \ + query = "请用单行shell命令回答以下问题:\n" + question + \ "\n\n请直接以纯文本形式回复shell命令,不要添加任何多余内容。\n" + \ "请注意你是 openEuler 的小助手,你所回答的命令必须被 openEuler 系统支持" - return self.get_general_answer(question).replace("`", "") + return self._extract_shell_code_blocks(self.get_general_answer(query)) def _stream_response(self, headers, data): spinner = Spinner('material') diff --git a/src/backends/llm_service.py b/src/backends/llm_service.py index 69f66ae..7b07412 100644 --- a/src/backends/llm_service.py +++ b/src/backends/llm_service.py @@ -1,7 +1,10 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +import re from abc import ABC, abstractmethod +from utilities.env_info import get_os_info, is_root + class LLMService(ABC): @abstractmethod @@ -11,3 +14,43 @@ class LLMService(ABC): @abstractmethod def get_shell_answer(self, question: str) -> str: pass + + def _extract_shell_code_blocks(self, markdown_text): + shell_code_pattern = re.compile(r'```(?:bash|sh|shell)\n(?P(?:\n|.)*?)\n```', re.DOTALL) + matches = shell_code_pattern.finditer(markdown_text) + cmds = [match.group('code') for match in matches] + if cmds: + return cmds[0] + return markdown_text.replace('`', '') + + def _get_context_length(self, context: list) -> int: + length = 0 + for content in context: + temp = content['content'] + leng = len(temp) + length += leng + return length + + def _gen_sudo_prompt(self) -> str: + if is_root(): + return '当前用户为 root 用户,你生成的 shell 命令不能包涵 sudo' + else: + return '当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo' + + def _gen_system_prompt(self) -> str: + return f'''你是当前操作系统 {get_os_info()} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 + 你的任务是:根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 + 你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 + 除非有特殊要求,你的回答必须使用 Markdown 格式,并使用中文标点符号; + 但如果用户要求你只输出单行 shell 命令,你就不能输出多余的格式或文字。 + + 用户可能问你一些操作系统相关的问题,你尤其需要注意安装软件包的情景: + openEuler 使用 dnf 或 yum 管理软件包,你不能在回答中使用 apt 或其他命令; + Debian 和 Ubuntu 使用 apt 管理软件包,你也不能在回答中使用 dnf 或 yum 命令; + 你可能还会遇到使用其他类 unix 系统的情景,比如 macOS 要使用 Homebrew 安装软件包。 + + 请特别注意当前用户的权限: + {self._gen_system_prompt()} + + 由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过300字。 + ''' diff --git a/src/backends/openai_api.py b/src/backends/openai_api.py index 7b77216..9beb3a9 100644 --- a/src/backends/openai_api.py +++ b/src/backends/openai_api.py @@ -1,24 +1,23 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. import json -import sys -import re +from typing import Union import requests from rich.console import Console from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from utilities.os_info import get_os_info +from utilities.env_info import get_os_info from backends.llm_service import LLMService -class OpenAI(LLMService): - def __init__(self, url, api_key, model = 'qwen-7b', max_tokens = 2048): +class ChatOpenAI(LLMService): + def __init__(self, url: str, api_key: Union[str, None], model: Union[str, None], max_tokens = 2048): self.url: str = url - self.api_key: str = api_key - self.model: str = model + self.api_key: Union[str, None] = api_key + self.model: Union[str, None] = model self.max_tokens: int = max_tokens self.answer: str = '' self.history: list = [] @@ -35,16 +34,8 @@ class OpenAI(LLMService): 当前操作系统是:{get_os_info()},请返回符合当前系统要求的命令。' return self._extract_shell_code_blocks(self.get_general_answer(query)) - def _get_length(self, context: list) -> int: - length = 0 - for content in context: - temp = content['content'] - leng = len(temp) - length += leng - return length - def _check_len(self, context: list) -> list: - while self._get_length(context) > self.max_tokens / 2: + while self._get_context_length(context) > self.max_tokens / 2: del context[0] return context @@ -53,12 +44,13 @@ class OpenAI(LLMService): history = self._check_len( self.history if len(self.history) < 5 else self.history[-5:] ) + history.insert(0, {'content': self._gen_system_prompt(), 'role': 'system'}) return { 'messages': history, 'model': self.model, 'stream': stream, 'max_tokens': self.max_tokens, - 'temperature': 0.7, + 'temperature': 0.1, 'top_p': 0.95 } @@ -73,15 +65,25 @@ class OpenAI(LLMService): self.answer = '' with Live(console=self.console) as live: live.update(spinner, refresh=True) - response = requests.post( - self.url, - headers=self._gen_headers(), - data=json.dumps(self._gen_params(query)), - stream=True, - timeout=60 - ) + try: + response = requests.post( + self.url, + headers=self._gen_headers(), + data=json.dumps(self._gen_params(query)), + stream=True, + timeout=60 + ) + except requests.exceptions.ConnectionError: + live.update(Markdown('连接大模型失败'), refresh=True) + return + except requests.exceptions.Timeout: + live.update(Markdown('请求大模型超时'), refresh=True) + return + except requests.exceptions.RequestException: + live.update(Markdown('请求大模型异常'), refresh=True) + return if response.status_code != 200: - sys.stderr.write(f'{response.status_code} 请求失败\n') + live.update(Markdown(f'请求失败\n\n状态码: {response.status_code}'), refresh=True) return for line in response.iter_lines(): if line is None: @@ -92,18 +94,13 @@ class OpenAI(LLMService): except json.JSONDecodeError: continue else: - chunk = jcontent['choices'][0]['delta']['content'] - finish_reason = jcontent['choices'][0]['finish_reason'] - self.answer += chunk - live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) - if finish_reason == 'stop': - self.history.append({'content': self.answer, 'role': 'assistant'}) - break - - def _extract_shell_code_blocks(self, markdown_text): - shell_code_pattern = re.compile(r'```shell\n(?P(?:\n|.)*?)\n```', re.DOTALL) - matches = shell_code_pattern.finditer(markdown_text) - cmds: list = [match.group('code') for match in matches] - if len(cmds) > 0: - return cmds[0] - return markdown_text.replace('`', '') + choices = jcontent.get('choices', []) + if choices: + delta = choices[0].get('delta', {}) + chunk = delta.get('content', '') + finish_reason = choices[0].get('finish_reason') + self.answer += chunk + live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) + if finish_reason == 'stop': + self.history.append({'content': self.answer, 'role': 'assistant'}) + break diff --git a/src/backends/spark_api.py b/src/backends/spark_api.py index e3cfa3b..c009a55 100644 --- a/src/backends/spark_api.py +++ b/src/backends/spark_api.py @@ -5,7 +5,6 @@ import base64 import hashlib import hmac import json -import re import sys from datetime import datetime from time import mktime @@ -17,7 +16,7 @@ from rich.console import Console from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from utilities.os_info import get_os_info +from utilities.env_info import get_os_info from backends.llm_service import LLMService @@ -70,13 +69,13 @@ class Spark(LLMService): live.update(f'请求错误: {code}\n{message}', refresh=True) await websocket.close() else: - choices = data["payload"]["choices"] - status = choices["status"] - content = choices["text"][0]["content"] + choices = data['payload']['choices'] + status = choices['status'] + content = choices['text'][0]['content'] self.answer += content live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) if status == 2: - self.history.append({"role": "assistant", "content": self.answer}) + self.history.append({'role': 'assistant', 'content': self.answer}) break except websockets.exceptions.ConnectionClosed: break @@ -105,61 +104,47 @@ class Spark(LLMService): # 将请求的鉴权参数组合为字典 v = { - "authorization": authorization, - "date": date, - "host": self.host + 'authorization': authorization, + 'date': date, + 'host': self.host } # 拼接鉴权参数,生成url url = self.spark_url + '?' + urlencode(v) # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致 return url - def _get_length(self, context: list) -> int: - length = 0 - for content in context: - temp = content["content"] - leng = len(temp) - length += leng - return length - def _check_len(self, context: list) -> list: - while self._get_length(context) > self.max_tokens / 2: + while self._get_context_length(context) > self.max_tokens / 2: del context[0] return context def _gen_params(self, query: str): - """ + ''' 通过appid和用户的提问来生成请参数 - """ - self.history.append({"role": "user", "content": query}) + ''' + self.history.append({'role': 'user', 'content': query}) history = self._check_len( self.history if len(self.history) < 5 else self.history[-5:] ) + if self.domain == 'generalv3.5': + history.insert(0, {'role': 'system', 'content': self._gen_system_prompt()}) data = { - "header": { - "app_id": self.app_id, - "uid": "1234", + 'header': { + 'app_id': self.app_id, + 'uid': '1234', }, - "parameter": { - "chat": { - "domain": self.domain, - "temperature": 0.5, - "max_tokens": self.max_tokens, - "auditing": "default", + 'parameter': { + 'chat': { + 'domain': self.domain, + 'temperature': 0.5, + 'max_tokens': self.max_tokens, + 'auditing': 'default', } }, - "payload": { - "message": { - "text": history + 'payload': { + 'message': { + 'text': history } } } return data - - def _extract_shell_code_blocks(self, markdown_text): - shell_code_pattern = re.compile(r'```shell\n(?P(?:\n|.)*?)\n```', re.DOTALL) - matches = shell_code_pattern.finditer(markdown_text) - cmds: list = [match.group('code') for match in matches] - if len(cmds) > 0: - return cmds[0] - return markdown_text.replace('`', '') diff --git a/src/copilot.py b/src/copilot.py index edd7bc5..5b56ab4 100755 --- a/src/copilot.py +++ b/src/copilot.py @@ -1,46 +1,7 @@ #!/usr/bin/env python3 # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. -import argparse - -from app.copilot_app import main -from app.copilot_init import setup_copilot -from utilities.config_manager import edit_config, select_backend, select_query_mode - - -def parse_arguments(): - parser = argparse.ArgumentParser(prog="copilot", description="EulerCopilot 命令行工具") - parser.add_argument("user_input", nargs="?", default=None, help="用自然语言提问") - parser.add_argument("--init", action="store_true", help="初始化 copilot 设置") - parser.add_argument("--shell", "-s", action="store_true", help="选择 Shell 命令模式") - parser.add_argument("--chat", "-c", action="store_true", help="选择智能问答模式") - parser.add_argument("--diagnose", "-d", action="store_true", help="选择智能诊断模式") - parser.add_argument("--tuning", "-t", action="store_true", help="选择智能调优模式") - parser.add_argument("--backend", action="store_true", help="选择大语言模型后端") - parser.add_argument("--settings", action="store_true", help="编辑 copilot 设置") - return parser.parse_args() - - -def run_command_line(): - args = parse_arguments() - - if args.init: - setup_copilot() - elif args.shell: - select_query_mode(0) - elif args.chat: - select_query_mode(1) - elif args.diagnose: - select_query_mode(2) - elif args.tuning: - select_query_mode(3) - elif args.backend: - select_backend() - elif args.settings: - edit_config() - else: - main(args.user_input) - +from app.copilot_cli import run_command_line if __name__ == "__main__": run_command_line() diff --git a/src/eulercopilot_shortcut.sh b/src/eulercopilot_shortcut.sh index b6309da..3296d2b 100644 --- a/src/eulercopilot_shortcut.sh +++ b/src/eulercopilot_shortcut.sh @@ -6,9 +6,26 @@ run_copilot() { READLINE_POINT=${#READLINE_LINE} elif [[ ! "${readline}" =~ ^copilot ]]; then READLINE_LINE="" + local username=$(whoami) + local hostname=$(hostname -s) + if [[ "$PWD" == "$HOME" ]]; then + local current_base_dir='~' + else + local current_base_dir=$(basename "$PWD") + fi + if [[ $EUID -eq 0 ]]; then + local prompt_end='#' + else + local prompt_end='$' + fi + local prompt="${PS1//\\u/$username}" + prompt="${prompt//\\h/$hostname}" + prompt="${prompt//\\W/$current_base_dir}" + prompt="${prompt//\\$/$prompt_end}" + history -s "${readline}" && echo "${prompt}${readline}" stty sane && (copilot "${readline}") fi stty "${terminal_settings}" } -bind -x '"\C-h": run_copilot' 2>/dev/null +bind -x '"\C-o": run_copilot' 2>/dev/null diff --git a/src/setup.py b/src/setup.py index ca62d2e..9d8fc05 100644 --- a/src/setup.py +++ b/src/setup.py @@ -34,7 +34,7 @@ extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] # 定义 setup() 参数 setup( name='copilot', - version='1.0', + version='1.1', description='EulerCopilot CLI Tool', author='Hongyu Shi', author_email='shihongyu15@huawei.com', @@ -62,6 +62,7 @@ setup( install_requires=[ # 添加项目依赖的库:websockets 和 requests 'websockets', 'requests', + 'rich', ], entry_points={ 'console_scripts': ['copilot=copilot:run_command_line'] diff --git a/src/utilities/config_manager.py b/src/utilities/config_manager.py index a6ddee6..a6a83e3 100644 --- a/src/utilities/config_manager.py +++ b/src/utilities/config_manager.py @@ -7,18 +7,18 @@ CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') EMPTY_CONFIG = { - "backend": "openai", + "backend": "spark", "query_mode": "shell", "spark_app_id": "", "spark_api_key": "", "spark_api_secret": "", "spark_url": "wss://spark-api.xf-yun.com/v3.5/chat", "spark_domain": "generalv3.5", - "framework_api_key": "", "framework_url": "", + "framework_api_key": "", "model_url": "http://localhost:1337/v1/chat/completions", "model_api_key": "", - "model_name": "_" + "model_name": "" } @@ -58,10 +58,11 @@ def select_query_mode(mode: int): def select_backend(): - backends = ['spark', 'framework'] + backends = ['framework', 'spark', 'openai'] print('\n\033[1;33m请选择大模型后端:\033[0m\n') - print('\t<1> 讯飞星火大模型 3.5') - print('\t<2> EulerCopilot') + print('\t<1> EulerCopilot') + print('\t<2> 讯飞星火大模型 3.5') + print('\t<3> 类 ChatGPT(兼容 llama.cpp)') print() try: while True: diff --git a/src/utilities/os_info.py b/src/utilities/env_info.py similarity index 95% rename from src/utilities/os_info.py rename to src/utilities/env_info.py index 56a1576..c8d434a 100644 --- a/src/utilities/os_info.py +++ b/src/utilities/env_info.py @@ -1,5 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +import os import platform import re import subprocess @@ -45,3 +46,7 @@ def get_os_info() -> str: return _porc_macos_info(_exec_shell_cmd(['sw_vers'])) else: return system + + +def is_root() -> bool: + return os.geteuid() == 0 -- Gitee From c346022c20343fb5115d972afa424fedcaa7329e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 14 May 2024 12:10:49 +0800 Subject: [PATCH 10/32] pyinstaller & system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新RPM包构建流程:使用 pyinstaller 构建二进制文件 - 优化 system prompt:规避危险操作 - 优化 shell 模式输出,新增代码解释 - 使用 Python 虚拟环境进行构建 - 默认情况下隐藏编辑配置文件的选项 Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 31 ++++++++------ src/app/copilot_app.py | 52 +++++++++++------------ src/app/copilot_cli.py | 37 ++++++++-------- src/backends/llm_service.py | 27 ++++++++++-- src/backends/openai_api.py | 13 +++--- src/backends/spark_api.py | 21 +++++---- src/utilities/config_manager.py | 8 ++-- src/utilities/env_info.py | 75 +++++++++++++++++++-------------- 8 files changed, 152 insertions(+), 112 deletions(-) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index 182f70c..c5cf3fe 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -1,36 +1,43 @@ Name: eulercopilot Version: 1.1 -Release: 1%{?dist}%{?_timestamp} +Release: 1%{?dist}.%{?_timestamp} Group: Applications/Utilities Summary: EulerCopilot CLI Tool Source: %{name}-%{version}.tar.gz License: MulanPSL-2.0 URL: https://www.openeuler.org/zh/ -BuildRequires: python3-devel python3-setuptools python3-Cython gcc +BuildRequires: python3-devel python3-setuptools +BuildRequires: python3-pip +BuildRequires: python3-Cython gcc -Requires: python3 python3-pip +Requires: python3 %description -EulerCopilot CLI Tool +EulerCopilot Command Line Tool %prep %setup -q +python3 -m venv .venv +.venv/bin/python3 -m pip install -U pip setuptools +.venv/bin/python3 -m pip install -U Cython pyinstaller +.venv/bin/python3 -m pip install -U websockets requests rich %build -python3 setup.py build_ext +.venv/bin/python3 setup.py build_ext +.venv/bin/pyinstaller --onefile --clean \ + --distpath=%{_builddir}/%{name}-%{version}/dist \ + --workpath=%{_builddir}/%{name}-%{version}/build \ + copilot.py %install %define _unpackaged_files_terminate_build 0 -python3 setup.py install --root=%{buildroot} --single-version-externally-managed --record=INSTALLED_FILES +install -d %{buildroot}/%{_bindir} +install -c -m 0755 %{_builddir}/%{name}-%{version}/dist/copilot %{buildroot}/%{_bindir} install -d %{buildroot}/etc/profile.d install -c -m 0755 %{_builddir}/%{name}-%{version}/eulercopilot_shortcut.sh %{buildroot}/etc/profile.d -%files -f INSTALLED_FILES +%files %defattr(-,root,root,-) +/usr/bin/copilot /etc/profile.d/eulercopilot_shortcut.sh - -%post -/usr/bin/python3 -m pip install --upgrade websockets >/dev/null -/usr/bin/python3 -m pip install --upgrade requests >/dev/null -/usr/bin/python3 -m pip install --upgrade rich >/dev/null diff --git a/src/app/copilot_app.py b/src/app/copilot_app.py index 08854b3..6c83abc 100644 --- a/src/app/copilot_app.py +++ b/src/app/copilot_app.py @@ -10,9 +10,7 @@ import uuid from typing import Union from backends import framework_api, llm_service, spark_api, openai_api -from utilities import config_manager, interact - -EXIT_MESSAGE = "\033[33m>>>\033[0m 很高兴为您服务,下次再见~" +from utilities import interact def check_shell_features(cmd: str) -> bool: @@ -44,7 +42,7 @@ def check_shell_features(cmd: str) -> bool: def execute_shell_command(cmd: str) -> int: - """Execute a shell command and exit.""" + '''Execute a shell command and exit.''' if check_shell_features(cmd): try: process = subprocess.Popen(cmd, shell=True) @@ -63,17 +61,23 @@ def execute_shell_command(cmd: str) -> int: def handle_user_input(service: llm_service.LLMService, user_input: str, mode: str) -> None: - """Process user input based on the given flag and backend configuration.""" + '''Process user input based on the given flag and backend configuration.''' if mode == 'shell': cmd = service.get_shell_answer(user_input) exit_code: int = 0 - if cmd and interact.query_yes_or_no("\033[33m是否执行命令?\033[0m "): + if cmd and interact.query_yes_or_no('\n\033[33m是否执行命令?\033[0m '): exit_code = execute_shell_command(cmd) sys.exit(exit_code) elif mode == 'chat': service.get_general_answer(user_input) +def exit(msg: str = '', code: int = 0): + '''Exit the program with a message.''' + print(msg) + sys.exit(code) + + def main(user_input: Union[str, None], config: dict): backend = config.get('backend') mode = str(config.get('query_mode')) @@ -100,23 +104,19 @@ def main(user_input: Union[str, None], config: dict): ) if service is None: - sys.stderr.write("\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m") - sys.exit(1) - - if mode == 'shell': - print("\033[33m当前模式:Shell 命令生成\033[0m") - if mode == 'chat': - print("\033[33m当前模式:智能问答\033[0m 输入 \"exit\" 或按下 Ctrl+C 退出服务") - - try: - while True: - if user_input is None: - user_input = input("\033[35m>>>\033[0m ") - if user_input.lower().startswith('exit'): - print(EXIT_MESSAGE) - sys.exit(0) - handle_user_input(service, user_input, mode) - user_input = None # Reset user_input for next iteration (only if continuing service) - except KeyboardInterrupt: - print() - sys.exit(0) + exit('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m', 1) + else: + if mode == 'shell': + print('\033[33m当前模式:Shell 命令生成\033[0m') + if mode == 'chat': + print('\033[33m当前模式:智能问答\033[0m 输入 \'exit\' 或按下 Ctrl+C 退出服务') + try: + while True: + if user_input is None: + user_input = input('\033[35m>>>\033[0m ') + if user_input.lower().startswith('exit'): + exit() + handle_user_input(service, user_input, mode) + user_input = None # Reset user_input for next iteration (only if continuing service) + except KeyboardInterrupt: + exit() diff --git a/src/app/copilot_cli.py b/src/app/copilot_cli.py index 25f35e8..230bd80 100644 --- a/src/app/copilot_cli.py +++ b/src/app/copilot_cli.py @@ -4,23 +4,25 @@ import argparse from app.copilot_app import main from app.copilot_init import setup_copilot -from utilities.config_manager import edit_config, load_config, select_backend, select_query_mode +from utilities.config_manager import edit_config, load_config, select_backend, select_query_mode, DEFAULT_CONFIG -config = load_config() -backend = config.get('backend') +CONFIG: dict = load_config() +BACKEND: str = CONFIG.get('backend', DEFAULT_CONFIG['backend']) +ADVANCED_MODE: bool = CONFIG.get('advanced_mode', DEFAULT_CONFIG['advanced_mode']) def _parse_arguments(): - parser = argparse.ArgumentParser(prog="copilot", description="EulerCopilot 命令行工具") + parser = argparse.ArgumentParser(prog="copilot", description="EulerCopilot 命令行助手") parser.add_argument("user_input", nargs="?", default=None, help="用自然语言提问") parser.add_argument("--init", action="store_true", help="初始化 copilot 设置") - parser.add_argument("--shell", "-s", action="store_true", help="选择 Shell 命令模式") - parser.add_argument("--chat", "-c", action="store_true", help="选择智能问答模式") - if backend == 'framework': - parser.add_argument("--diagnose", "-d", action="store_true", help="选择智能诊断模式") - parser.add_argument("--tuning", "-t", action="store_true", help="选择智能调优模式") - parser.add_argument("--backend", action="store_true", help="选择大语言模型后端") - parser.add_argument("--settings", action="store_true", help="编辑 copilot 设置") + parser.add_argument("--shell", "-s", action="store_true", help="切换到 Shell 命令模式") + parser.add_argument("--chat", "-c", action="store_true", help="切换到智能问答模式") + if BACKEND == 'framework': + parser.add_argument("--diagnose", "-d", action="store_true", help="切换到智能诊断模式") + parser.add_argument("--tuning", "-t", action="store_true", help="切换到智能调优模式") + if ADVANCED_MODE: + parser.add_argument("--backend", action="store_true", help="选择大语言模型后端") + parser.add_argument("--settings", action="store_true", help="编辑 copilot 设置") return parser.parse_args() @@ -33,14 +35,15 @@ def run_command_line(): select_query_mode(0) elif args.chat: select_query_mode(1) - elif backend == 'framework': + elif BACKEND == 'framework': if args.diagnose: select_query_mode(2) elif args.tuning: select_query_mode(3) - elif args.backend: - select_backend() - elif args.settings: - edit_config() + elif ADVANCED_MODE: + if args.backend: + select_backend() + elif args.settings: + edit_config() else: - main(args.user_input, config) + main(args.user_input, CONFIG) diff --git a/src/backends/llm_service.py b/src/backends/llm_service.py index 7b07412..fd5806a 100644 --- a/src/backends/llm_service.py +++ b/src/backends/llm_service.py @@ -38,8 +38,9 @@ class LLMService(ABC): return '当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo' def _gen_system_prompt(self) -> str: - return f'''你是当前操作系统 {get_os_info()} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 - 你的任务是:根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 + return f'''你是操作系统 {get_os_info()} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 + 你的任务是: + 根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 除非有特殊要求,你的回答必须使用 Markdown 格式,并使用中文标点符号; 但如果用户要求你只输出单行 shell 命令,你就不能输出多余的格式或文字。 @@ -50,7 +51,27 @@ class LLMService(ABC): 你可能还会遇到使用其他类 unix 系统的情景,比如 macOS 要使用 Homebrew 安装软件包。 请特别注意当前用户的权限: - {self._gen_system_prompt()} + {self._gen_sudo_prompt()} + + 在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! + 如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 + 危险操作举例: + `rm -rf /path/to/sth` + `dnf remove -y package_name` + 你不能输出类似于上述例子的命令! 由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过300字。 ''' + + def _gen_shell_prompt(self, question: str) -> str: + return f'''根据用户输入的问题,生成单行 shell 命令,并使用 Markdown 格式输出。 + + 用户的问题: + {question} + + 要求: + 1. 请用单行 shell 命令输出你的回答,不能使用多行 shell 命令 + 2. 请用 Markdown 代码块输出 shell 命令 + 3. 请解释你的回答,你要将你的解释附在命令代码块下方,你要有条理地解释命令中的每个步骤 + 4. 当前操作系统是 {get_os_info()},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 + ''' diff --git a/src/backends/openai_api.py b/src/backends/openai_api.py index 9beb3a9..3345d86 100644 --- a/src/backends/openai_api.py +++ b/src/backends/openai_api.py @@ -9,7 +9,6 @@ from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from utilities.env_info import get_os_info from backends.llm_service import LLMService @@ -29,9 +28,7 @@ class ChatOpenAI(LLMService): return self.answer def get_shell_answer(self, question: str) -> str: - query = f'请用单行shell命令回答以下问题:\n{question}\n\ - \n要求:\n请直接回复命令,不要添加任何多余内容;\n\ - 当前操作系统是:{get_os_info()},请返回符合当前系统要求的命令。' + query = self._gen_shell_prompt(question) return self._extract_shell_code_blocks(self.get_general_answer(query)) def _check_len(self, context: list) -> list: @@ -74,16 +71,16 @@ class ChatOpenAI(LLMService): timeout=60 ) except requests.exceptions.ConnectionError: - live.update(Markdown('连接大模型失败'), refresh=True) + live.update('连接大模型失败', refresh=True) return except requests.exceptions.Timeout: - live.update(Markdown('请求大模型超时'), refresh=True) + live.update('请求大模型超时', refresh=True) return except requests.exceptions.RequestException: - live.update(Markdown('请求大模型异常'), refresh=True) + live.update('请求大模型异常', refresh=True) return if response.status_code != 200: - live.update(Markdown(f'请求失败\n\n状态码: {response.status_code}'), refresh=True) + live.update(f'请求失败: {response.status_code}', refresh=True) return for line in response.iter_lines(): if line is None: diff --git a/src/backends/spark_api.py b/src/backends/spark_api.py index c009a55..d1a4ea9 100644 --- a/src/backends/spark_api.py +++ b/src/backends/spark_api.py @@ -5,7 +5,6 @@ import base64 import hashlib import hmac import json -import sys from datetime import datetime from time import mktime from urllib.parse import urlencode, urlparse @@ -16,7 +15,6 @@ from rich.console import Console from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from utilities.env_info import get_os_info from backends.llm_service import LLMService @@ -43,18 +41,16 @@ class Spark(LLMService): return self.answer def get_shell_answer(self, question: str) -> str: - query = f'请用单行shell命令回答以下问题:\n{question}\n\ - \n要求:\n请直接回复命令,不要添加任何多余内容;\n\ - 当前操作系统是:{get_os_info()},请返回符合当前系统要求的命令。' + query = self._gen_shell_prompt(question) return self._extract_shell_code_blocks(self.get_general_answer(query)) async def _query_spark_ai(self, query: str): url = self._create_url() self.answer = '' spinner = Spinner('material') - try: - with Live(console=self.console) as live: - live.update(spinner, refresh=True) + with Live(console=self.console) as live: + live.update(spinner, refresh=True) + try: async with websockets.connect(url) as websocket: data = json.dumps(self._gen_params(query)) await websocket.send(data) @@ -80,9 +76,12 @@ class Spark(LLMService): except websockets.exceptions.ConnectionClosed: break - except websockets.exceptions.InvalidStatusCode: - sys.stderr.write('\033[1;31m请求错误!\033[0m\n请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。') - print('输入 "copilot --settings" 来查看和编辑配置') + except websockets.exceptions.InvalidStatusCode: + live.update(f'\033[1;31m请求错误\033[0m\n\ + 请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。\n\ + 输入 "vi ~/.config/eulercopilot/config.json" 查看和编辑配置;\n\ + 或尝试 ping {self.spark_url}', + refresh=True) def _create_url(self): now = datetime.now() # 生成RFC1123格式的时间戳 diff --git a/src/utilities/config_manager.py b/src/utilities/config_manager.py index a6a83e3..7ff2510 100644 --- a/src/utilities/config_manager.py +++ b/src/utilities/config_manager.py @@ -6,9 +6,10 @@ import os CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') -EMPTY_CONFIG = { +DEFAULT_CONFIG = { "backend": "spark", "query_mode": "shell", + "advanced_mode": False, "spark_app_id": "", "spark_api_key": "", "spark_api_secret": "", @@ -35,16 +36,17 @@ def load_config() -> dict: def write_config(config): with open(CONFIG_PATH, 'w', encoding='utf-8') as json_file: json.dump(config, json_file, indent=4) + json_file.write('\n') # 追加一行空行 def init_config(): if not os.path.exists(CONFIG_DIR): os.makedirs(CONFIG_DIR) - write_config(EMPTY_CONFIG) + write_config(DEFAULT_CONFIG) def update_config(key: str, value): - if key not in EMPTY_CONFIG: + if key not in DEFAULT_CONFIG: return config = load_config() config.update({key: value}) diff --git a/src/utilities/env_info.py b/src/utilities/env_info.py index c8d434a..b9c05a0 100644 --- a/src/utilities/env_info.py +++ b/src/utilities/env_info.py @@ -4,38 +4,49 @@ import os import platform import re import subprocess - - -def _exec_shell_cmd(cmd: list) -> subprocess.CompletedProcess: - return subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - check=False - ) - - -def _porc_linux_info(shell_result: subprocess.CompletedProcess): - pattern = r'PRETTY_NAME="(.+?)"' - match = re.search(pattern, shell_result.stdout) - if match: - return match.group(1) # 返回括号内匹配的内容,即PRETTY_NAME的值 - return 'Unknown' - - -def _porc_macos_info(shell_result: subprocess.CompletedProcess): - macos_info = {} - if shell_result.returncode == 0: - lines = shell_result.stdout.splitlines() - for line in lines: - key, value = line.split(':\t\t', maxsplit=1) - macos_info[key.strip()] = value.strip() - product_name = macos_info.get('ProductName') - product_version = macos_info.get('ProductVersion') - if product_name is not None and product_version is not None: - return f'{product_name} {product_version}' - return 'Unknown' +import sys +from typing import Union + + +def _exec_shell_cmd(cmd: list) -> Union[subprocess.CompletedProcess, None]: + try: + process = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True + ) + except subprocess.CalledProcessError as e: + sys.stderr.write(e.stderr) + except Exception as e: + sys.stderr.write(str(e)) + else: + return process + + +def _porc_linux_info(shell_result: Union[subprocess.CompletedProcess, None]): + if shell_result is not None: + pattern = r'PRETTY_NAME="(.+?)"' + match = re.search(pattern, shell_result.stdout) + if match: + return match.group(1) # 返回括号内匹配的内容,即PRETTY_NAME的值 + return 'Unknown Linux distribution' + + +def _porc_macos_info(shell_result: Union[subprocess.CompletedProcess, None]): + if shell_result is not None: + macos_info = {} + if shell_result.returncode == 0: + lines = shell_result.stdout.splitlines() + for line in lines: + key, value = line.split(':\t\t', maxsplit=1) + macos_info[key.strip()] = value.strip() + product_name = macos_info.get('ProductName') + product_version = macos_info.get('ProductVersion') + if product_name is not None and product_version is not None: + return f'{product_name} {product_version}' + return 'Unknown macOS version' def get_os_info() -> str: -- Gitee From d1f4930276e8da91214ae2d99b2a10082c7f500a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 16 May 2024 17:50:19 +0800 Subject: [PATCH 11/32] Use Typer for command line interface. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 2 +- src/app/copilot_cli.py | 143 ++++++++++++++++++++++++--------- src/copilot.py | 4 +- src/setup.py | 3 +- 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index c5cf3fe..9e1fd44 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -21,7 +21,7 @@ EulerCopilot Command Line Tool python3 -m venv .venv .venv/bin/python3 -m pip install -U pip setuptools .venv/bin/python3 -m pip install -U Cython pyinstaller -.venv/bin/python3 -m pip install -U websockets requests rich +.venv/bin/python3 -m pip install -U websockets requests rich typer %build .venv/bin/python3 setup.py build_ext diff --git a/src/app/copilot_cli.py b/src/app/copilot_cli.py index 230bd80..0f01b9e 100644 --- a/src/app/copilot_cli.py +++ b/src/app/copilot_cli.py @@ -1,49 +1,120 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. -import argparse +import os +from typing import Optional + +import typer +from rich import print +from utilities.config_manager import ( + CONFIG_PATH, + DEFAULT_CONFIG, + edit_config, + load_config, + select_backend, + select_query_mode, +) from app.copilot_app import main from app.copilot_init import setup_copilot -from utilities.config_manager import edit_config, load_config, select_backend, select_query_mode, DEFAULT_CONFIG -CONFIG: dict = load_config() -BACKEND: str = CONFIG.get('backend', DEFAULT_CONFIG['backend']) -ADVANCED_MODE: bool = CONFIG.get('advanced_mode', DEFAULT_CONFIG['advanced_mode']) +config: dict = load_config() +BACKEND: str = config.get('backend', DEFAULT_CONFIG['backend']) +ADVANCED_MODE: bool = config.get('advanced_mode', DEFAULT_CONFIG['advanced_mode']) +CONFIG_INITIALIZED: bool = os.path.exists(CONFIG_PATH) +app = typer.Typer( + context_settings={"help_option_names": ["-h", "--help"]}, + add_completion=False +) -def _parse_arguments(): - parser = argparse.ArgumentParser(prog="copilot", description="EulerCopilot 命令行助手") - parser.add_argument("user_input", nargs="?", default=None, help="用自然语言提问") - parser.add_argument("--init", action="store_true", help="初始化 copilot 设置") - parser.add_argument("--shell", "-s", action="store_true", help="切换到 Shell 命令模式") - parser.add_argument("--chat", "-c", action="store_true", help="切换到智能问答模式") - if BACKEND == 'framework': - parser.add_argument("--diagnose", "-d", action="store_true", help="切换到智能诊断模式") - parser.add_argument("--tuning", "-t", action="store_true", help="切换到智能调优模式") - if ADVANCED_MODE: - parser.add_argument("--backend", action="store_true", help="选择大语言模型后端") - parser.add_argument("--settings", action="store_true", help="编辑 copilot 设置") - return parser.parse_args() +@app.command() +def cli( + question: Optional[str] = typer.Argument( + None, show_default=False, + help='通过自然语言提问'), + shell: bool = typer.Option( + False, '--shell', '-s', + help='切换到 Shell 命令模式', + rich_help_panel='选择问答模式' + ), + chat: bool = typer.Option( + False, '--chat', '-c', + help='切换到智能问答模式', + rich_help_panel='选择问答模式' + ), + diagnose: bool = typer.Option( + False, '--diagnose', '-d', + help='切换到智能诊断模式', + rich_help_panel='选择问答模式', + hidden=(BACKEND != 'framework') + ), + tuning: bool = typer.Option( + False, '--tuning', '-t', + help='切换到智能调优模式', + rich_help_panel='选择问答模式', + hidden=(BACKEND != 'framework') + ), + init: bool = typer.Option( + False, '--init', + help='初始化 copilot 设置', + hidden=(CONFIG_INITIALIZED) + ), + backend: bool = typer.Option( + False, '--backend', + help='选择大语言模型后端', + rich_help_panel='高级选项', + hidden=(not ADVANCED_MODE) + ), + settings: bool = typer.Option( + False, '--settings', + help='编辑 copilot 设置', + rich_help_panel='高级选项', + hidden=(not ADVANCED_MODE) + ) +) -> None: + '''EulerCopilot 命令行助手''' + if init: + setup_copilot() + return + if backend: + select_backend() + return + if settings: + edit_config() + return -def run_command_line(): - args = _parse_arguments() + if sum(map(bool, [shell, chat, diagnose, tuning])) > 1: + print('只能选择一种模式') + return - if args.init: - setup_copilot() - elif args.shell: + if shell: select_query_mode(0) - elif args.chat: + if not question: + return + elif chat: select_query_mode(1) - elif BACKEND == 'framework': - if args.diagnose: - select_query_mode(2) - elif args.tuning: - select_query_mode(3) - elif ADVANCED_MODE: - if args.backend: - select_backend() - elif args.settings: - edit_config() - else: - main(args.user_input, CONFIG) + if not question: + return + elif diagnose and BACKEND == 'framework': + select_query_mode(2) + if not question: + return + elif tuning and BACKEND == 'framework': + select_query_mode(3) + if not question: + return + + if question: + question = question.strip() + + config = load_config() + main(question, config) + + +def entry_point() -> None: + app() + + +if __name__ == '__main__': + entry_point() diff --git a/src/copilot.py b/src/copilot.py index 5b56ab4..c029f40 100755 --- a/src/copilot.py +++ b/src/copilot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. -from app.copilot_cli import run_command_line +from app.copilot_cli import entry_point if __name__ == "__main__": - run_command_line() + entry_point() diff --git a/src/setup.py b/src/setup.py index 9d8fc05..4b2da9e 100644 --- a/src/setup.py +++ b/src/setup.py @@ -59,10 +59,11 @@ setup( 'Operating System :: MacOS :: MacOS X' ], python_requires='>=3.9', # Python 版本要求为 3.9 及以上 - install_requires=[ # 添加项目依赖的库:websockets 和 requests + install_requires=[ # 添加项目依赖的库 'websockets', 'requests', 'rich', + 'typer', ], entry_points={ 'console_scripts': ['copilot=copilot:run_command_line'] -- Gitee From 51a251391434700f126284b07dd560b86869d5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 16 May 2024 20:08:14 +0800 Subject: [PATCH 12/32] =?UTF-8?q?=E6=95=B4=E7=90=86=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=9B=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=B7=A5=E5=85=B7=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- ruff.toml | 12 ++++ src/backends/__init__.py | 0 src/copilot.py | 2 +- src/{app => copilot}/__init__.py | 0 src/copilot/__main__.py | 7 +++ src/{ => copilot}/app/copilot_app.py | 15 ++--- src/{ => copilot}/app/copilot_cli.py | 55 ++++++++++++------- src/{ => copilot}/app/copilot_init.py | 7 ++- src/{ => copilot}/backends/framework_api.py | 2 +- src/{ => copilot}/backends/llm_service.py | 2 +- src/{ => copilot}/backends/openai_api.py | 2 +- src/{ => copilot}/backends/spark_api.py | 2 +- src/{ => copilot}/utilities/config_manager.py | 0 src/{ => copilot}/utilities/env_info.py | 0 src/{ => copilot}/utilities/interact.py | 0 src/setup.py | 11 ++-- src/utilities/__init__.py | 0 17 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 ruff.toml delete mode 100644 src/backends/__init__.py rename src/{app => copilot}/__init__.py (100%) create mode 100644 src/copilot/__main__.py rename src/{ => copilot}/app/copilot_app.py (91%) rename src/{ => copilot}/app/copilot_cli.py (64%) rename src/{ => copilot}/app/copilot_init.py (87%) rename src/{ => copilot}/backends/framework_api.py (97%) rename src/{ => copilot}/backends/llm_service.py (98%) rename src/{ => copilot}/backends/openai_api.py (98%) rename src/{ => copilot}/backends/spark_api.py (99%) rename src/{ => copilot}/utilities/config_manager.py (100%) rename src/{ => copilot}/utilities/env_info.py (100%) rename src/{ => copilot}/utilities/interact.py (100%) delete mode 100644 src/utilities/__init__.py diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..0c21937 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,12 @@ +# Allow imports relative to the "src" and "test" directories. +src = ["src"] + +# Allow lines to be as long as 120. +line-length = 120 + +# Always generate Python 3.9-compatible code. +target-version = "py39" + +[format] +# Prefer single quotes over double quotes. +quote-style = "single" diff --git a/src/backends/__init__.py b/src/backends/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/copilot.py b/src/copilot.py index c029f40..1f13657 100755 --- a/src/copilot.py +++ b/src/copilot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. -from app.copilot_cli import entry_point +from copilot.__main__ import entry_point if __name__ == "__main__": entry_point() diff --git a/src/app/__init__.py b/src/copilot/__init__.py similarity index 100% rename from src/app/__init__.py rename to src/copilot/__init__.py diff --git a/src/copilot/__main__.py b/src/copilot/__main__.py new file mode 100644 index 0000000..ec296b2 --- /dev/null +++ b/src/copilot/__main__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +from copilot.app.copilot_cli import entry_point + +if __name__ == "__main__": + entry_point() diff --git a/src/app/copilot_app.py b/src/copilot/app/copilot_app.py similarity index 91% rename from src/app/copilot_app.py rename to src/copilot/app/copilot_app.py index 6c83abc..c17c887 100644 --- a/src/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -1,16 +1,17 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + # pylint: disable=W0611 import re import readline # noqa: F401 -import subprocess import shlex +import subprocess import sys import uuid from typing import Union -from backends import framework_api, llm_service, spark_api, openai_api -from utilities import interact +from copilot.backends import framework_api, llm_service, openai_api, spark_api +from copilot.utilities import interact def check_shell_features(cmd: str) -> bool: @@ -72,7 +73,7 @@ def handle_user_input(service: llm_service.LLMService, service.get_general_answer(user_input) -def exit(msg: str = '', code: int = 0): +def exit_copilot(msg: str = '', code: int = 0): '''Exit the program with a message.''' print(msg) sys.exit(code) @@ -104,7 +105,7 @@ def main(user_input: Union[str, None], config: dict): ) if service is None: - exit('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m', 1) + exit_copilot('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m', 1) else: if mode == 'shell': print('\033[33m当前模式:Shell 命令生成\033[0m') @@ -115,8 +116,8 @@ def main(user_input: Union[str, None], config: dict): if user_input is None: user_input = input('\033[35m>>>\033[0m ') if user_input.lower().startswith('exit'): - exit() + exit_copilot() handle_user_input(service, user_input, mode) user_input = None # Reset user_input for next iteration (only if continuing service) except KeyboardInterrupt: - exit() + exit_copilot() diff --git a/src/app/copilot_cli.py b/src/copilot/app/copilot_cli.py similarity index 64% rename from src/app/copilot_cli.py rename to src/copilot/app/copilot_cli.py index 0f01b9e..75d67b1 100644 --- a/src/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -1,11 +1,15 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +# pylint: disable=R0911,R0912,R0913 + import os from typing import Optional import typer -from rich import print -from utilities.config_manager import ( + +from copilot.app.copilot_app import main +from copilot.app.copilot_init import setup_copilot +from copilot.utilities.config_manager import ( CONFIG_PATH, DEFAULT_CONFIG, edit_config, @@ -14,16 +18,16 @@ from utilities.config_manager import ( select_query_mode, ) -from app.copilot_app import main -from app.copilot_init import setup_copilot - -config: dict = load_config() -BACKEND: str = config.get('backend', DEFAULT_CONFIG['backend']) -ADVANCED_MODE: bool = config.get('advanced_mode', DEFAULT_CONFIG['advanced_mode']) +CONFIG: dict = load_config() +BACKEND: str = CONFIG.get('backend', DEFAULT_CONFIG['backend']) +ADVANCED_MODE: bool = CONFIG.get('advanced_mode', DEFAULT_CONFIG['advanced_mode']) CONFIG_INITIALIZED: bool = os.path.exists(CONFIG_PATH) app = typer.Typer( - context_settings={"help_option_names": ["-h", "--help"]}, + context_settings={ + 'help_option_names': ['-h', '--help'], + 'allow_interspersed_args': True + }, add_completion=False ) @@ -78,14 +82,16 @@ def cli( setup_copilot() return if backend: - select_backend() + if ADVANCED_MODE: + select_backend() return if settings: - edit_config() + if ADVANCED_MODE: + edit_config() return if sum(map(bool, [shell, chat, diagnose, tuning])) > 1: - print('只能选择一种模式') + print('\033[1;31m当前版本只能选择一种问答模式\033[0m') return if shell: @@ -96,20 +102,29 @@ def cli( select_query_mode(1) if not question: return - elif diagnose and BACKEND == 'framework': - select_query_mode(2) - if not question: + elif diagnose: + if BACKEND == 'framework': + select_query_mode(2) + if not question: + return + else: + print('\033[33m当前大模型后端不支持智能诊断功能\033[0m') + print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') return - elif tuning and BACKEND == 'framework': - select_query_mode(3) - if not question: + elif tuning: + if BACKEND == 'framework': + select_query_mode(3) + if not question: + return + else: + print('\033[33m当前大模型后端不支持智能调参功能\033[0m') + print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') return if question: question = question.strip() - config = load_config() - main(question, config) + main(question, load_config()) def entry_point() -> None: diff --git a/src/app/copilot_init.py b/src/copilot/app/copilot_init.py similarity index 87% rename from src/app/copilot_init.py rename to src/copilot/app/copilot_init.py index 4dff2d5..7922415 100644 --- a/src/app/copilot_init.py +++ b/src/copilot/app/copilot_init.py @@ -1,10 +1,11 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + # pylint: disable=W0611 import os import readline # noqa: F401 -from utilities import config_manager +from copilot.utilities import config_manager def setup_copilot(): @@ -31,9 +32,9 @@ def setup_copilot(): _prompt_for_config('spark_api_secret', '请输入你的星火大模型 App Secret:') if config.get('backend') == 'framework': if config.get('framework_url') == '': - _prompt_for_config('framework_url', '请输入你的 EulerCopilot URL:') + _prompt_for_config('framework_url', '请输入 EulerCopilot 智能体 URL:') if config.get('framework_api_key') == '': - _prompt_for_config('framework_api_key', '请输入你的 EulerCopilot API Key:') + _prompt_for_config('framework_api_key', '请输入 EulerCopilot 智能体 API Key:') if config.get('backend') == 'openai': if config.get('model_url') == '': _prompt_for_config('model_url', '请输入你的大模型 URL:') diff --git a/src/backends/framework_api.py b/src/copilot/backends/framework_api.py similarity index 97% rename from src/backends/framework_api.py rename to src/copilot/backends/framework_api.py index e9ad794..a734f47 100644 --- a/src/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -9,7 +9,7 @@ from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from backends.llm_service import LLMService +from copilot.backends.llm_service import LLMService class Framework(LLMService): diff --git a/src/backends/llm_service.py b/src/copilot/backends/llm_service.py similarity index 98% rename from src/backends/llm_service.py rename to src/copilot/backends/llm_service.py index fd5806a..959ebea 100644 --- a/src/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -3,7 +3,7 @@ import re from abc import ABC, abstractmethod -from utilities.env_info import get_os_info, is_root +from copilot.utilities.env_info import get_os_info, is_root class LLMService(ABC): diff --git a/src/backends/openai_api.py b/src/copilot/backends/openai_api.py similarity index 98% rename from src/backends/openai_api.py rename to src/copilot/backends/openai_api.py index 3345d86..f4cfa8c 100644 --- a/src/backends/openai_api.py +++ b/src/copilot/backends/openai_api.py @@ -9,7 +9,7 @@ from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from backends.llm_service import LLMService +from copilot.backends.llm_service import LLMService class ChatOpenAI(LLMService): diff --git a/src/backends/spark_api.py b/src/copilot/backends/spark_api.py similarity index 99% rename from src/backends/spark_api.py rename to src/copilot/backends/spark_api.py index d1a4ea9..a2e8434 100644 --- a/src/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -16,7 +16,7 @@ from rich.live import Live from rich.markdown import Markdown from rich.spinner import Spinner -from backends.llm_service import LLMService +from copilot.backends.llm_service import LLMService class Spark(LLMService): diff --git a/src/utilities/config_manager.py b/src/copilot/utilities/config_manager.py similarity index 100% rename from src/utilities/config_manager.py rename to src/copilot/utilities/config_manager.py diff --git a/src/utilities/env_info.py b/src/copilot/utilities/env_info.py similarity index 100% rename from src/utilities/env_info.py rename to src/copilot/utilities/env_info.py diff --git a/src/utilities/interact.py b/src/copilot/utilities/interact.py similarity index 100% rename from src/utilities/interact.py rename to src/copilot/utilities/interact.py diff --git a/src/setup.py b/src/setup.py index 4b2da9e..2378985 100644 --- a/src/setup.py +++ b/src/setup.py @@ -25,9 +25,9 @@ cython_compile_options = { # 定义 Cython 编译规则 cython_files = [] -cython_files += add_py_files('app') -cython_files += add_py_files('backends') -cython_files += add_py_files('utilities') +cython_files += add_py_files('copilot/app') +cython_files += add_py_files('copilot/backends') +cython_files += add_py_files('copilot/utilities') extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] @@ -39,13 +39,14 @@ setup( author='Hongyu Shi', author_email='shihongyu15@huawei.com', url='https://gitee.com/openeuler-customization/euler-copilot-shell', - py_modules=['copilot'], + py_modules=['copilot.__init__', 'copilot.__main__'], ext_modules=cythonize( extensions, compiler_directives=cython_compile_options['compiler_directives'], annotate=cython_compile_options['annotate'], language_level=cython_compile_options['language_level'] ), + packages=['copilot'], cmdclass={'build_ext': build_ext}, include_package_data=True, zip_safe=False, @@ -66,6 +67,6 @@ setup( 'typer', ], entry_points={ - 'console_scripts': ['copilot=copilot:run_command_line'] + 'console_scripts': ['copilot=copilot.__main__:entry_point'] } ) diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py deleted file mode 100644 index e69de29..0000000 -- Gitee From 63cf5434b32ed535b2c4df62e5e4ce41357432e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 16 May 2024 22:51:16 +0800 Subject: [PATCH 13/32] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20gitignore=EF=BC=9A?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20vscode=20=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- .gitignore | 1 - .vscode/settings.json | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 4649808..dcce618 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ # ide .idea -.vscode # Cython *.c diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1e37660 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "python.languageServer": "Pylance", + "python.terminal.focusAfterLaunch": true, + "python.analysis.extraPaths": ["${workspaceFolder}/src"], + "python.analysis.typeCheckingMode": "standard", + "[python]": { + "editor.formatOnSave": false, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "never", + "source.organizeImports": "always" + } + }, + "pylint.cwd": "${workspaceFolder}/src", + "ruff.lint.enable": true, + "ruff.organizeImports": true +} \ No newline at end of file -- Gitee From f1dfa7d59d3bd7e70244dfe51cf27bb62693c6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 21 May 2024 15:07:04 +0800 Subject: [PATCH 14/32] =?UTF-8?q?=E4=BC=98=E5=8C=96=20prompt=20&=20Markdow?= =?UTF-8?q?n=20=E8=A1=A8=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/copilot/app/copilot_app.py | 2 +- src/copilot/backends/framework_api.py | 10 ++++------ src/copilot/backends/llm_service.py | 13 ++++++++----- src/copilot/backends/openai_api.py | 6 +++--- src/copilot/backends/spark_api.py | 6 +++--- src/copilot/utilities/markdown_renderer.py | 18 ++++++++++++++++++ 6 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 src/copilot/utilities/markdown_renderer.py diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index c17c887..9456085 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -66,7 +66,7 @@ def handle_user_input(service: llm_service.LLMService, if mode == 'shell': cmd = service.get_shell_answer(user_input) exit_code: int = 0 - if cmd and interact.query_yes_or_no('\n\033[33m是否执行命令?\033[0m '): + if cmd and interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): exit_code = execute_shell_command(cmd) sys.exit(exit_code) elif mode == 'chat': diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index a734f47..c15f72f 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -6,10 +6,10 @@ import sys import requests from rich.console import Console from rich.live import Live -from rich.markdown import Markdown from rich.spinner import Spinner from copilot.backends.llm_service import LLMService +from copilot.utilities.markdown_renderer import MarkdownRenderer class Framework(LLMService): @@ -28,14 +28,12 @@ class Framework(LLMService): return self.content def get_shell_answer(self, question: str) -> str: - query = "请用单行shell命令回答以下问题:\n" + question + \ - "\n\n请直接以纯文本形式回复shell命令,不要添加任何多余内容。\n" + \ - "请注意你是 openEuler 的小助手,你所回答的命令必须被 openEuler 系统支持" + query = self._gen_shell_prompt(question) return self._extract_shell_code_blocks(self.get_general_answer(query)) def _stream_response(self, headers, data): spinner = Spinner('material') - with Live(console=self.console) as live: + with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) response = requests.post( self.endpoint, @@ -58,7 +56,7 @@ class Framework(LLMService): else: chunk = jcontent.get("content", "") self.content += chunk - live.update(Markdown(self.content, code_theme='github-dark'), refresh=True) + MarkdownRenderer.update(live, self.content) def _get_headers(self) -> dict: return { diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index 959ebea..9829214 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -30,7 +30,7 @@ class LLMService(ABC): leng = len(temp) length += leng return length - + def _gen_sudo_prompt(self) -> str: if is_root(): return '当前用户为 root 用户,你生成的 shell 命令不能包涵 sudo' @@ -42,8 +42,10 @@ class LLMService(ABC): 你的任务是: 根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 - 除非有特殊要求,你的回答必须使用 Markdown 格式,并使用中文标点符号; - 但如果用户要求你只输出单行 shell 命令,你就不能输出多余的格式或文字。 + + 格式要求: + 你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; + 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 用户可能问你一些操作系统相关的问题,你尤其需要注意安装软件包的情景: openEuler 使用 dnf 或 yum 管理软件包,你不能在回答中使用 apt 或其他命令; @@ -72,6 +74,7 @@ class LLMService(ABC): 要求: 1. 请用单行 shell 命令输出你的回答,不能使用多行 shell 命令 2. 请用 Markdown 代码块输出 shell 命令 - 3. 请解释你的回答,你要将你的解释附在命令代码块下方,你要有条理地解释命令中的每个步骤 - 4. 当前操作系统是 {get_os_info()},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 + 3. 如果用户要求你生成的命令涉及到数据输入,你需要正确处理数据输入的方式,包括用户交互 + 4. 请解释你的回答,你要将你的解释附在命令代码块下方,你要有条理地解释命令中的每个步骤 + 5. 当前操作系统是 {get_os_info()},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 ''' diff --git a/src/copilot/backends/openai_api.py b/src/copilot/backends/openai_api.py index f4cfa8c..dd55645 100644 --- a/src/copilot/backends/openai_api.py +++ b/src/copilot/backends/openai_api.py @@ -6,10 +6,10 @@ from typing import Union import requests from rich.console import Console from rich.live import Live -from rich.markdown import Markdown from rich.spinner import Spinner from copilot.backends.llm_service import LLMService +from copilot.utilities.markdown_renderer import MarkdownRenderer class ChatOpenAI(LLMService): @@ -60,7 +60,7 @@ class ChatOpenAI(LLMService): def _stream_response(self, query: str): spinner = Spinner('material') self.answer = '' - with Live(console=self.console) as live: + with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) try: response = requests.post( @@ -97,7 +97,7 @@ class ChatOpenAI(LLMService): chunk = delta.get('content', '') finish_reason = choices[0].get('finish_reason') self.answer += chunk - live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) + MarkdownRenderer.update(live, self.answer) if finish_reason == 'stop': self.history.append({'content': self.answer, 'role': 'assistant'}) break diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index a2e8434..3a457fd 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -13,10 +13,10 @@ from wsgiref.handlers import format_date_time import websockets from rich.console import Console from rich.live import Live -from rich.markdown import Markdown from rich.spinner import Spinner from copilot.backends.llm_service import LLMService +from copilot.utilities.markdown_renderer import MarkdownRenderer class Spark(LLMService): @@ -48,7 +48,7 @@ class Spark(LLMService): url = self._create_url() self.answer = '' spinner = Spinner('material') - with Live(console=self.console) as live: + with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) try: async with websockets.connect(url) as websocket: @@ -69,7 +69,7 @@ class Spark(LLMService): status = choices['status'] content = choices['text'][0]['content'] self.answer += content - live.update(Markdown(self.answer, code_theme='github-dark'), refresh=True) + MarkdownRenderer.update(live, self.answer) if status == 2: self.history.append({'role': 'assistant', 'content': self.answer}) break diff --git a/src/copilot/utilities/markdown_renderer.py b/src/copilot/utilities/markdown_renderer.py new file mode 100644 index 0000000..9b31a90 --- /dev/null +++ b/src/copilot/utilities/markdown_renderer.py @@ -0,0 +1,18 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +from rich.live import Live +from rich.markdown import Markdown +from rich.panel import Panel + + +class MarkdownRenderer: + + @staticmethod + def update(live: Live, markup: str): + live.update( + Panel( + Markdown(markup, code_theme='github-dark'), + border_style='gray50' + ), + refresh=True + ) -- Gitee From 86a6e09bf9bd1028069eeec005d9449e2cf1935d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 24 May 2024 11:00:16 +0800 Subject: [PATCH 15/32] =?UTF-8?q?=E4=BA=A7=E5=93=81=E6=94=B9=E5=90=8D?= =?UTF-8?q?=EF=BC=9ANeoCopilot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 4 ++-- src/copilot/app/copilot_cli.py | 6 +++--- src/copilot/app/copilot_init.py | 4 ++-- src/copilot/utilities/config_manager.py | 2 +- src/setup.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index 9e1fd44..a21d0e8 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -2,7 +2,7 @@ Name: eulercopilot Version: 1.1 Release: 1%{?dist}.%{?_timestamp} Group: Applications/Utilities -Summary: EulerCopilot CLI Tool +Summary: NeoCopilot Command Line Tool Source: %{name}-%{version}.tar.gz License: MulanPSL-2.0 URL: https://www.openeuler.org/zh/ @@ -14,7 +14,7 @@ BuildRequires: python3-Cython gcc Requires: python3 %description -EulerCopilot Command Line Tool +NeoCopilot Command Line Tool %prep %setup -q diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index 75d67b1..d5aa309 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -77,7 +77,7 @@ def cli( hidden=(not ADVANCED_MODE) ) ) -> None: - '''EulerCopilot 命令行助手''' + '''NeoCopilot 命令行助手''' if init: setup_copilot() return @@ -109,7 +109,7 @@ def cli( return else: print('\033[33m当前大模型后端不支持智能诊断功能\033[0m') - print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') + print('\033[33m推荐使用 NeoCopilot 智能体框架\033[0m') return elif tuning: if BACKEND == 'framework': @@ -118,7 +118,7 @@ def cli( return else: print('\033[33m当前大模型后端不支持智能调参功能\033[0m') - print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') + print('\033[33m推荐使用 NeoCopilot 智能体框架\033[0m') return if question: diff --git a/src/copilot/app/copilot_init.py b/src/copilot/app/copilot_init.py index 7922415..b10d8b7 100644 --- a/src/copilot/app/copilot_init.py +++ b/src/copilot/app/copilot_init.py @@ -32,9 +32,9 @@ def setup_copilot(): _prompt_for_config('spark_api_secret', '请输入你的星火大模型 App Secret:') if config.get('backend') == 'framework': if config.get('framework_url') == '': - _prompt_for_config('framework_url', '请输入 EulerCopilot 智能体 URL:') + _prompt_for_config('framework_url', '请输入 NeoCopilot 智能体 URL:') if config.get('framework_api_key') == '': - _prompt_for_config('framework_api_key', '请输入 EulerCopilot 智能体 API Key:') + _prompt_for_config('framework_api_key', '请输入 NeoCopilot 智能体 API Key:') if config.get('backend') == 'openai': if config.get('model_url') == '': _prompt_for_config('model_url', '请输入你的大模型 URL:') diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 7ff2510..4833ef6 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -62,7 +62,7 @@ def select_query_mode(mode: int): def select_backend(): backends = ['framework', 'spark', 'openai'] print('\n\033[1;33m请选择大模型后端:\033[0m\n') - print('\t<1> EulerCopilot') + print('\t<1> NeoCopilot') print('\t<2> 讯飞星火大模型 3.5') print('\t<3> 类 ChatGPT(兼容 llama.cpp)') print() diff --git a/src/setup.py b/src/setup.py index 2378985..3225ff9 100644 --- a/src/setup.py +++ b/src/setup.py @@ -35,7 +35,7 @@ extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] setup( name='copilot', version='1.1', - description='EulerCopilot CLI Tool', + description='NeoCopilot CLI Tool', author='Hongyu Shi', author_email='shihongyu15@huawei.com', url='https://gitee.com/openeuler-customization/euler-copilot-shell', -- Gitee From 19e0a60480daa7bbcf87064ccb80375361a31289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 30 May 2024 16:15:58 +0800 Subject: [PATCH 16/32] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/copilot/app/copilot_app.py | 7 +++- src/copilot/backends/framework_api.py | 49 ++++++++++++++++++--------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 9456085..191f0f1 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -54,7 +54,12 @@ def execute_shell_command(cmd: str) -> int: try: process = subprocess.Popen(shlex.split(cmd)) except FileNotFoundError as e: - print(f'命令不存在:{e}') + builtin_cmds = ['source', 'history', 'cd', 'export', 'alias', 'test'] + cmd_prefix = cmd.split()[0] + if cmd_prefix in builtin_cmds: + print(f'不支持执行 Shell 内置命令 "{cmd_prefix}",请复制后手动执行') + else: + print(f'命令不存在:{e}') return 1 exit_code = process.wait() return exit_code diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index c15f72f..a339773 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -1,7 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. import json -import sys import requests from rich.console import Console @@ -17,13 +16,13 @@ class Framework(LLMService): self.endpoint: str = url self.api_key: str = api_key self.session_id: str = session_id - self.content: str = "" + self.content: str = '' # 富文本显示 self.console = Console() def get_general_answer(self, question: str) -> str: headers = self._get_headers() - data = {"question": question, "session_id": self.session_id} + data = {'question': question, 'session_id': self.session_id} self._stream_response(headers, data) return self.content @@ -35,32 +34,50 @@ class Framework(LLMService): spinner = Spinner('material') with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) - response = requests.post( - self.endpoint, - headers=headers, - json=data, - stream=True, - timeout=60 - ) + try: + response = requests.post( + self.endpoint, + headers=headers, + json=data, + stream=True, + timeout=60 + ) + except requests.exceptions.ConnectionError: + live.update('NeoCopilot 智能体连接失败', refresh=True) + return + except requests.exceptions.Timeout: + live.update('NeoCopilot 智能体请求超时', refresh=True) + return + except requests.exceptions.RequestException: + live.update('NeoCopilot 智能体请求异常', refresh=True) + return if response.status_code != 200: - sys.stderr.write(f"{response.status_code} 请求失败\n") + live.update(f'请求失败: {response.status_code}', refresh=True) return for line in response.iter_lines(): if line is None: continue - content = line.decode('utf-8').strip("data: ") + content = line.decode('utf-8').strip('data: ') try: jcontent = json.loads(content) except json.JSONDecodeError: - continue + if content == '[ERROR]': + MarkdownRenderer.update(live, 'NeoCopilot 智能体系统繁忙,请稍候再试') + self.content = '' + elif content == '[SENSITIVE]': + MarkdownRenderer.update(live, '检测到违规信息,请重新提问') + self.content = '' + elif content != '[DONE]': + MarkdownRenderer.update(live, f'NeoCopilot 智能体返回了未知内容:{content}') + break else: - chunk = jcontent.get("content", "") + chunk = jcontent.get('content', '') self.content += chunk MarkdownRenderer.update(live, self.content) def _get_headers(self) -> dict: return { - "Accept": "application/json", - "Content-Type": "application/json", + 'Accept': 'application/json', + 'Content-Type': 'application/json', 'Authorization': f'Bearer {self.api_key}' } -- Gitee From 11dd15b817538f66581e7617a9fad517a2bebed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 31 May 2024 18:01:33 +0800 Subject: [PATCH 17/32] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 自动获取内网IP地址 Signed-off-by: 史鸿宇 --- src/copilot/app/copilot_app.py | 9 ++++ src/copilot/backends/framework_api.py | 62 ++++++++++++++++++++++++- src/copilot/utilities/config_manager.py | 2 +- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 191f0f1..45dc493 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -76,6 +76,15 @@ def handle_user_input(service: llm_service.LLMService, sys.exit(exit_code) elif mode == 'chat': service.get_general_answer(user_input) + elif mode == 'diagnose': + if isinstance(service, framework_api.Framework): + report = service.diagnose(user_input) + if report: + sys.exit(0) + sys.exit(1) + elif mode == 'tuning': + if isinstance(service, framework_api.Framework): + service.tuning(user_input) def exit_copilot(msg: str = '', code: int = 0): diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index a339773..0ab6754 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -1,6 +1,9 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. import json +import re +import socket +import subprocess import requests from rich.console import Console @@ -30,6 +33,38 @@ class Framework(LLMService): query = self._gen_shell_prompt(question) return self._extract_shell_code_blocks(self.get_general_answer(query)) + def diagnose(self, question: str) -> str: + # 确保用户输入的问题中包含有效的IP地址,若没有,则诊断本机 + if not self._contains_valid_ip(question): + local_ip = self._get_local_ip() + if local_ip: + question = f'当前机器的IP为 {local_ip},' + question + headers = self._get_headers() + data = { + 'question': question, + 'session_id': self.session_id, + 'user_selected_plugins': [ + { + 'plugin_name': 'Diagnostic' + } + ] + } + self._stream_response(headers, data) + return self.content + + def tuning(self, question: str): + headers = self._get_headers() + data = { + 'question': question, + 'session_id': self.session_id, + 'user_selected_plugins': [ + { + 'plugin_name': 'tuning' + } + ] + } + self._stream_response(headers, data) + def _stream_response(self, headers, data): spinner = Spinner('material') with Live(console=self.console, vertical_overflow='visible') as live: @@ -40,7 +75,7 @@ class Framework(LLMService): headers=headers, json=data, stream=True, - timeout=60 + timeout=300 ) except requests.exceptions.ConnectionError: live.update('NeoCopilot 智能体连接失败', refresh=True) @@ -61,6 +96,8 @@ class Framework(LLMService): try: jcontent = json.loads(content) except json.JSONDecodeError: + if content == '': + continue if content == '[ERROR]': MarkdownRenderer.update(live, 'NeoCopilot 智能体系统繁忙,请稍候再试') self.content = '' @@ -81,3 +118,26 @@ class Framework(LLMService): 'Content-Type': 'application/json', 'Authorization': f'Bearer {self.api_key}' } + + def _contains_valid_ip(self, text: str) -> bool: + ip_pattern = r'\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b' + match = re.search(ip_pattern, text) + return bool(match) + + def _get_local_ip(self) -> str: + try: + process = subprocess.run( + ['hostname', '-I'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True) + except (FileNotFoundError, subprocess.CalledProcessError): + try: + ip_list = socket.gethostbyname_ex(socket.gethostname())[2] + except socket.gaierror: + return '' + return ip_list[-1] + if process.stdout: + ip_address = process.stdout.decode('utf-8').strip().split(' ', maxsplit=1)[0] + return ip_address + return '' diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 4833ef6..31b37d9 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -54,7 +54,7 @@ def update_config(key: str, value): def select_query_mode(mode: int): - modes = ['shell', 'chat'] + modes = ['shell', 'chat', 'diagnose', 'tuning'] if mode < len(modes): update_config('query_mode', modes[mode]) -- Gitee From 7ae9f449e8cac96f0faf82cb331848458df5c0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 4 Jun 2024 15:26:32 +0800 Subject: [PATCH 18/32] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E8=B0=83=E4=BC=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 其他修复:只在 main 中调用 sys.exit() Signed-off-by: 史鸿宇 --- src/copilot.py | 5 +++- src/copilot/__main__.py | 5 +++- src/copilot/app/copilot_app.py | 36 ++++++++++++++------------- src/copilot/app/copilot_cli.py | 32 +++++++++++++----------- src/copilot/backends/framework_api.py | 5 ++-- src/copilot/backends/llm_service.py | 5 ++-- src/copilot/utilities/env_info.py | 2 +- 7 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/copilot.py b/src/copilot.py index 1f13657..88e8640 100755 --- a/src/copilot.py +++ b/src/copilot.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +import sys + from copilot.__main__ import entry_point if __name__ == "__main__": - entry_point() + code = entry_point() + sys.exit(code) diff --git a/src/copilot/__main__.py b/src/copilot/__main__.py index ec296b2..d266ac4 100644 --- a/src/copilot/__main__.py +++ b/src/copilot/__main__.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +import sys + from copilot.app.copilot_cli import entry_point if __name__ == "__main__": - entry_point() + code = entry_point() + sys.exit(code) diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 45dc493..f8b858c 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -6,7 +6,6 @@ import re import readline # noqa: F401 import shlex import subprocess -import sys import uuid from typing import Union @@ -66,34 +65,34 @@ def execute_shell_command(cmd: str) -> int: def handle_user_input(service: llm_service.LLMService, - user_input: str, mode: str) -> None: + user_input: str, mode: str) -> int: '''Process user input based on the given flag and backend configuration.''' if mode == 'shell': cmd = service.get_shell_answer(user_input) exit_code: int = 0 if cmd and interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): exit_code = execute_shell_command(cmd) - sys.exit(exit_code) + return exit_code elif mode == 'chat': service.get_general_answer(user_input) + return -1 elif mode == 'diagnose': if isinstance(service, framework_api.Framework): report = service.diagnose(user_input) if report: - sys.exit(0) - sys.exit(1) + return 0 + return 1 elif mode == 'tuning': if isinstance(service, framework_api.Framework): - service.tuning(user_input) - - -def exit_copilot(msg: str = '', code: int = 0): - '''Exit the program with a message.''' - print(msg) - sys.exit(code) + report = service.tuning(user_input) + if report: + return 0 + return 1 + else: + return 1 -def main(user_input: Union[str, None], config: dict): +def main(user_input: Union[str, None], config: dict) -> int: backend = config.get('backend') mode = str(config.get('query_mode')) service: Union[llm_service.LLMService, None] = None @@ -119,7 +118,8 @@ def main(user_input: Union[str, None], config: dict): ) if service is None: - exit_copilot('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m', 1) + print('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') + return 1 else: if mode == 'shell': print('\033[33m当前模式:Shell 命令生成\033[0m') @@ -130,8 +130,10 @@ def main(user_input: Union[str, None], config: dict): if user_input is None: user_input = input('\033[35m>>>\033[0m ') if user_input.lower().startswith('exit'): - exit_copilot() - handle_user_input(service, user_input, mode) + return 0 + exit_code = handle_user_input(service, user_input, mode) + if exit_code != -1: + return exit_code user_input = None # Reset user_input for next iteration (only if continuing service) except KeyboardInterrupt: - exit_copilot() + return 0 diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index d5aa309..ad59b54 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -3,6 +3,7 @@ # pylint: disable=R0911,R0912,R0913 import os +import sys from typing import Optional import typer @@ -76,60 +77,61 @@ def cli( rich_help_panel='高级选项', hidden=(not ADVANCED_MODE) ) -) -> None: +) -> int: '''NeoCopilot 命令行助手''' if init: setup_copilot() - return + return 0 if backend: if ADVANCED_MODE: select_backend() - return + return 0 if settings: if ADVANCED_MODE: edit_config() - return + return 0 if sum(map(bool, [shell, chat, diagnose, tuning])) > 1: print('\033[1;31m当前版本只能选择一种问答模式\033[0m') - return + return 1 if shell: select_query_mode(0) if not question: - return + return 0 elif chat: select_query_mode(1) if not question: - return + return 0 elif diagnose: if BACKEND == 'framework': select_query_mode(2) if not question: - return + return 0 else: print('\033[33m当前大模型后端不支持智能诊断功能\033[0m') print('\033[33m推荐使用 NeoCopilot 智能体框架\033[0m') - return + return 1 elif tuning: if BACKEND == 'framework': select_query_mode(3) if not question: - return + return 0 else: print('\033[33m当前大模型后端不支持智能调参功能\033[0m') print('\033[33m推荐使用 NeoCopilot 智能体框架\033[0m') - return + return 1 if question: question = question.strip() - main(question, load_config()) + return main(question, load_config()) -def entry_point() -> None: - app() +def entry_point() -> int: + return app() if __name__ == '__main__': - entry_point() + code = entry_point() + sys.exit(code) diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index 0ab6754..d3ba382 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -52,18 +52,19 @@ class Framework(LLMService): self._stream_response(headers, data) return self.content - def tuning(self, question: str): + def tuning(self, question: str) -> str: headers = self._get_headers() data = { 'question': question, 'session_id': self.session_id, 'user_selected_plugins': [ { - 'plugin_name': 'tuning' + 'plugin_name': 'Tuning' } ] } self._stream_response(headers, data) + return self.content def _stream_response(self, headers, data): spinner = Spinner('material') diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index 9829214..54507cb 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -34,8 +34,7 @@ class LLMService(ABC): def _gen_sudo_prompt(self) -> str: if is_root(): return '当前用户为 root 用户,你生成的 shell 命令不能包涵 sudo' - else: - return '当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo' + return '当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo' def _gen_system_prompt(self) -> str: return f'''你是操作系统 {get_os_info()} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 @@ -62,7 +61,7 @@ class LLMService(ABC): `dnf remove -y package_name` 你不能输出类似于上述例子的命令! - 由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过300字。 + 由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 ''' def _gen_shell_prompt(self, question: str) -> str: diff --git a/src/copilot/utilities/env_info.py b/src/copilot/utilities/env_info.py index b9c05a0..9283534 100644 --- a/src/copilot/utilities/env_info.py +++ b/src/copilot/utilities/env_info.py @@ -19,7 +19,7 @@ def _exec_shell_cmd(cmd: list) -> Union[subprocess.CompletedProcess, None]: ) except subprocess.CalledProcessError as e: sys.stderr.write(e.stderr) - except Exception as e: + except FileNotFoundError as e: sys.stderr.write(str(e)) else: return process -- Gitee From a980659d3bc440dd6e275fdccb76bf275edde8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Wed, 5 Jun 2024 09:18:43 +0800 Subject: [PATCH 19/32] =?UTF-8?q?=E5=90=8D=E7=A7=B0=E6=94=B9=E5=9B=9E=20Eu?= =?UTF-8?q?lerCopilot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 4 ++-- src/copilot/app/copilot_app.py | 8 +++++++- src/copilot/app/copilot_cli.py | 6 +++--- src/copilot/app/copilot_init.py | 4 ++-- src/copilot/backends/framework_api.py | 23 ++++++++++++++--------- src/copilot/utilities/config_manager.py | 3 ++- src/setup.py | 2 +- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index a21d0e8..03a1b63 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -2,7 +2,7 @@ Name: eulercopilot Version: 1.1 Release: 1%{?dist}.%{?_timestamp} Group: Applications/Utilities -Summary: NeoCopilot Command Line Tool +Summary: EulerCopilot 命令行助手 Source: %{name}-%{version}.tar.gz License: MulanPSL-2.0 URL: https://www.openeuler.org/zh/ @@ -14,7 +14,7 @@ BuildRequires: python3-Cython gcc Requires: python3 %description -NeoCopilot Command Line Tool +EulerCopilot 命令行助手 %prep %setup -q diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index f8b858c..64ac1f5 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -100,7 +100,8 @@ def main(user_input: Union[str, None], config: dict) -> int: service = framework_api.Framework( url=config.get('framework_url'), api_key=config.get('framework_api_key'), - session_id=str(uuid.uuid4().hex) + session_id=str(uuid.uuid4().hex), + debug_mode=config.get('debug_mode', False) ) elif backend == 'spark': service = spark_api.Spark( @@ -125,6 +126,10 @@ def main(user_input: Union[str, None], config: dict) -> int: print('\033[33m当前模式:Shell 命令生成\033[0m') if mode == 'chat': print('\033[33m当前模式:智能问答\033[0m 输入 \'exit\' 或按下 Ctrl+C 退出服务') + if mode == 'diagnose': + print('\033[33m当前模式:智能诊断\033[0m') + if mode == 'tuning': + print('\033[33m当前模式:智能调优\033[0m') try: while True: if user_input is None: @@ -136,4 +141,5 @@ def main(user_input: Union[str, None], config: dict) -> int: return exit_code user_input = None # Reset user_input for next iteration (only if continuing service) except KeyboardInterrupt: + print() return 0 diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index ad59b54..efc5930 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -78,7 +78,7 @@ def cli( hidden=(not ADVANCED_MODE) ) ) -> int: - '''NeoCopilot 命令行助手''' + '''EulerCopilot 命令行助手''' if init: setup_copilot() return 0 @@ -110,7 +110,7 @@ def cli( return 0 else: print('\033[33m当前大模型后端不支持智能诊断功能\033[0m') - print('\033[33m推荐使用 NeoCopilot 智能体框架\033[0m') + print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') return 1 elif tuning: if BACKEND == 'framework': @@ -119,7 +119,7 @@ def cli( return 0 else: print('\033[33m当前大模型后端不支持智能调参功能\033[0m') - print('\033[33m推荐使用 NeoCopilot 智能体框架\033[0m') + print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') return 1 if question: diff --git a/src/copilot/app/copilot_init.py b/src/copilot/app/copilot_init.py index b10d8b7..7922415 100644 --- a/src/copilot/app/copilot_init.py +++ b/src/copilot/app/copilot_init.py @@ -32,9 +32,9 @@ def setup_copilot(): _prompt_for_config('spark_api_secret', '请输入你的星火大模型 App Secret:') if config.get('backend') == 'framework': if config.get('framework_url') == '': - _prompt_for_config('framework_url', '请输入 NeoCopilot 智能体 URL:') + _prompt_for_config('framework_url', '请输入 EulerCopilot 智能体 URL:') if config.get('framework_api_key') == '': - _prompt_for_config('framework_api_key', '请输入 NeoCopilot 智能体 API Key:') + _prompt_for_config('framework_api_key', '请输入 EulerCopilot 智能体 API Key:') if config.get('backend') == 'openai': if config.get('model_url') == '': _prompt_for_config('model_url', '请输入你的大模型 URL:') diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index d3ba382..d91e63e 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -15,10 +15,11 @@ from copilot.utilities.markdown_renderer import MarkdownRenderer class Framework(LLMService): - def __init__(self, url, api_key, session_id): + def __init__(self, url, api_key, session_id, debug_mode=False): self.endpoint: str = url self.api_key: str = api_key self.session_id: str = session_id + self.debug_mode: bool = debug_mode self.content: str = '' # 富文本显示 self.console = Console() @@ -67,6 +68,7 @@ class Framework(LLMService): return self.content def _stream_response(self, headers, data): + self.content = '' spinner = Spinner('material') with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) @@ -79,13 +81,13 @@ class Framework(LLMService): timeout=300 ) except requests.exceptions.ConnectionError: - live.update('NeoCopilot 智能体连接失败', refresh=True) + live.update('EulerCopilot 智能体连接失败', refresh=True) return except requests.exceptions.Timeout: - live.update('NeoCopilot 智能体请求超时', refresh=True) + live.update('EulerCopilot 智能体请求超时', refresh=True) return except requests.exceptions.RequestException: - live.update('NeoCopilot 智能体请求异常', refresh=True) + live.update('EulerCopilot 智能体请求异常', refresh=True) return if response.status_code != 200: live.update(f'请求失败: {response.status_code}', refresh=True) @@ -100,13 +102,15 @@ class Framework(LLMService): if content == '': continue if content == '[ERROR]': - MarkdownRenderer.update(live, 'NeoCopilot 智能体系统繁忙,请稍候再试') - self.content = '' + if not self.content: + MarkdownRenderer.update(live, 'EulerCopilot 智能体遇到错误,请联系管理员定位问题') elif content == '[SENSITIVE]': MarkdownRenderer.update(live, '检测到违规信息,请重新提问') self.content = '' elif content != '[DONE]': - MarkdownRenderer.update(live, f'NeoCopilot 智能体返回了未知内容:{content}') + if not self.debug_mode: + continue + MarkdownRenderer.update(live, f'EulerCopilot 智能体返回了未知内容:\n```json\n{content}\n```') break else: chunk = jcontent.get('content', '') @@ -115,8 +119,9 @@ class Framework(LLMService): def _get_headers(self) -> dict: return { - 'Accept': 'application/json', - 'Content-Type': 'application/json', + 'Accept': '*/*', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'keep-alive', 'Authorization': f'Bearer {self.api_key}' } diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 31b37d9..56bacbe 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -10,6 +10,7 @@ DEFAULT_CONFIG = { "backend": "spark", "query_mode": "shell", "advanced_mode": False, + "debug_mode": False, "spark_app_id": "", "spark_api_key": "", "spark_api_secret": "", @@ -62,7 +63,7 @@ def select_query_mode(mode: int): def select_backend(): backends = ['framework', 'spark', 'openai'] print('\n\033[1;33m请选择大模型后端:\033[0m\n') - print('\t<1> NeoCopilot') + print('\t<1> EulerCopilot') print('\t<2> 讯飞星火大模型 3.5') print('\t<3> 类 ChatGPT(兼容 llama.cpp)') print() diff --git a/src/setup.py b/src/setup.py index 3225ff9..2378985 100644 --- a/src/setup.py +++ b/src/setup.py @@ -35,7 +35,7 @@ extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] setup( name='copilot', version='1.1', - description='NeoCopilot CLI Tool', + description='EulerCopilot CLI Tool', author='Hongyu Shi', author_email='shihongyu15@huawei.com', url='https://gitee.com/openeuler-customization/euler-copilot-shell', -- Gitee From cf121919da54086277bcb656ee2d6ceaddda8e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 7 Jun 2024 18:08:00 +0800 Subject: [PATCH 20/32] =?UTF-8?q?=E5=9C=A8=E5=91=BD=E4=BB=A4=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E7=AC=A6=E6=98=BE=E7=A4=BA=E5=BD=93=E5=89=8D=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 25 +++++++- src/copilot/app/copilot_app.py | 8 +-- src/eulercopilot.sh | 103 +++++++++++++++++++++++++++++++++ src/eulercopilot_shortcut.sh | 31 ---------- 4 files changed, 126 insertions(+), 41 deletions(-) create mode 100644 src/eulercopilot.sh delete mode 100644 src/eulercopilot_shortcut.sh diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index 03a1b63..c135c2c 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -11,7 +11,7 @@ BuildRequires: python3-devel python3-setuptools BuildRequires: python3-pip BuildRequires: python3-Cython gcc -Requires: python3 +Requires: python3 jq %description EulerCopilot 命令行助手 @@ -35,9 +35,28 @@ python3 -m venv .venv install -d %{buildroot}/%{_bindir} install -c -m 0755 %{_builddir}/%{name}-%{version}/dist/copilot %{buildroot}/%{_bindir} install -d %{buildroot}/etc/profile.d -install -c -m 0755 %{_builddir}/%{name}-%{version}/eulercopilot_shortcut.sh %{buildroot}/etc/profile.d +install -c -m 0755 %{_builddir}/%{name}-%{version}/eulercopilot.sh %{buildroot}/etc/profile.d %files %defattr(-,root,root,-) /usr/bin/copilot -/etc/profile.d/eulercopilot_shortcut.sh +/etc/profile.d/eulercopilot.sh + +%pre +cat << 'EOF' >> /etc/bashrc +# >>> eulercopilot >>> +run_after_return() { + if [[ "$PS1" == *"\[\033[1;33m"* ]]; then + revert_copilot_prompt + set_copilot_prompt + fi +} +PROMPT_COMMAND="${PROMPT_COMMAND:+${PROMPT_COMMAND}; }run_after_return" +set_copilot_prompt +# <<< eulercopilot <<< +EOF + +%postun +if [ ! -f /usr/bin/copilot ]; then + sed -i '/# >>> eulercopilot >>>/,/# <<< eulercopilot << int: print('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') return 1 else: - if mode == 'shell': - print('\033[33m当前模式:Shell 命令生成\033[0m') if mode == 'chat': - print('\033[33m当前模式:智能问答\033[0m 输入 \'exit\' 或按下 Ctrl+C 退出服务') - if mode == 'diagnose': - print('\033[33m当前模式:智能诊断\033[0m') - if mode == 'tuning': - print('\033[33m当前模式:智能调优\033[0m') + print('\033[33m输入 \'exit\' 或按下 Ctrl+C 结束对话\033[0m') try: while True: if user_input is None: diff --git a/src/eulercopilot.sh b/src/eulercopilot.sh new file mode 100644 index 0000000..164e8c1 --- /dev/null +++ b/src/eulercopilot.sh @@ -0,0 +1,103 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +read_query_mode() { + if [ ! -f ~/.config/eulercopilot/config.json ]; then + return + fi + + local query_mode + query_mode=$(jq '.query_mode' ~/.config/eulercopilot/config.json) + + if [ "$query_mode" = "\"shell\"" ]; then + echo "命令生成" + elif [ "$query_mode" = "\"chat\"" ]; then + echo "智能问答" + elif [ "$query_mode" = "\"diagnose\"" ]; then + echo "智能诊断" + elif [ "$query_mode" = "\"tuning\"" ]; then + echo "智能调优" + else + echo "未知模式" + fi +} + +get_prompt() { + local username + local hostname + local current_base_dir + local prompt_end + local prompt + + username=$(whoami) + hostname=$(hostname -s) + if [[ "$PWD" == "$HOME" ]]; then + current_base_dir='~' + else + current_base_dir=$(basename "$PWD") + fi + if [[ $EUID -eq 0 ]]; then + prompt_end='#' + else + prompt_end='$' + fi + prompt="${PS1//\\u/$username}" + prompt="${prompt//\\h/$hostname}" + prompt="${prompt//\\W/$current_base_dir}" + prompt="${prompt//\\$/$prompt_end}" + echo "${prompt}" +} + +set_prompt() { + local query_mode + query_mode="$(read_query_mode)" + + if [ -z "$query_mode" ]; then + return + fi + + if [[ "$PS1" != *"\[\033[1;33m"* ]]; then + PS1="╭─ \[\033[1;33m\]${query_mode}\[\033[0m\] ─╮\n╰${PS1}" + fi +} + +revert_prompt() { + PS1="${PS1#*╰}" +} + +run_copilot() { + local terminal_settings + local readline="${READLINE_LINE}" + if [[ -z "${readline}" ]]; then + READLINE_LINE="copilot " + READLINE_POINT=${#READLINE_LINE} + elif [[ ! "${readline}" =~ ^copilot ]]; then + terminal_settings=$(stty -g) + READLINE_LINE="" + local _ps1 + local prompt + _ps1=$(get_prompt) + prompt=${_ps1#*\\n} + history -s "${readline}" && echo "${prompt}${readline}" + stty sane && (copilot "${readline}") + stty "${terminal_settings}" + if [[ $_ps1 =~ \\n ]]; then + prompt="${_ps1%%\\n*}" + prompt="${prompt//\\[/}" + prompt="${prompt//\\]/}" + echo -e "${prompt}" + fi + elif [[ "${readline}" == "copilot " ]]; then + READLINE_LINE="" + if [[ "$PS1" == *"\[\033[1;33m"* ]]; then + revert_prompt + printf "\033[1;31m已关闭 EulerCopilot 提示符\033[0m\n" + else + set_prompt + printf "\033[1;32m已开启 EulerCopilot 提示符\033[0m\n" + fi + fi +} + +bind -x '"\C-o": run_copilot' 2>/dev/null +alias set_copilot_prompt='set_prompt' +alias revert_copilot_prompt='revert_prompt' diff --git a/src/eulercopilot_shortcut.sh b/src/eulercopilot_shortcut.sh deleted file mode 100644 index 3296d2b..0000000 --- a/src/eulercopilot_shortcut.sh +++ /dev/null @@ -1,31 +0,0 @@ -run_copilot() { - local terminal_settings=$(stty -g) - local readline="${READLINE_LINE}" - if [[ -z "${readline}" ]]; then - READLINE_LINE="copilot " - READLINE_POINT=${#READLINE_LINE} - elif [[ ! "${readline}" =~ ^copilot ]]; then - READLINE_LINE="" - local username=$(whoami) - local hostname=$(hostname -s) - if [[ "$PWD" == "$HOME" ]]; then - local current_base_dir='~' - else - local current_base_dir=$(basename "$PWD") - fi - if [[ $EUID -eq 0 ]]; then - local prompt_end='#' - else - local prompt_end='$' - fi - local prompt="${PS1//\\u/$username}" - prompt="${prompt//\\h/$hostname}" - prompt="${prompt//\\W/$current_base_dir}" - prompt="${prompt//\\$/$prompt_end}" - history -s "${readline}" && echo "${prompt}${readline}" - stty sane && (copilot "${readline}") - fi - stty "${terminal_settings}" -} - -bind -x '"\C-o": run_copilot' 2>/dev/null -- Gitee From 9fe8bffabee5a0d15bdafd5b89275fc9c8c44b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 13 Jun 2024 11:50:37 +0800 Subject: [PATCH 21/32] =?UTF-8?q?Feat=EF=BC=9A=E6=8E=A5=E5=85=A5=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E8=B0=83=E4=BC=98=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/copilot/app/copilot_app.py | 47 +++++++++++++-------------- src/copilot/backends/framework_api.py | 7 +++- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index b2d2051..452dce5 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -73,23 +73,19 @@ def handle_user_input(service: llm_service.LLMService, if cmd and interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): exit_code = execute_shell_command(cmd) return exit_code - elif mode == 'chat': + if mode == 'chat': service.get_general_answer(user_input) return -1 - elif mode == 'diagnose': - if isinstance(service, framework_api.Framework): + if isinstance(service, framework_api.Framework): + if mode == 'diagnose': report = service.diagnose(user_input) if report: return 0 - return 1 - elif mode == 'tuning': - if isinstance(service, framework_api.Framework): + if mode == 'tuning': report = service.tuning(user_input) if report: return 0 - return 1 - else: - return 1 + return 1 def main(user_input: Union[str, None], config: dict) -> int: @@ -121,19 +117,20 @@ def main(user_input: Union[str, None], config: dict) -> int: if service is None: print('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') return 1 - else: - if mode == 'chat': - print('\033[33m输入 \'exit\' 或按下 Ctrl+C 结束对话\033[0m') - try: - while True: - if user_input is None: - user_input = input('\033[35m>>>\033[0m ') - if user_input.lower().startswith('exit'): - return 0 - exit_code = handle_user_input(service, user_input, mode) - if exit_code != -1: - return exit_code - user_input = None # Reset user_input for next iteration (only if continuing service) - except KeyboardInterrupt: - print() - return 0 + + if mode == 'chat': + print('\033[33m输入 \'exit\' 或按下 Ctrl+C 结束对话\033[0m') + + try: + while True: + if user_input is None: + user_input = input('\033[35m>>>\033[0m ') + if user_input.lower().startswith('exit'): + return 0 + exit_code = handle_user_input(service, user_input, mode) + if exit_code != -1: + return exit_code + user_input = None # Reset user_input for next iteration (only if continuing service) + except KeyboardInterrupt: + print() + return 0 diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index d91e63e..e2c48db 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -54,13 +54,18 @@ class Framework(LLMService): return self.content def tuning(self, question: str) -> str: + # 确保用户输入的问题中包含有效的IP地址,若没有,则调优本机 + if not self._contains_valid_ip(question): + local_ip = self._get_local_ip() + if local_ip: + question = f'当前机器的IP为 {local_ip},' + question headers = self._get_headers() data = { 'question': question, 'session_id': self.session_id, 'user_selected_plugins': [ { - 'plugin_name': 'Tuning' + 'plugin_name': 'A-Tune' } ] } -- Gitee From e8325c926e949bf376a615317775901abb84daf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Sat, 22 Jun 2024 22:10:20 +0800 Subject: [PATCH 22/32] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A3=80?= =?UTF-8?q?=E6=9F=A5IP=E5=9C=B0=E5=9D=80=E7=9A=84=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/copilot/backends/framework_api.py | 32 +++++++-------------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index e2c48db..45547f4 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -44,11 +44,7 @@ class Framework(LLMService): data = { 'question': question, 'session_id': self.session_id, - 'user_selected_plugins': [ - { - 'plugin_name': 'Diagnostic' - } - ] + 'user_selected_plugins': [{'plugin_name': 'Diagnostic'}], } self._stream_response(headers, data) return self.content @@ -63,11 +59,7 @@ class Framework(LLMService): data = { 'question': question, 'session_id': self.session_id, - 'user_selected_plugins': [ - { - 'plugin_name': 'A-Tune' - } - ] + 'user_selected_plugins': [{'plugin_name': 'A-Tune'}], } self._stream_response(headers, data) return self.content @@ -78,13 +70,7 @@ class Framework(LLMService): with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) try: - response = requests.post( - self.endpoint, - headers=headers, - json=data, - stream=True, - timeout=300 - ) + response = requests.post(self.endpoint, headers=headers, json=data, stream=True, timeout=300) except requests.exceptions.ConnectionError: live.update('EulerCopilot 智能体连接失败', refresh=True) return @@ -127,21 +113,19 @@ class Framework(LLMService): 'Accept': '*/*', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'keep-alive', - 'Authorization': f'Bearer {self.api_key}' + 'Authorization': f'Bearer {self.api_key}', } def _contains_valid_ip(self, text: str) -> bool: - ip_pattern = r'\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b' + ip_pattern = re.compile( + r'(? str: try: - process = subprocess.run( - ['hostname', '-I'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=True) + process = subprocess.run(['hostname', '-I'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) except (FileNotFoundError, subprocess.CalledProcessError): try: ip_list = socket.gethostbyname_ex(socket.gethostname())[2] -- Gitee From 10854be020e73cc76e363ca00e51f3eb46ea418f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Mon, 15 Jul 2024 17:17:04 +0800 Subject: [PATCH 23/32] =?UTF-8?q?ShellCheck:=20SC2046=20=E2=80=93=20Quote?= =?UTF-8?q?=20this=20to=20prevent=20word=20splitting.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/build_rpm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/build_rpm.sh b/distribution/build_rpm.sh index 22d439d..587c8f3 100644 --- a/distribution/build_rpm.sh +++ b/distribution/build_rpm.sh @@ -21,7 +21,7 @@ if [[ ! -f "$spec_file" ]]; then fi # Remove old builds -rm -f ~/rpmbuild/RPMS/$(uname -m)/eulercopilot-* +rm -f ~/rpmbuild/RPMS/"$(uname -m)"/eulercopilot-* # Build the RPM package using rpmbuild rpmbuild --define "_timestamp $(date +%s)" -bb "$spec_file" --nodebuginfo -- Gitee From fbf6738247da7f330abb8cf449a24b3deb3cae01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 30 Jul 2024 09:35:19 +0800 Subject: [PATCH 24/32] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=98=BE=E7=A4=BA=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/copilot/backends/framework_api.py | 1 + src/copilot/backends/spark_api.py | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index 45547f4..c2479aa 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -64,6 +64,7 @@ class Framework(LLMService): self._stream_response(headers, data) return self.content + # pylint: disable=R0912 def _stream_response(self, headers, data): self.content = '' spinner = Spinner('material') diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index 3a457fd..364ed99 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -14,12 +14,15 @@ import websockets from rich.console import Console from rich.live import Live from rich.spinner import Spinner +from rich.text import Text from copilot.backends.llm_service import LLMService from copilot.utilities.markdown_renderer import MarkdownRenderer +# pylint: disable=R0902 class Spark(LLMService): + # pylint: disable=R0913 def __init__(self, app_id, api_key, api_secret, spark_url, domain, max_tokens=4096): self.app_id: str = app_id self.api_key: str = api_key @@ -77,11 +80,15 @@ class Spark(LLMService): break except websockets.exceptions.InvalidStatusCode: - live.update(f'\033[1;31m请求错误\033[0m\n\ - 请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。\n\ - 输入 "vi ~/.config/eulercopilot/config.json" 查看和编辑配置;\n\ - 或尝试 ping {self.spark_url}', - refresh=True) + live.update( + Text.from_ansi('\033[1;31m请求错误\033[0m\n\n')\ + .append('请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。\n')\ + .append('输入 "vi ~/.config/eulercopilot/config.json" 查看和编辑配置;\n')\ + .append(f'或尝试 ping {self.spark_url}'), + refresh=True + ) + except Exception: # pylint: disable=W0718 + live.update('访问大模型失败,请检查网络连接') def _create_url(self): now = datetime.now() # 生成RFC1123格式的时间戳 -- Gitee From 8b49c97469acaf2aee1816a9e449b8da4f9f518d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Wed, 28 Aug 2024 16:21:03 +0800 Subject: [PATCH 25/32] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=95=B4=E5=90=88?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E9=97=AE=E7=AD=94/=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E7=94=9F=E6=88=90=EF=BC=9B=E6=94=AF=E6=8C=81=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E6=8E=A8=E8=8D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- ruff.toml | 3 +++ src/copilot/app/copilot_app.py | 15 +++++++---- src/copilot/app/copilot_cli.py | 17 +++++------- src/copilot/backends/framework_api.py | 20 ++++++++++++--- src/copilot/backends/llm_service.py | 5 ++-- src/copilot/utilities/config_manager.py | 30 +++++++++++----------- src/copilot/utilities/markdown_renderer.py | 15 ++++++----- src/eulercopilot.sh | 4 +-- 8 files changed, 64 insertions(+), 45 deletions(-) diff --git a/ruff.toml b/ruff.toml index 0c21937..9e4f59f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,9 @@ # Allow imports relative to the "src" and "test" directories. src = ["src"] +# Exclude the ".vscode" directory. +exclude = [".vscode"] + # Allow lines to be as long as 120. line-length = 120 diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 452dce5..92330ad 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -27,6 +27,8 @@ def check_shell_features(cmd: str) -> bool: r'!', # 后台运行符号 r'&', + # 分号 + r';', # 括号命令分组 r'\(|\)|\{|\}', # 逻辑操作符 @@ -67,14 +69,17 @@ def execute_shell_command(cmd: str) -> int: def handle_user_input(service: llm_service.LLMService, user_input: str, mode: str) -> int: '''Process user input based on the given flag and backend configuration.''' - if mode == 'shell': + if mode == 'chat': cmd = service.get_shell_answer(user_input) + if not cmd: + return -1 exit_code: int = 0 - if cmd and interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): + print(cmd) # TODO: Pretty print the command + if interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): exit_code = execute_shell_command(cmd) - return exit_code - if mode == 'chat': - service.get_general_answer(user_input) + if exit_code != 0: + print(f'命令 "{cmd}" 执行失败,退出码:{exit_code}') + return exit_code return -1 if isinstance(service, framework_api.Framework): if mode == 'diagnose': diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index efc5930..80981a4 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -38,11 +38,6 @@ def cli( question: Optional[str] = typer.Argument( None, show_default=False, help='通过自然语言提问'), - shell: bool = typer.Option( - False, '--shell', '-s', - help='切换到 Shell 命令模式', - rich_help_panel='选择问答模式' - ), chat: bool = typer.Option( False, '--chat', '-c', help='切换到智能问答模式', @@ -82,6 +77,10 @@ def cli( if init: setup_copilot() return 0 + if not CONFIG_INITIALIZED: + print('\033[1;31m请先初始化 copilot 设置\033[0m') + print('\033[33m请使用 "copilot --init" 命令初始化\033[0m') + return 1 if backend: if ADVANCED_MODE: select_backend() @@ -91,15 +90,11 @@ def cli( edit_config() return 0 - if sum(map(bool, [shell, chat, diagnose, tuning])) > 1: + if sum(map(bool, [chat, diagnose, tuning])) > 1: print('\033[1;31m当前版本只能选择一种问答模式\033[0m') return 1 - if shell: - select_query_mode(0) - if not question: - return 0 - elif chat: + if chat: select_query_mode(1) if not question: return 0 diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index c2479aa..1b5b8d7 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -20,7 +20,9 @@ class Framework(LLMService): self.api_key: str = api_key self.session_id: str = session_id self.debug_mode: bool = debug_mode + # 缓存 self.content: str = '' + self.sugggestion: str = '' # 富文本显示 self.console = Console() @@ -30,8 +32,9 @@ class Framework(LLMService): self._stream_response(headers, data) return self.content - def get_shell_answer(self, question: str) -> str: - query = self._gen_shell_prompt(question) + def get_shell_answer(self, question: str): + # query = self._gen_shell_prompt(question) + query = question return self._extract_shell_code_blocks(self.get_general_answer(query)) def diagnose(self, question: str) -> str: @@ -67,6 +70,7 @@ class Framework(LLMService): # pylint: disable=R0912 def _stream_response(self, headers, data): self.content = '' + self.sugggestion = '' spinner = Spinner('material') with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) @@ -107,7 +111,17 @@ class Framework(LLMService): else: chunk = jcontent.get('content', '') self.content += chunk - MarkdownRenderer.update(live, self.content) + suggestions = jcontent.get('search_suggestions', []) + if suggestions: + self.sugggestion = suggestions[0].strip() + if not self.sugggestion: + MarkdownRenderer.update(live, self.content) + else: + MarkdownRenderer.update( + live, + content=self.content, + sugggestion=f'**你可以继续问** {self.sugggestion}' + ) def _get_headers(self) -> dict: return { diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index 54507cb..ce69aec 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -2,6 +2,7 @@ import re from abc import ABC, abstractmethod +from typing import Any from copilot.utilities.env_info import get_os_info, is_root @@ -12,7 +13,7 @@ class LLMService(ABC): pass @abstractmethod - def get_shell_answer(self, question: str) -> str: + def get_shell_answer(self, question: str) -> Any: pass def _extract_shell_code_blocks(self, markdown_text): @@ -21,7 +22,7 @@ class LLMService(ABC): cmds = [match.group('code') for match in matches] if cmds: return cmds[0] - return markdown_text.replace('`', '') + # return markdown_text.replace('`', '') def _get_context_length(self, context: list) -> int: length = 0 diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 56bacbe..06e9faa 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -7,20 +7,20 @@ CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') DEFAULT_CONFIG = { - "backend": "spark", - "query_mode": "shell", - "advanced_mode": False, - "debug_mode": False, - "spark_app_id": "", - "spark_api_key": "", - "spark_api_secret": "", - "spark_url": "wss://spark-api.xf-yun.com/v3.5/chat", - "spark_domain": "generalv3.5", - "framework_url": "", - "framework_api_key": "", - "model_url": "http://localhost:1337/v1/chat/completions", - "model_api_key": "", - "model_name": "" + 'backend': 'framework', + 'query_mode': 'chat', + 'advanced_mode': False, + 'debug_mode': False, + 'spark_app_id': '', + 'spark_api_key': '', + 'spark_api_secret': '', + 'spark_url': 'wss://spark-api.xf-yun.com/v3.5/chat', + 'spark_domain': 'generalv3.5', + 'framework_url': '', + 'framework_api_key': '', + 'model_url': '', + 'model_api_key': '', + 'model_name': '' } @@ -86,7 +86,7 @@ def select_backend(): def edit_config(): config = load_config() print('\n\033[1;33m当前设置:\033[0m') - format_string = "{:<32} {}".format + format_string = '{:<32} {}'.format for key, value in config.items(): print(f'- {format_string(key, value)}') diff --git a/src/copilot/utilities/markdown_renderer.py b/src/copilot/utilities/markdown_renderer.py index 9b31a90..261c3b6 100644 --- a/src/copilot/utilities/markdown_renderer.py +++ b/src/copilot/utilities/markdown_renderer.py @@ -1,5 +1,6 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +from rich.console import Group from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel @@ -8,11 +9,13 @@ from rich.panel import Panel class MarkdownRenderer: @staticmethod - def update(live: Live, markup: str): + def update(live: Live, content: str, sugggestion: str = '', refresh: bool = True): + content_panel = Panel(Markdown(content, code_theme='github-dark'), border_style='gray50') + if not sugggestion: + live.update(content_panel, refresh=refresh) + return + sugggestion_panel = Panel(Markdown(sugggestion, code_theme='github-dark'), border_style='gray50') live.update( - Panel( - Markdown(markup, code_theme='github-dark'), - border_style='gray50' - ), - refresh=True + Group(content_panel, sugggestion_panel), + refresh=refresh ) diff --git a/src/eulercopilot.sh b/src/eulercopilot.sh index 164e8c1..48f6a78 100644 --- a/src/eulercopilot.sh +++ b/src/eulercopilot.sh @@ -8,9 +8,7 @@ read_query_mode() { local query_mode query_mode=$(jq '.query_mode' ~/.config/eulercopilot/config.json) - if [ "$query_mode" = "\"shell\"" ]; then - echo "命令生成" - elif [ "$query_mode" = "\"chat\"" ]; then + if [ "$query_mode" = "\"chat\"" ]; then echo "智能问答" elif [ "$query_mode" = "\"diagnose\"" ]; then echo "智能诊断" -- Gitee From 9f6c0db5b5c369c66fc97219d3adad29011278ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Sat, 31 Aug 2024 10:07:32 +0800 Subject: [PATCH 26/32] =?UTF-8?q?=E6=95=B4=E5=90=88=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=92=8C=E6=99=BA=E8=83=BD=E9=97=AE=E7=AD=94?= =?UTF-8?q?=20(Part=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 19 ++-- src/copilot/app/copilot_app.py | 16 ++-- src/copilot/app/copilot_cli.py | 8 +- src/copilot/backends/framework_api.py | 110 ++++++++++++++++-------- src/copilot/backends/llm_service.py | 103 ++++++++++++---------- src/copilot/backends/openai_api.py | 8 +- src/copilot/backends/spark_api.py | 8 +- src/copilot/utilities/config_manager.py | 2 +- 8 files changed, 165 insertions(+), 109 deletions(-) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index c135c2c..af7ca29 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -43,16 +43,19 @@ install -c -m 0755 %{_builddir}/%{name}-%{version}/eulercopilot.sh %{buildroot}/ /etc/profile.d/eulercopilot.sh %pre +sed -i '/# >>> eulercopilot >>>/,/# <<< eulercopilot <<> /etc/bashrc # >>> eulercopilot >>> -run_after_return() { - if [[ "$PS1" == *"\[\033[1;33m"* ]]; then - revert_copilot_prompt - set_copilot_prompt - fi -} -PROMPT_COMMAND="${PROMPT_COMMAND:+${PROMPT_COMMAND}; }run_after_return" -set_copilot_prompt +if type revert_copilot_prompt &> /dev/null && type set_copilot_prompt &> /dev/null; then + run_after_return() { + if [[ "$PS1" == *"\[\033[1;33m"* ]]; then + revert_copilot_prompt + set_copilot_prompt + fi + } + PROMPT_COMMAND="${PROMPT_COMMAND:+${PROMPT_COMMAND}; }run_after_return" + set_copilot_prompt +fi # <<< eulercopilot <<< EOF diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 92330ad..b871796 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -70,16 +70,18 @@ def handle_user_input(service: llm_service.LLMService, user_input: str, mode: str) -> int: '''Process user input based on the given flag and backend configuration.''' if mode == 'chat': - cmd = service.get_shell_answer(user_input) - if not cmd: + cmds = service.get_shell_commands(user_input) + if not cmds: return -1 exit_code: int = 0 - print(cmd) # TODO: Pretty print the command + print(cmds) # TODO: Pretty print the command if interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): - exit_code = execute_shell_command(cmd) - if exit_code != 0: - print(f'命令 "{cmd}" 执行失败,退出码:{exit_code}') - return exit_code + for cmd in cmds: + print(f'执行命令:{cmd}') + exit_code = execute_shell_command(cmd) + if exit_code != 0: + print(f'命令 "{cmds}" 执行失败,退出码:{exit_code}') + return exit_code return -1 if isinstance(service, framework_api.Framework): if mode == 'diagnose': diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index 80981a4..36ef3ae 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -22,6 +22,7 @@ from copilot.utilities.config_manager import ( CONFIG: dict = load_config() BACKEND: str = CONFIG.get('backend', DEFAULT_CONFIG['backend']) ADVANCED_MODE: bool = CONFIG.get('advanced_mode', DEFAULT_CONFIG['advanced_mode']) +DEBUG_MODE: bool = CONFIG.get('debug_mode', DEFAULT_CONFIG['debug_mode']) CONFIG_INITIALIZED: bool = os.path.exists(CONFIG_PATH) app = typer.Typer( @@ -29,6 +30,7 @@ app = typer.Typer( 'help_option_names': ['-h', '--help'], 'allow_interspersed_args': True }, + pretty_exceptions_show_locals=DEBUG_MODE, add_completion=False ) @@ -95,12 +97,12 @@ def cli( return 1 if chat: - select_query_mode(1) + select_query_mode(0) if not question: return 0 elif diagnose: if BACKEND == 'framework': - select_query_mode(2) + select_query_mode(1) if not question: return 0 else: @@ -109,7 +111,7 @@ def cli( return 1 elif tuning: if BACKEND == 'framework': - select_query_mode(3) + select_query_mode(2) if not question: return 0 else: diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index 1b5b8d7..4a538e8 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -26,16 +26,15 @@ class Framework(LLMService): # 富文本显示 self.console = Console() - def get_general_answer(self, question: str) -> str: + def get_model_output(self, question: str) -> str: headers = self._get_headers() data = {'question': question, 'session_id': self.session_id} self._stream_response(headers, data) return self.content - def get_shell_answer(self, question: str): - # query = self._gen_shell_prompt(question) - query = question - return self._extract_shell_code_blocks(self.get_general_answer(query)) + def get_shell_commands(self, question: str) -> list: + query = self._gen_chat_prompt(question) + self._gen_framework_extra_prompt() + return self._extract_shell_code_blocks(self.get_model_output(query)) def diagnose(self, question: str) -> str: # 确保用户输入的问题中包含有效的IP地址,若没有,则诊断本机 @@ -67,7 +66,6 @@ class Framework(LLMService): self._stream_response(headers, data) return self.content - # pylint: disable=R0912 def _stream_response(self, headers, data): self.content = '' self.sugggestion = '' @@ -88,40 +86,48 @@ class Framework(LLMService): if response.status_code != 200: live.update(f'请求失败: {response.status_code}', refresh=True) return - for line in response.iter_lines(): - if line is None: + self._handle_response_content(response, live) + + # pylint: disable=R0912 + def _handle_response_content( + self, + response: requests.Response, + live: Live + ): + for line in response.iter_lines(): + if line is None: + continue + content = line.decode('utf-8').strip('data: ') + try: + jcontent = json.loads(content) + except json.JSONDecodeError: + if content == '': continue - content = line.decode('utf-8').strip('data: ') - try: - jcontent = json.loads(content) - except json.JSONDecodeError: - if content == '': + if content == '[ERROR]': + if not self.content: + MarkdownRenderer.update(live, 'EulerCopilot 智能体遇到错误,请联系管理员定位问题') + elif content == '[SENSITIVE]': + MarkdownRenderer.update(live, '检测到违规信息,请重新提问') + self.content = '' + elif content != '[DONE]': + if not self.debug_mode: continue - if content == '[ERROR]': - if not self.content: - MarkdownRenderer.update(live, 'EulerCopilot 智能体遇到错误,请联系管理员定位问题') - elif content == '[SENSITIVE]': - MarkdownRenderer.update(live, '检测到违规信息,请重新提问') - self.content = '' - elif content != '[DONE]': - if not self.debug_mode: - continue - MarkdownRenderer.update(live, f'EulerCopilot 智能体返回了未知内容:\n```json\n{content}\n```') - break + MarkdownRenderer.update(live, f'EulerCopilot 智能体返回了未知内容:\n```json\n{content}\n```') + break + else: + chunk = jcontent.get('content', '') + self.content += chunk + suggestions = jcontent.get('search_suggestions', []) + if suggestions: + self.sugggestion = suggestions[0].strip() + if not self.sugggestion: + MarkdownRenderer.update(live, self.content) else: - chunk = jcontent.get('content', '') - self.content += chunk - suggestions = jcontent.get('search_suggestions', []) - if suggestions: - self.sugggestion = suggestions[0].strip() - if not self.sugggestion: - MarkdownRenderer.update(live, self.content) - else: - MarkdownRenderer.update( - live, - content=self.content, - sugggestion=f'**你可以继续问** {self.sugggestion}' - ) + MarkdownRenderer.update( + live, + content=self.content, + sugggestion=f'**你可以继续问** {self.sugggestion}' + ) def _get_headers(self) -> dict: return { @@ -151,3 +157,33 @@ class Framework(LLMService): ip_address = process.stdout.decode('utf-8').strip().split(' ', maxsplit=1)[0] return ip_address return '' + + def _gen_framework_extra_prompt(self) -> str: + return f'''\n +你的任务是: +根据用户输入的问题,提供相应的操作系统的管理和运维解决方案。 +你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 + +格式要求: ++ 你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; ++ 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 + +其他要求: ++ 如果用户要求安装软件包,请注意 openEuler 使用 dnf 管理软件包,你不能在回答中使用 apt 或其他软件包管理器 ++ 请特别注意当前用户的权限:{self._gen_sudo_prompt()} + +在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! +如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令\n +危险操作举例:\n ++ 例1: 强制删除 + ```bash + rm -rf /path/to/sth + ``` ++ 例2: 卸载软件包时默认同意 + ```bash + dnf remove -y package_name + ``` +你不能输出类似于上述例子的命令! + +由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 +''' diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index ce69aec..b403259 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -2,27 +2,24 @@ import re from abc import ABC, abstractmethod -from typing import Any from copilot.utilities.env_info import get_os_info, is_root class LLMService(ABC): @abstractmethod - def get_general_answer(self, question: str) -> str: + def get_model_output(self, question: str) -> str: pass @abstractmethod - def get_shell_answer(self, question: str) -> Any: + def get_shell_commands(self, question: str) -> list: pass def _extract_shell_code_blocks(self, markdown_text): shell_code_pattern = re.compile(r'```(?:bash|sh|shell)\n(?P(?:\n|.)*?)\n```', re.DOTALL) matches = shell_code_pattern.finditer(markdown_text) cmds = [match.group('code') for match in matches] - if cmds: - return cmds[0] - # return markdown_text.replace('`', '') + return cmds def _get_context_length(self, context: list) -> int: length = 0 @@ -39,42 +36,58 @@ class LLMService(ABC): def _gen_system_prompt(self) -> str: return f'''你是操作系统 {get_os_info()} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 - 你的任务是: - 根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 - 你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 - - 格式要求: - 你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; - 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 - - 用户可能问你一些操作系统相关的问题,你尤其需要注意安装软件包的情景: - openEuler 使用 dnf 或 yum 管理软件包,你不能在回答中使用 apt 或其他命令; - Debian 和 Ubuntu 使用 apt 管理软件包,你也不能在回答中使用 dnf 或 yum 命令; - 你可能还会遇到使用其他类 unix 系统的情景,比如 macOS 要使用 Homebrew 安装软件包。 - - 请特别注意当前用户的权限: - {self._gen_sudo_prompt()} - - 在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! - 如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 - 危险操作举例: - `rm -rf /path/to/sth` - `dnf remove -y package_name` - 你不能输出类似于上述例子的命令! - - 由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 - ''' - - def _gen_shell_prompt(self, question: str) -> str: - return f'''根据用户输入的问题,生成单行 shell 命令,并使用 Markdown 格式输出。 - - 用户的问题: - {question} - - 要求: - 1. 请用单行 shell 命令输出你的回答,不能使用多行 shell 命令 - 2. 请用 Markdown 代码块输出 shell 命令 - 3. 如果用户要求你生成的命令涉及到数据输入,你需要正确处理数据输入的方式,包括用户交互 - 4. 请解释你的回答,你要将你的解释附在命令代码块下方,你要有条理地解释命令中的每个步骤 - 5. 当前操作系统是 {get_os_info()},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 - ''' +你的任务是: +根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 +你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 + +格式要求: +你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; +你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 + +用户可能问你一些操作系统相关的问题,你尤其需要注意安装软件包的情景: +openEuler 使用 dnf 或 yum 管理软件包,你不能在回答中使用 apt 或其他命令; +Debian 和 Ubuntu 使用 apt 管理软件包,你也不能在回答中使用 dnf 或 yum 命令; +你可能还会遇到使用其他类 unix 系统的情景,比如 macOS 要使用 Homebrew 安装软件包。 + +请特别注意当前用户的权限: +{self._gen_sudo_prompt()} + +在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! +如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令\n +危险操作举例:\n +例1: 强制删除 +```bash +rm -rf /path/to/sth +``` +例2: 卸载软件包时默认同意 +```bash +dnf remove -y package_name +``` +你不能输出类似于上述例子的命令! + +由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 +''' + + def _gen_chat_prompt(self, question: str) -> str: + return f'''根据用户输入的问题,使用 Markdown 格式输出。 + +用户的问题: +{question} + +基本要求: +1. 如果涉及到生成 shell 命令,请用单行 shell 命令回答,不能使用多行 shell 命令 +2. 如果涉及 shell 命令或代码,请用 Markdown 代码块输出,必须标明代码的语言 +3. 如果用户要求你生成的命令涉及到数据输入,你需要正确处理数据输入的方式,包括用户交互 +4. 当前操作系统是 {get_os_info()},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 +''' + + def _gen_explain_cmd_prompt(self, cmd: str) -> str: + return f'''Shell 命令: +```bash +{cmd} +``` +请解释上面的 Shell 命令 + +要求: +你要有条理地解释命令中的每个步骤 +''' diff --git a/src/copilot/backends/openai_api.py b/src/copilot/backends/openai_api.py index dd55645..82f67c9 100644 --- a/src/copilot/backends/openai_api.py +++ b/src/copilot/backends/openai_api.py @@ -23,13 +23,13 @@ class ChatOpenAI(LLMService): # 富文本显示 self.console = Console() - def get_general_answer(self, question: str) -> str: + def get_model_output(self, question: str) -> str: self._stream_response(question) return self.answer - def get_shell_answer(self, question: str) -> str: - query = self._gen_shell_prompt(question) - return self._extract_shell_code_blocks(self.get_general_answer(query)) + def get_shell_commands(self, question: str) -> list: + query = self._gen_chat_prompt(question) + return self._extract_shell_code_blocks(self.get_model_output(query)) def _check_len(self, context: list) -> list: while self._get_context_length(context) > self.max_tokens / 2: diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index 364ed99..c030861 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -37,15 +37,15 @@ class Spark(LLMService): # 富文本显示 self.console = Console() - def get_general_answer(self, question: str) -> str: + def get_model_output(self, question: str) -> str: asyncio.get_event_loop().run_until_complete( self._query_spark_ai(question) ) return self.answer - def get_shell_answer(self, question: str) -> str: - query = self._gen_shell_prompt(question) - return self._extract_shell_code_blocks(self.get_general_answer(query)) + def get_shell_commands(self, question: str) -> list: + query = self._gen_chat_prompt(question) + return self._extract_shell_code_blocks(self.get_model_output(query)) async def _query_spark_ai(self, query: str): url = self._create_url() diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 06e9faa..a67e707 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -55,7 +55,7 @@ def update_config(key: str, value): def select_query_mode(mode: int): - modes = ['shell', 'chat', 'diagnose', 'tuning'] + modes = ['chat', 'diagnose', 'tuning'] if mode < len(modes): update_config('query_mode', modes[mode]) -- Gitee From b504a831ee91d76718fa95cd2ebcd27ec1981820 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Sat, 31 Aug 2024 02:56:13 +0000 Subject: [PATCH 27/32] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E3=80=8A=E6=9C=A8?= =?UTF-8?q?=E5=85=B0=E5=AE=BD=E6=9D=BE=E8=AE=B8=E5=8F=AF=E8=AF=81=EF=BC=8C?= =?UTF-8?q?=E7=AC=AC2=E7=89=88=E3=80=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- LICENSE | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6c2697 --- /dev/null +++ b/LICENSE @@ -0,0 +1,194 @@ +木兰宽松许可证,第2版 + +木兰宽松许可证,第2版 + +2020年1月 http://license.coscl.org.cn/MulanPSL2 + +您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + +0. 定义 + +“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + +“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + +“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + +“法人实体” 是指提交贡献的机构及其“关联实体”。 + +“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是 +指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + +1. 授予版权许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可 +以复制、使用、修改、分发其“贡献”,不论修改与否。 + +2. 授予专利许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定 +撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡 +献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软 +件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“ +关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或 +其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权 +行动之日终止。 + +3. 无商标许可 + +“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定 +的声明义务而必须使用除外。 + +4. 分发限制 + +您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“ +本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + +5. 免责声明与责任限制 + +“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对 +任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于 +何种法律理论,即使其曾被建议有此种损失的可能性。 + +6. 语言 + +“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文 +版为准。 + +条款结束 + +如何将木兰宽松许可证,第2版,应用到您的软件 + +如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + +1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + +2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + +3, 请将如下声明文本放入每个源文件的头部注释中。 + +Copyright (c) [Year] [name of copyright holder] +[Software Name] is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan +PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. + +Mulan Permissive Software License,Version 2 + +Mulan Permissive Software License,Version 2 (Mulan PSL v2) + +January 2020 http://license.coscl.org.cn/MulanPSL2 + +Your reproduction, use, modification and distribution of the Software shall +be subject to Mulan PSL v2 (this License) with the following terms and +conditions: + +0. Definition + +Software means the program and related documents which are licensed under +this License and comprise all Contribution(s). + +Contribution means the copyrightable work licensed by a particular +Contributor under this License. + +Contributor means the Individual or Legal Entity who licenses its +copyrightable work under this License. + +Legal Entity means the entity making a Contribution and all its +Affiliates. + +Affiliates means entities that control, are controlled by, or are under +common control with the acting entity under this License, ‘control’ means +direct or indirect ownership of at least fifty percent (50%) of the voting +power, capital or other securities of controlled or commonly controlled +entity. + +1. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable copyright license to reproduce, use, modify, or distribute its +Contribution, with modification or not. + +2. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable (except for revocation under this Section) patent license to +make, have made, use, offer for sale, sell, import or otherwise transfer its +Contribution, where such patent license is only limited to the patent claims +owned or controlled by such Contributor now or in future which will be +necessarily infringed by its Contribution alone, or by combination of the +Contribution with the Software to which the Contribution was contributed. +The patent license shall not apply to any modification of the Contribution, +and any other combination which includes the Contribution. If you or your +Affiliates directly or indirectly institute patent litigation (including a +cross claim or counterclaim in a litigation) or other patent enforcement +activities against any individual or entity by alleging that the Software or +any Contribution in it infringes patents, then any patent license granted to +you under this License for the Software shall terminate as of the date such +litigation or activity is filed or taken. + +3. No Trademark License + +No trademark license is granted to use the trade names, trademarks, service +marks, or product names of Contributor, except as required to fulfill notice +requirements in section 4. + +4. Distribution Restriction + +You may distribute the Software in any medium with or without modification, +whether in source or executable forms, provided that you provide recipients +with a copy of this License and retain copyright, patent, trademark and +disclaimer statements in the Software. + +5. Disclaimer of Warranty and Limitation of Liability + +THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR +COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT +LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING +FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO +MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGES. + +6. Language + +THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION +AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF +DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION +SHALL PREVAIL. + +END OF THE TERMS AND CONDITIONS + +How to Apply the Mulan Permissive Software License,Version 2 +(Mulan PSL v2) to Your Software + +To apply the Mulan PSL v2 to your work, for easy identification by +recipients, you are suggested to complete following three steps: + +i. Fill in the blanks in following statement, including insert your software +name, the year of the first publication of your software, and your name +identified as the copyright owner; + +ii. Create a file named "LICENSE" which contains the whole context of this +License in the first directory of your software package; + +iii. Attach the statement to the appropriate annotated syntax at the +beginning of each source file. + +Copyright (c) [Year] [name of copyright holder] +[Software Name] is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan +PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. -- Gitee From 8ee84104b6e1575b7eea3961a490a8d011f6f794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Tue, 3 Sep 2024 15:03:01 +0800 Subject: [PATCH 28/32] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/eulercopilot.spec | 5 +- src/copilot/app/copilot_app.py | 99 +++++++++++++++++++++------ src/copilot/backends/framework_api.py | 87 ++++++++++++++--------- src/copilot/backends/llm_service.py | 22 +++--- src/copilot/backends/openai_api.py | 11 +-- src/copilot/backends/spark_api.py | 13 ++-- src/copilot/utilities/interact.py | 90 ++++++++++++++++++++++-- 7 files changed, 245 insertions(+), 82 deletions(-) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index af7ca29..a78dc97 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -11,7 +11,7 @@ BuildRequires: python3-devel python3-setuptools BuildRequires: python3-pip BuildRequires: python3-Cython gcc -Requires: python3 jq +Requires: python3 jq hostname %description EulerCopilot 命令行助手 @@ -21,7 +21,8 @@ EulerCopilot 命令行助手 python3 -m venv .venv .venv/bin/python3 -m pip install -U pip setuptools .venv/bin/python3 -m pip install -U Cython pyinstaller -.venv/bin/python3 -m pip install -U websockets requests rich typer +.venv/bin/python3 -m pip install -U websockets requests +.venv/bin/python3 -m pip install -U rich typer questionary %build .venv/bin/python3 setup.py build_ext diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index b871796..ea2a00f 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -9,6 +9,11 @@ import subprocess import uuid from typing import Union +from rich.console import Console +from rich.live import Live +from rich.markdown import Markdown +from rich.panel import Panel + from copilot.backends import framework_api, llm_service, openai_api, spark_api from copilot.utilities import interact @@ -55,7 +60,7 @@ def execute_shell_command(cmd: str) -> int: try: process = subprocess.Popen(shlex.split(cmd)) except FileNotFoundError as e: - builtin_cmds = ['source', 'history', 'cd', 'export', 'alias', 'test'] + builtin_cmds = ['.', 'source', 'history', 'cd', 'export', 'alias', 'test'] cmd_prefix = cmd.split()[0] if cmd_prefix in builtin_cmds: print(f'不支持执行 Shell 内置命令 "{cmd_prefix}",请复制后手动执行') @@ -66,32 +71,87 @@ def execute_shell_command(cmd: str) -> int: return exit_code +def print_shell_commands(cmds: list): + console = Console() + with Live(console=console, vertical_overflow='visible') as live: + live.update( + Panel( + Markdown( + '```bash\n' + '\n\n'.join(cmds) + '\n```', + code_theme='github-dark' + ), + border_style='gray50' + ) + ) + + +def command_interaction_loop(cmds: list, service: llm_service.LLMService) -> int: + if not cmds: + return -1 + print_shell_commands(cmds) + while True: + action = interact.select_action(len(cmds) > 1) + if action in ('execute_all', 'execute_selected', 'execute'): + exit_code: int = 0 + selected_cmds = get_selected_cmds(cmds, action) + for cmd in selected_cmds: + exit_code = execute_shell_command(cmd) + if exit_code != 0: + print(f'命令 "{cmd}" 执行中止,退出码:{exit_code}') + break + return -1 + if action == 'explain': + service.explain_shell_command(select_one_cmd(cmds)) + elif action == 'edit': + i = select_one_cmd_with_index(cmds) + readline.set_startup_hook(lambda: readline.insert_text(cmds[i])) + try: + cmds[i] = input() + finally: + readline.set_startup_hook() + print_shell_commands(cmds) + elif action == 'cancel': + return -1 + + +def get_selected_cmds(cmds: list, action: str) -> list: + if action in ('execute', 'execute_all'): + return cmds + if action == 'execute_selected': + return interact.select_multiple_commands(cmds) + return [] + + +def select_one_cmd(cmds: list) -> str: + if len(cmds) == 1: + return cmds[0] + return interact.select_command(cmds) + + +def select_one_cmd_with_index(cmds: list) -> int: + if len(cmds) == 1: + return 0 + return interact.select_command_with_index(cmds) + + def handle_user_input(service: llm_service.LLMService, user_input: str, mode: str) -> int: '''Process user input based on the given flag and backend configuration.''' if mode == 'chat': - cmds = service.get_shell_commands(user_input) - if not cmds: - return -1 - exit_code: int = 0 - print(cmds) # TODO: Pretty print the command - if interact.query_yes_or_no('\033[33m是否执行命令?\033[0m '): - for cmd in cmds: - print(f'执行命令:{cmd}') - exit_code = execute_shell_command(cmd) - if exit_code != 0: - print(f'命令 "{cmds}" 执行失败,退出码:{exit_code}') - return exit_code - return -1 + cmds = list( + dict.fromkeys( # Remove duplicate commands + service.get_shell_commands(user_input) + ) + ) + return command_interaction_loop(cmds, service) if isinstance(service, framework_api.Framework): + report: str = '' if mode == 'diagnose': report = service.diagnose(user_input) - if report: - return 0 if mode == 'tuning': report = service.tuning(user_input) - if report: - return 0 + if report: + return 0 return 1 @@ -125,8 +185,7 @@ def main(user_input: Union[str, None], config: dict) -> int: print('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') return 1 - if mode == 'chat': - print('\033[33m输入 \'exit\' 或按下 Ctrl+C 结束对话\033[0m') + print('\033[33m输入 "exit" 或按下 Ctrl+C 结束对话\033[0m') try: while True: diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index 4a538e8..f666e4e 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -14,6 +14,7 @@ from copilot.backends.llm_service import LLMService from copilot.utilities.markdown_renderer import MarkdownRenderer +# pylint: disable=R0902 class Framework(LLMService): def __init__(self, url, api_key, session_id, debug_mode=False): self.endpoint: str = url @@ -22,19 +23,21 @@ class Framework(LLMService): self.debug_mode: bool = debug_mode # 缓存 self.content: str = '' + self.commands: list = [] self.sugggestion: str = '' # 富文本显示 self.console = Console() - def get_model_output(self, question: str) -> str: - headers = self._get_headers() - data = {'question': question, 'session_id': self.session_id} - self._stream_response(headers, data) - return self.content - def get_shell_commands(self, question: str) -> list: query = self._gen_chat_prompt(question) + self._gen_framework_extra_prompt() - return self._extract_shell_code_blocks(self.get_model_output(query)) + self._query_llm_service(query) + if self.commands: + return self.commands + return self._extract_shell_code_blocks(self.content) + + def explain_shell_command(self, cmd: str): + query = self._gen_explain_cmd_prompt(cmd) + self._query_llm_service(query, show_suggestion=False) def diagnose(self, question: str) -> str: # 确保用户输入的问题中包含有效的IP地址,若没有,则诊断本机 @@ -66,9 +69,14 @@ class Framework(LLMService): self._stream_response(headers, data) return self.content - def _stream_response(self, headers, data): - self.content = '' - self.sugggestion = '' + # pylint: disable=W0221 + def _query_llm_service(self, question: str, show_suggestion: bool = True): + headers = self._get_headers() + data = {'question': question, 'session_id': self.session_id} + self._stream_response(headers, data, show_suggestion) + + def _stream_response(self, headers, data, show_suggestion: bool = True): + self._clear_previous_data() spinner = Spinner('material') with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) @@ -86,13 +94,18 @@ class Framework(LLMService): if response.status_code != 200: live.update(f'请求失败: {response.status_code}', refresh=True) return - self._handle_response_content(response, live) + self._handle_response_stream(live, response, show_suggestion) - # pylint: disable=R0912 - def _handle_response_content( + def _clear_previous_data(self): + self.content = '' + self.commands = [] + self.sugggestion = '' + + def _handle_response_stream( self, + live: Live, response: requests.Response, - live: Live + show_suggestion: bool ): for line in response.iter_lines(): if line is None: @@ -115,19 +128,23 @@ class Framework(LLMService): MarkdownRenderer.update(live, f'EulerCopilot 智能体返回了未知内容:\n```json\n{content}\n```') break else: - chunk = jcontent.get('content', '') - self.content += chunk - suggestions = jcontent.get('search_suggestions', []) - if suggestions: - self.sugggestion = suggestions[0].strip() - if not self.sugggestion: - MarkdownRenderer.update(live, self.content) - else: - MarkdownRenderer.update( - live, - content=self.content, - sugggestion=f'**你可以继续问** {self.sugggestion}' - ) + self._handle_json_chunk(jcontent, live, show_suggestion) + + def _handle_json_chunk(self, jcontent, live: Live, show_suggestion: bool): + chunk = jcontent.get('content', '') + self.content += chunk + if show_suggestion: + suggestions = jcontent.get('search_suggestions', []) + if suggestions: + self.sugggestion = suggestions[0].strip() + if not self.sugggestion: + MarkdownRenderer.update(live, self.content) + else: + MarkdownRenderer.update( + live, + content=self.content, + sugggestion=f'**你可以继续问** {self.sugggestion}' + ) def _get_headers(self) -> dict: return { @@ -146,7 +163,11 @@ class Framework(LLMService): def _get_local_ip(self) -> str: try: - process = subprocess.run(['hostname', '-I'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + process = subprocess.run( + ['hostname', '-I'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True) except (FileNotFoundError, subprocess.CalledProcessError): try: ip_list = socket.gethostbyname_ex(socket.gethostname())[2] @@ -159,13 +180,12 @@ class Framework(LLMService): return '' def _gen_framework_extra_prompt(self) -> str: - return f'''\n -你的任务是: + return f'''你的任务是: 根据用户输入的问题,提供相应的操作系统的管理和运维解决方案。 你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 格式要求: -+ 你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; ++ 你的回答中的代码块和表格都必须用 Markdown 呈现; + 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 其他要求: @@ -173,8 +193,9 @@ class Framework(LLMService): + 请特别注意当前用户的权限:{self._gen_sudo_prompt()} 在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! -如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令\n -危险操作举例:\n +如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 + +危险操作举例: + 例1: 强制删除 ```bash rm -rf /path/to/sth diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index b403259..90320ea 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -8,18 +8,21 @@ from copilot.utilities.env_info import get_os_info, is_root class LLMService(ABC): @abstractmethod - def get_model_output(self, question: str) -> str: + def get_shell_commands(self, question: str) -> list: pass + def explain_shell_command(self, cmd: str): + query = self._gen_explain_cmd_prompt(cmd) + self._query_llm_service(query) + @abstractmethod - def get_shell_commands(self, question: str) -> list: + def _query_llm_service(self, question: str, *args, **kwargs): pass - def _extract_shell_code_blocks(self, markdown_text): - shell_code_pattern = re.compile(r'```(?:bash|sh|shell)\n(?P(?:\n|.)*?)\n```', re.DOTALL) - matches = shell_code_pattern.finditer(markdown_text) - cmds = [match.group('code') for match in matches] - return cmds + def _extract_shell_code_blocks(self, markdown_text) -> list: + pattern = r'```(bash|sh|shell)\n(.*?)(?=\n\s*```)' + bash_blocks = re.findall(pattern, markdown_text, re.DOTALL | re.MULTILINE) + return '\n'.join([block[1].strip() for block in bash_blocks]).splitlines() def _get_context_length(self, context: list) -> int: length = 0 @@ -82,12 +85,11 @@ dnf remove -y package_name ''' def _gen_explain_cmd_prompt(self, cmd: str) -> str: - return f'''Shell 命令: -```bash + return f'''```bash {cmd} ``` 请解释上面的 Shell 命令 要求: -你要有条理地解释命令中的每个步骤 +先在代码块中打印一次上述命令,再有条理地解释命令中的主要步骤 ''' diff --git a/src/copilot/backends/openai_api.py b/src/copilot/backends/openai_api.py index 82f67c9..287dec5 100644 --- a/src/copilot/backends/openai_api.py +++ b/src/copilot/backends/openai_api.py @@ -23,13 +23,14 @@ class ChatOpenAI(LLMService): # 富文本显示 self.console = Console() - def get_model_output(self, question: str) -> str: - self._stream_response(question) - return self.answer - def get_shell_commands(self, question: str) -> list: query = self._gen_chat_prompt(question) - return self._extract_shell_code_blocks(self.get_model_output(query)) + self._query_llm_service(query) + return self._extract_shell_code_blocks(self.answer) + + # pylint: disable=W0221 + def _query_llm_service(self, question: str): + self._stream_response(question) def _check_len(self, context: list) -> list: while self._get_context_length(context) > self.max_tokens / 2: diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index c030861..a3804de 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -37,15 +37,16 @@ class Spark(LLMService): # 富文本显示 self.console = Console() - def get_model_output(self, question: str) -> str: + def get_shell_commands(self, question: str) -> list: + query = self._gen_chat_prompt(question) + self._query_llm_service(query) + return self._extract_shell_code_blocks(self.answer) + + # pylint: disable=W0221 + def _query_llm_service(self, question: str): asyncio.get_event_loop().run_until_complete( self._query_spark_ai(question) ) - return self.answer - - def get_shell_commands(self, question: str) -> list: - query = self._gen_chat_prompt(question) - return self._extract_shell_code_blocks(self.get_model_output(query)) async def _query_spark_ai(self, query: str): url = self._create_url() diff --git a/src/copilot/utilities/interact.py b/src/copilot/utilities/interact.py index 6d34afe..8562a29 100644 --- a/src/copilot/utilities/interact.py +++ b/src/copilot/utilities/interact.py @@ -1,14 +1,92 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +import questionary + +ACTIONS_SINGLE_CMD = [ + questionary.Choice('解释命令', value='explain', shortcut_key='a'), + questionary.Choice('编辑命令', value='edit', shortcut_key='z'), + questionary.Choice('执行命令', value='execute', shortcut_key='x'), + questionary.Choice('取消', value='cancel', shortcut_key='c'), +] + +ACTIONS_MULTI_CMDS = [ + questionary.Choice('解释指定命令', value='explain', shortcut_key='a'), + questionary.Choice('编辑指定命令', value='edit', shortcut_key='z'), + questionary.Choice('执行所有命令', value='execute_all', shortcut_key='x'), + questionary.Choice('执行指定命令', value='execute_selected', shortcut_key='s'), + questionary.Choice('取消', value='cancel', shortcut_key='c'), +] + +QUESTIONS = [ + '选择要执行的操作:', + '选择命令:' +] + +CUSTOM_STYLE_FANCY = questionary.Style( + [ + ('separator', 'fg:#cc5454'), + ('qmark', 'fg:#673ab7 bold'), + ('question', 'bold'), + ('selected', 'fg:#cc5454'), + ('pointer', 'fg:#673ab7 bold'), + ('highlighted', 'fg:#673ab7 bold'), + ('answer', 'fg:#f44336 bold'), + ('text', 'fg:#FBE9E7'), + ('disabled', 'fg:#858585 italic'), + ] +) + + def query_yes_or_no(question: str) -> bool: - valid = {"yes": True, "y": True, "no": False, "n": False} - prompt = " [Y/n] " + valid = {'yes': True, 'y': True, 'no': False, 'n': False} + prompt = ' [Y/n] ' while True: choice = input(question + prompt).lower() - if choice == "": - return valid["y"] + if choice == '': + return valid['y'] elif choice in valid: return valid[choice] - else: - print('请用 "yes (y)" 或 "no (n)" 回答') + print('请用 "yes (y)" 或 "no (n)" 回答') + + +def select_action(has_multi_cmds: bool) -> str: + return questionary.select( + QUESTIONS[0], + choices=ACTIONS_MULTI_CMDS if has_multi_cmds else ACTIONS_SINGLE_CMD, + pointer=None, + use_shortcuts=True, + use_indicator=True, + style=CUSTOM_STYLE_FANCY + ).ask() + + +def select_command(commands: list) -> str: + return questionary.select( + QUESTIONS[1], + choices=commands, + pointer=None, + use_indicator=True, + style=CUSTOM_STYLE_FANCY + ).ask() + + +def select_command_with_index(commands: list) -> int: + command = questionary.select( + QUESTIONS[1], + choices=commands, + pointer=None, + use_indicator=True, + style=CUSTOM_STYLE_FANCY + ).ask() + return commands.index(command) + + +def select_multiple_commands(commands: list) -> list: + return questionary.checkbox( + QUESTIONS[1], + choices=commands, + pointer=None, + use_indicator=True, + style=CUSTOM_STYLE_FANCY + ).ask() -- Gitee From 49fa6fb4244e5a9963d80259b5f86dd193342045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 5 Sep 2024 16:48:46 +0800 Subject: [PATCH 29/32] =?UTF-8?q?framework:=20=E9=80=82=E9=85=8D=E6=96=B0?= =?UTF-8?q?=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/build_rpm.sh | 3 +- distribution/eulercopilot.spec | 10 +- src/copilot/app/copilot_app.py | 48 +++-- src/copilot/app/copilot_cli.py | 75 +++++-- src/copilot/backends/framework_api.py | 272 ++++++++++++++++++------ src/copilot/backends/llm_service.py | 66 ++---- src/copilot/backends/openai_api.py | 22 +- src/copilot/backends/spark_api.py | 23 +- src/copilot/utilities/config_manager.py | 28 +-- src/copilot/utilities/env_info.py | 13 +- src/copilot/utilities/i18n.py | 155 ++++++++++++++ src/copilot/utilities/interact.py | 151 +++++++++---- src/copilot/utilities/shell_script.py | 15 ++ src/eulercopilot.sh | 2 + src/setup.py | 3 +- 15 files changed, 642 insertions(+), 244 deletions(-) create mode 100644 src/copilot/utilities/i18n.py create mode 100644 src/copilot/utilities/shell_script.py diff --git a/distribution/build_rpm.sh b/distribution/build_rpm.sh index 587c8f3..ef217a2 100644 --- a/distribution/build_rpm.sh +++ b/distribution/build_rpm.sh @@ -24,4 +24,5 @@ fi rm -f ~/rpmbuild/RPMS/"$(uname -m)"/eulercopilot-* # Build the RPM package using rpmbuild -rpmbuild --define "_timestamp $(date +%s)" -bb "$spec_file" --nodebuginfo +rpmbuild --define "_tag .a$(date +%s)" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo +# rpmbuild --define "_tag .beta1" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot.spec index a78dc97..41f65c3 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot.spec @@ -1,8 +1,8 @@ -Name: eulercopilot -Version: 1.1 -Release: 1%{?dist}.%{?_timestamp} +Name: eulercopilot-cli +Version: 1.2 +Release: 1%{?_tag}%{?dist} Group: Applications/Utilities -Summary: EulerCopilot 命令行助手 +Summary: EulerCopilot Command Line Assistant Source: %{name}-%{version}.tar.gz License: MulanPSL-2.0 URL: https://www.openeuler.org/zh/ @@ -14,7 +14,7 @@ BuildRequires: python3-Cython gcc Requires: python3 jq hostname %description -EulerCopilot 命令行助手 +EulerCopilot Command Line Assistant %prep %setup -q diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index ea2a00f..82c35ed 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -6,8 +6,7 @@ import re import readline # noqa: F401 import shlex import subprocess -import uuid -from typing import Union +from typing import Optional from rich.console import Console from rich.live import Live @@ -15,7 +14,9 @@ from rich.markdown import Markdown from rich.panel import Panel from copilot.backends import framework_api, llm_service, openai_api, spark_api -from copilot.utilities import interact +from copilot.utilities import i18n, interact + +selected_plugins: list = [] def check_shell_features(cmd: str) -> bool: @@ -54,7 +55,7 @@ def execute_shell_command(cmd: str) -> int: try: process = subprocess.Popen(cmd, shell=True) except ValueError as e: - print(f'执行命令时出错:{e}') + print(i18n.main_exec_value_error.format(error=e)) return 1 else: try: @@ -63,9 +64,9 @@ def execute_shell_command(cmd: str) -> int: builtin_cmds = ['.', 'source', 'history', 'cd', 'export', 'alias', 'test'] cmd_prefix = cmd.split()[0] if cmd_prefix in builtin_cmds: - print(f'不支持执行 Shell 内置命令 "{cmd_prefix}",请复制后手动执行') + print(i18n.main_exec_builtin_cmd.format(cmd_prefix=cmd_prefix)) else: - print(f'命令不存在:{e}') + print(i18n.main_exec_not_found_error.format(error=e)) return 1 exit_code = process.wait() return exit_code @@ -97,7 +98,12 @@ def command_interaction_loop(cmds: list, service: llm_service.LLMService) -> int for cmd in selected_cmds: exit_code = execute_shell_command(cmd) if exit_code != 0: - print(f'命令 "{cmd}" 执行中止,退出码:{exit_code}') + print( + i18n.main_exec_cmd_failed_with_exit_code.format( + cmd=cmd, + exit_code=exit_code + ) + ) break return -1 if action == 'explain': @@ -138,14 +144,13 @@ def handle_user_input(service: llm_service.LLMService, user_input: str, mode: str) -> int: '''Process user input based on the given flag and backend configuration.''' if mode == 'chat': - cmds = list( - dict.fromkeys( # Remove duplicate commands - service.get_shell_commands(user_input) - ) - ) + cmds = list(dict.fromkeys(service.get_shell_commands(user_input))) return command_interaction_loop(cmds, service) if isinstance(service, framework_api.Framework): report: str = '' + if mode == 'flow': + cmds = list(dict.fromkeys(service.flow(user_input, selected_plugins))) + return command_interaction_loop(cmds, service) if mode == 'diagnose': report = service.diagnose(user_input) if mode == 'tuning': @@ -155,17 +160,26 @@ def handle_user_input(service: llm_service.LLMService, return 1 -def main(user_input: Union[str, None], config: dict) -> int: +# pylint: disable=W0603 +def main(user_input: Optional[str], config: dict) -> int: + global selected_plugins backend = config.get('backend') mode = str(config.get('query_mode')) - service: Union[llm_service.LLMService, None] = None + service: Optional[llm_service.LLMService] = None if backend == 'framework': service = framework_api.Framework( url=config.get('framework_url'), api_key=config.get('framework_api_key'), - session_id=str(uuid.uuid4().hex), debug_mode=config.get('debug_mode', False) ) + service.update_session_id() # get "ECSESSION" cookie + service.create_new_conversation() # get conversation_id from backend + if mode == 'flow': # get plugin list from current backend + plugins: list[framework_api.PluginData] = service.get_plugins() + if not plugins: + print(i18n.main_service_framework_plugin_is_none) + return 1 + selected_plugins = interact.select_plugins(plugins) elif backend == 'spark': service = spark_api.Spark( app_id=config.get('spark_app_id'), @@ -182,10 +196,10 @@ def main(user_input: Union[str, None], config: dict) -> int: ) if service is None: - print('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') + print(i18n.main_service_is_none) return 1 - print('\033[33m输入 "exit" 或按下 Ctrl+C 结束对话\033[0m') + print(i18n.main_exit_prompt) try: while True: diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index 36ef3ae..6f0f846 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -10,6 +10,7 @@ import typer from copilot.app.copilot_app import main from copilot.app.copilot_init import setup_copilot +from copilot.backends.framework_api import QUERY_MODS from copilot.utilities.config_manager import ( CONFIG_PATH, DEFAULT_CONFIG, @@ -18,6 +19,19 @@ from copilot.utilities.config_manager import ( select_backend, select_query_mode, ) +from copilot.utilities.i18n import ( + BRAND_NAME, + cli_help_panel_advanced_options, + cli_help_panel_switch_mode, + cli_help_prompt_edit_settings, + cli_help_prompt_init_settings, + cli_help_prompt_question, + cli_help_prompt_select_backend, + cli_help_prompt_switch_mode, + cli_notif_compatibility, + cli_notif_no_config, + cli_notif_select_one_mode, +) CONFIG: dict = load_config() BACKEND: str = CONFIG.get('backend', DEFAULT_CONFIG['backend']) @@ -39,49 +53,54 @@ app = typer.Typer( def cli( question: Optional[str] = typer.Argument( None, show_default=False, - help='通过自然语言提问'), + help=cli_help_prompt_question), chat: bool = typer.Option( False, '--chat', '-c', - help='切换到智能问答模式', - rich_help_panel='选择问答模式' + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["chat"]), + rich_help_panel=cli_help_panel_switch_mode + ), + flow: bool = typer.Option( + False, '--flow', '-f', + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["flow"]), + rich_help_panel=cli_help_panel_switch_mode, + hidden=(BACKEND != 'framework'), ), diagnose: bool = typer.Option( False, '--diagnose', '-d', - help='切换到智能诊断模式', - rich_help_panel='选择问答模式', + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["diagnose"]), + rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework') ), tuning: bool = typer.Option( False, '--tuning', '-t', - help='切换到智能调优模式', - rich_help_panel='选择问答模式', + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["tuning"]), + rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework') ), init: bool = typer.Option( False, '--init', - help='初始化 copilot 设置', + help=cli_help_prompt_init_settings, hidden=(CONFIG_INITIALIZED) ), backend: bool = typer.Option( False, '--backend', - help='选择大语言模型后端', - rich_help_panel='高级选项', + help=cli_help_prompt_select_backend, + rich_help_panel=cli_help_panel_advanced_options, hidden=(not ADVANCED_MODE) ), settings: bool = typer.Option( False, '--settings', - help='编辑 copilot 设置', - rich_help_panel='高级选项', + help=cli_help_prompt_edit_settings, + rich_help_panel=cli_help_panel_advanced_options, hidden=(not ADVANCED_MODE) ) ) -> int: - '''EulerCopilot 命令行助手''' + '''EulerCopilot CLI''' if init: setup_copilot() return 0 if not CONFIG_INITIALIZED: - print('\033[1;31m请先初始化 copilot 设置\033[0m') - print('\033[33m请使用 "copilot --init" 命令初始化\033[0m') + print(cli_notif_no_config) return 1 if backend: if ADVANCED_MODE: @@ -92,31 +111,37 @@ def cli( edit_config() return 0 - if sum(map(bool, [chat, diagnose, tuning])) > 1: - print('\033[1;31m当前版本只能选择一种问答模式\033[0m') + if sum(map(bool, [chat, flow, diagnose, tuning])) > 1: + print(cli_notif_select_one_mode) return 1 if chat: select_query_mode(0) if not question: return 0 - elif diagnose: + elif flow: if BACKEND == 'framework': select_query_mode(1) if not question: return 0 else: - print('\033[33m当前大模型后端不支持智能诊断功能\033[0m') - print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') + compatibility_notification(QUERY_MODS['flow']) return 1 - elif tuning: + elif diagnose: if BACKEND == 'framework': select_query_mode(2) if not question: return 0 else: - print('\033[33m当前大模型后端不支持智能调参功能\033[0m') - print('\033[33m推荐使用 EulerCopilot 智能体框架\033[0m') + compatibility_notification(QUERY_MODS['diagnose']) + return 1 + elif tuning: + if BACKEND == 'framework': + select_query_mode(3) + if not question: + return 0 + else: + compatibility_notification(QUERY_MODS['tuning']) return 1 if question: @@ -125,6 +150,10 @@ def cli( return main(question, load_config()) +def compatibility_notification(mode: str): + print(cli_notif_compatibility.format(mode=mode, brand_name=BRAND_NAME)) + + def entry_point() -> int: return app() diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index f666e4e..d2eb520 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -4,6 +4,9 @@ import json import re import socket import subprocess +from dataclasses import dataclass +from typing import Optional +from urllib.parse import urljoin import requests from rich.console import Console @@ -11,17 +14,48 @@ from rich.live import Live from rich.spinner import Spinner from copilot.backends.llm_service import LLMService +from copilot.utilities.i18n import ( + BRAND_NAME, + backend_framework_auth_invalid_api_key, + backend_framework_request_connection_error, + backend_framework_request_exceptions, + backend_framework_request_timeout, + backend_framework_request_too_many_requests, + backend_framework_request_unauthorized, + backend_framework_response_ended_prematurely, + backend_framework_stream_error, + backend_framework_stream_sensitive, + backend_framework_stream_unknown, + backend_framework_sugggestion, + backend_general_request_failed, + prompt_framework_plugin_ip, + prompt_framework_primary, + query_mode_chat, + query_mode_diagnose, + query_mode_flow, + query_mode_tuning, +) from copilot.utilities.markdown_renderer import MarkdownRenderer +from copilot.utilities.shell_script import write_shell_script + +QUERY_MODS = { + 'chat': query_mode_chat, + 'flow': query_mode_flow, + 'diagnose': query_mode_diagnose, + 'tuning': query_mode_tuning, +} # pylint: disable=R0902 class Framework(LLMService): - def __init__(self, url, api_key, session_id, debug_mode=False): + def __init__(self, url, api_key, debug_mode=False): self.endpoint: str = url self.api_key: str = api_key - self.session_id: str = session_id self.debug_mode: bool = debug_mode - # 缓存 + # 临时数据 + self.session_id: str = '' + self.plugins: list = [] + self.conversation_id: str = '' self.content: str = '' self.commands: list = [] self.sugggestion: str = '' @@ -39,19 +73,80 @@ class Framework(LLMService): query = self._gen_explain_cmd_prompt(cmd) self._query_llm_service(query, show_suggestion=False) + def update_session_id(self): + headers = self._get_headers() + try: + response = requests.get( + urljoin(self.endpoint, 'api/client/session'), + headers=headers, + timeout=30 + ) + except requests.exceptions.RequestException: + self.console.print(backend_framework_request_exceptions.format(brand_name=BRAND_NAME)) + return + if response.status_code == 401: + self.console.print(backend_framework_auth_invalid_api_key.format(brand_name=BRAND_NAME)) + return + if response.status_code != 200: + self.console.print(backend_general_request_failed.format(code=response.status_code)) + return + self.session_id = response.json().get('result', {}).get('session_id', '') + + def create_new_conversation(self): + headers = self._get_headers() + try: + response = requests.post( + urljoin(self.endpoint, 'api/client/conversation'), + headers=headers, + timeout=30 + ) + except requests.exceptions.RequestException: + self.console.print(backend_framework_request_exceptions.format(brand_name=BRAND_NAME)) + return + if response.status_code == 401: + self.console.print(backend_framework_auth_invalid_api_key.format(brand_name=BRAND_NAME)) + return + if response.status_code != 200: + self.console.print(backend_general_request_failed.format(code=response.status_code)) + return + self.conversation_id = response.json().get('result', {}).get('conversation_id', '') + + def get_plugins(self) -> list: + headers = self._get_headers() + try: + response = requests.get( + urljoin(self.endpoint, 'api/client/plugin'), + headers=headers, + timeout=30 + ) + except requests.exceptions.RequestException: + self.console.print(backend_framework_request_exceptions.format(brand_name=BRAND_NAME)) + return [] + if response.status_code == 401: + self.console.print(backend_framework_auth_invalid_api_key.format(brand_name=BRAND_NAME)) + return [] + if response.status_code != 200: + self.console.print(backend_general_request_failed.format(code=response.status_code)) + return [] + self.session_id = self._reset_session_from_cookie(response.headers.get('set-cookie', '')) + plugins = response.json().get('result', []) + if plugins: + self.plugins = [PluginData(**plugin) for plugin in plugins] + return self.plugins + + def flow(self, question: str, plugins: list) -> list: + self._query_llm_service(question, user_selected_plugins=plugins) + if self.commands: + return self.commands + return self._extract_shell_code_blocks(self.content) + def diagnose(self, question: str) -> str: # 确保用户输入的问题中包含有效的IP地址,若没有,则诊断本机 if not self._contains_valid_ip(question): local_ip = self._get_local_ip() if local_ip: - question = f'当前机器的IP为 {local_ip},' + question - headers = self._get_headers() - data = { - 'question': question, - 'session_id': self.session_id, - 'user_selected_plugins': [{'plugin_name': 'Diagnostic'}], - } - self._stream_response(headers, data) + question = f'{prompt_framework_plugin_ip} {local_ip},' + question + self._query_llm_service(question) return self.content def tuning(self, question: str) -> str: @@ -59,20 +154,25 @@ class Framework(LLMService): if not self._contains_valid_ip(question): local_ip = self._get_local_ip() if local_ip: - question = f'当前机器的IP为 {local_ip},' + question - headers = self._get_headers() - data = { - 'question': question, - 'session_id': self.session_id, - 'user_selected_plugins': [{'plugin_name': 'A-Tune'}], - } - self._stream_response(headers, data) + question = f'{prompt_framework_plugin_ip} {local_ip},' + question + self._query_llm_service(question) return self.content # pylint: disable=W0221 - def _query_llm_service(self, question: str, show_suggestion: bool = True): + def _query_llm_service( + self, + question: str, + user_selected_plugins: Optional[list] = None, + show_suggestion: bool = True + ): + if not user_selected_plugins: + user_selected_plugins = ['auto'] headers = self._get_headers() - data = {'question': question, 'session_id': self.session_id} + data = { + 'question': question, + 'conversation_id': self.conversation_id, + 'user_selected_plugins': user_selected_plugins + } self._stream_response(headers, data, show_suggestion) def _stream_response(self, headers, data, show_suggestion: bool = True): @@ -81,20 +181,44 @@ class Framework(LLMService): with Live(console=self.console, vertical_overflow='visible') as live: live.update(spinner, refresh=True) try: - response = requests.post(self.endpoint, headers=headers, json=data, stream=True, timeout=300) + stream_answer_url = urljoin(self.endpoint, 'api/client/chat') + response = requests.post( + url=stream_answer_url, + headers=headers, + json=data, + stream=True, + timeout=300 + ) except requests.exceptions.ConnectionError: - live.update('EulerCopilot 智能体连接失败', refresh=True) + live.update( + backend_framework_request_connection_error.format(brand_name=BRAND_NAME), refresh=True) return except requests.exceptions.Timeout: - live.update('EulerCopilot 智能体请求超时', refresh=True) + live.update( + backend_framework_request_timeout.format(brand_name=BRAND_NAME), refresh=True) return except requests.exceptions.RequestException: - live.update('EulerCopilot 智能体请求异常', refresh=True) + live.update( + backend_framework_request_exceptions.format(brand_name=BRAND_NAME), refresh=True) + return + if response.status_code == 401: + live.update( + backend_framework_request_unauthorized, refresh=True) + return + if response.status_code == 429: + live.update( + backend_framework_request_too_many_requests, refresh=True) return if response.status_code != 200: - live.update(f'请求失败: {response.status_code}', refresh=True) + live.update( + backend_general_request_failed.format(code=response.status_code), refresh=True) + return + self.session_id = self._reset_session_from_cookie(response.headers.get('set-cookie', '')) + try: + self._handle_response_stream(live, response, show_suggestion) + except requests.exceptions.ChunkedEncodingError: + live.update(backend_framework_response_ended_prematurely, refresh=True) return - self._handle_response_stream(live, response, show_suggestion) def _clear_previous_data(self): self.content = '' @@ -118,14 +242,23 @@ class Framework(LLMService): continue if content == '[ERROR]': if not self.content: - MarkdownRenderer.update(live, 'EulerCopilot 智能体遇到错误,请联系管理员定位问题') + MarkdownRenderer.update( + live, + backend_framework_stream_error.format(brand_name=BRAND_NAME) + ) elif content == '[SENSITIVE]': - MarkdownRenderer.update(live, '检测到违规信息,请重新提问') + MarkdownRenderer.update(live, backend_framework_stream_sensitive) self.content = '' elif content != '[DONE]': if not self.debug_mode: continue - MarkdownRenderer.update(live, f'EulerCopilot 智能体返回了未知内容:\n```json\n{content}\n```') + MarkdownRenderer.update( + live, + backend_framework_stream_unknown.format( + brand_name=BRAND_NAME, + content=content + ) + ) break else: self._handle_json_chunk(jcontent, live, show_suggestion) @@ -133,17 +266,45 @@ class Framework(LLMService): def _handle_json_chunk(self, jcontent, live: Live, show_suggestion: bool): chunk = jcontent.get('content', '') self.content += chunk + # 获取推荐问题 if show_suggestion: suggestions = jcontent.get('search_suggestions', []) if suggestions: - self.sugggestion = suggestions[0].strip() + suggested_plugin = suggestions[0].get('name', '') + suggested_question = suggestions[0].get('question', '') + if suggested_plugin and suggested_question: + self.sugggestion = f'**{suggested_plugin}** {suggested_question}' + elif suggested_question: + self.sugggestion = suggested_question + # 获取插件返回数据 + plugin_tool_type = jcontent.get('type', '') + if plugin_tool_type == 'extract': + data_str = jcontent.get('data', '') + if data_str: + try: + data = json.loads(data_str) + except json.JSONDecodeError: + return + # 返回 Markdown 报告 + output = data.get('output', '') + if output: + self.content += output + # 返回单行 Shell 命令 + cmd = data.get('shell', '') + if cmd: + self.commands.append(cmd) + # 返回 Shell 脚本 + script = data.get('script', '') + if script: + self.commands.append(write_shell_script(script)) + # 刷新终端 if not self.sugggestion: MarkdownRenderer.update(live, self.content) else: MarkdownRenderer.update( live, content=self.content, - sugggestion=f'**你可以继续问** {self.sugggestion}' + sugggestion=backend_framework_sugggestion.format(sugggestion=self.sugggestion), ) def _get_headers(self) -> dict: @@ -152,8 +313,18 @@ class Framework(LLMService): 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'keep-alive', 'Authorization': f'Bearer {self.api_key}', + 'Cookie': f'ECSESSION={self.session_id};' if self.session_id else '', } + def _reset_session_from_cookie(self, cookie: str) -> str: + if not cookie: + return '' + for item in cookie.split(';'): + item = item.strip() + if item.startswith('ECSESSION'): + return item.split('=')[1] + return '' + def _contains_valid_ip(self, text: str) -> bool: ip_pattern = re.compile( r'(? str: - return f'''你的任务是: -根据用户输入的问题,提供相应的操作系统的管理和运维解决方案。 -你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 - -格式要求: -+ 你的回答中的代码块和表格都必须用 Markdown 呈现; -+ 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 - -其他要求: -+ 如果用户要求安装软件包,请注意 openEuler 使用 dnf 管理软件包,你不能在回答中使用 apt 或其他软件包管理器 -+ 请特别注意当前用户的权限:{self._gen_sudo_prompt()} - -在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! -如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 - -危险操作举例: -+ 例1: 强制删除 - ```bash - rm -rf /path/to/sth - ``` -+ 例2: 卸载软件包时默认同意 - ```bash - dnf remove -y package_name - ``` -你不能输出类似于上述例子的命令! - -由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 -''' + return prompt_framework_primary.format(prompt_general_root=self._gen_sudo_prompt()) + + +@dataclass +class PluginData: + id: str + plugin_name: str + plugin_description: str + plugin_auth: Optional[dict] = None diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index 90320ea..5af2b20 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -4,6 +4,13 @@ import re from abc import ABC, abstractmethod from copilot.utilities.env_info import get_os_info, is_root +from copilot.utilities.i18n import ( + prompt_general_chat, + prompt_general_explain_cmd, + prompt_general_root_false, + prompt_general_root_true, + prompt_general_system, +) class LLMService(ABC): @@ -34,62 +41,15 @@ class LLMService(ABC): def _gen_sudo_prompt(self) -> str: if is_root(): - return '当前用户为 root 用户,你生成的 shell 命令不能包涵 sudo' - return '当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo' + return prompt_general_root_true + return prompt_general_root_false def _gen_system_prompt(self) -> str: - return f'''你是操作系统 {get_os_info()} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 -你的任务是: -根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 -你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 - -格式要求: -你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; -你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 - -用户可能问你一些操作系统相关的问题,你尤其需要注意安装软件包的情景: -openEuler 使用 dnf 或 yum 管理软件包,你不能在回答中使用 apt 或其他命令; -Debian 和 Ubuntu 使用 apt 管理软件包,你也不能在回答中使用 dnf 或 yum 命令; -你可能还会遇到使用其他类 unix 系统的情景,比如 macOS 要使用 Homebrew 安装软件包。 - -请特别注意当前用户的权限: -{self._gen_sudo_prompt()} - -在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! -如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令\n -危险操作举例:\n -例1: 强制删除 -```bash -rm -rf /path/to/sth -``` -例2: 卸载软件包时默认同意 -```bash -dnf remove -y package_name -``` -你不能输出类似于上述例子的命令! - -由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 -''' + return prompt_general_system.format( + os=get_os_info(), prompt_general_root=self._gen_sudo_prompt()) def _gen_chat_prompt(self, question: str) -> str: - return f'''根据用户输入的问题,使用 Markdown 格式输出。 - -用户的问题: -{question} - -基本要求: -1. 如果涉及到生成 shell 命令,请用单行 shell 命令回答,不能使用多行 shell 命令 -2. 如果涉及 shell 命令或代码,请用 Markdown 代码块输出,必须标明代码的语言 -3. 如果用户要求你生成的命令涉及到数据输入,你需要正确处理数据输入的方式,包括用户交互 -4. 当前操作系统是 {get_os_info()},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 -''' + return prompt_general_chat.format(question=question, os=get_os_info()) def _gen_explain_cmd_prompt(self, cmd: str) -> str: - return f'''```bash -{cmd} -``` -请解释上面的 Shell 命令 - -要求: -先在代码块中打印一次上述命令,再有条理地解释命令中的主要步骤 -''' + return prompt_general_explain_cmd.format(cmd=cmd) diff --git a/src/copilot/backends/openai_api.py b/src/copilot/backends/openai_api.py index 287dec5..1385aa7 100644 --- a/src/copilot/backends/openai_api.py +++ b/src/copilot/backends/openai_api.py @@ -1,7 +1,7 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. import json -from typing import Union +from typing import Optional import requests from rich.console import Console @@ -9,14 +9,20 @@ from rich.live import Live from rich.spinner import Spinner from copilot.backends.llm_service import LLMService +from copilot.utilities.i18n import ( + backend_general_request_failed, + backend_openai_request_connection_error, + backend_openai_request_exceptions, + backend_openai_request_timeout, +) from copilot.utilities.markdown_renderer import MarkdownRenderer class ChatOpenAI(LLMService): - def __init__(self, url: str, api_key: Union[str, None], model: Union[str, None], max_tokens = 2048): + def __init__(self, url: str, api_key: Optional[str], model: Optional[str], max_tokens = 2048): self.url: str = url - self.api_key: Union[str, None] = api_key - self.model: Union[str, None] = model + self.api_key: Optional[str] = api_key + self.model: Optional[str] = model self.max_tokens: int = max_tokens self.answer: str = '' self.history: list = [] @@ -72,16 +78,16 @@ class ChatOpenAI(LLMService): timeout=60 ) except requests.exceptions.ConnectionError: - live.update('连接大模型失败', refresh=True) + live.update(backend_openai_request_connection_error, refresh=True) return except requests.exceptions.Timeout: - live.update('请求大模型超时', refresh=True) + live.update(backend_openai_request_timeout, refresh=True) return except requests.exceptions.RequestException: - live.update('请求大模型异常', refresh=True) + live.update(backend_openai_request_exceptions, refresh=True) return if response.status_code != 200: - live.update(f'请求失败: {response.status_code}', refresh=True) + live.update(backend_general_request_failed.format(code=response.status_code), refresh=True) return for line in response.iter_lines(): if line is None: diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index a3804de..0f0db2f 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -17,6 +17,14 @@ from rich.spinner import Spinner from rich.text import Text from copilot.backends.llm_service import LLMService +from copilot.utilities.i18n import ( + backend_spark_network_error, + backend_spark_stream_error, + backend_spark_websockets_exceptions_msg_a, + backend_spark_websockets_exceptions_msg_b, + backend_spark_websockets_exceptions_msg_c, + backend_spark_websockets_exceptions_msg_title, +) from copilot.utilities.markdown_renderer import MarkdownRenderer @@ -66,7 +74,10 @@ class Spark(LLMService): code = data['header']['code'] if code != 0: message = data['header']['message'] - live.update(f'请求错误: {code}\n{message}', refresh=True) + live.update(backend_spark_stream_error.format( + code=code, + message=message + ), refresh=True) await websocket.close() else: choices = data['payload']['choices'] @@ -82,14 +93,14 @@ class Spark(LLMService): except websockets.exceptions.InvalidStatusCode: live.update( - Text.from_ansi('\033[1;31m请求错误\033[0m\n\n')\ - .append('请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。\n')\ - .append('输入 "vi ~/.config/eulercopilot/config.json" 查看和编辑配置;\n')\ - .append(f'或尝试 ping {self.spark_url}'), + Text.from_ansi(backend_spark_websockets_exceptions_msg_title)\ + .append(backend_spark_websockets_exceptions_msg_a)\ + .append(backend_spark_websockets_exceptions_msg_b)\ + .append(backend_spark_websockets_exceptions_msg_c.format(spark_url=self.spark_url)), refresh=True ) except Exception: # pylint: disable=W0718 - live.update('访问大模型失败,请检查网络连接') + live.update(backend_spark_network_error) def _create_url(self): now = datetime.now() # 生成RFC1123格式的时间戳 diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index a67e707..4286bed 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -3,6 +3,9 @@ import json import os +from copilot.backends.framework_api import QUERY_MODS +from copilot.utilities import interact + CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') @@ -55,32 +58,15 @@ def update_config(key: str, value): def select_query_mode(mode: int): - modes = ['chat', 'diagnose', 'tuning'] + modes = list(QUERY_MODS.keys()) if mode < len(modes): update_config('query_mode', modes[mode]) def select_backend(): - backends = ['framework', 'spark', 'openai'] - print('\n\033[1;33m请选择大模型后端:\033[0m\n') - print('\t<1> EulerCopilot') - print('\t<2> 讯飞星火大模型 3.5') - print('\t<3> 类 ChatGPT(兼容 llama.cpp)') - print() - try: - while True: - backend_input = input('\033[33m>>>\033[0m ').strip() - try: - backend_index = int(backend_input) - 1 - backend = backends[backend_index] - except (ValueError, TypeError): - print('请输入正确的序号') - continue - else: - update_config('backend', backend) - break - except KeyboardInterrupt: - print('\n\033[1;31m用户已取消选择\033[0m\n') + backend = interact.select_backend() + if backend in ['framework', 'spark', 'openai']: + update_config('backend', backend) def edit_config(): diff --git a/src/copilot/utilities/env_info.py b/src/copilot/utilities/env_info.py index 9283534..c86a025 100644 --- a/src/copilot/utilities/env_info.py +++ b/src/copilot/utilities/env_info.py @@ -5,10 +5,10 @@ import platform import re import subprocess import sys -from typing import Union +from typing import Optional -def _exec_shell_cmd(cmd: list) -> Union[subprocess.CompletedProcess, None]: +def _exec_shell_cmd(cmd: list) -> Optional[subprocess.CompletedProcess]: try: process = subprocess.run( cmd, @@ -19,13 +19,14 @@ def _exec_shell_cmd(cmd: list) -> Union[subprocess.CompletedProcess, None]: ) except subprocess.CalledProcessError as e: sys.stderr.write(e.stderr) + return None except FileNotFoundError as e: sys.stderr.write(str(e)) - else: - return process + return None + return process -def _porc_linux_info(shell_result: Union[subprocess.CompletedProcess, None]): +def _porc_linux_info(shell_result: Optional[subprocess.CompletedProcess]): if shell_result is not None: pattern = r'PRETTY_NAME="(.+?)"' match = re.search(pattern, shell_result.stdout) @@ -34,7 +35,7 @@ def _porc_linux_info(shell_result: Union[subprocess.CompletedProcess, None]): return 'Unknown Linux distribution' -def _porc_macos_info(shell_result: Union[subprocess.CompletedProcess, None]): +def _porc_macos_info(shell_result: Optional[subprocess.CompletedProcess]): if shell_result is not None: macos_info = {} if shell_result.returncode == 0: diff --git a/src/copilot/utilities/i18n.py b/src/copilot/utilities/i18n.py new file mode 100644 index 0000000..a4ff3b3 --- /dev/null +++ b/src/copilot/utilities/i18n.py @@ -0,0 +1,155 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +from gettext import gettext as _ + +BRAND_NAME = 'EulerCopilot' + +main_exit_prompt = _('\033[33m输入 "exit" 或按下 Ctrl+C 结束对话\033[0m') +main_service_is_none = _('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') +main_service_framework_plugin_is_none = _('\033[1;31m获取插件失败或插件列表为空\n请联系管理员检查后端配置\033[0m') +main_exec_builtin_cmd = _('不支持执行 Shell 内置命令 "{cmd_prefix}",请复制后手动执行') +main_exec_value_error = _('执行命令时出错:{error}') +main_exec_not_found_error = _('命令不存在:{error}') +main_exec_cmd_failed_with_exit_code = _('命令 "{cmd}" 执行中止,退出码:{exit_code}') + +cli_help_prompt_question = _('通过自然语言提问') +cli_help_prompt_switch_mode = _('切换到{mode}模式') +cli_help_prompt_init_settings = _('初始化 copilot 设置') +cli_help_prompt_edit_settings = _('编辑 copilot 设置') +cli_help_prompt_select_backend = _('选择大语言模型后端') +cli_help_panel_switch_mode = _('选择问答模式') +cli_help_panel_advanced_options = _('高级选项') +cli_notif_select_one_mode = _('\033[1;31m当前版本只能选择一种问答模式\033[0m') +cli_notif_compatibility = _('\033[33m当前大模型后端不支持{mode}功能\033[0m\n\ + \033[33m推荐使用 {brand_name} 智能体框架\033[0m') +cli_notif_no_config = _('\033[1;31m请先初始化 copilot 设置\033[0m\n\ + \033[33m请使用 "copilot --init" 命令初始化\033[0m') + +interact_action_explain = _('解释命令') +interact_action_edit = _('编辑命令') +interact_action_execute = _('执行命令') +interact_action_explain_selected = _('解释指定命令') +interact_action_edit_selected = _('编辑指定命令') +interact_action_execute_selected = _('执行指定命令') +interact_action_execute_all = _('执行所有命令') +interact_backend_framework = _('{brand_name} 智能体') +interact_backend_spark = _('讯飞星火大模型') +interact_backend_openai = _('OpenAI 兼容模式') +interact_cancel = _('取消') + +interact_question_select_action = _('选择要执行的操作:') +interact_question_select_cmd = _('选择命令:') +interact_question_select_backend = _('请选择大模型后端:') +interact_question_select_plugin = _('请选择插件:') +interact_select_plugins_valiidate = _('请选择至少一个插件') + +backend_general_request_failed = _('请求失败: {code}') +backend_framework_auth_invalid_api_key = _('{brand_name} 智能体 API 密钥无效,请检查配置文件') +backend_framework_request_connection_error = _('{brand_name} 智能体连接失败,请检查网络连接') +backend_framework_request_timeout = _('{brand_name} 智能体请求超时,请检查网络连接') +backend_framework_request_exceptions = _('{brand_name} 智能体请求异常,请检查网络连接') +backend_framework_request_unauthorized = _('当前会话已过期,请退出后重试') +backend_framework_request_too_many_requests = _('请求过于频繁,请稍后再试') +backend_framework_response_ended_prematurely = _('请求异常中止,请检查网络连接') +backend_framework_stream_error = _('{brand_name} 智能体遇到错误,请联系管理员定位问题') +backend_framework_stream_unknown = _('{brand_name} 智能体返回了未知内容:\n```json\n{content}\n```') +backend_framework_stream_sensitive = _('检测到违规信息,请重新提问') +backend_framework_sugggestion = _('**你可以继续问** {sugggestion}') +backend_spark_stream_error = _('请求错误: {code}\n{message}') +backend_spark_websockets_exceptions_msg_title = _('\033[1;31m请求错误\033[0m\n\n') +backend_spark_websockets_exceptions_msg_a = _('请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。\n') +backend_spark_websockets_exceptions_msg_b = _('输入 "vi ~/.config/eulercopilot/config.json" 查看和编辑配置;\n') +backend_spark_websockets_exceptions_msg_c = _('或尝试 ping {spark_url}') +backend_spark_network_error = _('访问大模型失败,请检查网络连接') +backend_openai_request_connection_error = _('连接大模型失败') +backend_openai_request_timeout = _('请求大模型超时') +backend_openai_request_exceptions = _('请求大模型异常') + +query_mode_chat = _('智能问答') +query_mode_flow = _('智能工作流') +query_mode_diagnose = _('智能诊断') +query_mode_tuning = _('智能调优') + +prompt_general_root_true = _('当前用户为 root 用户,你生成的 shell 命令不能包涵 sudo') +prompt_general_root_false = _('当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo') +prompt_general_system = _('''你是操作系统 {os} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 +你的任务是: +根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 +你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 + +格式要求: +你的回答必须使用 Markdown 格式,代码块和表格都必须用 Markdown 呈现; +你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 + +用户可能问你一些操作系统相关的问题,你尤其需要注意安装软件包的情景: +openEuler 使用 dnf 或 yum 管理软件包,你不能在回答中使用 apt 或其他命令; +Debian 和 Ubuntu 使用 apt 管理软件包,你也不能在回答中使用 dnf 或 yum 命令; +你可能还会遇到使用其他类 unix 系统的情景,比如 macOS 要使用 Homebrew 安装软件包。 + +请特别注意当前用户的权限: +{prompt_general_root} + +在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! +如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 + +危险操作举例: ++ 例1: 强制删除 + ```bash + rm -rf /path/to/sth + ``` ++ 例2: 卸载软件包时默认同意 + ```bash + dnf remove -y package_name + ``` +你不能输出类似于上述例子的命令! + +由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 +''') +prompt_general_chat = _('''根据用户输入的问题,使用 Markdown 格式输出。 + +用户的问题: +{question} + +基本要求: +1. 如果涉及到生成 shell 命令,请用单行 shell 命令回答,不能使用多行 shell 命令 +2. 如果涉及 shell 命令或代码,请用 Markdown 代码块输出,必须标明代码的语言 +3. 如果用户要求你生成的命令涉及到数据输入,你需要正确处理数据输入的方式,包括用户交互 +4. 当前操作系统是 {os},你的回答必须符合当前系统要求,不能使用当前系统没有的功能 +''') +prompt_general_explain_cmd = _('''```bash +{cmd} +``` +请解释上面的 Shell 命令 + +要求: +先在代码块中打印一次上述命令,再有条理地解释命令中的主要步骤 +''') +prompt_framework_primary = _('''你的任务是: +根据用户输入的问题,提供相应的操作系统的管理和运维解决方案。 +你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 + +格式要求: ++ 你的回答中的代码块和表格都必须用 Markdown 呈现; ++ 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 + +其他要求: ++ 如果用户要求安装软件包,请注意 openEuler 使用 dnf 管理软件包,你不能在回答中使用 apt 或其他软件包管理器 ++ 请特别注意当前用户的权限:{prompt_general_root} + +在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! +如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 + +危险操作举例: ++ 例1: 强制删除 + ```bash + rm -rf /path/to/sth + ``` ++ 例2: 卸载软件包时默认同意 + ```bash + dnf remove -y package_name + ``` +你不能输出类似于上述例子的命令! + +由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 +''') +prompt_framework_plugin_ip = _('当前机器的IP为') diff --git a/src/copilot/utilities/interact.py b/src/copilot/utilities/interact.py index 8562a29..730bbef 100644 --- a/src/copilot/utilities/interact.py +++ b/src/copilot/utilities/interact.py @@ -2,81 +2,128 @@ import questionary +from copilot.backends.framework_api import PluginData +from copilot.utilities import i18n + ACTIONS_SINGLE_CMD = [ - questionary.Choice('解释命令', value='explain', shortcut_key='a'), - questionary.Choice('编辑命令', value='edit', shortcut_key='z'), - questionary.Choice('执行命令', value='execute', shortcut_key='x'), - questionary.Choice('取消', value='cancel', shortcut_key='c'), + questionary.Choice( + i18n.interact_action_explain, + value='explain', + shortcut_key='a' + ), + questionary.Choice( + i18n.interact_action_edit, + value='edit', + shortcut_key='z' + ), + questionary.Choice( + i18n.interact_action_execute, + value='execute', + shortcut_key='x' + ), + questionary.Choice( + i18n.interact_cancel, + value='cancel', + shortcut_key='c' + ) ] ACTIONS_MULTI_CMDS = [ - questionary.Choice('解释指定命令', value='explain', shortcut_key='a'), - questionary.Choice('编辑指定命令', value='edit', shortcut_key='z'), - questionary.Choice('执行所有命令', value='execute_all', shortcut_key='x'), - questionary.Choice('执行指定命令', value='execute_selected', shortcut_key='s'), - questionary.Choice('取消', value='cancel', shortcut_key='c'), + questionary.Choice( + i18n.interact_action_explain_selected, + value='explain', + shortcut_key='a' + ), + questionary.Choice( + i18n.interact_action_edit_selected, + value='edit', + shortcut_key='z' + ), + questionary.Choice( + i18n.interact_action_execute_all, + value='execute_all', + shortcut_key='x' + ), + questionary.Choice( + i18n.interact_action_execute_selected, + value='execute_selected', + shortcut_key='s' + ), + questionary.Choice( + i18n.interact_cancel, + value='cancel', + shortcut_key='c' + ) ] -QUESTIONS = [ - '选择要执行的操作:', - '选择命令:' +BACKEND_CHOICES = [ + questionary.Choice( + i18n.interact_backend_framework.format(brand_name=i18n.BRAND_NAME), + value='framework', + shortcut_key='e' + ), + questionary.Choice( + i18n.interact_backend_spark, + value='spark', + shortcut_key='s' + ), + questionary.Choice( + i18n.interact_backend_openai, + value='openai', + shortcut_key='o' + ), + questionary.Choice( + i18n.interact_cancel, + value='cancel', + shortcut_key='c' + ) ] CUSTOM_STYLE_FANCY = questionary.Style( [ - ('separator', 'fg:#cc5454'), - ('qmark', 'fg:#673ab7 bold'), + ('separator', 'fg:#00afff'), + ('qmark', 'fg:#005f87 bold'), ('question', 'bold'), - ('selected', 'fg:#cc5454'), - ('pointer', 'fg:#673ab7 bold'), - ('highlighted', 'fg:#673ab7 bold'), - ('answer', 'fg:#f44336 bold'), - ('text', 'fg:#FBE9E7'), - ('disabled', 'fg:#858585 italic'), + ('selected', 'fg:#00afff bold'), + ('pointer', 'fg:#005f87 bold'), + ('highlighted', 'bold'), + ('answer', 'fg:#00afff bold'), + ('text', 'fg:#808080'), + ('disabled', 'fg:#808080 italic'), ] ) -def query_yes_or_no(question: str) -> bool: - valid = {'yes': True, 'y': True, 'no': False, 'n': False} - prompt = ' [Y/n] ' - - while True: - choice = input(question + prompt).lower() - if choice == '': - return valid['y'] - elif choice in valid: - return valid[choice] - print('请用 "yes (y)" 或 "no (n)" 回答') +def select_backend() -> str: + return questionary.select( + i18n.interact_question_select_backend, + choices=BACKEND_CHOICES, + use_shortcuts=True, + style=CUSTOM_STYLE_FANCY, + ).ask() def select_action(has_multi_cmds: bool) -> str: return questionary.select( - QUESTIONS[0], + i18n.interact_question_select_action, choices=ACTIONS_MULTI_CMDS if has_multi_cmds else ACTIONS_SINGLE_CMD, - pointer=None, use_shortcuts=True, - use_indicator=True, style=CUSTOM_STYLE_FANCY ).ask() def select_command(commands: list) -> str: return questionary.select( - QUESTIONS[1], + i18n.interact_question_select_cmd, choices=commands, - pointer=None, - use_indicator=True, style=CUSTOM_STYLE_FANCY ).ask() def select_command_with_index(commands: list) -> int: command = questionary.select( - QUESTIONS[1], + i18n.interact_question_select_cmd, choices=commands, - pointer=None, - use_indicator=True, style=CUSTOM_STYLE_FANCY ).ask() return commands.index(command) @@ -84,9 +131,27 @@ def select_command_with_index(commands: list) -> int: def select_multiple_commands(commands: list) -> list: return questionary.checkbox( - QUESTIONS[1], + i18n.interact_question_select_cmd, choices=commands, - pointer=None, - use_indicator=True, style=CUSTOM_STYLE_FANCY ).ask() + + +def select_plugins(plugins: list[PluginData]) -> list: + return questionary.checkbox( + i18n.interact_question_select_plugin, + choices=get_plugin_choices(plugins), + validate=lambda a: ( + True if len(a) > 0 else i18n.interact_select_plugins_valiidate + ), + style=CUSTOM_STYLE_FANCY + ).ask() + + +def get_plugin_choices(plugins: list[PluginData]) -> list: + return [ + questionary.Choice( + plugin.plugin_name, + value=plugin.id + ) for plugin in plugins + ] diff --git a/src/copilot/utilities/shell_script.py b/src/copilot/utilities/shell_script.py new file mode 100644 index 0000000..61f6deb --- /dev/null +++ b/src/copilot/utilities/shell_script.py @@ -0,0 +1,15 @@ +# Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. + +import os +import uuid + + +def write_shell_script(content: str) -> str: + '''将脚本内容写进 sh 文件中,并返回执行命令''' + script_name = f'plugin_gen_script_{str(uuid.uuid4())[:8]}.sh' + script_path = os.path.join(os.path.expanduser('~'), '.eulercopilot', 'scripts', script_name) + os.makedirs(os.path.dirname(script_path), exist_ok=True) + with open(script_path, 'w', encoding='utf-8') as script_file: + script_file.write(content) + os.chmod(script_path, 0o700) + return f'bash {script_path}' diff --git a/src/eulercopilot.sh b/src/eulercopilot.sh index 48f6a78..8249136 100644 --- a/src/eulercopilot.sh +++ b/src/eulercopilot.sh @@ -10,6 +10,8 @@ read_query_mode() { if [ "$query_mode" = "\"chat\"" ]; then echo "智能问答" + elif [ "$query_mode" = "\"flow\"" ]; then + echo "智能工作流" elif [ "$query_mode" = "\"diagnose\"" ]; then echo "智能诊断" elif [ "$query_mode" = "\"tuning\"" ]; then diff --git a/src/setup.py b/src/setup.py index 2378985..61f1d80 100644 --- a/src/setup.py +++ b/src/setup.py @@ -34,7 +34,7 @@ extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] # 定义 setup() 参数 setup( name='copilot', - version='1.1', + version='1.2', description='EulerCopilot CLI Tool', author='Hongyu Shi', author_email='shihongyu15@huawei.com', @@ -65,6 +65,7 @@ setup( 'requests', 'rich', 'typer', + 'questionary' ], entry_points={ 'console_scripts': ['copilot=copilot.__main__:entry_point'] -- Gitee From 6abe32fecd0d1ed4cceff376238f5f7815970628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Fri, 6 Sep 2024 16:16:41 +0800 Subject: [PATCH 30/32] =?UTF-8?q?UI:=20=E7=BC=96=E8=BE=91=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- src/copilot/app/copilot_app.py | 59 +++++++++++++++++++----- src/copilot/app/copilot_cli.py | 19 ++++---- src/copilot/backends/framework_api.py | 59 +++++++++++++----------- src/copilot/backends/openai_api.py | 2 +- src/copilot/backends/spark_api.py | 2 +- src/copilot/utilities/config_manager.py | 59 ++++++++++++------------ src/copilot/utilities/i18n.py | 61 +++++++++++++------------ src/copilot/utilities/interact.py | 53 +++++++++++++++++---- 8 files changed, 196 insertions(+), 118 deletions(-) diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 82c35ed..519e7a2 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -12,9 +12,11 @@ from rich.console import Console from rich.live import Live from rich.markdown import Markdown from rich.panel import Panel +from rich.text import Text from copilot.backends import framework_api, llm_service, openai_api, spark_api from copilot.utilities import i18n, interact +from copilot.utilities.config_manager import CONFIG_ENTRY_NAME, config_to_markdown, load_config, update_config selected_plugins: list = [] @@ -143,21 +145,56 @@ def select_one_cmd_with_index(cmds: list) -> int: def handle_user_input(service: llm_service.LLMService, user_input: str, mode: str) -> int: '''Process user input based on the given flag and backend configuration.''' + cmds: list = [] if mode == 'chat': - cmds = list(dict.fromkeys(service.get_shell_commands(user_input))) - return command_interaction_loop(cmds, service) + cmds = service.get_shell_commands(user_input) if isinstance(service, framework_api.Framework): - report: str = '' if mode == 'flow': - cmds = list(dict.fromkeys(service.flow(user_input, selected_plugins))) - return command_interaction_loop(cmds, service) + cmds = service.flow(user_input, selected_plugins) if mode == 'diagnose': - report = service.diagnose(user_input) + cmds = service.diagnose(user_input) if mode == 'tuning': - report = service.tuning(user_input) - if report: - return 0 - return 1 + cmds = service.tuning(user_input) + if cmds: + return command_interaction_loop(list(dict.fromkeys(cmds)), service) + return -1 + + +def edit_config(): + console = Console() + with Live(console=console) as live: + live.update( + Panel(Markdown(config_to_markdown(), code_theme='github-dark'), + border_style='gray50')) + while True: + selected_entry = interact.select_settings_entry() + if selected_entry == 'cancel': + return + if selected_entry == 'backend': + backend = interact.select_backend() + update_config(selected_entry, backend) + elif selected_entry == 'query_mode': + mode = interact.select_query_mode() + update_config(selected_entry, mode) + elif selected_entry in ('advanced_mode', 'debug_mode'): + update_config( + selected_entry, + interact.ask_boolean( + i18n.interact_question_yes_or_no.format( + question_body=CONFIG_ENTRY_NAME.get(selected_entry)))) + else: + original_text: str = load_config().get(selected_entry, '') + new_text = '' + input_prompt = i18n.interact_question_input_text.format( + question_body=CONFIG_ENTRY_NAME.get(selected_entry)) + stylized_input_prompt = Text('❯ ', style='#005f87 bold')\ + .append(input_prompt, style='bold') + readline.set_startup_hook(lambda: readline.insert_text(original_text)) + try: + new_text = console.input(stylized_input_prompt) + finally: + readline.set_startup_hook() + update_config(selected_entry, new_text) # pylint: disable=W0603 @@ -179,7 +216,7 @@ def main(user_input: Optional[str], config: dict) -> int: if not plugins: print(i18n.main_service_framework_plugin_is_none) return 1 - selected_plugins = interact.select_plugins(plugins) + selected_plugins = [interact.select_one_plugin(plugins)] elif backend == 'spark': service = spark_api.Spark( app_id=config.get('spark_app_id'), diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index 6f0f846..8f0069f 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -8,13 +8,12 @@ from typing import Optional import typer -from copilot.app.copilot_app import main +from copilot.app.copilot_app import edit_config, main from copilot.app.copilot_init import setup_copilot -from copilot.backends.framework_api import QUERY_MODS +from copilot.backends.framework_api import QUERY_MODE from copilot.utilities.config_manager import ( CONFIG_PATH, DEFAULT_CONFIG, - edit_config, load_config, select_backend, select_query_mode, @@ -56,24 +55,24 @@ def cli( help=cli_help_prompt_question), chat: bool = typer.Option( False, '--chat', '-c', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["chat"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["chat"]), rich_help_panel=cli_help_panel_switch_mode ), flow: bool = typer.Option( False, '--flow', '-f', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["flow"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["flow"]), rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework'), ), diagnose: bool = typer.Option( False, '--diagnose', '-d', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["diagnose"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["diagnose"]), rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework') ), tuning: bool = typer.Option( False, '--tuning', '-t', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODS["tuning"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["tuning"]), rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework') ), @@ -125,7 +124,7 @@ def cli( if not question: return 0 else: - compatibility_notification(QUERY_MODS['flow']) + compatibility_notification(QUERY_MODE['flow']) return 1 elif diagnose: if BACKEND == 'framework': @@ -133,7 +132,7 @@ def cli( if not question: return 0 else: - compatibility_notification(QUERY_MODS['diagnose']) + compatibility_notification(QUERY_MODE['diagnose']) return 1 elif tuning: if BACKEND == 'framework': @@ -141,7 +140,7 @@ def cli( if not question: return 0 else: - compatibility_notification(QUERY_MODS['tuning']) + compatibility_notification(QUERY_MODE['tuning']) return 1 if question: diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index d2eb520..1c199d4 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -28,8 +28,10 @@ from copilot.utilities.i18n import ( backend_framework_stream_unknown, backend_framework_sugggestion, backend_general_request_failed, + prompt_framework_extra_install, + prompt_framework_keyword_install, + prompt_framework_markdown_format, prompt_framework_plugin_ip, - prompt_framework_primary, query_mode_chat, query_mode_diagnose, query_mode_flow, @@ -38,13 +40,18 @@ from copilot.utilities.i18n import ( from copilot.utilities.markdown_renderer import MarkdownRenderer from copilot.utilities.shell_script import write_shell_script -QUERY_MODS = { +QUERY_MODE = { 'chat': query_mode_chat, 'flow': query_mode_flow, 'diagnose': query_mode_diagnose, 'tuning': query_mode_tuning, } +FRAMEWORK_LLM_STREAM_BAD_REQUEST_MSG = { + 401: backend_framework_request_unauthorized, + 429: backend_framework_request_too_many_requests +} + # pylint: disable=R0902 class Framework(LLMService): @@ -52,10 +59,11 @@ class Framework(LLMService): self.endpoint: str = url self.api_key: str = api_key self.debug_mode: bool = debug_mode - # 临时数据 + # 临时数据 (本轮对话) self.session_id: str = '' self.plugins: list = [] self.conversation_id: str = '' + # 临时数据 (本次问答) self.content: str = '' self.commands: list = [] self.sugggestion: str = '' @@ -63,7 +71,9 @@ class Framework(LLMService): self.console = Console() def get_shell_commands(self, question: str) -> list: - query = self._gen_chat_prompt(question) + self._gen_framework_extra_prompt() + query = self._add_framework_extra_prompt(self._gen_chat_prompt(question)) + if prompt_framework_keyword_install in question.lower(): + query = self._add_framework_software_install_prompt(query) self._query_llm_service(query) if self.commands: return self.commands @@ -140,23 +150,23 @@ class Framework(LLMService): return self.commands return self._extract_shell_code_blocks(self.content) - def diagnose(self, question: str) -> str: + def diagnose(self, question: str) -> list: # 确保用户输入的问题中包含有效的IP地址,若没有,则诊断本机 if not self._contains_valid_ip(question): local_ip = self._get_local_ip() if local_ip: question = f'{prompt_framework_plugin_ip} {local_ip},' + question - self._query_llm_service(question) - return self.content + self._query_llm_service(question, user_selected_plugins=['euler-copilot-rca']) + return self._extract_shell_code_blocks(self.content) - def tuning(self, question: str) -> str: + def tuning(self, question: str) -> list: # 确保用户输入的问题中包含有效的IP地址,若没有,则调优本机 if not self._contains_valid_ip(question): local_ip = self._get_local_ip() if local_ip: question = f'{prompt_framework_plugin_ip} {local_ip},' + question - self._query_llm_service(question) - return self.content + self._query_llm_service(question, user_selected_plugins=['euler-copilot-tune']) + return self._extract_shell_code_blocks(self.content) # pylint: disable=W0221 def _query_llm_service( @@ -178,12 +188,11 @@ class Framework(LLMService): def _stream_response(self, headers, data, show_suggestion: bool = True): self._clear_previous_data() spinner = Spinner('material') - with Live(console=self.console, vertical_overflow='visible') as live: + with Live(console=self.console) as live: live.update(spinner, refresh=True) try: - stream_answer_url = urljoin(self.endpoint, 'api/client/chat') response = requests.post( - url=stream_answer_url, + urljoin(self.endpoint, 'api/client/chat'), headers=headers, json=data, stream=True, @@ -201,24 +210,18 @@ class Framework(LLMService): live.update( backend_framework_request_exceptions.format(brand_name=BRAND_NAME), refresh=True) return - if response.status_code == 401: - live.update( - backend_framework_request_unauthorized, refresh=True) - return - if response.status_code == 429: - live.update( - backend_framework_request_too_many_requests, refresh=True) - return if response.status_code != 200: - live.update( - backend_general_request_failed.format(code=response.status_code), refresh=True) + msg = FRAMEWORK_LLM_STREAM_BAD_REQUEST_MSG.get( + response.status_code, + backend_general_request_failed.format(code=response.status_code) + ) + live.update(msg, refresh=True) return self.session_id = self._reset_session_from_cookie(response.headers.get('set-cookie', '')) try: self._handle_response_stream(live, response, show_suggestion) except requests.exceptions.ChunkedEncodingError: live.update(backend_framework_response_ended_prematurely, refresh=True) - return def _clear_previous_data(self): self.content = '' @@ -350,8 +353,12 @@ class Framework(LLMService): return ip_address return '' - def _gen_framework_extra_prompt(self) -> str: - return prompt_framework_primary.format(prompt_general_root=self._gen_sudo_prompt()) + def _add_framework_extra_prompt(self, query: str) -> str: + return query + '\n\n' + prompt_framework_markdown_format + + def _add_framework_software_install_prompt(self, query: str) -> str: + return query + '\n\n' + prompt_framework_extra_install.format( + prompt_general_root=self._gen_sudo_prompt()) @dataclass diff --git a/src/copilot/backends/openai_api.py b/src/copilot/backends/openai_api.py index 1385aa7..d44e9f6 100644 --- a/src/copilot/backends/openai_api.py +++ b/src/copilot/backends/openai_api.py @@ -67,7 +67,7 @@ class ChatOpenAI(LLMService): def _stream_response(self, query: str): spinner = Spinner('material') self.answer = '' - with Live(console=self.console, vertical_overflow='visible') as live: + with Live(console=self.console) as live: live.update(spinner, refresh=True) try: response = requests.post( diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index 0f0db2f..ca4db94 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -60,7 +60,7 @@ class Spark(LLMService): url = self._create_url() self.answer = '' spinner = Spinner('material') - with Live(console=self.console, vertical_overflow='visible') as live: + with Live(console=self.console) as live: live.update(spinner, refresh=True) try: async with websockets.connect(url) as websocket: diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 4286bed..05d669b 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -3,12 +3,29 @@ import json import os -from copilot.backends.framework_api import QUERY_MODS -from copilot.utilities import interact +from copilot.backends.framework_api import QUERY_MODE +from copilot.utilities import i18n, interact CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json') +CONFIG_ENTRY_NAME = { + 'backend': i18n.settings_config_entry_backend, + 'query_mode': i18n.settings_config_entry_query_mode, + 'advanced_mode': i18n.settings_config_entry_advanced_mode, + 'debug_mode': i18n.settings_config_entry_debug_mode, + 'spark_app_id': i18n.settings_config_entry_spark_app_id, + 'spark_api_key': i18n.settings_config_entry_spark_api_key, + 'spark_api_secret': i18n.settings_config_entry_spark_api_secret, + 'spark_url': i18n.settings_config_entry_spark_url, + 'spark_domain': i18n.settings_config_entry_spark_domain, + 'framework_url': i18n.settings_config_entry_framework_url.format(brand_name=i18n.BRAND_NAME), + 'framework_api_key': i18n.settings_config_entry_framework_api_key.format(brand_name=i18n.BRAND_NAME), + 'model_url': i18n.settings_config_entry_model_url, + 'model_api_key': i18n.settings_config_entry_model_api_key, + 'model_name': i18n.settings_config_entry_model_name +} + DEFAULT_CONFIG = { 'backend': 'framework', 'query_mode': 'chat', @@ -37,7 +54,7 @@ def load_config() -> dict: return config -def write_config(config): +def write_config(config: dict): with open(CONFIG_PATH, 'w', encoding='utf-8') as json_file: json.dump(config, json_file, indent=4) json_file.write('\n') # 追加一行空行 @@ -58,7 +75,7 @@ def update_config(key: str, value): def select_query_mode(mode: int): - modes = list(QUERY_MODS.keys()) + modes = list(QUERY_MODE.keys()) if mode < len(modes): update_config('query_mode', modes[mode]) @@ -69,30 +86,12 @@ def select_backend(): update_config('backend', backend) -def edit_config(): +def config_to_markdown() -> str: config = load_config() - print('\n\033[1;33m当前设置:\033[0m') - format_string = '{:<32} {}'.format - for key, value in config.items(): - print(f'- {format_string(key, value)}') - - print('\n\033[33m输入要修改的设置项以修改设置:\033[0m') - print('示例:') - print('>>> spark_api_key(按下回车)') - print('<<< (在此处输入你的星火大模型 API Key)') - print('* 输入空白值以退出程序') - print('* 建议在管理员指导下操作') - try: - while True: - key = input('\033[35m>>>\033[0m ') - if key in config: - value = input('\033[33m<<<\033[0m ') - if value == '': - break - config[key] = value - elif key == '': - break - else: - print('输入有误,请重试') - except KeyboardInterrupt: - print('\n\033[1;31m用户已取消编辑\033[0m\n') + config_table = '\n'.join([ + f'| {CONFIG_ENTRY_NAME.get(key)} | {value} |' for key, value in config.items() + ]) + return f'### {i18n.settings_markdown_title}\n\n\ + | {i18n.settings_markdown_header_key} \ + | {i18n.settings_markdown_header_value} |\n\ + | ----------- | ----------- |\n{config_table}' diff --git a/src/copilot/utilities/i18n.py b/src/copilot/utilities/i18n.py index a4ff3b3..1974f05 100644 --- a/src/copilot/utilities/i18n.py +++ b/src/copilot/utilities/i18n.py @@ -37,9 +37,13 @@ interact_backend_spark = _('讯飞星火大模型') interact_backend_openai = _('OpenAI 兼容模式') interact_cancel = _('取消') +interact_question_yes_or_no = _('是否{question_body}:') +interact_question_input_text = _('请输入{question_body}:') interact_question_select_action = _('选择要执行的操作:') interact_question_select_cmd = _('选择命令:') +interact_question_select_settings_entry = _('选择设置项:') interact_question_select_backend = _('请选择大模型后端:') +interact_question_select_query_mode = _('请选择问答模式:') interact_question_select_plugin = _('请选择插件:') interact_select_plugins_valiidate = _('请选择至少一个插件') @@ -50,7 +54,7 @@ backend_framework_request_timeout = _('{brand_name} 智能体请求超时,请 backend_framework_request_exceptions = _('{brand_name} 智能体请求异常,请检查网络连接') backend_framework_request_unauthorized = _('当前会话已过期,请退出后重试') backend_framework_request_too_many_requests = _('请求过于频繁,请稍后再试') -backend_framework_response_ended_prematurely = _('请求异常中止,请检查网络连接') +backend_framework_response_ended_prematurely = _('响应异常中止,请检查网络连接') backend_framework_stream_error = _('{brand_name} 智能体遇到错误,请联系管理员定位问题') backend_framework_stream_unknown = _('{brand_name} 智能体返回了未知内容:\n```json\n{content}\n```') backend_framework_stream_sensitive = _('检测到违规信息,请重新提问') @@ -65,16 +69,32 @@ backend_openai_request_connection_error = _('连接大模型失败') backend_openai_request_timeout = _('请求大模型超时') backend_openai_request_exceptions = _('请求大模型异常') +settings_markdown_title = _('当前配置') +settings_markdown_header_key = _('设置项') +settings_markdown_header_value = _('值') +settings_config_entry_backend = _('大模型后端') +settings_config_entry_query_mode = _('问答模式') +settings_config_entry_advanced_mode = _('启用高级模式') +settings_config_entry_debug_mode = _('启用调试模式') +settings_config_entry_spark_app_id = _('星火大模型 App ID') +settings_config_entry_spark_api_key = _('星火大模型 API Key') +settings_config_entry_spark_api_secret = _('星火大模型 API Secret') +settings_config_entry_spark_url = _('星火大模型 URL') +settings_config_entry_spark_domain = _('星火大模型领域') +settings_config_entry_framework_url = _('{brand_name} 智能体 URL') +settings_config_entry_framework_api_key = _('{brand_name} 智能体 API Key') +settings_config_entry_model_url = _('OpenAI 模型 URL') +settings_config_entry_model_api_key = _('OpenAI 模型 API Key') +settings_config_entry_model_name = _('OpenAI 模型名称') + query_mode_chat = _('智能问答') query_mode_flow = _('智能工作流') query_mode_diagnose = _('智能诊断') query_mode_tuning = _('智能调优') -prompt_general_root_true = _('当前用户为 root 用户,你生成的 shell 命令不能包涵 sudo') -prompt_general_root_false = _('当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 sudo') +prompt_general_root_true = _('当前用户为 root 用户,你生成的 shell 命令不能包含 "sudo"') +prompt_general_root_false = _('当前用户为普通用户,若你生成的 shell 命令需要 root 权限,需要包含 "sudo"') prompt_general_system = _('''你是操作系统 {os} 的运维助理,你精通当前操作系统的管理和运维,熟悉运维脚本的编写。 -你的任务是: -根据用户输入的问题,提供相应的操作系统的管理和运维解决方案,并使用 shell 脚本或其它常用编程语言实现。 你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 格式要求: @@ -124,32 +144,13 @@ prompt_general_explain_cmd = _('''```bash 要求: 先在代码块中打印一次上述命令,再有条理地解释命令中的主要步骤 ''') -prompt_framework_primary = _('''你的任务是: -根据用户输入的问题,提供相应的操作系统的管理和运维解决方案。 -你给出的答案必须符合当前操作系统要求,你不能使用当前操作系统没有的功能。 - -格式要求: +prompt_framework_markdown_format = _('''格式要求: + 你的回答中的代码块和表格都必须用 Markdown 呈现; + 你需要用中文回答问题,除了代码,其他内容都要符合汉语的规范。 - -其他要求: -+ 如果用户要求安装软件包,请注意 openEuler 使用 dnf 管理软件包,你不能在回答中使用 apt 或其他软件包管理器 -+ 请特别注意当前用户的权限:{prompt_general_root} - -在给用户返回 shell 命令时,你必须返回安全的命令,不能进行任何危险操作! -如果涉及到删除文件、清理缓存、删除用户、卸载软件、wget下载文件等敏感操作,你必须生成安全的命令 - -危险操作举例: -+ 例1: 强制删除 - ```bash - rm -rf /path/to/sth - ``` -+ 例2: 卸载软件包时默认同意 - ```bash - dnf remove -y package_name - ``` -你不能输出类似于上述例子的命令! - -由于用户使用命令行与你交互,你需要避免长篇大论,请使用简洁的语言,一般情况下你的回答不应超过1000字。 ''') +prompt_framework_extra_install = _('''其他要求: ++ openEuler 使用 dnf 管理软件包,你不能在回答中使用 apt 或其他软件包管理器 ++ {prompt_general_root} +''') +prompt_framework_keyword_install = _('安装') prompt_framework_plugin_ip = _('当前机器的IP为') diff --git a/src/copilot/utilities/interact.py b/src/copilot/utilities/interact.py index 730bbef..8009c89 100644 --- a/src/copilot/utilities/interact.py +++ b/src/copilot/utilities/interact.py @@ -2,8 +2,8 @@ import questionary -from copilot.backends.framework_api import PluginData -from copilot.utilities import i18n +from copilot.backends.framework_api import QUERY_MODE, PluginData +from copilot.utilities import config_manager, i18n ACTIONS_SINGLE_CMD = [ questionary.Choice( @@ -98,6 +98,7 @@ def select_backend() -> str: return questionary.select( i18n.interact_question_select_backend, choices=BACKEND_CHOICES, + qmark='❯', use_shortcuts=True, style=CUSTOM_STYLE_FANCY, ).ask() @@ -107,6 +108,7 @@ def select_action(has_multi_cmds: bool) -> str: return questionary.select( i18n.interact_question_select_action, choices=ACTIONS_MULTI_CMDS if has_multi_cmds else ACTIONS_SINGLE_CMD, + qmark='❯', use_shortcuts=True, style=CUSTOM_STYLE_FANCY ).ask() @@ -116,6 +118,7 @@ def select_command(commands: list) -> str: return questionary.select( i18n.interact_question_select_cmd, choices=commands, + qmark='❯', style=CUSTOM_STYLE_FANCY ).ask() @@ -124,6 +127,7 @@ def select_command_with_index(commands: list) -> int: command = questionary.select( i18n.interact_question_select_cmd, choices=commands, + qmark='❯', style=CUSTOM_STYLE_FANCY ).ask() return commands.index(command) @@ -133,25 +137,56 @@ def select_multiple_commands(commands: list) -> list: return questionary.checkbox( i18n.interact_question_select_cmd, choices=commands, + qmark='❯', style=CUSTOM_STYLE_FANCY ).ask() -def select_plugins(plugins: list[PluginData]) -> list: - return questionary.checkbox( +def select_one_plugin(plugins: list[PluginData]) -> str: + return questionary.select( i18n.interact_question_select_plugin, - choices=get_plugin_choices(plugins), - validate=lambda a: ( - True if len(a) > 0 else i18n.interact_select_plugins_valiidate - ), + choices=__get_plugin_choices(plugins), + qmark='❯', style=CUSTOM_STYLE_FANCY ).ask() -def get_plugin_choices(plugins: list[PluginData]) -> list: +def select_settings_entry() -> str: + return questionary.select( + i18n.interact_question_select_settings_entry, + choices=__get_settings_entry_choices(), + qmark='❯', + style=CUSTOM_STYLE_FANCY, + ).ask() + + +def select_query_mode() -> str: + return questionary.select( + i18n.interact_question_select_query_mode, + choices=__get_query_mode_choices(), + qmark='❯', + style=CUSTOM_STYLE_FANCY, + ).ask() + + +def ask_boolean(question: str) -> bool: + return questionary.confirm(question, default=False, style=CUSTOM_STYLE_FANCY).ask() + + +def __get_plugin_choices(plugins: list[PluginData]) -> list: return [ questionary.Choice( plugin.plugin_name, value=plugin.id ) for plugin in plugins ] + + +def __get_settings_entry_choices() -> list: + choices = [questionary.Choice(name, item) for item, name in config_manager.CONFIG_ENTRY_NAME.items()] + choices.append(questionary.Choice(i18n.interact_cancel, value='cancel')) + return choices + + +def __get_query_mode_choices() -> list: + return [questionary.Choice(name, item) for item, name in QUERY_MODE.items()] -- Gitee From f1ff7c6dbd65b61cd1d867a2e127b91ccaed954b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 24 Oct 2024 15:20:38 +0800 Subject: [PATCH 31/32] =?UTF-8?q?930:=20=E4=BF=AE=E5=A4=8D=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/build_rpm.sh | 7 ++- distribution/create_tarball.py | 4 +- ...ulercopilot.spec => eulercopilot-cli.spec} | 8 ++- src/copilot/app/copilot_app.py | 29 +++++----- src/copilot/app/copilot_cli.py | 27 ++++----- src/copilot/app/copilot_init.py | 37 ++++++++---- src/copilot/backends/framework_api.py | 56 +++++++++++-------- src/copilot/backends/llm_service.py | 3 +- src/copilot/backends/spark_api.py | 2 +- src/copilot/utilities/config_manager.py | 37 +++++++++--- src/copilot/utilities/i18n.py | 26 +++++---- src/copilot/utilities/interact.py | 25 +++++++-- src/eulercopilot.sh | 6 +- src/setup.py | 2 +- 14 files changed, 172 insertions(+), 97 deletions(-) rename distribution/{eulercopilot.spec => eulercopilot-cli.spec} (91%) diff --git a/distribution/build_rpm.sh b/distribution/build_rpm.sh index ef217a2..acd7106 100644 --- a/distribution/build_rpm.sh +++ b/distribution/build_rpm.sh @@ -13,7 +13,7 @@ generated_tarball=$(find . -maxdepth 1 -type f -name "*.tar.gz" -printf "%f\n") mv "./$generated_tarball" ~/rpmbuild/SOURCES/ # Locate the spec file in the parent directory -spec_file="eulercopilot.spec" +spec_file="eulercopilot-cli.spec" if [[ ! -f "$spec_file" ]]; then echo "Error: Could not find the spec file ($spec_file) in the parent directory." @@ -21,8 +21,9 @@ if [[ ! -f "$spec_file" ]]; then fi # Remove old builds -rm -f ~/rpmbuild/RPMS/"$(uname -m)"/eulercopilot-* +rm -f ~/rpmbuild/RPMS/"$(uname -m)"/eulercopilot-cli-* # Build the RPM package using rpmbuild rpmbuild --define "_tag .a$(date +%s)" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo -# rpmbuild --define "_tag .beta1" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo +# rpmbuild --define "_tag .beta3" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo +# rpmbuild --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo diff --git a/distribution/create_tarball.py b/distribution/create_tarball.py index ec4b6bf..14923da 100644 --- a/distribution/create_tarball.py +++ b/distribution/create_tarball.py @@ -41,7 +41,7 @@ def create_cache_folder(spec_info, src_dir): def copy_files(src_dir, dst_dir): for dirpath, _, files in os.walk(src_dir): relative_path = os.path.relpath(dirpath, src_dir) - target_path = os.path.join(dst_dir, relative_path) + target_path = os.path.join(dst_dir, relative_path.strip(f'{os.curdir}{os.sep}')) if not os.path.exists(target_path): os.makedirs(target_path) @@ -63,7 +63,7 @@ def delete_cache_folder(folder_path): if __name__ == "__main__": - SPEC_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "eulercopilot.spec")) + SPEC_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "eulercopilot-cli.spec")) SRC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) info = extract_spec_fields(SPEC_FILE) diff --git a/distribution/eulercopilot.spec b/distribution/eulercopilot-cli.spec similarity index 91% rename from distribution/eulercopilot.spec rename to distribution/eulercopilot-cli.spec index 41f65c3..5e88e27 100644 --- a/distribution/eulercopilot.spec +++ b/distribution/eulercopilot-cli.spec @@ -1,8 +1,10 @@ +%global debug_package %{nil} + Name: eulercopilot-cli Version: 1.2 -Release: 1%{?_tag}%{?dist} +Release: 2%{?_tag}%{?dist} Group: Applications/Utilities -Summary: EulerCopilot Command Line Assistant +Summary: openEuler Copilot System Command Line Assistant Source: %{name}-%{version}.tar.gz License: MulanPSL-2.0 URL: https://www.openeuler.org/zh/ @@ -14,7 +16,7 @@ BuildRequires: python3-Cython gcc Requires: python3 jq hostname %description -EulerCopilot Command Line Assistant +openEuler Copilot System Command Line Assistant %prep %setup -q diff --git a/src/copilot/app/copilot_app.py b/src/copilot/app/copilot_app.py index 519e7a2..3d4592e 100644 --- a/src/copilot/app/copilot_app.py +++ b/src/copilot/app/copilot_app.py @@ -97,6 +97,8 @@ def command_interaction_loop(cmds: list, service: llm_service.LLMService) -> int if action in ('execute_all', 'execute_selected', 'execute'): exit_code: int = 0 selected_cmds = get_selected_cmds(cmds, action) + if not selected_cmds: + return -1 for cmd in selected_cmds: exit_code = execute_shell_command(cmd) if exit_code != 0: @@ -156,7 +158,7 @@ def handle_user_input(service: llm_service.LLMService, if mode == 'tuning': cmds = service.tuning(user_input) if cmds: - return command_interaction_loop(list(dict.fromkeys(cmds)), service) + return command_interaction_loop(cmds, service) return -1 @@ -172,16 +174,15 @@ def edit_config(): return if selected_entry == 'backend': backend = interact.select_backend() - update_config(selected_entry, backend) + if selected_entry != 'cancel': + update_config(selected_entry, backend) elif selected_entry == 'query_mode': - mode = interact.select_query_mode() - update_config(selected_entry, mode) + backend = load_config().get('backend', '') + update_config(selected_entry, interact.select_query_mode(backend)) elif selected_entry in ('advanced_mode', 'debug_mode'): - update_config( - selected_entry, - interact.ask_boolean( - i18n.interact_question_yes_or_no.format( - question_body=CONFIG_ENTRY_NAME.get(selected_entry)))) + input_prompt = i18n.interact_question_yes_or_no.format( + question_body=CONFIG_ENTRY_NAME.get(selected_entry)) + update_config(selected_entry, interact.ask_boolean(input_prompt)) else: original_text: str = load_config().get(selected_entry, '') new_text = '' @@ -214,7 +215,7 @@ def main(user_input: Optional[str], config: dict) -> int: if mode == 'flow': # get plugin list from current backend plugins: list[framework_api.PluginData] = service.get_plugins() if not plugins: - print(i18n.main_service_framework_plugin_is_none) + print(f'\033[1;31m{i18n.main_service_framework_plugin_is_none}\033[0m') return 1 selected_plugins = [interact.select_one_plugin(plugins)] elif backend == 'spark': @@ -233,15 +234,15 @@ def main(user_input: Optional[str], config: dict) -> int: ) if service is None: - print(i18n.main_service_is_none) + print(f'\033[1;31m{i18n.main_service_is_none}\033[0m') return 1 - print(i18n.main_exit_prompt) + print(f'\033[33m{i18n.main_exit_prompt}\033[0m') try: while True: if user_input is None: - user_input = input('\033[35m>>>\033[0m ') + user_input = input('\033[35m❯\033[0m ') if user_input.lower().startswith('exit'): return 0 exit_code = handle_user_input(service, user_input, mode) @@ -249,5 +250,7 @@ def main(user_input: Optional[str], config: dict) -> int: return exit_code user_input = None # Reset user_input for next iteration (only if continuing service) except KeyboardInterrupt: + if isinstance(service, framework_api.Framework): + service.stop() print() return 0 diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index 8f0069f..5621eb3 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -10,10 +10,10 @@ import typer from copilot.app.copilot_app import edit_config, main from copilot.app.copilot_init import setup_copilot -from copilot.backends.framework_api import QUERY_MODE from copilot.utilities.config_manager import ( CONFIG_PATH, DEFAULT_CONFIG, + QUERY_MODE_NAME, load_config, select_backend, select_query_mode, @@ -55,24 +55,24 @@ def cli( help=cli_help_prompt_question), chat: bool = typer.Option( False, '--chat', '-c', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["chat"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE_NAME["chat"]), rich_help_panel=cli_help_panel_switch_mode ), flow: bool = typer.Option( - False, '--flow', '-f', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["flow"]), + False, '--plugin', '-p', + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE_NAME["flow"]), rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework'), ), diagnose: bool = typer.Option( False, '--diagnose', '-d', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["diagnose"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE_NAME["diagnose"]), rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework') ), tuning: bool = typer.Option( False, '--tuning', '-t', - help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE["tuning"]), + help=cli_help_prompt_switch_mode.format(mode=QUERY_MODE_NAME["tuning"]), rich_help_panel=cli_help_panel_switch_mode, hidden=(BACKEND != 'framework') ), @@ -94,12 +94,12 @@ def cli( hidden=(not ADVANCED_MODE) ) ) -> int: - '''EulerCopilot CLI''' + '''openEuler Copilot System CLI''' if init: setup_copilot() return 0 if not CONFIG_INITIALIZED: - print(cli_notif_no_config) + print(f'\033[1;31m{cli_notif_no_config}\033[0m') return 1 if backend: if ADVANCED_MODE: @@ -111,7 +111,7 @@ def cli( return 0 if sum(map(bool, [chat, flow, diagnose, tuning])) > 1: - print(cli_notif_select_one_mode) + print(f'\033[1;31m{cli_notif_select_one_mode}\033[0m') return 1 if chat: @@ -124,7 +124,7 @@ def cli( if not question: return 0 else: - compatibility_notification(QUERY_MODE['flow']) + compatibility_notification(QUERY_MODE_NAME['flow']) return 1 elif diagnose: if BACKEND == 'framework': @@ -132,7 +132,7 @@ def cli( if not question: return 0 else: - compatibility_notification(QUERY_MODE['diagnose']) + compatibility_notification(QUERY_MODE_NAME['diagnose']) return 1 elif tuning: if BACKEND == 'framework': @@ -140,7 +140,7 @@ def cli( if not question: return 0 else: - compatibility_notification(QUERY_MODE['tuning']) + compatibility_notification(QUERY_MODE_NAME['tuning']) return 1 if question: @@ -150,7 +150,8 @@ def cli( def compatibility_notification(mode: str): - print(cli_notif_compatibility.format(mode=mode, brand_name=BRAND_NAME)) + print('\033[33m', cli_notif_compatibility.format(mode=mode, brand_name=BRAND_NAME), + '\033[0m', sep='') def entry_point() -> int: diff --git a/src/copilot/app/copilot_init.py b/src/copilot/app/copilot_init.py index 7922415..b5566d9 100644 --- a/src/copilot/app/copilot_init.py +++ b/src/copilot/app/copilot_init.py @@ -5,7 +5,9 @@ import os import readline # noqa: F401 -from copilot.utilities import config_manager +from rich import print as rprint + +from copilot.utilities import config_manager, i18n def setup_copilot(): @@ -15,9 +17,10 @@ def setup_copilot(): if not os.path.exists(config_manager.CONFIG_PATH): config_manager.init_config() - def _prompt_for_config(config_key: str, prompt_text: str): + def _prompt_for_config(config_key: str, prompt_text: str) -> str: config_value = input(prompt_text) config_manager.update_config(config_key, config_value) + return config_value if not os.path.exists(config_manager.CONFIG_PATH): _init_config() @@ -25,20 +28,32 @@ def setup_copilot(): config = config_manager.load_config() if config.get('backend') == 'spark': if config.get('spark_app_id') == '': - _prompt_for_config('spark_app_id', '请输入你的星火大模型 App ID:') + _prompt_for_config('spark_app_id', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_spark_app_id)) if config.get('spark_api_key') == '': - _prompt_for_config('spark_api_key', '请输入你的星火大模型 API Key:') + _prompt_for_config('spark_api_key', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_spark_api_key)) if config.get('spark_api_secret') == '': - _prompt_for_config('spark_api_secret', '请输入你的星火大模型 App Secret:') + _prompt_for_config('spark_api_secret', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_spark_api_secret)) if config.get('backend') == 'framework': - if config.get('framework_url') == '': - _prompt_for_config('framework_url', '请输入 EulerCopilot 智能体 URL:') + framework_url = config.get('framework_url') + if framework_url == '': + framework_url = _prompt_for_config('framework_url', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_framework_url)) if config.get('framework_api_key') == '': - _prompt_for_config('framework_api_key', '请输入 EulerCopilot 智能体 API Key:') + title = i18n.settings_init_framework_api_key_notice_title.format(brand_name=i18n.BRAND_NAME) + rprint(f'[bold]{title}[/bold]') + rprint(i18n.settings_init_framework_api_key_notice_content.format(url=framework_url)) + _prompt_for_config('framework_api_key', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_framework_api_key.format(brand_name=i18n.BRAND_NAME))) if config.get('backend') == 'openai': if config.get('model_url') == '': - _prompt_for_config('model_url', '请输入你的大模型 URL:') + _prompt_for_config('model_url', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_model_url)) if config.get('model_api_key') == '': - _prompt_for_config('model_api_key', '请输入你的大模型 API Key:') + _prompt_for_config('model_api_key', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_model_api_key)) if config.get('model_name') == '': - _prompt_for_config('model_name', '请输入你的大模型名称:') + _prompt_for_config('model_name', i18n.interact_question_input_text.format( + question_body=i18n.settings_config_entry_model_name)) diff --git a/src/copilot/backends/framework_api.py b/src/copilot/backends/framework_api.py index 1c199d4..c3210e6 100644 --- a/src/copilot/backends/framework_api.py +++ b/src/copilot/backends/framework_api.py @@ -25,6 +25,7 @@ from copilot.utilities.i18n import ( backend_framework_response_ended_prematurely, backend_framework_stream_error, backend_framework_stream_sensitive, + backend_framework_stream_stop, backend_framework_stream_unknown, backend_framework_sugggestion, backend_general_request_failed, @@ -32,21 +33,10 @@ from copilot.utilities.i18n import ( prompt_framework_keyword_install, prompt_framework_markdown_format, prompt_framework_plugin_ip, - query_mode_chat, - query_mode_diagnose, - query_mode_flow, - query_mode_tuning, ) from copilot.utilities.markdown_renderer import MarkdownRenderer from copilot.utilities.shell_script import write_shell_script -QUERY_MODE = { - 'chat': query_mode_chat, - 'flow': query_mode_flow, - 'diagnose': query_mode_diagnose, - 'tuning': query_mode_tuning, -} - FRAMEWORK_LLM_STREAM_BAD_REQUEST_MSG = { 401: backend_framework_request_unauthorized, 429: backend_framework_request_too_many_requests @@ -71,7 +61,7 @@ class Framework(LLMService): self.console = Console() def get_shell_commands(self, question: str) -> list: - query = self._add_framework_extra_prompt(self._gen_chat_prompt(question)) + query = self._add_framework_extra_prompt(question) if prompt_framework_keyword_install in question.lower(): query = self._add_framework_software_install_prompt(query) self._query_llm_service(query) @@ -86,8 +76,9 @@ class Framework(LLMService): def update_session_id(self): headers = self._get_headers() try: - response = requests.get( + response = requests.post( urljoin(self.endpoint, 'api/client/session'), + json={'session_id': self.session_id} if self.session_id else {}, headers=headers, timeout=30 ) @@ -157,6 +148,8 @@ class Framework(LLMService): if local_ip: question = f'{prompt_framework_plugin_ip} {local_ip},' + question self._query_llm_service(question, user_selected_plugins=['euler-copilot-rca']) + if self.commands: + return self.commands return self._extract_shell_code_blocks(self.content) def tuning(self, question: str) -> list: @@ -166,8 +159,23 @@ class Framework(LLMService): if local_ip: question = f'{prompt_framework_plugin_ip} {local_ip},' + question self._query_llm_service(question, user_selected_plugins=['euler-copilot-tune']) + if self.commands: + return self.commands return self._extract_shell_code_blocks(self.content) + def stop(self): + headers = self._get_headers() + try: + response = requests.post( + urljoin(self.endpoint, 'api/client/stop'), + headers=headers, + timeout=30 + ) + except requests.exceptions.RequestException: + return + if response.status_code == 200: + self.console.print(backend_framework_stream_stop.format(brand_name=BRAND_NAME)) + # pylint: disable=W0221 def _query_llm_service( self, @@ -176,10 +184,13 @@ class Framework(LLMService): show_suggestion: bool = True ): if not user_selected_plugins: - user_selected_plugins = ['auto'] + user_selected_plugins = [] headers = self._get_headers() + self.update_session_id() data = { - 'question': question, + 'session_id': self.session_id, + 'question': question, + 'language': 'zh', 'conversation_id': self.conversation_id, 'user_selected_plugins': user_selected_plugins } @@ -282,16 +293,17 @@ class Framework(LLMService): # 获取插件返回数据 plugin_tool_type = jcontent.get('type', '') if plugin_tool_type == 'extract': - data_str = jcontent.get('data', '') - if data_str: - try: - data = json.loads(data_str) - except json.JSONDecodeError: - return + data = jcontent.get('data', '') + if data: + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError: + return # 返回 Markdown 报告 output = data.get('output', '') if output: - self.content += output + self.content = output # 返回单行 Shell 命令 cmd = data.get('shell', '') if cmd: diff --git a/src/copilot/backends/llm_service.py b/src/copilot/backends/llm_service.py index 5af2b20..acd4459 100644 --- a/src/copilot/backends/llm_service.py +++ b/src/copilot/backends/llm_service.py @@ -29,7 +29,8 @@ class LLMService(ABC): def _extract_shell_code_blocks(self, markdown_text) -> list: pattern = r'```(bash|sh|shell)\n(.*?)(?=\n\s*```)' bash_blocks = re.findall(pattern, markdown_text, re.DOTALL | re.MULTILINE) - return '\n'.join([block[1].strip() for block in bash_blocks]).splitlines() + cmds = list(dict.fromkeys('\n'.join([block[1].strip() for block in bash_blocks]).splitlines())) + return [cmd for cmd in cmds if cmd and not cmd.startswith('#')] # remove comments and empty lines def _get_context_length(self, context: list) -> int: length = 0 diff --git a/src/copilot/backends/spark_api.py b/src/copilot/backends/spark_api.py index ca4db94..7f5282c 100644 --- a/src/copilot/backends/spark_api.py +++ b/src/copilot/backends/spark_api.py @@ -93,7 +93,7 @@ class Spark(LLMService): except websockets.exceptions.InvalidStatusCode: live.update( - Text.from_ansi(backend_spark_websockets_exceptions_msg_title)\ + Text.from_ansi(f'\033[1;31m{backend_spark_websockets_exceptions_msg_title}\033[0m\n\n')\ .append(backend_spark_websockets_exceptions_msg_a)\ .append(backend_spark_websockets_exceptions_msg_b)\ .append(backend_spark_websockets_exceptions_msg_c.format(spark_url=self.spark_url)), diff --git a/src/copilot/utilities/config_manager.py b/src/copilot/utilities/config_manager.py index 05d669b..0dac12a 100644 --- a/src/copilot/utilities/config_manager.py +++ b/src/copilot/utilities/config_manager.py @@ -3,7 +3,6 @@ import json import os -from copilot.backends.framework_api import QUERY_MODE from copilot.utilities import i18n, interact CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config/eulercopilot') @@ -26,6 +25,19 @@ CONFIG_ENTRY_NAME = { 'model_name': i18n.settings_config_entry_model_name } +BACKEND_NAME = { + 'framework': i18n.interact_backend_framework.format(brand_name=i18n.BRAND_NAME), + 'spark': i18n.interact_backend_spark, + 'openai': i18n.interact_backend_openai +} + +QUERY_MODE_NAME = { + 'chat': i18n.query_mode_chat, + 'flow': i18n.query_mode_flow, + 'diagnose': i18n.query_mode_diagnose, + 'tuning': i18n.query_mode_tuning, +} + DEFAULT_CONFIG = { 'backend': 'framework', 'query_mode': 'chat', @@ -36,7 +48,7 @@ DEFAULT_CONFIG = { 'spark_api_secret': '', 'spark_url': 'wss://spark-api.xf-yun.com/v3.5/chat', 'spark_domain': 'generalv3.5', - 'framework_url': '', + 'framework_url': 'https://eulercopilot.gitee.com', 'framework_api_key': '', 'model_url': '', 'model_api_key': '', @@ -75,7 +87,7 @@ def update_config(key: str, value): def select_query_mode(mode: int): - modes = list(QUERY_MODE.keys()) + modes = list(QUERY_MODE_NAME.keys()) if mode < len(modes): update_config('query_mode', modes[mode]) @@ -89,9 +101,18 @@ def select_backend(): def config_to_markdown() -> str: config = load_config() config_table = '\n'.join([ - f'| {CONFIG_ENTRY_NAME.get(key)} | {value} |' for key, value in config.items() + f'| {CONFIG_ENTRY_NAME.get(key)} | {__get_config_item_display_name(key, value)} |' + for key, value in config.items() ]) - return f'### {i18n.settings_markdown_title}\n\n\ - | {i18n.settings_markdown_header_key} \ - | {i18n.settings_markdown_header_value} |\n\ - | ----------- | ----------- |\n{config_table}' + return f'# {i18n.settings_markdown_title}\n\ +| {i18n.settings_markdown_header_key} \ +| {i18n.settings_markdown_header_value} |\n\ +| ----------- | ----------- |\n{config_table}' + + +def __get_config_item_display_name(key, value): + if key == 'backend': + return BACKEND_NAME.get(value, value) + if key == 'query_mode': + return QUERY_MODE_NAME.get(value, value) + return value diff --git a/src/copilot/utilities/i18n.py b/src/copilot/utilities/i18n.py index 1974f05..e4b6a05 100644 --- a/src/copilot/utilities/i18n.py +++ b/src/copilot/utilities/i18n.py @@ -2,11 +2,11 @@ from gettext import gettext as _ -BRAND_NAME = 'EulerCopilot' +BRAND_NAME = 'openEuler Copilot System' -main_exit_prompt = _('\033[33m输入 "exit" 或按下 Ctrl+C 结束对话\033[0m') -main_service_is_none = _('\033[1;31m未正确配置 LLM 后端,请检查配置文件\033[0m') -main_service_framework_plugin_is_none = _('\033[1;31m获取插件失败或插件列表为空\n请联系管理员检查后端配置\033[0m') +main_exit_prompt = _('输入 "exit" 或按下 Ctrl+C 结束对话') +main_service_is_none = _('未正确配置 LLM 后端,请检查配置文件') +main_service_framework_plugin_is_none = _('获取插件失败或插件列表为空\n请联系管理员检查后端配置') main_exec_builtin_cmd = _('不支持执行 Shell 内置命令 "{cmd_prefix}",请复制后手动执行') main_exec_value_error = _('执行命令时出错:{error}') main_exec_not_found_error = _('命令不存在:{error}') @@ -19,11 +19,11 @@ cli_help_prompt_edit_settings = _('编辑 copilot 设置') cli_help_prompt_select_backend = _('选择大语言模型后端') cli_help_panel_switch_mode = _('选择问答模式') cli_help_panel_advanced_options = _('高级选项') -cli_notif_select_one_mode = _('\033[1;31m当前版本只能选择一种问答模式\033[0m') -cli_notif_compatibility = _('\033[33m当前大模型后端不支持{mode}功能\033[0m\n\ - \033[33m推荐使用 {brand_name} 智能体框架\033[0m') -cli_notif_no_config = _('\033[1;31m请先初始化 copilot 设置\033[0m\n\ - \033[33m请使用 "copilot --init" 命令初始化\033[0m') +cli_notif_select_one_mode = _('当前版本只能选择一种问答模式') +cli_notif_compatibility = _('当前大模型后端不支持{mode}功能\n\ +推荐使用 {brand_name} 智能体框架') +cli_notif_no_config = _('请先初始化 copilot 设置\n\ +请使用 "copilot --init" 命令初始化') interact_action_explain = _('解释命令') interact_action_edit = _('编辑命令') @@ -58,9 +58,10 @@ backend_framework_response_ended_prematurely = _('响应异常中止,请检查 backend_framework_stream_error = _('{brand_name} 智能体遇到错误,请联系管理员定位问题') backend_framework_stream_unknown = _('{brand_name} 智能体返回了未知内容:\n```json\n{content}\n```') backend_framework_stream_sensitive = _('检测到违规信息,请重新提问') +backend_framework_stream_stop = _('{brand_name} 智能体已停止生成内容') backend_framework_sugggestion = _('**你可以继续问** {sugggestion}') backend_spark_stream_error = _('请求错误: {code}\n{message}') -backend_spark_websockets_exceptions_msg_title = _('\033[1;31m请求错误\033[0m\n\n') +backend_spark_websockets_exceptions_msg_title = _('请求错误') backend_spark_websockets_exceptions_msg_a = _('请检查 appid 和 api_key 是否正确,或检查网络连接是否正常。\n') backend_spark_websockets_exceptions_msg_b = _('输入 "vi ~/.config/eulercopilot/config.json" 查看和编辑配置;\n') backend_spark_websockets_exceptions_msg_c = _('或尝试 ping {spark_url}') @@ -86,9 +87,12 @@ settings_config_entry_framework_api_key = _('{brand_name} 智能体 API Key') settings_config_entry_model_url = _('OpenAI 模型 URL') settings_config_entry_model_api_key = _('OpenAI 模型 API Key') settings_config_entry_model_name = _('OpenAI 模型名称') +settings_config_interact_query_mode_disabled_explain = _('当前后端无法使用{mode}模式') +settings_init_framework_api_key_notice_title = _('获取 {brand_name} 智能体 API Key') +settings_init_framework_api_key_notice_content = _('请前往 {url},点击右上角头像图标获取 API Key') query_mode_chat = _('智能问答') -query_mode_flow = _('智能工作流') +query_mode_flow = _('智能插件') query_mode_diagnose = _('智能诊断') query_mode_tuning = _('智能调优') diff --git a/src/copilot/utilities/interact.py b/src/copilot/utilities/interact.py index 8009c89..705af4e 100644 --- a/src/copilot/utilities/interact.py +++ b/src/copilot/utilities/interact.py @@ -1,8 +1,10 @@ # Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved. +from typing import Optional + import questionary -from copilot.backends.framework_api import QUERY_MODE, PluginData +from copilot.backends.framework_api import PluginData from copilot.utilities import config_manager, i18n ACTIONS_SINGLE_CMD = [ @@ -160,10 +162,10 @@ def select_settings_entry() -> str: ).ask() -def select_query_mode() -> str: +def select_query_mode(backend: str) -> str: return questionary.select( i18n.interact_question_select_query_mode, - choices=__get_query_mode_choices(), + choices=__get_query_mode_choices(backend), qmark='❯', style=CUSTOM_STYLE_FANCY, ).ask() @@ -188,5 +190,18 @@ def __get_settings_entry_choices() -> list: return choices -def __get_query_mode_choices() -> list: - return [questionary.Choice(name, item) for item, name in QUERY_MODE.items()] +def __get_query_mode_choices(backend: str) -> list: + def __disabled(name: str, item: str) -> Optional[str]: + return ( + i18n.settings_config_interact_query_mode_disabled_explain.format(mode=name) + if backend != 'framework' and item != 'chat' + else None + ) + + return [ + questionary.Choice( + name, + item, + disabled=__disabled(name, item) + ) for item, name in config_manager.QUERY_MODE_NAME.items() + ] diff --git a/src/eulercopilot.sh b/src/eulercopilot.sh index 8249136..4f10f29 100644 --- a/src/eulercopilot.sh +++ b/src/eulercopilot.sh @@ -11,7 +11,7 @@ read_query_mode() { if [ "$query_mode" = "\"chat\"" ]; then echo "智能问答" elif [ "$query_mode" = "\"flow\"" ]; then - echo "智能工作流" + echo "智能插件" elif [ "$query_mode" = "\"diagnose\"" ]; then echo "智能诊断" elif [ "$query_mode" = "\"tuning\"" ]; then @@ -90,10 +90,10 @@ run_copilot() { READLINE_LINE="" if [[ "$PS1" == *"\[\033[1;33m"* ]]; then revert_prompt - printf "\033[1;31m已关闭 EulerCopilot 提示符\033[0m\n" + printf "\033[1;31m已关闭 openEuler Copilot System 提示符\033[0m\n" else set_prompt - printf "\033[1;32m已开启 EulerCopilot 提示符\033[0m\n" + printf "\033[1;32m已开启 openEuler Copilot System 提示符\033[0m\n" fi fi } diff --git a/src/setup.py b/src/setup.py index 61f1d80..54e7c32 100644 --- a/src/setup.py +++ b/src/setup.py @@ -35,7 +35,7 @@ extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] setup( name='copilot', version='1.2', - description='EulerCopilot CLI Tool', + description='openEuler Copilot System CLI Tool', author='Hongyu Shi', author_email='shihongyu15@huawei.com', url='https://gitee.com/openeuler-customization/euler-copilot-shell', -- Gitee From fa6a5589b31a5e8dd2e6ffc5cc63e6d8d933bb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E9=B8=BF=E5=AE=87?= Date: Thu, 24 Oct 2024 15:21:26 +0800 Subject: [PATCH 32/32] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 史鸿宇 --- distribution/build_rpm.sh | 3 ++- distribution/eulercopilot-cli.spec | 4 ++-- src/copilot/app/copilot_cli.py | 2 +- src/copilot/app/copilot_init.py | 5 +++++ src/copilot/utilities/i18n.py | 5 +++++ src/setup.py | 6 +++--- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/distribution/build_rpm.sh b/distribution/build_rpm.sh index acd7106..4fe2ddc 100644 --- a/distribution/build_rpm.sh +++ b/distribution/build_rpm.sh @@ -24,6 +24,7 @@ fi rm -f ~/rpmbuild/RPMS/"$(uname -m)"/eulercopilot-cli-* # Build the RPM package using rpmbuild -rpmbuild --define "_tag .a$(date +%s)" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo +rpmbuild --define "dist .oe2403" -bb "$spec_file" --nodebuginfo +# rpmbuild --define "_tag .a$(date +%s)" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo # rpmbuild --define "_tag .beta3" --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo # rpmbuild --define "dist .oe2203sp3" -bb "$spec_file" --nodebuginfo diff --git a/distribution/eulercopilot-cli.spec b/distribution/eulercopilot-cli.spec index 5e88e27..b8ee515 100644 --- a/distribution/eulercopilot-cli.spec +++ b/distribution/eulercopilot-cli.spec @@ -1,8 +1,8 @@ %global debug_package %{nil} Name: eulercopilot-cli -Version: 1.2 -Release: 2%{?_tag}%{?dist} +Version: 1.2.1 +Release: 4%{?_tag}%{?dist} Group: Applications/Utilities Summary: openEuler Copilot System Command Line Assistant Source: %{name}-%{version}.tar.gz diff --git a/src/copilot/app/copilot_cli.py b/src/copilot/app/copilot_cli.py index 5621eb3..caac80d 100644 --- a/src/copilot/app/copilot_cli.py +++ b/src/copilot/app/copilot_cli.py @@ -94,7 +94,7 @@ def cli( hidden=(not ADVANCED_MODE) ) ) -> int: - '''openEuler Copilot System CLI''' + '''openEuler Copilot System CLI\n\nPress Ctrl+O to ask a question''' if init: setup_copilot() return 0 diff --git a/src/copilot/app/copilot_init.py b/src/copilot/app/copilot_init.py index b5566d9..655e8bf 100644 --- a/src/copilot/app/copilot_init.py +++ b/src/copilot/app/copilot_init.py @@ -25,6 +25,11 @@ def setup_copilot(): if not os.path.exists(config_manager.CONFIG_PATH): _init_config() + rprint(f'\n[bold]{i18n.settings_init_welcome_msg.format(brand_name=i18n.BRAND_NAME)}[/bold]\n') + rprint(i18n.settings_init_welcome_usage_guide + '\n') + rprint(i18n.settings_init_welcome_help_hint) + rprint(i18n.settings_init_welcome_docs_link.format(url=i18n.DOCS_URL) + '\n') + config = config_manager.load_config() if config.get('backend') == 'spark': if config.get('spark_app_id') == '': diff --git a/src/copilot/utilities/i18n.py b/src/copilot/utilities/i18n.py index e4b6a05..4907840 100644 --- a/src/copilot/utilities/i18n.py +++ b/src/copilot/utilities/i18n.py @@ -3,6 +3,7 @@ from gettext import gettext as _ BRAND_NAME = 'openEuler Copilot System' +DOCS_URL = _('https://gitee.com/openeuler/euler-copilot-framework/blob/master/docs/user-guide/README.md') main_exit_prompt = _('输入 "exit" 或按下 Ctrl+C 结束对话') main_service_is_none = _('未正确配置 LLM 后端,请检查配置文件') @@ -88,6 +89,10 @@ settings_config_entry_model_url = _('OpenAI 模型 URL') settings_config_entry_model_api_key = _('OpenAI 模型 API Key') settings_config_entry_model_name = _('OpenAI 模型名称') settings_config_interact_query_mode_disabled_explain = _('当前后端无法使用{mode}模式') +settings_init_welcome_msg = _('欢迎使用 {brand_name} 智能体') +settings_init_welcome_usage_guide = _('使用方法:输入问题,按下 Ctrl+O 提问') +settings_init_welcome_help_hint = _('更多用法详见命令行帮助:"copilot --help"') +settings_init_welcome_docs_link = _('使用指南:{url}') settings_init_framework_api_key_notice_title = _('获取 {brand_name} 智能体 API Key') settings_init_framework_api_key_notice_content = _('请前往 {url},点击右上角头像图标获取 API Key') diff --git a/src/setup.py b/src/setup.py index 54e7c32..ab26b8a 100644 --- a/src/setup.py +++ b/src/setup.py @@ -34,11 +34,11 @@ extensions = [Extension(f.replace("/", ".")[:-3], [f]) for f in cython_files] # 定义 setup() 参数 setup( name='copilot', - version='1.2', - description='openEuler Copilot System CLI Tool', + version='1.2.1', + description='openEuler Copilot System Command Line Assistant', author='Hongyu Shi', author_email='shihongyu15@huawei.com', - url='https://gitee.com/openeuler-customization/euler-copilot-shell', + url='https://gitee.com/openeuler/euler-copilot-shell', py_modules=['copilot.__init__', 'copilot.__main__'], ext_modules=cythonize( extensions, -- Gitee