From 4b3286f5460e8a106c5558e3f0ae4be724d0c153 Mon Sep 17 00:00:00 2001 From: Mimikko-zeus Date: Wed, 7 Jan 2026 00:17:46 +0800 Subject: [PATCH] Initial commit --- .env | 4 + .env.example | 4 + PRD.md | 233 ++++++++ __pycache__/main.cpython-313.pyc | Bin 0 -> 21809 bytes debug_env.py | 25 + executor/__init__.py | 2 + executor/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 133 bytes executor/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 137 bytes .../sandbox_runner.cpython-310.pyc | Bin 0 -> 5394 bytes .../sandbox_runner.cpython-313.pyc | Bin 0 -> 7972 bytes executor/sandbox_runner.py | 240 ++++++++ intent/__init__.py | 2 + intent/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 131 bytes intent/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 135 bytes intent/__pycache__/classifier.cpython-310.pyc | Bin 0 -> 3998 bytes intent/__pycache__/classifier.cpython-313.pyc | Bin 0 -> 5373 bytes intent/__pycache__/labels.cpython-310.pyc | Bin 0 -> 281 bytes intent/__pycache__/labels.cpython-313.pyc | Bin 0 -> 314 bytes intent/classifier.py | 152 +++++ intent/labels.py | 15 + llm/__init__.py | 2 + llm/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 128 bytes llm/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 132 bytes llm/__pycache__/client.cpython-310.pyc | Bin 0 -> 3473 bytes llm/__pycache__/client.cpython-313.pyc | Bin 0 -> 4801 bytes llm/__pycache__/prompts.cpython-310.pyc | Bin 0 -> 2920 bytes llm/__pycache__/prompts.cpython-313.pyc | Bin 0 -> 2979 bytes llm/client.py | 124 +++++ llm/prompts.py | 130 +++++ main.py | 518 +++++++++++++++++ requirements.txt | 9 + safety/__init__.py | 2 + safety/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 131 bytes safety/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 135 bytes .../__pycache__/llm_reviewer.cpython-310.pyc | Bin 0 -> 3456 bytes .../__pycache__/llm_reviewer.cpython-313.pyc | Bin 0 -> 4670 bytes .../__pycache__/rule_checker.cpython-310.pyc | Bin 0 -> 4637 bytes .../__pycache__/rule_checker.cpython-313.pyc | Bin 0 -> 7908 bytes safety/llm_reviewer.py | 132 +++++ safety/rule_checker.py | 208 +++++++ ui/__init__.py | 2 + ui/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 127 bytes ui/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 131 bytes ui/__pycache__/chat_view.cpython-310.pyc | Bin 0 -> 4434 bytes ui/__pycache__/chat_view.cpython-313.pyc | Bin 0 -> 7224 bytes .../task_guide_view.cpython-310.pyc | Bin 0 -> 7811 bytes .../task_guide_view.cpython-313.pyc | Bin 0 -> 22485 bytes ui/chat_view.py | 164 ++++++ ui/task_guide_view.py | 524 ++++++++++++++++++ 49 files changed, 2492 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 PRD.md create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 debug_env.py create mode 100644 executor/__init__.py create mode 100644 executor/__pycache__/__init__.cpython-310.pyc create mode 100644 executor/__pycache__/__init__.cpython-313.pyc create mode 100644 executor/__pycache__/sandbox_runner.cpython-310.pyc create mode 100644 executor/__pycache__/sandbox_runner.cpython-313.pyc create mode 100644 executor/sandbox_runner.py create mode 100644 intent/__init__.py create mode 100644 intent/__pycache__/__init__.cpython-310.pyc create mode 100644 intent/__pycache__/__init__.cpython-313.pyc create mode 100644 intent/__pycache__/classifier.cpython-310.pyc create mode 100644 intent/__pycache__/classifier.cpython-313.pyc create mode 100644 intent/__pycache__/labels.cpython-310.pyc create mode 100644 intent/__pycache__/labels.cpython-313.pyc create mode 100644 intent/classifier.py create mode 100644 intent/labels.py create mode 100644 llm/__init__.py create mode 100644 llm/__pycache__/__init__.cpython-310.pyc create mode 100644 llm/__pycache__/__init__.cpython-313.pyc create mode 100644 llm/__pycache__/client.cpython-310.pyc create mode 100644 llm/__pycache__/client.cpython-313.pyc create mode 100644 llm/__pycache__/prompts.cpython-310.pyc create mode 100644 llm/__pycache__/prompts.cpython-313.pyc create mode 100644 llm/client.py create mode 100644 llm/prompts.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 safety/__init__.py create mode 100644 safety/__pycache__/__init__.cpython-310.pyc create mode 100644 safety/__pycache__/__init__.cpython-313.pyc create mode 100644 safety/__pycache__/llm_reviewer.cpython-310.pyc create mode 100644 safety/__pycache__/llm_reviewer.cpython-313.pyc create mode 100644 safety/__pycache__/rule_checker.cpython-310.pyc create mode 100644 safety/__pycache__/rule_checker.cpython-313.pyc create mode 100644 safety/llm_reviewer.py create mode 100644 safety/rule_checker.py create mode 100644 ui/__init__.py create mode 100644 ui/__pycache__/__init__.cpython-310.pyc create mode 100644 ui/__pycache__/__init__.cpython-313.pyc create mode 100644 ui/__pycache__/chat_view.cpython-310.pyc create mode 100644 ui/__pycache__/chat_view.cpython-313.pyc create mode 100644 ui/__pycache__/task_guide_view.cpython-310.pyc create mode 100644 ui/__pycache__/task_guide_view.cpython-313.pyc create mode 100644 ui/chat_view.py create mode 100644 ui/task_guide_view.py diff --git a/.env b/.env new file mode 100644 index 0000000..1fbe5b8 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions +LLM_API_KEY=sk-fxsxbgatrjjhsnjpkdfgfngukqoqqgitjpxfqfxifcipaqpc +INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct +GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7b4d2ff --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions +LLM_API_KEY=sk-fxsxbgatrjjhsnjpkdfgfngukqoqqgitjpxfqfxifcipaqpc +INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct +GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct \ No newline at end of file diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..389e067 --- /dev/null +++ b/PRD.md @@ -0,0 +1,233 @@ +==================== +【产品目标】 +==================== +- 面向 Windows 小白用户:一句话输入 +- 自动判断任务类型: + - chat:普通对话(如“今天天气怎么样”) + - execution:本地执行任务(文件处理) +- chat: + - 直接调用 LLM 返回文本 +- execution: + - 生成执行计划 + - 生成 Python 代码 + - 安全校验 + - 用户确认 + - 一次性子进程执行 +- 强制工作区副本目录: + workspace/input + workspace/output + workspace/logs +- MVP 明确不做: + - 联网任务(搜索 / 爬取) + - 鼠标 / 键盘自动化 + - 后台常驻 + - 多任务并行 +- 核心安全原则: + - LLM 可以联网“思考” + - Executor(执行器)禁止联网“动手” + +==================== +【项目结构(必须严格按此生成)】 +==================== +LocalAgent/ + main.py + requirements.txt + .env.example + ui/ + chat_view.py + task_guide_view.py + llm/ + client.py + prompts.py + intent/ + classifier.py + labels.py + safety/ + rule_checker.py + llm_reviewer.py + executor/ + sandbox_runner.py + workspace/ + input/ + output/ + logs/ + +==================== +【统一 LLM 调用规则(非常重要)】 +==================== +- 所有模型(包括 qwen2.5:7b-instruct)都通过同一个 API: + https://api.siliconflow.cn/v1/chat/completions +- 不区分“本地 / 云端”客户端 +- 区分只体现在: + - model name + - prompt + - temperature / max_tokens + +.env.example 中必须包含: + +LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions +LLM_API_KEY=your_api_key_here + +# 用于意图识别的小模型 +INTENT_MODEL_NAME=qwen2.5:7b-instruct + +# 用于对话 / 计划 / 代码生成的模型(可先用同一个) +GENERATION_MODEL_NAME=Pro/zai-org/GLM-4.7 + +==================== +【llm/client.py 要求】 +==================== +实现统一的 LLMClient: + +- 使用 requests.post +- URL / API KEY 从 .env 读取 +- 提供方法: + chat( + messages: list[dict], + model: str, + temperature: float, + max_tokens: int + ) -> str + +- payload 结构参考: +{ + "model": model, + "messages": messages, + "stream": false, + "temperature": temperature, + "max_tokens": max_tokens +} + +- headers: + Authorization: Bearer +- 对网络异常 / 非 200 状态码做明确异常抛出 +- 不要在 client 中写任何业务逻辑 + +==================== +【意图识别(核心修改点)】 +==================== +实现 intent/classifier.py: + +- 使用“小参数 LLM”(INTENT_MODEL_NAME,例如 qwen2.5:7b-instruct) +- 目标:二分类 + - chat + - execution +- 要求输出结构化结果: + { + "label": "chat" | "execution", + "confidence": 0.0 ~ 1.0, + "reason": "中文解释,说明为什么这是执行任务/对话任务" + } + +- Prompt 必须极短、强约束、可解析 +- Prompt 模板放在 llm/prompts.py +- 对 LLM 输出: + - 尝试解析 JSON + - 若解析失败 / 字段缺失 → 走兜底逻辑(判为 chat) + +intent/labels.py: +- 定义常量: + CHAT + EXECUTION +- 定义阈值: + EXECUTION_CONFIDENCE_THRESHOLD = 0.6 +- 低于阈值一律判定为 chat(宁可少执行,不可误执行) + +==================== +【Chat Task 流程】 +==================== +- 使用 GENERATION_MODEL_NAME +- messages = 用户原始输入 +- 返回文本直接展示 +- 不触碰本地、不产出文件 + +==================== +【Execution Task 流程】 +==================== +1) 生成执行计划 + - 可用 GENERATION_MODEL_NAME + - 输出中文、可读 + - 明确: + - 会做什么 + - 不会动原文件 + - 输入 / 输出目录 + - 可能失败的情况 + +2) 生成 Python 执行代码 + - MVP 先内置“安全示例代码”: + - 遍历 workspace/input + - 复制文件到 workspace/output + - 不依赖第三方库 + - 不修改原文件 + - 保存为 workspace/task_.py + +3) safety/rule_checker.py(硬规则) + - 静态扫描执行代码: + - 禁止 requests / socket / urllib + - 禁止访问非 workspace 路径 + - 禁止危险操作(os.remove, shutil.rmtree, subprocess 等) + - 若违反,直接 fail + +4) safety/llm_reviewer.py(软规则) + - 使用 GENERATION_MODEL_NAME + - 输入:用户需求 + 执行计划 + 代码 + - 输出:pass / fail + 中文原因 + +5) UI(小白引导式,方案 C) + - 显示: + - 判定原因 reason + - 三步引导: + 1) 把文件复制到 input + 2) 我来处理 + 3) 去 output 取 + - 执行计划 + - 风险提示 + - 【开始执行】按钮 + +6) executor/sandbox_runner.py + - 使用 subprocess 启动一次性 Python 子进程 + - 工作目录限定为 workspace + - 捕获 stdout / stderr + - 写入 workspace/logs/task_.log + - 执行完即退出 + - 执行器层不允许任何联网能力(由 rule_checker 保证) + +==================== +【UI(Tkinter)最小可跑要求】 +==================== +- main.py 启动 Tkinter 窗口 +- 顶部:输入框 + 发送按钮 +- 中部:输出区 +- 当识别为 execution: + - 切换或弹出 task_guide_view +- 执行完成后展示: + - success / partial / failed + - 成功 / 失败数量 + - 日志路径 + +==================== +【requirements.txt(最小集)】 +==================== +- python-dotenv +- requests + +==================== +【最小可跑验收标准】 +==================== +- 未配置 LLM_KEY 时给出明确错误提示 +- 输入“今天天气怎么样” → chat +- 输入“把这个文件夹里的图片复制一份” → execution +- execution 能生成 task_.py 并真正执行 +- output / logs 中有真实文件 + +==================== +【Plan 模式输出要求】 +==================== +1) 先输出整体实现计划(步骤、模块职责) +2) 列出所有文件及其责任说明 +3) 再按文件路径逐个输出代码内容 +4) 确保 main.py 可直接运行 +5) main.py 顶部注释说明: + - 如何配置 .env + - 如何运行 + - 如何测试(往 input 放文件) \ No newline at end of file diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a159e7909cd952c8a09c18e11587dd4f49bea26 GIT binary patch literal 21809 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 F{cpaxhQ$B? literal 0 HcmV?d00001 diff --git a/debug_env.py b/debug_env.py new file mode 100644 index 0000000..29d0eee --- /dev/null +++ b/debug_env.py @@ -0,0 +1,25 @@ +"""调试脚本""" +from pathlib import Path +from dotenv import load_dotenv +import os + +ENV_PATH = Path(__file__).parent / ".env" + +print(f"ENV_PATH: {ENV_PATH}") +print(f"ENV_PATH exists: {ENV_PATH.exists()}") + +# 读取文件内容 +if ENV_PATH.exists(): + print(f"File content:") + print(ENV_PATH.read_text(encoding='utf-8')) + +# 加载环境变量 +result = load_dotenv(ENV_PATH) +print(f"load_dotenv result: {result}") + +# 检查环境变量 +print(f"LLM_API_URL: {os.getenv('LLM_API_URL')}") +print(f"LLM_API_KEY: {os.getenv('LLM_API_KEY')}") +print(f"INTENT_MODEL_NAME: {os.getenv('INTENT_MODEL_NAME')}") +print(f"GENERATION_MODEL_NAME: {os.getenv('GENERATION_MODEL_NAME')}") + diff --git a/executor/__init__.py b/executor/__init__.py new file mode 100644 index 0000000..7e9d55e --- /dev/null +++ b/executor/__init__.py @@ -0,0 +1,2 @@ +# 执行器模块 + diff --git a/executor/__pycache__/__init__.cpython-310.pyc b/executor/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3aff9b0cb657426ce31a4b935a5aaa945d383b0 GIT binary patch literal 133 zcmd1j<>g`kf)$#vnW8}YF^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*hO1SKbAC!{ zag0xXa$=5SdTL%tOln1Ha%o9^QA~V%W?p7Ve7s&kRSGev>)V-N=h7@>^MEI`IohI9r^M!%H|MNB~6XOPq_4Ogoe=lqn^ z;uxR&) literal 0 HcmV?d00001 diff --git a/executor/__pycache__/sandbox_runner.cpython-310.pyc b/executor/__pycache__/sandbox_runner.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ce059626022a408659b6a93152ecf907578f7a3 GIT binary patch literal 5394 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 tuple[str, Path]: + """ + 保存任务代码到文件 + + Args: + code: Python 代码 + task_id: 任务 ID(可选,自动生成) + + Returns: + (task_id, code_path) + """ + if not task_id: + task_id = self._generate_task_id() + + code_path = self.workspace / f"task_{task_id}.py" + code_path.write_text(code, encoding='utf-8') + + return task_id, code_path + + def execute(self, code: str, task_id: Optional[str] = None, timeout: int = 60) -> ExecutionResult: + """ + 执行代码 + + Args: + code: Python 代码 + task_id: 任务 ID + timeout: 超时时间(秒) + + Returns: + ExecutionResult: 执行结果 + """ + # 保存代码 + task_id, code_path = self.save_task_code(code, task_id) + + # 准备日志 + log_path = self.logs_dir / f"task_{task_id}.log" + + start_time = datetime.now() + + try: + # 使用 subprocess 执行 + result = subprocess.run( + [sys.executable, str(code_path)], + cwd=str(self.workspace), + capture_output=True, + text=True, + timeout=timeout, + # 不继承父进程的环境变量中的网络代理等 + env=self._get_safe_env() + ) + + end_time = datetime.now() + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + # 写入日志 + self._write_log( + log_path=log_path, + task_id=task_id, + code_path=code_path, + stdout=result.stdout, + stderr=result.stderr, + return_code=result.returncode, + duration_ms=duration_ms + ) + + return ExecutionResult( + success=result.returncode == 0, + task_id=task_id, + stdout=result.stdout, + stderr=result.stderr, + return_code=result.returncode, + log_path=str(log_path), + duration_ms=duration_ms + ) + + except subprocess.TimeoutExpired: + end_time = datetime.now() + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + error_msg = f"执行超时(超过 {timeout} 秒)" + + self._write_log( + log_path=log_path, + task_id=task_id, + code_path=code_path, + stdout="", + stderr=error_msg, + return_code=-1, + duration_ms=duration_ms + ) + + return ExecutionResult( + success=False, + task_id=task_id, + stdout="", + stderr=error_msg, + return_code=-1, + log_path=str(log_path), + duration_ms=duration_ms + ) + + except Exception as e: + end_time = datetime.now() + duration_ms = int((end_time - start_time).total_seconds() * 1000) + + error_msg = f"执行异常: {str(e)}" + + self._write_log( + log_path=log_path, + task_id=task_id, + code_path=code_path, + stdout="", + stderr=error_msg, + return_code=-1, + duration_ms=duration_ms + ) + + return ExecutionResult( + success=False, + task_id=task_id, + stdout="", + stderr=error_msg, + return_code=-1, + log_path=str(log_path), + duration_ms=duration_ms + ) + + def _generate_task_id(self) -> str: + """生成任务 ID""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + short_uuid = uuid.uuid4().hex[:6] + return f"{timestamp}_{short_uuid}" + + def _get_safe_env(self) -> dict: + """获取安全的环境变量(移除网络代理等)""" + safe_env = os.environ.copy() + + # 移除可能的网络代理设置 + proxy_vars = [ + 'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', + 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy' + ] + for var in proxy_vars: + safe_env.pop(var, None) + + return safe_env + + def _write_log( + self, + log_path: Path, + task_id: str, + code_path: Path, + stdout: str, + stderr: str, + return_code: int, + duration_ms: int + ): + """写入执行日志""" + log_content = f"""======================================== +任务执行日志 +======================================== +任务 ID: {task_id} +代码文件: {code_path} +执行时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} +耗时: {duration_ms} ms +返回码: {return_code} +状态: {"成功" if return_code == 0 else "失败"} + +======================================== +标准输出 (stdout) +======================================== +{stdout if stdout else "(无输出)"} + +======================================== +标准错误 (stderr) +======================================== +{stderr if stderr else "(无错误)"} +""" + log_path.write_text(log_content, encoding='utf-8') + + +def run_task(code: str, task_id: Optional[str] = None) -> ExecutionResult: + """便捷函数:执行任务""" + runner = SandboxRunner() + return runner.execute(code, task_id) + diff --git a/intent/__init__.py b/intent/__init__.py new file mode 100644 index 0000000..0e1c9d8 --- /dev/null +++ b/intent/__init__.py @@ -0,0 +1,2 @@ +# 意图识别模块 + diff --git a/intent/__pycache__/__init__.cpython-310.pyc b/intent/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ebd3b50b97fd4b6e5291b2c86c15c144e63e3cd GIT binary patch literal 131 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 literal 0 HcmV?d00001 diff --git a/intent/__pycache__/classifier.cpython-313.pyc b/intent/__pycache__/classifier.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3ee21d424b1a4556a6650d24003e0c313c0c61b GIT binary patch literal 5373 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 literal 0 HcmV?d00001 diff --git a/intent/__pycache__/labels.cpython-313.pyc b/intent/__pycache__/labels.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..796d3b6ef0dd769bdd06825e9e6f24ad368dcd86 GIT binary patch literal 314 zcmey&%ge<81S>RSGxZr67#@Q-Fu(+5e3k(+rZNOG6fp!d7BL1h6)^=f7cs{$#xNN& z6|tnVXtI`y0F`h(Yw3SFd*8DK?a$Zld%AAclbxMhw^))h5=(AzrdFgTmzHGa=cOBi z!DD+rP1alN@$o77$?@^GSe!i^LvC@pMz}hMhIsn>-I9YdMa#ls~G3}l+@xFpZw&+9LMz3ypovAyb=(V zlbDp6Q><4|d5gm)H$SB`C)KWq2dD|?X@+7gAn}2jk&*Eu6B8rL2QCHXJmL+U cA2=A4R4*{dePCk{6rGTHfnTAKrw9~e05?WjsQ>@~ literal 0 HcmV?d00001 diff --git a/intent/classifier.py b/intent/classifier.py new file mode 100644 index 0000000..194ac49 --- /dev/null +++ b/intent/classifier.py @@ -0,0 +1,152 @@ +""" +意图识别器 +使用小参数 LLM 进行意图二分类 +""" + +import os +import json +from pathlib import Path +from typing import Optional +from dataclasses import dataclass +from dotenv import load_dotenv + +from llm.client import get_client, LLMClientError, ENV_PATH +from llm.prompts import INTENT_CLASSIFICATION_SYSTEM, INTENT_CLASSIFICATION_USER +from intent.labels import CHAT, EXECUTION, EXECUTION_CONFIDENCE_THRESHOLD, VALID_LABELS + + +@dataclass +class IntentResult: + """意图识别结果""" + label: str # chat 或 execution + confidence: float # 0.0 ~ 1.0 + reason: str # 中文解释 + raw_response: Optional[str] = None # 原始 LLM 响应(调试用) + + +class IntentClassifier: + """ + 意图分类器 + + 使用小参数 LLM(如 qwen2.5:7b-instruct)进行快速意图识别 + """ + + def __init__(self): + load_dotenv(ENV_PATH) + self.model_name = os.getenv("INTENT_MODEL_NAME") + + def classify(self, user_input: str) -> IntentResult: + """ + 对用户输入进行意图分类 + + Args: + user_input: 用户输入的文本 + + Returns: + IntentResult: 包含 label, confidence, reason 的结果 + """ + try: + client = get_client() + + messages = [ + {"role": "system", "content": INTENT_CLASSIFICATION_SYSTEM}, + {"role": "user", "content": INTENT_CLASSIFICATION_USER.format(user_input=user_input)} + ] + + response = client.chat( + messages=messages, + model=self.model_name, + temperature=0.1, # 低温度,更确定性的输出 + max_tokens=256 + ) + + return self._parse_response(response) + + except LLMClientError as e: + # LLM 调用失败,走兜底逻辑 + return IntentResult( + label=CHAT, + confidence=0.0, + reason=f"意图识别失败({str(e)}),默认为对话模式" + ) + except Exception as e: + # 其他异常,走兜底逻辑 + return IntentResult( + label=CHAT, + confidence=0.0, + reason=f"意图识别异常({str(e)}),默认为对话模式" + ) + + def _parse_response(self, response: str) -> IntentResult: + """ + 解析 LLM 响应 + + 尝试解析 JSON,若失败则走兜底逻辑 + """ + try: + # 尝试提取 JSON(LLM 可能会在 JSON 前后加一些文字) + json_str = self._extract_json(response) + data = json.loads(json_str) + + # 验证必要字段 + label = data.get("label", "").lower() + confidence = float(data.get("confidence", 0.0)) + reason = data.get("reason", "无") + + # 验证 label 有效性 + if label not in VALID_LABELS: + return IntentResult( + label=CHAT, + confidence=0.0, + reason=f"无效的意图标签 '{label}',默认为对话模式", + raw_response=response + ) + + # 应用置信度阈值 + if label == EXECUTION and confidence < EXECUTION_CONFIDENCE_THRESHOLD: + return IntentResult( + label=CHAT, + confidence=confidence, + reason=f"执行任务置信度不足({confidence:.2f} < {EXECUTION_CONFIDENCE_THRESHOLD}),降级为对话模式。原因: {reason}", + raw_response=response + ) + + return IntentResult( + label=label, + confidence=confidence, + reason=reason, + raw_response=response + ) + + except (json.JSONDecodeError, ValueError, TypeError) as e: + # JSON 解析失败,走兜底逻辑 + return IntentResult( + label=CHAT, + confidence=0.0, + reason=f"响应解析失败,默认为对话模式", + raw_response=response + ) + + def _extract_json(self, text: str) -> str: + """ + 从文本中提取 JSON 字符串 + + LLM 可能会在 JSON 前后添加解释文字,需要提取纯 JSON 部分 + """ + # 尝试找到 JSON 对象的起止位置 + start = text.find('{') + end = text.rfind('}') + + if start != -1 and end != -1 and end > start: + return text[start:end + 1] + + # 如果找不到,返回原文本让 json.loads 报错 + return text + + +# 便捷函数 +def classify_intent(user_input: str) -> IntentResult: + """快速进行意图分类""" + classifier = IntentClassifier() + return classifier.classify(user_input) + diff --git a/intent/labels.py b/intent/labels.py new file mode 100644 index 0000000..128e309 --- /dev/null +++ b/intent/labels.py @@ -0,0 +1,15 @@ +""" +意图标签定义 +""" + +# 意图类型常量 +CHAT = "chat" +EXECUTION = "execution" + +# 执行任务置信度阈值 +# 低于此阈值一律判定为 chat(宁可少执行,不可误执行) +EXECUTION_CONFIDENCE_THRESHOLD = 0.6 + +# 所有有效标签 +VALID_LABELS = {CHAT, EXECUTION} + diff --git a/llm/__init__.py b/llm/__init__.py new file mode 100644 index 0000000..61b410d --- /dev/null +++ b/llm/__init__.py @@ -0,0 +1,2 @@ +# LLM 模块 + diff --git a/llm/__pycache__/__init__.cpython-310.pyc b/llm/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..743fed2f453ee33072ac4781a963e396fa4c5427 GIT binary patch literal 128 zcmd1j<>g`kf)$#vnF2uiF^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*va3~$bAC!{ yag0xXa$=5SdTL%tOioU2OniK1US>&ryk0@&Ee@O9{FKt1R6CI7VkRKL!TRSGX;S3V-N=h7@>^MEI`IohI9r^M!%H|MNB~6XOPq_Wml^h=lqn^ z;uxR&9j7u*WSH1_i2kD{0A3HKLr+>VYnv%yeAOB6o^P9M>IwLFPRej%Z{8^Ohv>! z#fjxrQ|0rR6VGd=CJI!ci7|0lAJ)wzO%ipDV5WX1kT}tf2t=dlG1+W^b%N-y*2g5X zl}L6QRcY&Lfwq(6MuDVg>wrYkq~(ZWb`a@ZA=B#jf>QeS?Y}VUcP`D&j5cnJ)lXlm zUH@}9ezX3!DLp(k8crMwuT9o2empmNu5tfE-LVHyL$H z#ez#=d;ad(A=_Kq+(!X<>bmwIbvOOycb%;0(9RyC(|0NBTxWDbfPk+(M9tp2T)T3w zb7b$j<=kLrXH(}d570t4g4eo!w0^^z>$eqLkNH`zb2+s~^Cik`&u4Vgt6dwGZ|3cT zmRHLzWUnh&10hLWHGW9KAg)iw!(1V2H**vI3|jMCta3_I4M>IOW|{g&tp|# zSaDZ*Dv`&;!>~IXV=p2k%07v`1o#-@i2y!Q`^+TR(t@P0-s|5UG#BsmzVEMxv%-nL z&K*5ozjqx?zCiZhyPq_sZib(ahL^4wm>EWR_QTmb(?NT==rhYM<*a?QYz(KJR=k4bJ&CX_+{~IuvNVeZR7HZrSzoKq|U{I!G}igE*ezGbftBaXh3kmtE=% zEG7uUpvLsd6>^?sy$ma!u9}#VQlctpVw08S<78sWTfK_U{7C!~>BRjNWIt;$t} z#3o`RY9-}~!xB)*)mT*}Dv2LgM&i4LLbn1TFsuy6%8*z11E*@})Omco{6-TG6}XAl z%G*U@1S)eS0UWxQr~(&izyl&VqWxYwARZ8^@nM~$_Q~uRN#p#yKw2)yBZcxqrSf z`AKc+R%7ZyGXa~EEo{(TtUSoN;Qk&XoE{GkP1VLfYfN5)@C`5Dg)%iebK7{LtaRJ1 zn{z$8;B~uY7oz2fI@8tF1x2QLBtHZr3r^tASMAjC`qdlD_LqblP|bWq*LtF`gzxpODOiQD1m)n$B((Ftr5qqQ%t)=ysw$4?o&3K<|^8F_Qj&m;K>OF*g^ z5j*5E=%k8IFm^v(SXcKq)F z;62L{V;F1eA5Dj&hw7IP^%xJtOc0~kU{s_YBZy~*in%Ox8NM4qd_HIa9a+Lh3pB2N z9$q}fZCv~GcV{rr;f&_x+00fEy1tm~O zF6*&%h=>iq#l1kz6+HGjk}ckc&+z?YDF7(mH2`1>sHLS=NmL%HlJr=TrTR2)HrdocJeGQwu?RQ z@iVhue~kD2F;MWco~3f2!$Uu>!hRr(npauH=CudbnH7;yu=QqZ0R#{O-jI{~EkoS` z)hm~Bg+V6sMbyU6_Y;;ikmK#OT7p7S@PgFN{vB`kZrx?|@7S@6wIWBN8M!>dBQ;Us xpw+VMLZJwO4AJgd7T4yPH_$dljr>nA6J0Dis-VT7@rZshM2_4NYuno){~rKP6x9F# literal 0 HcmV?d00001 diff --git a/llm/__pycache__/client.cpython-313.pyc b/llm/__pycache__/client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f3f1a3cc0cd4f59414a070806fa5be28d699d39 GIT binary patch literal 4801 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 UDK08L0e>aMpTU(cK3Q_|U$b`1uK)l5 literal 0 HcmV?d00001 diff --git a/llm/__pycache__/prompts.cpython-313.pyc b/llm/__pycache__/prompts.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adcaa0c8b8bec3442751fce46555a3290fb5e5b9 GIT binary patch literal 2979 zcmai0-BTM?6i=YFb;ml2ZLPKA+(};vudZ) znNU6wS}3$o3KWFc@X<~aKx=`7kbePRdF|f2o2TORAMo702_&dv9^mfYbI(2JcYf!0 z_W6MWoCSXKRW06E4_GWu=|kaX%`cu;!s3pFx5yUWDqHz7xr{HD%lSR>9)7R9mw!Qi zf!`i9D`7k_PZDuG8Pf-QIQ0eup=GAY=mKnMD-TK60KT`5qdodk8jsCC4PV32 zI2lRk!}BD%Y^)FA?inz)k+EbhJdPJujfF{@ksUSPO+3OAi==nXi1lgdG`=3Q!Fi4| z=DKin?Ah8l_YU%0a)%r!clS4=$9~ep1p?S56B|j zA|s2ryJ=V$bN6$R>jbv4e>_|31#jZdqDKvRr2x2`2ph8q-@Y(=P z<%-Scz9{-^4rHS_k?6>FFt%!VCk5KPtzy6ruaH~GNWV@=q4Yg&6Yv~znl3;}~Z&npi7QBI=8oH_;Dl4rY%hg2uHopFV z^xl9Hkm*&@pJXKjoXFBONJSr>BGEyz(oMz|seiL?ltZ6G$>SV?-YA*uQIr&b`O1A}I#9 zmYu|jQGlt)P*OvXB=^1@>FG&rrC-Y?$?z&bjib}Ku{gdth39Ut!1Qsj;071*# z(KEO7^aP6;NNAR^P`@aH9coMyJboN8+@K6VGVUCd#>g%*z)0usxMN&Phqcvc3rB4k zR3tHzdl;wSoMX$AEj07|!>Ja|ZnvA}3=4KYaF#tX%p66@GE~@pB=yhzJQy^9-2I!m zu?J*eP>-j$b9i9E=*-~wlJR(k7P7M&_x#bKegI4=E&2GBBIW=h3(I*9l$Kuou)LI0 zsOfU(k`w@p?(5SX>)P=MA)*%rx&dl39?l zofr|0k3jm2h{o~)Uhkp|g(O*&WIBQ)JvcQ)?NBI%y36LQM z9n*SB*b5gf1kIXoUVl)ML*S&aVzF{p(v zqSJ^PR0=|y*Cz@>l|AT|#XyLwYiK&_63)~$gT;>ZyU1oT*2XoSb^V6{E0m4nTE#Zx zcY6bsRSpip?>N$9N$7ai6nXuC?w^=9w|6|+{q3z@xl;VutAqrp{j^I~#VXS#@5j}6 zrqO|L@)!N)BJN(+GDEp=+E~A}V-u9Ufl#H5H8@W8f#}()IL_OK1R>z|L#F8TX=D=w z8lWK9ScEKE3UtvBC~XALP^vhy9k~06mL6f#3Fxf~(0Ro@VT$(lbP$3*ci<|=vEF1F zRuJX?>d?}Q=^dV3bDJ>)n|+6qF&L!y)+iW~`(aq;`7^qJ2WRz1v+%9JEMZOF%WilOBVGV41I72pBzkTK~P|{u)AniZExfLCD5xKIWW~bFU%nB+ja~z zXq{Z=Jj`S*9XET3s*b7erv9+{vNQ0hQ!H`t31dVa7qp&^?1J9Yc^z~s3e4RQPzXjy zo7<;AFBi>YHWCl63l=klxkJKt&Wjq7?*!(IkBNV5h z>c!1{g3u~?1Yz@7U4zTn;1X)o075mV))(y+CtqSW~uWVS3ZoTsQ8*gq{4sD$(d;Qpk<+ZH} kbo9Lq%i*mv*1bpG+OQnlI>H@su>XgT{b{L?Ujm1J1BB7!asU7T literal 0 HcmV?d00001 diff --git a/llm/client.py b/llm/client.py new file mode 100644 index 0000000..9b9a754 --- /dev/null +++ b/llm/client.py @@ -0,0 +1,124 @@ +""" +LLM 统一调用客户端 +所有模型通过 SiliconFlow API 调用 +""" + +import os +import requests +from pathlib import Path +from typing import Optional +from dotenv import load_dotenv + +# 获取项目根目录 +PROJECT_ROOT = Path(__file__).parent.parent +ENV_PATH = PROJECT_ROOT / ".env" + + +class LLMClientError(Exception): + """LLM 客户端异常""" + pass + + +class LLMClient: + """ + 统一的 LLM 调用客户端 + + 使用方式: + client = LLMClient() + response = client.chat( + messages=[{"role": "user", "content": "你好"}], + model="Qwen/Qwen2.5-7B-Instruct", + temperature=0.7, + max_tokens=1024 + ) + """ + + def __init__(self): + load_dotenv(ENV_PATH) + + self.api_url = os.getenv("LLM_API_URL") + self.api_key = os.getenv("LLM_API_KEY") + + if not self.api_url: + raise LLMClientError("未配置 LLM_API_URL,请检查 .env 文件") + if not self.api_key or self.api_key == "your_api_key_here": + raise LLMClientError("未配置有效的 LLM_API_KEY,请检查 .env 文件") + + def chat( + self, + messages: list[dict], + model: str, + temperature: float = 0.7, + max_tokens: int = 1024 + ) -> str: + """ + 调用 LLM 进行对话 + + Args: + messages: 消息列表,格式为 [{"role": "user/assistant/system", "content": "..."}] + model: 模型名称 + temperature: 温度参数,控制随机性 + max_tokens: 最大生成 token 数 + + Returns: + LLM 生成的文本内容 + + Raises: + LLMClientError: 网络异常或 API 返回错误 + """ + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + payload = { + "model": model, + "messages": messages, + "stream": False, + "temperature": temperature, + "max_tokens": max_tokens + } + + try: + response = requests.post( + self.api_url, + headers=headers, + json=payload, + timeout=60 + ) + except requests.exceptions.Timeout: + raise LLMClientError("请求超时,请检查网络连接") + except requests.exceptions.ConnectionError: + raise LLMClientError("网络连接失败,请检查网络设置") + except requests.exceptions.RequestException as e: + raise LLMClientError(f"网络请求异常: {str(e)}") + + if response.status_code != 200: + error_msg = f"API 返回错误 (状态码: {response.status_code})" + try: + error_detail = response.json() + if "error" in error_detail: + error_msg += f": {error_detail['error']}" + except: + error_msg += f": {response.text[:200]}" + raise LLMClientError(error_msg) + + try: + result = response.json() + content = result["choices"][0]["message"]["content"] + return content + except (KeyError, IndexError, TypeError) as e: + raise LLMClientError(f"解析 API 响应失败: {str(e)}") + + +# 全局单例(延迟初始化) +_client: Optional[LLMClient] = None + + +def get_client() -> LLMClient: + """获取 LLM 客户端单例""" + global _client + if _client is None: + _client = LLMClient() + return _client + diff --git a/llm/prompts.py b/llm/prompts.py new file mode 100644 index 0000000..c425833 --- /dev/null +++ b/llm/prompts.py @@ -0,0 +1,130 @@ +""" +Prompt 模板集合 +所有与 LLM 交互的 Prompt 统一在此管理 +""" + +# ======================================== +# 意图识别 Prompt +# ======================================== + +INTENT_CLASSIFICATION_SYSTEM = """你是一个意图分类器。判断用户输入是"普通对话"还是"本地执行任务"。 + +规则: +- chat: 闲聊、问答、知识查询(如天气、新闻、解释概念) +- execution: 需要操作本地文件的任务(如复制、移动、重命名、整理文件) + +只输出JSON,格式: +{"label": "chat或execution", "confidence": 0.0到1.0, "reason": "简短中文理由"}""" + +INTENT_CLASSIFICATION_USER = """判断以下输入的意图: +{user_input}""" + + +# ======================================== +# 执行计划生成 Prompt +# ======================================== + +EXECUTION_PLAN_SYSTEM = """你是一个任务规划助手。根据用户需求,生成清晰的执行计划。 + +约束: +1. 所有操作只在 workspace 目录内进行 +2. 输入文件来自 workspace/input +3. 输出文件保存到 workspace/output +4. 绝不修改或删除原始文件 +5. 不进行任何网络操作 + +输出格式(中文): +## 任务理解 +[简述用户想做什么] + +## 执行步骤 +1. [步骤1] +2. [步骤2] +... + +## 输入输出 +- 输入目录: workspace/input +- 输出目录: workspace/output + +## 风险提示 +[可能失败的情况]""" + +EXECUTION_PLAN_USER = """用户需求:{user_input} + +请生成执行计划。""" + + +# ======================================== +# 代码生成 Prompt +# ======================================== + +CODE_GENERATION_SYSTEM = """你是一个 Python 代码生成器。根据执行计划生成安全的文件处理代码。 + +硬性约束: +1. 只能操作 workspace/input 和 workspace/output 目录 +2. 禁止使用: requests, socket, urllib, subprocess, os.system +3. 禁止删除文件: os.remove, shutil.rmtree, os.unlink +4. 禁止访问 workspace 外的任何路径 +5. 只使用标准库: os, shutil, pathlib, json, csv 等 + +代码模板: +```python +import os +import shutil +from pathlib import Path + +# 工作目录 +WORKSPACE = Path(__file__).parent +INPUT_DIR = WORKSPACE / "input" +OUTPUT_DIR = WORKSPACE / "output" + +def main(): + # 确保输出目录存在 + OUTPUT_DIR.mkdir(exist_ok=True) + + # TODO: 实现具体逻辑 + + print("任务完成") + +if __name__ == "__main__": + main() +``` + +只输出 Python 代码,不要其他解释。""" + +CODE_GENERATION_USER = """执行计划: +{execution_plan} + +用户原始需求:{user_input} + +请生成 Python 代码。""" + + +# ======================================== +# 安全审查 Prompt +# ======================================== + +SAFETY_REVIEW_SYSTEM = """你是一个代码安全审查员。检查代码是否符合安全规范。 + +检查项: +1. 是否只操作 workspace 目录 +2. 是否有网络请求代码 +3. 是否有危险的文件删除操作 +4. 是否有执行外部命令的代码 +5. 代码逻辑是否与用户需求一致 + +输出JSON格式: +{"pass": true或false, "reason": "中文审查结论"}""" + +SAFETY_REVIEW_USER = """用户需求:{user_input} + +执行计划: +{execution_plan} + +待审查代码: +```python +{code} +``` + +请进行安全审查。""" + diff --git a/main.py b/main.py new file mode 100644 index 0000000..3dd14e2 --- /dev/null +++ b/main.py @@ -0,0 +1,518 @@ +""" +LocalAgent - Windows 本地 AI 执行助手 (MVP) + +======================================== +配置说明 +======================================== +1. 复制 .env.example 为 .env +2. 在 .env 中填入你的 SiliconFlow API Key: + LLM_API_KEY=sk-xxxxx + +======================================== +运行方式 +======================================== +方式一:使用 Anaconda + conda create -n localagent python=3.10 + conda activate localagent + pip install -r requirements.txt + python main.py + +方式二:直接运行(需已安装依赖) + python main.py + +======================================== +测试方法 +======================================== +1. 对话测试:输入 "今天天气怎么样" → 应识别为 chat +2. 执行测试: + - 将测试文件放入 workspace/input 目录 + - 输入 "把这些文件复制一份" → 应识别为 execution + - 确认执行后,检查 workspace/output 目录 + +======================================== +""" + +import os +import sys +import tkinter as tk +from tkinter import messagebox +from pathlib import Path +from typing import Optional +from dotenv import load_dotenv +import threading +import queue + +# 确保项目根目录在 Python 路径中 +PROJECT_ROOT = Path(__file__).parent +ENV_PATH = PROJECT_ROOT / ".env" +sys.path.insert(0, str(PROJECT_ROOT)) + +# 在导入其他模块之前先加载环境变量 +load_dotenv(ENV_PATH) + +from llm.client import get_client, LLMClientError +from llm.prompts import ( + EXECUTION_PLAN_SYSTEM, EXECUTION_PLAN_USER, + CODE_GENERATION_SYSTEM, CODE_GENERATION_USER +) +from intent.classifier import classify_intent, IntentResult +from intent.labels import CHAT, EXECUTION +from safety.rule_checker import check_code_safety +from safety.llm_reviewer import review_code_safety +from executor.sandbox_runner import SandboxRunner, ExecutionResult +from ui.chat_view import ChatView +from ui.task_guide_view import TaskGuideView + + +class LocalAgentApp: + """ + LocalAgent 主应用 + + 职责: + 1. 管理 UI 状态切换 + 2. 协调各模块工作流程 + 3. 处理用户交互 + """ + + def __init__(self): + self.workspace = PROJECT_ROOT / "workspace" + self.runner = SandboxRunner(str(self.workspace)) + + # 当前任务状态 + self.current_task: Optional[dict] = None + + # 线程通信队列 + self.result_queue = queue.Queue() + + # 初始化 UI + self._init_ui() + + def _init_ui(self): + """初始化 UI""" + self.root = tk.Tk() + self.root.title("LocalAgent - 本地 AI 助手") + self.root.geometry("800x700") + self.root.configure(bg='#1e1e1e') + + # 设置窗口图标(如果有的话) + try: + self.root.iconbitmap(PROJECT_ROOT / "icon.ico") + except: + pass + + # 主容器 + self.main_container = tk.Frame(self.root, bg='#1e1e1e') + self.main_container.pack(fill=tk.BOTH, expand=True) + + # 聊天视图 + self.chat_view = ChatView(self.main_container, self._on_user_input) + + # 任务引导视图(初始隐藏) + self.task_view: Optional[TaskGuideView] = None + + # 定期检查后台任务结果 + self._check_queue() + + def _check_queue(self): + """检查后台任务队列""" + try: + while True: + callback, args = self.result_queue.get_nowait() + callback(*args) + except queue.Empty: + pass + + # 每 100ms 检查一次 + self.root.after(100, self._check_queue) + + def _run_in_thread(self, func, callback, *args): + """在后台线程运行函数,完成后回调""" + def wrapper(): + try: + result = func(*args) + self.result_queue.put((callback, (result, None))) + except Exception as e: + self.result_queue.put((callback, (None, e))) + + thread = threading.Thread(target=wrapper, daemon=True) + thread.start() + + def _on_user_input(self, user_input: str): + """处理用户输入""" + # 显示用户消息 + self.chat_view.add_message(user_input, 'user') + self.chat_view.set_input_enabled(False) + self.chat_view.add_message("正在分析您的需求...", 'system') + + # 在后台线程进行意图识别 + self._run_in_thread( + classify_intent, + lambda result, error: self._on_intent_result(user_input, result, error), + user_input + ) + + def _on_intent_result(self, user_input: str, intent_result: Optional[IntentResult], error: Optional[Exception]): + """意图识别完成回调""" + if error: + self.chat_view.add_message(f"意图识别失败: {str(error)}", 'error') + self.chat_view.set_input_enabled(True) + return + + if intent_result.label == CHAT: + # 对话模式 + self._handle_chat(user_input, intent_result) + else: + # 执行模式 + self._handle_execution(user_input, intent_result) + + def _handle_chat(self, user_input: str, intent_result: IntentResult): + """处理对话任务""" + self.chat_view.add_message( + f"识别为对话模式 (原因: {intent_result.reason})", + 'system' + ) + self.chat_view.add_message("正在生成回复...", 'system') + + # 在后台线程调用 LLM + def do_chat(): + client = get_client() + model = os.getenv("GENERATION_MODEL_NAME") + return client.chat( + messages=[{"role": "user", "content": user_input}], + model=model, + temperature=0.7, + max_tokens=2048 + ) + + self._run_in_thread( + do_chat, + self._on_chat_result + ) + + def _on_chat_result(self, response: Optional[str], error: Optional[Exception]): + """对话完成回调""" + if error: + self.chat_view.add_message(f"对话失败: {str(error)}", 'error') + else: + self.chat_view.add_message(response, 'assistant') + + self.chat_view.set_input_enabled(True) + + def _handle_execution(self, user_input: str, intent_result: IntentResult): + """处理执行任务""" + self.chat_view.add_message( + f"识别为执行任务 (置信度: {intent_result.confidence:.0%})\n原因: {intent_result.reason}", + 'system' + ) + self.chat_view.add_message("正在生成执行计划...", 'system') + + # 保存用户输入和意图结果 + self.current_task = { + 'user_input': user_input, + 'intent_result': intent_result + } + + # 在后台线程生成执行计划 + self._run_in_thread( + self._generate_execution_plan, + self._on_plan_generated, + user_input + ) + + def _on_plan_generated(self, plan: Optional[str], error: Optional[Exception]): + """执行计划生成完成回调""" + if error: + self.chat_view.add_message(f"生成执行计划失败: {str(error)}", 'error') + self.chat_view.set_input_enabled(True) + self.current_task = None + return + + self.current_task['execution_plan'] = plan + self.chat_view.add_message("正在生成执行代码...", 'system') + + # 在后台线程生成代码 + self._run_in_thread( + self._generate_code, + self._on_code_generated, + self.current_task['user_input'], + plan + ) + + def _on_code_generated(self, code: Optional[str], error: Optional[Exception]): + """代码生成完成回调""" + if error: + self.chat_view.add_message(f"生成代码失败: {str(error)}", 'error') + self.chat_view.set_input_enabled(True) + self.current_task = None + return + + self.current_task['code'] = code + self.chat_view.add_message("正在进行安全检查...", 'system') + + # 硬规则检查(同步,很快) + rule_result = check_code_safety(code) + if not rule_result.passed: + violations = "\n".join(f" • {v}" for v in rule_result.violations) + self.chat_view.add_message( + f"安全检查未通过,任务已取消:\n{violations}", + 'error' + ) + self.chat_view.set_input_enabled(True) + self.current_task = None + return + + # 在后台线程进行 LLM 安全审查 + self._run_in_thread( + review_code_safety, + self._on_safety_reviewed, + self.current_task['user_input'], + self.current_task['execution_plan'], + code + ) + + def _on_safety_reviewed(self, review_result, error: Optional[Exception]): + """安全审查完成回调""" + if error: + self.chat_view.add_message(f"安全审查失败: {str(error)}", 'error') + self.chat_view.set_input_enabled(True) + self.current_task = None + return + + if not review_result.passed: + self.chat_view.add_message( + f"安全审查未通过: {review_result.reason}", + 'error' + ) + self.chat_view.set_input_enabled(True) + self.current_task = None + return + + self.chat_view.add_message("安全检查通过,请确认执行", 'system') + + # 显示任务引导视图 + self._show_task_guide() + + def _generate_execution_plan(self, user_input: str) -> str: + """生成执行计划""" + client = get_client() + model = os.getenv("GENERATION_MODEL_NAME") + + response = client.chat( + messages=[ + {"role": "system", "content": EXECUTION_PLAN_SYSTEM}, + {"role": "user", "content": EXECUTION_PLAN_USER.format(user_input=user_input)} + ], + model=model, + temperature=0.3, + max_tokens=1024 + ) + + return response + + def _generate_code(self, user_input: str, execution_plan: str) -> str: + """生成执行代码""" + client = get_client() + model = os.getenv("GENERATION_MODEL_NAME") + + response = client.chat( + messages=[ + {"role": "system", "content": CODE_GENERATION_SYSTEM}, + {"role": "user", "content": CODE_GENERATION_USER.format( + user_input=user_input, + execution_plan=execution_plan + )} + ], + model=model, + temperature=0.2, + max_tokens=2048 + ) + + # 提取代码块 + code = self._extract_code(response) + return code + + def _extract_code(self, response: str) -> str: + """从 LLM 响应中提取代码""" + import re + + # 尝试提取 ```python ... ``` 代码块 + pattern = r'```python\s*(.*?)\s*```' + matches = re.findall(pattern, response, re.DOTALL) + + if matches: + return matches[0] + + # 尝试提取 ``` ... ``` 代码块 + pattern = r'```\s*(.*?)\s*```' + matches = re.findall(pattern, response, re.DOTALL) + + if matches: + return matches[0] + + # 如果没有代码块,返回原始响应 + return response + + def _show_task_guide(self): + """显示任务引导视图""" + if not self.current_task: + return + + # 隐藏聊天视图 + self.chat_view.get_frame().pack_forget() + + # 创建任务引导视图 + self.task_view = TaskGuideView( + self.main_container, + on_execute=self._on_execute_task, + on_cancel=self._on_cancel_task, + workspace_path=self.workspace + ) + + # 设置内容 + self.task_view.set_intent_result( + self.current_task['intent_result'].reason, + self.current_task['intent_result'].confidence + ) + self.task_view.set_execution_plan(self.current_task['execution_plan']) + + # 显示 + self.task_view.show() + + def _on_execute_task(self): + """执行任务""" + if not self.current_task: + return + + self.task_view.set_buttons_enabled(False) + + # 在后台线程执行 + def do_execute(): + return self.runner.execute(self.current_task['code']) + + self._run_in_thread( + do_execute, + self._on_execution_complete + ) + + def _on_execution_complete(self, result: Optional[ExecutionResult], error: Optional[Exception]): + """执行完成回调""" + if error: + messagebox.showerror("执行错误", f"执行失败: {str(error)}") + else: + self._show_execution_result(result) + # 刷新输出文件列表 + if self.task_view: + self.task_view.refresh_output() + + self._back_to_chat() + + def _show_execution_result(self, result: ExecutionResult): + """显示执行结果""" + if result.success: + status = "执行成功" + else: + status = "执行失败" + + message = f"""{status} + +任务 ID: {result.task_id} +耗时: {result.duration_ms} ms +日志文件: {result.log_path} + +输出: +{result.stdout if result.stdout else '(无输出)'} + +{f'错误信息: {result.stderr}' if result.stderr else ''} +""" + + if result.success: + messagebox.showinfo("执行结果", message) + # 打开 output 目录 + os.startfile(str(self.workspace / "output")) + else: + messagebox.showerror("执行结果", message) + + def _on_cancel_task(self): + """取消任务""" + self.current_task = None + self._back_to_chat() + + def _back_to_chat(self): + """返回聊天视图""" + if self.task_view: + self.task_view.hide() + self.task_view = None + + self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + self.chat_view.set_input_enabled(True) + self.current_task = None + + def run(self): + """运行应用""" + self.root.mainloop() + + +def check_environment(): + """检查运行环境""" + load_dotenv(ENV_PATH) + + api_key = os.getenv("LLM_API_KEY") + + if not api_key or api_key == "your_api_key_here": + print("=" * 50) + print("错误: 未配置 LLM API Key") + print("=" * 50) + print() + print("请按以下步骤配置:") + print("1. 复制 .env.example 为 .env") + print("2. 在 .env 中设置 LLM_API_KEY=你的API密钥") + print() + print("获取 API Key: https://siliconflow.cn") + print("=" * 50) + + # 显示 GUI 错误提示 + root = tk.Tk() + root.withdraw() + messagebox.showerror( + "配置错误", + "未配置 LLM API Key\n\n" + "请按以下步骤配置:\n" + "1. 复制 .env.example 为 .env\n" + "2. 在 .env 中设置 LLM_API_KEY=你的API密钥\n\n" + "获取 API Key: https://siliconflow.cn" + ) + root.destroy() + return False + + return True + + +def main(): + """主入口""" + print("=" * 50) + print("LocalAgent - Windows 本地 AI 执行助手") + print("=" * 50) + + # 检查环境 + if not check_environment(): + sys.exit(1) + + # 创建工作目录 + workspace = PROJECT_ROOT / "workspace" + (workspace / "input").mkdir(parents=True, exist_ok=True) + (workspace / "output").mkdir(parents=True, exist_ok=True) + (workspace / "logs").mkdir(parents=True, exist_ok=True) + + print(f"工作目录: {workspace}") + print(f"输入目录: {workspace / 'input'}") + print(f"输出目录: {workspace / 'output'}") + print(f"日志目录: {workspace / 'logs'}") + print("=" * 50) + + # 启动应用 + app = LocalAgentApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..08157af --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# LocalAgent MVP 依赖 +# 使用 Anaconda 创建虚拟环境后安装: +# conda create -n localagent python=3.10 +# conda activate localagent +# pip install -r requirements.txt + +python-dotenv>=1.0.0 +requests>=2.31.0 + diff --git a/safety/__init__.py b/safety/__init__.py new file mode 100644 index 0000000..e44309f --- /dev/null +++ b/safety/__init__.py @@ -0,0 +1,2 @@ +# 安全检查模块 + diff --git a/safety/__pycache__/__init__.cpython-310.pyc b/safety/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64521a4451fcc07ec2ad2e0dc7baf83fa0073e54 GIT binary patch literal 131 zcmd1j<>g`kf)$#vnG!(yF^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*nyXcebAC!{ zag0xXa$=5SdTL%tOmSjbYDr~Ge0*kJW=VX!UP0w84x8Nkl+v73JCOcjCLqDW004ql B8x;Tm literal 0 HcmV?d00001 diff --git a/safety/__pycache__/__init__.cpython-313.pyc b/safety/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a00445b36fd7af62b5b9fa83cf1bdb2fa924511 GIT binary patch literal 135 zcmey&%ge<81S>RSGbMoZV-N=h7@>^MEI`IohI9r^M!%H|MNB~6XOPq_HCL+`=lqn^ z;uxR&g LjEsy$%s>_ZI-(u! literal 0 HcmV?d00001 diff --git a/safety/__pycache__/llm_reviewer.cpython-310.pyc b/safety/__pycache__/llm_reviewer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff7b24b652fc453f2a326a4b799a4edb79b351a3 GIT binary patch literal 3456 zcma)8TW=f36`q+LE|*J+x>UIsNn5okin1*nBo8Sn7>xyoP7~O4A;~sOxZQ3!BbD;< zlG&wgsZ^mxGHk<BMo6Zwwg)z7A2@JuVB^m9 zjg`5^xl4^}%gs+dZ(O=+uid$~{^8YuaP;2gjpcXNZhyMI^oPc^^NllCBQ^Uz%qO`F z)z+bEP^^?)59EY!1GnJ0z7LJ0S8;_SDuFDY%oy4pmw{98in1K&G%UR@6thZIl(w?D zBhD{|Mt`1Rx_@M7e|GekGn_s0M)s&Pa%^NYdrbNp^^lQ zPV($9m^p7qE@Oi20kgr^*h2)*$E79BT?E=OY=cou-3|L1mM4qy)UfnxUQp|SmCy{) zq1SJJ(7dv!%_~-WA*PJW(SY&s|e{I zKTs*S-q1Lh^tkVi$zbxh=apb5cmk<*S0@$16%XN~U=z+PYSDfg_J7TQE%Y7%v*dJCAARC{pG_NiI~=is5& zvIm^p(7|jbrZ`Mgai}z__}T<$1IV=vzLs9-d%!>Ur8lOC_qfYCrZU{aP-d$wlY&urw6TBX)lL_##FhzWxPn&hJWY*1P zdK4_eNf4hLqGUQw$y8qu`aVe1nvNY1mP#3aXvNKMCoA0OHLo}w_*p8QqE z%&z}$<|&d%*Ph^s(34oAP;6(U=FCcKv?UCwaeuJY!in3)92@|8Yo7b6aq*MJ%7y!j zGtDpG?ojQt_25AB{0gw(#jD>bctYVC2E- z`3I}-G|t{yyLBjvsM?CN%^uJUDzUzLg42yoSpwL*vCL(qcf;@-^R-MmM2xKbEd1Eth=|m#1gXEYo`6?y&1aTns zc%*|NJ3=7TtXeN@V`Jsh=9Tv%M*d*FaqC0P@y6R9y0Zpp4{0v`rTOlAYj-bW1YKSN zc=15hq&6l2U-Tt~7cfAwg@N!QtNj#u+rb_EXA52b3sNcF<;c^4atnbo0oShPVI9LP z!`B8-u(peSvFrzKxgaBKz#!a%5m@@*@4EZVkwdxHB!+F2dDur>ueK>CMklM0m@y+H zq06*||8d|t)*REDwo|pPM%JT#6G4NI{fIrD zdafNPJCF`Wg#&j$26-OeklXiyP^U|f;2>Z?LXaR}fZ2ybBKwRL%R}vxmQE`)%kMyl@qs4zGD1slg(gS|;P*g2&8Yk3f3JV)lRfUVi%d zP;&=&sPDj}f&tZ5LE|H5g$}@tgjXxZC?pPO;AI1hN<*exyL+!W|5fAco#vnZ{$O<> zg8q(+3z!mjsPH6U&Q9ve16xI@T-(vYG@>sWa1h8&l={_Sg)pHo%`{bS;(Gm&(RSev zegc&?gUM>KJRYW$I@wCrrLXXe+KiI*FuTz{TgsWW8hkaxqJCy7Hv({UysOrC7}D*f=k*V^NN literal 0 HcmV?d00001 diff --git a/safety/__pycache__/llm_reviewer.cpython-313.pyc b/safety/__pycache__/llm_reviewer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84999ce234d0751d8e7c125bc68715e8edbf469c GIT binary patch literal 4670 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 Rsh?0aeNgQ;mEUa`{vWJhUvvNf literal 0 HcmV?d00001 diff --git a/safety/__pycache__/rule_checker.cpython-313.pyc b/safety/__pycache__/rule_checker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36c1432edb62610965be60e422a407fa8b42ec96 GIT binary patch literal 7908 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 literal 0 HcmV?d00001 diff --git a/safety/llm_reviewer.py b/safety/llm_reviewer.py new file mode 100644 index 0000000..b745154 --- /dev/null +++ b/safety/llm_reviewer.py @@ -0,0 +1,132 @@ +""" +LLM 软规则审查器 +使用 LLM 进行代码安全审查 +""" + +import os +import json +from typing import Optional +from dataclasses import dataclass +from dotenv import load_dotenv + +from llm.client import get_client, LLMClientError, ENV_PATH +from llm.prompts import SAFETY_REVIEW_SYSTEM, SAFETY_REVIEW_USER + + +@dataclass +class LLMReviewResult: + """LLM 审查结果""" + passed: bool + reason: str + raw_response: Optional[str] = None + + +class LLMReviewer: + """ + LLM 安全审查器 + + 使用大模型对代码进行语义级别的安全审查 + """ + + def __init__(self): + load_dotenv(ENV_PATH) + self.model_name = os.getenv("GENERATION_MODEL_NAME") + + def review( + self, + user_input: str, + execution_plan: str, + code: str + ) -> LLMReviewResult: + """ + 审查代码安全性 + + Args: + user_input: 用户原始需求 + execution_plan: 执行计划 + code: 待审查的代码 + + Returns: + LLMReviewResult: 审查结果 + """ + try: + client = get_client() + + messages = [ + {"role": "system", "content": SAFETY_REVIEW_SYSTEM}, + {"role": "user", "content": SAFETY_REVIEW_USER.format( + user_input=user_input, + execution_plan=execution_plan, + code=code + )} + ] + + response = client.chat( + messages=messages, + model=self.model_name, + temperature=0.1, + max_tokens=512 + ) + + return self._parse_response(response) + + except LLMClientError as e: + # LLM 调用失败,保守起见判定为不通过 + return LLMReviewResult( + passed=False, + reason=f"安全审查失败({str(e)}),出于安全考虑拒绝执行" + ) + except Exception as e: + return LLMReviewResult( + passed=False, + reason=f"安全审查异常({str(e)}),出于安全考虑拒绝执行" + ) + + def _parse_response(self, response: str) -> LLMReviewResult: + """解析 LLM 响应""" + try: + # 提取 JSON + json_str = self._extract_json(response) + data = json.loads(json_str) + + passed = data.get("pass", False) + reason = data.get("reason", "未提供原因") + + # 确保 passed 是布尔值 + if isinstance(passed, str): + passed = passed.lower() in ('true', 'yes', '1', 'pass') + + return LLMReviewResult( + passed=bool(passed), + reason=reason, + raw_response=response + ) + + except (json.JSONDecodeError, ValueError, TypeError): + # 解析失败,保守判定 + return LLMReviewResult( + passed=False, + reason=f"审查结果解析失败,出于安全考虑拒绝执行", + raw_response=response + ) + + def _extract_json(self, text: str) -> str: + """从文本中提取 JSON""" + start = text.find('{') + end = text.rfind('}') + + if start != -1 and end != -1 and end > start: + return text[start:end + 1] + + return text + + +def review_code_safety( + user_input: str, + execution_plan: str, + code: str +) -> LLMReviewResult: + """便捷函数:审查代码安全性""" + reviewer = LLMReviewer() + return reviewer.review(user_input, execution_plan, code) + diff --git a/safety/rule_checker.py b/safety/rule_checker.py new file mode 100644 index 0000000..be53481 --- /dev/null +++ b/safety/rule_checker.py @@ -0,0 +1,208 @@ +""" +硬规则安全检查器 +静态扫描执行代码,检测危险操作 +""" + +import re +import ast +from typing import List, Tuple +from dataclasses import dataclass + + +@dataclass +class RuleCheckResult: + """规则检查结果""" + passed: bool + violations: List[str] # 违规项列表 + + +class RuleChecker: + """ + 硬规则检查器 + + 静态扫描代码,检测以下危险操作: + 1. 网络请求: requests, socket, urllib, http.client + 2. 危险文件操作: os.remove, shutil.rmtree, os.unlink + 3. 执行外部命令: subprocess, os.system, os.popen + 4. 访问非 workspace 路径 + """ + + # 禁止导入的模块 + FORBIDDEN_IMPORTS = { + 'requests', + 'socket', + 'urllib', + 'urllib.request', + 'urllib.parse', + 'urllib.error', + 'http.client', + 'httplib', + 'ftplib', + 'smtplib', + 'telnetlib', + 'subprocess', + } + + # 禁止调用的函数(模块.函数 或 单独函数名) + FORBIDDEN_CALLS = { + 'os.remove', + 'os.unlink', + 'os.rmdir', + 'os.removedirs', + 'os.system', + 'os.popen', + 'os.spawn', + 'os.spawnl', + 'os.spawnle', + 'os.spawnlp', + 'os.spawnlpe', + 'os.spawnv', + 'os.spawnve', + 'os.spawnvp', + 'os.spawnvpe', + 'os.exec', + 'os.execl', + 'os.execle', + 'os.execlp', + 'os.execlpe', + 'os.execv', + 'os.execve', + 'os.execvp', + 'os.execvpe', + 'shutil.rmtree', + 'shutil.move', # move 可能导致原文件丢失 + 'eval', + 'exec', + 'compile', + '__import__', + } + + # 危险路径模式(正则) + DANGEROUS_PATH_PATTERNS = [ + r'[A-Za-z]:\\', # Windows 绝对路径 + r'\\\\', # UNC 路径 + r'/etc/', + r'/usr/', + r'/bin/', + r'/home/', + r'/root/', + r'\.\./', # 父目录遍历 + r'\.\.', # 父目录 + ] + + def check(self, code: str) -> RuleCheckResult: + """ + 检查代码是否符合安全规则 + + Args: + code: Python 代码字符串 + + Returns: + RuleCheckResult: 检查结果 + """ + violations = [] + + # 1. 检查禁止的导入 + import_violations = self._check_imports(code) + violations.extend(import_violations) + + # 2. 检查禁止的函数调用 + call_violations = self._check_calls(code) + violations.extend(call_violations) + + # 3. 检查危险路径 + path_violations = self._check_paths(code) + violations.extend(path_violations) + + return RuleCheckResult( + passed=len(violations) == 0, + violations=violations + ) + + def _check_imports(self, code: str) -> List[str]: + """检查禁止的导入""" + violations = [] + + try: + tree = ast.parse(code) + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + module_name = alias.name.split('.')[0] + if alias.name in self.FORBIDDEN_IMPORTS or module_name in self.FORBIDDEN_IMPORTS: + violations.append(f"禁止导入模块: {alias.name}") + + elif isinstance(node, ast.ImportFrom): + if node.module: + module_name = node.module.split('.')[0] + if node.module in self.FORBIDDEN_IMPORTS or module_name in self.FORBIDDEN_IMPORTS: + violations.append(f"禁止导入模块: {node.module}") + + except SyntaxError: + # 如果代码有语法错误,使用正则匹配 + for module in self.FORBIDDEN_IMPORTS: + pattern = rf'\bimport\s+{re.escape(module)}\b|\bfrom\s+{re.escape(module)}\b' + if re.search(pattern, code): + violations.append(f"禁止导入模块: {module}") + + return violations + + def _check_calls(self, code: str) -> List[str]: + """检查禁止的函数调用""" + violations = [] + + try: + tree = ast.parse(code) + + for node in ast.walk(tree): + if isinstance(node, ast.Call): + call_name = self._get_call_name(node) + if call_name in self.FORBIDDEN_CALLS: + violations.append(f"禁止调用函数: {call_name}") + + except SyntaxError: + # 如果代码有语法错误,使用正则匹配 + for func in self.FORBIDDEN_CALLS: + pattern = rf'\b{re.escape(func)}\s*\(' + if re.search(pattern, code): + violations.append(f"禁止调用函数: {func}") + + return violations + + def _get_call_name(self, node: ast.Call) -> str: + """获取函数调用的完整名称""" + if isinstance(node.func, ast.Name): + return node.func.id + elif isinstance(node.func, ast.Attribute): + parts = [] + current = node.func + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + return '.'.join(reversed(parts)) + return '' + + def _check_paths(self, code: str) -> List[str]: + """检查危险路径访问""" + violations = [] + + for pattern in self.DANGEROUS_PATH_PATTERNS: + matches = re.findall(pattern, code, re.IGNORECASE) + if matches: + # 排除 workspace 相关的合法路径 + for match in matches: + if 'workspace' not in code[max(0, code.find(match)-50):code.find(match)+50].lower(): + violations.append(f"检测到可疑路径模式: {match}") + break + + return violations + + +def check_code_safety(code: str) -> RuleCheckResult: + """便捷函数:检查代码安全性""" + checker = RuleChecker() + return checker.check(code) + diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..03b9341 --- /dev/null +++ b/ui/__init__.py @@ -0,0 +1,2 @@ +# UI 模块 + diff --git a/ui/__pycache__/__init__.cpython-310.pyc b/ui/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96adfc56e2effeb03c9e850cb63374ae6dff2b74 GIT binary patch literal 127 zcmd1j<>g`kf)$#vnfyTdF^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*lB-pWbAC!{ xag0xXa$=5SdTL%tOlf9Je0*kJW=VX!UP0w84x8Nkl+v73JCNRDCLqDW0001L8Cn1U literal 0 HcmV?d00001 diff --git a/ui/__pycache__/__init__.cpython-313.pyc b/ui/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3c367b33cdd0c55cd8fcb5c9f7a7ab133176aa4 GIT binary patch literal 131 zcmey&%ge<81S>RSGx>q^V-N=h7@>^MEI`IohI9r^M!%H|MNB~6XOPq_C0DB$=lqn^ z;uxR&gjEsy$ H%s>_Zo}3&r literal 0 HcmV?d00001 diff --git a/ui/__pycache__/chat_view.cpython-310.pyc b/ui/__pycache__/chat_view.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad4db0aa909a9e106cc73238baaa9ab2934b39e8 GIT binary patch literal 4434 zcmZu!?RON_8K3vvY&OXz1_HjW)mm4JfTCFHv4ViogP{dl+ZE58$=4;$pYd5x`NtEY`@9~%Py z6vk`VT8#Ud0VUjqGr#2czjXQG3deyGX_Cy-$~|HV?Ta-4sO19iF>Tzx>E-8Y1{o8wXY&r0BnQ`yH?sCp4ZY@AL2P*D? z986;$f^8lep9tc%?Ur2MwpZa3kG8RrmYUQi`&$-#s6~-evD&%}bb9efbxvW$S4N@6 zQHmFBhLoyfq>mbyhx;)Dw>e{|L}$?ov&i{C-8Ew#Rj1TZgTeh~R8Yi5<3s_BeV}CFzi-FP-^*t=%MX3L2e|-cQUFNh^B7AtI>@HpFhm#Mu%;p zJ8K?($@O`rD=-cVXIu<{N^%ku#6ij}d0hA-PVQhqlq-5ns7+(B*iZ3w9+R-#xxDxB2l+ zf#pqjJ|DwGK0mT~6KgRd5wn)-J4H7am|Tc5ZN~GDKJw@Ws6lSlR0AEXIn_v_54uNk zbW5grTp4qP?e1tcEj7q~qlQmz0Z#NXCKwSdHu$<< zbhekOZ@&s~jj*+G{#f<%SHjCD>K8wOh}BaUL0|oJF8ulp&_}}?i;Lmux$x9Xec_An z_(>2)9I9up176aH%i^Fn?&SD-w=`by)3tN+;kCDhGiw9QKNy%hg)_!wrVY$I^(k(0 z2LofD2f%>b!+)?tyM9?jdVk3KFVC>$pb3a1Og{Sx101-@cVUG*2 z;&A?L72hwHf)p(g2--P#-$AT&%h5=*Z0_Cl%(MH0Zig|ud5DZ5y6E8DzUvpcje>(0 zv(2rgsM+qrya?-Yd(10HgozpoTR|>(A0AJj8PHd&hHj`@^S1t#jL~>$Ch8e#{GWr# z0ewKFJEiyHtJXIfsN>(>8c!{oqtzA{tp6j`s~OsVQ`Y2~CA3-&k2$hN>!NYOY`2o=Q|IRhN>QN~fe$C0fmb&ik#^`@*j^$MlJ&Vs1+%2(m5 zi;)B}-wtPD;xP~fI%)<=h{7EYEXIo*-jk6FJBGK*3Zp4$6!(KdZY$M(II(9RbM0Qm3XJg>H&53c>~C&&l*6@ z*qnCMkeV?;X|ec}F@%C{NMUijwYjJgP-bJaO1f%uMa3bxb^$dLm5;DtyUwb9fz&V? z6`gUc->WYyyqHE<-w9f229c)T z-Dm^5}~Ug>jb*Tx!8yvu!!~-+o{=sCYwP1 z$-77FF$Z21^;d?-lb}0>$0LkM#SynMJ6QitT6b!_>YzF~*mk0hw6rW-sB*X|Tsoaa zx`e?rn7tV+O=q&2X4heMqe4|d^~>Y6OIMrLmOmO|C#DN{aQ*!!6fvY7v5OiC5%E)M zwoya+kg9-bq$-xjNzj`sa!0gMtM?AbWo@0(QY7h6ThirpL5!E^M~^MbS~|X_hc@*# zpg~$NsX#4hXf+h4vd~6YNY}>59Hk&>h7TmxE93+9`Rlc7^AVy(zgE?E7s59Il?wn~ zDgAK9>JXJ|Ewe_XgC{UVZ6oFV87+}ht}t`Z?{Oc06u?d(la%EHP&I|`MVjx(jW>}} z?M79~O-3ifOYy0CAtOU5wLEN;_5tfo9bv!Dp;tEi( z%h0s%l6vAl@&5RpxG@-u5A|AumLSSZTs)6g5c3bZR3JoFCAbMJe`4G%732v8MU(vC Z|LqyXpNM3BlJqZgzM;g;B(na1{y$k@z!U%g literal 0 HcmV?d00001 diff --git a/ui/__pycache__/chat_view.cpython-313.pyc b/ui/__pycache__/chat_view.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcec184ef550aa63c105ecf3425a391827945428 GIT binary patch literal 7224 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 literal 0 HcmV?d00001 diff --git a/ui/__pycache__/task_guide_view.cpython-313.pyc b/ui/__pycache__/task_guide_view.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98063b72acda76a207b0fccf444e806edfc2fa99 GIT binary patch literal 22485 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 literal 0 HcmV?d00001 diff --git a/ui/chat_view.py b/ui/chat_view.py new file mode 100644 index 0000000..951de95 --- /dev/null +++ b/ui/chat_view.py @@ -0,0 +1,164 @@ +""" +聊天视图组件 +处理普通对话的 UI 展示 +""" + +import tkinter as tk +from tkinter import scrolledtext +from typing import Callable, Optional + + +class ChatView: + """ + 聊天视图 + + 包含: + - 消息显示区域 + - 输入框 + - 发送按钮 + """ + + def __init__( + self, + parent: tk.Widget, + on_send: Callable[[str], None] + ): + """ + 初始化聊天视图 + + Args: + parent: 父容器 + on_send: 发送消息回调函数 + """ + self.parent = parent + self.on_send = on_send + + self._create_widgets() + + def _create_widgets(self): + """创建 UI 组件""" + # 主框架 + self.frame = tk.Frame(self.parent, bg='#1e1e1e') + self.frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 标题 + title_label = tk.Label( + self.frame, + text="LocalAgent - 本地 AI 助手", + font=('Microsoft YaHei UI', 16, 'bold'), + fg='#61dafb', + bg='#1e1e1e' + ) + title_label.pack(pady=(0, 10)) + + # 消息显示区域 + self.message_area = scrolledtext.ScrolledText( + self.frame, + wrap=tk.WORD, + font=('Microsoft YaHei UI', 11), + bg='#2d2d2d', + fg='#d4d4d4', + insertbackground='white', + relief=tk.FLAT, + padx=10, + pady=10, + state=tk.DISABLED + ) + self.message_area.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + # 配置消息标签样式 + self.message_area.tag_configure('user', foreground='#4fc3f7', font=('Microsoft YaHei UI', 11, 'bold')) + self.message_area.tag_configure('assistant', foreground='#81c784', font=('Microsoft YaHei UI', 11)) + self.message_area.tag_configure('system', foreground='#ffb74d', font=('Microsoft YaHei UI', 10, 'italic')) + self.message_area.tag_configure('error', foreground='#ef5350', font=('Microsoft YaHei UI', 10)) + + # 输入区域框架 + input_frame = tk.Frame(self.frame, bg='#1e1e1e') + input_frame.pack(fill=tk.X) + + # 输入框 + self.input_entry = tk.Entry( + input_frame, + font=('Microsoft YaHei UI', 12), + bg='#3c3c3c', + fg='#ffffff', + insertbackground='white', + relief=tk.FLAT + ) + self.input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=8, padx=(0, 10)) + self.input_entry.bind('', self._on_enter_pressed) + + # 发送按钮 + self.send_button = tk.Button( + input_frame, + text="发送", + font=('Microsoft YaHei UI', 11, 'bold'), + bg='#0078d4', + fg='white', + activebackground='#106ebe', + activeforeground='white', + relief=tk.FLAT, + padx=20, + pady=5, + cursor='hand2', + command=self._on_send_clicked + ) + self.send_button.pack(side=tk.RIGHT) + + # 显示欢迎消息 + welcome_msg = ( + "欢迎使用 LocalAgent!\n" + "- 输入问题进行对话\n" + "- 输入文件处理需求(如\"复制文件\"、\"整理图片\")将触发执行模式" + ) + self.add_message(welcome_msg, 'system') + + def _on_enter_pressed(self, event): + """回车键处理""" + self._on_send_clicked() + + def _on_send_clicked(self): + """发送按钮点击处理""" + text = self.input_entry.get().strip() + if text: + self.input_entry.delete(0, tk.END) + self.on_send(text) + + def add_message(self, message: str, tag: str = 'assistant'): + """ + 添加消息到显示区域 + + Args: + message: 消息内容 + tag: 消息类型 (user/assistant/system/error) + """ + self.message_area.config(state=tk.NORMAL) + + # 添加前缀 + prefix_map = { + 'user': '[你] ', + 'assistant': '[助手] ', + 'system': '[系统] ', + 'error': '[错误] ' + } + prefix = prefix_map.get(tag, '') + + self.message_area.insert(tk.END, "\n" + prefix + message + "\n", tag) + self.message_area.see(tk.END) + self.message_area.config(state=tk.DISABLED) + + def clear_messages(self): + """清空消息区域""" + self.message_area.config(state=tk.NORMAL) + self.message_area.delete(1.0, tk.END) + self.message_area.config(state=tk.DISABLED) + + def set_input_enabled(self, enabled: bool): + """设置输入区域是否可用""" + state = tk.NORMAL if enabled else tk.DISABLED + self.input_entry.config(state=state) + self.send_button.config(state=state) + + def get_frame(self) -> tk.Frame: + """获取主框架""" + return self.frame diff --git a/ui/task_guide_view.py b/ui/task_guide_view.py new file mode 100644 index 0000000..b69544d --- /dev/null +++ b/ui/task_guide_view.py @@ -0,0 +1,524 @@ +""" +任务引导视图组件 +执行任务的引导式 UI - 支持文件拖拽和 Markdown 渲染 +""" + +import tkinter as tk +from tkinter import scrolledtext, messagebox +from tkinter import ttk +from typing import Callable, Optional, List +from pathlib import Path +import shutil +import re + + +class MarkdownText(tk.Text): + """支持简单 Markdown 渲染的 Text 组件""" + + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self._setup_tags() + + def _setup_tags(self): + """设置 Markdown 样式标签""" + # 标题样式 + self.tag_configure('h1', font=('Microsoft YaHei UI', 14, 'bold'), foreground='#ffd54f', spacing1=10, spacing3=5) + self.tag_configure('h2', font=('Microsoft YaHei UI', 12, 'bold'), foreground='#81c784', spacing1=8, spacing3=4) + self.tag_configure('h3', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#4fc3f7', spacing1=6, spacing3=3) + + # 列表样式 + self.tag_configure('bullet', foreground='#ce93d8', lmargin1=20, lmargin2=35) + self.tag_configure('numbered', foreground='#ce93d8', lmargin1=20, lmargin2=35) + + # 代码样式 + self.tag_configure('code', font=('Consolas', 10), background='#3c3c3c', foreground='#f8f8f2') + + # 粗体和斜体 + self.tag_configure('bold', font=('Microsoft YaHei UI', 10, 'bold')) + self.tag_configure('italic', font=('Microsoft YaHei UI', 10, 'italic')) + + # 普通文本 + self.tag_configure('normal', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4') + + def set_markdown(self, text: str): + """设置 Markdown 内容并渲染""" + self.config(state=tk.NORMAL) + self.delete(1.0, tk.END) + + lines = text.split('\n') + for line in lines: + self._render_line(line) + + self.config(state=tk.DISABLED) + + def _render_line(self, line: str): + """渲染单行 Markdown""" + stripped = line.strip() + + # 标题 + if stripped.startswith('### '): + self.insert(tk.END, stripped[4:] + '\n', 'h3') + elif stripped.startswith('## '): + self.insert(tk.END, stripped[3:] + '\n', 'h2') + elif stripped.startswith('# '): + self.insert(tk.END, stripped[2:] + '\n', 'h1') + # 无序列表 + elif stripped.startswith('- ') or stripped.startswith('* '): + self.insert(tk.END, ' • ' + stripped[2:] + '\n', 'bullet') + # 有序列表 + elif re.match(r'^\d+\.\s', stripped): + match = re.match(r'^(\d+\.)\s(.*)$', stripped) + if match: + self.insert(tk.END, ' ' + match.group(1) + ' ' + match.group(2) + '\n', 'numbered') + # 普通文本 + else: + # 处理行内格式 + self._render_inline(line + '\n') + + def _render_inline(self, text: str): + """渲染行内 Markdown(粗体、斜体、代码)""" + # 简化处理:直接插入普通文本 + # 完整实现需要更复杂的解析 + self.insert(tk.END, text, 'normal') + + +class DropZone(tk.Frame): + """文件拖拽区域""" + + def __init__( + self, + parent, + title: str, + target_dir: Path, + is_input: bool = True, + **kwargs + ): + super().__init__(parent, **kwargs) + self.target_dir = target_dir + self.is_input = is_input + self.configure(bg='#2d2d2d', relief=tk.GROOVE, bd=2) + + # 确保目录存在 + self.target_dir.mkdir(parents=True, exist_ok=True) + + self._create_widgets(title) + self._setup_drag_drop() + self._refresh_file_list() + + def _create_widgets(self, title: str): + """创建组件""" + # 标题 + title_frame = tk.Frame(self, bg='#2d2d2d') + title_frame.pack(fill=tk.X, padx=5, pady=5) + + tk.Label( + title_frame, + text=title, + font=('Microsoft YaHei UI', 11, 'bold'), + fg='#4fc3f7' if self.is_input else '#81c784', + bg='#2d2d2d' + ).pack(side=tk.LEFT) + + # 打开文件夹按钮 + open_btn = tk.Button( + title_frame, + text="📂", + font=('Microsoft YaHei UI', 10), + bg='#424242', + fg='white', + relief=tk.FLAT, + cursor='hand2', + command=self._open_folder + ) + open_btn.pack(side=tk.RIGHT) + + # 刷新按钮 + refresh_btn = tk.Button( + title_frame, + text="🔄", + font=('Microsoft YaHei UI', 10), + bg='#424242', + fg='white', + relief=tk.FLAT, + cursor='hand2', + command=self._refresh_file_list + ) + refresh_btn.pack(side=tk.RIGHT, padx=(0, 5)) + + # 拖拽提示区域 + self.drop_label = tk.Label( + self, + text="将文件拖拽到此处\n或点击 📂 打开文件夹", + font=('Microsoft YaHei UI', 10), + fg='#888888', + bg='#3c3c3c', + relief=tk.SUNKEN, + padx=20, + pady=15 + ) + self.drop_label.pack(fill=tk.X, padx=5, pady=5) + + # 文件列表 + self.file_listbox = tk.Listbox( + self, + font=('Microsoft YaHei UI', 9), + bg='#2d2d2d', + fg='#d4d4d4', + selectbackground='#0078d4', + relief=tk.FLAT, + height=4 + ) + self.file_listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 文件计数 + self.count_label = tk.Label( + self, + text="0 个文件", + font=('Microsoft YaHei UI', 9), + fg='#888888', + bg='#2d2d2d' + ) + self.count_label.pack(pady=(0, 5)) + + def _setup_drag_drop(self): + """设置拖拽功能(Windows 需要 windnd 库,这里用简化方案)""" + # 由于 Tkinter 原生不支持文件拖拽,使用点击打开文件夹的方式 + self.drop_label.bind('', lambda e: self._open_folder()) + + def _open_folder(self): + """打开目标文件夹""" + import os + os.startfile(str(self.target_dir)) + + def _refresh_file_list(self): + """刷新文件列表""" + self.file_listbox.delete(0, tk.END) + + files = list(self.target_dir.glob('*')) + files = [f for f in files if f.is_file()] + + for f in files: + self.file_listbox.insert(tk.END, f.name) + + self.count_label.config(text=f"{len(files)} 个文件") + + def get_files(self) -> List[Path]: + """获取目录中的文件列表""" + files = list(self.target_dir.glob('*')) + return [f for f in files if f.is_file()] + + def clear_files(self): + """清空目录中的文件""" + for f in self.target_dir.glob('*'): + if f.is_file(): + f.unlink() + self._refresh_file_list() + + +class TaskGuideView: + """ + 任务引导视图 + + 小白引导式界面,包含: + - 意图识别结果 + - 文件拖拽区域(输入/输出) + - 执行计划展示(Markdown 渲染) + - 风险提示 + - 执行按钮 + """ + + def __init__( + self, + parent: tk.Widget, + on_execute: Callable[[], None], + on_cancel: Callable[[], None], + workspace_path: Optional[Path] = None + ): + self.parent = parent + self.on_execute = on_execute + self.on_cancel = on_cancel + + if workspace_path: + self.workspace = workspace_path + else: + self.workspace = Path(__file__).parent.parent / "workspace" + + self.input_dir = self.workspace / "input" + self.output_dir = self.workspace / "output" + + self._create_widgets() + + def _create_widgets(self): + """创建 UI 组件""" + # 主框架 + self.frame = tk.Frame(self.parent, bg='#1e1e1e') + + # 标题 + title_label = tk.Label( + self.frame, + text="执行任务确认", + font=('Microsoft YaHei UI', 16, 'bold'), + fg='#ffd54f', + bg='#1e1e1e' + ) + title_label.pack(pady=(10, 15)) + + # 上半部分:文件区域 + file_section = tk.Frame(self.frame, bg='#1e1e1e') + file_section.pack(fill=tk.X, padx=10, pady=5) + + # 输入文件区域 + input_frame = tk.LabelFrame( + file_section, + text=" 📥 输入文件 ", + font=('Microsoft YaHei UI', 11, 'bold'), + fg='#4fc3f7', + bg='#1e1e1e', + relief=tk.GROOVE + ) + input_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) + + self.input_zone = DropZone( + input_frame, + title="待处理文件", + target_dir=self.input_dir, + is_input=True, + bg='#2d2d2d' + ) + self.input_zone.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 箭头 + arrow_frame = tk.Frame(file_section, bg='#1e1e1e') + arrow_frame.pack(side=tk.LEFT, padx=10) + tk.Label( + arrow_frame, + text="➡️", + font=('Microsoft YaHei UI', 20), + fg='#ffd54f', + bg='#1e1e1e' + ).pack(pady=30) + + # 输出文件区域 + output_frame = tk.LabelFrame( + file_section, + text=" 📤 输出文件 ", + font=('Microsoft YaHei UI', 11, 'bold'), + fg='#81c784', + bg='#1e1e1e', + relief=tk.GROOVE + ) + output_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0)) + + self.output_zone = DropZone( + output_frame, + title="处理结果", + target_dir=self.output_dir, + is_input=False, + bg='#2d2d2d' + ) + self.output_zone.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 意图识别结果区域 + self._create_intent_section() + + # 执行计划区域(Markdown) + self._create_plan_section() + + # 风险提示区域 + self._create_risk_section() + + # 按钮区域 + self._create_button_section() + + def _create_intent_section(self): + """创建意图识别结果区域""" + section = tk.LabelFrame( + self.frame, + text=" 🎯 意图识别 ", + font=('Microsoft YaHei UI', 11, 'bold'), + fg='#81c784', + bg='#1e1e1e', + relief=tk.GROOVE + ) + section.pack(fill=tk.X, padx=10, pady=5) + + self.intent_label = tk.Label( + section, + text="", + font=('Microsoft YaHei UI', 10), + fg='#d4d4d4', + bg='#1e1e1e', + wraplength=650, + justify=tk.LEFT + ) + self.intent_label.pack(padx=10, pady=8, anchor=tk.W) + + def _create_plan_section(self): + """创建执行计划区域(支持 Markdown)""" + section = tk.LabelFrame( + self.frame, + text=" 📄 执行计划 ", + font=('Microsoft YaHei UI', 11, 'bold'), + fg='#ce93d8', + bg='#1e1e1e', + relief=tk.GROOVE + ) + section.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + # 使用 Markdown 渲染的 Text + self.plan_text = MarkdownText( + section, + wrap=tk.WORD, + bg='#2d2d2d', + fg='#d4d4d4', + relief=tk.FLAT, + height=8, + padx=10, + pady=10 + ) + + # 添加滚动条 + scrollbar = ttk.Scrollbar(section, orient=tk.VERTICAL, command=self.plan_text.yview) + self.plan_text.configure(yscrollcommand=scrollbar.set) + + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.plan_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + def _create_risk_section(self): + """创建风险提示区域""" + section = tk.LabelFrame( + self.frame, + text=" ⚠️ 安全提示 ", + font=('Microsoft YaHei UI', 11, 'bold'), + fg='#ffb74d', + bg='#1e1e1e', + relief=tk.GROOVE + ) + section.pack(fill=tk.X, padx=10, pady=5) + + self.risk_label = tk.Label( + section, + text="• 所有操作仅在 workspace 目录内进行\n• 原始文件不会被修改或删除\n• 执行代码已通过安全检查", + font=('Microsoft YaHei UI', 10), + fg='#d4d4d4', + bg='#1e1e1e', + justify=tk.LEFT + ) + self.risk_label.pack(padx=10, pady=8, anchor=tk.W) + + def _create_button_section(self): + """创建按钮区域""" + button_frame = tk.Frame(self.frame, bg='#1e1e1e') + button_frame.pack(fill=tk.X, padx=10, pady=15) + + # 刷新文件列表按钮 + self.refresh_btn = tk.Button( + button_frame, + text="🔄 刷新文件", + font=('Microsoft YaHei UI', 10), + bg='#424242', + fg='white', + activebackground='#616161', + activeforeground='white', + relief=tk.FLAT, + padx=15, + pady=5, + cursor='hand2', + command=self._refresh_all + ) + self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10)) + + # 取消按钮 + self.cancel_btn = tk.Button( + button_frame, + text="取消", + font=('Microsoft YaHei UI', 11), + bg='#616161', + fg='white', + activebackground='#757575', + activeforeground='white', + relief=tk.FLAT, + padx=20, + pady=5, + cursor='hand2', + command=self.on_cancel + ) + self.cancel_btn.pack(side=tk.RIGHT, padx=(10, 0)) + + # 执行按钮 + self.execute_btn = tk.Button( + button_frame, + text="🚀 开始执行", + font=('Microsoft YaHei UI', 12, 'bold'), + bg='#4caf50', + fg='white', + activebackground='#66bb6a', + activeforeground='white', + relief=tk.FLAT, + padx=30, + pady=8, + cursor='hand2', + command=self._on_execute_clicked + ) + self.execute_btn.pack(side=tk.RIGHT) + + def _refresh_all(self): + """刷新所有文件列表""" + self.input_zone._refresh_file_list() + self.output_zone._refresh_file_list() + + def _on_execute_clicked(self): + """执行按钮点击""" + # 刷新文件列表 + self.input_zone._refresh_file_list() + + # 检查 input 目录是否有文件 + files = self.input_zone.get_files() + + if not files: + result = messagebox.askyesno( + "确认执行", + "输入文件夹为空,确定要继续执行吗?", + icon='warning' + ) + if not result: + return + + self.on_execute() + + def set_intent_result(self, reason: str, confidence: float): + """设置意图识别结果""" + self.intent_label.config( + text=f"识别结果: 执行任务 (置信度: {confidence:.0%})\n原因: {reason}" + ) + + def set_execution_plan(self, plan: str): + """设置执行计划(Markdown 格式)""" + self.plan_text.set_markdown(plan) + + def set_risk_info(self, info: str): + """设置风险提示""" + self.risk_label.config(text=info) + + def set_buttons_enabled(self, enabled: bool): + """设置按钮是否可用""" + state = tk.NORMAL if enabled else tk.DISABLED + self.execute_btn.config(state=state) + self.cancel_btn.config(state=state) + + def refresh_output(self): + """刷新输出文件列表""" + self.output_zone._refresh_file_list() + + def show(self): + """显示视图""" + self.frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + self._refresh_all() + + def hide(self): + """隐藏视图""" + self.frame.pack_forget() + + def get_frame(self) -> tk.Frame: + """获取主框架""" + return self.frame