"""对话循环 — 唯一循环,无重复""" import threading from datetime import datetime from .types import ToolCall, ToolResult from xcli_core.tool_result import ErrorCode from .parser import interpret class Runner: def __init__(self, engine, manager, history, safety, display): self._engine = engine # EngineAdapter self._mgr = manager # UnifiedToolManager self._history = history # History self._safety = safety # Safety self._display = display # Display self._cancel = threading.Event() self._max_rounds = 50 self._prompt_builder = None def set_prompt_builder(self, b): self._prompt_builder = b def set_engine(self, e): self._engine = e def run(self, user_input): self._history.add_user(user_input) self._cancel.clear() return self._loop(user_input) def _loop(self, seed): rounds = 0 current = seed fc = self._engine.supports_fc() final_text = "" while rounds < self._max_rounds: rounds += 1 if self._cancel.is_set(): self._display.info("(用户取消)") break # 系统提示词 sp = self._prompt_builder() if self._prompt_builder else "" if not fc: sp = (sp + "\n" + self._mgr.get_tools_prompt()) if sp else self._mgr.get_tools_prompt() # 生成 if fc: msgs = self._history.render(sp, current, fc=True) tools = self._get_tools() else: msgs = self._history.render(sp, current, fc=False) tools = None resp = self._engine.generate(msgs, tools, sp) if resp.thinking: self._display.thinking(resp.thinking) # 空响应 if not resp.text and not resp.tool_calls: break # 解释 plan = interpret(resp, fc) # 显示+历史 if plan.text: self._history.add_assistant(plan.text) self._display.text(plan.text) if plan.action == "final": final_text = plan.text break if plan.action == "continue": current = None continue if plan.action == "use_tool": results = self._exec_tools(plan.tool_calls, fc) if results is None: break current = None continue else: self._display.info(f"(达到最大轮数{self._max_rounds})") return final_text def _exec_tools(self, calls, fc): if self._cancel.is_set(): for tc in calls: self._history.add_cancel(tc.name, tc.args) return None results = [] for tc in calls: r = self._exec_one(tc) results.append((tc, r)) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if fc: self._history.add_assistant("", tool_calls=calls) for tc, r in results: text = r.data if r.data is not None else (r.error_message or "") self._history.add_tool(tc.id, tc.name, str(text or "")) else: if len(results) == 1: tc, r = results[0] text = r.data if r.data is not None else (r.error_message or "") self._history.add_user(f"工具调用结果 (时间: {now}):\n{text}") else: parts = [f"工具{i+1}结果:\n{r.data if r.data is not None else (r.error_message or '')}" for i, (tc, r) in enumerate(results)] self._history.add_user(f"批量工具结果 (时间: {now}):\n" + "\n".join(parts)) return results def _exec_one(self, tc): if self._cancel.is_set(): return ToolResult.fail("用户取消", code=ErrorCode.NOT_IMPLEMENTED, tool_name=tc.name) allowed, reason = self._safety.check(tc) if not allowed: return ToolResult.fail(f"被拒绝: {reason}", code=ErrorCode.PERMISSION_DENIED, tool_name=tc.name) self._display.tool_start(tc) r = self._mgr.execute(tc.name, tc.args) self._display.tool_result(tc, r) return r def _get_tools(self): """生成FC工具定义""" defs = [] for tool in self._mgr.tools: mcp = tool.get("mcp_definition") if mcp: defs.append({"type": "function", "function": { "name": mcp.get("name", tool.get("name", "")), "description": mcp.get("description", tool.get("description", "")), "parameters": mcp.get("inputSchema", {"type": "object", "properties": {}}) }}) else: defs.append({"type": "function", "function": { "name": tool.get("name", ""), "description": tool.get("description", ""), "parameters": {"type": "object", "properties": {}} }}) return defs