loop-memory 0.4.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 (84) hide show
  1. loop_memory/__init__.py +62 -0
  2. loop_memory/backends/__init__.py +13 -0
  3. loop_memory/backends/embedding.py +82 -0
  4. loop_memory/backends/sentence_embedder.py +30 -0
  5. loop_memory/backends/vector_store.py +139 -0
  6. loop_memory/cli/__init__.py +0 -0
  7. loop_memory/cli/_common.py +68 -0
  8. loop_memory/cli/commands/__init__.py +13 -0
  9. loop_memory/cli/commands/cognitive.py +205 -0
  10. loop_memory/cli/commands/diag.py +346 -0
  11. loop_memory/cli/commands/graph.py +21 -0
  12. loop_memory/cli/commands/hooks.py +212 -0
  13. loop_memory/cli/commands/read.py +362 -0
  14. loop_memory/cli/commands/serve.py +147 -0
  15. loop_memory/cli/commands/write.py +138 -0
  16. loop_memory/cli/main.py +115 -0
  17. loop_memory/engine/__init__.py +0 -0
  18. loop_memory/engine/loop.py +247 -0
  19. loop_memory/engine/reflect.py +89 -0
  20. loop_memory/examples/__init__.py +0 -0
  21. loop_memory/examples/demo.py +39 -0
  22. loop_memory/export/__init__.py +39 -0
  23. loop_memory/export/memory_md.py +629 -0
  24. loop_memory/graph/__init__.py +0 -0
  25. loop_memory/graph/build.py +259 -0
  26. loop_memory/graph/extract.py +197 -0
  27. loop_memory/ingest/__init__.py +0 -0
  28. loop_memory/ingest/loader.py +782 -0
  29. loop_memory/ingest/pipeline.py +458 -0
  30. loop_memory/jobs/__init__.py +0 -0
  31. loop_memory/jobs/cognitive.py +353 -0
  32. loop_memory/jobs/compact.py +371 -0
  33. loop_memory/jobs/consolidate.py +95 -0
  34. loop_memory/jobs/contradiction.py +281 -0
  35. loop_memory/jobs/evolution.py +2021 -0
  36. loop_memory/jobs/graph.py +395 -0
  37. loop_memory/jobs/llm_compact_pass.py +24 -0
  38. loop_memory/jobs/llm_consolidate.py +980 -0
  39. loop_memory/jobs/scheduler.py +495 -0
  40. loop_memory/llm/__init__.py +0 -0
  41. loop_memory/llm/base.py +80 -0
  42. loop_memory/llm/openai_adapter.py +31 -0
  43. loop_memory/llm/providers.py +517 -0
  44. loop_memory/mcp/__init__.py +804 -0
  45. loop_memory/memory/__init__.py +0 -0
  46. loop_memory/memory/types.py +199 -0
  47. loop_memory/privacy/__init__.py +22 -0
  48. loop_memory/privacy/private.py +46 -0
  49. loop_memory/privacy/redact.py +188 -0
  50. loop_memory/py.typed +0 -0
  51. loop_memory/sdk.py +875 -0
  52. loop_memory/sdk_extensions.py +384 -0
  53. loop_memory/security/__init__.py +20 -0
  54. loop_memory/security/secrets.py +464 -0
  55. loop_memory/serve/__init__.py +0 -0
  56. loop_memory/serve/app.py +506 -0
  57. loop_memory/serve/handlers.py +316 -0
  58. loop_memory/serve/routes/_shared.py +59 -0
  59. loop_memory/serve/routes/admin.py +970 -0
  60. loop_memory/serve/routes/cognitive.py +64 -0
  61. loop_memory/serve/routes/export.py +65 -0
  62. loop_memory/serve/routes/graph.py +101 -0
  63. loop_memory/serve/routes/insights.py +702 -0
  64. loop_memory/serve/routes/memories.py +435 -0
  65. loop_memory/serve/routes/sessions.py +75 -0
  66. loop_memory/serve/routes/system.py +493 -0
  67. loop_memory/serve/routes/wiki.py +812 -0
  68. loop_memory/serve/static/__init__.py +0 -0
  69. loop_memory/serve/static/index.html +15 -0
  70. loop_memory/serve/watcher.py +451 -0
  71. loop_memory/storage/__init__.py +5 -0
  72. loop_memory/storage/retrieval.py +365 -0
  73. loop_memory/storage/sqlite_store.py +3627 -0
  74. loop_memory/wiki/__init__.py +41 -0
  75. loop_memory/wiki/backfill.py +143 -0
  76. loop_memory/wiki/classifier.py +238 -0
  77. loop_memory/wiki/prompts.py +295 -0
  78. loop_memory/wiki/scope.py +227 -0
  79. loop_memory-0.4.0.dist-info/METADATA +627 -0
  80. loop_memory-0.4.0.dist-info/RECORD +84 -0
  81. loop_memory-0.4.0.dist-info/WHEEL +5 -0
  82. loop_memory-0.4.0.dist-info/entry_points.txt +2 -0
  83. loop_memory-0.4.0.dist-info/licenses/LICENSE +21 -0
  84. loop_memory-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,435 @@
