mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-11 15:00:00 +08:00
✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)
* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 - 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片 - 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式 - 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统 - 【新增】引入基于 Docker 的安全代码执行沙箱环境 - 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务 - 【新增】引入输入输出安全合规护栏与自愈反思机制 - 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等) - 【优化】优化日志脱敏与 Token 预估机制 - 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令 * 🔧 chore(deps): 更新项目依赖与配置 - 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt - 在 pyright 配置中设置 reportMissingImports 为 none - 调整 .gitignore 中 resources 目录的忽略规则 * ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出 - 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束 - 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用 - 调整指令处理器(Directive)的返回值为 `tool_res.output` - 修复部分类型检查警告并优化联合类型判断语法 * ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制 - 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用 - 移除通过 `context.state` 传递魔术变量的隐式控制流设计 - 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult` - 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令 - 补全 `MessageBuilder` 中部分核心方法的文档注释 * 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑 -【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值 -【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题 -【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求 -【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建 -【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常 * 📝 docs(core): 补充核心模块初始化方法的文档注释 * 🚨 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>
This commit is contained in:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -0,0 +1,20 @@
|
||||
from .builders import (
|
||||
AgentProfileResolver,
|
||||
CapabilityBuilder,
|
||||
ContextBuilder,
|
||||
ToolBuilder,
|
||||
)
|
||||
from .directive import DirectiveManager, directive, directive_manager
|
||||
from .executor import BaseAgentExecutor, StandardAgentExecutor
|
||||
|
||||
__all__ = [
|
||||
"AgentProfileResolver",
|
||||
"BaseAgentExecutor",
|
||||
"CapabilityBuilder",
|
||||
"ContextBuilder",
|
||||
"DirectiveManager",
|
||||
"StandardAgentExecutor",
|
||||
"ToolBuilder",
|
||||
"directive",
|
||||
"directive_manager",
|
||||
]
|
||||
@@ -0,0 +1,434 @@
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
import inspect
|
||||
from typing import Any, cast
|
||||
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
|
||||
from zhenxun.services.ai.capabilities import CombinedCapability
|
||||
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
||||
from zhenxun.services.ai.core.messages import LLMMessage
|
||||
from zhenxun.services.ai.core.options import GenerationConfig
|
||||
from zhenxun.services.ai.core.templates import PromptTemplate
|
||||
from zhenxun.services.ai.flow.agent.models import Persona
|
||||
from zhenxun.services.ai.run import RunContext
|
||||
from zhenxun.services.ai.run.di import DependencyInjector
|
||||
from zhenxun.services.ai.tools.engine.registry import (
|
||||
ToolCollection,
|
||||
tool_provider_manager,
|
||||
)
|
||||
from zhenxun.services.ai.tools.models import GlobalToolFilter, ResolvedToolPayload
|
||||
from zhenxun.utils.pydantic_compat import model_copy
|
||||
|
||||
|
||||
class AgentProfileResolver:
|
||||
"""Agent 配置解析器:负责提取与合并 Agent 的运行时 Profile"""
|
||||
|
||||
@staticmethod
|
||||
def resolve_memory(
|
||||
agent_memory_config: MemoryConfig, override_memory: Any | None
|
||||
) -> MemoryConfig:
|
||||
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
||||
|
||||
if override_memory is not None:
|
||||
return MemoryBuilder.resolve(override_memory)
|
||||
return model_copy(agent_memory_config, deep=True)
|
||||
|
||||
@staticmethod
|
||||
def resolve_generation_config(
|
||||
base_config: GenerationConfig,
|
||||
cap_config: GenerationConfig | None,
|
||||
profile_config: GenerationConfig | None,
|
||||
) -> GenerationConfig:
|
||||
final_gen_config = model_copy(base_config, deep=True)
|
||||
if cap_config:
|
||||
final_gen_config = final_gen_config.merge_with(cap_config)
|
||||
if profile_config:
|
||||
final_gen_config = final_gen_config.merge_with(profile_config)
|
||||
return final_gen_config
|
||||
|
||||
|
||||
class CapabilityBuilder:
|
||||
"""拦截器能力组装器:负责合并 Agent, Task, Profile 和全局的中间件"""
|
||||
|
||||
@staticmethod
|
||||
async def build_for_run(
|
||||
agent_name: str,
|
||||
namespace: str,
|
||||
output_type: Any | None,
|
||||
raw_schema: dict | None,
|
||||
agent_guardrails: list,
|
||||
task_guardrails: list,
|
||||
task_obj: Any | None,
|
||||
agent_capabilities: list,
|
||||
profile_capabilities: list | None,
|
||||
context: RunContext,
|
||||
) -> CombinedCapability:
|
||||
from zhenxun.services.ai.capabilities import (
|
||||
AbstractCapability,
|
||||
DynamicCapability,
|
||||
)
|
||||
from zhenxun.services.ai.flow.agent.capabilities import (
|
||||
OutputValidationCapability,
|
||||
TaskTrackingCapability,
|
||||
)
|
||||
|
||||
dynamic_caps = []
|
||||
combined_guardrails = agent_guardrails + task_guardrails
|
||||
|
||||
if output_type is not None and output_type is not str:
|
||||
dynamic_caps.append(
|
||||
OutputValidationCapability(output_type, combined_guardrails)
|
||||
)
|
||||
elif raw_schema is not None:
|
||||
dynamic_caps.append(
|
||||
OutputValidationCapability(
|
||||
None, combined_guardrails, raw_schema=raw_schema
|
||||
)
|
||||
)
|
||||
elif combined_guardrails:
|
||||
dynamic_caps.append(OutputValidationCapability(None, combined_guardrails))
|
||||
|
||||
if task_obj:
|
||||
dynamic_caps.append(TaskTrackingCapability(task_obj, agent_name))
|
||||
|
||||
run_level_caps = []
|
||||
if profile_capabilities:
|
||||
for cap in profile_capabilities:
|
||||
if isinstance(cap, AbstractCapability):
|
||||
run_level_caps.append(cap)
|
||||
elif callable(cap):
|
||||
run_level_caps.append(DynamicCapability(cap))
|
||||
|
||||
from zhenxun.services.ai.run import (
|
||||
GLOBAL_CAPABILITIES,
|
||||
)
|
||||
|
||||
base_caps = GLOBAL_CAPABILITIES.get("global", []).copy()
|
||||
if namespace != "global" and namespace in GLOBAL_CAPABILITIES:
|
||||
base_caps.extend(GLOBAL_CAPABILITIES[namespace])
|
||||
|
||||
combined_cap = CombinedCapability(
|
||||
base_caps
|
||||
+ getattr(context, "capabilities", [])
|
||||
+ agent_capabilities
|
||||
+ run_level_caps
|
||||
+ dynamic_caps
|
||||
)
|
||||
return cast(CombinedCapability, await combined_cap.for_run(context))
|
||||
|
||||
|
||||
class ContextBuilder:
|
||||
"""系统提示词与上下文记忆构建器"""
|
||||
|
||||
@staticmethod
|
||||
async def build_prompts(
|
||||
instruction: str | PromptTemplate,
|
||||
system_prompts: list[Any],
|
||||
run_context: RunContext,
|
||||
run_scoped_cap: CombinedCapability,
|
||||
persona: Persona | None = None,
|
||||
) -> tuple[str, list[Any]]:
|
||||
"""解析提示词,返回 (静态系统提示词文本, 动态独立消息列表) 元组"""
|
||||
|
||||
static_instructions = []
|
||||
dynamic_messages = []
|
||||
|
||||
for sp_func in system_prompts:
|
||||
sig = inspect.signature(sp_func)
|
||||
if len(sig.parameters) > 0:
|
||||
injected_kwargs = await DependencyInjector.resolve_all(
|
||||
sig=sig,
|
||||
call_kwargs={},
|
||||
context=run_context,
|
||||
)
|
||||
res = (
|
||||
(await sp_func(**injected_kwargs))
|
||||
if is_coroutine_callable(sp_func)
|
||||
else sp_func(**injected_kwargs)
|
||||
)
|
||||
else:
|
||||
res = (await sp_func()) if is_coroutine_callable(sp_func) else sp_func()
|
||||
if res:
|
||||
if isinstance(res, LLMMessage):
|
||||
dynamic_messages.append(res)
|
||||
elif isinstance(res, list) and all(
|
||||
isinstance(m, LLMMessage) for m in res
|
||||
):
|
||||
dynamic_messages.extend(res)
|
||||
else:
|
||||
if isinstance(res, list):
|
||||
for item in res:
|
||||
if item:
|
||||
dynamic_messages.append(LLMMessage.system(str(item)))
|
||||
else:
|
||||
dynamic_messages.append(LLMMessage.system(str(res)))
|
||||
|
||||
if persona:
|
||||
persona_parts = [
|
||||
f"## 扮演角色 (Role)\n{persona.role}",
|
||||
f"## 核心目标 (Goal)\n{persona.goal}",
|
||||
]
|
||||
if persona.backstory:
|
||||
persona_parts.append(f"## 角色背景 (Backstory)\n{persona.backstory}")
|
||||
static_instructions.append("\n\n".join(persona_parts))
|
||||
|
||||
if instruction:
|
||||
static_instructions.append("## 本次任务指令 (Task)")
|
||||
|
||||
if instruction:
|
||||
if isinstance(instruction, PromptTemplate):
|
||||
static_instructions.append(instruction.format_with_context(run_context))
|
||||
else:
|
||||
static_instructions.append(str(instruction))
|
||||
|
||||
caps = (
|
||||
run_scoped_cap.capabilities
|
||||
if run_scoped_cap
|
||||
else getattr(run_context, "capabilities", [])
|
||||
)
|
||||
for cap in caps:
|
||||
cap_prompts = await cap.get_system_prompts(run_context)
|
||||
for prompt_text in cap_prompts:
|
||||
if prompt_text and prompt_text.strip():
|
||||
dynamic_messages.append(LLMMessage.system(prompt_text))
|
||||
|
||||
static_text = "\n\n".join(static_instructions)
|
||||
|
||||
render_context = {
|
||||
"deps": run_context.deps,
|
||||
"bot": getattr(run_context.deps, "bot", None),
|
||||
"event": getattr(run_context.deps, "event", None),
|
||||
"matcher": getattr(run_context.deps, "matcher", None),
|
||||
}
|
||||
if run_context.state:
|
||||
render_context.update(run_context.state)
|
||||
|
||||
rendered_dynamic_messages = []
|
||||
from zhenxun.services.ai.core.messages import TextPart
|
||||
|
||||
for msg in dynamic_messages:
|
||||
if msg.role == "system":
|
||||
new_content = []
|
||||
changed = False
|
||||
for part in msg.content:
|
||||
if isinstance(part, TextPart) and part.text:
|
||||
try:
|
||||
rendered_text = PromptTemplate(part.text).render(
|
||||
**render_context
|
||||
)
|
||||
new_content.append(TextPart(text=rendered_text))
|
||||
if rendered_text != part.text:
|
||||
changed = True
|
||||
except Exception:
|
||||
new_content.append(part)
|
||||
else:
|
||||
new_content.append(part)
|
||||
if changed:
|
||||
new_msg = msg.model_copy(deep=True)
|
||||
new_msg.content = new_content
|
||||
rendered_dynamic_messages.append(new_msg)
|
||||
else:
|
||||
rendered_dynamic_messages.append(msg)
|
||||
else:
|
||||
rendered_dynamic_messages.append(msg)
|
||||
|
||||
return (
|
||||
PromptTemplate(static_text).render(**render_context),
|
||||
rendered_dynamic_messages,
|
||||
)
|
||||
|
||||
|
||||
class ToolBuilder:
|
||||
"""系统工具集合解析与构建器"""
|
||||
|
||||
@staticmethod
|
||||
async def resolve_tools(
|
||||
tool_definitions: list[Any],
|
||||
toolset_funcs: list[Any],
|
||||
system_tools: list[Any],
|
||||
namespace: str,
|
||||
tool_filter: GlobalToolFilter | None,
|
||||
run_context: RunContext,
|
||||
run_scoped_cap: CombinedCapability,
|
||||
) -> ResolvedToolPayload:
|
||||
"""解析、合并并过滤工具集"""
|
||||
defs_to_resolve = list(tool_definitions)
|
||||
|
||||
for ts_func in toolset_funcs:
|
||||
sig = inspect.signature(ts_func)
|
||||
injected_kwargs = {}
|
||||
if len(sig.parameters) > 0:
|
||||
injected_kwargs = await DependencyInjector.resolve_all(
|
||||
sig=sig,
|
||||
call_kwargs={},
|
||||
context=run_context,
|
||||
)
|
||||
|
||||
res = (
|
||||
(await ts_func(**injected_kwargs))
|
||||
if is_coroutine_callable(ts_func)
|
||||
else ts_func(**injected_kwargs)
|
||||
)
|
||||
|
||||
if res is not None:
|
||||
if isinstance(res, list):
|
||||
defs_to_resolve.extend(res)
|
||||
else:
|
||||
defs_to_resolve.append(res)
|
||||
|
||||
if system_tools:
|
||||
for st in system_tools:
|
||||
if st not in defs_to_resolve:
|
||||
defs_to_resolve.append(st)
|
||||
|
||||
caps = (
|
||||
run_scoped_cap.capabilities
|
||||
if run_scoped_cap
|
||||
else getattr(run_context, "capabilities", [])
|
||||
)
|
||||
for cap in caps:
|
||||
cap_tools = await cap.get_tools(run_context)
|
||||
defs_to_resolve.extend(cap_tools)
|
||||
|
||||
payload = await tool_provider_manager.resolve_tools(
|
||||
defs_to_resolve, namespace, context=run_context
|
||||
)
|
||||
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
async def prepare_effective_tools(
|
||||
effective_tools: list[Any],
|
||||
context: RunContext,
|
||||
tool_filters: list[Callable],
|
||||
run_scoped_cap: CombinedCapability,
|
||||
) -> ToolCollection:
|
||||
"""处理生命周期:在工具发往执行器前,进行最终的 Schema 拦截和清洗"""
|
||||
current_tool_defs = []
|
||||
for t_exec in effective_tools:
|
||||
if hasattr(t_exec, "get_definition"):
|
||||
t_def = await t_exec.get_definition(context)
|
||||
if t_def:
|
||||
current_tool_defs.append(t_def)
|
||||
|
||||
if tool_filters:
|
||||
for filter_func in tool_filters:
|
||||
sig = inspect.signature(filter_func)
|
||||
call_kwargs = {"tool_defs": current_tool_defs}
|
||||
resolved_kwargs = await DependencyInjector.resolve_all(
|
||||
sig, call_kwargs, context
|
||||
)
|
||||
filtered_kwargs = {
|
||||
k: v for k, v in resolved_kwargs.items() if k in sig.parameters
|
||||
}
|
||||
_res = (
|
||||
await filter_func(**filtered_kwargs)
|
||||
if is_coroutine_callable(filter_func)
|
||||
else filter_func(**filtered_kwargs)
|
||||
)
|
||||
if _res is not None:
|
||||
current_tool_defs = list(_res)
|
||||
|
||||
_cap_res = await run_scoped_cap.prepare_tools(context, current_tool_defs)
|
||||
if _cap_res is not None:
|
||||
current_tool_defs = list(_cap_res)
|
||||
|
||||
final_defs_map = {d.name.lower(): d for d in current_tool_defs if d}
|
||||
final_effective_tools = ToolCollection()
|
||||
for t_exec in effective_tools:
|
||||
t_name = getattr(t_exec, "name", "unknown")
|
||||
if t_name.lower() in final_defs_map:
|
||||
cloned_tool = copy.copy(t_exec)
|
||||
cloned_tool._dynamic_def = final_defs_map[t_name.lower()]
|
||||
final_effective_tools.append(cloned_tool)
|
||||
return final_effective_tools
|
||||
|
||||
|
||||
class SessionBuilder:
|
||||
"""会话与记忆域构建器:负责隔离前缀计算和读写门面装配"""
|
||||
|
||||
@staticmethod
|
||||
def build_session_and_memory(
|
||||
context: RunContext,
|
||||
namespace: str,
|
||||
agent_name: str,
|
||||
effective_memory: MemoryConfig,
|
||||
) -> tuple[Any, Any, Any]:
|
||||
from zhenxun.services.ai.context.memory.engine import MemoryReader, MemoryWriter
|
||||
from zhenxun.services.ai.context.memory.types import SessionMetadata
|
||||
from zhenxun.services.ai.utils.scope import ScopeSelector
|
||||
|
||||
bot_id = None
|
||||
bot_inst = context.get_bot()
|
||||
if bot_inst and hasattr(bot_inst, "self_id"):
|
||||
bot_id = str(bot_inst.self_id)
|
||||
|
||||
selector = ScopeSelector(
|
||||
user_id=context.get_user_id(),
|
||||
group_id=context.get_group_id(),
|
||||
platform=context.get_platform(),
|
||||
bot_id=bot_id,
|
||||
namespace=namespace,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
|
||||
all_scopes = {"/"}
|
||||
scope_name_mapping = {}
|
||||
|
||||
if effective_memory.short_term and effective_memory.short_term.isolation:
|
||||
sel = effective_memory.short_term.isolation.resolve(
|
||||
deps=context.deps,
|
||||
prefix="",
|
||||
default_namespace=namespace,
|
||||
default_agent=agent_name,
|
||||
)
|
||||
all_scopes.add(sel.scope_prefix)
|
||||
|
||||
for config_part in [effective_memory.slots, effective_memory.long_term]:
|
||||
if config_part and hasattr(config_part, "scopes") and config_part.scopes:
|
||||
for name, builder in config_part.scopes.items():
|
||||
sel = builder.resolve(
|
||||
deps=context.deps,
|
||||
prefix="",
|
||||
default_namespace=namespace,
|
||||
default_agent=agent_name,
|
||||
)
|
||||
all_scopes.add(sel.scope_prefix)
|
||||
scope_name_mapping[sel.scope_prefix] = name
|
||||
|
||||
parts = selector.get_scope_parts()
|
||||
for i in range(len(parts)):
|
||||
all_scopes.add("/" + "/".join(parts[: i + 1]))
|
||||
|
||||
accessible_scopes = sorted(all_scopes, key=lambda x: len(x.split("/")))
|
||||
|
||||
short_term_builder = (
|
||||
effective_memory.short_term.isolation
|
||||
if effective_memory.short_term
|
||||
else effective_memory.base_isolation
|
||||
)
|
||||
short_term_selector = short_term_builder.resolve(
|
||||
deps=context.deps,
|
||||
prefix="",
|
||||
default_namespace=namespace,
|
||||
default_agent=agent_name,
|
||||
)
|
||||
|
||||
session_metadata = SessionMetadata(
|
||||
session_id=short_term_selector.scope_prefix,
|
||||
selector=selector,
|
||||
scope_prefix=selector.scope_prefix,
|
||||
accessible_scopes=accessible_scopes,
|
||||
scope_name_mapping=scope_name_mapping,
|
||||
)
|
||||
reader = MemoryReader(
|
||||
session_meta=session_metadata, memory_config=effective_memory
|
||||
)
|
||||
writer = MemoryWriter(
|
||||
session_meta=session_metadata,
|
||||
memory_config=effective_memory,
|
||||
context=context,
|
||||
)
|
||||
|
||||
return session_metadata, reader, writer
|
||||
@@ -0,0 +1,115 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
|
||||
from zhenxun.services.ai.run.models import AgentRunResult, HandoffPayload
|
||||
from zhenxun.services.ai.tools.models import ToolResult
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.pydantic_compat import model_construct
|
||||
from zhenxun.utils.utils import infer_plugin_namespace
|
||||
|
||||
DirectiveHandlerFunc = Callable[
|
||||
[AgentState, AgentRunResources, ToolResult], Awaitable[None]
|
||||
]
|
||||
|
||||
|
||||
class DirectiveManager:
|
||||
"""工具指令路由注册中心"""
|
||||
|
||||
def __init__(self):
|
||||
self._handlers: dict[str, dict[str, DirectiveHandlerFunc]] = defaultdict(dict)
|
||||
|
||||
def register(
|
||||
self, name: str, handler: DirectiveHandlerFunc, namespace: str = "global"
|
||||
) -> None:
|
||||
self._handlers[namespace][name] = handler
|
||||
logger.debug(f"已注册工具副作用指令: '{name}' -> Namespace: '{namespace}'")
|
||||
|
||||
def get_handler(
|
||||
self, name: str, namespace: str = "global"
|
||||
) -> DirectiveHandlerFunc | None:
|
||||
"""优先从指定 namespace 找,找不到回退到 global"""
|
||||
ns_dict = self._handlers.get(namespace, {})
|
||||
if name in ns_dict:
|
||||
return ns_dict[name]
|
||||
return self._handlers.get("global", {}).get(name)
|
||||
|
||||
|
||||
directive_manager = DirectiveManager()
|
||||
|
||||
|
||||
def directive(name: str | None = None, namespace: str | None = None):
|
||||
"""
|
||||
注册一个自定义工具副作用指令处理器的装饰器。
|
||||
|
||||
参数:
|
||||
name: 指令的名称,如果不填则默认使用被装饰的函数名。
|
||||
namespace: 插件命名空间,如果不填则基于代码调用栈自动推断。
|
||||
"""
|
||||
|
||||
def decorator(func: DirectiveHandlerFunc):
|
||||
dir_name = name or func.__name__
|
||||
ns = namespace if namespace is not None else infer_plugin_namespace()
|
||||
directive_manager.register(dir_name, func, ns)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@directive("submit_structured", namespace="global")
|
||||
async def handle_submit_structured(
|
||||
state: AgentState, resources: AgentRunResources, tool_res: ToolResult
|
||||
) -> None:
|
||||
parsed_obj = (
|
||||
tool_res.directive.payload.get("parsed_obj") if tool_res.directive else None
|
||||
)
|
||||
logger.info("✅ 拦截到结构化结果提交,结束循环。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=None,
|
||||
messages=state.messages,
|
||||
structured_data=parsed_obj,
|
||||
usage=state.usage,
|
||||
)
|
||||
|
||||
|
||||
@directive("end_run", namespace="global")
|
||||
async def handle_end_run(
|
||||
state: AgentState, resources: AgentRunResources, tool_res: ToolResult
|
||||
) -> None:
|
||||
output = (
|
||||
tool_res.directive.payload.get("output", tool_res.output)
|
||||
if tool_res.directive
|
||||
else tool_res.output
|
||||
)
|
||||
logger.debug("✅ 捕获到工具发出的终止信号,提前结束推理循环。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=output,
|
||||
messages=state.messages,
|
||||
usage=state.usage,
|
||||
)
|
||||
|
||||
|
||||
@directive("handoff", namespace="global")
|
||||
async def handle_handoff(
|
||||
state: AgentState, resources: AgentRunResources, tool_res: ToolResult
|
||||
) -> None:
|
||||
payload = tool_res.directive.payload if tool_res.directive else {}
|
||||
handoff = HandoffPayload(
|
||||
target=payload.get("target", "unknown"),
|
||||
reason=payload.get("reason", ""),
|
||||
context_data=payload.get("context_data", ""),
|
||||
)
|
||||
output_text = f"已触发控制权移交 -> {handoff.target}。原因: {handoff.reason}"
|
||||
logger.info(f"✅ 拦截到移交(Handoff)信号: 移交给 -> {handoff.target}。结束循环。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=output_text,
|
||||
messages=state.messages,
|
||||
usage=state.usage,
|
||||
handoff=handoff,
|
||||
)
|
||||
@@ -0,0 +1,691 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, cast
|
||||
|
||||
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,
|
||||
)
|
||||
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
|
||||
from zhenxun.services.ai.run import AgentRunResult, RunContext
|
||||
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:
|
||||
from zhenxun.services.ai.capabilities import CombinedCapability
|
||||
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
|
||||
|
||||
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)
|
||||
from zhenxun.services.ai.run.session import session_manager
|
||||
|
||||
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
|
||||
|
||||
from zhenxun.services.ai.run.session import session_manager
|
||||
|
||||
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:
|
||||
from zhenxun.services.ai.tools.models import ToolResult
|
||||
|
||||
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
|
||||
|
||||
from zhenxun.services.ai.flow.agent.engine.directive import directive_manager
|
||||
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user