Files
xiaoli-cli/core/runner.py
T
shuiyu1123 78fea7ccdd xiaoli-cli 7.0.0: 重构底层架构 + 移除工具截断
- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display
- FC原生调用为主,prompt-JSON保底
- 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search)
- 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件)
- 清理 constants.py 死代码
- 版本号更新为 7.0.0
2026-07-05 13:17:27 +08:00

147 lines
5.1 KiB
Python

"""对话循环 — 唯一循环,无重复"""
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