1
+ """Route group: memories.
2
+
3
+ Memory CRUD + recall + v1/memories + v1/recall + /api/memories/page.
4
+
5
+ All routes were extracted from ``serve/app.py`` as part of the O1
6
+ refactor to keep the central ``create_app`` small. Each block lives
7
+ inside ``register(app, store, scheduler=None)`` so closures over the
8
+ three captured variables work unchanged from the original layout.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Optional
13
+
14
+ import re
15
+ from fastapi import FastAPI, HTTPException
16
+ from fastapi.responses import JSONResponse
17
+
18
+ from ...storage.sqlite_store import MemoryStore
19
+ from ._shared import _memory_to_dict, _export_safe_segment
20
+
21
+
22
+ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None) -> None:
23
+ """Mount every route in this bucket onto ``app``.
24
+
25
+ ``store`` and ``scheduler`` are captured in the route closures so
26
+ the function bodies stay byte-identical to the pre-split layout.
27
+ """
28
+ @app.get("/api/memories/{mid}/score")
29
+ def memory_score_components(mid: str):
30
+ """Return the v2 score breakdown (importance / recency / usage /
31
+ feedback) so the UI can render a radar / breakdown chart per
32
+ memory."""
33
+ m = store.get_memory(mid)
34
+ if m is None:
35
+ raise HTTPException(404, "memory not found")
36
+ sig = store.get_signal(mid)
37
+ comps = MemoryStore.score_components(
38
+ importance=m.importance or 0.0,
39
+ created_at=m.created_at,
40
+ recall_count=sig["recall_count"],
41
+ last_recalled_at=sig["last_recalled_at"],
42
+ positive=sig["positive"],
43
+ negative=sig["negative"],
44
+ half_life_days=30.0,
45
+ )
46
+ return {"memory_id": mid, **comps}
47
+
48
+
49
+ @app.get("/api/memories")
50
+ def memories(
51
+ source: str | None = None,
52
+ session_id: str | None = None,
53
+ kind: str | None = None,
54
+ min_score: float | None = None,
55
+ since: float | None = None,
56
+ until: float | None = None,
57
+ q: str | None = None,
58
+ limit: int = 200,
59
+ ):
60
+ if source:
61
+ rows = store.list_memories(
62
+ kind=kind,
63
+ min_score=min_score,
64
+ since=since,
65
+ until=until,
66
+ query=q,
67
+ limit=limit * 4,
68
+ )
69
+ rows = [r for r in rows if (r.source == source)]
70
+ rows = rows[:limit]
71
+ else:
72
+ rows = store.list_memories(
73
+ session_id=session_id,
74
+ kind=kind,
75
+ min_score=min_score,
76
+ since=since,
77
+ until=until,
78
+ query=q,
79
+ limit=limit,
80
+ )
81
+ # If a session_id was passed, drop the source-coded full scan and
82
+ # apply the session filter on top of either branch (catches the
83
+ # ``source=`` branch where list_memories was called without
84
+ # session_id so the filter didn't propagate).
85
+ if session_id:
86
+ rows = [r for r in rows if r.session_id == session_id]
87
+ return [_memory_to_dict(m) for m in rows]
88
+
89
+
90
+ @app.get("/api/memories/page")
91
+ def memories_page(
92
+ before_id: str | None = None,
93
+ after_id: str | None = None,
94
+ session_id: str | None = None,
95
+ source: str | None = None,
96
+ kind: str | None = None,
97
+ min_score: float | None = None,
98
+ limit: int = 100,
99
+ ):
100
+ """Cursor-paginated memory list (audit O11).
101
+
102
+ Returns ``{rows: [...], next_before_id, next_after_id}`` so
103
+ the UI can request additional pages with a stable, opaque
104
+ cursor (a memory id) instead of an offset that breaks on
105
+ concurrent inserts.
106
+
107
+ Pass ``before_id`` to fetch rows older than the boundary;
108
+ pass ``after_id`` to fetch rows newer. Omit both to start
109
+ from the most-recent page.
110
+ """
111
+ from fastapi import HTTPException as _HE
112
+ try:
113
+ rows, next_before_id, next_after_id = store.list_memories_cursor(
114
+ limit=min(max(limit, 1), 500),
115
+ before_id=before_id,
116
+ after_id=after_id,
117
+ session_id=session_id,
118
+ source=source,
119
+ kind=kind,
120
+ min_score=min_score,
121
+ )
122
+ except ValueError as e:
123
+ raise _HE(404, str(e))
124
+ return {
125
+ "rows": [_memory_to_dict(m) for m in rows],
126
+ "next_before_id": next_before_id,
127
+ "next_after_id": next_after_id,
128
+ "count": len(rows),
129
+ }
130
+
131
+
132
+ @app.post("/api/memories/{mid}/feedback")
133
+ def memory_feedback(mid: str, value: str = "up", reason: str | None = None):
134
+ """Record 👍/👎 feedback on a memory.
135
+
136
+ ``value`` is one of:
137
+ - ``up`` — bump positive counter (user kept / liked this)
138
+ - ``down`` — bump negative counter (user rejected this)
139
+ - ``ignore``— same as down + soft-delete the memory
140
+ """
141
+ v = (value or "").strip().lower()
142
+ if v not in ("up", "down", "ignore"):
143
+ raise HTTPException(400, f"value must be up|down|ignore (got {value!r})")
144
+ try:
145
+ store.record_signal(mid, positive=(v == "up"))
146
+ except Exception as e:
147
+ raise HTTPException(500, f"signal failed: {e}")
148
+ deleted = 0
149
+ if v == "ignore":
150
+ deleted = store.delete_memory(mid)
151
+ return {"ok": True, "value": v, "deleted": deleted, "reason": reason}
152
+
153
+
154
+ @app.post("/api/v1/memories")
155
+ def v1_create_memory(body: dict):
156
+ """Idempotent remember(). Body: {text, kind?, importance?,
157
+ tags?, source?, session_id?, external_id?, agent_id?, user_id?,
158
+ ttl?}. When ``external_id`` is set, the (agent_id, user_id,
159
+ external_id) tuple must be unique; re-pushing updates the
160
+ row in place.
161
+ """
162
+ text = (body.get("text") or "").strip()
163
+ if not text:
164
+ raise HTTPException(400, "text is required")
165
+ kind = (body.get("kind") or "fact").strip()
166
+ importance = body.get("importance")
167
+ try:
168
+ importance_f = float(importance) if importance is not None else 0.5
169
+ except (TypeError, ValueError):
170
+ raise HTTPException(400, f"importance must be a number, got {importance!r}")
171
+ importance_f = max(0.0, min(1.0, importance_f))
172
+ ttl = body.get("ttl")
173
+ try:
174
+ ttl_f = float(ttl) if ttl is not None else None
175
+ except (TypeError, ValueError):
176
+ raise HTTPException(400, f"ttl must be a number, got {ttl!r}")
177
+ ext = body.get("external_id")
178
+ if ext is not None:
179
+ ext = str(ext).strip() or None
180
+ _ca = body.get("created_at")
181
+ try:
182
+ _ca_f = float(_ca) if _ca is not None else None
183
+ except (TypeError, ValueError):
184
+ raise HTTPException(400, "created_at must be a number, got " + repr(_ca))
185
+ stored = store.upsert_memory(
186
+ kind=kind,
187
+ text=text,
188
+ importance=importance_f,
189
+ source=body.get("source"),
190
+ session_id=body.get("session_id"),
191
+ tags=list(body.get("tags") or []),
192
+ agent_id=body.get("agent_id"),
193
+ user_id=body.get("user_id"),
194
+ external_id=ext,
195
+ ttl=ttl_f,
196
+ )
197
+ return _memory_to_dict(stored)
198
+
199
+
200
+ @app.post("/api/v1/memories:batch")
201
+ def v1_create_memories_batch(body: dict):
202
+ """Bulk remember(). Body: {items: [ {...same as single...}, ... ]}.
203
+
204
+ Returns a per-item list with either the created memory or an
205
+ error string. The HTTP status is always 200 so a single
206
+ malformed item doesn't fail the whole batch — callers should
207
+ inspect each ``items[].error`` field.
208
+ """
209
+ items = body.get("items") or []
210
+ if not isinstance(items, list):
211
+ raise HTTPException(400, "items must be a list")
212
+ if len(items) > 500:
213
+ raise HTTPException(400, "batch size capped at 500 per request")
214
+ out = []
215
+ for raw in items:
216
+ try:
217
+ if not isinstance(raw, dict):
218
+ raise ValueError("item must be an object")
219
+ text = (raw.get("text") or "").strip()
220
+ if not text:
221
+ raise ValueError("text is required")
222
+ ext = raw.get("external_id")
223
+ if ext is not None:
224
+ ext = str(ext).strip() or None
225
+ importance = raw.get("importance")
226
+ try:
227
+ importance_f = float(importance) if importance is not None else 0.5
228
+ except (TypeError, ValueError):
229
+ raise ValueError(f"importance must be a number, got {importance!r}")
230
+ importance_f = max(0.0, min(1.0, importance_f))
231
+ _ca = raw.get("created_at")
232
+ try:
233
+ _ca_f = float(_ca) if _ca is not None else None
234
+ except (TypeError, ValueError):
235
+ raise ValueError("created_at must be a number, got " + repr(_ca))
236
+ stored = store.upsert_memory(
237
+ kind=(raw.get("kind") or "fact").strip(),
238
+ text=text,
239
+ importance=importance_f,
240
+ source=raw.get("source"),
241
+ session_id=raw.get("session_id"),
242
+ tags=list(raw.get("tags") or []),
243
+ agent_id=raw.get("agent_id"),
244
+ user_id=raw.get("user_id"),
245
+ external_id=ext,
246
+ created_at=_ca_f,
247
+ )
248
+ out.append(_memory_to_dict(stored))
249
+ except Exception as e:
250
+ out.append({"error": str(e), "input": raw})
251
+ return {"items": out, "count": len(out)}
252
+
253
+
254
+ @app.get("/api/v1/memories")
255
+ def v1_list_memories(
256
+ agent_id: str | None = None,
257
+ user_id: str | None = None,
258
+ session_id: str | None = None,
259
+ kind: str | None = None,
260
+ external_id: str | None = None,
261
+ min_score: float | None = None,
262
+ q: str | None = None,
263
+ limit: int = 50,
264
+ ):
265
+ """List memories with simple filters. ``external_id`` is exact-
266
+ match; combine with ``agent_id`` / ``user_id`` to address a
267
+ specific row pushed by an external Agent."""
268
+ rows = store.list_memories(
269
+ agent_id=agent_id,
270
+ user_id=user_id,
271
+ session_id=session_id,
272
+ kind=kind,
273
+ external_id=external_id,
274
+ min_score=min_score,
275
+ query=q,
276
+ limit=min(max(limit, 1), 500),
277
+ )
278
+ return {"memories": [_memory_to_dict(m) for m in rows], "count": len(rows)}
279
+
280
+
281
+ @app.get("/api/v1/recall")
282
+ def v1_recall(
283
+ q: str,
284
+ limit: int = 8,
285
+ include: str = "memories,wiki,entities",
286
+ source: str | None = None,
287
+ agent_id: str | None = None,
288
+ user_id: str | None = None,
289
+ mode: str = "hybrid",
290
+ ):
291
+ """Unified recall filtered to a single Agent namespace.
292
+
293
+ ``agent_id`` and ``user_id`` are optional narrowing filters
294
+ applied on top of the existing BM25+semantic+entity hybrid
295
+ pipeline. Memories with no ``agent_id`` set are treated as
296
+ "global" and visible to every caller (matches the SDK
297
+ semantics).
298
+ """
299
+ wanted = tuple(s.strip() for s in include.split(",") if s.strip())
300
+ if not wanted:
301
+ wanted = ("memories", "wiki", "entities")
302
+ if mode == "legacy" or not hasattr(store, "recall_hybrid"):
303
+ r = store.recall(
304
+ q,
305
+ limit=limit,
306
+ include=wanted,
307
+ bump_signals=True,
308
+ source=source,
309
+ )
310
+ else:
311
+ r = store.recall_hybrid(
312
+ q, limit=limit, include=wanted,
313
+ bump_signals=True, source=source, level=1,
314
+ )
315
+ if agent_id is not None or user_id is not None:
316
+ def _own(m: dict) -> bool:
317
+ ag = m.get("agent_id")
318
+ ur = m.get("user_id")
319
+ # Global memories (no agent_id) are visible to all.
320
+ if ag is None and ur is None:
321
+ return True
322
+ if agent_id is not None and ag not in (None, agent_id):
323
+ return False
324
+ if user_id is not None and ur not in (None, user_id):
325
+ return False
326
+ return True
327
+ r["memories"] = [m for m in r.get("memories", []) if _own(m)]
328
+ return {
329
+ "query": q,
330
+ "tokens": r.get("tokens", []),
331
+ "memories": r.get("memories", []),
332
+ "wiki": r.get("wiki", []),
333
+ "entities": r.get("entities", []),
334
+ "mode": mode,
335
+ "source": source,
336
+ "temporal_intent": r.get("temporal_intent", "any"),
337
+ "temporal_confidence": r.get("temporal_confidence", 0.0),
338
+ }
339
+
340
+
341
+ @app.post("/api/v1/memories/{mid}/feedback")
342
+ def v1_feedback(mid: str, body: dict):
343
+ """Record 👍/👎 on a memory by id. Body: {value, reason?}."""
344
+ value = (body.get("value") or "up").strip().lower()
345
+ if value not in ("up", "down", "ignore"):
346
+ raise HTTPException(400, "value must be up|down|ignore")
347
+ row = store.get_memory(mid)
348
+ if row is None:
349
+ raise HTTPException(404, "memory not found")
350
+ try:
351
+ store.record_signal(mid, positive=(value == "up"))
352
+ except Exception as e:
353
+ raise HTTPException(500, f"signal failed: {e}")
354
+ deleted = 0
355
+ if value == "ignore":
356
+ deleted = store.delete_memory(mid)
357
+ return {"ok": True, "value": value, "deleted": deleted}
358
+
359
+
360
+ @app.post("/api/v1/memories/feedback")
361
+ def v1_feedback_by_external(body: dict):
362
+ """Record 👍/👎 on a memory addressed by ``(agent_id, user_id,
363
+ external_id)``. Body: {external_id, agent_id?, user_id?,
364
+ value, reason?}. Returns 404 when no memory matches.
365
+ """
366
+ ext = (body.get("external_id") or "").strip()
367
+ if not ext:
368
+ raise HTTPException(400, "external_id is required")
369
+ agent_id = body.get("agent_id")
370
+ user_id = body.get("user_id")
371
+ row = store.find_memory_by_external_id(agent_id or "", ext, user_id=user_id)
372
+ if row is None:
373
+ raise HTTPException(404, "no memory matches that external_id")
374
+ value = (body.get("value") or "up").strip().lower()
375
+ if value not in ("up", "down", "ignore"):
376
+ raise HTTPException(400, "value must be up|down|ignore")
377
+ store.record_signal(row.id, positive=(value == "up"))
378
+ deleted = 0
379
+ if value == "ignore":
380
+ deleted = store.delete_memory(row.id)
381
+ return {"ok": True, "memory_id": row.id, "value": value, "deleted": deleted}
382
+
383
+
384
+ @app.delete("/api/v1/memories")
385
+ def v1_delete_memory_by_external(
386
+ external_id: str,
387
+ agent_id: str | None = None,
388
+ user_id: str | None = None,
389
+ ):
390
+ """Delete a memory by its external triple. Returns 404 if no
391
+ row matches so the Agent can retry with a corrected tuple."""
392
+ if not external_id:
393
+ raise HTTPException(400, "external_id is required")
394
+ row = store.find_memory_by_external_id(agent_id or "", external_id, user_id=user_id)
395
+ if row is None:
396
+ raise HTTPException(404, "no memory matches that external_id")
397
+ deleted = store.delete_memory(row.id)
398
+ return {"deleted": deleted, "memory_id": row.id}
399
+
400
+ # ------------------------------------------------------------------
401
+ # /api/v1 — graph, cognitive sleep, export/import/fork (v7)
402
+ # ------------------------------------------------------------------
403
+ # Closes the gaps the article called out: 3D adaptive scoring,
404
+ # semantic graph edges, cognitive audit, white-box MEMORY.md
405
+ # bundles, and a fork primitive so any Agent can snapshot the
406
+ # wiki and `git revert` later.
407
+
408
+
409
+ @app.delete("/api/memories/{mid}")
410
+ def delete_memory(mid: str):
411
+ n = store.delete_memory(mid)
412
+ if n == 0:
413
+ raise HTTPException(404, "memory not found")
414
+ return {"deleted": n}
415
+
416
+
417
+ @app.get("/api/memories/{mid}", name="memory_drill_down")
418
+ def memory_drill_down(mid: str):
419
+ """L2 drill-down for a single memory row (full text)."""
420
+ mem = store.get_memory(mid)
421
+ if not mem:
422
+ from fastapi import HTTPException
423
+ raise HTTPException(status_code=404, detail="memory not found")
424
+ return {
425
+ "id": mem.id,
426
+ "kind": "memory",
427
+ "text": mem.text,
428
+ "importance": mem.importance,
429
+ "score": mem.score,
430
+ "source": mem.source,
431
+ "tags": mem.tags,
432
+ "created_at": mem.created_at,
433
+ "updated_at": mem.updated_at,
434
+ }
435
+
@@ -0,0 +1,75 @@
1
+ """Route group: sessions.
2
+
3
+ Session listing + per-source counts.
4
+
5
+ All routes were extracted from ``serve/app.py`` as part of the O1
6
+ refactor to keep the central ``create_app`` small. Each block lives
7
+ inside ``register(app, store, scheduler=None)`` so closures over the
8
+ three captured variables work unchanged from the original layout.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Optional
13
+
14
+ import re
15
+ from fastapi import FastAPI, HTTPException
16
+ from fastapi.responses import JSONResponse
17
+
18
+ from ...storage.sqlite_store import MemoryStore
19
+ from ._shared import _memory_to_dict, _session_to_dict, _export_safe_segment
20
+
21
+
22
+ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None) -> None:
23
+ """Mount every route in this bucket onto ``app``.
24
+
25
+ ``store`` and ``scheduler`` are captured in the route closures so
26
+ the function bodies stay byte-identical to the pre-split layout.
27
+ """
28
+ @app.get("/api/sessions")
29
+ def sessions(source: str | None = None, limit: int = 100):
30
+ return [
31
+ _session_to_dict(s)
32
+ for s in store.list_sessions(source=source, limit=limit)
33
+ ]
34
+
35
+
36
+ @app.get("/api/sessions/counts")
37
+ def session_counts():
38
+ """Per-source session + memory counts, for the sidebar filter UI."""
39
+ try:
40
+ with store._conn() as c:
41
+ rows = c.execute(
42
+ """SELECT source,
43
+ COUNT(*) AS sessions,
44
+ COALESCE(SUM(message_count), 0) AS turns
45
+ FROM sessions
46
+ GROUP BY source"""
47
+ ).fetchall()
48
+ except Exception:
49
+ rows = []
50
+ out = {"all": {"sessions": 0, "turns": 0}, "by_source": {}}
51
+ for r in rows:
52
+ src = r["source"] or "unknown"
53
+ out["by_source"][src] = {
54
+ "sessions": r["sessions"],
55
+ "turns": r["turns"],
56
+ }
57
+ out["all"]["sessions"] += r["sessions"]
58
+ out["all"]["turns"] += r["turns"]
59
+ return out
60
+
61
+
62
+ @app.get("/api/sessions/{session_id}/memories")
63
+ def session_memories(session_id: str, limit: int = 1000):
64
+ return [
65
+ _memory_to_dict(m)
66
+ for m in store.list_memories(session_id=session_id, limit=limit)
67
+ ]
68
+
69
+
70
+ @app.delete("/api/sessions/{sid}")
71
+ def delete_session(sid: str):
72
+ n = store.delete_session(sid)
73
+ return {"deleted": n}
74
+
75
+