mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-02 02:00:01 +08:00
* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统 - 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理 - 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册 - 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器 - 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签 - 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据 - 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度 - 重构执行分发器,支持并发限制、串行间隔和随机延迟打散 - 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入 - 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法 - 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出 - 将各模块的全局 logger 替换为对应的模块专属日志代理 - 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题 - 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token * ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性 - 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构 - 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型 - 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义 - 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑 - 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构 * ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入 - 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性 - 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费 - 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号 * 🚨 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>
676 lines
24 KiB
Python
676 lines
24 KiB
Python
from abc import ABC, abstractmethod
|
||
import asyncio
|
||
import json
|
||
from typing import Any
|
||
|
||
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,
|
||
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.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.ai.utils.logger import log_agent as logger
|
||
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
|
||
|
||
from .directive import (
|
||
DirectiveHandlerFunc,
|
||
directive_manager,
|
||
)
|
||
|
||
|
||
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
|
||
)
|
||
|
||
await run_context.run.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,
|
||
)
|
||
|
||
await run_context.run.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=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"(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("✅ 模型未请求工具调用,推理循环结束。")
|
||
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(
|
||
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("✅ 无本地客户端工具需执行,推理循环平滑结束。")
|
||
|
||
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
|
||
|
||
if not resources.config.enable_fallback_summary:
|
||
raise UpstreamServerException(
|
||
f"超过最大工具调用循环次数 ({resources.config.max_cycles})。",
|
||
)
|
||
|
||
logger.warning(
|
||
f"达到最大循环次数 ({resources.config.max_cycles}),触发兜底总结机制。"
|
||
)
|
||
|
||
await run_context.run.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,
|
||
)
|