fischer-agentkit/src/agentkit/core/rewoo.py

1619 lines
68 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ReWOO (Reasoning Without Observation Others) 执行引擎
实现 ReWOO 模式:先规划所有工具调用,再批量执行,最后综合结果。
与 ReAct 的区别在于ReWOO 不在中间步骤观察结果来调整策略,
而是预先规划完整执行计划,一次性执行后综合输出。
"""
import asyncio
import json
import logging
import re
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Awaitable, Callable
from agentkit.core.exceptions import LLMProviderError, TaskCancelledError, TaskTimeoutError
from agentkit.core.protocol import CancellationToken
from agentkit.core.react import ReActEngine, ReActEvent, ReActResult, ReActStep
from agentkit.llm.gateway import LLMGateway
from agentkit.tools.base import Tool, ToolValidationError
from agentkit.telemetry.tracing import start_span, _OTEL_AVAILABLE
from agentkit.telemetry.metrics import (
agent_request_counter,
agent_duration_histogram,
)
if TYPE_CHECKING:
from agentkit.core.compressor import CompressionStrategy
from agentkit.core.trace import TraceRecorder
from agentkit.memory.retriever import MemoryRetriever
logger = logging.getLogger(__name__)
# ── Internal Exceptions ──────────────────────────────────
class _FallbackFailedError(Exception):
"""Internal signal: a fallback strategy failed, try the next one."""
def __init__(self, strategy: str):
self.strategy = strategy
super().__init__(f"Fallback strategy '{strategy}' failed")
# ── Data Structures ───────────────────────────────────────
@dataclass
class ReWOOPlanStep:
"""ReWOO 计划中的单步"""
step_id: int
tool_name: str
arguments: dict[str, object]
reasoning: str = ""
@dataclass
class ReWOOPlan:
"""ReWOO 执行计划"""
steps: list[ReWOOPlanStep] = field(default_factory=list)
reasoning: str = "" # 整体规划推理
@dataclass
class ReWOOStep(ReActStep):
"""ReWOO 执行步骤,扩展 ReActStep 增加 plan_step_id"""
plan_step_id: int | None = None
# ── Planning Prompt ───────────────────────────────────────
_PLANNING_SYSTEM_PROMPT = """\
You are a planning agent. Given a task and a set of available tools, \
create a step-by-step execution plan.
IMPORTANT: You must output a JSON object with the following structure:
{
"reasoning": "Your overall reasoning about how to approach the task",
"steps": [
{
"step_id": 1,
"tool_name": "name_of_tool_to_call",
"arguments": {"arg1": "value1", "arg2": "value2"},
"reasoning": "Why this step is needed"
},
{
"step_id": 2,
"tool_name": "name_of_another_tool",
"arguments": {"arg1": "value1"},
"reasoning": "Why this step is needed"
}
]
}
Rules:
- List ALL tool calls needed to complete the task in order
- Each step must use one of the available tools
- Arguments must match the tool's input schema
- If the task does not require any tools, return an empty steps list
- Output ONLY the JSON object, no other text
"""
_SYNTHESIS_SYSTEM_PROMPT = """\
You are a synthesis agent. Given the original task and the results of \
all planned tool executions, produce a final comprehensive answer.
Review all tool results below and synthesize them into a coherent response \
that fully addresses the original task.
"""
# ── ReWOO Engine ──────────────────────────────────────────
class ReWOOEngine:
"""ReWOO (Reasoning Without Observation Others) 执行引擎
三阶段执行:
1. Planning Phase: 一次性生成完整执行计划
2. Execution Phase: 按计划顺序执行所有工具调用
3. Synthesis Phase: 综合所有工具结果生成最终输出
"""
FALLBACK_STRATEGIES = ["simplified_rewoo", "react", "direct"]
VALID_STRATEGIES = {"simplified_rewoo", "react", "direct", "plan_exec"}
def __init__(self, llm_gateway: LLMGateway, max_plan_steps: int = 10, default_timeout: float = 300.0, fallback_strategies: list[str] | None = None):
if max_plan_steps < 1:
raise ValueError(f"max_plan_steps must be >= 1, got {max_plan_steps}")
self._llm_gateway = llm_gateway
self._max_plan_steps = max_plan_steps
self._default_timeout = default_timeout
# Validate and store fallback strategies
raw_strategies = fallback_strategies if fallback_strategies is not None else self.FALLBACK_STRATEGIES
self._fallback_strategies: list[str] = []
for s in raw_strategies:
if s in self.VALID_STRATEGIES:
self._fallback_strategies.append(s)
else:
logger.warning(f"Invalid fallback strategy '{s}', skipping. Valid: {self.VALID_STRATEGIES}")
if not self._fallback_strategies:
logger.warning("No valid fallback strategies, using defaults")
self._fallback_strategies = list(self.FALLBACK_STRATEGIES)
# ReActEngine 作为 fallback
self._react_engine = ReActEngine(
llm_gateway=llm_gateway,
max_steps=max_plan_steps,
default_timeout=default_timeout,
)
async def execute(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
timeout_seconds: float | None = None,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
) -> ReActResult:
"""执行 ReWOO 三阶段流程
1. Planning: 调用 LLM 生成完整执行计划
2. Execution: 按计划顺序执行所有工具调用
3. Synthesis: 调用 LLM 综合所有结果生成最终输出
如果 Planning 阶段失败LLM 未返回有效 JSON则回退到 ReActEngine。
Args:
cancellation_token: 协作式取消令牌
timeout_seconds: 超时秒数0 表示无超时None 使用 default_timeout
"""
effective_timeout = timeout_seconds if timeout_seconds is not None else self._default_timeout
try:
if effective_timeout > 0:
result = await asyncio.wait_for(
self._execute_rewoo(
messages=messages,
tools=tools,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id,
compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
confirmation_handler=confirmation_handler,
),
timeout=effective_timeout,
)
else:
result = await self._execute_rewoo(
messages=messages,
tools=tools,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id,
compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
confirmation_handler=confirmation_handler,
)
except asyncio.TimeoutError:
raise TaskTimeoutError(
task_id=task_id or "",
timeout_seconds=int(effective_timeout),
)
except TaskCancelledError:
raise
return result
async def _execute_rewoo(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
) -> ReActResult:
tools = tools or []
tool_schemas = self._build_tool_schemas(tools) if tools else None
# Telemetry: record agent request
agent_request_counter().add(1, {"agent.name": agent_name, "agent.type": task_type or "rewoo"})
# Start telemetry span
_span_cm = None
_span = None
_exec_start = time.monotonic()
if _OTEL_AVAILABLE:
_span_cm = start_span(
"agent.execute.rewoo",
attributes={"agent.name": agent_name, "agent.type": task_type or "rewoo"},
)
_span = _span_cm.__enter__()
# Initialize before try so finally can access them
trajectory: list[ReActStep] = []
total_tokens = 0
trace_outcome = "error"
try:
# 启动轨迹记录
if trace_recorder is not None:
trace_recorder.start_trace(
task_id="",
agent_name=agent_name,
skill_name=task_type or None,
)
# Memory retrieval: 执行前检索相关上下文注入 system_prompt
effective_system_prompt = system_prompt
if memory_retriever:
try:
query = str(messages[-1].get("content", "")) if messages else ""
top_k = (retrieval_config or {}).get("top_k", 5)
token_budget = (retrieval_config or {}).get("token_budget", 2000)
memory_context = await memory_retriever.get_context_string(
query=query,
top_k=top_k,
token_budget=token_budget,
)
if memory_context:
if effective_system_prompt:
effective_system_prompt += f"\n\n## 参考信息\n{memory_context}"
else:
effective_system_prompt = f"## 参考信息\n{memory_context}"
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Memory retrieval failed, continuing without context: {e}")
# ── Phase 1: Planning ──
plan, planning_tokens = await self._plan_phase(
messages=messages,
tools=tools,
tool_schemas=tool_schemas,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=effective_system_prompt,
compressor=compressor,
cancellation_token=cancellation_token,
)
total_tokens += planning_tokens
fallback_strategy: str | None = None
# 记录规划步骤
if trace_recorder is not None:
trace_recorder.record_step(
step=0,
action="planning",
duration_ms=0,
tokens_used=planning_tokens,
)
# 如果规划失败,按配置的 fallback 策略顺序尝试回退
if plan is None:
fallback_strategy = await self._try_fallback_strategies(
strategies=self._fallback_strategies,
messages=messages,
tools=tools,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=system_prompt,
effective_system_prompt=effective_system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id,
compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
trajectory=trajectory,
total_tokens=total_tokens,
confirmation_handler=confirmation_handler,
)
if fallback_strategy is not None:
return fallback_strategy
# All fallback strategies exhausted
raise RuntimeError("All ReWOO fallback strategies exhausted")
# 如果计划为空(无需工具),直接让 LLM 回答
if not plan.steps:
llm_messages: list[dict[str, object]] = []
if effective_system_prompt:
llm_messages.append({"role": "system", "content": effective_system_prompt})
llm_messages.extend(messages)
if compressor:
try:
llm_messages = await compressor.compress(llm_messages)
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Context compression failed: {e}")
response = await self._llm_gateway.chat(
messages=llm_messages,
model=model,
agent_name=agent_name,
task_type=task_type,
)
total_tokens += response.usage.total_tokens
step = ReWOOStep(
step=1,
action="final_answer",
content=response.content,
tokens=response.usage.total_tokens,
plan_step_id=None,
)
trajectory.append(step)
if trace_recorder is not None:
trace_recorder.record_step(
step=1,
action="final_answer",
output_data={"content": response.content},
tokens_used=response.usage.total_tokens,
)
trace_outcome = "success"
if trace_recorder is not None:
trace_recorder.end_trace(outcome=trace_outcome)
return ReActResult(
output=response.content or "",
trajectory=trajectory,
total_steps=len(trajectory),
total_tokens=total_tokens,
fallback_strategy=fallback_strategy,
)
# ── Phase 2: Execution ──
tool_results: list[dict[str, object]] = []
for plan_step in plan.steps:
# 协作式取消检查
if cancellation_token is not None:
cancellation_token.check()
tool_start = time.monotonic()
tool_result = await self._execute_tool(plan_step.tool_name, plan_step.arguments, tools)
tool_duration_ms = int((time.monotonic() - tool_start) * 1000)
rewoo_step = ReWOOStep(
step=plan_step.step_id,
action="tool_call",
tool_name=plan_step.tool_name,
arguments=plan_step.arguments,
result=tool_result,
tokens=0, # tool execution tokens tracked separately
plan_step_id=plan_step.step_id,
)
trajectory.append(rewoo_step)
tool_results.append({
"step_id": plan_step.step_id,
"tool_name": plan_step.tool_name,
"arguments": plan_step.arguments,
"result": tool_result,
"reasoning": plan_step.reasoning,
})
# 记录工具调用步骤
if trace_recorder is not None:
tool_error = None
if isinstance(tool_result, dict) and "error" in tool_result:
tool_error = tool_result["error"]
trace_recorder.record_step(
step=plan_step.step_id,
action="tool_call",
tool_name=plan_step.tool_name,
input_data=plan_step.arguments,
output_data=tool_result,
duration_ms=tool_duration_ms,
tokens_used=0,
error=tool_error,
)
# ── Phase 3: Synthesis ──
output, synthesis_tokens = await self._synthesis_phase(
messages=messages,
tool_results=tool_results,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=effective_system_prompt,
compressor=compressor,
cancellation_token=cancellation_token,
)
total_tokens += synthesis_tokens
# 记录综合步骤
synthesis_step = ReWOOStep(
step=len(plan.steps) + 1,
action="final_answer",
content=output,
tokens=synthesis_tokens,
plan_step_id=None,
)
trajectory.append(synthesis_step)
if trace_recorder is not None:
trace_recorder.record_step(
step=len(plan.steps) + 1,
action="final_answer",
output_data={"content": output},
tokens_used=synthesis_tokens,
)
trace_outcome = "success"
# 结束轨迹记录
if trace_recorder is not None:
trace_recorder.end_trace(outcome=trace_outcome)
# Memory storage: 执行后写入轨迹摘要到 EpisodicMemory
if memory_retriever and hasattr(memory_retriever, "store_episode"):
try:
summary = output[:500] if output else ""
await memory_retriever.store_episode(
key=f"task:{task_id or 'unknown'}",
value={"output_summary": summary, "agent_name": agent_name},
metadata={"task_type": task_type, "outcome": trace_outcome},
)
except (asyncio.TimeoutError, ConnectionError, ValueError) as e:
logger.warning(f"Failed to store task result in episodic memory: {e}")
return ReActResult(
output=output,
trajectory=trajectory,
total_steps=len(trajectory),
total_tokens=total_tokens,
fallback_strategy=fallback_strategy,
)
finally:
# Telemetry: end span and record duration
_duration_ms = int((time.monotonic() - _exec_start) * 1000)
if _span is not None:
_span.set_attribute("agent.total_steps", len(trajectory))
_span.set_attribute("agent.total_tokens", total_tokens)
_span.set_attribute("agent.outcome", trace_outcome)
_span.set_attribute("agent.duration_ms", _duration_ms)
if _span_cm is not None:
_span_cm.__exit__(None, None, None)
agent_duration_histogram().record(_duration_ms, {"agent.name": agent_name})
async def execute_stream(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
timeout_seconds: float | None = None,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
):
"""Execute ReWOO flow, yielding ReActEvent objects.
Events:
- "planning": planning phase started
- "plan_generated": plan generated with step details
- "tool_call": a tool is being called
- "tool_result": tool execution result
- "synthesis": synthesis phase started
- "final_answer": final synthesized answer
"""
tools = tools or []
tool_schemas = self._build_tool_schemas(tools) if tools else None
# 启动轨迹记录
if trace_recorder is not None:
trace_recorder.start_trace(
task_id="",
agent_name=agent_name,
skill_name=task_type or None,
)
# Memory retrieval
effective_system_prompt = system_prompt
if memory_retriever:
try:
query = str(messages[-1].get("content", "")) if messages else ""
top_k = (retrieval_config or {}).get("top_k", 5)
token_budget = (retrieval_config or {}).get("token_budget", 2000)
memory_context = await memory_retriever.get_context_string(
query=query,
top_k=top_k,
token_budget=token_budget,
)
if memory_context:
if effective_system_prompt:
effective_system_prompt += f"\n\n## 参考信息\n{memory_context}"
else:
effective_system_prompt = f"## 参考信息\n{memory_context}"
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Memory retrieval failed, continuing without context: {e}")
trajectory: list[ReActStep] = []
total_tokens = 0
output = ""
trace_outcome = "success"
try:
yield ReActEvent(
event_type="planning",
step=0,
data={"message": "Generating execution plan..."},
)
plan, planning_tokens = await self._plan_phase(
messages=messages,
tools=tools,
tool_schemas=tool_schemas,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=effective_system_prompt,
compressor=compressor,
cancellation_token=cancellation_token,
)
total_tokens += planning_tokens
if plan is None:
# Planning failed, try fallback strategies in configured order
async for event in self._try_fallback_strategies_stream(
strategies=self._fallback_strategies,
messages=messages,
tools=tools,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=system_prompt,
effective_system_prompt=effective_system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id,
compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
total_tokens=total_tokens,
confirmation_handler=confirmation_handler,
):
yield event
return
yield ReActEvent(
event_type="plan_generated",
step=0,
data={
"reasoning": plan.reasoning,
"steps": [
{
"step_id": s.step_id,
"tool_name": s.tool_name,
"arguments": s.arguments,
"reasoning": s.reasoning,
}
for s in plan.steps
],
},
)
# Empty plan: direct answer
if not plan.steps:
llm_messages: list[dict[str, object]] = []
if effective_system_prompt:
llm_messages.append({"role": "system", "content": effective_system_prompt})
llm_messages.extend(messages)
if compressor:
try:
llm_messages = await compressor.compress(llm_messages)
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Context compression failed: {e}")
response = await self._llm_gateway.chat(
messages=llm_messages,
model=model,
agent_name=agent_name,
task_type=task_type,
)
total_tokens += response.usage.total_tokens
output = response.content or ""
trajectory.append(ReWOOStep(
step=1,
action="final_answer",
content=output,
tokens=response.usage.total_tokens,
))
yield ReActEvent(
event_type="final_answer",
step=1,
data={
"output": output,
"total_steps": len(trajectory),
"total_tokens": total_tokens,
},
)
return
# ── Phase 2: Execution ──
tool_results: list[dict[str, object]] = []
for plan_step in plan.steps:
if cancellation_token is not None:
cancellation_token.check()
yield ReActEvent(
event_type="tool_call",
step=plan_step.step_id,
data={"tool_name": plan_step.tool_name, "arguments": plan_step.arguments},
)
tool_start = time.monotonic()
tool_result = await self._execute_tool(plan_step.tool_name, plan_step.arguments, tools)
tool_duration_ms = int((time.monotonic() - tool_start) * 1000)
rewoo_step = ReWOOStep(
step=plan_step.step_id,
action="tool_call",
tool_name=plan_step.tool_name,
arguments=plan_step.arguments,
result=tool_result,
tokens=0,
plan_step_id=plan_step.step_id,
)
trajectory.append(rewoo_step)
tool_results.append({
"step_id": plan_step.step_id,
"tool_name": plan_step.tool_name,
"arguments": plan_step.arguments,
"result": tool_result,
"reasoning": plan_step.reasoning,
})
# 记录工具调用步骤
if trace_recorder is not None:
tool_error = None
if isinstance(tool_result, dict) and "error" in tool_result:
tool_error = tool_result["error"]
trace_recorder.record_step(
step=plan_step.step_id,
action="tool_call",
tool_name=plan_step.tool_name,
input_data=plan_step.arguments,
output_data=tool_result,
duration_ms=tool_duration_ms,
tokens_used=0,
error=tool_error,
)
yield ReActEvent(
event_type="tool_result",
step=plan_step.step_id,
data={"tool_name": plan_step.tool_name, "result": tool_result},
)
# ── Phase 3: Synthesis ──
yield ReActEvent(
event_type="synthesis",
step=len(plan.steps) + 1,
data={"message": "Synthesizing results..."},
)
output, synthesis_tokens = await self._synthesis_phase(
messages=messages,
tool_results=tool_results,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=effective_system_prompt,
compressor=compressor,
cancellation_token=cancellation_token,
)
total_tokens += synthesis_tokens
trajectory.append(ReWOOStep(
step=len(plan.steps) + 1,
action="final_answer",
content=output,
tokens=synthesis_tokens,
))
yield ReActEvent(
event_type="final_answer",
step=len(plan.steps) + 1,
data={
"output": output,
"total_steps": len(trajectory),
"total_tokens": total_tokens,
},
)
except asyncio.CancelledError:
trace_outcome = "cancelled"
raise
except Exception as e:
trace_outcome = "error"
logger.error(f"ReWOO execute_stream failed: {e}")
raise
finally:
if trace_recorder is not None:
trace_recorder.end_trace(outcome=trace_outcome)
# Memory storage
if memory_retriever and hasattr(memory_retriever, "store_episode"):
try:
summary = output[:500] if output else ""
await memory_retriever.store_episode(
key=f"task:{task_id or 'unknown'}",
value={"output_summary": summary, "agent_name": agent_name},
metadata={"task_type": task_type, "outcome": trace_outcome},
)
except (asyncio.TimeoutError, ConnectionError, ValueError) as e:
logger.warning(f"Failed to store task result in episodic memory: {e}")
# ── Fallback Strategy Helpers ──────────────────────────
async def _try_fallback_strategies_stream(
self,
strategies: list[str],
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
effective_system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
total_tokens: int = 0,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
):
"""Stream version: try fallback strategies in configured order, yielding events from the first successful one.
If all strategies fail, raises RuntimeError.
"""
for strategy in strategies:
if strategy == "simplified_rewoo":
try:
async for event in self._fallback_simplified_rewoo_stream(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
effective_system_prompt=effective_system_prompt,
compressor=compressor, cancellation_token=cancellation_token,
):
yield event
return
except _FallbackFailedError:
continue
elif strategy == "react":
try:
async for event in self._fallback_react_stream(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
system_prompt=system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id, compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
confirmation_handler=confirmation_handler,
):
yield event
return
except _FallbackFailedError:
continue
elif strategy == "direct":
try:
async for event in self._fallback_direct_stream(
messages=messages, model=model,
agent_name=agent_name, task_type=task_type,
effective_system_prompt=effective_system_prompt,
compressor=compressor, total_tokens=total_tokens,
):
yield event
return
except _FallbackFailedError:
continue
elif strategy == "plan_exec":
try:
async for event in self._fallback_plan_exec_stream(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
effective_system_prompt=effective_system_prompt,
compressor=compressor, cancellation_token=cancellation_token,
):
yield event
return
except _FallbackFailedError:
continue
raise RuntimeError("All ReWOO fallback strategies exhausted in stream mode")
async def _fallback_simplified_rewoo_stream(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
effective_system_prompt: str | None = None,
compressor: "CompressionStrategy | None" = None,
cancellation_token: CancellationToken | None = None,
):
"""Stream: Simplified ReWOO fallback with max_steps=3"""
logger.warning("ReWOO planning failed in stream mode, trying simplified planning with max_steps=3")
try:
tool_schemas = self._build_tool_schemas(tools) if tools else None
plan, simplified_tokens = await self._plan_phase(
messages=messages, tools=tools or [], tool_schemas=tool_schemas,
model=model, agent_name=agent_name, task_type=task_type,
system_prompt=effective_system_prompt, compressor=compressor,
cancellation_token=cancellation_token, max_steps=3,
)
if plan is not None and plan.steps:
logger.info("Simplified ReWOO planning succeeded in stream mode")
yield ReActEvent(event_type="plan_generated", step=0, data={
"reasoning": plan.reasoning,
"steps": [{"step_id": s.step_id, "tool_name": s.tool_name, "arguments": s.arguments, "reasoning": s.reasoning} for s in plan.steps],
})
tool_results: list[dict[str, object]] = []
for plan_step in plan.steps:
if cancellation_token is not None:
cancellation_token.check()
yield ReActEvent(event_type="tool_call", step=plan_step.step_id, data={"tool_name": plan_step.tool_name, "arguments": plan_step.arguments})
tool_result = await self._execute_tool(plan_step.tool_name, plan_step.arguments, tools or [])
tool_results.append({"step_id": plan_step.step_id, "tool_name": plan_step.tool_name, "arguments": plan_step.arguments, "result": tool_result, "reasoning": plan_step.reasoning})
yield ReActEvent(event_type="tool_result", step=plan_step.step_id, data={"tool_name": plan_step.tool_name, "result": tool_result})
yield ReActEvent(event_type="synthesis", step=len(plan.steps) + 1, data={"message": "Synthesizing results..."})
output, synthesis_tokens = await self._synthesis_phase(messages=messages, tool_results=tool_results, model=model, agent_name=agent_name, task_type=task_type, system_prompt=effective_system_prompt, compressor=compressor, cancellation_token=cancellation_token)
yield ReActEvent(event_type="final_answer", step=len(plan.steps) + 1, data={"output": output, "total_steps": len(plan.steps) + 1, "total_tokens": simplified_tokens + synthesis_tokens})
return
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ValueError, TypeError, ToolValidationError, json.JSONDecodeError) as e:
logger.warning(f"Simplified ReWOO planning also failed in stream mode: {e}")
# Failed, continue to next strategy by not returning
# This signals the caller to try the next strategy
# We need a different approach - raise a specific exception
raise _FallbackFailedError("simplified_rewoo")
async def _fallback_react_stream(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
):
"""Stream: ReAct fallback"""
logger.warning("ReWOO planning failed in stream mode, falling back to ReActEngine")
try:
async for event in self._react_engine.execute_stream(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
system_prompt=system_prompt, trace_recorder=trace_recorder,
memory_retriever=memory_retriever, task_id=task_id,
compressor=compressor, retrieval_config=retrieval_config,
cancellation_token=cancellation_token, timeout_seconds=0,
confirmation_handler=confirmation_handler,
):
yield event
return
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ToolValidationError) as e:
logger.warning(f"ReAct fallback also failed in stream mode: {e}")
raise _FallbackFailedError("react")
async def _fallback_direct_stream(
self,
messages: list[dict[str, str]],
model: str = "default",
agent_name: str = "",
task_type: str = "",
effective_system_prompt: str | None = None,
compressor: "CompressionStrategy | None" = None,
total_tokens: int = 0,
):
"""Stream: Direct LLM fallback"""
logger.warning("Falling back to direct LLM call in stream mode")
try:
direct_messages: list[dict[str, object]] = []
if effective_system_prompt:
direct_messages.append({"role": "system", "content": effective_system_prompt})
direct_messages.extend(messages)
if compressor:
try:
direct_messages = await compressor.compress(direct_messages)
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Context compression failed in direct fallback: {e}")
direct_response = await self._llm_gateway.chat(messages=direct_messages, model=model, agent_name=agent_name, task_type=task_type)
output = direct_response.content or ""
yield ReActEvent(event_type="final_answer", step=1, data={"output": output, "total_steps": 1, "total_tokens": total_tokens + direct_response.usage.total_tokens})
return
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ValueError) as e:
logger.error(f"Direct LLM fallback also failed in stream mode: {e}")
raise _FallbackFailedError("direct")
async def _fallback_plan_exec_stream(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
effective_system_prompt: str | None = None,
compressor: "CompressionStrategy | None" = None,
cancellation_token: CancellationToken | None = None,
):
"""Stream: Plan-Exec fallback with max_steps=5"""
logger.warning("Falling back to plan-exec mode in stream mode (max_steps=5)")
try:
tool_schemas = self._build_tool_schemas(tools) if tools else None
plan, plan_tokens = await self._plan_phase(
messages=messages, tools=tools or [], tool_schemas=tool_schemas,
model=model, agent_name=agent_name, task_type=task_type,
system_prompt=effective_system_prompt, compressor=compressor,
cancellation_token=cancellation_token, max_steps=5,
)
if plan is not None and plan.steps:
yield ReActEvent(event_type="plan_generated", step=0, data={
"reasoning": plan.reasoning,
"steps": [{"step_id": s.step_id, "tool_name": s.tool_name, "arguments": s.arguments, "reasoning": s.reasoning} for s in plan.steps],
})
tool_results: list[dict[str, object]] = []
for plan_step in plan.steps:
if cancellation_token is not None:
cancellation_token.check()
yield ReActEvent(event_type="tool_call", step=plan_step.step_id, data={"tool_name": plan_step.tool_name, "arguments": plan_step.arguments})
tool_result = await self._execute_tool(plan_step.tool_name, plan_step.arguments, tools or [])
tool_results.append({"step_id": plan_step.step_id, "tool_name": plan_step.tool_name, "arguments": plan_step.arguments, "result": tool_result, "reasoning": plan_step.reasoning})
yield ReActEvent(event_type="tool_result", step=plan_step.step_id, data={"tool_name": plan_step.tool_name, "result": tool_result})
yield ReActEvent(event_type="synthesis", step=len(plan.steps) + 1, data={"message": "Synthesizing results..."})
output, synthesis_tokens = await self._synthesis_phase(messages=messages, tool_results=tool_results, model=model, agent_name=agent_name, task_type=task_type, system_prompt=effective_system_prompt, compressor=compressor, cancellation_token=cancellation_token)
yield ReActEvent(event_type="final_answer", step=len(plan.steps) + 1, data={"output": output, "total_steps": len(plan.steps) + 1, "total_tokens": plan_tokens + synthesis_tokens})
return
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ValueError, TypeError, ToolValidationError, json.JSONDecodeError) as e:
logger.warning(f"Plan-exec fallback also failed in stream mode: {e}")
raise _FallbackFailedError("plan_exec")
async def _try_fallback_strategies(
self,
strategies: list[str],
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
effective_system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
trajectory: list[ReActStep] | None = None,
total_tokens: int = 0,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
) -> ReActResult | None:
"""按配置的 fallback 策略顺序尝试回退,返回第一个成功的结果
Returns:
ReActResult if a fallback succeeded, None if all strategies exhausted
"""
for strategy in strategies:
if strategy == "simplified_rewoo":
result = await self._fallback_simplified_rewoo(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
effective_system_prompt=effective_system_prompt,
compressor=compressor, cancellation_token=cancellation_token,
)
if result is not None:
return result
elif strategy == "react":
result = await self._fallback_react(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
system_prompt=system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id, compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
confirmation_handler=confirmation_handler,
)
if result is not None:
return result
elif strategy == "direct":
result = await self._fallback_direct(
messages=messages, model=model,
agent_name=agent_name, task_type=task_type,
effective_system_prompt=effective_system_prompt,
compressor=compressor, cancellation_token=cancellation_token,
trajectory=trajectory, total_tokens=total_tokens,
trace_recorder=trace_recorder,
)
if result is not None:
return result
elif strategy == "plan_exec":
result = await self._fallback_plan_exec(
messages=messages, tools=tools, model=model,
agent_name=agent_name, task_type=task_type,
effective_system_prompt=effective_system_prompt,
compressor=compressor, cancellation_token=cancellation_token,
)
if result is not None:
return result
return None
async def _fallback_simplified_rewoo(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
effective_system_prompt: str | None = None,
compressor: "CompressionStrategy | None" = None,
cancellation_token: CancellationToken | None = None,
) -> ReActResult | None:
"""Simplified ReWOO fallback: retry planning with max_steps=3"""
logger.warning("ReWOO planning failed, trying simplified planning with max_steps=3")
try:
tool_schemas = self._build_tool_schemas(tools) if tools else None
plan, simplified_tokens = await self._plan_phase(
messages=messages,
tools=tools or [],
tool_schemas=tool_schemas,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=effective_system_prompt,
compressor=compressor,
cancellation_token=cancellation_token,
max_steps=3,
)
if plan is not None and plan.steps:
logger.info("Simplified ReWOO planning succeeded")
# Execute the simplified plan
trajectory: list[ReActStep] = []
total_tokens = simplified_tokens
tool_results: list[dict[str, object]] = []
for plan_step in plan.steps:
if cancellation_token is not None:
cancellation_token.check()
tool_result = await self._execute_tool(plan_step.tool_name, plan_step.arguments, tools or [])
rewoo_step = ReWOOStep(
step=plan_step.step_id,
action="tool_call",
tool_name=plan_step.tool_name,
arguments=plan_step.arguments,
result=tool_result,
tokens=0,
plan_step_id=plan_step.step_id,
)
trajectory.append(rewoo_step)
tool_results.append({
"step_id": plan_step.step_id,
"tool_name": plan_step.tool_name,
"arguments": plan_step.arguments,
"result": tool_result,
"reasoning": plan_step.reasoning,
})
output, synthesis_tokens = await self._synthesis_phase(
messages=messages, tool_results=tool_results,
model=model, agent_name=agent_name, task_type=task_type,
system_prompt=effective_system_prompt, compressor=compressor,
cancellation_token=cancellation_token,
)
total_tokens += synthesis_tokens
trajectory.append(ReWOOStep(
step=len(plan.steps) + 1,
action="final_answer",
content=output,
tokens=synthesis_tokens,
))
return ReActResult(
output=output,
trajectory=trajectory,
total_steps=len(trajectory),
total_tokens=total_tokens,
fallback_strategy="simplified_rewoo",
)
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ValueError, TypeError, ToolValidationError, json.JSONDecodeError) as e:
logger.warning(f"Simplified ReWOO planning also failed: {e}")
return None
async def _fallback_react(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
system_prompt: str | None = None,
trace_recorder: "TraceRecorder | None" = None,
memory_retriever: "MemoryRetriever | None" = None,
task_id: str | None = None,
compressor: "CompressionStrategy | None" = None,
retrieval_config: dict[str, object] | None = None,
cancellation_token: CancellationToken | None = None,
confirmation_handler: Callable[..., Awaitable[object]] | None = None,
) -> ReActResult | None:
"""ReAct fallback: delegate to ReActEngine"""
logger.warning("ReWOO planning failed, falling back to ReActEngine")
try:
react_result = await self._react_engine.execute(
messages=messages,
tools=tools,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=system_prompt,
trace_recorder=trace_recorder,
memory_retriever=memory_retriever,
task_id=task_id,
compressor=compressor,
retrieval_config=retrieval_config,
cancellation_token=cancellation_token,
timeout_seconds=0, # timeout already handled by outer wrapper
confirmation_handler=confirmation_handler,
)
react_result.fallback_strategy = "react"
return react_result
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ToolValidationError) as e:
logger.warning(f"ReAct fallback also failed: {e}")
return None
async def _fallback_direct(
self,
messages: list[dict[str, str]],
model: str = "default",
agent_name: str = "",
task_type: str = "",
effective_system_prompt: str | None = None,
compressor: "CompressionStrategy | None" = None,
cancellation_token: CancellationToken | None = None,
trajectory: list[ReActStep] | None = None,
total_tokens: int = 0,
trace_recorder: "TraceRecorder | None" = None,
) -> ReActResult | None:
"""Direct fallback: simple LLM call without tools"""
logger.warning("Falling back to direct LLM call")
try:
direct_messages: list[dict[str, object]] = []
if effective_system_prompt:
direct_messages.append({"role": "system", "content": effective_system_prompt})
direct_messages.extend(messages)
if compressor:
try:
direct_messages = await compressor.compress(direct_messages)
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Context compression failed in direct fallback: {e}")
direct_response = await self._llm_gateway.chat(
messages=direct_messages,
model=model,
agent_name=agent_name,
task_type=task_type,
)
total_tokens += direct_response.usage.total_tokens
direct_step = ReWOOStep(
step=1,
action="final_answer",
content=direct_response.content,
tokens=direct_response.usage.total_tokens,
plan_step_id=None,
)
if trajectory is not None:
trajectory.append(direct_step)
if trace_recorder is not None:
trace_recorder.record_step(
step=1,
action="final_answer",
output_data={"content": direct_response.content},
tokens_used=direct_response.usage.total_tokens,
)
trace_recorder.end_trace(outcome="success")
return ReActResult(
output=direct_response.content or "",
trajectory=trajectory or [direct_step],
total_steps=len(trajectory or [direct_step]),
total_tokens=total_tokens,
fallback_strategy="direct",
)
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ValueError) as e:
logger.error(f"Direct LLM fallback also failed: {e}")
return None
async def _fallback_plan_exec(
self,
messages: list[dict[str, str]],
tools: list[Tool] | None = None,
model: str = "default",
agent_name: str = "",
task_type: str = "",
effective_system_prompt: str | None = None,
compressor: "CompressionStrategy | None" = None,
cancellation_token: CancellationToken | None = None,
) -> ReActResult | None:
"""Plan-Exec fallback: plan then execute sequentially (like simplified ReWOO but with max_steps=5)"""
logger.warning("Falling back to plan-exec mode (max_steps=5)")
try:
tool_schemas = self._build_tool_schemas(tools) if tools else None
plan, plan_tokens = await self._plan_phase(
messages=messages,
tools=tools or [],
tool_schemas=tool_schemas,
model=model,
agent_name=agent_name,
task_type=task_type,
system_prompt=effective_system_prompt,
compressor=compressor,
cancellation_token=cancellation_token,
max_steps=5,
)
if plan is not None and plan.steps:
trajectory: list[ReActStep] = []
total_tokens = plan_tokens
tool_results: list[dict[str, object]] = []
for plan_step in plan.steps:
if cancellation_token is not None:
cancellation_token.check()
tool_result = await self._execute_tool(plan_step.tool_name, plan_step.arguments, tools or [])
rewoo_step = ReWOOStep(
step=plan_step.step_id,
action="tool_call",
tool_name=plan_step.tool_name,
arguments=plan_step.arguments,
result=tool_result,
tokens=0,
plan_step_id=plan_step.step_id,
)
trajectory.append(rewoo_step)
tool_results.append({
"step_id": plan_step.step_id,
"tool_name": plan_step.tool_name,
"arguments": plan_step.arguments,
"result": tool_result,
"reasoning": plan_step.reasoning,
})
output, synthesis_tokens = await self._synthesis_phase(
messages=messages, tool_results=tool_results,
model=model, agent_name=agent_name, task_type=task_type,
system_prompt=effective_system_prompt, compressor=compressor,
cancellation_token=cancellation_token,
)
total_tokens += synthesis_tokens
trajectory.append(ReWOOStep(
step=len(plan.steps) + 1,
action="final_answer",
content=output,
tokens=synthesis_tokens,
))
return ReActResult(
output=output,
trajectory=trajectory,
total_steps=len(trajectory),
total_tokens=total_tokens,
fallback_strategy="plan_exec",
)
except (LLMProviderError, asyncio.TimeoutError, ConnectionError, RuntimeError, ValueError, TypeError, ToolValidationError, json.JSONDecodeError) as e:
logger.warning(f"Plan-exec fallback also failed: {e}")
return None
# ── Phase Implementations ─────────────────────────────
async def _plan_phase(
self,
messages: list[dict[str, str]],
tools: list[Tool],
tool_schemas: list[dict] | None,
model: str,
agent_name: str,
task_type: str,
system_prompt: str | None,
compressor: "CompressionStrategy | None",
cancellation_token: CancellationToken | None,
max_steps: int | None = None,
) -> tuple[ReWOOPlan | None, int]:
"""Planning Phase: 调用 LLM 生成完整执行计划
Args:
max_steps: 限制计划最大步数None 则使用 self._max_plan_steps
Returns:
(plan, tokens_used) - plan 为 None 表示规划失败
"""
if cancellation_token is not None:
cancellation_token.check()
# 构建工具描述
tool_descriptions = self._build_tool_descriptions(tools)
# 构建规划消息
planning_messages: list[dict[str, object]] = [
{"role": "system", "content": _PLANNING_SYSTEM_PROMPT},
]
# 添加上下文信息
context_parts = []
if system_prompt:
context_parts.append(f"Context: {system_prompt}")
if tool_descriptions:
context_parts.append(f"Available tools:\n{tool_descriptions}")
user_content = "\n\n".join(context_parts) if context_parts else ""
# 添加原始用户消息
for msg in messages:
if msg.get("role") == "user":
user_content += f"\n\nTask: {msg.get('content', '')}"
planning_messages.append({"role": "user", "content": user_content})
# 压缩
if compressor:
try:
planning_messages = await compressor.compress(planning_messages)
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Context compression failed during planning: {e}")
try:
response = await self._llm_gateway.chat(
messages=planning_messages,
model=model,
agent_name=agent_name,
task_type=task_type,
tools=tool_schemas,
)
except (LLMProviderError, asyncio.TimeoutError, ConnectionError) as e:
logger.warning(f"LLM call failed during planning: {e}")
return None, 0
tokens_used = response.usage.total_tokens
# 解析计划
plan = self._parse_plan(response.content or "")
if plan is None:
return None, tokens_used
# 限制计划步数
effective_max_steps = max_steps if max_steps is not None else self._max_plan_steps
if len(plan.steps) > effective_max_steps:
plan.steps = plan.steps[:effective_max_steps]
return plan, tokens_used
async def _synthesis_phase(
self,
messages: list[dict[str, str]],
tool_results: list[dict[str, object]],
model: str,
agent_name: str,
task_type: str,
system_prompt: str | None,
compressor: "CompressionStrategy | None",
cancellation_token: CancellationToken | None,
) -> tuple[str, int]:
"""Synthesis Phase: 综合所有工具结果生成最终输出
Returns:
(output, tokens_used)
"""
if cancellation_token is not None:
cancellation_token.check()
# 构建综合消息
synthesis_messages: list[dict[str, object]] = [
{"role": "system", "content": _SYNTHESIS_SYSTEM_PROMPT},
]
# 构建工具结果摘要
results_text = "Tool execution results:\n\n"
for tr in tool_results:
results_text += f"Step {tr['step_id']}: {tr['tool_name']}"
if tr.get("reasoning"):
results_text += f" (Reason: {tr['reasoning']})"
results_text += "\n"
results_text += f" Arguments: {json.dumps(tr['arguments'], ensure_ascii=False)}\n"
results_text += f" Result: {json.dumps(tr['result'], ensure_ascii=False, default=str)}\n\n"
# 添加原始用户消息
user_content = results_text
for msg in messages:
if msg.get("role") == "user":
user_content = f"Original task: {msg.get('content', '')}\n\n{user_content}"
if system_prompt:
user_content = f"Context: {system_prompt}\n\n{user_content}"
synthesis_messages.append({"role": "user", "content": user_content})
# 压缩
if compressor:
try:
synthesis_messages = await compressor.compress(synthesis_messages)
except (asyncio.TimeoutError, ConnectionError, LLMProviderError) as e:
logger.warning(f"Context compression failed during synthesis: {e}")
response = await self._llm_gateway.chat(
messages=synthesis_messages,
model=model,
agent_name=agent_name,
task_type=task_type,
)
return response.content or "", response.usage.total_tokens
# ── Helper Methods ────────────────────────────────────
def _build_tool_schemas(self, tools: list[Tool]) -> list[dict]:
"""将 Tool 对象转换为 OpenAI Function Calling schema 格式"""
schemas = []
for tool in tools:
schema = {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.input_schema or {"type": "object", "properties": {}},
},
}
schemas.append(schema)
return schemas
def _build_tool_descriptions(self, tools: list[Tool]) -> str:
"""构建工具描述文本,用于规划 prompt"""
descriptions = []
for tool in tools:
desc = f"- {tool.name}: {tool.description}"
if tool.input_schema:
props = tool.input_schema.get("properties", {})
if props:
params = ", ".join(
f"{k} ({v.get('type', 'any')}: {v.get('description', '')})"
for k, v in props.items()
)
desc += f"\n Parameters: {params}"
descriptions.append(desc)
return "\n".join(descriptions)
def _parse_plan(self, content: str) -> ReWOOPlan | None:
"""从 LLM 响应中解析执行计划
尝试从响应内容中提取 JSON 格式的计划。
支持纯 JSON 和 markdown 代码块中的 JSON。
"""
# 尝试提取 JSON 代码块
json_str = content.strip()
# 尝试从 markdown 代码块中提取
if "```" in json_str:
code_block_match = re.search(r"```(?:json)?\s*\n(.*?)\n\s*```", json_str, re.DOTALL)
if code_block_match:
json_str = code_block_match.group(1).strip()
# 尝试提取 JSON 对象(处理 LLM 可能在 JSON 前后添加文本的情况)
brace_start = json_str.find("{")
brace_end = json_str.rfind("}")
if brace_start != -1 and brace_end != -1 and brace_end > brace_start:
json_str = json_str[brace_start:brace_end + 1]
try:
data = json.loads(json_str)
except (json.JSONDecodeError, TypeError):
logger.warning(f"Failed to parse plan from LLM response: {content[:200]}")
return None
if not isinstance(data, dict) or "steps" not in data:
logger.warning(f"Plan JSON missing 'steps' key: {content[:200]}")
return None
steps = []
for i, step_data in enumerate(data["steps"]):
if not isinstance(step_data, dict):
continue
tool_name = step_data.get("tool_name", "")
if not tool_name:
continue
steps.append(ReWOOPlanStep(
step_id=step_data.get("step_id", i + 1),
tool_name=tool_name,
arguments=step_data.get("arguments", {}),
reasoning=step_data.get("reasoning", ""),
))
return ReWOOPlan(
steps=steps,
reasoning=data.get("reasoning", ""),
)
def _find_tool(self, name: str, tools: list[Tool]) -> Tool | None:
"""根据名称从可用工具中查找工具"""
for tool in tools:
if tool.name == name:
return tool
return None
async def _execute_tool(
self, tool_name: str, arguments: dict[str, object], tools: list[Tool]
) -> dict:
"""执行工具调用,处理成功和失败情况"""
tool = self._find_tool(tool_name, tools)
if tool is None:
error_msg = f"Tool '{tool_name}' not found"
logger.warning(error_msg)
return {"error": error_msg}
try:
result = await tool.safe_execute(**arguments)
return result
except (ToolValidationError, ValueError, TypeError, RuntimeError) as e:
error_msg = f"Tool '{tool_name}' execution failed: {e}"
logger.warning(error_msg)
return {"error": error_msg}