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.
- zhkj_llm_function/__init__.py +78 -0
- zhkj_llm_function/agent.py +285 -0
- zhkj_llm_function/compat.py +63 -0
- zhkj_llm_function/errors.py +29 -0
- zhkj_llm_function/protocol.py +109 -0
- zhkj_llm_function/result.py +82 -0
- zhkj_llm_function/runtime.py +412 -0
- zhkj_llm_function/schema.py +139 -0
- zhkj_llm_function/session.py +95 -0
- zhkj_llm_function/tools.py +130 -0
- zhkj_llm_function-0.2.0.dist-info/METADATA +383 -0
- zhkj_llm_function-0.2.0.dist-info/RECORD +14 -0
- zhkj_llm_function-0.2.0.dist-info/WHEEL +5 -0
- zhkj_llm_function-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|