"""主 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