apsimo 1.3.0__py3-none-any.whl
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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
"""SQLite storage backend for the Colony World Model."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import aiosqlite
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _fts_escape(q: str, max_len: int = 200) -> str:
|
|
15
|
+
"""Strip FTS5 operators and special chars; keep only word chars and spaces.
|
|
16
|
+
|
|
17
|
+
Caps input at max_len before processing to prevent regex DoS on huge
|
|
18
|
+
inputs. Specials are replaced with SPACES (not deleted): deleting fused
|
|
19
|
+
hyphenated names into single tokens ("huggingface-hub" ->
|
|
20
|
+
"huggingfacehub") that the FTS tokenizer (which splits on the hyphen)
|
|
21
|
+
could never match, so exact-name resolution silently failed and every
|
|
22
|
+
mention minted a duplicate entity.
|
|
23
|
+
"""
|
|
24
|
+
safe = re.sub(r'[^\w\s]', ' ', q[:max_len], flags=re.UNICODE)
|
|
25
|
+
return re.sub(r'\s+', ' ', safe).strip()
|
|
26
|
+
|
|
27
|
+
from ..constants import (
|
|
28
|
+
CAUSAL_RELATIONSHIP_TYPES,
|
|
29
|
+
ENTITY_ID_PREFIX,
|
|
30
|
+
RELATIONSHIP_ID_PREFIX,
|
|
31
|
+
OBSERVATION_ID_PREFIX,
|
|
32
|
+
MERGE_PROPOSAL_ID_PREFIX,
|
|
33
|
+
MERGE_AUDIT_ID_PREFIX,
|
|
34
|
+
)
|
|
35
|
+
from ..entities import BaseEntity, ENTITY_CLASS_MAP, entity_from_dict
|
|
36
|
+
from ..relationships import WorldRelationship
|
|
37
|
+
|
|
38
|
+
# Query-only causal guard (H2.5): untyped (generic) reads NEVER return
|
|
39
|
+
# causal edges; only /world/causal/* and explicitly typed queries do.
|
|
40
|
+
_CAUSAL_EXCLUSION = tuple(sorted(CAUSAL_RELATIONSHIP_TYPES))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _generate_id(prefix: str) -> str:
|
|
44
|
+
"""Generate a world model ID: prefix-<unix_ms>-<rand12>."""
|
|
45
|
+
import secrets
|
|
46
|
+
ts = int(time.time() * 1000)
|
|
47
|
+
rand = secrets.token_hex(6) # 12 hex chars, 48 bits of CSPRNG entropy
|
|
48
|
+
return f"{prefix}-{ts}-{rand}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _now_iso() -> str:
|
|
52
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _entity_to_row(entity: BaseEntity) -> Dict[str, Any]:
|
|
56
|
+
"""Flatten a BaseEntity to a dict suitable for SQLite insertion."""
|
|
57
|
+
import dataclasses
|
|
58
|
+
|
|
59
|
+
row: Dict[str, Any] = {
|
|
60
|
+
"id": entity.id,
|
|
61
|
+
"name": entity.name,
|
|
62
|
+
"entity_type": entity.entity_type,
|
|
63
|
+
"aliases": json.dumps(entity.aliases),
|
|
64
|
+
"external_ids": json.dumps(entity.external_ids),
|
|
65
|
+
"confidence": entity.confidence,
|
|
66
|
+
"properties": json.dumps(_collect_type_properties(entity)),
|
|
67
|
+
"first_seen": (
|
|
68
|
+
entity.first_seen.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
69
|
+
if entity.first_seen
|
|
70
|
+
else _now_iso()
|
|
71
|
+
),
|
|
72
|
+
"last_seen": (
|
|
73
|
+
entity.last_seen.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
74
|
+
if entity.last_seen
|
|
75
|
+
else _now_iso()
|
|
76
|
+
),
|
|
77
|
+
"created_at": (
|
|
78
|
+
entity.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
79
|
+
if entity.created_at
|
|
80
|
+
else _now_iso()
|
|
81
|
+
),
|
|
82
|
+
"updated_at": _now_iso(),
|
|
83
|
+
}
|
|
84
|
+
return row
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _collect_type_properties(entity: BaseEntity) -> Dict[str, Any]:
|
|
88
|
+
"""Merge entity.properties with type-specific fields into a single dict."""
|
|
89
|
+
import dataclasses
|
|
90
|
+
from ..entities import BaseEntity
|
|
91
|
+
|
|
92
|
+
base_fields = {f.name for f in dataclasses.fields(BaseEntity)}
|
|
93
|
+
extra: Dict[str, Any] = dict(entity.properties)
|
|
94
|
+
for f in dataclasses.fields(entity):
|
|
95
|
+
if f.name not in base_fields:
|
|
96
|
+
v = getattr(entity, f.name)
|
|
97
|
+
if v is not None:
|
|
98
|
+
extra[f.name] = v
|
|
99
|
+
return extra
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _row_to_entity(row: aiosqlite.Row) -> BaseEntity:
|
|
103
|
+
"""Reconstruct an entity from a SQLite row."""
|
|
104
|
+
d = dict(row)
|
|
105
|
+
d["aliases"] = json.loads(d.get("aliases") or "[]")
|
|
106
|
+
d["external_ids"] = json.loads(d.get("external_ids") or "{}")
|
|
107
|
+
props = json.loads(d.get("properties") or "{}")
|
|
108
|
+
d["properties"] = {}
|
|
109
|
+
|
|
110
|
+
entity_type = d.get("entity_type", "")
|
|
111
|
+
cls = ENTITY_CLASS_MAP.get(entity_type, BaseEntity)
|
|
112
|
+
import dataclasses
|
|
113
|
+
valid_fields = {f.name for f in dataclasses.fields(cls)}
|
|
114
|
+
|
|
115
|
+
# Pull type-specific fields out of properties into top-level keys
|
|
116
|
+
for k, v in props.items():
|
|
117
|
+
if k in valid_fields:
|
|
118
|
+
d[k] = v
|
|
119
|
+
else:
|
|
120
|
+
d["properties"][k] = v
|
|
121
|
+
|
|
122
|
+
# Parse datetime fields
|
|
123
|
+
for dt_field in ("first_seen", "last_seen", "created_at", "updated_at"):
|
|
124
|
+
val = d.get(dt_field)
|
|
125
|
+
if val and isinstance(val, str):
|
|
126
|
+
try:
|
|
127
|
+
d[dt_field] = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
128
|
+
except ValueError:
|
|
129
|
+
d[dt_field] = None
|
|
130
|
+
|
|
131
|
+
filtered = {k: v for k, v in d.items() if k in valid_fields}
|
|
132
|
+
return cls(**filtered)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _row_to_relationship(row: aiosqlite.Row) -> WorldRelationship:
|
|
136
|
+
"""Reconstruct a WorldRelationship from a SQLite row."""
|
|
137
|
+
d = dict(row)
|
|
138
|
+
d["properties"] = json.loads(d.get("properties") or "{}")
|
|
139
|
+
return WorldRelationship(
|
|
140
|
+
id=d["id"],
|
|
141
|
+
source_id=d["source_id"],
|
|
142
|
+
target_id=d["target_id"],
|
|
143
|
+
relationship_type=d["relationship_type"],
|
|
144
|
+
confidence=d.get("confidence", 0.5),
|
|
145
|
+
valid_from=d.get("valid_from"),
|
|
146
|
+
valid_to=d.get("valid_to"),
|
|
147
|
+
properties=d["properties"],
|
|
148
|
+
source_observation_id=d.get("source_observation_id"),
|
|
149
|
+
created_at=d.get("created_at"),
|
|
150
|
+
updated_at=d.get("updated_at"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
_SCHEMA_DIR = Path(__file__).parent
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SQLiteBackend:
|
|
158
|
+
"""Async SQLite backend for the World Model.
|
|
159
|
+
|
|
160
|
+
All methods are async and safe for concurrent use within a single process.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
MIGRATIONS = [
|
|
164
|
+
("wm-001", "Create base entity tables and FTS index"),
|
|
165
|
+
("wm-002", "Create relationship table"),
|
|
166
|
+
("wm-003", "Create observations table"),
|
|
167
|
+
("wm-004", "Create merge proposals and audit log"),
|
|
168
|
+
("wm-005", "Create schema migrations table"),
|
|
169
|
+
("wm-006", "Add mention_count reinforcement column to wm_entities"),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
def __init__(self, db_path: str = ":memory:") -> None:
|
|
173
|
+
self._db_path = db_path
|
|
174
|
+
self._db: Optional[aiosqlite.Connection] = None
|
|
175
|
+
|
|
176
|
+
async def connect(self) -> None:
|
|
177
|
+
"""Open database connection and apply schema."""
|
|
178
|
+
self._db = await aiosqlite.connect(self._db_path)
|
|
179
|
+
self._db.row_factory = aiosqlite.Row
|
|
180
|
+
await self._db.execute("PRAGMA journal_mode=WAL")
|
|
181
|
+
await self._db.execute("PRAGMA foreign_keys=ON")
|
|
182
|
+
await self._apply_schema()
|
|
183
|
+
|
|
184
|
+
async def close(self) -> None:
|
|
185
|
+
if self._db:
|
|
186
|
+
await self._db.close()
|
|
187
|
+
self._db = None
|
|
188
|
+
|
|
189
|
+
async def _apply_schema(self) -> None:
|
|
190
|
+
schema_path = _SCHEMA_DIR / "schema.sql"
|
|
191
|
+
sql = schema_path.read_text()
|
|
192
|
+
await self._db.executescript(sql)
|
|
193
|
+
await self._db.commit()
|
|
194
|
+
# wm-006: additive column for repeat-mention reinforcement. Guarded so
|
|
195
|
+
# pre-existing databases migrate in place ("duplicate column name" on
|
|
196
|
+
# every later connect is the expected no-op). DEFAULT 1: every stored
|
|
197
|
+
# entity has, by definition, been mentioned at least once.
|
|
198
|
+
try:
|
|
199
|
+
await self._db.execute(
|
|
200
|
+
"ALTER TABLE wm_entities ADD COLUMN "
|
|
201
|
+
"mention_count INTEGER NOT NULL DEFAULT 1")
|
|
202
|
+
await self._db.commit()
|
|
203
|
+
except Exception:
|
|
204
|
+
pass # column already exists
|
|
205
|
+
await self._record_migrations()
|
|
206
|
+
|
|
207
|
+
async def _record_migrations(self) -> None:
|
|
208
|
+
for version, description in self.MIGRATIONS:
|
|
209
|
+
await self._db.execute(
|
|
210
|
+
"""
|
|
211
|
+
INSERT OR IGNORE INTO wm_schema_migrations(version, description)
|
|
212
|
+
VALUES (?, ?)
|
|
213
|
+
""",
|
|
214
|
+
(version, description),
|
|
215
|
+
)
|
|
216
|
+
await self._db.commit()
|
|
217
|
+
|
|
218
|
+
# ── Entity CRUD ───────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
async def get_entity(self, entity_id: str) -> Optional[BaseEntity]:
|
|
221
|
+
async with self._db.execute(
|
|
222
|
+
"SELECT * FROM wm_entities WHERE id = ?", (entity_id,)
|
|
223
|
+
) as cur:
|
|
224
|
+
row = await cur.fetchone()
|
|
225
|
+
return _row_to_entity(row) if row else None
|
|
226
|
+
|
|
227
|
+
async def get_entity_by_external_id(
|
|
228
|
+
self, key: str, value: str
|
|
229
|
+
) -> Optional[BaseEntity]:
|
|
230
|
+
"""Find entity where external_ids JSON contains {key: value}."""
|
|
231
|
+
# SQLite JSON1 function to extract the key
|
|
232
|
+
async with self._db.execute(
|
|
233
|
+
"""
|
|
234
|
+
SELECT * FROM wm_entities
|
|
235
|
+
WHERE json_extract(external_ids, '$.' || ?) = ?
|
|
236
|
+
LIMIT 1
|
|
237
|
+
""",
|
|
238
|
+
(key, value),
|
|
239
|
+
) as cur:
|
|
240
|
+
row = await cur.fetchone()
|
|
241
|
+
return _row_to_entity(row) if row else None
|
|
242
|
+
|
|
243
|
+
async def find_entities(
|
|
244
|
+
self,
|
|
245
|
+
query: str,
|
|
246
|
+
entity_type: Optional[str] = None,
|
|
247
|
+
min_confidence: float = 0.30,
|
|
248
|
+
limit: int = 20,
|
|
249
|
+
) -> List[BaseEntity]:
|
|
250
|
+
"""Full-text search on name + aliases (plus exact-name fallback).
|
|
251
|
+
|
|
252
|
+
Exact canonical-name matching must never depend on FTS tokenizer
|
|
253
|
+
quirks, so a direct case-insensitive name lookup is unioned in; the
|
|
254
|
+
EntityResolver's dedup depends on it.
|
|
255
|
+
"""
|
|
256
|
+
rows = []
|
|
257
|
+
if query:
|
|
258
|
+
sql = """
|
|
259
|
+
SELECT e.* FROM wm_entities e
|
|
260
|
+
JOIN wm_entities_fts fts ON fts.id = e.id
|
|
261
|
+
WHERE wm_entities_fts MATCH ?
|
|
262
|
+
AND e.confidence >= ?
|
|
263
|
+
"""
|
|
264
|
+
params: list = [_fts_escape(query) + "*", min_confidence]
|
|
265
|
+
if entity_type:
|
|
266
|
+
sql += " AND e.entity_type = ?"
|
|
267
|
+
params.append(entity_type)
|
|
268
|
+
sql += " LIMIT ?"
|
|
269
|
+
params.append(limit)
|
|
270
|
+
try:
|
|
271
|
+
async with self._db.execute(sql, params) as cur:
|
|
272
|
+
rows = list(await cur.fetchall())
|
|
273
|
+
except Exception:
|
|
274
|
+
rows = []
|
|
275
|
+
# Exact-name fallback/union.
|
|
276
|
+
sql2 = ("SELECT * FROM wm_entities WHERE lower(name) = lower(?) "
|
|
277
|
+
"AND confidence >= ?")
|
|
278
|
+
params2: list = [query.strip(), min_confidence]
|
|
279
|
+
if entity_type:
|
|
280
|
+
sql2 += " AND entity_type = ?"
|
|
281
|
+
params2.append(entity_type)
|
|
282
|
+
sql2 += " LIMIT ?"
|
|
283
|
+
params2.append(limit)
|
|
284
|
+
async with self._db.execute(sql2, params2) as cur:
|
|
285
|
+
exact = await cur.fetchall()
|
|
286
|
+
seen = {r["id"] for r in rows} if rows else set()
|
|
287
|
+
rows.extend(r for r in exact if r["id"] not in seen)
|
|
288
|
+
else:
|
|
289
|
+
sql = "SELECT * FROM wm_entities WHERE confidence >= ?"
|
|
290
|
+
params = [min_confidence]
|
|
291
|
+
if entity_type:
|
|
292
|
+
sql += " AND entity_type = ?"
|
|
293
|
+
params.append(entity_type)
|
|
294
|
+
sql += " ORDER BY confidence DESC LIMIT ?"
|
|
295
|
+
params.append(limit)
|
|
296
|
+
async with self._db.execute(sql, params) as cur:
|
|
297
|
+
rows = await cur.fetchall()
|
|
298
|
+
return [_row_to_entity(r) for r in rows]
|
|
299
|
+
|
|
300
|
+
async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
|
|
301
|
+
"""Insert or replace an entity row."""
|
|
302
|
+
if not entity.id:
|
|
303
|
+
entity.id = _generate_id(ENTITY_ID_PREFIX)
|
|
304
|
+
row = _entity_to_row(entity)
|
|
305
|
+
await self._db.execute(
|
|
306
|
+
"""
|
|
307
|
+
INSERT INTO wm_entities
|
|
308
|
+
(id, name, entity_type, aliases, external_ids, properties,
|
|
309
|
+
confidence, first_seen, last_seen, created_at, updated_at)
|
|
310
|
+
VALUES
|
|
311
|
+
(:id, :name, :entity_type, :aliases, :external_ids, :properties,
|
|
312
|
+
:confidence, :first_seen, :last_seen, :created_at, :updated_at)
|
|
313
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
314
|
+
name = excluded.name,
|
|
315
|
+
aliases = excluded.aliases,
|
|
316
|
+
external_ids = excluded.external_ids,
|
|
317
|
+
properties = excluded.properties,
|
|
318
|
+
confidence = excluded.confidence,
|
|
319
|
+
last_seen = excluded.last_seen,
|
|
320
|
+
updated_at = excluded.updated_at
|
|
321
|
+
""",
|
|
322
|
+
row,
|
|
323
|
+
)
|
|
324
|
+
await self._db.commit()
|
|
325
|
+
return entity
|
|
326
|
+
|
|
327
|
+
async def update_entity_property(
|
|
328
|
+
self, entity_id: str, property_key: str, property_value: Any, confidence: float
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Update a property only if new confidence is higher than existing."""
|
|
331
|
+
entity = await self.get_entity(entity_id)
|
|
332
|
+
if not entity:
|
|
333
|
+
return
|
|
334
|
+
import dataclasses
|
|
335
|
+
existing_fields = {f.name for f in dataclasses.fields(type(entity))}
|
|
336
|
+
if property_key in existing_fields:
|
|
337
|
+
# Update the field directly
|
|
338
|
+
setattr(entity, property_key, property_value)
|
|
339
|
+
if confidence > entity.confidence:
|
|
340
|
+
entity.confidence = confidence
|
|
341
|
+
await self.upsert_entity(entity)
|
|
342
|
+
else:
|
|
343
|
+
# Store in properties JSON
|
|
344
|
+
if confidence >= entity.properties.get(f"_conf_{property_key}", 0.0):
|
|
345
|
+
entity.properties[property_key] = property_value
|
|
346
|
+
entity.properties[f"_conf_{property_key}"] = confidence
|
|
347
|
+
await self.upsert_entity(entity)
|
|
348
|
+
|
|
349
|
+
async def add_entity_alias(self, entity_id: str, alias: str) -> None:
|
|
350
|
+
entity = await self.get_entity(entity_id)
|
|
351
|
+
if entity and alias not in entity.aliases:
|
|
352
|
+
entity.aliases.append(alias)
|
|
353
|
+
await self._db.execute(
|
|
354
|
+
"UPDATE wm_entities SET aliases = ?, updated_at = ? WHERE id = ?",
|
|
355
|
+
(json.dumps(entity.aliases), _now_iso(), entity_id),
|
|
356
|
+
)
|
|
357
|
+
await self._db.commit()
|
|
358
|
+
|
|
359
|
+
async def reinforce_entity(self, entity_id: str) -> None:
|
|
360
|
+
"""Record familiarity without treating repetition as corroboration."""
|
|
361
|
+
now = _now_iso()
|
|
362
|
+
await self._db.execute(
|
|
363
|
+
"""
|
|
364
|
+
UPDATE wm_entities
|
|
365
|
+
SET last_seen = MAX(last_seen, ?),
|
|
366
|
+
updated_at = ?,
|
|
367
|
+
mention_count = COALESCE(mention_count, 1) + 1
|
|
368
|
+
WHERE id = ?
|
|
369
|
+
""",
|
|
370
|
+
(now, now, entity_id),
|
|
371
|
+
)
|
|
372
|
+
await self._db.commit()
|
|
373
|
+
|
|
374
|
+
async def delete_entity(self, entity_id: str) -> None:
|
|
375
|
+
await self._db.execute("DELETE FROM wm_entities WHERE id = ?", (entity_id,))
|
|
376
|
+
await self._db.commit()
|
|
377
|
+
|
|
378
|
+
async def prune_entities(self, last_seen_before: str,
|
|
379
|
+
max_confidence: float) -> int:
|
|
380
|
+
"""Delete entities not seen since ``last_seen_before`` (ISO, the
|
|
381
|
+
schema's Z-suffixed format) whose confidence is below
|
|
382
|
+
``max_confidence``. Relationships and observations cascade
|
|
383
|
+
(foreign_keys=ON); the FTS mirror follows via triggers. Returns the
|
|
384
|
+
number of entities removed."""
|
|
385
|
+
cur = await self._db.execute(
|
|
386
|
+
"DELETE FROM wm_entities WHERE last_seen < ? AND confidence < ?",
|
|
387
|
+
(last_seen_before, max_confidence),
|
|
388
|
+
)
|
|
389
|
+
count = cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
|
|
390
|
+
await self._db.commit()
|
|
391
|
+
return count
|
|
392
|
+
|
|
393
|
+
# ── Relationship CRUD ─────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
async def get_relationship(self, rel_id: str) -> Optional[WorldRelationship]:
|
|
396
|
+
async with self._db.execute(
|
|
397
|
+
"SELECT * FROM wm_relationships WHERE id = ?", (rel_id,)
|
|
398
|
+
) as cur:
|
|
399
|
+
row = await cur.fetchone()
|
|
400
|
+
return _row_to_relationship(row) if row else None
|
|
401
|
+
|
|
402
|
+
async def upsert_relationship(self, rel: WorldRelationship) -> WorldRelationship:
|
|
403
|
+
if not rel.id:
|
|
404
|
+
rel.id = _generate_id(RELATIONSHIP_ID_PREFIX)
|
|
405
|
+
now = _now_iso()
|
|
406
|
+
await self._db.execute(
|
|
407
|
+
"""
|
|
408
|
+
INSERT INTO wm_relationships
|
|
409
|
+
(id, source_id, target_id, relationship_type, confidence,
|
|
410
|
+
valid_from, valid_to, properties, source_observation_id,
|
|
411
|
+
created_at, updated_at)
|
|
412
|
+
VALUES
|
|
413
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
414
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
415
|
+
confidence = excluded.confidence,
|
|
416
|
+
valid_from = excluded.valid_from,
|
|
417
|
+
valid_to = excluded.valid_to,
|
|
418
|
+
properties = excluded.properties,
|
|
419
|
+
updated_at = excluded.updated_at
|
|
420
|
+
""",
|
|
421
|
+
(
|
|
422
|
+
rel.id,
|
|
423
|
+
rel.source_id,
|
|
424
|
+
rel.target_id,
|
|
425
|
+
rel.relationship_type,
|
|
426
|
+
rel.confidence,
|
|
427
|
+
rel.valid_from,
|
|
428
|
+
rel.valid_to,
|
|
429
|
+
json.dumps(rel.properties),
|
|
430
|
+
rel.source_observation_id,
|
|
431
|
+
rel.created_at or now,
|
|
432
|
+
now,
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
await self._db.commit()
|
|
436
|
+
return rel
|
|
437
|
+
|
|
438
|
+
async def close_relationship(self, rel_id: str, valid_to: str) -> None:
|
|
439
|
+
await self._db.execute(
|
|
440
|
+
"UPDATE wm_relationships SET valid_to = ?, updated_at = ? WHERE id = ?",
|
|
441
|
+
(valid_to, _now_iso(), rel_id),
|
|
442
|
+
)
|
|
443
|
+
await self._db.commit()
|
|
444
|
+
|
|
445
|
+
async def query_relationships(
|
|
446
|
+
self,
|
|
447
|
+
source_id: Optional[str] = None,
|
|
448
|
+
target_id: Optional[str] = None,
|
|
449
|
+
relationship_type: Optional[str] = None,
|
|
450
|
+
target_types: Optional[List[str]] = None,
|
|
451
|
+
active_only: bool = False,
|
|
452
|
+
min_confidence: float = 0.30,
|
|
453
|
+
limit: int = 100,
|
|
454
|
+
) -> List[WorldRelationship]:
|
|
455
|
+
sql = "SELECT r.* FROM wm_relationships r"
|
|
456
|
+
conditions = ["r.confidence >= ?"]
|
|
457
|
+
params: list = [min_confidence]
|
|
458
|
+
|
|
459
|
+
if target_types:
|
|
460
|
+
sql += " JOIN wm_entities te ON te.id = r.target_id"
|
|
461
|
+
placeholders = ",".join("?" * len(target_types))
|
|
462
|
+
conditions.append(f"te.entity_type IN ({placeholders})")
|
|
463
|
+
params.extend(target_types)
|
|
464
|
+
|
|
465
|
+
if source_id:
|
|
466
|
+
conditions.append("r.source_id = ?")
|
|
467
|
+
params.append(source_id)
|
|
468
|
+
if target_id:
|
|
469
|
+
conditions.append("r.target_id = ?")
|
|
470
|
+
params.append(target_id)
|
|
471
|
+
if relationship_type:
|
|
472
|
+
conditions.append("r.relationship_type = ?")
|
|
473
|
+
params.append(relationship_type)
|
|
474
|
+
else:
|
|
475
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
476
|
+
ph = ",".join("?" * len(_CAUSAL_EXCLUSION))
|
|
477
|
+
conditions.append(f"r.relationship_type NOT IN ({ph})")
|
|
478
|
+
params.extend(_CAUSAL_EXCLUSION)
|
|
479
|
+
if active_only:
|
|
480
|
+
conditions.append("r.valid_to IS NULL")
|
|
481
|
+
|
|
482
|
+
sql += " WHERE " + " AND ".join(conditions)
|
|
483
|
+
sql += " ORDER BY r.confidence DESC LIMIT ?"
|
|
484
|
+
params.append(limit)
|
|
485
|
+
|
|
486
|
+
async with self._db.execute(sql, params) as cur:
|
|
487
|
+
rows = await cur.fetchall()
|
|
488
|
+
return [_row_to_relationship(r) for r in rows]
|
|
489
|
+
|
|
490
|
+
async def query_at_time(
|
|
491
|
+
self,
|
|
492
|
+
entity_id: str,
|
|
493
|
+
as_of: str,
|
|
494
|
+
relationship_types: Optional[List[str]] = None,
|
|
495
|
+
) -> List[WorldRelationship]:
|
|
496
|
+
"""Relationships for entity active at as_of datetime."""
|
|
497
|
+
sql = """
|
|
498
|
+
SELECT * FROM wm_relationships
|
|
499
|
+
WHERE (source_id = ? OR target_id = ?)
|
|
500
|
+
AND (valid_from IS NULL OR valid_from <= ?)
|
|
501
|
+
AND (valid_to IS NULL OR valid_to > ?)
|
|
502
|
+
"""
|
|
503
|
+
params: list = [entity_id, entity_id, as_of, as_of]
|
|
504
|
+
if relationship_types:
|
|
505
|
+
placeholders = ",".join("?" * len(relationship_types))
|
|
506
|
+
sql += f" AND relationship_type IN ({placeholders})"
|
|
507
|
+
params.extend(relationship_types)
|
|
508
|
+
else:
|
|
509
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
510
|
+
ph = ",".join("?" * len(_CAUSAL_EXCLUSION))
|
|
511
|
+
sql += f" AND relationship_type NOT IN ({ph})"
|
|
512
|
+
params.extend(_CAUSAL_EXCLUSION)
|
|
513
|
+
async with self._db.execute(sql, params) as cur:
|
|
514
|
+
rows = await cur.fetchall()
|
|
515
|
+
return [_row_to_relationship(r) for r in rows]
|
|
516
|
+
|
|
517
|
+
# ── Graph Traversal ───────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
async def get_neighbors(
|
|
520
|
+
self,
|
|
521
|
+
entity_id: str,
|
|
522
|
+
min_confidence: float = 0.30,
|
|
523
|
+
relationship_types: Optional[List[str]] = None,
|
|
524
|
+
) -> List[tuple]:
|
|
525
|
+
"""Return (neighbor_entity, relationship) pairs for direct neighbors."""
|
|
526
|
+
sql = """
|
|
527
|
+
SELECT e.*, r.id as rel_id, r.source_id, r.target_id,
|
|
528
|
+
r.relationship_type, r.confidence as rel_confidence,
|
|
529
|
+
r.valid_from, r.valid_to, r.properties as rel_props,
|
|
530
|
+
r.source_observation_id, r.created_at as rel_created_at,
|
|
531
|
+
r.updated_at as rel_updated_at
|
|
532
|
+
FROM wm_relationships r
|
|
533
|
+
JOIN wm_entities e ON (
|
|
534
|
+
(r.source_id = ? AND e.id = r.target_id) OR
|
|
535
|
+
(r.target_id = ? AND e.id = r.source_id)
|
|
536
|
+
)
|
|
537
|
+
WHERE r.confidence >= ? AND e.confidence >= ?
|
|
538
|
+
"""
|
|
539
|
+
params: list = [entity_id, entity_id, min_confidence, min_confidence]
|
|
540
|
+
if relationship_types:
|
|
541
|
+
placeholders = ",".join("?" * len(relationship_types))
|
|
542
|
+
sql += f" AND r.relationship_type IN ({placeholders})"
|
|
543
|
+
params.extend(relationship_types)
|
|
544
|
+
else:
|
|
545
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
546
|
+
ph = ",".join("?" * len(_CAUSAL_EXCLUSION))
|
|
547
|
+
sql += f" AND r.relationship_type NOT IN ({ph})"
|
|
548
|
+
params.extend(_CAUSAL_EXCLUSION)
|
|
549
|
+
async with self._db.execute(sql, params) as cur:
|
|
550
|
+
rows = await cur.fetchall()
|
|
551
|
+
|
|
552
|
+
results = []
|
|
553
|
+
for row in rows:
|
|
554
|
+
d = dict(row)
|
|
555
|
+
entity = _row_to_entity(row)
|
|
556
|
+
rel = WorldRelationship(
|
|
557
|
+
id=d["rel_id"],
|
|
558
|
+
source_id=d["source_id"],
|
|
559
|
+
target_id=d["target_id"],
|
|
560
|
+
relationship_type=d["relationship_type"],
|
|
561
|
+
confidence=d["rel_confidence"],
|
|
562
|
+
valid_from=d.get("valid_from"),
|
|
563
|
+
valid_to=d.get("valid_to"),
|
|
564
|
+
properties=json.loads(d.get("rel_props") or "{}"),
|
|
565
|
+
source_observation_id=d.get("source_observation_id"),
|
|
566
|
+
created_at=d.get("rel_created_at"),
|
|
567
|
+
updated_at=d.get("rel_updated_at"),
|
|
568
|
+
)
|
|
569
|
+
results.append((entity, rel))
|
|
570
|
+
return results
|
|
571
|
+
|
|
572
|
+
# ── Observations ──────────────────────────────────────────────────────────
|
|
573
|
+
|
|
574
|
+
async def put_property_observation(self, data):
|
|
575
|
+
from ..observations import canonical, SOURCE_PREFIX
|
|
576
|
+
encoded, marker = canonical(data), SOURCE_PREFIX + data['property_key']
|
|
577
|
+
await self._db.execute('''INSERT INTO wm_observations
|
|
578
|
+
(id,entity_id,observation,source) VALUES (?,?,?,?) ON CONFLICT(id) DO NOTHING''',
|
|
579
|
+
(data['observation_id'], data['entity_id'], encoded, marker))
|
|
580
|
+
await self._db.commit()
|
|
581
|
+
async with self._db.execute('SELECT observation,source FROM wm_observations WHERE id=?',
|
|
582
|
+
(data['observation_id'],)) as cur:
|
|
583
|
+
row = await cur.fetchone()
|
|
584
|
+
if row['observation'] != encoded or row['source'] != marker:
|
|
585
|
+
raise ValueError('world_observation_id_conflict')
|
|
586
|
+
return data
|
|
587
|
+
|
|
588
|
+
async def current_property_observations(self, entity_id, property_key, *, subject_person_id,
|
|
589
|
+
viewer_scope, shareability, as_of, limit=2001):
|
|
590
|
+
"""Reduce in SQL so regular telemetry does not fill a context window.
|
|
591
|
+
|
|
592
|
+
Deduplicate one evidence packet before selecting the latest report per
|
|
593
|
+
producer. Equal-time contradictory values remain separate candidates.
|
|
594
|
+
Raw observation history stays in the existing journal.
|
|
595
|
+
"""
|
|
596
|
+
from ..observations import SOURCE_PREFIX
|
|
597
|
+
async with self._db.execute('''WITH scoped AS (
|
|
598
|
+
SELECT id,observation,
|
|
599
|
+
json_extract(observation,'$.producer') AS producer,
|
|
600
|
+
json_extract(observation,'$.kind') AS kind,
|
|
601
|
+
json_extract(observation,'$.observed_at') AS observed_at,
|
|
602
|
+
json_extract(observation,'$.evidence_refs') AS evidence_refs,
|
|
603
|
+
json_type(observation,'$.value') AS value_type,
|
|
604
|
+
json_extract(observation,'$.value') AS value
|
|
605
|
+
FROM wm_observations WHERE entity_id=? AND source=?
|
|
606
|
+
AND json_extract(observation,'$.subject_person_id')=?
|
|
607
|
+
AND json_extract(observation,'$.viewer_scope')=?
|
|
608
|
+
AND json_extract(observation,'$.shareability')=?
|
|
609
|
+
AND json_extract(observation,'$.observed_at')<=?
|
|
610
|
+
AND json_extract(observation,'$.valid_from')<=?
|
|
611
|
+
), dedup AS (
|
|
612
|
+
SELECT *,ROW_NUMBER() OVER (PARTITION BY producer,kind,evidence_refs,value_type,value
|
|
613
|
+
ORDER BY observed_at,id) AS copy_rank FROM scoped
|
|
614
|
+
), latest AS (
|
|
615
|
+
SELECT *,RANK() OVER (PARTITION BY producer,kind ORDER BY observed_at DESC) AS recency
|
|
616
|
+
FROM dedup WHERE copy_rank=1
|
|
617
|
+
) SELECT observation FROM latest WHERE recency=1 ORDER BY producer,kind,id LIMIT ?''',
|
|
618
|
+
(entity_id, SOURCE_PREFIX + property_key, subject_person_id, viewer_scope, shareability,
|
|
619
|
+
as_of, as_of, limit)) as cur:
|
|
620
|
+
return [json.loads(row['observation']) for row in await cur.fetchall()]
|
|
621
|
+
|
|
622
|
+
async def property_keys(self, entity_id, *, subject_person_id, viewer_scope, shareability, limit=16):
|
|
623
|
+
from ..observations import SOURCE_PREFIX
|
|
624
|
+
async with self._db.execute('''SELECT DISTINCT substr(source,?) AS property_key
|
|
625
|
+
FROM wm_observations WHERE entity_id=? AND source LIKE ?
|
|
626
|
+
AND json_extract(observation,'$.subject_person_id')=?
|
|
627
|
+
AND json_extract(observation,'$.viewer_scope')=?
|
|
628
|
+
AND json_extract(observation,'$.shareability')=? ORDER BY property_key LIMIT ?''',
|
|
629
|
+
(len(SOURCE_PREFIX)+1, entity_id, SOURCE_PREFIX+'%', subject_person_id,
|
|
630
|
+
viewer_scope, shareability, limit)) as cur:
|
|
631
|
+
return [row['property_key'] for row in await cur.fetchall()]
|
|
632
|
+
|
|
633
|
+
async def erase_property_evidence(self, evidence_refs, *, subject_person_id):
|
|
634
|
+
from ..observations import SOURCE_PREFIX
|
|
635
|
+
# Canonical erasure/correction has already selected these exact refs.
|
|
636
|
+
# Redact derived values, retaining the latest invalidated head so an
|
|
637
|
+
# older report cannot silently become current after a correction.
|
|
638
|
+
removed = []
|
|
639
|
+
for ref in sorted(set(evidence_refs)):
|
|
640
|
+
async with self._db.execute('''UPDATE wm_observations
|
|
641
|
+
SET observation=json_set(observation,'$.value',NULL,'$.invalidated',json('true'))
|
|
642
|
+
WHERE id IN (
|
|
643
|
+
SELECT o.id FROM wm_observations o,json_each(
|
|
644
|
+
CASE WHEN o.source LIKE ? THEN o.observation ELSE '{}' END,'$.evidence_refs') e
|
|
645
|
+
WHERE o.source LIKE ? AND json_extract(o.observation,'$.subject_person_id')=?
|
|
646
|
+
AND e.value=?) RETURNING id''', (SOURCE_PREFIX + '%', SOURCE_PREFIX + '%', subject_person_id, ref)) as cur:
|
|
647
|
+
removed.extend(row['id'] for row in await cur.fetchall())
|
|
648
|
+
await self._db.commit()
|
|
649
|
+
return sorted(set(removed))
|
|
650
|
+
|
|
651
|
+
async def add_observation(
|
|
652
|
+
self,
|
|
653
|
+
entity_id: Optional[str],
|
|
654
|
+
relationship_id: Optional[str],
|
|
655
|
+
observation: str,
|
|
656
|
+
source: str,
|
|
657
|
+
) -> str:
|
|
658
|
+
obs_id = _generate_id(OBSERVATION_ID_PREFIX)
|
|
659
|
+
await self._db.execute(
|
|
660
|
+
"""
|
|
661
|
+
INSERT INTO wm_observations(id, entity_id, relationship_id, observation, source)
|
|
662
|
+
VALUES (?, ?, ?, ?, ?)
|
|
663
|
+
""",
|
|
664
|
+
(obs_id, entity_id, relationship_id, observation, source),
|
|
665
|
+
)
|
|
666
|
+
await self._db.commit()
|
|
667
|
+
return obs_id
|
|
668
|
+
|
|
669
|
+
# ── Merge Proposals ───────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
async def create_merge_proposal(
|
|
672
|
+
self,
|
|
673
|
+
candidate_id: str,
|
|
674
|
+
existing_id: str,
|
|
675
|
+
confidence: float,
|
|
676
|
+
reason: str,
|
|
677
|
+
evidence: Optional[Dict[str, Any]] = None,
|
|
678
|
+
) -> str:
|
|
679
|
+
proposal_id = _generate_id(MERGE_PROPOSAL_ID_PREFIX)
|
|
680
|
+
await self._db.execute(
|
|
681
|
+
"""
|
|
682
|
+
INSERT INTO wm_merge_proposals
|
|
683
|
+
(id, candidate_id, existing_id, match_confidence, match_reason, evidence)
|
|
684
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
685
|
+
""",
|
|
686
|
+
(
|
|
687
|
+
proposal_id,
|
|
688
|
+
candidate_id,
|
|
689
|
+
existing_id,
|
|
690
|
+
confidence,
|
|
691
|
+
reason,
|
|
692
|
+
json.dumps(evidence or {}),
|
|
693
|
+
),
|
|
694
|
+
)
|
|
695
|
+
await self._db.commit()
|
|
696
|
+
return proposal_id
|
|
697
|
+
|
|
698
|
+
async def get_merge_proposal(self, proposal_id: str) -> Optional[Dict[str, Any]]:
|
|
699
|
+
async with self._db.execute(
|
|
700
|
+
"SELECT * FROM wm_merge_proposals WHERE id = ?", (proposal_id,)
|
|
701
|
+
) as cur:
|
|
702
|
+
row = await cur.fetchone()
|
|
703
|
+
if not row:
|
|
704
|
+
return None
|
|
705
|
+
d = dict(row)
|
|
706
|
+
d["evidence"] = json.loads(d.get("evidence") or "{}")
|
|
707
|
+
return d
|
|
708
|
+
|
|
709
|
+
async def update_merge_proposal_status(
|
|
710
|
+
self, proposal_id: str, status: str
|
|
711
|
+
) -> None:
|
|
712
|
+
await self._db.execute(
|
|
713
|
+
"""
|
|
714
|
+
UPDATE wm_merge_proposals
|
|
715
|
+
SET status = ?, resolved_at = ?
|
|
716
|
+
WHERE id = ?
|
|
717
|
+
""",
|
|
718
|
+
(status, _now_iso(), proposal_id),
|
|
719
|
+
)
|
|
720
|
+
await self._db.commit()
|
|
721
|
+
|
|
722
|
+
async def get_pending_merge_proposals(self) -> List[Dict[str, Any]]:
|
|
723
|
+
async with self._db.execute(
|
|
724
|
+
"SELECT * FROM wm_merge_proposals WHERE status = 'pending'"
|
|
725
|
+
) as cur:
|
|
726
|
+
rows = await cur.fetchall()
|
|
727
|
+
result = []
|
|
728
|
+
for row in rows:
|
|
729
|
+
d = dict(row)
|
|
730
|
+
d["evidence"] = json.loads(d.get("evidence") or "{}")
|
|
731
|
+
result.append(d)
|
|
732
|
+
return result
|
|
733
|
+
|
|
734
|
+
# ── Merge Execution ───────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
async def execute_merge(
|
|
737
|
+
self,
|
|
738
|
+
surviving_id: str,
|
|
739
|
+
retired_id: str,
|
|
740
|
+
executed_by: str,
|
|
741
|
+
proposal_id: Optional[str] = None,
|
|
742
|
+
) -> str:
|
|
743
|
+
"""Repoint all relationships from retired_id to surviving_id.
|
|
744
|
+
|
|
745
|
+
All writes are wrapped in a SAVEPOINT so a failure midway through
|
|
746
|
+
(e.g. the DELETE or audit INSERT raising) rolls back the relationship
|
|
747
|
+
repoint and alias merge as a unit.
|
|
748
|
+
"""
|
|
749
|
+
await self._db.execute("SAVEPOINT execute_merge")
|
|
750
|
+
try:
|
|
751
|
+
# Read both entities up-front; bail out before mutating if either
|
|
752
|
+
# was deleted by a concurrent merge.
|
|
753
|
+
surviving = await self.get_entity(surviving_id)
|
|
754
|
+
retired = await self.get_entity(retired_id)
|
|
755
|
+
|
|
756
|
+
async with self._db.execute(
|
|
757
|
+
"SELECT COUNT(*) FROM wm_relationships WHERE source_id = ? OR target_id = ?",
|
|
758
|
+
(retired_id, retired_id),
|
|
759
|
+
) as cur:
|
|
760
|
+
row = await cur.fetchone()
|
|
761
|
+
rel_count = row[0] if row else 0
|
|
762
|
+
|
|
763
|
+
await self._db.execute(
|
|
764
|
+
"UPDATE wm_relationships SET source_id = ? WHERE source_id = ?",
|
|
765
|
+
(surviving_id, retired_id),
|
|
766
|
+
)
|
|
767
|
+
await self._db.execute(
|
|
768
|
+
"UPDATE wm_relationships SET target_id = ? WHERE target_id = ?",
|
|
769
|
+
(surviving_id, retired_id),
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
props_updated = 0
|
|
773
|
+
if surviving and retired:
|
|
774
|
+
new_aliases = surviving.aliases.copy()
|
|
775
|
+
if retired_id not in new_aliases:
|
|
776
|
+
new_aliases.append(retired_id)
|
|
777
|
+
for alias in retired.aliases:
|
|
778
|
+
if alias not in new_aliases:
|
|
779
|
+
new_aliases.append(alias)
|
|
780
|
+
await self._db.execute(
|
|
781
|
+
"UPDATE wm_entities SET aliases = ?, updated_at = ? WHERE id = ?",
|
|
782
|
+
(json.dumps(new_aliases), _now_iso(), surviving_id),
|
|
783
|
+
)
|
|
784
|
+
props_updated = len(new_aliases) - len(surviving.aliases)
|
|
785
|
+
|
|
786
|
+
await self._db.execute(
|
|
787
|
+
"DELETE FROM wm_entities WHERE id = ?", (retired_id,)
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
audit_id = _generate_id(MERGE_AUDIT_ID_PREFIX)
|
|
791
|
+
await self._db.execute(
|
|
792
|
+
"""
|
|
793
|
+
INSERT INTO wm_merge_log
|
|
794
|
+
(id, surviving_id, retired_id, relationships_repointed,
|
|
795
|
+
properties_updated, executed_by, merge_proposal_id)
|
|
796
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
797
|
+
""",
|
|
798
|
+
(
|
|
799
|
+
audit_id,
|
|
800
|
+
surviving_id,
|
|
801
|
+
retired_id,
|
|
802
|
+
rel_count,
|
|
803
|
+
props_updated,
|
|
804
|
+
executed_by,
|
|
805
|
+
proposal_id,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
await self._db.execute("RELEASE SAVEPOINT execute_merge")
|
|
809
|
+
await self._db.commit()
|
|
810
|
+
return audit_id
|
|
811
|
+
except Exception:
|
|
812
|
+
await self._db.execute("ROLLBACK TO SAVEPOINT execute_merge")
|
|
813
|
+
await self._db.execute("RELEASE SAVEPOINT execute_merge")
|
|
814
|
+
raise
|
|
815
|
+
|
|
816
|
+
# ── Stats ─────────────────────────────────────────────────────────────────
|
|
817
|
+
|
|
818
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
819
|
+
async with self._db.execute(
|
|
820
|
+
"SELECT entity_type, COUNT(*) as cnt FROM wm_entities GROUP BY entity_type"
|
|
821
|
+
) as cur:
|
|
822
|
+
type_rows = await cur.fetchall()
|
|
823
|
+
|
|
824
|
+
async with self._db.execute("SELECT COUNT(*) FROM wm_entities") as cur:
|
|
825
|
+
row = await cur.fetchone()
|
|
826
|
+
total_entities = row[0] if row else 0
|
|
827
|
+
|
|
828
|
+
async with self._db.execute("SELECT COUNT(*) FROM wm_relationships") as cur:
|
|
829
|
+
row = await cur.fetchone()
|
|
830
|
+
total_rels = row[0] if row else 0
|
|
831
|
+
|
|
832
|
+
async with self._db.execute(
|
|
833
|
+
"SELECT COUNT(*) FROM wm_relationships WHERE valid_to IS NULL"
|
|
834
|
+
) as cur:
|
|
835
|
+
row = await cur.fetchone()
|
|
836
|
+
active_rels = row[0] if row else 0
|
|
837
|
+
|
|
838
|
+
async with self._db.execute("SELECT COUNT(*) FROM wm_observations") as cur:
|
|
839
|
+
row = await cur.fetchone()
|
|
840
|
+
total_obs = row[0] if row else 0
|
|
841
|
+
|
|
842
|
+
async with self._db.execute(
|
|
843
|
+
"SELECT COUNT(*) FROM wm_merge_proposals WHERE status = 'pending'"
|
|
844
|
+
) as cur:
|
|
845
|
+
row = await cur.fetchone()
|
|
846
|
+
pending_proposals = row[0] if row else 0
|
|
847
|
+
|
|
848
|
+
return {
|
|
849
|
+
"total_entities": total_entities,
|
|
850
|
+
"entities_by_type": {r["entity_type"]: r["cnt"] for r in type_rows},
|
|
851
|
+
"total_relationships": total_rels,
|
|
852
|
+
"active_relationships": active_rels,
|
|
853
|
+
"total_observations": total_obs,
|
|
854
|
+
"merge_proposals_pending": pending_proposals,
|
|
855
|
+
}
|