superlocalmemory 3.3.29 → 3.4.1

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 (69) hide show
  1. package/ATTRIBUTION.md +1 -1
  2. package/CHANGELOG.md +3 -0
  3. package/LICENSE +633 -70
  4. package/README.md +14 -11
  5. package/docs/screenshots/01-dashboard-main.png +0 -0
  6. package/docs/screenshots/02-knowledge-graph.png +0 -0
  7. package/docs/screenshots/03-patterns-learning.png +0 -0
  8. package/docs/screenshots/04-learning-dashboard.png +0 -0
  9. package/docs/screenshots/05-behavioral-analysis.png +0 -0
  10. package/docs/screenshots/06-graph-communities.png +0 -0
  11. package/docs/v2-archive/ACCESSIBILITY.md +1 -1
  12. package/docs/v2-archive/FRAMEWORK-INTEGRATIONS.md +1 -1
  13. package/docs/v2-archive/MCP-MANUAL-SETUP.md +1 -1
  14. package/docs/v2-archive/SEARCH-ENGINE-V2.2.0.md +2 -2
  15. package/docs/v2-archive/SEARCH-INTEGRATION-GUIDE.md +1 -1
  16. package/docs/v2-archive/UNIVERSAL-INTEGRATION.md +1 -1
  17. package/docs/v2-archive/V2.2.0-OPTIONAL-SEARCH.md +1 -1
  18. package/docs/v2-archive/example_graph_usage.py +1 -1
  19. package/ide/configs/codex-mcp.toml +1 -1
  20. package/ide/integrations/langchain/README.md +1 -1
  21. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +1 -1
  22. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +1 -1
  23. package/ide/integrations/langchain/pyproject.toml +2 -2
  24. package/ide/integrations/langchain/tests/__init__.py +1 -1
  25. package/ide/integrations/langchain/tests/test_chat_message_history.py +1 -1
  26. package/ide/integrations/langchain/tests/test_security.py +1 -1
  27. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +1 -1
  28. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +1 -1
  29. package/ide/integrations/llamaindex/pyproject.toml +2 -2
  30. package/ide/integrations/llamaindex/tests/__init__.py +1 -1
  31. package/ide/integrations/llamaindex/tests/test_chat_store.py +1 -1
  32. package/ide/integrations/llamaindex/tests/test_security.py +1 -1
  33. package/ide/skills/slm-build-graph/SKILL.md +3 -3
  34. package/ide/skills/slm-list-recent/SKILL.md +3 -3
  35. package/ide/skills/slm-recall/SKILL.md +3 -3
  36. package/ide/skills/slm-remember/SKILL.md +3 -3
  37. package/ide/skills/slm-show-patterns/SKILL.md +3 -3
  38. package/ide/skills/slm-status/SKILL.md +3 -3
  39. package/ide/skills/slm-switch-profile/SKILL.md +3 -3
  40. package/package.json +3 -3
  41. package/pyproject.toml +3 -3
  42. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  43. package/src/superlocalmemory/core/graph_analyzer.py +254 -12
  44. package/src/superlocalmemory/learning/consolidation_worker.py +240 -52
  45. package/src/superlocalmemory/retrieval/entity_channel.py +135 -4
  46. package/src/superlocalmemory/retrieval/spreading_activation.py +45 -0
  47. package/src/superlocalmemory/server/api.py +9 -1
  48. package/src/superlocalmemory/server/routes/behavioral.py +8 -4
  49. package/src/superlocalmemory/server/routes/chat.py +320 -0
  50. package/src/superlocalmemory/server/routes/insights.py +368 -0
  51. package/src/superlocalmemory/server/routes/learning.py +106 -6
  52. package/src/superlocalmemory/server/routes/memories.py +20 -9
  53. package/src/superlocalmemory/server/routes/stats.py +25 -3
  54. package/src/superlocalmemory/server/routes/timeline.py +252 -0
  55. package/src/superlocalmemory/server/routes/v3_api.py +161 -0
  56. package/src/superlocalmemory/server/ui.py +8 -0
  57. package/src/superlocalmemory/ui/index.html +168 -58
  58. package/src/superlocalmemory/ui/js/graph-event-bus.js +83 -0
  59. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  60. package/src/superlocalmemory/ui/js/knowledge-graph.js +942 -0
  61. package/src/superlocalmemory/ui/js/memory-chat.js +344 -0
  62. package/src/superlocalmemory/ui/js/memory-timeline.js +265 -0
  63. package/src/superlocalmemory/ui/js/quick-actions.js +334 -0
  64. package/src/superlocalmemory.egg-info/PKG-INFO +597 -0
  65. package/src/superlocalmemory.egg-info/SOURCES.txt +287 -0
  66. package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
  67. package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
  68. package/src/superlocalmemory.egg-info/requires.txt +47 -0
  69. package/src/superlocalmemory.egg-info/top_level.txt +1 -0
