ni.agentkit 0.3.2__tar.gz → 0.4.1__tar.gz
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.
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/PKG-INFO +21 -9
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/README.md +20 -8
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/__init__.py +4 -2
- ni_agentkit-0.4.1/agents/agent.py +460 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/agents/base_agent.py +34 -7
- ni_agentkit-0.4.1/agents/orchestrators.py +138 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/docs/Architecture.md +98 -21
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/docs/QuickStart.md +445 -4
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/docs/README.md +8 -8
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/docs/Reference.md +151 -24
- ni_agentkit-0.4.1/docs/TestReport.md +100 -0
- ni_agentkit-0.4.1/examples/ollama/09a_structured_data_sql.py +59 -0
- ni_agentkit-0.4.1/examples/ollama/09b_structured_data_graph.py +78 -0
- ni_agentkit-0.4.1/examples/ollama/10_skill_lifecycle.py +47 -0
- ni_agentkit-0.4.1/examples/ollama/11_orchestration_enhancement.py +80 -0
- ni_agentkit-0.4.1/examples/ollama/12_run_context_serialization.py +60 -0
- ni_agentkit-0.4.1/examples/ollama/13_human_in_the_loop.py +93 -0
- ni_agentkit-0.4.1/examples/ollama/14_event_standardization.py +55 -0
- ni_agentkit-0.4.1/examples/ollama/15_multi_tenant_isolation.py +93 -0
- ni_agentkit-0.4.1/examples/ollama/16_lifecycle_hooks.py +96 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/README.md +6 -2
- ni_agentkit-0.4.1/examples/standard/09a_structured_data_sql.py +56 -0
- ni_agentkit-0.4.1/examples/standard/09b_structured_data_graph.py +75 -0
- ni_agentkit-0.4.1/examples/standard/10_skill_lifecycle.py +51 -0
- ni_agentkit-0.4.1/examples/standard/11_orchestration_enhancement.py +80 -0
- ni_agentkit-0.4.1/examples/standard/12_run_context_serialization.py +57 -0
- ni_agentkit-0.4.1/examples/standard/13_human_in_the_loop.py +90 -0
- ni_agentkit-0.4.1/examples/standard/14_event_standardization.py +50 -0
- ni_agentkit-0.4.1/examples/standard/15_multi_tenant_isolation.py +103 -0
- ni_agentkit-0.4.1/examples/standard/16_lifecycle_hooks.py +102 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/README.md +6 -2
- ni_agentkit-0.4.1/examples/test_ollama.py +45 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/ni.agentkit.egg-info/PKG-INFO +21 -9
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/ni.agentkit.egg-info/SOURCES.txt +44 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/pyproject.toml +1 -1
- ni_agentkit-0.4.1/runner/context.py +107 -0
- ni_agentkit-0.4.1/runner/context_store.py +84 -0
- ni_agentkit-0.4.1/runner/events.py +116 -0
- ni_agentkit-0.4.1/runner/runner.py +194 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/skills/models.py +41 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/skills/registry.py +23 -3
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/tools/base_tool.py +16 -0
- ni_agentkit-0.4.1/tools/nebula_tool.py +105 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/tools/skill_toolset.py +15 -11
- ni_agentkit-0.4.1/tools/sqlite_tool.py +69 -0
- ni_agentkit-0.4.1/tools/structured_data.py +92 -0
- ni_agentkit-0.3.2/agents/agent.py +0 -364
- ni_agentkit-0.3.2/agents/orchestrators.py +0 -62
- ni_agentkit-0.3.2/docs/TestReport.md +0 -80
- ni_agentkit-0.3.2/examples/test_ollama.py +0 -272
- ni_agentkit-0.3.2/runner/context.py +0 -47
- ni_agentkit-0.3.2/runner/events.py +0 -36
- ni_agentkit-0.3.2/runner/runner.py +0 -105
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/LICENSE +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/_cli.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/agents/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/01_basic_chat.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/02_tool_calling.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/03_skill_usage.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/04_multi_agent.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/05_guardrail.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/06_orchestration.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/07_sync_async_stream.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/08_memory.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/ollama/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/quickstart.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/01_basic_chat.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/02_tool_calling.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/03_skill_usage.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/04_multi_agent.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/05_guardrail.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/06_orchestration.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/07_sync_async_stream.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/08_memory.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/examples/standard/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/adapters/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/adapters/anthropic_adapter.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/adapters/google_adapter.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/adapters/ollama_adapter.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/adapters/openai_adapter.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/adapters/openai_compatible.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/base.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/cache.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/middleware.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/registry.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/llm/types.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/memory/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/memory/base.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/memory/mem0_provider.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/ni.agentkit.egg-info/dependency_links.txt +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/ni.agentkit.egg-info/entry_points.txt +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/ni.agentkit.egg-info/requires.txt +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/ni.agentkit.egg-info/top_level.txt +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/runner/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/safety/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/safety/guardrails.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/safety/permissions.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/setup.cfg +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/skills/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/skills/loader.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/tools/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/tools/function_tool.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/utils/__init__.py +0 -0
- {ni_agentkit-0.3.2 → ni_agentkit-0.4.1}/utils/schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ni.agentkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A Python-native Agent framework with first-class Skill support and multi-LLM adapter
|
|
5
5
|
Author-email: Krix Tam <krix.tam@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -52,15 +52,27 @@ Dynamic: license-file
|
|
|
52
52
|
- 🤖 **声明式 Agent** — 零继承配置,支持 Handoff 转介 + as_tool 委派两种协作模式
|
|
53
53
|
- 📚 **一等公民 Skill** — 三级渐进式加载(L1 元数据 → L2 指令 → L3 资源),按需加载省 token
|
|
54
54
|
- 🔧 **灵活工具系统** — `@function_tool` 装饰器自动推断 JSON Schema
|
|
55
|
-
- 🧠 **多 LLM 适配器** — 自研统一适配层,
|
|
56
|
-
- 🛡️ **内置安全** — Guardrail 护栏 +
|
|
55
|
+
- 🧠 **多 LLM 适配器** — 自研统一适配层,5 个适配器覆盖主流 LLM(含 OpenAI Compatible)
|
|
56
|
+
- 🛡️ **内置安全** — Guardrail 护栏 + 权限控制;`run_skill_script` 当前为占位执行(SandboxExecutor 预留扩展)
|
|
57
57
|
- 🎭 **编排 Agent** — Sequential / Parallel / Loop 三种模式
|
|
58
58
|
- 💾 **记忆系统** — 可选集成 Mem0,支持自定义记忆提供者
|
|
59
|
+
- ⚡ **全异步设计与 Hooks** — 底层全面采用 `asyncio`,流式响应(Streaming)、事件驱动(Event-Driven)、断点续跑机制(Checkpoint/Resume)。提供细粒度的 生命周期 Hooks,支持改写请求与结果。
|
|
60
|
+
- 🏢 **多租户数据隔离** — 框架级支持 `user_id` / `session_id` 贯穿,Memory 默认分桶,Session 结束自动释放资源。
|
|
61
|
+
- 🔗 **与大模型平台解耦** — 内置多模型支持,轻松对接 Ollama 等本地模型。
|
|
59
62
|
|
|
60
63
|
## 🚀 安装
|
|
61
64
|
|
|
62
65
|
```bash
|
|
66
|
+
# 基础安装
|
|
63
67
|
pip install ni.agentkit
|
|
68
|
+
|
|
69
|
+
# 如果需要 OpenAI / DeepSeek / 通义千问等
|
|
70
|
+
pip install "ni.agentkit[openai]"
|
|
71
|
+
pip install "ni.agentkit[anthropic]"
|
|
72
|
+
pip install "ni.agentkit[google]"
|
|
73
|
+
pip install "ni.agentkit[memory]"
|
|
74
|
+
pip install "ni.agentkit[docker]"
|
|
75
|
+
pip install "ni.agentkit[all]"
|
|
64
76
|
```
|
|
65
77
|
|
|
66
78
|
## ⚡ 30 秒快速开始
|
|
@@ -101,21 +113,21 @@ print(agentkit.get_examples_dir()) # 示例目录路径
|
|
|
101
113
|
| 文档 | 说明 |
|
|
102
114
|
|------|------|
|
|
103
115
|
| [README](docs/README.md) | 项目概述与特性 |
|
|
104
|
-
| [QuickStart](docs/QuickStart.md) |
|
|
116
|
+
| [QuickStart](docs/QuickStart.md) | 16 个渐进式入门示例 |
|
|
105
117
|
| [Architecture](docs/Architecture.md) | 六层架构设计说明 |
|
|
106
118
|
| [Reference](docs/Reference.md) | 完整 API 参考手册 |
|
|
107
119
|
|
|
108
120
|
## 🧪 示例
|
|
109
121
|
|
|
110
|
-
安装包内含
|
|
122
|
+
安装包内含 32 个可运行示例(标准版 × 16 + Ollama 本地版 × 16):
|
|
111
123
|
|
|
112
124
|
```bash
|
|
113
125
|
# Ollama 本地版(无需 API Key)
|
|
114
126
|
python -c "import agentkit; print(agentkit.get_examples_dir())"
|
|
115
127
|
# 然后运行对应目录下的示例文件
|
|
116
128
|
|
|
117
|
-
#
|
|
118
|
-
python -
|
|
129
|
+
# 或者直接运行示例文件:
|
|
130
|
+
python "$(python -c "import agentkit, os; print(os.path.join(agentkit.get_examples_dir(), 'ollama', '01_basic_chat.py'))")"
|
|
119
131
|
```
|
|
120
132
|
|
|
121
133
|
## 🔌 支持的 LLM
|
|
@@ -141,8 +153,8 @@ python -m agentkit.examples.ollama.01_basic_chat
|
|
|
141
153
|
|
|
142
154
|
```bash
|
|
143
155
|
dist/
|
|
144
|
-
├── ni_agentkit-0.
|
|
145
|
-
└── ni_agentkit-0.
|
|
156
|
+
├── ni_agentkit-0.4.1-py3-none-any.whl # pip install 用这个
|
|
157
|
+
└── ni_agentkit-0.4.1.tar.gz # 源码分发
|
|
146
158
|
```
|
|
147
159
|
|
|
148
160
|
## 📄 License
|
|
@@ -10,15 +10,27 @@
|
|
|
10
10
|
- 🤖 **声明式 Agent** — 零继承配置,支持 Handoff 转介 + as_tool 委派两种协作模式
|
|
11
11
|
- 📚 **一等公民 Skill** — 三级渐进式加载(L1 元数据 → L2 指令 → L3 资源),按需加载省 token
|
|
12
12
|
- 🔧 **灵活工具系统** — `@function_tool` 装饰器自动推断 JSON Schema
|
|
13
|
-
- 🧠 **多 LLM 适配器** — 自研统一适配层,
|
|
14
|
-
- 🛡️ **内置安全** — Guardrail 护栏 +
|
|
13
|
+
- 🧠 **多 LLM 适配器** — 自研统一适配层,5 个适配器覆盖主流 LLM(含 OpenAI Compatible)
|
|
14
|
+
- 🛡️ **内置安全** — Guardrail 护栏 + 权限控制;`run_skill_script` 当前为占位执行(SandboxExecutor 预留扩展)
|
|
15
15
|
- 🎭 **编排 Agent** — Sequential / Parallel / Loop 三种模式
|
|
16
16
|
- 💾 **记忆系统** — 可选集成 Mem0,支持自定义记忆提供者
|
|
17
|
+
- ⚡ **全异步设计与 Hooks** — 底层全面采用 `asyncio`,流式响应(Streaming)、事件驱动(Event-Driven)、断点续跑机制(Checkpoint/Resume)。提供细粒度的 生命周期 Hooks,支持改写请求与结果。
|
|
18
|
+
- 🏢 **多租户数据隔离** — 框架级支持 `user_id` / `session_id` 贯穿,Memory 默认分桶,Session 结束自动释放资源。
|
|
19
|
+
- 🔗 **与大模型平台解耦** — 内置多模型支持,轻松对接 Ollama 等本地模型。
|
|
17
20
|
|
|
18
21
|
## 🚀 安装
|
|
19
22
|
|
|
20
23
|
```bash
|
|
24
|
+
# 基础安装
|
|
21
25
|
pip install ni.agentkit
|
|
26
|
+
|
|
27
|
+
# 如果需要 OpenAI / DeepSeek / 通义千问等
|
|
28
|
+
pip install "ni.agentkit[openai]"
|
|
29
|
+
pip install "ni.agentkit[anthropic]"
|
|
30
|
+
pip install "ni.agentkit[google]"
|
|
31
|
+
pip install "ni.agentkit[memory]"
|
|
32
|
+
pip install "ni.agentkit[docker]"
|
|
33
|
+
pip install "ni.agentkit[all]"
|
|
22
34
|
```
|
|
23
35
|
|
|
24
36
|
## ⚡ 30 秒快速开始
|
|
@@ -59,21 +71,21 @@ print(agentkit.get_examples_dir()) # 示例目录路径
|
|
|
59
71
|
| 文档 | 说明 |
|
|
60
72
|
|------|------|
|
|
61
73
|
| [README](docs/README.md) | 项目概述与特性 |
|
|
62
|
-
| [QuickStart](docs/QuickStart.md) |
|
|
74
|
+
| [QuickStart](docs/QuickStart.md) | 16 个渐进式入门示例 |
|
|
63
75
|
| [Architecture](docs/Architecture.md) | 六层架构设计说明 |
|
|
64
76
|
| [Reference](docs/Reference.md) | 完整 API 参考手册 |
|
|
65
77
|
|
|
66
78
|
## 🧪 示例
|
|
67
79
|
|
|
68
|
-
安装包内含
|
|
80
|
+
安装包内含 32 个可运行示例(标准版 × 16 + Ollama 本地版 × 16):
|
|
69
81
|
|
|
70
82
|
```bash
|
|
71
83
|
# Ollama 本地版(无需 API Key)
|
|
72
84
|
python -c "import agentkit; print(agentkit.get_examples_dir())"
|
|
73
85
|
# 然后运行对应目录下的示例文件
|
|
74
86
|
|
|
75
|
-
#
|
|
76
|
-
python -
|
|
87
|
+
# 或者直接运行示例文件:
|
|
88
|
+
python "$(python -c "import agentkit, os; print(os.path.join(agentkit.get_examples_dir(), 'ollama', '01_basic_chat.py'))")"
|
|
77
89
|
```
|
|
78
90
|
|
|
79
91
|
## 🔌 支持的 LLM
|
|
@@ -99,8 +111,8 @@ python -m agentkit.examples.ollama.01_basic_chat
|
|
|
99
111
|
|
|
100
112
|
```bash
|
|
101
113
|
dist/
|
|
102
|
-
├── ni_agentkit-0.
|
|
103
|
-
└── ni_agentkit-0.
|
|
114
|
+
├── ni_agentkit-0.4.1-py3-none-any.whl # pip install 用这个
|
|
115
|
+
└── ni_agentkit-0.4.1.tar.gz # 源码分发
|
|
104
116
|
```
|
|
105
117
|
|
|
106
118
|
## 📄 License
|
|
@@ -28,8 +28,10 @@ from .skills.models import Skill, SkillFrontmatter, SkillResources
|
|
|
28
28
|
from .skills.registry import SkillRegistry
|
|
29
29
|
from .tools.base_tool import BaseTool, BaseToolset
|
|
30
30
|
from .tools.function_tool import FunctionTool, function_tool
|
|
31
|
+
from .tools.structured_data import ResultFormatter, StructuredDataTool
|
|
32
|
+
from .tools.sqlite_tool import SQLiteTool, SQLiteResultFormatter
|
|
31
33
|
|
|
32
|
-
__version__ = "0.
|
|
34
|
+
__version__ = "0.4.1"
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
def get_docs_dir() -> str:
|
|
@@ -48,7 +50,7 @@ __all__ = [
|
|
|
48
50
|
"Agent", "BaseAgent", "SequentialAgent", "ParallelAgent", "LoopAgent",
|
|
49
51
|
"Runner", "RunResult", "Event",
|
|
50
52
|
"BaseLLM", "LLMConfig", "LLMRegistry", "LLMResponse", "Message", "ToolCall", "ToolDefinition",
|
|
51
|
-
"BaseTool", "BaseToolset", "FunctionTool", "function_tool",
|
|
53
|
+
"BaseTool", "BaseToolset", "FunctionTool", "function_tool", "StructuredDataTool", "ResultFormatter", "SQLiteTool",
|
|
52
54
|
"Skill", "SkillFrontmatter", "SkillResources", "SkillRegistry", "load_skill_from_dir",
|
|
53
55
|
"GuardrailResult", "InputGuardrail", "OutputGuardrail", "PermissionPolicy",
|
|
54
56
|
"input_guardrail", "output_guardrail",
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentkit/agents/agent.py — 核心 LLM Agent
|
|
3
|
+
|
|
4
|
+
开发者 99% 情况下使用的类。融合:
|
|
5
|
+
- OpenAI 的声明式配置
|
|
6
|
+
- Google 的丰富回调
|
|
7
|
+
- Skill 一等公民
|
|
8
|
+
- Handoff + as_tool 双协作模式
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, AsyncGenerator, Callable, Optional, Union
|
|
15
|
+
|
|
16
|
+
from pydantic import ConfigDict, Field, PrivateAttr
|
|
17
|
+
|
|
18
|
+
from ..llm.base import BaseLLM
|
|
19
|
+
from ..llm.registry import LLMRegistry
|
|
20
|
+
from ..llm.types import LLMConfig, Message, MessageRole, ToolCall as LLMToolCall
|
|
21
|
+
from ..runner.events import Event, EventType
|
|
22
|
+
from ..skills.models import Skill
|
|
23
|
+
from ..tools.base_tool import BaseTool, BaseToolset, HumanInputRequested
|
|
24
|
+
from ..tools.function_tool import FunctionTool
|
|
25
|
+
from ..tools.skill_toolset import SkillToolset
|
|
26
|
+
from .base_agent import BaseAgent
|
|
27
|
+
|
|
28
|
+
from ..runner.context import RunContext
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("agentkit.agent")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Agent(BaseAgent):
|
|
34
|
+
"""核心 LLM Agent"""
|
|
35
|
+
|
|
36
|
+
# === LLM 配置 ===
|
|
37
|
+
model: Union[str, LLMConfig, BaseLLM, None] = ""
|
|
38
|
+
instructions: Union[str, Callable] = ""
|
|
39
|
+
|
|
40
|
+
# === 工具 & 技能 ===
|
|
41
|
+
tools: list[Any] = Field(default_factory=list)
|
|
42
|
+
skills: list[Skill] = Field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
# === Agent 间协作 ===
|
|
45
|
+
handoffs: list[Any] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
# === 输入输出 ===
|
|
48
|
+
output_type: Optional[type] = None
|
|
49
|
+
|
|
50
|
+
# === 安全 ===
|
|
51
|
+
input_guardrails: list[Any] = Field(default_factory=list)
|
|
52
|
+
output_guardrails: list[Any] = Field(default_factory=list)
|
|
53
|
+
permission_policy: Optional[Any] = None
|
|
54
|
+
|
|
55
|
+
# === 记忆 ===
|
|
56
|
+
memory: Optional[Any] = None
|
|
57
|
+
|
|
58
|
+
# === 行为 ===
|
|
59
|
+
tool_use_behavior: str = "run_llm_again"
|
|
60
|
+
max_tool_rounds: int = 20
|
|
61
|
+
enable_cache: bool = True # LLM 响应缓存(默认开启,绑定 Agent 实例生命周期)
|
|
62
|
+
cache_ttl: int = 300 # 缓存有效期(秒)
|
|
63
|
+
memory_async_write: bool = True # 记忆写入是否异步(True=不阻塞, False=等写完再返回)
|
|
64
|
+
|
|
65
|
+
# === 精细回调 ===
|
|
66
|
+
before_model_callback: Optional[Callable] = None
|
|
67
|
+
after_model_callback: Optional[Callable] = None
|
|
68
|
+
before_tool_callback: Optional[Callable] = None
|
|
69
|
+
after_tool_callback: Optional[Callable] = None
|
|
70
|
+
before_handoff_callback: Optional[Callable] = None
|
|
71
|
+
after_handoff_callback: Optional[Callable] = None
|
|
72
|
+
on_error_callback: Optional[Callable] = None
|
|
73
|
+
fail_fast_on_hook_error: bool = False
|
|
74
|
+
_callable_tool_cache: dict[int, FunctionTool] = PrivateAttr(default_factory=dict)
|
|
75
|
+
_handoff_tool_cache: dict[int, FunctionTool] = PrivateAttr(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# 核心方法
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
async def get_instructions(self, ctx: "RunContext") -> str:
|
|
82
|
+
"""获取系统提示词(支持动态函数)"""
|
|
83
|
+
if callable(self.instructions):
|
|
84
|
+
result = self.instructions(ctx, self)
|
|
85
|
+
if inspect.isawaitable(result):
|
|
86
|
+
return await result
|
|
87
|
+
return result
|
|
88
|
+
return self.instructions or ""
|
|
89
|
+
|
|
90
|
+
async def get_all_tools(self, ctx: "RunContext") -> list[BaseTool]:
|
|
91
|
+
"""汇总所有可用工具"""
|
|
92
|
+
all_tools: list[BaseTool] = []
|
|
93
|
+
|
|
94
|
+
# 1. 处理 tools
|
|
95
|
+
for tool_union in self.tools:
|
|
96
|
+
if isinstance(tool_union, BaseTool):
|
|
97
|
+
all_tools.append(tool_union)
|
|
98
|
+
elif isinstance(tool_union, BaseToolset):
|
|
99
|
+
all_tools.extend(await tool_union.get_tools(ctx))
|
|
100
|
+
elif callable(tool_union):
|
|
101
|
+
fn_key = id(tool_union)
|
|
102
|
+
cached_tool = self._callable_tool_cache.get(fn_key)
|
|
103
|
+
if cached_tool is None:
|
|
104
|
+
cached_tool = FunctionTool.from_function(tool_union)
|
|
105
|
+
self._callable_tool_cache[fn_key] = cached_tool
|
|
106
|
+
all_tools.append(cached_tool)
|
|
107
|
+
|
|
108
|
+
# 2. 处理 skills → SkillToolset
|
|
109
|
+
if self.skills:
|
|
110
|
+
additional = all_tools.copy() # 让 Skill 能看到已注册的工具
|
|
111
|
+
skill_toolset = SkillToolset(skills=self.skills, additional_tools=additional)
|
|
112
|
+
all_tools.extend(await skill_toolset.get_tools(ctx))
|
|
113
|
+
|
|
114
|
+
# 3. 处理 handoffs → transfer_to_xxx 工具
|
|
115
|
+
for target in self.handoffs:
|
|
116
|
+
if isinstance(target, BaseAgent):
|
|
117
|
+
target_key = id(target)
|
|
118
|
+
cached_handoff_tool = self._handoff_tool_cache.get(target_key)
|
|
119
|
+
if cached_handoff_tool is None:
|
|
120
|
+
cached_handoff_tool = self._create_handoff_tool(target)
|
|
121
|
+
self._handoff_tool_cache[target_key] = cached_handoff_tool
|
|
122
|
+
all_tools.append(cached_handoff_tool)
|
|
123
|
+
|
|
124
|
+
return all_tools
|
|
125
|
+
|
|
126
|
+
def as_tool(self, name: str, description: str) -> FunctionTool:
|
|
127
|
+
"""把自己变成一个工具,供其他 Agent 调用"""
|
|
128
|
+
agent_ref = self
|
|
129
|
+
|
|
130
|
+
async def _invoke(**kwargs: Any) -> Any:
|
|
131
|
+
input_text = kwargs.get("input", "")
|
|
132
|
+
from ..runner.runner import Runner
|
|
133
|
+
result = await Runner.run(agent_ref, input=str(input_text))
|
|
134
|
+
return result.final_output
|
|
135
|
+
|
|
136
|
+
return FunctionTool(
|
|
137
|
+
name=name,
|
|
138
|
+
description=description,
|
|
139
|
+
handler=_invoke,
|
|
140
|
+
json_schema={
|
|
141
|
+
"type": "object",
|
|
142
|
+
"properties": {"input": {"type": "string", "description": "任务输入"}},
|
|
143
|
+
"required": ["input"],
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
# 核心执行循环
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
|
|
152
|
+
round_count = 0
|
|
153
|
+
|
|
154
|
+
# 在执行前加载所有未加载的 Skill
|
|
155
|
+
for skill in self.skills:
|
|
156
|
+
try:
|
|
157
|
+
await skill.on_load(ctx)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
yield Event(
|
|
160
|
+
agent=self.name,
|
|
161
|
+
type="error",
|
|
162
|
+
data={"context": "skill_on_load", "skill": skill.name, "error": str(e)}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# 这些数据在单次 run 内稳定,提前计算可减少重复开销
|
|
166
|
+
llm = self._resolve_model()
|
|
167
|
+
memory_injection = ""
|
|
168
|
+
skill_prompt_injection = ""
|
|
169
|
+
|
|
170
|
+
if self.memory:
|
|
171
|
+
try:
|
|
172
|
+
relevant = await self.memory.search(ctx.input, user_id=ctx.user_id, agent_id=self.name, limit=5)
|
|
173
|
+
if relevant:
|
|
174
|
+
mem_text = "\n".join([f"- {m.content}" for m in relevant])
|
|
175
|
+
memory_injection = f"\n\n## 相关记忆\n{mem_text}"
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.warning("检索记忆失败: %s", e)
|
|
178
|
+
|
|
179
|
+
if self.skills:
|
|
180
|
+
skill_toolset = SkillToolset(skills=self.skills)
|
|
181
|
+
skill_prompt_injection = "\n\n" + skill_toolset.get_system_prompt_injection()
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
while round_count < self.max_tool_rounds:
|
|
185
|
+
round_count += 1
|
|
186
|
+
|
|
187
|
+
# 1. 构建指令
|
|
188
|
+
instructions = await self.get_instructions(ctx)
|
|
189
|
+
instructions += memory_injection
|
|
190
|
+
instructions += skill_prompt_injection
|
|
191
|
+
|
|
192
|
+
# 2. 获取工具
|
|
193
|
+
tools = await self.get_all_tools(ctx)
|
|
194
|
+
tool_map = {tool.name: tool for tool in tools}
|
|
195
|
+
tool_defs = [t.to_tool_definition() for t in tools]
|
|
196
|
+
|
|
197
|
+
# 3. 构建消息
|
|
198
|
+
messages = [Message.system(instructions)]
|
|
199
|
+
messages.append(Message.user(ctx.input))
|
|
200
|
+
|
|
201
|
+
# 追加历史消息
|
|
202
|
+
for msg_dict in ctx.messages:
|
|
203
|
+
role = MessageRole(msg_dict.get("role", "user"))
|
|
204
|
+
tool_calls_raw = msg_dict.get("tool_calls", [])
|
|
205
|
+
tool_calls_parsed = [
|
|
206
|
+
LLMToolCall(id=tc["id"], name=tc["name"], arguments=tc["arguments"])
|
|
207
|
+
for tc in tool_calls_raw
|
|
208
|
+
] if tool_calls_raw else []
|
|
209
|
+
messages.append(Message(
|
|
210
|
+
role=role,
|
|
211
|
+
content=msg_dict.get("content"),
|
|
212
|
+
tool_call_id=msg_dict.get("tool_call_id"),
|
|
213
|
+
tool_calls=tool_calls_parsed,
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
# 4. before_model 回调
|
|
217
|
+
if self.before_model_callback:
|
|
218
|
+
override, duration, err = await self._run_hook(self.before_model_callback, "before_model_callback", ctx, instructions, tools)
|
|
219
|
+
if err:
|
|
220
|
+
yield Event(agent=self.name, type="error", data={"hook": "before_model", "error": str(err), "duration": duration})
|
|
221
|
+
if self.fail_fast_on_hook_error:
|
|
222
|
+
return
|
|
223
|
+
elif override is not None:
|
|
224
|
+
yield Event(agent=self.name, type="model_override", data={"override": override, "duration": duration})
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# 5. 调用 LLM(支持缓存)
|
|
228
|
+
cached = False
|
|
229
|
+
|
|
230
|
+
# 检查缓存
|
|
231
|
+
if self.enable_cache:
|
|
232
|
+
cache = self._get_cache()
|
|
233
|
+
cached_response = cache.get(messages, tool_defs if tool_defs else None)
|
|
234
|
+
if cached_response is not None:
|
|
235
|
+
response = cached_response
|
|
236
|
+
cached = True
|
|
237
|
+
|
|
238
|
+
if not cached:
|
|
239
|
+
try:
|
|
240
|
+
response = await llm.generate(messages=messages, tools=tool_defs if tool_defs else None)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
error_msg = str(e) or f"{type(e).__name__}: LLM 调用失败"
|
|
243
|
+
if self.on_error_callback:
|
|
244
|
+
_, duration, err = await self._run_hook(self.on_error_callback, "on_error_callback", ctx, e)
|
|
245
|
+
if err:
|
|
246
|
+
yield Event(agent=self.name, type="error", data={"hook": "on_error", "error": str(err), "duration": duration})
|
|
247
|
+
if self.fail_fast_on_hook_error:
|
|
248
|
+
return
|
|
249
|
+
yield Event(agent=self.name, type="error", data=error_msg)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# 写入缓存
|
|
253
|
+
if self.enable_cache:
|
|
254
|
+
cache.put(messages, tool_defs if tool_defs else None, response)
|
|
255
|
+
|
|
256
|
+
# 6. after_model 回调
|
|
257
|
+
if self.after_model_callback:
|
|
258
|
+
hook_res, duration, err = await self._run_hook(self.after_model_callback, "after_model_callback", ctx, response)
|
|
259
|
+
if err:
|
|
260
|
+
yield Event(agent=self.name, type="error", data={"hook": "after_model", "error": str(err), "duration": duration})
|
|
261
|
+
if self.fail_fast_on_hook_error:
|
|
262
|
+
return
|
|
263
|
+
elif hook_res is not None:
|
|
264
|
+
response = hook_res
|
|
265
|
+
|
|
266
|
+
yield Event(agent=self.name, type="llm_response", data=response)
|
|
267
|
+
|
|
268
|
+
# 7. 分析响应
|
|
269
|
+
if response.has_tool_calls:
|
|
270
|
+
# ⭐ 关键:先把 assistant 的 tool_calls 加入历史,
|
|
271
|
+
# 这样下一轮 LLM 能看到完整的调用链(assistant→tool→assistant...)
|
|
272
|
+
ctx.messages.append({
|
|
273
|
+
"role": "assistant",
|
|
274
|
+
"content": response.content,
|
|
275
|
+
"tool_calls": [
|
|
276
|
+
{"id": tc.id, "name": tc.name, "arguments": tc.arguments}
|
|
277
|
+
for tc in response.tool_calls
|
|
278
|
+
],
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
for tool_call in response.tool_calls:
|
|
282
|
+
tool = tool_map.get(tool_call.name)
|
|
283
|
+
if not tool:
|
|
284
|
+
yield Event(agent=self.name, type="error", data=f"工具 '{tool_call.name}' 未找到")
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# 检查是否是 handoff
|
|
288
|
+
if tool_call.name.startswith("transfer_to_"):
|
|
289
|
+
target_agent = tool_call.name.replace("transfer_to_", "")
|
|
290
|
+
|
|
291
|
+
# before_handoff 回调
|
|
292
|
+
if self.before_handoff_callback:
|
|
293
|
+
override, duration, err = await self._run_hook(self.before_handoff_callback, "before_handoff_callback", ctx, target_agent, tool_call)
|
|
294
|
+
if err:
|
|
295
|
+
yield Event(agent=self.name, type="error", data={"hook": "before_handoff", "error": str(err), "duration": duration})
|
|
296
|
+
if self.fail_fast_on_hook_error:
|
|
297
|
+
return
|
|
298
|
+
elif override is not None:
|
|
299
|
+
target_agent = override
|
|
300
|
+
|
|
301
|
+
# 保持 tool-call 消息链完整,避免后续 Agent 看到未闭合的 tool_call
|
|
302
|
+
# 导致模型输出空内容 (content=None)。
|
|
303
|
+
ctx.add_tool_result(tool_call.id, f"Handoff to {target_agent}")
|
|
304
|
+
|
|
305
|
+
# after_handoff 回调
|
|
306
|
+
if self.after_handoff_callback:
|
|
307
|
+
_, duration, err = await self._run_hook(self.after_handoff_callback, "after_handoff_callback", ctx, target_agent)
|
|
308
|
+
if err:
|
|
309
|
+
yield Event(agent=self.name, type="error", data={"hook": "after_handoff", "error": str(err), "duration": duration})
|
|
310
|
+
if self.fail_fast_on_hook_error:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
yield Event(agent=self.name, type="handoff", data={"target": target_agent})
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# before_tool 回调
|
|
317
|
+
if self.before_tool_callback:
|
|
318
|
+
override, duration, err = await self._run_hook(self.before_tool_callback, "before_tool_callback", ctx, tool, tool_call)
|
|
319
|
+
if err:
|
|
320
|
+
yield Event(agent=self.name, type="error", data={"hook": "before_tool", "error": str(err), "duration": duration})
|
|
321
|
+
if self.fail_fast_on_hook_error:
|
|
322
|
+
return
|
|
323
|
+
elif override is not None:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
# 权限检查
|
|
327
|
+
if self.permission_policy:
|
|
328
|
+
allowed = await self.permission_policy.check(tool.name, tool_call.arguments)
|
|
329
|
+
if not allowed:
|
|
330
|
+
yield Event(agent=self.name, type="permission_denied", data={"tool": tool_call.name})
|
|
331
|
+
ctx.add_tool_result(tool_call.id, "Permission denied")
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
# 执行工具
|
|
335
|
+
try:
|
|
336
|
+
result = await tool.execute(ctx, tool_call.arguments)
|
|
337
|
+
except HumanInputRequested as e:
|
|
338
|
+
# 触发挂起事件,并记录挂起的工具信息
|
|
339
|
+
ctx.state["__suspended_tool_call_id__"] = tool_call.id
|
|
340
|
+
ctx.state["__suspended_tool_name__"] = tool_call.name
|
|
341
|
+
yield Event(
|
|
342
|
+
agent=self.name,
|
|
343
|
+
type=EventType.SUSPEND_REQUESTED,
|
|
344
|
+
data={"prompt": e.prompt, "tool": tool_call.name, "tool_call_id": tool_call.id, **e.kwargs}
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
except Exception as e:
|
|
348
|
+
result = f"工具执行错误: {e}"
|
|
349
|
+
|
|
350
|
+
# after_tool 回调
|
|
351
|
+
if self.after_tool_callback:
|
|
352
|
+
hook_res, duration, err = await self._run_hook(self.after_tool_callback, "after_tool_callback", ctx, tool, result)
|
|
353
|
+
if err:
|
|
354
|
+
yield Event(agent=self.name, type="error", data={"hook": "after_tool", "error": str(err), "duration": duration})
|
|
355
|
+
if self.fail_fast_on_hook_error:
|
|
356
|
+
return
|
|
357
|
+
elif hook_res is not None:
|
|
358
|
+
result = hook_res
|
|
359
|
+
|
|
360
|
+
yield Event(agent=self.name, type="tool_result", data={"tool": tool_call.name, "result": result})
|
|
361
|
+
ctx.add_tool_result(tool_call.id, result)
|
|
362
|
+
|
|
363
|
+
if self.tool_use_behavior == "stop":
|
|
364
|
+
return
|
|
365
|
+
continue # run_llm_again
|
|
366
|
+
|
|
367
|
+
else:
|
|
368
|
+
# 最终输出
|
|
369
|
+
output = response.content
|
|
370
|
+
|
|
371
|
+
# 存储记忆
|
|
372
|
+
if self.memory and output:
|
|
373
|
+
conversation = f"User: {ctx.input}\nAssistant: {output}"
|
|
374
|
+
if self.memory_async_write:
|
|
375
|
+
# fire-and-forget:不阻塞返回,后台异步写入(更快)
|
|
376
|
+
import asyncio
|
|
377
|
+
|
|
378
|
+
async def _save_memory():
|
|
379
|
+
try:
|
|
380
|
+
await self.memory.add(conversation, user_id=ctx.user_id, agent_id=self.name)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.warning("存储记忆失败: %s", e)
|
|
383
|
+
|
|
384
|
+
asyncio.create_task(_save_memory())
|
|
385
|
+
else:
|
|
386
|
+
# 同步等待写入完成(适合需要即时读取记忆的场景)
|
|
387
|
+
try:
|
|
388
|
+
await self.memory.add(conversation, user_id=ctx.user_id, agent_id=self.name)
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.warning("存储记忆失败: %s", e)
|
|
391
|
+
|
|
392
|
+
yield Event(agent=self.name, type="final_output", data=output)
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
yield Event(agent=self.name, type="error", data=f"超过最大工具调用轮次 {self.max_tool_rounds}")
|
|
396
|
+
finally:
|
|
397
|
+
for skill in self.skills:
|
|
398
|
+
try:
|
|
399
|
+
import time
|
|
400
|
+
start_time = time.time()
|
|
401
|
+
await skill.on_unload(ctx)
|
|
402
|
+
duration = time.time() - start_time
|
|
403
|
+
logger.info(f"[{self.name}] resource_released: skill={skill.name} duration={duration:.4f}s session={ctx.session_id}")
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.error(f"[{self.name}] skill_on_unload error: skill={skill.name} error={e}")
|
|
406
|
+
|
|
407
|
+
# ------------------------------------------------------------------
|
|
408
|
+
# 辅助方法
|
|
409
|
+
# ------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
def clear_cache(self) -> None:
|
|
412
|
+
"""清空 LLM 响应缓存"""
|
|
413
|
+
if self._cache_instance is not None:
|
|
414
|
+
self._cache_instance.clear()
|
|
415
|
+
|
|
416
|
+
_cache_instance: Any = PrivateAttr(default=None)
|
|
417
|
+
|
|
418
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
419
|
+
|
|
420
|
+
def _get_cache(self):
|
|
421
|
+
"""获取或创建缓存实例(懒初始化)"""
|
|
422
|
+
if self._cache_instance is None:
|
|
423
|
+
from ..llm.cache import LLMCache
|
|
424
|
+
self._cache_instance = LLMCache(max_size=128, ttl=self.cache_ttl)
|
|
425
|
+
return self._cache_instance
|
|
426
|
+
|
|
427
|
+
def _resolve_model(self) -> BaseLLM:
|
|
428
|
+
if isinstance(self.model, BaseLLM):
|
|
429
|
+
return self.model
|
|
430
|
+
if self.model:
|
|
431
|
+
return LLMRegistry.create(self.model)
|
|
432
|
+
# 向上继承
|
|
433
|
+
ancestor = self.parent_agent
|
|
434
|
+
while ancestor:
|
|
435
|
+
if isinstance(ancestor, Agent) and ancestor.model:
|
|
436
|
+
return ancestor._resolve_model()
|
|
437
|
+
ancestor = ancestor.parent_agent
|
|
438
|
+
return LLMRegistry.create_default()
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _find_tool(tools: list[BaseTool], name: str) -> BaseTool | None:
|
|
442
|
+
for tool in tools:
|
|
443
|
+
if tool.name == name:
|
|
444
|
+
return tool
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
@staticmethod
|
|
448
|
+
def _create_handoff_tool(target: BaseAgent) -> FunctionTool:
|
|
449
|
+
async def _handler(**_kwargs: Any) -> str:
|
|
450
|
+
return f"Handoff to {target.name}"
|
|
451
|
+
|
|
452
|
+
return FunctionTool(
|
|
453
|
+
name=f"transfer_to_{target.name}",
|
|
454
|
+
description=f"将对话交给 {target.description or target.name}",
|
|
455
|
+
handler=_handler,
|
|
456
|
+
json_schema={
|
|
457
|
+
"type": "object",
|
|
458
|
+
"properties": {"reason": {"type": "string", "description": "转交原因"}},
|
|
459
|
+
},
|
|
460
|
+
)
|