1619 lines
68 KiB
Python
1619 lines
68 KiB
Python
"""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}
|