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
|
@@ -9,27 +9,81 @@ Routes: /api/learning/status, /api/feedback, /api/feedback/dwell,
|
|
|
9
9
|
/api/learning/retrain
|
|
10
10
|
Uses V3 learning modules: FeedbackCollector, EngagementTracker, AdaptiveLearner.
|
|
11
11
|
"""
|
|
12
|
-
import shutil
|
|
13
12
|
import logging
|
|
13
|
+
import shutil
|
|
14
|
+
import sqlite3
|
|
15
|
+
from contextlib import closing
|
|
14
16
|
from datetime import datetime, timezone
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
|
|
17
|
-
from fastapi import APIRouter
|
|
19
|
+
from fastapi import APIRouter, Request
|
|
18
20
|
from fastapi.concurrency import run_in_threadpool
|
|
19
21
|
|
|
20
|
-
from .helpers import
|
|
22
|
+
from .helpers import MEMORY_DIR, get_active_profile
|
|
23
|
+
from .learning_telemetry import ReadOnlyRankerStore
|
|
24
|
+
from .learning_telemetry import (
|
|
25
|
+
load_model_state as _load_model_state,
|
|
26
|
+
)
|
|
27
|
+
from .learning_telemetry import (
|
|
28
|
+
load_source_quality_state as _load_source_quality_state,
|
|
29
|
+
)
|
|
30
|
+
from .learning_telemetry import (
|
|
31
|
+
sqlite_status as _sqlite_status,
|
|
32
|
+
)
|
|
21
33
|
|
|
22
34
|
logger = logging.getLogger("superlocalmemory.routes.learning")
|
|
23
35
|
router = APIRouter()
|
|
24
36
|
|
|
25
37
|
LEARNING_DB = MEMORY_DIR / "learning.db"
|
|
26
38
|
|
|
39
|
+
# F10 fix: track which profiles have completed their initial cache-bypass check
|
|
40
|
+
# per daemon start. First call uses use_cache=False (tamper detection);
|
|
41
|
+
# subsequent calls use use_cache=True so tab navigation does not re-deserialize
|
|
42
|
+
# a LightGBM model on every dashboard request.
|
|
43
|
+
_ranker_initial_check_done: set[str] = set()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _require_write(request: Request) -> None:
|
|
47
|
+
from superlocalmemory.access.rbac import Permission
|
|
48
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
49
|
+
|
|
50
|
+
require_permission(request, Permission.WRITE, profile=get_active_profile())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _require_delete(request: Request) -> None:
|
|
54
|
+
from superlocalmemory.access.rbac import Permission
|
|
55
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
56
|
+
|
|
57
|
+
require_permission(request, Permission.DELETE, profile=get_active_profile())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _require_manage(request: Request) -> None:
|
|
61
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
62
|
+
|
|
63
|
+
require_manage(request, profile=get_active_profile())
|
|
64
|
+
|
|
27
65
|
|
|
28
66
|
# ---------------------------------------------------------------------------
|
|
29
67
|
# LLD-02 §4.10 — Dashboard phase truth
|
|
30
68
|
# ---------------------------------------------------------------------------
|
|
31
69
|
|
|
32
70
|
|
|
71
|
+
def _phase_payload(
|
|
72
|
+
phase: int,
|
|
73
|
+
key: str,
|
|
74
|
+
label: str,
|
|
75
|
+
model_active: bool,
|
|
76
|
+
signals: int,
|
|
77
|
+
gates: dict,
|
|
78
|
+
status: str,
|
|
79
|
+
) -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"phase": phase, "key": key, "label": label,
|
|
82
|
+
"model_active": model_active, "signals": signals,
|
|
83
|
+
"gates": gates, "status": status,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
33
87
|
def _compute_ranker_phase(
|
|
34
88
|
profile_id: str,
|
|
35
89
|
*,
|
|
@@ -41,60 +95,79 @@ def _compute_ranker_phase(
|
|
|
41
95
|
SHA-256 verification on the model_cache load. Tampered bytes fall
|
|
42
96
|
back to phase 2.
|
|
43
97
|
"""
|
|
44
|
-
from superlocalmemory.learning.
|
|
45
|
-
from superlocalmemory.learning.
|
|
98
|
+
from superlocalmemory.learning.model_cache import load_active
|
|
99
|
+
from superlocalmemory.learning.ranker import (
|
|
100
|
+
PHASE_2_THRESHOLD,
|
|
101
|
+
PHASE_3_THRESHOLD,
|
|
102
|
+
)
|
|
46
103
|
|
|
47
104
|
db_path = Path(learning_db_path) if learning_db_path else LEARNING_DB
|
|
105
|
+
gates = {
|
|
106
|
+
"rule_based_min_signals": PHASE_2_THRESHOLD,
|
|
107
|
+
"ml_model_min_signals": PHASE_3_THRESHOLD,
|
|
108
|
+
"ml_model_requires_verified_active_model": True,
|
|
109
|
+
}
|
|
48
110
|
if not db_path.exists():
|
|
49
|
-
return
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"signals": 0,
|
|
54
|
-
}
|
|
111
|
+
return _phase_payload(
|
|
112
|
+
1, "baseline", "Cold start (cross-encoder only)", False, 0,
|
|
113
|
+
gates, "missing_database",
|
|
114
|
+
)
|
|
55
115
|
|
|
56
|
-
|
|
116
|
+
try:
|
|
117
|
+
db = ReadOnlyRankerStore(db_path)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.warning("ranker database unavailable: %s", exc)
|
|
120
|
+
status = (
|
|
121
|
+
_sqlite_status(exc)
|
|
122
|
+
if isinstance(exc, sqlite3.Error) else "query_error"
|
|
123
|
+
)
|
|
124
|
+
return _phase_payload(
|
|
125
|
+
1, "baseline", "Cold start (cross-encoder only)", False, 0,
|
|
126
|
+
gates, status,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
signal_status = "available"
|
|
57
130
|
try:
|
|
58
131
|
signals = db.count_signals(profile_id)
|
|
59
132
|
except Exception as exc:
|
|
60
133
|
logger.warning("count_signals failed: %s", exc)
|
|
61
134
|
signals = 0
|
|
135
|
+
signal_status = (
|
|
136
|
+
"missing_table"
|
|
137
|
+
if isinstance(exc, sqlite3.OperationalError)
|
|
138
|
+
and "no such table" in str(exc).lower()
|
|
139
|
+
else (
|
|
140
|
+
_sqlite_status(exc)
|
|
141
|
+
if isinstance(exc, sqlite3.Error) else "query_error"
|
|
142
|
+
)
|
|
143
|
+
)
|
|
62
144
|
|
|
63
|
-
# Force a cache-bypass load — the dashboard read is rare and we want
|
|
64
|
-
# tamper detection to surface immediately.
|
|
65
|
-
invalidate(profile_id)
|
|
66
145
|
try:
|
|
67
|
-
|
|
146
|
+
# F10 fix: first call per daemon start per profile must bypass the cache
|
|
147
|
+
# so a tampered model file is detected before being served. Subsequent
|
|
148
|
+
# calls reuse the cache so tab navigation does not re-deserialize a
|
|
149
|
+
# LightGBM model on every dashboard request.
|
|
150
|
+
bypass_cache = profile_id not in _ranker_initial_check_done
|
|
151
|
+
if bypass_cache:
|
|
152
|
+
_ranker_initial_check_done.add(profile_id)
|
|
153
|
+
model = load_active(db, profile_id, use_cache=not bypass_cache)
|
|
68
154
|
except Exception as exc:
|
|
69
155
|
logger.warning("load_active failed: %s", exc)
|
|
70
156
|
model = None
|
|
71
157
|
|
|
72
158
|
active = model is not None
|
|
73
159
|
|
|
74
|
-
if active and signals >=
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if signals >= 50:
|
|
82
|
-
return {
|
|
83
|
-
"phase": 2,
|
|
84
|
-
"label": "Contextual bandit",
|
|
85
|
-
"model_active": False,
|
|
86
|
-
"signals": signals,
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
"phase": 1,
|
|
90
|
-
"label": "Cold start (cross-encoder only)",
|
|
91
|
-
"model_active": False,
|
|
92
|
-
"signals": signals,
|
|
93
|
-
}
|
|
160
|
+
if active and signals >= PHASE_3_THRESHOLD:
|
|
161
|
+
phase = (3, "ml_model", "LightGBM ranker active", True)
|
|
162
|
+
elif signals >= PHASE_2_THRESHOLD:
|
|
163
|
+
phase = (2, "rule_based", "Contextual bandit", False)
|
|
164
|
+
else:
|
|
165
|
+
phase = (1, "baseline", "Cold start (cross-encoder only)", False)
|
|
166
|
+
return _phase_payload(*phase, signals, gates, signal_status)
|
|
94
167
|
|
|
95
168
|
|
|
96
169
|
@router.get("/api/learning/ranker_phase")
|
|
97
|
-
|
|
170
|
+
def ranker_phase():
|
|
98
171
|
"""Dashboard endpoint — LLD-02 §4.10 phase truth."""
|
|
99
172
|
try:
|
|
100
173
|
profile = get_active_profile()
|
|
@@ -106,14 +179,14 @@ async def ranker_phase():
|
|
|
106
179
|
LEARNING_AVAILABLE = False
|
|
107
180
|
BEHAVIORAL_AVAILABLE = False
|
|
108
181
|
try:
|
|
109
|
-
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
110
182
|
from superlocalmemory.learning.engagement import EngagementTracker
|
|
111
|
-
from superlocalmemory.learning.
|
|
183
|
+
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
184
|
+
from superlocalmemory.learning.ranker import AdaptiveRanker # noqa: F401
|
|
112
185
|
LEARNING_AVAILABLE = True
|
|
113
186
|
except ImportError as e:
|
|
114
187
|
logger.warning("V3 learning primary import failed: %s", e)
|
|
115
188
|
try:
|
|
116
|
-
from superlocalmemory.learning.adaptive import AdaptiveLearner
|
|
189
|
+
from superlocalmemory.learning.adaptive import AdaptiveLearner # noqa: F401
|
|
117
190
|
LEARNING_AVAILABLE = True
|
|
118
191
|
except ImportError as e2:
|
|
119
192
|
logger.warning("V3 learning fallback import failed: %s", e2)
|
|
@@ -149,30 +222,73 @@ def _get_engagement() -> "EngagementTracker | None":
|
|
|
149
222
|
return _engagement
|
|
150
223
|
|
|
151
224
|
|
|
225
|
+
def _source_quality_repair_telemetry(request: Request) -> dict:
|
|
226
|
+
status = getattr(
|
|
227
|
+
request.app.state, "source_quality_repair_status", None,
|
|
228
|
+
)
|
|
229
|
+
if not isinstance(status, dict):
|
|
230
|
+
return {
|
|
231
|
+
"state": "not_scheduled",
|
|
232
|
+
"source": "daemon_app_state",
|
|
233
|
+
"last_error": None,
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
"state": str(status.get("state") or "unknown"),
|
|
237
|
+
"source": str(status.get("source") or "daemon_app_state"),
|
|
238
|
+
"batch_size": int(status.get("batch_size") or 0),
|
|
239
|
+
"profiles_total": len(status.get("profiles") or []),
|
|
240
|
+
"profiles_complete": len(status.get("completed_profiles") or []),
|
|
241
|
+
"batches_completed": int(status.get("batches_completed") or 0),
|
|
242
|
+
"scanned": int(status.get("scanned") or 0),
|
|
243
|
+
"observations": int(status.get("observations") or 0),
|
|
244
|
+
"last_error": status.get("last_error"),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
152
248
|
@router.get("/api/learning/status")
|
|
153
|
-
|
|
249
|
+
def learning_status(request: Request):
|
|
154
250
|
"""Get comprehensive learning system status for dashboard."""
|
|
251
|
+
repair_telemetry = _source_quality_repair_telemetry(request)
|
|
155
252
|
if not LEARNING_AVAILABLE:
|
|
156
253
|
return {
|
|
157
254
|
"available": False, "ranking_phase": None,
|
|
255
|
+
"ranker_phase": None, "ranking_phase_gates": {},
|
|
158
256
|
"stats": None, "tech_preferences": [], "workflow_patterns": [],
|
|
159
257
|
"source_scores": {}, "engagement": None,
|
|
258
|
+
"telemetry_status": {
|
|
259
|
+
"source_quality_repair": repair_telemetry["state"],
|
|
260
|
+
},
|
|
261
|
+
"source_quality_repair": repair_telemetry,
|
|
160
262
|
"message": "Learning features not installed.",
|
|
161
263
|
}
|
|
162
264
|
|
|
163
|
-
result = {
|
|
265
|
+
result = {
|
|
266
|
+
"available": True,
|
|
267
|
+
"telemetry_status": {
|
|
268
|
+
"source_quality_repair": repair_telemetry["state"],
|
|
269
|
+
},
|
|
270
|
+
"source_quality_repair": repair_telemetry,
|
|
271
|
+
}
|
|
164
272
|
|
|
165
273
|
try:
|
|
166
274
|
active_profile = get_active_profile()
|
|
167
275
|
result["active_profile"] = active_profile
|
|
276
|
+
ranker_state = _compute_ranker_phase(active_profile)
|
|
277
|
+
result["ranker_phase"] = ranker_state
|
|
278
|
+
result["ranking_phase"] = ranker_state["key"]
|
|
279
|
+
result["ranking_phase_gates"] = ranker_state["gates"]
|
|
280
|
+
result["telemetry_status"]["ranker"] = ranker_state.get(
|
|
281
|
+
"status", "available",
|
|
282
|
+
)
|
|
168
283
|
|
|
169
284
|
# Real signal count from V3.1 learning_feedback table
|
|
170
285
|
signal_count = 0
|
|
171
286
|
unique_queries = 0
|
|
172
287
|
try:
|
|
173
|
-
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
174
|
-
from pathlib import Path
|
|
175
288
|
import sqlite3 as _sqlite3
|
|
289
|
+
|
|
290
|
+
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
291
|
+
|
|
176
292
|
learning_db = LEARNING_DB
|
|
177
293
|
if learning_db.exists():
|
|
178
294
|
collector = FeedbackCollector(learning_db)
|
|
@@ -194,25 +310,25 @@ async def learning_status():
|
|
|
194
310
|
except Exception:
|
|
195
311
|
pass
|
|
196
312
|
|
|
197
|
-
# Ranking phase based on real signal count
|
|
198
|
-
if signal_count >= 200:
|
|
199
|
-
result["ranking_phase"] = "ml_model"
|
|
200
|
-
elif signal_count >= 20:
|
|
201
|
-
result["ranking_phase"] = "rule_based"
|
|
202
|
-
else:
|
|
203
|
-
result["ranking_phase"] = "baseline"
|
|
204
|
-
|
|
205
313
|
# Feedback stats — merge old system + new V3.1 signals
|
|
206
|
-
stats_dict = {
|
|
314
|
+
stats_dict = {
|
|
315
|
+
"feedback_count": signal_count,
|
|
316
|
+
"unique_queries": unique_queries,
|
|
317
|
+
"ranker_signal_count": ranker_state["signals"],
|
|
318
|
+
"active_profile": active_profile,
|
|
319
|
+
}
|
|
207
320
|
feedback = _get_feedback()
|
|
208
321
|
if feedback:
|
|
209
322
|
try:
|
|
210
323
|
old_stats = feedback.get_feedback_summary(active_profile)
|
|
211
324
|
if isinstance(old_stats, dict):
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
325
|
+
stats_dict = {
|
|
326
|
+
**old_stats,
|
|
327
|
+
"feedback_count": signal_count,
|
|
328
|
+
"unique_queries": unique_queries,
|
|
329
|
+
"ranker_signal_count": ranker_state["signals"],
|
|
330
|
+
"active_profile": active_profile,
|
|
331
|
+
}
|
|
216
332
|
except Exception as exc:
|
|
217
333
|
logger.debug("feedback summary: %s", exc)
|
|
218
334
|
|
|
@@ -252,21 +368,37 @@ async def learning_status():
|
|
|
252
368
|
# Tech preferences + workflow patterns from V3.1 behavioral store
|
|
253
369
|
try:
|
|
254
370
|
from superlocalmemory.learning.behavioral import BehavioralPatternStore
|
|
255
|
-
|
|
371
|
+
|
|
256
372
|
learning_db = LEARNING_DB
|
|
257
373
|
if learning_db.exists():
|
|
258
374
|
store = BehavioralPatternStore(str(learning_db))
|
|
259
375
|
all_patterns = store.get_patterns(profile_id=active_profile)
|
|
260
376
|
tech = [
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
377
|
+
{
|
|
378
|
+
"type": "tech_preference",
|
|
379
|
+
"key": p.get("pattern_key", ""),
|
|
380
|
+
"value": p.get("metadata", {}).get(
|
|
381
|
+
"value", p.get("pattern_key", ""),
|
|
382
|
+
),
|
|
383
|
+
"confidence": p.get("confidence", 0),
|
|
384
|
+
"evidence_count": p.get("evidence_count", 0),
|
|
385
|
+
# Compatibility alias for pre-3.8 dashboard clients.
|
|
386
|
+
"evidence": p.get("evidence_count", 0),
|
|
387
|
+
}
|
|
388
|
+
for p in all_patterns
|
|
389
|
+
if p.get("pattern_type") == "tech_preference"
|
|
264
390
|
]
|
|
391
|
+
workflow_types = {
|
|
392
|
+
"temporal", "workflow", "session_activity",
|
|
393
|
+
"co_retrieval_clusters",
|
|
394
|
+
}
|
|
265
395
|
workflows = [
|
|
266
396
|
{"type": p.get("pattern_type"), "key": p.get("pattern_key", ""),
|
|
267
397
|
"value": p.get("metadata", {}).get("value", ""),
|
|
268
|
-
"confidence": p.get("confidence", 0)
|
|
269
|
-
|
|
398
|
+
"confidence": p.get("confidence", 0),
|
|
399
|
+
"evidence_count": p.get("evidence_count", 0)}
|
|
400
|
+
for p in all_patterns
|
|
401
|
+
if p.get("pattern_type") in workflow_types
|
|
270
402
|
]
|
|
271
403
|
result["tech_preferences"] = tech
|
|
272
404
|
result["workflow_patterns"] = workflows
|
|
@@ -276,10 +408,6 @@ async def learning_status():
|
|
|
276
408
|
db_size = os.path.getsize(str(learning_db)) // 1024 if learning_db.exists() else 0
|
|
277
409
|
stats_dict["db_size_kb"] = db_size
|
|
278
410
|
stats_dict["transferable_patterns"] = len(all_patterns)
|
|
279
|
-
stats_dict["models_trained"] = 1 if signal_count >= 200 else 0
|
|
280
|
-
stats_dict["tracked_sources"] = len(set(
|
|
281
|
-
p.get("pattern_type") for p in all_patterns
|
|
282
|
-
))
|
|
283
411
|
else:
|
|
284
412
|
result["tech_preferences"] = []
|
|
285
413
|
result["workflow_patterns"] = []
|
|
@@ -288,9 +416,22 @@ async def learning_status():
|
|
|
288
416
|
result["tech_preferences"] = []
|
|
289
417
|
result["workflow_patterns"] = []
|
|
290
418
|
result["pattern_error"] = str(exc)
|
|
291
|
-
|
|
419
|
+
source_state = _load_source_quality_state(
|
|
420
|
+
active_profile, LEARNING_DB,
|
|
421
|
+
)
|
|
422
|
+
model_state = _load_model_state(active_profile, LEARNING_DB)
|
|
423
|
+
result["source_scores"] = source_state["scores"]
|
|
424
|
+
result["source_scores_source"] = "learning.db:source_quality"
|
|
425
|
+
result["source_scores_are_posterior"] = True
|
|
426
|
+
result["telemetry_status"]["source_quality"] = source_state["status"]
|
|
427
|
+
result["telemetry_status"]["model_state"] = model_state["status"]
|
|
428
|
+
stats_dict["tracked_sources"] = source_state["tracked_sources"]
|
|
429
|
+
stats_dict["models_trained"] = model_state["models_trained"]
|
|
430
|
+
stats_dict["models_active_verified"] = int(
|
|
431
|
+
bool(ranker_state["model_active"]),
|
|
432
|
+
)
|
|
292
433
|
|
|
293
|
-
except Exception
|
|
434
|
+
except Exception:
|
|
294
435
|
logger.exception("Error getting learning status")
|
|
295
436
|
result["error"] = "Internal server error"
|
|
296
437
|
|
|
@@ -302,8 +443,9 @@ async def learning_status():
|
|
|
302
443
|
# ============================================================================
|
|
303
444
|
|
|
304
445
|
@router.post("/api/feedback")
|
|
305
|
-
|
|
446
|
+
def record_feedback(request: Request, data: dict):
|
|
306
447
|
"""Record explicit feedback from dashboard (thumbs up/down, pin)."""
|
|
448
|
+
_require_write(request)
|
|
307
449
|
if not LEARNING_AVAILABLE:
|
|
308
450
|
return {"success": False, "error": "Learning system not available"}
|
|
309
451
|
|
|
@@ -333,14 +475,15 @@ async def record_feedback(data: dict):
|
|
|
333
475
|
"message": f"Feedback recorded: {feedback_type} for memory #{memory_id}",
|
|
334
476
|
"feedback_id": row_id,
|
|
335
477
|
}
|
|
336
|
-
except Exception
|
|
478
|
+
except Exception:
|
|
337
479
|
logger.exception("Error recording feedback")
|
|
338
480
|
return {"success": False, "error": "Internal server error"}
|
|
339
481
|
|
|
340
482
|
|
|
341
483
|
@router.post("/api/feedback/dwell")
|
|
342
|
-
|
|
484
|
+
def record_dwell(request: Request, data: dict):
|
|
343
485
|
"""Record dwell time feedback from dashboard modal."""
|
|
486
|
+
_require_write(request)
|
|
344
487
|
if not LEARNING_AVAILABLE:
|
|
345
488
|
return {"success": False, "error": "Learning system not available"}
|
|
346
489
|
|
|
@@ -378,13 +521,13 @@ async def record_dwell(data: dict):
|
|
|
378
521
|
"message": f"Dwell feedback recorded: {feedback_type} ({dwell_seconds:.1f}s)",
|
|
379
522
|
"feedback_id": row_id,
|
|
380
523
|
}
|
|
381
|
-
except Exception
|
|
524
|
+
except Exception:
|
|
382
525
|
logger.exception("Error recording dwell")
|
|
383
526
|
return {"success": False, "error": "Internal server error"}
|
|
384
527
|
|
|
385
528
|
|
|
386
529
|
@router.get("/api/feedback/stats")
|
|
387
|
-
|
|
530
|
+
def feedback_stats():
|
|
388
531
|
"""Get feedback signal statistics for dashboard progress bar."""
|
|
389
532
|
if not LEARNING_AVAILABLE:
|
|
390
533
|
return {
|
|
@@ -413,9 +556,12 @@ async def feedback_stats():
|
|
|
413
556
|
"progress": round(progress, 1), "target": target,
|
|
414
557
|
"by_channel": by_channel, "by_type": by_type, "available": True,
|
|
415
558
|
}
|
|
416
|
-
except Exception
|
|
559
|
+
except Exception:
|
|
417
560
|
logger.exception("Error getting feedback stats")
|
|
418
|
-
return {
|
|
561
|
+
return {
|
|
562
|
+
"total_signals": 0, "ranking_phase": "baseline",
|
|
563
|
+
"progress": 0, "error": "Internal server error",
|
|
564
|
+
}
|
|
419
565
|
|
|
420
566
|
|
|
421
567
|
# ============================================================================
|
|
@@ -424,7 +570,7 @@ async def feedback_stats():
|
|
|
424
570
|
|
|
425
571
|
|
|
426
572
|
@router.delete("/api/patterns/delete")
|
|
427
|
-
|
|
573
|
+
def delete_pattern(request: Request, data: dict) -> dict:
|
|
428
574
|
"""S9-DASH-04: delete a single auto-detected pattern by key.
|
|
429
575
|
|
|
430
576
|
Body: ``{pattern_type: str, pattern_key: str}``
|
|
@@ -432,6 +578,7 @@ async def delete_pattern(data: dict) -> dict:
|
|
|
432
578
|
Returns ``{success: bool, deleted: int}``. The pattern is scoped
|
|
433
579
|
to the active profile so cross-profile deletion is impossible.
|
|
434
580
|
"""
|
|
581
|
+
_require_delete(request)
|
|
435
582
|
if not BEHAVIORAL_AVAILABLE:
|
|
436
583
|
return {"success": False, "error": "Behavioral engine not available"}
|
|
437
584
|
ptype = (data or {}).get("pattern_type", "")
|
|
@@ -459,7 +606,7 @@ async def delete_pattern(data: dict) -> dict:
|
|
|
459
606
|
|
|
460
607
|
|
|
461
608
|
@router.get("/api/patterns")
|
|
462
|
-
|
|
609
|
+
def get_patterns():
|
|
463
610
|
"""Get learned behavioral patterns for the Patterns dashboard tab.
|
|
464
611
|
|
|
465
612
|
v3.4.1: This endpoint was MISSING — patterns.js calls /api/patterns
|
|
@@ -510,43 +657,74 @@ async def get_patterns():
|
|
|
510
657
|
result["pattern_error"] = str(exc)
|
|
511
658
|
|
|
512
659
|
# Learning signal stats (feedback count, co-retrieval, channel credits)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
660
|
+
if LEARNING_AVAILABLE:
|
|
661
|
+
try:
|
|
662
|
+
from superlocalmemory.learning.signals import LearningSignals
|
|
663
|
+
signals = LearningSignals(str(LEARNING_DB))
|
|
664
|
+
result["signal_stats"] = signals.get_signal_stats(active_profile)
|
|
665
|
+
except Exception as exc:
|
|
666
|
+
logger.debug("Signal stats unavailable: %s", exc)
|
|
519
667
|
|
|
520
668
|
# Graph intelligence contribution to learning (v3.4.1)
|
|
521
669
|
try:
|
|
522
670
|
import sqlite3 as _sqlite3
|
|
671
|
+
|
|
523
672
|
from superlocalmemory.server.routes.helpers import DB_PATH
|
|
524
673
|
if DB_PATH.exists():
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
"
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
674
|
+
with closing(_sqlite3.connect(str(DB_PATH))) as conn:
|
|
675
|
+
conn.row_factory = _sqlite3.Row
|
|
676
|
+
row = conn.execute(
|
|
677
|
+
"SELECT COUNT(*) AS cnt, "
|
|
678
|
+
"COUNT(DISTINCT community_id) AS communities, "
|
|
679
|
+
"ROUND(AVG(pagerank_score), 4) AS avg_pagerank "
|
|
680
|
+
"FROM fact_importance WHERE profile_id = ?",
|
|
681
|
+
(active_profile,),
|
|
682
|
+
).fetchone()
|
|
683
|
+
if row:
|
|
684
|
+
d = dict(row)
|
|
685
|
+
result["graph_intelligence"] = {
|
|
686
|
+
"facts_analyzed": d.get("cnt", 0),
|
|
687
|
+
"communities_detected": d.get("communities", 0),
|
|
688
|
+
"avg_pagerank": float(
|
|
689
|
+
d.get("avg_pagerank", 0) or 0,
|
|
690
|
+
),
|
|
691
|
+
}
|
|
541
692
|
except Exception:
|
|
542
693
|
pass
|
|
543
694
|
|
|
695
|
+
all_patterns = [
|
|
696
|
+
pattern
|
|
697
|
+
for category_patterns in patterns.values()
|
|
698
|
+
for pattern in category_patterns
|
|
699
|
+
]
|
|
700
|
+
confidences = [
|
|
701
|
+
float(pattern["confidence"])
|
|
702
|
+
for pattern in all_patterns
|
|
703
|
+
if pattern.get("confidence") is not None
|
|
704
|
+
]
|
|
705
|
+
result.update({
|
|
706
|
+
"total_patterns": len(all_patterns),
|
|
707
|
+
"pattern_types": [
|
|
708
|
+
category
|
|
709
|
+
for category, category_patterns in patterns.items()
|
|
710
|
+
if category_patterns
|
|
711
|
+
],
|
|
712
|
+
"confidence_stats": {
|
|
713
|
+
"avg": (
|
|
714
|
+
sum(confidences) / len(confidences)
|
|
715
|
+
if confidences else 0.0
|
|
716
|
+
),
|
|
717
|
+
"min": min(confidences) if confidences else 0.0,
|
|
718
|
+
"max": max(confidences) if confidences else 0.0,
|
|
719
|
+
},
|
|
720
|
+
})
|
|
544
721
|
return result
|
|
545
722
|
|
|
546
723
|
|
|
547
724
|
@router.post("/api/learning/backup")
|
|
548
|
-
|
|
725
|
+
def learning_backup(request: Request):
|
|
549
726
|
"""Backup learning.db to a timestamped file."""
|
|
727
|
+
_require_manage(request)
|
|
550
728
|
try:
|
|
551
729
|
if not LEARNING_DB.exists():
|
|
552
730
|
return {"success": False, "error": "No learning.db found"}
|
|
@@ -561,14 +739,15 @@ async def learning_backup():
|
|
|
561
739
|
"path": str(backup_path),
|
|
562
740
|
"message": f"Learning DB backed up to {backup_name}",
|
|
563
741
|
}
|
|
564
|
-
except Exception
|
|
742
|
+
except Exception:
|
|
565
743
|
logger.exception("Error backing up learning DB")
|
|
566
744
|
return {"success": False, "error": "Internal server error"}
|
|
567
745
|
|
|
568
746
|
|
|
569
747
|
@router.post("/api/learning/reset")
|
|
570
|
-
|
|
748
|
+
def learning_reset(request: Request):
|
|
571
749
|
"""Reset all learning data for the active profile. Memories preserved."""
|
|
750
|
+
_require_delete(request)
|
|
572
751
|
if not LEARNING_AVAILABLE:
|
|
573
752
|
return {"success": False, "error": "Learning system not available"}
|
|
574
753
|
try:
|
|
@@ -587,13 +766,14 @@ async def learning_reset():
|
|
|
587
766
|
|
|
588
767
|
|
|
589
768
|
@router.post("/api/learning/retrain")
|
|
590
|
-
async def learning_retrain(data: dict | None = None):
|
|
769
|
+
async def learning_retrain(request: Request, data: dict | None = None):
|
|
591
770
|
"""Force a retrain of the LightGBM ranker.
|
|
592
771
|
|
|
593
772
|
Body (optional, JSON):
|
|
594
773
|
``{"include_synthetic": bool}`` — when True, migrated legacy rows
|
|
595
774
|
(``is_synthetic=1``) participate in training. Default False.
|
|
596
775
|
"""
|
|
776
|
+
_require_manage(request)
|
|
597
777
|
if not LEARNING_AVAILABLE:
|
|
598
778
|
return {"success": False, "error": "Learning system not available"}
|
|
599
779
|
include_synthetic = bool(
|
|
@@ -647,7 +827,7 @@ async def learning_retrain(data: dict | None = None):
|
|
|
647
827
|
|
|
648
828
|
|
|
649
829
|
@router.post("/api/learning/migrate-legacy")
|
|
650
|
-
|
|
830
|
+
def learning_migrate_legacy(request: Request):
|
|
651
831
|
"""Copy ``learning_feedback`` rows into LLD-02 tables for training.
|
|
652
832
|
|
|
653
833
|
Idempotent: subsequent calls detect the migration_log sentinel and
|
|
@@ -655,6 +835,7 @@ async def learning_migrate_legacy():
|
|
|
655
835
|
with ``is_synthetic=1`` to preserve provenance; the trainer must be
|
|
656
836
|
invoked with ``include_synthetic=True`` to use them.
|
|
657
837
|
"""
|
|
838
|
+
_require_manage(request)
|
|
658
839
|
if not LEARNING_AVAILABLE:
|
|
659
840
|
return {"success": False, "error": "Learning system not available"}
|
|
660
841
|
try:
|