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.
Files changed (54) hide show
  1. storage/adapters/__init__.py +0 -0
  2. storage/adapters/file.py +397 -0
  3. storage/adapters/postgres_qdrant_redis.py +32 -0
  4. storage/adapters/sqlite_chroma.py +95 -0
  5. supermemory_agent-0.2.3.dist-info/METADATA +170 -0
  6. supermemory_agent-0.2.3.dist-info/RECORD +54 -0
  7. supermemory_agent-0.2.3.dist-info/WHEEL +4 -0
  8. supermemory_agent-0.2.3.dist-info/entry_points.txt +2 -0
  9. supermemory_agent-0.2.3.dist-info/licenses/LICENSE +21 -0
  10. supermemory_mcp/__init__.py +5 -0
  11. supermemory_mcp/bridge.py +35 -0
  12. supermemory_mcp/handlers.py +772 -0
  13. supermemory_mcp/server.py +522 -0
  14. supermemory_mcp/text.py +16 -0
  15. uall/__init__.py +0 -0
  16. uall/analytics/service.py +35 -0
  17. uall/collector/__init__.py +0 -0
  18. uall/collector/service.py +100 -0
  19. uall/distillation/distiller.py +80 -0
  20. uall/evaluation/engine.py +38 -0
  21. uall/experiments/manager.py +83 -0
  22. uall/memory/__init__.py +0 -0
  23. uall/memory/confidence.py +36 -0
  24. uall/memory/freshness.py +28 -0
  25. uall/memory/graph.py +24 -0
  26. uall/memory/namespaces.py +40 -0
  27. uall/memory/policies.py +44 -0
  28. uall/memory/provenance.py +23 -0
  29. uall/memory/pruning.py +55 -0
  30. uall/memory/retrieval.py +98 -0
  31. uall/memory/ttl.py +22 -0
  32. uall/memory/validator.py +144 -0
  33. uall/optimization/optimizers.py +59 -0
  34. uall/promotion/queue.py +107 -0
  35. uall/recommendations/engine.py +66 -0
  36. uall/reflection/engine.py +72 -0
  37. uall/rollback/manager.py +49 -0
  38. uall/service.py +572 -0
  39. uall/skills/library.py +19 -0
  40. uall/telemetry/retrieval.py +40 -0
  41. uall_core/__init__.py +0 -0
  42. uall_core/ports/storage.py +71 -0
  43. uall_core/providers/heuristic.py +58 -0
  44. uall_core/providers/llm.py +6 -0
  45. uall_core/schemas/__init__.py +0 -0
  46. uall_core/schemas/common.py +73 -0
  47. uall_core/schemas/events.py +75 -0
  48. uall_core/schemas/graph.py +32 -0
  49. uall_core/schemas/lesson.py +109 -0
  50. uall_core/schemas/namespace.py +76 -0
  51. uall_python/__init__.py +3 -0
  52. uall_python/client.py +337 -0
  53. uall_server/__init__.py +0 -0
  54. uall_server/main.py +284 -0
File without changes
@@ -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.