zero-agent-core 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.
Files changed (104) hide show
  1. agent_core/__init__.py +525 -0
  2. agent_core/access/__init__.py +6 -0
  3. agent_core/access/context.py +45 -0
  4. agent_core/access/guard.py +40 -0
  5. agent_core/acp/README.md +236 -0
  6. agent_core/acp/__init__.py +20 -0
  7. agent_core/acp/server.py +351 -0
  8. agent_core/acp/types.py +86 -0
  9. agent_core/callbacks/__init__.py +17 -0
  10. agent_core/callbacks/manager.py +105 -0
  11. agent_core/checkpoint/__init__.py +19 -0
  12. agent_core/checkpoint/naming.py +93 -0
  13. agent_core/checkpoint/store.py +186 -0
  14. agent_core/checkpoint/travel.py +448 -0
  15. agent_core/compaction/__init__.py +57 -0
  16. agent_core/compaction/middleware.py +428 -0
  17. agent_core/compaction/prompts.py +32 -0
  18. agent_core/compaction/pruner.py +121 -0
  19. agent_core/compaction/region.py +145 -0
  20. agent_core/compaction/spill.py +81 -0
  21. agent_core/compaction/spill_tool.py +42 -0
  22. agent_core/compaction/summarizer.py +95 -0
  23. agent_core/compaction/token_limit.py +74 -0
  24. agent_core/compaction/types.py +93 -0
  25. agent_core/docs.py +15 -0
  26. agent_core/documents/__init__.py +23 -0
  27. agent_core/documents/loaders.py +79 -0
  28. agent_core/documents/splitters.py +94 -0
  29. agent_core/documents/types.py +197 -0
  30. agent_core/errors.py +81 -0
  31. agent_core/guardrails/__init__.py +19 -0
  32. agent_core/guardrails/core.py +186 -0
  33. agent_core/hitl/__init__.py +15 -0
  34. agent_core/hitl/core.py +517 -0
  35. agent_core/mcp/__init__.py +14 -0
  36. agent_core/mcp/client.py +212 -0
  37. agent_core/mcp/errors.py +26 -0
  38. agent_core/middleware/__init__.py +26 -0
  39. agent_core/middleware/base.py +283 -0
  40. agent_core/model/__init__.py +73 -0
  41. agent_core/model/catalog.py +73 -0
  42. agent_core/model/core.py +179 -0
  43. agent_core/model/governance.py +317 -0
  44. agent_core/model/providers/__init__.py +43 -0
  45. agent_core/model/providers/anthropic.py +294 -0
  46. agent_core/model/providers/common.py +28 -0
  47. agent_core/model/providers/factory.py +107 -0
  48. agent_core/model/providers/gemini.py +273 -0
  49. agent_core/model/providers/ollama.py +80 -0
  50. agent_core/model/providers/openai_compatible.py +526 -0
  51. agent_core/model/structured.py +200 -0
  52. agent_core/observability/__init__.py +21 -0
  53. agent_core/observability/core.py +311 -0
  54. agent_core/ports/__init__.py +31 -0
  55. agent_core/ports/memory.py +130 -0
  56. agent_core/ports/model_selection.py +64 -0
  57. agent_core/ports/storage.py +114 -0
  58. agent_core/prompts/__init__.py +19 -0
  59. agent_core/prompts/registry.py +236 -0
  60. agent_core/prompts/templates.py +166 -0
  61. agent_core/protocol/__init__.py +31 -0
  62. agent_core/protocol/capabilities.py +49 -0
  63. agent_core/protocol/messages.py +100 -0
  64. agent_core/protocol/runtime.py +293 -0
  65. agent_core/py.typed +1 -0
  66. agent_core/retrieval/__init__.py +40 -0
  67. agent_core/retrieval/_common.py +121 -0
  68. agent_core/retrieval/chroma.py +269 -0
  69. agent_core/retrieval/core.py +63 -0
  70. agent_core/retrieval/factory.py +72 -0
  71. agent_core/retrieval/memory.py +232 -0
  72. agent_core/retrieval/pgvector.py +239 -0
  73. agent_core/retrieval/types.py +95 -0
  74. agent_core/runtime/__init__.py +28 -0
  75. agent_core/runtime/compat.py +98 -0
  76. agent_core/runtime/react.py +888 -0
  77. agent_core/runtime/service.py +557 -0
  78. agent_core/runtime/store.py +55 -0
  79. agent_core/serialization/__init__.py +21 -0
  80. agent_core/serialization/registry.py +318 -0
  81. agent_core/skills/__init__.py +24 -0
  82. agent_core/skills/errors.py +27 -0
  83. agent_core/skills/loader.py +258 -0
  84. agent_core/skills/registry.py +205 -0
  85. agent_core/skills/types.py +56 -0
  86. agent_core/storage/__init__.py +107 -0
  87. agent_core/storage/_sqlite_utils.py +92 -0
  88. agent_core/storage/context.py +382 -0
  89. agent_core/storage/lease.py +435 -0
  90. agent_core/storage/memory.py +604 -0
  91. agent_core/storage/model_selection.py +360 -0
  92. agent_core/storage/runtime.py +899 -0
  93. agent_core/storage/session.py +904 -0
  94. agent_core/storage/workflow.py +411 -0
  95. agent_core/tools/__init__.py +5 -0
  96. agent_core/tools/executor.py +306 -0
  97. agent_core/workflow/__init__.py +25 -0
  98. agent_core/workflow/durable.py +175 -0
  99. agent_core/workflow/state_machine.py +427 -0
  100. zero_agent_core-0.2.0.dist-info/METADATA +237 -0
  101. zero_agent_core-0.2.0.dist-info/RECORD +104 -0
  102. zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
  103. zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
  104. zero_agent_core-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,212 @@
