sdpy-kit 0.7.5__tar.gz → 0.8.0__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.
Files changed (70) hide show
  1. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/PKG-INFO +48 -1
  2. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/README.md +41 -0
  3. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/__init__.py +22 -1
  5. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/_internal/failover.py +52 -2
  6. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/_internal/rerank_transport.py +2 -2
  7. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/_internal/sdk_client.py +90 -10
  8. sdpy_kit-0.7.5/kit/ai/_internal/_service_base.py → sdpy_kit-0.8.0/kit/ai/_internal/service_base.py +1 -1
  9. {sdpy_kit-0.7.5/kit/ai → sdpy_kit-0.8.0/kit/ai/_internal}/types.py +1 -1
  10. sdpy_kit-0.8.0/kit/ai/agent/__init__.py +127 -0
  11. sdpy_kit-0.8.0/kit/ai/agent/approval.py +66 -0
  12. sdpy_kit-0.8.0/kit/ai/agent/errors.py +119 -0
  13. sdpy_kit-0.8.0/kit/ai/agent/loop.py +622 -0
  14. sdpy_kit-0.8.0/kit/ai/agent/mcp.py +266 -0
  15. sdpy_kit-0.8.0/kit/ai/agent/registry.py +199 -0
  16. sdpy_kit-0.8.0/kit/ai/agent/session.py +150 -0
  17. sdpy_kit-0.8.0/kit/ai/agent/skill.py +362 -0
  18. sdpy_kit-0.8.0/kit/ai/agent/types.py +157 -0
  19. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/chat_service.py +109 -4
  20. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/embedding_service.py +1 -9
  21. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/image_service.py +1 -18
  22. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/messages.py +16 -0
  23. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/model_client.py +1 -1
  24. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/rerank_service.py +2 -2
  25. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/pyproject.toml +11 -0
  26. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/.gitignore +0 -0
  27. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/_internal/__init__.py +0 -0
  28. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/_internal/pool.py +0 -0
  29. /sdpy_kit-0.7.5/kit/ai/_internal/_retry_after.py → /sdpy_kit-0.8.0/kit/ai/_internal/retry_after.py +0 -0
  30. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/decision/__init__.py +0 -0
  31. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/decision/decima.py +0 -0
  32. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/decision/laya.py +0 -0
  33. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/exceptions.py +0 -0
  34. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/model_builder.py +0 -0
  35. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/profiles.py +0 -0
  36. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/ai/runner.py +0 -0
  37. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/common_kit.py +0 -0
  38. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/config/__init__.py +0 -0
  39. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/config/kit_config.py +0 -0
  40. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/config/log_config.py +0 -0
  41. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/crawl/__init__.py +0 -0
  42. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/crawl/article_io.py +0 -0
  43. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/crawl/article_models.py +0 -0
  44. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/crawl/deepseek_kit.py +0 -0
  45. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/crawl/web_article_kit.py +0 -0
  46. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/crawl/wx_article_kit.py +0 -0
  47. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/__init__.py +0 -0
  48. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/audit.py +0 -0
  49. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/base_db_kit.py +0 -0
  50. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/crud.py +0 -0
  51. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/engine.py +0 -0
  52. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/exceptions.py +0 -0
  53. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/models/__init__.py +0 -0
  54. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/models/base.py +0 -0
  55. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/mysql_kit.py +0 -0
  56. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/pg_kit.py +0 -0
  57. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/query.py +0 -0
  58. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/session.py +0 -0
  59. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/sqlite_kit.py +0 -0
  60. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/db/transaction.py +0 -0
  61. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/__init__.py +0 -0
  62. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/anydoc_kit.py +0 -0
  63. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/docx_kit.py +0 -0
  64. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/marker_kit.py +0 -0
  65. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/md2doc_kit.py +0 -0
  66. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/mineru_kit.py +0 -0
  67. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/docs/rc_docparser_kit.py +0 -0
  68. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/file_kit.py +0 -0
  69. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/license_kit.py +0 -0
  70. {sdpy_kit-0.7.5 → sdpy_kit-0.8.0}/kit/redis_kit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.7.5
3
+ Version: 0.8.0
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -22,6 +22,12 @@ Requires-Dist: pydantic-settings>=2.16.1
22
22
  Requires-Dist: pydantic>=2.14.1
