diff --git a/bilibili_page.html b/bilibili_page.html deleted file mode 100644 index d3cd089..0000000 --- a/bilibili_page.html +++ /dev/null @@ -1,344 +0,0 @@ - - - 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
  • 大会员
  • 消息
  • 动态
  • 收藏
  • 历史
  • 创作中心
  • 投稿
  • 点击视频卡片右上角添加!
    - - - - - - - - - - -
    card
    查看详情
    \ No newline at end of file diff --git a/src/agent/__pycache__/chatbot_agent.cpython-312.pyc b/src/agent/__pycache__/chatbot_agent.cpython-312.pyc index f5b236c..be78a99 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 new file mode 100644 index 0000000..bb5e361 Binary files /dev/null and b/src/agent/__pycache__/chatbot_agent.cpython-313.pyc differ diff --git a/src/agent/agents/__pycache__/agent.cpython-312.pyc b/src/agent/agents/__pycache__/agent.cpython-312.pyc index 9f67f3e..53fe921 100644 Binary files a/src/agent/agents/__pycache__/agent.cpython-312.pyc and b/src/agent/agents/__pycache__/agent.cpython-312.pyc differ diff --git a/src/agent/agents/__pycache__/agent.cpython-313.pyc b/src/agent/agents/__pycache__/agent.cpython-313.pyc new file mode 100644 index 0000000..d8ee44d Binary files /dev/null and b/src/agent/agents/__pycache__/agent.cpython-313.pyc differ diff --git a/src/agent/agents/__pycache__/pyautogui.cpython-312.pyc b/src/agent/agents/__pycache__/pyautogui.cpython-312.pyc new file mode 100644 index 0000000..aad2118 Binary files /dev/null and b/src/agent/agents/__pycache__/pyautogui.cpython-312.pyc differ diff --git a/src/agent/agents/agent.py b/src/agent/agents/agent.py index 80cabe2..a708739 100644 --- a/src/agent/agents/agent.py +++ b/src/agent/agents/agent.py @@ -38,12 +38,19 @@ def _build_llm_for_agent(cfg: AgentConfig): break if model_config: + # enable_thinking: true 时传入空 extra_body 恢复模型原生行为(启用思考) + # 默认不传 extra_body,由 ChatQwen 使用禁用思考的默认值 + if model_config.get("enable_thinking"): + extra_body = {} + else: + extra_body = None 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)), + extra_body=extra_body, ) # 回退:使用环境变量中已加载的默认 LLM 配置 from agent.core.llm import llm as _default_llm diff --git a/src/agent/agents/gui.py b/src/agent/agents/gui.py new file mode 100644 index 0000000..0452e2c --- /dev/null +++ b/src/agent/agents/gui.py @@ -0,0 +1,3 @@ +import pyautogui + +pyautogui.displayMousePosition() \ No newline at end of file diff --git a/src/agent/chatbot_agent.py b/src/agent/chatbot_agent.py index c785313..0af229f 100644 --- a/src/agent/chatbot_agent.py +++ b/src/agent/chatbot_agent.py @@ -10,7 +10,7 @@ from agent.agents.agent import ( 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"] +AgentMode = Literal["agent", "team", "plan"] # 首次导入时从配置构建主 Agent 运行时并缓存引用 _built = build_all_agent_runtimes() diff --git a/src/agent/core/__pycache__/__init__.cpython-313.pyc b/src/agent/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..c601a06 Binary files /dev/null and b/src/agent/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/agent/core/__pycache__/config_manager.cpython-312.pyc b/src/agent/core/__pycache__/config_manager.cpython-312.pyc index 7ffb088..086c7fe 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 index 16600d3..f5a13b5 100644 Binary files a/src/agent/core/__pycache__/graph.cpython-312.pyc and b/src/agent/core/__pycache__/graph.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/llm.cpython-312.pyc b/src/agent/core/__pycache__/llm.cpython-312.pyc index f5d6d93..1e58a2f 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__/nodes.cpython-312.pyc b/src/agent/core/__pycache__/nodes.cpython-312.pyc index df564b5..771bf01 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 new file mode 100644 index 0000000..15a1791 Binary files /dev/null and b/src/agent/core/__pycache__/nodes.cpython-313.pyc differ diff --git a/src/agent/core/__pycache__/runtime.cpython-312.pyc b/src/agent/core/__pycache__/runtime.cpython-312.pyc index 37b5d81..9f7e1d8 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 new file mode 100644 index 0000000..3f1606c Binary files /dev/null and b/src/agent/core/__pycache__/runtime.cpython-313.pyc differ diff --git a/src/agent/core/__pycache__/state.cpython-312.pyc b/src/agent/core/__pycache__/state.cpython-312.pyc index 53edabd..0cb06b3 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/agent_config.json b/src/agent/core/agent_config.json index f56a0bd..365ca10 100644 --- a/src/agent/core/agent_config.json +++ b/src/agent/core/agent_config.json @@ -1,12 +1,14 @@ [ { "name": "main", - "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是优先调用gui工具解决用户问题。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。", + "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是优先调用工具解决用户问题。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。", "max_iterations": 200, "llm_model_id": "affiaa7r", "tools": [ "gui_tool", - "browser_control" + "browser_control", + "human_interaction", + "todo_list" ], "enabled": true, "role": "main", diff --git a/src/agent/core/config_manager.py b/src/agent/core/config_manager.py index ece57e8..2bf1cfa 100644 --- a/src/agent/core/config_manager.py +++ b/src/agent/core/config_manager.py @@ -305,8 +305,8 @@ def _default_env_config() -> dict: "IMG_SIZE": "768", "RAG_CHUNK_SIZE": "500", "RAG_CHUNK_OVERLAP": "50", - "GROUNDING_WIDTH": "1000", - "GROUNDING_HEIGHT": "1000", + "GROUNDING_WIDTH": "1024", + "GROUNDING_HEIGHT": "576", } diff --git a/src/agent/core/graph.py b/src/agent/core/graph.py index ba1ca2c..08ca802 100644 --- a/src/agent/core/graph.py +++ b/src/agent/core/graph.py @@ -82,7 +82,7 @@ GRAPH_REGISTRY: dict[str, dict[str, Any]] = { "react": { "builder": build_agent_graph, "display_name": "ReAct (标准)", - "description": "标准 ReAct 循环:agent 推理 -> 工具执行 -> 结果处理 -> agent 继续推理,直至完成。适用于绝大多数单 Agent 场景。", + "description": "标准 ReAct 循环:agent 推理 -> 工具执行 -> 结果处理 -> agent 继续推理,直至完成。Plan 模式通过 agent_mode 参数控制(首次强制生成 todolist,后续每步注入反思提示),无需单独图。", }, } diff --git a/src/agent/core/llm.py b/src/agent/core/llm.py index c4a083a..c011489 100644 --- a/src/agent/core/llm.py +++ b/src/agent/core/llm.py @@ -72,15 +72,26 @@ class ChatQwen(ChatOpenAI): 可扩展性: 可增加 default_headers、proxy 等 ChatOpenAI 参数。 """ + # 允许调用方覆盖 extra_body: + # - None(默认):使用禁用思考的默认值 + # - 空 dict {}:清空 extra_body,恢复模型原生行为(含思考模式) + # - 非空 dict:直接使用传入值 + custom_extra_body = kwargs.pop("extra_body", None) + if custom_extra_body is None: + extra_body = { + "chat_template_kwargs": {"enable_thinking": False}, + "thinking": {"type": "disabled"}, + } + else: + extra_body = custom_extra_body + super().__init__( model=kwargs.pop("model", os.environ.get("LLM_MODEL", "")), api_key=kwargs.pop("api_key", os.environ.get("LLM_API_KEY", "")), base_url=kwargs.pop("base_url", os.environ.get("LLM_BASE_URL", "")), timeout=kwargs.pop("timeout", float(os.environ.get("LLM_TIMEOUT", 60))), max_retries=kwargs.pop("max_retries", 0), - extra_body={ - "chat_template_kwargs": {"enable_thinking": False} - }, + extra_body=extra_body, **kwargs ) diff --git a/src/agent/core/nodes.py b/src/agent/core/nodes.py index bdf251f..36b04fe 100644 --- a/src/agent/core/nodes.py +++ b/src/agent/core/nodes.py @@ -1,20 +1,27 @@ -"""LangGraph 图节点:模型调用与工具产物后处理。 +"""LangGraph 图节点:模型调用、反思机制与工具产物后处理。 系统定位: 实现 ReAct 循环中的 ``agent`` 节点工厂与 ``process_tool_artifact`` 节点, 被 ``graph`` 注册到 StateGraph。 +反思机制: + ``make_call_model_node`` 在每次调用 LLM 前注入反思引导提示, + 模型收到后会在输出 tool_calls 的同时产出文本思考内容(思维链), + 该文本思考内容被提取并记录到 think store 供前端实时展示。 + 可扩展性: - ``make_call_model_node`` 可注入 streaming、重试、用量统计回调。 - ``process_tool_artifact`` 可扩展 audio、文件、结构化 JSON 等 artifact 类型。 """ from __future__ import annotations +import json from typing import Any, Callable -from langchain_core.messages import HumanMessage, ToolMessage +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage from agent.core.state import AgentState +from agent.memory.system_prompt import PLAN_FIRST_CALL_PROMPT, REFLECTION_PROMPT from agent.utils.image_utils import image_bytes_to_openai_image_url_part from agent.utils.tool_call_utils import normalize_tool_call @@ -47,7 +54,6 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list: 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 @@ -61,17 +67,151 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list: return [messages[i] for i in sorted(kept_indices)] +def _get_todolist_status() -> str: + """获取当前会话的 todolist 状态摘要。 + + 输出: + todolist 状态文本;无 todolist 时返回空字符串。 + """ + try: + from agent.history.tool_call_recorder import get_current_session + from agent.tools.tool import get_todo_store_data + sid = get_current_session() + if not sid: + return "" + todo = get_todo_store_data(sid) + if not todo or not todo.get("steps"): + return "" + steps = todo["steps"] + done = set(todo.get("done_steps", [])) + lines = [] + for i, s in enumerate(steps): + status = "[已完成]" if i in done else "[待完成]" + lines.append(f" {i+1}. {status} {s}") + total = len(steps) + done_count = len(done) + header = f"当前TodoList进度:{done_count}/{total} 已完成" + return header + "\n" + "\n".join(lines) + except Exception: + return "" + + +def _build_reflection_prompt(agent_mode: str, is_first_call: bool) -> str | None: + """构建反思提示词,引导 Agent 在下一步执行前进行思考。 + + 输入: + messages: 当前消息列表(已过滤短期记忆)。 + agent_mode: 运行模式("agent" | "plan")。 + is_first_call: 是否为当前任务的首次 agent 调用。 + + 输出: + 反思提示文本,不需要反思时返回 None。 + """ + todo_status = _get_todolist_status() + + # Plan 模式首次调用:强制要求生成 todolist + if agent_mode == "plan" and is_first_call: + return PLAN_FIRST_CALL_PROMPT + + # 非 plan 模式首次调用,不需要反思 + if is_first_call: + return None + + # 后续调用:引导模型先思考再行动 + parts = [REFLECTION_PROMPT] + + if todo_status: + parts.append(todo_status) + else: + parts.append("当前无 TodoList。") + + return "\n".join(parts) + + +def _is_first_agent_call(messages: list) -> bool: + """判断是否为当前用户任务的首轮 agent 调用。 + + 规则:找到最后一条非 synthetic 的 HumanMessage(即用户最新输入), + 检查其后是否存在 AIMessage。若不存在,说明 agent 尚未对该输入做出响应。 + + 输入: + messages: 已过滤短期记忆的消息列表。 + + 输出: + True 表示首次调用。 + """ + # 从后向前找到最后一条非 synthetic 的 HumanMessage + last_user_idx = -1 + for i in range(len(messages) - 1, -1, -1): + msg = messages[i] + if isinstance(msg, HumanMessage) and not msg.additional_kwargs.get("synthetic"): + last_user_idx = i + break + + if last_user_idx < 0: + return True + + # 检查该用户消息之后是否有 AIMessage + for i in range(last_user_idx + 1, len(messages)): + if isinstance(messages[i], AIMessage): + return False + return True + + def make_call_model_node( model_with_tools: Any, trajectory_rounds: int = 3, ) -> Callable[[AgentState], dict]: - """创建调用已绑定工具的 LLM 的图节点闭包。""" + """创建带反思机制的调用已绑定工具的 LLM 的图节点闭包。 + + 每次调用前注入反思引导提示;调用后提取模型同时产出的文本思考内容(content) + 记录到 think store,供前端实时展示思维链。Plan 模式下首次调用强制要求生成 todolist。 + """ def call_model(state: AgentState) -> dict: - # 过滤短期记忆后再传给 LLM all_messages = list(state["messages"]) + agent_mode = state.get("agent_mode", "agent") + + # 检查 abort 标志 + try: + from agent.core.runtime import check_abort + from agent.history.tool_call_recorder import get_current_session + sid = get_current_session() + if sid and check_abort(sid): + raise RuntimeError("ABORTED_BY_USER") + except RuntimeError: + raise + except Exception: + pass + filtered_messages = _filter_short_term_memory(all_messages, trajectory_rounds) - response = model_with_tools.invoke(filtered_messages) + + # 判断是否为首次 agent 调用(当前轮尚无 AIMessage) + is_first_call = _is_first_agent_call(filtered_messages) + + # 构建反思提示并注入到消息列表(仅作引导,不记录到前端) + reflection = _build_reflection_prompt(agent_mode, is_first_call) + messages_for_llm = list(filtered_messages) + if reflection: + reflection_msg = HumanMessage(content=reflection, additional_kwargs={"synthetic": True, "reflection": True}) + messages_for_llm.append(reflection_msg) + + response = model_with_tools.invoke(messages_for_llm) + + # 提取模型真实输出的思考内容(content),记录到前端 think 面板 + # 模型同时输出 content + tool_calls 时,content 就是其思维链 + response_content = getattr(response, "content", "") + if response_content and isinstance(response_content, str) and response_content.strip(): + has_tool_calls = getattr(response, "tool_calls", None) + if has_tool_calls: + # 有 tool_calls 时 content 是思考过程,记录到 think 面板 + try: + from agent.history.tool_call_recorder import record_reflection_live, get_current_session + sid = get_current_session() + if sid: + record_reflection_live(sid, response_content.strip()) + except Exception: + pass if hasattr(response, "tool_calls") and response.tool_calls: for tc in response.tool_calls: diff --git a/src/agent/core/runtime.py b/src/agent/core/runtime.py index cd00e99..064bd9d 100644 --- a/src/agent/core/runtime.py +++ b/src/agent/core/runtime.py @@ -14,6 +14,7 @@ """ from __future__ import annotations +import threading from dataclasses import dataclass from typing import Any, Literal @@ -24,14 +25,14 @@ from langgraph.errors import GraphRecursionError 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.memory.system_prompt import PLAN_MODE_PROMPT, TEAM_MODE_PROMPT from agent.core.routing import first_pending_tool_meta from agent.core.tool_policy import classify_tool_execution from agent.utils.agent_utils import assistant_text, get_history, get_session_meta, summarizer, update_session_meta from agent.utils.tool_call_utils import normalize_tool_call from web.ui_session import rewrite_user_message_files_to_session_dir, session_dir, sync_manifest_from_chat_history -AgentMode = Literal["agent", "team"] +AgentMode = Literal["agent", "team", "plan"] @dataclass @@ -66,6 +67,39 @@ class AgentRuntime: _OOM_MESSAGE = "OOM: 已达到最大迭代次数限制,任务未能完成。请简化任务或增加 max_iterations 配置后重试。" +# ---------- Abort 机制 ---------- +_abort_flags: dict[str, threading.Event] = {} +_abort_lock = threading.Lock() + + +def set_abort_flag(session_id: str) -> None: + """设置指定会话的 abort 标志,通知正在执行的 Agent 终止。""" + if not session_id: + return + with _abort_lock: + event = _abort_flags.get(session_id) + if event is None: + event = threading.Event() + _abort_flags[session_id] = event + event.set() + + +def check_abort(session_id: str) -> bool: + """检查指定会话是否已被标记 abort。""" + if not session_id: + return False + with _abort_lock: + event = _abort_flags.get(session_id) + return event is not None and event.is_set() + + +def clear_abort_flag(session_id: str) -> None: + """清除指定会话的 abort 标志。""" + if not session_id: + return + with _abort_lock: + _abort_flags.pop(session_id, None) + def _invoke_with_recursion_limit( runtime: AgentRuntime, @@ -177,11 +211,12 @@ def _invoke_after_manual_tool_result( session_id: str, *, max_steps: int = 8, + agent_mode: str = "agent", ) -> tuple[list[Any], dict[str, Any] | None]: """人工确认后继续推进图,并在遇到新的待确认项时暂停。""" current_messages = messages for _ in range(max_steps): - result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": current_messages}, {"configurable": {"session_id": session_id}}) + result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": current_messages, "agent_mode": agent_mode}, {"configurable": {"session_id": session_id}}) all_messages = result["messages"] pending_meta = first_pending_tool_meta(all_messages, session_id) if pending_meta: @@ -276,10 +311,13 @@ def execute_agent( sync_manifest_from_chat_history(session_id, chat_history, "") return chat_history + # Plan 模式使用专用系统提示词 + system_prompt = PLAN_MODE_PROMPT if agent_mode == "plan" else runtime.system_prompt + messages, context_exceeded = summarizer( session_id=session_id, agent_name=runtime.name, - system_prompt=runtime.system_prompt, + system_prompt=system_prompt, user_content=user_content, llm=runtime.llm, ) @@ -304,14 +342,38 @@ def execute_agent( if session_id: turn_id = start_live_turn(session_id) set_current_session(session_id) - result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": messages}, graph_config) + clear_abort_flag(session_id) + result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": messages, "agent_mode": agent_mode}, graph_config) set_current_turn("") set_current_session("") + + # 检查是否被 abort + if session_id and check_abort(session_id): + clear_abort_flag(session_id) + end_live_turn(session_id) + try: + from agent.tools.tool import clear_todo_store_session + clear_todo_store_session(session_id) + except Exception: + pass + rolled_back = list(chat_history) + while rolled_back and rolled_back[-1].get("role") != "user": + rolled_back.pop() + if rolled_back: + rolled_back.pop() + if session_id: + sync_manifest_from_chat_history(session_id, rolled_back, "") + return rolled_back + all_messages = result["messages"] pending_meta = first_pending_tool_meta(all_messages, session_id) if pending_meta: end_live_turn(session_id) _remember_pending_user_content(pending_meta, user_content) + # 存储 agent_mode 以便 continue 时恢复 + approval_id = _pending_action_id(pending_meta) + if approval_id and approval_id in PENDING_TOOL_APPROVALS: + PENDING_TOOL_APPROVALS[approval_id]["agent_mode"] = agent_mode chat_history.append(pending_assistant_message(pending_meta)) if session_id: sync_manifest_from_chat_history(session_id, chat_history, "") @@ -361,6 +423,40 @@ def execute_agent( if session_id and turn_id: save_turn(session_id, turn_id, {"role": "user", "content": user_content}, all_messages) + # 任务正常完成:清空 todolist 防止跨任务纠缠 + if session_id: + try: + from agent.tools.tool import clear_todo_store_session + clear_todo_store_session(session_id) + except Exception: + pass + + except RuntimeError as e: + if str(e) == "ABORTED_BY_USER": + set_current_turn("") + set_current_session("") + clear_abort_flag(session_id) + end_live_turn(session_id) + try: + from agent.tools.tool import clear_todo_store_session + clear_todo_store_session(session_id) + except Exception: + pass + rolled_back = list(chat_history) + while rolled_back and rolled_back[-1].get("role") != "user": + rolled_back.pop() + if rolled_back: + rolled_back.pop() + if session_id: + sync_manifest_from_chat_history(session_id, rolled_back, "") + return rolled_back + import traceback + set_current_turn("") + set_current_session("") + end_live_turn(session_id) + print("[ERROR] 调用模型时出错:") + traceback.print_exc() + chat_history.append({"role": "assistant", "content": "抱歉,我遇到了一些问题,请稍后再试。"}) except Exception: import traceback set_current_turn("") @@ -413,6 +509,8 @@ def continue_after_human_action( remove_pending_ui_message(chat_history, approval_id) + agent_mode = pending.get("agent_mode", "agent") + if decision == "reject" and pending.get("type") != "human_interaction": content = "已拒绝,本次待确认操作不会执行。" chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": "text"}}) @@ -428,7 +526,7 @@ def continue_after_human_action( call_id = tool_call.get("id") or f"human_{approval_id}" tool_content = human_interaction_result(pending, decision, instruction) prior_messages.append(ToolMessage(content=tool_content, tool_call_id=call_id, name="human_interaction")) - all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id) + all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id, agent_mode=agent_mode) if pending_meta: _remember_pending_user_content(pending_meta, pending.get("user_content")) chat_history.append(pending_assistant_message(pending_meta)) @@ -485,7 +583,7 @@ def continue_after_human_action( 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) + all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id, agent_mode=agent_mode) if pending_meta: _remember_pending_user_content(pending_meta, pending.get("user_content")) chat_history.append(pending_assistant_message(pending_meta)) @@ -512,5 +610,11 @@ def continue_after_human_action( _persist_messages_from_pending_turn(session_id, persisted_messages, pending) if session_id: save_turn(session_id, manual_turn_id, {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages) + # 任务完成:清空 todolist + try: + from agent.tools.tool import clear_todo_store_session + clear_todo_store_session(session_id) + except Exception: + pass 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 1c88012..1a4351a 100644 --- a/src/agent/core/state.py +++ b/src/agent/core/state.py @@ -1,26 +1,27 @@ -"""LangGraph Agent 状态类型定义。 +"""LangGraph 状态类型定义。 系统定位: 定义图运行时共享状态 ``AgentState``,被 ``graph``、``nodes``、 ``routing`` 作为 StateGraph 的 schema 使用。 可扩展性: - - 可在此增加 ``session_id``、``agent_mode``、``pending_approval`` 等字段, + - 可在此增加 ``session_id``、``pending_approval`` 等字段, 需配合 reducer(如 add_messages)声明合并策略。 """ from __future__ import annotations -from typing import Annotated, Sequence, TypedDict +from typing import Annotated, Literal, Sequence, TypedDict from langchain_core.messages import BaseMessage from langgraph.graph.message import add_messages class AgentState(TypedDict): - """LangGraph 图状态:消息列表为唯一状态字段。 + """LangGraph 图状态。 字段: messages: 对话消息序列,通过 ``add_messages`` 追加合并。 + agent_mode: 运行模式 — "agent"(标准)| "plan"(先规划再执行)| "team"(多代理)。 系统定位: ReAct 循环(agent -> tools -> process_tool_artifact -> agent)的载体。 @@ -30,3 +31,4 @@ class AgentState(TypedDict): """ messages: Annotated[Sequence[BaseMessage], add_messages] + agent_mode: str # "agent" | "plan" | "team" diff --git a/src/agent/core/tool_config.json b/src/agent/core/tool_config.json index 677b157..8b0ee74 100644 --- a/src/agent/core/tool_config.json +++ b/src/agent/core/tool_config.json @@ -27,7 +27,7 @@ "name": "browser_control", "display_name": "BrowserControl", "description": "统一控制浏览器:打开/关闭浏览器、刷新/关闭标签页、打开指定 URL。", - "permission": "confirm", + "permission": "direct", "auto_execute_rules": [ { "parameter": "action", 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 84098e9..17ff537 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 new file mode 100644 index 0000000..1092446 Binary files /dev/null and b/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc differ diff --git a/src/agent/history/data_base/chat_history.db b/src/agent/history/data_base/chat_history.db index 686abdf..e206efc 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/20260620_183029/ui_messages.json b/src/agent/history/multi_inputs/20260620_183029/ui_messages.json deleted file mode 100644 index 2fe6590..0000000 --- a/src/agent/history/multi_inputs/20260620_183029/ui_messages.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "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/20260621_160508/ui_messages.json b/src/agent/history/multi_inputs/20260621_160508/ui_messages.json deleted file mode 100644 index db4839d..0000000 --- a/src/agent/history/multi_inputs/20260621_160508/ui_messages.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "version": 3, - "messages": [ - { - "role": "user", - "user": { - "content": [ - { - "type": "text", - "text": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频" - } - ] - } - }, - { - "role": "assistant", - "assistant": { - "text": "好的,我已经帮你完成了所有操作:\n1. 打开了Bilibili网站。\n2. 在搜索框中输入“麻薯波比呀”并进行搜索。\n3. 在搜索结果中找到了该UP主的最新视频《美国答应给伊朗3000亿,但美国不出钱》。\n4. 点击播放了该视频。\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": 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 - } - } - ] -} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260622_135934/ui_messages.json b/src/agent/history/multi_inputs/20260622_135934/ui_messages.json deleted file mode 100644 index 23bae5d..0000000 --- a/src/agent/history/multi_inputs/20260622_135934/ui_messages.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": 3, - "messages": [ - { - "role": "user", - "user": { - "content": [ - { - "type": "text", - "text": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频" - } - ] - } - }, - { - "role": "assistant", - "assistant": { - "text": "好的,我已经成功打开了Bilibili网站,搜索了UP主“麻薯波比呀”,并按发布时间排序找到了他的最新视频。当前屏幕上正在播放的视频标题为《美国答应给伊朗3000亿, 但美国不出钱》,发布者为“麻薯波比呀”。\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/20260622_150847/ui_messages.json b/src/agent/history/multi_inputs/20260622_150847/ui_messages.json deleted file mode 100644 index 8b99742..0000000 --- a/src/agent/history/multi_inputs/20260622_150847/ui_messages.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "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 - } - } - ] -} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260627_175231/ui_messages.json b/src/agent/history/multi_inputs/20260627_175231/ui_messages.json new file mode 100644 index 0000000..5eeeb9b --- /dev/null +++ b/src/agent/history/multi_inputs/20260627_175231/ui_messages.json @@ -0,0 +1,16 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "1、打开https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288#password;\r\n2、输入账号:19553226590;密码:3139785954并点击登录" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260627_175913/ui_messages.json b/src/agent/history/multi_inputs/20260627_175913/ui_messages.json new file mode 100644 index 0000000..1f82850 --- /dev/null +++ b/src/agent/history/multi_inputs/20260627_175913/ui_messages.json @@ -0,0 +1,28 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "请按照以下步骤进行:\r\n1、https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288登录该网站\r\n2、输入账号:19553226590;密码:3139785954\r\n3、点击登录" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "1. 分析上一轮执行结果:上一轮调用了 `todo_list` 标记所有步骤完成,结果显示任务已全部完成(9/9)。从之前的截图可以看到,登录尝试失败了,页面显示\"用户名或密码错误\"的红色提示。\n\n2. 任务状态:所有规划步骤已完成,但登录结果是失败的。需要向用户报告最终结果。\n\n3. 下一步操作:任务已完成,向用户汇报执行过程和结果。\n\n## 任务执行总结\n\n我已按照您的要求完成了所有步骤:\n\n✅ **已完成的步骤:**\n1. 打开浏览器并访问登录页面\n2. 输入账号:19553226590\n3. 输入密码:3139785954\n4. 点击登录按钮\n\n❌ **登录结果:失败**\n\n页面显示错误提示:\"**用户名或密码错误**\"\n\n可能的原因:\n- 账号或密码输入有误\n- 账号密码已变更\n- 账号可能不存在或被禁用\n\n建议您:\n1. 核对账号密码是否正确\n2. 尝试使用\"忘记密码\"功能重置密码\n3. 确认账号是否已注册并激活", + "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 5ade465..6ff56f4 100644 --- a/src/agent/history/tool_call_recorder.py +++ b/src/agent/history/tool_call_recorder.py @@ -145,8 +145,9 @@ def _artifact_image_data_urls(artifact: Any | None) -> list[str]: def end_live_turn(session_id: str) -> list[dict[str, Any]]: - """结束实时记录并返回该 turn 所有记录。""" + """结束实时记录并返回该 turn 所有记录(同时清空反思)。""" set_current_turn("") + clear_live_reflections(session_id) with _live_lock: return _live_records.pop(session_id, []) @@ -164,6 +165,50 @@ def _short_args(args: dict[str, Any], max_len: int = 40) -> str: return s +# ---------- 反思内容共享存储 ---------- +# Agent 反思(think)内容通过此 store 实时推送到前端展示 +_think_store: dict[str, list[dict[str, Any]]] = {} +_think_lock = threading.Lock() + + +def record_reflection_live(session_id: str, content: str) -> None: + """记录一次反思内容到共享存储,供前端轮询展示。 + + 输入: + session_id: 会话 ID。 + content: 反思提示文本。 + + 输出: 无。 + """ + if not session_id or not content: + return + with _think_lock: + if session_id not in _think_store: + _think_store[session_id] = [] + # 避免重复记录相同内容 + for entry in _think_store[session_id]: + if entry["content"] == content: + return + entry = { + "content": content, + "summary": content[:60].replace("\n", " "), + "timestamp": time.time(), + } + _think_store[session_id].append(entry) + + +def get_live_reflections(session_id: str) -> list[dict[str, Any]]: + """获取当前会话的反思列表(前端轮询)。""" + with _think_lock: + return list(_think_store.get(session_id, [])) + + +def clear_live_reflections(session_id: str) -> None: + """清空指定会话的反思记录。""" + with _think_lock: + _think_store.pop(session_id, None) + + # ---------- 持久化 ---------- def _ensure_dir(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) 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 deleted file mode 100644 index b9958e1..0000000 Binary files a/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png and /dev/null 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 deleted file mode 100644 index e431d64..0000000 Binary files a/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png and /dev/null 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 deleted file mode 100644 index 59962de..0000000 --- a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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 deleted file mode 100644 index dadcffc..0000000 --- a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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/20260621_160508/20260621_160537/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_0_0.png deleted file mode 100644 index 0323e29..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_0_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_1_0.png deleted file mode 100644 index 413be68..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_1_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_0_0.png deleted file mode 100644 index 7270313..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_0_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_1_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_1_0.png deleted file mode 100644 index dde4852..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_1_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_2_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_2_0.png deleted file mode 100644 index 27a8010..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_2_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_3_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_3_0.png deleted file mode 100644 index 171c377..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_3_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_4_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_4_0.png deleted file mode 100644 index 4eb333a..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_4_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_5_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_5_0.png deleted file mode 100644 index be5dc4e..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_5_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/20260622_123753/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260621_160508/20260622_123753/_artifact_browser_control_0_0.png deleted file mode 100644 index a1a7765..0000000 Binary files a/src/agent/history/tool_calling/20260621_160508/20260622_123753/_artifact_browser_control_0_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260621_160508/turn_20260621_160537.json b/src/agent/history/tool_calling/20260621_160508/turn_20260621_160537.json deleted file mode 100644 index 11e6c4c..0000000 --- a/src/agent/history/tool_calling/20260621_160508/turn_20260621_160537.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "session_id": "20260621_160508", - "turn_id": "20260621_160537", - "user_message": { - "role": "user", - "content": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频" - }, - "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": "current" - }, - "result_text": "已刷新当前标签页", - "result_files": [ - "_artifact_browser_control_1_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "bilibili website search bar at the top of the page" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_0_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "text": "麻薯波比呀", - "enter": true, - "action": "type" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_1_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "wait", - "wait_seconds": 3 - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_2_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "the first video thumbnail in the search results, titled \"美国答应给伊朗3000亿,美国又开始打窝了?\"" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_3_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "wait", - "wait_seconds": 5 - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_4_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "the main video player area to pause the video" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_5_0.png" - ], - "result_images": [] - } - ], - "assistant_response": "好的,我已经帮你完成了所有操作:\n1. 打开了Bilibili网站。\n2. 在搜索框中输入“麻薯波比呀”并进行搜索。\n3. 在搜索结果中找到了该UP主的最新视频《美国答应给伊朗3000亿,但美国不出钱》。\n4. 点击播放了该视频。\n\n现在视频正在播放中,你可以观看了。", - "timestamp": "20260621_160537" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260621_160508/turn_20260622_123753.json b/src/agent/history/tool_calling/20260621_160508/turn_20260622_123753.json deleted file mode 100644 index c02a41c..0000000 --- a/src/agent/history/tool_calling/20260621_160508/turn_20260622_123753.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "session_id": "20260621_160508", - "turn_id": "20260622_123753", - "user_message": { - "role": "user", - "content": "你好" - }, - "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": "你好!看起来你正在使用一个聊天界面。有什么我可以帮你的吗?", - "timestamp": "20260622_123753" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_0_0.png deleted file mode 100644 index f09bc2d..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_0_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_1_0.png deleted file mode 100644 index 71e4eee..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_1_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_2_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_2_0.png deleted file mode 100644 index b39d7a7..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_2_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_3_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_3_0.png deleted file mode 100644 index 24bff8e..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_3_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_0_0.png deleted file mode 100644 index 8d71db2..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_0_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_1_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_1_0.png deleted file mode 100644 index 9ba7cb1..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_1_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_2_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_2_0.png deleted file mode 100644 index 0e31180..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_2_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_3_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_3_0.png deleted file mode 100644 index 31558f5..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_3_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_4_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_4_0.png deleted file mode 100644 index 6c36b0f..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_4_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_5_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_5_0.png deleted file mode 100644 index 5536bf7..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_5_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_6_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_6_0.png deleted file mode 100644 index b0ada61..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_6_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_7_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_7_0.png deleted file mode 100644 index 8db1de4..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_7_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_8_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_8_0.png deleted file mode 100644 index fe2c23f..0000000 Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_8_0.png and /dev/null differ diff --git a/src/agent/history/tool_calling/20260622_135934/turn_20260622_140405.json b/src/agent/history/tool_calling/20260622_135934/turn_20260622_140405.json deleted file mode 100644 index 76fd945..0000000 --- a/src/agent/history/tool_calling/20260622_135934/turn_20260622_140405.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "session_id": "20260622_135934", - "turn_id": "20260622_140405", - "user_message": { - "role": "user", - "content": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频" - }, - "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": [] - }, - { - "name": "browser_control", - "args": { - "action": "open", - "url": "https://www.bilibili.com" - }, - "result_text": "已尝试打开 URL: https://www.bilibili.com", - "result_files": [ - "_artifact_browser_control_2_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "搜索栏" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_0_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "type", - "text": "麻薯波比呀" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_1_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "hotkey", - "keys": [ - "enter" - ] - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_2_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "最新发布 按钮" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_3_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "最新发布的视频,缩略图上有特朗普和伊朗国旗,标题为“美国答应给伊朗3000亿, 但美国不出钱”" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_4_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "视频播放器中央区域" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_5_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "暂停/播放按钮" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_6_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "播放按钮" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_7_0.png" - ], - "result_images": [] - }, - { - "name": "gui_tool", - "args": { - "action": "click", - "element_description": "“取消”按钮" - }, - "result_text": "", - "result_files": [ - "_artifact_gui_tool_8_0.png" - ], - "result_images": [] - }, - { - "name": "browser_control", - "args": { - "action": "open", - "url": "current" - }, - "result_text": "已刷新当前标签页", - "result_files": [ - "_artifact_browser_control_3_0.png" - ], - "result_images": [] - } - ], - "assistant_response": "好的,我已经成功打开了Bilibili网站,搜索了UP主“麻薯波比呀”,并按发布时间排序找到了他的最新视频。当前屏幕上正在播放的视频标题为《美国答应给伊朗3000亿, 但美国不出钱》,发布者为“麻薯波比呀”。\n\n任务已完成,您现在可以观看这个视频。", - "timestamp": "20260622_140405" -} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_browser_control_0_0.png new file mode 100644 index 0000000..2c5c8c0 Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_browser_control_0_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_0_0.png new file mode 100644 index 0000000..cf12d3a Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_0_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_1_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_1_0.png new file mode 100644 index 0000000..00b131d Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_1_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_2_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_2_0.png new file mode 100644 index 0000000..3e48dc5 Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_2_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_3_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_3_0.png new file mode 100644 index 0000000..ed6ce05 Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_3_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_4_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_4_0.png new file mode 100644 index 0000000..7a083a6 Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_4_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_5_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_5_0.png new file mode 100644 index 0000000..2d3afda Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_5_0.png differ diff --git a/src/agent/history/tool_calling/20260627_175913/turn_20260627_180047.json b/src/agent/history/tool_calling/20260627_175913/turn_20260627_180047.json new file mode 100644 index 0000000..4b12b79 --- /dev/null +++ b/src/agent/history/tool_calling/20260627_175913/turn_20260627_180047.json @@ -0,0 +1,172 @@ +{ + "session_id": "20260627_175913", + "turn_id": "20260627_180047", + "user_message": { + "role": "user", + "content": "请按照以下步骤进行:\r\n1、https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288登录该网站\r\n2、输入账号:19553226590;密码:3139785954\r\n3、点击登录" + }, + "tool_calls": [ + { + "name": "todo_list", + "args": { + "steps": [ + "打开浏览器", + "在地址栏输入登录URL并访问", + "等待页面加载完成", + "点击账号输入框", + "输入账号19553226590", + "点击密码输入框", + "输入密码3139785954", + "点击登录按钮", + "验证登录是否成功" + ], + "done_step": -1 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": []}}\n[Todo List]\n- [1/9] [Pending]: 打开浏览器\n- [2/9] [Pending]: 在地址栏输入登录URL并访问\n- [3/9] [Pending]: 等待页面加载完成\n- [4/9] [Pending]: 点击账号输入框\n- [5/9] [Pending]: 输入账号19553226590\n- [6/9] [Pending]: 点击密码输入框\n- [7/9] [Pending]: 输入密码3139785954\n- [8/9] [Pending]: 点击登录按钮\n- [9/9] [Pending]: 验证登录是否成功", + "result_files": [], + "result_images": [] + }, + { + "name": "browser_control", + "args": { + "action": "open", + "url": "https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288" + }, + "result_text": "已尝试打开 URL: https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288", + "result_files": [ + "_artifact_browser_control_0_0.png" + ], + "result_images": [] + }, + { + "name": "todo_list", + "args": { + "done_step": 2 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": [0, 1]}}\n[Todo List]\n- [1/9] [Done]: 打开浏览器\n- [2/9] [Done]: 在地址栏输入登录URL并访问\n- [3/9] [Pending]: 等待页面加载完成\n- [4/9] [Pending]: 点击账号输入框\n- [5/9] [Pending]: 输入账号19553226590\n- [6/9] [Pending]: 点击密码输入框\n- [7/9] [Pending]: 输入密码3139785954\n- [8/9] [Pending]: 点击登录按钮\n- [9/9] [Pending]: 验证登录是否成功", + "result_files": [], + "result_images": [] + }, + { + "name": "gui_tool", + "args": { + "actions": [ + { + "action": "click", + "element_description": "登录用户名/邮箱输入框" + } + ] + }, + "result_text": "1. Clicked target. grounded (502,531)", + "result_files": [ + "_artifact_gui_tool_0_0.png" + ], + "result_images": [] + }, + { + "name": "gui_tool", + "args": { + "actions": [ + { + "action": "type", + "element_description": "登录用户名/邮箱输入框", + "text": "19553226590" + } + ] + }, + "result_text": "1. Typed text.", + "result_files": [ + "_artifact_gui_tool_1_0.png" + ], + "result_images": [] + }, + { + "name": "gui_tool", + "args": { + "actions": [ + { + "action": "click", + "element_description": "密码输入框" + } + ] + }, + "result_text": "1. Clicked target. grounded (501,592)", + "result_files": [ + "_artifact_gui_tool_2_0.png" + ], + "result_images": [] + }, + { + "name": "gui_tool", + "args": { + "actions": [ + { + "action": "type", + "element_description": "密码输入框", + "text": "3139785954" + } + ] + }, + "result_text": "1. Typed text.", + "result_files": [ + "_artifact_gui_tool_3_0.png" + ], + "result_images": [] + }, + { + "name": "gui_tool", + "args": { + "actions": [ + { + "action": "click", + "element_description": "登录按钮" + } + ] + }, + "result_text": "1. Clicked target. grounded (500,689)", + "result_files": [ + "_artifact_gui_tool_4_0.png" + ], + "result_images": [] + }, + { + "name": "todo_list", + "args": { + "done_step": 8 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6, 7]}}\n[Todo List]\n- [1/9] [Done]: 打开浏览器\n- [2/9] [Done]: 在地址栏输入登录URL并访问\n- [3/9] [Done]: 等待页面加载完成\n- [4/9] [Done]: 点击账号输入框\n- [5/9] [Done]: 输入账号19553226590\n- [6/9] [Done]: 点击密码输入框\n- [7/9] [Done]: 输入密码3139785954\n- [8/9] [Done]: 点击登录按钮\n- [9/9] [Pending]: 验证登录是否成功", + "result_files": [], + "result_images": [] + }, + { + "name": "gui_tool", + "args": { + "actions": [ + { + "action": "wait", + "wait_seconds": 3.0 + }, + { + "action": "screenshot" + } + ] + }, + "result_text": "1. Waited 1.50 seconds.\n2. Screenshot (skipped, final screenshot will be taken)", + "result_files": [ + "_artifact_gui_tool_5_0.png" + ], + "result_images": [] + }, + { + "name": "todo_list", + "args": { + "done_step": 9 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6, 7, 8]}}\n[Todo List]\n- [1/9] [Done]: 打开浏览器\n- [2/9] [Done]: 在地址栏输入登录URL并访问\n- [3/9] [Done]: 等待页面加载完成\n- [4/9] [Done]: 点击账号输入框\n- [5/9] [Done]: 输入账号19553226590\n- [6/9] [Done]: 点击密码输入框\n- [7/9] [Done]: 输入密码3139785954\n- [8/9] [Done]: 点击登录按钮\n- [9/9] [Done]: 验证登录是否成功", + "result_files": [], + "result_images": [] + } + ], + "assistant_response": "1. 分析上一轮执行结果:上一轮调用了 `todo_list` 标记所有步骤完成,结果显示任务已全部完成(9/9)。从之前的截图可以看到,登录尝试失败了,页面显示\"用户名或密码错误\"的红色提示。\n\n2. 任务状态:所有规划步骤已完成,但登录结果是失败的。需要向用户报告最终结果。\n\n3. 下一步操作:任务已完成,向用户汇报执行过程和结果。\n\n## 任务执行总结\n\n我已按照您的要求完成了所有步骤:\n\n✅ **已完成的步骤:**\n1. 打开浏览器并访问登录页面\n2. 输入账号:19553226590\n3. 输入密码:3139785954\n4. 点击登录按钮\n\n❌ **登录结果:失败**\n\n页面显示错误提示:\"**用户名或密码错误**\"\n\n可能的原因:\n- 账号或密码输入有误\n- 账号密码已变更\n- 账号可能不存在或被禁用\n\n建议您:\n1. 核对账号密码是否正确\n2. 尝试使用\"忘记密码\"功能重置密码\n3. 确认账号是否已注册并激活", + "timestamp": "20260627_180047" +} \ No newline at end of file diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc index 382e635..083784f 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/system_prompt.py b/src/agent/memory/system_prompt.py index 7090c2c..f0fe204 100644 --- a/src/agent/memory/system_prompt.py +++ b/src/agent/memory/system_prompt.py @@ -75,6 +75,53 @@ DB_SYSTEM_PROMT = """ 请记住,你是一个协作伙伴,目标是让用户以最小的学习成本从数据库中获得有价值的信息。开始工作吧! """.strip() +PLAN_MODE_PROMPT = """ +你是一个具备多模态理解能力的桌面操作助手,运行在 **Plan 模式**下。Plan 模式要求你在执行任务前先进行详细规划。 + +【Plan 模式核心规则】 +- **首要任务**:收到用户请求后,你必须**第一步就调用 todo_list 工具**,将用户的任务拆解为非常细致、可执行的步骤列表。每个步骤应该具体到单个操作(如"打开浏览器"、"搜索XXX"、"截取屏幕"、"分析结果"等)。 +- **规划要求**:步骤拆解必须详尽,覆盖从开始到完成的完整流程。不要跳过任何中间步骤。步骤描述要清晰具体,便于后续逐一执行和跟踪。 +- **规划完成后**:生成 todolist 后,向用户简要汇报规划结果,然后**立即开始按步骤执行**,不要等待用户确认。 +- **执行过程中**:每完成一个步骤,调用 todo_list 标记该步骤为已完成(done_step),让用户实时看到进度。 +- **任务完成后**:确认所有步骤已完成,向用户总结执行过程和结果。 + +【通用行为准则】 +- 你的首要职责是理解用户的意图,并高效地完成用户的任务。 +- 对于任何用户上传的图片、音频或文件,直接利用你的多模态能力分析并回答,不要为此调用截图、OCR 等无关工具。 +- 只有在信息确实无法从当前对话或上传内容中获取,且必须通过外部工具才能获得时,才调用工具。 +- 当你调用工具收到截图或返回结果后,必须仔细观察返回内容,基于实际结果继续推进任务,严禁脱离实际结果凭空编造。 +- 每次工具调用后,向用户简要汇报当前进度和下一步计划,确保用户始终了解任务状态。 +- 如果不确定是否需要工具,优先选择不调用,用已有信息给出最佳答案。 +""".strip() + +# ---------- 反思 / Plan 首轮注入提示(作为 synthetic HumanMessage 注入到对话上下文)---------- +# 这些不是系统提示词,而是在 call_model 节点中动态拼入消息列表的引导文本。 + +PLAN_FIRST_CALL_PROMPT = """ +【系统指令 - Plan 模式】 +你现在处于 Plan 模式。请**立即调用 todo_list 工具**,按照用户任务拆解为步骤列表。 +规划要求: +- 如果用户任务包含多个子任务,每个子任务都需要被拆解为一个步骤。 +- 每一步骤必须具体到GUI操作(如"打开XXX浏览器"、"搜索XXX"、"单击XXX"等),但注意必须是确定的GUI操作,不能是模糊的操作(如"找到XXX按钮"等)。 +- **重要:合并同类操作**。同一个页面/界面上的多个连续操作应合并为一条步骤,用"并"连接,而不是拆成多条。例如: + * "点击顶部搜索框,输入'燕三嘤嘤嘤'并点击搜索"(合并了点击、输入、搜索三个操作) + * "在搜索结果中找到燕三嘤嘤嘤并点击头像进入他的主页"(合并了查找和点击) + * "在他的主页点开视频列表里的第一个视频并等待播放"(合并了点击和验证) + * "打开哔哩哔哩网站"(单个操作不需要合并) + * 反面示例(过于细碎,禁止):"1.点击搜索框 2.输入文字 3.点击搜索按钮 4.找到用户 5.点击头像" +- 步骤覆盖从开始到完成的完整流程,不要遗漏任何中间步骤,然后立即开始执行第一步,无需等待用户确认。 +- 请注意:如果用户明确分条步骤,每个步骤都需要被拆解为一个步骤(但仍需合并同一页面的连续操作)。 +""".strip() + +REFLECTION_PROMPT = """ +【请先在你的回复中输出你的反思,然后再调用工具或给出最终回复】 +你需要先用简短的一句话在文字中思考下一步该做什么,然后进行相应的工具调用。格式如下: +1. 先分析上一轮执行结果:上一个调用了什么工具?结果是成功还是失败? +2. 如果上一轮是 GUI 操作(点击、输入等)但未命中目标或未达预期效果,请根据最新的截图重新估算目标位置,调整坐标或参数后重试。 +3. 如果上一轮操作成功,说明你根据分析结果进行下一步操作。 +4. 然后调用对应的工具执行下一步,或如果任务已完成则直接回复用户(注意只需要回复用户最终结果即可)。 +""".strip() + TEAM_MODE_PROMPT = """ Team 模式目前处于预留阶段,多 Agent 协同功能尚未实现。 请向用户说明当前无法启动 Team 协同模式,并建议使用 Agent 模式来完成单 Agent 任务。 diff --git a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc index c0efc59..86d861e 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 0dd4bd5..4e2dc6e 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_software_control.cpython-312.pyc b/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc index ee7f7c9..22384e6 100644 Binary files a/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc differ diff --git a/src/agent/tools/gui_tool.py b/src/agent/tools/gui_tool.py index e150683..d5e4efd 100644 --- a/src/agent/tools/gui_tool.py +++ b/src/agent/tools/gui_tool.py @@ -15,31 +15,30 @@ from PIL import Image from pydantic import BaseModel, Field from agent.core.llm import llm -from agent.utils.env_utils import GROUNDING_HEIGHT, GROUNDING_WIDTH +import agent.utils.env_utils as env_utils GUIAction = Literal[ "click", "type", - "clickandtype", "drag_and_drop", "scroll", "hotkey", "hold_and_press", "wait", "screenshot", - "move_to", + "move_to", ] -class GUIToolInput(BaseModel): - # action 字段保持原样,无需修改 - action: Optional[GUIAction] = Field( - default=None, - description="GUI action to execute. If None or empty, returns current screenshot.", +class GUIActionItem(BaseModel): + """A single GUI action within an ordered sequence.""" + action: GUIAction = Field( + ..., + description="GUI action to execute.", ) element_description: Optional[str] = Field( default=None, - description="Natural-language description of the target screen element for grounded actions.", + description="Natural-language description of the target screen element for grounded actions (click, type, scroll, drag_and_drop, move_to). For type action, providing this will first locate and click the target input field before typing.", ) ending_description: Optional[str] = Field( default=None, @@ -58,6 +57,13 @@ class GUIToolInput(BaseModel): wait_seconds: float = Field(default=0.5, description="Seconds to wait.") +class GUIToolInput(BaseModel): + actions: list[GUIActionItem] = Field( + default_factory=list, + description="Ordered list of GUI actions to execute sequentially. Each item specifies one GUI operation (click, type, drag_and_drop, scroll, hotkey, hold_and_press, wait, screenshot, move_to). If empty, returns current screenshot without performing any action.", + ) + + def _get_gui_region() -> dict | None: """获取当前 GUI 操作显示器区域,失败返回 None(全屏)。""" try: @@ -81,7 +87,7 @@ def _screenshot_png() -> tuple[bytes, tuple[int, int]]: def _resize_for_grounding(image_bytes: bytes) -> bytes: image = Image.open(io.BytesIO(image_bytes)).convert("RGB") - image = image.resize((GROUNDING_WIDTH, GROUNDING_HEIGHT), Image.Resampling.LANCZOS) + image = image.resize((env_utils.GROUNDING_WIDTH, env_utils.GROUNDING_HEIGHT), Image.Resampling.LANCZOS) buf = io.BytesIO() image.save(buf, format="PNG") return buf.getvalue() @@ -98,8 +104,8 @@ def _parse_coordinates(text: str) -> tuple[int, int]: raise ValueError(f"grounding model did not return coordinates: {text!r}") x = int(float(numbers[0])) y = int(float(numbers[1])) - x = max(0, min(GROUNDING_WIDTH - 1, x)) - y = max(0, min(GROUNDING_HEIGHT - 1, y)) + x = max(0, min(env_utils.GROUNDING_WIDTH - 1, x)) + y = max(0, min(env_utils.GROUNDING_HEIGHT - 1, y)) return x, y @@ -107,8 +113,8 @@ def _scale_to_screen(coords: tuple[int, int], screen_size: tuple[int, int]) -> t """将 grounding 坐标映射到绝对屏幕坐标(考虑 GUI 操作区域偏移)。""" screen_width, screen_height = screen_size x, y = coords - local_x = round(x * screen_width / GROUNDING_WIDTH) - local_y = round(y * screen_height / GROUNDING_HEIGHT) + local_x = round(x * screen_width / env_utils.GROUNDING_WIDTH) + local_y = round(y * screen_height / env_utils.GROUNDING_HEIGHT) # 加上目标显示器在屏幕空间的偏移 region = _get_gui_region() offset_x = region["x"] if region else 0 @@ -124,10 +130,10 @@ def _ground_element(element_description: str) -> tuple[int, int, str]: grounding_png = _resize_for_grounding(raw_png) prompt = ( "You are a visual grounding model for desktop GUI control.\n" - f"The screenshot has been resized to exactly {GROUNDING_WIDTH}x{GROUNDING_HEIGHT} pixels.\n" + f"The screenshot has been resized to exactly {env_utils.GROUNDING_WIDTH}x{env_utils.GROUNDING_HEIGHT} pixels.\n" f"Query: {element_description.strip()}\n" "Return only one target point as pixel coordinates in this exact format: x,y\n" - f"Example: {GROUNDING_WIDTH // 2},{GROUNDING_HEIGHT // 2}\n" + f"Example: {env_utils.GROUNDING_WIDTH // 2},{env_utils.GROUNDING_HEIGHT // 2}\n" "Do not include any explanation." ) response = llm.invoke( @@ -183,9 +189,26 @@ class GUITool(BaseTool): args_schema: type[BaseModel] = GUIToolInput name: str = "gui_tool" 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, clickandtype, drag_and_drop, scroll, " - "hotkey, hold_and_press, wait, screenshot, move_to. Returns a screenshot of the current screen after the action." + "GUI 自动化工具。通过截图视觉定位桌面元素,将坐标映射到真实屏幕,并执行 pyautogui 动作。" + "接收一个有序的动作列表,按顺序依次执行。每个动作项支持的操作:click(点击)、type(输入文本,可通过 element_description 指定目标输入框自动聚焦)、" + "drag_and_drop(拖拽)、scroll(滚动)、hotkey(快捷键)、" + "hold_and_press(按住组合键并敲击)、wait(等待)、screenshot(截图)、move_to(移动鼠标)。\n" + "【type 使用说明】type 支持 element_description 参数来描述在哪里输入。当提供了 element_description 时," + "会先视觉定位并点击目标输入框,然后再输入文本,无需单独调用 click。不提供 element_description 则直接在当前焦点输入。\n" + "所有动作执行完毕后,返回当前屏幕的截图。\n" + "\n" + "示例:\n" + "- 登录表单:{\"actions\": [" + "{\"action\":\"type\",\"element_description\":\"用户名输入框\",\"text\":\"admin\"}," + "{\"action\":\"type\",\"element_description\":\"密码输入框\",\"text\":\"123456\",\"enter\":true}" + "]}\n" + "- 浏览器搜索:{\"actions\": [" + "{\"action\":\"type\",\"element_description\":\"浏览器地址栏\",\"text\":\"github.com\",\"enter\":true}" + "]}\n" + "- 等待并确认:{\"actions\": [" + "{\"action\":\"click\",\"element_description\":\"提交按钮\"}," + "{\"action\":\"wait\",\"wait_seconds\":2.0}" + "]}" ) response_format: Literal["content", "content_and_artifact"] = "content_and_artifact" @@ -199,53 +222,32 @@ class GUITool(BaseTool): def _run( self, - action: Optional[str] = None, - element_description: Optional[str] = None, - ending_description: Optional[str] = None, - text: str = "", - overwrite: bool = False, - enter: bool = False, - num_clicks: int = 1, - button_type: str = "left", - hold_keys: Optional[list[str]] = None, - keys: Optional[list[str]] = None, - press_keys: Optional[list[str]] = None, - clicks: int = 0, - shift: bool = False, - wait_seconds: float = 0.0, + actions: list[GUIActionItem] | None = None, ) -> tuple[str, dict]: - # ---------- screenshot 动作:仅截图 ---------- - if action == "screenshot": - return ("", self._artifact_screenshot()) # ---------- 无动作时直接截图 ---------- - if not action: + if not actions: return ("", self._artifact_screenshot()) - # ------------------------------------------- - dispatch_error: str | None = None - try: - self._dispatch( - action=action, - element_description=element_description, - ending_description=ending_description, - text=text, - overwrite=overwrite, - enter=enter, - num_clicks=num_clicks, - button_type=button_type, - hold_keys=hold_keys or [], - keys=keys or [], - press_keys=press_keys or [], - clicks=clicks, - shift=shift, - wait_seconds=wait_seconds, - ) - except Exception as exc: - dispatch_error = f"[GUI Error] {action}: {exc}" - print(dispatch_error) - time.sleep(1) - if dispatch_error: - return dispatch_error, self._artifact_screenshot() - return "", self._artifact_screenshot() + + # ---------- 顺序执行动作列表 ---------- + result_messages: list[str] = [] # 收集每个动作的返回信息 + for idx, act in enumerate(actions, start=1): + if act.action == "screenshot": + # screenshot 动作不需要实际执行,跳过并可选记录 + result_messages.append(f"{idx}. Screenshot (skipped, final screenshot will be taken)") + continue + + try: + msg = self._dispatch(**act.model_dump()) + result_messages.append(f"{idx}. {msg}") + except Exception as exc: + error_msg = f"{idx}. [GUI Error] {act.action}: {exc}" + result_messages.append(error_msg) + print(error_msg) + + time.sleep(1.2) + + final_text = "\n".join(result_messages) + return (final_text, self._artifact_screenshot()) def _dispatch(self, **kwargs: Any) -> str: action = str(kwargs["action"]).strip() @@ -253,8 +255,6 @@ class GUITool(BaseTool): return self._click(**kwargs) if action == "type": return self._type(**kwargs) - if action == "clickandtype": - return self._clickandtype(**kwargs) if action == "drag_and_drop": return self._drag_and_drop(**kwargs) if action == "scroll": @@ -264,7 +264,7 @@ class GUITool(BaseTool): if action == "hold_and_press": return self._hold_and_press(**kwargs) if action == "wait": - seconds = min(1.0, float(kwargs["wait_seconds"])) + seconds = min(1.5, float(kwargs["wait_seconds"])) time.sleep(seconds) return f"Waited {seconds:.2f} seconds." if action == "move_to": @@ -282,6 +282,18 @@ class GUITool(BaseTool): return f"Clicked target. {grounding}" def _type(self, **kwargs: Any) -> str: + element_desc = (kwargs.get("element_description") or "").strip() + if element_desc: + # 提供了目标元素描述:先定位点击再输入 + x, y, grounding = _ground_element(element_desc) + pyautogui.click(x, y) + if kwargs["overwrite"]: + _select_all_and_clear() + _type_text(kwargs["text"]) + if kwargs["enter"]: + pyautogui.press("enter") + return f"Clicked target and typed text. {grounding}" + # 不提供 element_description:直接在当前焦点输入 if kwargs["overwrite"]: _select_all_and_clear() _type_text(kwargs["text"]) @@ -289,16 +301,6 @@ class GUITool(BaseTool): pyautogui.press("enter") return "Typed text." - def _clickandtype(self, **kwargs: Any) -> str: - x, y, grounding = _ground_element(kwargs["element_description"]) - pyautogui.click(x, y) - if kwargs["overwrite"]: - _select_all_and_clear() - _type_text(kwargs["text"]) - if kwargs["enter"]: - pyautogui.press("enter") - return f"Clicked target and typed text. {grounding}" - def _drag_and_drop(self, **kwargs: Any) -> str: x1, y1, start_grounding = _ground_element(kwargs["element_description"] or "") x2, y2, end_grounding = _ground_element(kwargs["ending_description"] or "") diff --git a/src/agent/tools/tool.py b/src/agent/tools/tool.py index f1c12fe..3b6c34c 100644 --- a/src/agent/tools/tool.py +++ b/src/agent/tools/tool.py @@ -16,7 +16,7 @@ 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, GROUNDING_HEIGHT, GROUNDING_WIDTH +import agent.utils.env_utils as env_utils class WebSearch(BaseTool): @@ -99,9 +99,9 @@ class BrowserControl(BaseTool): if browser_name is None: return webbrowser.get(), None name_lower = browser_name.lower().strip() - if name_lower not in BROWSER_MAP: - raise ValueError(f"不支持的浏览器: {browser_name},支持: {', '.join(BROWSER_MAP.keys())}") - reg_name, title_keyword = BROWSER_MAP[name_lower] + if name_lower not in env_utils.BROWSER_MAP: + raise ValueError(f"不支持的浏览器: {browser_name},支持: {', '.join(env_utils.BROWSER_MAP.keys())}") + reg_name, title_keyword = env_utils.BROWSER_MAP[name_lower] try: controller = webbrowser.get(reg_name) except webbrowser.Error: @@ -132,7 +132,7 @@ class BrowserControl(BaseTool): # ---------- 核心操作(返回操作结果描述) ---------- def _try_activate_any_browser(self) -> bool: """尝试激活任意已知浏览器窗口(不指定 browser 时的回退方案)。""" - for _reg_name, title_keyword in BROWSER_MAP.values(): + for _reg_name, title_keyword in env_utils.BROWSER_MAP.values(): if self._activate_window(title_keyword): return True return False @@ -212,7 +212,7 @@ class BrowserControl(BaseTool): time.sleep(0.3) try: pil_img = pyautogui.screenshot() - pil_img = pil_img.resize((GROUNDING_WIDTH, GROUNDING_HEIGHT), Image.Resampling.LANCZOS) + pil_img = pil_img.resize((env_utils.GROUNDING_WIDTH, env_utils.GROUNDING_HEIGHT), Image.Resampling.LANCZOS) buf = io.BytesIO() pil_img.save(buf, format="PNG") raw_png = buf.getvalue() @@ -375,10 +375,10 @@ def update_todo_store_session( done_steps = [] if stored_steps and 1 <= done_step <= len(stored_steps): - idx = done_step - 1 - if idx not in done_steps: - done_steps.append(idx) - done_steps.sort() + for i in range(done_step): + if i not in done_steps: + done_steps.append(i) + done_steps.sort() todo_data = {"steps": stored_steps, "done_steps": done_steps} _todo_store[session_id] = todo_data @@ -423,7 +423,7 @@ class TodoListInput(BaseModel): ) done_step: int = Field( default=-1, - description="本次新完成的步骤编号(从1开始)。-1 表示仅查看当前状态,不标记完成。", + description="当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为完成。-1 表示仅查看当前状态。", ) @model_validator(mode="before") @@ -441,20 +441,30 @@ class TodoListInput(BaseModel): class TodoList(BaseTool): - """可选的任务列表工具,模型可主动调用以跟踪任务进度。""" + """任务管理工具,用于拆分复杂任务为多个步骤并跟踪进度。 + + Plan 模式下此工具为**必须调用**,Agent 模式下为可选。 + 首次调用时传入完整步骤列表,后续仅传 done_step 标记进度。 + """ args_schema: type[BaseModel] = TodoListInput name: str = "todo_list" description: str = ( - "可选的任务管理工具。当你需要拆分复杂任务为多个步骤并跟踪进度时调用。\n" + "任务管理工具。用于将复杂任务拆分为多个步骤并跟踪执行进度。\n" + "Plan 模式下必须首先调用此工具创建任务计划;Agent 模式下按需调用。\n" "参数说明:\n" - "- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。后续更新进度时请省略此参数(传 null 或不传),工具会复用之前设定的步骤列表,省去重复传输。\n" - "- done_step (int, 可选):刚完成的步骤编号(从1开始),一次只标记一个。不传或传 -1 则只查看当前状态不做修改。\n" + "- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。\n" + " **步骤粒度要求**:同一页面/界面上的连续操作应合并为一条步骤,用\"并\"连接。例如:\n" + ' * 推荐:"点击搜索框,输入XXX并点击搜索"(合并了点击、输入、搜索)\n' + ' * 推荐:"在结果中点击目标进入主页"(合并了查找+点击)\n' + ' * 禁止:"1.点击搜索框 2.输入文字 3.点击搜索"(过于细碎)\n' + "- done_step (int, 可选):当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为已完成。不传或传 -1 则只查看当前状态。\n" "调用示例:\n" - '- 首次创建:{"steps": ["准备食材", "切菜", "烹饪", "装盘"], "done_step": -1}\n' - '- 完成第1步:"done_step": 1}(省略 steps)\n' - '- 查看状态:不传任何参数或 {"done_step": -1}\n' - "仅在需要跟踪多步骤任务进度时调用,简单任务不需要调用。" + '- 首次创建(B站搜索示例):{"steps": ["打开哔哩哔哩网站", "点击顶部搜索框,输入\'燕三嘤嘤嘤\'并点击搜索", "在搜索结果中找到燕三嘤嘤嘤并点击头像或名称进入他的主页", "在他的主页点开视频列表里的第一个视频", "验证视频是否正在播放"], "done_step": -1}\n' + '- 已完成前2步:{"done_step": 2} (步骤1和2均标记为完成)\n' + '- 已完成前4步:{"done_step": 4}\n' + '- 查看状态:{"done_step": -1}\n' + "任务全部完成后无需再次调用。" ) response_format: Literal["content", "content_and_artifact"] = "content" @@ -472,12 +482,12 @@ class TodoList(BaseTool): self._stored_steps = steps self._done_set = [] - # 标记新完成步骤(1-indexed → 0-indexed) + # 标记步骤完成:传入 done_step 时,该步及之前所有步骤均标记为完成(1-indexed → 0-indexed) if self._stored_steps and 1 <= done_step <= len(self._stored_steps): - idx = done_step - 1 - if idx not in self._done_set: - self._done_set.append(idx) - self._done_set.sort() + for i in range(done_step): + if i not in self._done_set: + self._done_set.append(i) + self._done_set.sort() todo_data = { "steps": self._stored_steps, diff --git a/src/agent/tools/tool_software_control.py b/src/agent/tools/tool_software_control.py index 54596a1..75efacd 100644 --- a/src/agent/tools/tool_software_control.py +++ b/src/agent/tools/tool_software_control.py @@ -23,6 +23,7 @@ except Exception: _windll = getattr(ctypes, "windll", None) user32: object | None = _windll.user32 if os.name == "nt" and _windll is not None else None WM_CLOSE = 0x0010 +SW_RESTORE = 9 _INDEX_PATH = Path(__file__).resolve().parents[1] / "history" / "data_base" / "software_index.json" @@ -48,6 +49,14 @@ _SKIP_KEYWORDS = ( "license", "update", ) +_CHAT_APP_PROCESS_NAMES = { + "qq": ["QQ", "NTQQ"], + "腾讯qq": ["QQ", "NTQQ"], + "腾讯QQ": ["QQ", "NTQQ"], + "wechat": ["WeChat", "Weixin"], + "weixin": ["WeChat", "Weixin"], + "微信": ["WeChat", "Weixin"], +} @dataclass @@ -354,6 +363,43 @@ def _find_visible_windows_for_pids(pids: list[int]) -> list[tuple[int, int, str] return results +def _chat_app_process_names(query: str, record: SoftwareRecord) -> list[str]: + candidates = [query, record.display_name, *record.aliases] + for candidate in candidates: + key = _normalize(candidate) + if key in _CHAT_APP_PROCESS_NAMES: + return _CHAT_APP_PROCESS_NAMES[key] + return [] + + +def _find_pids_for_process_names(process_names: list[str]) -> list[int]: + process_keys = {_normalize(name) for name in process_names if name} + matched: list[int] = [] + for proc in _list_running_processes(): + if _normalize(proc["stem"]) in process_keys: + try: + matched.append(int(proc["pid"])) + except ValueError: + continue + return sorted(set(matched)) + + +def _activate_existing_window_for_pids(pids: list[int]) -> tuple[int, int, str] | None: + if user32 is None or not pids: + return None + windows = _find_visible_windows_for_pids(pids) + if not windows: + return None + pid, hwnd, title = windows[0] + try: + user32.ShowWindow(hwnd, SW_RESTORE) + user32.BringWindowToTop(hwnd) + user32.SetForegroundWindow(hwnd) + except Exception: + pass + return pid, hwnd, title + + def _wait_for_pids_exit(pids: list[int], timeout_seconds: float = 5.0) -> list[int]: deadline = time.time() + timeout_seconds remaining = set(pids) @@ -449,6 +495,21 @@ class SoftwareControl(BaseTool): ) if action == "open": + running_pids = _find_running_pids(best_record) + chat_process_names = _chat_app_process_names(query, best_record) + if chat_process_names: + running_pids = sorted(set(running_pids + _find_pids_for_process_names(chat_process_names))) + existing_window = _activate_existing_window_for_pids(running_pids) + if existing_window: + pid, hwnd, title = existing_window + return ( + f"Switched to existing software window: {best_record.display_name}\n" + f"Match score: {best_score:.2f}\n" + f"PID: {pid}\n" + f"Window title: {title}\n" + f"Window handle: {hwnd}\n" + "Note: an existing visible window was detected, so the shortcut was not launched again." + ) try: opened_target = _open_record(best_record) except Exception as exc: diff --git a/src/agent/utils/__pycache__/env_utils.cpython-312.pyc b/src/agent/utils/__pycache__/env_utils.cpython-312.pyc index f404c4f..d46cb51 100644 Binary files a/src/agent/utils/__pycache__/env_utils.cpython-312.pyc and b/src/agent/utils/__pycache__/env_utils.cpython-312.pyc differ diff --git a/src/agent/utils/env_utils.py b/src/agent/utils/env_utils.py index 6b8dad4..19e52d6 100644 --- a/src/agent/utils/env_utils.py +++ b/src/agent/utils/env_utils.py @@ -121,6 +121,22 @@ def _load_config() -> dict: return {} +def reload_config() -> dict: + """实时重载 env_config.json 配置,刷新所有模块级常量。 + + 可在不重启进程的情况下重新读取配置文件,更新 GROUNDING_WIDTH 等运行时参数。 + 注意:调用方需通过 ``import agent.utils.env_utils as env_utils`` 然后访问 + ``env_utils.GROUNDING_WIDTH`` 才能获取到更新后的值,直接 ``from ... import`` + 的方式会绑定旧值。 + + 输出: + 加载到的配置字典,失败时返回空字典。 + """ + result = _load_config() + _refresh_globals() + return result + + _load_config() _refresh_globals() # 确保无论配置加载成功与否,都有默认值 diff --git a/src/web/__pycache__/server.cpython-312.pyc b/src/web/__pycache__/server.cpython-312.pyc index b3584f7..e4d9a5d 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 new file mode 100644 index 0000000..2e4d3a9 Binary files /dev/null and b/src/web/__pycache__/server.cpython-313.pyc differ diff --git a/src/web/app.css b/src/web/app.css index 321b09d..a4ced32 100644 --- a/src/web/app.css +++ b/src/web/app.css @@ -1209,6 +1209,17 @@ body { cursor: not-allowed; } +.btn-pause { + background: linear-gradient(165deg, #f59e0b, #d97706); + border-color: rgba(217, 119, 6, 0.85); + color: #fff; + box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35); +} + +.btn-pause:hover:not(:disabled) { + background: linear-gradient(165deg, #fbbf24, #f59e0b); +} + .btn-secondary { background: rgba(255, 255, 255, 0.35); } @@ -2034,6 +2045,7 @@ body { .model-table .col-name { flex: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: #1e293b; } .model-table .col-model { flex: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #475569; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.78rem; } .model-table .col-url { flex: 2.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #94a3b8; font-size: 0.72rem; } +.model-table .col-think { flex: 0 0 52px; text-align: center; font-size: 0.72rem; font-weight: 600; } .model-table .col-actions { flex: 0 0 90px; display: flex; gap: 0.35rem; justify-content: flex-end; align-items: center; } /* 表格内操作按钮 */ @@ -2119,6 +2131,13 @@ body { gap: 0.5rem; } +.toggle-hint { + font-size: 0.72rem; + color: #94a3b8; + margin-left: 0.35rem; + vertical-align: middle; +} + .toggle-row .toggle-label { font-size: 0.82rem; font-weight: 600; @@ -2374,6 +2393,91 @@ body { opacity: 0.92; } +/* ===== 反思(think)面板 ===== */ +.think-indicator { + margin-top: 0.35rem; + padding: 0.25rem 0; + border-top: 1px solid rgba(148, 163, 184, 0.10); + cursor: pointer; + user-select: none; + -webkit-user-select: none; +} + +.think-indicator-icon { + font-size: 0.8rem; +} + +.think-indicator-text { + font-size: 0.72rem; + color: rgba(100, 116, 139, 0.55); + font-weight: 400; + letter-spacing: 0.02em; + transition: color 0.3s; +} + +.think-indicator:hover .think-indicator-text { + color: rgba(100, 116, 139, 0.75); +} + +.think-list-wrap { + display: none; + margin-top: 0.15rem; + max-height: 220px; + overflow-y: auto; + padding: 0.15rem 0; + border-top: 1px solid rgba(148, 163, 184, 0.06); +} + +.think-list-wrap--expanded { + display: block; +} + +.think-item { + padding: 0.18rem 0.3rem; + border-radius: 4px; + font-size: 0.7rem; + color: rgba(51, 65, 85, 0.48); + line-height: 1.35; + transition: color 0.3s; +} + +.think-item:hover { + background: rgba(148, 163, 184, 0.05); +} + +.think-item-summary { + display: flex; + align-items: center; + gap: 0.2rem; +} + +.think-bullet { + color: rgba(100, 116, 139, 0.40); + font-size: 0.65rem; +} + +.think-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.think-item-detail { + margin-top: 0.3rem; + padding: 0.35rem 0.45rem; + background: rgba(148, 163, 184, 0.06); + border-radius: 4px; + font-size: 0.68rem; + line-height: 1.5; + color: rgba(51, 65, 85, 0.55); + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; + font-family: inherit; +} + .todo-list-overlay { position: fixed; top: 80px; diff --git a/src/web/app.js b/src/web/app.js index 18e7965..90657e4 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -39,6 +39,7 @@ /** @type {File[]} */ let pendingFiles = []; let sending = false; + let abortController = null; let mediaRecorder = null; let recorderStream = null; let recordedChunks = []; @@ -1171,6 +1172,93 @@ // ===== 工具调用展示(chat 视图 / 持久化) ===== let liveToolCallExpanded = false; + let thinkExpanded = false; + + /** 在思考气泡内渲染反思(think)面板 */ + function injectThinkPanel(typingEl, reflections) { + if (!typingEl) return; + const bubble = typingEl.querySelector(".msg-bubble") || typingEl; + + // 反思指示器 + let thinkIndicator = bubble.querySelector(".think-indicator"); + if (!thinkIndicator) { + thinkIndicator = document.createElement("div"); + thinkIndicator.className = "think-indicator"; + thinkIndicator.title = "点击展开/收起 Agent 反思内容"; + thinkIndicator.style.display = "none"; + // 插入到 agent-running-indicator 前面 + const runningIndicator = bubble.querySelector(".agent-running-indicator"); + if (runningIndicator) { + bubble.insertBefore(thinkIndicator, runningIndicator); + } else { + bubble.appendChild(thinkIndicator); + } + thinkIndicator.addEventListener("click", () => { + thinkExpanded = !thinkExpanded; + const wrap = bubble.querySelector(".think-list-wrap"); + if (wrap) wrap.classList.toggle("think-list-wrap--expanded", thinkExpanded); + updateThinkIndicatorText(thinkIndicator, reflections); + }); + } + + if (!reflections || !reflections.length) { + thinkIndicator.style.display = "none"; + const wrap = bubble.querySelector(".think-list-wrap"); + if (wrap) wrap.remove(); + return; + } + + thinkIndicator.style.display = ""; + updateThinkIndicatorText(thinkIndicator, reflections); + + // 反思列表容器 + let listWrap = bubble.querySelector(".think-list-wrap"); + if (!listWrap) { + listWrap = document.createElement("div"); + listWrap.className = "think-list-wrap" + (thinkExpanded ? " think-list-wrap--expanded" : ""); + // 插入到 think-indicator 后面 + thinkIndicator.insertAdjacentElement("afterend", listWrap); + } + listWrap.classList.toggle("think-list-wrap--expanded", thinkExpanded); + + // 增量更新列表项 + const existingCount = listWrap.querySelectorAll(".think-item").length; + if (reflections.length <= existingCount) return; // 无新增 + + for (let i = existingCount; i < reflections.length; i++) { + const ref = reflections[i]; + const item = document.createElement("div"); + item.className = "think-item"; + // 摘要行 + const summary = document.createElement("div"); + summary.className = "think-item-summary"; + summary.innerHTML = `• ${escapeHtml(ref.summary || "反思")}`; + item.appendChild(summary); + // 存储完整内容 + item._thinkContent = ref.content || ""; + // 点击展开完整内容 + item.style.cursor = "pointer"; + item.addEventListener("click", () => { + let detail = item.querySelector(".think-item-detail"); + if (detail) { + detail.remove(); + item.setAttribute("aria-expanded", "false"); + return; + } + detail = document.createElement("pre"); + detail.className = "think-item-detail"; + detail.textContent = item._thinkContent; + item.appendChild(detail); + item.setAttribute("aria-expanded", "true"); + }); + listWrap.appendChild(item); + } + } + + function updateThinkIndicatorText(indicator, reflections) { + const count = reflections ? reflections.length : 0; + indicator.innerHTML = `🧠 think (${count})${thinkExpanded ? " ▾" : " ▸"}`; + } /** 在思考气泡内更新工具调用指示器与列表(增量更新,不重建 DOM) */ function injectLiveToolCalls(typingEl, liveCalls) { @@ -1552,6 +1640,43 @@ renderMessages(data.messages); } + function setSendBtnPauseMode(pauseMode) { + if (!sendBtn) return; + if (pauseMode) { + sendBtn.textContent = "暂停"; + sendBtn.classList.add("btn-pause"); + } else { + sendBtn.textContent = "发送"; + sendBtn.classList.remove("btn-pause"); + } + } + + async function handlePauseClick() { + if (!sessionId || !sending || !abortController) return; + if (!window.confirm("确认暂停当前任务?\n\n暂停后当前对话将恢复到上一条消息的状态,工具调用记录和TodoList将被清空。")) return; + // 1. 调用后端 abort + try { + const fd = new FormData(); + fd.append("session_id", sessionId); + const data = await api("/api/chat/abort", { method: "POST", body: fd }); + sessionId = data.sessionId; + sessions = data.sessions || []; + clearTodoOverlay(); + renderSessions(); + renderMessages(data.messages || []); + } catch (e) { + // 回退:至少恢复 priorMessages + } + // 2. 取消前端 fetch + if (abortController) { + abortController.abort(); + abortController = null; + } + sending = false; + setSendBtnPauseMode(false); + sendBtn.disabled = false; + } + async function sendChat() { if (!sessionId || sending) return; const trimmed = textInput.value.trim(); @@ -1565,6 +1690,9 @@ const blobUrls = []; const hadOptimisticMedia = savedFiles.length > 0; + // 切换发送按钮为暂停按钮 + setSendBtnPauseMode(true); + if (hadOptimisticMedia) { const fileParts = savedFiles.map((f) => { const kind = inferUploadKind(f); @@ -1585,6 +1713,7 @@ renderAttachmentChips(); } + abortController = new AbortController(); const fd = new FormData(); fd.append("session_id", sessionId); fd.append("text", trimmed); @@ -1608,17 +1737,24 @@ // 清空上轮工具调用状态(新 turn) liveToolCallExpanded = false; + thinkExpanded = false; clearTodoOverlay(); - // 启动实时工具调用轮询(增量更新,低频率不卡) + // 启动实时工具调用与反思轮询(增量更新,低频率不卡) let livePollTimer = null; let livePollActive = true; const pollLiveToolCalls = async () => { if (!livePollActive) return; try { const liveData = await api(`/api/tool-calls/${encodeURIComponent(sessionId)}/live`); - if (liveData.tool_calls && liveData.tool_calls.length > 0 && typingEl) { - injectLiveToolCalls(typingEl, liveData.tool_calls); + if (typingEl) { + // 先更新 think 面板 + const reflections = liveData.reflections || []; + injectThinkPanel(typingEl, reflections); + // 再更新工具调用列表 + if (liveData.tool_calls && liveData.tool_calls.length > 0) { + injectLiveToolCalls(typingEl, liveData.tool_calls); + } } } catch { /* ignore */ } if (livePollActive) livePollTimer = setTimeout(pollLiveToolCalls, 1200); @@ -1643,7 +1779,7 @@ fd2.append("session_id", sessionId); fd2.append("output_type", outputModeSelect ? outputModeSelect.value : "text"); fd2.append("agent_mode", agentModeSelect ? agentModeSelect.value : "agent"); - const done = await api("/api/chat/complete", { method: "POST", body: fd2 }); + const done = await api("/api/chat/complete", { method: "POST", body: fd2, signal: abortController.signal }); try { const todoData = await api(`/api/todo-list/${encodeURIComponent(sessionId)}`); @@ -1681,16 +1817,23 @@ } } catch (e) { revokeBlobUrlStack(blobUrls); - if (hadOptimisticMedia) { + if (e && e.name === "AbortError") { + // 用户点击了暂停 → 不显示错误,恢复 priorMessages + renderMessages(priorMessages); + } else if (hadOptimisticMedia) { textInput.value = savedTextRaw; pendingFiles = savedFiles.slice(); renderAttachmentChips(); renderMessages(priorMessages); } if (typingEl && typingEl.parentNode) typingEl.remove(); - alert(e.message || String(e)); + if (!(e && e.name === "AbortError")) { + alert(e.message || String(e)); + } } finally { sending = false; + abortController = null; + setSendBtnPauseMode(false); sendBtn.disabled = false; } } @@ -1721,7 +1864,13 @@ } }); - sendBtn.addEventListener("click", sendChat); + sendBtn.addEventListener("click", () => { + if (sending) { + handlePauseClick().catch((e) => alert(e.message || String(e))); + } else { + sendChat(); + } + }); micBtn?.addEventListener("click", () => toggleMicRecording().catch((e) => alert(e.message || String(e)))); autoInputCheck?.addEventListener("change", (ev) => toggleAutoInput(ev.target.checked)); clearBtn.addEventListener("click", () => clearChat().catch((e) => alert(e.message))); @@ -2615,14 +2764,15 @@ table.className = "model-table"; const thead = document.createElement("div"); thead.className = "model-table-head"; - thead.innerHTML = '名称ModelBase URL操作'; + thead.innerHTML = '名称ModelBase URL思考操作'; table.appendChild(thead); cachedModels.forEach((m, mi) => { const row = document.createElement("div"); row.className = "model-table-row"; row.innerHTML = `${escapeHtml(m.name)} ${escapeHtml(m.model)} - ${escapeHtml(m.base_url)}`; + ${escapeHtml(m.base_url)} + ${m.enable_thinking ? 'ON' : 'OFF'}`; const actions = document.createElement("span"); actions.className = "col-actions"; const editBtn = document.createElement("button"); @@ -2676,6 +2826,27 @@ modalFields[key] = inp; modalBody.appendChild(div); }); + // 思考模式开关 + const thinkingDiv = document.createElement("div"); + thinkingDiv.className = "modal-field"; + const thinkingLabel = document.createElement("label"); + thinkingLabel.textContent = "思考模式"; + const thinkingWrap = document.createElement("label"); + thinkingWrap.className = "toggle-switch"; + const thinkingCheckbox = document.createElement("input"); + thinkingCheckbox.type = "checkbox"; + const thinkingSlider = document.createElement("span"); + thinkingSlider.className = "toggle-slider"; + thinkingWrap.appendChild(thinkingCheckbox); + thinkingWrap.appendChild(thinkingSlider); + const thinkingHint = document.createElement("span"); + thinkingHint.className = "toggle-hint"; + thinkingHint.textContent = " 关闭后禁用模型思考(默认关闭,减少延迟)"; + thinkingDiv.appendChild(thinkingLabel); + thinkingDiv.appendChild(thinkingWrap); + thinkingDiv.appendChild(thinkingHint); + modalFields["enable_thinking"] = thinkingCheckbox; + modalBody.appendChild(thinkingDiv); modalBox.appendChild(modalBody); const modalActions = document.createElement("div"); modalActions.className = "model-modal-actions"; @@ -2697,6 +2868,7 @@ base_url: modalFields.base_url.value.trim(), timeout: parseFloat(modalFields.timeout.value) || 60, max_retries: 0, + enable_thinking: modalFields.enable_thinking.checked, }; if (!data.name || !data.model) { alert("名称和模型名不能为空"); return; } if (editingModelIdx >= 0) { @@ -2758,12 +2930,14 @@ fields.api_key.value = m.api_key || ""; fields.base_url.value = m.base_url || ""; fields.timeout.value = m.timeout || 60; + fields.enable_thinking.checked = !!m.enable_thinking; } else { fields.name.value = ""; fields.model.value = ""; fields.api_key.value = ""; fields.base_url.value = ""; fields.timeout.value = 60; + fields.enable_thinking.checked = false; } overlay.classList.add("show"); } diff --git a/src/web/index.html b/src/web/index.html index 747047c..8a43e76 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -125,6 +125,7 @@ diff --git a/src/web/server.py b/src/web/server.py index 6625ae8..1fc8f78 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -580,6 +580,47 @@ def api_chat_complete( return _chat_complete_core(session_id, output_type=output_type, agent_mode=agent_mode) +@app.post("/api/chat/abort") +def api_chat_abort(session_id: str = Form(...)) -> dict[str, Any]: + """中止当前正在执行的 Agent 任务,回滚到最后一条消息之前的状态。 + + 输入: + session_id: 表单字段,会话 ID。 + + 输出: + {"sessionId": str, "sessions": list[str], "messages": list[dict]} + """ + if not session_id: + raise HTTPException(status_code=400, detail="缺少 session_id") + try: + from agent.core.runtime import set_abort_flag + set_abort_flag(session_id) + except Exception: + pass + # 回滚 manifest:移除最后一条 user 消息及后续 assistant + hist = ui.load_ui_messages_for_session(session_id) + rolled_back = list(hist or []) + while rolled_back and rolled_back[-1].get("role") != "user": + rolled_back.pop() + if rolled_back: + rolled_back.pop() + ui.sync_manifest_from_chat_history(session_id, rolled_back, "") + # 清除 todo store + try: + from agent.tools.tool import clear_todo_store_session + clear_todo_store_session(session_id) + except Exception: + pass + ids = ui.list_session_ids_ordered() + if session_id not in ids: + ids = [session_id, *ids] + return { + "sessionId": session_id, + "sessions": ids, + "messages": _serialize_messages(rolled_back, session_id), + } + + @@ -859,12 +900,15 @@ def api_reload_configs() -> dict[str, Any]: # ---------- 工具调用历史 API ---------- @app.get("/api/tool-calls/{session_id}/live") def api_get_live_tool_calls(session_id: str) -> dict[str, Any]: - """获取当前进行中的工具调用(前端执行期间实时轮询)。""" + """获取当前进行中的工具调用与反思内容(前端执行期间实时轮询)。""" try: - from agent.history.tool_call_recorder import get_live_tool_calls - return {"tool_calls": get_live_tool_calls(session_id)} + from agent.history.tool_call_recorder import get_live_tool_calls, get_live_reflections + return { + "tool_calls": get_live_tool_calls(session_id), + "reflections": get_live_reflections(session_id), + } except Exception as e: - return {"tool_calls": [], "error": str(e)} + return {"tool_calls": [], "reflections": [], "error": str(e)} @app.get("/api/tool-image")