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:
2
app/__init__.py
Normal file
2
app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# 应用模块
|
||||
|
||||
526
app/agent.py
Normal file
526
app/agent.py
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user