23
23
  Requires-Dist: sqlalchemy~=2.0.54
24
24
  Requires-Dist: sqlmodel>=0.0.48
25
+ Provides-Extra: agent
26
+ Requires-Dist: fastmcp-slim[client]<5,>=4.1.7; extra == 'agent'
27
+ Requires-Dist: httpx>=0.28.1; extra == 'agent'
28
+ Requires-Dist: openai<4,>=3.44; extra == 'agent'
29
+ Requires-Dist: python-dotenv>=1.2.4; extra == 'agent'
30
+ Requires-Dist: pyyaml>=6.0.3; extra == 'agent'
25
31
  Provides-Extra: ai
26
32
  Requires-Dist: httpx>=0.28.1; extra == 'ai'
27
33
  Requires-Dist: openai<4,>=3.44; extra == 'ai'
@@ -97,6 +103,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
97
103
  | `kit.config` | `get_settings` / `get_default_logger` | 配置单例与统一日志 | 无 |
98
104
  | `kit.db` | `SQLiteKit` / `PgKit` / `MySqlKit` / `CRUDBase` | 统一 CRUD、原生 SQL、原生表操作、事务、表管理 | `pg` / `mysql` / `sqliteai` |
99
105
  | `kit.ai` | `AI`(同步门面)/ 4 个原生异步 Service | 对话 / 向量化 / 重排序 / 文生图 | `ai` |
106
+ | `kit.ai.agent` | `Agent` / `ToolRegistry` / `SkillRegistry` / `McpServerConfig` / `Session` | MCP client(HTTP / stdio)+ Skill 包加载 + 模型驱动工具循环 | `agent` |
100
107
  | `kit.redis_kit` | `RedisKit` | Redis string / hash / list / set / zset | `redis` |
101
108
  | `kit.file_kit` | `FileKit` | 文件与目录读写、移动、遍历 | 无 |
102
109
  | `kit.common_kit` | `CommonKit` | 雪花 ID、时间戳 ID、时间格式化 | 无 |
@@ -140,8 +147,48 @@ uv add sdpy-kit
140
147
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
141
148
  | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
142
149
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
150
+ | `agent` | Agent 能力:MCP client(fastmcp-slim)+ Skill 加载 + 工具循环(含 ai 三项依赖与 pyyaml) | `pip install "sdpy-kit[agent]"` |
143
151
  | `recommended` | 常用能力集合(DB / 缓存 / AI / 文档 / 抓取);不含重依赖(laya / decima)与 Marker(0.7.0 起暂停发布) | `pip install "sdpy-kit[recommended]"` |
144
152
 
153
+ ## Agent 快速开始(extra `agent`)
154
+
155
+ ```python
156
+ import asyncio
157
+
158
+ from kit.ai.agent import Agent, McpServerConfig, SkillRegistry
159
+
160
+
161
+ async def main() -> None:
162
+ skills = SkillRegistry()
163
+ skills.load("./skills") # 一级子目录含 SKILL.md 即注册
164
+
165
+ agent = Agent(
166
+ skills=skills,
167
+ servers=[
168
+ McpServerConfig(
169
+ name="filesystem",
170
+ transport="stdio",
171
+ command="npx",
172
+ args=["-y", "@modelcontextprotocol/server-filesystem", "./data"],
173
+ )
174
+ ],
175
+ system_prompt="你是一个谨慎的文档助手。",
176
+ )
177
+
178
+ async with agent: # 建连一次,可多次 run()
179
+ result = await agent.run("总结 data 目录里的文件")
180
+ print(result.text)
181
+
182
+
183
+ asyncio.run(main())
184
+ ```
185
+
186
+ - 模型档案沿用 `kit.ai`(`config/ai_models.json`),首轮可 failover,成功后锁定档案;
187
+ - Skill 只读工具:`read_skill` / `list_skill_files` / `read_skill_file`(路径越界拒绝);
188
+ - MCP 工具默认放行,skill 脚本默认拒绝;可用 `DenyAllApproval` 整体收紧;
189
+ - `timeout` / `max_turns` / `tool_timeout` 控制总预算与轮次;
190
+ - 完整说明见 `docs/agents/modules/agent-kit.md`(仓库内)。
191
+
145
192
  ## 配置(.env)
146
193
 
