mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-28 16:20:56 +08:00
♻️ refactor(agent): 重构 Agent 状态管理与执行器流程,优化 Token 预估与自愈反思机制 (#2150)
- 统一使用 `run_context.run.messages` 作为消息历史的单一数据源,清理 `AgentState` 冗余字段 - 将工具消息装配逻辑 `assemble_tool_message` 提取并重构至 `ToolExecutor` - 引入 `token_drift` 动态校准偏移量,并精确计算工具与系统提示词的 Token 开销 - 重构 `ReflexionCapability` 自愈反思引擎,基于异常多态与模板字典动态生成反馈提示词 - 支持通过 `resolve_model_capabilities` 解析并合并用户自定义的模型能力覆盖 - 在执行器循环中支持 `should_reset_cycle`,以优雅处理外部干预(如用户追加指示) - 扩展 `capabilities` 中对 `gpt-[5-9]*` 等新型号模型的能力定义与上下文限制 Co-authored-by: webjoin111 <455457521@qq.com>
This commit is contained in:
@@ -2,18 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from typing import Any, cast
|
from typing import Any, ClassVar, cast
|
||||||
|
|
||||||
from zhenxun.models.user_console import UserConsole
|
from zhenxun.models.user_console import UserConsole
|
||||||
from zhenxun.services.ai.config import get_llm_config
|
from zhenxun.services.ai.config import get_llm_config
|
||||||
from zhenxun.services.ai.core.exceptions import (
|
from zhenxun.services.ai.core.exceptions import (
|
||||||
AbortException,
|
AbortException,
|
||||||
ControlFlowExit,
|
ControlFlowExit,
|
||||||
GuardrailViolationError,
|
|
||||||
LLMException,
|
LLMException,
|
||||||
ModelRetry,
|
ModelRetry,
|
||||||
ResponseParseException,
|
ResponseParseException,
|
||||||
SchemaParseError,
|
|
||||||
ToolFatalError,
|
ToolFatalError,
|
||||||
ToolFinishException,
|
ToolFinishException,
|
||||||
ToolRetryError,
|
ToolRetryError,
|
||||||
@@ -279,18 +277,52 @@ class ReflexionCapability(AbstractCapability):
|
|||||||
"""自愈反思与验证引擎 (Reflexion Engine)。
|
"""自愈反思与验证引擎 (Reflexion Engine)。
|
||||||
统一处理结构化解析失败和语义护栏拦截。"""
|
统一处理结构化解析失败和语义护栏拦截。"""
|
||||||
|
|
||||||
|
_PROMPT_TEMPLATES: ClassVar[dict[str, str]] = {
|
||||||
|
"schema_validation_error": (
|
||||||
|
"### ⚠️ [数据内容校验失败]\n"
|
||||||
|
"你输出的 JSON 格式完全正确,但部分字段的内容未能通过业务规则约束。\n\n"
|
||||||
|
"**失败详情:**\n"
|
||||||
|
"> {error_msg}\n\n"
|
||||||
|
"**修正要求:** 请仔细阅读上述失败详情,你必须打破先前的部分指令限制以满足上述规则," # noqa: E501
|
||||||
|
"调整报错字段的值并重新输出."
|
||||||
|
),
|
||||||
|
"schema_parse_error": (
|
||||||
|
"### ❌ [格式解析失败]\n"
|
||||||
|
"你输出的结构化数据(JSON)格式损坏或字段类型不匹配,未能通过校验。\n\n"
|
||||||
|
"**解析错误报告:**\n"
|
||||||
|
"> {error_msg}\n\n"
|
||||||
|
"**修正要求:** 请仔细检查缺失的必填字段、错误的数据类型或未闭合的括号,"
|
||||||
|
"严格参考你可用的 Schema 定义,重新输出正确格式的数据。"
|
||||||
|
),
|
||||||
|
"guardrail_violation": (
|
||||||
|
"### 🛡️ [业务护栏违规]\n"
|
||||||
|
"你输出的数据格式完全正确,但在业务逻辑层触发了合规/风控护栏。\n\n"
|
||||||
|
"**拦截原因报告:**\n"
|
||||||
|
"> {error_msg}\n\n"
|
||||||
|
"**修正要求:** 请结合上述反馈报告,反思你的决策逻辑或内容生成,"
|
||||||
|
"在保持数据格式正确的前提下,重新生成符合护栏规范的内容。"
|
||||||
|
),
|
||||||
|
"default_retry": (
|
||||||
|
"### ❌ [输出内容或格式验证失败]\n"
|
||||||
|
"你的上一次输出未能通过系统的校验与规则检查。请立即启动修正流程:\n\n"
|
||||||
|
"**错误反馈报告:**\n"
|
||||||
|
"> {error_msg}\n\n"
|
||||||
|
"**修正要求:** 请结合反馈报告,仔细反思你的输出内容或格式,"
|
||||||
|
"并重新生成正确的数据以满足所有的规则与规范。"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
async def wrap_tool_execute(self, context, tool_name, arguments, handler):
|
async def wrap_tool_execute(self, context, tool_name, arguments, handler):
|
||||||
try:
|
try:
|
||||||
return await handler(arguments)
|
return await handler(arguments)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
from zhenxun.services.ai.core.engine.structured_parser import (
|
|
||||||
DEFAULT_IVR_TEMPLATE,
|
|
||||||
)
|
|
||||||
from zhenxun.services.ai.tools.models import ToolResult
|
from zhenxun.services.ai.tools.models import ToolResult
|
||||||
|
|
||||||
if isinstance(error, ToolRetryError | ModelRetry):
|
if isinstance(error, ToolRetryError | ModelRetry):
|
||||||
error_msg = getattr(error, "message", str(error))
|
error_msg = getattr(error, "message", str(error))
|
||||||
feedback_prompt = DEFAULT_IVR_TEMPLATE.format(error_msg=error_msg)
|
feedback_prompt = self._PROMPT_TEMPLATES["default_retry"].format(
|
||||||
|
error_msg=error_msg
|
||||||
|
)
|
||||||
context.run.add_system_prompt(feedback_prompt)
|
context.run.add_system_prompt(feedback_prompt)
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
output=f"执行失败:{error_msg}",
|
output=f"执行失败:{error_msg}",
|
||||||
@@ -337,41 +369,22 @@ class ReflexionCapability(AbstractCapability):
|
|||||||
self, e: Exception, error_msg: str, error_template: str | None
|
self, e: Exception, error_msg: str, error_template: str | None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
根据不同的异常类型生成针对性的自愈反思提示词 (Feedback Prompt)
|
基于异常多态与模板字典生成自愈反思提示词
|
||||||
"""
|
"""
|
||||||
if isinstance(e, SchemaParseError):
|
if error_template:
|
||||||
if "数据内容未通过规则校验" in error_msg:
|
return error_template.format(error_msg=error_msg)
|
||||||
return f"""### ⚠️ [数据内容校验失败]
|
|
||||||
你输出的 JSON 格式完全正确,但部分字段的内容未能通过业务规则约束。
|
|
||||||
|
|
||||||
**失败详情:**
|
template_name = (
|
||||||
> {error_msg}
|
e.get_template_name() if isinstance(e, ModelRetry) else "default_retry"
|
||||||
|
)
|
||||||
|
|
||||||
**修正要求:** 请仔细阅读上述失败详情,你必须打破先前的部分指令限制以满足上述规则,调整报错字段的值并重新输出。""" # noqa: E501
|
template = self._PROMPT_TEMPLATES.get(
|
||||||
else:
|
template_name, self._PROMPT_TEMPLATES["default_retry"]
|
||||||
return f"""### ❌ [格式解析失败]
|
)
|
||||||
你输出的结构化数据(JSON)格式损坏或字段类型不匹配,未能通过校验。
|
context_data = e.get_feedback_context() if isinstance(e, ModelRetry) else {}
|
||||||
|
context_data["error_msg"] = error_msg
|
||||||
|
|
||||||
**解析错误报告:**
|
return template.format(**context_data)
|
||||||
> {error_msg}
|
|
||||||
|
|
||||||
**修正要求:** 请仔细检查缺失的必填字段、错误的数据类型或未闭合的括号,严格参考你可用的 Schema 定义,重新输出正确格式的数据。""" # noqa: E501
|
|
||||||
elif isinstance(e, GuardrailViolationError):
|
|
||||||
return f"""### 🛡️ [业务护栏违规]
|
|
||||||
你输出的数据格式完全正确,但在业务逻辑层触发了合规/风控护栏。
|
|
||||||
|
|
||||||
**拦截原因报告:**
|
|
||||||
> {error_msg}
|
|
||||||
|
|
||||||
**修正要求:** 请结合上述反馈报告,反思你的决策逻辑或内容生成,在保持数据格式正确的前提下,重新生成符合护栏规范的内容。""" # noqa: E501
|
|
||||||
else:
|
|
||||||
if error_template:
|
|
||||||
return error_template.format(error_msg=error_msg)
|
|
||||||
from zhenxun.services.ai.core.engine.structured_parser import (
|
|
||||||
DEFAULT_IVR_TEMPLATE,
|
|
||||||
)
|
|
||||||
|
|
||||||
return DEFAULT_IVR_TEMPLATE.format(error_msg=error_msg)
|
|
||||||
|
|
||||||
async def wrap_model_request(
|
async def wrap_model_request(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import Any, Generic, TypeVar
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from zhenxun.services.ai.config import get_llm_config
|
||||||
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
||||||
from zhenxun.services.ai.core.engine.token_counter import token_counter
|
from zhenxun.services.ai.core.engine.token_counter import token_counter
|
||||||
from zhenxun.services.ai.core.messages import (
|
from zhenxun.services.ai.core.messages import (
|
||||||
@@ -15,7 +16,10 @@ from zhenxun.services.ai.core.messages import (
|
|||||||
TextPart,
|
TextPart,
|
||||||
VideoPart,
|
VideoPart,
|
||||||
)
|
)
|
||||||
|
from zhenxun.services.ai.core.models import ModelCapabilities
|
||||||
|
from zhenxun.services.ai.llm.api import chat, generate_structured
|
||||||
from zhenxun.services.ai.llm.manager import get_default_model
|
from zhenxun.services.ai.llm.manager import get_default_model
|
||||||
|
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
|
||||||
from zhenxun.services.ai.utils.logger import log_memory as logger
|
from zhenxun.services.ai.utils.logger import log_memory as logger
|
||||||
from zhenxun.utils.pydantic_compat import model_copy
|
from zhenxun.utils.pydantic_compat import model_copy
|
||||||
|
|
||||||
@@ -396,8 +400,6 @@ class LLMSummarizerReducer(AbstractSummarizerReducer):
|
|||||||
prompt_text += f"[{speaker}]: {c_str}\n"
|
prompt_text += f"[{speaker}]: {c_str}\n"
|
||||||
prompt_text += "</需要合并的旧对话记录>\n"
|
prompt_text += "</需要合并的旧对话记录>\n"
|
||||||
|
|
||||||
from zhenxun.services.ai.llm.api import chat
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_to_use = self.summarization_model or get_default_model("chat")
|
model_to_use = self.summarization_model or get_default_model("chat")
|
||||||
response = await chat(
|
response = await chat(
|
||||||
@@ -474,8 +476,6 @@ class StructuredSummaryReducer(AbstractSummarizerReducer, Generic[_T_Summary]):
|
|||||||
prev_summary=prev_summary, dialogue=dialogue_text
|
prev_summary=prev_summary, dialogue=dialogue_text
|
||||||
)
|
)
|
||||||
|
|
||||||
from zhenxun.services.ai.llm.api import generate_structured
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_to_use = self.summarization_model or get_default_model("chat")
|
model_to_use = self.summarization_model or get_default_model("chat")
|
||||||
summary_obj = await generate_structured(
|
summary_obj = await generate_structured(
|
||||||
@@ -513,15 +513,19 @@ class CondenserPipeline:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_configs(
|
def create_from_configs(
|
||||||
cls, memory_config: MemoryConfig | None, model_name: str
|
cls,
|
||||||
|
memory_config: MemoryConfig | None,
|
||||||
|
capabilities: ModelCapabilities | None,
|
||||||
|
model_name: str,
|
||||||
) -> "CondenserPipeline":
|
) -> "CondenserPipeline":
|
||||||
"""基于全局和局部配置组装压缩管线工厂方法"""
|
"""基于全局和局部配置组装压缩管线工厂方法"""
|
||||||
from zhenxun.services.ai.config import get_llm_config
|
|
||||||
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
|
|
||||||
|
|
||||||
config = get_llm_config().context_settings
|
config = get_llm_config().context_settings
|
||||||
pipeline_reducers = []
|
pipeline_reducers = []
|
||||||
caps = get_model_capabilities(model_name)
|
caps = (
|
||||||
|
capabilities
|
||||||
|
if capabilities is not None
|
||||||
|
else get_model_capabilities(model_name)
|
||||||
|
)
|
||||||
|
|
||||||
vw = config.vision_window_size
|
vw = config.vision_window_size
|
||||||
if memory_config and memory_config.compression.vision_window is not None:
|
if memory_config and memory_config.compression.vision_window is not None:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import cast
|
|||||||
|
|
||||||
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
|
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
|
||||||
from zhenxun.services.ai.core.messages import AgentMessage
|
from zhenxun.services.ai.core.messages import AgentMessage
|
||||||
|
from zhenxun.services.ai.core.models import ModelCapabilities
|
||||||
from zhenxun.services.ai.run.context import RunContext
|
from zhenxun.services.ai.run.context import RunContext
|
||||||
from zhenxun.services.ai.utils.logger import log_memory as logger
|
from zhenxun.services.ai.utils.logger import log_memory as logger
|
||||||
from zhenxun.utils.pydantic_compat import model_copy
|
from zhenxun.utils.pydantic_compat import model_copy
|
||||||
@@ -42,7 +43,9 @@ class SessionMemoryContext:
|
|||||||
async def read(
|
async def read(
|
||||||
self,
|
self,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
|
capabilities: ModelCapabilities | None = None,
|
||||||
override_history: Sequence[AgentMessage] | None = None,
|
override_history: Sequence[AgentMessage] | None = None,
|
||||||
|
base_overhead: int = 0,
|
||||||
) -> list[AgentMessage]:
|
) -> list[AgentMessage]:
|
||||||
"""
|
"""
|
||||||
拉取短期对话历史,并执行 Token 压缩与管线修剪。
|
拉取短期对话历史,并执行 Token 压缩与管线修剪。
|
||||||
@@ -69,14 +72,16 @@ class SessionMemoryContext:
|
|||||||
)
|
)
|
||||||
|
|
||||||
pipeline = CondenserPipeline.create_from_configs(
|
pipeline = CondenserPipeline.create_from_configs(
|
||||||
self.memory_config, model_name
|
self.memory_config, capabilities, model_name
|
||||||
)
|
)
|
||||||
if pipeline.reducers:
|
if pipeline.reducers:
|
||||||
flattened_to_reduce = ContextConverter.flatten_to_llm_messages(
|
flattened_to_reduce = ContextConverter.flatten_to_llm_messages(
|
||||||
current_history
|
current_history
|
||||||
)
|
)
|
||||||
new_history, changed = await pipeline.run(
|
new_history, changed = await pipeline.run(
|
||||||
flattened_to_reduce, model_name=model_name, base_overhead=0
|
flattened_to_reduce,
|
||||||
|
model_name=model_name,
|
||||||
|
base_overhead=base_overhead,
|
||||||
)
|
)
|
||||||
if changed:
|
if changed:
|
||||||
await chat_context.set_messages(self.session_meta, new_history)
|
await chat_context.set_messages(self.session_meta, new_history)
|
||||||
|
|||||||
@@ -8,21 +8,12 @@ from pydantic import BaseModel, Field, ValidationError, create_model
|
|||||||
|
|
||||||
from zhenxun.services.ai.core.exceptions import (
|
from zhenxun.services.ai.core.exceptions import (
|
||||||
SchemaParseError,
|
SchemaParseError,
|
||||||
|
SchemaValidationError,
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.core.messages.types import OutputDataT
|
from zhenxun.services.ai.core.messages.types import OutputDataT
|
||||||
from zhenxun.services.ai.utils.logger import log_core as logger
|
from zhenxun.services.ai.utils.logger import log_core as logger
|
||||||
from zhenxun.utils.pydantic_compat import model_json_schema, model_validate
|
from zhenxun.utils.pydantic_compat import model_json_schema, model_validate
|
||||||
|
|
||||||
DEFAULT_IVR_TEMPLATE = (
|
|
||||||
"### ❌ [输出内容或格式验证失败]\n"
|
|
||||||
"你的上一次输出未能通过系统的校验与规则检查。请立即启动修正流程:\n\n"
|
|
||||||
"**错误反馈报告:**\n"
|
|
||||||
"> {error_msg}\n\n"
|
|
||||||
"**修正要求:** 请结合反馈报告,"
|
|
||||||
"仔细反思你的输出内容或格式,\n"
|
|
||||||
"并重新生成正确的数据以满足所有的规则与规范。"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseOutputProcessor(Generic[OutputDataT]):
|
class BaseOutputProcessor(Generic[OutputDataT]):
|
||||||
"""
|
"""
|
||||||
@@ -47,7 +38,7 @@ class BaseOutputProcessor(Generic[OutputDataT]):
|
|||||||
如果不为 None 则跳过根据 response_model 生成,默认 None。
|
如果不为 None 则跳过根据 response_model 生成,默认 None。
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
self.original_model = response_model
|
self.original_model = response_model
|
||||||
self.error_template = error_template or DEFAULT_IVR_TEMPLATE
|
self.error_template = error_template
|
||||||
self.raw_schema = raw_schema
|
self.raw_schema = raw_schema
|
||||||
self.target_model = None
|
self.target_model = None
|
||||||
self.is_union_wrapped = False
|
self.is_union_wrapped = False
|
||||||
@@ -120,7 +111,7 @@ class BaseOutputProcessor(Generic[OutputDataT]):
|
|||||||
msg = err.get("msg", "")
|
msg = err.get("msg", "")
|
||||||
error_msgs.append(f"字段 `{loc}`: {msg}")
|
error_msgs.append(f"字段 `{loc}`: {msg}")
|
||||||
clean_error_str = "\n".join(error_msgs)
|
clean_error_str = "\n".join(error_msgs)
|
||||||
raise SchemaParseError(
|
raise SchemaValidationError(
|
||||||
f"数据内容未通过规则校验:\n{clean_error_str}"
|
f"数据内容未通过规则校验:\n{clean_error_str}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ LLM Token 动态预估与上下文管理模块
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -56,17 +57,11 @@ class TokenCounter:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def count_tools_schema(cls, obj: dict | list | str | Any) -> int:
|
def count_tools_schema(cls, obj: dict | list | str | Any) -> int:
|
||||||
"""递归计算 JSON Schema 结构在被大模型作为工具时的 Token 开销。"""
|
"""递归计算 JSON Schema 结构在被大模型作为工具时的 Token 开销。"""
|
||||||
if isinstance(obj, dict):
|
try:
|
||||||
cost = len(obj.keys()) * 12
|
json_str = json.dumps(obj, ensure_ascii=False)
|
||||||
for k, v in obj.items():
|
return int(cls._count_text(json_str) * 1.2) + 15
|
||||||
if k == "description" and isinstance(v, str):
|
except Exception:
|
||||||
cost += int(len(v) * 0.3)
|
return 50
|
||||||
else:
|
|
||||||
cost += cls.count_tools_schema(v)
|
|
||||||
return cost
|
|
||||||
elif isinstance(obj, list):
|
|
||||||
return sum(cls.count_tools_schema(item) for item in obj)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def count_message(cls, msg: LLMMessage, model_name: str) -> int:
|
def count_message(cls, msg: LLMMessage, model_name: str) -> int:
|
||||||
|
|||||||
@@ -8,41 +8,70 @@ from typing import Any
|
|||||||
class ModelRetry(Exception):
|
class ModelRetry(Exception):
|
||||||
"""用于通知大模型修正并重试的异常"""
|
"""用于通知大模型修正并重试的异常"""
|
||||||
|
|
||||||
def __init__(self, message: str):
|
def __init__(self, message: str, payload: dict[str, Any] | None = None):
|
||||||
"""
|
"""
|
||||||
初始化用于通知大模型重试的异常。
|
初始化用于通知大模型重试的异常。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
message: 用于提示大模型的具体重试和自我纠错信息。
|
message: 用于提示大模型的具体重试和自我纠错信息。
|
||||||
|
payload: 携带产生错误时的上下文状态字典,用于渲染反馈提示词。
|
||||||
"""
|
"""
|
||||||
self.message = message
|
self.message = message
|
||||||
super().__init__(message)
|
self.payload = payload or {}
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
def get_template_name(self) -> str:
|
||||||
|
"""多态:子类告诉渲染引擎,自己应该使用哪个 Prompt 模板标识"""
|
||||||
|
return "default_retry"
|
||||||
|
|
||||||
|
def get_feedback_context(self) -> dict[str, Any]:
|
||||||
|
"""多态:子类提供渲染模板所需的领域数据上下文"""
|
||||||
|
return {"error_msg": self.message, **self.payload}
|
||||||
|
|
||||||
|
|
||||||
class SchemaParseError(ModelRetry):
|
class SchemaParseError(ModelRetry):
|
||||||
"""格式解析异常。当大模型返回的 JSON 损坏或不符合 Schema 时抛出。"""
|
"""格式解析异常。当大模型返回的 JSON 损坏或不符合 Schema 时抛出。"""
|
||||||
|
|
||||||
def __init__(self, message: str):
|
def __init__(self, message: str, payload: dict[str, Any] | None = None):
|
||||||
"""
|
"""
|
||||||
初始化 Schema 格式解析错误异常。
|
初始化 Schema 格式解析错误异常。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
message: 详细的 JSON 解析失败或 Schema 校验报错信息。
|
message: 详细的 JSON 解析失败或 Schema 校验报错信息。
|
||||||
"""
|
"""
|
||||||
super().__init__(message)
|
super().__init__(message, payload)
|
||||||
|
|
||||||
|
def get_template_name(self) -> str:
|
||||||
|
return "schema_parse_error"
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaValidationError(ModelRetry):
|
||||||
|
"""数据校验异常。当大模型返回的 JSON 格式正确,但业务字段约束不满足时抛出。"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, payload: dict[str, Any] | None = None):
|
||||||
|
"""
|
||||||
|
初始化 Schema 业务字段约束验证错误异常。
|
||||||
|
"""
|
||||||
|
super().__init__(message, payload)
|
||||||
|
|
||||||
|
def get_template_name(self) -> str:
|
||||||
|
return "schema_validation_error"
|
||||||
|
|
||||||
|
|
||||||
class GuardrailViolationError(ModelRetry):
|
class GuardrailViolationError(ModelRetry):
|
||||||
"""护栏违规异常。当大模型返回的数据格式正确,但违反业务规则时抛出。"""
|
"""护栏违规异常。当大模型返回的数据格式正确,但违反业务规则时抛出。"""
|
||||||
|
|
||||||
def __init__(self, message: str):
|
def __init__(self, message: str, payload: dict[str, Any] | None = None):
|
||||||
"""
|
"""
|
||||||
初始化安全护栏校验未通过的异常。
|
初始化安全护栏校验未通过的异常。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
message: 触发业务护栏违规拦截的详细原因说明。
|
message: 触发业务护栏违规拦截的详细原因说明。
|
||||||
"""
|
"""
|
||||||
super().__init__(message)
|
super().__init__(message, payload)
|
||||||
|
|
||||||
|
def get_template_name(self) -> str:
|
||||||
|
return "guardrail_violation"
|
||||||
|
|
||||||
|
|
||||||
class ControlFlowExit(BaseException):
|
class ControlFlowExit(BaseException):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from zhenxun.services.ai.config import get_llm_config
|
|||||||
from zhenxun.services.ai.context.knowledge.base import BaseKnowledge
|
from zhenxun.services.ai.context.knowledge.base import BaseKnowledge
|
||||||
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
||||||
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
||||||
|
from zhenxun.services.ai.core.engine.token_counter import token_counter
|
||||||
from zhenxun.services.ai.core.exceptions import (
|
from zhenxun.services.ai.core.exceptions import (
|
||||||
ControlFlowExit,
|
ControlFlowExit,
|
||||||
)
|
)
|
||||||
@@ -29,6 +30,7 @@ from zhenxun.services.ai.flow.core.base import BaseRunnable
|
|||||||
from zhenxun.services.ai.flow.core.models import InterventionPolicy
|
from zhenxun.services.ai.flow.core.models import InterventionPolicy
|
||||||
from zhenxun.services.ai.guardrails import GuardrailSource, parse_guardrails
|
from zhenxun.services.ai.guardrails import GuardrailSource, parse_guardrails
|
||||||
from zhenxun.services.ai.llm.builder import IntentBuilder
|
from zhenxun.services.ai.llm.builder import IntentBuilder
|
||||||
|
from zhenxun.services.ai.llm.manager import resolve_model_capabilities
|
||||||
from zhenxun.services.ai.message_builder import MessageBuilder
|
from zhenxun.services.ai.message_builder import MessageBuilder
|
||||||
from zhenxun.services.ai.run import (
|
from zhenxun.services.ai.run import (
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
@@ -700,6 +702,10 @@ class Agent(
|
|||||||
self.model_name() if callable(self.model_name) else self.model_name
|
self.model_name() if callable(self.model_name) else self.model_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
resources.model_capabilities = await resolve_model_capabilities(
|
||||||
|
context.run.current_model
|
||||||
|
)
|
||||||
|
|
||||||
return state, resources
|
return state, resources
|
||||||
|
|
||||||
async def on_context_build(
|
async def on_context_build(
|
||||||
@@ -752,10 +758,29 @@ class Agent(
|
|||||||
if tool_payload.injected_prompts:
|
if tool_payload.injected_prompts:
|
||||||
static_prompts_list.extend(tool_payload.injected_prompts)
|
static_prompts_list.extend(tool_payload.injected_prompts)
|
||||||
|
|
||||||
|
final_tools = await ToolBuilder.prepare_effective_tools(
|
||||||
|
effective_tools, context, self.tool_filters, run_scoped_cap
|
||||||
|
)
|
||||||
|
|
||||||
|
base_overhead = 0
|
||||||
|
for sp in static_prompts_list:
|
||||||
|
if sp:
|
||||||
|
base_overhead += token_counter._count_text(str(sp))
|
||||||
|
for m in dynamic_messages:
|
||||||
|
base_overhead += token_counter.count_message(
|
||||||
|
m, context.run.current_model or ""
|
||||||
|
)
|
||||||
|
for t in final_tools:
|
||||||
|
t_def = getattr(t, "_dynamic_def", None)
|
||||||
|
if t_def and getattr(t_def, "parameters", None):
|
||||||
|
base_overhead += token_counter.count_tools_schema(t_def.parameters)
|
||||||
|
|
||||||
messages_for_run = (
|
messages_for_run = (
|
||||||
await memory_context.read(
|
await memory_context.read(
|
||||||
model_name=context.run.current_model or "",
|
model_name=context.run.current_model or "",
|
||||||
|
capabilities=resources.model_capabilities,
|
||||||
override_history=resources.config.message_history,
|
override_history=resources.config.message_history,
|
||||||
|
base_overhead=base_overhead,
|
||||||
)
|
)
|
||||||
if memory_context
|
if memory_context
|
||||||
else []
|
else []
|
||||||
@@ -772,17 +797,14 @@ class Agent(
|
|||||||
if resources.memory_context:
|
if resources.memory_context:
|
||||||
await resources.memory_context.write([msgs[-1]])
|
await resources.memory_context.write([msgs[-1]])
|
||||||
|
|
||||||
final_tools = await ToolBuilder.prepare_effective_tools(
|
|
||||||
effective_tools, context, self.tool_filters, run_scoped_cap
|
|
||||||
)
|
|
||||||
context.session.append_only_manager.build(static_prompts_list, final_tools)
|
context.session.append_only_manager.build(static_prompts_list, final_tools)
|
||||||
context.session.append_only_manager.sync_messages(messages_for_run)
|
context.session.append_only_manager.sync_messages(messages_for_run)
|
||||||
|
|
||||||
state.messages = messages_for_run
|
context.run.messages = messages_for_run
|
||||||
state.tools = final_tools
|
state.tools = final_tools
|
||||||
state.static_system_prompt = static_prompts_list
|
state.static_system_prompt = static_prompts_list
|
||||||
state.dynamic_system_messages = dynamic_messages
|
state.dynamic_system_messages = dynamic_messages
|
||||||
state.origin_msg_len = len(messages_for_run)
|
state._origin_msg_len = len(messages_for_run)
|
||||||
|
|
||||||
async def on_execute(
|
async def on_execute(
|
||||||
self, state: AgentState, resources: AgentRunResources
|
self, state: AgentState, resources: AgentRunResources
|
||||||
@@ -793,7 +815,7 @@ class Agent(
|
|||||||
for tk in resources.toolkits:
|
for tk in resources.toolkits:
|
||||||
if hasattr(tk, "before_llm_request"):
|
if hasattr(tk, "before_llm_request"):
|
||||||
await DependencyInjector.invoke(
|
await DependencyInjector.invoke(
|
||||||
tk.before_llm_request, {"messages": state.messages}, context
|
tk.before_llm_request, {"messages": context.run.messages}, context
|
||||||
)
|
)
|
||||||
|
|
||||||
config_exec = resources.config.executor if resources.config else None
|
config_exec = resources.config.executor if resources.config else None
|
||||||
@@ -802,12 +824,11 @@ class Agent(
|
|||||||
or self.executor
|
or self.executor
|
||||||
or StandardAgentExecutor(directive_handlers=self.directive_handlers)
|
or StandardAgentExecutor(directive_handlers=self.directive_handlers)
|
||||||
)
|
)
|
||||||
resources.model_name = context.run.current_model
|
|
||||||
raw_result: AgentRunResult[Any] = await executor.run(
|
raw_result: AgentRunResult[Any] = await executor.run(
|
||||||
state=state, resources=resources
|
state=state, resources=resources
|
||||||
)
|
)
|
||||||
|
|
||||||
new_msgs = raw_result.messages[state.origin_msg_len :]
|
new_msgs = raw_result.messages[state._origin_msg_len :]
|
||||||
if resources.memory_context:
|
if resources.memory_context:
|
||||||
await resources.memory_context.write(new_msgs)
|
await resources.memory_context.write(new_msgs)
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ async def handle_submit_structured(
|
|||||||
)
|
)
|
||||||
logger.debug("✅ 拦截到结构化结果提交,结束循环。")
|
logger.debug("✅ 拦截到结构化结果提交,结束循环。")
|
||||||
state.is_finished = True
|
state.is_finished = True
|
||||||
state.final_result = model_construct(
|
state.pending_result = model_construct(
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
output=None,
|
output=None,
|
||||||
messages=state.messages,
|
messages=list(resources.run_context.run.messages),
|
||||||
structured_data=parsed_obj,
|
structured_data=parsed_obj,
|
||||||
usage=state.usage,
|
usage=state.usage,
|
||||||
)
|
)
|
||||||
@@ -85,10 +85,10 @@ async def handle_end_run(
|
|||||||
)
|
)
|
||||||
logger.debug("✅ 捕获到工具发出的终止信号,提前结束推理循环。")
|
logger.debug("✅ 捕获到工具发出的终止信号,提前结束推理循环。")
|
||||||
state.is_finished = True
|
state.is_finished = True
|
||||||
state.final_result = model_construct(
|
state.pending_result = model_construct(
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
output=output,
|
output=output,
|
||||||
messages=state.messages,
|
messages=list(resources.run_context.run.messages),
|
||||||
usage=state.usage,
|
usage=state.usage,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,10 +106,10 @@ async def handle_handoff(
|
|||||||
output_text = f"已触发控制权移交 -> {handoff.target}。原因: {handoff.reason}"
|
output_text = f"已触发控制权移交 -> {handoff.target}。原因: {handoff.reason}"
|
||||||
logger.info(f"✅ 拦截到移交(Handoff)信号: 移交给 -> {handoff.target}。结束循环。")
|
logger.info(f"✅ 拦截到移交(Handoff)信号: 移交给 -> {handoff.target}。结束循环。")
|
||||||
state.is_finished = True
|
state.is_finished = True
|
||||||
state.final_result = model_construct(
|
state.pending_result = model_construct(
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
output=output_text,
|
output=output_text,
|
||||||
messages=state.messages,
|
messages=list(resources.run_context.run.messages),
|
||||||
usage=state.usage,
|
usage=state.usage,
|
||||||
handoff=handoff,
|
handoff=handoff,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Iterable
|
||||||
import json
|
from typing import Any, cast
|
||||||
from typing import Any, Literal, cast
|
|
||||||
|
|
||||||
from zhenxun.services.ai.capabilities import CombinedCapability
|
from zhenxun.services.ai.capabilities import CombinedCapability
|
||||||
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
|
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
|
||||||
@@ -11,22 +10,15 @@ from zhenxun.services.ai.core.engine.token_counter import (
|
|||||||
token_counter,
|
token_counter,
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.core.exceptions import (
|
from zhenxun.services.ai.core.exceptions import (
|
||||||
ControlFlowExit,
|
|
||||||
UpstreamServerException,
|
UpstreamServerException,
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.core.messages import (
|
from zhenxun.services.ai.core.messages import (
|
||||||
AgentMessage,
|
AgentMessage,
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
AudioPart,
|
|
||||||
ChatRequest,
|
ChatRequest,
|
||||||
ChatResponse,
|
ChatResponse,
|
||||||
FilePart,
|
|
||||||
ImagePart,
|
|
||||||
LLMMessage,
|
LLMMessage,
|
||||||
TextPart,
|
|
||||||
ToolCallPart,
|
|
||||||
ToolReturnPart,
|
ToolReturnPart,
|
||||||
VideoPart,
|
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.core.models import CancellationToken, LLMContext, ToolChoice
|
from zhenxun.services.ai.core.models import CancellationToken, LLMContext, ToolChoice
|
||||||
from zhenxun.services.ai.core.options import GenerationConfig
|
from zhenxun.services.ai.core.options import GenerationConfig
|
||||||
@@ -40,11 +32,11 @@ from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
|
|||||||
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
|
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
|
||||||
from zhenxun.services.ai.run import AgentRunResult, RunContext
|
from zhenxun.services.ai.run import AgentRunResult, RunContext
|
||||||
from zhenxun.services.ai.run.models import OutputDataT
|
from zhenxun.services.ai.run.models import OutputDataT
|
||||||
from zhenxun.services.ai.run.session import SessionInfo, session_manager
|
from zhenxun.services.ai.run.session import session_manager
|
||||||
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
|
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
|
||||||
from zhenxun.services.ai.tools.models import ToolResult
|
from zhenxun.services.ai.tools.models import ToolResult
|
||||||
from zhenxun.services.ai.utils.logger import log_agent as logger
|
from zhenxun.services.ai.utils.logger import log_agent as logger
|
||||||
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
|
from zhenxun.utils.pydantic_compat import model_construct
|
||||||
|
|
||||||
from .directive import (
|
from .directive import (
|
||||||
DirectiveHandlerFunc,
|
DirectiveHandlerFunc,
|
||||||
@@ -68,29 +60,42 @@ class BaseAgentExecutor(ABC):
|
|||||||
await self.on_start(state, resources)
|
await self.on_start(state, resources)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for cycle_index in range(resources.config.max_cycles):
|
cycle_count = 0
|
||||||
state.current_cycle = cycle_index
|
while cycle_count < resources.config.max_cycles:
|
||||||
|
state.current_cycle = cycle_count
|
||||||
|
state.should_reset_cycle = False
|
||||||
await self.on_cycle_start(state, resources)
|
await self.on_cycle_start(state, resources)
|
||||||
|
|
||||||
await self.build_llm_request(state, resources)
|
await self.build_llm_request(state, resources)
|
||||||
await self.execute_llm(state, resources)
|
await self.execute_llm(state, resources)
|
||||||
|
|
||||||
await self.handle_llm_response(state, resources)
|
await self.handle_llm_response(state, resources)
|
||||||
|
if state.should_reset_cycle:
|
||||||
|
cycle_count = 0
|
||||||
|
continue
|
||||||
if state.is_finished:
|
if state.is_finished:
|
||||||
assert state.final_result is not None
|
assert state.pending_result is not None
|
||||||
return state.final_result
|
return state.pending_result
|
||||||
|
|
||||||
await self.filter_tool_calls(state, resources)
|
await self.filter_tool_calls(state, resources)
|
||||||
|
if state.should_reset_cycle:
|
||||||
|
cycle_count = 0
|
||||||
|
continue
|
||||||
if state.is_finished:
|
if state.is_finished:
|
||||||
assert state.final_result is not None
|
assert state.pending_result is not None
|
||||||
return state.final_result
|
return state.pending_result
|
||||||
|
|
||||||
await self.execute_tools(state, resources)
|
await self.execute_tools(state, resources)
|
||||||
|
|
||||||
await self.handle_tool_results(state, resources)
|
await self.handle_tool_results(state, resources)
|
||||||
|
if state.should_reset_cycle:
|
||||||
|
cycle_count = 0
|
||||||
|
continue
|
||||||
if state.is_finished:
|
if state.is_finished:
|
||||||
assert state.final_result is not None
|
assert state.pending_result is not None
|
||||||
return state.final_result
|
return state.pending_result
|
||||||
|
|
||||||
|
cycle_count += 1
|
||||||
|
|
||||||
return await self.on_fallback(state, resources)
|
return await self.on_fallback(state, resources)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -179,23 +184,45 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
directive_handlers or {}
|
directive_handlers or {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calculate_tool_overhead(tools: Iterable[Any] | None) -> int:
|
||||||
|
"""辅助方法:计算工具集合的 Schema Token 开销"""
|
||||||
|
if not tools:
|
||||||
|
return 0
|
||||||
|
overhead = 0
|
||||||
|
for t in tools:
|
||||||
|
if isinstance(t, dict) and "function" in t:
|
||||||
|
overhead += token_counter.count_tools_schema(
|
||||||
|
t["function"].get("parameters", {})
|
||||||
|
)
|
||||||
|
elif hasattr(t, "get_definition"):
|
||||||
|
t_def = getattr(t, "_dynamic_def", None)
|
||||||
|
if t_def and getattr(t_def, "parameters", None):
|
||||||
|
overhead += token_counter.count_tools_schema(t_def.parameters)
|
||||||
|
return overhead
|
||||||
|
|
||||||
def _can_retry_via_llm(self, result: ToolResult) -> bool:
|
def _can_retry_via_llm(self, result: ToolResult) -> bool:
|
||||||
"""通过新版的专属字段直接判断是否允许重试"""
|
"""通过新版的专属字段直接判断是否允许重试"""
|
||||||
return result.is_retryable
|
return result.is_retryable
|
||||||
|
|
||||||
def _check_follow_up(
|
async def _check_follow_up(
|
||||||
self, state: AgentState, resources: AgentRunResources, session_info: SessionInfo
|
self, state: AgentState, resources: AgentRunResources
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""检查追加队列,排空并合并数据到上下文,返回是否发现新消息"""
|
"""检查追加队列,排空并合并数据到上下文,更新状态机标志位"""
|
||||||
|
session_id = resources.run_context.session_id or "default_session"
|
||||||
|
session_info = await session_manager.get_or_create(session_id)
|
||||||
follow_ups = session_info.follow_up_queue.drain()
|
follow_ups = session_info.follow_up_queue.drain()
|
||||||
if follow_ups:
|
if follow_ups:
|
||||||
for fm in follow_ups:
|
for fm in follow_ups:
|
||||||
state.messages.append(LLMMessage.user(f"💬 [用户追加指示]:{fm}"))
|
resources.run_context.run.messages.append(
|
||||||
|
LLMMessage.user(f"💬 [用户追加指示]:{fm}")
|
||||||
|
)
|
||||||
resources.run_context.session.append_only_manager.sync_messages(
|
resources.run_context.session.append_only_manager.sync_messages(
|
||||||
state.messages
|
resources.run_context.run.messages
|
||||||
)
|
)
|
||||||
state.is_finished = False
|
state.is_finished = False
|
||||||
state.final_result = None
|
state.pending_result = None
|
||||||
|
state.should_reset_cycle = True
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -224,13 +251,13 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
|
|
||||||
await run_context.run.emit(
|
await run_context.run.emit(
|
||||||
LLMStartEvent(
|
LLMStartEvent(
|
||||||
model_name=resources.model_name or "unknown",
|
model_name=resources.run_context.run.current_model or "unknown",
|
||||||
messages=flattened_messages,
|
messages=flattened_messages,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await self._execute_model_request(
|
response = await self._execute_model_request(
|
||||||
model_name=resources.model_name,
|
model_name=resources.run_context.run.current_model,
|
||||||
messages=flattened_messages,
|
messages=flattened_messages,
|
||||||
config=resources.generation_config or GenerationConfig(),
|
config=resources.generation_config or GenerationConfig(),
|
||||||
run_context=run_context,
|
run_context=run_context,
|
||||||
@@ -266,8 +293,16 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
if usage_obj.completion_tokens > 0:
|
if usage_obj.completion_tokens > 0:
|
||||||
assistant_message.token_cost = usage_obj.completion_tokens
|
assistant_message.token_cost = usage_obj.completion_tokens
|
||||||
|
|
||||||
state.messages.append(assistant_message)
|
if usage_obj.prompt_tokens > 0:
|
||||||
run_context.session.append_only_manager.sync_messages(state.messages)
|
est_prompt_tokens = token_counter.count_context(
|
||||||
|
messages, resources.run_context.run.current_model or "", base_overhead=0
|
||||||
|
)
|
||||||
|
tool_overhead = self._calculate_tool_overhead(tools)
|
||||||
|
est_total = est_prompt_tokens + tool_overhead
|
||||||
|
state.token_drift = usage_obj.prompt_tokens - est_total
|
||||||
|
|
||||||
|
run_context.run.messages.append(assistant_message)
|
||||||
|
run_context.session.append_only_manager.sync_messages(run_context.run.messages)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -308,97 +343,53 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
|
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
|
||||||
resources.run_context.run.messages = state.messages
|
pass
|
||||||
|
|
||||||
async def _execute_phase(
|
|
||||||
self,
|
|
||||||
phase_func: Callable[[AgentState, AgentRunResources], Awaitable[None]],
|
|
||||||
state: AgentState,
|
|
||||||
resources: AgentRunResources,
|
|
||||||
session_info: SessionInfo,
|
|
||||||
) -> Literal["NEXT", "RESET_CYCLE", "FINISH"]:
|
|
||||||
"""统一处理阶段执行与状态机完成/追加检查的高阶函数"""
|
|
||||||
await phase_func(state, resources)
|
|
||||||
if state.is_finished:
|
|
||||||
if self._check_follow_up(state, resources, session_info):
|
|
||||||
return "RESET_CYCLE"
|
|
||||||
return "FINISH"
|
|
||||||
return "NEXT"
|
|
||||||
|
|
||||||
async def run(
|
|
||||||
self, state: AgentState, resources: AgentRunResources
|
|
||||||
) -> AgentRunResult[OutputDataT]:
|
|
||||||
"""覆盖基类的模板方法,实现灵活的 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)
|
|
||||||
|
|
||||||
status = await self._execute_phase(
|
|
||||||
self.handle_llm_response, state, resources, session_info
|
|
||||||
)
|
|
||||||
if status == "RESET_CYCLE":
|
|
||||||
cycle_count = 0
|
|
||||||
continue
|
|
||||||
if status == "FINISH":
|
|
||||||
assert state.final_result is not None
|
|
||||||
return state.final_result
|
|
||||||
|
|
||||||
status = await self._execute_phase(
|
|
||||||
self.filter_tool_calls, state, resources, session_info
|
|
||||||
)
|
|
||||||
if status == "RESET_CYCLE":
|
|
||||||
cycle_count = 0
|
|
||||||
continue
|
|
||||||
if status == "FINISH":
|
|
||||||
assert state.final_result is not None
|
|
||||||
return state.final_result
|
|
||||||
|
|
||||||
await self.execute_tools(state, resources)
|
|
||||||
|
|
||||||
status = await self._execute_phase(
|
|
||||||
self.handle_tool_results, state, resources, session_info
|
|
||||||
)
|
|
||||||
if status == "RESET_CYCLE":
|
|
||||||
cycle_count = 0
|
|
||||||
continue
|
|
||||||
if status == "FINISH":
|
|
||||||
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(
|
async def on_cycle_start(
|
||||||
self, state: AgentState, resources: AgentRunResources
|
self, state: AgentState, resources: AgentRunResources
|
||||||
) -> None:
|
) -> None:
|
||||||
|
state.current_request_messages = []
|
||||||
|
state.current_response = None
|
||||||
|
state.current_tool_calls = []
|
||||||
|
state.current_tool_results = []
|
||||||
|
state.should_reset_cycle = False
|
||||||
|
|
||||||
cancellation_token = resources.run_context.run.cancellation_token
|
cancellation_token = resources.run_context.run.cancellation_token
|
||||||
if cancellation_token:
|
if cancellation_token:
|
||||||
cancellation_token.raise_if_cancelled()
|
cancellation_token.raise_if_cancelled()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
est_tokens = token_counter.count_context(
|
base_overhead = 0
|
||||||
state.messages, resources.model_name or "", base_overhead=0
|
if state.static_system_prompt:
|
||||||
|
sp_list = (
|
||||||
|
state.static_system_prompt
|
||||||
|
if isinstance(state.static_system_prompt, list)
|
||||||
|
else [state.static_system_prompt]
|
||||||
|
)
|
||||||
|
for sp in sp_list:
|
||||||
|
if sp:
|
||||||
|
base_overhead += token_counter._count_text(str(sp))
|
||||||
|
for m in state.dynamic_system_messages:
|
||||||
|
base_overhead += token_counter.count_message(
|
||||||
|
m, resources.run_context.run.current_model or ""
|
||||||
|
)
|
||||||
|
base_overhead += self._calculate_tool_overhead(state.tools)
|
||||||
|
|
||||||
|
est_tokens = (
|
||||||
|
token_counter.count_context(
|
||||||
|
resources.run_context.run.messages,
|
||||||
|
resources.run_context.run.current_model or "",
|
||||||
|
base_overhead=base_overhead,
|
||||||
|
)
|
||||||
|
+ state.token_drift
|
||||||
)
|
)
|
||||||
|
|
||||||
|
est_tokens = max(est_tokens, 0)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"(Iter {state.current_cycle + 1}) "
|
f"(Iter {state.current_cycle + 1}) "
|
||||||
f"预估将消耗 {est_tokens} Token "
|
f"预估将消耗 {est_tokens} Token "
|
||||||
f"(Model: {resources.model_name or 'Unknown'})"
|
f"(Model: {resources.run_context.run.current_model or 'Unknown'})"
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -413,8 +404,12 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
steer_msgs = session_info.steer_queue.drain()
|
steer_msgs = session_info.steer_queue.drain()
|
||||||
if steer_msgs:
|
if steer_msgs:
|
||||||
for sm in steer_msgs:
|
for sm in steer_msgs:
|
||||||
state.messages.append(LLMMessage.user(f"💬 [用户实时修正指示]:{sm}"))
|
run_context.run.messages.append(
|
||||||
run_context.session.append_only_manager.sync_messages(state.messages)
|
LLMMessage.user(f"💬 [用户实时修正指示]:{sm}")
|
||||||
|
)
|
||||||
|
run_context.session.append_only_manager.sync_messages(
|
||||||
|
run_context.run.messages
|
||||||
|
)
|
||||||
|
|
||||||
messages_to_send = []
|
messages_to_send = []
|
||||||
if state.static_system_prompt:
|
if state.static_system_prompt:
|
||||||
@@ -439,7 +434,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
if prompt_text and prompt_text.strip():
|
if prompt_text and prompt_text.strip():
|
||||||
messages_to_send.append(LLMMessage.system(prompt_text))
|
messages_to_send.append(LLMMessage.system(prompt_text))
|
||||||
|
|
||||||
messages_to_send.extend(state.messages)
|
messages_to_send.extend(run_context.run.messages)
|
||||||
|
|
||||||
state.current_request_messages = messages_to_send
|
state.current_request_messages = messages_to_send
|
||||||
|
|
||||||
@@ -466,13 +461,16 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
if not response.tool_calls:
|
if not response.tool_calls:
|
||||||
logger.debug("✅ 模型未请求工具调用,推理循环结束。")
|
logger.debug("✅ 模型未请求工具调用,推理循环结束。")
|
||||||
state.is_finished = True
|
state.is_finished = True
|
||||||
state.final_result = model_construct(
|
state.pending_result = model_construct(
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
output=response.text,
|
output=response.text,
|
||||||
messages=state.messages,
|
messages=list(resources.run_context.run.messages),
|
||||||
usage=state.usage,
|
usage=state.usage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if state.is_finished:
|
||||||
|
await self._check_follow_up(state, resources)
|
||||||
|
|
||||||
async def filter_tool_calls(
|
async def filter_tool_calls(
|
||||||
self, state: AgentState, resources: AgentRunResources
|
self, state: AgentState, resources: AgentRunResources
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -522,14 +520,16 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
logger.info("✅ 无本地客户端工具需执行,推理循环平滑结束。")
|
logger.info("✅ 无本地客户端工具需执行,推理循环平滑结束。")
|
||||||
|
|
||||||
state.is_finished = True
|
state.is_finished = True
|
||||||
state.final_result = model_construct(
|
state.pending_result = model_construct(
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
output=response.text,
|
output=response.text,
|
||||||
messages=state.messages,
|
messages=list(resources.run_context.run.messages),
|
||||||
usage=state.usage,
|
usage=state.usage,
|
||||||
)
|
)
|
||||||
|
|
||||||
state.current_tool_calls = client_tool_calls
|
state.current_tool_calls = client_tool_calls
|
||||||
|
if state.is_finished:
|
||||||
|
await self._check_follow_up(state, resources)
|
||||||
|
|
||||||
async def execute_tools(
|
async def execute_tools(
|
||||||
self, state: AgentState, resources: AgentRunResources
|
self, state: AgentState, resources: AgentRunResources
|
||||||
@@ -565,51 +565,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
tool_results = await asyncio.gather(*exec_tasks, return_exceptions=True)
|
tool_results = await asyncio.gather(*exec_tasks, return_exceptions=True)
|
||||||
state.current_tool_results = tool_results
|
state.current_tool_results = tool_results
|
||||||
|
|
||||||
def _assemble_tool_message(
|
|
||||||
self,
|
|
||||||
original_call: ToolCallPart,
|
|
||||||
res_or_exc: BaseException | tuple[ToolCallPart, ToolResult],
|
|
||||||
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(
|
async def handle_tool_results(
|
||||||
self, state: AgentState, resources: AgentRunResources
|
self, state: AgentState, resources: AgentRunResources
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -627,10 +582,12 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
_, raw_tool_res = res_or_exc
|
_, raw_tool_res = res_or_exc
|
||||||
tool_res = raw_tool_res
|
tool_res = raw_tool_res
|
||||||
|
|
||||||
msg = self._assemble_tool_message(
|
msg, usage = ToolExecutor.assemble_tool_message(
|
||||||
original_call, res_or_exc, tool_res, state
|
original_call, res_or_exc, tool_res
|
||||||
)
|
)
|
||||||
state.messages.append(msg)
|
if usage:
|
||||||
|
state.usage += usage
|
||||||
|
resources.run_context.run.messages.append(msg)
|
||||||
|
|
||||||
if tool_res and getattr(tool_res, "directive", None):
|
if tool_res and getattr(tool_res, "directive", None):
|
||||||
ns = getattr(resources.run_context.session, "namespace", "global")
|
ns = getattr(resources.run_context.session, "namespace", "global")
|
||||||
@@ -642,8 +599,10 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
await handler(state, resources, tool_res)
|
await handler(state, resources, tool_res)
|
||||||
if state.is_finished:
|
if state.is_finished:
|
||||||
resources.run_context.session.append_only_manager.sync_messages(
|
resources.run_context.session.append_only_manager.sync_messages(
|
||||||
state.messages
|
resources.run_context.run.messages
|
||||||
)
|
)
|
||||||
|
if state.is_finished:
|
||||||
|
await self._check_follow_up(state, resources)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -651,7 +610,11 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
f"的指令处理器 (Namespace: {ns})"
|
f"的指令处理器 (Namespace: {ns})"
|
||||||
)
|
)
|
||||||
|
|
||||||
resources.run_context.session.append_only_manager.sync_messages(state.messages)
|
resources.run_context.session.append_only_manager.sync_messages(
|
||||||
|
resources.run_context.run.messages
|
||||||
|
)
|
||||||
|
if state.is_finished:
|
||||||
|
await self._check_follow_up(state, resources)
|
||||||
|
|
||||||
async def on_fallback(
|
async def on_fallback(
|
||||||
self, state: AgentState, resources: AgentRunResources
|
self, state: AgentState, resources: AgentRunResources
|
||||||
@@ -680,12 +643,12 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
"请**诚实地**向用户总结:你目前进行到了哪一步?遇到了什么困难导致循环耗尽?还有哪些预期步骤未能完成?\n"
|
"请**诚实地**向用户总结:你目前进行到了哪一步?遇到了什么困难导致循环耗尽?还有哪些预期步骤未能完成?\n"
|
||||||
"**绝对禁止**对用户撒谎声声称你已经完成了任务。严禁再次尝试调用任何工具!请直接输出纯文本结果。"
|
"**绝对禁止**对用户撒谎声声称你已经完成了任务。严禁再次尝试调用任何工具!请直接输出纯文本结果。"
|
||||||
)
|
)
|
||||||
state.messages.append(fallback_msg)
|
run_context.run.messages.append(fallback_msg)
|
||||||
|
|
||||||
fallback_response = await self._invoke_and_record_llm(
|
fallback_response = await self._invoke_and_record_llm(
|
||||||
state=state,
|
state=state,
|
||||||
resources=resources,
|
resources=resources,
|
||||||
messages=state.messages,
|
messages=run_context.run.messages,
|
||||||
tools=[],
|
tools=[],
|
||||||
tool_choice="none",
|
tool_choice="none",
|
||||||
)
|
)
|
||||||
@@ -695,7 +658,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
|
|||||||
model_construct(
|
model_construct(
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
output=fallback_response.text,
|
output=fallback_response.text,
|
||||||
messages=state.messages,
|
messages=list(run_context.run.messages),
|
||||||
structured_data=None,
|
structured_data=None,
|
||||||
usage=state.usage,
|
usage=state.usage,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from collections.abc import Sequence
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
||||||
|
|
||||||
from zhenxun.services.ai.capabilities import CapabilitySource, CombinedCapability
|
from zhenxun.services.ai.capabilities import CapabilitySource, CombinedCapability
|
||||||
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
||||||
@@ -18,10 +18,11 @@ from zhenxun.services.ai.core.messages import (
|
|||||||
ToolCallPart,
|
ToolCallPart,
|
||||||
UsageInfo,
|
UsageInfo,
|
||||||
)
|
)
|
||||||
|
from zhenxun.services.ai.core.models import ModelCapabilities
|
||||||
from zhenxun.services.ai.core.options import GenerationConfig
|
from zhenxun.services.ai.core.options import GenerationConfig
|
||||||
from zhenxun.services.ai.flow.core.models import BaseRuntimeConfig
|
from zhenxun.services.ai.flow.core.models import BaseRuntimeConfig
|
||||||
from zhenxun.services.ai.run import RunContext
|
from zhenxun.services.ai.run import RunContext
|
||||||
from zhenxun.services.ai.run.models import AgentRunResult, AgentTask, HandoffPayload
|
from zhenxun.services.ai.run.models import AgentRunResult, AgentTask
|
||||||
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
|
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
|
||||||
from zhenxun.services.ai.tools.engine.registry import ToolCollection
|
from zhenxun.services.ai.tools.engine.registry import ToolCollection
|
||||||
from zhenxun.utils.pydantic_compat import model_copy
|
from zhenxun.utils.pydantic_compat import model_copy
|
||||||
@@ -121,27 +122,15 @@ class AgentState(BaseModel):
|
|||||||
tools: ToolCollection | None = None
|
tools: ToolCollection | None = None
|
||||||
"""当前轮次生效的、已完成鉴权和过滤的工具集合"""
|
"""当前轮次生效的、已完成鉴权和过滤的工具集合"""
|
||||||
|
|
||||||
messages: list[AgentMessage] = Field(default_factory=list)
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
"""大模型将看到的完整历史消息列表 (执行历史)"""
|
"""供第三方开发者或生命周期钩子使用的自定义插槽,
|
||||||
usage: UsageInfo = Field(default_factory=UsageInfo)
|
在单个 Agent FSM 循环中存取临时变量"""
|
||||||
"""累计的 Token 消耗"""
|
|
||||||
structured_result: Any | None = None
|
_origin_msg_len: int = PrivateAttr(default=0)
|
||||||
"""拦截到的结构化输出结果"""
|
"""内部变量:初始进入循环时的消息历史长度 (用于增量保存记忆,防止被外部业务篡改)"""
|
||||||
early_result_output: Any | None = None
|
|
||||||
"""拦截到的早期终止输出结果"""
|
|
||||||
should_terminate: bool = False
|
|
||||||
"""标记是否应提前终止循环"""
|
|
||||||
handoff_triggered: HandoffPayload | None = None
|
|
||||||
"""标记是否触发了移交"""
|
|
||||||
is_finished: bool = False
|
|
||||||
"""标记大模型循环是否彻底结束"""
|
|
||||||
final_result: AgentRunResult[Any] | None = None
|
|
||||||
"""最终的运行结果 (AgentRunResult)"""
|
|
||||||
origin_msg_len: int = 0
|
|
||||||
"""初始进入循环时的消息历史长度 (用于增量保存记忆)"""
|
|
||||||
current_cycle: int = 0
|
current_cycle: int = 0
|
||||||
"""当前思考循环的轮次索引"""
|
"""当前思考循环的轮次索引"""
|
||||||
|
|
||||||
current_request_messages: list[AgentMessage] = Field(default_factory=list)
|
current_request_messages: list[AgentMessage] = Field(default_factory=list)
|
||||||
"""当前即将发往大模型的实际请求消息"""
|
"""当前即将发往大模型的实际请求消息"""
|
||||||
current_request_extra: dict[str, Any] = Field(default_factory=dict)
|
current_request_extra: dict[str, Any] = Field(default_factory=dict)
|
||||||
@@ -153,6 +142,17 @@ class AgentState(BaseModel):
|
|||||||
current_tool_results: list[Any] = Field(default_factory=list)
|
current_tool_results: list[Any] = Field(default_factory=list)
|
||||||
"""当前轮次工具执行的结果或异常收集"""
|
"""当前轮次工具执行的结果或异常收集"""
|
||||||
|
|
||||||
|
should_reset_cycle: bool = False
|
||||||
|
"""标记是否需要重置当前思考循环(例如由于外部干预插入了新消息)"""
|
||||||
|
is_finished: bool = False
|
||||||
|
"""标记大模型循环是否彻底结束"""
|
||||||
|
pending_result: AgentRunResult[Any] | None = None
|
||||||
|
"""单一数据源:等待返回的最终运行结果 (SSOT)"""
|
||||||
|
usage: UsageInfo = Field(default_factory=UsageInfo)
|
||||||
|
"""累计的 Token 消耗"""
|
||||||
|
token_drift: int = 0
|
||||||
|
"""动态 Token 校准偏移量 (真实 - 预估)"""
|
||||||
|
|
||||||
|
|
||||||
class AgentRunResources(BaseModel):
|
class AgentRunResources(BaseModel):
|
||||||
"""大模型执行过程中的全局静态资源与配置载体"""
|
"""大模型执行过程中的全局静态资源与配置载体"""
|
||||||
@@ -175,5 +175,5 @@ class AgentRunResources(BaseModel):
|
|||||||
"""Agent 全局与运行时的统一策略配置"""
|
"""Agent 全局与运行时的统一策略配置"""
|
||||||
generation_config: GenerationConfig | None = None
|
generation_config: GenerationConfig | None = None
|
||||||
"""大模型生成配置"""
|
"""大模型生成配置"""
|
||||||
model_name: str | None = None
|
model_capabilities: ModelCapabilities | None = None
|
||||||
"""当前实际调用的模型名称"""
|
"""当前底层大模型的能力配置(合并了用户自定义覆盖)"""
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ from zhenxun.services.ai.config import (
|
|||||||
get_llm_config,
|
get_llm_config,
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.core.exceptions import ConfigurationException
|
from zhenxun.services.ai.core.exceptions import ConfigurationException
|
||||||
from zhenxun.services.ai.core.models import ModelDetail
|
from zhenxun.services.ai.core.models import ModelCapabilities, ModelDetail
|
||||||
from zhenxun.services.ai.core.options import GenerationConfig
|
from zhenxun.services.ai.core.options import GenerationConfig
|
||||||
from zhenxun.services.ai.utils.logger import log_llm as logger
|
from zhenxun.services.ai.utils.logger import log_llm as logger
|
||||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||||
from zhenxun.utils.pydantic_compat import model_dump
|
from zhenxun.utils.pydantic_compat import model_copy, model_dump
|
||||||
|
|
||||||
from .system.cache import clear_model_cache, get_or_create_model
|
from .system.cache import clear_model_cache, get_or_create_model
|
||||||
from .system.capabilities import get_model_capabilities
|
from .system.capabilities import get_model_capabilities
|
||||||
@@ -191,6 +191,40 @@ def get_default_model(task: str = "chat") -> str | None:
|
|||||||
return getattr(config.default_models, task, None)
|
return getattr(config.default_models, task, None)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_model_capabilities(
|
||||||
|
provider_model_name: str | None = None, task: str = "chat"
|
||||||
|
) -> ModelCapabilities:
|
||||||
|
"""解析并合并带有用户自定义覆盖(如 max_input_tokens) 的模型能力。"""
|
||||||
|
resolved_name = provider_model_name
|
||||||
|
if resolved_name is None:
|
||||||
|
resolved_name = get_default_model(task)
|
||||||
|
if resolved_name is None:
|
||||||
|
avail = list_available_models()
|
||||||
|
if not avail:
|
||||||
|
return get_model_capabilities("unknown")
|
||||||
|
resolved_name = avail[0]["full_name"]
|
||||||
|
|
||||||
|
group_name = _get_group_name(resolved_name)
|
||||||
|
if group_name is not None:
|
||||||
|
model_names = _resolve_model_group(group_name)
|
||||||
|
if model_names:
|
||||||
|
resolved_name = model_names[0]
|
||||||
|
|
||||||
|
prov_name, mod_name = parse_provider_model_string(resolved_name)
|
||||||
|
caps = get_model_capabilities(mod_name or resolved_name)
|
||||||
|
|
||||||
|
if prov_name and mod_name:
|
||||||
|
config_tuple = find_model_config(prov_name, mod_name)
|
||||||
|
if config_tuple:
|
||||||
|
_, model_detail = config_tuple
|
||||||
|
if model_detail.max_input_tokens is not None:
|
||||||
|
caps = model_copy(
|
||||||
|
caps, update={"max_input_tokens": model_detail.max_input_tokens}
|
||||||
|
)
|
||||||
|
|
||||||
|
return caps
|
||||||
|
|
||||||
|
|
||||||
async def get_key_usage_stats() -> dict[str, Any]:
|
async def get_key_usage_stats() -> dict[str, Any]:
|
||||||
"""获取所有 Provider 的 Key 使用统计。"""
|
"""获取所有 Provider 的 Key 使用统计。"""
|
||||||
providers = get_configured_providers()
|
providers = get_configured_providers()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from zhenxun.services.ai.core.models import (
|
|||||||
)
|
)
|
||||||
from zhenxun.utils.pydantic_compat import model_copy
|
from zhenxun.utils.pydantic_compat import model_copy
|
||||||
|
|
||||||
|
CTX_1_05M = 1_050_000
|
||||||
CTX_1M = 1_000_000
|
CTX_1M = 1_000_000
|
||||||
CTX_400K = 400_000
|
CTX_400K = 400_000
|
||||||
CTX_256K = 256_000
|
CTX_256K = 256_000
|
||||||
@@ -283,7 +284,17 @@ _ROUTING_TABLE: list[tuple[list[str], ModelCapabilities, int]] = [
|
|||||||
(["*MiniMax-M3*"], CAP_MINIMAX_MULTIMODAL, CTX_1M),
|
(["*MiniMax-M3*"], CAP_MINIMAX_MULTIMODAL, CTX_1M),
|
||||||
(["mimo-v2.5-pro*", "mimo-v2-pro*", "mimo-v2-flash*"], CAP_MIMO_TEXT, CTX_1M),
|
(["mimo-v2.5-pro*", "mimo-v2-pro*", "mimo-v2-flash*"], CAP_MIMO_TEXT, CTX_1M),
|
||||||
(["mimo-v2.5", "mimo-v2-omni*"], CAP_MIMO_MULTIMODAL, CTX_1M),
|
(["mimo-v2.5", "mimo-v2-omni*"], CAP_MIMO_MULTIMODAL, CTX_1M),
|
||||||
(["gpt-5.5*", "gpt-5.4*"], CAP_OPENAI_MULTIMODAL, CTX_1M),
|
(
|
||||||
|
["gpt-5.3-chat*", "gpt-5.3-instant*", "*codex-spark*"],
|
||||||
|
CAP_OPENAI_MULTIMODAL,
|
||||||
|
CTX_128K,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
["gpt-[5-9]*mini*", "gpt-[5-9]*nano*", "gpt-[5-9]*-instant*", "*codex*"],
|
||||||
|
CAP_OPENAI_MULTIMODAL,
|
||||||
|
CTX_400K,
|
||||||
|
),
|
||||||
|
(["gpt-[5-9]*"], CAP_OPENAI_MULTIMODAL, CTX_1_05M),
|
||||||
(["gemini-3*pro*"], CAP_GEMINI_3_PRO, CTX_1M),
|
(["gemini-3*pro*"], CAP_GEMINI_3_PRO, CTX_1M),
|
||||||
(["gemini-3*"], CAP_GEMINI_3_FLASH, CTX_1M),
|
(["gemini-3*"], CAP_GEMINI_3_FLASH, CTX_1M),
|
||||||
(
|
(
|
||||||
@@ -291,11 +302,6 @@ _ROUTING_TABLE: list[tuple[list[str], ModelCapabilities, int]] = [
|
|||||||
CAP_GEMINI_2_5,
|
CAP_GEMINI_2_5,
|
||||||
CTX_1M,
|
CTX_1M,
|
||||||
),
|
),
|
||||||
(
|
|
||||||
["gpt-5*", "gpt-5-mini*", "gpt-5-nano*", "*codex*"],
|
|
||||||
CAP_OPENAI_MULTIMODAL,
|
|
||||||
CTX_400K,
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
["kimi-k2.7*", "kimi-k2.6*", "kimi-k2.5*"],
|
["kimi-k2.7*", "kimi-k2.6*", "kimi-k2.5*"],
|
||||||
DEFAULT_PERMISSIVE_CAPABILITIES,
|
DEFAULT_PERMISSIVE_CAPABILITIES,
|
||||||
|
|||||||
@@ -17,7 +17,18 @@ from zhenxun.services.ai.core.exceptions import (
|
|||||||
ToolFatalError,
|
ToolFatalError,
|
||||||
ToolRetryError,
|
ToolRetryError,
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.core.messages import AnyLLMMessage, LLMMessage, ToolCallPart
|
from zhenxun.services.ai.core.messages import (
|
||||||
|
AnyLLMMessage,
|
||||||
|
AudioPart,
|
||||||
|
FilePart,
|
||||||
|
ImagePart,
|
||||||
|
LLMMessage,
|
||||||
|
TextPart,
|
||||||
|
ToolCallPart,
|
||||||
|
ToolMessage,
|
||||||
|
UsageInfo,
|
||||||
|
VideoPart,
|
||||||
|
)
|
||||||
from zhenxun.services.ai.core.stream_events import (
|
from zhenxun.services.ai.core.stream_events import (
|
||||||
EventBus,
|
EventBus,
|
||||||
ToolCallEndEvent,
|
ToolCallEndEvent,
|
||||||
@@ -37,6 +48,7 @@ from zhenxun.services.ai.tools.models import (
|
|||||||
ValidatedToolCall,
|
ValidatedToolCall,
|
||||||
)
|
)
|
||||||
from zhenxun.services.ai.utils.logger import log_tool as logger
|
from zhenxun.services.ai.utils.logger import log_tool as logger
|
||||||
|
from zhenxun.utils.pydantic_compat import dump_json_safely
|
||||||
|
|
||||||
from .registry import ToolCollection
|
from .registry import ToolCollection
|
||||||
|
|
||||||
@@ -51,6 +63,49 @@ class ToolExecutor:
|
|||||||
"""初始化工具执行器。"""
|
"""初始化工具执行器。"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assemble_tool_message(
|
||||||
|
original_call: ToolCallPart,
|
||||||
|
res_or_exc: BaseException | tuple[ToolCallPart, ToolResult],
|
||||||
|
tool_res: ToolResult | None,
|
||||||
|
) -> tuple[ToolMessage, UsageInfo | None]:
|
||||||
|
"""负责处理异常、解析多模态、序列化,并装配为最终的工具消息载体"""
|
||||||
|
media_parts = []
|
||||||
|
final_content = "Success"
|
||||||
|
usage = None
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
usage = getattr(tool_res, "usage", None)
|
||||||
|
|
||||||
|
msg = LLMMessage.tool_response(
|
||||||
|
original_call.id, original_call.tool_name, final_content
|
||||||
|
)
|
||||||
|
if media_parts:
|
||||||
|
msg.content.extend(media_parts)
|
||||||
|
return msg, usage
|
||||||
|
|
||||||
def _get_combined_capability(
|
def _get_combined_capability(
|
||||||
self, executable: Any, context: RunContext
|
self, executable: Any, context: RunContext
|
||||||
) -> CombinedCapability:
|
) -> CombinedCapability:
|
||||||
@@ -396,15 +451,15 @@ class ToolExecutor:
|
|||||||
ToolResult(output=f"Crash: {result_pair}").as_error(),
|
ToolResult(output=f"Crash: {result_pair}").as_error(),
|
||||||
)
|
)
|
||||||
|
|
||||||
tool_call_result = cast(tuple[ToolCallPart, ToolResult], result_pair)
|
tool_res = None
|
||||||
_, tool_result = tool_call_result
|
if not isinstance(result_pair, BaseException):
|
||||||
tool_messages.append(
|
_, raw_tool_res = cast(tuple[ToolCallPart, ToolResult], result_pair)
|
||||||
LLMMessage.tool_response(
|
tool_res = raw_tool_res
|
||||||
tool_call_id=original_call.id,
|
|
||||||
function_name=func_name,
|
msg, _ = ToolExecutor.assemble_tool_message(
|
||||||
result=tool_result.output,
|
original_call, result_pair, tool_res
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
tool_messages.append(msg)
|
||||||
return tool_messages
|
return tool_messages
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user