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,382 @@
|
|
|
1
|
+
"""轻量级长期上下文存储。
|
|
2
|
+
|
|
3
|
+
它保存业务引用和用户偏好,而不是把所有历史消息无限塞进 prompt。
|
|
4
|
+
应用按环境选择内存、SQLite 或 PostgreSQL;JSON 文件实现仅保留兼容。
|
|
5
|
+
|
|
6
|
+
存跨会话的业务记忆
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import copy
|
|
13
|
+
import json
|
|
14
|
+
import sqlite3
|
|
15
|
+
from collections.abc import Generator
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from agent_core.access import AccessContext
|
|
21
|
+
from agent_core.storage._sqlite_utils import (
|
|
22
|
+
_run_in_thread,
|
|
23
|
+
run_sync_in_thread,
|
|
24
|
+
sqlite_connection,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ──────────────────────────────────────────────
|
|
29
|
+
# 上下文存储实现
|
|
30
|
+
# ──────────────────────────────────────────────
|
|
31
|
+
class ContextStore:
|
|
32
|
+
"""按 thread 隔离的持久化上下文存储。"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
path: str | Path | None = "./agent_context.json",
|
|
37
|
+
*,
|
|
38
|
+
require_access: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._path = Path(path) if path else None
|
|
41
|
+
self._strict_access = require_access
|
|
42
|
+
self._data: dict[str, dict[str, Any]] = {}
|
|
43
|
+
self._lock = asyncio.Lock()
|
|
44
|
+
self._loaded = False
|
|
45
|
+
|
|
46
|
+
async def initialize(self) -> None:
|
|
47
|
+
"""初始化存储;文件和内存实现不需要额外操作。"""
|
|
48
|
+
|
|
49
|
+
async def close(self) -> None:
|
|
50
|
+
"""关闭存储;文件和内存实现不需要额外操作。"""
|
|
51
|
+
|
|
52
|
+
# 确保上下文数据已加载
|
|
53
|
+
async def _ensure_loaded(self) -> None:
|
|
54
|
+
if self._loaded:
|
|
55
|
+
return
|
|
56
|
+
if self._path and self._path.exists():
|
|
57
|
+
loaded = json.loads(self._path.read_text(encoding="utf-8"))
|
|
58
|
+
if not isinstance(loaded, dict):
|
|
59
|
+
raise ValueError(f"上下文文件根节点必须是对象:{self._path}")
|
|
60
|
+
self._data = loaded
|
|
61
|
+
self._loaded = True
|
|
62
|
+
|
|
63
|
+
# 刷新上下文数据到文件
|
|
64
|
+
async def _flush(self) -> None:
|
|
65
|
+
if not self._path:
|
|
66
|
+
return
|
|
67
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
tmp = self._path.with_suffix(self._path.suffix + ".tmp")
|
|
69
|
+
tmp.write_text(json.dumps(self._data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
70
|
+
tmp.replace(self._path)
|
|
71
|
+
|
|
72
|
+
# 检查上下文数据访问权限
|
|
73
|
+
def _check_scope(
|
|
74
|
+
self,
|
|
75
|
+
data: dict[str, Any],
|
|
76
|
+
access: AccessContext | None,
|
|
77
|
+
) -> None:
|
|
78
|
+
if self._strict_access and access is None:
|
|
79
|
+
raise PermissionError("该 ContextStore 要求提供 AccessContext")
|
|
80
|
+
owner = data.get("_access")
|
|
81
|
+
if self._strict_access and data and not isinstance(owner, dict):
|
|
82
|
+
raise PermissionError("该 thread 的上下文尚未绑定所有者")
|
|
83
|
+
if (
|
|
84
|
+
access
|
|
85
|
+
and isinstance(owner, dict)
|
|
86
|
+
and not access.can_access(owner.get("user_id"), owner.get("tenant_id"))
|
|
87
|
+
):
|
|
88
|
+
raise PermissionError("无权访问该 thread 的上下文")
|
|
89
|
+
|
|
90
|
+
# 获取上下文数据
|
|
91
|
+
async def get(
|
|
92
|
+
self,
|
|
93
|
+
thread_id: str,
|
|
94
|
+
*,
|
|
95
|
+
access: AccessContext | None = None,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
async with self._lock:
|
|
98
|
+
await self._ensure_loaded()
|
|
99
|
+
value = copy.deepcopy(self._data.get(thread_id, {}))
|
|
100
|
+
self._check_scope(value, access)
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
# 更新上下文数据
|
|
104
|
+
async def update(
|
|
105
|
+
self,
|
|
106
|
+
thread_id: str,
|
|
107
|
+
values: dict[str, Any],
|
|
108
|
+
*,
|
|
109
|
+
access: AccessContext | None = None,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
self._validate_values(values)
|
|
112
|
+
async with self._lock:
|
|
113
|
+
await self._ensure_loaded()
|
|
114
|
+
current = self._data.setdefault(thread_id, {})
|
|
115
|
+
self._check_scope(current, access)
|
|
116
|
+
if access and "_access" not in current:
|
|
117
|
+
current["_access"] = access.to_dict()
|
|
118
|
+
current.update(copy.deepcopy(values))
|
|
119
|
+
await self._flush()
|
|
120
|
+
return copy.deepcopy(current)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _validate_values(values: dict[str, Any]) -> None:
|
|
124
|
+
if "_access" in values:
|
|
125
|
+
raise ValueError("_access 是存储层保留字段,不能通过上下文更新修改")
|
|
126
|
+
|
|
127
|
+
# 清除上下文数据
|
|
128
|
+
async def clear(
|
|
129
|
+
self,
|
|
130
|
+
thread_id: str,
|
|
131
|
+
*,
|
|
132
|
+
access: AccessContext | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
async with self._lock:
|
|
135
|
+
await self._ensure_loaded()
|
|
136
|
+
self._check_scope(self._data.get(thread_id, {}), access)
|
|
137
|
+
self._data.pop(thread_id, None)
|
|
138
|
+
await self._flush()
|
|
139
|
+
|
|
140
|
+
# 统一端口名称,同时保留业务层已有的 get/update/clear 调用。
|
|
141
|
+
async def get_context(self, thread_id: str, *, access: AccessContext | None = None) -> dict[str, Any]:
|
|
142
|
+
return await self.get(thread_id, access=access)
|
|
143
|
+
|
|
144
|
+
async def update_context(
|
|
145
|
+
self,
|
|
146
|
+
thread_id: str,
|
|
147
|
+
values: dict[str, Any],
|
|
148
|
+
*,
|
|
149
|
+
access: AccessContext | None = None,
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
return await self.update(thread_id, values, access=access)
|
|
152
|
+
|
|
153
|
+
async def clear_context(self, thread_id: str, *, access: AccessContext | None = None) -> None:
|
|
154
|
+
await self.clear(thread_id, access=access)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ──────────────────────────────────────────────
|
|
158
|
+
# 内存上下文存储实现
|
|
159
|
+
# ──────────────────────────────────────────────
|
|
160
|
+
class MemoryContextStore(ContextStore):
|
|
161
|
+
"""测试专用内存上下文。"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, *, require_access: bool = False) -> None:
|
|
164
|
+
super().__init__(path=None, require_access=require_access)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ──────────────────────────────────────────────
|
|
168
|
+
# SQLite 上下文存储实现
|
|
169
|
+
# ──────────────────────────────────────────────
|
|
170
|
+
class SqliteContextStore(ContextStore):
|
|
171
|
+
"""开发环境 SQLite 上下文存储。"""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
db_path: str | Path = "./sessions.db",
|
|
176
|
+
*,
|
|
177
|
+
require_access: bool = False,
|
|
178
|
+
) -> None:
|
|
179
|
+
self._db_path = str(Path(db_path).resolve())
|
|
180
|
+
self._strict_access = require_access
|
|
181
|
+
# 建表 DDL/PRAGMA 初始化同样放入工作线程执行,与 async 方法共用同一连接参数规范。
|
|
182
|
+
run_sync_in_thread(self._init_db)
|
|
183
|
+
|
|
184
|
+
def _init_db(self) -> None:
|
|
185
|
+
with self._connect() as conn:
|
|
186
|
+
conn.execute(
|
|
187
|
+
"""CREATE TABLE IF NOT EXISTS agent_context (
|
|
188
|
+
thread_id TEXT PRIMARY KEY,
|
|
189
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
190
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
191
|
+
)"""
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# 连接 SQLite 数据库
|
|
195
|
+
@contextmanager
|
|
196
|
+
def _connect(self) -> Generator[sqlite3.Connection]:
|
|
197
|
+
"""打开一个自动提交或回滚的 SQLite 连接(统一 timeout 与 WAL 规范)。"""
|
|
198
|
+
with sqlite_connection(self._db_path) as conn:
|
|
199
|
+
yield conn
|
|
200
|
+
|
|
201
|
+
# 获取上下文数据
|
|
202
|
+
async def get(
|
|
203
|
+
self,
|
|
204
|
+
thread_id: str,
|
|
205
|
+
*,
|
|
206
|
+
access: AccessContext | None = None,
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
def _op() -> Any:
|
|
209
|
+
with self._connect() as conn:
|
|
210
|
+
return conn.execute(
|
|
211
|
+
"SELECT data FROM agent_context WHERE thread_id = ?", (thread_id,)
|
|
212
|
+
).fetchone()
|
|
213
|
+
|
|
214
|
+
row = await _run_in_thread(_op)
|
|
215
|
+
value = json.loads(row[0]) if row else {}
|
|
216
|
+
self._check_scope(value, access)
|
|
217
|
+
return value
|
|
218
|
+
|
|
219
|
+
# 更新上下文数据
|
|
220
|
+
async def update(
|
|
221
|
+
self,
|
|
222
|
+
thread_id: str,
|
|
223
|
+
values: dict[str, Any],
|
|
224
|
+
*,
|
|
225
|
+
access: AccessContext | None = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
self._validate_values(values)
|
|
228
|
+
|
|
229
|
+
def _op() -> dict[str, Any]:
|
|
230
|
+
with self._connect() as conn:
|
|
231
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
232
|
+
row = conn.execute(
|
|
233
|
+
"SELECT data FROM agent_context WHERE thread_id = ?",
|
|
234
|
+
(thread_id,),
|
|
235
|
+
).fetchone()
|
|
236
|
+
current = json.loads(row[0]) if row else {}
|
|
237
|
+
self._check_scope(current, access)
|
|
238
|
+
if access and "_access" not in current:
|
|
239
|
+
current["_access"] = access.to_dict()
|
|
240
|
+
current.update(copy.deepcopy(values))
|
|
241
|
+
conn.execute(
|
|
242
|
+
"""INSERT INTO agent_context(thread_id, data) VALUES (?, ?)
|
|
243
|
+
ON CONFLICT(thread_id) DO UPDATE SET data=excluded.data,
|
|
244
|
+
updated_at=CURRENT_TIMESTAMP""",
|
|
245
|
+
(thread_id, json.dumps(current, ensure_ascii=False)),
|
|
246
|
+
)
|
|
247
|
+
return copy.deepcopy(current)
|
|
248
|
+
|
|
249
|
+
return await _run_in_thread(_op)
|
|
250
|
+
|
|
251
|
+
# 清除上下文数据
|
|
252
|
+
async def clear(
|
|
253
|
+
self,
|
|
254
|
+
thread_id: str,
|
|
255
|
+
*,
|
|
256
|
+
access: AccessContext | None = None,
|
|
257
|
+
) -> None:
|
|
258
|
+
await self.get(thread_id, access=access)
|
|
259
|
+
|
|
260
|
+
def _op() -> None:
|
|
261
|
+
with self._connect() as conn:
|
|
262
|
+
conn.execute("DELETE FROM agent_context WHERE thread_id = ?", (thread_id,))
|
|
263
|
+
|
|
264
|
+
await _run_in_thread(_op)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ──────────────────────────────────────────────
|
|
268
|
+
# PostgreSQL 上下文存储实现
|
|
269
|
+
# ──────────────────────────────────────────────
|
|
270
|
+
class PostgresContextStore(ContextStore):
|
|
271
|
+
"""生产环境 PostgreSQL 上下文存储。"""
|
|
272
|
+
|
|
273
|
+
def __init__(self, dsn: str, *, require_access: bool = False) -> None:
|
|
274
|
+
self._dsn = dsn.replace("postgresql+asyncpg://", "postgresql://")
|
|
275
|
+
self._pool: Any = None
|
|
276
|
+
self._strict_access = require_access
|
|
277
|
+
|
|
278
|
+
# 初始化 PostgreSQL 数据库连接池
|
|
279
|
+
# 创建上下文表
|
|
280
|
+
async def initialize(self) -> None:
|
|
281
|
+
try:
|
|
282
|
+
import asyncpg
|
|
283
|
+
except ImportError as exc:
|
|
284
|
+
raise ImportError(
|
|
285
|
+
"PostgresContextStore 需要 asyncpg,请执行:uv sync --extra production"
|
|
286
|
+
) from exc
|
|
287
|
+
self._pool = await asyncpg.create_pool(self._dsn, min_size=2, max_size=10)
|
|
288
|
+
async with self._pool.acquire() as conn:
|
|
289
|
+
# 创建上下文表
|
|
290
|
+
await conn.execute("""
|
|
291
|
+
CREATE TABLE IF NOT EXISTS agent_context (
|
|
292
|
+
thread_id TEXT PRIMARY KEY,
|
|
293
|
+
data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
294
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
295
|
+
)
|
|
296
|
+
""")
|
|
297
|
+
|
|
298
|
+
def _check(self) -> None:
|
|
299
|
+
if self._pool is None:
|
|
300
|
+
raise RuntimeError("PostgresContextStore 未初始化")
|
|
301
|
+
|
|
302
|
+
async def close(self) -> None:
|
|
303
|
+
if self._pool:
|
|
304
|
+
await self._pool.close()
|
|
305
|
+
|
|
306
|
+
async def get(
|
|
307
|
+
self,
|
|
308
|
+
thread_id: str,
|
|
309
|
+
*,
|
|
310
|
+
access: AccessContext | None = None,
|
|
311
|
+
) -> dict[str, Any]:
|
|
312
|
+
self._check()
|
|
313
|
+
async with self._pool.acquire() as conn:
|
|
314
|
+
row = await conn.fetchrow(
|
|
315
|
+
"SELECT data FROM agent_context WHERE thread_id=$1",
|
|
316
|
+
thread_id,
|
|
317
|
+
)
|
|
318
|
+
if row is None:
|
|
319
|
+
self._check_scope({}, access)
|
|
320
|
+
return {}
|
|
321
|
+
value = json.loads(row["data"]) if isinstance(row["data"], str) else dict(row["data"])
|
|
322
|
+
self._check_scope(value, access)
|
|
323
|
+
return value
|
|
324
|
+
|
|
325
|
+
async def update(
|
|
326
|
+
self,
|
|
327
|
+
thread_id: str,
|
|
328
|
+
values: dict[str, Any],
|
|
329
|
+
*,
|
|
330
|
+
access: AccessContext | None = None,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
self._check()
|
|
333
|
+
self._validate_values(values)
|
|
334
|
+
candidate = copy.deepcopy(values)
|
|
335
|
+
if access:
|
|
336
|
+
candidate = {"_access": access.to_dict(), **candidate}
|
|
337
|
+
async with self._pool.acquire() as conn:
|
|
338
|
+
async with conn.transaction():
|
|
339
|
+
# 冲突更新永远保留已有 _access;首次并发认领后再校验返回归属。
|
|
340
|
+
row = await conn.fetchrow(
|
|
341
|
+
"""INSERT INTO agent_context(thread_id,data) VALUES ($1,$2::jsonb)
|
|
342
|
+
ON CONFLICT(thread_id) DO UPDATE SET
|
|
343
|
+
data=agent_context.data || (EXCLUDED.data - '_access'), updated_at=NOW()
|
|
344
|
+
RETURNING data""",
|
|
345
|
+
thread_id,
|
|
346
|
+
json.dumps(candidate, ensure_ascii=False),
|
|
347
|
+
)
|
|
348
|
+
result = json.loads(row["data"]) if isinstance(row["data"], str) else dict(row["data"])
|
|
349
|
+
self._check_scope(result, access)
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
async def clear(
|
|
353
|
+
self,
|
|
354
|
+
thread_id: str,
|
|
355
|
+
*,
|
|
356
|
+
access: AccessContext | None = None,
|
|
357
|
+
) -> None:
|
|
358
|
+
await self.get(thread_id, access=access)
|
|
359
|
+
self._check()
|
|
360
|
+
async with self._pool.acquire() as conn:
|
|
361
|
+
await conn.execute("DELETE FROM agent_context WHERE thread_id=$1", thread_id)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ──────────────────────────────────────────────
|
|
365
|
+
# 创建上下文存储
|
|
366
|
+
# ──────────────────────────────────────────────
|
|
367
|
+
def create_context_store(
|
|
368
|
+
env: str = "development",
|
|
369
|
+
*,
|
|
370
|
+
sqlite_path: str = "./sessions.db",
|
|
371
|
+
postgres_url: str | None = None,
|
|
372
|
+
require_access: bool = False,
|
|
373
|
+
) -> ContextStore:
|
|
374
|
+
"""按环境创建上下文存储。"""
|
|
375
|
+
normalized = env.lower()
|
|
376
|
+
if normalized in {"test", "testing"}:
|
|
377
|
+
return MemoryContextStore(require_access=require_access)
|
|
378
|
+
if normalized in {"production", "prod"}:
|
|
379
|
+
if not postgres_url:
|
|
380
|
+
raise ValueError("生产环境需要配置 PostgreSQL 连接地址")
|
|
381
|
+
return PostgresContextStore(postgres_url, require_access=require_access)
|
|
382
|
+
return SqliteContextStore(sqlite_path, require_access=require_access)
|