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.
- package/ATTRIBUTION.md +1 -1
- package/CHANGELOG.md +3 -0
- package/LICENSE +633 -70
- package/README.md +14 -11
- package/docs/screenshots/01-dashboard-main.png +0 -0
- package/docs/screenshots/02-knowledge-graph.png +0 -0
- package/docs/screenshots/03-patterns-learning.png +0 -0
- package/docs/screenshots/04-learning-dashboard.png +0 -0
- package/docs/screenshots/05-behavioral-analysis.png +0 -0
- package/docs/screenshots/06-graph-communities.png +0 -0
- package/docs/v2-archive/ACCESSIBILITY.md +1 -1
- package/docs/v2-archive/FRAMEWORK-INTEGRATIONS.md +1 -1
- package/docs/v2-archive/MCP-MANUAL-SETUP.md +1 -1
- package/docs/v2-archive/SEARCH-ENGINE-V2.2.0.md +2 -2
- package/docs/v2-archive/SEARCH-INTEGRATION-GUIDE.md +1 -1
- package/docs/v2-archive/UNIVERSAL-INTEGRATION.md +1 -1
- package/docs/v2-archive/V2.2.0-OPTIONAL-SEARCH.md +1 -1
- package/docs/v2-archive/example_graph_usage.py +1 -1
- package/ide/configs/codex-mcp.toml +1 -1
- package/ide/integrations/langchain/README.md +1 -1
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +1 -1
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +1 -1
- package/ide/integrations/langchain/pyproject.toml +2 -2
- package/ide/integrations/langchain/tests/__init__.py +1 -1
- package/ide/integrations/langchain/tests/test_chat_message_history.py +1 -1
- package/ide/integrations/langchain/tests/test_security.py +1 -1
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +1 -1
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +1 -1
- package/ide/integrations/llamaindex/pyproject.toml +2 -2
- package/ide/integrations/llamaindex/tests/__init__.py +1 -1
- package/ide/integrations/llamaindex/tests/test_chat_store.py +1 -1
- package/ide/integrations/llamaindex/tests/test_security.py +1 -1
- package/ide/skills/slm-build-graph/SKILL.md +3 -3
- package/ide/skills/slm-list-recent/SKILL.md +3 -3
- package/ide/skills/slm-recall/SKILL.md +3 -3
- package/ide/skills/slm-remember/SKILL.md +3 -3
- package/ide/skills/slm-show-patterns/SKILL.md +3 -3
- package/ide/skills/slm-status/SKILL.md +3 -3
- package/ide/skills/slm-switch-profile/SKILL.md +3 -3
- package/package.json +3 -3
- package/pyproject.toml +3 -3
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/graph_analyzer.py +254 -12
- package/src/superlocalmemory/learning/consolidation_worker.py +240 -52
- package/src/superlocalmemory/retrieval/entity_channel.py +135 -4
- package/src/superlocalmemory/retrieval/spreading_activation.py +45 -0
- package/src/superlocalmemory/server/api.py +9 -1
- package/src/superlocalmemory/server/routes/behavioral.py +8 -4
- package/src/superlocalmemory/server/routes/chat.py +320 -0
- package/src/superlocalmemory/server/routes/insights.py +368 -0
- package/src/superlocalmemory/server/routes/learning.py +106 -6
- package/src/superlocalmemory/server/routes/memories.py +20 -9
- package/src/superlocalmemory/server/routes/stats.py +25 -3
- package/src/superlocalmemory/server/routes/timeline.py +252 -0
- package/src/superlocalmemory/server/routes/v3_api.py +161 -0
- package/src/superlocalmemory/server/ui.py +8 -0
- package/src/superlocalmemory/ui/index.html +168 -58
- package/src/superlocalmemory/ui/js/graph-event-bus.js +83 -0
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/knowledge-graph.js +942 -0
- package/src/superlocalmemory/ui/js/memory-chat.js +344 -0
- package/src/superlocalmemory/ui/js/memory-timeline.js +265 -0
- package/src/superlocalmemory/ui/js/quick-actions.js +334 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +597 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +287 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +47 -0
- 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.
|
|
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
|
-
|
|
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.
|
|
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", "")
|