147
194
  kit 通过 pydantic-settings 读取项目根目录 `.env`;**同名真实环境变量优先于 `.env`**。全部变量都有默认值,按需覆盖即可。
@@ -15,6 +15,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
15
15
  | `kit.config` | `get_settings` / `get_default_logger` | 配置单例与统一日志 | 无 |
16
16
  | `kit.db` | `SQLiteKit` / `PgKit` / `MySqlKit` / `CRUDBase` | 统一 CRUD、原生 SQL、原生表操作、事务、表管理 | `pg` / `mysql` / `sqliteai` |
17
17
  | `kit.ai` | `AI`(同步门面)/ 4 个原生异步 Service | 对话 / 向量化 / 重排序 / 文生图 | `ai` |
18
+ | `kit.ai.agent` | `Agent` / `ToolRegistry` / `SkillRegistry` / `McpServerConfig` / `Session` | MCP client(HTTP / stdio)+ Skill 包加载 + 模型驱动工具循环 | `agent` |
18
19
  | `kit.redis_kit` | `RedisKit` | Redis string / hash / list / set / zset | `redis` |
19
20
  | `kit.file_kit` | `FileKit` | 文件与目录读写、移动、遍历 | 无 |
20
21
  | `kit.common_kit` | `CommonKit` | 雪花 ID、时间戳 ID、时间格式化 | 无 |
@@ -58,8 +59,48 @@ uv add sdpy-kit
58
59
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
59
60
  | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
60
61
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
62
+ | `agent` | Agent 能力:MCP client(fastmcp-slim)+ Skill 加载 + 工具循环(含 ai 三项依赖与 pyyaml) | `pip install "sdpy-kit[agent]"` |
61
63
  | `recommended` | 常用能力集合(DB / 缓存 / AI / 文档 / 抓取);不含重依赖(laya / decima)与 Marker(0.7.0 起暂停发布) | `pip install "sdpy-kit[recommended]"` |
62
64
 
65
+ ## Agent 快速开始(extra `agent`)
66
+
67
+ ```python
68
+ import asyncio
69
+
70
+ from kit.ai.agent import Agent, McpServerConfig, SkillRegistry
71
+
72
+
73
+ async def main() -> None:
74
+ skills = SkillRegistry()
75
+ skills.load("./skills") # 一级子目录含 SKILL.md 即注册
76
+
77
+ agent = Agent(
78
+ skills=skills,
79
+ servers=[
80
+ McpServerConfig(
81
+ name="filesystem",
82
+ transport="stdio",
83
+ command="npx",
84
+ args=["-y", "@modelcontextprotocol/server-filesystem", "./data"],
85
+ )
86
+ ],
87
+ system_prompt="你是一个谨慎的文档助手。",
88
+ )
89
+
90
+ async with agent: # 建连一次,可多次 run()
91
+ result = await agent.run("总结 data 目录里的文件")
92
+ print(result.text)
93
+
94
+
95
+ asyncio.run(main())
96
+ ```
97
+
98
+ - 模型档案沿用 `kit.ai`(`config/ai_models.json`),首轮可 failover,成功后锁定档案;
99
+ - Skill 只读工具:`read_skill` / `list_skill_files` / `read_skill_file`(路径越界拒绝);
100
+ - MCP 工具默认放行,skill 脚本默认拒绝;可用 `DenyAllApproval` 整体收紧;
101
+ - `timeout` / `max_turns` / `tool_timeout` 控制总预算与轮次;
102
+ - 完整说明见 `docs/agents/modules/agent-kit.md`(仓库内)。
103
+
63
104
  ## 配置(.env)
64
105
 
65
106
  kit 通过 pydantic-settings 读取项目根目录 `.env`;**同名真实环境变量优先于 `.env`**。全部变量都有默认值,按需覆盖即可。
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.7.5"
26
+ __version__ = "0.8.0"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -32,6 +32,9 @@ Author: Clark Chang
32
32
  同步门面与决策引擎:
33
33
  - AI / run_sync / reload_config / shutdown / ReloadResult
34
34
  - DecimaKit / LayaKit(extra ``decima`` / ``laya``,不进 ``__all__``)
