✨ 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:
Rumio
2026-07-03 08:53:56 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bdc1374848
commit 80fc5b86a7
223 changed files with 41885 additions and 10006 deletions
@@ -0,0 +1,11 @@
from .agent import Agent
from .models import (
AgentConfig,
Persona,
)
__all__ = [
"Agent",
"AgentConfig",
"Persona",
]
File diff suppressed because it is too large Load Diff
+142
View File
@@ -0,0 +1,142 @@
import asyncio
from typing import Any, Generic, cast
from typing_extensions import TypeVar
from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna.uniseg import UniMessage
from zhenxun.services.ai.core.exceptions import (
ConcurrencyInterruptException,
ConcurrencyRejectException,
ControlFlowExit,
InterventionHandledException,
)
from zhenxun.services.ai.core.messages import UsageInfo
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError
from zhenxun.services.ai.run.ui import UIController
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
T_Deps = TypeVar("T_Deps", default=Any)
T_Out = TypeVar("T_Out", default=str)
class AgentRunner(Generic[T_Out]):
"""
智能体运行器。
负责将大模型的纯净数据流包装为平台交互动作(发消息、UI渲染)。
自带 ContextVars 隐式上下文提取魔法。
"""
def __init__(
self,
runnable: BaseRunnable,
context: RunContext | None = None,
**kwargs: Any,
):
self.runnable = runnable
self.context = context or RunContext(**kwargs)
is_stateless = (
getattr(self.runnable.runtime_config, "stateless", True)
if hasattr(self.runnable, "runtime_config")
else True
)
if is_stateless and self.context.session_id:
if not self.context.session_id.startswith("stateless_"):
import uuid
self.context.session_id = (
f"stateless_{self.context.session_id}_{uuid.uuid4().hex[:8]}"
)
if self.context.session:
self.context.session.session_id = self.context.session_id
@property
def _bot(self) -> Bot | None:
return self.context.get_bot()
@property
def _event(self) -> Event | None:
return self.context.get_event()
async def reply(
self, prompt: Any = None, reply_to: bool = False, **kwargs: Any
) -> AgentRunResult[T_Out]:
"""交互式执行:将 Agent 运行过程中的工具调用状态和最终结果自动发送给用户。"""
final_result = None
profile = kwargs.pop("profile", None)
try:
async with self.runnable.run_stream(
prompt=prompt,
context=self.context,
profile=profile,
**kwargs,
) as stream_result:
async for stream_event in stream_result.stream_events():
if isinstance(stream_event, AgentRunEnd):
final_result = stream_event.result
elif isinstance(stream_event, AgentRunError):
raise stream_event.error
except ControlFlowExit as e:
if isinstance(e, InterventionHandledException):
logger.info(f"✨ {self.runnable.name} 触发运行时干预: {e.message}")
if e.display_content and self._bot and self._event:
await MessageUtils.build_message(str(e.display_content)).send(
reply_to=reply_to
)
return cast(
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
)
if isinstance(e, ConcurrencyRejectException):
logger.warning(
f"⏳ {self.runnable.name} 触发并发拒绝 (REJECT): {e.message}"
)
return cast(
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
)
if isinstance(e, ConcurrencyInterruptException):
logger.warning(
f"🛑 {self.runnable.name} 触发并发中断 (INTERRUPT): {e.message}"
)
return cast(
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
)
logger.debug(
f"{self.runnable.name} 控制流正常中断: {type(e).__name__} - {e}"
)
await UIController.handle_control_flow_exit_display(
e, self.context, reply_to
)
raise asyncio.CancelledError()
except Exception as e:
logger.error(f"{self.runnable.name} 运行失败: {e}", e=e)
if self._bot and self._event:
await MessageUtils.build_message(f"❌ 运行发生错误: {e}").send()
raise e
if final_result and final_result.output and self._bot and self._event:
if isinstance(final_result.output, UniMessage):
await final_result.output.send(
self._event, bot=self._bot, reply_to=reply_to
)
final_result.output = final_result.output.extract_plain_text()
else:
final_msg = str(final_result.output)
await MessageUtils.build_message(final_msg).send()
if final_result is None:
raise RuntimeError("智能体运行流异常结束:未返回最终结果。")
return cast(AgentRunResult[T_Out], final_result)
@@ -0,0 +1,156 @@
import asyncio
from typing import Any, cast
from zhenxun.services.ai.capabilities import AbstractCapability
from zhenxun.services.ai.capabilities.base import CapabilityOrdering
from zhenxun.services.ai.core.engine.structured_parser import (
BaseOutputProcessor,
SubmitFinalResultExecutable,
)
from zhenxun.services.ai.core.exceptions import UpstreamServerException
from zhenxun.services.ai.core.messages import TaskLifecycleEvent
from zhenxun.services.ai.core.options import BaseOutputDefinition, ToolOutput
from zhenxun.services.ai.run import AgentRunResult, RunContext, Task
from zhenxun.services.log import logger
class OutputValidationCapability(AbstractCapability):
"""输出拦截与校验能力组件 (支持纯文本及结构化护栏)"""
def get_ordering(self) -> Any:
from zhenxun.services.ai.capabilities.builtin import (
ReflexionCapability,
)
return CapabilityOrdering(wraps=[ReflexionCapability])
def __init__(
self,
output_type: Any | None = None,
guardrails: list[Any] | None = None,
raw_schema: dict[str, Any] | None = None,
):
self.output_type = output_type
self.raw_schema = raw_schema
from zhenxun.services.ai.guardrails import parse_guardrails
self.guardrails = parse_guardrails(guardrails)
self.processor = None
self.submit_tool = None
if self.output_type is not None:
if isinstance(self.output_type, BaseOutputDefinition):
out_type = self.output_type.type_
tool_name_override = (
self.output_type.name
if isinstance(self.output_type, ToolOutput)
else None
)
else:
out_type = cast(type[Any], self.output_type)
tool_name_override = None
self.processor = BaseOutputProcessor(
response_model=out_type,
)
self.submit_tool = SubmitFinalResultExecutable(
self.processor, self.guardrails
)
if tool_name_override:
self.submit_tool.name = tool_name_override
elif self.raw_schema is not None:
self.processor = BaseOutputProcessor(
response_model=None,
raw_schema=self.raw_schema,
)
self.submit_tool = SubmitFinalResultExecutable(
self.processor, self.guardrails
)
async def get_system_prompts(self, context: RunContext) -> list[str]:
"""动态注入结构化要求提示词"""
if self.submit_tool:
return [
"### ⚠️ [核心任务:结构化输出要求]\n"
"当前任务处于严格的 **结构化输出模式**。\n"
"当你完成所有调查和思考后,必须且只能调用 "
f"`{self.submit_tool.name}` 工具来提交最终结果,"
"禁止用纯文本直接作答。\n"
"(📌 提示:最终需要返回的数据结构要求,"
"请严格查阅并遵循 "
f"`{self.submit_tool.name}` 工具的参数 Schema 定义,"
"将其视为唯一的数据约束)"
]
return []
async def get_tools(self, context: RunContext) -> list[Any]:
"""动态挂载提交最终结果的工具"""
if self.submit_tool:
return [self.submit_tool]
return []
async def wrap_model_request(self, context, llm_context, handler):
"""将 Processor 和 Guardrails 传给底层的 IvrCapability"""
llm_context.request.extra["output_processor"] = self.processor
llm_context.request.extra["guardrails"] = self.guardrails
return await handler(llm_context)
async def wrap_run(self, context: RunContext, handler: Any) -> AgentRunResult[Any]:
"""运行结束后,校验是否成功提取了结构化数据"""
result = await handler()
if self.output_type is not None or self.raw_schema is not None:
if result.structured_data is not None:
result.output = result.structured_data
else:
tool_name = self.submit_tool.name if self.submit_tool else "unknown"
logger.error(f"Agent 未能调用 {tool_name} 提交结构化数据。")
raise UpstreamServerException(
"模型未能输出符合要求的结构化数据。",
)
return result
class TaskTrackingCapability(AbstractCapability):
"""数据契约任务状态追踪与事件遥测组件"""
def __init__(self, task: Task, agent_name: str):
self.task = task
self.agent_name = agent_name
async def wrap_run(self, context: RunContext, handler: Any) -> AgentRunResult[Any]:
"""任务生命周期追踪"""
task_name = self.task.name or self.task.id[:8]
logger.debug(f"📋 **开始任务**: `{task_name}` (由 {self.agent_name} 执行)")
context.run.add_event(TaskLifecycleEvent(task_name=task_name, action="start"))
try:
result = await handler()
logger.debug(f"✅ **任务完成**: `{task_name}`")
context.run.add_event(
TaskLifecycleEvent(task_name=task_name, action="complete")
)
return result
except asyncio.CancelledError as e:
logger.warning(f"⚠️ **任务被强制取消**: `{task_name}`")
context.run.add_event(
TaskLifecycleEvent(
task_name=task_name,
action="fail",
error_msg="任务执行被中止或取消",
)
)
raise e
except BaseException as error:
event_error = (
error if isinstance(error, Exception) else Exception(str(error))
)
logger.error(f"❌ **任务失败**: `{task_name}` - {event_error}")
context.run.add_event(
TaskLifecycleEvent(
task_name=task_name,
action="fail",
error_msg=str(event_error),
)
)
raise error
@@ -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,
)
+178
View File
@@ -0,0 +1,178 @@
"""
Agent 相关静态声明类型定义
"""
from collections.abc import Sequence
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.core.messages import (
AgentMessage,
ChatResponse,
LLMMessage,
ToolCallPart,
UsageInfo,
)
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.flow.base import BaseRuntimeConfig
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.engine.registry import ToolCollection
from zhenxun.services.ai.tools.models import GlobalToolFilter
from zhenxun.utils.pydantic_compat import model_copy
class Persona(BaseModel):
"""智能体人设与上下文背景"""
role: str = Field(...)
"""扮演的角色身份"""
goal: str = Field(...)
"""角色的核心目标"""
backstory: str | None = Field(default=None)
"""角色背景故事或性格设定"""
model_config = ConfigDict(extra="ignore") # type: ignore
class AgentConfig(BaseRuntimeConfig):
"""统一的智能体全局与单次运行配置 (Unification of Settings & Profile)"""
model_config = ConfigDict(arbitrary_types_allowed=True)
max_cycles: int = Field(default=10)
"""工具调用最大循环次数"""
global_max_cycles: int | None = Field(default=None)
"""整个会话生命周期内的绝对最大循环次数上限(覆盖全局配置)。"""
enable_parallel_calls: bool = Field(default=True)
"""允许并行工具调用"""
reflexion_retries: int = Field(default=1)
"""反思重试次数"""
enable_fallback_summary: bool = Field(default=True)
"""达到最大循环次数时,是否触发大模型兜底总结(而不是直接报错)"""
enable_hitl: bool | None = Field(default=None)
"""是否允许智能体主动挂起任务,向用户求助 (Human-in-the-Loop)。
若为 None 则跟随全局设置。
"""
message_history: Sequence[AgentMessage] | None = Field(default=None)
"""初始化的底层对话历史记录。"""
tool_filter: GlobalToolFilter | None = Field(default=None)
"""全局工具过滤器,限制本次运行可用的工具池。"""
memory: Any | None = Field(default=None)
"""单次运行级别的记忆门面覆盖 (支持 bool, MemoryConfig, MemoryBuilder)。"""
generation_config: GenerationConfig | None = Field(default=None)
"""单次运行覆盖的大模型生成配置。"""
capabilities: list[Any] | None = Field(default=None)
"""仅针对本次运行动态注入的临时拦截器/能力组件列表。"""
skills: Sequence[Any] | None = Field(default=None)
"""仅针对本次运行动态注入的临时技能集合。"""
executor: Any | None = Field(default=None)
"""单次运行覆盖的核心执行引擎策略 (BaseAgentExecutor)。"""
verbose_ui: bool = Field(default=False)
"""是否在 UI 前端展示细粒度的工具执行中间过程。
在不支持流式更新的平台(如QQ)建议保持 False。"""
def merge_with(self, other: "AgentConfig | dict | None") -> "AgentConfig":
"""深度合并另一份配置,生成一个新的覆盖实例"""
if not other:
return model_copy(self, deep=True)
update_dict = {}
if isinstance(other, dict):
other_dict = {k: v for k, v in other.items() if v is not None}
else:
fields_set = getattr(
other, "model_fields_set", getattr(other, "__fields_set__", set())
)
other_dict = {}
for k in fields_set:
val = getattr(other, k)
if val is not None:
other_dict[k] = val
for k, v in other_dict.items():
if k in ("capabilities", "skills") and isinstance(v, list):
base_list = getattr(self, k) or []
update_dict[k] = base_list + v
else:
update_dict[k] = v
return model_copy(self, update=update_dict, deep=True)
class AgentState(BaseModel):
"""大模型思考循环的有限状态机 (FSM) 流转状态"""
model_config = ConfigDict(arbitrary_types_allowed=True)
static_system_prompt: str | list[str] = ""
"""绝对不变的系统提示词(用于前缀缓存)"""
dynamic_system_messages: list[LLMMessage] = Field(default_factory=list)
"""包含变量与实时状态的动态独立提示消息列表(绝对头部注入)"""
tools: ToolCollection | None = None
"""当前轮次生效的、已完成鉴权和过滤的工具集合"""
messages: list[AgentMessage] = Field(default_factory=list)
"""大模型将看到的完整历史消息列表 (执行历史)"""
usage: UsageInfo = Field(default_factory=UsageInfo)
"""累计的 Token 消耗"""
structured_result: Any | None = None
"""拦截到的结构化输出结果"""
early_result_output: Any | None = None
"""拦截到的早期终止输出结果"""
should_terminate: bool = False
"""标记是否应提前终止循环"""
handoff_triggered: Any | None = None
"""标记是否触发了移交"""
is_finished: bool = False
"""标记大模型循环是否彻底结束"""
final_result: Any | None = None
"""最终的运行结果 (AgentRunResult)"""
origin_msg_len: int = 0
"""初始进入循环时的消息历史长度 (用于增量保存记忆)"""
current_cycle: int = 0
"""当前思考循环的轮次索引"""
current_request_messages: list[AgentMessage] = Field(default_factory=list)
"""当前即将发往大模型的实际请求消息"""
current_request_extra: dict[str, Any] = Field(default_factory=dict)
"""当前请求附加的Extra控制参数"""
current_response: ChatResponse | None = None
"""大模型最新返回的响应实体"""
current_tool_calls: list[ToolCallPart] = Field(default_factory=list)
"""当前轮次被提取出准备执行的客户端工具调用"""
current_tool_results: list[Any] = Field(default_factory=list)
"""当前轮次工具执行的结果或异常收集"""
class AgentRunResources(BaseModel):
"""大模型执行过程中的全局静态资源与配置载体"""
model_config = ConfigDict(arbitrary_types_allowed=True)
run_context: RunContext
"""保留依赖注入(DI)与黑板引用的全局运行时上下文"""
session_meta: SessionMetadata | None = None
"""隔离会话的元信息(Session ID, 命名空间, 权限等)"""
memory_reader: Any | None = None
"""用于读取短/中/长期上下文记忆的读取器"""
memory_writer: Any | None = None
"""用于将对话历史安全落盘的写入器"""
run_scoped_cap: Any | None = None
"""聚合了 Agent/Task/全局 的复合能力拦截器 (CombinedCapability)"""
task_obj: Any | None = None
"""(如有) 解析后的结构化数据任务契约"""
toolkits: list[Any] = Field(default_factory=list)
"""当前轮次生效的工具箱列表 (需要执行生命周期挂载)"""
config: AgentConfig = Field(default_factory=AgentConfig)
"""Agent 全局与运行时的统一策略配置"""
generation_config: GenerationConfig | None = None
"""大模型生成配置"""
model_name: str | None = None
"""当前实际调用的模型名称"""