superlocalmemory 3.8.0 → 3.8.2
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/CHANGELOG.md +112 -0
- package/README.md +32 -120
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +1 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +2 -2
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +3 -5
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +2 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +3 -5
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall.js +7 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +494 -9
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +72 -7
- package/src/superlocalmemory/cli/setup_wizard.py +142 -16
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/component_healer.py +144 -0
- package/src/superlocalmemory/core/component_registry.py +487 -0
- package/src/superlocalmemory/core/config.py +21 -0
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +132 -45
- package/src/superlocalmemory/core/engine.py +29 -22
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +154 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance.py +43 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
- package/src/superlocalmemory/core/recall_pipeline.py +42 -4
- package/src/superlocalmemory/core/store_pipeline.py +195 -20
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/portable_kit.py +34 -2
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/ledger.py +25 -5
- package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_active.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +21 -5
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/routes/backup.py +293 -70
- package/src/superlocalmemory/server/routes/behavioral.py +342 -61
- package/src/superlocalmemory/server/routes/brain.py +57 -16
- package/src/superlocalmemory/server/routes/config_api.py +84 -82
- package/src/superlocalmemory/server/routes/entity.py +100 -23
- package/src/superlocalmemory/server/routes/evolution.py +103 -100
- package/src/superlocalmemory/server/routes/learning.py +286 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/memories.py +8 -3
- package/src/superlocalmemory/server/routes/mesh.py +121 -32
- package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
- package/src/superlocalmemory/server/routes/stats.py +93 -155
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +184 -20
- package/src/superlocalmemory/server/unified_daemon.py +732 -41
- package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
- package/src/superlocalmemory/storage/migration_runner.py +79 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/schema.py +49 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/ui/index.html +6 -8
- package/src/superlocalmemory/ui/js/core.js +52 -9
- package/src/superlocalmemory/ui/js/dashboard.js +169 -82
- package/src/superlocalmemory/ui/js/od-backup.js +156 -65
- package/src/superlocalmemory/ui/js/od-brain.js +88 -51
- package/src/superlocalmemory/ui/js/od-components.js +147 -0
- package/src/superlocalmemory/ui/js/od-entities.js +65 -22
- package/src/superlocalmemory/ui/js/od-graph.js +46 -4
- package/src/superlocalmemory/ui/js/od-health.js +18 -0
- package/src/superlocalmemory/ui/js/od-memories.js +84 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-operations.js +36 -0
- package/src/superlocalmemory/ui/js/od-settings.js +186 -63
- package/src/superlocalmemory/ui/js/od-shell.js +249 -33
- package/src/superlocalmemory/ui/js/od-skills.js +44 -17
- package/src/superlocalmemory/ui/js/settings.js +15 -1
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-governance-advisor.md +0 -80
- package/plugin-src/agents/slm-loop-runner.md +0 -71
- package/plugin-src/agents/slm-memory-advisor.md +0 -49
- package/plugin-src/agents/slm-optimize-advisor.md +0 -44
- package/plugin-src/commands/slm-loop.md +0 -31
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -102
- package/plugin-src/manifest.json +0 -30
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -62
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -25
- package/plugin-src/skills/slm-governance/SKILL.md +0 -248
- package/plugin-src/skills/slm-loop/SKILL.md +0 -99
- package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
- package/plugin-src/skills/slm-profile/SKILL.md +0 -148
- package/plugin-src/skills/slm-scope/SKILL.md +0 -176
|
@@ -4,21 +4,78 @@
|
|
|
4
4
|
"""SuperLocalMemory V3 - Stats Routes
|
|
5
5
|
- AGPL-3.0-or-later
|
|
6
6
|
|
|
7
|
-
Routes: /api/stats, /api/timeline
|
|
7
|
+
Routes: /api/stats, /api/timeline
|
|
8
8
|
"""
|
|
9
|
-
import json
|
|
10
9
|
import logging
|
|
11
|
-
|
|
10
|
+
import sqlite3
|
|
12
11
|
from typing import Optional
|
|
13
12
|
|
|
14
13
|
from fastapi import APIRouter, HTTPException, Query
|
|
15
14
|
|
|
16
|
-
from .helpers import get_db_connection, dict_factory, get_active_profile, DB_PATH
|
|
15
|
+
from .helpers import get_db_connection, dict_factory, get_active_profile, DB_PATH
|
|
17
16
|
|
|
18
17
|
logger = logging.getLogger("superlocalmemory.routes.stats")
|
|
19
18
|
router = APIRouter()
|
|
20
19
|
|
|
21
20
|
|
|
21
|
+
def _query_ingestion_sources(cursor, profile_id: str) -> list[dict]:
|
|
22
|
+
"""Query profile-scoped ingestion provenance without double-counting facts."""
|
|
23
|
+
cursor.execute(
|
|
24
|
+
"""
|
|
25
|
+
SELECT COALESCE(NULLIF(TRIM(source_type), ''), 'unknown') AS source_type,
|
|
26
|
+
COUNT(DISTINCT fact_id) AS count
|
|
27
|
+
FROM provenance
|
|
28
|
+
WHERE profile_id = ?
|
|
29
|
+
GROUP BY COALESCE(NULLIF(TRIM(source_type), ''), 'unknown')
|
|
30
|
+
ORDER BY count DESC, source_type ASC
|
|
31
|
+
""",
|
|
32
|
+
(profile_id,),
|
|
33
|
+
)
|
|
34
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_ingestion_sources_with_status(
|
|
38
|
+
cursor, profile_id: str,
|
|
39
|
+
) -> tuple[list[dict], dict]:
|
|
40
|
+
"""Return provenance plus a truthful availability state.
|
|
41
|
+
|
|
42
|
+
Older databases may not have the provenance table. In that case the
|
|
43
|
+
dashboard must report an empty/unknown state rather than inventing a split.
|
|
44
|
+
Lock or I/O failures are distinct from a legitimate legacy-empty state.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
return _query_ingestion_sources(cursor, profile_id), {
|
|
48
|
+
"available": True,
|
|
49
|
+
"state": "recorded",
|
|
50
|
+
"source": "memory.db:provenance",
|
|
51
|
+
}
|
|
52
|
+
except sqlite3.OperationalError as exc:
|
|
53
|
+
if "no such table" in str(exc).lower():
|
|
54
|
+
return [], {
|
|
55
|
+
"available": True,
|
|
56
|
+
"state": "legacy_not_recorded",
|
|
57
|
+
"source": "memory.db:provenance",
|
|
58
|
+
}
|
|
59
|
+
logger.warning("provenance metrics temporarily unavailable: %s", exc)
|
|
60
|
+
return [], {
|
|
61
|
+
"available": False,
|
|
62
|
+
"state": "temporarily_unavailable",
|
|
63
|
+
"source": "memory.db:provenance",
|
|
64
|
+
}
|
|
65
|
+
except sqlite3.Error as exc:
|
|
66
|
+
logger.warning("provenance metrics unavailable: %s", exc)
|
|
67
|
+
return [], {
|
|
68
|
+
"available": False,
|
|
69
|
+
"state": "temporarily_unavailable",
|
|
70
|
+
"source": "memory.db:provenance",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _load_ingestion_sources(cursor, profile_id: str) -> list[dict]:
|
|
75
|
+
"""Compatibility helper returning only the provenance rows."""
|
|
76
|
+
return _load_ingestion_sources_with_status(cursor, profile_id)[0]
|
|
77
|
+
|
|
78
|
+
|
|
22
79
|
def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
23
80
|
"""SEC-H-02: log full traceback server-side; return a generic message to the client."""
|
|
24
81
|
logger.exception("stats route error")
|
|
@@ -26,7 +83,7 @@ def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
|
26
83
|
|
|
27
84
|
|
|
28
85
|
@router.get("/api/stats")
|
|
29
|
-
|
|
86
|
+
def get_stats():
|
|
30
87
|
"""Get comprehensive system statistics."""
|
|
31
88
|
try:
|
|
32
89
|
conn = get_db_connection()
|
|
@@ -205,6 +262,9 @@ async def get_stats():
|
|
|
205
262
|
importance_dist = cursor.fetchall()
|
|
206
263
|
|
|
207
264
|
db_size = DB_PATH.stat().st_size if DB_PATH.exists() else 0
|
|
265
|
+
ingestion_sources, ingestion_sources_status = (
|
|
266
|
+
_load_ingestion_sources_with_status(cursor, active_profile)
|
|
267
|
+
)
|
|
208
268
|
|
|
209
269
|
if total_graph_nodes > 1:
|
|
210
270
|
max_edges = (total_graph_nodes * (total_graph_nodes - 1)) / 2
|
|
@@ -241,6 +301,8 @@ async def get_stats():
|
|
|
241
301
|
if total_graph_nodes > 0 else 0
|
|
242
302
|
),
|
|
243
303
|
},
|
|
304
|
+
"ingestion_sources": ingestion_sources,
|
|
305
|
+
"ingestion_sources_status": ingestion_sources_status,
|
|
244
306
|
}
|
|
245
307
|
|
|
246
308
|
except Exception:
|
|
@@ -248,11 +310,12 @@ async def get_stats():
|
|
|
248
310
|
|
|
249
311
|
|
|
250
312
|
@router.get("/api/timeline")
|
|
251
|
-
|
|
313
|
+
def get_timeline(
|
|
252
314
|
days: int = Query(30, ge=1, le=365),
|
|
253
315
|
group_by: str = Query("day", pattern="^(day|week|month)$"),
|
|
316
|
+
include_categories: bool = Query(True),
|
|
254
317
|
):
|
|
255
|
-
"""Get temporal
|
|
318
|
+
"""Get temporal memory creation; dashboard callers may skip category scans."""
|
|
256
319
|
try:
|
|
257
320
|
conn = get_db_connection()
|
|
258
321
|
conn.row_factory = dict_factory
|
|
@@ -286,22 +349,29 @@ async def get_timeline(
|
|
|
286
349
|
""", (days, active_profile))
|
|
287
350
|
timeline = cursor.fetchall()
|
|
288
351
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
352
|
+
if include_categories:
|
|
353
|
+
cursor.execute(f"""
|
|
354
|
+
SELECT {date_group} as period, {cat_col} as category, COUNT(*) as count
|
|
355
|
+
FROM {table}
|
|
356
|
+
WHERE created_at >= datetime('now', '-' || ? || ' days')
|
|
357
|
+
AND {cat_col} IS NOT NULL AND {profile_col} = ?
|
|
358
|
+
GROUP BY {date_group}, {cat_col} ORDER BY period DESC, count DESC
|
|
359
|
+
""", (days, active_profile))
|
|
360
|
+
category_trend = cursor.fetchall()
|
|
361
|
+
|
|
362
|
+
cursor.execute(f"""
|
|
363
|
+
SELECT COUNT(*) as total_memories,
|
|
364
|
+
COUNT(DISTINCT {cat_col}) as categories_used
|
|
365
|
+
FROM {table}
|
|
366
|
+
WHERE created_at >= datetime('now', '-' || ? || ' days') AND {profile_col} = ?
|
|
367
|
+
""", (days, active_profile))
|
|
368
|
+
period_stats = cursor.fetchone()
|
|
369
|
+
else:
|
|
370
|
+
category_trend = []
|
|
371
|
+
period_stats = {
|
|
372
|
+
"total_memories": sum(int(row["count"] or 0) for row in timeline),
|
|
373
|
+
"categories_used": None,
|
|
374
|
+
}
|
|
305
375
|
|
|
306
376
|
conn.close()
|
|
307
377
|
|
|
@@ -313,135 +383,3 @@ async def get_timeline(
|
|
|
313
383
|
|
|
314
384
|
except Exception:
|
|
315
385
|
raise _internal_error("Timeline error")
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
@router.get("/api/patterns")
|
|
319
|
-
async def get_patterns():
|
|
320
|
-
"""Get learned patterns."""
|
|
321
|
-
try:
|
|
322
|
-
conn = get_db_connection()
|
|
323
|
-
conn.row_factory = dict_factory
|
|
324
|
-
cursor = conn.cursor()
|
|
325
|
-
active_profile = get_active_profile()
|
|
326
|
-
|
|
327
|
-
# Check for V3 learning tables or V2 identity_patterns
|
|
328
|
-
patterns = []
|
|
329
|
-
table_name = None
|
|
330
|
-
for candidate in ('learned_patterns', 'identity_patterns'):
|
|
331
|
-
cursor.execute(
|
|
332
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
|
333
|
-
(candidate,),
|
|
334
|
-
)
|
|
335
|
-
if cursor.fetchone():
|
|
336
|
-
table_name = candidate
|
|
337
|
-
break
|
|
338
|
-
|
|
339
|
-
if not table_name:
|
|
340
|
-
conn.close()
|
|
341
|
-
# Fall through to V3.1 behavioral pattern store
|
|
342
|
-
try:
|
|
343
|
-
from superlocalmemory.learning.behavioral import BehavioralPatternStore
|
|
344
|
-
store = BehavioralPatternStore(str(MEMORY_DIR / "learning.db"))
|
|
345
|
-
raw = store.get_patterns(profile_id=active_profile)
|
|
346
|
-
# v3.4.7: Map all pattern types to frontend categories
|
|
347
|
-
type_map = {
|
|
348
|
-
"tech_preference": "preference",
|
|
349
|
-
"interest": "preference",
|
|
350
|
-
"entity_preferences": "preference",
|
|
351
|
-
"style": "style",
|
|
352
|
-
"fact_type_distribution": "style",
|
|
353
|
-
"knowledge_structure": "style",
|
|
354
|
-
"terminology": "terminology",
|
|
355
|
-
"temporal": "workflow",
|
|
356
|
-
"session_activity": "workflow",
|
|
357
|
-
"workflow": "workflow",
|
|
358
|
-
"co_retrieval_clusters": "workflow",
|
|
359
|
-
"channel_performance": "performance",
|
|
360
|
-
}
|
|
361
|
-
grouped = defaultdict(list)
|
|
362
|
-
for p in raw:
|
|
363
|
-
meta = p.get("metadata", {})
|
|
364
|
-
data = meta # metadata IS the data dict
|
|
365
|
-
frontend_key = type_map.get(p.get("pattern_type", ""), "preference")
|
|
366
|
-
# Extract human-readable value from data fields
|
|
367
|
-
readable_value = (
|
|
368
|
-
data.get("value")
|
|
369
|
-
or data.get("topic")
|
|
370
|
-
or data.get("pattern_key", "")
|
|
371
|
-
or p.get("pattern_key", "")
|
|
372
|
-
)
|
|
373
|
-
readable_key = (
|
|
374
|
-
data.get("pattern_key")
|
|
375
|
-
or data.get("key")
|
|
376
|
-
or p.get("pattern_key", "")
|
|
377
|
-
)
|
|
378
|
-
grouped[frontend_key].append({
|
|
379
|
-
"pattern_type": p.get("pattern_type", ""),
|
|
380
|
-
"key": readable_key,
|
|
381
|
-
"value": readable_value,
|
|
382
|
-
"confidence": p.get("confidence", 0),
|
|
383
|
-
"evidence_count": data.get("evidence", p.get("evidence_count", 0)),
|
|
384
|
-
})
|
|
385
|
-
all_patterns = [p for ps in grouped.values() for p in ps]
|
|
386
|
-
confs = [p["confidence"] for p in all_patterns if p.get("confidence")]
|
|
387
|
-
return {
|
|
388
|
-
"patterns": dict(grouped),
|
|
389
|
-
"total_patterns": len(all_patterns),
|
|
390
|
-
"pattern_types": list(grouped.keys()),
|
|
391
|
-
"confidence_stats": {
|
|
392
|
-
"avg": sum(confs) / len(confs) if confs else 0,
|
|
393
|
-
"min": min(confs) if confs else 0,
|
|
394
|
-
"max": max(confs) if confs else 0,
|
|
395
|
-
},
|
|
396
|
-
}
|
|
397
|
-
except Exception:
|
|
398
|
-
return {
|
|
399
|
-
"patterns": {}, "total_patterns": 0, "pattern_types": [],
|
|
400
|
-
"message": "Pattern learning not initialized.",
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if table_name == 'identity_patterns':
|
|
404
|
-
cursor.execute("""
|
|
405
|
-
SELECT pattern_type, key, value, confidence, evidence_count,
|
|
406
|
-
updated_at as last_updated
|
|
407
|
-
FROM identity_patterns WHERE profile = ?
|
|
408
|
-
ORDER BY confidence DESC, evidence_count DESC
|
|
409
|
-
""", (active_profile,))
|
|
410
|
-
else:
|
|
411
|
-
cursor.execute("""
|
|
412
|
-
SELECT pattern_type, key, value, confidence, evidence_count,
|
|
413
|
-
last_updated
|
|
414
|
-
FROM learned_patterns WHERE is_active = 1
|
|
415
|
-
ORDER BY confidence DESC, evidence_count DESC
|
|
416
|
-
""")
|
|
417
|
-
|
|
418
|
-
patterns = cursor.fetchall()
|
|
419
|
-
|
|
420
|
-
for pattern in patterns:
|
|
421
|
-
if pattern.get('value'):
|
|
422
|
-
try:
|
|
423
|
-
pattern['value'] = json.loads(pattern['value'])
|
|
424
|
-
except Exception:
|
|
425
|
-
pass
|
|
426
|
-
|
|
427
|
-
grouped = defaultdict(list)
|
|
428
|
-
for pattern in patterns:
|
|
429
|
-
grouped[pattern['pattern_type']].append(pattern)
|
|
430
|
-
|
|
431
|
-
confidences = [p['confidence'] for p in patterns if p.get('confidence')]
|
|
432
|
-
confidence_stats = {
|
|
433
|
-
"avg": sum(confidences) / len(confidences) if confidences else 0,
|
|
434
|
-
"min": min(confidences) if confidences else 0,
|
|
435
|
-
"max": max(confidences) if confidences else 0,
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
conn.close()
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
"patterns": dict(grouped), "total_patterns": len(patterns),
|
|
442
|
-
"pattern_types": list(grouped.keys()), "confidence_stats": confidence_stats,
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
except Exception:
|
|
446
|
-
logger.exception("patterns route error")
|
|
447
|
-
return {"patterns": {}, "total_patterns": 0, "error": "Internal server error"}
|
|
@@ -31,21 +31,11 @@ logger = logging.getLogger(__name__)
|
|
|
31
31
|
router = APIRouter(tags=["internal"])
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
_ALLOWED_ORIGIN_PREFIXES = (
|
|
35
|
-
"http://127.0.0.1",
|
|
36
|
-
"https://127.0.0.1",
|
|
37
|
-
"http://localhost",
|
|
38
|
-
"https://localhost",
|
|
39
|
-
"http://[::1]",
|
|
40
|
-
"https://[::1]",
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
34
|
def _origin_is_loopback(origin: str) -> bool:
|
|
45
35
|
"""Return True iff ``origin`` is absent or a loopback URL."""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
36
|
+
from superlocalmemory.server.origin import origin_is_loopback
|
|
37
|
+
|
|
38
|
+
return origin_is_loopback(origin)
|
|
49
39
|
|
|
50
40
|
|
|
51
41
|
@router.get("/internal/token")
|