35
+
36
+ Agent(extra ``agent``,不进 ``__all__``):
37
+ - Agent / ToolRegistry / Session / McpServerConfig
35
38
  """
36
39
 
37
40
  from importlib import import_module
@@ -73,6 +76,12 @@ _EXPORTS: dict[str, str] = {
73
76
  "DecimaError": "kit.ai.decision",
74
77
  "LayaKit": "kit.ai.decision",
75
78
  "LayaError": "kit.ai.decision",
79
+ # Agent 能力(extra ``agent``)只支持显式导入,不进 __all__:
80
+ # ``from kit.ai import *`` 不应在基础安装下触发 fastmcp。
81
+ "Agent": "kit.ai.agent",
82
+ "ToolRegistry": "kit.ai.agent",
83
+ "Session": "kit.ai.agent",
84
+ "McpServerConfig": "kit.ai.agent",
76
85
  }
77
86
 
78
87
 
@@ -86,4 +95,16 @@ def __getattr__(name: str) -> Any:
86
95
 
87
96
 
88
97
  def __dir__() -> list[str]:
89
- return sorted(set(__all__) | {"DecimaError", "DecimaKit", "LayaError", "LayaKit"})
98
+ return sorted(
99
+ set(__all__)
100
+ | {
101
+ "DecimaError",
102
+ "DecimaKit",
103
+ "LayaError",
104
+ "LayaKit",
105
+ "Agent",
106
+ "ToolRegistry",
107
+ "Session",
108
+ "McpServerConfig",
109
+ }
110
+ )
@@ -34,14 +34,22 @@ Author: Clark Chang
34
34
 
35
35
  import asyncio
36
36
  import time
37
- from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
37
+ from collections.abc import (
38
+ AsyncGenerator,
39
+ AsyncIterator,
40
+ Awaitable,
41
+ Callable,
42
+ Mapping,
43
+ Sequence,
44
+ )
38
45
  from typing import Any, Final, TypeVar, cast
39
46
 
40
47
  from kit.ai._internal.pool import AIServicePool
41
- from kit.ai.chat_service import ChatService, StreamStats
48
+ from kit.ai.chat_service import ChatService, StreamStats, ToolSpecLike
42
49
  from kit.ai.embedding_service import EmbeddingService
43
50
  from kit.ai.exceptions import AIAggregateError, AIError, AIRequestError, AITimeoutError
44
51
  from kit.ai.image_service import ImageResult, ImageService
52
+ from kit.ai.messages import ChatReply
45
53
  from kit.ai.model_builder import Service
46
54
  from kit.ai.rerank_service import RerankHit, RerankService
47
55
 
@@ -68,6 +76,13 @@ class _FailoverBase:
68
76
  def __init__(self, pool: AIServicePool, model_type: str) -> None:
69
77
  self._pool: AIServicePool = pool
70
78
  self._model_type: str = model_type
79
+ # 最近一次成功的档案名(工具循环据此锁定档案,避免中途换模型)
80
+ self._last_profile: str | None = None
81
+
82
+ @property
83
+ def last_profile(self) -> str | None:
84
+ """最近一次成功调用使用的档案名;从未成功时为 None。"""
85
+ return self._last_profile
71
86
 
72
87
  def _aggregate_error(
73
88
  self,
@@ -146,6 +161,7 @@ class _FailoverBase:
146
161
  )
147
162
  continue
148
163
  self._pool.mark_success(self._model_type, name)
164
+ self._last_profile = name
149
165
  return result
150
166
  raise self._aggregate_error(
151
167
  label, tried, errors, budget_exhausted=budget_exhausted
@@ -194,6 +210,40 @@ class FailoverChatService(_FailoverBase):
194
210
 
195
211
  return await self._attempt("对话", _run, deadline=deadline)
196
212
 
213
+ async def create_with_tools(
214
+ self,
215
+ messages: list[dict[str, Any]],
216
+ tools: Sequence[ToolSpecLike | Mapping[str, Any]],
217
+ *,
218
+ tool_choice: str | dict[str, Any] | None = None,
219
+ temperature: float | None = None,
220
+ max_tokens: int | None = None,
221
+ thinking: bool | None = None,
222
+ stats: StreamStats | None = None,
223
+ deadline: float | None = None,
224
+ ) -> ChatReply:
225
+ """带工具定义的对话(链式故障切换)。
226
+
227
+ 仅第 1 轮使用本方法:成功后调用方可读 :attr:`last_profile` 锁定档案,
228
+ 后续轮次改用单档案 ``ChatService``,不再切换候选。
229
+ """
230
+
231
+ async def _run(service: Service) -> ChatReply:
232
+ if stats is not None:
233
+ stats.reset()
234
+ return await cast(ChatService, service).create_with_tools(
235
+ messages,
236
+ tools,
237
+ tool_choice=tool_choice,
238
+ temperature=temperature,
239
+ max_tokens=max_tokens,
240
+ thinking=thinking,
241
+ stats=stats,
242
+ deadline=deadline,
243
+ )
244
+
245
+ return await self._attempt("工具对话", _run, deadline=deadline)
246
+
197
247
  async def create_once(
198
248
  self,
199
249
  prompt: str,
@@ -37,10 +37,10 @@ except ImportError as e:
37
37
  "kit.ai 需要可选依赖,请安装:pip install sdpy-kit[ai]"
38
38
  ) from e
39
39
 
40
- from kit.ai._internal._retry_after import MIN_RETRY_BUDGET, parse_retry_after
40
+ from kit.ai._internal.retry_after import MIN_RETRY_BUDGET, parse_retry_after
41
+ from kit.ai._internal.types import JSONObject
41
42
  from kit.ai.exceptions import AIRequestError, AIResponseError, AITimeoutError
42
43
  from kit.ai.profiles import ModelProfile
43
- from kit.ai.types import JSONObject
44
44
 
45
45
  __all__ = ["RerankTransport"]
46
46
 
@@ -46,16 +46,17 @@ except ImportError as e:
46
46
  "kit.ai 需要可选依赖,请安装:pip install sdpy-kit[ai]"
47
47
  ) from e
48
48
 
49
- from kit.ai._internal._retry_after import MIN_RETRY_BUDGET, parse_retry_after
50
- from kit.ai._internal._service_base import LoggingServiceBase
49
+ from kit.ai._internal.retry_after import MIN_RETRY_BUDGET, parse_retry_after
50
+ from kit.ai._internal.service_base import LoggingServiceBase
51
+ from kit.ai._internal.types import JSONObject
51
52
  from kit.ai.exceptions import (
52
53
  AIError,
53
54
  AIRequestError,
54
55
  AIResponseError,
55
56
  AITimeoutError,
56
57
  )
58
+ from kit.ai.messages import ChatReply, ToolCall
57
59
  from kit.ai.profiles import ModelProfile, resolve_max_tokens
58
- from kit.ai.types import JSONObject
59
60
  from kit.config.log_config import get_default_logger
60
61
 
61
62
  __all__ = ["SdkClient", "SdkServiceBase"]
@@ -101,6 +102,41 @@ def _reasoning_of(holder: Any) -> str:
101
102
  return ""
102
103
 
103
104
 
105
+ def _parse_tool_calls(message: Any) -> list[ToolCall]:
106
+ """把 SDK 的 ``message.tool_calls`` 收敛为 kit 的 ``ToolCall`` 列表。
107
+
108
+ ``arguments`` 在 OpenAI 协议里是 JSON 字符串;部分兼容端点回 dict,
109
+ 统一序列化为字符串(``ToolCall`` 的契约就是字符串)。
110
+ """
111
+ raw = getattr(message, "tool_calls", None)
112
+ if raw is None:
113
+ raw = _extra_of(message, "tool_calls")
114
+ if not raw:
115
+ return []
116
+ if not isinstance(raw, list):
117
+ raise AIResponseError("tool_calls 类型异常")
118
+ calls: list[ToolCall] = []
119
+ for index, item in enumerate(raw):
120
+ function = getattr(item, "function", None)
121
+ name = getattr(function, "name", None)
122
+ arguments = getattr(function, "arguments", None)
123
+ call_id = getattr(item, "id", None)
124
+ if not isinstance(name, str) or not name:
125
+ raise AIResponseError(f"tool_calls[{index}] 缺少 function.name")
126
+ if arguments is None:
127
+ arguments = "{}"
128
+ elif not isinstance(arguments, str):
129
+ arguments = json.dumps(arguments, ensure_ascii=False)
130
+ calls.append(
131
+ ToolCall(
132
+ id=str(call_id or f"call_{index}"),
133
+ type="function",
134
+ function={"name": name, "arguments": arguments},
135
+ )
136
+ )
137
+ return calls
138
+
139
+
104
140
  class SdkClient:
105
141
  """openai SDK 客户端(全异步,单实例复用)。
