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.
Files changed (42) hide show
  1. memtrust/__init__.py +11 -0
  2. memtrust/adapters/__init__.py +62 -0
  3. memtrust/adapters/base.py +2020 -0
  4. memtrust/adapters/mem0_adapter.py +456 -0
  5. memtrust/adapters/mem0_direct_adapter.py +1217 -0
  6. memtrust/adapters/mempalace_adapter.py +1166 -0
  7. memtrust/adapters/openviking_adapter.py +570 -0
  8. memtrust/adapters/zep_graphiti_adapter.py +181 -0
  9. memtrust/adapters/zep_graphiti_selfhosted_adapter.py +882 -0
  10. memtrust/cli.py +1201 -0
  11. memtrust/evals/__init__.py +5 -0
  12. memtrust/evals/compression.py +235 -0
  13. memtrust/evals/contradiction.py +451 -0
  14. memtrust/evals/crash_recovery.py +290 -0
  15. memtrust/evals/embedder_cost.py +163 -0
  16. memtrust/evals/embedding_drift.py +273 -0
  17. memtrust/evals/episode_temporal_leak.py +182 -0
  18. memtrust/evals/extraction_quality.py +373 -0
  19. memtrust/evals/filter_injection.py +371 -0
  20. memtrust/evals/language_degradation.py +189 -0
  21. memtrust/evals/lock_contention.py +263 -0
  22. memtrust/evals/locomo.py +392 -0
  23. memtrust/evals/longmemeval.py +249 -0
  24. memtrust/evals/mempalace_metadata_scale.py +494 -0
  25. memtrust/evals/migration_rollback.py +276 -0
  26. memtrust/evals/orphan_cleanup.py +235 -0
  27. memtrust/evals/ranking_quality.py +303 -0
  28. memtrust/evals/resource_sync_safety.py +336 -0
  29. memtrust/evals/result_consistency.py +239 -0
  30. memtrust/evals/scale_fixtures.py +189 -0
  31. memtrust/evals/scale_stress.py +502 -0
  32. memtrust/evals/stats_accuracy.py +240 -0
  33. memtrust/evals/temporal_kg_boundary.py +281 -0
  34. memtrust/receipt.py +318 -0
  35. memtrust/scoring/__init__.py +1 -0
  36. memtrust/scoring/cost_tracker.py +199 -0
  37. memtrust/scoring/llm_judge.py +161 -0
  38. memtrust_cli-0.3.0.dist-info/METADATA +624 -0
  39. memtrust_cli-0.3.0.dist-info/RECORD +42 -0
  40. memtrust_cli-0.3.0.dist-info/WHEEL +4 -0
  41. memtrust_cli-0.3.0.dist-info/entry_points.txt +2 -0
  42. 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()