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.
- agent_core/__init__.py +525 -0
- agent_core/access/__init__.py +6 -0
- agent_core/access/context.py +45 -0
- agent_core/access/guard.py +40 -0
- agent_core/acp/README.md +236 -0
- agent_core/acp/__init__.py +20 -0
- agent_core/acp/server.py +351 -0
- agent_core/acp/types.py +86 -0
- agent_core/callbacks/__init__.py +17 -0
- agent_core/callbacks/manager.py +105 -0
- agent_core/checkpoint/__init__.py +19 -0
- agent_core/checkpoint/naming.py +93 -0
- agent_core/checkpoint/store.py +186 -0
- agent_core/checkpoint/travel.py +448 -0
- agent_core/compaction/__init__.py +57 -0
- agent_core/compaction/middleware.py +428 -0
- agent_core/compaction/prompts.py +32 -0
- agent_core/compaction/pruner.py +121 -0
- agent_core/compaction/region.py +145 -0
- agent_core/compaction/spill.py +81 -0
- agent_core/compaction/spill_tool.py +42 -0
- agent_core/compaction/summarizer.py +95 -0
- agent_core/compaction/token_limit.py +74 -0
- agent_core/compaction/types.py +93 -0
- agent_core/docs.py +15 -0
- agent_core/documents/__init__.py +23 -0
- agent_core/documents/loaders.py +79 -0
- agent_core/documents/splitters.py +94 -0
- agent_core/documents/types.py +197 -0
- agent_core/errors.py +81 -0
- agent_core/guardrails/__init__.py +19 -0
- agent_core/guardrails/core.py +186 -0
- agent_core/hitl/__init__.py +15 -0
- agent_core/hitl/core.py +517 -0
- agent_core/mcp/__init__.py +14 -0
- agent_core/mcp/client.py +212 -0
- agent_core/mcp/errors.py +26 -0
- agent_core/middleware/__init__.py +26 -0
- agent_core/middleware/base.py +283 -0
- agent_core/model/__init__.py +73 -0
- agent_core/model/catalog.py +73 -0
- agent_core/model/core.py +179 -0
- agent_core/model/governance.py +317 -0
- agent_core/model/providers/__init__.py +43 -0
- agent_core/model/providers/anthropic.py +294 -0
- agent_core/model/providers/common.py +28 -0
- agent_core/model/providers/factory.py +107 -0
- agent_core/model/providers/gemini.py +273 -0
- agent_core/model/providers/ollama.py +80 -0
- agent_core/model/providers/openai_compatible.py +526 -0
- agent_core/model/structured.py +200 -0
- agent_core/observability/__init__.py +21 -0
- agent_core/observability/core.py +311 -0
- agent_core/ports/__init__.py +31 -0
- agent_core/ports/memory.py +130 -0
- agent_core/ports/model_selection.py +64 -0
- agent_core/ports/storage.py +114 -0
- agent_core/prompts/__init__.py +19 -0
- agent_core/prompts/registry.py +236 -0
- agent_core/prompts/templates.py +166 -0
- agent_core/protocol/__init__.py +31 -0
- agent_core/protocol/capabilities.py +49 -0
- agent_core/protocol/messages.py +100 -0
- agent_core/protocol/runtime.py +293 -0
- agent_core/py.typed +1 -0
- agent_core/retrieval/__init__.py +40 -0
- agent_core/retrieval/_common.py +121 -0
- agent_core/retrieval/chroma.py +269 -0
- agent_core/retrieval/core.py +63 -0
- agent_core/retrieval/factory.py +72 -0
- agent_core/retrieval/memory.py +232 -0
- agent_core/retrieval/pgvector.py +239 -0
- agent_core/retrieval/types.py +95 -0
- agent_core/runtime/__init__.py +28 -0
- agent_core/runtime/compat.py +98 -0
- agent_core/runtime/react.py +888 -0
- agent_core/runtime/service.py +557 -0
- agent_core/runtime/store.py +55 -0
- agent_core/serialization/__init__.py +21 -0
- agent_core/serialization/registry.py +318 -0
- agent_core/skills/__init__.py +24 -0
- agent_core/skills/errors.py +27 -0
- agent_core/skills/loader.py +258 -0
- agent_core/skills/registry.py +205 -0
- agent_core/skills/types.py +56 -0
- agent_core/storage/__init__.py +107 -0
- agent_core/storage/_sqlite_utils.py +92 -0
- agent_core/storage/context.py +382 -0
- agent_core/storage/lease.py +435 -0
- agent_core/storage/memory.py +604 -0
- agent_core/storage/model_selection.py +360 -0
- agent_core/storage/runtime.py +899 -0
- agent_core/storage/session.py +904 -0
- agent_core/storage/workflow.py +411 -0
- agent_core/tools/__init__.py +5 -0
- agent_core/tools/executor.py +306 -0
- agent_core/workflow/__init__.py +25 -0
- agent_core/workflow/durable.py +175 -0
- agent_core/workflow/state_machine.py +427 -0
- zero_agent_core-0.2.0.dist-info/METADATA +237 -0
- zero_agent_core-0.2.0.dist-info/RECORD +104 -0
- zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
- zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
- zero_agent_core-0.2.0.dist-info/licenses/LICENSE +201 -0
agent_core/model/core.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""模型调用协议。
|
|
2
|
+
|
|
3
|
+
Agent Core 只依赖该协议,不依赖 OpenAI、Anthropic、Gemini 或其他模型 SDK。
|
|
4
|
+
具体模型适配器由 ``agent_core.model.providers`` 提供,应用也可以注册自定义适配器。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import AsyncIterator, Callable
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from agent_core.protocol.messages import Message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class StreamChunk:
|
|
18
|
+
"""模型流式输出的统一数据块。"""
|
|
19
|
+
|
|
20
|
+
text: str = ""
|
|
21
|
+
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
22
|
+
is_tool_call: bool = False
|
|
23
|
+
finish_reason: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ContextUsage:
|
|
28
|
+
"""模型返回的上下文使用量。
|
|
29
|
+
|
|
30
|
+
``exact`` 用于区分提供商真实计数与本地估算;Core 不规定计数实现,
|
|
31
|
+
由具体模型适配器决定如何填充。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
input_tokens: int | None
|
|
35
|
+
context_window_tokens: int | None
|
|
36
|
+
exact: bool
|
|
37
|
+
source: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class ModelAdapter(Protocol):
|
|
42
|
+
"""Agent Loop 使用的最小模型协议。"""
|
|
43
|
+
|
|
44
|
+
async def chat(
|
|
45
|
+
self,
|
|
46
|
+
messages: list[Message],
|
|
47
|
+
*,
|
|
48
|
+
tools: list[dict[str, Any]] | None = None,
|
|
49
|
+
temperature: float | None = None,
|
|
50
|
+
max_tokens: int | None = None,
|
|
51
|
+
) -> Message: ...
|
|
52
|
+
|
|
53
|
+
def stream_chat(
|
|
54
|
+
self,
|
|
55
|
+
messages: list[Message],
|
|
56
|
+
*,
|
|
57
|
+
tools: list[dict[str, Any]] | None = None,
|
|
58
|
+
temperature: float | None = None,
|
|
59
|
+
max_tokens: int | None = None,
|
|
60
|
+
) -> AsyncIterator[StreamChunk]: ...
|
|
61
|
+
|
|
62
|
+
async def context_usage(
|
|
63
|
+
self,
|
|
64
|
+
messages: list[Message],
|
|
65
|
+
*,
|
|
66
|
+
tools: list[dict[str, Any]] | None = None,
|
|
67
|
+
) -> ContextUsage: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# 模型提供商构造器。Core 不关心配置对象的具体类型,应用可以传入
|
|
71
|
+
# Pydantic Settings、字典或自己的配置类。
|
|
72
|
+
ModelProviderFactory = Callable[..., ModelAdapter]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ModelProviderRegistry:
|
|
76
|
+
"""可注册的模型提供商工厂。
|
|
77
|
+
|
|
78
|
+
Core 管理名称到构造器的映射,并内置 OpenAI、Anthropic、Ollama、Gemini
|
|
79
|
+
适配器;应用可以继续注册自己的适配器。具体 SDK 由 Provider 延迟导入。
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
self._factories: dict[str, ModelProviderFactory] = {}
|
|
84
|
+
|
|
85
|
+
def register(
|
|
86
|
+
self,
|
|
87
|
+
name: str,
|
|
88
|
+
factory: ModelProviderFactory,
|
|
89
|
+
*,
|
|
90
|
+
replace: bool = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""注册一个模型提供商构造器。"""
|
|
93
|
+
normalized = name.strip().lower()
|
|
94
|
+
if not normalized:
|
|
95
|
+
raise ValueError("模型提供商名称不能为空")
|
|
96
|
+
if normalized in self._factories and not replace:
|
|
97
|
+
raise ValueError(f"模型提供商已注册:{normalized}")
|
|
98
|
+
self._factories[normalized] = factory
|
|
99
|
+
|
|
100
|
+
def unregister(self, name: str) -> None:
|
|
101
|
+
"""移除一个已注册的提供商。"""
|
|
102
|
+
self._factories.pop(name.strip().lower(), None)
|
|
103
|
+
|
|
104
|
+
def names(self) -> tuple[str, ...]:
|
|
105
|
+
"""返回已注册的提供商名称。"""
|
|
106
|
+
return tuple(sorted(self._factories))
|
|
107
|
+
|
|
108
|
+
def create(
|
|
109
|
+
self,
|
|
110
|
+
name: str,
|
|
111
|
+
config: Any,
|
|
112
|
+
*,
|
|
113
|
+
model: str | None = None,
|
|
114
|
+
**kwargs: Any,
|
|
115
|
+
) -> ModelAdapter:
|
|
116
|
+
"""根据提供商名称创建模型适配器。"""
|
|
117
|
+
normalized = name.strip().lower()
|
|
118
|
+
factory = self._factories.get(normalized)
|
|
119
|
+
if factory is None:
|
|
120
|
+
available = ", ".join(self.names()) or "无"
|
|
121
|
+
raise ValueError(f"不支持的模型提供商:{normalized or '<empty>'};已注册:{available}")
|
|
122
|
+
return factory(config, model=model, **kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
default_model_provider_registry = ModelProviderRegistry()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def register_model_provider(
|
|
129
|
+
name: str,
|
|
130
|
+
factory: ModelProviderFactory,
|
|
131
|
+
*,
|
|
132
|
+
replace: bool = False,
|
|
133
|
+
registry: ModelProviderRegistry | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""向默认或指定注册表注册模型提供商。"""
|
|
136
|
+
(registry or default_model_provider_registry).register(
|
|
137
|
+
name,
|
|
138
|
+
factory,
|
|
139
|
+
replace=replace,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_model_provider(
|
|
144
|
+
config: Any,
|
|
145
|
+
*,
|
|
146
|
+
registry: ModelProviderRegistry | None = None,
|
|
147
|
+
provider: str | None = None,
|
|
148
|
+
model: str | None = None,
|
|
149
|
+
**kwargs: Any,
|
|
150
|
+
) -> ModelAdapter:
|
|
151
|
+
"""使用注册表创建模型适配器。
|
|
152
|
+
|
|
153
|
+
``provider`` 省略时读取配置对象的 ``llm_provider``,默认为 ``openai``。
|
|
154
|
+
具体密钥校验和 SDK 初始化由对应的 Provider 工厂完成。
|
|
155
|
+
"""
|
|
156
|
+
selected = provider or getattr(config, "llm_provider", None) or "openai"
|
|
157
|
+
return (registry or default_model_provider_registry).create(
|
|
158
|
+
str(selected),
|
|
159
|
+
config,
|
|
160
|
+
model=model,
|
|
161
|
+
**kwargs,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_tool_definition(
|
|
166
|
+
name: str,
|
|
167
|
+
description: str,
|
|
168
|
+
parameters: dict[str, Any],
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""构造通用的 function-calling 工具定义。"""
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"type": "function",
|
|
174
|
+
"function": {
|
|
175
|
+
"name": name,
|
|
176
|
+
"description": description,
|
|
177
|
+
"parameters": parameters,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""模型并发、窗口限流、熔断和 fallback 治理。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from collections import defaultdict, deque
|
|
8
|
+
from collections.abc import AsyncIterator, Callable, Iterator
|
|
9
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
10
|
+
from contextvars import ContextVar
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from agent_core.errors import (
|
|
15
|
+
ModelInvocationError,
|
|
16
|
+
ModelRateLimitError,
|
|
17
|
+
ModelTimeoutError,
|
|
18
|
+
ModelUnavailableError,
|
|
19
|
+
)
|
|
20
|
+
from agent_core.model.core import ContextUsage, ModelAdapter, StreamChunk
|
|
21
|
+
from agent_core.protocol.messages import Message
|
|
22
|
+
|
|
23
|
+
_CURRENT_SCOPE: ContextVar[str] = ContextVar("agent_core_model_scope", default="default")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def model_execution_scope(scope: str) -> Iterator[None]:
|
|
28
|
+
"""为当前异步调用链设置模型治理范围,通常使用租户或用户 ID。"""
|
|
29
|
+
|
|
30
|
+
normalized = scope.strip() or "default"
|
|
31
|
+
token = _CURRENT_SCOPE.set(normalized)
|
|
32
|
+
try:
|
|
33
|
+
yield
|
|
34
|
+
finally:
|
|
35
|
+
_CURRENT_SCOPE.reset(token)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ModelExecutionPolicy:
|
|
40
|
+
"""模型调用治理策略。"""
|
|
41
|
+
|
|
42
|
+
max_concurrency: int = 8
|
|
43
|
+
requests_per_window: int | None = None
|
|
44
|
+
tokens_per_window: int | None = None
|
|
45
|
+
window_seconds: float = 60.0
|
|
46
|
+
queue_timeout_seconds: float | None = 30.0
|
|
47
|
+
call_timeout_seconds: float | None = 120.0
|
|
48
|
+
failure_threshold: int = 5
|
|
49
|
+
recovery_timeout_seconds: float = 30.0
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if self.max_concurrency <= 0:
|
|
53
|
+
raise ValueError("max_concurrency 必须大于 0")
|
|
54
|
+
if self.requests_per_window is not None and self.requests_per_window <= 0:
|
|
55
|
+
raise ValueError("requests_per_window 必须大于 0")
|
|
56
|
+
if self.tokens_per_window is not None and self.tokens_per_window <= 0:
|
|
57
|
+
raise ValueError("tokens_per_window 必须大于 0")
|
|
58
|
+
if self.window_seconds <= 0:
|
|
59
|
+
raise ValueError("window_seconds 必须大于 0")
|
|
60
|
+
if self.failure_threshold <= 0:
|
|
61
|
+
raise ValueError("failure_threshold 必须大于 0")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SlidingWindowRateLimiter:
|
|
65
|
+
"""按 scope 统计请求数和估算输入 Token 的滑动窗口限流器。"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, policy: ModelExecutionPolicy) -> None:
|
|
68
|
+
self._policy = policy
|
|
69
|
+
self._requests: dict[str, deque[float]] = defaultdict(deque)
|
|
70
|
+
self._tokens: dict[str, deque[tuple[float, int]]] = defaultdict(deque)
|
|
71
|
+
self._lock = asyncio.Lock()
|
|
72
|
+
|
|
73
|
+
async def acquire(self, scope: str, estimated_tokens: int) -> None:
|
|
74
|
+
deadline = (
|
|
75
|
+
None
|
|
76
|
+
if self._policy.queue_timeout_seconds is None
|
|
77
|
+
else time.monotonic() + self._policy.queue_timeout_seconds
|
|
78
|
+
)
|
|
79
|
+
while True:
|
|
80
|
+
async with self._lock:
|
|
81
|
+
now = time.monotonic()
|
|
82
|
+
wait_for = self._reserve_or_wait(scope, estimated_tokens, now)
|
|
83
|
+
if wait_for <= 0:
|
|
84
|
+
return
|
|
85
|
+
if deadline is not None and time.monotonic() + wait_for > deadline:
|
|
86
|
+
raise ModelRateLimitError(f"模型限流队列等待超过 {self._policy.queue_timeout_seconds} 秒")
|
|
87
|
+
await asyncio.sleep(wait_for)
|
|
88
|
+
|
|
89
|
+
def _reserve_or_wait(self, scope: str, estimated_tokens: int, now: float) -> float:
|
|
90
|
+
cutoff = now - self._policy.window_seconds
|
|
91
|
+
requests = self._requests[scope]
|
|
92
|
+
tokens = self._tokens[scope]
|
|
93
|
+
while requests and requests[0] <= cutoff:
|
|
94
|
+
requests.popleft()
|
|
95
|
+
while tokens and tokens[0][0] <= cutoff:
|
|
96
|
+
tokens.popleft()
|
|
97
|
+
|
|
98
|
+
waits: list[float] = []
|
|
99
|
+
request_limit = self._policy.requests_per_window
|
|
100
|
+
if request_limit is not None and len(requests) >= request_limit:
|
|
101
|
+
waits.append(requests[0] + self._policy.window_seconds - now)
|
|
102
|
+
token_limit = self._policy.tokens_per_window
|
|
103
|
+
token_total = sum(item[1] for item in tokens)
|
|
104
|
+
if token_limit is not None and token_total + estimated_tokens > token_limit:
|
|
105
|
+
if estimated_tokens > token_limit:
|
|
106
|
+
raise ModelRateLimitError("单次请求估算 Token 已超过窗口上限")
|
|
107
|
+
waits.append(tokens[0][0] + self._policy.window_seconds - now if tokens else 0.01)
|
|
108
|
+
if waits:
|
|
109
|
+
return max(0.01, min(waits))
|
|
110
|
+
requests.append(now)
|
|
111
|
+
tokens.append((now, max(0, estimated_tokens)))
|
|
112
|
+
return 0.0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _ScopedConcurrencyLimiter:
|
|
116
|
+
def __init__(self, limit: int) -> None:
|
|
117
|
+
self._limit = limit
|
|
118
|
+
self._items: dict[str, asyncio.Semaphore] = {}
|
|
119
|
+
self._lock = asyncio.Lock()
|
|
120
|
+
|
|
121
|
+
async def _get(self, scope: str) -> asyncio.Semaphore:
|
|
122
|
+
async with self._lock:
|
|
123
|
+
return self._items.setdefault(scope, asyncio.Semaphore(self._limit))
|
|
124
|
+
|
|
125
|
+
@asynccontextmanager
|
|
126
|
+
async def slot(self, scope: str, acquire_timeout: float | None) -> AsyncIterator[None]:
|
|
127
|
+
semaphore = await self._get(scope)
|
|
128
|
+
try:
|
|
129
|
+
if acquire_timeout is None:
|
|
130
|
+
await semaphore.acquire()
|
|
131
|
+
else:
|
|
132
|
+
await asyncio.wait_for(semaphore.acquire(), timeout=acquire_timeout)
|
|
133
|
+
except TimeoutError as exc:
|
|
134
|
+
raise ModelRateLimitError(f"模型并发队列等待超过 {acquire_timeout} 秒") from exc
|
|
135
|
+
try:
|
|
136
|
+
yield
|
|
137
|
+
finally:
|
|
138
|
+
semaphore.release()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class _CircuitState:
|
|
143
|
+
failures: int = 0
|
|
144
|
+
opened_at: float | None = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class GovernedModelAdapter:
|
|
148
|
+
"""为任意 ModelAdapter 增加限流、超时、熔断和候选模型降级。"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
primary: ModelAdapter,
|
|
153
|
+
*,
|
|
154
|
+
fallbacks: list[ModelAdapter] | None = None,
|
|
155
|
+
policy: ModelExecutionPolicy | None = None,
|
|
156
|
+
scope_resolver: Callable[[], str] | None = None,
|
|
157
|
+
max_circuit_entries: int = 4096,
|
|
158
|
+
) -> None:
|
|
159
|
+
self._adapters = [primary, *(fallbacks or [])]
|
|
160
|
+
self._policy = policy or ModelExecutionPolicy()
|
|
161
|
+
self._rate_limiter = SlidingWindowRateLimiter(self._policy)
|
|
162
|
+
self._concurrency = _ScopedConcurrencyLimiter(self._policy.max_concurrency)
|
|
163
|
+
self._scope_resolver = scope_resolver or _CURRENT_SCOPE.get
|
|
164
|
+
self._circuits: dict[tuple[int, str], _CircuitState] = {}
|
|
165
|
+
self._circuit_lock = asyncio.Lock()
|
|
166
|
+
if max_circuit_entries <= 0:
|
|
167
|
+
raise ValueError("max_circuit_entries 必须大于 0")
|
|
168
|
+
self._max_circuit_entries = max_circuit_entries
|
|
169
|
+
|
|
170
|
+
def _evict_circuits_locked(self) -> None:
|
|
171
|
+
"""熔断状态按 (adapter, scope) 累积;容量满时先淘汰干净状态,再按插入序淘汰最旧。"""
|
|
172
|
+
if len(self._circuits) < self._max_circuit_entries:
|
|
173
|
+
return
|
|
174
|
+
for key, state in list(self._circuits.items()):
|
|
175
|
+
if len(self._circuits) < self._max_circuit_entries:
|
|
176
|
+
break
|
|
177
|
+
if state.opened_at is None and state.failures == 0:
|
|
178
|
+
self._circuits.pop(key, None)
|
|
179
|
+
while len(self._circuits) >= self._max_circuit_entries:
|
|
180
|
+
self._circuits.pop(next(iter(self._circuits)))
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _estimate_tokens(messages: list[Message]) -> int:
|
|
184
|
+
return max(1, sum(len(message.content) for message in messages) // 4)
|
|
185
|
+
|
|
186
|
+
async def _allow(self, index: int, scope: str) -> bool:
|
|
187
|
+
async with self._circuit_lock:
|
|
188
|
+
self._evict_circuits_locked()
|
|
189
|
+
state = self._circuits.setdefault((index, scope), _CircuitState())
|
|
190
|
+
if state.opened_at is None:
|
|
191
|
+
return True
|
|
192
|
+
if time.monotonic() - state.opened_at < self._policy.recovery_timeout_seconds:
|
|
193
|
+
return False
|
|
194
|
+
state.opened_at = None
|
|
195
|
+
state.failures = max(0, self._policy.failure_threshold - 1)
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
async def _success(self, index: int, scope: str) -> None:
|
|
199
|
+
async with self._circuit_lock:
|
|
200
|
+
self._circuits[(index, scope)] = _CircuitState()
|
|
201
|
+
|
|
202
|
+
async def _failure(self, index: int, scope: str) -> None:
|
|
203
|
+
async with self._circuit_lock:
|
|
204
|
+
self._evict_circuits_locked()
|
|
205
|
+
state = self._circuits.setdefault((index, scope), _CircuitState())
|
|
206
|
+
state.failures += 1
|
|
207
|
+
if state.failures >= self._policy.failure_threshold:
|
|
208
|
+
state.opened_at = time.monotonic()
|
|
209
|
+
|
|
210
|
+
async def _any_adapter_available(self, scope: str) -> bool:
|
|
211
|
+
"""是否存在至少一个未处于熔断恢复期的候选;短路探测,无副作用。"""
|
|
212
|
+
for index in range(len(self._adapters)):
|
|
213
|
+
if await self._allow(index, scope):
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
async def chat(
|
|
218
|
+
self,
|
|
219
|
+
messages: list[Message],
|
|
220
|
+
*,
|
|
221
|
+
tools: list[dict[str, Any]] | None = None,
|
|
222
|
+
temperature: float | None = None,
|
|
223
|
+
max_tokens: int | None = None,
|
|
224
|
+
) -> Message:
|
|
225
|
+
scope = self._scope_resolver().strip() or "default"
|
|
226
|
+
last_error: Exception | None = None
|
|
227
|
+
# 全部候选处于熔断恢复期时直接失败,不消耗本地限流配额。
|
|
228
|
+
if not await self._any_adapter_available(scope):
|
|
229
|
+
raise ModelUnavailableError("没有可用的模型适配器(全部处于熔断恢复期)")
|
|
230
|
+
# 一个逻辑调用只消耗一次本地限流配额;fallback 属于同一次调用。
|
|
231
|
+
await self._rate_limiter.acquire(scope, self._estimate_tokens(messages))
|
|
232
|
+
async with self._concurrency.slot(scope, self._policy.queue_timeout_seconds):
|
|
233
|
+
for index, adapter in enumerate(self._adapters):
|
|
234
|
+
if not await self._allow(index, scope):
|
|
235
|
+
last_error = ModelUnavailableError(f"第 {index + 1} 个模型适配器熔断中")
|
|
236
|
+
continue
|
|
237
|
+
try:
|
|
238
|
+
async with asyncio.timeout(self._policy.call_timeout_seconds):
|
|
239
|
+
result = await adapter.chat(
|
|
240
|
+
messages,
|
|
241
|
+
tools=tools,
|
|
242
|
+
temperature=temperature,
|
|
243
|
+
max_tokens=max_tokens,
|
|
244
|
+
)
|
|
245
|
+
await self._success(index, scope)
|
|
246
|
+
return result
|
|
247
|
+
except TimeoutError as exc:
|
|
248
|
+
last_error = ModelTimeoutError("模型调用超时")
|
|
249
|
+
last_error.__cause__ = exc
|
|
250
|
+
await self._failure(index, scope)
|
|
251
|
+
except ModelInvocationError as exc:
|
|
252
|
+
last_error = exc
|
|
253
|
+
await self._failure(index, scope)
|
|
254
|
+
if last_error is None:
|
|
255
|
+
raise ModelUnavailableError("没有可用的模型适配器")
|
|
256
|
+
raise last_error
|
|
257
|
+
|
|
258
|
+
async def stream_chat(
|
|
259
|
+
self,
|
|
260
|
+
messages: list[Message],
|
|
261
|
+
*,
|
|
262
|
+
tools: list[dict[str, Any]] | None = None,
|
|
263
|
+
temperature: float | None = None,
|
|
264
|
+
max_tokens: int | None = None,
|
|
265
|
+
) -> AsyncIterator[StreamChunk]:
|
|
266
|
+
scope = self._scope_resolver().strip() or "default"
|
|
267
|
+
last_error: Exception | None = None
|
|
268
|
+
# 与 chat() 相同:全部候选熔断时直接失败,不消耗本地限流配额。
|
|
269
|
+
if not await self._any_adapter_available(scope):
|
|
270
|
+
raise ModelUnavailableError("没有可用的模型适配器(全部处于熔断恢复期)")
|
|
271
|
+
await self._rate_limiter.acquire(scope, self._estimate_tokens(messages))
|
|
272
|
+
async with self._concurrency.slot(scope, self._policy.queue_timeout_seconds):
|
|
273
|
+
for index, adapter in enumerate(self._adapters):
|
|
274
|
+
emitted = False
|
|
275
|
+
if not await self._allow(index, scope):
|
|
276
|
+
last_error = ModelUnavailableError(f"第 {index + 1} 个模型适配器熔断中")
|
|
277
|
+
continue
|
|
278
|
+
try:
|
|
279
|
+
async with asyncio.timeout(self._policy.call_timeout_seconds):
|
|
280
|
+
async for chunk in adapter.stream_chat(
|
|
281
|
+
messages,
|
|
282
|
+
tools=tools,
|
|
283
|
+
temperature=temperature,
|
|
284
|
+
max_tokens=max_tokens,
|
|
285
|
+
):
|
|
286
|
+
emitted = True
|
|
287
|
+
yield chunk
|
|
288
|
+
await self._success(index, scope)
|
|
289
|
+
return
|
|
290
|
+
except TimeoutError as exc:
|
|
291
|
+
last_error = ModelTimeoutError("模型流式调用超时")
|
|
292
|
+
last_error.__cause__ = exc
|
|
293
|
+
await self._failure(index, scope)
|
|
294
|
+
except ModelInvocationError as exc:
|
|
295
|
+
last_error = exc
|
|
296
|
+
await self._failure(index, scope)
|
|
297
|
+
if emitted and last_error is not None:
|
|
298
|
+
raise last_error
|
|
299
|
+
if last_error is None:
|
|
300
|
+
raise ModelUnavailableError("没有可用的模型适配器")
|
|
301
|
+
raise last_error
|
|
302
|
+
|
|
303
|
+
async def context_usage(
|
|
304
|
+
self,
|
|
305
|
+
messages: list[Message],
|
|
306
|
+
*,
|
|
307
|
+
tools: list[dict[str, Any]] | None = None,
|
|
308
|
+
) -> ContextUsage:
|
|
309
|
+
return await self._adapters[0].context_usage(messages, tools=tools)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = [
|
|
313
|
+
"GovernedModelAdapter",
|
|
314
|
+
"ModelExecutionPolicy",
|
|
315
|
+
"SlidingWindowRateLimiter",
|
|
316
|
+
"model_execution_scope",
|
|
317
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""多模型切换:Protocol 抽象 + 各提供商适配器。
|
|
2
|
+
|
|
3
|
+
使用方式:
|
|
4
|
+
# 开发环境(OpenAI 兼容接口)
|
|
5
|
+
llm = OpenAIProvider(api_key="sk-...", model="gpt-4o-mini")
|
|
6
|
+
|
|
7
|
+
# Anthropic Claude
|
|
8
|
+
llm = AnthropicProvider(api_key="sk-ant-...", model="claude-3-5-sonnet-20241022")
|
|
9
|
+
|
|
10
|
+
# 本地 Ollama
|
|
11
|
+
llm = OllamaProvider(model="qwen2.5:7b")
|
|
12
|
+
|
|
13
|
+
# 从环境变量/配置自动选择
|
|
14
|
+
llm = create_llm_provider(config)
|
|
15
|
+
|
|
16
|
+
所有提供商均实现 ``ModelAdapter`` 协议,ReActAgent 只依赖该协议,
|
|
17
|
+
切换提供商无需修改任何 Agent 代码。
|
|
18
|
+
|
|
19
|
+
模块结构:
|
|
20
|
+
common.py 共享辅助(已知上下文窗口规格、统一 logger)
|
|
21
|
+
openai_compatible.py OpenAI 兼容接口适配器(DashScope/Qwen 复用该实现)
|
|
22
|
+
anthropic.py Anthropic Claude 适配器
|
|
23
|
+
gemini.py Google Gemini 适配器
|
|
24
|
+
ollama.py Ollama 本地模型适配器(复用 OpenAI 兼容接口)
|
|
25
|
+
factory.py 提供商构造器、注册与 create_llm_provider 工厂入口
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from agent_core.model.providers.anthropic import AnthropicProvider
|
|
29
|
+
from agent_core.model.providers.common import known_context_window
|
|
30
|
+
from agent_core.model.providers.factory import MODEL_PROVIDER_REGISTRY, create_llm_provider
|
|
31
|
+
from agent_core.model.providers.gemini import GeminiProvider
|
|
32
|
+
from agent_core.model.providers.ollama import OllamaProvider
|
|
33
|
+
from agent_core.model.providers.openai_compatible import OpenAIProvider
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"MODEL_PROVIDER_REGISTRY",
|
|
37
|
+
"AnthropicProvider",
|
|
38
|
+
"GeminiProvider",
|
|
39
|
+
"OllamaProvider",
|
|
40
|
+
"OpenAIProvider",
|
|
41
|
+
"create_llm_provider",
|
|
42
|
+
"known_context_window",
|
|
43
|
+
]
|