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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""适配器 3:Gemini。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agent_core.errors import ModelInvocationError
|
|
10
|
+
from agent_core.model.core import ContextUsage, StreamChunk
|
|
11
|
+
from agent_core.model.providers.common import logger
|
|
12
|
+
from agent_core.protocol.messages import Message, assistant_message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GeminiProvider:
|
|
16
|
+
"""Google Gemini 模型适配器。
|
|
17
|
+
|
|
18
|
+
使用官方 ``google-genai`` SDK,安装方式:``uv add google-genai``。
|
|
19
|
+
该 SDK 只在选择 Gemini 时延迟导入,不影响现有提供商。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
api_key: str,
|
|
26
|
+
model: str = "gemini-2.5-flash",
|
|
27
|
+
temperature: float = 0.0,
|
|
28
|
+
max_tokens: int = 4096,
|
|
29
|
+
context_window_tokens: int | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
try:
|
|
32
|
+
from google import genai
|
|
33
|
+
from google.genai import types
|
|
34
|
+
except ImportError as exc:
|
|
35
|
+
raise ImportError("GeminiProvider 需要 google-genai 包,请执行:uv add google-genai") from exc
|
|
36
|
+
|
|
37
|
+
self._genai_types = types
|
|
38
|
+
self._client = genai.Client(api_key=api_key)
|
|
39
|
+
self.model = model
|
|
40
|
+
self._temperature = temperature
|
|
41
|
+
self._max_tokens = max_tokens
|
|
42
|
+
self._context_window_tokens = context_window_tokens
|
|
43
|
+
self.provider_name = "gemini"
|
|
44
|
+
|
|
45
|
+
# ──────────────────────────────────────────────
|
|
46
|
+
# 调用 Gemini API 进行上下文使用
|
|
47
|
+
# ──────────────────────────────────────────────
|
|
48
|
+
async def context_usage(
|
|
49
|
+
self,
|
|
50
|
+
messages: list[Message],
|
|
51
|
+
*,
|
|
52
|
+
tools: list[dict[str, Any]] | None = None,
|
|
53
|
+
) -> ContextUsage:
|
|
54
|
+
_system_prompt, contents = self._to_gemini_contents(messages)
|
|
55
|
+
try:
|
|
56
|
+
response = await self._client.aio.models.count_tokens(
|
|
57
|
+
model=self.model,
|
|
58
|
+
contents=contents,
|
|
59
|
+
)
|
|
60
|
+
input_tokens = getattr(response, "total_tokens", None)
|
|
61
|
+
if isinstance(input_tokens, int):
|
|
62
|
+
model = await self._client.aio.models.get(model=self.model)
|
|
63
|
+
model_limit = getattr(model, "input_token_limit", None)
|
|
64
|
+
return ContextUsage(
|
|
65
|
+
input_tokens=input_tokens,
|
|
66
|
+
context_window_tokens=(
|
|
67
|
+
self._context_window_tokens or (model_limit if isinstance(model_limit, int) else None)
|
|
68
|
+
),
|
|
69
|
+
exact=True,
|
|
70
|
+
source="provider_tokenizer",
|
|
71
|
+
)
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logger.debug("Gemini tokenizer 不可用:%s", exc)
|
|
74
|
+
return ContextUsage(
|
|
75
|
+
input_tokens=None,
|
|
76
|
+
context_window_tokens=self._context_window_tokens,
|
|
77
|
+
exact=False,
|
|
78
|
+
source="unavailable",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# ──────────────────────────────────────────────
|
|
82
|
+
# 转换为 Gemini API 格式消息
|
|
83
|
+
# ──────────────────────────────────────────────
|
|
84
|
+
def _to_gemini_contents(self, messages: list[Message]) -> tuple[str | None, list[Any]]:
|
|
85
|
+
"""把内部消息转换为 Gemini Content/Part。"""
|
|
86
|
+
types = self._genai_types
|
|
87
|
+
system_instruction: str | None = None
|
|
88
|
+
contents: list[Any] = []
|
|
89
|
+
|
|
90
|
+
for message in messages:
|
|
91
|
+
if message.role == "system":
|
|
92
|
+
system_instruction = (
|
|
93
|
+
f"{system_instruction}\n\n{message.content}" if system_instruction else message.content
|
|
94
|
+
)
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if message.role == "user":
|
|
98
|
+
contents.append(
|
|
99
|
+
types.Content(
|
|
100
|
+
role="user",
|
|
101
|
+
parts=[types.Part.from_text(text=message.content)],
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if message.role == "assistant":
|
|
107
|
+
parts: list[Any] = []
|
|
108
|
+
if message.content:
|
|
109
|
+
parts.append(types.Part.from_text(text=message.content))
|
|
110
|
+
for call in message.tool_calls:
|
|
111
|
+
parts.append(
|
|
112
|
+
types.Part.from_function_call(
|
|
113
|
+
name=str(call.get("name", "")),
|
|
114
|
+
args=dict(call.get("args") or {}),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
if parts:
|
|
118
|
+
contents.append(types.Content(role="model", parts=parts))
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if message.role == "tool":
|
|
122
|
+
# Gemini 使用 function_response 表达工具结果。
|
|
123
|
+
contents.append(
|
|
124
|
+
types.Content(
|
|
125
|
+
role="user",
|
|
126
|
+
parts=[
|
|
127
|
+
types.Part.from_function_response(
|
|
128
|
+
name=message.name or "tool",
|
|
129
|
+
response={"result": message.content},
|
|
130
|
+
)
|
|
131
|
+
],
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return system_instruction, contents
|
|
136
|
+
|
|
137
|
+
# ──────────────────────────────────────────────
|
|
138
|
+
# 转换为 Gemini API 格式工具
|
|
139
|
+
# ──────────────────────────────────────────────
|
|
140
|
+
def _to_gemini_tools(self, tools: list[dict[str, Any]]) -> list[Any]:
|
|
141
|
+
"""把 OpenAI function schema 转换为 Gemini function declarations。"""
|
|
142
|
+
types = self._genai_types
|
|
143
|
+
declarations = []
|
|
144
|
+
for tool in tools:
|
|
145
|
+
function = tool.get("function", tool)
|
|
146
|
+
declarations.append(
|
|
147
|
+
types.FunctionDeclaration(
|
|
148
|
+
name=function["name"],
|
|
149
|
+
description=function.get("description", ""),
|
|
150
|
+
parameters=function.get("parameters", {"type": "object", "properties": {}}),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return [types.Tool(function_declarations=declarations)] if declarations else []
|
|
154
|
+
|
|
155
|
+
# ──────────────────────────────────────────────
|
|
156
|
+
# 构造 Gemini 请求配置
|
|
157
|
+
# ──────────────────────────────────────────────
|
|
158
|
+
def _config(
|
|
159
|
+
self,
|
|
160
|
+
*,
|
|
161
|
+
system_instruction: str | None,
|
|
162
|
+
tools: list[dict[str, Any]] | None,
|
|
163
|
+
temperature: float | None,
|
|
164
|
+
max_tokens: int | None,
|
|
165
|
+
) -> Any:
|
|
166
|
+
"""构造 Gemini 请求配置。"""
|
|
167
|
+
kwargs: dict[str, Any] = {
|
|
168
|
+
"temperature": temperature if temperature is not None else self._temperature,
|
|
169
|
+
"max_output_tokens": max_tokens if max_tokens is not None else self._max_tokens,
|
|
170
|
+
}
|
|
171
|
+
if system_instruction:
|
|
172
|
+
kwargs["system_instruction"] = system_instruction
|
|
173
|
+
if tools:
|
|
174
|
+
kwargs["tools"] = self._to_gemini_tools(tools)
|
|
175
|
+
return self._genai_types.GenerateContentConfig(**kwargs)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _response_message(response: Any) -> Message:
|
|
179
|
+
"""从 Gemini 响应中提取文本和函数调用。"""
|
|
180
|
+
content = ""
|
|
181
|
+
tool_calls: list[dict[str, Any]] = []
|
|
182
|
+
candidates = getattr(response, "candidates", None) or []
|
|
183
|
+
parts = getattr(getattr(candidates[0], "content", None), "parts", []) if candidates else []
|
|
184
|
+
for part in parts or []:
|
|
185
|
+
text = getattr(part, "text", None)
|
|
186
|
+
if text:
|
|
187
|
+
content += text
|
|
188
|
+
function_call = getattr(part, "function_call", None)
|
|
189
|
+
if function_call is not None:
|
|
190
|
+
tool_calls.append(
|
|
191
|
+
{
|
|
192
|
+
"id": str(getattr(function_call, "id", "") or uuid.uuid4()),
|
|
193
|
+
"type": "function",
|
|
194
|
+
"name": str(getattr(function_call, "name", "")),
|
|
195
|
+
"args": dict(getattr(function_call, "args", {}) or {}),
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
return assistant_message(content, tool_calls=tool_calls)
|
|
199
|
+
|
|
200
|
+
# ──────────────────────────────────────────────
|
|
201
|
+
# 调用 Gemini API 进行聊天
|
|
202
|
+
# ──────────────────────────────────────────────
|
|
203
|
+
async def chat(
|
|
204
|
+
self,
|
|
205
|
+
messages: list[Message],
|
|
206
|
+
*,
|
|
207
|
+
tools: list[dict[str, Any]] | None = None,
|
|
208
|
+
temperature: float | None = None,
|
|
209
|
+
max_tokens: int | None = None,
|
|
210
|
+
) -> Message:
|
|
211
|
+
system_instruction, contents = self._to_gemini_contents(messages)
|
|
212
|
+
config = self._config(
|
|
213
|
+
system_instruction=system_instruction,
|
|
214
|
+
tools=tools,
|
|
215
|
+
temperature=temperature,
|
|
216
|
+
max_tokens=max_tokens,
|
|
217
|
+
)
|
|
218
|
+
try:
|
|
219
|
+
response = await self._client.aio.models.generate_content(
|
|
220
|
+
model=self.model, contents=contents, config=config
|
|
221
|
+
)
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
raise ModelInvocationError(f"Gemini API call failed: {exc}") from exc
|
|
224
|
+
return self._response_message(response)
|
|
225
|
+
|
|
226
|
+
# ──────────────────────────────────────────────
|
|
227
|
+
# 调用 Gemini API 进行流式聊天
|
|
228
|
+
# ──────────────────────────────────────────────
|
|
229
|
+
async def stream_chat(
|
|
230
|
+
self,
|
|
231
|
+
messages: list[Message],
|
|
232
|
+
*,
|
|
233
|
+
tools: list[dict[str, Any]] | None = None,
|
|
234
|
+
temperature: float | None = None,
|
|
235
|
+
max_tokens: int | None = None,
|
|
236
|
+
) -> AsyncIterator[StreamChunk]:
|
|
237
|
+
system_instruction, contents = self._to_gemini_contents(messages)
|
|
238
|
+
config = self._config(
|
|
239
|
+
system_instruction=system_instruction,
|
|
240
|
+
tools=tools,
|
|
241
|
+
temperature=temperature,
|
|
242
|
+
max_tokens=max_tokens,
|
|
243
|
+
)
|
|
244
|
+
tool_call_map: dict[str, dict[str, Any]] = {}
|
|
245
|
+
try:
|
|
246
|
+
stream = await self._client.aio.models.generate_content_stream(
|
|
247
|
+
model=self.model, contents=contents, config=config
|
|
248
|
+
)
|
|
249
|
+
async for chunk in stream:
|
|
250
|
+
message = self._response_message(chunk)
|
|
251
|
+
if message.content:
|
|
252
|
+
yield StreamChunk(text=message.content)
|
|
253
|
+
if message.tool_calls:
|
|
254
|
+
for call_index, call in enumerate(message.tool_calls):
|
|
255
|
+
# 同一轮允许多次调用同名函数,按位置而不是名称归并增量。
|
|
256
|
+
key = f"{call_index}:{call['name']}"
|
|
257
|
+
current = tool_call_map.setdefault(
|
|
258
|
+
key,
|
|
259
|
+
{
|
|
260
|
+
"id": call["id"],
|
|
261
|
+
"type": "function",
|
|
262
|
+
"name": call["name"],
|
|
263
|
+
"args": {},
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
current["args"].update(call.get("args") or {})
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
raise ModelInvocationError(f"Gemini stream failed: {exc}") from exc
|
|
269
|
+
|
|
270
|
+
if tool_call_map:
|
|
271
|
+
yield StreamChunk(tool_calls=list(tool_call_map.values()), is_tool_call=True)
|
|
272
|
+
else:
|
|
273
|
+
yield StreamChunk()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""适配器 4:Ollama(本地模型)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_core.model.core import ContextUsage, StreamChunk
|
|
9
|
+
from agent_core.model.providers.openai_compatible import OpenAIProvider
|
|
10
|
+
from agent_core.protocol.messages import Message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OllamaProvider:
|
|
14
|
+
"""Ollama 本地模型提供商(复用 OpenAI 兼容接口)。
|
|
15
|
+
|
|
16
|
+
Ollama 提供 OpenAI 兼容的 API,直接复用 OpenAIProvider。
|
|
17
|
+
|
|
18
|
+
需要先启动 Ollama:ollama serve
|
|
19
|
+
支持的模型:qwen2.5, llama3.2, mistral, deepseek-r1 等
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
model: str = "qwen2.5:7b",
|
|
26
|
+
base_url: str = "http://localhost:11434/v1",
|
|
27
|
+
temperature: float = 0.0,
|
|
28
|
+
max_tokens: int = 4096,
|
|
29
|
+
context_window_tokens: int | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._inner = OpenAIProvider(
|
|
32
|
+
api_key="ollama", # Ollama 不需要真实 key
|
|
33
|
+
base_url=base_url,
|
|
34
|
+
model=model,
|
|
35
|
+
temperature=temperature,
|
|
36
|
+
max_tokens=max_tokens,
|
|
37
|
+
context_window_tokens=context_window_tokens,
|
|
38
|
+
)
|
|
39
|
+
self.model = model
|
|
40
|
+
self.provider_name = "ollama"
|
|
41
|
+
|
|
42
|
+
# ──────────────────────────────────────────────
|
|
43
|
+
# 获取上下文使用统计
|
|
44
|
+
# ──────────────────────────────────────────────
|
|
45
|
+
async def context_usage(
|
|
46
|
+
self,
|
|
47
|
+
messages: list[Message],
|
|
48
|
+
*,
|
|
49
|
+
tools: list[dict[str, Any]] | None = None,
|
|
50
|
+
) -> ContextUsage:
|
|
51
|
+
return await self._inner.context_usage(messages, tools=tools)
|
|
52
|
+
|
|
53
|
+
# ──────────────────────────────────────────────
|
|
54
|
+
# 调用 Ollama API 进行聊天
|
|
55
|
+
# ──────────────────────────────────────────────
|
|
56
|
+
async def chat(
|
|
57
|
+
self,
|
|
58
|
+
messages: list[Message],
|
|
59
|
+
*,
|
|
60
|
+
tools: list[dict[str, Any]] | None = None,
|
|
61
|
+
temperature: float | None = None,
|
|
62
|
+
max_tokens: int | None = None,
|
|
63
|
+
) -> Message:
|
|
64
|
+
return await self._inner.chat(messages, tools=tools, temperature=temperature, max_tokens=max_tokens)
|
|
65
|
+
|
|
66
|
+
# ──────────────────────────────────────────────
|
|
67
|
+
# 调用 Ollama API 进行流式聊天
|
|
68
|
+
# ──────────────────────────────────────────────
|
|
69
|
+
async def stream_chat(
|
|
70
|
+
self,
|
|
71
|
+
messages: list[Message],
|
|
72
|
+
*,
|
|
73
|
+
tools: list[dict[str, Any]] | None = None,
|
|
74
|
+
temperature: float | None = None,
|
|
75
|
+
max_tokens: int | None = None,
|
|
76
|
+
) -> AsyncIterator[StreamChunk]:
|
|
77
|
+
async for chunk in self._inner.stream_chat(
|
|
78
|
+
messages, tools=tools, temperature=temperature, max_tokens=max_tokens
|
|
79
|
+
):
|
|
80
|
+
yield chunk
|