superlocalmemory 3.8.13 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -151,14 +151,26 @@ def build_immediate_admission_handler(
|
|
|
151
151
|
from datetime import UTC, datetime
|
|
152
152
|
|
|
153
153
|
from superlocalmemory.core.ingest_gate import apply_ingest_gate
|
|
154
|
+
from superlocalmemory.core.ingest_policy import scrub_secrets_for_ingest
|
|
154
155
|
from superlocalmemory.storage.models import AtomicFact, FactType, MemoryRecord
|
|
155
156
|
|
|
157
|
+
# Secrets are ALWAYS scrubbed at this shared queryable-write chokepoint,
|
|
158
|
+
# which both the canonical HTTP /remember runtime and the canonical_store
|
|
159
|
+
# Python/CLI path funnel through. A credential must never persist verbatim
|
|
160
|
+
# in any durable representation (memory rows, fact rows), regardless of
|
|
161
|
+
# ingress surface. The scrub is idempotent: content already scrubbed by an
|
|
162
|
+
# upstream caller is returned unchanged.
|
|
163
|
+
scrub = scrub_secrets_for_ingest(request.content)
|
|
164
|
+
content = scrub.content
|
|
165
|
+
if scrub.redacted:
|
|
166
|
+
logger.info("secret scrub: redacted credential material on queryable write")
|
|
167
|
+
|
|
156
168
|
metadata = dict(request.metadata)
|
|
157
169
|
metadata["ingestion_operation_id"] = operation_id
|
|
158
170
|
if request.session_id:
|
|
159
171
|
metadata.setdefault("session_id", request.session_id)
|
|
160
172
|
gate = apply_ingest_gate(
|
|
161
|
-
|
|
173
|
+
content,
|
|
162
174
|
max_verbatim_chars=max_verbatim_chars,
|
|
163
175
|
max_ingest_bytes=max_ingest_bytes,
|
|
164
176
|
)
|
|
@@ -225,7 +237,7 @@ def build_immediate_admission_handler(
|
|
|
225
237
|
record = MemoryRecord(
|
|
226
238
|
memory_id=memory_id,
|
|
227
239
|
profile_id=request.profile_id,
|
|
228
|
-
content=
|
|
240
|
+
content=content,
|
|
229
241
|
session_id=request.session_id,
|
|
230
242
|
session_date=observation_date,
|
|
231
243
|
speaker=request.speaker,
|
|
@@ -302,6 +314,15 @@ def canonical_store(
|
|
|
302
314
|
error=ValueError("content rejected by local admission policy"),
|
|
303
315
|
)
|
|
304
316
|
return []
|
|
317
|
+
# Secrets are ALWAYS scrubbed pre-admission (not opt-in): a credential must
|
|
318
|
+
# never persist verbatim in any durable or queryable representation
|
|
319
|
+
# (facts, receipts, journal, exports, backups, mesh).
|
|
320
|
+
from superlocalmemory.core.ingest_policy import scrub_secrets_for_ingest
|
|
321
|
+
|
|
322
|
+
_secret_scrub = scrub_secrets_for_ingest(content)
|
|
323
|
+
if _secret_scrub.redacted:
|
|
324
|
+
content = _secret_scrub.content
|
|
325
|
+
logger.info("secret scrub: redacted credential material on ingest")
|
|
305
326
|
# C4: opt-in PII redaction. When enabled (config.pii_redaction or
|
|
306
327
|
# SLM_PII_REDACTION), scrub personal identifiers BEFORE the content is
|
|
307
328
|
# extracted, embedded, or persisted — nothing sensitive ever reaches disk.
|
|
@@ -426,7 +447,19 @@ def build_engine_ingestion_command(engine: MemoryEngine) -> IngestionCommand:
|
|
|
426
447
|
)
|
|
427
448
|
|
|
428
449
|
def validate_admission(request: IngestionRequest) -> None:
|
|
429
|
-
"""Apply trust policy before the journal or canonical transaction.
|
|
450
|
+
"""Apply trust policy before the journal or canonical transaction.
|
|
451
|
+
|
|
452
|
+
V4 Phase 4 addition: evaluate the OperationPolicyRegistry AFTER the
|
|
453
|
+
trust hook and BEFORE the coordinator transaction. This evaluation is
|
|
454
|
+
CPU-only (dict lookup + frozenset operations, no I/O) and runs in the
|
|
455
|
+
validate_admission slot which is already outside the SQLite writer.
|
|
456
|
+
|
|
457
|
+
The default registry allows every REMEMBER from an OWNER via the
|
|
458
|
+
INTERNAL transport in local mode — zero new rejections on the current
|
|
459
|
+
happy path. A denial raises ValueError, which propagates through
|
|
460
|
+
IngestionCommand.submit() to the caller exactly as existing slot
|
|
461
|
+
violations do.
|
|
462
|
+
"""
|
|
430
463
|
engine._hooks.run_pre("store", {
|
|
431
464
|
"operation": "store",
|
|
432
465
|
"agent_id": request.trusted_actor_id,
|
|
@@ -434,6 +467,35 @@ def build_engine_ingestion_command(engine: MemoryEngine) -> IngestionCommand:
|
|
|
434
467
|
"content_preview": request.content[:100],
|
|
435
468
|
})
|
|
436
469
|
|
|
470
|
+
# Phase 4: additive policy check — runs after the trust hook.
|
|
471
|
+
# Imports are lazy to avoid import-order coupling at module init.
|
|
472
|
+
from superlocalmemory.core.actor_context import ActorContext, ActorRole, Transport
|
|
473
|
+
from superlocalmemory.core.operation_policy_registry import _DEFAULT_REGISTRY
|
|
474
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
475
|
+
|
|
476
|
+
# A missing actor is rejected by the existing admission check with its
|
|
477
|
+
# canonical message; the policy layer is strictly additive and must not
|
|
478
|
+
# preempt that rejection, so it only evaluates when an actor is present.
|
|
479
|
+
if request.trusted_actor_id:
|
|
480
|
+
_actor = ActorContext(
|
|
481
|
+
# trusted_actor_id is already server-validated by the caller;
|
|
482
|
+
# it is NEVER taken from the request body here.
|
|
483
|
+
principal_id=request.trusted_actor_id,
|
|
484
|
+
roles=frozenset({ActorRole.OWNER}),
|
|
485
|
+
active_profile_id=request.profile_id,
|
|
486
|
+
transport=Transport.INTERNAL,
|
|
487
|
+
client_host="", # in-process: always local
|
|
488
|
+
)
|
|
489
|
+
_decision = _DEFAULT_REGISTRY.evaluate(
|
|
490
|
+
OperationKind.REMEMBER,
|
|
491
|
+
_actor,
|
|
492
|
+
"local", # internal Python-API path is always local/single-user
|
|
493
|
+
)
|
|
494
|
+
if not _decision.allowed:
|
|
495
|
+
raise ValueError(
|
|
496
|
+
f"operation policy denied REMEMBER: {_decision.reason}"
|
|
497
|
+
)
|
|
498
|
+
|
|
437
499
|
def resume_checkpoint(operation: IngestionOperation) -> MaterializationResult:
|
|
438
500
|
"""Repair only stages whose writes have an idempotent natural key."""
|
|
439
501
|
state = dict(operation.derivation_state)
|
|
@@ -264,7 +264,10 @@ def init_encoding(
|
|
|
264
264
|
db, embedder, llm, config.encoding,
|
|
265
265
|
)
|
|
266
266
|
observation_builder = ObservationBuilder(db)
|
|
267
|
-
|
|
267
|
+
# V3.2: VectorStore (Phase 1) -- sqlite-vec KNN. Scene assignment also
|
|
268
|
+
# consumes it, so initialize it before the encoding component is wired.
|
|
269
|
+
vector_store = _init_vector_store(config)
|
|
270
|
+
scene_builder = SceneBuilder(db, embedder, vector_store=vector_store)
|
|
268
271
|
entropy_gate = EntropyGate(
|
|
269
272
|
embedder, config.encoding.entropy_threshold,
|
|
270
273
|
)
|
|
@@ -276,9 +279,6 @@ def init_encoding(
|
|
|
276
279
|
db, config.math.sheaf_contradiction_threshold,
|
|
277
280
|
)
|
|
278
281
|
|
|
279
|
-
# V3.2: VectorStore (Phase 1) -- sqlite-vec KNN
|
|
280
|
-
vector_store = _init_vector_store(config)
|
|
281
|
-
|
|
282
282
|
# V3.2: AccessLog (Phase 1) -- fact access tracking
|
|
283
283
|
access_log = _init_access_log(db)
|
|
284
284
|
|
|
@@ -486,6 +486,25 @@ def _init_spreading_activation(
|
|
|
486
486
|
return None
|
|
487
487
|
|
|
488
488
|
|
|
489
|
+
def _init_prompt_injector(config: SLMConfig, db: DatabaseManager) -> Any | None:
|
|
490
|
+
"""Construct a profile-scoped PromptInjector for soft-prompt injection (P1-8).
|
|
491
|
+
|
|
492
|
+
Returns None when parameterization is disabled or construction fails.
|
|
493
|
+
Failure is fail-soft — soft-prompt absence must never block auto-invoke.
|
|
494
|
+
"""
|
|
495
|
+
if not hasattr(config, "parameterization") or not config.parameterization.enabled:
|
|
496
|
+
return None
|
|
497
|
+
try:
|
|
498
|
+
from superlocalmemory.parameterization.prompt_injector import PromptInjector
|
|
499
|
+
from superlocalmemory.parameterization.soft_prompt_generator import SoftPromptGenerator
|
|
500
|
+
|
|
501
|
+
generator = SoftPromptGenerator(config.parameterization)
|
|
502
|
+
return PromptInjector(db=db, generator=generator, config=config.parameterization)
|
|
503
|
+
except Exception as exc:
|
|
504
|
+
logger.warning("PromptInjector init failed — soft-prompt injection disabled: %s", exc)
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
|
|
489
508
|
def _init_auto_invoker(
|
|
490
509
|
config: SLMConfig,
|
|
491
510
|
db: DatabaseManager,
|
|
@@ -493,20 +512,28 @@ def _init_auto_invoker(
|
|
|
493
512
|
trust_scorer: Any,
|
|
494
513
|
embedder: Any,
|
|
495
514
|
) -> Any | None:
|
|
496
|
-
"""Create AutoInvoker for Phase 2 multi-signal retrieval.
|
|
515
|
+
"""Create AutoInvoker for Phase 2 multi-signal retrieval.
|
|
516
|
+
|
|
517
|
+
V3.3 (P1-8): A profile-scoped PromptInjector is constructed and wired in
|
|
518
|
+
so stored behavioral soft prompts are prepended to every session context.
|
|
519
|
+
The injector is fail-soft — its absence never blocks auto-invoke.
|
|
520
|
+
"""
|
|
497
521
|
if not hasattr(config, "auto_invoke") or not config.auto_invoke.enabled:
|
|
498
522
|
return None
|
|
499
523
|
try:
|
|
500
524
|
from superlocalmemory.hooks.auto_invoker import AutoInvoker
|
|
525
|
+
|
|
526
|
+
prompt_injector = _init_prompt_injector(config, db)
|
|
501
527
|
return AutoInvoker(
|
|
502
528
|
db=db,
|
|
503
529
|
vector_store=vector_store,
|
|
504
530
|
trust_scorer=trust_scorer,
|
|
505
531
|
embedder=embedder,
|
|
506
532
|
config=config.auto_invoke,
|
|
533
|
+
prompt_injector=prompt_injector,
|
|
507
534
|
)
|
|
508
535
|
except Exception as exc:
|
|
509
|
-
logger.
|
|
536
|
+
logger.warning("AutoInvoker init failed — auto-invoke disabled: %s", exc)
|
|
510
537
|
return None
|
|
511
538
|
|
|
512
539
|
|
|
@@ -580,9 +607,9 @@ def init_retrieval(
|
|
|
580
607
|
trust_scorer: Any,
|
|
581
608
|
vector_store: Any = None,
|
|
582
609
|
) -> Any:
|
|
583
|
-
"""Create the RetrievalEngine —
|
|
584
|
-
temporal, spreading_activation, hopfield)
|
|
585
|
-
|
|
610
|
+
"""Create the RetrievalEngine — six candidate producers (semantic, BM25,
|
|
611
|
+
entity_graph, temporal, spreading_activation, hopfield) fused via RRF.
|
|
612
|
+
Returns the engine."""
|
|
586
613
|
from superlocalmemory.retrieval.engine import RetrievalEngine
|
|
587
614
|
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
588
615
|
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Pre-admission scrub policy that removes secret material from content.
|
|
2
|
+
|
|
3
|
+
A single ordered stage that redacts secrets BEFORE content can reach any durable
|
|
4
|
+
or queryable representation (atomic facts, memory records, ingestion receipts,
|
|
5
|
+
journal, exports, backups, mesh). Unlike PII redaction — an opt-in operator
|
|
6
|
+
policy — secret scrubbing is unconditional: a credential persisted verbatim at
|
|
7
|
+
rest is always incorrect.
|
|
8
|
+
|
|
9
|
+
The detector is the ``security_primitives.redact_secrets`` redactor; this module
|
|
10
|
+
wraps it in an immutable result so callers can log the fact of redaction without
|
|
11
|
+
ever handling the secret value.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from superlocalmemory.core.security_primitives import redact_secrets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ScrubResult:
|
|
22
|
+
"""Outcome of a pre-admission scrub. Never carries the original secret."""
|
|
23
|
+
|
|
24
|
+
content: str
|
|
25
|
+
redacted: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def scrub_secrets_for_ingest(content: str) -> ScrubResult:
|
|
29
|
+
"""Redact secret material from ``content`` before durable admission.
|
|
30
|
+
|
|
31
|
+
Returns a :class:`ScrubResult` whose ``content`` is safe to persist. When
|
|
32
|
+
nothing matched, ``content`` is returned unchanged and ``redacted`` is False,
|
|
33
|
+
so the caller can keep the original object and skip a log line.
|
|
34
|
+
"""
|
|
35
|
+
if not content:
|
|
36
|
+
return ScrubResult(content=content, redacted=False)
|
|
37
|
+
scrubbed = redact_secrets(content)
|
|
38
|
+
return ScrubResult(content=scrubbed, redacted=scrubbed != content)
|
|
@@ -36,6 +36,12 @@ _BACKFILL_BURN_IN_STEPS = 50
|
|
|
36
36
|
_LANGEVIN_DIM = 8
|
|
37
37
|
_MAX_NORM = 0.99
|
|
38
38
|
|
|
39
|
+
# ELC zone vocabulary: EbbinghausCurve returns 'archive'/'forgotten' but
|
|
40
|
+
# atomic_facts.lifecycle CHECK only allows 'active|warm|cold|archived'.
|
|
41
|
+
# Remap at write boundary so ELC never triggers IntegrityError.
|
|
42
|
+
_VALID_LIFECYCLE_ZONES: frozenset[str] = frozenset({"active", "warm", "cold", "archived"})
|
|
43
|
+
_ELC_ZONE_REMAP: dict[str, str] = {"archive": "archived", "forgotten": "archived"}
|
|
44
|
+
|
|
39
45
|
|
|
40
46
|
def _age_days(created_at: str | None) -> float:
|
|
41
47
|
"""Age in days from an ISO timestamp.
|
|
@@ -105,6 +111,86 @@ def _seed_langevin_position(
|
|
|
105
111
|
return (direction / norm * r_eq).tolist()
|
|
106
112
|
|
|
107
113
|
|
|
114
|
+
def close_stale_sessions(
|
|
115
|
+
db: DatabaseManager,
|
|
116
|
+
profile_id: str = "default",
|
|
117
|
+
*,
|
|
118
|
+
idle_hours: float = 24.0,
|
|
119
|
+
max_per_pass: int = 50,
|
|
120
|
+
) -> int:
|
|
121
|
+
"""Close application sessions idle longer than ``idle_hours``.
|
|
122
|
+
|
|
123
|
+
Nothing auto-calls ``close_session`` except the MCP tool — un-closed
|
|
124
|
+
sessions never get temporal summaries. This maintenance pass finds
|
|
125
|
+
sessions whose newest fact is older than the idle window and closes
|
|
126
|
+
them via ``run_close_session``.
|
|
127
|
+
|
|
128
|
+
Properties:
|
|
129
|
+
- Idempotent: already-summarised sessions are skipped (no double write).
|
|
130
|
+
- Bounded: at most ``max_per_pass`` sessions closed per call.
|
|
131
|
+
- Only sessions with entity-linked facts (summarisable) are selected.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Number of sessions successfully summarised in this pass.
|
|
135
|
+
"""
|
|
136
|
+
if idle_hours <= 0 or max_per_pass <= 0:
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
from superlocalmemory.core.store_pipeline import (
|
|
140
|
+
_session_already_summarised,
|
|
141
|
+
run_close_session,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
cutoff = (datetime.now(UTC) - timedelta(hours=float(idle_hours))).isoformat()
|
|
145
|
+
# Over-fetch slightly so already-closed rows in the window do not starve
|
|
146
|
+
# the bounded close budget.
|
|
147
|
+
fetch_limit = max(int(max_per_pass) * 3, int(max_per_pass))
|
|
148
|
+
try:
|
|
149
|
+
rows = db.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT session_id, MAX(created_at) AS last_at
|
|
152
|
+
FROM atomic_facts
|
|
153
|
+
WHERE profile_id = ?
|
|
154
|
+
AND session_id IS NOT NULL
|
|
155
|
+
AND session_id != ''
|
|
156
|
+
AND canonical_entities_json IS NOT NULL
|
|
157
|
+
AND canonical_entities_json != '[]'
|
|
158
|
+
GROUP BY session_id
|
|
159
|
+
HAVING MAX(created_at) < ?
|
|
160
|
+
ORDER BY last_at ASC
|
|
161
|
+
LIMIT ?
|
|
162
|
+
""",
|
|
163
|
+
(profile_id, cutoff, fetch_limit),
|
|
164
|
+
)
|
|
165
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
166
|
+
logger.debug("stale session query failed: %s", exc)
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
closed = 0
|
|
170
|
+
for row in rows:
|
|
171
|
+
if closed >= int(max_per_pass):
|
|
172
|
+
break
|
|
173
|
+
d = dict(row)
|
|
174
|
+
sid = str(d.get("session_id") or "")
|
|
175
|
+
if not sid:
|
|
176
|
+
continue
|
|
177
|
+
if _session_already_summarised(db, profile_id, sid):
|
|
178
|
+
continue
|
|
179
|
+
try:
|
|
180
|
+
n = run_close_session(sid, profile_id, db=db)
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
logger.warning("stale session close failed for %s: %s", sid, exc)
|
|
183
|
+
continue
|
|
184
|
+
if n > 0:
|
|
185
|
+
closed += 1
|
|
186
|
+
if closed:
|
|
187
|
+
logger.info(
|
|
188
|
+
"Closed %d stale session(s) (idle > %.1fh, profile=%s)",
|
|
189
|
+
closed, idle_hours, profile_id,
|
|
190
|
+
)
|
|
191
|
+
return closed
|
|
192
|
+
|
|
193
|
+
|
|
108
194
|
def run_maintenance(
|
|
109
195
|
db: DatabaseManager,
|
|
110
196
|
config: SLMConfig,
|
|
@@ -130,13 +216,30 @@ def run_maintenance(
|
|
|
130
216
|
"langevin_backfilled": 0,
|
|
131
217
|
"langevin_updated": 0,
|
|
132
218
|
"fisher_coupled": 0,
|
|
219
|
+
"fisher_posterior_updated": 0, # P1-9: Fisher bayesian_update on access
|
|
220
|
+
"ebbinghaus_coupled": 0, # Phase 5: Ebbinghaus-Langevin coupling
|
|
133
221
|
"sheaf_checked": 0,
|
|
134
222
|
"entity_summaries_consolidated": 0, # V3.4.40
|
|
135
223
|
"orphan_metadata_gc": 0, # v3.6.4 (P1-3)
|
|
136
224
|
"expansion_backfilled": 0, # T3b
|
|
137
225
|
"embeddings_backfilled": 0, # v3.8.x NULL-embedding self-heal
|
|
226
|
+
"stale_sessions_closed": 0, # v4: orphaned application sessions
|
|
138
227
|
}
|
|
139
228
|
|
|
229
|
+
# Close idle application sessions so temporal summaries exist even when
|
|
230
|
+
# clients never call close_session. Bounded + idempotent; fail-soft.
|
|
231
|
+
try:
|
|
232
|
+
idle_hours = float(getattr(config, "session_idle_close_hours", 24.0) or 24.0)
|
|
233
|
+
max_close = int(getattr(config, "session_idle_close_max_per_pass", 50) or 50)
|
|
234
|
+
counts["stale_sessions_closed"] = close_stale_sessions(
|
|
235
|
+
db,
|
|
236
|
+
profile_id,
|
|
237
|
+
idle_hours=idle_hours,
|
|
238
|
+
max_per_pass=max_close,
|
|
239
|
+
)
|
|
240
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
241
|
+
logger.debug("stale session close skipped: %s", exc)
|
|
242
|
+
|
|
140
243
|
# P1-3 (embeddings-vector-02): sweep orphaned embedding_metadata left by
|
|
141
244
|
# any FK-off delete path, so the semantic channel never maps to dead facts.
|
|
142
245
|
# Runs before the early-return so it sweeps even for empty profiles.
|
|
@@ -330,6 +433,158 @@ def run_maintenance(
|
|
|
330
433
|
except Exception as exc:
|
|
331
434
|
logger.warning("Fisher-Langevin coupling failed: %s", exc)
|
|
332
435
|
|
|
436
|
+
# 1c. Fisher posterior update (P1-9): tighten variance per new access event.
|
|
437
|
+
# Access-delta semantics: apply one Bayesian update per net-new access since
|
|
438
|
+
# the last maintenance run. Zero new accesses → variance unchanged.
|
|
439
|
+
# This prevents idle-corpus drift that tick-based updates would cause.
|
|
440
|
+
# Inline schema migration: adds fisher_last_applied_access when absent.
|
|
441
|
+
# Gate: config.math.fisher_bayesian_update (default True).
|
|
442
|
+
if config.math.fisher_bayesian_update:
|
|
443
|
+
try:
|
|
444
|
+
import json as _json
|
|
445
|
+
from superlocalmemory.math.fisher import FisherRaoMetric
|
|
446
|
+
|
|
447
|
+
# Inline migration — harmless no-op if column already exists.
|
|
448
|
+
try:
|
|
449
|
+
db.execute(
|
|
450
|
+
"ALTER TABLE atomic_facts "
|
|
451
|
+
"ADD COLUMN fisher_last_applied_access INTEGER NOT NULL DEFAULT 0"
|
|
452
|
+
)
|
|
453
|
+
except Exception:
|
|
454
|
+
pass # already migrated on a previous run
|
|
455
|
+
|
|
456
|
+
frm = FisherRaoMetric(temperature=config.math.fisher_temperature)
|
|
457
|
+
posterior_count = 0
|
|
458
|
+
for f in facts:
|
|
459
|
+
if f.fisher_variance is None:
|
|
460
|
+
continue
|
|
461
|
+
rows = db.execute(
|
|
462
|
+
"SELECT access_count, fisher_last_applied_access "
|
|
463
|
+
"FROM atomic_facts WHERE fact_id = ?",
|
|
464
|
+
(f.fact_id,),
|
|
465
|
+
)
|
|
466
|
+
if not rows:
|
|
467
|
+
continue
|
|
468
|
+
r = dict(rows[0])
|
|
469
|
+
acc = r.get("access_count") or 0
|
|
470
|
+
last_applied = r.get("fisher_last_applied_access") or 0
|
|
471
|
+
delta = acc - last_applied
|
|
472
|
+
if delta <= 0:
|
|
473
|
+
continue # no new accesses — variance unchanged this run
|
|
474
|
+
# Apply min(delta, 100) unit-information Bayesian updates.
|
|
475
|
+
# One update per access: 1/v_new = 1/v_old + 1 (unit obs_var).
|
|
476
|
+
current_var = list(f.fisher_variance)
|
|
477
|
+
dim = len(current_var)
|
|
478
|
+
obs_var = [1.0] * dim
|
|
479
|
+
applied = min(delta, 100)
|
|
480
|
+
for _ in range(applied):
|
|
481
|
+
current_var = frm.bayesian_update(current_var, obs_var)
|
|
482
|
+
# Single atomic write: variance + watermark together. Advance the
|
|
483
|
+
# watermark only by the number of updates ACTUALLY applied (not to
|
|
484
|
+
# acc), so accesses beyond the per-run cap are applied on subsequent
|
|
485
|
+
# runs instead of being silently dropped.
|
|
486
|
+
db.execute(
|
|
487
|
+
"UPDATE atomic_facts "
|
|
488
|
+
"SET fisher_variance = ?, fisher_last_applied_access = ? "
|
|
489
|
+
"WHERE fact_id = ?",
|
|
490
|
+
(_json.dumps(current_var), last_applied + applied, f.fact_id),
|
|
491
|
+
)
|
|
492
|
+
# Refresh in-memory so step 1d ELC sees the updated variance.
|
|
493
|
+
f.fisher_variance = current_var
|
|
494
|
+
posterior_count += 1
|
|
495
|
+
counts["fisher_posterior_updated"] = posterior_count
|
|
496
|
+
except Exception as exc:
|
|
497
|
+
logger.warning("Fisher posterior update failed: %s", exc)
|
|
498
|
+
|
|
499
|
+
# 1d. Ebbinghaus-Langevin coupling (Phase 5 — P1-ELC): combine forgetting
|
|
500
|
+
# drift with Fisher-Langevin dynamics to produce a unified lifecycle state.
|
|
501
|
+
# Updates the lifecycle zone of each fact based on Ebbinghaus retention.
|
|
502
|
+
# NOTE: this step overwrites the Langevin-only lifecycle set in step 1b.
|
|
503
|
+
# The Ebbinghaus zone is intentionally authoritative when ELC is ON.
|
|
504
|
+
# Gate: config.math.ebbinghaus_langevin_coupling_enabled (default False).
|
|
505
|
+
if config.math.ebbinghaus_langevin_coupling_enabled:
|
|
506
|
+
try:
|
|
507
|
+
from superlocalmemory.dynamics.ebbinghaus_langevin_coupling import (
|
|
508
|
+
EbbinghausLangevinCoupling,
|
|
509
|
+
)
|
|
510
|
+
from superlocalmemory.dynamics.fisher_langevin_coupling import (
|
|
511
|
+
FisherLangevinCoupling,
|
|
512
|
+
)
|
|
513
|
+
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
514
|
+
from superlocalmemory.math.langevin import LangevinDynamics
|
|
515
|
+
|
|
516
|
+
ebbinghaus = EbbinghausCurve(config.forgetting)
|
|
517
|
+
langevin = LangevinDynamics(
|
|
518
|
+
dim=_LANGEVIN_DIM,
|
|
519
|
+
dt=config.math.langevin_dt,
|
|
520
|
+
temperature=config.math.langevin_temperature,
|
|
521
|
+
)
|
|
522
|
+
fisher_coupling = FisherLangevinCoupling(
|
|
523
|
+
base_temperature=config.math.langevin_temperature,
|
|
524
|
+
)
|
|
525
|
+
coupling = EbbinghausLangevinCoupling(
|
|
526
|
+
ebbinghaus, langevin, fisher_coupling, config.forgetting,
|
|
527
|
+
)
|
|
528
|
+
import numpy as np
|
|
529
|
+
|
|
530
|
+
# Build fact_id → last_accessed_at lookup from fact_retention.
|
|
531
|
+
# Using real last-access time (not created_at) so hot facts are not
|
|
532
|
+
# mis-classified as forgotten due to old creation timestamps.
|
|
533
|
+
if facts:
|
|
534
|
+
retention_rows = db.execute(
|
|
535
|
+
"SELECT fact_id, last_accessed_at FROM fact_retention "
|
|
536
|
+
"WHERE fact_id IN ({})".format(",".join("?" * len(facts))),
|
|
537
|
+
tuple(f.fact_id for f in facts),
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
retention_rows = []
|
|
541
|
+
last_accessed_map: dict[str, str | None] = {
|
|
542
|
+
dict(r)["fact_id"]: dict(r)["last_accessed_at"]
|
|
543
|
+
for r in retention_rows
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
elc_count = 0
|
|
547
|
+
for f in facts:
|
|
548
|
+
if f.fisher_variance is None or f.langevin_position is None:
|
|
549
|
+
continue
|
|
550
|
+
# Prefer real last-access timestamp; fall back to created_at.
|
|
551
|
+
raw_ts = last_accessed_map.get(f.fact_id) or f.created_at
|
|
552
|
+
hours_since = _age_days(raw_ts) * 24.0
|
|
553
|
+
state = coupling.compute_coupled_state(
|
|
554
|
+
fact_id=f.fact_id,
|
|
555
|
+
fisher_variance=np.asarray(f.fisher_variance, dtype=np.float64),
|
|
556
|
+
langevin_radius=float(np.linalg.norm(f.langevin_position)),
|
|
557
|
+
access_count=f.access_count,
|
|
558
|
+
importance=f.importance,
|
|
559
|
+
confirmation_count=f.evidence_count,
|
|
560
|
+
emotional_salience=0.0,
|
|
561
|
+
hours_since_last_access=hours_since,
|
|
562
|
+
)
|
|
563
|
+
# Remap ELC zone vocabulary to atomic_facts CHECK constraint.
|
|
564
|
+
# EbbinghausCurve returns 'archive'/'forgotten'; schema only allows
|
|
565
|
+
# 'active|warm|cold|archived'.
|
|
566
|
+
zone = _ELC_ZONE_REMAP.get(state.lifecycle_zone, state.lifecycle_zone)
|
|
567
|
+
if zone not in _VALID_LIFECYCLE_ZONES:
|
|
568
|
+
logger.warning(
|
|
569
|
+
"ELC returned unknown lifecycle zone %r for fact %s — skipping",
|
|
570
|
+
state.lifecycle_zone, f.fact_id,
|
|
571
|
+
)
|
|
572
|
+
continue
|
|
573
|
+
# Count fact as processed regardless of whether we write.
|
|
574
|
+
elc_count += 1
|
|
575
|
+
# Skip write when zone hasn't changed — avoids O(N) UPDATEs per tick.
|
|
576
|
+
current_zone = (
|
|
577
|
+
f.lifecycle.value
|
|
578
|
+
if hasattr(f.lifecycle, "value")
|
|
579
|
+
else str(f.lifecycle)
|
|
580
|
+
)
|
|
581
|
+
if zone == current_zone:
|
|
582
|
+
continue
|
|
583
|
+
db.update_fact(f.fact_id, {"lifecycle": zone})
|
|
584
|
+
counts["ebbinghaus_coupled"] = elc_count
|
|
585
|
+
except Exception as exc:
|
|
586
|
+
logger.warning("Ebbinghaus-Langevin coupling failed: %s", exc)
|
|
587
|
+
|
|
333
588
|
# 2. Sheaf batch consistency on recent facts (last 24h)
|
|
334
589
|
if config.math.sheaf_at_encoding:
|
|
335
590
|
try:
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
4
|
|
|
5
|
-
"""SuperLocalMemory
|
|
5
|
+
"""SuperLocalMemory V4 mode capability descriptors.
|
|
6
6
|
|
|
7
7
|
Three operating modes with clear capability boundaries.
|
|
8
|
-
Mode A:
|
|
9
|
-
Mode B:
|
|
10
|
-
Mode C:
|
|
8
|
+
Mode A: deterministic local extraction and local inference.
|
|
9
|
+
Mode B: local LLM enrichment and local inference.
|
|
10
|
+
Mode C: configured provider-assisted inference.
|
|
11
|
+
|
|
12
|
+
An operating mode does not determine EU AI Act compliance. That assessment
|
|
13
|
+
depends on the deployment, intended use, operator role, and applicable duties.
|
|
11
14
|
|
|
12
15
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
13
16
|
"""
|
|
@@ -41,12 +44,20 @@ class ModeCapabilities:
|
|
|
41
44
|
embedding_dimension: int # Expected embedding dimension
|
|
42
45
|
|
|
43
46
|
# Compliance
|
|
44
|
-
eu_ai_act_compliant: bool
|
|
47
|
+
eu_ai_act_compliant: bool | None # None: requires deployment assessment
|
|
45
48
|
data_stays_local: bool # Does ALL data stay on device?
|
|
46
49
|
|
|
47
50
|
# Description
|
|
48
51
|
description: str = ""
|
|
49
52
|
|
|
53
|
+
@property
|
|
54
|
+
def data_locality_label(self) -> str:
|
|
55
|
+
"""UI/API locality label derived from the mode record (single source).
|
|
56
|
+
|
|
57
|
+
Never invent a parallel mode→label map in the dashboard — consume this.
|
|
58
|
+
"""
|
|
59
|
+
return "local-only" if self.data_stays_local else "provider-assisted"
|
|
60
|
+
|
|
50
61
|
|
|
51
62
|
# ---------------------------------------------------------------------------
|
|
52
63
|
# Mode Definitions
|
|
@@ -63,13 +74,13 @@ MODE_A = ModeCapabilities(
|
|
|
63
74
|
cloud_reranker=False,
|
|
64
75
|
cloud_embeddings=False,
|
|
65
76
|
embedding_dimension=768,
|
|
66
|
-
eu_ai_act_compliant=
|
|
77
|
+
eu_ai_act_compliant=None,
|
|
67
78
|
data_stays_local=True,
|
|
68
79
|
description=(
|
|
69
80
|
"Local Guardian — Zero LLM, zero cloud. "
|
|
70
81
|
"Uses nomic-embed-text-v1.5 encoder (768d, 8K context) for embeddings. "
|
|
71
|
-
"
|
|
72
|
-
"
|
|
82
|
+
"Deterministic rules for extraction and a local PyTorch cross-encoder. "
|
|
83
|
+
"EU AI Act classification requires deployment assessment."
|
|
73
84
|
),
|
|
74
85
|
)
|
|
75
86
|
|
|
@@ -84,13 +95,13 @@ MODE_B = ModeCapabilities(
|
|
|
84
95
|
cloud_reranker=False,
|
|
85
96
|
cloud_embeddings=False,
|
|
86
97
|
embedding_dimension=768,
|
|
87
|
-
eu_ai_act_compliant=
|
|
98
|
+
eu_ai_act_compliant=None,
|
|
88
99
|
data_stays_local=True,
|
|
89
100
|
description=(
|
|
90
101
|
"Smart Local — Local Ollama LLM (Phi-3, Llama 3.2). "
|
|
91
102
|
"LLM-quality extraction and classification, fully local. "
|
|
92
|
-
"
|
|
93
|
-
"
|
|
103
|
+
"Local PyTorch cross-encoder reranking. No configured cloud inference. "
|
|
104
|
+
"EU AI Act classification requires deployment assessment."
|
|
94
105
|
),
|
|
95
106
|
)
|
|
96
107
|
|
|
@@ -105,12 +116,13 @@ MODE_C = ModeCapabilities(
|
|
|
105
116
|
cloud_reranker=True,
|
|
106
117
|
cloud_embeddings=True,
|
|
107
118
|
embedding_dimension=3072,
|
|
108
|
-
eu_ai_act_compliant=
|
|
119
|
+
eu_ai_act_compliant=None,
|
|
109
120
|
data_stays_local=False,
|
|
110
121
|
description=(
|
|
111
122
|
"FULL POWER — UNRESTRICTED. Best embeddings (text-embedding-3-large, 3072-dim). "
|
|
112
123
|
"Best configured cloud LLMs (e.g. GPT-5, Claude Opus 4). Agentic multi-round retrieval. "
|
|
113
|
-
"Cohere reranker option.
|
|
124
|
+
"Cohere reranker option. Cloud processing requires deployment-specific "
|
|
125
|
+
"privacy, contractual, and EU AI Act assessment."
|
|
114
126
|
),
|
|
115
127
|
)
|
|
116
128
|
|
|
@@ -121,6 +133,21 @@ def get_capabilities(mode: Mode) -> ModeCapabilities:
|
|
|
121
133
|
return _map[mode]
|
|
122
134
|
|
|
123
135
|
|
|
136
|
+
def dashboard_mode_fields(mode: Mode | str) -> dict[str, object]:
|
|
137
|
+
"""Fields the dashboard API must surface from the mode record.
|
|
138
|
+
|
|
139
|
+
Keeps UI locality claims bound to :class:`ModeCapabilities` so Mode C
|
|
140
|
+
cannot be labeled local-only by a divergent hardcode.
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(mode, str):
|
|
143
|
+
mode = Mode(mode.strip().lower())
|
|
144
|
+
caps = get_capabilities(mode)
|
|
145
|
+
return {
|
|
146
|
+
"data_locality_label": caps.data_locality_label,
|
|
147
|
+
"data_stays_local": caps.data_stays_local,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
124
151
|
def validate_mode_config(mode: Mode, *, has_ollama: bool = False, has_cloud_llm: bool = False) -> list[str]:
|
|
125
152
|
"""Validate that required services are available for the chosen mode.
|
|
126
153
|
|