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,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
+ )