ni.agentkit 0.3.1__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.
Files changed (72) hide show
  1. agentkit/__init__.py +57 -0
  2. agentkit/_cli.py +72 -0
  3. agentkit/agents/__init__.py +0 -0
  4. agentkit/agents/agent.py +364 -0
  5. agentkit/agents/base_agent.py +59 -0
  6. agentkit/agents/orchestrators.py +62 -0
  7. agentkit/docs/Architecture.md +536 -0
  8. agentkit/docs/QuickStart.md +806 -0
  9. agentkit/docs/README.md +119 -0
  10. agentkit/docs/Reference.md +576 -0
  11. agentkit/docs/TestReport.md +80 -0
  12. agentkit/examples/__init__.py +0 -0
  13. agentkit/examples/ollama/01_basic_chat.py +34 -0
  14. agentkit/examples/ollama/02_tool_calling.py +70 -0
  15. agentkit/examples/ollama/03_skill_usage.py +86 -0
  16. agentkit/examples/ollama/04_multi_agent.py +84 -0
  17. agentkit/examples/ollama/05_guardrail.py +101 -0
  18. agentkit/examples/ollama/06_orchestration.py +123 -0
  19. agentkit/examples/ollama/07_sync_async_stream.py +180 -0
  20. agentkit/examples/ollama/08_memory.py +201 -0
  21. agentkit/examples/ollama/README.md +51 -0
  22. agentkit/examples/ollama/__init__.py +0 -0
  23. agentkit/examples/quickstart.py +204 -0
  24. agentkit/examples/standard/01_basic_chat.py +33 -0
  25. agentkit/examples/standard/02_tool_calling.py +70 -0
  26. agentkit/examples/standard/03_skill_usage.py +89 -0
  27. agentkit/examples/standard/04_multi_agent.py +87 -0
  28. agentkit/examples/standard/05_guardrail.py +104 -0
  29. agentkit/examples/standard/06_orchestration.py +122 -0
  30. agentkit/examples/standard/07_sync_async_stream.py +171 -0
  31. agentkit/examples/standard/08_memory.py +140 -0
  32. agentkit/examples/standard/README.md +31 -0
  33. agentkit/examples/standard/__init__.py +0 -0
  34. agentkit/examples/test_ollama.py +272 -0
  35. agentkit/llm/__init__.py +0 -0
  36. agentkit/llm/adapters/__init__.py +0 -0
  37. agentkit/llm/adapters/anthropic_adapter.py +192 -0
  38. agentkit/llm/adapters/google_adapter.py +184 -0
  39. agentkit/llm/adapters/ollama_adapter.py +250 -0
  40. agentkit/llm/adapters/openai_adapter.py +183 -0
  41. agentkit/llm/adapters/openai_compatible.py +35 -0
  42. agentkit/llm/base.py +66 -0
  43. agentkit/llm/cache.py +121 -0
  44. agentkit/llm/middleware.py +133 -0
  45. agentkit/llm/registry.py +138 -0
  46. agentkit/llm/types.py +191 -0
  47. agentkit/memory/__init__.py +0 -0
  48. agentkit/memory/base.py +47 -0
  49. agentkit/memory/mem0_provider.py +48 -0
  50. agentkit/runner/__init__.py +0 -0
  51. agentkit/runner/context.py +47 -0
  52. agentkit/runner/events.py +36 -0
  53. agentkit/runner/runner.py +105 -0
  54. agentkit/safety/__init__.py +0 -0
  55. agentkit/safety/guardrails.py +55 -0
  56. agentkit/safety/permissions.py +41 -0
  57. agentkit/skills/__init__.py +0 -0
  58. agentkit/skills/loader.py +90 -0
  59. agentkit/skills/models.py +106 -0
  60. agentkit/skills/registry.py +48 -0
  61. agentkit/tools/__init__.py +0 -0
  62. agentkit/tools/base_tool.py +44 -0
  63. agentkit/tools/function_tool.py +118 -0
  64. agentkit/tools/skill_toolset.py +199 -0
  65. agentkit/utils/__init__.py +0 -0
  66. agentkit/utils/schema.py +83 -0
  67. ni_agentkit-0.3.1.dist-info/METADATA +157 -0
  68. ni_agentkit-0.3.1.dist-info/RECORD +72 -0
  69. ni_agentkit-0.3.1.dist-info/WHEEL +5 -0
  70. ni_agentkit-0.3.1.dist-info/entry_points.txt +2 -0
  71. ni_agentkit-0.3.1.dist-info/licenses/LICENSE +21 -0
  72. ni_agentkit-0.3.1.dist-info/top_level.txt +1 -0