@@ -25,8 +25,8 @@ try:
25
25
  from superlocalmemory.learning.behavioral import BehavioralPatternStore
26
26
  from superlocalmemory.learning.outcomes import OutcomeTracker
27
27
  BEHAVIORAL_AVAILABLE = True
28
- except ImportError:
29
- logger.info("V3 behavioral engine not available")
28
+ except ImportError as e:
29
+ logger.warning("V3 behavioral engine import failed: %s", e)
30
30
 
31
31
 
32
32
  @router.get("/api/behavioral/status")
@@ -66,9 +66,13 @@ async def behavioral_status():
66
66
  try:
67
67
  store = BehavioralPatternStore(db_path)
68
68
  patterns = store.get_patterns(profile_id=profile)
69
- cross_project_transfers = 0
69
+ # Count patterns spanning multiple projects
70
+ cross_project_transfers = len([
71
+ p for p in patterns
72
+ if isinstance(p, dict) and p.get("project_count", 1) > 1
73
+ ])
70
74
  except Exception as exc:
71
- logger.debug("pattern store: %s", exc)
75
+ logger.warning("pattern store error: %s", exc)
72
76
 
73
77
  return {
74
78
  "available": True,
@@ -0,0 +1,320 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later — see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.1 | https://qualixar.com
4
+
5
+ """Ask My Memory — SSE chat endpoint.
6
+
7
+ Flow: query → 6-channel retrieval → format context → LLM stream → SSE
8
+ Mode A: No LLM, returns formatted retrieval results.
9
+ Mode B: Ollama local streaming via /api/chat.
10
+ Mode C: Cloud LLM streaming (OpenAI-compatible).
11
+
12
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import json
19
+ import logging
20
+ import re
21
+ from typing import AsyncGenerator
22
+
23
+ import httpx
24
+ from fastapi import APIRouter, Request
25
+ from fastapi.responses import StreamingResponse
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ router = APIRouter(tags=["chat"])
30
+
31
+ # Citation marker pattern: [MEM-1], [MEM-2], etc.
32
+ _CITATION_RE = re.compile(r"\[MEM-(\d+)\]")
33
+
34
+ # System prompt for LLM — instructs citation usage
35
+ _SYSTEM_PROMPT = (
36
+ "You are a memory assistant. Answer the user's question using ONLY the "
37
+ "provided memories. When you use information from a memory, include its "
38
+ "marker inline, e.g. [MEM-1]. If no memories are relevant, say so. "
39
+ "Be concise and factual."
40
+ )
41
+
42
+
43
+ # ── SSE Stream Endpoint ─────────────────────────────────────────
44
+
45
+ @router.post("/api/v3/chat/stream")
46
+ async def chat_stream(request: Request):
47
+ """Stream a memory-grounded chat response via SSE.
48
+
49
+ Body: {"query": "...", "mode": "a"|"b"|"c", "limit": 10}
50
+ Response: text/event-stream with events: token, citation, done, error
51
+ """
52
+ try:
53
+ body = await request.json()
54
+ except Exception:
55
+ return StreamingResponse(
56
+ _sse_error("Invalid JSON body"),
57
+ media_type="text/event-stream",
58
+ )
59
+
60
+ query = (body.get("query") or "").strip()
61
+ if not query:
62
+ return StreamingResponse(
63
+ _sse_error("Query is required"),
64
+ media_type="text/event-stream",
65
+ )
66
+
67
+ mode = (body.get("mode") or "a").lower()
68
+ limit = min(body.get("limit", 10), 20)
69
+
70
+ return StreamingResponse(
71
+ _stream_chat(query, mode, limit),
72
+ media_type="text/event-stream",
73
+ headers={
74
+ "Cache-Control": "no-cache",
75
+ "X-Accel-Buffering": "no", # nginx
76
+ "Content-Encoding": "identity", # bypass GZipMiddleware
77
+ },
78
+ )
79
+
80
+
81
+ # ── Core Chat Logic ──────────────────────────────────────────────
82
+
83
+ async def _stream_chat(
84
+ query: str, mode: str, limit: int,
85
+ ) -> AsyncGenerator[str, None]:
86
+ """Retrieve memories, then stream LLM response with citations."""
87
+
88
+ # Step 1: Retrieve memories via WorkerPool (run in executor to avoid blocking)
89
+ memories = []
90
+ try:
91
+ loop = asyncio.get_event_loop()
92
+ memories = await loop.run_in_executor(None, _recall_memories, query, limit)
93
+ except Exception as exc:
94
+ yield _sse_event("error", json.dumps({"message": f"Retrieval failed: {exc}"}))
95
+ yield _sse_event("done", "")
96
+ return
97
+
98
+ if not memories:
99
+ yield _sse_event("token", "No relevant memories found for your query.")
100
+ yield _sse_event("done", "")
101
+ return
102
+
103
+ # Step 2: Send citation metadata
104
+ for i, mem in enumerate(memories):
105
+ citation_data = {
106
+ "index": i + 1,
107
+ "fact_id": mem.get("fact_id", ""),
108
+ "content_preview": (mem.get("content") or "")[:80],
109
+ "trust_score": mem.get("trust_score", 0),
110
+ "score": mem.get("score", 0),
111
+ }
112
+ yield _sse_event("citation", json.dumps(citation_data))
113
+
114
+ # Step 3: Route to appropriate mode
115
+ if mode == "a":
116
+ # Mode A: No LLM — return formatted retrieval results
117
+ async for event in _stream_mode_a(query, memories):
118
+ yield event
119
+ elif mode in ("b", "c"):
120
+ # Mode B/C: LLM streaming
121
+ async for event in _stream_mode_bc(query, memories, mode):
122
+ yield event
123
+ else:
124
+ yield _sse_event("token", "Unknown mode. Use a, b, or c.")
125
+
126
+ yield _sse_event("done", "")
127
+
128
+
129
+ # ── Mode A: Raw Retrieval Results ────────────────────────────────
130
+
131
+ async def _stream_mode_a(
132
+ query: str, memories: list,
133
+ ) -> AsyncGenerator[str, None]:
134
+ """Format retrieval results as readable answer (no LLM).
135
+
136
+ Mode A = zero-cloud. No LLM available, so we show raw retrieval
137
+ results in a structured format. For conversational AI answers,
138
+ users should switch to Mode B (Ollama) or Mode C (Cloud) in Settings.
139
+ """
140
+ yield _sse_event("token", "**Mode A — Raw Memory Retrieval** (no LLM connected)\n")
141
+ yield _sse_event("token", "For AI-powered answers, switch to Mode B or C in Settings.\n")
142
+ yield _sse_event("token", f"Found **{len(memories)}** relevant memories for: *{query}*\n\n")
143
+ await asyncio.sleep(0.03)
144
+
145
+ for i, mem in enumerate(memories):
146
+ content = mem.get("content") or mem.get("source_content") or ""
147
+ score = mem.get("score", 0)
148
+ trust = mem.get("trust_score", 0)
149
+ text = (
150
+ f"**[MEM-{i+1}]** (relevance: {score:.2f}, trust: {trust:.2f})\n"
151
+ f"{content}\n\n"
152
+ )
153
+ yield _sse_event("token", text)
154
+ await asyncio.sleep(0.03)
155
+
156
+
157
+ # ── Mode B/C: LLM Streaming ─────────────────────────────────────
158
+
159
+ async def _stream_mode_bc(
160
+ query: str, memories: list, mode: str,
161
+ ) -> AsyncGenerator[str, None]:
162
+ """Stream LLM response with memory context and citation detection."""
163
+
164
+ # Build context with citation markers
165
+ context_parts = []
166
+ for i, mem in enumerate(memories):
167
+ content = mem.get("content") or mem.get("source_content") or ""
168
+ trust = mem.get("trust_score", 0)
169
+ context_parts.append(f"[MEM-{i+1}] {content} (trust: {trust:.2f})")
170
+ context = "\n".join(context_parts)
171
+
172
+ messages = [
173
+ {"role": "system", "content": _SYSTEM_PROMPT},
174
+ {"role": "user", "content": f"Memories:\n{context}\n\nQuestion: {query}"},
175
+ ]
176
+
177
+ # Load LLM config
178
+ try:
179
+ from superlocalmemory.core.config import SLMConfig
180
+ config = SLMConfig.load()
181
+ provider = config.llm.provider or ""
182
+ model = config.llm.model or ""
183
+ api_key = config.llm.api_key or ""
184
+ api_base = config.llm.api_base or ""
185
+ except Exception:
186
+ yield _sse_event("token", "LLM not configured. Use Mode A or configure a provider in Settings.")
187
+ return
188
+
189
+ if not provider:
190
+ yield _sse_event("token", "No LLM provider configured. Showing raw results instead.\n\n")
191
+ async for event in _stream_mode_a(query, memories):
192
+ yield event
193
+ return
194
+
195
+ # Stream from provider
196
+ try:
197
+ if provider == "ollama":
198
+ async for token in _stream_ollama(messages, model, api_base):
199
+ yield _sse_event("token", token)
200
+ else:
201
+ async for token in _stream_openai_compat(
202
+ messages, model, api_key, api_base, provider,
203
+ ):
204
+ yield _sse_event("token", token)
205
+ except httpx.ConnectError:
206
+ yield _sse_event("token", f"\n\n[Connection failed — is {provider} running?]")
207
+ except Exception as exc:
208
+ yield _sse_event("token", f"\n\n[LLM error: {exc}]")
209
+
210
+
211
+ # ── Ollama Streaming (/api/chat with messages) ───────────────────
212
+
213
+ async def _stream_ollama(
214
+ messages: list, model: str, api_base: str,
215
+ ) -> AsyncGenerator[str, None]:
216
+ """Stream tokens from Ollama /api/chat endpoint."""
217
+ import os
218
+ base = api_base or os.environ.get("OLLAMA_HOST", "http://localhost:11434")
219
+ url = f"{base.rstrip('/')}/api/chat"
220
+
221
+ payload = {
222
+ "model": model or "llama3.2",
223
+ "messages": messages,
224
+ "stream": True,
225
+ "options": {"num_predict": 1024, "temperature": 0.3, "num_ctx": 4096},
226
+ }
227
+
228
+ async with httpx.AsyncClient(timeout=httpx.Timeout(120.0)) as client:
229
+ async with client.stream("POST", url, json=payload) as resp:
230
+ resp.raise_for_status()
231
+ async for line in resp.aiter_lines():
232
+ if not line:
233
+ continue
234
+ try:
235
+ chunk = json.loads(line)
236
+ if chunk.get("done"):
237
+ break
238
+ token = chunk.get("message", {}).get("content", "")
239
+ if token:
240
+ yield token
241
+ except json.JSONDecodeError:
242
+ continue
243
+
244
+
245
+ # ── OpenAI-Compatible Streaming ──────────────────────────────────
246
+
247
+ async def _stream_openai_compat(
248
+ messages: list, model: str, api_key: str,
249
+ api_base: str, provider: str,
250
+ ) -> AsyncGenerator[str, None]:
251
+ """Stream tokens from OpenAI-compatible API (OpenAI, Azure, OpenRouter)."""
252
+ import os
253
+
254
+ if provider == "azure":
255
+ url = api_base # Azure uses full deployment URL
256
+ headers = {"api-key": api_key, "Content-Type": "application/json"}
257
+ elif provider == "openrouter":
258
+ url = api_base or "https://openrouter.ai/api/v1/chat/completions"
259
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
260
+ elif provider == "anthropic":
261
+ # Anthropic uses a different streaming format — simplified here
262
+ url = api_base or "https://api.anthropic.com/v1/messages"
263
+ headers = {
264
+ "x-api-key": api_key,
265
+ "anthropic-version": "2023-06-01",
266
+ "Content-Type": "application/json",
267
+ }
268
+ else:
269
+ url = api_base or "https://api.openai.com/v1/chat/completions"
270
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
271
+
272
+ payload = {
273
+ "model": model,
274
+ "messages": messages,
275
+ "stream": True,
276
+ "max_tokens": 1024,
277
+ "temperature": 0.3,
278
+ }
279
+
280
+ async with httpx.AsyncClient(timeout=httpx.Timeout(120.0)) as client:
281
+ async with client.stream("POST", url, json=payload, headers=headers) as resp:
282
+ resp.raise_for_status()
283
+ async for line in resp.aiter_lines():
284
+ if not line.startswith("data: "):
285
+ continue
286
+ data = line[6:]
287
+ if data == "[DONE]":
288
+ break
289
+ try:
290
+ chunk = json.loads(data)
291
+ token = chunk.get("choices", [{}])[0].get("delta", {}).get("content", "")
292
+ if token:
293
+ yield token
294
+ except json.JSONDecodeError:
295
+ continue
296
+
297
+
298
+ # ── Retrieval Helper ─────────────────────────────────────────────
299
+
300
+ def _recall_memories(query: str, limit: int) -> list:
301
+ """Run 6-channel retrieval via WorkerPool (synchronous, runs in executor)."""
302
+ from superlocalmemory.core.worker_pool import WorkerPool
303
+ pool = WorkerPool.shared()
304
+ result = pool.recall(query, limit=limit)
305
+ if result.get("ok"):
306
+ return result.get("results", [])
307
+ return []
308
+
309
+
310
+ # ── SSE Formatting ───────────────────────────────────────────────
311
+
312
+ def _sse_event(event_type: str, data: str) -> str:
313
+ """Format a single SSE event."""
314
+ return f"event: {event_type}\ndata: {data}\n\n"
315
+
316
+
317
+ async def _sse_error(message: str) -> AsyncGenerator[str, None]:
318
+ """Yield a single SSE error event."""
319
+ yield _sse_event("error", json.dumps({"message": message}))
320
+ yield _sse_event("done", "")