superlocalmemory 3.7.7 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- 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/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- 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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- 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 +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- 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 +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- 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/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 +5 -43
- 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 +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- 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/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- 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 +2 -0
- 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 +35 -11
- 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/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- 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/reward_proxy.py +42 -9
- 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 +243 -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 +21 -49
- 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 +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- 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 +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- 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 +21 -4
- 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 +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- 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 +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- 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 +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -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/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- 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 +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- 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 +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -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 +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -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 +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -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 +2 -2
- 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/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Reflexion self-review over LLM-extracted entities (Wave Q1).
|
|
6
|
+
|
|
7
|
+
After primary LLM extraction (Mode B/C only), one bounded self-review pass
|
|
8
|
+
audits the extracted entities against the source text and:
|
|
9
|
+
|
|
10
|
+
- drops hallucinated entities (not supported by the source), and
|
|
11
|
+
- adds clearly-missed entities (whose exact text appears in the source).
|
|
12
|
+
|
|
13
|
+
This closes the two distinct extraction failure modes the market leaders
|
|
14
|
+
separate: coverage (missed) and grounding (hallucinated). Coverage is the
|
|
15
|
+
"gleaning" idea (re-present what was extracted, ask for misses); grounding is
|
|
16
|
+
the "reflexion" idea (verify each extracted entity is real).
|
|
17
|
+
|
|
18
|
+
Design guarantees:
|
|
19
|
+
- Mode A never reaches here — the caller gates on Mode B/C + LLM available.
|
|
20
|
+
- Fail-open: any error, unavailable LLM, or unparseable output returns the
|
|
21
|
+
input facts unchanged. This module NEVER raises.
|
|
22
|
+
- Immutable: corrected facts are new objects; inputs are not mutated.
|
|
23
|
+
- Bounded: one LLM call, capped facts reviewed, capped entities per fact.
|
|
24
|
+
|
|
25
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
26
|
+
License: AGPL-3.0-or-later
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import re
|
|
34
|
+
from dataclasses import replace
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from superlocalmemory.storage.models import AtomicFact
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_MAX_ENTITIES_PER_FACT = 12
|
|
42
|
+
_MIN_ENTITY_LEN = 2
|
|
43
|
+
|
|
44
|
+
_SYSTEM = (
|
|
45
|
+
"You are a precise information-extraction auditor. You verify that "
|
|
46
|
+
"entities extracted from text are actually supported by that text, and "
|
|
47
|
+
"you catch clearly-named entities that were missed. You never invent "
|
|
48
|
+
"entities. You reply with JSON only."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EntityReflexion:
|
|
53
|
+
"""Bounded, fail-open reflexion pass over extracted entities."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
llm: Any,
|
|
58
|
+
max_facts: int = 8,
|
|
59
|
+
max_tokens: int = 512,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._llm = llm
|
|
62
|
+
self._max_facts = max(1, int(max_facts))
|
|
63
|
+
self._max_tokens = max(64, int(max_tokens))
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Public API
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def refine(
|
|
70
|
+
self, source_text: str, facts: list[AtomicFact],
|
|
71
|
+
) -> list[AtomicFact]:
|
|
72
|
+
"""Return facts with entity lists corrected, or unchanged on failure."""
|
|
73
|
+
if not self._llm or not facts or not source_text or not source_text.strip():
|
|
74
|
+
return facts
|
|
75
|
+
try:
|
|
76
|
+
is_avail = getattr(self._llm, "is_available", None)
|
|
77
|
+
if callable(is_avail) and not is_avail():
|
|
78
|
+
return facts
|
|
79
|
+
except Exception:
|
|
80
|
+
return facts
|
|
81
|
+
|
|
82
|
+
subject = facts[: self._max_facts]
|
|
83
|
+
try:
|
|
84
|
+
raw = self._invoke(source_text, subject)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
logger.debug("EntityReflexion LLM call failed: %s", exc)
|
|
87
|
+
return facts
|
|
88
|
+
|
|
89
|
+
corrections = self._parse(raw)
|
|
90
|
+
if not corrections:
|
|
91
|
+
return facts
|
|
92
|
+
try:
|
|
93
|
+
return self._apply(facts, corrections, source_text)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
logger.debug("EntityReflexion apply failed: %s", exc)
|
|
96
|
+
return facts
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
# Internal
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _invoke(self, source_text: str, subject: list[AtomicFact]) -> str:
|
|
103
|
+
lines = []
|
|
104
|
+
for i, f in enumerate(subject):
|
|
105
|
+
lines.append(f'{i}: "{f.content}" entities={list(f.entities)}')
|
|
106
|
+
facts_block = "\n".join(lines)
|
|
107
|
+
prompt = (
|
|
108
|
+
"Audit the extracted entities for each fact against the SOURCE "
|
|
109
|
+
"text below.\n"
|
|
110
|
+
"For each fact, report:\n"
|
|
111
|
+
"- drop: entities in the fact's list that are NOT supported by the "
|
|
112
|
+
"source (hallucinated).\n"
|
|
113
|
+
"- add: named entities clearly present in the source but missing "
|
|
114
|
+
"from the fact's list. Only add entities whose exact text appears "
|
|
115
|
+
"in the source.\n\n"
|
|
116
|
+
f"--- SOURCE ---\n{source_text}\n--- END ---\n\n"
|
|
117
|
+
f"--- FACTS ---\n{facts_block}\n--- END ---\n\n"
|
|
118
|
+
'Respond with ONLY a JSON array: '
|
|
119
|
+
'[{"index": <int>, "drop": [..], "add": [..]}]. '
|
|
120
|
+
"Use [] for an empty list. Omit facts that need no change."
|
|
121
|
+
)
|
|
122
|
+
out = self._llm.generate(
|
|
123
|
+
prompt=prompt,
|
|
124
|
+
system=_SYSTEM,
|
|
125
|
+
temperature=0.0,
|
|
126
|
+
max_tokens=self._max_tokens,
|
|
127
|
+
)
|
|
128
|
+
return out if isinstance(out, str) else str(out or "")
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _parse(raw: str) -> list[dict]:
|
|
132
|
+
if not raw or not raw.strip():
|
|
133
|
+
return []
|
|
134
|
+
match = re.search(r"\[.*\]", raw, re.DOTALL)
|
|
135
|
+
if not match:
|
|
136
|
+
return []
|
|
137
|
+
try:
|
|
138
|
+
data = json.loads(match.group())
|
|
139
|
+
except (json.JSONDecodeError, ValueError):
|
|
140
|
+
return []
|
|
141
|
+
if not isinstance(data, list):
|
|
142
|
+
return []
|
|
143
|
+
return [d for d in data if isinstance(d, dict)]
|
|
144
|
+
|
|
145
|
+
def _apply(
|
|
146
|
+
self,
|
|
147
|
+
facts: list[AtomicFact],
|
|
148
|
+
corrections: list[dict],
|
|
149
|
+
source_text: str,
|
|
150
|
+
) -> list[AtomicFact]:
|
|
151
|
+
by_index: dict[int, dict] = {}
|
|
152
|
+
for c in corrections:
|
|
153
|
+
idx = c.get("index")
|
|
154
|
+
if isinstance(idx, int) and 0 <= idx < self._max_facts:
|
|
155
|
+
by_index[idx] = c
|
|
156
|
+
|
|
157
|
+
src_low = source_text.lower()
|
|
158
|
+
out: list[AtomicFact] = []
|
|
159
|
+
for i, fact in enumerate(facts):
|
|
160
|
+
c = by_index.get(i)
|
|
161
|
+
if c is None:
|
|
162
|
+
out.append(fact)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
ents = list(fact.entities)
|
|
166
|
+
drop = {
|
|
167
|
+
str(d).strip().lower()
|
|
168
|
+
for d in _as_list(c.get("drop"))
|
|
169
|
+
if str(d).strip()
|
|
170
|
+
}
|
|
171
|
+
if drop:
|
|
172
|
+
ents = [e for e in ents if e.strip().lower() not in drop]
|
|
173
|
+
|
|
174
|
+
seen = {e.strip().lower() for e in ents}
|
|
175
|
+
for a in _as_list(c.get("add")):
|
|
176
|
+
cand = str(a).strip()
|
|
177
|
+
low = cand.lower()
|
|
178
|
+
if len(cand) < _MIN_ENTITY_LEN or low in seen:
|
|
179
|
+
continue
|
|
180
|
+
# Grounding guard: only add entities literally present in source.
|
|
181
|
+
if low not in src_low:
|
|
182
|
+
continue
|
|
183
|
+
ents.append(cand)
|
|
184
|
+
seen.add(low)
|
|
185
|
+
if len(ents) >= _MAX_ENTITIES_PER_FACT:
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
if ents != list(fact.entities):
|
|
189
|
+
out.append(replace(fact, entities=ents))
|
|
190
|
+
else:
|
|
191
|
+
out.append(fact)
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _as_list(value: Any) -> list:
|
|
196
|
+
if isinstance(value, list):
|
|
197
|
+
return value
|
|
198
|
+
if value is None:
|
|
199
|
+
return []
|
|
200
|
+
return [value]
|
|
@@ -283,22 +283,22 @@ class EntityResolver:
|
|
|
283
283
|
entity = self._db.get_entity_by_name(name, profile_id)
|
|
284
284
|
if entity is not None:
|
|
285
285
|
resolution[raw] = entity.entity_id
|
|
286
|
-
self._touch_last_seen(entity.entity_id)
|
|
286
|
+
self._touch_last_seen(entity.entity_id, profile_id)
|
|
287
287
|
continue
|
|
288
288
|
|
|
289
289
|
# Tier b: alias match (case-insensitive, indexed)
|
|
290
290
|
entity_id = self._alias_lookup(name, profile_id)
|
|
291
291
|
if entity_id is not None:
|
|
292
292
|
resolution[raw] = entity_id
|
|
293
|
-
self._touch_last_seen(entity_id)
|
|
293
|
+
self._touch_last_seen(entity_id, profile_id)
|
|
294
294
|
continue
|
|
295
295
|
|
|
296
296
|
# Tier c: fuzzy match via Jaro-Winkler
|
|
297
297
|
match_id, score = self._fuzzy_match(name, profile_id)
|
|
298
298
|
if match_id is not None and score >= JARO_WINKLER_AUTO_MERGE:
|
|
299
299
|
resolution[raw] = match_id
|
|
300
|
-
self._persist_alias(match_id, name, score, "jaro_winkler")
|
|
301
|
-
self._touch_last_seen(match_id)
|
|
300
|
+
self._persist_alias(match_id, name, score, "jaro_winkler", profile_id)
|
|
301
|
+
self._touch_last_seen(match_id, profile_id)
|
|
302
302
|
continue
|
|
303
303
|
|
|
304
304
|
# Candidate zone (0.70–0.85): queue for LLM in Mode B/C
|
|
@@ -384,7 +384,7 @@ class EntityResolver:
|
|
|
384
384
|
and removes the merged entity record.
|
|
385
385
|
"""
|
|
386
386
|
# Move aliases from merge -> keep
|
|
387
|
-
aliases = self._db.get_aliases_for_entity(entity_id_merge)
|
|
387
|
+
aliases = self._db.get_aliases_for_entity(entity_id_merge, profile_id)
|
|
388
388
|
for alias in aliases:
|
|
389
389
|
new_alias = EntityAlias(
|
|
390
390
|
alias_id=_new_id(),
|
|
@@ -393,7 +393,7 @@ class EntityResolver:
|
|
|
393
393
|
confidence=alias.confidence,
|
|
394
394
|
source=f"merge_from:{entity_id_merge}",
|
|
395
395
|
)
|
|
396
|
-
self._db.store_alias(new_alias)
|
|
396
|
+
self._db.store_alias(new_alias, profile_id)
|
|
397
397
|
|
|
398
398
|
# Also add the merged entity's canonical name as an alias of keep
|
|
399
399
|
merged = self._db.get_entity_by_name("", "") # placeholder
|
|
@@ -403,7 +403,7 @@ class EntityResolver:
|
|
|
403
403
|
)
|
|
404
404
|
if rows:
|
|
405
405
|
merged_name = str(dict(rows[0])["canonical_name"])
|
|
406
|
-
self._persist_alias(entity_id_keep, merged_name, 1.0, "merge")
|
|
406
|
+
self._persist_alias(entity_id_keep, merged_name, 1.0, "merge", profile_id)
|
|
407
407
|
|
|
408
408
|
# Update atomic_facts: replace entity_id_merge with entity_id_keep
|
|
409
409
|
# in canonical_entities_json column
|
|
@@ -435,14 +435,14 @@ class EntityResolver:
|
|
|
435
435
|
except (json.JSONDecodeError, TypeError):
|
|
436
436
|
continue
|
|
437
437
|
|
|
438
|
-
# Delete the merged entity
|
|
438
|
+
# Delete the merged entity (tenant-scoped)
|
|
439
439
|
self._db.execute(
|
|
440
|
-
"DELETE FROM entity_aliases WHERE entity_id = ?",
|
|
441
|
-
(entity_id_merge,),
|
|
440
|
+
"DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
|
|
441
|
+
(entity_id_merge, profile_id),
|
|
442
442
|
)
|
|
443
443
|
self._db.execute(
|
|
444
|
-
"DELETE FROM canonical_entities WHERE entity_id = ?",
|
|
445
|
-
(entity_id_merge,),
|
|
444
|
+
"DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
|
|
445
|
+
(entity_id_merge, profile_id),
|
|
446
446
|
)
|
|
447
447
|
logger.info(
|
|
448
448
|
"Merged entity %s into %s (profile=%s)",
|
|
@@ -526,7 +526,7 @@ class EntityResolver:
|
|
|
526
526
|
self._db.store_entity(entity)
|
|
527
527
|
|
|
528
528
|
# Store name as its own alias for uniform lookup
|
|
529
|
-
self._persist_alias(entity.entity_id, name, 1.0, "canonical")
|
|
529
|
+
self._persist_alias(entity.entity_id, name, 1.0, "canonical", profile_id)
|
|
530
530
|
|
|
531
531
|
logger.debug(
|
|
532
532
|
"Created entity '%s' [%s] (type=%s, profile=%s)",
|
|
@@ -540,13 +540,16 @@ class EntityResolver:
|
|
|
540
540
|
alias_text: str,
|
|
541
541
|
confidence: float,
|
|
542
542
|
source: str,
|
|
543
|
+
profile_id: str,
|
|
543
544
|
) -> None:
|
|
544
|
-
"""Store an alias, skipping duplicates."""
|
|
545
|
-
# Check if alias already exists for this entity
|
|
545
|
+
"""Store an alias under a profile, skipping duplicates within it."""
|
|
546
|
+
# Check if alias already exists for this entity IN THIS PROFILE. The
|
|
547
|
+
# dedup must be profile-scoped or one profile's alias suppresses
|
|
548
|
+
# another profile's identical alias for a same-id entity.
|
|
546
549
|
existing = self._db.execute(
|
|
547
550
|
"SELECT alias_id FROM entity_aliases "
|
|
548
|
-
"WHERE entity_id = ? AND LOWER(alias) = LOWER(?)",
|
|
549
|
-
(entity_id, alias_text),
|
|
551
|
+
"WHERE entity_id = ? AND profile_id = ? AND LOWER(alias) = LOWER(?)",
|
|
552
|
+
(entity_id, profile_id, alias_text),
|
|
550
553
|
)
|
|
551
554
|
if existing:
|
|
552
555
|
return
|
|
@@ -557,13 +560,20 @@ class EntityResolver:
|
|
|
557
560
|
confidence=confidence,
|
|
558
561
|
source=source,
|
|
559
562
|
)
|
|
560
|
-
self._db.store_alias(alias)
|
|
563
|
+
self._db.store_alias(alias, profile_id)
|
|
561
564
|
|
|
562
|
-
def _touch_last_seen(self, entity_id: str) -> None:
|
|
563
|
-
"""Update last_seen timestamp on a canonical entity.
|
|
565
|
+
def _touch_last_seen(self, entity_id: str, profile_id: str = "default") -> None:
|
|
566
|
+
"""Update last_seen timestamp on a canonical entity scoped to profile.
|
|
567
|
+
|
|
568
|
+
L-01 fix: the original query had no profile_id guard. Since entity_ids are
|
|
569
|
+
UUIDs today the blast radius is theoretical, but the guard is required for
|
|
570
|
+
defense-in-depth against future import/sharing features that could introduce
|
|
571
|
+
UUID collisions across profiles.
|
|
572
|
+
"""
|
|
564
573
|
self._db.execute(
|
|
565
|
-
"UPDATE canonical_entities SET last_seen = ?
|
|
566
|
-
|
|
574
|
+
"UPDATE canonical_entities SET last_seen = ? "
|
|
575
|
+
"WHERE entity_id = ? AND profile_id = ?",
|
|
576
|
+
(_now(), entity_id, profile_id),
|
|
567
577
|
)
|
|
568
578
|
|
|
569
579
|
# -- Internal: LLM disambiguation (Mode B/C) ---------------------------
|
|
@@ -621,9 +631,9 @@ class EntityResolver:
|
|
|
621
631
|
entity_id = known[name_str]
|
|
622
632
|
resolved[mention_str] = entity_id
|
|
623
633
|
self._persist_alias(
|
|
624
|
-
entity_id, mention_str, 0.9, "llm",
|
|
634
|
+
entity_id, mention_str, 0.9, "llm", profile_id,
|
|
625
635
|
)
|
|
626
|
-
self._touch_last_seen(entity_id)
|
|
636
|
+
self._touch_last_seen(entity_id, profile_id)
|
|
627
637
|
# If LLM says it's itself, leave for caller to create
|
|
628
638
|
return resolved
|
|
629
639
|
|
|
@@ -685,11 +685,36 @@ class FactExtractor:
|
|
|
685
685
|
temperature=0.0,
|
|
686
686
|
max_tokens=1024,
|
|
687
687
|
)
|
|
688
|
-
|
|
688
|
+
facts = self._parse_llm_response(raw, session_id, session_date)
|
|
689
|
+
return self._reflexion_refine(conversation_text, facts)
|
|
689
690
|
except Exception as exc:
|
|
690
691
|
logger.warning("LLM fact extraction failed: %s", exc)
|
|
691
692
|
return []
|
|
692
693
|
|
|
694
|
+
def _reflexion_refine(
|
|
695
|
+
self, source_text: str, facts: list[AtomicFact],
|
|
696
|
+
) -> list[AtomicFact]:
|
|
697
|
+
"""Wave Q1: bounded, fail-open entity self-review (Mode B/C only).
|
|
698
|
+
|
|
699
|
+
Reached only from the LLM extraction path, so Mode A never runs this.
|
|
700
|
+
Any failure returns the facts unchanged.
|
|
701
|
+
"""
|
|
702
|
+
if not facts or not getattr(
|
|
703
|
+
self._config, "enable_entity_reflexion", True,
|
|
704
|
+
):
|
|
705
|
+
return facts
|
|
706
|
+
try:
|
|
707
|
+
from superlocalmemory.encoding.entity_reflexion import EntityReflexion
|
|
708
|
+
|
|
709
|
+
reflexion = EntityReflexion(
|
|
710
|
+
self._llm,
|
|
711
|
+
max_facts=getattr(self._config, "reflexion_max_facts", 8),
|
|
712
|
+
)
|
|
713
|
+
return reflexion.refine(source_text, facts)
|
|
714
|
+
except Exception as exc:
|
|
715
|
+
logger.debug("Entity reflexion skipped: %s", exc)
|
|
716
|
+
return facts
|
|
717
|
+
|
|
693
718
|
def _parse_llm_response(
|
|
694
719
|
self,
|
|
695
720
|
raw: str,
|
|
@@ -96,6 +96,18 @@ class TemporalValidator:
|
|
|
96
96
|
severity = contradiction["severity"]
|
|
97
97
|
reason = contradiction["description"]
|
|
98
98
|
|
|
99
|
+
# Temporal-anchor guard: if the new fact and the old fact carry
|
|
100
|
+
# distinct explicit event anchors, they describe the same subject at
|
|
101
|
+
# different times (a valid timeline — "lived in Delhi (2020)" vs
|
|
102
|
+
# "lives in Mumbai (2024)"), NOT a contradiction. Do not supersede;
|
|
103
|
+
# both stay recallable so historical queries still find them.
|
|
104
|
+
if self._is_historical_progression(new_fact, old_fact_id):
|
|
105
|
+
logger.debug(
|
|
106
|
+
"Temporal: %s vs %s have distinct event anchors — historical "
|
|
107
|
+
"progression, not superseding", new_fact.fact_id, old_fact_id,
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
99
111
|
# Step 1: Invalidate the old fact (bi-temporal)
|
|
100
112
|
self.invalidate_fact(
|
|
101
113
|
fact_id=old_fact_id,
|
|
@@ -119,6 +131,54 @@ class TemporalValidator:
|
|
|
119
131
|
)
|
|
120
132
|
return actions
|
|
121
133
|
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _event_anchor(
|
|
136
|
+
referenced_date: str | None,
|
|
137
|
+
observation_date: str | None,
|
|
138
|
+
interval_start: str | None,
|
|
139
|
+
) -> str | None:
|
|
140
|
+
"""The most-specific explicit event time, or None when undated."""
|
|
141
|
+
return referenced_date or observation_date or interval_start or None
|
|
142
|
+
|
|
143
|
+
def _is_historical_progression(
|
|
144
|
+
self, new_fact: AtomicFact, old_fact_id: str,
|
|
145
|
+
) -> bool:
|
|
146
|
+
"""True when new_fact and the old fact have distinct explicit event
|
|
147
|
+
anchors (same subject, different times) — a valid timeline rather than a
|
|
148
|
+
contradiction. Conservative: False unless BOTH anchors are present and
|
|
149
|
+
resolve to different calendar days, so undated facts supersede as before.
|
|
150
|
+
"""
|
|
151
|
+
new_anchor = self._event_anchor(
|
|
152
|
+
getattr(new_fact, "referenced_date", None),
|
|
153
|
+
getattr(new_fact, "observation_date", None),
|
|
154
|
+
getattr(new_fact, "interval_start", None),
|
|
155
|
+
)
|
|
156
|
+
if not new_anchor:
|
|
157
|
+
return False
|
|
158
|
+
try:
|
|
159
|
+
rows = self._db.execute(
|
|
160
|
+
"SELECT referenced_date, observation_date, interval_start "
|
|
161
|
+
"FROM atomic_facts WHERE fact_id = ?",
|
|
162
|
+
(old_fact_id,),
|
|
163
|
+
)
|
|
164
|
+
except Exception:
|
|
165
|
+
return False
|
|
166
|
+
if not rows:
|
|
167
|
+
return False
|
|
168
|
+
d = dict(rows[0])
|
|
169
|
+
old_anchor = self._event_anchor(
|
|
170
|
+
d.get("referenced_date"), d.get("observation_date"),
|
|
171
|
+
d.get("interval_start"),
|
|
172
|
+
)
|
|
173
|
+
if not old_anchor:
|
|
174
|
+
return False
|
|
175
|
+
from superlocalmemory.retrieval.time_window import parse_timestamp
|
|
176
|
+
nd = parse_timestamp(new_anchor)
|
|
177
|
+
od = parse_timestamp(old_anchor)
|
|
178
|
+
if nd is None or od is None:
|
|
179
|
+
return False
|
|
180
|
+
return nd.date() != od.date()
|
|
181
|
+
|
|
122
182
|
def detect_contradiction(
|
|
123
183
|
self,
|
|
124
184
|
new_fact: AtomicFact,
|
|
@@ -206,7 +266,10 @@ class TemporalValidator:
|
|
|
206
266
|
Both params are REQUIRED in the call site. Do NOT rename to is_valid().
|
|
207
267
|
"""
|
|
208
268
|
try:
|
|
209
|
-
|
|
269
|
+
# profile_id or None: an empty-string default must NOT become a
|
|
270
|
+
# `profile_id = ''` filter (which never matches → an expired fact
|
|
271
|
+
# would be reported valid). None falls back to the unscoped lookup.
|
|
272
|
+
tv = self._db.get_temporal_validity(fact_id, profile_id or None)
|
|
210
273
|
if tv is None:
|
|
211
274
|
return True # No temporal record = assumed valid
|
|
212
275
|
return (
|