106
142
 
@@ -181,11 +217,13 @@ class SdkClient:
181
217
 
182
218
  def _chat_params(
183
219
  self,
184
- messages: list[dict[str, str]],
220
+ messages: list[dict[str, Any]],
185
221
  extra_body: JSONObject | None,
186
222
  response_format: JSONObject | None,
187
223
  temperature: float | None,
188
224
  max_tokens: int | None,
225
+ tools: list[dict[str, Any]] | None = None,
226
+ tool_choice: str | dict[str, Any] | None = None,
189
227
  ) -> dict[str, Any]:
190
228
  params: dict[str, Any] = {
191
229
  "model": self._profile.model,
@@ -198,23 +236,39 @@ class SdkClient:
198
236
  params["max_tokens"] = effective_max_tokens
199
237
  if response_format is not None:
200
238
  params["response_format"] = response_format
239
+ if tools:
240
+ params["tools"] = tools
241
+ if tool_choice is not None:
242
+ params["tool_choice"] = tool_choice
201
243
  if extra_body:
202
244
  params["extra_body"] = extra_body
203
245
  return params
204
246
 
205
- async def chat(
247
+ async def chat_raw(
206
248
  self,
207
249
  *,
208
- messages: list[dict[str, str]],
250
+ messages: list[dict[str, Any]],
251
+ tools: list[dict[str, Any]] | None = None,
252
+ tool_choice: str | dict[str, Any] | None = None,
209
253
  extra_body: JSONObject | None = None,
210
254
  response_format: JSONObject | None = None,
211
255
  temperature: float | None = None,
212
256
  max_tokens: int | None = None,
213
257
  deadline: float | None = None,
214
- ) -> tuple[str | None, str, str | None]:
215
- """非流式对话,返回 ``(正文, 推理文本, finish_reason)``。"""
258
+ ) -> ChatReply:
259
+ """非流式对话,返回完整回复(含 ``tool_calls``)。
260
+
261
+ ``tools`` 必须是已包装好的 OpenAI 结构(``type=function`` +
262
+ ``function.{name,description,parameters}``),字段改名由服务层完成。
263
+ """
216
264
  params = self._chat_params(
217
- messages, extra_body, response_format, temperature, max_tokens
265
+ messages,
266
+ extra_body,
267
+ response_format,
268
+ temperature,
269
+ max_tokens,
270
+ tools=tools,
271
+ tool_choice=tool_choice,
218
272
  )
219
273
  resp = await self._request(
220
274
  lambda: self._client.chat.completions.create(
@@ -233,7 +287,33 @@ class SdkClient:
233
287
  if content is not None and not isinstance(content, str):
234
288
  raise AIResponseError(f"content 类型异常: {type(content).__name__}")
235
289
  finish = getattr(choice, "finish_reason", None)
236
- return content, _reasoning_of(message), finish
290
+ return ChatReply(
291
+ content=content,
292
+ reasoning=_reasoning_of(message),
293
+ finish_reason=finish if isinstance(finish, str) else None,
294
+ tool_calls=_parse_tool_calls(message),
295
+ )
296
+
297
+ async def chat(
298
+ self,
299
+ *,
300
+ messages: list[dict[str, Any]],
301
+ extra_body: JSONObject | None = None,
302
+ response_format: JSONObject | None = None,
303
+ temperature: float | None = None,
304
+ max_tokens: int | None = None,
305
+ deadline: float | None = None,
306
+ ) -> tuple[str | None, str, str | None]:
307
+ """非流式对话,返回 ``(正文, 推理文本, finish_reason)``。"""
308
+ reply = await self.chat_raw(
309
+ messages=messages,
310
+ extra_body=extra_body,
311
+ response_format=response_format,
312
+ temperature=temperature,
313
+ max_tokens=max_tokens,
314
+ deadline=deadline,
315
+ )
316
+ return reply.content, reply.reasoning, reply.finish_reason
237
317
 
238
318
  async def chat_stream(
239
319
  self,
@@ -31,9 +31,9 @@ from collections.abc import Iterator
31
31
  from contextlib import contextmanager
32
32
  from typing import ClassVar
33
33
 
34
+ from kit.ai._internal.types import JSONValue
34
35
  from kit.ai.exceptions import AIError, AIResponseError
35
36
  from kit.ai.profiles import ModelProfile
36
- from kit.ai.types import JSONValue
37
37
  from kit.config.log_config import get_default_logger
38
38
 
39
39
  __all__ = ["LoggingServiceBase", "safe_get"]
@@ -17,7 +17,7 @@ Project: sdpy
17
17
  Author: Clark Chang
18
18
  """
19
19
 
20
- """kit/ai 公共类型别名(PEP 695 type 语句,递归 JSON 类型)"""
20
+ """kit/ai 内部 JSON 类型别名(PEP 695 type 语句,递归 JSON 类型)"""
21
21
 
22
22
  # 递归 JSON 值类型:标量 | 数组 | 对象
23
23
  type JSONScalar = str | int | float | bool | None
@@ -0,0 +1,127 @@
1
+ """
2
+ Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ Project: sdpy
17
+ Author: Clark Chang
18
+ """
19
+
20
+ # date: 2026-11-10
21
+ """kit.ai.agent - MCP + Skill + 工具循环(PEP 562 延迟导出)。
22
+
23
+ ``import kit.ai.agent`` 本身不加载任何可选依赖;访问 ``Agent`` /
24
+ ``McpConnection`` 时才检查 fastmcp 是否安装,缺依赖报错会提示
25
+ ``pip install sdpy-kit[agent]``。
26
+ """
27
+
28
+ from importlib import import_module, util
29
+ from typing import Any
30
+
31
+ __all__ = [
32
+ "Agent",
33
+ "AgentConfigError",
34
+ "AgentEmptyReplyError",
35
+ "AgentError",
36
+ "AgentModelError",
37
+ "AgentResult",
38
+ "AgentTimeoutError",
39
+ "AgentTruncatedError",
40
+ "ApprovalDeniedError",
41
+ "ApprovalHook",
42
+ "ApprovalRequest",
43
+ "DefaultApproval",
44
+ "DenyAllApproval",
45
+ "DuplicateSkillError",
46
+ "MaxTurnsExceeded",
47
+ "McpConnection",
48
+ "McpConnectionError",
49
+ "McpServerConfig",
50
+ "SkillError",
51
+ "SkillNotFoundError",
52
+ "SkillPackage",
53
+ "SkillRegistry",
54
+ "SkillScript",
55
+ "SkillValidationError",
56
+ "Session",
57
+ "ToolCallRecord",
58
+ "ToolExecutionError",
59
+ "ToolNotFoundError",
60
+ "ToolRegistry",
61
+ "ToolResult",
62
+ "ToolSpec",
63
+ "load_servers",
64
+ ]
65
+
66
+ # 符号 → (模块, 模块内属性名);模块内属性名缺省与符号同名
67
+ _EXPORTS: dict[str, tuple[str, str]] = {
68
+ "Agent": ("kit.ai.agent.loop", "Agent"),
69
+ "AgentResult": ("kit.ai.agent.types", "AgentResult"),
70
+ "ToolSpec": ("kit.ai.agent.types", "ToolSpec"),
71
+ "ToolResult": ("kit.ai.agent.types", "ToolResult"),
72
+ "ToolCallRecord": ("kit.ai.agent.types", "ToolCallRecord"),
73
+ "McpServerConfig": ("kit.ai.agent.types", "McpServerConfig"),
74
+ "load_servers": ("kit.ai.agent.types", "load_servers"),
75
+ "ToolRegistry": ("kit.ai.agent.registry", "ToolRegistry"),
76
+ "Session": ("kit.ai.agent.session", "Session"),
77
+ "ApprovalHook": ("kit.ai.agent.approval", "ApprovalHook"),
78
+ "ApprovalRequest": ("kit.ai.agent.approval", "ApprovalRequest"),
79
+ "DefaultApproval": ("kit.ai.agent.approval", "DefaultApproval"),
80
+ "DenyAllApproval": ("kit.ai.agent.approval", "DenyAllApproval"),
81
+ "SkillPackage": ("kit.ai.agent.skill", "SkillPackage"),
82
+ "SkillScript": ("kit.ai.agent.skill", "SkillScript"),
83
+ "SkillRegistry": ("kit.ai.agent.skill", "SkillRegistry"),
84
+ "McpConnection": ("kit.ai.agent.mcp", "McpConnection"),
85
+ "AgentError": ("kit.ai.agent.errors", "AgentError"),
86
+ "AgentConfigError": ("kit.ai.agent.errors", "AgentConfigError"),
87
+ "AgentModelError": ("kit.ai.agent.errors", "AgentModelError"),
88
+ "AgentTimeoutError": ("kit.ai.agent.errors", "AgentTimeoutError"),
89
+ "AgentEmptyReplyError": ("kit.ai.agent.errors", "AgentEmptyReplyError"),
90
+ "AgentTruncatedError": ("kit.ai.agent.errors", "AgentTruncatedError"),
91
+ "MaxTurnsExceeded": ("kit.ai.agent.errors", "MaxTurnsExceeded"),
92
+ "ApprovalDeniedError": ("kit.ai.agent.errors", "ApprovalDeniedError"),
93
+ "ToolNotFoundError": ("kit.ai.agent.errors", "ToolNotFoundError"),
94
+ "ToolExecutionError": ("kit.ai.agent.errors", "ToolExecutionError"),
95
+ "McpConnectionError": ("kit.ai.agent.errors", "McpConnectionError"),
96
+ "SkillError": ("kit.ai.agent.errors", "SkillError"),
97
+ "SkillValidationError": ("kit.ai.agent.errors", "SkillValidationError"),
98
+ "SkillNotFoundError": ("kit.ai.agent.errors", "SkillNotFoundError"),
99
+ "DuplicateSkillError": ("kit.ai.agent.errors", "DuplicateSkillError"),
100
+ }
101
+
102
+ # 需要 fastmcp 的符号:在导入前显式守卫,报错必须指向 [agent]
103
+ _GUARDED = frozenset({"Agent", "McpConnection"})
104
+
105
+
106
+ def _require_fastmcp(name: str) -> None:
107
+ if util.find_spec("fastmcp") is not None:
108
+ return
109
+ raise ImportError(
110
+ f"kit.ai.agent.{name} 需要可选依赖,请安装:pip install sdpy-kit[agent]"
111
+ )
112
+
113
+
114
+ def __getattr__(name: str) -> Any:
115
+ target = _EXPORTS.get(name)
116
+ if target is None:
117
+ raise AttributeError(f"module 'kit.ai.agent' has no attribute {name!r}")
118
+ if name in _GUARDED:
119
+ _require_fastmcp(name)
120
+ module, attribute = target
121
+ value = getattr(import_module(module), attribute)
122
+ globals()[name] = value
123
+ return value
124
+
125
+
126
+ def __dir__() -> list[str]:
127
+ return sorted(__all__)
@@ -0,0 +1,66 @@
1
+ """
2
+ Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ Project: sdpy
17
+ Author: Clark Chang
18
+ """
19
+
20
+ # date: 2026-11-10
21
+ """工具调用审批 Hook。
22
+
23
+ 默认策略:MCP 工具放行(用户显式配置 server 才引入,信任边界在配置层);
24
+ skill 脚本拒绝(``subprocess`` 执行任意代码,v1 不开放该工具)。
25
+ """
26
+
27
+ from dataclasses import dataclass
28
+ from typing import Any, Literal, Protocol
29
+
30
+ __all__ = [
31
+ "ApprovalHook",
32
+ "ApprovalRequest",
33
+ "DefaultApproval",
34
+ "DenyAllApproval",
35
+ ]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ApprovalRequest:
40
+ """一次待审批的工具调用。"""
41
+
42
+ kind: Literal["mcp_tool", "skill_script"]
43
+ target: str
44
+ arguments: dict[str, Any]
45
+
46
+
47
+ class ApprovalHook(Protocol):
48
+ """审批 Hook:返回 True 放行,False 拒绝。"""
49
+
50
+ async def __call__(self, request: ApprovalRequest) -> bool:
51
+ """判断是否放行。"""
52
+ ...
53
+
54
+
55
+ class DefaultApproval:
56
+ """默认审批:MCP 工具放行,skill 脚本拒绝。"""
57
+
58
+ async def __call__(self, request: ApprovalRequest) -> bool:
59
+ return request.kind == "mcp_tool"
60
+
61
+
62
+ class DenyAllApproval:
63
+ """全部拒绝(最严格策略)。"""
64
+
65
+ async def __call__(self, request: ApprovalRequest) -> bool:
66
+ return False