feat: enhance LocalAgent configuration and UI components

- Updated .env.example to provide clearer configuration instructions and API key setup.
- Removed debug_env.py as it was no longer needed.
- Refactored main.py to streamline application initialization and workspace setup.
- Introduced a new HistoryManager for managing task execution history.
- Enhanced UI components in chat_view.py and task_guide_view.py to improve user interaction and code preview functionality.
- Added loading indicators and improved task history display in the UI.
- Implemented unit tests for history management and intent classification.
This commit is contained in:
Mimikko-zeus
2026-01-07 10:29:13 +08:00
parent 1ba5f0f7d6
commit 0a92355bfb
18 changed files with 2144 additions and 557 deletions

2
app/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# 应用模块

526
app/agent.py Normal file
View File

@@ -0,0 +1,526 @@
"""
LocalAgent 主应用类
管理 UI 状态切换和协调各模块工作流程
"""
import os
import tkinter as tk
from tkinter import messagebox
from pathlib import Path
from typing import Optional, Dict, Any, Tuple
import threading
import queue
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, LLMReviewResult
from executor.sandbox_runner import SandboxRunner, ExecutionResult
from ui.chat_view import ChatView
from ui.task_guide_view import TaskGuideView
from ui.history_view import HistoryView
from history.manager import get_history_manager, HistoryManager
class LocalAgentApp:
"""
LocalAgent 主应用
职责:
1. 管理 UI 状态切换
2. 协调各模块工作流程
3. 处理用户交互
"""
def __init__(self, project_root: Path):
self.project_root: Path = project_root
self.workspace: Path = project_root / "workspace"
self.runner: SandboxRunner = SandboxRunner(str(self.workspace))
self.history: HistoryManager = get_history_manager(self.workspace)
# 当前任务状态
self.current_task: Optional[Dict[str, Any]] = None
# 线程通信队列
self.result_queue: queue.Queue = queue.Queue()
# UI 组件
self.root: Optional[tk.Tk] = None
self.main_container: Optional[tk.Frame] = None
self.chat_view: Optional[ChatView] = None
self.task_view: Optional[TaskGuideView] = None
self.history_view: Optional[HistoryView] = None
# 初始化 UI
self._init_ui()
def _init_ui(self) -> None:
"""初始化 UI"""
self.root = tk.Tk()
self.root.title("LocalAgent - 本地 AI 助手")
self.root.geometry("800x700")
self.root.configure(bg='#1e1e1e')
# 设置窗口图标(如果有的话)
try:
self.root.iconbitmap(self.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,
on_show_history=self._show_history
)
# 定期检查后台任务结果
self._check_queue()
def _check_queue(self) -> None:
"""检查后台任务队列"""
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: callable, callback: callable, *args) -> None:
"""在后台线程运行函数,完成后回调"""
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) -> None:
"""处理用户输入"""
# 显示用户消息
self.chat_view.add_message(user_input, 'user')
self.chat_view.set_input_enabled(False)
self.chat_view.show_loading("正在分析您的需求")
# 在后台线程进行意图识别
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]) -> None:
"""意图识别完成回调"""
self.chat_view.hide_loading()
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) -> None:
"""处理对话任务"""
self.chat_view.add_message(
f"识别为对话模式 (原因: {intent_result.reason})",
'system'
)
# 开始流式消息
self.chat_view.start_stream_message('assistant')
# 在后台线程调用 LLM流式
def do_chat_stream():
client = get_client()
model = os.getenv("GENERATION_MODEL_NAME")
full_response = []
for chunk in client.chat_stream(
messages=[{"role": "user", "content": user_input}],
model=model,
temperature=0.7,
max_tokens=2048,
timeout=300
):
full_response.append(chunk)
# 通过队列发送 chunk 到主线程更新 UI
self.result_queue.put((self._on_chat_chunk, (chunk,)))
return ''.join(full_response)
self._run_in_thread(
do_chat_stream,
self._on_chat_complete
)
def _on_chat_chunk(self, chunk: str):
"""收到对话片段回调(主线程)"""
self.chat_view.append_stream_chunk(chunk)
def _on_chat_complete(self, response: Optional[str], error: Optional[Exception]):
"""对话完成回调"""
self.chat_view.end_stream_message()
if error:
self.chat_view.add_message(f"对话失败: {str(error)}", 'error')
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.show_loading("正在生成执行计划")
# 保存用户输入和意图结果
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.hide_loading()
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.update_loading_text("正在生成执行代码")
# 在后台线程生成代码
self._run_in_thread(
self._generate_code,
self._on_code_generated,
self.current_task['user_input'],
plan
)
def _on_code_generated(self, result: tuple, error: Optional[Exception]):
"""代码生成完成回调"""
if error:
self.chat_view.hide_loading()
self.chat_view.add_message(f"生成代码失败: {str(error)}", 'error')
self.chat_view.set_input_enabled(True)
self.current_task = None
return
# result 可能是 (code, extract_error) 元组
if isinstance(result, tuple):
code, extract_error = result
if extract_error:
self.chat_view.hide_loading()
self.chat_view.add_message(f"代码提取失败: {str(extract_error)}", 'error')
self.chat_view.set_input_enabled(True)
self.current_task = None
return
else:
code = result
self.current_task['code'] = code
self.chat_view.update_loading_text("正在进行安全检查")
# 硬规则检查(同步,很快)
rule_result = check_code_safety(code)
if not rule_result.passed:
self.chat_view.hide_loading()
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.current_task['warnings'] = rule_result.warnings
# 在后台线程进行 LLM 安全审查
self._run_in_thread(
lambda: review_code_safety(
self.current_task['user_input'],
self.current_task['execution_plan'],
code,
rule_result.warnings # 传递警告给 LLM
),
self._on_safety_reviewed
)
def _on_safety_reviewed(self, review_result, error: Optional[Exception]):
"""安全审查完成回调"""
self.chat_view.hide_loading()
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_stream_collect(
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,
timeout=300 # 5分钟超时
)
return response
def _generate_code(self, user_input: str, execution_plan: str) -> tuple:
"""生成执行代码(使用流式传输)"""
client = get_client()
model = os.getenv("GENERATION_MODEL_NAME")
# 使用流式传输,避免超时
response = client.chat_stream_collect(
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=4096, # 代码可能较长
timeout=300 # 5分钟超时
)
# 提取代码块,捕获可能的异常
try:
code = self._extract_code(response)
return (code, None)
except ValueError as e:
return (None, e)
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]
# 如果没有代码块,检查是否看起来像 Python 代码
# 简单检查:是否包含 def 或 import 语句
if 'import ' in response or 'def ' in response:
return response
# 无法提取代码,抛出异常
raise ValueError(
"无法从 LLM 响应中提取代码块。\n"
f"响应内容预览: {response[:200]}..."
)
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.set_code(self.current_task['code'])
# 显示
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:
# 保存历史记录
if self.current_task:
self.history.add_record(
task_id=result.task_id,
user_input=self.current_task['user_input'],
intent_label=self.current_task['intent_result'].label,
intent_confidence=self.current_task['intent_result'].confidence,
execution_plan=self.current_task['execution_plan'],
code=self.current_task['code'],
success=result.success,
duration_ms=result.duration_ms,
stdout=result.stdout,
stderr=result.stderr,
log_path=result.log_path
)
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.stdout if result.stdout else '(无输出)'}
{f'错误信息: {result.stderr}' if result.stderr else ''}
"""
if result.success:
# 成功时显示结果并询问是否打开输出目录
open_output = messagebox.askyesno(
"执行结果",
message + "\n\n是否打开输出文件夹?"
)
if open_output:
os.startfile(str(self.workspace / "output"))
else:
# 失败时显示结果并询问是否打开日志
open_log = messagebox.askyesno(
"执行结果",
message + "\n\n是否打开日志文件查看详情?"
)
if open_log and result.log_path:
os.startfile(result.log_path)
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 _show_history(self):
"""显示历史记录视图"""
# 隐藏聊天视图
self.chat_view.get_frame().pack_forget()
# 创建历史记录视图
self.history_view = HistoryView(
self.main_container,
self.history,
on_back=self._hide_history
)
self.history_view.show()
def _hide_history(self):
"""隐藏历史记录视图,返回聊天"""
if self.history_view:
self.history_view.hide()
self.history_view = None
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
def run(self):
"""运行应用"""
self.root.mainloop()