mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-19 11:10:10 +08:00
✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)
* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 - 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片 - 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式 - 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统 - 【新增】引入基于 Docker 的安全代码执行沙箱环境 - 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务 - 【新增】引入输入输出安全合规护栏与自愈反思机制 - 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等) - 【优化】优化日志脱敏与 Token 预估机制 - 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令 * 🔧 chore(deps): 更新项目依赖与配置 - 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt - 在 pyright 配置中设置 reportMissingImports 为 none - 调整 .gitignore 中 resources 目录的忽略规则 * ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出 - 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束 - 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用 - 调整指令处理器(Directive)的返回值为 `tool_res.output` - 修复部分类型检查警告并优化联合类型判断语法 * ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制 - 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用 - 移除通过 `context.state` 传递魔术变量的隐式控制流设计 - 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult` - 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令 - 补全 `MessageBuilder` 中部分核心方法的文档注释 * 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑 -【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值 -【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题 -【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求 -【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建 -【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常 * 📝 docs(core): 补充核心模块初始化方法的文档注释 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
消息与响应域类型定义 - 统一导出门面
|
||||
"""
|
||||
|
||||
from nonebot.compat import PYDANTIC_V2
|
||||
|
||||
from .context_events import (
|
||||
AgentEvent,
|
||||
HandoffEvent,
|
||||
TaskLifecycleEvent,
|
||||
)
|
||||
from .models import (
|
||||
AssistantMessage,
|
||||
LLMMessage,
|
||||
SystemMessage,
|
||||
ToolMessage,
|
||||
UserMessage,
|
||||
)
|
||||
from .parts import (
|
||||
AudioPart,
|
||||
BaseContentPart,
|
||||
EmbedBatch,
|
||||
EmbedPayload,
|
||||
FilePart,
|
||||
ImagePart,
|
||||
LLMContentPart,
|
||||
TextDeltaPart,
|
||||
TextPart,
|
||||
ThoughtDeltaPart,
|
||||
ThoughtPart,
|
||||
ToolCallDeltaPart,
|
||||
ToolCallPart,
|
||||
ToolReturnPart,
|
||||
VideoPart,
|
||||
)
|
||||
from .requests import (
|
||||
BaseRequest,
|
||||
ChatRequest,
|
||||
EmbeddingRequest,
|
||||
ImageRequest,
|
||||
RerankRequest,
|
||||
SpeechRequest,
|
||||
)
|
||||
from .responses import (
|
||||
AudioResponse,
|
||||
ChatResponse,
|
||||
EmbeddingResponse,
|
||||
ImageResponse,
|
||||
RerankResponse,
|
||||
)
|
||||
from .shared import (
|
||||
LLMCodeExecution,
|
||||
LLMGroundingAttribution,
|
||||
LLMGroundingMetadata,
|
||||
RerankDocument,
|
||||
RerankResult,
|
||||
UsageInfo,
|
||||
)
|
||||
from .types import (
|
||||
AgentMessage,
|
||||
AnyLLMMessage,
|
||||
AssistantContentUnion,
|
||||
ContentT,
|
||||
PromptInput,
|
||||
RoleT,
|
||||
SystemContentUnion,
|
||||
ToolContentUnion,
|
||||
UserContentUnion,
|
||||
)
|
||||
|
||||
if PYDANTIC_V2:
|
||||
ChatResponse.model_rebuild()
|
||||
LLMMessage.model_rebuild()
|
||||
SystemMessage.model_rebuild()
|
||||
UserMessage.model_rebuild()
|
||||
AssistantMessage.model_rebuild()
|
||||
ToolMessage.model_rebuild()
|
||||
RerankResponse.model_rebuild()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentEvent",
|
||||
"AgentMessage",
|
||||
"AnyLLMMessage",
|
||||
"AssistantContentUnion",
|
||||
"AssistantMessage",
|
||||
"AudioPart",
|
||||
"AudioResponse",
|
||||
"BaseContentPart",
|
||||
"BaseRequest",
|
||||
"ChatRequest",
|
||||
"ChatResponse",
|
||||
"ContentT",
|
||||
"EmbedBatch",
|
||||
"EmbedPayload",
|
||||
"EmbeddingRequest",
|
||||
"EmbeddingResponse",
|
||||
"FilePart",
|
||||
"HandoffEvent",
|
||||
"ImagePart",
|
||||
"ImageRequest",
|
||||
"ImageResponse",
|
||||
"LLMCodeExecution",
|
||||
"LLMContentPart",
|
||||
"LLMGroundingAttribution",
|
||||
"LLMGroundingMetadata",
|
||||
"LLMMessage",
|
||||
"PromptInput",
|
||||
"RerankDocument",
|
||||
"RerankRequest",
|
||||
"RerankResponse",
|
||||
"RerankResult",
|
||||
"RoleT",
|
||||
"SpeechRequest",
|
||||
"SystemContentUnion",
|
||||
"SystemMessage",
|
||||
"TaskLifecycleEvent",
|
||||
"TextDeltaPart",
|
||||
"TextPart",
|
||||
"ThoughtDeltaPart",
|
||||
"ThoughtPart",
|
||||
"ToolCallDeltaPart",
|
||||
"ToolCallPart",
|
||||
"ToolContentUnion",
|
||||
"ToolMessage",
|
||||
"ToolReturnPart",
|
||||
"UsageInfo",
|
||||
"UserContentUnion",
|
||||
"UserMessage",
|
||||
"VideoPart",
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from .models import LLMMessage
|
||||
|
||||
|
||||
class AgentEvent(BaseModel):
|
||||
"""
|
||||
业务事件抽象基类/协议。
|
||||
支持作为一种特殊的消息,直接被混入到大模型的上下文(记忆)中。
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow") # type: ignore
|
||||
|
||||
def to_llm_message(
|
||||
self, context: Any | None = None
|
||||
) -> LLMMessage | list[LLMMessage] | str | None:
|
||||
"""
|
||||
将业务事件渲染为大模型能看懂的 API 原生消息。
|
||||
子类必须重写此方法。
|
||||
返回 None 代表此事件对大模型不可见(例如:纯后台打点或审计日志)。
|
||||
如果返回 str,系统将默认包装为 SystemMessage 发送给大模型。
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class TaskLifecycleEvent(AgentEvent):
|
||||
"""内置业务事件:任务状态打点追踪"""
|
||||
|
||||
task_name: str
|
||||
"""任务名称"""
|
||||
action: Literal["start", "complete", "fail"]
|
||||
"""任务状态动作,支持 "start"(开始)、"complete"(完成)、"fail"(失败)"""
|
||||
error_msg: str | None = None
|
||||
"""任务执行失败时的具体错误描述,可选"""
|
||||
|
||||
def to_llm_message(self, context: Any | None = None) -> str | None:
|
||||
if self.action == "start":
|
||||
return f"[任务生命周期] 开始执行任务:{self.task_name}"
|
||||
elif self.action == "complete":
|
||||
return f"[任务生命周期] 任务已完美达成:{self.task_name}"
|
||||
elif self.action == "fail":
|
||||
return (
|
||||
f"[任务生命周期] 任务执行失败:{self.task_name},原因:{self.error_msg}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class HandoffEvent(AgentEvent):
|
||||
"""内置业务事件:控制权移交记录"""
|
||||
|
||||
target: str
|
||||
"""目标接收节点或 Agent 的名称"""
|
||||
reason: str
|
||||
"""移交控制权的具体原因说明"""
|
||||
context_data: Any = None
|
||||
"""移交时附带的上下文数据,默认为 None"""
|
||||
|
||||
def to_llm_message(self, context: Any | None = None) -> str | None:
|
||||
return (
|
||||
f"[控制权移交] 任务及会话控制权已被系统转移至节点 "
|
||||
f"'{self.target}'。移交原因:{self.reason}"
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentEvent",
|
||||
"HandoffEvent",
|
||||
"TaskLifecycleEvent",
|
||||
]
|
||||
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
标准消息实体 - 依赖 parts.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import Sequence
|
||||
import time
|
||||
from typing import Annotated, Any, Generic, Literal, cast
|
||||
from typing_extensions import Self, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.utils.pydantic_compat import model_copy, model_dump, model_validator
|
||||
|
||||
from .parts import (
|
||||
BaseContentPart,
|
||||
FilePart,
|
||||
ImagePart,
|
||||
LLMContentPart,
|
||||
TextPart,
|
||||
ToolCallPart,
|
||||
ToolReturnPart,
|
||||
)
|
||||
|
||||
RoleT = TypeVar("RoleT", default=str, covariant=True)
|
||||
"""泛型:消息参与者角色类型变量"""
|
||||
|
||||
ContentT = TypeVar("ContentT", default=LLMContentPart, covariant=True)
|
||||
"""泛型:多模态片段数组的元素内容类型变量"""
|
||||
|
||||
|
||||
class LLMMessage(BaseModel, Generic[RoleT, ContentT]):
|
||||
"""
|
||||
LLM 消息基类与门面工厂。
|
||||
提供统一的元数据访问、魔法加法重载以及极简实例化方法。
|
||||
"""
|
||||
|
||||
role: RoleT
|
||||
"""消息参与者角色 (如 user, assistant, system, tool)"""
|
||||
content: list[ContentT] = Field(default_factory=list)
|
||||
"""容纳实际数据的多模态片段数组"""
|
||||
created_at: float = Field(default_factory=time.time)
|
||||
"""消息最初被构建的 Unix 时间戳"""
|
||||
metadata: dict[str, Any] | None = Field(default=None)
|
||||
"""自由存取字典,供系统内穿透传递额外状态数据"""
|
||||
|
||||
@property
|
||||
def tool_calls(self) -> list[ToolCallPart]:
|
||||
"""获取当前消息中包含的所有工具调用请求片段"""
|
||||
return [p for p in self.content if isinstance(p, ToolCallPart)]
|
||||
|
||||
@property
|
||||
def tool_returns(self) -> list[ToolReturnPart]:
|
||||
"""获取当前消息中包含的所有工具执行结果片段"""
|
||||
return [p for p in self.content if isinstance(p, ToolReturnPart)]
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_content(cls, data: Any) -> Any:
|
||||
"""核心拦截:外部传入 str 时自动转为 Part,保持内部类型绝对纯净"""
|
||||
if isinstance(data, dict):
|
||||
content = data.get("content")
|
||||
if isinstance(content, str):
|
||||
data["content"] = (
|
||||
[{"type": "text", "text": content}] if content.strip() else []
|
||||
)
|
||||
elif content is None:
|
||||
data["content"] = []
|
||||
elif isinstance(content, list):
|
||||
new_content = []
|
||||
for item in content:
|
||||
if isinstance(item, str):
|
||||
new_content.append({"type": "text", "text": item})
|
||||
else:
|
||||
new_content.append(item)
|
||||
data["content"] = new_content
|
||||
elif not isinstance(content, list):
|
||||
import json
|
||||
|
||||
try:
|
||||
text_val = json.dumps(content, ensure_ascii=False)
|
||||
except Exception:
|
||||
text_val = str(content)
|
||||
data["content"] = [{"type": "text", "text": text_val}]
|
||||
return data
|
||||
|
||||
def __add__(self, other: str | LLMContentPart | "LLMMessage") -> Self:
|
||||
"""极简语法糖:支持通过加号拼接文本或多模态片段。
|
||||
示例: msg = LLMMessage.user("查看图片:") + ImagePart(url="...")
|
||||
"""
|
||||
new_msg = cast(Self, model_copy(self, deep=True))
|
||||
if isinstance(other, str):
|
||||
new_msg.content.append(cast(Any, TextPart(text=other)))
|
||||
elif isinstance(other, BaseContentPart):
|
||||
new_msg.content.append(cast(Any, other))
|
||||
elif isinstance(other, LLMMessage):
|
||||
new_msg.content.extend(cast(Any, other.content))
|
||||
return new_msg
|
||||
|
||||
@property
|
||||
def extract_text(self) -> str:
|
||||
"""便捷属性:提取当前消息中所有的纯文本"""
|
||||
return "".join(p.text for p in self.content if isinstance(p, TextPart))
|
||||
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""获取消息的来源标识 (存取于 metadata 中)"""
|
||||
return self.metadata.get("source") if self.metadata else None
|
||||
|
||||
@source.setter
|
||||
def source(self, value: str | None):
|
||||
"""设置消息的来源标识"""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
self.metadata["source"] = value
|
||||
|
||||
@property
|
||||
def source_name(self) -> str | None:
|
||||
"""获取消息来源的具体名称 (例如真实用户的昵称)"""
|
||||
return self.metadata.get("source_name") if self.metadata else None
|
||||
|
||||
@source_name.setter
|
||||
def source_name(self, value: str | None):
|
||||
"""设置消息来源的具体名称"""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
self.metadata["source_name"] = value
|
||||
|
||||
@property
|
||||
def scope(self) -> str | None:
|
||||
"""获取该消息所绑定的作用域或会话ID"""
|
||||
return self.metadata.get("scope") if self.metadata else None
|
||||
|
||||
@scope.setter
|
||||
def scope(self, value: str | None):
|
||||
"""设置该消息的作用域"""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
self.metadata["scope"] = value
|
||||
|
||||
@property
|
||||
def thought_signature(self) -> str | None:
|
||||
"""获取连续对话中用于保持思考一致性的加密签名 (Gemini 专属)"""
|
||||
return self.metadata.get("thought_signature") if self.metadata else None
|
||||
|
||||
@thought_signature.setter
|
||||
def thought_signature(self, value: str | None):
|
||||
"""设置思考加密签名"""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
self.metadata["thought_signature"] = value
|
||||
|
||||
@property
|
||||
def token_cost(self) -> int | None:
|
||||
"""获取该消息自身消耗的预估或真实 Token 数"""
|
||||
return self.metadata.get("token_cost") if self.metadata else None
|
||||
|
||||
@token_cost.setter
|
||||
def token_cost(self, value: int | None):
|
||||
"""设置 Token 消耗数"""
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
self.metadata["token_cost"] = value
|
||||
|
||||
@classmethod
|
||||
def user(
|
||||
cls,
|
||||
content: str | Sequence[Any],
|
||||
source: str | None = None,
|
||||
source_name: str | None = None,
|
||||
scope: str | None = None,
|
||||
) -> "UserMessage":
|
||||
"""
|
||||
工厂方法:创建一条 User (用户) 角色的消息。
|
||||
|
||||
参数:
|
||||
content: 消息内容,支持纯字符串或多模态片段数组。
|
||||
source: 可选,消息来源标识。
|
||||
source_name: 可选,消息来源的可读名称。
|
||||
scope: 可选,消息关联的会话作用域。
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
content = [TextPart(text=content)] if content.strip() else []
|
||||
elif not isinstance(content, list):
|
||||
content = list(content)
|
||||
msg = UserMessage(content=cast(Any, content))
|
||||
msg.source = source
|
||||
msg.source_name = source_name
|
||||
msg.scope = scope
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def assistant_tool_calls(
|
||||
cls,
|
||||
tool_calls: list[ToolCallPart],
|
||||
content: str | Sequence[Any] = "",
|
||||
scope: str | None = None,
|
||||
) -> "AssistantMessage":
|
||||
"""
|
||||
工厂方法:创建一条包含工具调用请求的 Assistant (助手) 角色消息。
|
||||
|
||||
参数:
|
||||
tool_calls: 大模型发出的工具调用片段列表。
|
||||
content: 伴随工具调用的其他文本或思维过程。
|
||||
scope: 可选,消息关联的会话作用域。
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
_content: list[Any] = [TextPart(text=content)] if content else []
|
||||
else:
|
||||
_content = list(content)
|
||||
_content.extend(tool_calls)
|
||||
msg = AssistantMessage(content=cast(Any, _content))
|
||||
msg.scope = scope
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def assistant_text_response(
|
||||
cls,
|
||||
content: str | Sequence[Any],
|
||||
scope: str | None = None,
|
||||
) -> "AssistantMessage":
|
||||
"""
|
||||
工厂方法:创建一条仅包含普通文本/思维过程的 Assistant (助手) 角色消息。
|
||||
|
||||
参数:
|
||||
content: 大模型生成的文本回复内容。
|
||||
scope: 可选,消息关联的会话作用域。
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
content = [TextPart(text=content)] if content and content.strip() else []
|
||||
elif not isinstance(content, list):
|
||||
content = list(content)
|
||||
msg = AssistantMessage(content=cast(Any, content))
|
||||
msg.scope = scope
|
||||
return msg
|
||||
|
||||
def add_text(self, text: str) -> Self:
|
||||
"""链式添加文本内容"""
|
||||
self.content.append(cast(Any, TextPart(text=text)))
|
||||
return self
|
||||
|
||||
def add_image_url(self, url: str) -> Self:
|
||||
"""链式添加网络图片"""
|
||||
self.content.append(cast(Any, ImagePart(url=url)))
|
||||
return self
|
||||
|
||||
def add_image_base64(self, b64_data: str, mime_type: str = "image/png") -> Self:
|
||||
"""链式添加 Base64 图片"""
|
||||
self.content.append(
|
||||
cast(Any, ImagePart(raw=base64.b64decode(b64_data), mime_type=mime_type))
|
||||
)
|
||||
return self
|
||||
|
||||
def add_file_url(self, url: str, mime_type: str | None = None) -> Self:
|
||||
"""链式添加文件链接"""
|
||||
self.content.append(cast(Any, FilePart(url=url, mime_type=mime_type)))
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def tool_response(
|
||||
cls,
|
||||
tool_call_id: str,
|
||||
function_name: str,
|
||||
result: Any,
|
||||
scope: str | None = None,
|
||||
) -> "ToolMessage":
|
||||
"""
|
||||
工厂方法:创建一条 Tool (工具) 角色消息,用于承载工具执行完毕后的返回结果。
|
||||
|
||||
参数:
|
||||
tool_call_id: 对应的大模型发出调用请求时的 ID。
|
||||
function_name: 执行的工具名称。
|
||||
result: 工具执行的结果负载 (会被自动 JSON 序列化)。
|
||||
scope: 可选,消息关联的会话作用域。
|
||||
"""
|
||||
_content = [
|
||||
ToolReturnPart(
|
||||
tool_call_id=tool_call_id, tool_name=function_name, output=result
|
||||
)
|
||||
]
|
||||
msg = ToolMessage(content=cast(Any, _content))
|
||||
msg.scope = scope
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def system(
|
||||
cls,
|
||||
content: str | Sequence[Any],
|
||||
scope: str | None = None,
|
||||
) -> "SystemMessage":
|
||||
"""
|
||||
工厂方法:创建一条 System (系统) 角色的设定消息。
|
||||
|
||||
参数:
|
||||
content: 系统的 Prompt 指令。
|
||||
scope: 可选,消息关联的会话作用域。
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
content = [TextPart(text=content)] if content.strip() else []
|
||||
elif not isinstance(content, list):
|
||||
content = list(content)
|
||||
msg = SystemMessage(content=cast(Any, content))
|
||||
msg.scope = scope
|
||||
return msg
|
||||
|
||||
def to_storage_dict(self) -> dict[str, Any]:
|
||||
"""数据库瘦身存储,仅保留核心字段"""
|
||||
return model_dump(
|
||||
self,
|
||||
exclude_none=True,
|
||||
include={
|
||||
"role",
|
||||
"content",
|
||||
"metadata",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SystemMessage(LLMMessage[Literal["system"], TextPart]):
|
||||
"""系统消息:通常用于在对话开头向模型提供系统级指令 (System Prompt)、
|
||||
角色设定或背景上下文。"""
|
||||
|
||||
role: Literal["system"] = "system"
|
||||
content: list[Annotated[TextPart, Field(discriminator="type")]] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
|
||||
class UserMessage(LLMMessage[Literal["user"], LLMContentPart]):
|
||||
"""用户消息:代表来自最终用户或外部触发源的输入,支持包含文本、图片、文件等多模态数据。"""
|
||||
|
||||
role: Literal["user"] = "user"
|
||||
content: list[LLMContentPart] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AssistantMessage(LLMMessage[Literal["assistant"], LLMContentPart]):
|
||||
"""助手消息:代表大模型 (AI) 生成的回复。
|
||||
可能包含纯文本、思维链过程或工具调用请求。"""
|
||||
|
||||
role: Literal["assistant"] = "assistant"
|
||||
content: list[LLMContentPart] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ToolMessage(LLMMessage[Literal["tool"], LLMContentPart]):
|
||||
"""工具消息:用于承载由用户端执行工具后,将结果返回给大模型的消息容器。"""
|
||||
|
||||
role: Literal["tool"] = "tool"
|
||||
content: list[LLMContentPart] = Field(default_factory=list)
|
||||
|
||||
def model_post_init(self, context: Any, /) -> None:
|
||||
"""验证消息的有效性"""
|
||||
if not self.tool_returns:
|
||||
raise ValueError("工具角色的消息必须包含 ToolReturnPart")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AssistantMessage",
|
||||
"ContentT",
|
||||
"LLMMessage",
|
||||
"RoleT",
|
||||
"SystemMessage",
|
||||
"ToolMessage",
|
||||
"UserMessage",
|
||||
]
|
||||
@@ -0,0 +1,341 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Literal
|
||||
from typing_extensions import Self
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.utils.pydantic_compat import model_validator
|
||||
|
||||
|
||||
class BaseContentPart(BaseModel):
|
||||
"""多态消息内容的底层基类"""
|
||||
|
||||
metadata: dict[str, Any] | None = Field(default=None)
|
||||
"""该部件的内部元数据,提供如 thought_signature、解析结果等非展示用数据"""
|
||||
|
||||
@classmethod
|
||||
def text_part(cls, text: str) -> "TextPart":
|
||||
"""创建一个纯文本片段"""
|
||||
return TextPart(text=text)
|
||||
|
||||
@classmethod
|
||||
def thought_part(cls, text: str) -> "ThoughtPart":
|
||||
"""创建一个思维链 (CoT) 思考片段"""
|
||||
return ThoughtPart(thought_text=text)
|
||||
|
||||
@classmethod
|
||||
def image_url_part(cls, url: str) -> "ImagePart":
|
||||
"""创建一个基于外网 URL 的图片片段"""
|
||||
return ImagePart(url=url)
|
||||
|
||||
@classmethod
|
||||
def image_base64_part(cls, data: str, mime_type: str = "image/png") -> "ImagePart":
|
||||
"""创建一个基于 Base64 编码数据的图片片段"""
|
||||
return ImagePart(raw=base64.b64decode(data), mime_type=mime_type)
|
||||
|
||||
@classmethod
|
||||
def audio_url_part(cls, url: str, mime_type: str = "audio/wav") -> "AudioPart":
|
||||
"""创建一个基于外网 URL 的音频片段"""
|
||||
return AudioPart(url=url, mime_type=mime_type)
|
||||
|
||||
@classmethod
|
||||
def video_url_part(cls, url: str, mime_type: str = "video/mp4") -> "VideoPart":
|
||||
"""创建一个基于外网 URL 的视频片段"""
|
||||
return VideoPart(url=url, mime_type=mime_type)
|
||||
|
||||
@classmethod
|
||||
def video_base64_part(cls, data: str, mime_type: str = "video/mp4") -> "VideoPart":
|
||||
"""创建一个基于 Base64 编码数据的视频片段"""
|
||||
return VideoPart(raw=base64.b64decode(data), mime_type=mime_type)
|
||||
|
||||
@classmethod
|
||||
def audio_base64_part(cls, data: str, mime_type: str = "audio/wav") -> "AudioPart":
|
||||
"""创建一个基于 Base64 编码数据的音频片段"""
|
||||
return AudioPart(raw=base64.b64decode(data), mime_type=mime_type)
|
||||
|
||||
@classmethod
|
||||
def file_uri_part(
|
||||
cls,
|
||||
file_uri: str,
|
||||
mime_type: str | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> "FilePart":
|
||||
"""创建一个基于云端 URI (如 Google Cloud Storage gs://) 的通用文件片段"""
|
||||
return FilePart(url=file_uri, mime_type=mime_type, metadata=metadata or {})
|
||||
|
||||
@classmethod
|
||||
def tool_call_part(
|
||||
cls, id: str, tool_name: str, args: dict[str, Any] | str
|
||||
) -> "ToolCallPart":
|
||||
"""创建一个工具调用请求片段"""
|
||||
return ToolCallPart(id=id, tool_name=tool_name, args=args)
|
||||
|
||||
@classmethod
|
||||
def tool_return_part(cls, call_id: str, name: str, result: Any) -> "ToolReturnPart":
|
||||
"""创建一个工具执行结果片段"""
|
||||
return ToolReturnPart(
|
||||
tool_call_id=call_id,
|
||||
tool_name=name,
|
||||
output=result,
|
||||
)
|
||||
|
||||
async def get_raw_bytes(self) -> bytes:
|
||||
"""统一获取多模态原始字节数据 (自动适配 raw、本地 path 或自动下载公网 url)"""
|
||||
raw_data = getattr(self, "raw", None)
|
||||
if isinstance(raw_data, bytes):
|
||||
return raw_data
|
||||
|
||||
path_data = getattr(self, "path", None)
|
||||
if path_data is not None:
|
||||
from pathlib import Path
|
||||
|
||||
if isinstance(path_data, Path):
|
||||
return path_data.read_bytes()
|
||||
|
||||
url_data = getattr(self, "url", None)
|
||||
if isinstance(url_data, str):
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
|
||||
content = await AsyncHttpx.get_content(url_data)
|
||||
if isinstance(content, bytes):
|
||||
return content
|
||||
return b""
|
||||
|
||||
raise ValueError(
|
||||
f"{self.__class__.__name__} 未提供有效的数据源 (url, raw, path)"
|
||||
)
|
||||
|
||||
async def get_base64_data(self) -> str:
|
||||
"""统一获取 Base64 编码的字符串数据"""
|
||||
return base64.b64encode(await self.get_raw_bytes()).decode("utf-8")
|
||||
|
||||
async def get_data_uri(self, default_mime: str = "application/octet-stream") -> str:
|
||||
"""统一获取 Data URI (data:mime;base64,...) 格式的字符串"""
|
||||
mime = getattr(self, "mime_type", None) or default_mime
|
||||
return f"data:{mime};base64,{await self.get_base64_data()}"
|
||||
|
||||
|
||||
class ImagePart(BaseContentPart):
|
||||
"""图片媒体片段"""
|
||||
|
||||
type: Literal["image"] = "image"
|
||||
url: str | None = None
|
||||
"""外网图片 URL (需能直接访问)"""
|
||||
raw: bytes | None = None
|
||||
"""图片二进制裸数据"""
|
||||
path: Path | None = None
|
||||
"""本地文件系统中的图片路径"""
|
||||
mime_type: str | None = None
|
||||
"""图片 MIME 类型 (如 image/jpeg)"""
|
||||
media_resolution: str | None = None
|
||||
"""媒体处理强制分辨率策略"""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_source(self) -> Self:
|
||||
sources = [s for s in (self.url, self.raw, self.path) if s is not None]
|
||||
if len(sources) != 1:
|
||||
raise ValueError("ImagePart 必须且只能提供 url, raw, path 中的一个")
|
||||
return self
|
||||
|
||||
|
||||
class AudioPart(BaseContentPart):
|
||||
"""音频媒体片段"""
|
||||
|
||||
type: Literal["audio"] = "audio"
|
||||
url: str | None = None
|
||||
"""外网音频 URL"""
|
||||
raw: bytes | None = None
|
||||
"""音频二进制裸数据"""
|
||||
path: Path | None = None
|
||||
"""本地文件系统中的音频路径"""
|
||||
mime_type: str | None = None
|
||||
"""音频 MIME 类型 (如 audio/mp3)"""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_source(self) -> Self:
|
||||
sources = [s for s in (self.url, self.raw, self.path) if s is not None]
|
||||
if len(sources) != 1:
|
||||
raise ValueError("AudioPart 必须且只能提供 url, raw, path 中的一个")
|
||||
return self
|
||||
|
||||
|
||||
class VideoPart(BaseContentPart):
|
||||
"""视频媒体片段"""
|
||||
|
||||
type: Literal["video"] = "video"
|
||||
url: str | None = None
|
||||
"""外网视频 URL"""
|
||||
raw: bytes | None = None
|
||||
"""视频二进制裸数据"""
|
||||
path: Path | None = None
|
||||
"""本地文件系统中的视频路径"""
|
||||
mime_type: str | None = None
|
||||
"""视频 MIME 类型 (如 video/mp4)"""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_source(self) -> Self:
|
||||
sources = [s for s in (self.url, self.raw, self.path) if s is not None]
|
||||
if len(sources) != 1:
|
||||
raise ValueError("VideoPart 必须且只能提供 url, raw, path 中的一个")
|
||||
return self
|
||||
|
||||
|
||||
class FilePart(BaseContentPart):
|
||||
"""通用文件片段 (如 PDF、代码文件等)"""
|
||||
|
||||
type: Literal["file"] = "file"
|
||||
url: str | None = None
|
||||
"""外网文件 URL (或 Google Cloud gs:// URI)"""
|
||||
raw: bytes | None = None
|
||||
"""文件二进制裸数据"""
|
||||
path: Path | None = None
|
||||
"""本地文件系统中的文件路径"""
|
||||
mime_type: str | None = None
|
||||
"""文件 MIME 类型 (如 application/pdf)"""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_source(self) -> Self:
|
||||
sources = [s for s in (self.url, self.raw, self.path) if s is not None]
|
||||
if len(sources) != 1:
|
||||
raise ValueError("FilePart 必须且只能提供 url, raw, path 中的一个")
|
||||
return self
|
||||
|
||||
|
||||
class TextPart(BaseContentPart):
|
||||
"""基础文本片段"""
|
||||
|
||||
type: Literal["text"] = "text"
|
||||
text: str
|
||||
"""具体的纯文本内容"""
|
||||
|
||||
|
||||
class ThoughtPart(BaseContentPart):
|
||||
"""思考链 (Chain of Thought) 片段,用于容纳模型的内部思维过程"""
|
||||
|
||||
type: Literal["thought"] = "thought"
|
||||
thought_text: str
|
||||
"""思考过程的文本"""
|
||||
|
||||
|
||||
class ToolCallPart(BaseContentPart):
|
||||
"""工具调用请求片段 (由模型发出)"""
|
||||
|
||||
type: Literal["tool_call"] = "tool_call"
|
||||
id: str
|
||||
"""工具调用唯一 ID"""
|
||||
tool_name: str
|
||||
"""调用的函数名称"""
|
||||
args: dict[str, Any] | str
|
||||
"""传递给工具的参数 (解析后的字典或原始 JSON 字符串)"""
|
||||
|
||||
|
||||
class ToolReturnPart(BaseContentPart):
|
||||
"""工具调用结果片段 (发给模型)"""
|
||||
|
||||
type: Literal["tool_return"] = "tool_return"
|
||||
tool_call_id: str
|
||||
"""关联的原始调用 ID"""
|
||||
tool_name: str
|
||||
"""执行的工具名称"""
|
||||
output: Any
|
||||
"""工具执行结果的有效载荷"""
|
||||
|
||||
|
||||
class TextDeltaPart(BaseModel):
|
||||
"""流式文本增量片段"""
|
||||
|
||||
type: Literal["text_delta"] = "text_delta"
|
||||
content_delta: str
|
||||
"""流式返回的文本增量字符串"""
|
||||
|
||||
|
||||
class ThoughtDeltaPart(BaseModel):
|
||||
"""流式思考链增量片段"""
|
||||
|
||||
type: Literal["thought_delta"] = "thought_delta"
|
||||
content_delta: str
|
||||
"""流式返回的思考过程增量字符串"""
|
||||
|
||||
|
||||
class ToolCallDeltaPart(BaseModel):
|
||||
"""流式工具调用增量片段"""
|
||||
|
||||
type: Literal["tool_call_delta"] = "tool_call_delta"
|
||||
tool_call_id: str | None = None
|
||||
"""(可选) 流式返回的工具调用唯一ID"""
|
||||
tool_name_delta: str | None = None
|
||||
"""(可选) 流式返回的工具名称增量"""
|
||||
args_delta: str | None = None
|
||||
"""(可选) 流式返回的 JSON 格式参数增量"""
|
||||
|
||||
|
||||
LLMContentPart = Annotated[
|
||||
TextPart
|
||||
| ImagePart
|
||||
| AudioPart
|
||||
| VideoPart
|
||||
| FilePart
|
||||
| ThoughtPart
|
||||
| ToolCallPart
|
||||
| ToolReturnPart,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
"""大模型底层标准内容片段的 Annotated 联合类型"""
|
||||
|
||||
|
||||
class EmbedPayload(BaseModel):
|
||||
"""单一的嵌入载体,包含一个或多个多模态片段(融合向量)"""
|
||||
|
||||
parts: list[LLMContentPart] = Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""快速提取纯文本(用于向下兼容不支持多模态的模型)"""
|
||||
return "".join(p.text for p in self.parts if isinstance(p, TextPart)).strip()
|
||||
|
||||
@property
|
||||
def has_multimodal(self) -> bool:
|
||||
"""判断是否包含图像/音频/视频/文件等非文本模态"""
|
||||
return any(not isinstance(p, TextPart) for p in self.parts)
|
||||
|
||||
|
||||
class EmbedBatch(BaseModel):
|
||||
"""一次嵌入 API 请求的批次载体"""
|
||||
|
||||
payloads: list[EmbedPayload] = Field(default_factory=list)
|
||||
|
||||
def to_text_only(self, context_name: str) -> list[str]:
|
||||
"""降级工具:将多模态的批量向量安全剔除图片等内容,回退为纯文本数组"""
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
texts = []
|
||||
for payload in self.payloads:
|
||||
if payload.has_multimodal:
|
||||
logger.warning(
|
||||
f"⚠️ 模型 {context_name} "
|
||||
"不支持多模态嵌入,已自动剔除富媒体内容,静默降级为纯文本进行向量化..."
|
||||
)
|
||||
texts.append(payload.text if payload.text else " ")
|
||||
return texts
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AudioPart",
|
||||
"BaseContentPart",
|
||||
"EmbedBatch",
|
||||
"EmbedPayload",
|
||||
"FilePart",
|
||||
"ImagePart",
|
||||
"LLMContentPart",
|
||||
"TextDeltaPart",
|
||||
"TextPart",
|
||||
"ThoughtDeltaPart",
|
||||
"ThoughtPart",
|
||||
"ToolCallDeltaPart",
|
||||
"ToolCallPart",
|
||||
"ToolReturnPart",
|
||||
"VideoPart",
|
||||
]
|
||||
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from zhenxun.services.ai.core.models import ToolChoice
|
||||
from zhenxun.services.ai.core.options import (
|
||||
GenerationConfig,
|
||||
LLMEmbeddingConfig,
|
||||
TTSConfig,
|
||||
)
|
||||
from zhenxun.utils.pydantic_compat import model_dump
|
||||
|
||||
from .models import LLMMessage
|
||||
from .parts import EmbedBatch
|
||||
|
||||
|
||||
class BaseRequest(BaseModel):
|
||||
"""基础请求 DTO"""
|
||||
|
||||
timeout: float | None = Field(default=None)
|
||||
extra: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
def get_cache_hash_payload(self) -> dict[str, Any]:
|
||||
"""获取用于计算缓存 Hash 的安全载荷,排除所有运行时动态变量"""
|
||||
request_dict = model_dump(self, exclude_none=True)
|
||||
|
||||
if "config" in request_dict and isinstance(request_dict["config"], dict):
|
||||
custom_kwargs = request_dict["config"].get("custom_kwargs", {})
|
||||
if "__cache_ttl__" in custom_kwargs:
|
||||
custom_kwargs.pop("__cache_ttl__")
|
||||
|
||||
if "extra" in request_dict:
|
||||
request_dict["extra"] = {
|
||||
k: v
|
||||
for k, v in request_dict["extra"].items()
|
||||
if not k.startswith("_")
|
||||
and k not in ("run_context", "output_processor", "guardrails")
|
||||
}
|
||||
return request_dict
|
||||
|
||||
|
||||
class ChatRequest(BaseRequest):
|
||||
"""对话生成请求 DTO"""
|
||||
|
||||
messages: list[LLMMessage]
|
||||
config: GenerationConfig | None = None
|
||||
tools: list[Any] | None = None
|
||||
tool_choice: str | dict[str, Any] | ToolChoice | None = None
|
||||
|
||||
def get_cache_hash_payload(self) -> dict[str, Any]:
|
||||
payload = super().get_cache_hash_payload()
|
||||
for msg in payload.get("messages", []):
|
||||
msg.pop("created_at", None)
|
||||
msg.pop("token_cost", None)
|
||||
msg.pop("metadata", None)
|
||||
|
||||
if "tools" in payload and isinstance(payload["tools"], list):
|
||||
safe_tools = []
|
||||
for t in payload["tools"]:
|
||||
if isinstance(t, dict | str):
|
||||
safe_tools.append(t)
|
||||
else:
|
||||
safe_tools.append(getattr(t, "name", type(t).__name__))
|
||||
payload["tools"] = safe_tools
|
||||
return payload
|
||||
|
||||
|
||||
class EmbeddingRequest(BaseRequest):
|
||||
"""向量嵌入请求 DTO"""
|
||||
|
||||
batch: EmbedBatch
|
||||
"""向量嵌入的批次载体"""
|
||||
config: LLMEmbeddingConfig | None = None
|
||||
"""向量嵌入配置"""
|
||||
|
||||
|
||||
class ImageRequest(BaseRequest):
|
||||
"""图像生成请求 DTO"""
|
||||
|
||||
prompt: str
|
||||
"""图像生成提示词"""
|
||||
images: list[Any] | None = None
|
||||
"""输入参考图像列表"""
|
||||
config: GenerationConfig | None = None
|
||||
"""图像生成配置"""
|
||||
|
||||
|
||||
class SpeechRequest(BaseRequest):
|
||||
"""语音合成请求 DTO"""
|
||||
|
||||
input_text: str
|
||||
"""待合成的文本内容"""
|
||||
voice: str | None = None
|
||||
"""发音人/音色标识 (快捷覆盖参数,为空则使用模型默认音色)"""
|
||||
config: TTSConfig | None = None
|
||||
"""语音合成配置"""
|
||||
|
||||
|
||||
class RerankRequest(BaseRequest):
|
||||
"""文本重排请求 DTO"""
|
||||
|
||||
query: str
|
||||
"""检索查询词"""
|
||||
documents: list[str | dict[str, str]]
|
||||
"""待排序的候选文档列表"""
|
||||
top_n: int = 3
|
||||
"""返回的最相关文档数量"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseRequest",
|
||||
"ChatRequest",
|
||||
"EmbeddingRequest",
|
||||
"ImageRequest",
|
||||
"RerankRequest",
|
||||
"SpeechRequest",
|
||||
]
|
||||
@@ -0,0 +1,189 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.utils.pydantic_compat import parse_as
|
||||
|
||||
from .parts import (
|
||||
AudioPart,
|
||||
ImagePart,
|
||||
LLMContentPart,
|
||||
TextPart,
|
||||
ThoughtPart,
|
||||
ToolCallPart,
|
||||
ToolReturnPart,
|
||||
)
|
||||
from .shared import RerankResult, UsageInfo
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
"""
|
||||
文本/多模态对话响应对象,确立 SSOT (单一数据源) 架构。
|
||||
"""
|
||||
|
||||
content_parts: list[LLMContentPart] = Field(default_factory=list)
|
||||
"""经由解析、统一封装后的标准内容部件列表"""
|
||||
usage_info: dict[str, Any] | None = None
|
||||
"""厂商返回的原始用量遥测字段"""
|
||||
raw_response: dict[str, Any] | None = None
|
||||
"""完整的 API 层级原生响应字典 (未经框架清洗)"""
|
||||
grounding_metadata: Any | None = None
|
||||
"""联网搜索、位置等基底事实归因元数据"""
|
||||
parsed_obj: Any | None = Field(default=None)
|
||||
"""在结构化输出模式下,由中间件反序列化得出的强类型 Pydantic 对象实例"""
|
||||
|
||||
def get_parsed_obj(self, model_class: type[T]) -> T | None:
|
||||
"""
|
||||
从结构化生成结果中获取强类型的 Pydantic 解析对象,提供完善的 IDE 类型推导支持。
|
||||
|
||||
参数:
|
||||
model_class: 期望提取的 Pydantic 模型类。
|
||||
|
||||
返回:
|
||||
强类型的模型实例,如果不存在则返回 None
|
||||
"""
|
||||
if self.parsed_obj is None:
|
||||
return None
|
||||
if isinstance(self.parsed_obj, model_class):
|
||||
return self.parsed_obj
|
||||
|
||||
try:
|
||||
return parse_as(model_class, self.parsed_obj)
|
||||
except Exception:
|
||||
return self.parsed_obj
|
||||
|
||||
@property
|
||||
def tool_calls(self) -> list[ToolCallPart]:
|
||||
return [p for p in self.content_parts if isinstance(p, ToolCallPart)]
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""动态视图:提取并拼接所有文本块"""
|
||||
return "".join(
|
||||
p.text for p in self.content_parts if isinstance(p, TextPart)
|
||||
).strip()
|
||||
|
||||
@property
|
||||
def thought_text(self) -> str | None:
|
||||
"""动态视图:提取并拼接所有思考/推理块"""
|
||||
thoughts = [
|
||||
p.thought_text for p in self.content_parts if isinstance(p, ThoughtPart)
|
||||
]
|
||||
return "\n".join(thoughts).strip() if thoughts else None
|
||||
|
||||
@property
|
||||
def thought_signature(self) -> str | None:
|
||||
"""动态视图:获取当前响应中的思考指纹"""
|
||||
for p in reversed(self.content_parts):
|
||||
if isinstance(p, ThoughtPart | ToolCallPart | ToolReturnPart):
|
||||
if p.metadata and "thought_signature" in p.metadata:
|
||||
return p.metadata["thought_signature"]
|
||||
return None
|
||||
|
||||
@property
|
||||
def images(self) -> list[bytes | Path | str]:
|
||||
"""动态视图:提取响应中包含的所有图片数据"""
|
||||
imgs = []
|
||||
for p in self.content_parts:
|
||||
if isinstance(p, ImagePart):
|
||||
if p.url:
|
||||
imgs.append(p.url)
|
||||
elif p.raw:
|
||||
imgs.append(p.raw)
|
||||
elif p.path:
|
||||
imgs.append(p.path)
|
||||
return imgs
|
||||
|
||||
@property
|
||||
def audios(self) -> list[bytes | Path | str]:
|
||||
"""动态视图:提取音频数据"""
|
||||
audios = []
|
||||
for p in self.content_parts:
|
||||
if isinstance(p, AudioPart):
|
||||
if p.url:
|
||||
audios.append(p.url)
|
||||
elif p.raw:
|
||||
audios.append(p.raw)
|
||||
elif p.path:
|
||||
audios.append(p.path)
|
||||
return audios
|
||||
|
||||
|
||||
class EmbeddingResponse(BaseModel):
|
||||
"""Embedding 向量富响应对象"""
|
||||
|
||||
embeddings: list[list[float]]
|
||||
"""多段输入文本对应生成的高维浮点数向量数组 (二维)"""
|
||||
usage: UsageInfo = Field(default_factory=UsageInfo)
|
||||
"""执行向量化任务产生的 Token 用量开销统计"""
|
||||
model_name: str
|
||||
"""实际用于执行本次编码任务的模型名称"""
|
||||
|
||||
@property
|
||||
def vector(self) -> list[float]:
|
||||
"""便捷属性:当且仅当只需提取单一文本向量时,直接返回一维向量"""
|
||||
return self.embeddings[0] if self.embeddings else []
|
||||
|
||||
|
||||
class AudioResponse(BaseModel):
|
||||
"""统一的语音合成响应对象"""
|
||||
|
||||
audio_bytes: bytes
|
||||
"""生成的音频二进制裸数据,可直接用于发送或保存"""
|
||||
audio_format: str
|
||||
"""实际返回的音频格式 (如 mp3, wav)"""
|
||||
usage: UsageInfo = Field(default_factory=UsageInfo)
|
||||
"""Token 或 字符数消耗统计"""
|
||||
raw_response: Any | None = None
|
||||
"""原生响应体,供高级调试使用"""
|
||||
model_name: str
|
||||
"""实际执行任务的模型名称"""
|
||||
|
||||
|
||||
class ImageResponse(BaseModel):
|
||||
"""图像生成响应对象"""
|
||||
|
||||
content_parts: list[LLMContentPart] = Field(default_factory=list)
|
||||
raw_response: dict[str, Any] | None = None
|
||||
|
||||
@property
|
||||
def images(self) -> list[bytes | Path | str]:
|
||||
"""动态视图:提取响应中包含的所有图片数据"""
|
||||
imgs = []
|
||||
for p in self.content_parts:
|
||||
if isinstance(p, ImagePart):
|
||||
if p.url:
|
||||
imgs.append(p.url)
|
||||
elif p.raw:
|
||||
imgs.append(p.raw)
|
||||
elif p.path:
|
||||
imgs.append(p.path)
|
||||
return imgs
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""动态视图:提取并拼接所有 TextPart 文本内容"""
|
||||
return "".join(
|
||||
p.text for p in self.content_parts if isinstance(p, TextPart)
|
||||
).strip()
|
||||
|
||||
|
||||
class RerankResponse(BaseModel):
|
||||
"""文本重排响应对象"""
|
||||
|
||||
results: list[RerankResult]
|
||||
"""重排后的文档结果列表"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AudioResponse",
|
||||
"ChatResponse",
|
||||
"EmbeddingResponse",
|
||||
"ImageResponse",
|
||||
"RerankResponse",
|
||||
]
|
||||
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@dataclass
|
||||
class UsageInfo:
|
||||
"""使用信息数据类"""
|
||||
|
||||
prompt_tokens: int = 0
|
||||
"""请求发送的 Token 消耗数 (含系统提示词与历史记录)"""
|
||||
completion_tokens: int = 0
|
||||
"""模型回复生成的 Token 消耗数"""
|
||||
total_tokens: int = 0
|
||||
"""本次交互总计产生的 Token 数"""
|
||||
cost: float = 0.0
|
||||
"""(可选) 本次交互产生的实际账单估价"""
|
||||
prompt_cache_hit_tokens: int = 0
|
||||
"""被上下文缓存系统命中的 Prompt Token 数 (往往价格更低)"""
|
||||
prompt_cache_miss_tokens: int = 0
|
||||
"""未能命中缓存、实际执行了计算的 Prompt Token 数"""
|
||||
reasoning_tokens: int = 0
|
||||
"""专门用于内部思考/推理链 (CoT) 消耗的 Token 数"""
|
||||
|
||||
@property
|
||||
def efficiency_ratio(self) -> float:
|
||||
return self.completion_tokens / max(self.prompt_tokens, 1)
|
||||
|
||||
def __add__(self, other: "UsageInfo") -> "UsageInfo":
|
||||
"""支持 UsageInfo 相加,用于汇聚子智能体的 Token 消耗"""
|
||||
if not isinstance(other, UsageInfo):
|
||||
return self
|
||||
return UsageInfo(
|
||||
prompt_tokens=self.prompt_tokens + other.prompt_tokens,
|
||||
completion_tokens=self.completion_tokens + other.completion_tokens,
|
||||
total_tokens=self.total_tokens + other.total_tokens,
|
||||
cost=self.cost + other.cost,
|
||||
prompt_cache_hit_tokens=self.prompt_cache_hit_tokens
|
||||
+ other.prompt_cache_hit_tokens,
|
||||
prompt_cache_miss_tokens=self.prompt_cache_miss_tokens
|
||||
+ other.prompt_cache_miss_tokens,
|
||||
reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
|
||||
)
|
||||
|
||||
|
||||
class LLMCodeExecution(BaseModel):
|
||||
"""大模型代码执行(沙箱/本地)结果实体"""
|
||||
|
||||
code: str
|
||||
"""被执行的原始代码"""
|
||||
output: str | None = None
|
||||
"""标准输出 (stdout)"""
|
||||
error: str | None = None
|
||||
"""标准错误 (stderr) 或框架抛出的异常"""
|
||||
execution_time: float | None = None
|
||||
"""代码执行耗时 (秒)"""
|
||||
files_generated: list[str] | None = None
|
||||
"""代码执行过程中生成的工件(Artifacts)文件路径或名称列表"""
|
||||
|
||||
|
||||
class LLMGroundingAttribution(BaseModel):
|
||||
"""基础事实溯源引用对象 (Grounding Attribution)"""
|
||||
|
||||
title: str | None = None
|
||||
"""来源网页或文档的标题"""
|
||||
uri: str | None = None
|
||||
"""来源内容的统一资源标识符 (URL)"""
|
||||
snippet: str | None = None
|
||||
"""从来源网页中提取的、支撑当前生成内容的文本片段"""
|
||||
confidence_score: float | None = None
|
||||
"""该引用来源与生成内容之间相关性的置信度分数"""
|
||||
|
||||
|
||||
class LLMGroundingMetadata(BaseModel):
|
||||
"""检索增强/搜索引擎溯源 (Grounding) 的完整元数据字典,
|
||||
用于为大模型返回的信息提供可信背书"""
|
||||
|
||||
web_search_queries: list[str] | None = None
|
||||
"""模型在执行检索时,实际使用的底层搜索引擎 Query 查询词列表"""
|
||||
grounding_attributions: list[LLMGroundingAttribution] | None = None
|
||||
"""溯源引用的详情列表,用于在 UI 端构建点击跳转链接或角标"""
|
||||
search_suggestions: list[dict[str, Any]] | None = None
|
||||
"""随搜索返回的相关搜索建议 (Search Suggestions)"""
|
||||
search_entry_point: str | None = None
|
||||
"""一段 HTML/CSS 内容,可用于在客户端渲染标准的搜索引擎入口/建议组件"""
|
||||
map_widget_token: str | None = None
|
||||
"""用于渲染 Google Maps 交互式地点小组件 (Places widget) 的
|
||||
上下文 Token (针对 googleMaps 工具)"""
|
||||
|
||||
|
||||
class RerankDocument(BaseModel):
|
||||
"""重排候选文档 (支持纯文本或图文字典)"""
|
||||
|
||||
text: str | None = None
|
||||
"""被用于重排检索的文本内容"""
|
||||
image: str | None = None
|
||||
"""用于多模态重排的图片内容"""
|
||||
|
||||
|
||||
class RerankResult(BaseModel):
|
||||
"""重排返回结果"""
|
||||
|
||||
index: int
|
||||
"""此记录对应于输入时的原始文档数组中的索引位置"""
|
||||
relevance_score: float
|
||||
"""计算出的相关性得分 (越大相关度通常越高)"""
|
||||
document: RerankDocument | None = Field(default=None)
|
||||
"""实际被命中的文档数据"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LLMCodeExecution",
|
||||
"LLMGroundingAttribution",
|
||||
"LLMGroundingMetadata",
|
||||
"RerankDocument",
|
||||
"RerankResult",
|
||||
"UsageInfo",
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from pydantic import Field
|
||||
|
||||
from .parts import (
|
||||
AudioPart,
|
||||
FilePart,
|
||||
ImagePart,
|
||||
LLMContentPart,
|
||||
TextPart,
|
||||
ThoughtPart,
|
||||
ToolCallPart,
|
||||
ToolReturnPart,
|
||||
VideoPart,
|
||||
)
|
||||
|
||||
SystemContentUnion = TextPart
|
||||
"""系统消息允许的内容片段联合类型"""
|
||||
|
||||
UserContentUnion = TextPart | ImagePart | AudioPart | VideoPart | FilePart
|
||||
"""用户消息允许的多模态内容片段联合类型"""
|
||||
|
||||
AssistantContentUnion = (
|
||||
TextPart
|
||||
| ThoughtPart
|
||||
| ToolCallPart
|
||||
| ToolReturnPart
|
||||
| ImagePart
|
||||
| AudioPart
|
||||
| VideoPart
|
||||
| FilePart
|
||||
)
|
||||
"""助手回复允许的内容片段联合类型"""
|
||||
|
||||
ToolContentUnion = ToolReturnPart | ImagePart | AudioPart | VideoPart | FilePart
|
||||
"""工具消息允许的内容片段联合类型"""
|
||||
|
||||
|
||||
RoleT = TypeVar("RoleT", default=str, covariant=True)
|
||||
"""泛型:消息参与者角色类型变量"""
|
||||
|
||||
ContentT = TypeVar("ContentT", default=LLMContentPart, covariant=True)
|
||||
"""泛型:多模态片段数组的元素内容类型变量"""
|
||||
|
||||
|
||||
from .context_events import AgentEvent
|
||||
from .models import (
|
||||
AssistantMessage,
|
||||
LLMMessage,
|
||||
SystemMessage,
|
||||
ToolMessage,
|
||||
UserMessage,
|
||||
)
|
||||
|
||||
AnyLLMMessage = Annotated[
|
||||
SystemMessage | UserMessage | AssistantMessage | ToolMessage,
|
||||
Field(discriminator="role"),
|
||||
]
|
||||
"""LLM 消息类型的合集联合类型"""
|
||||
|
||||
PromptInput = str | UniMessage | LLMMessage | list[LLMContentPart] | Any
|
||||
"""支持作为 LLM 输入的提示词对象联合类型,包括纯文本、UniMessage、LLMMessage 消息实体"""
|
||||
|
||||
AgentMessage = LLMMessage | AgentEvent
|
||||
"""Agent 上下文业务事件载体与原生网络载体的联合类型"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentMessage",
|
||||
"AnyLLMMessage",
|
||||
"AssistantContentUnion",
|
||||
"ContentT",
|
||||
"LLMContentPart",
|
||||
"PromptInput",
|
||||
"RoleT",
|
||||
"SystemContentUnion",
|
||||
"ToolContentUnion",
|
||||
"UserContentUnion",
|
||||
]
|
||||
Reference in New Issue
Block a user