diff --git a/docs/feishu_development_guide.md b/docs/feishu_development_guide.md index d7185a4..297c06e 100644 --- a/docs/feishu_development_guide.md +++ b/docs/feishu_development_guide.md @@ -37,7 +37,7 @@ Agent/ │ └─ main_agent.py # 主 Agent 装配入口 ├─ core/ │ ├─ runtime.py # Agent 执行、Plan 确认、人工交互续跑 - │ ├─ graph_builder.py # LangGraph ReAct 图构建 + │ ├─ graph.py # LangGraph ReAct 图构建 │ ├─ nodes.py # LLM 节点与工具产物处理 │ ├─ routing.py # 工具调用路由/审批识别 │ ├─ plan.py # Plan 模式路径选择 pending action diff --git a/langgraph.json b/langgraph.json deleted file mode 100644 index d6cc441..0000000 --- a/langgraph.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": [ - "." - ], - "graphs": { - "agent": "./src/agent/agent.py:agent" - }, - "env": ".env" -} \ No newline at end of file diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 72175c5..0000000 Binary files a/src/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 8743c99..0000000 Binary files a/src/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/__init__.py b/src/agent/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/agent/__pycache__/__init__.cpython-312.pyc b/src/agent/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 54b9c69..0000000 Binary files a/src/agent/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/src/agent/__pycache__/__init__.cpython-313.pyc b/src/agent/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 3d30ff6..0000000 Binary files a/src/agent/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/__pycache__/chatbot_agent.cpython-312.pyc b/src/agent/__pycache__/chatbot_agent.cpython-312.pyc index ac61aeb..f5b236c 100644 Binary files a/src/agent/__pycache__/chatbot_agent.cpython-312.pyc and b/src/agent/__pycache__/chatbot_agent.cpython-312.pyc differ diff --git a/src/agent/__pycache__/chatbot_agent.cpython-313.pyc b/src/agent/__pycache__/chatbot_agent.cpython-313.pyc deleted file mode 100644 index a7bc611..0000000 Binary files a/src/agent/__pycache__/chatbot_agent.cpython-313.pyc and /dev/null differ diff --git a/src/agent/__pycache__/llm.cpython-312.pyc b/src/agent/__pycache__/llm.cpython-312.pyc deleted file mode 100644 index 4d2f8a0..0000000 Binary files a/src/agent/__pycache__/llm.cpython-312.pyc and /dev/null differ diff --git a/src/agent/__pycache__/llm.cpython-313.pyc b/src/agent/__pycache__/llm.cpython-313.pyc deleted file mode 100644 index 63cd085..0000000 Binary files a/src/agent/__pycache__/llm.cpython-313.pyc and /dev/null differ diff --git a/src/agent/agents/__init__.py b/src/agent/agents/__init__.py index 0f3c597..e5d1194 100644 --- a/src/agent/agents/__init__.py +++ b/src/agent/agents/__init__.py @@ -2,18 +2,24 @@ 系统定位: 对外导出各 Agent 的 ``AgentRuntime`` 单例及多 Agent 运行时。 + 所有 Agent 实例均从 ``agent_config.json`` 动态构建,不再 hardcoded。 Agent 列表: - - main_agent_runtime: 主 Agent 运行时(单 Agent 模式)。 + - main_agent_runtime: 主 Agent 运行时(从配置中 role="main" 的 agent 动态创建)。 + - reload_all_agent_runtimes: 重新加载配置并重建所有 Agent 运行时。 - build_multi_agent_runtime: 构建一主多子 Agent 运行时。 - get_agent_graph_snapshot: 获取多 Agent 图状态快照。 可扩展性: - 子 Agent 实现后在此追加导出,如:: - from agent.agents.research_agent import research_agent_runtime - __all__ = ["main_agent_runtime", "research_agent_runtime"] + 新增 Agent 只需在 agent_config.json 中添加配置项,无需编写 .py 文件。 """ -from agent.agents.main_agent import main_agent_runtime, reload_main_agent_runtime +from agent.agents.agent import ( + build_all_agent_runtimes, + get_main_agent_runtime, + get_all_agent_runtimes, + get_agent_runtime, + reload_all_agent_runtimes, +) from agent.agents.multi_agent_runtime import ( MultiAgentRuntime, SubAgentConfig, @@ -21,9 +27,19 @@ from agent.agents.multi_agent_runtime import ( get_agent_graph_snapshot, ) +# 首次导入时从配置构建,并暴露为模块级属性(向后兼容旧导入路径) +_built = build_all_agent_runtimes() +main_agent_runtime = get_main_agent_runtime() +reload_main_agent_runtime = reload_all_agent_runtimes # 向后兼容别名 + __all__ = [ "main_agent_runtime", "reload_main_agent_runtime", + "reload_all_agent_runtimes", + "build_all_agent_runtimes", + "get_main_agent_runtime", + "get_all_agent_runtimes", + "get_agent_runtime", "MultiAgentRuntime", "SubAgentConfig", "build_multi_agent_runtime", diff --git a/src/agent/agents/__pycache__/__init__.cpython-312.pyc b/src/agent/agents/__pycache__/__init__.cpython-312.pyc index 3d664d3..08cf924 100644 Binary files a/src/agent/agents/__pycache__/__init__.cpython-312.pyc and b/src/agent/agents/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/agent/agents/__pycache__/__init__.cpython-313.pyc b/src/agent/agents/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 57814d3..0000000 Binary files a/src/agent/agents/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/agent.cpython-312.pyc b/src/agent/agents/__pycache__/agent.cpython-312.pyc new file mode 100644 index 0000000..9f67f3e Binary files /dev/null and b/src/agent/agents/__pycache__/agent.cpython-312.pyc differ diff --git a/src/agent/agents/__pycache__/browser_agent.cpython-312.pyc b/src/agent/agents/__pycache__/browser_agent.cpython-312.pyc deleted file mode 100644 index 3591b5e..0000000 Binary files a/src/agent/agents/__pycache__/browser_agent.cpython-312.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/browser_agent.cpython-313.pyc b/src/agent/agents/__pycache__/browser_agent.cpython-313.pyc deleted file mode 100644 index 2c3181a..0000000 Binary files a/src/agent/agents/__pycache__/browser_agent.cpython-313.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/code_agent.cpython-312.pyc b/src/agent/agents/__pycache__/code_agent.cpython-312.pyc deleted file mode 100644 index 7518ff6..0000000 Binary files a/src/agent/agents/__pycache__/code_agent.cpython-312.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/code_agent.cpython-313.pyc b/src/agent/agents/__pycache__/code_agent.cpython-313.pyc deleted file mode 100644 index 304a822..0000000 Binary files a/src/agent/agents/__pycache__/code_agent.cpython-313.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/main_agent.cpython-312.pyc b/src/agent/agents/__pycache__/main_agent.cpython-312.pyc deleted file mode 100644 index b976594..0000000 Binary files a/src/agent/agents/__pycache__/main_agent.cpython-312.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/main_agent.cpython-313.pyc b/src/agent/agents/__pycache__/main_agent.cpython-313.pyc deleted file mode 100644 index c784715..0000000 Binary files a/src/agent/agents/__pycache__/main_agent.cpython-313.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc b/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc index 6ef8c03..e3d9460 100644 Binary files a/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc and b/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc differ diff --git a/src/agent/agents/__pycache__/research_agent.cpython-312.pyc b/src/agent/agents/__pycache__/research_agent.cpython-312.pyc deleted file mode 100644 index 63ddb09..0000000 Binary files a/src/agent/agents/__pycache__/research_agent.cpython-312.pyc and /dev/null differ diff --git a/src/agent/agents/__pycache__/research_agent.cpython-313.pyc b/src/agent/agents/__pycache__/research_agent.cpython-313.pyc deleted file mode 100644 index de0c8c0..0000000 Binary files a/src/agent/agents/__pycache__/research_agent.cpython-313.pyc and /dev/null differ diff --git a/src/agent/agents/agent.py b/src/agent/agents/agent.py new file mode 100644 index 0000000..80cabe2 --- /dev/null +++ b/src/agent/agents/agent.py @@ -0,0 +1,193 @@ +"""动态 Agent 工厂:根据 agent_config.json 全动态创建 AgentRuntime。 + +系统定位: + 替代原有的 hardcoded main_agent / code_agent / research_agent 等单文件, + 实现从配置文件全动态加载 Agent 实例。所有 Agent 的运行参数(图类型、 + 模型、工具集、系统提示词、最大迭代次数)均由 agent_config.json 驱动。 + +使用方式: + from agent.agents.agent import get_main_agent_runtime, reload_all_agent_runtimes + runtime = get_main_agent_runtime() +""" +from __future__ import annotations + +from agent.core.config_manager import load_agent_configs, load_models, AgentConfig +from agent.core.graph import build_graph_by_name +from agent.core.runtime import AgentRuntime +from agent.tools import get_tools + + +# ---------- 缓存 ---------- +_all_agent_runtimes: dict[str, AgentRuntime] = {} +_main_agent_name: str = "main" + + +def _build_llm_for_agent(cfg: AgentConfig): + """根据 Agent 配置中指定的 llm_model_id 构建 LLM 实例。 + + 从 env_config.json 的 models 列表中查找匹配的模型配置, + 用其参数构造 ChatQwen 实例。若未找到匹配模型则回退到环境变量默认值。 + """ + from agent.core.llm import ChatQwen + + models = load_models() + model_config = None + for m in models: + if m.get("id") == cfg.llm_model_id: + model_config = m + break + + if model_config: + return ChatQwen( + model=model_config.get("model", ""), + api_key=model_config.get("api_key", ""), + base_url=model_config.get("base_url", ""), + timeout=float(model_config.get("timeout", 60)), + max_retries=int(model_config.get("max_retries", 0)), + ) + # 回退:使用环境变量中已加载的默认 LLM 配置 + from agent.core.llm import llm as _default_llm + return ChatQwen( + model=_default_llm.model_name, + api_key=_default_llm.openai_api_key or "", + base_url=_default_llm.openai_api_base or "", + ) + + +def _build_agent_runtime(cfg: AgentConfig) -> AgentRuntime: + """根据单个 AgentConfig 构建 AgentRuntime 实例。 + + 输入: + cfg: Agent 配置(name/graph/tools/llm_model_id/system_prompt/max_iterations)。 + + 输出: + 完整的 AgentRuntime 实例,可被 execute_agent 直接使用。 + """ + # 1. 构建 LLM + agent_llm = _build_llm_for_agent(cfg) + + # 2. 获取所有可用工具并按配置过滤 + all_tools = get_tools() + if cfg.tools: + agent_tools = [t for t in all_tools if t.name in cfg.tools] + else: + agent_tools = all_tools + + # 3. bind_tools + model_with_tools = agent_llm.bind_tools(agent_tools, tool_choice="auto") + + # 4. 根据 graph 配置动态构建图 + graph = build_graph_by_name( + graph_name=cfg.graph, + tools=agent_tools, + model_with_tools=model_with_tools, + trajectory_rounds=cfg.trajectory_rounds, + parallel_tools=cfg.parallel_tools, + ) + + # 5. 组装 AgentRuntime + return AgentRuntime( + name=cfg.name, + graph=graph, + llm=agent_llm, + tools=agent_tools, + tools_by_name={tool.name: tool for tool in agent_tools}, + system_prompt=cfg.system_prompt, + max_iterations=cfg.max_iterations, + trajectory_rounds=cfg.trajectory_rounds, + ) + + +def build_all_agent_runtimes() -> dict[str, AgentRuntime]: + """从 agent_config.json 读取所有启用 Agent 并构建运行时字典。 + + 输出: + {agent_name: AgentRuntime} 字典,其中第一个 role="main" 的为 main_agent。 + + 系统定位: + 启动时调用一次,构建全局运行时缓存。 + """ + global _all_agent_runtimes, _main_agent_name + configs = load_agent_configs() + _all_agent_runtimes = {} + _main_agent_name = "main" + + for cfg in configs: + if not cfg.enabled: + continue + try: + runtime = _build_agent_runtime(cfg) + _all_agent_runtimes[cfg.name] = runtime + if cfg.role == "main": + _main_agent_name = cfg.name + except Exception as e: + print(f"[AgentFactory] 构建 Agent '{cfg.name}' 失败: {e}") + + # 确保至少有一个 main agent + if _main_agent_name not in _all_agent_runtimes and _all_agent_runtimes: + first_name = next(iter(_all_agent_runtimes)) + _main_agent_name = first_name + print(f"[AgentFactory] 未找到 role='main' 的 Agent,自动将 '{first_name}' 设为主 Agent。") + + return _all_agent_runtimes + + +def get_main_agent_runtime() -> AgentRuntime | None: + """获取当前配置中 role="main" 的 AgentRuntime。 + + 若运行时缓存为空则自动构建。 + """ + global _all_agent_runtimes + if not _all_agent_runtimes: + build_all_agent_runtimes() + return _all_agent_runtimes.get(_main_agent_name) + + +def get_all_agent_runtimes() -> dict[str, AgentRuntime]: + """获取所有已构建的 Agent 运行时字典。""" + if not _all_agent_runtimes: + build_all_agent_runtimes() + return dict(_all_agent_runtimes) + + +def get_agent_runtime(name: str) -> AgentRuntime | None: + """按名称获取指定 Agent 运行时。""" + if not _all_agent_runtimes: + build_all_agent_runtimes() + return _all_agent_runtimes.get(name) + + +def reload_all_agent_runtimes() -> dict: + """重新加载配置并重建所有 Agent 运行时,返回当前状态。 + + 输出: + {"agents": [{"name": str, "tools_count": int, "graph": str, "max_iterations": int}, ...], + "main_agent": str} + """ + global _all_agent_runtimes, _main_agent_name + _all_agent_runtimes.clear() + build_all_agent_runtimes() + + # 同步更新 chatbot_agent 中的缓存引用 + import agent.chatbot_agent as ca + main_rt = _all_agent_runtimes.get(_main_agent_name) + if main_rt: + ca.main_agent_runtime = main_rt + ca.agent_graph = main_rt.graph + ca.tools = main_rt.tools + ca.TOOLS_BY_NAME = main_rt.tools_by_name + ca.llm = main_rt.llm + + agents_info = [] + for name, rt in _all_agent_runtimes.items(): + agents_info.append({ + "name": name, + "tools_count": len(rt.tools), + "graph": rt.graph.__class__.__name__ if rt.graph else "unknown", + "max_iterations": rt.max_iterations, + }) + + return { + "agents": agents_info, + "main_agent": _main_agent_name, + } diff --git a/src/agent/agents/code_agent.py b/src/agent/agents/code_agent.py deleted file mode 100644 index 2c65d72..0000000 --- a/src/agent/agents/code_agent.py +++ /dev/null @@ -1,10 +0,0 @@ -"""代码子 Agent(预留)。 - -系统定位: - 计划用于代码生成、静态分析、仓库操作等,可绑定代码执行沙箱工具 - 与目录读写类工具。 - -可扩展性: - 可实现独立 AgentRuntime,或在 team 模式下由 main Agent 委派任务; - 建议对执行类工具保持 confirm 策略。 -""" diff --git a/src/agent/agents/doc_agent.py b/src/agent/agents/doc_agent.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/agent/agents/gui_agent.py b/src/agent/agents/gui_agent.py deleted file mode 100644 index 032ad16..0000000 --- a/src/agent/agents/gui_agent.py +++ /dev/null @@ -1,11 +0,0 @@ -"""GUI子 Agent(预留)。 - -系统定位: - 计划用于GUI自动化、页面理解与操作编排,工具以 gui_control - 及 screen_capture 为主。 - -可扩展性: - 实现时可单独配置 tool_policy 白名单、更严格的 confirm 策略, - 并绑定专用 system_prompt 与较短上下文窗口。 -""" - diff --git a/src/agent/agents/main_agent.py b/src/agent/agents/main_agent.py deleted file mode 100644 index e5a3dca..0000000 --- a/src/agent/agents/main_agent.py +++ /dev/null @@ -1,77 +0,0 @@ -"""主 Agent 装配入口:工具、模型、图与运行时单例。 - -系统定位: - 项目默认 Agent 实例 ``main_agent_runtime``,由 Web 层导入并调用 - ``core.runtime.execute_agent`` / ``continue_after_human_action``。 - -装配内容: - - get_tools(): 主 Agent 可用工具集 - - llm.bind_tools: 工具调用能力 - - build_agent_graph: 标准 ReAct 图 - - MAIN_SYSTEM_PROMPT: 系统提示词 - -可扩展性: - - 可按环境变量切换工具子集或模型。 - - 子 Agent(research/browser/code)可仿照本文件独立装配 AgentRuntime。 -""" -from __future__ import annotations - -from agent.core.graph_builder import build_agent_graph -from agent.core.llm import llm -from agent.core.runtime import AgentRuntime -from agent.memory.system_prompt import MAIN_SYSTEM_PROMPT -from agent.tools import get_tools - - -def _build_main_agent_runtime(): - """根据当前配置重新构建主 Agent 运行时。""" - from agent.core.llm import llm as _llm - _system_prompt = MAIN_SYSTEM_PROMPT - _max_iterations = 200 - _tools = get_tools() - try: - from agent.core.config_manager import load_agent_configs - configs = load_agent_configs() - for cfg in configs: - if cfg.name == "main" and cfg.enabled: - if cfg.system_prompt: - _system_prompt = cfg.system_prompt - _max_iterations = cfg.max_iterations - # 根据 agent_config.json 中的 tools 列表过滤工具 - if cfg.tools: - _tools = [t for t in _tools if t.name in cfg.tools] - break - except Exception: - pass - - model_with_tools = _llm.bind_tools(_tools, tool_choice="auto") - return AgentRuntime( - name="main", - graph=build_agent_graph(_tools, model_with_tools), - llm=_llm, - tools=_tools, - tools_by_name={tool.name: tool for tool in _tools}, - system_prompt=_system_prompt, - max_iterations=_max_iterations, - ) - - -main_agent_runtime = _build_main_agent_runtime() - - -def reload_main_agent_runtime() -> dict: - """重新加载配置并重建主 Agent 运行时,返回当前状态供调试。""" - global main_agent_runtime - import agent.chatbot_agent as ca - main_agent_runtime = _build_main_agent_runtime() - # 更新 chatbot_agent 中缓存的引用 - ca.main_agent_runtime = main_agent_runtime - ca.agent_graph = main_agent_runtime.graph - ca.tools = main_agent_runtime.tools - ca.TOOLS_BY_NAME = main_agent_runtime.tools_by_name - ca.llm = main_agent_runtime.llm - return { - "name": main_agent_runtime.name, - "tools_count": len(main_agent_runtime.tools), - "max_iterations": main_agent_runtime.max_iterations, - } diff --git a/src/agent/agents/multi_agent_runtime.py b/src/agent/agents/multi_agent_runtime.py index ef9b7e5..8f16ffa 100644 --- a/src/agent/agents/multi_agent_runtime.py +++ b/src/agent/agents/multi_agent_runtime.py @@ -25,7 +25,7 @@ from agent.core.multi_agent_state import ( ToolCallEvent, init_multi_agent_state, ) -from agent.tools.tool_agent_call import AgentCallTool +from agent.tools.tool_agent_call import CallSubAgentTool @dataclass @@ -92,9 +92,9 @@ def build_multi_agent_runtime( 示例:: - from agent.agents.main_agent import main_agent_runtime + from agent.agents.agent import get_main_agent_runtime runtime = build_multi_agent_runtime( - main_runtime=main_agent_runtime, + main_runtime=get_main_agent_runtime(), sub_runtimes=[ {"runtime": code_agent_runtime, "icon": "💻"}, {"runtime": research_agent_runtime, "icon": "🔍"}, @@ -104,9 +104,9 @@ def build_multi_agent_runtime( sub_runtimes = sub_runtimes or [] # 组装主 agent 工具(含 agent_call) - main_tools = list(main_runtime.tools) + [AgentCallTool()] + main_tools = list(main_runtime.tools) + [CallSubAgentTool()] main_tools_by_name = dict(main_runtime.tools_by_name) - main_tools_by_name["agent_call"] = AgentCallTool() + main_tools_by_name["agent_call"] = CallSubAgentTool() main_model_with_tools = main_runtime.llm.bind_tools(main_tools, tool_choice="auto") # 子 agent 配置列表 diff --git a/src/agent/agents/research_agent.py b/src/agent/agents/research_agent.py deleted file mode 100644 index 76a7d8a..0000000 --- a/src/agent/agents/research_agent.py +++ /dev/null @@ -1,12 +0,0 @@ -"""研究子 Agent(预留)。 - -系统定位: - 计划用于深度检索、文献综述等专用任务,与 main Agent 通过 team 模式 - 或子图委派协作。 - -可扩展性: - 实现步骤建议: - 1. 定义 RESEARCH_SYSTEM_PROMPT 与精简工具集(web_search、RAG 等) - 2. build_agent_graph(tools, model) 或自定义图(含检索节点) - 3. 导出 research_agent_runtime = AgentRuntime(...) -""" diff --git a/src/agent/chatbot_agent.py b/src/agent/chatbot_agent.py index 5579c56..c785313 100644 --- a/src/agent/chatbot_agent.py +++ b/src/agent/chatbot_agent.py @@ -2,12 +2,24 @@ from __future__ import annotations from typing import Any, Literal -from agent.agents.main_agent import main_agent_runtime +from agent.agents.agent import ( + build_all_agent_runtimes, + get_main_agent_runtime, + reload_all_agent_runtimes, +) from agent.core.runtime import continue_after_human_action as continue_runtime_after_human_action from agent.core.runtime import execute_agent as execute_runtime_agent AgentMode = Literal["agent", "team"] +# 首次导入时从配置构建主 Agent 运行时并缓存引用 +_built = build_all_agent_runtimes() +main_agent_runtime = get_main_agent_runtime() +agent_graph = main_agent_runtime.graph +tools = main_agent_runtime.tools +TOOLS_BY_NAME = main_agent_runtime.tools_by_name +llm = main_agent_runtime.llm + def execute_agent( chat_history: list[dict[str, Any]], @@ -43,9 +55,3 @@ def continue_after_human_action( instruction=instruction, output_type=output_type, ) - - -agent_graph = main_agent_runtime.graph -tools = main_agent_runtime.tools -TOOLS_BY_NAME = main_agent_runtime.tools_by_name -llm = main_agent_runtime.llm diff --git a/src/agent/core/__init__.py b/src/agent/core/__init__.py index c2a4525..65bc090 100644 --- a/src/agent/core/__init__.py +++ b/src/agent/core/__init__.py @@ -1,12 +1,12 @@ """Agent 核心运行时包。 包含: - - state / graph_builder: LangGraph 状态与图拓扑 + - state / graph: LangGraph 状态与图拓扑 - nodes / routing: 图节点与条件路由 - runtime: 对外执行入口 execute_agent / continue_after_human_action - llm: ChatQwen 多模态网关 - - approvals / human_interaction / tool_policy: HITL 与安全策略 - - plan: Plan 模式 UI 构建 + - approvals: HITL 待确认管理(含 human_interaction 参数规范化) + - tool_policy: 工具执行安全策略 系统定位: Agent 业务逻辑中枢,``agents`` 包在此之上组装具体 Agent 实例。 diff --git a/src/agent/core/__pycache__/__init__.cpython-312.pyc b/src/agent/core/__pycache__/__init__.cpython-312.pyc index eb135f9..fea417d 100644 Binary files a/src/agent/core/__pycache__/__init__.cpython-312.pyc and b/src/agent/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/__init__.cpython-313.pyc b/src/agent/core/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index a535309..0000000 Binary files a/src/agent/core/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/approvals.cpython-312.pyc b/src/agent/core/__pycache__/approvals.cpython-312.pyc index 2cc2dab..2c67bb0 100644 Binary files a/src/agent/core/__pycache__/approvals.cpython-312.pyc and b/src/agent/core/__pycache__/approvals.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/approvals.cpython-313.pyc b/src/agent/core/__pycache__/approvals.cpython-313.pyc deleted file mode 100644 index a159393..0000000 Binary files a/src/agent/core/__pycache__/approvals.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/config_manager.cpython-312.pyc b/src/agent/core/__pycache__/config_manager.cpython-312.pyc index 08873b5..7ffb088 100644 Binary files a/src/agent/core/__pycache__/config_manager.cpython-312.pyc and b/src/agent/core/__pycache__/config_manager.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/graph.cpython-312.pyc b/src/agent/core/__pycache__/graph.cpython-312.pyc new file mode 100644 index 0000000..16600d3 Binary files /dev/null and b/src/agent/core/__pycache__/graph.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/graph_builder.cpython-312.pyc b/src/agent/core/__pycache__/graph_builder.cpython-312.pyc deleted file mode 100644 index acff7f3..0000000 Binary files a/src/agent/core/__pycache__/graph_builder.cpython-312.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/graph_builder.cpython-313.pyc b/src/agent/core/__pycache__/graph_builder.cpython-313.pyc deleted file mode 100644 index e0a80ee..0000000 Binary files a/src/agent/core/__pycache__/graph_builder.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/human_interaction.cpython-312.pyc b/src/agent/core/__pycache__/human_interaction.cpython-312.pyc deleted file mode 100644 index c9b0bd3..0000000 Binary files a/src/agent/core/__pycache__/human_interaction.cpython-312.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/human_interaction.cpython-313.pyc b/src/agent/core/__pycache__/human_interaction.cpython-313.pyc deleted file mode 100644 index e2ca262..0000000 Binary files a/src/agent/core/__pycache__/human_interaction.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/llm.cpython-312.pyc b/src/agent/core/__pycache__/llm.cpython-312.pyc index 567cd61..f5d6d93 100644 Binary files a/src/agent/core/__pycache__/llm.cpython-312.pyc and b/src/agent/core/__pycache__/llm.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/llm.cpython-313.pyc b/src/agent/core/__pycache__/llm.cpython-313.pyc deleted file mode 100644 index f882e02..0000000 Binary files a/src/agent/core/__pycache__/llm.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc b/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc index d31a527..dbb0e09 100644 Binary files a/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc and b/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc b/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc index 8d56cbc..491215d 100644 Binary files a/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc and b/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/multi_agent_state.cpython-313.pyc b/src/agent/core/__pycache__/multi_agent_state.cpython-313.pyc deleted file mode 100644 index 99b879c..0000000 Binary files a/src/agent/core/__pycache__/multi_agent_state.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/nodes.cpython-312.pyc b/src/agent/core/__pycache__/nodes.cpython-312.pyc index 341ee25..df564b5 100644 Binary files a/src/agent/core/__pycache__/nodes.cpython-312.pyc and b/src/agent/core/__pycache__/nodes.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/nodes.cpython-313.pyc b/src/agent/core/__pycache__/nodes.cpython-313.pyc deleted file mode 100644 index 2443de7..0000000 Binary files a/src/agent/core/__pycache__/nodes.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/plan.cpython-312.pyc b/src/agent/core/__pycache__/plan.cpython-312.pyc deleted file mode 100644 index e060a31..0000000 Binary files a/src/agent/core/__pycache__/plan.cpython-312.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/plan.cpython-313.pyc b/src/agent/core/__pycache__/plan.cpython-313.pyc deleted file mode 100644 index eb601fb..0000000 Binary files a/src/agent/core/__pycache__/plan.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/routing.cpython-312.pyc b/src/agent/core/__pycache__/routing.cpython-312.pyc index 7c64f49..d4cc34c 100644 Binary files a/src/agent/core/__pycache__/routing.cpython-312.pyc and b/src/agent/core/__pycache__/routing.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/routing.cpython-313.pyc b/src/agent/core/__pycache__/routing.cpython-313.pyc deleted file mode 100644 index 751a04d..0000000 Binary files a/src/agent/core/__pycache__/routing.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/runtime.cpython-312.pyc b/src/agent/core/__pycache__/runtime.cpython-312.pyc index d2d342b..37b5d81 100644 Binary files a/src/agent/core/__pycache__/runtime.cpython-312.pyc and b/src/agent/core/__pycache__/runtime.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/runtime.cpython-313.pyc b/src/agent/core/__pycache__/runtime.cpython-313.pyc deleted file mode 100644 index c98d6d8..0000000 Binary files a/src/agent/core/__pycache__/runtime.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/state.cpython-312.pyc b/src/agent/core/__pycache__/state.cpython-312.pyc index 13cb367..53edabd 100644 Binary files a/src/agent/core/__pycache__/state.cpython-312.pyc and b/src/agent/core/__pycache__/state.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/state.cpython-313.pyc b/src/agent/core/__pycache__/state.cpython-313.pyc deleted file mode 100644 index f387c17..0000000 Binary files a/src/agent/core/__pycache__/state.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/todo_manager.cpython-312.pyc b/src/agent/core/__pycache__/todo_manager.cpython-312.pyc deleted file mode 100644 index 5814011..0000000 Binary files a/src/agent/core/__pycache__/todo_manager.cpython-312.pyc and /dev/null differ diff --git a/src/agent/core/__pycache__/tool_policy.cpython-312.pyc b/src/agent/core/__pycache__/tool_policy.cpython-312.pyc index eba5259..41a4322 100644 Binary files a/src/agent/core/__pycache__/tool_policy.cpython-312.pyc and b/src/agent/core/__pycache__/tool_policy.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/tool_policy.cpython-313.pyc b/src/agent/core/__pycache__/tool_policy.cpython-313.pyc deleted file mode 100644 index 08578dc..0000000 Binary files a/src/agent/core/__pycache__/tool_policy.cpython-313.pyc and /dev/null differ diff --git a/src/agent/core/agent_config.json b/src/agent/core/agent_config.json index 910886d..b7be52c 100644 --- a/src/agent/core/agent_config.json +++ b/src/agent/core/agent_config.json @@ -1,20 +1,17 @@ [ { "name": "main", - "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是直接回答用户问题,不要调用任何工具。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。", + "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是优先调用gui工具解决用户问题。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。", "max_iterations": 200, "llm_model_id": "a44b226e", "tools": [ - "todo_list", "gui_tool", - "directory_listing", - "web_search_class", - "date_query_class", - "human_interaction", - "software_control", "browser_control" ], "enabled": true, - "role": "main" + "role": "main", + "graph": "react", + "trajectory_rounds": 3, + "parallel_tools": true } ] \ No newline at end of file diff --git a/src/agent/core/approvals.py b/src/agent/core/approvals.py index e94d348..9b667f8 100644 --- a/src/agent/core/approvals.py +++ b/src/agent/core/approvals.py @@ -1,8 +1,13 @@ -"""待确认操作(HITL)内存注册与 UI 元数据构建。 +"""待确认操作(HITL):内存注册、UI 元数据构建、参数规范化与结果生成。 系统定位: - 维护进程内 ``PENDING_TOOL_APPROVALS`` 字典,在图因需确认工具而暂停时 - 保存上下文;``runtime.continue_after_human_action`` 消费并清理。 + 维护进程内 ``PENDING_TOOL_APPROVALS`` 字典;在图因需确认工具而暂停时 + 保存上下文并构建前端 UI 元数据;在用户操作后生成 ToolMessage 供图继续执行。 + +内容分区: + - PENDING 存储与元数据构建(build_approval_meta / build_human_interaction_meta) + - UI 消息辅助(pending_assistant_message / remove_pending_ui_message) + - human_interaction 工具的参数规范化与结果文案(normalize / result) 注意: 当前为内存存储,进程重启后待确认项丢失;生产环境可改为 Redis/DB。 @@ -10,6 +15,7 @@ 可扩展性: - 增加 TTL、session 级清理、持久化后端。 - ``build_*_meta`` 可与前端组件协议版本化。 + - 新增 interaction_type 时同步扩展 normalize / result 分支。 """ from __future__ import annotations @@ -20,12 +26,20 @@ from typing import Any, Sequence from langchain_core.messages import BaseMessage from langchain_core.messages import messages_to_dict -from agent.core.human_interaction import normalize_human_interaction_args +from agent.tools.tool import HumanInteraction from agent.utils.tool_call_utils import json_safe +# ============================================================ +# PENDING 存储 +# ============================================================ + PENDING_TOOL_APPROVALS: dict[str, dict[str, Any]] = {} +# ============================================================ +# UI 元数据构建 +# ============================================================ + def build_approval_meta(session_id: str, tool_call: dict[str, Any], messages: Sequence[BaseMessage]) -> dict[str, Any]: """为需确认的普通工具调用构建 UI 元数据并注册 pending。 @@ -36,12 +50,6 @@ def build_approval_meta(session_id: str, tool_call: dict[str, Any], messages: Se 输出: ``{"pending_action": {...}}``,含 id、type=tool_call、args、options 等。 - - 系统定位: - ``routing.first_pending_tool_meta`` 对 confirm 类工具调用。 - - 可扩展性: - 可按工具名定制 title、风险提示、参数展示模板。 """ approval_id = uuid.uuid4().hex tool_name = tool_call.get("name") or "unknown_tool" @@ -78,12 +86,6 @@ def build_human_interaction_meta(session_id: str, tool_call: dict[str, Any], mes 输出: ``{"pending_action": {...}}``,type=human_interaction,含 interaction_type 等。 - - 系统定位: - 模型主动请求用户补充信息/选择/审批时使用。 - - 可扩展性: - 可扩展表单 schema、多步向导式交互。 """ approval_id = uuid.uuid4().hex args = normalize_human_interaction_args(tool_call.get("args") or {}) @@ -120,21 +122,7 @@ def build_human_interaction_meta(session_id: str, tool_call: dict[str, Any], mes def pending_assistant_message(meta: dict[str, Any], content: str = "需要你确认后继续执行。") -> dict[str, Any]: - """构造带 pending_action 的 assistant 聊天消息 dict。 - - 输入: - meta: 通常含 pending_action 的字典。 - content: 展示给用户的提示文案。 - - 输出: - Web UI 使用的 ``{"role": "assistant", "content": ..., "meta": ...}``。 - - 系统定位: - ``runtime.execute_agent`` 检测到待确认时追加到 chat_history。 - - 可扩展性: - 可统一 output_type、国际化 content 模板。 - """ + """构造带 pending_action 的 assistant 聊天消息 dict。""" return { "role": "assistant", "content": content, @@ -143,24 +131,60 @@ def pending_assistant_message(meta: dict[str, Any], content: str = "需要你确 def remove_pending_ui_message(chat_history: list[dict[str, Any]], approval_id: str) -> None: - """在 chat_history 中将指定 approval_id 的 pending_action 标记为已解决。 - - 输入: - chat_history: 可变聊天历史列表(原地修改)。 - approval_id: 待确认项 ID。 - - 输出: - 无(副作用:匹配消息的 meta.pending_action.resolved = True)。 - - 系统定位: - ``runtime.continue_after_human_action`` 用户操作后更新 UI 状态。 - - 可扩展性: - 可改为从 history 中移除而非仅标记 resolved。 - """ + """在 chat_history 中将指定 approval_id 的 pending_action 标记为已解决(原地修改)。""" for msg in reversed(chat_history): pending = ((msg.get("meta") or {}).get("pending_action") or {}) if isinstance(msg, dict) else {} if pending.get("id") == approval_id: pending["resolved"] = True msg.setdefault("meta", {})["pending_action"] = pending return + + +# ============================================================ +# human_interaction 工具的参数规范化与结果文案 +# ============================================================ + +def normalize_human_interaction_args(args: dict[str, Any]) -> dict[str, Any]: + """规范化 human_interaction 工具参数为固定 schema。 + + 输入: + args: 模型传入的原始参数字典。 + + 输出: + 含 interaction_type、title、prompt、options、suggested_response 的字典; + interaction_type 限定为 information | selection | approval。 + """ + normalized = dict(args or {}) + normalized["interaction_type"] = str(normalized.get("interaction_type") or "information") + if normalized["interaction_type"] not in {"information", "selection", "approval"}: + normalized["interaction_type"] = "information" + normalized["title"] = str(normalized.get("title") or "需要人工处理") + normalized["prompt"] = str(normalized.get("prompt") or "请处理后继续。") + normalized["options"] = HumanInteraction.normalize_options(normalized.get("options")) + normalized["suggested_response"] = str(normalized.get("suggested_response") or "") + return normalized + + +def human_interaction_result(pending: dict[str, Any], decision: str, instruction: str | None) -> str: + """根据用户决策生成写入 ToolMessage 的文本结果。 + + 输入: + pending: PENDING_TOOL_APPROVALS 中 type=human_interaction 的条目。 + decision: approve | reject | edit。 + instruction: 用户输入文本(补充信息、选项、修改说明等)。 + + 输出: + 供 LangGraph 继续推理的 ToolMessage 字符串内容。 + """ + tool_call = pending.get("tool_call") or {} + args = normalize_human_interaction_args(tool_call.get("args") or {}) + interaction_type = args.get("interaction_type") or "information" + text = (instruction or "").strip() + if interaction_type == "approval": + if decision == "reject": + return "用户拒绝" + return "用户同意" + if interaction_type == "selection": + selected = text or args.get("suggested_response") or "" + return f"用户选择:{selected}" + return f"用户补充信息如下:{text}" diff --git a/src/agent/core/config_manager.py b/src/agent/core/config_manager.py index ece2cbe..ece57e8 100644 --- a/src/agent/core/config_manager.py +++ b/src/agent/core/config_manager.py @@ -49,6 +49,9 @@ class AgentConfig: tools: list[str] = field(default_factory=list) enabled: bool = True role: str = "sub" # "main" | "sub" + graph: str = "react" # 图名,对应 graph.py 中 GRAPH_REGISTRY 的 key + trajectory_rounds: int = 3 # 短期记忆:执行过程中保留最近 N 轮工具返回结果 + parallel_tools: bool = False # 是否并行执行同一轮中的多个工具调用 @dataclass @@ -99,6 +102,9 @@ def _default_agent_config() -> list[dict]: "tools": get_all_available_tool_names(), "enabled": True, "role": "main", + "graph": "react", + "trajectory_rounds": 3, + "parallel_tools": False, } ] @@ -120,6 +126,9 @@ def load_agent_configs() -> list[AgentConfig]: tools=item.get("tools", []), enabled=item.get("enabled", True), role=item.get("role", "sub"), + graph=item.get("graph", "react"), + trajectory_rounds=item.get("trajectory_rounds", 3), + parallel_tools=item.get("parallel_tools", False), )) return configs @@ -329,3 +338,93 @@ def get_registered_tool_names() -> list[str]: """返回所有可用工具名称(含数据库工具)。""" from agent.tools import get_all_available_tool_names return get_all_available_tool_names() + + +# ---------- GUI 显示器配置 ---------- +GUI_CONFIG_PATH = _CORE_DIR / "gui_config.json" + + +def _default_gui_config() -> dict: + """生成默认 GUI 显示器配置(主显示器)。""" + try: + from screeninfo import get_monitors + monitors = get_monitors() + primary = next((m for m in monitors if m.is_primary), monitors[0] if monitors else None) + if primary: + return { + "gui_monitor_name": primary.name or "", + "gui_monitor_index": 0, + "monitors_snapshot": _serialize_monitors(monitors), + } + except Exception: + pass + return { + "gui_monitor_name": "", + "gui_monitor_index": 0, + "monitors_snapshot": [], + } + + +def _serialize_monitors(monitors) -> list[dict]: + """将 screeninfo Monitor 对象序列化为可 JSON 化的字典列表。""" + result = [] + for m in monitors: + result.append({ + "name": m.name or "", + "x": m.x, + "y": m.y, + "width": m.width, + "height": m.height, + "is_primary": m.is_primary, + }) + return result + + +def _get_live_monitors() -> list[dict]: + """实时获取当前连接的显示器列表(通过 screeninfo)。""" + from screeninfo import get_monitors + return _serialize_monitors(get_monitors()) + + +def load_gui_config() -> dict: + """从 gui_config.json 加载 GUI 显示器配置,无文件时返回默认。""" + raw = _read_json(GUI_CONFIG_PATH) + if not raw or not isinstance(raw, dict): + raw = _default_gui_config() + _write_json(GUI_CONFIG_PATH, raw) + return raw + + +def save_gui_config(config: dict) -> bool: + """将 GUI 显示器配置写入 gui_config.json。""" + return _write_json(GUI_CONFIG_PATH, config) + + +def get_active_monitor_region() -> dict | None: + """获取当前选中的 GUI 操作显示器的区域信息。 + + 返回: + {"x": int, "y": int, "width": int, "height": int, "name": str} 或 None + + 系统定位: + 供 gui_tool.py 截取指定显示器区域并做坐标变换。 + """ + config = load_gui_config() + monitor_name = config.get("gui_monitor_name", "") + + monitors = _get_live_monitors() + + if not monitors: + return None + + # 按名称模糊匹配 + if monitor_name: + for m in monitors: + if monitor_name.lower() in (m.get("name", "") or "").lower(): + return {"x": m["x"], "y": m["y"], "width": m["width"], "height": m["height"], "name": m["name"]} + + # 回退:主显示器 + primary = next((m for m in monitors if m.get("is_primary")), monitors[0] if monitors else None) + if primary: + return {"x": primary["x"], "y": primary["y"], "width": primary["width"], "height": primary["height"], "name": primary["name"]} + return None diff --git a/src/agent/core/graph.py b/src/agent/core/graph.py new file mode 100644 index 0000000..ba1ca2c --- /dev/null +++ b/src/agent/core/graph.py @@ -0,0 +1,162 @@ +"""LangGraph 工作流图构建与图注册中心。 + +系统定位: + 将 LLM(已 bind_tools)、工具列表组装为可编译的 StateGraph, + 供 ``agents/agent`` 及各子 Agent 的 ``AgentRuntime`` 使用。 + +图注册中心: + GRAPH_REGISTRY: {图名: 构建函数} 字典映射,供 agent_config.json 的 graph 字段 + 动态选择使用哪个图。用户可在此注册自定义图。 + +图结构: + react: START -> agent -> (tools | END) + tools -> process_tool_artifact -> agent + + multi_agent: START -> supervisor -> main | sub_N | END + main -> supervisor, sub_N -> supervisor + +可扩展性: + - 在 GRAPH_REGISTRY 中注册新图的 (build_fn, info) 元组。 + - 可增加 checkpoint、interrupt 节点、子图路由。 + - 可为不同 Agent 注入不同的 conditional_edges 或额外节点(如 RAG 检索)。 +""" +from __future__ import annotations + +from typing import Any + +from langgraph.graph import END, START, StateGraph +from langgraph.prebuilt import ToolNode + +from agent.core.nodes import make_call_model_node, process_tool_artifact +from agent.core.routing import should_continue +from agent.core.state import AgentState + + +def build_agent_graph(tools: list[Any], model_with_tools: Any, trajectory_rounds: int = 3, parallel_tools: bool = False): + """构建并编译标准 ReAct Agent 图。 + + 输入: + tools: LangChain BaseTool 列表,供 ToolNode 执行。 + model_with_tools: 已 ``bind_tools`` 的聊天模型,供 agent 节点调用。 + trajectory_rounds: 短期记忆保留最近 N 轮工具返回结果,默认 3。 + parallel_tools: 是否启用并行工具执行(多个 tool_call 在同一轮并行执行)。 + + 输出: + 编译后的 LangGraph CompiledGraph,支持 ``invoke({"messages": ...})``。 + + 系统定位: + 默认图类型 "react",供各 Agent 使用。 + + 可扩展性: + 返回前可 ``workflow.compile(checkpointer=...)`` 启用持久化; + 可参数化节点名以支持多 Agent 图合并。 + """ + from agent.tools.tool_agent_call import make_parallel_tool_node + + workflow = StateGraph(AgentState) + workflow.add_node("agent", make_call_model_node(model_with_tools, trajectory_rounds=trajectory_rounds)) + + # 根据 parallel_tools 选择工具执行节点 + if parallel_tools: + workflow.add_node("tools", make_parallel_tool_node(tools)) + else: + workflow.add_node("tools", ToolNode(tools)) + + workflow.add_node("process_tool_artifact", process_tool_artifact) + + workflow.add_edge(START, "agent") + workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END}) + workflow.add_edge("tools", "process_tool_artifact") + workflow.add_edge("process_tool_artifact", "agent") + return workflow.compile() + + + + +# ---------- 图注册中心 ---------- +# 格式: {图名: {"builder": 构建函数, "display_name": 显示名, "description": 描述}} +# 用户可在此追加自定义图定义。 +# 图结构可视化通过 Mermaid PNG 自动生成,无需手动配置 nodes/edges。 + +GRAPH_REGISTRY: dict[str, dict[str, Any]] = { + "react": { + "builder": build_agent_graph, + "display_name": "ReAct (标准)", + "description": "标准 ReAct 循环:agent 推理 -> 工具执行 -> 结果处理 -> agent 继续推理,直至完成。适用于绝大多数单 Agent 场景。", + }, +} + + +def get_available_graphs() -> list[dict]: + """返回所有已注册图的信息列表,供前端下拉框与图可视化使用。 + + 输出: [{"name": str, "display_name": str, "description": str}, ...] + """ + result = [] + for name, info in GRAPH_REGISTRY.items(): + result.append({ + "name": name, + "display_name": info.get("display_name", name), + "description": info.get("description", ""), + }) + return result + + +def get_graph_png_bytes(graph_name: str) -> bytes | None: + """为指定图名构建编译图并生成 Mermaid PNG 字节。 + + 输入: + graph_name: 图名(如 "react"),必须在 GRAPH_REGISTRY 中。 + + 输出: + PNG 字节数据,失败返回 None。 + + 系统定位: + 供前端 /api/config/graphs/png/{graph_name} 接口使用, + 动态生成图结构可视化图片。 + + 如何新增图: + 1. 在 GRAPH_REGISTRY 中注册: {"builder": your_build_fn, "display_name": "名称", "description": "描述"} + 2. builder 函数签名为 (tools, model_with_tools, **extra_kwargs) -> CompiledGraph + 3. PNG 自动生成,无需额外配置 + """ + from unittest.mock import MagicMock + + entry = GRAPH_REGISTRY.get(graph_name) + if entry is None: + return None + + try: + # 使用 minimal mock 构建仅用于可视化的编译图 + mock_llm = MagicMock() + mock_llm.bind_tools.return_value = MagicMock() + mock_tools: list = [] + compiled = entry["builder"](mock_tools, mock_llm) + graph_obj = compiled.get_graph() + return graph_obj.draw_mermaid_png() + except Exception as e: + print(f"[GraphViz] 生成图 '{graph_name}' 的 PNG 失败: {e}") + return None + + +def build_graph_by_name(graph_name: str, tools: list[Any], model_with_tools: Any, **extra_kwargs): + """根据图名从注册中心查找构建函数并构建编译后的图。 + + 输入: + graph_name: 图名(如 "react"),必须在 GRAPH_REGISTRY 中。 + tools: LangChain BaseTool 列表。 + model_with_tools: 已 bind_tools 的 LLM。 + **extra_kwargs: 传递给 builder 的额外参数(如 trajectory_rounds)。 + + 输出: + 编译后的 LangGraph CompiledGraph。 + + 异常: + ValueError: graph_name 未在注册中心找到。 + """ + entry = GRAPH_REGISTRY.get(graph_name) + if entry is None: + available = list(GRAPH_REGISTRY.keys()) + raise ValueError(f"未找到图 '{graph_name}',可用图: {available}") + builder = entry["builder"] + return builder(tools, model_with_tools, **extra_kwargs) diff --git a/src/agent/core/graph_builder.py b/src/agent/core/graph_builder.py deleted file mode 100644 index 1168926..0000000 --- a/src/agent/core/graph_builder.py +++ /dev/null @@ -1,53 +0,0 @@ -"""LangGraph ReAct 工作流图构建。 - -系统定位: - 将 LLM(已 bind_tools)、工具列表组装为可编译的 StateGraph, - 供 ``agents/main_agent`` 及各子 Agent 的 ``AgentRuntime`` 使用。 - -图结构: - START -> agent -> (tools | END) - tools -> process_tool_artifact -> agent - -可扩展性: - - 可增加 checkpoint、interrupt 节点、子图路由。 - - 可为不同 Agent 注入不同的 conditional_edges 或额外节点(如 RAG 检索)。 -""" -from __future__ import annotations - -from typing import Any - -from langgraph.graph import END, START, StateGraph -from langgraph.prebuilt import ToolNode - -from agent.core.nodes import make_call_model_node, process_tool_artifact -from agent.core.routing import should_continue -from agent.core.state import AgentState - - -def build_agent_graph(tools: list[Any], model_with_tools: Any): - """构建并编译标准 ReAct Agent 图。 - - 输入: - tools: LangChain BaseTool 列表,供 ToolNode 执行。 - model_with_tools: 已 ``bind_tools`` 的聊天模型,供 agent 节点调用。 - - 输出: - 编译后的 LangGraph CompiledGraph,支持 ``invoke({"messages": ...})``。 - - 系统定位: - ``agents/main_agent`` 入口;子 Agent 可复用同一拓扑并替换 tools/model。 - - 可扩展性: - 返回前可 ``workflow.compile(checkpointer=...)`` 启用持久化; - 可参数化节点名以支持多 Agent 图合并。 - """ - workflow = StateGraph(AgentState) - workflow.add_node("agent", make_call_model_node(model_with_tools)) - workflow.add_node("tools", ToolNode(tools)) - workflow.add_node("process_tool_artifact", process_tool_artifact) - - workflow.add_edge(START, "agent") - workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END}) - workflow.add_edge("tools", "process_tool_artifact") - workflow.add_edge("process_tool_artifact", "agent") - return workflow.compile() diff --git a/src/agent/core/gui_config.json b/src/agent/core/gui_config.json new file mode 100644 index 0000000..7359ba5 --- /dev/null +++ b/src/agent/core/gui_config.json @@ -0,0 +1,22 @@ +{ + "gui_monitor_name": "\\\\.\\DISPLAY5", + "gui_monitor_index": 0, + "monitors_snapshot": [ + { + "name": "\\\\.\\DISPLAY5", + "x": 0, + "y": 0, + "width": 2560, + "height": 1440, + "is_primary": true + }, + { + "name": "\\\\.\\DISPLAY1", + "x": -1350, + "y": 149, + "width": 1350, + "height": 2400, + "is_primary": false + } + ] +} \ No newline at end of file diff --git a/src/agent/core/human_interaction.py b/src/agent/core/human_interaction.py deleted file mode 100644 index f0bf1ba..0000000 --- a/src/agent/core/human_interaction.py +++ /dev/null @@ -1,75 +0,0 @@ -"""人工交互(HITL)参数规范化与结果文案生成。 - -系统定位: - 连接 ``human_interaction`` 工具与 ``core/approvals`` / ``core/runtime``, - 将模型发起的人工交互请求转为 UI 可展示结构,并在用户操作后生成 - ToolMessage 内容供图继续执行。 - -可扩展性: - - 新增 interaction_type 时同步扩展 ``normalize_human_interaction_args`` - 与 ``human_interaction_result`` 的分支。 - - 可与前端约定更丰富的 payload(表单字段、文件上传等)。 -""" -from __future__ import annotations - -from typing import Any - -from agent.tools.tool import HumanInteraction - - -def normalize_human_interaction_args(args: dict[str, Any]) -> dict[str, Any]: - """规范化 human_interaction 工具参数为固定 schema。 - - 输入: - args: 模型传入的原始参数字典。 - - 输出: - 含 interaction_type、title、prompt、options、suggested_response 的字典; - interaction_type 限定为 information | selection | approval。 - - 系统定位: - ``approvals.build_human_interaction_meta`` 构建待确认 UI 前调用。 - - 可扩展性: - 可增加 locale、超时、默认值校验;options 可支持 label/value 结构。 - """ - normalized = dict(args or {}) - normalized["interaction_type"] = str(normalized.get("interaction_type") or "information") - if normalized["interaction_type"] not in {"information", "selection", "approval"}: - normalized["interaction_type"] = "information" - normalized["title"] = str(normalized.get("title") or "需要人工处理") - normalized["prompt"] = str(normalized.get("prompt") or "请处理后继续。") - normalized["options"] = HumanInteraction.normalize_options(normalized.get("options")) - normalized["suggested_response"] = str(normalized.get("suggested_response") or "") - return normalized - - -def human_interaction_result(pending: dict[str, Any], decision: str, instruction: str | None) -> str: - """根据用户决策生成写入 ToolMessage 的文本结果。 - - 输入: - pending: PENDING_TOOL_APPROVALS 中 type=human_interaction 的条目。 - decision: approve | reject | edit。 - instruction: 用户输入文本(补充信息、选项、修改说明等)。 - - 输出: - 供 LangGraph 继续推理的 ToolMessage 字符串内容。 - - 系统定位: - ``runtime.continue_after_human_action`` 在人工交互分支调用。 - - 可扩展性: - 可返回结构化 JSON 字符串,供下游节点解析;支持多语言模板。 - """ - tool_call = pending.get("tool_call") or {} - args = normalize_human_interaction_args(tool_call.get("args") or {}) - interaction_type = args.get("interaction_type") or "information" - text = (instruction or "").strip() - if interaction_type == "approval": - if decision == "reject": - return "用户拒绝" - return "用户同意" - if interaction_type == "selection": - selected = text or args.get("suggested_response") or "" - return f"用户选择:{selected}" - return f"用户补充信息如下:{text}" diff --git a/src/agent/core/llm.py b/src/agent/core/llm.py index 37c1db3..c4a083a 100644 --- a/src/agent/core/llm.py +++ b/src/agent/core/llm.py @@ -476,13 +476,10 @@ class ChatQwen(ChatOpenAI): # ---------- 打印原始 tool_calls ---------- try: raw = response if isinstance(response, dict) else response.model_dump() - print("[RAW_RESPONSE]", json.dumps(raw, indent=2, ensure_ascii=False)) choices = raw.get("choices", []) if choices: msg = choices[0].get("message", {}) tool_calls = msg.get("tool_calls") - if tool_calls: - print("[RAW_TOOLCALLS]", json.dumps(tool_calls, indent=2, ensure_ascii=False)) except Exception: pass diff --git a/src/agent/core/multi_agent_graph.py b/src/agent/core/multi_agent_graph.py index a5f6ccf..40f3ee5 100644 --- a/src/agent/core/multi_agent_graph.py +++ b/src/agent/core/multi_agent_graph.py @@ -350,6 +350,7 @@ def make_main_agent_node( tools: list[Any], agent_name: str = "main", tools_by_name: dict[str, Any] | None = None, + trajectory_rounds: int = 3, ) -> Callable[[dict], dict]: """创建主 Agent 的图节点(含完整的 ReAct 循环 + 委派能力)。 @@ -362,6 +363,7 @@ def make_main_agent_node( tools: 工具列表(含 agent_call 工具)。 agent_name: 主 agent 名称。 tools_by_name: 工具名映射。 + trajectory_rounds: 短期记忆保留最近 N 轮工具返回结果。 输出: LangGraph 节点函数。 @@ -377,8 +379,10 @@ def make_main_agent_node( # 执行完整的 ReAct 循环(限制步数防止无限循环) max_steps = 50 for step in range(max_steps): - # 调用 LLM - response = model_with_tools.invoke(messages) + # 过滤短期记忆后再调用 LLM + from agent.core.nodes import _filter_short_term_memory + filtered = _filter_short_term_memory(messages, trajectory_rounds) + response = model_with_tools.invoke(filtered) messages.append(response) # 记录工具调用事件 @@ -471,6 +475,7 @@ def build_multi_agent_graph( main_tools_by_name: dict[str, Any], sub_agents: list[dict[str, Any]], main_agent_name: str = "main", + trajectory_rounds: int = 3, ) -> Any: """构建一主多子 SupervisorGraph。 @@ -505,7 +510,7 @@ def build_multi_agent_graph( # 主 Agent 节点 workflow.add_node( main_agent_name, - make_main_agent_node(main_model_with_tools, main_tools, main_agent_name, main_tools_by_name), + make_main_agent_node(main_model_with_tools, main_tools, main_agent_name, main_tools_by_name, trajectory_rounds=trajectory_rounds), ) # 子 Agent 节点 diff --git a/src/agent/core/nodes.py b/src/agent/core/nodes.py index 7803af5..bdf251f 100644 --- a/src/agent/core/nodes.py +++ b/src/agent/core/nodes.py @@ -2,7 +2,7 @@ 系统定位: 实现 ReAct 循环中的 ``agent`` 节点工厂与 ``process_tool_artifact`` 节点, - 被 ``graph_builder`` 注册到 StateGraph。 + 被 ``graph`` 注册到 StateGraph。 可扩展性: - ``make_call_model_node`` 可注入 streaming、重试、用量统计回调。 @@ -19,11 +19,59 @@ from agent.utils.image_utils import image_bytes_to_openai_image_url_part from agent.utils.tool_call_utils import normalize_tool_call -def make_call_model_node(model_with_tools: Any) -> Callable[[AgentState], dict]: +def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list: + """过滤短期记忆:保留所有工具调用,工具返回结果仅保留最近 trajectory_rounds 轮。 + + 输入: + messages: 完整消息列表。 + trajectory_rounds: 保留最近 N 轮的工具返回结果。0 或负数表示不过滤。 + + 输出: + 过滤后的消息列表。 + + 系统定位: + call_model 节点在调用 LLM 前进行消息裁剪,避免 GUI 等多轮工具调用 + 场景下上下文过长。 + + 规则: + - 保留所有非 ToolMessage 消息(SystemMessage、HumanMessage、AIMessage) + - ToolMessage 仅保留最近 trajectory_rounds 轮工具调用对应的返回结果 + - 每轮由一个 AIMessage(含 tool_calls)定义,其后的 ToolMessage 属于该轮 + """ + if trajectory_rounds <= 0: + return list(messages) + + # 从后向前遍历,统计已遇到的含 tool_calls 的 AIMessage 轮数 + rounds_seen = 0 + kept_indices = set() + + for i in range(len(messages) - 1, -1, -1): + msg = messages[i] + from langchain_core.messages import AIMessage, ToolMessage + + if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None): + rounds_seen += 1 + + if isinstance(msg, ToolMessage): + if rounds_seen < trajectory_rounds: + kept_indices.add(i) + else: + kept_indices.add(i) + + return [messages[i] for i in sorted(kept_indices)] + + +def make_call_model_node( + model_with_tools: Any, + trajectory_rounds: int = 3, +) -> Callable[[AgentState], dict]: """创建调用已绑定工具的 LLM 的图节点闭包。""" def call_model(state: AgentState) -> dict: - response = model_with_tools.invoke(state["messages"]) + # 过滤短期记忆后再传给 LLM + all_messages = list(state["messages"]) + filtered_messages = _filter_short_term_memory(all_messages, trajectory_rounds) + response = model_with_tools.invoke(filtered_messages) if hasattr(response, "tool_calls") and response.tool_calls: for tc in response.tool_calls: @@ -52,11 +100,8 @@ def make_call_model_node(model_with_tools: Any) -> Callable[[AgentState], dict]: def process_tool_artifact(state: AgentState) -> dict: last_msg = state["messages"][-1] + # ---- 实时记录工具结果到前端 ---- if isinstance(last_msg, ToolMessage): - content_preview = str(last_msg.content) - if len(content_preview) > 200: - content_preview = content_preview[:200] + "..." - try: from agent.history.tool_call_recorder import record_tool_result_live, get_current_session sid = get_current_session() diff --git a/src/agent/core/routing.py b/src/agent/core/routing.py index 8aa88fd..22356f3 100644 --- a/src/agent/core/routing.py +++ b/src/agent/core/routing.py @@ -1,7 +1,7 @@ """LangGraph 条件路由:工具循环 vs 结束 vs 待确认暂停。 系统定位: - 连接 ``graph_builder`` 的条件边与 ``runtime.execute_agent`` 的 + 连接 ``graph`` 的条件边与 ``runtime.execute_agent`` 的 后处理逻辑,在模型输出 tool_calls 时决定图走向或提取 UI 待确认元数据。 可扩展性: @@ -31,7 +31,7 @@ def should_continue(state: AgentState) -> Literal["tools", "__end__"]: ``"tools"`` 进入 ToolNode;``"__end__"`` 结束图(含需确认工具场景)。 系统定位: - ``graph_builder`` 中 agent 节点的 conditional_edges 路由函数。 + ``graph`` 中 agent 节点的 conditional_edges 路由函数。 若任一 tool_call 策略为 confirm,则直接 END,由 runtime 展示确认 UI。 可扩展性: diff --git a/src/agent/core/runtime.py b/src/agent/core/runtime.py index 0baad0e..cd00e99 100644 --- a/src/agent/core/runtime.py +++ b/src/agent/core/runtime.py @@ -18,11 +18,11 @@ from dataclasses import dataclass from typing import Any, Literal from langchain_core.messages import AIMessage, HumanMessage, ToolMessage +from langgraph.errors import GraphRecursionError from langchain_core.messages import messages_from_dict from langgraph.errors import GraphRecursionError -from agent.core.approvals import PENDING_TOOL_APPROVALS, pending_assistant_message, remove_pending_ui_message -from agent.core.human_interaction import human_interaction_result +from agent.core.approvals import PENDING_TOOL_APPROVALS, pending_assistant_message, remove_pending_ui_message, human_interaction_result from agent.history.tool_call_recorder import save_turn, start_live_turn, end_live_turn, set_current_turn, set_current_session from agent.memory.system_prompt import TEAM_MODE_PROMPT from agent.core.routing import first_pending_tool_meta @@ -61,6 +61,7 @@ class AgentRuntime: tools_by_name: dict[str, Any] system_prompt: str max_iterations: int = 200 + trajectory_rounds: int = 3 _OOM_MESSAGE = "OOM: 已达到最大迭代次数限制,任务未能完成。请简化任务或增加 max_iterations 配置后重试。" @@ -196,15 +197,33 @@ def _invoke_after_manual_tool_result( tool_call = normalize_tool_call(raw_tool_call) tool_name = tool_call.get("name") tool_args = tool_call.get("args") or {} + tool_call_id = tool_call.get("id") or f"manual_{session_id}_{len(next_messages)}" if classify_tool_execution(tool_name, tool_args) != "direct": continue tool = runtime.tools_by_name.get(tool_name or "") if not tool: continue - result = tool.invoke(tool_args) - tool_content = result[0] if isinstance(result, tuple) else result - call_id = tool_call.get("id") or f"manual_{session_id}_{len(next_messages)}" - next_messages.append(ToolMessage(content=str(tool_content), tool_call_id=call_id, name=tool_name or "unknown_tool")) + # 构造完整 ToolCall 格式,确保 content_and_artifact 工具的 artifact 被保留 + tool_call_input = { + "name": tool_name, + "args": tool_args, + "id": tool_call_id, + "type": "tool_call", + } + result = tool.invoke(tool_call_input) + if isinstance(result, ToolMessage): + tool_content = str(result.content) if result.content else "" + tool_artifact = getattr(result, "artifact", None) + elif isinstance(result, tuple): + tool_content = str(result[0]) if result[0] else "" + tool_artifact = result[1] if len(result) > 1 else None + else: + tool_content = str(result) + tool_artifact = None + msg_kwargs = {"content": tool_content, "tool_call_id": tool_call_id, "name": tool_name or "unknown_tool"} + if tool_artifact: + msg_kwargs["artifact"] = tool_artifact + next_messages.append(ToolMessage(**msg_kwargs)) progressed = True if not progressed: return all_messages, None @@ -324,6 +343,7 @@ def execute_agent( "output_type": output_type_from_config, "autoplay": False, "audio": audio_data.get("audio"), + "turn_id": turn_id, } if output_type_from_config == "audio" and assistant_meta.get("audio"): @@ -423,7 +443,8 @@ def continue_after_human_action( traceback.print_exc() content = "处理人工交互结果时遇到问题。" persisted_messages = messages_from_dict(pending.get("messages") or []) - chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False}}) + human_turn_id = f"human_{approval_id}" + chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False, "turn_id": human_turn_id}}) _persist_messages_from_pending_turn(session_id, persisted_messages, pending) if session_id: @@ -441,11 +462,29 @@ def continue_after_human_action( return chat_history try: - result = tool.invoke(tool_args) - tool_content = result[0] if isinstance(result, tuple) else result + # 构造完整 ToolCall 格式,确保 content_and_artifact 工具的 artifact 被保留 + tool_call_input = { + "name": tool_name, + "args": tool_args, + "id": tool_call.get("id", ""), + "type": "tool_call", + } + result = tool.invoke(tool_call_input) + if isinstance(result, ToolMessage): + tool_content = str(result.content) if result.content else "" + tool_artifact = getattr(result, "artifact", None) + elif isinstance(result, tuple): + tool_content = str(result[0]) if result[0] else "" + tool_artifact = result[1] if len(result) > 1 else None + else: + tool_content = str(result) + tool_artifact = None prior_messages = messages_from_dict(pending.get("messages") or []) call_id = tool_call.get("id") or f"manual_{approval_id}" - prior_messages.append(ToolMessage(content=str(tool_content), tool_call_id=call_id, name=tool_name)) + msg_kwargs = {"content": tool_content, "tool_call_id": call_id, "name": tool_name} + if tool_artifact: + msg_kwargs["artifact"] = tool_artifact + prior_messages.append(ToolMessage(**msg_kwargs)) all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id) if pending_meta: _remember_pending_user_content(pending_meta, pending.get("user_content")) @@ -468,9 +507,10 @@ def continue_after_human_action( ]) persisted_messages = prior_messages - chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False}}) + manual_turn_id = f"human_{approval_id}" + chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False, "turn_id": manual_turn_id}}) _persist_messages_from_pending_turn(session_id, persisted_messages, pending) if session_id: - save_turn(session_id, f"human_{approval_id}", {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages) + save_turn(session_id, manual_turn_id, {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages) sync_manifest_from_chat_history(session_id, chat_history, "") return chat_history diff --git a/src/agent/core/state.py b/src/agent/core/state.py index aaf9c01..1c88012 100644 --- a/src/agent/core/state.py +++ b/src/agent/core/state.py @@ -1,7 +1,7 @@ """LangGraph Agent 状态类型定义。 系统定位: - 定义图运行时共享状态 ``AgentState``,被 ``graph_builder``、``nodes``、 + 定义图运行时共享状态 ``AgentState``,被 ``graph``、``nodes``、 ``routing`` 作为 StateGraph 的 schema 使用。 可扩展性: diff --git a/src/agent/core/tool_config.json b/src/agent/core/tool_config.json index 2b49144..677b157 100644 --- a/src/agent/core/tool_config.json +++ b/src/agent/core/tool_config.json @@ -11,7 +11,7 @@ "name": "date_query_class", "display_name": "DateQuery", "description": "一个日期查询工具,可以返回当前的日期信息。仅当需要查询当前时间时调用,其余任何情况严禁调用。", - "permission": "confirm", + "permission": "direct", "auto_execute_rules": [], "enabled": true }, @@ -50,7 +50,13 @@ "display_name": "SoftwareControl", "description": "操作本机已安装的软件。工具会自动扫描 Windows 开始菜单快捷方式、桌面快捷方式和 App Paths,建立软件索引后根据用户输入做模糊匹配。action=open 时打开软件;action=close 时仅做正常关闭;action=force_close 时才允许在正常关闭失败后强制结束进程。仅当用户明确要求打开、关闭或强制关闭某个本机软件时调用。", "permission": "confirm", - "auto_execute_rules": [], + "auto_execute_rules": [ + { + "parameter": "action", + "operator": "equals", + "value": "open" + } + ], "enabled": true }, { @@ -90,5 +96,13 @@ "permission": "direct", "auto_execute_rules": [], "enabled": true + }, + { + "name": "call_subagent", + "display_name": "CallSubAgentTool", + "description": "调用已启用的子 Agent 执行指定任务。子 Agent 会独立运行并返回最终结果。", + "permission": "direct", + "auto_execute_rules": [], + "enabled": true } ] \ No newline at end of file diff --git a/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc b/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc index e12d71e..84098e9 100644 Binary files a/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc and b/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc differ diff --git a/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc b/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc deleted file mode 100644 index 25cc524..0000000 Binary files a/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc and /dev/null differ diff --git a/src/agent/history/data_base/chat_history.db b/src/agent/history/data_base/chat_history.db index 90ce534..b2020db 100644 Binary files a/src/agent/history/data_base/chat_history.db and b/src/agent/history/data_base/chat_history.db differ diff --git a/src/agent/history/multi_inputs/20260618_111759/ui_messages.json b/src/agent/history/multi_inputs/20260618_111759/ui_messages.json deleted file mode 100644 index f0ff54d..0000000 --- a/src/agent/history/multi_inputs/20260618_111759/ui_messages.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "version": 3, - "messages": [ - { - "role": "user", - "user": { - "content": [ - { - "type": "text", - "text": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划" - } - ] - } - }, - { - "role": "assistant", - "assistant": { - "text": "已为您生成详细的**西红柿炒鸡蛋**制作计划,共分为8个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n2. **处理食材**:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n3. **热锅倒油**:开中火加热炒锅,倒入适量食用油,待油微热\n4. **炒鸡蛋**:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n5. **炒西红柿**:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n6. **调味**:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n7. **混合**:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n8. **出锅**:撒上葱花点缀,装盘即可享用\n\n祝您烹饪愉快!如果需要标记某一步骤完成,请告诉我。", - "output_type": "text", - "autoplay": false, - "audio_duration_seconds": null, - "audio_relpath": null, - "pending_action": null, - "agent_mode": null - } - }, - { - "role": "user", - "user": { - "content": [ - { - "type": "text", - "text": "你的上下文中有什么" - } - ] - } - }, - { - "role": "assistant", - "assistant": { - "text": "需要你确认后继续执行。", - "output_type": "text", - "autoplay": false, - "audio_duration_seconds": null, - "audio_relpath": null, - "pending_action": { - "id": "b5de8b0742564742ba20ec9df9803b51", - "type": "tool_call", - "title": "待确认执行工具:rag_tool", - "tool_name": "rag_tool", - "args": { - "name": "rag_tool", - "args": { - "action_type": "list_sources" - } - }, - "instruction": "{\n \"name\": \"rag_tool\",\n \"args\": {\n \"action_type\": \"list_sources\"\n }\n}", - "options": [ - "approve", - "reject" - ], - "resolved": true - }, - "agent_mode": null - } - }, - { - "role": "assistant", - "assistant": { - "text": "已拒绝,本次待确认操作不会执行。", - "output_type": "text", - "autoplay": false, - "audio_duration_seconds": null, - "audio_relpath": null, - "pending_action": null, - "agent_mode": null - } - } - ] -} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260620_170530/ui_messages.json b/src/agent/history/multi_inputs/20260620_170530/ui_messages.json new file mode 100644 index 0000000..0b5082d --- /dev/null +++ b/src/agent/history/multi_inputs/20260620_170530/ui_messages.json @@ -0,0 +1,120 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我点击加号新建标签页" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "已成功点击加号,新建了一个标签页。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我左键点击当前视频进行暂停" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "已成功左键点击当前视频画面,视频已暂停。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我点击加号新建标签页" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "已成功点击加号,新建了一个标签页。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我点击加号新建标签页" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "已成功点击加号,新建了一个标签页。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "截张图看看我再干啥,如果读不到图片请直接告诉我" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "根据截图,我可以看到你正在使用一个浏览器(看起来像是 Edge 浏览器),并且打开了多个标签页。\n\n当前屏幕显示的内容包括:\n1. **浏览器顶部**:有几个打开的标签页,包括“哔哩哔哩”、“【野火】这视频怎么讲?... ”和当前的“新建标签页”。\n2. **页面内容**:这是一个浏览器的新标签页界面,背景是雪山风景图。中间有一个搜索框,下方有一些快捷方式图标,例如“哔哩哔哩”、“京东直播”、“山东大学”等。\n3. **底部任务栏**:显示了 Windows 系统的任务栏,有开始菜单、搜索、文件资源管理器、Edge 浏览器、微信等图标。右下角显示时间是 11:24,日期是 2025/8/27。\n\n所以,你目前的状态是在电脑上使用浏览器浏览网页或准备搜索新的内容。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260620_174345/ui_messages.json b/src/agent/history/multi_inputs/20260620_174345/ui_messages.json new file mode 100644 index 0000000..d392599 --- /dev/null +++ b/src/agent/history/multi_inputs/20260620_174345/ui_messages.json @@ -0,0 +1,51 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "截张图看看我再干啥,如果读不到图片请直接告诉我" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "根据截图,你当前正在使用 **Google Chrome** 浏览器。\n\n屏幕上打开了两个标签页:\n1. **左侧标签页**:显示的是 **YouTube** 网站,页面标题是 \"YouTube\",看起来可能是在浏览视频列表或者首页。\n2. **右侧标签页(当前激活)**:显示的是 **Wikipedia(维基百科)** 的中文页面,具体是关于 **\"中国\"** (China) 的条目。页面内容展示了中国的地图、国旗以及相关的介绍文字。\n\n此外,任务栏上还可以看到其他已打开的应用程序图标,包括文件资源管理器、Edge 浏览器等。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "截张图看看我再干啥,如果读不到图片请直接告诉我" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "截图已获取。\n\n从截图中可以看到,你当前正在使用 **Google Chrome** 浏览器。\n1. **左侧标签页**:打开的是 **YouTube** 网站。\n2. **右侧标签页(当前激活)**:显示的是 **Wikipedia(维基百科)** 的中文页面,标题为“中国”。\n\n你似乎正在浏览关于中国的百科信息。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260618_173653/ui_messages.json b/src/agent/history/multi_inputs/20260620_174454/ui_messages.json similarity index 68% rename from src/agent/history/multi_inputs/20260618_173653/ui_messages.json rename to src/agent/history/multi_inputs/20260620_174454/ui_messages.json index 0dc4ce4..8b1ed8a 100644 --- a/src/agent/history/multi_inputs/20260618_173653/ui_messages.json +++ b/src/agent/history/multi_inputs/20260620_174454/ui_messages.json @@ -7,7 +7,7 @@ "content": [ { "type": "text", - "text": "帮我关掉DeepSeek开放平台的标签页" + "text": "帮我点击加号新建标签页" } ] } @@ -15,7 +15,7 @@ { "role": "assistant", "assistant": { - "text": "已经帮您关闭了 DeepSeek 开放平台的标签页。现在浏览器显示的是 minor - Chatbot 页面。", + "text": "已为您点击新建标签页。现在应该已经打开了一个新的空白标签页。", "output_type": "text", "autoplay": false, "audio_duration_seconds": null, diff --git a/src/agent/history/multi_inputs/20260620_183029/ui_messages.json b/src/agent/history/multi_inputs/20260620_183029/ui_messages.json new file mode 100644 index 0000000..2fe6590 --- /dev/null +++ b/src/agent/history/multi_inputs/20260620_183029/ui_messages.json @@ -0,0 +1,51 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "截张图看看我在干啥,如果读不到图片请直接告诉我" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "根据截图,你正在使用 **DeepSeek 开放平台**(platform.deepseek.com)的网页界面。\n\n具体来说,你当前查看的是 **“用量信息”** 页面。页面上显示了以下关键信息:\n* **充值余额**:¥22.77 CNY\n* **六月消费**(按 UTC+0 时间):¥17.22 CNY\n* **每月用量图表**:展示了近期(6月1日至6月30日)的消费金额波动。\n* **模型使用情况**:下方还列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n简单来说,你正在查看自己在 DeepSeek 平台上的账户余额、消费记录以及 API 的使用情况。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "你现在的对话记录中有图片吗" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "根据刚才截取的屏幕截图,我**没有**在当前的对话记录中看到任何图片。\n\n这张截图显示的是你正在浏览 **DeepSeek 开放平台** 的“用量信息”页面,主要内容包括:\n1. **充值余额**:¥22.77 CNY\n2. **六月消费**(按 UTC+0 时间):¥17.22 CNY\n3. **每月用量图表**:展示了近期每天的消费金额波动。\n4. **模型使用情况**:下方列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n所以,目前的对话中只有文字交互,并没有上传图片。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260620_205109/ui_messages.json b/src/agent/history/multi_inputs/20260620_205109/ui_messages.json new file mode 100644 index 0000000..1aaa0ae --- /dev/null +++ b/src/agent/history/multi_inputs/20260620_205109/ui_messages.json @@ -0,0 +1,51 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "能不能帮我打开虎牙直播" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "虎牙直播已经成功打开了!可以看到:\n\n- 页面顶部显示\"虎牙直播\"的Logo\n- 当前正在播放主播\"王子酱\"的直播间\n- 页面下方展示了各种游戏分类,包括网游竞技(LOL、CS2等)、单机热游等\n- 右侧还有其他推荐的主播和直播间\n\n你现在可以浏览你感兴趣的直播内容了。如果你想观看特定的主播或游戏类别,可以直接点击相应的选项。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我打开b站" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "好的,B站(bilibili)已经成功打开。\n\n页面显示了:\n* **顶部导航栏**:包含首页、番剧、直播、游戏中心等主要分区。\n* **推荐内容**:首页展示了多个视频推荐,例如左侧的“BML-PLAY! 2025”活动预告,以及右侧的“GPT如何解决80年未解单位距离猜想?”等热门视频。\n* **分类标签**:可以看到动画、番剧、国创、音乐、舞蹈、游戏等多个内容分类。\n\n你现在可以浏览你感兴趣的内容了。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260621_114011/ui_messages.json b/src/agent/history/multi_inputs/20260621_114011/ui_messages.json new file mode 100644 index 0000000..08ef6a7 --- /dev/null +++ b/src/agent/history/multi_inputs/20260621_114011/ui_messages.json @@ -0,0 +1,28 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我打开b站" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "B站(哔哩哔哩)已经为您打开了。请问有什么我可以帮您的吗?", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/tool_call_recorder.py b/src/agent/history/tool_call_recorder.py index 4fe939c..5ade465 100644 --- a/src/agent/history/tool_call_recorder.py +++ b/src/agent/history/tool_call_recorder.py @@ -176,28 +176,35 @@ def session_tool_dir(session_id: str) -> Path: def extract_tool_calls_from_messages(messages: list[BaseMessage]) -> list[dict[str, Any]]: """从图执行后的消息列表中提取完整的工具调用链。""" records: list[dict[str, Any]] = [] - pending: dict[str, dict[str, Any]] = {} + pending: list[dict[str, Any]] = [] + # 同名工具调用计数器,避免多张截图文件名冲突 + name_counter: dict[str, int] = {} for msg in messages: if isinstance(msg, AIMessage) and msg.tool_calls: for tc in msg.tool_calls: d = tc if isinstance(tc, dict) else {"name": tc.name, "args": tc.args, "id": tc.id} - if d.get("id"): - pending[d["id"]] = { - "name": d.get("name", "unknown"), - "args": _json_safe(d.get("args", {})), - "result_text": "", - "result_files": [], - } - elif isinstance(msg, ToolMessage) and msg.tool_call_id: - cid = msg.tool_call_id - if cid in pending: - entry = pending.pop(cid) + pending.append({ + "name": d.get("name", "unknown"), + "args": _json_safe(d.get("args", {})), + "result_text": "", + "result_files": [], + "result_images": [], + }) + elif isinstance(msg, ToolMessage): + if pending: + entry = pending.pop(0) entry["result_text"] = str(msg.content) if msg.content else "" - if msg.artifact: - entry["result_files"] = _save_artifact_files(msg.artifact, msg.tool_call_id) + artifact = getattr(msg, "artifact", None) + if artifact: + tool_name = msg.name or entry["name"] + idx = name_counter.get(tool_name, 0) + name_counter[tool_name] = idx + 1 + unique_name = f"{tool_name}_{idx}" + result_files, image_bytes_map = _save_artifact_files(artifact, unique_name) + entry["result_files"] = result_files + entry["_image_bytes"] = image_bytes_map # 供 save_turn 落盘 records.append(entry) - for entry in pending.values(): - records.append(entry) + records.extend(pending) return records @@ -213,36 +220,29 @@ def _json_safe(value: Any) -> Any: return str(value) -def _save_artifact_files(artifact: Any, base_name: str) -> list[str]: +def _save_artifact_files(artifact: Any, base_name: str) -> tuple[list[str], dict[str, bytes]]: + """从 artifact 中提取文件名列表和 {文件名: bytes} 映射。""" files: list[str] = [] + bytes_map: dict[str, bytes] = {} if isinstance(artifact, dict) and "image_bytes" in artifact: - files.append(f"_artifact_{base_name}_0.png") + fname = f"_artifact_{base_name}_0.png" + files.append(fname) + bytes_map[fname] = artifact["image_bytes"] elif isinstance(artifact, list): for i, item in enumerate(artifact): if isinstance(item, dict) and "image_bytes" in item: - files.append(f"_artifact_{base_name}_{i}.png") + fname = f"_artifact_{base_name}_{i}.png" + files.append(fname) + bytes_map[fname] = item["image_bytes"] elif isinstance(item, bytes): - files.append(f"_artifact_{base_name}_{i}.png") + fname = f"_artifact_{base_name}_{i}.png" + files.append(fname) + bytes_map[fname] = item elif isinstance(artifact, bytes): - files.append(f"_artifact_{base_name}.png") - return files - - -def _find_artifact_bytes(messages: list[BaseMessage], fname: str) -> bytes | None: - for msg in messages: - if isinstance(msg, ToolMessage) and msg.artifact: - a = msg.artifact - if isinstance(a, dict) and "image_bytes" in a: - return a["image_bytes"] - if isinstance(a, list): - for item in a: - if isinstance(item, dict) and "image_bytes" in item: - return item["image_bytes"] - if isinstance(item, bytes): - return item - if isinstance(a, bytes): - return a - return None + fname = f"_artifact_{base_name}.png" + files.append(fname) + bytes_map[fname] = artifact + return files, bytes_map def save_turn(session_id: str, turn_id: str, user_message: dict[str, Any], @@ -263,12 +263,9 @@ def save_turn(session_id: str, turn_id: str, user_message: dict[str, Any], _ensure_dir(turn_dir) for tc in tool_calls: - for fname in list(tc.get("result_files", [])): - img_bytes = _find_artifact_bytes(messages, fname) - if img_bytes: - (turn_dir / fname).write_bytes(img_bytes) - else: - tc["result_files"].remove(fname) + image_bytes_map = tc.pop("_image_bytes", {}) + for fname, img_bytes in image_bytes_map.items(): + (turn_dir / fname).write_bytes(img_bytes) record = { "session_id": session_id, @@ -310,14 +307,40 @@ def get_tool_calls_for_latest_turn(session_id: str) -> list[dict[str, Any]]: latest = get_latest_turn(session_id) if not latest: return [] - turn_id = latest.get("turn_id", "") + return _tool_calls_from_turn_record(latest, session_id) + + +def get_tool_calls_for_turn(session_id: str, turn_id: str) -> list[dict[str, Any]]: + """获取指定 turn_id 的工具调用(供 UI 按消息还原)。""" + if not turn_id: + return [] + session_dir = session_tool_dir(session_id) + json_path = session_dir / f"turn_{turn_id}.json" + if not json_path.is_file(): + return [] + try: + with open(json_path, "r", encoding="utf-8") as f: + record = json.load(f) + except (json.JSONDecodeError, OSError): + return [] + return _tool_calls_from_turn_record(record, session_id) + + +def _tool_calls_from_turn_record(record: dict[str, Any], session_id: str) -> list[dict[str, Any]]: + """从 turn 记录中提取工具调用列表,补充图片路径。""" + turn_id = record.get("turn_id", "") result = [] - for tc in latest.get("tool_calls", []): + for tc in record.get("tool_calls", []): entry = dict(tc) full_files = [] for fname in tc.get("result_files", []): full_files.append(str(session_tool_dir(session_id) / turn_id / fname)) entry["result_files_full"] = full_files + # result_images 存图片文件路径而非 base64,前端通过 /api/tool-image 读取 + entry["result_images"] = [ + f"/api/tool-image?path={fp}&session={turn_id}" + for fp in full_files + ] entry["turn_id"] = turn_id result.append(entry) return result diff --git a/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json b/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json deleted file mode 100644 index 01635fe..0000000 --- a/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "session_id": "20260618_111759", - "turn_id": "20260618_111855", - "user_message": { - "role": "user", - "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划" - }, - "tool_calls": [ - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)", - "处理食材:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油,待油微热", - "炒鸡蛋:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂", - "调味:根据口味加入适量盐和少许糖提鲜,翻炒均匀", - "混合:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁", - "出锅:撒上葱花点缀,装盘即可享用" - ] - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a2-3\\u4e2a\\u9e21\\u86cb\\u30012\\u4e2a\\u4e2d\\u7b49\\u5927\\u5c0f\\u897f\\u7ea2\\u67ff\\u3001\\u8471\\u82b1\\u5c11\\u8bb8\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\\u3001\\u76d0\\u9002\\u91cf\\u3001\\u7cd6\\u5c11\\u8bb8\\uff08\\u53ef\\u9009\\uff09\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\uff0c\\u52a0\\u5c11\\u8bb8\\u76d0\\u6253\\u6563\\uff1b\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u8471\\u5207\\u6210\\u8471\\u82b1\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\uff0c\\u5f85\\u6cb9\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u7528\\u7b77\\u5b50\\u6216\\u94f2\\u5b50\\u5feb\\u901f\\u5212\\u6563\\uff0c\\u7092\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u5757\\u7ffb\\u7092\\uff0c\\u53ef\\u7528\\u94f2\\u5b50\\u538b\\u4e00\\u538b\\u5e2e\\u52a9\\u51fa\\u6c41\\uff0c\\u7092\\u81f3\\u8f6f\\u70c2\", \"\\u8c03\\u5473\\uff1a\\u6839\\u636e\\u53e3\\u5473\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\u548c\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff0c\\u7ffb\\u7092\\u5747\\u5300\", \"\\u6df7\\u5408\\uff1a\\u5c06\\u7092\\u597d\\u7684\\u9e21\\u86cb\\u5012\\u56de\\u9505\\u4e2d\\uff0c\\u4e0e\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u5747\\u5300\\uff0c\\u8ba9\\u9e21\\u86cb\\u5438\\u6536\\u6c64\\u6c41\", \"\\u51fa\\u9505\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u88c5\\u76d8\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": []}}\n[Todo List]\n- [1/8] [Pending]: 准备食材:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n- [2/8] [Pending]: 处理食材:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n- [3/8] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油,待油微热\n- [4/8] [Pending]: 炒鸡蛋:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n- [5/8] [Pending]: 炒西红柿:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n- [6/8] [Pending]: 调味:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n- [7/8] [Pending]: 混合:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n- [8/8] [Pending]: 出锅:撒上葱花点缀,装盘即可享用", - "result_files": [] - } - ], - "assistant_response": "已为您生成详细的**西红柿炒鸡蛋**制作计划,共分为8个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n2. **处理食材**:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n3. **热锅倒油**:开中火加热炒锅,倒入适量食用油,待油微热\n4. **炒鸡蛋**:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n5. **炒西红柿**:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n6. **调味**:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n7. **混合**:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n8. **出锅**:撒上葱花点缀,装盘即可享用\n\n祝您烹饪愉快!如果需要标记某一步骤完成,请告诉我。", - "timestamp": "20260618_111855" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json b/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json deleted file mode 100644 index 2a0d3cb..0000000 --- a/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "session_id": "20260618_112443", - "turn_id": "20260618_112445", - "user_message": { - "role": "user", - "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划" - }, - "tool_calls": [ - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量", - "准备调料:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅凉油:开中火加热炒锅,倒入适量食用油", - "炒鸡蛋:油热后倒入蛋液,快速划散至凝固,盛出备用", - "炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁变软", - "调味:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱", - "混合翻炒:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合", - "出锅装盘:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋" - ] - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a2-3\\u4e2a\\u9e21\\u86cb\\u30012-3\\u4e2a\\u4e2d\\u7b49\\u5927\\u5c0f\\u897f\\u7ea2\\u67ff\\u3001\\u8471\\u82b1\\u9002\\u91cf\", \"\\u51c6\\u5907\\u8c03\\u6599\\uff1a\\u98df\\u7528\\u6cb9\\u3001\\u76d0\\u3001\\u767d\\u7cd6\\uff08\\u53ef\\u9009\\uff0c\\u7528\\u4e8e\\u63d0\\u9c9c\\uff09\\u3001\\u756a\\u8304\\u9171\\uff08\\u53ef\\u9009\\uff09\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u51c9\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u6cb9\\u70ed\\u540e\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\uff0c\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u518d\\u52a0\\u5c11\\u8bb8\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u5757\\u7ffb\\u7092\\u81f3\\u51fa\\u6c41\\u53d8\\u8f6f\", \"\\u8c03\\u5473\\uff1a\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\u548c\\u5c11\\u8bb8\\u767d\\u7cd6\\uff0c\\u559c\\u6b22\\u6d53\\u90c1\\u53e3\\u611f\\u53ef\\u52a0\\u4e00\\u52fa\\u756a\\u8304\\u9171\", \"\\u6df7\\u5408\\u7ffb\\u7092\\uff1a\\u5c06\\u7092\\u597d\\u7684\\u9e21\\u86cb\\u5012\\u56de\\u9505\\u4e2d\\uff0c\\u4e0e\\u897f\\u7ea2\\u67ff\\u5747\\u5300\\u7ffb\\u7092\\u6df7\\u5408\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\\u7f8e\\u5473\\u7684\\u897f\\u7ea2\\u67ff\\u7092\\u9e21\\u86cb\"], \"done_steps\": []}}\n[Todo List]\n- [1/9] [Pending]: 准备食材:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量\n- [2/9] [Pending]: 准备调料:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)\n- [3/9] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [4/9] [Pending]: 热锅凉油:开中火加热炒锅,倒入适量食用油\n- [5/9] [Pending]: 炒鸡蛋:油热后倒入蛋液,快速划散至凝固,盛出备用\n- [6/9] [Pending]: 炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁变软\n- [7/9] [Pending]: 调味:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱\n- [8/9] [Pending]: 混合翻炒:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合\n- [9/9] [Pending]: 出锅装盘:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋", - "result_files": [] - } - ], - "assistant_response": "已为您生成详细的西红柿炒鸡蛋制作计划,共9个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量\n2. **准备调料**:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)\n3. **处理食材**:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n4. **热锅凉油**:开中火加热炒锅,倒入适量食用油\n5. **炒鸡蛋**:油热后倒入蛋液,快速划散至凝固,盛出备用\n6. **炒西红柿**:锅中再加少许油,放入西红柿块翻炒至出汁变软\n7. **调味**:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱\n8. **混合翻炒**:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合\n9. **出锅装盘**:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋\n\n祝您烹饪愉快!", - "timestamp": "20260618_112445" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json b/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json deleted file mode 100644 index 4668da3..0000000 --- a/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "session_id": "20260618_113558", - "turn_id": "20260618_113615", - "user_message": { - "role": "user", - "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划,并逐步更新这份计划" - }, - "tool_calls": [ - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ] - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": []}}\n[Todo List]\n- [1/7] [Pending]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 1 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 2 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 3 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 4 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 5 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 6 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4, 5]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Done]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": [ - "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", - "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", - "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", - "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", - "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", - "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", - "出锅装盘:撒上葱花点缀,即可享用" - ], - "done_step": 7 - }, - "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Done]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Done]: 出锅装盘:撒上葱花点缀,即可享用", - "result_files": [] - } - ], - "assistant_response": "太棒了!西红柿炒鸡蛋计划已全部完成。\n\n**最终状态:**\n- [x] 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [x] 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [x] 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [x] 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [x] 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [x] 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [x] 出锅装盘:撒上葱花点缀,即可享用\n\n现在你可以享受这道美味的家常菜了!🍅🥚", - "timestamp": "20260618_113615" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json b/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json deleted file mode 100644 index 886ed9a..0000000 --- a/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "session_id": "20260618_120438", - "turn_id": "20260618_120441", - "user_message": { - "role": "user", - "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划,并逐步更新这份计划" - }, - "tool_calls": [ - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": -1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': -1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": -1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': -1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": 1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": 1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": 1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": 1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": 1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - }, - { - "name": "todo_list", - "args": { - "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", - "done_step": 1 - }, - "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", - "result_files": [] - } - ], - "assistant_response": "", - "timestamp": "20260618_120441" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png b/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png deleted file mode 100644 index 13e1730..0000000 Binary files a/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json b/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json deleted file mode 100644 index 01a3ed4..0000000 --- a/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "session_id": "20260618_173653", - "turn_id": "20260618_173727", - "user_message": { - "role": "user", - "content": "帮我关掉DeepSeek开放平台的标签页" - }, - "tool_calls": [ - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "DeepSeek开放平台的标签页关闭按钮(X)" - }, - "result_text": "Clicked target. grounded (308,21)", - "result_files": [ - "_artifact_call_fd502781107945c7a6880635_0.png" - ] - } - ], - "assistant_response": "已经帮您关闭了 DeepSeek 开放平台的标签页。现在浏览器显示的是 minor - Chatbot 页面。", - "timestamp": "20260618_173727" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260620_174454/turn_20260620_174456.json b/src/agent/history/tool_calling/20260620_174454/turn_20260620_174456.json new file mode 100644 index 0000000..b5ed746 --- /dev/null +++ b/src/agent/history/tool_calling/20260620_174454/turn_20260620_174456.json @@ -0,0 +1,22 @@ +{ + "session_id": "20260620_174454", + "turn_id": "20260620_174456", + "user_message": { + "role": "user", + "content": "帮我点击加号新建标签页" + }, + "tool_calls": [ + { + "name": "gui_tool", + "args": { + "action": "click", + "element_description": "浏览器标签栏右侧的加号图标,用于新建标签页" + }, + "result_text": "", + "result_files": [], + "result_images": [] + } + ], + "assistant_response": "已为您点击新建标签页。现在应该已经打开了一个新的空白标签页。", + "timestamp": "20260620_174456" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png new file mode 100644 index 0000000..b9958e1 Binary files /dev/null and b/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png differ diff --git a/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png new file mode 100644 index 0000000..e431d64 Binary files /dev/null and b/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png differ diff --git a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json new file mode 100644 index 0000000..59962de --- /dev/null +++ b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json @@ -0,0 +1,23 @@ +{ + "session_id": "20260620_183029", + "turn_id": "20260620_183034", + "user_message": { + "role": "user", + "content": "截张图看看我在干啥,如果读不到图片请直接告诉我" + }, + "tool_calls": [ + { + "name": "gui_tool", + "args": { + "action": "screenshot" + }, + "result_text": "", + "result_files": [ + "_artifact_gui_tool_0_0.png" + ], + "result_images": [] + } + ], + "assistant_response": "根据截图,你正在使用 **DeepSeek 开放平台**(platform.deepseek.com)的网页界面。\n\n具体来说,你当前查看的是 **“用量信息”** 页面。页面上显示了以下关键信息:\n* **充值余额**:¥22.77 CNY\n* **六月消费**(按 UTC+0 时间):¥17.22 CNY\n* **每月用量图表**:展示了近期(6月1日至6月30日)的消费金额波动。\n* **模型使用情况**:下方还列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n简单来说,你正在查看自己在 DeepSeek 平台上的账户余额、消费记录以及 API 的使用情况。", + "timestamp": "20260620_183034" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json new file mode 100644 index 0000000..dadcffc --- /dev/null +++ b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json @@ -0,0 +1,23 @@ +{ + "session_id": "20260620_183029", + "turn_id": "20260620_183128", + "user_message": { + "role": "user", + "content": "你现在的对话记录中有图片吗" + }, + "tool_calls": [ + { + "name": "gui_tool", + "args": { + "action": "screenshot" + }, + "result_text": "", + "result_files": [ + "_artifact_gui_tool_0_0.png" + ], + "result_images": [] + } + ], + "assistant_response": "根据刚才截取的屏幕截图,我**没有**在当前的对话记录中看到任何图片。\n\n这张截图显示的是你正在浏览 **DeepSeek 开放平台** 的“用量信息”页面,主要内容包括:\n1. **充值余额**:¥22.77 CNY\n2. **六月消费**(按 UTC+0 时间):¥17.22 CNY\n3. **每月用量图表**:展示了近期每天的消费金额波动。\n4. **模型使用情况**:下方列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n所以,目前的对话中只有文字交互,并没有上传图片。", + "timestamp": "20260620_183128" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_0_0.png new file mode 100644 index 0000000..38a8de7 Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_0_0.png differ diff --git a/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_1_0.png new file mode 100644 index 0000000..99b6bde Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_1_0.png differ diff --git a/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_0_0.png new file mode 100644 index 0000000..f48232f Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_0_0.png differ diff --git a/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_1_0.png new file mode 100644 index 0000000..7f2bd13 Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_1_0.png differ diff --git a/src/agent/history/tool_calling/20260620_205109/turn_20260620_205218.json b/src/agent/history/tool_calling/20260620_205109/turn_20260620_205218.json new file mode 100644 index 0000000..265f684 --- /dev/null +++ b/src/agent/history/tool_calling/20260620_205109/turn_20260620_205218.json @@ -0,0 +1,36 @@ +{ + "session_id": "20260620_205109", + "turn_id": "20260620_205218", + "user_message": { + "role": "user", + "content": "能不能帮我打开虎牙直播" + }, + "tool_calls": [ + { + "name": "browser_control", + "args": { + "action": "open", + "url": "https://www.huya.com" + }, + "result_text": "已尝试打开 URL: https://www.huya.com", + "result_files": [ + "_artifact_browser_control_0_0.png" + ], + "result_images": [] + }, + { + "name": "browser_control", + "args": { + "action": "open", + "url": "https://www.huya.com" + }, + "result_text": "已尝试打开 URL: https://www.huya.com", + "result_files": [ + "_artifact_browser_control_1_0.png" + ], + "result_images": [] + } + ], + "assistant_response": "虎牙直播已经成功打开了!可以看到:\n\n- 页面顶部显示\"虎牙直播\"的Logo\n- 当前正在播放主播\"王子酱\"的直播间\n- 页面下方展示了各种游戏分类,包括网游竞技(LOL、CS2等)、单机热游等\n- 右侧还有其他推荐的主播和直播间\n\n你现在可以浏览你感兴趣的直播内容了。如果你想观看特定的主播或游戏类别,可以直接点击相应的选项。", + "timestamp": "20260620_205218" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260620_205109/turn_20260621_113525.json b/src/agent/history/tool_calling/20260620_205109/turn_20260621_113525.json new file mode 100644 index 0000000..8986033 --- /dev/null +++ b/src/agent/history/tool_calling/20260620_205109/turn_20260621_113525.json @@ -0,0 +1,36 @@ +{ + "session_id": "20260620_205109", + "turn_id": "20260621_113525", + "user_message": { + "role": "user", + "content": "帮我打开b站" + }, + "tool_calls": [ + { + "name": "browser_control", + "args": { + "action": "open", + "url": "https://www.bilibili.com" + }, + "result_text": "已尝试打开 URL: https://www.bilibili.com", + "result_files": [ + "_artifact_browser_control_0_0.png" + ], + "result_images": [] + }, + { + "name": "browser_control", + "args": { + "action": "open", + "url": "https://www.bilibili.com" + }, + "result_text": "已尝试打开 URL: https://www.bilibili.com", + "result_files": [ + "_artifact_browser_control_1_0.png" + ], + "result_images": [] + } + ], + "assistant_response": "好的,B站(bilibili)已经成功打开。\n\n页面显示了:\n* **顶部导航栏**:包含首页、番剧、直播、游戏中心等主要分区。\n* **推荐内容**:首页展示了多个视频推荐,例如左侧的“BML-PLAY! 2025”活动预告,以及右侧的“GPT如何解决80年未解单位距离猜想?”等热门视频。\n* **分类标签**:可以看到动画、番剧、国创、音乐、舞蹈、游戏等多个内容分类。\n\n你现在可以浏览你感兴趣的内容了。", + "timestamp": "20260621_113525" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260621_114011/20260621_114012/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260621_114011/20260621_114012/_artifact_browser_control_0_0.png new file mode 100644 index 0000000..0afa92c Binary files /dev/null and b/src/agent/history/tool_calling/20260621_114011/20260621_114012/_artifact_browser_control_0_0.png differ diff --git a/src/agent/history/tool_calling/20260621_114011/turn_20260621_114012.json b/src/agent/history/tool_calling/20260621_114011/turn_20260621_114012.json new file mode 100644 index 0000000..3bfa30d --- /dev/null +++ b/src/agent/history/tool_calling/20260621_114011/turn_20260621_114012.json @@ -0,0 +1,24 @@ +{ + "session_id": "20260621_114011", + "turn_id": "20260621_114012", + "user_message": { + "role": "user", + "content": "帮我打开b站" + }, + "tool_calls": [ + { + "name": "browser_control", + "args": { + "action": "open", + "url": "https://www.bilibili.com" + }, + "result_text": "已尝试打开 URL: https://www.bilibili.com", + "result_files": [ + "_artifact_browser_control_0_0.png" + ], + "result_images": [] + } + ], + "assistant_response": "B站(哔哩哔哩)已经为您打开了。请问有什么我可以帮您的吗?", + "timestamp": "20260621_114012" +} \ No newline at end of file diff --git a/src/agent/llm.py b/src/agent/llm.py deleted file mode 100644 index a88b66b..0000000 --- a/src/agent/llm.py +++ /dev/null @@ -1,3 +0,0 @@ -from agent.core.llm import ChatQwen, llm - -__all__ = ["ChatQwen", "llm"] diff --git a/src/agent/memory/__pycache__/__init__.cpython-313.pyc b/src/agent/memory/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index a2d3427..0000000 Binary files a/src/agent/memory/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc index 2bd94a4..382e635 100644 Binary files a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc and b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc differ diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-313.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-313.pyc deleted file mode 100644 index a3d7fa1..0000000 Binary files a/src/agent/memory/__pycache__/system_prompt.cpython-313.pyc and /dev/null differ diff --git a/src/agent/memory/system_prompt.py b/src/agent/memory/system_prompt.py index 1e0040d..7090c2c 100644 --- a/src/agent/memory/system_prompt.py +++ b/src/agent/memory/system_prompt.py @@ -1,10 +1,14 @@ -MAIN_SYSTEM_PROMPT = """你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。 - 【关键行为准则】 - - 你的默认行为是直接回答用户问题,不要调用任何工具。 - - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。 - - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。 - - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。 - - 违反上述规则是一种严重错误。 +MAIN_SYSTEM_PROMPT = """你是一个具备多模态理解能力的桌面操作助手,能够直接理解用户上传的图片、语音和文件,并通过调用工具完成桌面自动化任务。 + +【核心行为准则】 +- 你的首要职责是理解用户的意图,并高效地完成用户的任务。 +- 对于任何用户上传的图片、音频或文件,直接利用你的多模态能力分析并回答,不要为此调用截图、OCR 等无关工具。 +- 只有在信息确实无法从当前对话或上传内容中获取,且必须通过外部工具才能获得时,才调用工具。例如:需要查看当前屏幕状态、需要执行 GUI 操作、需要搜索网络信息等。 +- 当你调用工具收到截图或返回结果后,必须仔细观察返回内容,基于实际结果继续推进任务,严禁脱离实际结果凭空编造。 +- 每次工具调用后,向用户简要汇报当前进度和下一步计划,确保用户始终了解任务状态。 +- 如果不确定是否需要工具,优先选择不调用,用已有信息给出最佳答案。 +- 对于需要多次操作才能完成的任务,应主动使用 todo_list 工具规划步骤并逐步推进。 +- 当任务完成时,向用户总结执行过程和结果。 """.strip() SUMMARY_SYSTEM_PROMPT =""" @@ -38,12 +42,16 @@ SUMMARY_SYSTEM_PROMPT =""" VISION_SYSTEM_PROMPT = """ - 你是专业的图像理解助手。 - 首先请用客观、准确的中文描述用户提供的图片,覆盖主要物体、场景、人物动作、界面元素、图表数据与清晰可读的文字; - 涉及到你认识的具体品牌、时间、地点、人物、影视剧、物品等特定内容时请直接命名说明; - 如果用户提供了多张图片,请分别编号描述(如「图1」「图2」等),便于后续问答引用。 - 看不清的内容请如实说明,不要臆测。 - 其次请认真回答用户的问题,结合图片内容进行分析推理,给出具体、简短、清晰、实用的回复; + 你是专业的图像理解助手。你的任务是对用户提供的图片进行准确、全面的描述。 + + 【描述要求】 + 1. 用客观、准确的中文描述图片内容,包括主要物体、场景、人物动作、界面元素、图表数据和清晰可读的文字。 + 2. 涉及具体品牌、时间、地点、人物、影视剧名称、知名物品等特定内容时,请直接命名说明。 + 3. 如果用户提供了多张图片,请分别编号描述(如「图1」「图2」等),便于后续问答引用。 + 4. 看不清或无法确定的内容请如实说明,不要臆测或编造。 + + 【回答要求】 + 在完成图片描述后,请认真回答用户的问题,结合图片内容进行分析推理,给出具体、简短、清晰、实用的回复。 """.strip() DB_SYSTEM_PROMT = """ @@ -68,6 +76,6 @@ DB_SYSTEM_PROMT = """ """.strip() TEAM_MODE_PROMPT = """ -team 模式目前仅作为多 agent 协同的预留模式,尚未实现。 -请简短说明当前无法启动 team 协同,并建议用户改用 agent 模式。 + Team 模式目前处于预留阶段,多 Agent 协同功能尚未实现。 + 请向用户说明当前无法启动 Team 协同模式,并建议使用 Agent 模式来完成单 Agent 任务。 """.strip() diff --git a/src/agent/tools/__init__.py b/src/agent/tools/__init__.py index ade392b..acae83e 100644 --- a/src/agent/tools/__init__.py +++ b/src/agent/tools/__init__.py @@ -2,11 +2,11 @@ from .tool import * from .tool_software_control import SoftwareControl from .gui_tool import GUITool from .tool_rag import RAGTool +from .tool_agent_call import CallSubAgentTool _all_tools = [ WebSearch(), DateQuery(), - ScreenCapture(), BrowserControl(), HumanInteraction(), SoftwareControl(), @@ -14,6 +14,7 @@ _all_tools = [ DirectoryListing(), TodoList(), GUITool(), + CallSubAgentTool(), ] # 注册所有可用工具名称(供前端配置面板展示),从 _all_tools 实例推导 diff --git a/src/agent/tools/__pycache__/__init__.cpython-312.pyc b/src/agent/tools/__pycache__/__init__.cpython-312.pyc index 9f5f68b..48fa3e2 100644 Binary files a/src/agent/tools/__pycache__/__init__.cpython-312.pyc and b/src/agent/tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/__init__.cpython-313.pyc b/src/agent/tools/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 100e5fe..0000000 Binary files a/src/agent/tools/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc index aaa13e8..af869ac 100644 Binary files a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc and b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/tool.cpython-312.pyc b/src/agent/tools/__pycache__/tool.cpython-312.pyc index 92361e1..2d06923 100644 Binary files a/src/agent/tools/__pycache__/tool.cpython-312.pyc and b/src/agent/tools/__pycache__/tool.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/tool.cpython-313.pyc b/src/agent/tools/__pycache__/tool.cpython-313.pyc deleted file mode 100644 index 3dae7d2..0000000 Binary files a/src/agent/tools/__pycache__/tool.cpython-313.pyc and /dev/null differ diff --git a/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc b/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc index bc2aeb1..09c51f6 100644 Binary files a/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/tool_database.cpython-312.pyc b/src/agent/tools/__pycache__/tool_database.cpython-312.pyc deleted file mode 100644 index 5a32225..0000000 Binary files a/src/agent/tools/__pycache__/tool_database.cpython-312.pyc and /dev/null differ diff --git a/src/agent/tools/__pycache__/tool_database.cpython-313.pyc b/src/agent/tools/__pycache__/tool_database.cpython-313.pyc deleted file mode 100644 index 93f071c..0000000 Binary files a/src/agent/tools/__pycache__/tool_database.cpython-313.pyc and /dev/null differ diff --git a/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc b/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc index 43a58a1..2fc4b41 100644 Binary files a/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/tool_rag.cpython-313.pyc b/src/agent/tools/__pycache__/tool_rag.cpython-313.pyc deleted file mode 100644 index 3816508..0000000 Binary files a/src/agent/tools/__pycache__/tool_rag.cpython-313.pyc and /dev/null differ diff --git a/src/agent/tools/__pycache__/tool_software_control.cpython-313.pyc b/src/agent/tools/__pycache__/tool_software_control.cpython-313.pyc deleted file mode 100644 index 2240ded..0000000 Binary files a/src/agent/tools/__pycache__/tool_software_control.cpython-313.pyc and /dev/null differ diff --git a/src/agent/tools/gui_tool.py b/src/agent/tools/gui_tool.py index fd3d34a..3d17fc5 100644 --- a/src/agent/tools/gui_tool.py +++ b/src/agent/tools/gui_tool.py @@ -26,6 +26,7 @@ GUIAction = Literal[ "hotkey", "hold_and_press", "wait", + "screenshot", ] @@ -56,8 +57,21 @@ class GUIToolInput(BaseModel): wait_seconds: float = Field(default=0.5, description="Seconds to wait.") +def _get_gui_region() -> dict | None: + """获取当前 GUI 操作显示器区域,失败返回 None(全屏)。""" + try: + from agent.core.config_manager import get_active_monitor_region + return get_active_monitor_region() + except Exception: + return None + + def _screenshot_png() -> tuple[bytes, tuple[int, int]]: - image = pyautogui.screenshot() + region = _get_gui_region() + if region: + image = pyautogui.screenshot(region=(region["x"], region["y"], region["width"], region["height"])) + else: + image = pyautogui.screenshot() width, height = image.size buf = io.BytesIO() image.save(buf, format="PNG") @@ -89,12 +103,16 @@ def _parse_coordinates(text: str) -> tuple[int, int]: def _scale_to_screen(coords: tuple[int, int], screen_size: tuple[int, int]) -> tuple[int, int]: + """将 grounding 坐标映射到绝对屏幕坐标(考虑 GUI 操作区域偏移)。""" screen_width, screen_height = screen_size x, y = coords - return ( - round(x * screen_width / GROUNDING_WIDTH), - round(y * screen_height / GROUNDING_HEIGHT), - ) + local_x = round(x * screen_width / GROUNDING_WIDTH) + local_y = round(y * screen_height / GROUNDING_HEIGHT) + # 加上目标显示器在屏幕空间的偏移 + region = _get_gui_region() + offset_x = region["x"] if region else 0 + offset_y = region["y"] if region else 0 + return (offset_x + local_x, offset_y + local_y) def _ground_element(element_description: str) -> tuple[int, int, str]: @@ -166,14 +184,15 @@ class GUITool(BaseTool): description: str = ( "GUI automation tool. Visually locates desktop elements from a screenshot, maps coordinates to the " "real screen, and executes pyautogui actions. Supported actions: click, type, drag_and_drop, scroll, " - "hotkey, hold_and_press, wait. If no action is given, returns a screenshot of the current screen." + "hotkey, hold_and_press, wait, screenshot. Returns a screenshot of the current screen after the action." ) response_format: Literal["content", "content_and_artifact"] = "content_and_artifact" def _artifact_screenshot(self) -> dict: try: image_bytes, _ = _screenshot_png() - return {"image_bytes": image_bytes} + resized_bytes = _resize_for_grounding(image_bytes) + return {"image_bytes": resized_bytes} except Exception: return {} @@ -194,16 +213,16 @@ class GUITool(BaseTool): shift: bool = False, wait_seconds: float = 0.0, ) -> tuple[str, dict]: - # ---------- 新增:无动作时直接截图 ---------- + # ---------- screenshot 动作:仅截图 ---------- + if action == "screenshot": + return ("", self._artifact_screenshot()) + # ---------- 无动作时直接截图 ---------- if not action: - raw_png, size = _screenshot_png() - return ( - f"Screenshot captured (size {size[0]}x{size[1]}).", - self._artifact_screenshot(), # 再次截图,间隔极短,也可直接返回 {"image_bytes": raw_png} - ) + return ("", self._artifact_screenshot()) # ------------------------------------------- + dispatch_error: str | None = None try: - message = self._dispatch( + self._dispatch( action=action, element_description=element_description, ending_description=ending_description, @@ -219,10 +238,13 @@ class GUITool(BaseTool): shift=shift, wait_seconds=wait_seconds, ) - time.sleep(0.2) - return message, self._artifact_screenshot() except Exception as exc: - return f"GUI tool action failed: {exc}", self._artifact_screenshot() + dispatch_error = f"[GUI Error] {action}: {exc}" + print(dispatch_error) + time.sleep(0.5) + if dispatch_error: + return dispatch_error, self._artifact_screenshot() + return "", self._artifact_screenshot() def _dispatch(self, **kwargs: Any) -> str: action = str(kwargs["action"]).strip() diff --git a/src/agent/tools/tool.py b/src/agent/tools/tool.py index 108671d..f1c12fe 100644 --- a/src/agent/tools/tool.py +++ b/src/agent/tools/tool.py @@ -11,11 +11,12 @@ import pyautogui import asyncio from pathlib import Path +from PIL import Image from langchain.tools import BaseTool from pydantic import BaseModel, Field, create_model, model_validator from agent.core.llm import llm -from agent.utils.env_utils import BROWSER_MAP +from agent.utils.env_utils import BROWSER_MAP, GROUNDING_HEIGHT, GROUNDING_WIDTH class WebSearch(BaseTool): @@ -61,56 +62,6 @@ class DateQuery(BaseTool): return self._run() -class ScreenCapture(BaseTool): - """截取当前屏幕,生成文字描述并同时提供原始截图供模型直接查看。""" - - args_schema: type[BaseModel] = create_model( - "ScreenCaptureInput", - focus_query=( - Optional[str], - Field( - default=None, - description="可选。希望重点观察的界面或问题,例如「错误提示全文」「浏览器地址栏」;留空则整体描述屏幕。", - ), - ), - ) - name: str = "screen_capture" - description: str = ( - "截取当前主屏幕画面。除了返回文字描述外,也会提供原始截图给模型进行视觉分析。" - "注意:这个工具仅用于获取当前屏幕的实时画面,其余任何情况严禁调用。" - ) - response_format: Literal["content", "content_and_artifact"] = "content_and_artifact" - - def _run(self, focus_query: Optional[str] = None) -> Tuple[str, dict]: - # 1. 截屏 - try: - pil_img = pyautogui.screenshot() - buf = io.BytesIO() - pil_img.save(buf, format="PNG") - raw_png = buf.getvalue() - except Exception as e: - return f"截屏失败: {e}", {} - - query = (focus_query or "").strip() - - if query: # focus_query 存在且非空 - # 生成视觉描述,返回文字内容,不返回图片字节 - try: - description = llm.describe_image_bytes(raw_png, prompt=query) - except Exception as e: - description = f"屏幕图像视觉解析失败: {e}" - if not description.strip(): - description = "(未返回有效描述)" - content_text = f"【当前屏幕视觉描述】\n{description.strip()}" - return content_text, {} - else: - # 不生成描述,直接返回图片字节 - artifact = {"image_bytes": raw_png} - return "", artifact - - async def _arun(self, focus_query: Optional[str] = None) -> Tuple[str, dict]: - return await asyncio.to_thread(self._run, focus_query) - class BrowserControl(BaseTool): """统一浏览器控制工具:支持打开/关闭浏览器、刷新/关闭标签页、打开指定 URL""" @@ -179,6 +130,13 @@ class BrowserControl(BaseTool): pyautogui.hotkey(*keys) # ---------- 核心操作(返回操作结果描述) ---------- + def _try_activate_any_browser(self) -> bool: + """尝试激活任意已知浏览器窗口(不指定 browser 时的回退方案)。""" + for _reg_name, title_keyword in BROWSER_MAP.values(): + if self._activate_window(title_keyword): + return True + return False + def _open_action(self, url: Optional[str], browser: Optional[str]) -> str: """执行打开首页、刷新当前页或打开指定 URL。""" controller, title_keyword = self._get_browser_info(browser) @@ -186,6 +144,8 @@ class BrowserControl(BaseTool): controller.open("about:blank") if title_keyword: self._activate_window(title_keyword) + else: + self._try_activate_any_browser() browser_info = f" (使用 {browser})" if browser else "" return f"已打开浏览器首页{browser_info}" elif url.lower() == "current": @@ -202,6 +162,10 @@ class BrowserControl(BaseTool): controller.open(url) if title_keyword: self._activate_window(title_keyword) + else: + self._try_activate_any_browser() + # 等待浏览器窗口出现并开始加载 + time.sleep(1.0) return f"已尝试打开 URL: {url}" def _close_action(self, url: Optional[str], browser: Optional[str]) -> str: @@ -211,6 +175,8 @@ class BrowserControl(BaseTool): if title_keyword: if not self._activate_window(title_keyword): return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭" + else: + self._try_activate_any_browser() time.sleep(0.2) if sys.platform == "darwin": self._send_keys("command", "q") @@ -221,6 +187,8 @@ class BrowserControl(BaseTool): if title_keyword: if not self._activate_window(title_keyword): return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭标签页" + else: + self._try_activate_any_browser() time.sleep(0.2) if sys.platform == "darwin": self._send_keys("command", "w") @@ -240,9 +208,11 @@ class BrowserControl(BaseTool): except Exception as e: msg = f"执行浏览器操作时出错: {e}" - # 无论操作是否成功,都尝试截屏 + # 无论操作是否成功,都尝试截屏(等待窗口切换) + time.sleep(0.3) try: pil_img = pyautogui.screenshot() + pil_img = pil_img.resize((GROUNDING_WIDTH, GROUNDING_HEIGHT), Image.Resampling.LANCZOS) buf = io.BytesIO() pil_img.save(buf, format="PNG") raw_png = buf.getvalue() diff --git a/src/agent/tools/tool_agent_call.py b/src/agent/tools/tool_agent_call.py index b705d4f..84d258c 100644 --- a/src/agent/tools/tool_agent_call.py +++ b/src/agent/tools/tool_agent_call.py @@ -1,52 +1,241 @@ -"""Agent 委派工具:主 Agent 通过此工具将任务委派给子 Agent。 +"""Agent 委派工具:主 Agent 通过 call_subagent 工具将任务委派给子 Agent 执行。 系统定位: - 作为主 Agent 工具集中的特殊工具,调用此工具将触发 - SupervisorGraph 将执行路由到对应子 Agent。 + 封装在 ReAct 图的工具集中,使主 Agent 可以像调用普通工具一样 + 调用子 Agent 执行子任务。支持一轮中发起多个 call_subagent 调用, + 结合并行 ToolNode 实现子 Agent 并行执行。 工具签名: - agent_call(agent_name, task_description) + call_subagent(agent_name, task_description) -> str + +并行说明: + 将主 agent 图的 ToolNode 替换为 parallel_tool_node(本模块提供), + 即可在一次 LLM 响应中的多个 call_subagent 被并行执行。 """ from __future__ import annotations -from typing import Literal +import concurrent.futures +from typing import Any from langchain.tools import BaseTool -from pydantic import BaseModel, Field, create_model +from langchain_core.messages import HumanMessage, SystemMessage +from pydantic import BaseModel, Field + +from agent.core.state import AgentState -class AgentCallInput(BaseModel): - """agent_call 工具的输入参数。""" +class CallSubAgentInput(BaseModel): + """call_subagent 工具的输入参数。""" agent_name: str = Field( - description="要委派的子 Agent 名称,例如 'code_agent'、'research_agent'。" + description="要调用的子 Agent 名称。必须是 agent_config.json 中已启用且 role='sub' 的 Agent。" ) task_description: str = Field( - description="委派给子 Agent 的详细任务描述,子 Agent 将根据此描述执行任务。" + description="委派给子 Agent 的详细任务描述,子 Agent 将根据此描述独立执行任务。" ) -class AgentCallTool(BaseTool): - """主 Agent 委派任务给子 Agent 的工具。 +class CallSubAgentTool(BaseTool): + """主 Agent 调用子 Agent 执行任务的工具。 - 调用后 SupervisorGraph 将暂停主 Agent、激活对应子 Agent, - 子 Agent 完成后结果回传给主 Agent。 + 调用后直接运行子 Agent 的编译图,返回最终结果。 + 子 Agent 拥有独立的 system_prompt、工具集和 LLM,完全独立执行。 """ - args_schema: type[BaseModel] = AgentCallInput - name: str = "agent_call" + args_schema: type[BaseModel] = CallSubAgentInput + name: str = "call_subagent" description: str = ( - "将子任务委派给指定的子 Agent 执行。" - "仅在需要专业子 Agent 协助完成特定类型任务时调用。" - "可用的子 Agent 名称由系统配置决定。" + "调用一个已启用的子 Agent 执行指定任务。子 Agent 会独立运行并返回最终结果。" + "适用于需要专业能力(如代码分析、文档处理等)进行辅助的场景。" + "可用的子 Agent 名称取决于 agent_config.json 中已启用且 role='sub' 的配置。" ) + def _get_sub_agent_runtime(self, agent_name: str): + """查找并返回已启用的子 Agent 运行时。""" + from agent.agents.agent import get_agent_runtime, get_main_agent_runtime + runtime = get_agent_runtime(agent_name) + if runtime is None: + available = [] + from agent.agents.agent import get_all_agent_runtimes + all_rt = get_all_agent_runtimes() + for name, rt in all_rt.items(): + main_rt = get_main_agent_runtime() + if main_rt and rt is not main_rt: + available.append(name) + raise ValueError( + f"子 Agent '{agent_name}' 未找到或未启用。" + f"当前可用的子 Agent: {available if available else '无'}" + ) + # 确保不是主 agent 自己调自己 + main_rt = get_main_agent_runtime() + if main_rt and runtime is main_rt: + raise ValueError(f"不能委派任务给主 Agent 自身 '{agent_name}'") + return runtime + def _run(self, agent_name: str, task_description: str) -> str: - """占位实现:实际路由由 SupervisorGraph 处理。""" - return ( - f"已委派任务给子 Agent [{agent_name}]。\n" - f"任务描述: {task_description}\n" - f"等待子 Agent 返回结果..." - ) + """同步执行子 Agent 任务并返回结果。""" + try: + runtime = self._get_sub_agent_runtime(agent_name) + except ValueError as e: + return f"call_subagent 失败: {e}" + + # 构建子 Agent 的输入消息 + messages = [ + SystemMessage(content=runtime.system_prompt), + HumanMessage(content=task_description), + ] + + # 配置 recursion_limit + config = {"recursion_limit": runtime.max_iterations} + + try: + result = runtime.graph.invoke({"messages": messages}, config=config) + all_messages = result.get("messages", []) + + # 提取最后一条非工具调用的助手消息作为最终结果 + from langchain_core.messages import AIMessage + final_text = "" + for msg in reversed(all_messages): + if isinstance(msg, AIMessage) and not getattr(msg, "tool_calls", None): + content = msg.content + if isinstance(content, str): + final_text = content + break + elif isinstance(content, list): + # 多模态内容,提取文本部分 + text_parts = [ + p.get("text", "") for p in content + if isinstance(p, dict) and p.get("type") == "text" + ] + final_text = "\n".join(text_parts) + break + + if not final_text: + final_text = "子 Agent 已完成任务但未返回文字结果。" + + return f"[子Agent: {agent_name}] 执行结果:\n{final_text}" + + except Exception as e: + return f"[子Agent: {agent_name}] 执行出错: {str(e)}" async def _arun(self, agent_name: str, task_description: str) -> str: - return self._run(agent_name, task_description) + """异步执行(委托给同步实现)。""" + import asyncio + return await asyncio.to_thread(self._run, agent_name, task_description) + + +# ---------- 并行工具节点 ---------- +def make_parallel_tool_node(tools: list[Any], max_workers: int = 5): + """创建并行工具执行节点,替换 LangGraph 默认的 ToolNode。 + + 输入: + tools: LangChain BaseTool 列表。 + max_workers: 线程池最大并发数。 + + 输出: + LangGraph 节点函数(签名兼容 ToolNode)。 + + 系统定位: + 在 build_agent_graph 中替代 ToolNode,使一轮内的多个工具调用 + (包括多个 call_subagent)可以并行执行,显著提升多子 Agent 场景效率。 + + 可扩展性: + 可增加超时控制、错误隔离(单个工具失败不影响其他)。 + """ + + # 构建工具名 → 工具实例的映射 + tools_by_name: dict[str, BaseTool] = {t.name: t for t in tools} + + def _execute_single(tool_name: str, tool_args: dict, tool_call_id: str = "") -> tuple[str, str, Any]: + """在线程池中执行单个工具。 + + 构造完整的 ToolCall 格式,确保 BaseTool 能正确识别 content_and_artifact。 + 返回 (tool_name, content, artifact),artifact 为 None 表示无产物。 + """ + from langchain_core.messages import ToolMessage + + tool = tools_by_name.get(tool_name) + if tool is None: + return tool_name, f"工具 '{tool_name}' 未找到", None + try: + # 构造完整 ToolCall 格式,使 BaseTool._prep_run_args 能提取 tool_call_id, + # 进而 _format_output 会正确包装 ToolMessage(保留 artifact) + tool_call_input = { + "name": tool_name, + "args": tool_args, + "id": tool_call_id or "", + "type": "tool_call", + } + result = tool.invoke(tool_call_input) + if isinstance(result, ToolMessage): + content = str(result.content) if result.content else "" + artifact = getattr(result, "artifact", None) + return tool_name, content, artifact + if isinstance(result, tuple): + content = str(result[0]) if result[0] else "" + artifact = result[1] if len(result) > 1 else None + return tool_name, content, artifact + return tool_name, str(result), None + except Exception as e: + return tool_name, f"工具执行出错: {e}", None + + def parallel_tool_node(state: AgentState) -> dict: + """并行执行最后一条 AIMessage 中的所有 tool_calls。""" + from langchain_core.messages import AIMessage, ToolMessage + + last_msg = state["messages"][-1] + if not isinstance(last_msg, AIMessage) or not last_msg.tool_calls: + return {} + + tool_calls = last_msg.tool_calls + + # 只有 1 个工具调用时直接串行执行,避免线程开销 + if len(tool_calls) == 1: + tc = tool_calls[0] + tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "") + tc_args = tc.get("args") if isinstance(tc, dict) else getattr(tc, "args", {}) + tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "") + name, content, artifact = _execute_single(tc_name, tc_args, tc_id) + msg_kwargs = {"content": str(content), "tool_call_id": tc_id, "name": name} + if artifact: + msg_kwargs["artifact"] = artifact + return { + "messages": [ToolMessage(**msg_kwargs)] + } + + # 多个工具调用:线程池并行执行 + futures: dict[concurrent.futures.Future, dict] = {} + with concurrent.futures.ThreadPoolExecutor(max_workers=min(max_workers, len(tool_calls))) as executor: + for tc in tool_calls: + tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "") + tc_args = tc.get("args") if isinstance(tc, dict) else getattr(tc, "args", {}) + tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "") + future = executor.submit(_execute_single, tc_name, tc_args, tc_id) + futures[future] = {"id": tc_id, "name": tc_name} + + # 收集结果,保持顺序 + tool_messages = [] + for tc in tool_calls: + tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "") + tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "") + # 查找对应 future 的结果(无法保持顺序时降级为错误) + matched = False + for future, meta in dict(futures).items(): + if meta["id"] == tc_id: + _, content, artifact = future.result() + msg_kwargs = {"content": str(content), "tool_call_id": tc_id, "name": tc_name} + if artifact: + msg_kwargs["artifact"] = artifact + tool_messages.append(ToolMessage(**msg_kwargs)) + matched = True + break + if not matched: + # 降级:尝试单次执行 + name, content, artifact = _execute_single(tc_name, {}, tc_id) + msg_kwargs = {"content": str(content), "tool_call_id": tc_id, "name": tc_name} + if artifact: + msg_kwargs["artifact"] = artifact + tool_messages.append(ToolMessage(**msg_kwargs)) + + return {"messages": tool_messages} + + return parallel_tool_node diff --git a/src/agent/utils/__pycache__/__init__.cpython-313.pyc b/src/agent/utils/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 99c4056..0000000 Binary files a/src/agent/utils/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/agent_utils.cpython-313.pyc b/src/agent/utils/__pycache__/agent_utils.cpython-313.pyc deleted file mode 100644 index 6fbbddf..0000000 Binary files a/src/agent/utils/__pycache__/agent_utils.cpython-313.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/database_utils.cpython-312.pyc b/src/agent/utils/__pycache__/database_utils.cpython-312.pyc deleted file mode 100644 index 5479945..0000000 Binary files a/src/agent/utils/__pycache__/database_utils.cpython-312.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/database_utils.cpython-313.pyc b/src/agent/utils/__pycache__/database_utils.cpython-313.pyc deleted file mode 100644 index 8303bea..0000000 Binary files a/src/agent/utils/__pycache__/database_utils.cpython-313.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/env_utils.cpython-313.pyc b/src/agent/utils/__pycache__/env_utils.cpython-313.pyc deleted file mode 100644 index bcadbaa..0000000 Binary files a/src/agent/utils/__pycache__/env_utils.cpython-313.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/image_utils.cpython-313.pyc b/src/agent/utils/__pycache__/image_utils.cpython-313.pyc deleted file mode 100644 index 9815099..0000000 Binary files a/src/agent/utils/__pycache__/image_utils.cpython-313.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/log_utils.cpython-312.pyc b/src/agent/utils/__pycache__/log_utils.cpython-312.pyc deleted file mode 100644 index c236a80..0000000 Binary files a/src/agent/utils/__pycache__/log_utils.cpython-312.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/log_utils.cpython-313.pyc b/src/agent/utils/__pycache__/log_utils.cpython-313.pyc deleted file mode 100644 index ad22777..0000000 Binary files a/src/agent/utils/__pycache__/log_utils.cpython-313.pyc and /dev/null differ diff --git a/src/agent/utils/__pycache__/tool_call_utils.cpython-313.pyc b/src/agent/utils/__pycache__/tool_call_utils.cpython-313.pyc deleted file mode 100644 index b49d4f7..0000000 Binary files a/src/agent/utils/__pycache__/tool_call_utils.cpython-313.pyc and /dev/null differ diff --git a/src/web/__pycache__/__init__.cpython-313.pyc b/src/web/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 1537bed..0000000 Binary files a/src/web/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/web/__pycache__/server.cpython-312.pyc b/src/web/__pycache__/server.cpython-312.pyc index e470113..b3584f7 100644 Binary files a/src/web/__pycache__/server.cpython-312.pyc and b/src/web/__pycache__/server.cpython-312.pyc differ diff --git a/src/web/__pycache__/server.cpython-313.pyc b/src/web/__pycache__/server.cpython-313.pyc deleted file mode 100644 index 2f29706..0000000 Binary files a/src/web/__pycache__/server.cpython-313.pyc and /dev/null differ diff --git a/src/web/__pycache__/ui_session.cpython-313.pyc b/src/web/__pycache__/ui_session.cpython-313.pyc deleted file mode 100644 index 919b49e..0000000 Binary files a/src/web/__pycache__/ui_session.cpython-313.pyc and /dev/null differ diff --git a/src/web/app.css b/src/web/app.css index 6ba5f26..321b09d 100644 --- a/src/web/app.css +++ b/src/web/app.css @@ -1973,6 +1973,29 @@ body { margin-top: 1.2rem; } +/* ===== GUI 显示器设置 ===== */ +.settings-hint { + color: #94a3b8; + font-size: 0.78rem; + margin: 0 0 0.8rem; + line-height: 1.4; +} + +.gui-monitors-grid { + margin-bottom: 1rem; +} + +.gui-monitor-box { + user-select: none; +} +.gui-monitor-box:hover { + border-color: #818cf8 !important; + background: rgba(79,70,229,0.1) !important; +} +.gui-monitor-box--selected { + box-shadow: 0 0 12px rgba(79,70,229,0.35); +} + /* ===== 模型表格 ===== */ .model-table { border: 1px solid rgba(148,163,184,0.3); @@ -2123,120 +2146,210 @@ body { overflow: hidden; } -/* ===== 工具调用气泡(chat 视图) ===== */ -.tool-call-bubble-wrap { +/* ===== 工具调用展示(折叠式低透明度文本) ===== */ + +/* 指示器:"agent运行中,详情可展开" */ +.agent-running-indicator { margin-top: 0.5rem; - border: 1px solid rgba(148, 163, 184, 0.3); - border-radius: 10px; - overflow: hidden; - background: rgba(255, 255, 255, 0.18); - transition: background 0.3s; -} - -.tool-call-toggle { - display: block; - width: 100%; - padding: 0.35rem 0.65rem; - border: none; - background: rgba(255, 255, 255, 0.3); - font-size: 0.72rem; - font-weight: 600; - color: #475569; + padding: 0.3rem 0; + border-top: 1px solid rgba(148, 163, 184, 0.12); cursor: pointer; - text-align: left; - transition: background 0.2s; - font-family: inherit; + user-select: none; + -webkit-user-select: none; } -.tool-call-toggle:hover { - background: rgba(255, 255, 255, 0.5); -} - -.tool-call-collapsed { - display: flex; - flex-wrap: wrap; - gap: 0.3rem; - padding: 0.4rem 0.65rem; -} - -.tool-call-bubble-wrap--expanded .tool-call-collapsed { - display: none; -} - -.tool-call-chip { +.agent-running-text { + font-size: 0.75rem; + color: rgba(100, 116, 139, 0.55); + font-weight: 400; + letter-spacing: 0.02em; + transition: color 0.3s; display: inline-block; - padding: 0.15rem 0.45rem; - border-radius: 8px; - background: rgba(79, 70, 229, 0.1); - font-size: 0.68rem; - font-family: ui-monospace, SFMono-Regular, Consolas, monospace; - color: #4f46e5; - font-weight: 500; - transition: opacity 0.5s; } -.tool-call-more { +/* 文字从左到右跳动的动画 */ +.agent-running-text::after { + content: " ▸ ▹ ▸"; display: inline-block; - padding: 0.15rem 0.45rem; - border-radius: 8px; - background: rgba(148, 163, 184, 0.15); - font-size: 0.65rem; - color: #94a3b8; - font-weight: 600; + animation: agentJumpText 1.4s steps(3, end) infinite; + color: rgba(100, 116, 139, 0.40); + font-size: 0.7rem; } -.tool-call-expanded { +@keyframes agentJumpText { + 0% { content: " ▸ ▹ ▸"; } + 33% { content: " ▹ ▸ ▹"; } + 66% { content: " ▸ ▸ ▸"; } + 100% { content: " ▸ ▹ ▸"; } +} + +.agent-running-indicator:hover .agent-running-text { + color: rgba(100, 116, 139, 0.75); +} + +/* 工具调用列表容器 */ +.tool-call-list-wrap { display: none; - padding: 0.4rem 0.65rem 0.65rem; - max-height: 240px; + margin-top: 0.35rem; + max-height: 360px; overflow-y: auto; + padding: 0.25rem 0; + border-top: 1px solid rgba(148, 163, 184, 0.08); } -.tool-call-bubble-wrap--expanded .tool-call-expanded { +.tool-call-list-wrap--expanded { display: block; } -.tool-call-detail { - margin-bottom: 0.5rem; - padding: 0.4rem 0.55rem; - border-radius: 8px; - background: rgba(241, 245, 249, 0.6); - border: 1px solid rgba(148, 163, 184, 0.2); +/* 持久化的列表(在最终消息中) */ +.tool-call-list-wrap--persistent { + display: block; + max-height: none; + margin-top: 0.55rem; + border-top: 1px solid rgba(148, 163, 184, 0.15); } -.tool-call-detail--running { - border-color: rgba(245, 158, 11, 0.4); - background: rgba(245, 158, 11, 0.05); -} - -.tool-call-detail--running .tool-call-detail-name { - color: #d97706; -} - -.tool-call-detail-name { - font-weight: 700; - font-size: 0.72rem; - color: #1e293b; - margin-bottom: 0.25rem; -} - -.tool-call-detail-text { - margin: 0.2rem 0; - padding: 0.3rem 0.45rem; +/* 单条工具调用项 */ +.tool-call-item { + display: flex; + flex-direction: column; + padding: 0.22rem 0.3rem; border-radius: 6px; - background: rgba(148, 163, 184, 0.12); + font-size: 0.72rem; + color: rgba(51, 65, 85, 0.55); + line-height: 1.35; + transition: color 0.3s; +} + +.tool-call-item-summary { + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 0.25rem 0.35rem; +} + +.tool-call-item--running { + color: rgba(180, 115, 5, 0.60); +} + +.tool-call-item--done { + color: rgba(51, 65, 85, 0.50); +} + +.tool-call-item:hover { + background: rgba(148, 163, 184, 0.06); +} + +.tool-call-item-name { + font-weight: 600; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + margin-right: 0.3rem; +} + +.tool-call-item-args { + font-size: 0.62rem; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + color: rgba(100, 116, 139, 0.45); + background: rgba(148, 163, 184, 0.06); + padding: 0.1rem 0.35rem; + border-radius: 4px; + max-width: 260px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tool-call-item-result { + font-size: 0.62rem; + color: rgba(51, 65, 85, 0.40); + background: rgba(148, 163, 184, 0.05); + padding: 0.1rem 0.35rem; + border-radius: 4px; + max-width: 280px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tool-call-item-img { + display: block; + max-width: min(160px, 100%); + max-height: 120px; + object-fit: contain; + border-radius: 6px; + border: 1px solid rgba(148, 163, 184, 0.15); + margin-top: 0.2rem; +} + +/* ✓ 对号 */ +.tc-check { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1em; + height: 1em; + color: rgba(34, 197, 94, 0.6); + font-weight: 700; + font-size: 0.78rem; + flex-shrink: 0; +} + +/* 加载中旋转圆圈 */ +.tc-spinner { + display: inline-block; + width: 0.7em; + height: 0.7em; + border: 1.5px solid rgba(245, 158, 11, 0.5); + border-top-color: rgba(245, 158, 11, 0.85); + border-radius: 50%; + animation: tcSpin 0.7s linear infinite; + flex-shrink: 0; + margin-top: 0.15em; +} + +@keyframes tcSpin { + to { transform: rotate(360deg); } +} + +/* 摘要行缩略图 */ +.tool-call-item-thumb { + display: block; + max-width: min(120px, 100%); + max-height: 80px; + object-fit: cover; + border-radius: 4px; + border: 1px solid rgba(148, 163, 184, 0.2); + margin-top: 0.15rem; + cursor: pointer; +} + +/* 展开详情面板 */ +.tool-call-item-detail { + margin-top: 0.35rem; + padding: 0.4rem 0.5rem; + border-radius: 6px; + background: rgba(241, 245, 249, 0.5); + border: 1px solid rgba(148, 163, 184, 0.12); +} + +.tool-call-detail-pre { + margin: 0.2rem 0; + padding: 0.35rem 0.5rem; + border-radius: 5px; + background: rgba(148, 163, 184, 0.08); font-size: 0.65rem; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; - color: #64748b; + color: #475569; white-space: pre-wrap; word-break: break-all; - max-height: 80px; + max-height: 200px; overflow-y: auto; + line-height: 1.45; } .tool-call-detail-result { color: #334155; - background: rgba(148, 163, 184, 0.08); + background: rgba(148, 163, 184, 0.05); } .tool-call-detail-media { @@ -2246,61 +2359,21 @@ body { margin-top: 0.35rem; } -.tool-call-detail .tool-call-detail-image { +.tool-call-item-img--full { display: block; - max-width: min(220px, 100%); - max-height: 180px; + max-width: min(320px, 100%); + max-height: 260px; object-fit: contain; border-radius: 8px; - border: 1px solid rgba(148, 163, 184, 0.28); + border: 1px solid rgba(148, 163, 184, 0.25); background: rgba(255, 255, 255, 0.45); cursor: zoom-in; - margin: 0; } -.tool-call-detail .tool-call-detail-image:hover { +.tool-call-item-img--full:hover { opacity: 0.92; } -/* ===== 实时工具调用(thinking 气泡内) ===== */ -.tool-call-live-wrap { - display: block; - width: min(560px, 100%); - margin-top: 0.6rem; - padding-top: 0.55rem; - border-top: 1px solid rgba(148, 163, 184, 0.2); -} - -.tool-call-live-wrap .tool-call-bubble-wrap { - margin-top: 0; - width: 100%; -} - -.tool-call-live-chip { - display: inline-flex; - align-items: center; - gap: 0.2rem; - padding: 0.15rem 0.5rem; - border-radius: 8px; - background: rgba(99, 102, 241, 0.08); - font-size: 0.68rem; - font-family: ui-monospace, SFMono-Regular, Consolas, monospace; - color: #6366f1; - font-weight: 500; - transition: all 0.3s; -} - -.tool-call-live-chip--running { - background: rgba(245, 158, 11, 0.1); - color: #d97706; - animation: livePulse 1.2s ease-in-out infinite; -} - -@keyframes livePulse { - 0%, 100% { opacity: 0.7; } - 50% { opacity: 1; } -} - .todo-list-overlay { position: fixed; top: 80px; diff --git a/src/web/app.js b/src/web/app.js index c16b10e..18e7965 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -724,6 +724,11 @@ bubble.appendChild(renderPendingActionCard(msg.meta.pending_action)); } + if (msg.meta && Array.isArray(msg.meta.tool_calls) && msg.meta.tool_calls.length > 0) { + const tcList = renderPersistentToolCalls(msg.meta.tool_calls); + if (tcList) bubble.appendChild(tcList); + } + scrollChatToBottom(); } @@ -746,6 +751,16 @@ } bubble.appendChild(label); bubble.appendChild(dots); + // 工具调用折叠指示器(初始隐藏,有工具调用时才显示) + const indicator = document.createElement("div"); + indicator.className = "agent-running-indicator"; + indicator.style.display = "none"; + indicator.title = "点击展开/收起工具执行详情"; + const runningText = document.createElement("span"); + runningText.className = "agent-running-text"; + runningText.textContent = "agent运行中,详情可展开"; + indicator.appendChild(runningText); + bubble.appendChild(indicator); wrap.appendChild(bubble); chatMessages.appendChild(wrap); scrollChatToBottom(); @@ -1154,114 +1169,253 @@ document.querySelectorAll(".todo-list-overlay").forEach((el) => el.remove()); } - // ===== 工具调用展示(chat 视图) ===== + // ===== 工具调用展示(chat 视图 / 持久化) ===== let liveToolCallExpanded = false; + /** 在思考气泡内更新工具调用指示器与列表(增量更新,不重建 DOM) */ function injectLiveToolCalls(typingEl, liveCalls) { if (!typingEl || !liveCalls.length) return; const bubble = typingEl.querySelector(".msg-bubble") || typingEl; - let liveWrap = bubble.querySelector(".tool-call-live-wrap"); - if (!liveWrap) { - liveWrap = document.createElement("div"); - liveWrap.className = "tool-call-live-wrap"; - bubble.appendChild(liveWrap); + + // 显示 "agent运行中,详情可展开" 指示器 + const indicator = bubble.querySelector(".agent-running-indicator"); + if (indicator) { + indicator.style.display = ""; + // 更新指示文字中的下标提示 + const runCount = liveCalls.filter((c) => c.status === "running").length; + const doneCount = liveCalls.filter((c) => c.status === "done").length; + const textEl = indicator.querySelector(".agent-running-text"); + if (textEl) { + textEl.textContent = `agent运行中 (${doneCount}/${liveCalls.length}),详情可${liveToolCallExpanded ? "收起" : "展开"}`; + } } - const total = liveCalls.length; - let wrap = liveWrap.querySelector(".tool-call-bubble-wrap"); - const isNewPanel = !wrap; - if (!wrap) { - wrap = document.createElement("div"); - liveWrap.appendChild(wrap); + // 展开/收起列表容器 + let listWrap = bubble.querySelector(".tool-call-list-wrap"); + if (!listWrap) { + listWrap = document.createElement("div"); + listWrap.className = "tool-call-list-wrap" + (liveToolCallExpanded ? " tool-call-list-wrap--expanded" : ""); + bubble.appendChild(listWrap); } + listWrap.classList.toggle("tool-call-list-wrap--expanded", liveToolCallExpanded); - if (isNewPanel) liveToolCallExpanded = true; - liveToolCallExpanded = liveToolCallExpanded || wrap.classList.contains("tool-call-bubble-wrap--expanded"); - wrap.className = "tool-call-bubble-wrap" + (liveToolCallExpanded ? " tool-call-bubble-wrap--expanded" : ""); - wrap.innerHTML = ""; - - const runningCount = liveCalls.filter((call) => call.status === "running").length; - const header = document.createElement("button"); - header.type = "button"; - header.className = "tool-call-toggle"; - header.textContent = `🔧 工具调用 (${total}${runningCount ? `,执行中 ${runningCount}` : ""}) ${liveToolCallExpanded ? "▾" : "▸"}`; - header.addEventListener("click", () => { - liveToolCallExpanded = !liveToolCallExpanded; - wrap.classList.toggle("tool-call-bubble-wrap--expanded", liveToolCallExpanded); - header.textContent = `🔧 工具调用 (${total}${runningCount ? `,执行中 ${runningCount}` : ""}) ${liveToolCallExpanded ? "▾" : "▸"}`; - }); - wrap.appendChild(header); - - const collapsed = document.createElement("div"); - collapsed.className = "tool-call-collapsed"; - liveCalls.slice(-Math.min(2, total)).forEach((call) => { - const chip = document.createElement("span"); - chip.className = "tool-call-chip" + (call.status === "running" ? " tool-call-live-chip--running" : ""); - chip.textContent = call.name || "unknown"; - collapsed.appendChild(chip); - }); - if (total > 2) { - const more = document.createElement("span"); - more.className = "tool-call-more"; - more.textContent = `+${total - 2}`; - more.addEventListener("click", () => { - liveToolCallExpanded = true; - injectLiveToolCalls(typingEl, liveCalls); + // 指示器点击切换展开 + if (indicator && !indicator._toggleBound) { + indicator._toggleBound = true; + indicator.addEventListener("click", () => { + liveToolCallExpanded = !liveToolCallExpanded; + if (listWrap) listWrap.classList.toggle("tool-call-list-wrap--expanded", liveToolCallExpanded); + const textEl = indicator.querySelector(".agent-running-text"); + if (textEl) { + const runCount = liveCalls.filter((c) => c.status === "running").length; + textEl.textContent = `agent运行中 (${liveCalls.filter((c) => c.status === "done").length}/${liveCalls.length}),详情可${liveToolCallExpanded ? "收起" : "展开"}`; + } }); - collapsed.appendChild(more); } - wrap.appendChild(collapsed); - const expandedArea = document.createElement("div"); - expandedArea.className = "tool-call-expanded"; - liveCalls.forEach((call) => { - expandedArea.appendChild(renderLiveToolCallDetail(call)); + // 增量更新列表项(用 data-tc-id 做差量,避免每次重建 DOM) + const existingById = new Map(); + const currentIds = []; + listWrap.querySelectorAll(".tool-call-item").forEach((el) => { + const id = el.getAttribute("data-tc-id"); + if (id) existingById.set(id, el); }); - wrap.appendChild(expandedArea); - scrollChatToBottom(); + liveCalls.forEach((call, idx) => { + const callId = `${call.name || "unknown"}_${idx}`; + currentIds.push(callId); + let item = existingById.get(callId); + const isNew = !item; + if (!item) { + item = document.createElement("div"); + item.className = "tool-call-item"; + item.setAttribute("data-tc-id", callId); + listWrap.appendChild(item); + } + + // 状态图标 + const statusHtml = call.status === "done" + ? '✓' + : ''; + const name = escapeHtml(call.name || "unknown"); + + // 组装行内信息(摘要截断) + let detailHtml = ""; + if (call.args && Object.keys(call.args).length > 0) { + const short = JSON.stringify(call.args).slice(0, 200); + detailHtml += `${escapeHtml(short)}`; + } + if (call.result_text) { + const txt = String(call.result_text).slice(0, 200); + detailHtml += `${escapeHtml(txt)}`; + } + // 摘要行直接展示第一张缩略图 + if (Array.isArray(call.result_images) && call.result_images.length > 0) { + const first = call.result_images[0]; + if (first) { + detailHtml += `截图缩略图`; + } + } + + const summaryHtml = `${statusHtml} ${name}${detailHtml}`; + if (isNew || item.getAttribute("data-tc-summary") !== summaryHtml) { + // 只更新 summary,不破坏 detail + let summary = item.querySelector(".tool-call-item-summary"); + if (!summary) { + summary = document.createElement("span"); + summary.className = "tool-call-item-summary"; + item.insertBefore(summary, item.firstChild); + } + summary.innerHTML = summaryHtml; + item.setAttribute("data-tc-summary", summaryHtml); + } + item.className = "tool-call-item" + (call.status === "running" ? " tool-call-item--running" : " tool-call-item--done"); + + // 存储完整数据供展开 + item._callData = call; + + // 点击切换展开详情 + if (!item._boundClick) { + item._boundClick = true; + item.style.cursor = "pointer"; + item.addEventListener("click", () => { + let detail = item.querySelector(".tool-call-item-detail"); + if (detail) { + // 收起 + detail.remove(); + item.setAttribute("aria-expanded", "false"); + return; + } + // 展开 + const data = item._callData; + if (!data) return; + detail = document.createElement("div"); + detail.className = "tool-call-item-detail"; + // 完整参数 + if (data.args && Object.keys(data.args).length > 0) { + const pre = document.createElement("pre"); + pre.className = "tool-call-detail-pre"; + pre.textContent = JSON.stringify(data.args, null, 2); + detail.appendChild(pre); + } + // 完整返回文本 + if (data.result_text) { + const pre = document.createElement("pre"); + pre.className = "tool-call-detail-pre tool-call-detail-result"; + pre.textContent = data.result_text; + detail.appendChild(pre); + } + // 图片完整展示 + if (Array.isArray(data.result_images) && data.result_images.length > 0) { + const mediaWrap = document.createElement("div"); + mediaWrap.className = "tool-call-detail-media"; + data.result_images.forEach((url) => { + if (!url) return; + const img = document.createElement("img"); + img.className = "tool-call-item-img tool-call-item-img--full"; + img.src = url; + img.alt = "工具返回图片"; + img.addEventListener("click", (e) => { e.stopPropagation(); window.open(url, "_blank", "noopener"); }); + mediaWrap.appendChild(img); + }); + detail.appendChild(mediaWrap); + } + item.appendChild(detail); + item.setAttribute("aria-expanded", "true"); + }); + } + }); + + // 移除不再存在的旧项 + const idSet = new Set(currentIds); + existingById.forEach((el, id) => { + if (!idSet.has(id)) el.remove(); + }); } - function renderLiveToolCallDetail(call) { - const card = document.createElement("div"); - card.className = "tool-call-detail" + (call.status === "running" ? " tool-call-detail--running" : ""); - - const nameEl = document.createElement("div"); - nameEl.className = "tool-call-detail-name"; - const statusIcon = call.status === "running" ? "⌛" : "✓"; - nameEl.textContent = `${statusIcon} ${escapeHtml(call.name || "unknown")}`; - card.appendChild(nameEl); - - if (call.args && Object.keys(call.args).length > 0) { - const argsEl = document.createElement("pre"); - argsEl.className = "tool-call-detail-text"; - argsEl.textContent = JSON.stringify(call.args, null, 2); - card.appendChild(argsEl); - } - - if (call.result_text) { - const resultEl = document.createElement("pre"); - resultEl.className = "tool-call-detail-text tool-call-detail-result"; - resultEl.textContent = call.result_text; - card.appendChild(resultEl); - } - - if (Array.isArray(call.result_images) && call.result_images.length > 0) { - const mediaWrap = document.createElement("div"); - mediaWrap.className = "tool-call-detail-media"; + /** 根据消息 meta.tool_calls 渲染工具调用列表(持久化展示在最终消息中) */ + function toolCallImageUrls(call) { + const urls = []; + if (Array.isArray(call.result_images)) { call.result_images.forEach((url) => { - if (!url) return; - const img = document.createElement("img"); - img.className = "tool-call-detail-image"; - img.src = url; - img.alt = "工具返回图片"; - img.addEventListener("click", () => window.open(url, "_blank", "noopener")); - mediaWrap.appendChild(img); + if (typeof url === "string" && url) urls.push(url); }); - card.appendChild(mediaWrap); } + return urls; + } - return card; + function renderPersistentToolCalls(toolCalls) { + if (!toolCalls || !toolCalls.length) return null; + const wrap = document.createElement("div"); + wrap.className = "tool-call-list-wrap tool-call-list-wrap--persistent tool-call-list-wrap--expanded"; + toolCalls.forEach((call) => { + const item = document.createElement("div"); + item.className = "tool-call-item tool-call-item--done"; + item.style.cursor = "pointer"; + + // 行内摘要 + const summary = document.createElement("span"); + summary.className = "tool-call-item-summary"; + const name = escapeHtml(call.name || "unknown"); + let detailHtml = ""; + if (call.args && Object.keys(call.args).length > 0) { + const short = JSON.stringify(call.args).slice(0, 200); + detailHtml += `${escapeHtml(short)}`; + } + if (call.result_text) { + const txt = String(call.result_text).slice(0, 300); + detailHtml += `${escapeHtml(txt)}`; + } + const imageUrls = toolCallImageUrls(call); + if (imageUrls.length > 0) { + detailHtml += `?????`; + } + summary.innerHTML = `✓ ${name}${detailHtml}`; + item.appendChild(summary); + + // 存储数据 + item._callData = { args: call.args, result_text: call.result_text, image_urls: imageUrls }; + + // 点击展开 + item.addEventListener("click", () => { + let detail = item.querySelector(".tool-call-item-detail"); + if (detail) { detail.remove(); item.setAttribute("aria-expanded", "false"); return; } + detail = document.createElement("div"); + detail.className = "tool-call-item-detail"; + const data = item._callData; + if (data.args && Object.keys(data.args).length > 0) { + const pre = document.createElement("pre"); + pre.className = "tool-call-detail-pre"; + pre.textContent = JSON.stringify(data.args, null, 2); + detail.appendChild(pre); + } + if (data.result_text) { + const pre = document.createElement("pre"); + pre.className = "tool-call-detail-pre tool-call-detail-result"; + pre.textContent = data.result_text; + detail.appendChild(pre); + } + if (Array.isArray(data.image_urls) && data.image_urls.length > 0) { + const mediaWrap = document.createElement("div"); + mediaWrap.className = "tool-call-detail-media"; + data.image_urls.forEach((imgUrl) => { + const img = document.createElement("img"); + img.className = "tool-call-item-img tool-call-item-img--full"; + img.src = imgUrl; + img.alt = "??????"; + img.loading = "lazy"; + img.addEventListener("click", (e) => { e.stopPropagation(); window.open(imgUrl, "_blank", "noopener"); }); + mediaWrap.appendChild(img); + }); + detail.appendChild(mediaWrap); + } + item.appendChild(detail); + item.setAttribute("aria-expanded", "true"); + }); + + wrap.appendChild(item); + }); + return wrap; } function renderMessage(msg, allowAutoplay = false) { @@ -1306,6 +1460,11 @@ if (msg.meta && msg.meta.pending_action) { bubble.appendChild(renderPendingActionCard(msg.meta.pending_action)); } + // 持久化的工具调用列表(来自 meta.tool_calls) + if (msg.meta && msg.meta.tool_calls && Array.isArray(msg.meta.tool_calls) && msg.meta.tool_calls.length) { + const tcList = renderPersistentToolCalls(msg.meta.tool_calls); + if (tcList) bubble.appendChild(tcList); + } wrap.appendChild(bubble); return wrap; } @@ -1451,7 +1610,7 @@ liveToolCallExpanded = false; clearTodoOverlay(); - // 启动实时工具调用轮询 + // 启动实时工具调用轮询(增量更新,低频率不卡) let livePollTimer = null; let livePollActive = true; const pollLiveToolCalls = async () => { @@ -1462,11 +1621,11 @@ injectLiveToolCalls(typingEl, liveData.tool_calls); } } catch { /* ignore */ } - if (livePollActive) livePollTimer = setTimeout(pollLiveToolCalls, 800); + if (livePollActive) livePollTimer = setTimeout(pollLiveToolCalls, 1200); }; pollLiveToolCalls(); - // 启动 todo_list 实时轮询 + // 启动 todo_list 实时轮询(频率降低,减少卡顿) let todoPollTimer = null; const pollTodoList = async () => { if (!livePollActive) return; @@ -1476,7 +1635,7 @@ updateTodoOverlay(todoData.data); } } catch { /* ignore */ } - if (livePollActive) todoPollTimer = setTimeout(pollTodoList, 500); + if (livePollActive) todoPollTimer = setTimeout(pollTodoList, 2000); }; pollTodoList(); @@ -1723,17 +1882,22 @@ const settingsStatus = $("settingsStatus"); const agentsListEl = $("agentsList"); const toolsListEl = $("toolsList"); + const graphsListEl = $("graphsList"); const envListEl = $("envList"); const agentConfigPath = $("agentConfigPath"); const toolConfigPath = $("toolConfigPath"); const envConfigPath = $("envConfigPath"); + const guiConfigPath = $("guiConfigPath"); + const guiMonitorsGrid = $("guiMonitorsGrid"); const addAgentBtn = $("addAgentBtn"); let cachedAgentConfigs = []; let cachedToolConfigs = []; + let cachedGraphs = []; let cachedEnvConfig = {}; let cachedModels = []; let registeredToolNames = []; + let cachedGuiConfig = { monitors: [], selected_name: "" }; // 模型编辑状态 let editingModelIdx = -1; @@ -1767,30 +1931,39 @@ // 切换时重新渲染对应面板 if (target === "agent") renderAgentConfigs(); else if (target === "tool") renderToolConfigs(); + else if (target === "graph") renderGraphs(); else if (target === "env") renderEnvConfig(); + else if (target === "other") renderGuiConfig(); }); }); // ===== 加载所有配置 ===== async function loadAllConfigs() { - const [agentRes, toolRes, envRes] = await Promise.all([ + const [agentRes, toolRes, envRes, graphRes, guiRes] = await Promise.all([ api("/api/config/agents"), api("/api/config/tools"), api("/api/config/env"), + api("/api/config/graphs"), + api("/api/config/gui"), ]); cachedAgentConfigs = agentRes.agents || []; cachedToolConfigs = toolRes.tools || []; cachedEnvConfig = envRes.env || {}; cachedModels = envRes.models || []; + cachedGraphs = graphRes.graphs || []; + cachedGuiConfig = { monitors: guiRes.monitors || [], selected_name: guiRes.selected_name || "" }; registeredToolNames = toolRes.registered_tool_names || []; window._toolParameters = toolRes.tool_parameters || {}; window._forcedPermissions = toolRes.forced_permissions || {}; if (agentConfigPath) agentConfigPath.textContent = agentRes.config_path || ""; if (toolConfigPath) toolConfigPath.textContent = toolRes.config_path || ""; if (envConfigPath) envConfigPath.textContent = envRes.config_path || ""; + if (guiConfigPath) guiConfigPath.textContent = guiRes.config_path || ""; renderAgentConfigs(); renderToolConfigs(); + renderGraphs(); renderEnvConfig(); + renderGuiConfig(); } // ===== Agent 配置渲染 ===== @@ -1874,6 +2047,33 @@ llmField.appendChild(llmLabel); llmField.appendChild(llmRow); + // 图类型选择 + const graphField = document.createElement("div"); + graphField.className = "setting-field"; + const graphLabel = document.createElement("label"); + graphLabel.textContent = "图类型 (graph)"; + const graphRow = document.createElement("div"); + graphRow.style.cssText = "display:flex;gap:0.4rem;align-items:center;"; + const graphSelect = document.createElement("select"); + graphSelect.style.cssText = "flex:1;"; + const graphEmptyOpt = document.createElement("option"); + graphEmptyOpt.value = "react"; + graphEmptyOpt.textContent = "— 请选择 —"; + graphSelect.appendChild(graphEmptyOpt); + cachedGraphs.forEach((g) => { + const opt = document.createElement("option"); + opt.value = g.name; + opt.textContent = `${g.display_name || g.name}`; + if (g.name === (agent.graph || "react")) opt.selected = true; + graphSelect.appendChild(opt); + }); + graphSelect.addEventListener("change", () => { + agent.graph = graphSelect.value; + }); + graphRow.appendChild(graphSelect); + graphField.appendChild(graphLabel); + graphField.appendChild(graphRow); + // 角色与启用 const metaRow = document.createElement("div"); metaRow.className = "setting-field"; @@ -2002,11 +2202,40 @@ addToolRow.appendChild(addToolBtn); toolsField.appendChild(addToolRow); + // trajectory_rounds(短期记忆保留轮数) + const trajField = makeField("短期记忆轮数 (trajectory_rounds)", "number", agent.trajectory_rounds ?? 3, (v) => { agent.trajectory_rounds = parseInt(v) || 3; }); + + // parallel_tools(并行工具执行开关) + const parallelField = document.createElement("div"); + parallelField.className = "setting-field"; + const parallelWrap = document.createElement("div"); + parallelWrap.className = "toggle-row"; + const parallelToggleLabel = document.createElement("label"); + parallelToggleLabel.className = "toggle-switch"; + const parallelToggleInput = document.createElement("input"); + parallelToggleInput.type = "checkbox"; + parallelToggleInput.checked = agent.parallel_tools === true; + parallelToggleInput.addEventListener("change", () => { agent.parallel_tools = parallelToggleInput.checked; }); + const parallelToggleSlider = document.createElement("span"); + parallelToggleSlider.className = "toggle-slider"; + parallelToggleLabel.appendChild(parallelToggleInput); + parallelToggleLabel.appendChild(parallelToggleSlider); + const parallelLabel = document.createElement("span"); + parallelLabel.className = "toggle-label"; + parallelLabel.textContent = "并行工具执行 (parallel_tools)"; + parallelLabel.addEventListener("click", () => { parallelToggleInput.click(); }); + parallelWrap.appendChild(parallelToggleLabel); + parallelWrap.appendChild(parallelLabel); + parallelField.appendChild(parallelWrap); + card.appendChild(header); card.appendChild(nameField); card.appendChild(maxIterField); card.appendChild(spField); card.appendChild(llmField); + card.appendChild(graphField); + card.appendChild(trajField); + card.appendChild(parallelField); card.appendChild(metaRow); card.appendChild(toolsField); return card; @@ -2045,6 +2274,9 @@ tools: [], enabled: true, role: isFirst ? "main" : "sub", + graph: "react", + trajectory_rounds: 3, + parallel_tools: false, }); renderAgentConfigs(); }); @@ -2210,6 +2442,148 @@ return card; } + // ===== 图配置渲染 ===== + function renderGraphs() { + if (!graphsListEl) return; + graphsListEl.innerHTML = ""; + + if (!cachedGraphs || cachedGraphs.length === 0) { + const emptyHint = document.createElement("p"); + emptyHint.style.cssText = "color:#94a3b8;font-size:0.8rem;text-align:center;padding:1rem;"; + emptyHint.textContent = "暂无可用的图类型。"; + graphsListEl.appendChild(emptyHint); + return; + } + + cachedGraphs.forEach((graph) => { + const card = document.createElement("div"); + card.className = "graph-card"; + card.style.cssText = "border:1px solid rgba(79,70,229,0.2);border-radius:10px;padding:1rem;margin-bottom:0.8rem;background:rgba(255,255,255,0.03);"; + + // 标题行 + const header = document.createElement("div"); + header.style.cssText = "display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem;"; + const title = document.createElement("h4"); + title.style.cssText = "margin:0;font-size:0.95rem;"; + title.textContent = `${graph.display_name || graph.name}`; + const nameTag = document.createElement("code"); + nameTag.style.cssText = "font-size:0.72rem;background:rgba(79,70,229,0.15);padding:0.1rem 0.4rem;border-radius:4px;"; + nameTag.textContent = graph.name; + header.appendChild(title); + header.appendChild(nameTag); + + // 描述 + const desc = document.createElement("p"); + desc.style.cssText = "color:#94a3b8;font-size:0.78rem;margin:0 0 0.6rem;"; + desc.textContent = graph.description || ""; + + // Mermaid PNG 图结构可视化 + const imgContainer = document.createElement("div"); + imgContainer.style.cssText = "text-align:center;background:rgba(255,255,255,0.05);border-radius:8px;padding:0.5rem;overflow-x:auto;"; + const img = document.createElement("img"); + img.src = graph.png_url || `/api/config/graphs/png/${graph.name}`; + img.alt = `${graph.display_name || graph.name} 图结构`; + img.style.cssText = "max-width:100%;height:auto;border-radius:4px;"; + img.onerror = () => { + imgContainer.innerHTML = '图结构图片生成中,请稍后刷新...'; + }; + imgContainer.appendChild(img); + + card.appendChild(header); + card.appendChild(desc); + card.appendChild(imgContainer); + + graphsListEl.appendChild(card); + }); + } + + // ===== GUI 显示器配置渲染 ===== + function renderGuiConfig() { + if (!guiMonitorsGrid) return; + guiMonitorsGrid.innerHTML = ""; + + const monitors = cachedGuiConfig.monitors || []; + if (monitors.length === 0) { + const hint = document.createElement("p"); + hint.style.cssText = "color:#94a3b8;font-size:0.8rem;text-align:center;padding:1rem;"; + hint.textContent = "未检测到显示器,请确认 screeninfo 库已安装。"; + guiMonitorsGrid.appendChild(hint); + return; + } + + // 构建屏幕矩阵预览:计算包围盒以绘制相对位置 + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + monitors.forEach((m) => { + minX = Math.min(minX, m.x); + minY = Math.min(minY, m.y); + maxX = Math.max(maxX, m.x + m.width); + maxY = Math.max(maxY, m.y + m.height); + }); + const totalW = maxX - minX; + const totalH = maxY - minY; + const containerW = 560; + const containerH = 360; + const scale = Math.min(containerW / totalW, containerH / totalH, 1); + + const matrixWrap = document.createElement("div"); + matrixWrap.style.cssText = `position:relative;width:${containerW}px;height:${containerH}px;margin:0 auto;border:1px solid rgba(79,70,229,0.25);border-radius:10px;background:rgba(0,0,0,0.2);overflow:hidden;`; + + monitors.forEach((m) => { + const left = (m.x - minX) * scale; + const top = (m.y - minY) * scale; + const w = m.width * scale; + const h = m.height * scale; + const isSelected = m.name === cachedGuiConfig.selected_name || (!cachedGuiConfig.selected_name && m.is_primary); + + const box = document.createElement("div"); + box.className = "gui-monitor-box" + (isSelected ? " gui-monitor-box--selected" : ""); + box.style.cssText = `position:absolute;left:${left}px;top:${top}px;width:${w}px;height:${h}px;border:2px solid ${isSelected ? "#4f46e5" : "rgba(148,163,184,0.4)"};border-radius:6px;background:${isSelected ? "rgba(79,70,229,0.12)" : "rgba(255,255,255,0.03)"};cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:all 0.15s;`; + box.title = `${m.name}\n${m.width}x${m.height}${m.is_primary ? " (主显示器)" : ""}`; + + const nameEl = document.createElement("span"); + nameEl.style.cssText = `font-size:${Math.max(9, w / 14)}px;color:${isSelected ? "#e0e7ff" : "#94a3b8"};text-align:center;line-height:1.2;pointer-events:none;`; + nameEl.textContent = (m.name || "Unknown").substring(0, 20) + (m.name && m.name.length > 20 ? "…" : ""); + const resEl = document.createElement("span"); + resEl.style.cssText = `font-size:${Math.max(8, w / 18)}px;color:${isSelected ? "#a5b4fc" : "#64748b"};pointer-events:none;`; + resEl.textContent = `${m.width}x${m.height}`; + const priTag = document.createElement("span"); + priTag.style.cssText = `font-size:${Math.max(7, w / 22)}px;color:#cbd5e1;pointer-events:none;`; + priTag.textContent = m.is_primary ? "主屏" : ""; + + box.appendChild(nameEl); + box.appendChild(resEl); + box.appendChild(priTag); + + box.addEventListener("click", () => { + cachedGuiConfig.selected_name = m.name; + renderGuiConfig(); + }); + + // hover 高亮 + box.addEventListener("mouseenter", () => { + box.style.borderColor = "#818cf8"; + box.style.background = "rgba(79,70,229,0.08)"; + }); + box.addEventListener("mouseleave", () => { + if (!isSelected || m.name !== cachedGuiConfig.selected_name) { + box.style.borderColor = isSelected ? "#4f46e5" : "rgba(148,163,184,0.4)"; + box.style.background = isSelected ? "rgba(79,70,229,0.12)" : "rgba(255,255,255,0.03)"; + } + }); + + matrixWrap.appendChild(box); + }); + + guiMonitorsGrid.appendChild(matrixWrap); + + // 当前选中提示 + const selectedInfo = document.createElement("p"); + selectedInfo.style.cssText = "text-align:center;color:#a5b4fc;font-size:0.78rem;margin-top:0.6rem;"; + const selMon = monitors.find((m) => m.name === cachedGuiConfig.selected_name) || monitors.find((m) => m.is_primary); + selectedInfo.textContent = selMon ? `当前 GUI 操作区域: ${selMon.name} (${selMon.width}x${selMon.height})` : "当前 GUI 操作区域: 全屏"; + guiMonitorsGrid.appendChild(selectedInfo); + } + // ===== Env 配置渲染 ===== function renderEnvConfig() { envListEl.innerHTML = ""; @@ -2418,6 +2792,9 @@ tools: a.tools || [], enabled: a.enabled !== false, role: a.role || "sub", + graph: a.graph || "react", + trajectory_rounds: a.trajectory_rounds ?? 3, + parallel_tools: a.parallel_tools || false, })); await api("/api/config/agents", { method: "POST", body: JSON.stringify({ agents }) }); await api("/api/config/reload", { method: "POST", body: JSON.stringify({}) }); @@ -2437,6 +2814,8 @@ await api("/api/config/models", { method: "POST", body: JSON.stringify({ models: cachedModels }) }); await api("/api/config/env", { method: "POST", body: JSON.stringify({ env: cachedEnvConfig }) }); await api("/api/config/reload", { method: "POST", body: JSON.stringify({}) }); + } else if (saveType === "gui") { + await api("/api/config/gui", { method: "POST", body: JSON.stringify({ gui_monitor_name: cachedGuiConfig.selected_name }) }); } await loadAllConfigs(); setStatus("保存成功!配置已实时生效。", "success"); diff --git a/src/web/index.html b/src/web/index.html index 9572557..747047c 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -154,7 +154,9 @@
+ +
@@ -174,6 +176,13 @@
+ +