superlocalmemory 3.7.8 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- 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/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -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/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- 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 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -9,15 +9,27 @@ 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()
|
|
@@ -25,11 +37,47 @@ router = APIRouter()
|
|
|
25
37
|
LEARNING_DB = MEMORY_DIR / "learning.db"
|
|
26
38
|
|
|
27
39
|
|
|
40
|
+
def _require_write(request: Request) -> None:
|
|
41
|
+
from superlocalmemory.access.rbac import Permission
|
|
42
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
43
|
+
|
|
44
|
+
require_permission(request, Permission.WRITE, profile=get_active_profile())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _require_delete(request: Request) -> None:
|
|
48
|
+
from superlocalmemory.access.rbac import Permission
|
|
49
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
50
|
+
|
|
51
|
+
require_permission(request, Permission.DELETE, profile=get_active_profile())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _require_manage(request: Request) -> None:
|
|
55
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
56
|
+
|
|
57
|
+
require_manage(request, profile=get_active_profile())
|
|
58
|
+
|
|
59
|
+
|
|
28
60
|
# ---------------------------------------------------------------------------
|
|
29
61
|
# LLD-02 §4.10 — Dashboard phase truth
|
|
30
62
|
# ---------------------------------------------------------------------------
|
|
31
63
|
|
|
32
64
|
|
|
65
|
+
def _phase_payload(
|
|
66
|
+
phase: int,
|
|
67
|
+
key: str,
|
|
68
|
+
label: str,
|
|
69
|
+
model_active: bool,
|
|
70
|
+
signals: int,
|
|
71
|
+
gates: dict,
|
|
72
|
+
status: str,
|
|
73
|
+
) -> dict:
|
|
74
|
+
return {
|
|
75
|
+
"phase": phase, "key": key, "label": label,
|
|
76
|
+
"model_active": model_active, "signals": signals,
|
|
77
|
+
"gates": gates, "status": status,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
33
81
|
def _compute_ranker_phase(
|
|
34
82
|
profile_id: str,
|
|
35
83
|
*,
|
|
@@ -41,60 +89,75 @@ def _compute_ranker_phase(
|
|
|
41
89
|
SHA-256 verification on the model_cache load. Tampered bytes fall
|
|
42
90
|
back to phase 2.
|
|
43
91
|
"""
|
|
44
|
-
from superlocalmemory.learning.
|
|
45
|
-
from superlocalmemory.learning.
|
|
92
|
+
from superlocalmemory.learning.model_cache import load_active
|
|
93
|
+
from superlocalmemory.learning.ranker import (
|
|
94
|
+
PHASE_2_THRESHOLD,
|
|
95
|
+
PHASE_3_THRESHOLD,
|
|
96
|
+
)
|
|
46
97
|
|
|
47
98
|
db_path = Path(learning_db_path) if learning_db_path else LEARNING_DB
|
|
99
|
+
gates = {
|
|
100
|
+
"rule_based_min_signals": PHASE_2_THRESHOLD,
|
|
101
|
+
"ml_model_min_signals": PHASE_3_THRESHOLD,
|
|
102
|
+
"ml_model_requires_verified_active_model": True,
|
|
103
|
+
}
|
|
48
104
|
if not db_path.exists():
|
|
49
|
-
return
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"signals": 0,
|
|
54
|
-
}
|
|
105
|
+
return _phase_payload(
|
|
106
|
+
1, "baseline", "Cold start (cross-encoder only)", False, 0,
|
|
107
|
+
gates, "missing_database",
|
|
108
|
+
)
|
|
55
109
|
|
|
56
|
-
|
|
110
|
+
try:
|
|
111
|
+
db = ReadOnlyRankerStore(db_path)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.warning("ranker database unavailable: %s", exc)
|
|
114
|
+
status = (
|
|
115
|
+
_sqlite_status(exc)
|
|
116
|
+
if isinstance(exc, sqlite3.Error) else "query_error"
|
|
117
|
+
)
|
|
118
|
+
return _phase_payload(
|
|
119
|
+
1, "baseline", "Cold start (cross-encoder only)", False, 0,
|
|
120
|
+
gates, status,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
signal_status = "available"
|
|
57
124
|
try:
|
|
58
125
|
signals = db.count_signals(profile_id)
|
|
59
126
|
except Exception as exc:
|
|
60
127
|
logger.warning("count_signals failed: %s", exc)
|
|
61
128
|
signals = 0
|
|
129
|
+
signal_status = (
|
|
130
|
+
"missing_table"
|
|
131
|
+
if isinstance(exc, sqlite3.OperationalError)
|
|
132
|
+
and "no such table" in str(exc).lower()
|
|
133
|
+
else (
|
|
134
|
+
_sqlite_status(exc)
|
|
135
|
+
if isinstance(exc, sqlite3.Error) else "query_error"
|
|
136
|
+
)
|
|
137
|
+
)
|
|
62
138
|
|
|
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
139
|
try:
|
|
67
|
-
|
|
140
|
+
# Official train/promote/rollback paths invalidate this cache. Reuse
|
|
141
|
+
# the verified object here so tab navigation does not deserialize a
|
|
142
|
+
# LightGBM model on every dashboard request.
|
|
143
|
+
model = load_active(db, profile_id, use_cache=True)
|
|
68
144
|
except Exception as exc:
|
|
69
145
|
logger.warning("load_active failed: %s", exc)
|
|
70
146
|
model = None
|
|
71
147
|
|
|
72
148
|
active = model is not None
|
|
73
149
|
|
|
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
|
-
}
|
|
150
|
+
if active and signals >= PHASE_3_THRESHOLD:
|
|
151
|
+
phase = (3, "ml_model", "LightGBM ranker active", True)
|
|
152
|
+
elif signals >= PHASE_2_THRESHOLD:
|
|
153
|
+
phase = (2, "rule_based", "Contextual bandit", False)
|
|
154
|
+
else:
|
|
155
|
+
phase = (1, "baseline", "Cold start (cross-encoder only)", False)
|
|
156
|
+
return _phase_payload(*phase, signals, gates, signal_status)
|
|
94
157
|
|
|
95
158
|
|
|
96
159
|
@router.get("/api/learning/ranker_phase")
|
|
97
|
-
|
|
160
|
+
def ranker_phase():
|
|
98
161
|
"""Dashboard endpoint — LLD-02 §4.10 phase truth."""
|
|
99
162
|
try:
|
|
100
163
|
profile = get_active_profile()
|
|
@@ -106,14 +169,14 @@ async def ranker_phase():
|
|
|
106
169
|
LEARNING_AVAILABLE = False
|
|
107
170
|
BEHAVIORAL_AVAILABLE = False
|
|
108
171
|
try:
|
|
109
|
-
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
110
172
|
from superlocalmemory.learning.engagement import EngagementTracker
|
|
111
|
-
from superlocalmemory.learning.
|
|
173
|
+
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
174
|
+
from superlocalmemory.learning.ranker import AdaptiveRanker # noqa: F401
|
|
112
175
|
LEARNING_AVAILABLE = True
|
|
113
176
|
except ImportError as e:
|
|
114
177
|
logger.warning("V3 learning primary import failed: %s", e)
|
|
115
178
|
try:
|
|
116
|
-
from superlocalmemory.learning.adaptive import AdaptiveLearner
|
|
179
|
+
from superlocalmemory.learning.adaptive import AdaptiveLearner # noqa: F401
|
|
117
180
|
LEARNING_AVAILABLE = True
|
|
118
181
|
except ImportError as e2:
|
|
119
182
|
logger.warning("V3 learning fallback import failed: %s", e2)
|
|
@@ -149,30 +212,73 @@ def _get_engagement() -> "EngagementTracker | None":
|
|
|
149
212
|
return _engagement
|
|
150
213
|
|
|
151
214
|
|
|
215
|
+
def _source_quality_repair_telemetry(request: Request) -> dict:
|
|
216
|
+
status = getattr(
|
|
217
|
+
request.app.state, "source_quality_repair_status", None,
|
|
218
|
+
)
|
|
219
|
+
if not isinstance(status, dict):
|
|
220
|
+
return {
|
|
221
|
+
"state": "not_scheduled",
|
|
222
|
+
"source": "daemon_app_state",
|
|
223
|
+
"last_error": None,
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
"state": str(status.get("state") or "unknown"),
|
|
227
|
+
"source": str(status.get("source") or "daemon_app_state"),
|
|
228
|
+
"batch_size": int(status.get("batch_size") or 0),
|
|
229
|
+
"profiles_total": len(status.get("profiles") or []),
|
|
230
|
+
"profiles_complete": len(status.get("completed_profiles") or []),
|
|
231
|
+
"batches_completed": int(status.get("batches_completed") or 0),
|
|
232
|
+
"scanned": int(status.get("scanned") or 0),
|
|
233
|
+
"observations": int(status.get("observations") or 0),
|
|
234
|
+
"last_error": status.get("last_error"),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
152
238
|
@router.get("/api/learning/status")
|
|
153
|
-
|
|
239
|
+
def learning_status(request: Request):
|
|
154
240
|
"""Get comprehensive learning system status for dashboard."""
|
|
241
|
+
repair_telemetry = _source_quality_repair_telemetry(request)
|
|
155
242
|
if not LEARNING_AVAILABLE:
|
|
156
243
|
return {
|
|
157
244
|
"available": False, "ranking_phase": None,
|
|
245
|
+
"ranker_phase": None, "ranking_phase_gates": {},
|
|
158
246
|
"stats": None, "tech_preferences": [], "workflow_patterns": [],
|
|
159
247
|
"source_scores": {}, "engagement": None,
|
|
248
|
+
"telemetry_status": {
|
|
249
|
+
"source_quality_repair": repair_telemetry["state"],
|
|
250
|
+
},
|
|
251
|
+
"source_quality_repair": repair_telemetry,
|
|
160
252
|
"message": "Learning features not installed.",
|
|
161
253
|
}
|
|
162
254
|
|
|
163
|
-
result = {
|
|
255
|
+
result = {
|
|
256
|
+
"available": True,
|
|
257
|
+
"telemetry_status": {
|
|
258
|
+
"source_quality_repair": repair_telemetry["state"],
|
|
259
|
+
},
|
|
260
|
+
"source_quality_repair": repair_telemetry,
|
|
261
|
+
}
|
|
164
262
|
|
|
165
263
|
try:
|
|
166
264
|
active_profile = get_active_profile()
|
|
167
265
|
result["active_profile"] = active_profile
|
|
266
|
+
ranker_state = _compute_ranker_phase(active_profile)
|
|
267
|
+
result["ranker_phase"] = ranker_state
|
|
268
|
+
result["ranking_phase"] = ranker_state["key"]
|
|
269
|
+
result["ranking_phase_gates"] = ranker_state["gates"]
|
|
270
|
+
result["telemetry_status"]["ranker"] = ranker_state.get(
|
|
271
|
+
"status", "available",
|
|
272
|
+
)
|
|
168
273
|
|
|
169
274
|
# Real signal count from V3.1 learning_feedback table
|
|
170
275
|
signal_count = 0
|
|
171
276
|
unique_queries = 0
|
|
172
277
|
try:
|
|
173
|
-
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
174
|
-
from pathlib import Path
|
|
175
278
|
import sqlite3 as _sqlite3
|
|
279
|
+
|
|
280
|
+
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
281
|
+
|
|
176
282
|
learning_db = LEARNING_DB
|
|
177
283
|
if learning_db.exists():
|
|
178
284
|
collector = FeedbackCollector(learning_db)
|
|
@@ -194,25 +300,25 @@ async def learning_status():
|
|
|
194
300
|
except Exception:
|
|
195
301
|
pass
|
|
196
302
|
|
|
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
303
|
# Feedback stats — merge old system + new V3.1 signals
|
|
206
|
-
stats_dict = {
|
|
304
|
+
stats_dict = {
|
|
305
|
+
"feedback_count": signal_count,
|
|
306
|
+
"unique_queries": unique_queries,
|
|
307
|
+
"ranker_signal_count": ranker_state["signals"],
|
|
308
|
+
"active_profile": active_profile,
|
|
309
|
+
}
|
|
207
310
|
feedback = _get_feedback()
|
|
208
311
|
if feedback:
|
|
209
312
|
try:
|
|
210
313
|
old_stats = feedback.get_feedback_summary(active_profile)
|
|
211
314
|
if isinstance(old_stats, dict):
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
315
|
+
stats_dict = {
|
|
316
|
+
**old_stats,
|
|
317
|
+
"feedback_count": signal_count,
|
|
318
|
+
"unique_queries": unique_queries,
|
|
319
|
+
"ranker_signal_count": ranker_state["signals"],
|
|
320
|
+
"active_profile": active_profile,
|
|
321
|
+
}
|
|
216
322
|
except Exception as exc:
|
|
217
323
|
logger.debug("feedback summary: %s", exc)
|
|
218
324
|
|
|
@@ -252,21 +358,37 @@ async def learning_status():
|
|
|
252
358
|
# Tech preferences + workflow patterns from V3.1 behavioral store
|
|
253
359
|
try:
|
|
254
360
|
from superlocalmemory.learning.behavioral import BehavioralPatternStore
|
|
255
|
-
|
|
361
|
+
|
|
256
362
|
learning_db = LEARNING_DB
|
|
257
363
|
if learning_db.exists():
|
|
258
364
|
store = BehavioralPatternStore(str(learning_db))
|
|
259
365
|
all_patterns = store.get_patterns(profile_id=active_profile)
|
|
260
366
|
tech = [
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
367
|
+
{
|
|
368
|
+
"type": "tech_preference",
|
|
369
|
+
"key": p.get("pattern_key", ""),
|
|
370
|
+
"value": p.get("metadata", {}).get(
|
|
371
|
+
"value", p.get("pattern_key", ""),
|
|
372
|
+
),
|
|
373
|
+
"confidence": p.get("confidence", 0),
|
|
374
|
+
"evidence_count": p.get("evidence_count", 0),
|
|
375
|
+
# Compatibility alias for pre-3.8 dashboard clients.
|
|
376
|
+
"evidence": p.get("evidence_count", 0),
|
|
377
|
+
}
|
|
378
|
+
for p in all_patterns
|
|
379
|
+
if p.get("pattern_type") == "tech_preference"
|
|
264
380
|
]
|
|
381
|
+
workflow_types = {
|
|
382
|
+
"temporal", "workflow", "session_activity",
|
|
383
|
+
"co_retrieval_clusters",
|
|
384
|
+
}
|
|
265
385
|
workflows = [
|
|
266
386
|
{"type": p.get("pattern_type"), "key": p.get("pattern_key", ""),
|
|
267
387
|
"value": p.get("metadata", {}).get("value", ""),
|
|
268
|
-
"confidence": p.get("confidence", 0)
|
|
269
|
-
|
|
388
|
+
"confidence": p.get("confidence", 0),
|
|
389
|
+
"evidence_count": p.get("evidence_count", 0)}
|
|
390
|
+
for p in all_patterns
|
|
391
|
+
if p.get("pattern_type") in workflow_types
|
|
270
392
|
]
|
|
271
393
|
result["tech_preferences"] = tech
|
|
272
394
|
result["workflow_patterns"] = workflows
|
|
@@ -276,10 +398,6 @@ async def learning_status():
|
|
|
276
398
|
db_size = os.path.getsize(str(learning_db)) // 1024 if learning_db.exists() else 0
|
|
277
399
|
stats_dict["db_size_kb"] = db_size
|
|
278
400
|
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
401
|
else:
|
|
284
402
|
result["tech_preferences"] = []
|
|
285
403
|
result["workflow_patterns"] = []
|
|
@@ -288,11 +406,24 @@ async def learning_status():
|
|
|
288
406
|
result["tech_preferences"] = []
|
|
289
407
|
result["workflow_patterns"] = []
|
|
290
408
|
result["pattern_error"] = str(exc)
|
|
291
|
-
|
|
409
|
+
source_state = _load_source_quality_state(
|
|
410
|
+
active_profile, LEARNING_DB,
|
|
411
|
+
)
|
|
412
|
+
model_state = _load_model_state(active_profile, LEARNING_DB)
|
|
413
|
+
result["source_scores"] = source_state["scores"]
|
|
414
|
+
result["source_scores_source"] = "learning.db:source_quality"
|
|
415
|
+
result["source_scores_are_posterior"] = True
|
|
416
|
+
result["telemetry_status"]["source_quality"] = source_state["status"]
|
|
417
|
+
result["telemetry_status"]["model_state"] = model_state["status"]
|
|
418
|
+
stats_dict["tracked_sources"] = source_state["tracked_sources"]
|
|
419
|
+
stats_dict["models_trained"] = model_state["models_trained"]
|
|
420
|
+
stats_dict["models_active_verified"] = int(
|
|
421
|
+
bool(ranker_state["model_active"]),
|
|
422
|
+
)
|
|
292
423
|
|
|
293
|
-
except Exception
|
|
294
|
-
logger.
|
|
295
|
-
result["error"] =
|
|
424
|
+
except Exception:
|
|
425
|
+
logger.exception("Error getting learning status")
|
|
426
|
+
result["error"] = "Internal server error"
|
|
296
427
|
|
|
297
428
|
return result
|
|
298
429
|
|
|
@@ -302,8 +433,9 @@ async def learning_status():
|
|
|
302
433
|
# ============================================================================
|
|
303
434
|
|
|
304
435
|
@router.post("/api/feedback")
|
|
305
|
-
|
|
436
|
+
def record_feedback(request: Request, data: dict):
|
|
306
437
|
"""Record explicit feedback from dashboard (thumbs up/down, pin)."""
|
|
438
|
+
_require_write(request)
|
|
307
439
|
if not LEARNING_AVAILABLE:
|
|
308
440
|
return {"success": False, "error": "Learning system not available"}
|
|
309
441
|
|
|
@@ -333,14 +465,15 @@ async def record_feedback(data: dict):
|
|
|
333
465
|
"message": f"Feedback recorded: {feedback_type} for memory #{memory_id}",
|
|
334
466
|
"feedback_id": row_id,
|
|
335
467
|
}
|
|
336
|
-
except Exception
|
|
337
|
-
logger.
|
|
338
|
-
return {"success": False, "error":
|
|
468
|
+
except Exception:
|
|
469
|
+
logger.exception("Error recording feedback")
|
|
470
|
+
return {"success": False, "error": "Internal server error"}
|
|
339
471
|
|
|
340
472
|
|
|
341
473
|
@router.post("/api/feedback/dwell")
|
|
342
|
-
|
|
474
|
+
def record_dwell(request: Request, data: dict):
|
|
343
475
|
"""Record dwell time feedback from dashboard modal."""
|
|
476
|
+
_require_write(request)
|
|
344
477
|
if not LEARNING_AVAILABLE:
|
|
345
478
|
return {"success": False, "error": "Learning system not available"}
|
|
346
479
|
|
|
@@ -378,13 +511,13 @@ async def record_dwell(data: dict):
|
|
|
378
511
|
"message": f"Dwell feedback recorded: {feedback_type} ({dwell_seconds:.1f}s)",
|
|
379
512
|
"feedback_id": row_id,
|
|
380
513
|
}
|
|
381
|
-
except Exception
|
|
382
|
-
logger.
|
|
383
|
-
return {"success": False, "error":
|
|
514
|
+
except Exception:
|
|
515
|
+
logger.exception("Error recording dwell")
|
|
516
|
+
return {"success": False, "error": "Internal server error"}
|
|
384
517
|
|
|
385
518
|
|
|
386
519
|
@router.get("/api/feedback/stats")
|
|
387
|
-
|
|
520
|
+
def feedback_stats():
|
|
388
521
|
"""Get feedback signal statistics for dashboard progress bar."""
|
|
389
522
|
if not LEARNING_AVAILABLE:
|
|
390
523
|
return {
|
|
@@ -413,9 +546,12 @@ async def feedback_stats():
|
|
|
413
546
|
"progress": round(progress, 1), "target": target,
|
|
414
547
|
"by_channel": by_channel, "by_type": by_type, "available": True,
|
|
415
548
|
}
|
|
416
|
-
except Exception
|
|
417
|
-
logger.
|
|
418
|
-
return {
|
|
549
|
+
except Exception:
|
|
550
|
+
logger.exception("Error getting feedback stats")
|
|
551
|
+
return {
|
|
552
|
+
"total_signals": 0, "ranking_phase": "baseline",
|
|
553
|
+
"progress": 0, "error": "Internal server error",
|
|
554
|
+
}
|
|
419
555
|
|
|
420
556
|
|
|
421
557
|
# ============================================================================
|
|
@@ -424,7 +560,7 @@ async def feedback_stats():
|
|
|
424
560
|
|
|
425
561
|
|
|
426
562
|
@router.delete("/api/patterns/delete")
|
|
427
|
-
|
|
563
|
+
def delete_pattern(request: Request, data: dict) -> dict:
|
|
428
564
|
"""S9-DASH-04: delete a single auto-detected pattern by key.
|
|
429
565
|
|
|
430
566
|
Body: ``{pattern_type: str, pattern_key: str}``
|
|
@@ -432,6 +568,7 @@ async def delete_pattern(data: dict) -> dict:
|
|
|
432
568
|
Returns ``{success: bool, deleted: int}``. The pattern is scoped
|
|
433
569
|
to the active profile so cross-profile deletion is impossible.
|
|
434
570
|
"""
|
|
571
|
+
_require_delete(request)
|
|
435
572
|
if not BEHAVIORAL_AVAILABLE:
|
|
436
573
|
return {"success": False, "error": "Behavioral engine not available"}
|
|
437
574
|
ptype = (data or {}).get("pattern_type", "")
|
|
@@ -455,11 +592,11 @@ async def delete_pattern(data: dict) -> dict:
|
|
|
455
592
|
}
|
|
456
593
|
except Exception as exc: # noqa: BLE001
|
|
457
594
|
logger.error("delete_pattern failed: %s", exc)
|
|
458
|
-
return {"success": False, "error":
|
|
595
|
+
return {"success": False, "error": "internal error"}
|
|
459
596
|
|
|
460
597
|
|
|
461
598
|
@router.get("/api/patterns")
|
|
462
|
-
|
|
599
|
+
def get_patterns():
|
|
463
600
|
"""Get learned behavioral patterns for the Patterns dashboard tab.
|
|
464
601
|
|
|
465
602
|
v3.4.1: This endpoint was MISSING — patterns.js calls /api/patterns
|
|
@@ -510,43 +647,74 @@ async def get_patterns():
|
|
|
510
647
|
result["pattern_error"] = str(exc)
|
|
511
648
|
|
|
512
649
|
# Learning signal stats (feedback count, co-retrieval, channel credits)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
650
|
+
if LEARNING_AVAILABLE:
|
|
651
|
+
try:
|
|
652
|
+
from superlocalmemory.learning.signals import LearningSignals
|
|
653
|
+
signals = LearningSignals(str(LEARNING_DB))
|
|
654
|
+
result["signal_stats"] = signals.get_signal_stats(active_profile)
|
|
655
|
+
except Exception as exc:
|
|
656
|
+
logger.debug("Signal stats unavailable: %s", exc)
|
|
519
657
|
|
|
520
658
|
# Graph intelligence contribution to learning (v3.4.1)
|
|
521
659
|
try:
|
|
522
660
|
import sqlite3 as _sqlite3
|
|
661
|
+
|
|
523
662
|
from superlocalmemory.server.routes.helpers import DB_PATH
|
|
524
663
|
if DB_PATH.exists():
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
"
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
664
|
+
with closing(_sqlite3.connect(str(DB_PATH))) as conn:
|
|
665
|
+
conn.row_factory = _sqlite3.Row
|
|
666
|
+
row = conn.execute(
|
|
667
|
+
"SELECT COUNT(*) AS cnt, "
|
|
668
|
+
"COUNT(DISTINCT community_id) AS communities, "
|
|
669
|
+
"ROUND(AVG(pagerank_score), 4) AS avg_pagerank "
|
|
670
|
+
"FROM fact_importance WHERE profile_id = ?",
|
|
671
|
+
(active_profile,),
|
|
672
|
+
).fetchone()
|
|
673
|
+
if row:
|
|
674
|
+
d = dict(row)
|
|
675
|
+
result["graph_intelligence"] = {
|
|
676
|
+
"facts_analyzed": d.get("cnt", 0),
|
|
677
|
+
"communities_detected": d.get("communities", 0),
|
|
678
|
+
"avg_pagerank": float(
|
|
679
|
+
d.get("avg_pagerank", 0) or 0,
|
|
680
|
+
),
|
|
681
|
+
}
|
|
541
682
|
except Exception:
|
|
542
683
|
pass
|
|
543
684
|
|
|
685
|
+
all_patterns = [
|
|
686
|
+
pattern
|
|
687
|
+
for category_patterns in patterns.values()
|
|
688
|
+
for pattern in category_patterns
|
|
689
|
+
]
|
|
690
|
+
confidences = [
|
|
691
|
+
float(pattern["confidence"])
|
|
692
|
+
for pattern in all_patterns
|
|
693
|
+
if pattern.get("confidence") is not None
|
|
694
|
+
]
|
|
695
|
+
result.update({
|
|
696
|
+
"total_patterns": len(all_patterns),
|
|
697
|
+
"pattern_types": [
|
|
698
|
+
category
|
|
699
|
+
for category, category_patterns in patterns.items()
|
|
700
|
+
if category_patterns
|
|
701
|
+
],
|
|
702
|
+
"confidence_stats": {
|
|
703
|
+
"avg": (
|
|
704
|
+
sum(confidences) / len(confidences)
|
|
705
|
+
if confidences else 0.0
|
|
706
|
+
),
|
|
707
|
+
"min": min(confidences) if confidences else 0.0,
|
|
708
|
+
"max": max(confidences) if confidences else 0.0,
|
|
709
|
+
},
|
|
710
|
+
})
|
|
544
711
|
return result
|
|
545
712
|
|
|
546
713
|
|
|
547
714
|
@router.post("/api/learning/backup")
|
|
548
|
-
|
|
715
|
+
def learning_backup(request: Request):
|
|
549
716
|
"""Backup learning.db to a timestamped file."""
|
|
717
|
+
_require_manage(request)
|
|
550
718
|
try:
|
|
551
719
|
if not LEARNING_DB.exists():
|
|
552
720
|
return {"success": False, "error": "No learning.db found"}
|
|
@@ -561,14 +729,15 @@ async def learning_backup():
|
|
|
561
729
|
"path": str(backup_path),
|
|
562
730
|
"message": f"Learning DB backed up to {backup_name}",
|
|
563
731
|
}
|
|
564
|
-
except Exception
|
|
565
|
-
logger.
|
|
566
|
-
return {"success": False, "error":
|
|
732
|
+
except Exception:
|
|
733
|
+
logger.exception("Error backing up learning DB")
|
|
734
|
+
return {"success": False, "error": "Internal server error"}
|
|
567
735
|
|
|
568
736
|
|
|
569
737
|
@router.post("/api/learning/reset")
|
|
570
|
-
|
|
738
|
+
def learning_reset(request: Request):
|
|
571
739
|
"""Reset all learning data for the active profile. Memories preserved."""
|
|
740
|
+
_require_delete(request)
|
|
572
741
|
if not LEARNING_AVAILABLE:
|
|
573
742
|
return {"success": False, "error": "Learning system not available"}
|
|
574
743
|
try:
|
|
@@ -583,17 +752,18 @@ async def learning_reset():
|
|
|
583
752
|
}
|
|
584
753
|
except Exception as exc: # noqa: BLE001
|
|
585
754
|
logger.error("learning_reset failed: %s", exc)
|
|
586
|
-
return {"success": False, "error":
|
|
755
|
+
return {"success": False, "error": "internal error"}
|
|
587
756
|
|
|
588
757
|
|
|
589
758
|
@router.post("/api/learning/retrain")
|
|
590
|
-
async def learning_retrain(data: dict | None = None):
|
|
759
|
+
async def learning_retrain(request: Request, data: dict | None = None):
|
|
591
760
|
"""Force a retrain of the LightGBM ranker.
|
|
592
761
|
|
|
593
762
|
Body (optional, JSON):
|
|
594
763
|
``{"include_synthetic": bool}`` — when True, migrated legacy rows
|
|
595
764
|
(``is_synthetic=1``) participate in training. Default False.
|
|
596
765
|
"""
|
|
766
|
+
_require_manage(request)
|
|
597
767
|
if not LEARNING_AVAILABLE:
|
|
598
768
|
return {"success": False, "error": "Learning system not available"}
|
|
599
769
|
include_synthetic = bool(
|
|
@@ -643,11 +813,11 @@ async def learning_retrain(data: dict | None = None):
|
|
|
643
813
|
}
|
|
644
814
|
except Exception as exc: # noqa: BLE001
|
|
645
815
|
logger.error("learning_retrain failed: %s", exc)
|
|
646
|
-
return {"success": False, "error":
|
|
816
|
+
return {"success": False, "error": "internal error"}
|
|
647
817
|
|
|
648
818
|
|
|
649
819
|
@router.post("/api/learning/migrate-legacy")
|
|
650
|
-
|
|
820
|
+
def learning_migrate_legacy(request: Request):
|
|
651
821
|
"""Copy ``learning_feedback`` rows into LLD-02 tables for training.
|
|
652
822
|
|
|
653
823
|
Idempotent: subsequent calls detect the migration_log sentinel and
|
|
@@ -655,6 +825,7 @@ async def learning_migrate_legacy():
|
|
|
655
825
|
with ``is_synthetic=1`` to preserve provenance; the trainer must be
|
|
656
826
|
invoked with ``include_synthetic=True`` to use them.
|
|
657
827
|
"""
|
|
828
|
+
_require_manage(request)
|
|
658
829
|
if not LEARNING_AVAILABLE:
|
|
659
830
|
return {"success": False, "error": "Learning system not available"}
|
|
660
831
|
try:
|
|
@@ -665,4 +836,4 @@ async def learning_migrate_legacy():
|
|
|
665
836
|
return {"success": True, **stats}
|
|
666
837
|
except Exception as exc: # noqa: BLE001
|
|
667
838
|
logger.error("learning_migrate_legacy failed: %s", exc)
|
|
668
|
-
return {"success": False, "error":
|
|
839
|
+
return {"success": False, "error": "internal error"}
|