agentic-composite-memory 0.1.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.
- agentic_composite_memory/__init__.py +2 -0
- agentic_composite_memory/py.typed +0 -0
- agentic_composite_memory-0.1.0.dist-info/METADATA +167 -0
- agentic_composite_memory-0.1.0.dist-info/RECORD +31 -0
- agentic_composite_memory-0.1.0.dist-info/WHEEL +4 -0
- agentic_composite_memory-0.1.0.dist-info/entry_points.txt +2 -0
- agentic_composite_memory-0.1.0.dist-info/licenses/LICENSE +17 -0
- agentic_memory_sdk/__init__.py +163 -0
- agentic_memory_sdk/adapters/__init__.py +33 -0
- agentic_memory_sdk/adapters/in_memory.py +439 -0
- agentic_memory_sdk/adapters/neo4j_cypher.py +65 -0
- agentic_memory_sdk/adapters/neptune_cypher.py +309 -0
- agentic_memory_sdk/adapters/opensearch_seed.py +322 -0
- agentic_memory_sdk/adapters/postgres_pgvector.py +484 -0
- agentic_memory_sdk/adapters/postgres_unified.py +511 -0
- agentic_memory_sdk/adapters/redis_playbook.py +157 -0
- agentic_memory_sdk/adapters/redis_unified.py +490 -0
- agentic_memory_sdk/adk_integration.py +250 -0
- agentic_memory_sdk/auth.py +380 -0
- agentic_memory_sdk/cli.py +137 -0
- agentic_memory_sdk/governance.py +713 -0
- agentic_memory_sdk/llm_extractors.py +616 -0
- agentic_memory_sdk/models.py +443 -0
- agentic_memory_sdk/ontology_loader.py +189 -0
- agentic_memory_sdk/ports.py +159 -0
- agentic_memory_sdk/py.typed +0 -0
- agentic_memory_sdk/server/__init__.py +4 -0
- agentic_memory_sdk/server/api.py +328 -0
- agentic_memory_sdk/server/main.py +384 -0
- agentic_memory_sdk/server/mcp_server.py +198 -0
- agentic_memory_sdk/service.py +1214 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
from ..governance import EntityResolutionGuard
|
|
8
|
+
from ..models import (
|
|
9
|
+
CompiledTBox,
|
|
10
|
+
EpisodicRecord,
|
|
11
|
+
GraphEdgeMutation,
|
|
12
|
+
GraphFactReadResult,
|
|
13
|
+
GraphVertexUpsert,
|
|
14
|
+
OutboxOperation,
|
|
15
|
+
PlatformTenantContext,
|
|
16
|
+
ReasoningTraceStep,
|
|
17
|
+
)
|
|
18
|
+
from ..ports import (
|
|
19
|
+
EpisodicAndSeedStoreAdapter,
|
|
20
|
+
GraphStoreAdapter,
|
|
21
|
+
RegistryStoreAdapter,
|
|
22
|
+
)
|
|
23
|
+
from .redis_playbook import RedisPlaybookAndWorkingAdapter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RedisRegistryAdapter(RedisPlaybookAndWorkingAdapter, RegistryStoreAdapter):
|
|
27
|
+
"""
|
|
28
|
+
Standalone Redis implementation of `RegistryStoreAdapter`.
|
|
29
|
+
Can be paired with any Seed, Graph, or Playbook adapter.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def _k_registry(self, tenant_id: str) -> str:
|
|
33
|
+
return f"{self.key_prefix}:registry:{tenant_id}"
|
|
34
|
+
|
|
35
|
+
async def register_app(
|
|
36
|
+
self,
|
|
37
|
+
tenant_id: str,
|
|
38
|
+
app_name: str,
|
|
39
|
+
read_namespaces: list[str],
|
|
40
|
+
write_namespace: str,
|
|
41
|
+
tbox: CompiledTBox,
|
|
42
|
+
) -> None:
|
|
43
|
+
r = await self._get_redis()
|
|
44
|
+
if r is None:
|
|
45
|
+
raise RuntimeError("RedisRegistryAdapter requires a valid `redis_url`.")
|
|
46
|
+
payload = {
|
|
47
|
+
"tenant_id": tenant_id,
|
|
48
|
+
"app_name": app_name,
|
|
49
|
+
"read_namespaces": read_namespaces,
|
|
50
|
+
"write_namespace": write_namespace,
|
|
51
|
+
"tbox": tbox.model_dump(mode="json"),
|
|
52
|
+
}
|
|
53
|
+
await r.hset(self._k_registry(tenant_id), app_name, json.dumps(payload))
|
|
54
|
+
|
|
55
|
+
async def resolve_context(
|
|
56
|
+
self, tenant_id: str, app_name: str, user_id: str
|
|
57
|
+
) -> PlatformTenantContext:
|
|
58
|
+
r = await self._get_redis()
|
|
59
|
+
if r is None:
|
|
60
|
+
raise RuntimeError("RedisRegistryAdapter requires a valid `redis_url`.")
|
|
61
|
+
app_private_ns = f"app:{tenant_id}:{app_name}"
|
|
62
|
+
user_private_ns = f"user:{tenant_id}:{user_id}"
|
|
63
|
+
raw = await r.hget(self._k_registry(tenant_id), app_name)
|
|
64
|
+
if not raw:
|
|
65
|
+
return PlatformTenantContext(
|
|
66
|
+
tenant_id=tenant_id,
|
|
67
|
+
app_name=app_name,
|
|
68
|
+
user_id=user_id,
|
|
69
|
+
app_private_namespace=app_private_ns,
|
|
70
|
+
write_namespace=app_private_ns,
|
|
71
|
+
read_namespaces=[
|
|
72
|
+
app_private_ns,
|
|
73
|
+
user_private_ns,
|
|
74
|
+
f"global:{tenant_id}:core",
|
|
75
|
+
],
|
|
76
|
+
tbox=CompiledTBox(),
|
|
77
|
+
)
|
|
78
|
+
data = json.loads(raw)
|
|
79
|
+
merged_read_ns = list(
|
|
80
|
+
dict.fromkeys([*list(data["read_namespaces"]), user_private_ns])
|
|
81
|
+
)
|
|
82
|
+
return PlatformTenantContext(
|
|
83
|
+
tenant_id=tenant_id,
|
|
84
|
+
app_name=app_name,
|
|
85
|
+
user_id=user_id,
|
|
86
|
+
app_private_namespace=app_private_ns,
|
|
87
|
+
write_namespace=data["write_namespace"],
|
|
88
|
+
read_namespaces=merged_read_ns,
|
|
89
|
+
tbox=CompiledTBox.model_validate(data["tbox"]),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RedisEpisodicAndSeedAdapter(
|
|
94
|
+
RedisPlaybookAndWorkingAdapter, EpisodicAndSeedStoreAdapter
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Standalone Redis implementation of `EpisodicAndSeedStoreAdapter` (Tier 1 Episodic
|
|
98
|
+
Storage, Tier 2 Hybrid RRF Seeding, Entity Resolution Guard, and Transactional Outbox).
|
|
99
|
+
Can be paired with Neo4j, Neptune, PostgresGraphAdapter, or RedisGraphAdapter.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def _k_episodes(self, tenant_id: str) -> str:
|
|
103
|
+
return f"{self.key_prefix}:episodes:{tenant_id}"
|
|
104
|
+
|
|
105
|
+
def _k_vertices(self, tenant_id: str) -> str:
|
|
106
|
+
return f"{self.key_prefix}:vertices:{tenant_id}"
|
|
107
|
+
|
|
108
|
+
def _k_outbox(self) -> str:
|
|
109
|
+
return f"{self.key_prefix}:outbox:global"
|
|
110
|
+
|
|
111
|
+
async def resolve_existing_vertex_id(
|
|
112
|
+
self,
|
|
113
|
+
ctx: PlatformTenantContext,
|
|
114
|
+
entity_class: str,
|
|
115
|
+
canonical_name: str,
|
|
116
|
+
embedding: list[float],
|
|
117
|
+
similarity_threshold: float = 0.91,
|
|
118
|
+
) -> str | None:
|
|
119
|
+
r = await self._get_redis()
|
|
120
|
+
raw_vertices = await r.hvals(self._k_vertices(ctx.tenant_id))
|
|
121
|
+
best_vid: str | None = None
|
|
122
|
+
best_score: float = -1.0
|
|
123
|
+
for raw in raw_vertices:
|
|
124
|
+
v = GraphVertexUpsert.model_validate_json(raw)
|
|
125
|
+
if v.namespace not in ctx.read_namespaces or v.entity_class != entity_class:
|
|
126
|
+
continue
|
|
127
|
+
sim = self._cosine(v.embedding, embedding)
|
|
128
|
+
if EntityResolutionGuard.can_merge(
|
|
129
|
+
v.canonical_name, canonical_name, sim, similarity_threshold
|
|
130
|
+
):
|
|
131
|
+
if v.canonical_name.strip().lower() == canonical_name.strip().lower():
|
|
132
|
+
return v.vertex_id
|
|
133
|
+
if sim > best_score:
|
|
134
|
+
best_score = sim
|
|
135
|
+
best_vid = v.vertex_id
|
|
136
|
+
return best_vid
|
|
137
|
+
|
|
138
|
+
async def commit_episodes_and_outbox(
|
|
139
|
+
self,
|
|
140
|
+
ctx: PlatformTenantContext,
|
|
141
|
+
episodes: Sequence[EpisodicRecord],
|
|
142
|
+
vertex_upserts: Sequence[GraphVertexUpsert],
|
|
143
|
+
outbox_op: OutboxOperation | None,
|
|
144
|
+
) -> None:
|
|
145
|
+
r = await self._get_redis()
|
|
146
|
+
pipe = r.pipeline(transaction=True)
|
|
147
|
+
for ep in episodes:
|
|
148
|
+
pipe.hset(
|
|
149
|
+
self._k_episodes(ctx.tenant_id),
|
|
150
|
+
ep.episode_id,
|
|
151
|
+
ep.model_dump_json(),
|
|
152
|
+
)
|
|
153
|
+
for v in vertex_upserts:
|
|
154
|
+
pipe.hset(
|
|
155
|
+
self._k_vertices(ctx.tenant_id),
|
|
156
|
+
v.vertex_id,
|
|
157
|
+
v.model_dump_json(),
|
|
158
|
+
)
|
|
159
|
+
if outbox_op is not None:
|
|
160
|
+
pipe.hset(
|
|
161
|
+
self._k_outbox(),
|
|
162
|
+
outbox_op.outbox_id,
|
|
163
|
+
outbox_op.model_dump_json(),
|
|
164
|
+
)
|
|
165
|
+
await pipe.execute()
|
|
166
|
+
|
|
167
|
+
async def hybrid_seed_search(
|
|
168
|
+
self,
|
|
169
|
+
ctx: PlatformTenantContext,
|
|
170
|
+
query_text: str,
|
|
171
|
+
query_embedding: list[float],
|
|
172
|
+
limit: int = 8,
|
|
173
|
+
) -> list[GraphVertexUpsert]:
|
|
174
|
+
r = await self._get_redis()
|
|
175
|
+
raw_vertices = await r.hvals(self._k_vertices(ctx.tenant_id))
|
|
176
|
+
candidates: list[GraphVertexUpsert] = []
|
|
177
|
+
pinned_user_seeds: list[GraphVertexUpsert] = []
|
|
178
|
+
user_ns = f"user:{ctx.tenant_id}:{ctx.user_id}"
|
|
179
|
+
|
|
180
|
+
for raw in raw_vertices:
|
|
181
|
+
v = GraphVertexUpsert.model_validate_json(raw)
|
|
182
|
+
if v.namespace not in ctx.read_namespaces:
|
|
183
|
+
continue
|
|
184
|
+
if v.namespace == user_ns:
|
|
185
|
+
pinned_user_seeds.append(v)
|
|
186
|
+
candidates.append(v)
|
|
187
|
+
|
|
188
|
+
if not candidates:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
q_tokens = set(re.findall(r"[a-z0-9_]+", query_text.lower()))
|
|
192
|
+
vec_ranked = sorted(
|
|
193
|
+
candidates,
|
|
194
|
+
key=lambda x: self._cosine(x.embedding, query_embedding),
|
|
195
|
+
reverse=True,
|
|
196
|
+
)
|
|
197
|
+
lex_ranked = sorted(
|
|
198
|
+
candidates,
|
|
199
|
+
key=lambda x: len(
|
|
200
|
+
q_tokens
|
|
201
|
+
& set(
|
|
202
|
+
re.findall(
|
|
203
|
+
r"[a-z0-9_]+", f"{x.canonical_name} {x.summary}".lower()
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
),
|
|
207
|
+
reverse=True,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
rrf_scores: dict[str, float] = {}
|
|
211
|
+
by_id: dict[str, GraphVertexUpsert] = {}
|
|
212
|
+
for rank, v in enumerate(vec_ranked, start=1):
|
|
213
|
+
by_id[v.vertex_id] = v
|
|
214
|
+
rrf_scores[v.vertex_id] = rrf_scores.get(v.vertex_id, 0.0) + 1.0 / (
|
|
215
|
+
60 + rank
|
|
216
|
+
)
|
|
217
|
+
for rank, v in enumerate(lex_ranked, start=1):
|
|
218
|
+
by_id[v.vertex_id] = v
|
|
219
|
+
rrf_scores[v.vertex_id] = rrf_scores.get(v.vertex_id, 0.0) + 1.0 / (
|
|
220
|
+
60 + rank
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
sorted_ids = sorted(
|
|
224
|
+
rrf_scores.keys(), key=lambda vid: rrf_scores[vid], reverse=True
|
|
225
|
+
)
|
|
226
|
+
top_seeds = [by_id[vid] for vid in sorted_ids[:limit]]
|
|
227
|
+
seen_ids = {v.vertex_id for v in top_seeds}
|
|
228
|
+
for pv in pinned_user_seeds:
|
|
229
|
+
if pv.vertex_id not in seen_ids:
|
|
230
|
+
top_seeds.append(pv)
|
|
231
|
+
seen_ids.add(pv.vertex_id)
|
|
232
|
+
return top_seeds
|
|
233
|
+
|
|
234
|
+
async def search_episodes(
|
|
235
|
+
self,
|
|
236
|
+
ctx: PlatformTenantContext,
|
|
237
|
+
query_embedding: list[float],
|
|
238
|
+
limit: int = 4,
|
|
239
|
+
) -> list[EpisodicRecord]:
|
|
240
|
+
r = await self._get_redis()
|
|
241
|
+
raw_eps = await r.hvals(self._k_episodes(ctx.tenant_id))
|
|
242
|
+
eps: list[EpisodicRecord] = []
|
|
243
|
+
for raw in raw_eps:
|
|
244
|
+
ep = EpisodicRecord.model_validate_json(raw)
|
|
245
|
+
if ep.app_name == ctx.app_name and ep.user_id == ctx.user_id:
|
|
246
|
+
eps.append(ep)
|
|
247
|
+
eps.sort(key=lambda e: self._cosine(e.embedding, query_embedding), reverse=True)
|
|
248
|
+
return eps[:limit]
|
|
249
|
+
|
|
250
|
+
async def fetch_pending_outbox(self, batch_size: int = 25) -> list[OutboxOperation]:
|
|
251
|
+
r = await self._get_redis()
|
|
252
|
+
raw_ops = await r.hvals(self._k_outbox())
|
|
253
|
+
pending: list[OutboxOperation] = []
|
|
254
|
+
for raw in raw_ops:
|
|
255
|
+
op = OutboxOperation.model_validate_json(raw)
|
|
256
|
+
if op.status in ("PENDING", "RETRY") and op.attempts < 5:
|
|
257
|
+
pending.append(op)
|
|
258
|
+
return pending[:batch_size]
|
|
259
|
+
|
|
260
|
+
async def mark_outbox_processed(self, outbox_id: str) -> None:
|
|
261
|
+
r = await self._get_redis()
|
|
262
|
+
raw = await r.hget(self._k_outbox(), outbox_id)
|
|
263
|
+
if raw:
|
|
264
|
+
op = OutboxOperation.model_validate_json(raw)
|
|
265
|
+
op.status = "PROCESSED"
|
|
266
|
+
await r.hset(self._k_outbox(), outbox_id, op.model_dump_json())
|
|
267
|
+
|
|
268
|
+
async def mark_outbox_failed(self, outbox_id: str, error_msg: str) -> None:
|
|
269
|
+
r = await self._get_redis()
|
|
270
|
+
raw = await r.hget(self._k_outbox(), outbox_id)
|
|
271
|
+
if raw:
|
|
272
|
+
op = OutboxOperation.model_validate_json(raw)
|
|
273
|
+
op.attempts += 1
|
|
274
|
+
op.status = "DEAD_LETTER" if op.attempts >= 5 else "RETRY"
|
|
275
|
+
await r.hset(self._k_outbox(), outbox_id, op.model_dump_json())
|
|
276
|
+
|
|
277
|
+
async def purge_user_data(self, ctx: PlatformTenantContext) -> list[str]:
|
|
278
|
+
r = await self._get_redis()
|
|
279
|
+
raw_map = await r.hgetall(self._k_episodes(ctx.tenant_id))
|
|
280
|
+
purged_ids: list[str] = []
|
|
281
|
+
for ep_id, raw in raw_map.items():
|
|
282
|
+
ep = EpisodicRecord.model_validate_json(raw)
|
|
283
|
+
if ep.user_id == ctx.user_id:
|
|
284
|
+
purged_ids.append(ep_id)
|
|
285
|
+
if purged_ids:
|
|
286
|
+
await r.hdel(self._k_episodes(ctx.tenant_id), *purged_ids)
|
|
287
|
+
return purged_ids
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class RedisGraphAdapter(RedisPlaybookAndWorkingAdapter, GraphStoreAdapter):
|
|
291
|
+
"""
|
|
292
|
+
Standalone Redis implementation of `GraphStoreAdapter` (Bi-Temporal Semantic Graph
|
|
293
|
+
with functional edge invalidation & Tier 3 Procedural Reasoning Traces).
|
|
294
|
+
Can be paired with PostgresPgVectorAdapter, OpenSearchSeedAdapter, or RedisEpisodicAndSeedAdapter.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def _k_vertices(self, tenant_id: str) -> str:
|
|
298
|
+
return f"{self.key_prefix}:vertices:{tenant_id}"
|
|
299
|
+
|
|
300
|
+
def _k_edges(self, tenant_id: str) -> str:
|
|
301
|
+
return f"{self.key_prefix}:edges:{tenant_id}"
|
|
302
|
+
|
|
303
|
+
def _k_traces(self, tenant_id: str) -> str:
|
|
304
|
+
return f"{self.key_prefix}:traces:{tenant_id}"
|
|
305
|
+
|
|
306
|
+
async def upsert_vertices_and_bitemporal_edges(
|
|
307
|
+
self,
|
|
308
|
+
tenant_id: str,
|
|
309
|
+
vertices: Sequence[GraphVertexUpsert],
|
|
310
|
+
edges: Sequence[GraphEdgeMutation],
|
|
311
|
+
) -> None:
|
|
312
|
+
r = await self._get_redis()
|
|
313
|
+
for v in vertices:
|
|
314
|
+
existing_raw = await r.hget(self._k_vertices(tenant_id), v.vertex_id)
|
|
315
|
+
if existing_raw and not v.embedding:
|
|
316
|
+
old_v = GraphVertexUpsert.model_validate_json(existing_raw)
|
|
317
|
+
v.embedding = old_v.embedding
|
|
318
|
+
await r.hset(self._k_vertices(tenant_id), v.vertex_id, v.model_dump_json())
|
|
319
|
+
|
|
320
|
+
raw_edges = await r.hgetall(self._k_edges(tenant_id))
|
|
321
|
+
existing_edges: dict[str, GraphEdgeMutation] = {
|
|
322
|
+
eid: GraphEdgeMutation.model_validate_json(raw)
|
|
323
|
+
for eid, raw in raw_edges.items()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for new_e in edges:
|
|
327
|
+
if new_e.is_functional:
|
|
328
|
+
for eid, cur_e in existing_edges.items():
|
|
329
|
+
if (
|
|
330
|
+
cur_e.source_vertex_id == new_e.source_vertex_id
|
|
331
|
+
and cur_e.predicate == new_e.predicate
|
|
332
|
+
and cur_e.namespace == new_e.namespace
|
|
333
|
+
and cur_e.t_invalid == "ACTIVE"
|
|
334
|
+
and cur_e.target_vertex_id != new_e.target_vertex_id
|
|
335
|
+
):
|
|
336
|
+
cur_e.t_invalid = new_e.t_valid
|
|
337
|
+
await r.hset(
|
|
338
|
+
self._k_edges(tenant_id), eid, cur_e.model_dump_json()
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
matched_dup = False
|
|
342
|
+
for eid, cur_e in existing_edges.items():
|
|
343
|
+
if (
|
|
344
|
+
cur_e.source_vertex_id == new_e.source_vertex_id
|
|
345
|
+
and cur_e.predicate == new_e.predicate
|
|
346
|
+
and cur_e.target_vertex_id == new_e.target_vertex_id
|
|
347
|
+
and cur_e.namespace == new_e.namespace
|
|
348
|
+
and cur_e.t_invalid == "ACTIVE"
|
|
349
|
+
):
|
|
350
|
+
cur_e.fact_summary = new_e.fact_summary
|
|
351
|
+
cur_e.reinforcement_count += 1
|
|
352
|
+
cur_e.confidence = min(1.0, cur_e.confidence + 0.03)
|
|
353
|
+
await r.hset(self._k_edges(tenant_id), eid, cur_e.model_dump_json())
|
|
354
|
+
matched_dup = True
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
if not matched_dup:
|
|
358
|
+
existing_edges[new_e.edge_id] = new_e
|
|
359
|
+
await r.hset(
|
|
360
|
+
self._k_edges(tenant_id),
|
|
361
|
+
new_e.edge_id,
|
|
362
|
+
new_e.model_dump_json(),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
async def expand_active_subgraph(
|
|
366
|
+
self,
|
|
367
|
+
ctx: PlatformTenantContext,
|
|
368
|
+
seed_vertex_ids: Sequence[str],
|
|
369
|
+
as_of_iso: str | None = None,
|
|
370
|
+
max_hops: int = 2,
|
|
371
|
+
limit: int = 25,
|
|
372
|
+
) -> list[GraphFactReadResult]:
|
|
373
|
+
if not seed_vertex_ids:
|
|
374
|
+
return []
|
|
375
|
+
r = await self._get_redis()
|
|
376
|
+
raw_vertices = await r.hgetall(self._k_vertices(ctx.tenant_id))
|
|
377
|
+
vertices = {
|
|
378
|
+
vid: GraphVertexUpsert.model_validate_json(raw)
|
|
379
|
+
for vid, raw in raw_vertices.items()
|
|
380
|
+
}
|
|
381
|
+
raw_edges = await r.hvals(self._k_edges(ctx.tenant_id))
|
|
382
|
+
all_edges = [GraphEdgeMutation.model_validate_json(raw) for raw in raw_edges]
|
|
383
|
+
|
|
384
|
+
frontier = set(seed_vertex_ids)
|
|
385
|
+
visited_vertices = set(seed_vertex_ids)
|
|
386
|
+
seen_edge_ids: set[str] = set()
|
|
387
|
+
results: list[GraphFactReadResult] = []
|
|
388
|
+
|
|
389
|
+
for _hop in range(max(1, max_hops)):
|
|
390
|
+
if not frontier or len(results) >= limit:
|
|
391
|
+
break
|
|
392
|
+
next_frontier: set[str] = set()
|
|
393
|
+
for e in all_edges:
|
|
394
|
+
if e.edge_id in seen_edge_ids:
|
|
395
|
+
continue
|
|
396
|
+
if e.namespace not in ctx.read_namespaces:
|
|
397
|
+
continue
|
|
398
|
+
if (
|
|
399
|
+
e.source_vertex_id not in frontier
|
|
400
|
+
and e.target_vertex_id not in frontier
|
|
401
|
+
):
|
|
402
|
+
continue
|
|
403
|
+
if as_of_iso is None:
|
|
404
|
+
if e.t_invalid != "ACTIVE":
|
|
405
|
+
continue
|
|
406
|
+
else:
|
|
407
|
+
if not (
|
|
408
|
+
e.t_valid <= as_of_iso
|
|
409
|
+
and (e.t_invalid == "ACTIVE" or e.t_invalid > as_of_iso)
|
|
410
|
+
):
|
|
411
|
+
continue
|
|
412
|
+
|
|
413
|
+
src_v = vertices.get(e.source_vertex_id)
|
|
414
|
+
tgt_v = vertices.get(e.target_vertex_id)
|
|
415
|
+
if not src_v or not tgt_v:
|
|
416
|
+
continue
|
|
417
|
+
if (
|
|
418
|
+
src_v.namespace not in ctx.read_namespaces
|
|
419
|
+
or tgt_v.namespace not in ctx.read_namespaces
|
|
420
|
+
):
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
seen_edge_ids.add(e.edge_id)
|
|
424
|
+
results.append(
|
|
425
|
+
GraphFactReadResult(
|
|
426
|
+
source_name=src_v.canonical_name,
|
|
427
|
+
source_class=src_v.entity_class,
|
|
428
|
+
predicate=e.predicate,
|
|
429
|
+
target_name=tgt_v.canonical_name,
|
|
430
|
+
target_class=tgt_v.entity_class,
|
|
431
|
+
fact_summary=e.fact_summary,
|
|
432
|
+
t_valid=e.t_valid,
|
|
433
|
+
t_invalid=e.t_invalid,
|
|
434
|
+
namespace=e.namespace,
|
|
435
|
+
source_episode_id=e.source_episode_id,
|
|
436
|
+
confidence=e.confidence,
|
|
437
|
+
reinforcement_count=e.reinforcement_count,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
for vid in (e.source_vertex_id, e.target_vertex_id):
|
|
441
|
+
if vid not in visited_vertices:
|
|
442
|
+
visited_vertices.add(vid)
|
|
443
|
+
next_frontier.add(vid)
|
|
444
|
+
if len(results) >= limit:
|
|
445
|
+
break
|
|
446
|
+
frontier = next_frontier
|
|
447
|
+
|
|
448
|
+
return results[:limit]
|
|
449
|
+
|
|
450
|
+
async def record_reasoning_trace(
|
|
451
|
+
self,
|
|
452
|
+
ctx: PlatformTenantContext,
|
|
453
|
+
run_id: str,
|
|
454
|
+
session_id: str,
|
|
455
|
+
steps: Sequence[ReasoningTraceStep],
|
|
456
|
+
) -> None:
|
|
457
|
+
r = await self._get_redis()
|
|
458
|
+
payload = {
|
|
459
|
+
"run_id": run_id,
|
|
460
|
+
"session_id": session_id,
|
|
461
|
+
"app_name": ctx.app_name,
|
|
462
|
+
"namespace": ctx.app_private_namespace,
|
|
463
|
+
"steps": [s.model_dump(mode="json") for s in steps],
|
|
464
|
+
}
|
|
465
|
+
await r.hset(self._k_traces(ctx.tenant_id), run_id, json.dumps(payload))
|
|
466
|
+
|
|
467
|
+
async def purge_episode_lineage(
|
|
468
|
+
self, tenant_id: str, episode_ids: Sequence[str], user_id: str
|
|
469
|
+
) -> None:
|
|
470
|
+
r = await self._get_redis()
|
|
471
|
+
raw_edges = await r.hgetall(self._k_edges(tenant_id))
|
|
472
|
+
to_delete: list[str] = []
|
|
473
|
+
ep_set = set(episode_ids)
|
|
474
|
+
for eid, raw in raw_edges.items():
|
|
475
|
+
e = GraphEdgeMutation.model_validate_json(raw)
|
|
476
|
+
if e.source_episode_id in ep_set or e.user_id == user_id:
|
|
477
|
+
to_delete.append(eid)
|
|
478
|
+
if to_delete:
|
|
479
|
+
await r.hdel(self._k_edges(tenant_id), *to_delete)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class RedisUnifiedAdapter(
|
|
483
|
+
RedisRegistryAdapter,
|
|
484
|
+
RedisEpisodicAndSeedAdapter,
|
|
485
|
+
RedisGraphAdapter,
|
|
486
|
+
):
|
|
487
|
+
"""
|
|
488
|
+
Single-Stack Pure Redis Unified Adapter (`RegistryStoreAdapter` +
|
|
489
|
+
`EpisodicAndSeedStoreAdapter` + `GraphStoreAdapter` + `PlaybookStoreAdapter`).
|
|
490
|
+
"""
|