mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-02 02:00:01 +08:00
* ♻️ refactor(ai): 重构 AI 服务模块并完善文档注释 - 【重构】统一清理并优化所有 AI 服务模块文件的导入语句,将其移至文件顶部 - 【重构】重构 `hooks.py` 中的 `Hooks` 派发逻辑,使用通用管道函数消除重复代码,并引入 `HookPoint` 描述符 - 【重构】重构工具装饰器实现,新增 `toolkit` 类装饰器,优化 `BaseToolkit` 配置合并与前缀处理 - 【功能】Docker 沙箱容器创建时支持自动注入系统代理环境变量并配置 `ExtraHosts` - 【功能】Jupyter 服务启动前自动清理旧进程并初始化临时目录权限 - 【修复】优化 Pydantic 结构化输出校验失败时的错误信息提取,提供更详细的字段级错误反馈 - 【修复】在 `api.py` 中避免将 `ModelRetry` 和 `ControlFlowExit` 异常错误地包装为 `LLMException` - 【文档】为 AI 服务、沙箱、工具链、工作流等核心模块补充完整的 Docstring 和类型注释 * 📝 docs(ai): 补全核心模块文档注释并清理冗余代码 - 补全 `run/context`、`run/hooks` 和 `tools/engine/registry` 中类与方法的中文文档注释 - 清理 `tools/providers/builtin/sandbox` 中未使用的 `PythonPluginProtocol` 协议及相关导入 - 规范化部分代码的格式与尾随逗号 * ♻️ refactor!(flow): 重构 Task 为 AgentTask 并优化工作流元数据定义 - 【Breaking Change】将 `Task` 重命名为 `AgentTask` 以避免命名冲突 - 更新 Agent、Team、Workflow 等模块中的类型声明与相关逻辑 - 引入 `AutoNodeMeta` 强类型元数据,替换工作流装饰器中的裸字典定义 - 将 `StepMeta`、`ConditionMeta` 和 `RouterMeta` 统一移动至 `types.py` - 优化 `RunnableNode` 对上游 `AgentTask` 的处理与拼接逻辑 - 调整团队协作策略中 `FinishAction` 的返回值为完整结果对象 * ♻️ refactor(workflow): 移除人工确认机制并重构错误策略 - 移除工作流节点的人工确认(HITL)与挂起继续机制 - 删除 `auto` 自动化工作流及相关装饰器文件 - 将错误处理策略类从 `types.py` 拆分并移动到新文件 `policies.py` - 优化节点执行失败时的异常信息格式化输出 - 移除 `WorkflowRunResult` 和 `StepOutput` 中与挂起相关的状态字段 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
684 lines
25 KiB
Python
684 lines
25 KiB
Python
from abc import ABC, abstractmethod
|
||
import asyncio
|
||
import json
|
||
from typing import Any, cast
|
||
|
||
from zhenxun.services.ai.capabilities import CombinedCapability
|
||
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
|
||
from zhenxun.services.ai.core.engine.token_counter import (
|
||
parse_usage_info,
|
||
token_counter,
|
||
)
|
||
from zhenxun.services.ai.core.exceptions import (
|
||
ControlFlowExit,
|
||
UpstreamServerException,
|
||
)
|
||
from zhenxun.services.ai.core.messages import (
|
||
AgentMessage,
|
||
AssistantContentUnion,
|
||
AssistantMessage,
|
||
AudioPart,
|
||
ChatRequest,
|
||
ChatResponse,
|
||
FilePart,
|
||
ImagePart,
|
||
LLMMessage,
|
||
TextPart,
|
||
ToolCallPart,
|
||
ToolReturnPart,
|
||
VideoPart,
|
||
)
|
||
from zhenxun.services.ai.core.models import LLMContext
|
||
from zhenxun.services.ai.core.options import GenerationConfig
|
||
from zhenxun.services.ai.core.stream_events import (
|
||
LLMEndEvent,
|
||
LLMStartEvent,
|
||
ToolStreamChunkEvent,
|
||
)
|
||
from zhenxun.services.ai.flow.agent.engine.directive import (
|
||
DirectiveHandlerFunc,
|
||
directive_manager,
|
||
)
|
||
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
|
||
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
|
||
from zhenxun.services.ai.run import AgentRunResult, RunContext
|
||
from zhenxun.services.ai.run.session import session_manager
|
||
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
|
||
from zhenxun.services.ai.tools.models import ToolResult
|
||
from zhenxun.services.log import logger
|
||
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
|
||
|
||
|
||
class BaseAgentExecutor(ABC):
|
||
"""
|
||
Agent 核心执行器基类 (Template Method Pattern)。
|
||
定义了基于生命周期的大模型控制流。第三方开发者可通过重写特定钩子,
|
||
"""
|
||
|
||
async def run(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> AgentRunResult[Any]:
|
||
"""
|
||
核心模板方法 (Template Method)。
|
||
组织整个大模型推导与工具调用的生命周期循环。如无必要,请勿重写此方法。
|
||
"""
|
||
await self.on_start(state, resources)
|
||
|
||
try:
|
||
for cycle_index in range(resources.config.max_cycles):
|
||
state.current_cycle = cycle_index
|
||
await self.on_cycle_start(state, resources)
|
||
|
||
await self.build_llm_request(state, resources)
|
||
await self.execute_llm(state, resources)
|
||
|
||
await self.handle_llm_response(state, resources)
|
||
if state.is_finished:
|
||
assert state.final_result is not None
|
||
return state.final_result
|
||
|
||
await self.filter_tool_calls(state, resources)
|
||
if state.is_finished:
|
||
assert state.final_result is not None
|
||
return state.final_result
|
||
|
||
await self.execute_tools(state, resources)
|
||
|
||
await self.handle_tool_results(state, resources)
|
||
if state.is_finished:
|
||
assert state.final_result is not None
|
||
return state.final_result
|
||
|
||
return await self.on_fallback(state, resources)
|
||
except Exception as e:
|
||
raise e
|
||
|
||
@abstractmethod
|
||
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
|
||
"""生命周期: Agent 启动时调用,用于初始化状态或资源。"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def on_cycle_start(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""生命周期: 每次推理循环开始时调用。可用于 Token 预估或防死循环检测。"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def build_llm_request(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""生命周期: 构造请求大模型的 Messages 上下文和 Extra 参数。"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def execute_llm(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""生命周期: 触发大模型 API 请求并返回响应。"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def handle_llm_response(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""
|
||
生命周期: 处理大模型返回的结果,解析 Token 用量,
|
||
并将模型回复追加至对话历史。
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def filter_tool_calls(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""生命周期: 从大模型的响应中提取并过滤出需要在本地客户端执行的工具调用请求。"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def execute_tools(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""生命周期: 并发执行提取出的工具,并收集结果或异常。"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def handle_tool_results(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""
|
||
生命周期: 处理工具返回的结果。
|
||
包括异常拦截、UI 渲染、Handoff 移交指令以及将结果追加至对话历史。
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def on_fallback(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> AgentRunResult[Any]:
|
||
"""生命周期: 当大模型思考循环达到 max_cycles 时触发,执行兜底策略。"""
|
||
pass
|
||
|
||
|
||
class StandardAgentExecutor(BaseAgentExecutor):
|
||
"""
|
||
LLM 任务执行器(核心推理引擎)。
|
||
负责:生命周期回调触发、工具循环调用、
|
||
错误反思(Reflexion)、Token消耗追踪。
|
||
"""
|
||
|
||
def __init__(
|
||
self, directive_handlers: dict[str, DirectiveHandlerFunc] | None = None
|
||
):
|
||
self.tool_executor = ToolExecutor()
|
||
self._directive_handlers: dict[str, DirectiveHandlerFunc] = (
|
||
directive_handlers or {}
|
||
)
|
||
|
||
def _can_retry_via_llm(self, result: ToolResult) -> bool:
|
||
"""通过新版的专属字段直接判断是否允许重试"""
|
||
return result.is_retryable
|
||
|
||
def _check_follow_up(
|
||
self, state: AgentState, resources: AgentRunResources, session_info: Any
|
||
) -> bool:
|
||
"""检查追加队列,排空并合并数据到上下文,返回是否发现新消息"""
|
||
follow_ups = session_info.follow_up_queue.drain()
|
||
if follow_ups:
|
||
for fm in follow_ups:
|
||
state.messages.append(LLMMessage.user(f"💬 [用户追加指示]:{fm}"))
|
||
resources.run_context.session.append_only_manager.sync_messages(
|
||
state.messages
|
||
)
|
||
state.is_finished = False
|
||
state.final_result = None
|
||
return True
|
||
return False
|
||
|
||
async def _invoke_and_record_llm(
|
||
self,
|
||
state: AgentState,
|
||
resources: AgentRunResources,
|
||
messages: list[AgentMessage],
|
||
tools: list[Any] | None,
|
||
tool_choice: Any = None,
|
||
) -> ChatResponse:
|
||
"""执行 LLM 请求,处理基础指标遥测统计,并将新对话上下文追加到状态流"""
|
||
run_context = resources.run_context
|
||
cancellation_token = run_context.run.cancellation_token
|
||
|
||
current_extra = run_context.state.copy()
|
||
current_extra["__global_max_cycles__"] = getattr(
|
||
resources.config, "global_max_cycles", None
|
||
)
|
||
current_extra["__sys_capabilities"] = getattr(run_context, "capabilities", [])
|
||
current_extra["run_context"] = run_context
|
||
|
||
flattened_messages = ContextConverter.flatten_to_llm_messages(
|
||
messages, run_context
|
||
)
|
||
|
||
if run_context.run.event_bus:
|
||
await run_context.run.event_bus.emit(
|
||
LLMStartEvent(
|
||
model_name=resources.model_name or "unknown",
|
||
messages=flattened_messages,
|
||
)
|
||
)
|
||
|
||
response = await self._execute_model_request(
|
||
model_name=resources.model_name,
|
||
messages=flattened_messages,
|
||
config=resources.generation_config or GenerationConfig(),
|
||
run_context=run_context,
|
||
tools=tools,
|
||
tool_choice=tool_choice,
|
||
extra=current_extra,
|
||
cancellation_token=cancellation_token,
|
||
)
|
||
|
||
if run_context.run.event_bus:
|
||
await run_context.run.event_bus.emit(LLMEndEvent(response=response))
|
||
|
||
assistant_content = (
|
||
response.content_parts if response.content_parts else response.text
|
||
)
|
||
if response.thought_signature and isinstance(assistant_content, list):
|
||
for part in assistant_content:
|
||
if part.type == "thought":
|
||
if part.metadata is None:
|
||
part.metadata = {}
|
||
part.metadata["thought_signature"] = response.thought_signature
|
||
break
|
||
|
||
assistant_message = AssistantMessage(
|
||
content=cast(list[AssistantContentUnion], response.content_parts)
|
||
)
|
||
|
||
if hasattr(response, "parsed_obj") and response.parsed_obj is not None:
|
||
if not isinstance(response.parsed_obj, str):
|
||
if assistant_message.metadata is None:
|
||
assistant_message.metadata = {}
|
||
assistant_message.metadata["parsed_obj"] = response.parsed_obj
|
||
|
||
usage_obj = parse_usage_info(response.usage_info)
|
||
state.usage += usage_obj
|
||
if usage_obj.completion_tokens > 0:
|
||
assistant_message.token_cost = usage_obj.completion_tokens
|
||
|
||
state.messages.append(assistant_message)
|
||
run_context.session.append_only_manager.sync_messages(state.messages)
|
||
|
||
return response
|
||
|
||
async def _execute_model_request(
|
||
self,
|
||
model_name: str | None,
|
||
messages: list[LLMMessage],
|
||
config: GenerationConfig,
|
||
run_context: RunContext,
|
||
tools: list[Any] | None = None,
|
||
tool_choice: Any = None,
|
||
extra: dict[str, Any] | None = None,
|
||
cancellation_token: Any = None,
|
||
) -> ChatResponse:
|
||
request = ChatRequest(
|
||
messages=messages,
|
||
config=config,
|
||
tools=tools,
|
||
tool_choice=tool_choice,
|
||
extra=extra or {},
|
||
)
|
||
|
||
sys_caps = request.extra.pop("__sys_capabilities", [])
|
||
llm_context = LLMContext(request=request, cancellation_token=cancellation_token)
|
||
combined_cap = CombinedCapability(sys_caps)
|
||
|
||
async def inner_handler(ctx: LLMContext[Any, Any]) -> ChatResponse:
|
||
return await LLMOrchestrator.invoke(
|
||
request=ctx.request,
|
||
model_name=model_name,
|
||
task="chat",
|
||
override_config=config,
|
||
cancellation_token=ctx.cancellation_token,
|
||
)
|
||
|
||
return await combined_cap.wrap_model_request(
|
||
run_context, llm_context, inner_handler
|
||
)
|
||
|
||
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
|
||
resources.run_context.run.messages = state.messages
|
||
|
||
async def run(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> AgentRunResult[Any]:
|
||
"""覆盖基类的模板方法,实现灵活的 while 控制流和 FOLLOW_UP 合并"""
|
||
await self.on_start(state, resources)
|
||
session_info = await session_manager.get_or_create(
|
||
resources.run_context.session_id or "default_session"
|
||
)
|
||
|
||
try:
|
||
cycle_count = 0
|
||
while cycle_count < resources.config.max_cycles:
|
||
state.current_cycle = cycle_count
|
||
await self.on_cycle_start(state, resources)
|
||
|
||
await self.build_llm_request(state, resources)
|
||
await self.execute_llm(state, resources)
|
||
|
||
await self.handle_llm_response(state, resources)
|
||
if state.is_finished:
|
||
if self._check_follow_up(state, resources, session_info):
|
||
cycle_count = 0
|
||
continue
|
||
assert state.final_result is not None
|
||
return state.final_result
|
||
|
||
await self.filter_tool_calls(state, resources)
|
||
if state.is_finished:
|
||
if self._check_follow_up(state, resources, session_info):
|
||
cycle_count = 0
|
||
continue
|
||
assert state.final_result is not None
|
||
return state.final_result
|
||
|
||
await self.execute_tools(state, resources)
|
||
|
||
await self.handle_tool_results(state, resources)
|
||
if state.is_finished:
|
||
if self._check_follow_up(state, resources, session_info):
|
||
cycle_count = 0
|
||
continue
|
||
assert state.final_result is not None
|
||
return state.final_result
|
||
|
||
cycle_count += 1
|
||
|
||
if self._check_follow_up(state, resources, session_info):
|
||
return await self.run(state, resources)
|
||
|
||
return await self.on_fallback(state, resources)
|
||
except Exception as e:
|
||
raise e
|
||
|
||
async def on_cycle_start(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
cancellation_token = resources.run_context.run.cancellation_token
|
||
if cancellation_token:
|
||
cancellation_token.raise_if_cancelled()
|
||
|
||
try:
|
||
est_tokens = token_counter.count_context(
|
||
state.messages, resources.model_name or "", base_overhead=0
|
||
)
|
||
logger.debug(
|
||
f"[TokenTracker] (Iter {state.current_cycle + 1}) "
|
||
f"预估将消耗 {est_tokens} Token "
|
||
f"(Model: {resources.model_name or 'Unknown'})"
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
async def build_llm_request(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
run_context = resources.run_context
|
||
session_info = await session_manager.get_or_create(
|
||
run_context.session_id or "default_session"
|
||
)
|
||
steer_msgs = session_info.steer_queue.drain()
|
||
if steer_msgs:
|
||
for sm in steer_msgs:
|
||
state.messages.append(LLMMessage.user(f"💬 [用户实时修正指示]:{sm}"))
|
||
run_context.session.append_only_manager.sync_messages(state.messages)
|
||
|
||
messages_to_send = []
|
||
if state.static_system_prompt:
|
||
if isinstance(state.static_system_prompt, list):
|
||
for sp in state.static_system_prompt:
|
||
if sp and sp.strip():
|
||
messages_to_send.append(LLMMessage.system(sp))
|
||
else:
|
||
if state.static_system_prompt and state.static_system_prompt.strip():
|
||
messages_to_send.append(
|
||
LLMMessage.system(state.static_system_prompt)
|
||
)
|
||
|
||
if state.dynamic_system_messages:
|
||
messages_to_send.extend(state.dynamic_system_messages)
|
||
|
||
if (
|
||
hasattr(run_context.run, "dynamic_prompts")
|
||
and run_context.run.dynamic_prompts
|
||
):
|
||
for prompt_text in run_context.run.dynamic_prompts.values():
|
||
if prompt_text and prompt_text.strip():
|
||
messages_to_send.append(LLMMessage.system(prompt_text))
|
||
|
||
messages_to_send.extend(state.messages)
|
||
|
||
state.current_request_messages = messages_to_send
|
||
|
||
async def execute_llm(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
tools = state.tools
|
||
|
||
state.current_response = await self._invoke_and_record_llm(
|
||
state=state,
|
||
resources=resources,
|
||
messages=state.current_request_messages,
|
||
tools=list(tools) if tools else None,
|
||
tool_choice=None,
|
||
)
|
||
|
||
async def handle_llm_response(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
response = state.current_response
|
||
if not response:
|
||
return
|
||
|
||
if not response.tool_calls:
|
||
logger.debug("✅ AgentExecutor:模型未请求工具调用,推理循环结束。")
|
||
state.is_finished = True
|
||
state.final_result = model_construct(
|
||
AgentRunResult,
|
||
output=response.text,
|
||
messages=state.messages,
|
||
usage=state.usage,
|
||
)
|
||
|
||
async def filter_tool_calls(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
response = state.current_response
|
||
if not response:
|
||
return
|
||
tools = state.tools
|
||
event_bus = resources.run_context.run.event_bus
|
||
|
||
completed_call_ids = {
|
||
p.tool_call_id
|
||
for p in response.content_parts
|
||
if isinstance(p, ToolReturnPart)
|
||
}
|
||
client_tool_calls = []
|
||
for call in response.tool_calls:
|
||
tool_inst = tools.get(call.tool_name) if tools else None
|
||
is_server_side = call.id in completed_call_ids or (
|
||
tool_inst and getattr(tool_inst, "execution_side", "client") == "server"
|
||
)
|
||
|
||
if is_server_side:
|
||
logger.debug(
|
||
"☁️ [AgentExecutor] 检测到云端工具调用: "
|
||
f"{call.tool_name},已跳过本地执行。"
|
||
)
|
||
async with self.tool_executor._tool_stream_scope(
|
||
event_bus,
|
||
call.tool_name,
|
||
call.args if isinstance(call.args, dict) else {},
|
||
getattr(call, "intent", None),
|
||
) as box:
|
||
return_part = next(
|
||
(
|
||
p
|
||
for p in response.content_parts
|
||
if isinstance(p, ToolReturnPart)
|
||
and p.tool_call_id == call.id
|
||
),
|
||
None,
|
||
)
|
||
if return_part:
|
||
box["result"] = ToolResult(output=return_part.output)
|
||
else:
|
||
client_tool_calls.append(call)
|
||
|
||
if not client_tool_calls:
|
||
logger.info("✅ AgentExecutor:无本地客户端工具需执行,推理循环平滑结束。")
|
||
|
||
state.is_finished = True
|
||
state.final_result = model_construct(
|
||
AgentRunResult,
|
||
output=response.text,
|
||
messages=state.messages,
|
||
usage=state.usage,
|
||
)
|
||
|
||
state.current_tool_calls = client_tool_calls
|
||
|
||
async def execute_tools(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
run_context = resources.run_context
|
||
tools = state.tools
|
||
event_bus = run_context.run.event_bus
|
||
tool_calls = state.current_tool_calls
|
||
|
||
if not tool_calls:
|
||
return
|
||
|
||
val_tasks = [
|
||
self.tool_executor.validate_tool_call(
|
||
call,
|
||
tools,
|
||
run_context,
|
||
event_bus=event_bus,
|
||
)
|
||
for call in tool_calls
|
||
]
|
||
validated_calls = await asyncio.gather(*val_tasks)
|
||
|
||
exec_tasks = [
|
||
self.tool_executor.execute_tool_call(
|
||
val_call,
|
||
tools,
|
||
run_context,
|
||
event_bus=event_bus,
|
||
)
|
||
for val_call in validated_calls
|
||
]
|
||
tool_results = await asyncio.gather(*exec_tasks, return_exceptions=True)
|
||
state.current_tool_results = tool_results
|
||
|
||
def _assemble_tool_message(
|
||
self,
|
||
original_call: ToolCallPart,
|
||
res_or_exc: Any,
|
||
tool_res: ToolResult | None,
|
||
state: AgentState,
|
||
) -> LLMMessage:
|
||
"""负责处理异常、解析多模态、序列化,并装配为最终的工具消息载体"""
|
||
media_parts = []
|
||
final_content = "Success"
|
||
|
||
if isinstance(res_or_exc, BaseException):
|
||
if isinstance(res_or_exc, ControlFlowExit):
|
||
raise res_or_exc
|
||
final_content = json.dumps(
|
||
{"error": str(res_or_exc), "status": "failed"},
|
||
ensure_ascii=False,
|
||
)
|
||
elif tool_res is not None:
|
||
if isinstance(tool_res.output, list):
|
||
texts = []
|
||
for item in tool_res.output:
|
||
if isinstance(item, ImagePart | AudioPart | VideoPart | FilePart):
|
||
media_parts.append(item)
|
||
elif isinstance(item, TextPart):
|
||
texts.append(item.text)
|
||
else:
|
||
texts.append(str(item))
|
||
final_content = " ".join(texts) if texts else "Success"
|
||
elif isinstance(tool_res.output, str):
|
||
final_content = tool_res.output
|
||
else:
|
||
final_content = dump_json_safely(tool_res.output, ensure_ascii=False)
|
||
|
||
tool_usage = getattr(tool_res, "usage", None)
|
||
if tool_usage is not None:
|
||
state.usage += tool_usage
|
||
|
||
msg = LLMMessage.tool_response(
|
||
original_call.id, original_call.tool_name, final_content
|
||
)
|
||
if media_parts:
|
||
msg.content.extend(media_parts)
|
||
return msg
|
||
|
||
async def handle_tool_results(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> None:
|
||
"""处理所有工具执行结果,调度副作用指令并装配对话回传报文。"""
|
||
tool_calls = state.current_tool_calls
|
||
tool_results = state.current_tool_results
|
||
if not tool_calls or not tool_results:
|
||
return
|
||
|
||
for i, res_or_exc in enumerate(tool_results):
|
||
original_call = tool_calls[i]
|
||
tool_res = None
|
||
|
||
if not isinstance(res_or_exc, BaseException):
|
||
_, raw_tool_res = res_or_exc
|
||
tool_res = raw_tool_res
|
||
|
||
msg = self._assemble_tool_message(
|
||
original_call, res_or_exc, tool_res, state
|
||
)
|
||
state.messages.append(msg)
|
||
|
||
if tool_res and getattr(tool_res, "directive", None):
|
||
ns = getattr(resources.run_context.session, "namespace", "global")
|
||
handler = directive_manager.get_handler(
|
||
tool_res.directive.name, namespace=ns
|
||
)
|
||
|
||
if handler:
|
||
await handler(state, resources, tool_res)
|
||
if state.is_finished:
|
||
resources.run_context.session.append_only_manager.sync_messages(
|
||
state.messages
|
||
)
|
||
return
|
||
else:
|
||
logger.warning(
|
||
f"⚠️ 未能找到名为 '{tool_res.directive.name}' "
|
||
f"的指令处理器 (Namespace: {ns})"
|
||
)
|
||
|
||
resources.run_context.session.append_only_manager.sync_messages(state.messages)
|
||
|
||
async def on_fallback(
|
||
self, state: AgentState, resources: AgentRunResources
|
||
) -> AgentRunResult[Any]:
|
||
run_context = resources.run_context
|
||
event_bus = run_context.run.event_bus
|
||
|
||
if not resources.config.enable_fallback_summary:
|
||
raise UpstreamServerException(
|
||
f"超过最大工具调用循环次数 ({resources.config.max_cycles})。",
|
||
)
|
||
|
||
logger.warning(
|
||
f"AgentExecutor 达到最大循环次数 ({resources.config.max_cycles}),"
|
||
"触发兜底总结机制。"
|
||
)
|
||
|
||
if event_bus:
|
||
await event_bus.emit(
|
||
ToolStreamChunkEvent(
|
||
tool_name="System",
|
||
content="⏳ 思考过程过于复杂,正在强制生成最终总结...",
|
||
)
|
||
)
|
||
|
||
fallback_msg = LLMMessage.user(
|
||
"### 🚨 [系统强制指令]\n"
|
||
"你的任务执行已达到最大工具调用循环次数上限,当前思考流已被框架强制中断。\n"
|
||
"请**诚实地**向用户总结:你目前进行到了哪一步?遇到了什么困难导致循环耗尽?还有哪些预期步骤未能完成?\n"
|
||
"**绝对禁止**对用户撒谎声声称你已经完成了任务。严禁再次尝试调用任何工具!请直接输出纯文本结果。"
|
||
)
|
||
state.messages.append(fallback_msg)
|
||
|
||
fallback_response = await self._invoke_and_record_llm(
|
||
state=state,
|
||
resources=resources,
|
||
messages=state.messages,
|
||
tools=[],
|
||
tool_choice="none",
|
||
)
|
||
|
||
return model_construct(
|
||
AgentRunResult,
|
||
output=fallback_response.text,
|
||
messages=state.messages,
|
||
structured_data=None,
|
||
usage=state.usage,
|
||
)
|