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
|
@@ -11,8 +11,10 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import hashlib
|
|
15
15
|
import json
|
|
16
|
+
import logging
|
|
17
|
+
import uuid
|
|
16
18
|
from typing import TYPE_CHECKING, Any
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
@@ -21,7 +23,9 @@ if TYPE_CHECKING:
|
|
|
21
23
|
from superlocalmemory.storage.database import DatabaseManager
|
|
22
24
|
|
|
23
25
|
from superlocalmemory.storage.models import (
|
|
24
|
-
AtomicFact,
|
|
26
|
+
AtomicFact,
|
|
27
|
+
FactType,
|
|
28
|
+
MemoryRecord,
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
logger = logging.getLogger(__name__)
|
|
@@ -30,6 +34,63 @@ logger = logging.getLogger(__name__)
|
|
|
30
34
|
_INIT_LANGEVIN_RADIUS = 0.05
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
def _ingestion_effect_id(operation_id: str, *parts: object) -> str:
|
|
38
|
+
"""Return a stable ID for a relational effect owned by one ingestion."""
|
|
39
|
+
if not operation_id:
|
|
40
|
+
return uuid.uuid4().hex
|
|
41
|
+
payload = "\0".join((operation_id, *(str(part) for part in parts)))
|
|
42
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _record_fact_entity_association(
|
|
46
|
+
db: DatabaseManager,
|
|
47
|
+
*,
|
|
48
|
+
operation_id: str,
|
|
49
|
+
profile_id: str,
|
|
50
|
+
fact_id: str,
|
|
51
|
+
entity_id: str,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Apply one fact/entity count effect in O(1), exactly once."""
|
|
54
|
+
if not operation_id:
|
|
55
|
+
db.increment_entity_fact_count(entity_id, profile_id)
|
|
56
|
+
return
|
|
57
|
+
with db.transaction():
|
|
58
|
+
claimed = db.execute(
|
|
59
|
+
"INSERT INTO fact_entity_associations "
|
|
60
|
+
"(profile_id,fact_id,entity_id,first_operation_id,count_applied) "
|
|
61
|
+
"SELECT ?,?,?,?,"
|
|
62
|
+
"CASE WHEN fact.rowid > repair.target_fact_rowid THEN 1 ELSE 0 END "
|
|
63
|
+
"FROM canonical_entities AS entity "
|
|
64
|
+
"JOIN atomic_facts AS fact "
|
|
65
|
+
"ON fact.fact_id=? AND fact.profile_id=? "
|
|
66
|
+
"JOIN fact_entity_association_repair_state AS repair "
|
|
67
|
+
"ON repair.repair_key='historical-backfill' "
|
|
68
|
+
"WHERE entity.entity_id=? AND entity.profile_id=? "
|
|
69
|
+
"ON CONFLICT(profile_id,fact_id,entity_id) DO UPDATE SET "
|
|
70
|
+
"count_applied=excluded.count_applied,"
|
|
71
|
+
"first_operation_id=excluded.first_operation_id "
|
|
72
|
+
"WHERE fact_entity_associations.count_applied=0 "
|
|
73
|
+
"AND excluded.count_applied=1 "
|
|
74
|
+
"RETURNING count_applied",
|
|
75
|
+
(
|
|
76
|
+
profile_id,
|
|
77
|
+
fact_id,
|
|
78
|
+
entity_id,
|
|
79
|
+
operation_id,
|
|
80
|
+
fact_id,
|
|
81
|
+
profile_id,
|
|
82
|
+
entity_id,
|
|
83
|
+
profile_id,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
if claimed and int(claimed[0]["count_applied"]) == 1:
|
|
87
|
+
db.execute(
|
|
88
|
+
"UPDATE canonical_entities SET fact_count=fact_count+1 "
|
|
89
|
+
"WHERE entity_id=? AND profile_id=?",
|
|
90
|
+
(entity_id, profile_id),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
33
94
|
def _init_langevin_position(dim: int = 8) -> list[float]:
|
|
34
95
|
"""Initialize Langevin position near origin for a new fact.
|
|
35
96
|
|
|
@@ -60,7 +121,7 @@ def enrich_fact(
|
|
|
60
121
|
temporal_parser: Any,
|
|
61
122
|
) -> AtomicFact:
|
|
62
123
|
"""Enrich fact with embeddings, entities, temporal, emotional data."""
|
|
63
|
-
from superlocalmemory.encoding.emotional import
|
|
124
|
+
from superlocalmemory.encoding.emotional import emotional_importance_boost, tag_emotion
|
|
64
125
|
from superlocalmemory.encoding.signal_inference import infer_signal
|
|
65
126
|
|
|
66
127
|
embedding = embedder.embed(fact.content) if embedder else None
|
|
@@ -189,6 +250,9 @@ def run_store(
|
|
|
189
250
|
ingestion_source_type: str = "store",
|
|
190
251
|
ingestion_operation_id: str = "",
|
|
191
252
|
derivation_report: dict[str, bool] | None = None,
|
|
253
|
+
precompleted_derivation_stages: frozenset[str] = frozenset(),
|
|
254
|
+
materialization_progress: dict[str, Any] | None = None,
|
|
255
|
+
materialization_checkpoint: Any = None,
|
|
192
256
|
) -> list[str]:
|
|
193
257
|
"""Store content and extract structured facts. Returns fact_ids.
|
|
194
258
|
|
|
@@ -208,21 +272,33 @@ def run_store(
|
|
|
208
272
|
if not pre_authorized:
|
|
209
273
|
hooks.run_pre("store", hook_ctx)
|
|
210
274
|
|
|
211
|
-
|
|
275
|
+
# Admission gates apply to FRESH submissions only. A materialization pass
|
|
276
|
+
# re-runs this pipeline for content whose queryable projection was already
|
|
277
|
+
# committed at submit (``queryable_fact_ids`` is set). Re-applying admission
|
|
278
|
+
# here — the entropy near-duplicate gate especially — would discard that
|
|
279
|
+
# already-committed fact (return []) and wedge the operation in an endless
|
|
280
|
+
# materialize retry loop ("materialization produced no final facts"). The
|
|
281
|
+
# near-duplicate verdict is expected at materialize: the submitted
|
|
282
|
+
# projection itself is in the gate's window. Admission was already decided
|
|
283
|
+
# at submit (store_fast enforces the same gates), so skip it here.
|
|
284
|
+
is_materialization = bool(queryable_fact_ids)
|
|
285
|
+
|
|
286
|
+
if entropy_gate and not is_materialization and not entropy_gate.should_pass(content):
|
|
212
287
|
return []
|
|
213
288
|
|
|
214
289
|
# v3.5.0: store-side quality gate (H3). Reject prompt-template leakage,
|
|
215
290
|
# empty placeholders, and other non-memory content BEFORE it enters the
|
|
216
291
|
# DB. Uses the shared is_low_quality from core/injection so both store
|
|
217
292
|
# AND injection filter by identical rules. Saves DB IO + recall pollution.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
293
|
+
if not is_materialization:
|
|
294
|
+
try:
|
|
295
|
+
from superlocalmemory.core.injection import is_low_quality
|
|
296
|
+
if is_low_quality(content):
|
|
297
|
+
logger.debug("Store rejected (low-quality content): %s...",
|
|
298
|
+
content[:80].replace("\n", " "))
|
|
299
|
+
return []
|
|
300
|
+
except Exception:
|
|
301
|
+
pass # Best-effort gate; store succeeds if import fails
|
|
226
302
|
|
|
227
303
|
from superlocalmemory.encoding.temporal_parser import TemporalParser
|
|
228
304
|
parser = temporal_parser or TemporalParser()
|
|
@@ -274,10 +350,19 @@ def run_store(
|
|
|
274
350
|
)
|
|
275
351
|
db.store_memory(record)
|
|
276
352
|
|
|
277
|
-
extraction_complete =
|
|
278
|
-
consolidation_complete =
|
|
279
|
-
|
|
280
|
-
|
|
353
|
+
extraction_complete = "extraction" in precompleted_derivation_stages
|
|
354
|
+
consolidation_complete = (
|
|
355
|
+
"consolidation" in precompleted_derivation_stages
|
|
356
|
+
or consolidator is not None
|
|
357
|
+
)
|
|
358
|
+
canonicalization_complete = (
|
|
359
|
+
"canonicalization" in precompleted_derivation_stages
|
|
360
|
+
or entity_resolver is not None
|
|
361
|
+
)
|
|
362
|
+
graph_complete = (
|
|
363
|
+
"graph" in precompleted_derivation_stages
|
|
364
|
+
or graph_builder is not None
|
|
365
|
+
)
|
|
281
366
|
temporal_complete = True
|
|
282
367
|
provenance_complete = provenance is not None
|
|
283
368
|
|
|
@@ -318,7 +403,6 @@ def run_store(
|
|
|
318
403
|
and content.strip()
|
|
319
404
|
and len(content.strip()) >= 40
|
|
320
405
|
and len(content.strip().split()) >= _MIN_VERBATIM_WORDS):
|
|
321
|
-
import uuid
|
|
322
406
|
import re as _re
|
|
323
407
|
_verbatim_text = content.strip()
|
|
324
408
|
# Extract entities using the same regex as fact_extractor
|
|
@@ -362,7 +446,6 @@ def run_store(
|
|
|
362
446
|
# their data should NEVER be silently dropped. The min-length and min-word filters
|
|
363
447
|
# are designed for automatic conversation extraction, not explicit user storage.
|
|
364
448
|
if not facts and content.strip():
|
|
365
|
-
import uuid
|
|
366
449
|
facts = [AtomicFact(
|
|
367
450
|
fact_id=uuid.uuid4().hex[:16],
|
|
368
451
|
content=content.strip(),
|
|
@@ -391,6 +474,22 @@ def run_store(
|
|
|
391
474
|
temporal_parser=temporal_parser,
|
|
392
475
|
)
|
|
393
476
|
|
|
477
|
+
if (
|
|
478
|
+
materialization_progress is not None
|
|
479
|
+
and not materialization_progress.get("relational_started", False)
|
|
480
|
+
):
|
|
481
|
+
if materialization_checkpoint is not None:
|
|
482
|
+
materialization_checkpoint(
|
|
483
|
+
"relational_started",
|
|
484
|
+
(),
|
|
485
|
+
{
|
|
486
|
+
"pipeline_started": True,
|
|
487
|
+
"relational_started": True,
|
|
488
|
+
"pipeline": False,
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
materialization_progress["relational_started"] = True
|
|
492
|
+
|
|
394
493
|
is_queryable_promotion = fact.fact_id in queryable_ids
|
|
395
494
|
if is_queryable_promotion:
|
|
396
495
|
db.update_fact(fact.fact_id, {
|
|
@@ -503,6 +602,8 @@ def run_store(
|
|
|
503
602
|
|
|
504
603
|
if fact.fact_id not in stored_ids:
|
|
505
604
|
stored_ids.append(fact.fact_id)
|
|
605
|
+
if materialization_progress is not None:
|
|
606
|
+
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
506
607
|
|
|
507
608
|
# Dual-write embedding to ANN index + vector store (embed on-demand if
|
|
508
609
|
# a consolidated ADD fact arrived without one). See _upsert_fact_vectors.
|
|
@@ -583,16 +684,33 @@ def run_store(
|
|
|
583
684
|
fact.fact_id, exc,
|
|
584
685
|
)
|
|
585
686
|
|
|
687
|
+
# Phase 4b: fact-augmented key expansion (T3b). Index entity aliases /
|
|
688
|
+
# canonical names as BM25 alt-keys so paraphrased queries match. Mode A
|
|
689
|
+
# (entity graph) is zero-LLM and safe on the hot store path; LLM
|
|
690
|
+
# paraphrase enrichment (Mode B/C) is left to background consolidation.
|
|
691
|
+
try:
|
|
692
|
+
from superlocalmemory.core.key_expander import KeyExpander
|
|
693
|
+
_alt_keys = KeyExpander(db).expand(fact, profile_id, mode="a")
|
|
694
|
+
if _alt_keys:
|
|
695
|
+
db.upsert_fact_expansion(fact.fact_id, _alt_keys)
|
|
696
|
+
except Exception as exc:
|
|
697
|
+
logger.debug("Key expansion skipped for %s: %s", fact.fact_id, exc)
|
|
698
|
+
|
|
586
699
|
if observation_builder:
|
|
587
700
|
for eid in fact.canonical_entities:
|
|
588
701
|
observation_builder.update_profile(eid, fact, profile_id)
|
|
589
702
|
|
|
590
|
-
#
|
|
703
|
+
# The normalized association key makes this O(1) and exactly-once
|
|
704
|
+
# across retries, crashes, and separate operations that consolidate to
|
|
705
|
+
# the same fact.
|
|
591
706
|
for eid in fact.canonical_entities:
|
|
592
|
-
|
|
593
|
-
db
|
|
594
|
-
|
|
595
|
-
|
|
707
|
+
_record_fact_entity_association(
|
|
708
|
+
db,
|
|
709
|
+
operation_id=ingestion_operation_id,
|
|
710
|
+
profile_id=profile_id,
|
|
711
|
+
fact_id=fact.fact_id,
|
|
712
|
+
entity_id=eid,
|
|
713
|
+
)
|
|
596
714
|
if scene_builder:
|
|
597
715
|
scene_builder.assign_to_scene(fact, profile_id)
|
|
598
716
|
|
|
@@ -603,6 +721,13 @@ def run_store(
|
|
|
603
721
|
from superlocalmemory.storage.models import TemporalEvent
|
|
604
722
|
for eid in fact.canonical_entities:
|
|
605
723
|
event = TemporalEvent(
|
|
724
|
+
event_id=_ingestion_effect_id(
|
|
725
|
+
ingestion_operation_id,
|
|
726
|
+
"temporal",
|
|
727
|
+
fact.fact_id,
|
|
728
|
+
eid,
|
|
729
|
+
"observed",
|
|
730
|
+
),
|
|
606
731
|
profile_id=profile_id, entity_id=eid,
|
|
607
732
|
fact_id=fact.fact_id,
|
|
608
733
|
scope=fact.scope,
|
|
@@ -620,8 +745,16 @@ def run_store(
|
|
|
620
745
|
from superlocalmemory.encoding.foresight import extract_foresight_signals
|
|
621
746
|
from superlocalmemory.storage.models import TemporalEvent as _TE
|
|
622
747
|
foresight_signals = extract_foresight_signals(fact)
|
|
623
|
-
for sig in foresight_signals:
|
|
748
|
+
for signal_index, sig in enumerate(foresight_signals):
|
|
624
749
|
f_event = _TE(
|
|
750
|
+
event_id=_ingestion_effect_id(
|
|
751
|
+
ingestion_operation_id,
|
|
752
|
+
"temporal",
|
|
753
|
+
fact.fact_id,
|
|
754
|
+
sig.get("entity_id", ""),
|
|
755
|
+
"foresight",
|
|
756
|
+
signal_index,
|
|
757
|
+
),
|
|
625
758
|
profile_id=profile_id,
|
|
626
759
|
entity_id=sig.get("entity_id", ""),
|
|
627
760
|
fact_id=fact.fact_id,
|
|
@@ -655,11 +788,6 @@ def run_store(
|
|
|
655
788
|
|
|
656
789
|
logger.info("Stored %d facts (session=%s)", len(stored_ids), session_id)
|
|
657
790
|
|
|
658
|
-
# Post-operation hooks (audit, trust signal, event bus)
|
|
659
|
-
hook_ctx["fact_ids"] = stored_ids
|
|
660
|
-
hook_ctx["fact_count"] = len(stored_ids)
|
|
661
|
-
hooks.run_post("store", hook_ctx)
|
|
662
|
-
|
|
663
791
|
if derivation_report is not None:
|
|
664
792
|
derivation_report.update({
|
|
665
793
|
"extraction": extraction_complete,
|
|
@@ -670,6 +798,35 @@ def run_store(
|
|
|
670
798
|
"provenance": provenance_complete,
|
|
671
799
|
})
|
|
672
800
|
|
|
801
|
+
# Stage observations and emitted fact IDs are recorded before optional
|
|
802
|
+
# post-hooks. A hook failure must not erase the durable retry boundary and
|
|
803
|
+
# cause extraction/consolidation to run again.
|
|
804
|
+
if materialization_progress is not None:
|
|
805
|
+
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
806
|
+
materialization_progress["relational_complete"] = True
|
|
807
|
+
materialization_progress["post_hooks"] = False
|
|
808
|
+
if materialization_checkpoint is not None and stored_ids:
|
|
809
|
+
materialization_checkpoint(
|
|
810
|
+
"relational_complete",
|
|
811
|
+
tuple(stored_ids),
|
|
812
|
+
{
|
|
813
|
+
"pipeline_started": True,
|
|
814
|
+
"relational_started": True,
|
|
815
|
+
"pipeline": True,
|
|
816
|
+
"post_hooks": False,
|
|
817
|
+
"relational": True,
|
|
818
|
+
**dict(derivation_report or {}),
|
|
819
|
+
},
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# Post-operation hooks (audit, trust signal, event bus)
|
|
823
|
+
hook_ctx["ingestion_operation_id"] = ingestion_operation_id
|
|
824
|
+
hook_ctx["fact_ids"] = stored_ids
|
|
825
|
+
hook_ctx["fact_count"] = len(stored_ids)
|
|
826
|
+
hooks.run_post("store", hook_ctx)
|
|
827
|
+
if materialization_progress is not None:
|
|
828
|
+
materialization_progress["post_hooks"] = True
|
|
829
|
+
|
|
673
830
|
# Phase 5: Step-count trigger for lightweight consolidation (L7)
|
|
674
831
|
if consolidation_engine is not None:
|
|
675
832
|
try:
|
|
@@ -70,6 +70,7 @@ class WorkerPool:
|
|
|
70
70
|
fast: bool = False,
|
|
71
71
|
include_global: bool | None = None,
|
|
72
72
|
include_shared: bool | None = None,
|
|
73
|
+
window: str | None = None,
|
|
73
74
|
) -> dict:
|
|
74
75
|
"""Run recall in worker subprocess. Returns result dict.
|
|
75
76
|
|
|
@@ -91,6 +92,8 @@ class WorkerPool:
|
|
|
91
92
|
msg["include_global"] = bool(include_global)
|
|
92
93
|
if include_shared is not None:
|
|
93
94
|
msg["include_shared"] = bool(include_shared)
|
|
95
|
+
if window:
|
|
96
|
+
msg["window"] = window
|
|
94
97
|
return self._send(msg)
|
|
95
98
|
|
|
96
99
|
def store(self, content: str, metadata: dict | None = None) -> dict:
|
|
@@ -291,15 +294,27 @@ class WorkerPool:
|
|
|
291
294
|
self._idle_timer.cancel()
|
|
292
295
|
self._idle_timer = None
|
|
293
296
|
if self._proc is not None:
|
|
294
|
-
|
|
297
|
+
proc = self._proc
|
|
298
|
+
pid = proc.pid
|
|
295
299
|
try:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
300
|
+
proc.stdin.write('{"cmd":"quit"}\n')
|
|
301
|
+
proc.stdin.flush()
|
|
302
|
+
proc.wait(timeout=3)
|
|
299
303
|
except Exception:
|
|
300
304
|
try:
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
proc.kill()
|
|
306
|
+
proc.wait(timeout=2)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
# L-CONC-1: deterministically close the pipe fds rather than leaving
|
|
310
|
+
# them to GC. This releases the OS handles immediately and unblocks
|
|
311
|
+
# any orphaned _readline_with_timeout reader thread (its readline
|
|
312
|
+
# returns/raises on the closed pipe), so repeated request timeouts
|
|
313
|
+
# cannot accumulate reader threads or file descriptors. Cross-platform.
|
|
314
|
+
for stream in (proc.stdin, proc.stdout, proc.stderr):
|
|
315
|
+
try:
|
|
316
|
+
if stream is not None:
|
|
317
|
+
stream.close()
|
|
303
318
|
except Exception:
|
|
304
319
|
pass
|
|
305
320
|
self._proc = None
|
|
@@ -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]
|