1
+ """MCP stdio 客户端适配器。
2
+
3
+ Core 提供标准 MCP 会话管理和工具调用,不包含任何 Cesium、航线或业务白名单。
4
+ 应用层可以基于 ``McpToolCaller`` 包装自己的受限客户端。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ from collections.abc import Mapping
13
+ from contextlib import AsyncExitStack
14
+ from pathlib import Path
15
+ from typing import Protocol, cast
16
+
17
+ from agent_core.mcp.errors import McpConnectionError, McpTimeoutError, McpToolError
18
+
19
+ JsonObject = dict[str, object]
20
+ _INHERITED_ENV_KEYS = frozenset(
21
+ {
22
+ "COMSPEC",
23
+ "HOME",
24
+ "LANG",
25
+ "PATH",
26
+ "PATHEXT",
27
+ "SYSTEMROOT",
28
+ "TEMP",
29
+ "TMP",
30
+ "USERPROFILE",
31
+ }
32
+ )
33
+
34
+
35
+ class McpToolCaller(Protocol):
36
+ """MCP 工具调用端口。"""
37
+
38
+ async def list_tools(self) -> list[str]: ...
39
+
40
+ async def call_tool(self, name: str, arguments: Mapping[str, object] | None = None) -> JsonObject: ...
41
+
42
+
43
+ class McpToolClient:
44
+ """按需启动 MCP stdio 服务,并在生命周期内复用同一个会话。"""
45
+
46
+ def __init__(
47
+ self,
48
+ *,
49
+ command: str,
50
+ entrypoint: Path,
51
+ timeout_seconds: float,
52
+ env: Mapping[str, str] | None = None,
53
+ ) -> None:
54
+ if timeout_seconds <= 0:
55
+ raise ValueError("timeout_seconds 必须大于 0")
56
+ self._command = command
57
+ self._entrypoint = entrypoint
58
+ self._timeout_seconds = timeout_seconds
59
+ self._env = dict(env or {})
60
+ self._session: object | None = None
61
+ self._runner_task: asyncio.Task[None] | None = None
62
+ self._ready: asyncio.Future[None] | None = None
63
+ self._close_requested: asyncio.Event | None = None
64
+ self._terminal_error: McpConnectionError | McpTimeoutError | None = None
65
+ self._start_lock = asyncio.Lock()
66
+
67
+ async def start(self) -> None:
68
+ """启动会话并等待 MCP initialize 完成。"""
69
+ if self._session is not None:
70
+ return
71
+ async with self._start_lock:
72
+ if self._session is not None:
73
+ return
74
+ if not await asyncio.to_thread(self._entrypoint.is_file):
75
+ raise McpConnectionError(f"MCP 入口不存在:{self._entrypoint}")
76
+ if self._runner_task is None or self._runner_task.done():
77
+ loop = asyncio.get_running_loop()
78
+ self._ready = loop.create_future()
79
+ self._close_requested = asyncio.Event()
80
+ self._terminal_error = None
81
+ self._runner_task = asyncio.create_task(
82
+ self._run_session(self._ready, self._close_requested),
83
+ name=f"mcp-session:{self._entrypoint.name}",
84
+ )
85
+ ready = self._ready
86
+ if ready is None:
87
+ raise McpConnectionError("MCP 会话启动状态无效")
88
+ await asyncio.shield(ready)
89
+
90
+ async def list_tools(self) -> list[str]:
91
+ """列出 MCP 服务公开的工具名称。"""
92
+ session = await self._require_session()
93
+ try:
94
+ async with asyncio.timeout(self._timeout_seconds):
95
+ result = await session.list_tools() # type: ignore[attr-defined]
96
+ except TimeoutError as exc:
97
+ raise McpTimeoutError("MCP tools/list 超时") from exc
98
+ except Exception as exc:
99
+ raise McpToolError(f"MCP tools/list 调用失败:{exc}") from exc
100
+ return [str(tool.name) for tool in result.tools]
101
+
102
+ async def call_tool(self, name: str, arguments: Mapping[str, object] | None = None) -> JsonObject:
103
+ """调用 MCP 工具并要求其返回结构化对象。"""
104
+ session = await self._require_session()
105
+ try:
106
+ async with asyncio.timeout(self._timeout_seconds):
107
+ result = await session.call_tool(name, dict(arguments or {})) # type: ignore[attr-defined]
108
+ except TimeoutError as exc:
109
+ raise McpTimeoutError(f"MCP 工具 {name} 调用超时") from exc
110
+ except Exception as exc:
111
+ raise McpToolError(f"MCP 工具 {name} 调用失败:{exc}") from exc
112
+ if result.isError:
113
+ message = self._text_content(result.content) or "服务返回 isError=true"
114
+ raise McpToolError(f"MCP 工具 {name} 失败:{message}")
115
+ structured = result.structuredContent
116
+ if isinstance(structured, dict):
117
+ return cast(JsonObject, structured)
118
+ text = self._text_content(result.content)
119
+ if not text:
120
+ raise McpToolError(f"MCP 工具 {name} 未返回可读取的内容")
121
+ try:
122
+ decoded = json.loads(text)
123
+ except json.JSONDecodeError:
124
+ return {"content": text}
125
+ if isinstance(decoded, dict):
126
+ return cast(JsonObject, decoded)
127
+ return {"content": decoded}
128
+
129
+ async def aclose(self) -> None:
130
+ """关闭 MCP 会话及其 stdio 子进程。"""
131
+ async with self._start_lock:
132
+ task = self._runner_task
133
+ close_requested = self._close_requested
134
+ self._runner_task = None
135
+ self._ready = None
136
+ self._close_requested = None
137
+ if task is None:
138
+ return
139
+ if close_requested is not None:
140
+ close_requested.set()
141
+ await asyncio.shield(task)
142
+
143
+ async def __aenter__(self) -> McpToolClient:
144
+ await self.start()
145
+ return self
146
+
147
+ async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
148
+ await self.aclose()
149
+
150
+ async def _require_session(self) -> object:
151
+ await self.start()
152
+ if self._session is not None:
153
+ return self._session
154
+ if self._terminal_error is not None:
155
+ raise self._terminal_error
156
+ raise McpConnectionError("MCP 会话初始化失败")
157
+
158
+ async def _run_session(self, ready: asyncio.Future[None], close_requested: asyncio.Event) -> None:
159
+ """在同一协程内持有 MCP 的异步上下文,保证会话生命周期完整。"""
160
+ stack = AsyncExitStack()
161
+ try:
162
+ try:
163
+ from mcp import ClientSession, StdioServerParameters
164
+ from mcp.client.stdio import stdio_client
165
+ except ImportError as exc:
166
+ raise McpConnectionError("MCP stdio 适配器需要安装 agent-core[mcp]") from exc
167
+ params = StdioServerParameters(
168
+ command=self._command,
169
+ args=[str(self._entrypoint)],
170
+ # 只继承启动子进程必需的系统变量,避免把模型密钥等无关秘密透传给 MCP。
171
+ env={
172
+ **{key: value for key, value in os.environ.items() if key.upper() in _INHERITED_ENV_KEYS},
173
+ **self._env,
174
+ },
175
+ )
176
+ async with asyncio.timeout(self._timeout_seconds):
177
+ read, write = await stack.enter_async_context(stdio_client(params))
178
+ session = await stack.enter_async_context(ClientSession(read, write))
179
+ await session.initialize()
180
+ self._session = session
181
+ ready.set_result(None)
182
+ await close_requested.wait()
183
+ except TimeoutError:
184
+ timeout_error = McpTimeoutError(f"MCP 初始化超时:{self._entrypoint.name}")
185
+ self._terminal_error = timeout_error
186
+ if not ready.done():
187
+ ready.set_exception(timeout_error)
188
+ except BaseException as exc:
189
+ connection_error = (
190
+ exc
191
+ if isinstance(exc, McpConnectionError)
192
+ else McpConnectionError(f"无法启动或维持 MCP 服务:{self._entrypoint}({exc})")
193
+ )
194
+ self._terminal_error = connection_error
195
+ if not ready.done():
196
+ ready.set_exception(connection_error)
197
+ finally:
198
+ self._session = None
199
+ try:
200
+ await stack.aclose()
201
+ except BaseException as exc:
202
+ self._terminal_error = McpConnectionError(f"关闭 MCP 服务失败:{self._entrypoint}({exc})")
203
+
204
+ @staticmethod
205
+ def _text_content(content: object) -> str | None:
206
+ if not isinstance(content, list):
207
+ return None
208
+ texts = [text for item in content if isinstance((text := getattr(item, "text", None)), str)]
209
+ return "\n".join(texts) if texts else None
210
+
211
+
212
+ __all__ = ["JsonObject", "McpToolCaller", "McpToolClient"]
@@ -0,0 +1,26 @@
1
+ """MCP 通用错误类型。
2
+
3
+ 这些错误描述连接、超时和工具调用语义,与 Cesium、地图或任何具体业务无关。
4
+ 具体集成可以在此基础上继续定义领域错误。
5
+ """
6
+
7
+ from agent_core.errors import AgentError
8
+
9
+
10
+ class McpError(AgentError):
11
+ """MCP 基础错误。"""
12
+
13
+
14
+ class McpConnectionError(McpError):
15
+ """MCP 连接或会话初始化失败。"""
16
+
17
+
18
+ class McpTimeoutError(McpError):
19
+ """MCP 初始化、列举工具或调用工具超时。"""
20
+
21
+
22
+ class McpToolError(McpError):
23
+ """MCP 服务返回工具执行失败或无效结果。"""
24
+
25
+
26
+ __all__ = ["McpConnectionError", "McpError", "McpTimeoutError", "McpToolError"]
@@ -0,0 +1,26 @@
1
+ """Agent 中间件。"""
2
+
3
+ # 兼容旧导入路径;能力实现分别归属 hitl 和 compaction 领域模块。
4
+ from agent_core.compaction.token_limit import TokenLimitMiddleware
5
+ from agent_core.hitl.core import HumanInTheLoopMiddleware
6
+ from agent_core.middleware.base import (
7
+ LoggingMiddleware,
8
+ Middleware,
9
+ MiddlewareAction,
10
+ MiddlewareContext,
11
+ MiddlewareManager,
12
+ MiddlewareResult,
13
+ RetryMiddleware,
14
+ )
15
+
16
+ __all__ = [
17
+ "HumanInTheLoopMiddleware",
18
+ "LoggingMiddleware",
19
+ "Middleware",
20
+ "MiddlewareAction",
21
+ "MiddlewareContext",
22
+ "MiddlewareManager",
23
+ "MiddlewareResult",
24
+ "RetryMiddleware",
25
+ "TokenLimitMiddleware",
26
+ ]
@@ -0,0 +1,283 @@
1
+ """中间件机制实现。
2
+
3
+ 中间件可以在以下时机注入逻辑:
4
+ 1. before_model : LLM 调用前
5
+ 2. after_model : LLM 调用后
6
+ 3. before_tool : 工具执行前
7
+ 4. after_tool : 工具执行后
8
+
9
+ 用法:
10
+ agent = ReActAgent(
11
+ llm=llm,
12
+ tool_executor=executor,
13
+ middleware=[
14
+ LoggingMiddleware(),
15
+ RetryMiddleware(max_retries=3),
16
+ TokenLimitMiddleware(max_tokens=4000),
17
+ HumanInTheLoopMiddleware(approval_needed=["takeoff_drone"]),
18
+ ],
19
+ )
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import logging
26
+ from collections.abc import Callable
27
+ from dataclasses import dataclass, field
28
+ from enum import Enum
29
+ from typing import Any
30
+
31
+ from agent_core.errors import (
32
+ ModelRateLimitError,
33
+ ModelTimeoutError,
34
+ ModelUnavailableError,
35
+ )
36
+ from agent_core.protocol.messages import Message
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ # ──────────────────────────────────────────────
42
+ # 基础数据类型
43
+ # ──────────────────────────────────────────────
44
+
45
+
46
+ class MiddlewareAction(Enum):
47
+ CONTINUE = "continue" # 继续执行
48
+ STOP = "stop" # 停止执行(中止整个循环)
49
+ MODIFY = "modify" # 修改数据(如裁剪消息历史)
50
+ RETRY = "retry" # 重试当前 LLM 调用
51
+ ERROR = "error" # 返回错误
52
+
53
+
54
+ @dataclass
55
+ class MiddlewareResult:
56
+ action: MiddlewareAction
57
+ data: dict[str, Any] = field(default_factory=dict)
58
+ error: str | None = None
59
+
60
+
61
+ @dataclass
62
+ class MiddlewareContext:
63
+ messages: list[Message] = field(default_factory=list)
64
+ iteration: int = 0
65
+ tool_name: str | None = None
66
+ tool_args: dict[str, Any] = field(default_factory=dict)
67
+ tool_result: str | None = None
68
+ llm_response: Message | None = None
69
+ metadata: dict[str, Any] = field(default_factory=dict)
70
+ # 运行时事件上报入口。它让通用中间件可以记录压缩、重试等事件,
71
+ # 同时保持对独立单元测试和旧 Vanilla 调用方的兼容。
72
+ emit: Callable[..., Any] | None = None
73
+
74
+
75
+ # ──────────────────────────────────────────────
76
+ # 基类
77
+ # ──────────────────────────────────────────────
78
+
79
+
80
+ class Middleware:
81
+ """中间件基类,子类按需重写钩子方法。"""
82
+
83
+ @property
84
+ def name(self) -> str:
85
+ return self.__class__.__name__
86
+
87
+ async def before_model(self, context: MiddlewareContext) -> MiddlewareResult:
88
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
89
+
90
+ async def after_model(self, context: MiddlewareContext) -> MiddlewareResult:
91
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
92
+
93
+ async def before_tool(self, context: MiddlewareContext) -> MiddlewareResult:
94
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
95
+
96
+ async def after_tool(self, context: MiddlewareContext) -> MiddlewareResult:
97
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
98
+
99
+
100
+ # ──────────────────────────────────────────────
101
+ # 中间件管理器
102
+ # ──────────────────────────────────────────────
103
+
104
+
105
+ class MiddlewareManager:
106
+ def __init__(self, middlewares: list[Middleware] | None = None) -> None:
107
+ self._middlewares = middlewares or []
108
+
109
+ def add(self, middleware: Middleware) -> None:
110
+ self._middlewares.append(middleware)
111
+
112
+ async def execute_before_model(self, ctx: MiddlewareContext) -> MiddlewareResult:
113
+ return await self._run_chain("before_model", ctx)
114
+
115
+ async def execute_after_model(self, ctx: MiddlewareContext) -> MiddlewareResult:
116
+ return await self._run_chain("after_model", ctx)
117
+
118
+ async def execute_before_tool(self, ctx: MiddlewareContext) -> MiddlewareResult:
119
+ return await self._run_chain("before_tool", ctx)
120
+
121
+ async def execute_after_tool(self, ctx: MiddlewareContext) -> MiddlewareResult:
122
+ return await self._run_chain("after_tool", ctx)
123
+
124
+ async def handle_exception(self, exc: Exception, ctx: MiddlewareContext) -> bool:
125
+ """让支持异常重试的中间件决定是否再次调用模型。"""
126
+ for mw in self._middlewares:
127
+ handler = getattr(mw, "handle_exception", None)
128
+ if handler is None:
129
+ continue
130
+ if await handler(exc, ctx):
131
+ return True
132
+ return False
133
+
134
+ async def _run_chain(self, hook: str, ctx: MiddlewareContext) -> MiddlewareResult:
135
+ modified_data: dict[str, Any] = {}
136
+ for mw in self._middlewares:
137
+ logger.debug("中间件 %s.%s", mw.name, hook)
138
+ previous_messages = ctx.messages
139
+ result: MiddlewareResult = await getattr(mw, hook)(ctx)
140
+ if result.action == MiddlewareAction.MODIFY:
141
+ # MODIFY 有两种合法写法:替换 ctx 字段(推荐),或在 data 中携带修改。
142
+ # 此处对账:本轮替换了 ctx.messages 时以 ctx 为准,防止 data 携带的
143
+ # 同名旧值把 ctx 的修改覆盖掉(Agent Loop 只读取聚合后的 data)。
144
+ if result.data:
145
+ modified_data.update(result.data)
146
+ if ctx.messages is not previous_messages:
147
+ modified_data["messages"] = ctx.messages
148
+ continue
149
+ if result.action != MiddlewareAction.CONTINUE:
150
+ logger.info("中间件 %s 阻断: action=%s", mw.name, result.action)
151
+ return result
152
+ if modified_data:
153
+ return MiddlewareResult(action=MiddlewareAction.MODIFY, data=modified_data)
154
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
155
+
156
+
157
+ # ──────────────────────────────────────────────
158
+ # 内置中间件 1:日志
159
+ # ──────────────────────────────────────────────
160
+
161
+
162
+ class LoggingMiddleware(Middleware):
163
+ """记录所有 LLM / 工具调用的日志。"""
164
+
165
+ async def before_model(self, ctx: MiddlewareContext) -> MiddlewareResult:
166
+ logger.info("[Log] before_model: iter=%d, msgs=%d", ctx.iteration, len(ctx.messages))
167
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
168
+
169
+ async def after_model(self, ctx: MiddlewareContext) -> MiddlewareResult:
170
+ r = ctx.llm_response
171
+ logger.info(
172
+ "[Log] after_model: content_len=%d, tool_calls=%d",
173
+ len(r.content) if r else 0,
174
+ len(r.tool_calls) if r else 0,
175
+ )
176
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
177
+
178
+ async def before_tool(self, ctx: MiddlewareContext) -> MiddlewareResult:
179
+ logger.info("[Log] before_tool: %s args=%s", ctx.tool_name, ctx.tool_args)
180
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
181
+
182
+ async def after_tool(self, ctx: MiddlewareContext) -> MiddlewareResult:
183
+ logger.info(
184
+ "[Log] after_tool: %s result_len=%d",
185
+ ctx.tool_name,
186
+ len(ctx.tool_result) if ctx.tool_result else 0,
187
+ )
188
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
189
+
190
+
191
+ # ──────────────────────────────────────────────
192
+ # 内置中间件 2:重试(生产级,支持指数退避)
193
+ # ──────────────────────────────────────────────
194
+
195
+
196
+ class RetryMiddleware(Middleware):
197
+ """自动重试失败的 LLM 调用。
198
+
199
+ 处理场景:
200
+ - 空响应
201
+ - 模型适配器统一转换后的限流、超时和服务不可用错误
202
+
203
+ 退避策略:wait = base_delay * (backoff_factor ** attempt)
204
+ """
205
+
206
+ def __init__(
207
+ self,
208
+ max_retries: int = 3,
209
+ base_delay: float = 1.0,
210
+ backoff_factor: float = 2.0,
211
+ max_delay: float = 60.0,
212
+ ) -> None:
213
+ self._max_retries = max_retries
214
+ self._base_delay = base_delay
215
+ self._backoff_factor = backoff_factor
216
+ self._max_delay = max_delay
217
+
218
+ def _should_retry(self, exc: Exception) -> bool:
219
+ """判断异常是否值得重试。"""
220
+ return isinstance(
221
+ exc,
222
+ (ModelRateLimitError, ModelTimeoutError, ModelUnavailableError),
223
+ ) or bool(getattr(exc, "retryable", False))
224
+
225
+ async def after_model(self, ctx: MiddlewareContext) -> MiddlewareResult:
226
+ r = ctx.llm_response
227
+ retry_count = ctx.metadata.get("retry_count", 0)
228
+
229
+ if retry_count >= self._max_retries:
230
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
231
+
232
+ # 空响应触发重试
233
+ if r is not None and not r.content and not r.tool_calls:
234
+ wait = min(
235
+ self._base_delay * (self._backoff_factor**retry_count),
236
+ self._max_delay,
237
+ )
238
+ logger.warning(
239
+ "[Retry] 空响应,第 %d/%d 次重试,等待 %.1fs",
240
+ retry_count + 1,
241
+ self._max_retries,
242
+ wait,
243
+ )
244
+ await asyncio.sleep(wait)
245
+ ctx.metadata["retry_count"] = retry_count + 1
246
+ return MiddlewareResult(
247
+ action=MiddlewareAction.RETRY,
248
+ data={"retry_count": retry_count + 1},
249
+ )
250
+
251
+ return MiddlewareResult(action=MiddlewareAction.CONTINUE)
252
+
253
+ async def handle_exception(
254
+ self,
255
+ exc: Exception,
256
+ ctx: MiddlewareContext,
257
+ ) -> bool:
258
+ """外部调用:判断异常是否应重试,并执行等待。
259
+
260
+ Returns:
261
+ True 表示调用方应重试,False 表示直接抛出。
262
+ """
263
+ if not self._should_retry(exc):
264
+ return False
265
+
266
+ retry_count = ctx.metadata.get("retry_count", 0)
267
+ if retry_count >= self._max_retries:
268
+ return False
269
+
270
+ wait = min(
271
+ self._base_delay * (self._backoff_factor**retry_count),
272
+ self._max_delay,
273
+ )
274
+ logger.warning(
275
+ "[Retry] %s,第 %d/%d 次重试,等待 %.1fs",
276
+ type(exc).__name__,
277
+ retry_count + 1,
278
+ self._max_retries,
279
+ wait,
280
+ )
281
+ await asyncio.sleep(wait)
282
+ ctx.metadata["retry_count"] = retry_count + 1
283
+ return True
@@ -0,0 +1,73 @@
1
+ """模型协议和模型工厂公共导出。"""
2
+
3
+ from agent_core.model.catalog import (
4
+ ModelOption,
5
+ catalog_payload,
6
+ default_model_catalog,
7
+ provider_display_name,
8
+ )
9
+ from agent_core.model.core import (
10
+ ContextUsage,
11
+ ModelAdapter,
12
+ ModelProviderFactory,
13
+ ModelProviderRegistry,
14
+ StreamChunk,
15
+ build_tool_definition,
16
+ create_model_provider,
17
+ default_model_provider_registry,
18
+ register_model_provider,
19
+ )
20
+ from agent_core.model.governance import (
21
+ GovernedModelAdapter,
22
+ ModelExecutionPolicy,
23
+ SlidingWindowRateLimiter,
24
+ model_execution_scope,
25
+ )
26
+ from agent_core.model.providers import (
27
+ MODEL_PROVIDER_REGISTRY,
28
+ AnthropicProvider,
29
+ GeminiProvider,
30
+ OllamaProvider,
31
+ OpenAIProvider,
32
+ create_llm_provider,
33
+ known_context_window,
34
+ )
35
+ from agent_core.model.structured import (
36
+ StructuredOutputResult,
37
+ StructuredOutputSpec,
38
+ chat_structured,
39
+ parse_structured_output,
40
+ validate_json_schema,
41
+ )
42
+
43
+ __all__ = [
44
+ "MODEL_PROVIDER_REGISTRY",
45
+ "AnthropicProvider",
46
+ "ContextUsage",
47
+ "GeminiProvider",
48
+ "GovernedModelAdapter",
49
+ "ModelAdapter",
50
+ "ModelExecutionPolicy",
51
+ "ModelOption",
52
+ "ModelProviderFactory",
53
+ "ModelProviderRegistry",
54
+ "OllamaProvider",
55
+ "OpenAIProvider",
56
+ "SlidingWindowRateLimiter",
57
+ "StreamChunk",
58
+ "StructuredOutputResult",
59
+ "StructuredOutputSpec",
60
+ "build_tool_definition",
61
+ "catalog_payload",
62
+ "chat_structured",
63
+ "create_llm_provider",
64
+ "create_model_provider",
65
+ "default_model_catalog",
66
+ "default_model_provider_registry",
67
+ "known_context_window",
68
+ "model_execution_scope",
69
+ "parse_structured_output",
70
+ "provider_display_name",
71
+ "register_model_provider",
72
+ "validate_json_schema",
73
+ ]
@@ -0,0 +1,73 @@
1
+ """模型目录和运行时选择协议。
2
+
3
+ 目录只包含浏览器可以看到的模型标识和能力,不包含 API Key、地址或其他密钥。
4
+ 具体应用可以传入自己的目录,模型工厂仍由 ``model.core`` 负责创建适配器。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ModelOption:
15
+ """一个可供用户选择的模型。"""
16
+
17
+ provider: str
18
+ model: str
19
+ label: str
20
+ context_window_tokens: int | None = None
21
+ capabilities: frozenset[str] = field(
22
+ default_factory=lambda: frozenset({"chat", "stream", "tool_calling"})
23
+ )
24
+
25
+ def to_dict(self) -> dict[str, Any]:
26
+ return {
27
+ "provider": self.provider,
28
+ "model": self.model,
29
+ "label": self.label,
30
+ "context_window_tokens": self.context_window_tokens,
31
+ "capabilities": sorted(self.capabilities),
32
+ }
33
+
34
+
35
+ def default_model_catalog() -> tuple[ModelOption, ...]:
36
+ """返回不依赖第三方 SDK 的安全默认目录。"""
37
+ return (
38
+ ModelOption("openai", "gpt-4o-mini", "OpenAI · GPT-4o mini", 128_000),
39
+ ModelOption("openai", "gpt-4.1-mini", "OpenAI · GPT-4.1 mini", 1_047_576),
40
+ ModelOption("anthropic", "claude-3-5-sonnet-latest", "Anthropic · Claude Sonnet"),
41
+ ModelOption("gemini", "gemini-2.0-flash", "Gemini · 2.0 Flash"),
42
+ ModelOption("ollama", "qwen2.5:7b", "Ollama · Qwen 2.5 7B"),
43
+ )
44
+
45
+
46
+ #: provider 内部标识(小写)到 UI 显示名的映射。
47
+ PROVIDER_DISPLAY_NAMES: dict[str, str] = {
48
+ "openai": "OpenAI",
49
+ "anthropic": "Anthropic",
50
+ "gemini": "Gemini",
51
+ "ollama": "Ollama",
52
+ }
53
+
54
+
55
+ def provider_display_name(provider: str) -> str:
56
+ """把 provider 标识转成展示名;未知 provider 原样返回(不强行改写)。"""
57
+ return PROVIDER_DISPLAY_NAMES.get(provider.strip().lower(), provider)
58
+
59
+
60
+ def catalog_payload(
61
+ options: tuple[ModelOption, ...] | list[ModelOption] | None = None,
62
+ ) -> list[dict[str, Any]]:
63
+ """把模型目录转换为 API 可返回的 JSON。"""
64
+ return [item.to_dict() for item in (options or default_model_catalog())]
65
+
66
+
67
+ __all__ = [
68
+ "PROVIDER_DISPLAY_NAMES",
69
+ "ModelOption",
70
+ "catalog_payload",
71
+ "default_model_catalog",
72
+ "provider_display_name",
73
+ ]