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,100 @@
|
|
|
1
|
+
"""手写消息类型定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Message:
|
|
12
|
+
"""基础消息类型。"""
|
|
13
|
+
|
|
14
|
+
role: Literal["system", "user", "assistant", "tool"]
|
|
15
|
+
content: str
|
|
16
|
+
name: str | None = None
|
|
17
|
+
tool_call_id: str | None = None
|
|
18
|
+
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
|
21
|
+
"""转换为 Core 内部的提供商无关格式。"""
|
|
22
|
+
result: dict[str, Any] = {
|
|
23
|
+
"role": self.role,
|
|
24
|
+
"content": self.content,
|
|
25
|
+
}
|
|
26
|
+
if self.name:
|
|
27
|
+
result["name"] = self.name
|
|
28
|
+
if self.tool_call_id:
|
|
29
|
+
result["tool_call_id"] = self.tool_call_id
|
|
30
|
+
if self.tool_calls:
|
|
31
|
+
# Core 内部保留扁平结构,具体模型适配器负责转换格式。
|
|
32
|
+
result["tool_calls"] = [dict(call) for call in self.tool_calls]
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
def to_openai_dict(self) -> dict[str, Any]:
|
|
36
|
+
"""转换为 OpenAI API 格式,保留给现有适配器和 Web API 使用。"""
|
|
37
|
+
result = self.to_dict()
|
|
38
|
+
if self.tool_calls:
|
|
39
|
+
result["tool_calls"] = [
|
|
40
|
+
{
|
|
41
|
+
"id": call.get("id", ""),
|
|
42
|
+
"type": "function",
|
|
43
|
+
"function": {
|
|
44
|
+
"name": call.get("name", ""),
|
|
45
|
+
"arguments": json.dumps(call.get("args", {}), ensure_ascii=False),
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
for call in self.tool_calls
|
|
49
|
+
]
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, value: dict[str, Any]) -> Message:
|
|
54
|
+
"""从 Core 内部格式恢复消息。"""
|
|
55
|
+
|
|
56
|
+
return cls(
|
|
57
|
+
role=value["role"],
|
|
58
|
+
content=str(value.get("content", "")),
|
|
59
|
+
name=value.get("name"),
|
|
60
|
+
tool_call_id=value.get("tool_call_id"),
|
|
61
|
+
tool_calls=[dict(call) for call in value.get("tool_calls", [])],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def system_message(content: str) -> Message:
|
|
66
|
+
"""创建系统消息。"""
|
|
67
|
+
return Message(role="system", content=content)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def user_message(content: str) -> Message:
|
|
71
|
+
"""创建用户消息。"""
|
|
72
|
+
return Message(role="user", content=content)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def assistant_message(
|
|
76
|
+
content: str,
|
|
77
|
+
*,
|
|
78
|
+
tool_calls: list[dict[str, Any]] | None = None,
|
|
79
|
+
) -> Message:
|
|
80
|
+
"""创建助手消息。"""
|
|
81
|
+
return Message(
|
|
82
|
+
role="assistant",
|
|
83
|
+
content=content,
|
|
84
|
+
tool_calls=tool_calls or [],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tool_message(
|
|
89
|
+
content: str,
|
|
90
|
+
*,
|
|
91
|
+
tool_call_id: str,
|
|
92
|
+
name: str,
|
|
93
|
+
) -> Message:
|
|
94
|
+
"""创建工具消息。"""
|
|
95
|
+
return Message(
|
|
96
|
+
role="tool",
|
|
97
|
+
content=content,
|
|
98
|
+
tool_call_id=tool_call_id,
|
|
99
|
+
name=name,
|
|
100
|
+
)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Agent 运行时共享协议。
|
|
2
|
+
|
|
3
|
+
这些类型是 ReAct、业务编排器、工作流和 HTTP runtime 之间的公共边界,
|
|
4
|
+
避免每一层用不同的字典字段表示同一个运行状态。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
RunStatus = Literal[
|
|
16
|
+
"queued",
|
|
17
|
+
"running",
|
|
18
|
+
"waiting_approval",
|
|
19
|
+
"waiting_clarification",
|
|
20
|
+
"interrupted",
|
|
21
|
+
"completed",
|
|
22
|
+
"failed",
|
|
23
|
+
"cancelled",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _now() -> str:
|
|
28
|
+
return datetime.now(UTC).isoformat()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RunEvent:
|
|
33
|
+
"""一次运行中的生命周期事件。
|
|
34
|
+
|
|
35
|
+
CallbackManager、EventSink、WebSocket 和审计存储消费同一个事件对象,避免多套事件协议。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
event_type: str
|
|
39
|
+
run_id: str
|
|
40
|
+
thread_id: str
|
|
41
|
+
sequence: int
|
|
42
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
timestamp: str = field(default_factory=_now)
|
|
44
|
+
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
45
|
+
parent_run_id: str | None = None
|
|
46
|
+
component: str | None = None
|
|
47
|
+
tags: list[str] = field(default_factory=list)
|
|
48
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
usage: dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
error: str | None = None
|
|
51
|
+
duration_ms: float | None = None
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"event_type": self.event_type,
|
|
56
|
+
"run_id": self.run_id,
|
|
57
|
+
"thread_id": self.thread_id,
|
|
58
|
+
"sequence": self.sequence,
|
|
59
|
+
"payload": self.payload,
|
|
60
|
+
"timestamp": self.timestamp,
|
|
61
|
+
"event_id": self.event_id,
|
|
62
|
+
"parent_run_id": self.parent_run_id,
|
|
63
|
+
"component": self.component,
|
|
64
|
+
"tags": list(self.tags),
|
|
65
|
+
"metadata": dict(self.metadata),
|
|
66
|
+
"usage": dict(self.usage),
|
|
67
|
+
"error": self.error,
|
|
68
|
+
"duration_ms": self.duration_ms,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, value: dict[str, Any]) -> RunEvent:
|
|
73
|
+
return cls(
|
|
74
|
+
event_type=str(value["event_type"]),
|
|
75
|
+
run_id=str(value["run_id"]),
|
|
76
|
+
thread_id=str(value["thread_id"]),
|
|
77
|
+
sequence=int(value["sequence"]),
|
|
78
|
+
payload=dict(value.get("payload") or {}),
|
|
79
|
+
timestamp=str(value.get("timestamp") or _now()),
|
|
80
|
+
event_id=str(value.get("event_id") or uuid.uuid4()),
|
|
81
|
+
parent_run_id=value.get("parent_run_id"),
|
|
82
|
+
component=value.get("component"),
|
|
83
|
+
tags=[str(tag) for tag in value.get("tags", [])],
|
|
84
|
+
metadata=dict(value.get("metadata") or {}),
|
|
85
|
+
usage=dict(value.get("usage") or {}),
|
|
86
|
+
error=value.get("error"),
|
|
87
|
+
duration_ms=(float(value["duration_ms"]) if value.get("duration_ms") is not None else None),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class ApprovalRecord:
|
|
93
|
+
"""可持久化的人机审批记录。"""
|
|
94
|
+
|
|
95
|
+
thread_id: str
|
|
96
|
+
run_id: str
|
|
97
|
+
action: str
|
|
98
|
+
arguments: dict[str, Any] = field(default_factory=dict)
|
|
99
|
+
user_id: str | None = None
|
|
100
|
+
tenant_id: str | None = None
|
|
101
|
+
approval_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
102
|
+
status: Literal["pending", "approved", "rejected", "expired"] = "pending"
|
|
103
|
+
reason: str | None = None
|
|
104
|
+
created_at: str = field(default_factory=_now)
|
|
105
|
+
expires_at: str | None = None
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, Any]:
|
|
108
|
+
return {
|
|
109
|
+
"approval_id": self.approval_id,
|
|
110
|
+
"thread_id": self.thread_id,
|
|
111
|
+
"run_id": self.run_id,
|
|
112
|
+
"action": self.action,
|
|
113
|
+
"arguments": self.arguments,
|
|
114
|
+
"user_id": self.user_id,
|
|
115
|
+
"tenant_id": self.tenant_id,
|
|
116
|
+
"status": self.status,
|
|
117
|
+
"reason": self.reason,
|
|
118
|
+
"created_at": self.created_at,
|
|
119
|
+
"expires_at": self.expires_at,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, value: dict[str, Any]) -> ApprovalRecord:
|
|
124
|
+
return cls(
|
|
125
|
+
approval_id=str(value.get("approval_id", "")),
|
|
126
|
+
thread_id=str(value.get("thread_id", "")),
|
|
127
|
+
run_id=str(value.get("run_id", "")),
|
|
128
|
+
action=str(value.get("action", "")),
|
|
129
|
+
arguments=dict(value.get("arguments") or {}),
|
|
130
|
+
user_id=value.get("user_id"),
|
|
131
|
+
tenant_id=value.get("tenant_id"),
|
|
132
|
+
status=value.get("status", "pending"),
|
|
133
|
+
reason=value.get("reason"),
|
|
134
|
+
created_at=str(value.get("created_at") or _now()),
|
|
135
|
+
expires_at=value.get("expires_at"),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class ToolResult:
|
|
141
|
+
"""工具执行的结构化结果。"""
|
|
142
|
+
|
|
143
|
+
tool_name: str
|
|
144
|
+
success: bool
|
|
145
|
+
value: Any = None
|
|
146
|
+
error: str | None = None
|
|
147
|
+
error_kind: Literal["", "not_found", "invalid_arguments", "timeout", "approval", "execution"] = ""
|
|
148
|
+
retryable: bool = False
|
|
149
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
150
|
+
|
|
151
|
+
def to_text(self) -> str:
|
|
152
|
+
"""转换成兼容旧 ReAct 消息的文本。"""
|
|
153
|
+
if self.success:
|
|
154
|
+
return str(self.value) if self.value is not None else ""
|
|
155
|
+
return f"[工具错误:{self.error_kind or 'execution'}] {self.error or '未知错误'}"
|
|
156
|
+
|
|
157
|
+
def to_dict(self) -> dict[str, Any]:
|
|
158
|
+
value = self.value
|
|
159
|
+
try:
|
|
160
|
+
json.dumps(value, ensure_ascii=False)
|
|
161
|
+
except (TypeError, ValueError):
|
|
162
|
+
value = str(value)
|
|
163
|
+
return {
|
|
164
|
+
"tool_name": self.tool_name,
|
|
165
|
+
"success": self.success,
|
|
166
|
+
"value": value,
|
|
167
|
+
"error": self.error,
|
|
168
|
+
"error_kind": self.error_kind,
|
|
169
|
+
"retryable": self.retryable,
|
|
170
|
+
"metadata": self.metadata,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_dict(cls, value: dict[str, Any]) -> ToolResult:
|
|
175
|
+
return cls(
|
|
176
|
+
tool_name=str(value["tool_name"]),
|
|
177
|
+
success=bool(value.get("success", False)),
|
|
178
|
+
value=value.get("value"),
|
|
179
|
+
error=value.get("error"),
|
|
180
|
+
error_kind=value.get("error_kind", ""),
|
|
181
|
+
retryable=bool(value.get("retryable", False)),
|
|
182
|
+
metadata=dict(value.get("metadata") or {}),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class RunContext:
|
|
188
|
+
"""一次 Agent run 的统一可恢复上下文。"""
|
|
189
|
+
|
|
190
|
+
thread_id: str
|
|
191
|
+
run_id: str
|
|
192
|
+
user_id: str | None = None
|
|
193
|
+
tenant_id: str | None = None
|
|
194
|
+
parent_run_id: str | None = None
|
|
195
|
+
tags: list[str] = field(default_factory=list)
|
|
196
|
+
status: RunStatus = "queued"
|
|
197
|
+
version: int = 0
|
|
198
|
+
idempotency_key: str | None = None
|
|
199
|
+
iteration: int = 0
|
|
200
|
+
tool_calls_used: int = 0
|
|
201
|
+
pending_approval: ApprovalRecord | None = None
|
|
202
|
+
pending_clarification: dict[str, Any] | None = None
|
|
203
|
+
state: dict[str, Any] = field(default_factory=dict)
|
|
204
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
205
|
+
# 最后一个可恢复的 Agent Loop 快照;回调只存在于进程内,不参与序列化。
|
|
206
|
+
checkpoint: dict[str, Any] = field(default_factory=dict)
|
|
207
|
+
checkpoint_callback: Any = field(default=None, repr=False, compare=False)
|
|
208
|
+
events: list[RunEvent] = field(default_factory=list)
|
|
209
|
+
|
|
210
|
+
def emit(
|
|
211
|
+
self,
|
|
212
|
+
event_type: str,
|
|
213
|
+
*,
|
|
214
|
+
component: str | None = None,
|
|
215
|
+
event_metadata: dict[str, Any] | None = None,
|
|
216
|
+
usage: dict[str, Any] | None = None,
|
|
217
|
+
error: str | None = None,
|
|
218
|
+
duration_ms: float | None = None,
|
|
219
|
+
**payload: Any,
|
|
220
|
+
) -> RunEvent:
|
|
221
|
+
"""追加有序事件,供 API、审计和调试读取。"""
|
|
222
|
+
|
|
223
|
+
inferred_component = component or event_type.partition("_")[0] or None
|
|
224
|
+
if error is not None:
|
|
225
|
+
# 兼容旧消费者读取 payload["error"],同时提供统一顶层字段。
|
|
226
|
+
payload.setdefault("error", error)
|
|
227
|
+
event = RunEvent(
|
|
228
|
+
event_type=event_type,
|
|
229
|
+
run_id=self.run_id,
|
|
230
|
+
thread_id=self.thread_id,
|
|
231
|
+
sequence=len(self.events),
|
|
232
|
+
payload=payload,
|
|
233
|
+
parent_run_id=self.parent_run_id,
|
|
234
|
+
component=inferred_component,
|
|
235
|
+
tags=list(self.tags),
|
|
236
|
+
metadata=dict(event_metadata or {}),
|
|
237
|
+
usage=dict(usage or {}),
|
|
238
|
+
error=error,
|
|
239
|
+
duration_ms=duration_ms,
|
|
240
|
+
)
|
|
241
|
+
self.events.append(event)
|
|
242
|
+
return event
|
|
243
|
+
|
|
244
|
+
def to_dict(self) -> dict[str, Any]:
|
|
245
|
+
return {
|
|
246
|
+
"thread_id": self.thread_id,
|
|
247
|
+
"run_id": self.run_id,
|
|
248
|
+
"user_id": self.user_id,
|
|
249
|
+
"tenant_id": self.tenant_id,
|
|
250
|
+
"parent_run_id": self.parent_run_id,
|
|
251
|
+
"tags": list(self.tags),
|
|
252
|
+
"status": self.status,
|
|
253
|
+
"version": self.version,
|
|
254
|
+
"idempotency_key": self.idempotency_key,
|
|
255
|
+
"iteration": self.iteration,
|
|
256
|
+
"tool_calls_used": self.tool_calls_used,
|
|
257
|
+
"pending_approval": (self.pending_approval.to_dict() if self.pending_approval else None),
|
|
258
|
+
"pending_clarification": self.pending_clarification,
|
|
259
|
+
"state": self.state,
|
|
260
|
+
"metadata": self.metadata,
|
|
261
|
+
"checkpoint": self.checkpoint,
|
|
262
|
+
"events": [event.to_dict() for event in self.events],
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def from_dict(cls, value: dict[str, Any]) -> RunContext:
|
|
267
|
+
"""从持久化字典恢复上下文;进程内回调不会参与恢复。"""
|
|
268
|
+
|
|
269
|
+
pending_approval = value.get("pending_approval")
|
|
270
|
+
context = cls(
|
|
271
|
+
thread_id=str(value["thread_id"]),
|
|
272
|
+
run_id=str(value["run_id"]),
|
|
273
|
+
user_id=value.get("user_id"),
|
|
274
|
+
tenant_id=value.get("tenant_id"),
|
|
275
|
+
parent_run_id=value.get("parent_run_id"),
|
|
276
|
+
tags=[str(tag) for tag in value.get("tags", [])],
|
|
277
|
+
status=value.get("status", "queued"),
|
|
278
|
+
version=int(value.get("version", 0)),
|
|
279
|
+
idempotency_key=value.get("idempotency_key"),
|
|
280
|
+
iteration=int(value.get("iteration", 0)),
|
|
281
|
+
tool_calls_used=int(value.get("tool_calls_used", 0)),
|
|
282
|
+
pending_approval=(
|
|
283
|
+
ApprovalRecord.from_dict(pending_approval) if isinstance(pending_approval, dict) else None
|
|
284
|
+
),
|
|
285
|
+
pending_clarification=value.get("pending_clarification"),
|
|
286
|
+
state=dict(value.get("state") or {}),
|
|
287
|
+
metadata=dict(value.get("metadata") or {}),
|
|
288
|
+
checkpoint=dict(value.get("checkpoint") or {}),
|
|
289
|
+
)
|
|
290
|
+
context.events = [
|
|
291
|
+
RunEvent.from_dict(item) for item in value.get("events", []) if isinstance(item, dict)
|
|
292
|
+
]
|
|
293
|
+
return context
|
agent_core/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Embedding、向量存储和检索协议。"""
|
|
2
|
+
|
|
3
|
+
from agent_core.retrieval.chroma import ChromaVectorStore
|
|
4
|
+
from agent_core.retrieval.core import (
|
|
5
|
+
Embeddings,
|
|
6
|
+
Reranker,
|
|
7
|
+
Retriever,
|
|
8
|
+
VectorStore,
|
|
9
|
+
)
|
|
10
|
+
from agent_core.retrieval.factory import VectorStoreBackend, create_vector_store
|
|
11
|
+
from agent_core.retrieval.memory import EmbeddingRetriever, InMemoryVectorStore, KeywordRetriever
|
|
12
|
+
from agent_core.retrieval.pgvector import PgVectorStore
|
|
13
|
+
from agent_core.retrieval.types import (
|
|
14
|
+
RetrievalQuery,
|
|
15
|
+
RetrievalResult,
|
|
16
|
+
SearchType,
|
|
17
|
+
VectorQuery,
|
|
18
|
+
VectorRecord,
|
|
19
|
+
VectorSearchResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ChromaVectorStore",
|
|
24
|
+
"EmbeddingRetriever",
|
|
25
|
+
"Embeddings",
|
|
26
|
+
"InMemoryVectorStore",
|
|
27
|
+
"KeywordRetriever",
|
|
28
|
+
"PgVectorStore",
|
|
29
|
+
"Reranker",
|
|
30
|
+
"RetrievalQuery",
|
|
31
|
+
"RetrievalResult",
|
|
32
|
+
"Retriever",
|
|
33
|
+
"SearchType",
|
|
34
|
+
"VectorQuery",
|
|
35
|
+
"VectorRecord",
|
|
36
|
+
"VectorSearchResult",
|
|
37
|
+
"VectorStore",
|
|
38
|
+
"VectorStoreBackend",
|
|
39
|
+
"create_vector_store",
|
|
40
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""向量存储适配器共享的序列化、权限和向量校验。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agent_core.access import AccessContext
|
|
11
|
+
from agent_core.documents import Document
|
|
12
|
+
from agent_core.retrieval.types import VectorRecord
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_record(
|
|
16
|
+
record: VectorRecord,
|
|
17
|
+
access: AccessContext | None,
|
|
18
|
+
*,
|
|
19
|
+
require_access: bool,
|
|
20
|
+
) -> VectorRecord:
|
|
21
|
+
"""验证写入身份,并为未声明归属的新记录补齐用户和租户。"""
|
|
22
|
+
if require_access and access is None:
|
|
23
|
+
raise PermissionError("该 VectorStore 要求提供 AccessContext")
|
|
24
|
+
if access is None:
|
|
25
|
+
return record
|
|
26
|
+
if not access.can_access(record.user_id, record.tenant_id):
|
|
27
|
+
raise PermissionError(f"无权写入向量记录:{record.record_id}")
|
|
28
|
+
return replace(
|
|
29
|
+
record,
|
|
30
|
+
user_id=record.user_id or access.user_id,
|
|
31
|
+
tenant_id=record.tenant_id or access.tenant_id,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_record_access(record: VectorRecord, access: AccessContext | None) -> None:
|
|
36
|
+
"""拒绝覆盖或删除当前身份不可访问的既有记录。"""
|
|
37
|
+
if access is not None and not access.can_access(record.user_id, record.tenant_id):
|
|
38
|
+
raise PermissionError(f"无权访问向量记录:{record.record_id}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def record_accessible(record: VectorRecord, access: AccessContext | None) -> bool:
|
|
42
|
+
return access is None or access.can_access(record.user_id, record.tenant_id)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def metadata_matches(metadata: dict[str, Any], expected: dict[str, Any]) -> bool:
|
|
46
|
+
return all(metadata.get(key) == value for key, value in expected.items())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_vector(vector: tuple[float, ...], *, dimension: int | None = None) -> None:
|
|
50
|
+
if not vector:
|
|
51
|
+
raise ValueError("vector 不能为空")
|
|
52
|
+
if dimension is not None and len(vector) != dimension:
|
|
53
|
+
raise ValueError(f"向量维度不一致:期望 {dimension},实际 {len(vector)}")
|
|
54
|
+
if not all(math.isfinite(value) for value in vector):
|
|
55
|
+
raise ValueError("vector 只能包含有限数值")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def vector_literal(vector: tuple[float, ...]) -> str:
|
|
59
|
+
"""生成只包含已校验浮点数的 pgvector 文本表示。"""
|
|
60
|
+
validate_vector(vector)
|
|
61
|
+
return "[" + ",".join(repr(float(value)) for value in vector) + "]"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_vector(value: object) -> tuple[float, ...]:
|
|
65
|
+
if isinstance(value, str):
|
|
66
|
+
raw = json.loads(value)
|
|
67
|
+
elif isinstance(value, (list, tuple)):
|
|
68
|
+
raw = value
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError("无法解析向量存储返回的 embedding")
|
|
71
|
+
if not isinstance(raw, (list, tuple)):
|
|
72
|
+
raise ValueError("embedding 必须是数组")
|
|
73
|
+
vector = tuple(float(item) for item in raw)
|
|
74
|
+
validate_vector(vector)
|
|
75
|
+
return vector
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def encode_record_payload(record: VectorRecord) -> str:
|
|
79
|
+
"""把文档和适配器 metadata 编码成可跨后端保存的 JSON。"""
|
|
80
|
+
try:
|
|
81
|
+
return json.dumps(
|
|
82
|
+
{
|
|
83
|
+
"document": record.document.to_dict(),
|
|
84
|
+
"metadata": record.metadata,
|
|
85
|
+
},
|
|
86
|
+
ensure_ascii=False,
|
|
87
|
+
separators=(",", ":"),
|
|
88
|
+
)
|
|
89
|
+
except (TypeError, ValueError) as exc:
|
|
90
|
+
raise ValueError(f"向量记录 {record.record_id} 包含不可 JSON 序列化的 metadata") from exc
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def decode_record_payload(
|
|
94
|
+
payload: object,
|
|
95
|
+
*,
|
|
96
|
+
record_id: str,
|
|
97
|
+
vector: tuple[float, ...],
|
|
98
|
+
namespace: str,
|
|
99
|
+
user_id: str | None,
|
|
100
|
+
tenant_id: str | None,
|
|
101
|
+
) -> VectorRecord:
|
|
102
|
+
if isinstance(payload, str):
|
|
103
|
+
raw = json.loads(payload)
|
|
104
|
+
elif isinstance(payload, dict):
|
|
105
|
+
raw = payload
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(f"向量记录 {record_id} 的 payload 格式无效")
|
|
108
|
+
if not isinstance(raw, dict) or not isinstance(raw.get("document"), dict):
|
|
109
|
+
raise ValueError(f"向量记录 {record_id} 缺少 document payload")
|
|
110
|
+
metadata = raw.get("metadata") or {}
|
|
111
|
+
if not isinstance(metadata, dict):
|
|
112
|
+
raise ValueError(f"向量记录 {record_id} 的 metadata 必须是对象")
|
|
113
|
+
return VectorRecord(
|
|
114
|
+
record_id=record_id,
|
|
115
|
+
vector=vector,
|
|
116
|
+
document=Document.from_dict(raw["document"]),
|
|
117
|
+
namespace=namespace,
|
|
118
|
+
user_id=user_id,
|
|
119
|
+
tenant_id=tenant_id,
|
|
120
|
+
metadata=dict(metadata),
|
|
121
|
+
)
|