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,604 @@
|
|
|
1
|
+
"""长期记忆的内存、SQLite 和 PostgreSQL 实现。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import copy
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sqlite3
|
|
10
|
+
from collections.abc import Generator
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from importlib import import_module
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from agent_core.access import AccessContext
|
|
18
|
+
from agent_core.errors import MemoryConflictError
|
|
19
|
+
from agent_core.ports.memory import MemoryRecord, MemorySearchResult, MemoryStore, memory_now
|
|
20
|
+
from agent_core.storage._sqlite_utils import (
|
|
21
|
+
_run_in_thread,
|
|
22
|
+
run_sync_in_thread,
|
|
23
|
+
sqlite_connection,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_WORD = re.compile(r"[\w\u4e00-\u9fff]+", re.UNICODE)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _postgres_datetime(value: str | None) -> datetime | None:
|
|
30
|
+
"""把 Core 使用的 ISO 时间转换为 asyncpg 接受的 datetime。"""
|
|
31
|
+
|
|
32
|
+
if value is None:
|
|
33
|
+
return None
|
|
34
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
35
|
+
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=UTC)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _MemoryAccessMixin:
|
|
39
|
+
def __init__(self, *, require_access: bool = False) -> None:
|
|
40
|
+
self._strict_access = require_access
|
|
41
|
+
|
|
42
|
+
def _require_access(self, access: AccessContext | None) -> None:
|
|
43
|
+
if self._strict_access and access is None:
|
|
44
|
+
raise PermissionError("该 MemoryStore 要求提供 AccessContext")
|
|
45
|
+
|
|
46
|
+
def _can_access(self, record: MemoryRecord, access: AccessContext | None) -> bool:
|
|
47
|
+
self._require_access(access)
|
|
48
|
+
return access is None or access.can_access(record.user_id, record.tenant_id)
|
|
49
|
+
|
|
50
|
+
def _bind(self, record: MemoryRecord, access: AccessContext | None) -> MemoryRecord:
|
|
51
|
+
value = MemoryRecord.from_dict(copy.deepcopy(record.to_dict()))
|
|
52
|
+
self._require_access(access)
|
|
53
|
+
if access is None:
|
|
54
|
+
return value
|
|
55
|
+
if value.user_id is None:
|
|
56
|
+
value.user_id = access.user_id
|
|
57
|
+
if value.tenant_id is None:
|
|
58
|
+
value.tenant_id = access.tenant_id
|
|
59
|
+
if not access.can_access(value.user_id, value.tenant_id):
|
|
60
|
+
raise PermissionError("不能把记忆写入其他用户或租户")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _score(record: MemoryRecord, query: str) -> float:
|
|
65
|
+
words = [item.lower() for item in _WORD.findall(query)]
|
|
66
|
+
if not words:
|
|
67
|
+
return 1.0
|
|
68
|
+
haystack = f"{record.content} {json.dumps(record.metadata, ensure_ascii=False)}".lower()
|
|
69
|
+
hits = sum(haystack.count(word) for word in words)
|
|
70
|
+
return hits / len(words)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _metadata_matches(record: MemoryRecord, filters: dict[str, Any] | None) -> bool:
|
|
74
|
+
return not filters or all(record.metadata.get(key) == value for key, value in filters.items())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InMemoryMemoryStore(_MemoryAccessMixin):
|
|
78
|
+
"""测试和单进程运行使用的跨会话记忆存储。"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, *, require_access: bool = False) -> None:
|
|
81
|
+
super().__init__(require_access=require_access)
|
|
82
|
+
self._items: dict[str, MemoryRecord] = {}
|
|
83
|
+
self._lock = asyncio.Lock()
|
|
84
|
+
|
|
85
|
+
async def put(
|
|
86
|
+
self,
|
|
87
|
+
record: MemoryRecord,
|
|
88
|
+
*,
|
|
89
|
+
expected_version: int | None = None,
|
|
90
|
+
access: AccessContext | None = None,
|
|
91
|
+
) -> MemoryRecord:
|
|
92
|
+
async with self._lock:
|
|
93
|
+
value = self._bind(record, access)
|
|
94
|
+
current = self._items.get(value.memory_id)
|
|
95
|
+
if current is not None and not self._can_access(current, access):
|
|
96
|
+
raise PermissionError("无权更新该记忆")
|
|
97
|
+
actual = current.version if current else 0
|
|
98
|
+
if expected_version is not None and expected_version != actual:
|
|
99
|
+
raise MemoryConflictError(value.memory_id, expected_version, actual)
|
|
100
|
+
value.version = actual + 1
|
|
101
|
+
value.created_at = current.created_at if current else value.created_at
|
|
102
|
+
value.updated_at = memory_now()
|
|
103
|
+
self._items[value.memory_id] = value
|
|
104
|
+
return MemoryRecord.from_dict(copy.deepcopy(value.to_dict()))
|
|
105
|
+
|
|
106
|
+
async def get(
|
|
107
|
+
self,
|
|
108
|
+
memory_id: str,
|
|
109
|
+
*,
|
|
110
|
+
access: AccessContext | None = None,
|
|
111
|
+
) -> MemoryRecord | None:
|
|
112
|
+
async with self._lock:
|
|
113
|
+
self._require_access(access)
|
|
114
|
+
value = self._items.get(memory_id)
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
if value.is_expired():
|
|
118
|
+
self._items.pop(memory_id, None)
|
|
119
|
+
return None
|
|
120
|
+
if not self._can_access(value, access):
|
|
121
|
+
raise PermissionError("无权访问该记忆")
|
|
122
|
+
return MemoryRecord.from_dict(copy.deepcopy(value.to_dict()))
|
|
123
|
+
|
|
124
|
+
async def delete(
|
|
125
|
+
self,
|
|
126
|
+
memory_id: str,
|
|
127
|
+
*,
|
|
128
|
+
expected_version: int | None = None,
|
|
129
|
+
access: AccessContext | None = None,
|
|
130
|
+
) -> bool:
|
|
131
|
+
async with self._lock:
|
|
132
|
+
self._require_access(access)
|
|
133
|
+
current = self._items.get(memory_id)
|
|
134
|
+
if current is None:
|
|
135
|
+
return False
|
|
136
|
+
if not self._can_access(current, access):
|
|
137
|
+
raise PermissionError("无权删除该记忆")
|
|
138
|
+
if expected_version is not None and expected_version != current.version:
|
|
139
|
+
raise MemoryConflictError(memory_id, expected_version, current.version)
|
|
140
|
+
del self._items[memory_id]
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
async def search(
|
|
144
|
+
self,
|
|
145
|
+
namespace: str,
|
|
146
|
+
*,
|
|
147
|
+
query: str = "",
|
|
148
|
+
metadata: dict[str, Any] | None = None,
|
|
149
|
+
limit: int = 20,
|
|
150
|
+
access: AccessContext | None = None,
|
|
151
|
+
) -> list[MemorySearchResult]:
|
|
152
|
+
if limit <= 0:
|
|
153
|
+
return []
|
|
154
|
+
async with self._lock:
|
|
155
|
+
self._require_access(access)
|
|
156
|
+
expired = [key for key, item in self._items.items() if item.is_expired()]
|
|
157
|
+
for key in expired:
|
|
158
|
+
self._items.pop(key, None)
|
|
159
|
+
results = []
|
|
160
|
+
for item in self._items.values():
|
|
161
|
+
if item.namespace != namespace or not self._can_access(item, access):
|
|
162
|
+
continue
|
|
163
|
+
if not self._metadata_matches(item, metadata):
|
|
164
|
+
continue
|
|
165
|
+
score = self._score(item, query)
|
|
166
|
+
if query and score <= 0:
|
|
167
|
+
continue
|
|
168
|
+
results.append(
|
|
169
|
+
MemorySearchResult(
|
|
170
|
+
record=MemoryRecord.from_dict(copy.deepcopy(item.to_dict())),
|
|
171
|
+
score=score,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
results.sort(key=lambda item: (item.score, item.record.updated_at), reverse=True)
|
|
175
|
+
return results[:limit]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SqliteMemoryStore(_MemoryAccessMixin):
|
|
179
|
+
"""本地开发使用的 SQLite 长期记忆存储。"""
|
|
180
|
+
|
|
181
|
+
DDL = """
|
|
182
|
+
CREATE TABLE IF NOT EXISTS agent_memories (
|
|
183
|
+
memory_id TEXT PRIMARY KEY,
|
|
184
|
+
namespace TEXT NOT NULL,
|
|
185
|
+
memory_key TEXT,
|
|
186
|
+
content TEXT NOT NULL,
|
|
187
|
+
user_id TEXT,
|
|
188
|
+
tenant_id TEXT,
|
|
189
|
+
source TEXT,
|
|
190
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
191
|
+
expires_at TEXT,
|
|
192
|
+
version INTEGER NOT NULL,
|
|
193
|
+
created_at TEXT NOT NULL,
|
|
194
|
+
updated_at TEXT NOT NULL
|
|
195
|
+
);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_agent_memories_namespace
|
|
197
|
+
ON agent_memories(namespace, tenant_id, user_id, updated_at);
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, db_path: str | Path = "./sessions.db", *, require_access: bool = False) -> None:
|
|
201
|
+
super().__init__(require_access=require_access)
|
|
202
|
+
self._db_path = str(Path(db_path).resolve())
|
|
203
|
+
# 建表 DDL/PRAGMA 初始化同样放入工作线程执行,与 async 方法共用同一连接参数规范。
|
|
204
|
+
run_sync_in_thread(self._init_db)
|
|
205
|
+
|
|
206
|
+
def _init_db(self) -> None:
|
|
207
|
+
with self._connect() as conn:
|
|
208
|
+
conn.executescript(self.DDL)
|
|
209
|
+
|
|
210
|
+
@contextmanager
|
|
211
|
+
def _connect(self) -> Generator[sqlite3.Connection]:
|
|
212
|
+
"""获取 SQLite 连接(同步,自动提交/回滚;统一 timeout 与 WAL 规范)。"""
|
|
213
|
+
with sqlite_connection(self._db_path) as conn:
|
|
214
|
+
yield conn
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _row(row: tuple[Any, ...]) -> MemoryRecord:
|
|
218
|
+
return MemoryRecord(
|
|
219
|
+
memory_id=str(row[0]),
|
|
220
|
+
namespace=str(row[1]),
|
|
221
|
+
key=row[2],
|
|
222
|
+
content=str(row[3]),
|
|
223
|
+
user_id=row[4],
|
|
224
|
+
tenant_id=row[5],
|
|
225
|
+
source=row[6],
|
|
226
|
+
metadata=json.loads(row[7] or "{}"),
|
|
227
|
+
expires_at=row[8],
|
|
228
|
+
version=int(row[9]),
|
|
229
|
+
created_at=str(row[10]),
|
|
230
|
+
updated_at=str(row[11]),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def put(
|
|
234
|
+
self,
|
|
235
|
+
record: MemoryRecord,
|
|
236
|
+
*,
|
|
237
|
+
expected_version: int | None = None,
|
|
238
|
+
access: AccessContext | None = None,
|
|
239
|
+
) -> MemoryRecord:
|
|
240
|
+
value = self._bind(record, access)
|
|
241
|
+
|
|
242
|
+
def _op() -> MemoryRecord:
|
|
243
|
+
with self._connect() as conn:
|
|
244
|
+
row = conn.execute(
|
|
245
|
+
"SELECT memory_id,namespace,memory_key,content,user_id,tenant_id,source,metadata,"
|
|
246
|
+
"expires_at,version,created_at,updated_at FROM agent_memories WHERE memory_id=?",
|
|
247
|
+
(value.memory_id,),
|
|
248
|
+
).fetchone()
|
|
249
|
+
current = self._row(row) if row else None
|
|
250
|
+
if current is not None and not self._can_access(current, access):
|
|
251
|
+
raise PermissionError("无权更新该记忆")
|
|
252
|
+
actual = current.version if current else 0
|
|
253
|
+
if expected_version is not None and expected_version != actual:
|
|
254
|
+
raise MemoryConflictError(value.memory_id, expected_version, actual)
|
|
255
|
+
value.version = actual + 1
|
|
256
|
+
value.created_at = current.created_at if current else value.created_at
|
|
257
|
+
value.updated_at = memory_now()
|
|
258
|
+
payload = (
|
|
259
|
+
value.namespace,
|
|
260
|
+
value.key,
|
|
261
|
+
value.content,
|
|
262
|
+
value.user_id,
|
|
263
|
+
value.tenant_id,
|
|
264
|
+
value.source,
|
|
265
|
+
json.dumps(value.metadata, ensure_ascii=False),
|
|
266
|
+
value.expires_at,
|
|
267
|
+
value.version,
|
|
268
|
+
value.created_at,
|
|
269
|
+
value.updated_at,
|
|
270
|
+
)
|
|
271
|
+
if current is None:
|
|
272
|
+
try:
|
|
273
|
+
conn.execute(
|
|
274
|
+
"INSERT INTO agent_memories(memory_id,namespace,memory_key,content,"
|
|
275
|
+
"user_id,tenant_id,source,metadata,expires_at,version,created_at,updated_at) "
|
|
276
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
277
|
+
(value.memory_id, *payload),
|
|
278
|
+
)
|
|
279
|
+
except sqlite3.IntegrityError as exc:
|
|
280
|
+
raise MemoryConflictError(value.memory_id, 0, 1) from exc
|
|
281
|
+
else:
|
|
282
|
+
changed = conn.execute(
|
|
283
|
+
"UPDATE agent_memories SET namespace=?,memory_key=?,content=?,user_id=?,tenant_id=?,"
|
|
284
|
+
"source=?,metadata=?,expires_at=?,version=?,created_at=?,updated_at=? "
|
|
285
|
+
"WHERE memory_id=? AND version=?",
|
|
286
|
+
(*payload, value.memory_id, actual),
|
|
287
|
+
).rowcount
|
|
288
|
+
if changed != 1:
|
|
289
|
+
raise MemoryConflictError(value.memory_id, actual, actual + 1)
|
|
290
|
+
return MemoryRecord.from_dict(value.to_dict())
|
|
291
|
+
|
|
292
|
+
return await _run_in_thread(_op)
|
|
293
|
+
|
|
294
|
+
async def get(
|
|
295
|
+
self,
|
|
296
|
+
memory_id: str,
|
|
297
|
+
*,
|
|
298
|
+
access: AccessContext | None = None,
|
|
299
|
+
) -> MemoryRecord | None:
|
|
300
|
+
self._require_access(access)
|
|
301
|
+
|
|
302
|
+
def _op() -> MemoryRecord | None:
|
|
303
|
+
with self._connect() as conn:
|
|
304
|
+
row = conn.execute(
|
|
305
|
+
"SELECT memory_id,namespace,memory_key,content,user_id,tenant_id,source,metadata,"
|
|
306
|
+
"expires_at,version,created_at,updated_at FROM agent_memories WHERE memory_id=?",
|
|
307
|
+
(memory_id,),
|
|
308
|
+
).fetchone()
|
|
309
|
+
if row is None:
|
|
310
|
+
return None
|
|
311
|
+
value = self._row(row)
|
|
312
|
+
if value.is_expired():
|
|
313
|
+
conn.execute("DELETE FROM agent_memories WHERE memory_id=?", (memory_id,))
|
|
314
|
+
return None
|
|
315
|
+
return value
|
|
316
|
+
|
|
317
|
+
value = await _run_in_thread(_op)
|
|
318
|
+
if value is None:
|
|
319
|
+
return None
|
|
320
|
+
if not self._can_access(value, access):
|
|
321
|
+
raise PermissionError("无权访问该记忆")
|
|
322
|
+
return value
|
|
323
|
+
|
|
324
|
+
async def delete(
|
|
325
|
+
self,
|
|
326
|
+
memory_id: str,
|
|
327
|
+
*,
|
|
328
|
+
expected_version: int | None = None,
|
|
329
|
+
access: AccessContext | None = None,
|
|
330
|
+
) -> bool:
|
|
331
|
+
current = await self.get(memory_id, access=access)
|
|
332
|
+
if current is None:
|
|
333
|
+
return False
|
|
334
|
+
if expected_version is not None and expected_version != current.version:
|
|
335
|
+
raise MemoryConflictError(memory_id, expected_version, current.version)
|
|
336
|
+
|
|
337
|
+
def _op() -> int:
|
|
338
|
+
with self._connect() as conn:
|
|
339
|
+
return conn.execute(
|
|
340
|
+
"DELETE FROM agent_memories WHERE memory_id=? AND version=?",
|
|
341
|
+
(memory_id, current.version),
|
|
342
|
+
).rowcount
|
|
343
|
+
|
|
344
|
+
changed = await _run_in_thread(_op)
|
|
345
|
+
if changed != 1:
|
|
346
|
+
raise MemoryConflictError(memory_id, current.version, current.version + 1)
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
async def search(
|
|
350
|
+
self,
|
|
351
|
+
namespace: str,
|
|
352
|
+
*,
|
|
353
|
+
query: str = "",
|
|
354
|
+
metadata: dict[str, Any] | None = None,
|
|
355
|
+
limit: int = 20,
|
|
356
|
+
access: AccessContext | None = None,
|
|
357
|
+
) -> list[MemorySearchResult]:
|
|
358
|
+
self._require_access(access)
|
|
359
|
+
if limit <= 0:
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
def _op() -> list[Any]:
|
|
363
|
+
with self._connect() as conn:
|
|
364
|
+
return conn.execute(
|
|
365
|
+
"SELECT memory_id,namespace,memory_key,content,user_id,tenant_id,source,metadata,"
|
|
366
|
+
"expires_at,version,created_at,updated_at FROM agent_memories WHERE namespace=?",
|
|
367
|
+
(namespace,),
|
|
368
|
+
).fetchall()
|
|
369
|
+
|
|
370
|
+
rows = await _run_in_thread(_op)
|
|
371
|
+
results = []
|
|
372
|
+
for row in rows:
|
|
373
|
+
value = self._row(row)
|
|
374
|
+
if value.is_expired() or not self._can_access(value, access):
|
|
375
|
+
continue
|
|
376
|
+
if not self._metadata_matches(value, metadata):
|
|
377
|
+
continue
|
|
378
|
+
score = self._score(value, query)
|
|
379
|
+
if query and score <= 0:
|
|
380
|
+
continue
|
|
381
|
+
results.append(MemorySearchResult(value, score))
|
|
382
|
+
results.sort(key=lambda item: (item.score, item.record.updated_at), reverse=True)
|
|
383
|
+
return results[:limit]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class PostgresMemoryStore(_MemoryAccessMixin):
|
|
387
|
+
"""生产环境使用的 PostgreSQL 长期记忆存储。"""
|
|
388
|
+
|
|
389
|
+
def __init__(self, dsn: str, *, require_access: bool = False) -> None:
|
|
390
|
+
super().__init__(require_access=require_access)
|
|
391
|
+
self._dsn = dsn.replace("postgresql+asyncpg://", "postgresql://")
|
|
392
|
+
self._pool: Any = None
|
|
393
|
+
|
|
394
|
+
async def initialize(self) -> None:
|
|
395
|
+
try:
|
|
396
|
+
asyncpg = import_module("asyncpg")
|
|
397
|
+
except ImportError as exc:
|
|
398
|
+
raise ImportError("PostgresMemoryStore 需要 asyncpg") from exc
|
|
399
|
+
self._pool = await asyncpg.create_pool(self._dsn, min_size=2, max_size=10)
|
|
400
|
+
async with self._pool.acquire() as conn:
|
|
401
|
+
await conn.execute("""
|
|
402
|
+
CREATE TABLE IF NOT EXISTS agent_memories (
|
|
403
|
+
memory_id TEXT PRIMARY KEY,
|
|
404
|
+
namespace TEXT NOT NULL,
|
|
405
|
+
memory_key TEXT,
|
|
406
|
+
content TEXT NOT NULL,
|
|
407
|
+
user_id TEXT,
|
|
408
|
+
tenant_id TEXT,
|
|
409
|
+
source TEXT,
|
|
410
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
411
|
+
expires_at TIMESTAMPTZ,
|
|
412
|
+
version INTEGER NOT NULL,
|
|
413
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
414
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
415
|
+
);
|
|
416
|
+
CREATE INDEX IF NOT EXISTS idx_agent_memories_namespace
|
|
417
|
+
ON agent_memories(namespace, tenant_id, user_id, updated_at)
|
|
418
|
+
""")
|
|
419
|
+
|
|
420
|
+
async def close(self) -> None:
|
|
421
|
+
if self._pool is not None:
|
|
422
|
+
await self._pool.close()
|
|
423
|
+
|
|
424
|
+
def _check(self) -> None:
|
|
425
|
+
if self._pool is None:
|
|
426
|
+
raise RuntimeError("PostgresMemoryStore 未初始化")
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _row(row: Any) -> MemoryRecord:
|
|
430
|
+
value = dict(row)
|
|
431
|
+
metadata = value.get("metadata")
|
|
432
|
+
value["metadata"] = json.loads(metadata) if isinstance(metadata, str) else dict(metadata or {})
|
|
433
|
+
for name in ("expires_at", "created_at", "updated_at"):
|
|
434
|
+
if value.get(name) is not None and not isinstance(value[name], str):
|
|
435
|
+
value[name] = value[name].isoformat()
|
|
436
|
+
value["key"] = value.pop("memory_key", None)
|
|
437
|
+
return MemoryRecord.from_dict(value)
|
|
438
|
+
|
|
439
|
+
async def put(
|
|
440
|
+
self,
|
|
441
|
+
record: MemoryRecord,
|
|
442
|
+
*,
|
|
443
|
+
expected_version: int | None = None,
|
|
444
|
+
access: AccessContext | None = None,
|
|
445
|
+
) -> MemoryRecord:
|
|
446
|
+
self._check()
|
|
447
|
+
value = self._bind(record, access)
|
|
448
|
+
async with self._pool.acquire() as conn:
|
|
449
|
+
async with conn.transaction():
|
|
450
|
+
row = await conn.fetchrow("SELECT * FROM agent_memories WHERE memory_id=$1", value.memory_id)
|
|
451
|
+
current = self._row(row) if row else None
|
|
452
|
+
if current is not None and not self._can_access(current, access):
|
|
453
|
+
raise PermissionError("无权更新该记忆")
|
|
454
|
+
actual = current.version if current else 0
|
|
455
|
+
if expected_version is not None and expected_version != actual:
|
|
456
|
+
raise MemoryConflictError(value.memory_id, expected_version, actual)
|
|
457
|
+
value.version = actual + 1
|
|
458
|
+
value.created_at = current.created_at if current else value.created_at
|
|
459
|
+
value.updated_at = memory_now()
|
|
460
|
+
if current is None:
|
|
461
|
+
try:
|
|
462
|
+
await conn.execute(
|
|
463
|
+
"INSERT INTO agent_memories(memory_id,namespace,memory_key,content,"
|
|
464
|
+
"user_id,tenant_id,"
|
|
465
|
+
"source,metadata,expires_at,version,created_at,updated_at) "
|
|
466
|
+
"VALUES($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9,$10,$11,$12)",
|
|
467
|
+
value.memory_id,
|
|
468
|
+
value.namespace,
|
|
469
|
+
value.key,
|
|
470
|
+
value.content,
|
|
471
|
+
value.user_id,
|
|
472
|
+
value.tenant_id,
|
|
473
|
+
value.source,
|
|
474
|
+
json.dumps(value.metadata, ensure_ascii=False),
|
|
475
|
+
_postgres_datetime(value.expires_at),
|
|
476
|
+
value.version,
|
|
477
|
+
_postgres_datetime(value.created_at),
|
|
478
|
+
_postgres_datetime(value.updated_at),
|
|
479
|
+
)
|
|
480
|
+
except Exception as exc:
|
|
481
|
+
if getattr(exc, "sqlstate", None) == "23505":
|
|
482
|
+
raise MemoryConflictError(value.memory_id, 0, 1) from exc
|
|
483
|
+
raise
|
|
484
|
+
else:
|
|
485
|
+
status = await conn.execute(
|
|
486
|
+
"UPDATE agent_memories SET namespace=$2,memory_key=$3,content=$4,user_id=$5,"
|
|
487
|
+
"tenant_id=$6,source=$7,metadata=$8::jsonb,expires_at=$9,version=$10,"
|
|
488
|
+
"updated_at=$11 WHERE memory_id=$1 AND version=$12",
|
|
489
|
+
value.memory_id,
|
|
490
|
+
value.namespace,
|
|
491
|
+
value.key,
|
|
492
|
+
value.content,
|
|
493
|
+
value.user_id,
|
|
494
|
+
value.tenant_id,
|
|
495
|
+
value.source,
|
|
496
|
+
json.dumps(value.metadata, ensure_ascii=False),
|
|
497
|
+
_postgres_datetime(value.expires_at),
|
|
498
|
+
value.version,
|
|
499
|
+
_postgres_datetime(value.updated_at),
|
|
500
|
+
actual,
|
|
501
|
+
)
|
|
502
|
+
if not status.endswith(" 1"):
|
|
503
|
+
raise MemoryConflictError(value.memory_id, actual, actual + 1)
|
|
504
|
+
return MemoryRecord.from_dict(value.to_dict())
|
|
505
|
+
|
|
506
|
+
async def get(
|
|
507
|
+
self,
|
|
508
|
+
memory_id: str,
|
|
509
|
+
*,
|
|
510
|
+
access: AccessContext | None = None,
|
|
511
|
+
) -> MemoryRecord | None:
|
|
512
|
+
self._check()
|
|
513
|
+
self._require_access(access)
|
|
514
|
+
async with self._pool.acquire() as conn:
|
|
515
|
+
row = await conn.fetchrow("SELECT * FROM agent_memories WHERE memory_id=$1", memory_id)
|
|
516
|
+
if row is None:
|
|
517
|
+
return None
|
|
518
|
+
value = self._row(row)
|
|
519
|
+
if value.is_expired():
|
|
520
|
+
await conn.execute("DELETE FROM agent_memories WHERE memory_id=$1", memory_id)
|
|
521
|
+
return None
|
|
522
|
+
if not self._can_access(value, access):
|
|
523
|
+
raise PermissionError("无权访问该记忆")
|
|
524
|
+
return value
|
|
525
|
+
|
|
526
|
+
async def delete(
|
|
527
|
+
self,
|
|
528
|
+
memory_id: str,
|
|
529
|
+
*,
|
|
530
|
+
expected_version: int | None = None,
|
|
531
|
+
access: AccessContext | None = None,
|
|
532
|
+
) -> bool:
|
|
533
|
+
current = await self.get(memory_id, access=access)
|
|
534
|
+
if current is None:
|
|
535
|
+
return False
|
|
536
|
+
if expected_version is not None and expected_version != current.version:
|
|
537
|
+
raise MemoryConflictError(memory_id, expected_version, current.version)
|
|
538
|
+
async with self._pool.acquire() as conn:
|
|
539
|
+
status = await conn.execute(
|
|
540
|
+
"DELETE FROM agent_memories WHERE memory_id=$1 AND version=$2",
|
|
541
|
+
memory_id,
|
|
542
|
+
current.version,
|
|
543
|
+
)
|
|
544
|
+
if not status.endswith(" 1"):
|
|
545
|
+
raise MemoryConflictError(memory_id, current.version, current.version + 1)
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
async def search(
|
|
549
|
+
self,
|
|
550
|
+
namespace: str,
|
|
551
|
+
*,
|
|
552
|
+
query: str = "",
|
|
553
|
+
metadata: dict[str, Any] | None = None,
|
|
554
|
+
limit: int = 20,
|
|
555
|
+
access: AccessContext | None = None,
|
|
556
|
+
) -> list[MemorySearchResult]:
|
|
557
|
+
self._check()
|
|
558
|
+
self._require_access(access)
|
|
559
|
+
if limit <= 0:
|
|
560
|
+
return []
|
|
561
|
+
async with self._pool.acquire() as conn:
|
|
562
|
+
rows = await conn.fetch(
|
|
563
|
+
"SELECT * FROM agent_memories WHERE namespace=$1 "
|
|
564
|
+
"AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY updated_at DESC",
|
|
565
|
+
namespace,
|
|
566
|
+
)
|
|
567
|
+
results = []
|
|
568
|
+
for row in rows:
|
|
569
|
+
value = self._row(row)
|
|
570
|
+
if not self._can_access(value, access) or not self._metadata_matches(value, metadata):
|
|
571
|
+
continue
|
|
572
|
+
score = self._score(value, query)
|
|
573
|
+
if query and score <= 0:
|
|
574
|
+
continue
|
|
575
|
+
results.append(MemorySearchResult(value, score))
|
|
576
|
+
results.sort(key=lambda item: (item.score, item.record.updated_at), reverse=True)
|
|
577
|
+
return results[:limit]
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def create_memory_store(
|
|
581
|
+
env: str = "development",
|
|
582
|
+
*,
|
|
583
|
+
sqlite_path: str = "./sessions.db",
|
|
584
|
+
postgres_url: str | None = None,
|
|
585
|
+
require_access: bool = False,
|
|
586
|
+
) -> MemoryStore:
|
|
587
|
+
"""按环境创建长期记忆存储。"""
|
|
588
|
+
|
|
589
|
+
normalized = env.lower()
|
|
590
|
+
if normalized in {"test", "testing"}:
|
|
591
|
+
return InMemoryMemoryStore(require_access=require_access)
|
|
592
|
+
if normalized in {"production", "prod"}:
|
|
593
|
+
if not postgres_url:
|
|
594
|
+
raise ValueError("生产环境需要配置 PostgreSQL 连接地址")
|
|
595
|
+
return PostgresMemoryStore(postgres_url, require_access=require_access)
|
|
596
|
+
return SqliteMemoryStore(sqlite_path, require_access=require_access)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
__all__ = [
|
|
600
|
+
"InMemoryMemoryStore",
|
|
601
|
+
"PostgresMemoryStore",
|
|
602
|
+
"SqliteMemoryStore",
|
|
603
|
+
"create_memory_store",
|
|
604
|
+
]
|