supermemory-agent 0.2.3__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.
- storage/adapters/__init__.py +0 -0
- storage/adapters/file.py +397 -0
- storage/adapters/postgres_qdrant_redis.py +32 -0
- storage/adapters/sqlite_chroma.py +95 -0
- supermemory_agent-0.2.3.dist-info/METADATA +170 -0
- supermemory_agent-0.2.3.dist-info/RECORD +54 -0
- supermemory_agent-0.2.3.dist-info/WHEEL +4 -0
- supermemory_agent-0.2.3.dist-info/entry_points.txt +2 -0
- supermemory_agent-0.2.3.dist-info/licenses/LICENSE +21 -0
- supermemory_mcp/__init__.py +5 -0
- supermemory_mcp/bridge.py +35 -0
- supermemory_mcp/handlers.py +772 -0
- supermemory_mcp/server.py +522 -0
- supermemory_mcp/text.py +16 -0
- uall/__init__.py +0 -0
- uall/analytics/service.py +35 -0
- uall/collector/__init__.py +0 -0
- uall/collector/service.py +100 -0
- uall/distillation/distiller.py +80 -0
- uall/evaluation/engine.py +38 -0
- uall/experiments/manager.py +83 -0
- uall/memory/__init__.py +0 -0
- uall/memory/confidence.py +36 -0
- uall/memory/freshness.py +28 -0
- uall/memory/graph.py +24 -0
- uall/memory/namespaces.py +40 -0
- uall/memory/policies.py +44 -0
- uall/memory/provenance.py +23 -0
- uall/memory/pruning.py +55 -0
- uall/memory/retrieval.py +98 -0
- uall/memory/ttl.py +22 -0
- uall/memory/validator.py +144 -0
- uall/optimization/optimizers.py +59 -0
- uall/promotion/queue.py +107 -0
- uall/recommendations/engine.py +66 -0
- uall/reflection/engine.py +72 -0
- uall/rollback/manager.py +49 -0
- uall/service.py +572 -0
- uall/skills/library.py +19 -0
- uall/telemetry/retrieval.py +40 -0
- uall_core/__init__.py +0 -0
- uall_core/ports/storage.py +71 -0
- uall_core/providers/heuristic.py +58 -0
- uall_core/providers/llm.py +6 -0
- uall_core/schemas/__init__.py +0 -0
- uall_core/schemas/common.py +73 -0
- uall_core/schemas/events.py +75 -0
- uall_core/schemas/graph.py +32 -0
- uall_core/schemas/lesson.py +109 -0
- uall_core/schemas/namespace.py +76 -0
- uall_python/__init__.py +3 -0
- uall_python/client.py +337 -0
- uall_server/__init__.py +0 -0
- uall_server/main.py +284 -0
|
File without changes
|
storage/adapters/file.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from uall_core.ports.storage import StoragePort
|
|
9
|
+
from uall_core.providers.heuristic import cosine_similarity
|
|
10
|
+
from uall_core.schemas.common import (
|
|
11
|
+
Experiment,
|
|
12
|
+
PolicyVersion,
|
|
13
|
+
RetrievalTelemetryEvent,
|
|
14
|
+
Skill,
|
|
15
|
+
VersionRecord,
|
|
16
|
+
)
|
|
17
|
+
from uall_core.schemas.events import Event, Feedback, RunEnd, RunStart
|
|
18
|
+
from uall_core.schemas.graph import KnowledgeGraph
|
|
19
|
+
from uall_core.schemas.lesson import Lesson, MemorySearchRequest, PendingLesson
|
|
20
|
+
from uall_core.schemas.namespace import (
|
|
21
|
+
ConfidenceDimensions,
|
|
22
|
+
FreshnessMetrics,
|
|
23
|
+
NamespaceRef,
|
|
24
|
+
Provenance,
|
|
25
|
+
TTLConfig,
|
|
26
|
+
)
|
|
27
|
+
from uall_core.schemas.events import StageMetadata
|
|
28
|
+
|
|
29
|
+
UALL_DIRS = [
|
|
30
|
+
"events",
|
|
31
|
+
"runs",
|
|
32
|
+
"metrics",
|
|
33
|
+
"feedback",
|
|
34
|
+
"recommendations",
|
|
35
|
+
"prompt_versions",
|
|
36
|
+
"workflow_graphs",
|
|
37
|
+
"experiment_results",
|
|
38
|
+
"policies",
|
|
39
|
+
"skills",
|
|
40
|
+
"lessons",
|
|
41
|
+
"pending",
|
|
42
|
+
"telemetry",
|
|
43
|
+
"reflections",
|
|
44
|
+
"corrections",
|
|
45
|
+
"semantic_memories",
|
|
46
|
+
"failure_patterns",
|
|
47
|
+
"cache",
|
|
48
|
+
"logs",
|
|
49
|
+
"uploads",
|
|
50
|
+
"artifacts",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FileStorageAdapter:
|
|
55
|
+
"""Tier 1: file-based .uall/ storage."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, base_dir: str | Path = ".uall"):
|
|
58
|
+
self.base = Path(base_dir)
|
|
59
|
+
|
|
60
|
+
async def init(self) -> None:
|
|
61
|
+
self.base.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
for d in UALL_DIRS:
|
|
63
|
+
(self.base / d).mkdir(exist_ok=True)
|
|
64
|
+
config_path = self.base / "config.json"
|
|
65
|
+
if not config_path.exists():
|
|
66
|
+
self._write_json(
|
|
67
|
+
config_path,
|
|
68
|
+
{
|
|
69
|
+
"storage_backend": "file",
|
|
70
|
+
"promotion_delay_minutes": 0,
|
|
71
|
+
"created_at": datetime.utcnow().isoformat(),
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _dir(self, name: str) -> Path:
|
|
76
|
+
return self.base / name
|
|
77
|
+
|
|
78
|
+
def _write_json(self, path: Path, data: dict | list) -> None:
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
81
|
+
json.dump(data, f, indent=2, default=str)
|
|
82
|
+
|
|
83
|
+
def _read_json(self, path: Path) -> dict | list | None:
|
|
84
|
+
if not path.exists():
|
|
85
|
+
return None
|
|
86
|
+
with open(path, encoding="utf-8") as f:
|
|
87
|
+
return json.load(f)
|
|
88
|
+
|
|
89
|
+
def _next_id(self, directory: str, prefix: str) -> str:
|
|
90
|
+
existing = list(self._dir(directory).glob(f"{prefix}_*.json"))
|
|
91
|
+
nums = []
|
|
92
|
+
for p in existing:
|
|
93
|
+
try:
|
|
94
|
+
nums.append(int(p.stem.split("_")[-1]))
|
|
95
|
+
except ValueError:
|
|
96
|
+
pass
|
|
97
|
+
n = max(nums, default=0) + 1
|
|
98
|
+
return f"{prefix}_{n:03d}"
|
|
99
|
+
|
|
100
|
+
async def save_run_start(self, run: RunStart) -> None:
|
|
101
|
+
path = self._dir("runs") / f"{run.run_id}.json"
|
|
102
|
+
data = run.model_dump(mode="json")
|
|
103
|
+
data["status"] = "running"
|
|
104
|
+
data["started_at"] = datetime.utcnow().isoformat()
|
|
105
|
+
self._write_json(path, data)
|
|
106
|
+
|
|
107
|
+
async def save_run_end(self, run: RunEnd) -> None:
|
|
108
|
+
path = self._dir("runs") / f"{run.run_id}.json"
|
|
109
|
+
existing = self._read_json(path) or {"run_id": run.run_id}
|
|
110
|
+
existing.update(run.model_dump(mode="json"))
|
|
111
|
+
existing["status"] = "completed"
|
|
112
|
+
existing["ended_at"] = datetime.utcnow().isoformat()
|
|
113
|
+
self._write_json(path, existing)
|
|
114
|
+
|
|
115
|
+
async def save_event(self, event: Event) -> None:
|
|
116
|
+
event_data = event.model_dump(mode="json")
|
|
117
|
+
self._write_json(self._dir("events") / f"{event.event_id}.json", event_data)
|
|
118
|
+
run_path = self._dir("runs") / f"{event.run_id}.json"
|
|
119
|
+
run_data = self._read_json(run_path) or {"run_id": event.run_id, "events": []}
|
|
120
|
+
if "events" not in run_data:
|
|
121
|
+
run_data["events"] = []
|
|
122
|
+
run_data["events"].append(event_data)
|
|
123
|
+
self._write_json(run_path, run_data)
|
|
124
|
+
|
|
125
|
+
async def get_event(self, event_id: str) -> dict[str, Any] | None:
|
|
126
|
+
data = self._read_json(self._dir("events") / f"{event_id}.json")
|
|
127
|
+
if data:
|
|
128
|
+
return data
|
|
129
|
+
for run_path in self._dir("runs").glob("*.json"):
|
|
130
|
+
run_data = self._read_json(run_path)
|
|
131
|
+
if not run_data:
|
|
132
|
+
continue
|
|
133
|
+
for evt in run_data.get("events", []):
|
|
134
|
+
if evt.get("event_id") == event_id:
|
|
135
|
+
return evt
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
async def save_feedback(self, feedback: Feedback) -> str:
|
|
139
|
+
fid = self._next_id("feedback", "feedback")
|
|
140
|
+
self._write_json(self._dir("feedback") / f"{fid}.json", feedback.model_dump(mode="json"))
|
|
141
|
+
return fid
|
|
142
|
+
|
|
143
|
+
async def get_run(self, run_id: str) -> dict[str, Any] | None:
|
|
144
|
+
return self._read_json(self._dir("runs") / f"{run_id}.json")
|
|
145
|
+
|
|
146
|
+
async def list_runs(self) -> list[dict[str, Any]]:
|
|
147
|
+
runs = []
|
|
148
|
+
for p in self._dir("runs").glob("*.json"):
|
|
149
|
+
data = self._read_json(p)
|
|
150
|
+
if data:
|
|
151
|
+
runs.append(data)
|
|
152
|
+
return runs
|
|
153
|
+
|
|
154
|
+
def _lesson_from_dict(self, data: dict) -> Lesson:
|
|
155
|
+
graph = None
|
|
156
|
+
if data.get("graph"):
|
|
157
|
+
graph = KnowledgeGraph(**data["graph"])
|
|
158
|
+
return Lesson(
|
|
159
|
+
lesson_id=data["lesson_id"],
|
|
160
|
+
failure=data.get("failure", ""),
|
|
161
|
+
root_cause=data.get("root_cause", ""),
|
|
162
|
+
fix=data.get("fix", ""),
|
|
163
|
+
memory_type=data.get("memory_type", "failure"),
|
|
164
|
+
stage=StageMetadata(**data.get("stage", {})),
|
|
165
|
+
namespace=NamespaceRef(**data.get("namespace", {})),
|
|
166
|
+
confidence=ConfidenceDimensions(**data.get("confidence", {})),
|
|
167
|
+
freshness=FreshnessMetrics(**data.get("freshness", {})),
|
|
168
|
+
ttl=TTLConfig(**data.get("ttl", {})),
|
|
169
|
+
provenance=Provenance(**data.get("provenance", {})),
|
|
170
|
+
graph=graph,
|
|
171
|
+
occurrence_count=data.get("occurrence_count", 1),
|
|
172
|
+
quality_score=data.get("quality_score", 0.5),
|
|
173
|
+
embedding=data.get("embedding"),
|
|
174
|
+
status=data.get("status", "active"),
|
|
175
|
+
metadata=data.get("metadata", {}),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def save_lesson(self, lesson: Lesson) -> str:
|
|
179
|
+
path = self._dir("lessons") / f"{lesson.lesson_id}.json"
|
|
180
|
+
data = lesson.model_dump(mode="json")
|
|
181
|
+
self._write_json(path, data)
|
|
182
|
+
if lesson.embedding:
|
|
183
|
+
self._write_json(path.with_suffix(".embedding.json"), {"embedding": lesson.embedding})
|
|
184
|
+
return lesson.lesson_id
|
|
185
|
+
|
|
186
|
+
async def get_lesson(self, lesson_id: str) -> Lesson | None:
|
|
187
|
+
data = self._read_json(self._dir("lessons") / f"{lesson_id}.json")
|
|
188
|
+
if not data:
|
|
189
|
+
return None
|
|
190
|
+
emb_path = self._dir("lessons") / f"{lesson_id}.embedding.json"
|
|
191
|
+
emb_data = self._read_json(emb_path)
|
|
192
|
+
if emb_data:
|
|
193
|
+
data["embedding"] = emb_data.get("embedding")
|
|
194
|
+
return self._lesson_from_dict(data)
|
|
195
|
+
|
|
196
|
+
async def list_lessons(self, status: str = "active") -> list[Lesson]:
|
|
197
|
+
lessons = []
|
|
198
|
+
for p in self._dir("lessons").glob("*.json"):
|
|
199
|
+
if p.name.endswith(".embedding.json"):
|
|
200
|
+
continue
|
|
201
|
+
data = self._read_json(p)
|
|
202
|
+
if data and (status == "all" or data.get("status", "active") == status):
|
|
203
|
+
emb = self._read_json(p.with_suffix(".embedding.json"))
|
|
204
|
+
if emb:
|
|
205
|
+
data["embedding"] = emb.get("embedding")
|
|
206
|
+
lessons.append(self._lesson_from_dict(data))
|
|
207
|
+
return lessons
|
|
208
|
+
|
|
209
|
+
async def update_lesson(self, lesson: Lesson) -> None:
|
|
210
|
+
await self.save_lesson(lesson)
|
|
211
|
+
|
|
212
|
+
async def delete_lesson(self, lesson_id: str) -> None:
|
|
213
|
+
path = self._dir("lessons") / f"{lesson_id}.json"
|
|
214
|
+
if path.exists():
|
|
215
|
+
path.unlink()
|
|
216
|
+
emb = self._dir("lessons") / f"{lesson_id}.embedding.json"
|
|
217
|
+
if emb.exists():
|
|
218
|
+
emb.unlink()
|
|
219
|
+
|
|
220
|
+
async def search_lessons(self, request: MemorySearchRequest) -> list[Lesson]:
|
|
221
|
+
lessons = await self.list_lessons("active")
|
|
222
|
+
now = datetime.utcnow()
|
|
223
|
+
filtered = []
|
|
224
|
+
for lesson in lessons:
|
|
225
|
+
if lesson.ttl.expires_at and lesson.ttl.expires_at < now:
|
|
226
|
+
continue
|
|
227
|
+
if request.workflow and lesson.stage.workflow != request.workflow:
|
|
228
|
+
if lesson.stage.workflow is not None:
|
|
229
|
+
continue
|
|
230
|
+
if request.step and lesson.stage.step != request.step:
|
|
231
|
+
pass # soft filter handled in retrieval scoring
|
|
232
|
+
if request.namespace and lesson.namespace.namespace_id != request.namespace:
|
|
233
|
+
pass
|
|
234
|
+
filtered.append(lesson)
|
|
235
|
+
if not filtered:
|
|
236
|
+
filtered = await self.list_lessons("active")
|
|
237
|
+
|
|
238
|
+
# Simple vector search if embeddings exist
|
|
239
|
+
from uall_core.providers.heuristic import HeuristicLLMProvider
|
|
240
|
+
|
|
241
|
+
provider = HeuristicLLMProvider()
|
|
242
|
+
query_emb = await provider.embed(request.query)
|
|
243
|
+
scored = []
|
|
244
|
+
for lesson in filtered:
|
|
245
|
+
emb = lesson.embedding or await provider.embed(lesson.to_search_text())
|
|
246
|
+
sim = cosine_similarity(query_emb, emb)
|
|
247
|
+
scored.append((sim, lesson))
|
|
248
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
249
|
+
return [l for _, l in scored[: request.top_k * 4]]
|
|
250
|
+
|
|
251
|
+
async def save_pending(self, pending: PendingLesson) -> str:
|
|
252
|
+
path = self._dir("pending") / f"{pending.pending_id}.json"
|
|
253
|
+
self._write_json(path, pending.model_dump(mode="json"))
|
|
254
|
+
return pending.pending_id
|
|
255
|
+
|
|
256
|
+
async def get_pending(self, pending_id: str) -> PendingLesson | None:
|
|
257
|
+
data = self._read_json(self._dir("pending") / f"{pending_id}.json")
|
|
258
|
+
return PendingLesson(**data) if data else None
|
|
259
|
+
|
|
260
|
+
async def list_pending(self, status: str = "pending") -> list[PendingLesson]:
|
|
261
|
+
result = []
|
|
262
|
+
for p in self._dir("pending").glob("*.json"):
|
|
263
|
+
data = self._read_json(p)
|
|
264
|
+
if data and (status == "all" or data.get("status") == status):
|
|
265
|
+
result.append(PendingLesson(**data))
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
async def update_pending(self, pending: PendingLesson) -> None:
|
|
269
|
+
await self.save_pending(pending)
|
|
270
|
+
|
|
271
|
+
async def save_policy(self, policy: PolicyVersion) -> str:
|
|
272
|
+
fname = f"{policy.policy_id}_{policy.version}.json"
|
|
273
|
+
self._write_json(self._dir("policies") / fname, policy.model_dump(mode="json"))
|
|
274
|
+
return f"{policy.policy_id}:{policy.version}"
|
|
275
|
+
|
|
276
|
+
async def get_active_policies(self) -> list[PolicyVersion]:
|
|
277
|
+
policies: dict[str, PolicyVersion] = {}
|
|
278
|
+
for p in self._dir("policies").glob("*.json"):
|
|
279
|
+
data = self._read_json(p)
|
|
280
|
+
if data:
|
|
281
|
+
pv = PolicyVersion(**data)
|
|
282
|
+
key = pv.policy_id
|
|
283
|
+
if key not in policies or pv.version > policies[key].version:
|
|
284
|
+
policies[key] = pv
|
|
285
|
+
return list(policies.values())
|
|
286
|
+
|
|
287
|
+
async def list_policy_versions(self, policy_id: str) -> list[PolicyVersion]:
|
|
288
|
+
versions = []
|
|
289
|
+
for p in self._dir("policies").glob(f"{policy_id}_*.json"):
|
|
290
|
+
data = self._read_json(p)
|
|
291
|
+
if data:
|
|
292
|
+
versions.append(PolicyVersion(**data))
|
|
293
|
+
return sorted(versions, key=lambda x: x.version)
|
|
294
|
+
|
|
295
|
+
async def save_skill(self, skill: Skill) -> str:
|
|
296
|
+
self._write_json(self._dir("skills") / f"{skill.skill_id}.json", skill.model_dump(mode="json"))
|
|
297
|
+
return skill.skill_id
|
|
298
|
+
|
|
299
|
+
async def get_skill(self, skill_id: str) -> Skill | None:
|
|
300
|
+
data = self._read_json(self._dir("skills") / f"{skill_id}.json")
|
|
301
|
+
return Skill(**data) if data else None
|
|
302
|
+
|
|
303
|
+
async def search_skills(self, query: str) -> list[Skill]:
|
|
304
|
+
q = query.lower()
|
|
305
|
+
skills = []
|
|
306
|
+
for p in self._dir("skills").glob("*.json"):
|
|
307
|
+
data = self._read_json(p)
|
|
308
|
+
if data:
|
|
309
|
+
s = Skill(**data)
|
|
310
|
+
searchable = " ".join(
|
|
311
|
+
[s.name, s.description, " ".join(s.steps), " ".join(s.tool_bindings)]
|
|
312
|
+
).lower()
|
|
313
|
+
if q in searchable or all(word in searchable for word in q.split()):
|
|
314
|
+
skills.append(s)
|
|
315
|
+
return skills
|
|
316
|
+
|
|
317
|
+
async def save_telemetry(self, event: RetrievalTelemetryEvent) -> str:
|
|
318
|
+
self._write_json(
|
|
319
|
+
self._dir("telemetry") / f"{event.telemetry_id}.json", event.model_dump(mode="json")
|
|
320
|
+
)
|
|
321
|
+
return event.telemetry_id
|
|
322
|
+
|
|
323
|
+
async def list_telemetry(self, lesson_id: str | None = None) -> list[RetrievalTelemetryEvent]:
|
|
324
|
+
events = []
|
|
325
|
+
for p in self._dir("telemetry").glob("*.json"):
|
|
326
|
+
data = self._read_json(p)
|
|
327
|
+
if data:
|
|
328
|
+
e = RetrievalTelemetryEvent(**data)
|
|
329
|
+
if lesson_id is None or e.lesson_id == lesson_id:
|
|
330
|
+
events.append(e)
|
|
331
|
+
return events
|
|
332
|
+
|
|
333
|
+
async def save_experiment(self, experiment: Experiment) -> str:
|
|
334
|
+
self._write_json(
|
|
335
|
+
self._dir("experiment_results") / f"{experiment.experiment_id}.json",
|
|
336
|
+
experiment.model_dump(mode="json"),
|
|
337
|
+
)
|
|
338
|
+
return experiment.experiment_id
|
|
339
|
+
|
|
340
|
+
async def get_experiment(self, experiment_id: str) -> Experiment | None:
|
|
341
|
+
data = self._read_json(self._dir("experiment_results") / f"{experiment_id}.json")
|
|
342
|
+
return Experiment(**data) if data else None
|
|
343
|
+
|
|
344
|
+
async def update_experiment(self, experiment: Experiment) -> None:
|
|
345
|
+
await self.save_experiment(experiment)
|
|
346
|
+
|
|
347
|
+
async def save_version(self, record: VersionRecord) -> str:
|
|
348
|
+
vid = f"{record.resource_id}_{record.version}"
|
|
349
|
+
subdir = self._dir("prompt_versions" if record.resource_type == "prompt" else "workflow_graphs")
|
|
350
|
+
self._write_json(subdir / f"{vid}.json", record.model_dump(mode="json"))
|
|
351
|
+
return vid
|
|
352
|
+
|
|
353
|
+
async def list_versions(self, resource_type: str, resource_id: str) -> list[VersionRecord]:
|
|
354
|
+
subdir = self._dir("prompt_versions" if resource_type == "prompt" else "workflow_graphs")
|
|
355
|
+
records = []
|
|
356
|
+
for p in subdir.glob(f"{resource_id}_*.json"):
|
|
357
|
+
data = self._read_json(p)
|
|
358
|
+
if data:
|
|
359
|
+
records.append(VersionRecord(**data))
|
|
360
|
+
return sorted(records, key=lambda x: x.created_at)
|
|
361
|
+
|
|
362
|
+
async def save_reflection(self, data: dict[str, Any]) -> str:
|
|
363
|
+
rid = data.get("reflection_id") or self._next_id("reflections", "reflection")
|
|
364
|
+
data["reflection_id"] = rid
|
|
365
|
+
self._write_json(self._dir("reflections") / f"{rid}.json", data)
|
|
366
|
+
return rid
|
|
367
|
+
|
|
368
|
+
async def get_reflection(self, reflection_id: str) -> dict[str, Any] | None:
|
|
369
|
+
return self._read_json(self._dir("reflections") / f"{reflection_id}.json")
|
|
370
|
+
|
|
371
|
+
async def save_metrics(self, name: str, data: dict[str, Any]) -> None:
|
|
372
|
+
self._write_json(self._dir("metrics") / f"{name}.json", data)
|
|
373
|
+
|
|
374
|
+
async def get_metrics(self, name: str) -> dict[str, Any] | None:
|
|
375
|
+
return self._read_json(self._dir("metrics") / f"{name}.json")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def get_storage(backend: str | None = None, data_dir: str | None = None) -> StoragePort:
|
|
379
|
+
backend = backend or os.environ.get("UALL_STORAGE_BACKEND", "file")
|
|
380
|
+
data_dir = (
|
|
381
|
+
data_dir
|
|
382
|
+
or os.environ.get("SUPERMEMORY_STORAGE_PATH")
|
|
383
|
+
or os.environ.get("UALL_DATA_DIR")
|
|
384
|
+
or ".supermemory"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if backend == "file":
|
|
388
|
+
return FileStorageAdapter(data_dir)
|
|
389
|
+
if backend == "sqlite":
|
|
390
|
+
from storage.adapters.sqlite_chroma import SQLiteStorageAdapter
|
|
391
|
+
|
|
392
|
+
return SQLiteStorageAdapter(data_dir)
|
|
393
|
+
if backend == "postgres":
|
|
394
|
+
from storage.adapters.postgres_qdrant_redis import PostgresStorageAdapter
|
|
395
|
+
|
|
396
|
+
return PostgresStorageAdapter()
|
|
397
|
+
raise ValueError(f"Unknown storage backend: {backend}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tier 3: PostgreSQL enterprise adapter — delegates to file storage with postgres hooks."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from storage.adapters.file import FileStorageAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PostgresStorageAdapter(FileStorageAdapter):
|
|
9
|
+
"""
|
|
10
|
+
Enterprise tier stub: uses file storage locally with postgres connection config.
|
|
11
|
+
Full postgres/qdrant/redis integration requires optional deps and running services.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, base_dir: str | None = None):
|
|
15
|
+
data_dir = base_dir or os.environ.get("UALL_DATA_DIR", ".uall")
|
|
16
|
+
super().__init__(data_dir)
|
|
17
|
+
self.postgres_url = os.environ.get(
|
|
18
|
+
"UALL_POSTGRES_URL", "postgresql://uall:uall@localhost:5432/uall"
|
|
19
|
+
)
|
|
20
|
+
self.qdrant_url = os.environ.get("UALL_QDRANT_URL", "http://localhost:6333")
|
|
21
|
+
self.redis_url = os.environ.get("UALL_REDIS_URL", "redis://localhost:6379")
|
|
22
|
+
|
|
23
|
+
async def init(self) -> None:
|
|
24
|
+
await super().init()
|
|
25
|
+
# Enterprise hooks: connect to external services when available
|
|
26
|
+
self._enterprise_ready = False
|
|
27
|
+
try:
|
|
28
|
+
import asyncpg # noqa: F401
|
|
29
|
+
|
|
30
|
+
self._enterprise_ready = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Tier 2: SQLite storage — wraps file adapter with SQLite index for structured data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from storage.adapters.file import FileStorageAdapter
|
|
9
|
+
from uall_core.schemas.common import Experiment, PolicyVersion, Skill
|
|
10
|
+
from uall_core.schemas.events import Event, Feedback, RunEnd, RunStart
|
|
11
|
+
from uall_core.schemas.lesson import Lesson, MemorySearchRequest, PendingLesson
|
|
12
|
+
from uall_core.schemas.common import RetrievalTelemetryEvent, VersionRecord
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SQLiteStorageAdapter(FileStorageAdapter):
|
|
16
|
+
"""File storage + SQLite index for querying."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_dir: str | Path = ".uall"):
|
|
19
|
+
super().__init__(base_dir)
|
|
20
|
+
self.db_path = self.base / "uall.db"
|
|
21
|
+
|
|
22
|
+
async def init(self) -> None:
|
|
23
|
+
await super().init()
|
|
24
|
+
conn = sqlite3.connect(self.db_path)
|
|
25
|
+
conn.executescript(
|
|
26
|
+
"""
|
|
27
|
+
CREATE TABLE IF NOT EXISTS lessons_index (
|
|
28
|
+
lesson_id TEXT PRIMARY KEY,
|
|
29
|
+
status TEXT,
|
|
30
|
+
workflow TEXT,
|
|
31
|
+
step TEXT,
|
|
32
|
+
namespace_id TEXT,
|
|
33
|
+
overall_confidence REAL,
|
|
34
|
+
data_json TEXT
|
|
35
|
+
);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS runs_index (
|
|
37
|
+
run_id TEXT PRIMARY KEY,
|
|
38
|
+
workflow_id TEXT,
|
|
39
|
+
status TEXT,
|
|
40
|
+
data_json TEXT
|
|
41
|
+
);
|
|
42
|
+
CREATE TABLE IF NOT EXISTS pending_index (
|
|
43
|
+
pending_id TEXT PRIMARY KEY,
|
|
44
|
+
status TEXT,
|
|
45
|
+
data_json TEXT
|
|
46
|
+
);
|
|
47
|
+
"""
|
|
48
|
+
)
|
|
49
|
+
conn.commit()
|
|
50
|
+
conn.close()
|
|
51
|
+
|
|
52
|
+
def _index_lesson(self, lesson: Lesson) -> None:
|
|
53
|
+
conn = sqlite3.connect(self.db_path)
|
|
54
|
+
conn.execute(
|
|
55
|
+
"""INSERT OR REPLACE INTO lessons_index
|
|
56
|
+
(lesson_id, status, workflow, step, namespace_id, overall_confidence, data_json)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
58
|
+
(
|
|
59
|
+
lesson.lesson_id,
|
|
60
|
+
lesson.status,
|
|
61
|
+
lesson.stage.workflow,
|
|
62
|
+
lesson.stage.step,
|
|
63
|
+
lesson.namespace.namespace_id,
|
|
64
|
+
lesson.confidence.overall,
|
|
65
|
+
lesson.model_dump_json(),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
conn.commit()
|
|
69
|
+
conn.close()
|
|
70
|
+
|
|
71
|
+
async def save_lesson(self, lesson: Lesson) -> str:
|
|
72
|
+
result = await super().save_lesson(lesson)
|
|
73
|
+
self._index_lesson(lesson)
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
async def update_lesson(self, lesson: Lesson) -> None:
|
|
77
|
+
await super().update_lesson(lesson)
|
|
78
|
+
self._index_lesson(lesson)
|
|
79
|
+
|
|
80
|
+
async def search_lessons(self, request: MemorySearchRequest) -> list[Lesson]:
|
|
81
|
+
conn = sqlite3.connect(self.db_path)
|
|
82
|
+
query = "SELECT data_json FROM lessons_index WHERE status='active'"
|
|
83
|
+
params: list[Any] = []
|
|
84
|
+
if request.workflow:
|
|
85
|
+
query += " AND workflow=?"
|
|
86
|
+
params.append(request.workflow)
|
|
87
|
+
if request.step:
|
|
88
|
+
query += " AND step=?"
|
|
89
|
+
params.append(request.step)
|
|
90
|
+
rows = conn.execute(query, params).fetchall()
|
|
91
|
+
conn.close()
|
|
92
|
+
if rows:
|
|
93
|
+
lessons = [Lesson.model_validate_json(r[0]) for r in rows]
|
|
94
|
+
return lessons[: request.top_k * 4]
|
|
95
|
+
return await super().search_lessons(request)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: supermemory-agent
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: SuperMemory: MCP-first learning memory layer for Claude, Cursor, and agent workflows
|
|
5
|
+
Project-URL: Homepage, https://github.com/YashvantHange/SuperMemory
|
|
6
|
+
Project-URL: Repository, https://github.com/YashvantHange/SuperMemory
|
|
7
|
+
Project-URL: Issues, https://github.com/YashvantHange/SuperMemory/issues
|
|
8
|
+
Author-email: Yashvant Hange <yashvanthange420@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-memory,claude,cursor,mcp,model-context-protocol,supermemory
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: fastapi>=0.109
|
|
23
|
+
Requires-Dist: httpx>=0.26
|
|
24
|
+
Requires-Dist: mcp<2,>=1.27
|
|
25
|
+
Requires-Dist: pydantic>=2.5
|
|
26
|
+
Requires-Dist: python-dateutil>=2.8
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
32
|
+
Provides-Extra: postgres
|
|
33
|
+
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
|
|
34
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
|
|
35
|
+
Provides-Extra: sqlite
|
|
36
|
+
Requires-Dist: aiosqlite>=0.19; extra == 'sqlite'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# SuperMemory MCP
|
|
40
|
+
|
|
41
|
+
<!-- mcp-name: io.github.YashvantHange/supermemory -->
|
|
42
|
+
|
|
43
|
+
MCP-first learning memory layer for Claude, Cursor, and agent workflows. Captures distilled lessons from failures and corrections (not full transcripts), validates before storage, and improves agents over time through a closed-loop cycle.
|
|
44
|
+
|
|
45
|
+
## Install from PyPI (recommended for Claude / Cursor users)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install supermemory-agent
|
|
49
|
+
supermemory-agent --storage .supermemory --transport stdio
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uvx supermemory-agent --storage .supermemory --transport stdio
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Install from source (developers)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -e ".[dev]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Run MCP server
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m supermemory_mcp.server --storage .supermemory --transport stdio
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or via CLI entry point:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
supermemory-agent --storage .supermemory --transport stdio
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Streamable HTTP:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m supermemory_mcp.server --storage .supermemory --transport streamable-http
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## MCP tools (29 total)
|
|
83
|
+
|
|
84
|
+
**GitHub-compatible core (13):** `retrieve`, `record_event`, `record_failure`, `record_correction`, `reflect`, `validate`, `process_promotions`, `report_outcome`, `get_policies`, `add_policy`, `add_skill`, `search_skills`, `get_skill`
|
|
85
|
+
|
|
86
|
+
**Extended UALL (16):** `learn.run.start`, `learn.run.event`, `learn.run.end`, `learn.store`, `learn.retrieve`, `learn.reflect`, `learn.validate`, `learn.evaluate`, `learn.feedback`, `learn.improvements`, `learn.analytics`, `learn.policies`, `learn.experiment`, `learn.rollback`, `learn.skills`, `learn.telemetry`
|
|
87
|
+
|
|
88
|
+
## MCP resources
|
|
89
|
+
|
|
90
|
+
- `supermemory://policies/active`
|
|
91
|
+
- `supermemory://lessons/{lesson_id}`
|
|
92
|
+
- `supermemory://memory/{lesson_id}/provenance`
|
|
93
|
+
- `supermemory://skills/{skill_id}`
|
|
94
|
+
|
|
95
|
+
## Agent learning loop
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
retrieve → record_failure → reflect(event_ids) → validate → process_promotions
|
|
99
|
+
→ retrieve again → report_outcome
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Cursor / Claude Desktop
|
|
103
|
+
|
|
104
|
+
### MCP server
|
|
105
|
+
|
|
106
|
+
Copy `examples/cursor.mcp.json` to `.cursor/mcp.json` (Cursor project).
|
|
107
|
+
|
|
108
|
+
For Claude Desktop, merge `examples/claude_desktop_config.json` into:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
%APPDATA%\Claude\claude_desktop_config.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Restart Claude Desktop after editing the config.
|
|
115
|
+
|
|
116
|
+
### Agent skills (Cursor + Claude Code)
|
|
117
|
+
|
|
118
|
+
| Platform | Project path | Global path |
|
|
119
|
+
|----------|--------------|-------------|
|
|
120
|
+
| **Cursor** | `.cursor/skills/supermemory-agent-learning/` | `~/.cursor/skills/supermemory-agent-learning/` |
|
|
121
|
+
| **Claude Code** | `.claude/skills/supermemory-agent-learning/` | `~/.claude/skills/supermemory-agent-learning/` |
|
|
122
|
+
| **Canonical source** | `skills/supermemory-agent-learning/` | edit here, then run `python scripts/sync_skills.py` |
|
|
123
|
+
|
|
124
|
+
Mention **SuperMemory**, **agent learning**, or **MCP memory** in chat to load the skill.
|
|
125
|
+
|
|
126
|
+
## Python SDK
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from uall_python import UALLClient
|
|
130
|
+
|
|
131
|
+
client = UALLClient(storage="file")
|
|
132
|
+
|
|
133
|
+
with client.run(workflow_id="pdf-pipeline", step="planner", namespace="team:eng") as run:
|
|
134
|
+
lessons = run.retrieve(step="planner", max_tokens=800)
|
|
135
|
+
run.record_failure(snippet="chose OCR for searchable PDF", tags=["routing"])
|
|
136
|
+
run.report_lesson_outcome(lesson_id="lesson_001", used=True, accepted=True, improved=True)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## REST API
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python -m uall_server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Server runs at `http://localhost:8000`. See `api/openapi.yaml`.
|
|
146
|
+
|
|
147
|
+
## Storage
|
|
148
|
+
|
|
149
|
+
| Tier | Backend | Default path |
|
|
150
|
+
|------|---------|--------------|
|
|
151
|
+
| Default | `.supermemory/` JSON files | `SUPERMEMORY_STORAGE_PATH` or `UALL_DATA_DIR` |
|
|
152
|
+
| Optional | SQLite | `UALL_STORAGE_BACKEND=sqlite` |
|
|
153
|
+
| Enterprise | PostgreSQL + stubs | `UALL_STORAGE_BACKEND=postgres` |
|
|
154
|
+
|
|
155
|
+
## Tests
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python tests/run_all.py # full suite (pytest + agent demos)
|
|
159
|
+
python -m pytest tests/ -v # all unit/integration tests
|
|
160
|
+
python -m pytest tests/test_mcp_server.py -v # real stdio MCP transport
|
|
161
|
+
python -m pytest tests/test_core.py -v # GitHub-compatible closed loop
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT — see [LICENSE](LICENSE)
|
|
167
|
+
|
|
168
|
+
## Publish / list in directories
|
|
169
|
+
|
|
170
|
+
See [docs/PUBLISHING.md](docs/PUBLISHING.md) for MCP Registry, Cursor Directory, and Claude Connectors Directory submission steps.
|