agentkit/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ agentkit — Python 原生 Agent 框架,内置一等公民 Skill 支持
3
+
4
+ 用法:
5
+ from agentkit import Agent, Runner, function_tool
6
+ from agentkit import Skill, load_skill_from_dir
7
+ from agentkit import LLMRegistry, LLMConfig
8
+ """
9
+ from .agents.agent import Agent
10
+ from .agents.base_agent import BaseAgent
11
+ from .agents.orchestrators import LoopAgent, ParallelAgent, SequentialAgent
12
+ from .llm.base import BaseLLM
13
+ from .llm.registry import LLMRegistry
14
+ from .llm.types import LLMConfig, LLMResponse, Message, ToolCall, ToolDefinition
15
+ from .memory.base import BaseMemoryProvider, Memory
16
+ from .runner.events import Event, RunResult
17
+ from .runner.runner import Runner
18
+ from .safety.guardrails import (
19
+ GuardrailResult,
20
+ InputGuardrail,
21
+ OutputGuardrail,
22
+ input_guardrail,
23
+ output_guardrail,
24
+ )
25
+ from .safety.permissions import PermissionPolicy
26
+ from .skills.loader import load_skill_from_dir
27
+ from .skills.models import Skill, SkillFrontmatter, SkillResources
28
+ from .skills.registry import SkillRegistry
29
+ from .tools.base_tool import BaseTool, BaseToolset
30
+ from .tools.function_tool import FunctionTool, function_tool
31
+
32
+ __version__ = "0.3.1"
33
+
34
+
35
+ def get_docs_dir() -> str:
36
+ """返回 agentkit 文档目录的绝对路径"""
37
+ import os
38
+ return os.path.join(os.path.dirname(__file__), "docs")
39
+
40
+
41
+ def get_examples_dir() -> str:
42
+ """返回 agentkit 示例目录的绝对路径"""
43
+ import os
44
+ return os.path.join(os.path.dirname(__file__), "examples")
45
+
46
+
47
+ __all__ = [
48
+ "Agent", "BaseAgent", "SequentialAgent", "ParallelAgent", "LoopAgent",
49
+ "Runner", "RunResult", "Event",
50
+ "BaseLLM", "LLMConfig", "LLMRegistry", "LLMResponse", "Message", "ToolCall", "ToolDefinition",
51
+ "BaseTool", "BaseToolset", "FunctionTool", "function_tool",
52
+ "Skill", "SkillFrontmatter", "SkillResources", "SkillRegistry", "load_skill_from_dir",
53
+ "GuardrailResult", "InputGuardrail", "OutputGuardrail", "PermissionPolicy",
54
+ "input_guardrail", "output_guardrail",
55
+ "BaseMemoryProvider", "Memory",
56
+ "get_docs_dir", "get_examples_dir",
57
+ ]
agentkit/_cli.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ AgentKit CLI 工具。
3
+
4
+ 安装后提供以下命令:
5
+ agentkit-docs — 显示文档目录位置,或在浏览器中打开
6
+ """
7
+ import os
8
+
9
+
10
+ def _get_docs_dir() -> str:
11
+ """返回 docs 目录的绝对路径"""
12
+ return os.path.join(os.path.dirname(__file__), "docs")
13
+
14
+
15
+ def _get_examples_dir() -> str:
16
+ """返回 examples 目录的绝对路径"""
17
+ return os.path.join(os.path.dirname(__file__), "examples")
18
+
19
+
20
+ def show_docs():
21
+ """agentkit-docs 命令入口"""
22
+ docs_dir = _get_docs_dir()
23
+ examples_dir = _get_examples_dir()
24
+
25
+ print("=" * 60)
26
+ print(" AgentKit v0.3.1 — 文档与示例")
27
+ print("=" * 60)
28
+ print()
29
+
30
+ # 文档
31
+ print("📚 文档目录:")
32
+ print(f" {docs_dir}")
33
+ print()
34
+
35
+ if os.path.isdir(docs_dir):
36
+ print(" 文件列表:")
37
+ for f in sorted(os.listdir(docs_dir)):
38
+ fpath = os.path.join(docs_dir, f)
39
+ size = os.path.getsize(fpath) / 1024
40
+ print(f" • {f:30s} ({size:.1f} KB)")
41
+ else:
42
+ print(" ⚠️ 文档目录不存在(包可能未正确安装)")
43
+
44
+ print()
45
+
46
+ # 示例
47
+ print("💡 示例目录:")
48
+ print(f" {examples_dir}")
49
+ print()
50
+
51
+ if os.path.isdir(examples_dir):
52
+ for subdir in ["standard", "ollama"]:
53
+ sub_path = os.path.join(examples_dir, subdir)
54
+ if os.path.isdir(sub_path):
55
+ files = [f for f in sorted(os.listdir(sub_path)) if f.endswith(".py") and f != "__init__.py"]
56
+ print(f" 📁 {subdir}/ ({len(files)} 个示例)")
57
+ for f in files:
58
+ print(f" • {f}")
59
+ print()
60
+
61
+ print("-" * 60)
62
+ print("快速开始:")
63
+ print(" 1. 查看文档: cat $(agentkit-docs-path)/README.md")
64
+ print(" 2. 运行示例: python -m agentkit.examples.ollama.01_basic_chat")
65
+ print(" 3. Python 中获取路径:")
66
+ print(" >>> import agentkit; print(agentkit.get_docs_dir())")
67
+ print(" >>> import agentkit; print(agentkit.get_examples_dir())")
68
+ print()
69
+
70
+
71
+ if __name__ == "__main__":
72
+ show_docs()
File without changes
@@ -0,0 +1,364 @@
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 TYPE_CHECKING, 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, LLMResponse, Message, MessageRole, ToolCall as LLMToolCall
21
+ from ..runner.events import Event
22
+ from ..skills.models import Skill
23
+ from ..tools.base_tool import BaseTool, BaseToolset, ToolUnion
24
+ from ..tools.function_tool import FunctionTool
25
+ from ..tools.skill_toolset import SkillToolset
26
+ from .base_agent import BaseAgent
27
+
28
+ if TYPE_CHECKING:
29
+ from ..memory.base import BaseMemoryProvider
30
+ from ..runner.context import RunContext
31
+ from ..safety.guardrails import InputGuardrail, OutputGuardrail
32
+ from ..safety.permissions import PermissionPolicy
33
+
34
+ logger = logging.getLogger("agentkit.agent")
35
+
36
+
37
+ class Agent(BaseAgent):
38
+ """核心 LLM Agent"""
39
+
40
+ # === LLM 配置 ===
41
+ model: Union[str, LLMConfig, BaseLLM, None] = ""
42
+ instructions: Union[str, Callable] = ""
43
+
44
+ # === 工具 & 技能 ===
45
+ tools: list[Any] = Field(default_factory=list)
46
+ skills: list[Skill] = Field(default_factory=list)
47
+
48
+ # === Agent 间协作 ===
49
+ handoffs: list[Any] = Field(default_factory=list)
50
+
51
+ # === 输入输出 ===
52
+ output_type: Optional[type] = None
53
+
54
+ # === 安全 ===
55
+ input_guardrails: list[Any] = Field(default_factory=list)
56
+ output_guardrails: list[Any] = Field(default_factory=list)
57
+ permission_policy: Optional[Any] = None
58
+
59
+ # === 记忆 ===
60
+ memory: Optional[Any] = None
61
+
62
+ # === 行为 ===
63
+ tool_use_behavior: str = "run_llm_again"
64
+ max_tool_rounds: int = 20
65
+ enable_cache: bool = True # LLM 响应缓存(默认开启,绑定 Agent 实例生命周期)
66
+ cache_ttl: int = 300 # 缓存有效期(秒)
67
+ memory_async_write: bool = True # 记忆写入是否异步(True=不阻塞, False=等写完再返回)
68
+
69
+ # === 精细回调 ===
70
+ before_model_callback: Optional[Callable] = None
71
+ after_model_callback: Optional[Callable] = None
72
+ before_tool_callback: Optional[Callable] = None
73
+ after_tool_callback: Optional[Callable] = None
74
+ on_error_callback: Optional[Callable] = None
75
+
76
+ # ------------------------------------------------------------------
77
+ # 核心方法
78
+ # ------------------------------------------------------------------
79
+
80
+ async def get_instructions(self, ctx: "RunContext") -> str:
81
+ """获取系统提示词(支持动态函数)"""
82
+ if callable(self.instructions):
83
+ result = self.instructions(ctx, self)
84
+ if inspect.isawaitable(result):
85
+ return await result
86
+ return result
87
+ return self.instructions or ""
88
+
89
+ async def get_all_tools(self, ctx: "RunContext") -> list[BaseTool]:
90
+ """汇总所有可用工具"""
91
+ all_tools: list[BaseTool] = []
92
+
93
+ # 1. 处理 tools
94
+ for tool_union in self.tools:
95
+ if isinstance(tool_union, BaseTool):
96
+ all_tools.append(tool_union)
97
+ elif isinstance(tool_union, BaseToolset):
98
+ all_tools.extend(await tool_union.get_tools(ctx))
99
+ elif callable(tool_union):
100
+ all_tools.append(FunctionTool.from_function(tool_union))
101
+
102
+ # 2. 处理 skills → SkillToolset
103
+ if self.skills:
104
+ additional = [t for t in all_tools] # 让 Skill 能看到已注册的工具
105
+ skill_toolset = SkillToolset(skills=self.skills, additional_tools=additional)
106
+ all_tools.extend(await skill_toolset.get_tools(ctx))
107
+
108
+ # 3. 处理 handoffs → transfer_to_xxx 工具
109
+ for target in self.handoffs:
110
+ if isinstance(target, BaseAgent):
111
+ all_tools.append(self._create_handoff_tool(target))
112
+
113
+ return all_tools
114
+
115
+ def as_tool(self, name: str, description: str) -> FunctionTool:
116
+ """把自己变成一个工具,供其他 Agent 调用"""
117
+ agent_ref = self
118
+
119
+ async def _invoke(**kwargs: Any) -> Any:
120
+ input_text = kwargs.get("input", "")
121
+ from ..runner.runner import Runner
122
+ result = await Runner.run(agent_ref, input=str(input_text))
123
+ return result.final_output
124
+
125
+ return FunctionTool(
126
+ name=name,
127
+ description=description,
128
+ handler=_invoke,
129
+ json_schema={
130
+ "type": "object",
131
+ "properties": {"input": {"type": "string", "description": "任务输入"}},
132
+ "required": ["input"],
133
+ },
134
+ )
135
+
136
+ # ------------------------------------------------------------------
137
+ # 核心执行循环
138
+ # ------------------------------------------------------------------
139
+
140
+ async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
141
+ round_count = 0
142
+
143
+ while round_count < self.max_tool_rounds:
144
+ round_count += 1
145
+
146
+ # 1. 构建指令
147
+ instructions = await self.get_instructions(ctx)
148
+
149
+ # 注入记忆
150
+ if self.memory:
151
+ try:
152
+ relevant = await self.memory.search(ctx.input, user_id=ctx.user_id, agent_id=self.name, limit=5)
153
+ if relevant:
154
+ mem_text = "\n".join([f"- {m.content}" for m in relevant])
155
+ instructions += f"\n\n## 相关记忆\n{mem_text}"
156
+ except Exception as e:
157
+ logger.warning("检索记忆失败: %s", e)
158
+
159
+ # 注入 Skill 列表
160
+ if self.skills:
161
+ skill_toolset = SkillToolset(skills=self.skills)
162
+ instructions += "\n\n" + skill_toolset.get_system_prompt_injection()
163
+
164
+ # 2. 获取工具
165
+ tools = await self.get_all_tools(ctx)
166
+ tool_defs = [t.to_tool_definition() for t in tools]
167
+
168
+ # 3. 构建消息
169
+ messages = [Message.system(instructions)]
170
+ messages.append(Message.user(ctx.input))
171
+
172
+ # 追加历史消息
173
+ for msg_dict in ctx.get_messages():
174
+ role = MessageRole(msg_dict.get("role", "user"))
175
+ tool_calls_raw = msg_dict.get("tool_calls", [])
176
+ tool_calls_parsed = [
177
+ LLMToolCall(id=tc["id"], name=tc["name"], arguments=tc["arguments"])
178
+ for tc in tool_calls_raw
179
+ ] if tool_calls_raw else []
180
+ messages.append(Message(
181
+ role=role,
182
+ content=msg_dict.get("content"),
183
+ tool_call_id=msg_dict.get("tool_call_id"),
184
+ tool_calls=tool_calls_parsed,
185
+ ))
186
+
187
+ # 4. before_model 回调
188
+ if self.before_model_callback:
189
+ override = await self.before_model_callback(ctx, instructions, tools)
190
+ if override is not None:
191
+ yield Event(agent=self.name, type="model_override", data=override)
192
+ return
193
+
194
+ # 5. 调用 LLM(支持缓存)
195
+ llm = self._resolve_model()
196
+ cached = False
197
+
198
+ # 检查缓存
199
+ if self.enable_cache:
200
+ cache = self._get_cache()
201
+ cached_response = cache.get(messages, tool_defs if tool_defs else None)
202
+ if cached_response is not None:
203
+ response = cached_response
204
+ cached = True
205
+
206
+ if not cached:
207
+ try:
208
+ response = await llm.generate(messages=messages, tools=tool_defs if tool_defs else None)
209
+ except Exception as e:
210
+ error_msg = str(e) or f"{type(e).__name__}: LLM 调用失败"
211
+ if self.on_error_callback:
212
+ await self.on_error_callback(ctx, e)
213
+ yield Event(agent=self.name, type="error", data=error_msg)
214
+ return
215
+
216
+ # 写入缓存
217
+ if self.enable_cache:
218
+ cache.put(messages, tool_defs if tool_defs else None, response)
219
+
220
+ # 6. after_model 回调
221
+ if self.after_model_callback:
222
+ response = (await self.after_model_callback(ctx, response)) or response
223
+
224
+ yield Event(agent=self.name, type="llm_response", data=response)
225
+
226
+ # 7. 分析响应
227
+ if response.has_tool_calls:
228
+ # ⭐ 关键:先把 assistant 的 tool_calls 加入历史,
229
+ # 这样下一轮 LLM 能看到完整的调用链(assistant→tool→assistant...)
230
+ ctx.messages.append({
231
+ "role": "assistant",
232
+ "content": response.content,
233
+ "tool_calls": [
234
+ {"id": tc.id, "name": tc.name, "arguments": tc.arguments}
235
+ for tc in response.tool_calls
236
+ ],
237
+ })
238
+
239
+ for tool_call in response.tool_calls:
240
+ tool = self._find_tool(tools, tool_call.name)
241
+ if not tool:
242
+ yield Event(agent=self.name, type="error", data=f"工具 '{tool_call.name}' 未找到")
243
+ continue
244
+
245
+ # 检查是否是 handoff
246
+ if tool_call.name.startswith("transfer_to_"):
247
+ yield Event(agent=self.name, type="handoff", data={"target": tool_call.name.replace("transfer_to_", "")})
248
+ return
249
+
250
+ # before_tool 回调
251
+ if self.before_tool_callback:
252
+ override = await self.before_tool_callback(ctx, tool, tool_call)
253
+ if override is not None:
254
+ continue
255
+
256
+ # 权限检查
257
+ if self.permission_policy:
258
+ allowed = await self.permission_policy.check(tool.name, tool_call.arguments)
259
+ if not allowed:
260
+ yield Event(agent=self.name, type="permission_denied", data={"tool": tool_call.name})
261
+ ctx.add_tool_result(tool_call.id, "Permission denied")
262
+ continue
263
+
264
+ # 执行工具
265
+ try:
266
+ result = await tool.execute(ctx, tool_call.arguments)
267
+ except Exception as e:
268
+ result = f"工具执行错误: {e}"
269
+
270
+ # after_tool 回调
271
+ if self.after_tool_callback:
272
+ result = (await self.after_tool_callback(ctx, tool, result)) or result
273
+
274
+ yield Event(agent=self.name, type="tool_result", data={"tool": tool_call.name, "result": result})
275
+ ctx.add_tool_result(tool_call.id, result)
276
+
277
+ if self.tool_use_behavior == "stop":
278
+ return
279
+ continue # run_llm_again
280
+
281
+ else:
282
+ # 最终输出
283
+ output = response.content
284
+
285
+ # 存储记忆
286
+ if self.memory and output:
287
+ conversation = f"User: {ctx.input}\nAssistant: {output}"
288
+ if self.memory_async_write:
289
+ # fire-and-forget:不阻塞返回,后台异步写入(更快)
290
+ import asyncio
291
+
292
+ async def _save_memory():
293
+ try:
294
+ await self.memory.add(conversation, user_id=ctx.user_id, agent_id=self.name)
295
+ except Exception as e:
296
+ logger.warning("存储记忆失败: %s", e)
297
+
298
+ asyncio.create_task(_save_memory())
299
+ else:
300
+ # 同步等待写入完成(适合需要即时读取记忆的场景)
301
+ try:
302
+ await self.memory.add(conversation, user_id=ctx.user_id, agent_id=self.name)
303
+ except Exception as e:
304
+ logger.warning("存储记忆失败: %s", e)
305
+
306
+ yield Event(agent=self.name, type="final_output", data=output)
307
+ return
308
+
309
+ yield Event(agent=self.name, type="error", data=f"超过最大工具调用轮次 {self.max_tool_rounds}")
310
+
311
+ # ------------------------------------------------------------------
312
+ # 辅助方法
313
+ # ------------------------------------------------------------------
314
+
315
+ def clear_cache(self) -> None:
316
+ """清空 LLM 响应缓存"""
317
+ if self._cache_instance is not None:
318
+ self._cache_instance.clear()
319
+
320
+ _cache_instance: Any = PrivateAttr(default=None)
321
+
322
+ model_config = ConfigDict(arbitrary_types_allowed=True)
323
+
324
+ def _get_cache(self):
325
+ """获取或创建缓存实例(懒初始化)"""
326
+ if self._cache_instance is None:
327
+ from ..llm.cache import LLMCache
328
+ self._cache_instance = LLMCache(max_size=128, ttl=self.cache_ttl)
329
+ return self._cache_instance
330
+
331
+ def _resolve_model(self) -> BaseLLM:
332
+ if isinstance(self.model, BaseLLM):
333
+ return self.model
334
+ if self.model:
335
+ return LLMRegistry.create(self.model)
336
+ # 向上继承
337
+ ancestor = self.parent_agent
338
+ while ancestor:
339
+ if isinstance(ancestor, Agent) and ancestor.model:
340
+ return ancestor._resolve_model()
341
+ ancestor = ancestor.parent_agent
342
+ return LLMRegistry.create_default()
343
+
344
+ @staticmethod
345
+ def _find_tool(tools: list[BaseTool], name: str) -> BaseTool | None:
346
+ for tool in tools:
347
+ if tool.name == name:
348
+ return tool
349
+ return None
350
+
351
+ @staticmethod
352
+ def _create_handoff_tool(target: BaseAgent) -> FunctionTool:
353
+ async def _handler(**kwargs: Any) -> str:
354
+ return f"Handoff to {target.name}"
355
+
356
+ return FunctionTool(
357
+ name=f"transfer_to_{target.name}",
358
+ description=f"将对话交给 {target.description or target.name}",
359
+ handler=_handler,
360
+ json_schema={
361
+ "type": "object",
362
+ "properties": {"reason": {"type": "string", "description": "转交原因"}},
363
+ },
364
+ )
@@ -0,0 +1,59 @@
1
+ """
2
+ agentkit/agents/base_agent.py — 所有 Agent 的基类
3
+
4
+ 模板方法模式:run() 是 final 的,子类实现 _run_impl()。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from abc import abstractmethod
9
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Optional
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ from ..runner.events import Event
14
+
15
+ if TYPE_CHECKING:
16
+ from ..runner.context import RunContext
17
+
18
+
19
+ class BaseAgent(BaseModel):
20
+ """所有 Agent 的基类"""
21
+
22
+ name: str
23
+ description: str = ""
24
+ parent_agent: Optional["BaseAgent"] = None
25
+ sub_agents: list["BaseAgent"] = Field(default_factory=list)
26
+ before_agent_callback: Optional[Callable] = None
27
+ after_agent_callback: Optional[Callable] = None
28
+
29
+ model_config = ConfigDict(arbitrary_types_allowed=True)
30
+
31
+ def model_post_init(self, __context: Any) -> None:
32
+ for sub in self.sub_agents:
33
+ if sub.parent_agent is not None:
34
+ raise ValueError(f"Agent '{sub.name}' 已有父 Agent")
35
+ sub.parent_agent = self
36
+
37
+ async def run(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
38
+ """运行入口 — 子类不可覆盖"""
39
+ # 1. before callback
40
+ if self.before_agent_callback:
41
+ result = await self.before_agent_callback(ctx)
42
+ if result is not None:
43
+ yield Event(agent=self.name, type="callback", data=result)
44
+ return
45
+
46
+ # 2. 核心逻辑(子类实现)
47
+ async for event in self._run_impl(ctx):
48
+ yield event
49
+
50
+ # 3. after callback
51
+ if self.after_agent_callback:
52
+ result = await self.after_agent_callback(ctx)
53
+ if result is not None:
54
+ yield Event(agent=self.name, type="callback", data=result)
55
+
56
+ @abstractmethod
57
+ async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
58
+ raise NotImplementedError
59
+ yield # type: ignore # pragma: no cover
@@ -0,0 +1,62 @@
1
+ """
2
+ agentkit/agents/sequential_agent.py — 顺序执行子 Agent
3
+ agentkit/agents/parallel_agent.py — 并行执行子 Agent
4
+ agentkit/agents/loop_agent.py — 循环执行子 Agent
5
+
6
+ 编排 Agent 合并在一个文件中。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from typing import TYPE_CHECKING, AsyncGenerator
12
+
13
+ from ..runner.events import Event
14
+ from .base_agent import BaseAgent
15
+
16
+ if TYPE_CHECKING:
17
+ from ..runner.context import RunContext
18
+
19
+
20
+ class SequentialAgent(BaseAgent):
21
+ """按顺序执行子 Agent"""
22
+
23
+ async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
24
+ for sub in self.sub_agents:
25
+ async for event in sub.run(ctx):
26
+ yield event
27
+ if event.type == "escalate":
28
+ return
29
+
30
+
31
+ class ParallelAgent(BaseAgent):
32
+ """并行执行子 Agent(分支隔离)"""
33
+
34
+ async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
35
+ async def collect(agent: BaseAgent, branch_ctx: "RunContext") -> list[Event]:
36
+ events: list[Event] = []
37
+ async for event in agent.run(branch_ctx):
38
+ events.append(event)
39
+ return events
40
+
41
+ tasks = [
42
+ collect(sub, ctx.create_branch(sub.name))
43
+ for sub in self.sub_agents
44
+ ]
45
+ results = await asyncio.gather(*tasks)
46
+ for events in results:
47
+ for event in events:
48
+ yield event
49
+
50
+
51
+ class LoopAgent(BaseAgent):
52
+ """循环执行子 Agent,直到 escalate 或达到 max_iterations"""
53
+
54
+ max_iterations: int = 10
55
+
56
+ async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
57
+ for _ in range(self.max_iterations):
58
+ for sub in self.sub_agents:
59
+ async for event in sub.run(ctx):
60
+ yield event
61
+ if event.type == "escalate":
62
+ return