zcbot/core/loop.py

100 lines
3.3 KiB
Python

"""主 agent loop: ReAct 风格,LLM ↔ Tool 反复直到无 tool_call。"""
from __future__ import annotations
import json
from typing import Any, Dict, Optional
from rich.console import Console
from .capabilities import ModelCapabilities
from .llm import LLM
from .session import Session
class AgentLoop:
def __init__(
self,
llm: LLM,
tools: Dict[str, Any],
session: Session,
capabilities: ModelCapabilities,
console: Optional[Console] = None,
max_iterations: Optional[int] = None,
) -> None:
self.llm = llm
self.tools = tools
self.session = session
self.caps = capabilities
self.max_iterations = max_iterations or capabilities.max_iterations
self.console = console or Console()
def run(self, user_message: str) -> str:
self.session.append({"role": "user", "content": user_message})
for _ in range(self.max_iterations):
with self.console.status("[dim]thinking...[/dim]", spinner="dots"):
response = self.llm.chat(
messages=self.session.messages,
tools=[t.schema for t in self.tools.values()],
reasoning_effort=self.caps.default_reasoning_effort or None,
)
msg = response.choices[0].message
self.session.append(msg)
tool_calls = getattr(msg, "tool_calls", None) or []
content = getattr(msg, "content", None)
if content:
self.console.print(f"[cyan]assistant>[/cyan] {content}")
if not tool_calls:
return content or ""
for tc in tool_calls:
result = self._execute_tool_call(tc)
self.session.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": result,
}
)
return "[reached max iterations]"
def _execute_tool_call(self, tc: Any) -> str:
name = tc.function.name
raw_args = tc.function.arguments or "{}"
try:
args = json.loads(raw_args)
except json.JSONDecodeError as e:
return f"[Error] invalid JSON arguments for {name}: {e}"
preview = json.dumps(args, ensure_ascii=False)
if len(preview) > 200:
preview = preview[:200] + "..."
self.console.print(f"[yellow]tool>[/yellow] {name}({preview})")
tool = self.tools.get(name)
if tool is None:
return f"[Error] unknown tool: {name}"
try:
result = tool.execute(**args)
except TypeError as e:
return f"[Error] bad arguments to {name}: {e}"
except Exception as e:
return f"[Error executing {name}] {type(e).__name__}: {e}"
if not isinstance(result, str):
result = str(result)
# 控制返回给模型的 tool 结果体量,避免炸 context
MAX_LEN = 16_000
if len(result) > MAX_LEN:
result = result[:MAX_LEN] + f"\n[... truncated, {len(result) - MAX_LEN} chars ...]"
# 给用户预览(截短)
preview = result if len(result) < 400 else result[:400] + "..."
self.console.print(f"[dim]{preview}[/dim]")
return result