zhkj_llm_function 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ """zhkj_llm_function:基于 @agent_def 装饰器 + Pydantic 类型注解反射的 Agent 插件框架。
2
+
3
+ 执行引擎:tinyagent-py(ReAct 循环、工具调用、上下文压缩、token 统计)。
4
+ 框架层:@agent_def 声明、Input/Output 模型校验、外部工具注册、统一返回结构。
5
+
6
+ 快速开始:
7
+
8
+ from zhkj_llm_function import BaseAgentGroup, agent_def, ToolDef
9
+
10
+ class MyPlugin(BaseAgentGroup):
11
+ @agent_def(ttl=86400, compact_config=None)
12
+ async def my_agent(self, payload: MyInput, tools: list[ToolDef] | None = None) -> MyOutput:
13
+ \"\"\"
14
+ 角色:xxx
15
+ 业务规则:
16
+ 1. xxx
17
+ \"\"\"
18
+ ...
19
+
20
+ plugin = MyPlugin(model="gpt-5", api_key="...")
21
+ result = await plugin.my_agent({"...": "..."}, tools=[...]) # 返回 MyOutput 实例
22
+ """
23
+
24
+ from .agent import AgentSpec, BaseAgentGroup, DEFAULT_TTL, agent_def
25
+ from .errors import (
26
+ AgentNotRegisteredError,
27
+ FrameworkError,
28
+ InputSchemaError,
29
+ OutputSchemaError,
30
+ SessionError,
31
+ ToolExecutionError,
32
+ ToolNotFoundError,
33
+ )
34
+ from .protocol import (
35
+ TOOL_CALL_END,
36
+ TOOL_CALL_START,
37
+ ToolCall,
38
+ extract_json_payload,
39
+ parse_tool_call,
40
+ render_tool_call,
41
+ )
42
+ from .result import build_result, envelope_to_result
43
+ from .runtime import DEFAULT_COMPACT_CONFIG, AgentRuntime
44
+ from .session import Session, SessionStore
45
+ from .tools import ToolDef, ToolSpec, execute_tool, normalize_tools, register_tools
46
+
47
+ __all__ = [
48
+ "AgentSpec",
49
+ "AgentRuntime",
50
+ "BaseAgentGroup",
51
+ "DEFAULT_TTL",
52
+ "DEFAULT_COMPACT_CONFIG",
53
+ "agent_def",
54
+ "FrameworkError",
55
+ "AgentNotRegisteredError",
56
+ "InputSchemaError",
57
+ "OutputSchemaError",
58
+ "ToolExecutionError",
59
+ "ToolNotFoundError",
60
+ "SessionError",
61
+ "TOOL_CALL_START",
62
+ "TOOL_CALL_END",
63
+ "ToolCall",
64
+ "parse_tool_call",
65
+ "render_tool_call",
66
+ "extract_json_payload",
67
+ "build_result",
68
+ "envelope_to_result",
69
+ "Session",
70
+ "SessionStore",
71
+ "ToolDef",
72
+ "ToolSpec",
73
+ "normalize_tools",
74
+ "register_tools",
75
+ "execute_tool",
76
+ ]
77
+
78
+ __version__ = "0.2.0"
@@ -0,0 +1,285 @@
1
+ """Agent 声明层:`agent_def` 装饰器与 `BaseAgentGroup`。
2
+
3
+ 框架核心模式:基于 `@agent_def` 装饰器 + Pydantic 类型注解反射;
4
+ 执行引擎使用 tinyagent-py。
5
+ """
6
+
7
+ import functools
8
+ import inspect
9
+ from typing import Any, Callable, Dict, List, Optional
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from .errors import AgentNotRegisteredError, FrameworkError
14
+ from .schema import describe_docstring, model_schema, resolve_model_hints
15
+
16
+ DEFAULT_TTL = 86400
17
+
18
+
19
+ class AgentSpec(object):
20
+ """一个 agent 的声明信息(由 @agent_def 生成)。"""
21
+
22
+ __slots__ = (
23
+ "name",
24
+ "ttl",
25
+ "compact_config",
26
+ "input_model",
27
+ "output_model",
28
+ "input_schema",
29
+ "output_schema",
30
+ "business_instruction",
31
+ "func",
32
+ "group_cls",
33
+ )
34
+
35
+ def __init__(
36
+ self,
37
+ name: str,
38
+ ttl: int,
39
+ compact_config: Optional[Dict[str, Any]],
40
+ input_model: type,
41
+ output_model: type,
42
+ business_instruction: str,
43
+ func: Callable[..., Any],
44
+ group_cls: Optional[type] = None,
45
+ ) -> None:
46
+ self.name = name
47
+ self.ttl = ttl
48
+ self.compact_config = compact_config
49
+ self.input_model = input_model
50
+ self.output_model = output_model
51
+ self.input_schema = model_schema(input_model)
52
+ self.output_schema = model_schema(output_model)
53
+ self.business_instruction = business_instruction
54
+ self.func = func
55
+ self.group_cls = group_cls
56
+
57
+ def __repr__(self) -> str: # pragma: no cover - 调试用
58
+ return "AgentSpec(name={0!r}, input={1}, output={2})".format(
59
+ self.name, self.input_model.__name__, self.output_model.__name__
60
+ )
61
+
62
+
63
+ def _require_model(annotation: Any, kind: str, agent_name: str) -> type:
64
+ """校验注解必须是 Pydantic BaseModel 子类。"""
65
+ if annotation is None or not isinstance(annotation, type):
66
+ raise FrameworkError(
67
+ "agent [{0}] 的 {1} 必须使用 Pydantic BaseModel 进行类型注解".format(agent_name, kind)
68
+ )
69
+ if not issubclass(annotation, BaseModel):
70
+ raise FrameworkError(
71
+ "agent [{0}] 的 {1} 必须是 Pydantic BaseModel 子类,当前为 {2}".format(
72
+ agent_name, kind, annotation.__name__
73
+ )
74
+ )
75
+ return annotation
76
+
77
+
78
+ def agent_def(
79
+ ttl: int = DEFAULT_TTL, compact_config: Optional[Dict[str, Any]] = None
80
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
81
+ """标记一个 async 方法为 agent。
82
+
83
+ 参数:
84
+ - ttl:会话有效期(秒),常用 86400(一天)。
85
+ - compact_config:None=使用全局默认前缀压缩策略,也可传入自定义压缩配置 dict。
86
+
87
+ 方法签名固定为:
88
+ async def xxx_agent(self, payload: XXXInput, tools: list[ToolDef] | None = None) -> XXXOutput
89
+ """
90
+
91
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
92
+ if not inspect.iscoroutinefunction(func):
93
+ raise FrameworkError(
94
+ "@agent_def 只能装饰 async 方法: {0}".format(getattr(func, "__name__", func))
95
+ )
96
+ if not isinstance(ttl, int) or ttl <= 0:
97
+ raise FrameworkError("@agent_def 的 ttl 必须是正整数")
98
+ if compact_config is not None and not isinstance(compact_config, dict):
99
+ raise FrameworkError("@agent_def 的 compact_config 必须是 dict 或 None")
100
+
101
+ signature = inspect.signature(func)
102
+ parameters = list(signature.parameters.values())
103
+ if len(parameters) < 2 or parameters[0].name != "self":
104
+ raise FrameworkError("@agent_def 方法必须保留 self 作为第一个参数")
105
+ if parameters[1].name != "payload":
106
+ raise FrameworkError("@agent_def 方法的第二个参数必须命名为 payload")
107
+ if len(parameters) < 3 or parameters[2].name != "tools":
108
+ raise FrameworkError("@agent_def 方法的第三个参数必须命名为 tools")
109
+
110
+ hints = resolve_model_hints(func)
111
+ input_model = _require_model(hints.get("payload"), "入参(payload)", func.__name__)
112
+ output_model = _require_model(hints.get("return"), "出参(return)", func.__name__)
113
+
114
+ spec = AgentSpec(
115
+ name=func.__name__,
116
+ ttl=ttl,
117
+ compact_config=compact_config,
118
+ input_model=input_model,
119
+ output_model=output_model,
120
+ business_instruction=describe_docstring(func),
121
+ func=func,
122
+ )
123
+
124
+ @functools.wraps(func)
125
+ async def wrapper(
126
+ self: Any,
127
+ payload: Any = None,
128
+ tools: Optional[List[Any]] = None,
129
+ session_id: Optional[str] = None,
130
+ ) -> Any:
131
+ """以普通方法方式调用:await plugin.xxx_agent(payload, tools)。
132
+
133
+ 返回值即声明的出参模型实例(外层可按出参签名直接接收);
134
+ 扩展信息(ok / finish_reason / meta)通过代理属性提供。
135
+ 执行失败时抛出对应异常(InputSchemaError / OutputSchemaError 等)。
136
+ """
137
+ runtime = getattr(self, "_runtime", None)
138
+ if runtime is not None:
139
+ from .result import envelope_to_result
140
+
141
+ envelope = await runtime.run(
142
+ spec.name, payload, tools=tools, session_id=session_id
143
+ )
144
+ return envelope_to_result(envelope)
145
+ # 未接入引擎时退化为声明式空实现(插件桩)
146
+ return await func(self, payload, tools)
147
+
148
+ wrapper.__agent_spec__ = spec # type: ignore[attr-defined]
149
+ wrapper.__signature__ = inspect.Signature(
150
+ [
151
+ inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD),
152
+ inspect.Parameter("payload", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None),
153
+ inspect.Parameter("tools", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None),
154
+ inspect.Parameter("session_id", inspect.Parameter.KEYWORD_ONLY, default=None),
155
+ ]
156
+ )
157
+ wrapper.__annotations__ = dict(getattr(func, "__annotations__", {}))
158
+ return wrapper
159
+
160
+ return decorator
161
+
162
+
163
+ class BaseAgentGroup(object):
164
+ """插件基类:所有 agent 能力通过 @agent_def 注册在本类中。
165
+
166
+ 执行引擎为 tinyagent-py;模型、鉴权、压缩策略等通过构造参数注入。
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ model: str = "gpt-5",
172
+ api_key: Optional[str] = None,
173
+ model_kwargs: Optional[Dict[str, Any]] = None,
174
+ temperature: float = 0.0,
175
+ session_store: Any = None,
176
+ compact_config: Optional[Dict[str, Any]] = None,
177
+ global_compact_config: Optional[Dict[str, Any]] = None,
178
+ max_tool_iterations: int = 8,
179
+ max_turns: int = 10,
180
+ enable_todo_write: bool = False,
181
+ storage: Any = None,
182
+ logger: Any = None,
183
+ ) -> None:
184
+ from .runtime import AgentRuntime
185
+
186
+ effective_compact_config = (
187
+ compact_config if compact_config is not None else global_compact_config
188
+ )
189
+ self._global_compact_config = effective_compact_config
190
+ self._runtime = AgentRuntime(
191
+ group=self,
192
+ model=model,
193
+ api_key=api_key,
194
+ model_kwargs=model_kwargs,
195
+ temperature=temperature,
196
+ session_store=session_store,
197
+ global_compact_config=effective_compact_config,
198
+ max_tool_iterations=max_tool_iterations,
199
+ max_turns=max_turns,
200
+ enable_todo_write=enable_todo_write,
201
+ storage=storage,
202
+ logger=logger,
203
+ )
204
+
205
+ def __init_subclass__(cls, **kwargs: Any) -> None:
206
+ super().__init_subclass__(**kwargs)
207
+ specs: Dict[str, AgentSpec] = {}
208
+ for base in reversed(cls.__mro__):
209
+ for attr_name, attr in vars(base).items():
210
+ spec = getattr(attr, "__agent_spec__", None)
211
+ if isinstance(spec, AgentSpec):
212
+ spec.group_cls = cls
213
+ specs[attr_name] = spec
214
+ cls.__agent_specs__ = specs # type: ignore[attr-defined]
215
+
216
+ # ---------- 声明查询 ----------
217
+ @classmethod
218
+ def agent_specs(cls) -> Dict[str, AgentSpec]:
219
+ """本插件注册的全部 agent 声明。"""
220
+ return dict(getattr(cls, "__agent_specs__", {}))
221
+
222
+ @classmethod
223
+ def agent_names(cls) -> List[str]:
224
+ """已注册的 agent 名称列表。"""
225
+ return sorted(cls.agent_specs().keys())
226
+
227
+ @classmethod
228
+ def get_agent_spec(cls, agent_name: str) -> AgentSpec:
229
+ """按名称取 agent 声明;不存在则抛异常。"""
230
+ specs = cls.agent_specs()
231
+ if agent_name not in specs:
232
+ raise AgentNotRegisteredError(
233
+ "未注册的 agent: {0},可用: {1}".format(agent_name, ", ".join(sorted(specs)))
234
+ )
235
+ return specs[agent_name]
236
+
237
+ # ---------- 执行 ----------
238
+ @property
239
+ def runtime(self) -> Any:
240
+ """运行时对象。"""
241
+ return self._runtime
242
+
243
+ @property
244
+ def session_store(self) -> Any:
245
+ """会话存储(内存存储,进程内有效)。"""
246
+ return self._runtime.session_store
247
+
248
+ def session_ids(self) -> List[str]:
249
+ """当前进程内已知的 session_id 列表。"""
250
+ return self._runtime.session_store.list_session_ids()
251
+
252
+ def set_llm(self, client: Any) -> None:
253
+ """切换模型 / 鉴权信息。"""
254
+ if isinstance(client, str):
255
+ self._runtime.model = client
256
+ elif isinstance(client, dict):
257
+ self._runtime.model = client.get("model", self._runtime.model)
258
+ self._runtime.api_key = client.get("api_key", self._runtime.api_key)
259
+ else:
260
+ raise FrameworkError("set_llm 仅支持模型名或配置 dict")
261
+
262
+ async def run(
263
+ self,
264
+ agent_name: str,
265
+ payload: Any,
266
+ tools: Optional[List[Any]] = None,
267
+ session_id: Optional[str] = None,
268
+ ) -> Any:
269
+ """执行指定 agent,返回声明的出参模型实例(扩展信息见代理属性)。"""
270
+ from .result import envelope_to_result
271
+
272
+ envelope = await self._runtime.run(
273
+ agent_name, payload, tools=tools, session_id=session_id
274
+ )
275
+ return envelope_to_result(envelope)
276
+
277
+ async def run_envelope(
278
+ self,
279
+ agent_name: str,
280
+ payload: Any,
281
+ tools: Optional[List[Any]] = None,
282
+ session_id: Optional[str] = None,
283
+ ) -> Dict[str, Any]:
284
+ """执行指定 agent,返回框架原始封装结构 {ok, data, finish_reason, meta}。"""
285
+ return await self._runtime.run(agent_name, payload, tools=tools, session_id=session_id)
@@ -0,0 +1,63 @@
1
+ """与 tinyagent / litellm 消息对象的兼容转换。
2
+
3
+ 问题背景:
4
+ - tinyagent 的 `_format_conversation_for_summary()` 用 `tool_call.get("function", {})` 读取工具调用,
5
+ 但运行期 assistant 消息里的 `tool_calls` 是 litellm/OpenAI 对象(属性访问),压缩时会抛
6
+ `AttributeError: 'X' object has no attribute 'get'`。
7
+
8
+ 因此统一提供:对象 -> dict 的转换。
9
+ """
10
+
11
+ from typing import Any, Dict, Iterable, List
12
+
13
+
14
+ def _get(source: Any, key: str, default: Any = None) -> Any:
15
+ """同时兼容 dict 与对象属性两种取值方式。"""
16
+ if source is None:
17
+ return default
18
+ if isinstance(source, dict):
19
+ return source.get(key, default)
20
+ return getattr(source, key, default)
21
+
22
+
23
+ def tool_call_to_dict(call: Any) -> Dict[str, Any]:
24
+ """把一个工具调用(dict 或对象)转成可序列化的 dict。"""
25
+ if isinstance(call, dict):
26
+ function = call.get("function") or {}
27
+ return {
28
+ "id": call.get("id", ""),
29
+ "type": call.get("type", "function"),
30
+ "function": {
31
+ "name": _get(function, "name", ""),
32
+ "arguments": _get(function, "arguments", "{}"),
33
+ },
34
+ }
35
+
36
+ function = _get(call, "function")
37
+ return {
38
+ "id": _get(call, "id", "") or "",
39
+ "type": _get(call, "type", "function") or "function",
40
+ "function": {
41
+ "name": _get(function, "name", "") or "",
42
+ "arguments": _get(function, "arguments", "{}") or "{}",
43
+ },
44
+ }
45
+
46
+
47
+ def message_to_plain_dict(message: Any) -> Dict[str, Any]:
48
+ """把一条消息转成纯 dict(tool_calls 一并转换)。"""
49
+ if not isinstance(message, dict):
50
+ message = {
51
+ "role": _get(message, "role", "unknown"),
52
+ "content": _get(message, "content", ""),
53
+ }
54
+ plain = dict(message)
55
+ calls = plain.get("tool_calls")
56
+ if isinstance(calls, Iterable) and not isinstance(calls, (str, bytes, dict)):
57
+ plain["tool_calls"] = [tool_call_to_dict(call) for call in calls]
58
+ return plain
59
+
60
+
61
+ def messages_for_summary(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
62
+ """生成用于摘要的副本:tool_calls 统一为 dict,避免引擎侧属性访问报错。"""
63
+ return [message_to_plain_dict(message) for message in messages or []]
@@ -0,0 +1,29 @@
1
+ """框架内部异常类型。"""
2
+
3
+
4
+ class FrameworkError(Exception):
5
+ """所有框架异常的基类。"""
6
+
7
+
8
+ class AgentNotRegisteredError(FrameworkError):
9
+ """请求的 agent 未在 BaseAgentGroup 子类中注册。"""
10
+
11
+
12
+ class InputSchemaError(FrameworkError):
13
+ """入参不符合 agent 声明的 Input 模型。"""
14
+
15
+
16
+ class OutputSchemaError(FrameworkError):
17
+ """模型最终输出不符合 agent 声明的 Output 模型。"""
18
+
19
+
20
+ class ToolExecutionError(FrameworkError):
21
+ """工具执行过程中抛出异常。"""
22
+
23
+
24
+ class ToolNotFoundError(FrameworkError):
25
+ """模型请求了未传入的工具。"""
26
+
27
+
28
+ class SessionError(FrameworkError):
29
+ """会话相关的错误。"""
@@ -0,0 +1,109 @@
1
+ """工具调用协议解析。
2
+
3
+ 框架统一使用 <|tool_call|> 标记协议:
4
+
5
+ <|tool_call|>
6
+ {"name": "工具名称", "arguments": {}}
7
+ <|end_tool_call|>
8
+
9
+ 一轮只支持调用单个工具。
10
+ """
11
+
12
+ import json
13
+ import re
14
+ from typing import Any, Dict, Optional
15
+
16
+ TOOL_CALL_START = "<|tool_call|>"
17
+ TOOL_CALL_END = "<|end_tool_call|>"
18
+
19
+ _FENCE_RE = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL)
20
+
21
+
22
+ class ToolCall(object):
23
+ """一次工具调用请求。"""
24
+
25
+ __slots__ = ("name", "arguments", "raw")
26
+
27
+ def __init__(self, name: str, arguments: Dict[str, Any], raw: str) -> None:
28
+ self.name = name
29
+ self.arguments = arguments
30
+ self.raw = raw
31
+
32
+ def __repr__(self) -> str: # pragma: no cover - 调试用
33
+ return "ToolCall(name={0!r}, arguments={1!r})".format(self.name, self.arguments)
34
+
35
+
36
+ def strip_code_fence(text: str) -> str:
37
+ """去掉 markdown 代码围栏。"""
38
+ match = _FENCE_RE.search(text or "")
39
+ if match:
40
+ return match.group(1).strip()
41
+ return (text or "").strip()
42
+
43
+
44
+ def extract_json_payload(text: str) -> Optional[Any]:
45
+ """从模型输出里抽取 JSON 对象/数组。"""
46
+ if text is None:
47
+ return None
48
+ candidate = strip_code_fence(text)
49
+ if not candidate:
50
+ return None
51
+
52
+ try:
53
+ return json.loads(candidate)
54
+ except Exception:
55
+ pass
56
+
57
+ decoder = json.JSONDecoder()
58
+ for index, char in enumerate(candidate):
59
+ if char not in "{[":
60
+ continue
61
+ try:
62
+ value, _ = decoder.raw_decode(candidate[index:])
63
+ except Exception:
64
+ continue
65
+ return value
66
+ return None
67
+
68
+
69
+ def parse_tool_call(text: str) -> Optional[ToolCall]:
70
+ """解析文本中的工具调用标记;没有则返回 None。"""
71
+ if not text or TOOL_CALL_START not in text:
72
+ return None
73
+
74
+ segment = text.split(TOOL_CALL_START, 1)[1]
75
+ if TOOL_CALL_END in segment:
76
+ segment = segment.split(TOOL_CALL_END, 1)[0]
77
+ segment = strip_code_fence(segment.strip())
78
+ if not segment:
79
+ return None
80
+
81
+ payload = extract_json_payload(segment)
82
+ if not isinstance(payload, dict):
83
+ return None
84
+
85
+ name = payload.get("name")
86
+ if not isinstance(name, str) or not name:
87
+ return None
88
+
89
+ arguments = payload.get("arguments", {})
90
+ if arguments is None:
91
+ arguments = {}
92
+ if isinstance(arguments, str):
93
+ try:
94
+ arguments = json.loads(arguments)
95
+ except Exception:
96
+ arguments = {}
97
+ if not isinstance(arguments, dict):
98
+ arguments = {}
99
+
100
+ return ToolCall(name=name, arguments=arguments, raw=segment)
101
+
102
+
103
+ def render_tool_call(name: str, arguments: Dict[str, Any]) -> str:
104
+ """按框架协议渲染工具调用文本(供 mock / 测试使用)。"""
105
+ return "{0}\n{1}\n{2}".format(
106
+ TOOL_CALL_START,
107
+ json.dumps({"name": name, "arguments": arguments}, ensure_ascii=False),
108
+ TOOL_CALL_END,
109
+ )
@@ -0,0 +1,82 @@
1
+ """结果对象:按 agent 声明的出参签名返回,扩展信息通过代理属性暴露。
2
+
3
+ - 方法返回值就是声明的出参模型实例(`isinstance(result, XXXOutput) == True`),
4
+ 外层可以直接按出参签名接收,`model_dump()` 只包含声明字段。
5
+ - 框架扩展信息(ok / finish_reason / meta)通过动态子类的代理属性提供,
6
+ 不污染出参结构,也不会进入 `model_dump()`。
7
+ """
8
+
9
+ from typing import Any, Dict, Optional
10
+
11
+ from pydantic import PrivateAttr
12
+
13
+ from .errors import (
14
+ AgentNotRegisteredError,
15
+ FrameworkError,
16
+ InputSchemaError,
17
+ OutputSchemaError,
18
+ ToolExecutionError,
19
+ )
20
+
21
+ _FINISH_ERROR_MAP = {
22
+ "not_registered": AgentNotRegisteredError,
23
+ "input_schema_invalid": InputSchemaError,
24
+ "output_schema_invalid": OutputSchemaError,
25
+ "tool_error": ToolExecutionError,
26
+ }
27
+
28
+
29
+ def build_result(model: Any, meta: Optional[Dict[str, Any]] = None, finish_reason: str = "stop"):
30
+ """把出参模型包装成带代理属性的结果对象。
31
+
32
+ 返回对象仍然是出参模型(动态子类)的实例,字段与序列化结果完全不变。
33
+ """
34
+ meta = meta or {}
35
+ base = type(model)
36
+ meta_cls = type(base)
37
+
38
+ namespace = {
39
+ "_framework_meta": PrivateAttr(default=None),
40
+ "_framework_finish_reason": PrivateAttr(default=""),
41
+ "_framework_ok": PrivateAttr(default=True),
42
+ "ok": property(lambda self: self._framework_ok),
43
+ "finish_reason": property(lambda self: self._framework_finish_reason),
44
+ "meta": property(lambda self: self._framework_meta),
45
+ "error": property(lambda self: (self._framework_meta or {}).get("error", "")),
46
+ }
47
+ result_cls = meta_cls(
48
+ "{0}Result".format(base.__name__),
49
+ (base,),
50
+ namespace,
51
+ )
52
+
53
+ obj = model
54
+ try:
55
+ obj.__class__ = result_cls
56
+ object.__setattr__(
57
+ obj,
58
+ "__pydantic_private__",
59
+ {
60
+ "_framework_meta": meta,
61
+ "_framework_finish_reason": finish_reason,
62
+ "_framework_ok": True,
63
+ },
64
+ )
65
+ except Exception: # pragma: no cover - 极端情况下退化为原模型
66
+ return model
67
+ return obj
68
+
69
+
70
+ def envelope_to_result(envelope: Dict[str, Any]):
71
+ """把运行时 envelope 转成出参对象;失败时抛出对应异常。"""
72
+ if envelope.get("ok") and envelope.get("data") is not None:
73
+ return build_result(
74
+ envelope["data"],
75
+ envelope.get("meta") or {},
76
+ envelope.get("finish_reason", "stop"),
77
+ )
78
+
79
+ finish_reason = envelope.get("finish_reason", "error")
80
+ message = envelope.get("error") or "agent 执行失败: {0}".format(finish_reason)
81
+ error_cls = _FINISH_ERROR_MAP.get(finish_reason, FrameworkError)
82
+ raise error_cls(message)