memtrust-cli 0.3.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.
- memtrust/__init__.py +11 -0
- memtrust/adapters/__init__.py +62 -0
- memtrust/adapters/base.py +2020 -0
- memtrust/adapters/mem0_adapter.py +456 -0
- memtrust/adapters/mem0_direct_adapter.py +1217 -0
- memtrust/adapters/mempalace_adapter.py +1166 -0
- memtrust/adapters/openviking_adapter.py +570 -0
- memtrust/adapters/zep_graphiti_adapter.py +181 -0
- memtrust/adapters/zep_graphiti_selfhosted_adapter.py +882 -0
- memtrust/cli.py +1201 -0
- memtrust/evals/__init__.py +5 -0
- memtrust/evals/compression.py +235 -0
- memtrust/evals/contradiction.py +451 -0
- memtrust/evals/crash_recovery.py +290 -0
- memtrust/evals/embedder_cost.py +163 -0
- memtrust/evals/embedding_drift.py +273 -0
- memtrust/evals/episode_temporal_leak.py +182 -0
- memtrust/evals/extraction_quality.py +373 -0
- memtrust/evals/filter_injection.py +371 -0
- memtrust/evals/language_degradation.py +189 -0
- memtrust/evals/lock_contention.py +263 -0
- memtrust/evals/locomo.py +392 -0
- memtrust/evals/longmemeval.py +249 -0
- memtrust/evals/mempalace_metadata_scale.py +494 -0
- memtrust/evals/migration_rollback.py +276 -0
- memtrust/evals/orphan_cleanup.py +235 -0
- memtrust/evals/ranking_quality.py +303 -0
- memtrust/evals/resource_sync_safety.py +336 -0
- memtrust/evals/result_consistency.py +239 -0
- memtrust/evals/scale_fixtures.py +189 -0
- memtrust/evals/scale_stress.py +502 -0
- memtrust/evals/stats_accuracy.py +240 -0
- memtrust/evals/temporal_kg_boundary.py +281 -0
- memtrust/receipt.py +318 -0
- memtrust/scoring/__init__.py +1 -0
- memtrust/scoring/cost_tracker.py +199 -0
- memtrust/scoring/llm_judge.py +161 -0
- memtrust_cli-0.3.0.dist-info/METADATA +624 -0
- memtrust_cli-0.3.0.dist-info/RECORD +42 -0
- memtrust_cli-0.3.0.dist-info/WHEEL +4 -0
- memtrust_cli-0.3.0.dist-info/entry_points.txt +2 -0
- memtrust_cli-0.3.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Adapter for Zep / Graphiti (https://getzep.com, https://github.com/getzep/graphiti).
|
|
2
|
+
|
|
3
|
+
Confidence: MEDIUM-HIGH on behavior, MEDIUM on exact wire format.
|
|
4
|
+
|
|
5
|
+
Graphiti is the open-source temporal-knowledge-graph engine that powers
|
|
6
|
+
Zep. Its documented core operations are `add_episode()` (ingest a unit of
|
|
7
|
+
conversation, which triggers entity/relationship extraction, dedup, and
|
|
8
|
+
contradiction detection against the existing graph) and `search()`
|
|
9
|
+
(hybrid semantic + BM25 + graph-traversal retrieval). Confirmed via
|
|
10
|
+
Graphiti's own docs and DeepWiki: when a new episode contradicts an
|
|
11
|
+
existing fact, Graphiti stamps the old graph edge `invalid_at` rather
|
|
12
|
+
than deleting it -- old and new facts both remain inspectable, bi-
|
|
13
|
+
temporally. That is a real, documented product behavior, not a memtrust
|
|
14
|
+
assumption, and it maps directly onto ConflictSignal.FLAGGED when the
|
|
15
|
+
query response surfaces an edge with a non-null `invalid_at`.
|
|
16
|
+
|
|
17
|
+
This adapter targets Zep Cloud's hosted API (`ZEP_API_KEY`), which wraps
|
|
18
|
+
Graphiti, rather than a self-hosted `graphiti-core` + Neo4j/FalkorDB
|
|
19
|
+
deployment. Self-hosted Graphiti has no single API key to gate on -- it
|
|
20
|
+
requires a running graph database plus LLM credentials, which is
|
|
21
|
+
infrastructure memtrust cannot assume exists in a fresh clone or CI run.
|
|
22
|
+
The hosted-API choice keeps this adapter consistent with the rest of the
|
|
23
|
+
harness's "one env var, or SKIPPED" contract. See docs/methodology.md.
|
|
24
|
+
Exact REST paths below are a best-effort reconstruction; the fact-
|
|
25
|
+
invalidation behavior they read from is the documented part.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
from memtrust.adapters.base import (
|
|
35
|
+
BackendAPIError,
|
|
36
|
+
BackendNotConfiguredError,
|
|
37
|
+
ConflictSignal,
|
|
38
|
+
DeleteResult,
|
|
39
|
+
MemoryBackendAdapter,
|
|
40
|
+
MemoryRecord,
|
|
41
|
+
QueryResult,
|
|
42
|
+
StoreResult,
|
|
43
|
+
UpdateResult,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
DEFAULT_BASE_URL = "https://api.getzep.com"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ZepGraphitiAdapter(MemoryBackendAdapter):
|
|
50
|
+
name = "zep"
|
|
51
|
+
env_var = "ZEP_API_KEY"
|
|
52
|
+
supports_update = True
|
|
53
|
+
|
|
54
|
+
def __init__(self, base_url: str = DEFAULT_BASE_URL, timeout: float = 30.0) -> None:
|
|
55
|
+
api_key = os.environ.get(self.env_var)
|
|
56
|
+
if not api_key:
|
|
57
|
+
raise BackendNotConfiguredError(self.name, self.env_var)
|
|
58
|
+
self._http = httpx.Client(
|
|
59
|
+
base_url=base_url,
|
|
60
|
+
headers={
|
|
61
|
+
"Authorization": f"Api-Key {api_key}",
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
},
|
|
64
|
+
timeout=timeout,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def store(
|
|
68
|
+
self,
|
|
69
|
+
session_id: str,
|
|
70
|
+
content: str,
|
|
71
|
+
metadata: dict[str, str] | None = None,
|
|
72
|
+
mode: str | None = None,
|
|
73
|
+
) -> StoreResult:
|
|
74
|
+
# Graphiti has no documented operating-mode variant -- accepted
|
|
75
|
+
# and ignored (no-op); see MemoryBackendAdapter.supported_modes.
|
|
76
|
+
del mode
|
|
77
|
+
timer = self._timed()
|
|
78
|
+
payload: dict[str, object] = {
|
|
79
|
+
"group_id": session_id,
|
|
80
|
+
"data": content,
|
|
81
|
+
"type": "text",
|
|
82
|
+
"source_description": "memtrust-eval",
|
|
83
|
+
}
|
|
84
|
+
if metadata:
|
|
85
|
+
payload["metadata"] = metadata
|
|
86
|
+
try:
|
|
87
|
+
resp = self._http.post("/graph/episodes", json=payload)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
data = resp.json()
|
|
90
|
+
except httpx.HTTPError as exc:
|
|
91
|
+
raise BackendAPIError(self.name, str(exc)) from exc
|
|
92
|
+
episode_id = str(data.get("uuid", data.get("episode_id", "")))
|
|
93
|
+
return StoreResult(memory_id=episode_id, latency_ms=timer.elapsed_ms(), raw=data)
|
|
94
|
+
|
|
95
|
+
def query(
|
|
96
|
+
self, session_id: str, query: str, top_k: int = 5, mode: str | None = None
|
|
97
|
+
) -> QueryResult:
|
|
98
|
+
del mode # no-op, see store() above
|
|
99
|
+
timer = self._timed()
|
|
100
|
+
payload = {"group_id": session_id, "query": query, "limit": top_k}
|
|
101
|
+
try:
|
|
102
|
+
resp = self._http.post("/graph/search", json=payload)
|
|
103
|
+
resp.raise_for_status()
|
|
104
|
+
data = resp.json()
|
|
105
|
+
except httpx.HTTPError as exc:
|
|
106
|
+
raise BackendAPIError(self.name, str(exc)) from exc
|
|
107
|
+
|
|
108
|
+
edges = data.get("edges", data.get("facts", []))
|
|
109
|
+
records: list[MemoryRecord] = []
|
|
110
|
+
any_invalidated = False
|
|
111
|
+
for edge in edges:
|
|
112
|
+
invalid_at = edge.get("invalid_at")
|
|
113
|
+
if invalid_at:
|
|
114
|
+
any_invalidated = True
|
|
115
|
+
records.append(
|
|
116
|
+
MemoryRecord(
|
|
117
|
+
memory_id=str(edge.get("uuid", "")),
|
|
118
|
+
content=str(edge.get("fact", edge.get("name", ""))),
|
|
119
|
+
score=edge.get("score"),
|
|
120
|
+
created_at=edge.get("valid_at") or edge.get("created_at"),
|
|
121
|
+
metadata={"invalid_at": str(invalid_at)} if invalid_at else {},
|
|
122
|
+
raw=edge,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# A returned edge stamped invalid_at, alongside a live edge for the
|
|
127
|
+
# same fact slot, is Graphiti's documented mechanism for surfacing
|
|
128
|
+
# a superseded fact -- that is a FLAGGED signal by definition (the
|
|
129
|
+
# contradiction is visible in the response, not hidden). If no
|
|
130
|
+
# edge in the result set carries invalid_at, the harness cannot
|
|
131
|
+
# tell from this call alone whether that means "no contradiction
|
|
132
|
+
# occurred" or "the backend resolved it invisibly" -- the eval
|
|
133
|
+
# layer (evals/contradiction.py) disambiguates using the known
|
|
134
|
+
# eval fixture, not this adapter.
|
|
135
|
+
conflict_signal = ConflictSignal.FLAGGED if any_invalidated else ConflictSignal.SERVED_STALE
|
|
136
|
+
return QueryResult(
|
|
137
|
+
records=records,
|
|
138
|
+
conflict_signal=conflict_signal,
|
|
139
|
+
latency_ms=timer.elapsed_ms(),
|
|
140
|
+
raw=data,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def update(self, session_id: str, memory_id: str, content: str) -> UpdateResult:
|
|
144
|
+
# Graphiti has no separate "update" verb in its documented API --
|
|
145
|
+
# a contradicting fact is submitted the same way a new fact is,
|
|
146
|
+
# through add_episode/store(), and the graph's own extraction
|
|
147
|
+
# pipeline resolves the contradiction (invalidating the old edge)
|
|
148
|
+
# during ingestion. This method exists to satisfy the shared
|
|
149
|
+
# interface and to make that behavior explicit and testable,
|
|
150
|
+
# rather than silently aliasing to store() without comment.
|
|
151
|
+
timer = self._timed()
|
|
152
|
+
result = self.store(session_id, content)
|
|
153
|
+
return UpdateResult(
|
|
154
|
+
memory_id=result.memory_id,
|
|
155
|
+
acknowledged=True,
|
|
156
|
+
latency_ms=timer.elapsed_ms(),
|
|
157
|
+
raw=result.raw,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def delete(self, memory_id: str) -> DeleteResult:
|
|
161
|
+
# Best-effort reconstruction, same confidence level as store()'s
|
|
162
|
+
# /graph/episodes path above: Zep's hosted API is not confirmed
|
|
163
|
+
# here to expose a documented "delete episode" verb distinct from
|
|
164
|
+
# its bi-temporal invalidate-on-contradiction behavior (see the
|
|
165
|
+
# module docstring). This targets the REST path symmetrical with
|
|
166
|
+
# store()'s POST /graph/episodes -- DELETE /graph/episodes/{uuid}
|
|
167
|
+
# -- and should be corrected by whoever verifies it against a
|
|
168
|
+
# live Zep instance if the real surface differs.
|
|
169
|
+
timer = self._timed()
|
|
170
|
+
try:
|
|
171
|
+
resp = self._http.delete(f"/graph/episodes/{memory_id}")
|
|
172
|
+
resp.raise_for_status()
|
|
173
|
+
data = resp.json() if resp.content else {}
|
|
174
|
+
except httpx.HTTPError as exc:
|
|
175
|
+
raise BackendAPIError(self.name, str(exc)) from exc
|
|
176
|
+
return DeleteResult(
|
|
177
|
+
success=True, memory_id=memory_id, latency_ms=timer.elapsed_ms(), raw=data
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def close(self) -> None:
|
|
181
|
+
self._http.close()
|