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,683 @@
|
|
|
1
|
+
"""PostgreSQL storage backend for the Colony World Model.
|
|
2
|
+
|
|
3
|
+
Requires asyncpg. Install with: pip install colony-sidecar[postgres]
|
|
4
|
+
|
|
5
|
+
Same interface as SQLiteBackend — drop-in replacement for larger deployments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from ..constants import (
|
|
18
|
+
CAUSAL_RELATIONSHIP_TYPES,
|
|
19
|
+
ENTITY_ID_PREFIX,
|
|
20
|
+
RELATIONSHIP_ID_PREFIX,
|
|
21
|
+
OBSERVATION_ID_PREFIX,
|
|
22
|
+
MERGE_PROPOSAL_ID_PREFIX,
|
|
23
|
+
MERGE_AUDIT_ID_PREFIX,
|
|
24
|
+
)
|
|
25
|
+
from ..entities import BaseEntity, ENTITY_CLASS_MAP
|
|
26
|
+
from ..relationships import WorldRelationship
|
|
27
|
+
|
|
28
|
+
# Query-only causal guard (H2.5): untyped (generic) reads NEVER return
|
|
29
|
+
# causal edges; only /world/causal/* and explicitly typed queries do.
|
|
30
|
+
_CAUSAL_EXCLUSION = tuple(sorted(CAUSAL_RELATIONSHIP_TYPES))
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import asyncpg
|
|
36
|
+
HAS_ASYNCPG = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
HAS_ASYNCPG = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _generate_id(prefix: str) -> str:
|
|
42
|
+
import secrets
|
|
43
|
+
ts = int(time.time() * 1000)
|
|
44
|
+
rand = secrets.token_hex(6)
|
|
45
|
+
return f"{prefix}-{ts}-{rand}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _now_iso() -> str:
|
|
49
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _entity_to_params(entity: BaseEntity) -> dict:
|
|
53
|
+
"""Flatten a BaseEntity to a dict suitable for Postgres insertion."""
|
|
54
|
+
import dataclasses
|
|
55
|
+
from ..entities import BaseEntity as BaseEntityClass
|
|
56
|
+
|
|
57
|
+
base_fields = {f.name for f in dataclasses.fields(BaseEntityClass)}
|
|
58
|
+
extra: Dict[str, Any] = dict(entity.properties)
|
|
59
|
+
for f in dataclasses.fields(entity):
|
|
60
|
+
if f.name not in base_fields:
|
|
61
|
+
v = getattr(entity, f.name)
|
|
62
|
+
if v is not None:
|
|
63
|
+
extra[f.name] = v
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"id": entity.id,
|
|
67
|
+
"name": entity.name,
|
|
68
|
+
"entity_type": entity.entity_type,
|
|
69
|
+
"aliases": json.dumps(entity.aliases),
|
|
70
|
+
"external_ids": json.dumps(entity.external_ids),
|
|
71
|
+
"confidence": entity.confidence,
|
|
72
|
+
"properties": json.dumps(extra),
|
|
73
|
+
"first_seen": entity.first_seen.strftime("%Y-%m-%dT%H:%M:%SZ") if entity.first_seen else _now_iso(),
|
|
74
|
+
"last_seen": entity.last_seen.strftime("%Y-%m-%dT%H:%M:%SZ") if entity.last_seen else _now_iso(),
|
|
75
|
+
"created_at": entity.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if entity.created_at else _now_iso(),
|
|
76
|
+
"updated_at": _now_iso(),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _record_to_entity(record) -> BaseEntity:
|
|
81
|
+
"""Reconstruct an entity from a Postgres record."""
|
|
82
|
+
d = dict(record)
|
|
83
|
+
# Handle asyncpg Record objects
|
|
84
|
+
if not isinstance(d, dict):
|
|
85
|
+
d = {k: v for k, v in record.items()}
|
|
86
|
+
|
|
87
|
+
d["aliases"] = json.loads(d.get("aliases") or "[]")
|
|
88
|
+
d["external_ids"] = json.loads(d.get("external_ids") or "{}")
|
|
89
|
+
props = json.loads(d.get("properties") or "{}")
|
|
90
|
+
d["properties"] = {}
|
|
91
|
+
|
|
92
|
+
entity_type = d.get("entity_type", "")
|
|
93
|
+
cls = ENTITY_CLASS_MAP.get(entity_type, BaseEntity)
|
|
94
|
+
import dataclasses
|
|
95
|
+
valid_fields = {f.name for f in dataclasses.fields(cls)}
|
|
96
|
+
|
|
97
|
+
for k, v in props.items():
|
|
98
|
+
if k in valid_fields:
|
|
99
|
+
d[k] = v
|
|
100
|
+
else:
|
|
101
|
+
d["properties"][k] = v
|
|
102
|
+
|
|
103
|
+
for dt_field in ("first_seen", "last_seen", "created_at", "updated_at"):
|
|
104
|
+
val = d.get(dt_field)
|
|
105
|
+
if val and isinstance(val, str):
|
|
106
|
+
try:
|
|
107
|
+
d[dt_field] = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
108
|
+
except ValueError:
|
|
109
|
+
d[dt_field] = None
|
|
110
|
+
elif val and isinstance(val, datetime):
|
|
111
|
+
pass # Already a datetime
|
|
112
|
+
|
|
113
|
+
filtered = {k: v for k, v in d.items() if k in valid_fields}
|
|
114
|
+
return cls(**filtered)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _record_to_relationship(record) -> WorldRelationship:
|
|
118
|
+
d = dict(record)
|
|
119
|
+
if not isinstance(d, dict):
|
|
120
|
+
d = {k: v for k, v in record.items()}
|
|
121
|
+
d["properties"] = json.loads(d.get("properties") or "{}")
|
|
122
|
+
return WorldRelationship(
|
|
123
|
+
id=d["id"],
|
|
124
|
+
source_id=d["source_id"],
|
|
125
|
+
target_id=d["target_id"],
|
|
126
|
+
relationship_type=d["relationship_type"],
|
|
127
|
+
confidence=d.get("confidence", 0.5),
|
|
128
|
+
valid_from=d.get("valid_from"),
|
|
129
|
+
valid_to=d.get("valid_to"),
|
|
130
|
+
properties=d["properties"],
|
|
131
|
+
source_observation_id=d.get("source_observation_id"),
|
|
132
|
+
created_at=d.get("created_at"),
|
|
133
|
+
updated_at=d.get("updated_at"),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _fts_escape(q: str, max_len: int = 200) -> str:
|
|
138
|
+
safe = re.sub(r'[^\w\s]', '', q[:max_len], flags=re.UNICODE)
|
|
139
|
+
return safe.strip()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class PostgresBackend:
|
|
143
|
+
"""Async PostgreSQL backend for the World Model.
|
|
144
|
+
|
|
145
|
+
Requires asyncpg and a running Postgres instance.
|
|
146
|
+
Configure via WORLD_MODEL_PG_CONNECTION environment variable.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, connection_string: str) -> None:
|
|
150
|
+
if not HAS_ASYNCPG:
|
|
151
|
+
raise ImportError("asyncpg is required for the Postgres backend. Install with: pip install colony-sidecar[postgres]")
|
|
152
|
+
self._conn_string = connection_string
|
|
153
|
+
self._pool: Optional[asyncpg.Pool] = None
|
|
154
|
+
|
|
155
|
+
async def connect(self) -> None:
|
|
156
|
+
self._pool = await asyncpg.create_pool(self._conn_string, min_size=2, max_size=10)
|
|
157
|
+
await self._apply_schema()
|
|
158
|
+
|
|
159
|
+
async def close(self) -> None:
|
|
160
|
+
if self._pool:
|
|
161
|
+
await self._pool.close()
|
|
162
|
+
self._pool = None
|
|
163
|
+
|
|
164
|
+
async def _apply_schema(self) -> None:
|
|
165
|
+
async with self._pool.acquire() as conn:
|
|
166
|
+
await conn.execute("""
|
|
167
|
+
CREATE TABLE IF NOT EXISTS wm_entities (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
name TEXT NOT NULL,
|
|
170
|
+
entity_type TEXT NOT NULL,
|
|
171
|
+
aliases JSONB DEFAULT '[]',
|
|
172
|
+
external_ids JSONB DEFAULT '{}',
|
|
173
|
+
confidence REAL DEFAULT 0.5,
|
|
174
|
+
properties JSONB DEFAULT '{}',
|
|
175
|
+
first_seen TIMESTAMPTZ DEFAULT NOW(),
|
|
176
|
+
last_seen TIMESTAMPTZ DEFAULT NOW(),
|
|
177
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
178
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
CREATE INDEX IF NOT EXISTS idx_wm_entities_type ON wm_entities(entity_type);
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_wm_entities_name ON wm_entities USING gin(to_tsvector('english', name));
|
|
183
|
+
|
|
184
|
+
CREATE TABLE IF NOT EXISTS wm_relationships (
|
|
185
|
+
id TEXT PRIMARY KEY,
|
|
186
|
+
source_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
187
|
+
target_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
|
|
188
|
+
relationship_type TEXT NOT NULL,
|
|
189
|
+
confidence REAL DEFAULT 0.5,
|
|
190
|
+
valid_from TIMESTAMPTZ,
|
|
191
|
+
valid_to TIMESTAMPTZ,
|
|
192
|
+
properties JSONB DEFAULT '{}',
|
|
193
|
+
source_observation_id TEXT,
|
|
194
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
195
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_source ON wm_relationships(source_id);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_target ON wm_relationships(target_id);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_wm_rels_type ON wm_relationships(relationship_type);
|
|
201
|
+
|
|
202
|
+
CREATE TABLE IF NOT EXISTS wm_observations (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
entity_id TEXT REFERENCES wm_entities(id) ON DELETE SET NULL,
|
|
205
|
+
relationship_id TEXT REFERENCES wm_relationships(id) ON DELETE SET NULL,
|
|
206
|
+
observation TEXT NOT NULL,
|
|
207
|
+
source TEXT DEFAULT '',
|
|
208
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS wm_merge_proposals (
|
|
212
|
+
id TEXT PRIMARY KEY,
|
|
213
|
+
candidate_id TEXT NOT NULL,
|
|
214
|
+
existing_id TEXT NOT NULL,
|
|
215
|
+
match_confidence REAL DEFAULT 0.0,
|
|
216
|
+
match_reason TEXT DEFAULT '',
|
|
217
|
+
evidence JSONB DEFAULT '{}',
|
|
218
|
+
status TEXT DEFAULT 'pending',
|
|
219
|
+
resolved_at TIMESTAMPTZ,
|
|
220
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
CREATE TABLE IF NOT EXISTS wm_merge_log (
|
|
224
|
+
id TEXT PRIMARY KEY,
|
|
225
|
+
surviving_id TEXT NOT NULL,
|
|
226
|
+
retired_id TEXT NOT NULL,
|
|
227
|
+
relationships_repointed INTEGER DEFAULT 0,
|
|
228
|
+
properties_updated INTEGER DEFAULT 0,
|
|
229
|
+
executed_by TEXT DEFAULT '',
|
|
230
|
+
merge_proposal_id TEXT,
|
|
231
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CREATE TABLE IF NOT EXISTS wm_schema_migrations (
|
|
235
|
+
version TEXT PRIMARY KEY,
|
|
236
|
+
description TEXT DEFAULT '',
|
|
237
|
+
applied_at TIMESTAMPTZ DEFAULT NOW()
|
|
238
|
+
);
|
|
239
|
+
""")
|
|
240
|
+
# Record migrations
|
|
241
|
+
for version, desc in [
|
|
242
|
+
("wm-001", "Create base entity tables and GIN index"),
|
|
243
|
+
("wm-002", "Create relationship table"),
|
|
244
|
+
("wm-003", "Create observations table"),
|
|
245
|
+
("wm-004", "Create merge proposals and audit log"),
|
|
246
|
+
("wm-005", "Create schema migrations table"),
|
|
247
|
+
]:
|
|
248
|
+
await conn.execute(
|
|
249
|
+
"INSERT INTO wm_schema_migrations (version, description) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
|
250
|
+
version, desc,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# ── Entity CRUD ───────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
async def get_entity(self, entity_id: str) -> Optional[BaseEntity]:
|
|
256
|
+
async with self._pool.acquire() as conn:
|
|
257
|
+
return await self._get_entity_in_conn(conn, entity_id)
|
|
258
|
+
|
|
259
|
+
async def _get_entity_in_conn(
|
|
260
|
+
self, conn: Any, entity_id: str
|
|
261
|
+
) -> Optional[BaseEntity]:
|
|
262
|
+
"""Read an entity using an already-acquired connection.
|
|
263
|
+
|
|
264
|
+
Lets callers participate in an outer transaction (see ``execute_merge``)
|
|
265
|
+
instead of opening a second pool connection that would be unable to
|
|
266
|
+
see the in-flight writes.
|
|
267
|
+
"""
|
|
268
|
+
row = await conn.fetchrow("SELECT * FROM wm_entities WHERE id = $1", entity_id)
|
|
269
|
+
return _record_to_entity(row) if row else None
|
|
270
|
+
|
|
271
|
+
async def get_entity_by_external_id(self, key: str, value: str) -> Optional[BaseEntity]:
|
|
272
|
+
async with self._pool.acquire() as conn:
|
|
273
|
+
row = await conn.fetchrow(
|
|
274
|
+
"SELECT * FROM wm_entities WHERE external_ids->>$1 = $2 LIMIT 1",
|
|
275
|
+
key, value,
|
|
276
|
+
)
|
|
277
|
+
return _record_to_entity(row) if row else None
|
|
278
|
+
|
|
279
|
+
async def find_entities(
|
|
280
|
+
self,
|
|
281
|
+
query: str,
|
|
282
|
+
entity_type: Optional[str] = None,
|
|
283
|
+
min_confidence: float = 0.30,
|
|
284
|
+
limit: int = 20,
|
|
285
|
+
) -> List[BaseEntity]:
|
|
286
|
+
async with self._pool.acquire() as conn:
|
|
287
|
+
if query:
|
|
288
|
+
sql = """
|
|
289
|
+
SELECT * FROM wm_entities
|
|
290
|
+
WHERE to_tsvector('english', name) @@ to_tsquery('english', $1)
|
|
291
|
+
AND confidence >= $2
|
|
292
|
+
"""
|
|
293
|
+
# Convert to tsquery format
|
|
294
|
+
ts_query = " | ".join(_fts_escape(query).split())
|
|
295
|
+
params = [ts_query, min_confidence]
|
|
296
|
+
param_idx = 3
|
|
297
|
+
if entity_type:
|
|
298
|
+
sql += f" AND entity_type = ${param_idx}"
|
|
299
|
+
params.append(entity_type)
|
|
300
|
+
param_idx += 1
|
|
301
|
+
sql += f" ORDER BY confidence DESC LIMIT ${param_idx}"
|
|
302
|
+
params.append(limit)
|
|
303
|
+
rows = await conn.fetch(sql, *params)
|
|
304
|
+
else:
|
|
305
|
+
sql = "SELECT * FROM wm_entities WHERE confidence >= $1"
|
|
306
|
+
params = [min_confidence]
|
|
307
|
+
param_idx = 2
|
|
308
|
+
if entity_type:
|
|
309
|
+
sql += f" AND entity_type = ${param_idx}"
|
|
310
|
+
params.append(entity_type)
|
|
311
|
+
param_idx += 1
|
|
312
|
+
sql += f" ORDER BY confidence DESC LIMIT ${param_idx}"
|
|
313
|
+
params.append(limit)
|
|
314
|
+
rows = await conn.fetch(sql, *params)
|
|
315
|
+
return [_record_to_entity(r) for r in rows]
|
|
316
|
+
|
|
317
|
+
async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
|
|
318
|
+
if not entity.id:
|
|
319
|
+
entity.id = _generate_id(ENTITY_ID_PREFIX)
|
|
320
|
+
p = _entity_to_params(entity)
|
|
321
|
+
async with self._pool.acquire() as conn:
|
|
322
|
+
await conn.execute("""
|
|
323
|
+
INSERT INTO wm_entities
|
|
324
|
+
(id, name, entity_type, aliases, external_ids, properties,
|
|
325
|
+
confidence, first_seen, last_seen, created_at, updated_at)
|
|
326
|
+
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6::jsonb,
|
|
327
|
+
$7, $8, $9, $10, $11)
|
|
328
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
329
|
+
name = EXCLUDED.name,
|
|
330
|
+
aliases = EXCLUDED.aliases,
|
|
331
|
+
external_ids = EXCLUDED.external_ids,
|
|
332
|
+
properties = EXCLUDED.properties,
|
|
333
|
+
confidence = EXCLUDED.confidence,
|
|
334
|
+
last_seen = EXCLUDED.last_seen,
|
|
335
|
+
updated_at = EXCLUDED.updated_at
|
|
336
|
+
""", p["id"], p["name"], p["entity_type"], p["aliases"], p["external_ids"],
|
|
337
|
+
p["properties"], p["confidence"], p["first_seen"], p["last_seen"],
|
|
338
|
+
p["created_at"], p["updated_at"])
|
|
339
|
+
return entity
|
|
340
|
+
|
|
341
|
+
async def update_entity_property(
|
|
342
|
+
self, entity_id: str, property_key: str, property_value: Any, confidence: float
|
|
343
|
+
) -> None:
|
|
344
|
+
entity = await self.get_entity(entity_id)
|
|
345
|
+
if not entity:
|
|
346
|
+
return
|
|
347
|
+
import dataclasses
|
|
348
|
+
existing_fields = {f.name for f in dataclasses.fields(type(entity))}
|
|
349
|
+
if property_key in existing_fields:
|
|
350
|
+
setattr(entity, property_key, property_value)
|
|
351
|
+
if confidence > entity.confidence:
|
|
352
|
+
entity.confidence = confidence
|
|
353
|
+
await self.upsert_entity(entity)
|
|
354
|
+
else:
|
|
355
|
+
if confidence >= entity.properties.get(f"_conf_{property_key}", 0.0):
|
|
356
|
+
entity.properties[property_key] = property_value
|
|
357
|
+
entity.properties[f"_conf_{property_key}"] = confidence
|
|
358
|
+
await self.upsert_entity(entity)
|
|
359
|
+
|
|
360
|
+
async def add_entity_alias(self, entity_id: str, alias: str) -> None:
|
|
361
|
+
entity = await self.get_entity(entity_id)
|
|
362
|
+
if entity and alias not in entity.aliases:
|
|
363
|
+
entity.aliases.append(alias)
|
|
364
|
+
async with self._pool.acquire() as conn:
|
|
365
|
+
await conn.execute(
|
|
366
|
+
"UPDATE wm_entities SET aliases = $1::jsonb, updated_at = $2 WHERE id = $3",
|
|
367
|
+
json.dumps(entity.aliases), _now_iso(), entity_id,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async def delete_entity(self, entity_id: str) -> None:
|
|
371
|
+
async with self._pool.acquire() as conn:
|
|
372
|
+
await conn.execute("DELETE FROM wm_entities WHERE id = $1", entity_id)
|
|
373
|
+
|
|
374
|
+
# ── Relationship CRUD ─────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
async def get_relationship(self, rel_id: str) -> Optional[WorldRelationship]:
|
|
377
|
+
async with self._pool.acquire() as conn:
|
|
378
|
+
row = await conn.fetchrow("SELECT * FROM wm_relationships WHERE id = $1", rel_id)
|
|
379
|
+
return _record_to_relationship(row) if row else None
|
|
380
|
+
|
|
381
|
+
async def upsert_relationship(self, rel: WorldRelationship) -> WorldRelationship:
|
|
382
|
+
if not rel.id:
|
|
383
|
+
rel.id = _generate_id(RELATIONSHIP_ID_PREFIX)
|
|
384
|
+
now = _now_iso()
|
|
385
|
+
async with self._pool.acquire() as conn:
|
|
386
|
+
await conn.execute("""
|
|
387
|
+
INSERT INTO wm_relationships
|
|
388
|
+
(id, source_id, target_id, relationship_type, confidence,
|
|
389
|
+
valid_from, valid_to, properties, source_observation_id,
|
|
390
|
+
created_at, updated_at)
|
|
391
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)
|
|
392
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
393
|
+
confidence = EXCLUDED.confidence,
|
|
394
|
+
valid_from = EXCLUDED.valid_from,
|
|
395
|
+
valid_to = EXCLUDED.valid_to,
|
|
396
|
+
properties = EXCLUDED.properties,
|
|
397
|
+
updated_at = EXCLUDED.updated_at
|
|
398
|
+
""", rel.id, rel.source_id, rel.target_id, rel.relationship_type,
|
|
399
|
+
rel.confidence, rel.valid_from, rel.valid_to,
|
|
400
|
+
json.dumps(rel.properties), rel.source_observation_id,
|
|
401
|
+
rel.created_at or now, now)
|
|
402
|
+
return rel
|
|
403
|
+
|
|
404
|
+
async def close_relationship(self, rel_id: str, valid_to: str) -> None:
|
|
405
|
+
async with self._pool.acquire() as conn:
|
|
406
|
+
await conn.execute(
|
|
407
|
+
"UPDATE wm_relationships SET valid_to = $1, updated_at = $2 WHERE id = $3",
|
|
408
|
+
valid_to, _now_iso(), rel_id,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
async def query_relationships(
|
|
412
|
+
self,
|
|
413
|
+
source_id: Optional[str] = None,
|
|
414
|
+
target_id: Optional[str] = None,
|
|
415
|
+
relationship_type: Optional[str] = None,
|
|
416
|
+
target_types: Optional[List[str]] = None,
|
|
417
|
+
active_only: bool = False,
|
|
418
|
+
min_confidence: float = 0.30,
|
|
419
|
+
limit: int = 100,
|
|
420
|
+
) -> List[WorldRelationship]:
|
|
421
|
+
conditions = ["r.confidence >= $1"]
|
|
422
|
+
params: list = [min_confidence]
|
|
423
|
+
idx = 2
|
|
424
|
+
|
|
425
|
+
sql = "SELECT r.* FROM wm_relationships r"
|
|
426
|
+
|
|
427
|
+
if target_types:
|
|
428
|
+
sql += " JOIN wm_entities te ON te.id = r.target_id"
|
|
429
|
+
placeholders = ", ".join(f"${idx + i}" for i in range(len(target_types)))
|
|
430
|
+
conditions.append(f"te.entity_type IN ({placeholders})")
|
|
431
|
+
params.extend(target_types)
|
|
432
|
+
idx += len(target_types)
|
|
433
|
+
|
|
434
|
+
if source_id:
|
|
435
|
+
conditions.append(f"r.source_id = ${idx}")
|
|
436
|
+
params.append(source_id)
|
|
437
|
+
idx += 1
|
|
438
|
+
if target_id:
|
|
439
|
+
conditions.append(f"r.target_id = ${idx}")
|
|
440
|
+
params.append(target_id)
|
|
441
|
+
idx += 1
|
|
442
|
+
if relationship_type:
|
|
443
|
+
conditions.append(f"r.relationship_type = ${idx}")
|
|
444
|
+
params.append(relationship_type)
|
|
445
|
+
idx += 1
|
|
446
|
+
else:
|
|
447
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
448
|
+
placeholders = ", ".join(
|
|
449
|
+
f"${idx + i}" for i in range(len(_CAUSAL_EXCLUSION)))
|
|
450
|
+
conditions.append(f"r.relationship_type NOT IN ({placeholders})")
|
|
451
|
+
params.extend(_CAUSAL_EXCLUSION)
|
|
452
|
+
idx += len(_CAUSAL_EXCLUSION)
|
|
453
|
+
if active_only:
|
|
454
|
+
conditions.append("r.valid_to IS NULL")
|
|
455
|
+
|
|
456
|
+
sql += " WHERE " + " AND ".join(conditions)
|
|
457
|
+
sql += f" ORDER BY r.confidence DESC LIMIT ${idx}"
|
|
458
|
+
params.append(limit)
|
|
459
|
+
|
|
460
|
+
async with self._pool.acquire() as conn:
|
|
461
|
+
rows = await conn.fetch(sql, *params)
|
|
462
|
+
return [_record_to_relationship(r) for r in rows]
|
|
463
|
+
|
|
464
|
+
async def query_at_time(
|
|
465
|
+
self,
|
|
466
|
+
entity_id: str,
|
|
467
|
+
as_of: str,
|
|
468
|
+
relationship_types: Optional[List[str]] = None,
|
|
469
|
+
) -> List[WorldRelationship]:
|
|
470
|
+
sql = """
|
|
471
|
+
SELECT * FROM wm_relationships
|
|
472
|
+
WHERE (source_id = $1 OR target_id = $2)
|
|
473
|
+
AND (valid_from IS NULL OR valid_from <= $3)
|
|
474
|
+
AND (valid_to IS NULL OR valid_to > $4)
|
|
475
|
+
"""
|
|
476
|
+
params: list = [entity_id, entity_id, as_of, as_of]
|
|
477
|
+
if relationship_types:
|
|
478
|
+
placeholders = ", ".join(f"${5 + i}" for i in range(len(relationship_types)))
|
|
479
|
+
sql += f" AND relationship_type IN ({placeholders})"
|
|
480
|
+
params.extend(relationship_types)
|
|
481
|
+
else:
|
|
482
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
483
|
+
placeholders = ", ".join(
|
|
484
|
+
f"${5 + i}" for i in range(len(_CAUSAL_EXCLUSION)))
|
|
485
|
+
sql += f" AND relationship_type NOT IN ({placeholders})"
|
|
486
|
+
params.extend(_CAUSAL_EXCLUSION)
|
|
487
|
+
async with self._pool.acquire() as conn:
|
|
488
|
+
rows = await conn.fetch(sql, *params)
|
|
489
|
+
return [_record_to_relationship(r) for r in rows]
|
|
490
|
+
|
|
491
|
+
# ── Graph Traversal ───────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
async def get_neighbors(
|
|
494
|
+
self,
|
|
495
|
+
entity_id: str,
|
|
496
|
+
min_confidence: float = 0.30,
|
|
497
|
+
relationship_types: Optional[List[str]] = None,
|
|
498
|
+
) -> List[tuple]:
|
|
499
|
+
sql = """
|
|
500
|
+
SELECT e.*, r.id as rel_id, r.source_id, r.target_id,
|
|
501
|
+
r.relationship_type, r.confidence as rel_confidence,
|
|
502
|
+
r.valid_from, r.valid_to, r.properties as rel_props,
|
|
503
|
+
r.source_observation_id, r.created_at as rel_created_at,
|
|
504
|
+
r.updated_at as rel_updated_at
|
|
505
|
+
FROM wm_relationships r
|
|
506
|
+
JOIN wm_entities e ON (
|
|
507
|
+
(r.source_id = $1 AND e.id = r.target_id) OR
|
|
508
|
+
(r.target_id = $2 AND e.id = r.source_id)
|
|
509
|
+
)
|
|
510
|
+
WHERE r.confidence >= $3 AND e.confidence >= $4
|
|
511
|
+
"""
|
|
512
|
+
params: list = [entity_id, entity_id, min_confidence, min_confidence]
|
|
513
|
+
if relationship_types:
|
|
514
|
+
placeholders = ", ".join(f"${5 + i}" for i in range(len(relationship_types)))
|
|
515
|
+
sql += f" AND r.relationship_type IN ({placeholders})"
|
|
516
|
+
params.extend(relationship_types)
|
|
517
|
+
else:
|
|
518
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
519
|
+
placeholders = ", ".join(
|
|
520
|
+
f"${5 + i}" for i in range(len(_CAUSAL_EXCLUSION)))
|
|
521
|
+
sql += f" AND r.relationship_type NOT IN ({placeholders})"
|
|
522
|
+
params.extend(_CAUSAL_EXCLUSION)
|
|
523
|
+
|
|
524
|
+
async with self._pool.acquire() as conn:
|
|
525
|
+
rows = await conn.fetch(sql, *params)
|
|
526
|
+
|
|
527
|
+
results = []
|
|
528
|
+
for row in rows:
|
|
529
|
+
entity = _record_to_entity(row)
|
|
530
|
+
d = dict(row) if isinstance(row, dict) else {k: v for k, v in row.items()}
|
|
531
|
+
rel = WorldRelationship(
|
|
532
|
+
id=d["rel_id"],
|
|
533
|
+
source_id=d["source_id"],
|
|
534
|
+
target_id=d["target_id"],
|
|
535
|
+
relationship_type=d["relationship_type"],
|
|
536
|
+
confidence=d["rel_confidence"],
|
|
537
|
+
valid_from=d.get("valid_from"),
|
|
538
|
+
valid_to=d.get("valid_to"),
|
|
539
|
+
properties=json.loads(d.get("rel_props") or "{}") if isinstance(d.get("rel_props"), str) else (d.get("rel_props") or {}),
|
|
540
|
+
source_observation_id=d.get("source_observation_id"),
|
|
541
|
+
created_at=d.get("rel_created_at"),
|
|
542
|
+
updated_at=d.get("rel_updated_at"),
|
|
543
|
+
)
|
|
544
|
+
results.append((entity, rel))
|
|
545
|
+
return results
|
|
546
|
+
|
|
547
|
+
# ── Observations ──────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
async def add_observation(
|
|
550
|
+
self,
|
|
551
|
+
entity_id: Optional[str],
|
|
552
|
+
relationship_id: Optional[str],
|
|
553
|
+
observation: str,
|
|
554
|
+
source: str,
|
|
555
|
+
) -> str:
|
|
556
|
+
obs_id = _generate_id(OBSERVATION_ID_PREFIX)
|
|
557
|
+
async with self._pool.acquire() as conn:
|
|
558
|
+
await conn.execute(
|
|
559
|
+
"INSERT INTO wm_observations (id, entity_id, relationship_id, observation, source) VALUES ($1, $2, $3, $4, $5)",
|
|
560
|
+
obs_id, entity_id, relationship_id, observation, source,
|
|
561
|
+
)
|
|
562
|
+
return obs_id
|
|
563
|
+
|
|
564
|
+
# ── Merge Proposals ───────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
async def create_merge_proposal(
|
|
567
|
+
self,
|
|
568
|
+
candidate_id: str,
|
|
569
|
+
existing_id: str,
|
|
570
|
+
confidence: float,
|
|
571
|
+
reason: str,
|
|
572
|
+
evidence: Optional[Dict[str, Any]] = None,
|
|
573
|
+
) -> str:
|
|
574
|
+
proposal_id = _generate_id(MERGE_PROPOSAL_ID_PREFIX)
|
|
575
|
+
async with self._pool.acquire() as conn:
|
|
576
|
+
await conn.execute(
|
|
577
|
+
"INSERT INTO wm_merge_proposals (id, candidate_id, existing_id, match_confidence, match_reason, evidence) VALUES ($1, $2, $3, $4, $5, $6::jsonb)",
|
|
578
|
+
proposal_id, candidate_id, existing_id, confidence, reason, json.dumps(evidence or {}),
|
|
579
|
+
)
|
|
580
|
+
return proposal_id
|
|
581
|
+
|
|
582
|
+
async def get_merge_proposal(self, proposal_id: str) -> Optional[Dict[str, Any]]:
|
|
583
|
+
async with self._pool.acquire() as conn:
|
|
584
|
+
row = await conn.fetchrow("SELECT * FROM wm_merge_proposals WHERE id = $1", proposal_id)
|
|
585
|
+
if not row:
|
|
586
|
+
return None
|
|
587
|
+
d = {k: v for k, v in row.items()}
|
|
588
|
+
d["evidence"] = json.loads(d.get("evidence") or "{}") if isinstance(d.get("evidence"), str) else (d.get("evidence") or {})
|
|
589
|
+
return d
|
|
590
|
+
|
|
591
|
+
async def update_merge_proposal_status(self, proposal_id: str, status: str) -> None:
|
|
592
|
+
async with self._pool.acquire() as conn:
|
|
593
|
+
await conn.execute(
|
|
594
|
+
"UPDATE wm_merge_proposals SET status = $1, resolved_at = $2 WHERE id = $3",
|
|
595
|
+
status, _now_iso(), proposal_id,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
async def get_pending_merge_proposals(self) -> List[Dict[str, Any]]:
|
|
599
|
+
async with self._pool.acquire() as conn:
|
|
600
|
+
rows = await conn.fetch("SELECT * FROM wm_merge_proposals WHERE status = 'pending'")
|
|
601
|
+
result = []
|
|
602
|
+
for row in rows:
|
|
603
|
+
d = {k: v for k, v in row.items()}
|
|
604
|
+
d["evidence"] = json.loads(d.get("evidence") or "{}") if isinstance(d.get("evidence"), str) else (d.get("evidence") or {})
|
|
605
|
+
result.append(d)
|
|
606
|
+
return result
|
|
607
|
+
|
|
608
|
+
# ── Merge Execution ───────────────────────────────────────────────────────
|
|
609
|
+
|
|
610
|
+
async def execute_merge(
|
|
611
|
+
self,
|
|
612
|
+
surviving_id: str,
|
|
613
|
+
retired_id: str,
|
|
614
|
+
executed_by: str,
|
|
615
|
+
proposal_id: Optional[str] = None,
|
|
616
|
+
) -> str:
|
|
617
|
+
# Wrap the whole merge in a single Postgres transaction: asyncpg
|
|
618
|
+
# auto-commits each statement otherwise, so a failure between the
|
|
619
|
+
# relationship repoint and the DELETE would leave dangling references
|
|
620
|
+
# to a half-retired entity with no audit row.
|
|
621
|
+
async with self._pool.acquire() as conn:
|
|
622
|
+
async with conn.transaction():
|
|
623
|
+
# Read both entities up-front so we fail before mutating if
|
|
624
|
+
# either is missing — and so a concurrent merge cannot delete
|
|
625
|
+
# one of them between our reads and writes.
|
|
626
|
+
surviving = await self._get_entity_in_conn(conn, surviving_id)
|
|
627
|
+
retired = await self._get_entity_in_conn(conn, retired_id)
|
|
628
|
+
|
|
629
|
+
# Count relationships to repoint
|
|
630
|
+
rel_count = await conn.fetchval(
|
|
631
|
+
"SELECT COUNT(*) FROM wm_relationships WHERE source_id = $1 OR target_id = $1",
|
|
632
|
+
retired_id,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# Repoint
|
|
636
|
+
await conn.execute("UPDATE wm_relationships SET source_id = $1 WHERE source_id = $2", surviving_id, retired_id)
|
|
637
|
+
await conn.execute("UPDATE wm_relationships SET target_id = $1 WHERE target_id = $2", surviving_id, retired_id)
|
|
638
|
+
|
|
639
|
+
# Merge aliases
|
|
640
|
+
props_updated = 0
|
|
641
|
+
if surviving and retired:
|
|
642
|
+
new_aliases = surviving.aliases.copy()
|
|
643
|
+
if retired_id not in new_aliases:
|
|
644
|
+
new_aliases.append(retired_id)
|
|
645
|
+
for alias in retired.aliases:
|
|
646
|
+
if alias not in new_aliases:
|
|
647
|
+
new_aliases.append(alias)
|
|
648
|
+
props_updated = len(new_aliases) - len(surviving.aliases)
|
|
649
|
+
await conn.execute(
|
|
650
|
+
"UPDATE wm_entities SET aliases = $1::jsonb, updated_at = $2 WHERE id = $3",
|
|
651
|
+
json.dumps(new_aliases), _now_iso(), surviving_id,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# Delete retired entity
|
|
655
|
+
await conn.execute("DELETE FROM wm_entities WHERE id = $1", retired_id)
|
|
656
|
+
|
|
657
|
+
# Audit record
|
|
658
|
+
audit_id = _generate_id(MERGE_AUDIT_ID_PREFIX)
|
|
659
|
+
await conn.execute(
|
|
660
|
+
"INSERT INTO wm_merge_log (id, surviving_id, retired_id, relationships_repointed, properties_updated, executed_by, merge_proposal_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
|
661
|
+
audit_id, surviving_id, retired_id, rel_count, props_updated, executed_by, proposal_id,
|
|
662
|
+
)
|
|
663
|
+
return audit_id
|
|
664
|
+
|
|
665
|
+
# ── Stats ─────────────────────────────────────────────────────────────────
|
|
666
|
+
|
|
667
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
668
|
+
async with self._pool.acquire() as conn:
|
|
669
|
+
type_rows = await conn.fetch("SELECT entity_type, COUNT(*) as cnt FROM wm_entities GROUP BY entity_type")
|
|
670
|
+
total_entities = await conn.fetchval("SELECT COUNT(*) FROM wm_entities")
|
|
671
|
+
total_rels = await conn.fetchval("SELECT COUNT(*) FROM wm_relationships")
|
|
672
|
+
active_rels = await conn.fetchval("SELECT COUNT(*) FROM wm_relationships WHERE valid_to IS NULL")
|
|
673
|
+
total_obs = await conn.fetchval("SELECT COUNT(*) FROM wm_observations")
|
|
674
|
+
pending_proposals = await conn.fetchval("SELECT COUNT(*) FROM wm_merge_proposals WHERE status = 'pending'")
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
"total_entities": total_entities,
|
|
678
|
+
"entities_by_type": {r["entity_type"]: r["cnt"] for r in type_rows},
|
|
679
|
+
"total_relationships": total_rels,
|
|
680
|
+
"active_relationships": active_rels,
|
|
681
|
+
"total_observations": total_obs,
|
|
682
|
+
"merge_proposals_pending": pending_proposals,
|
|
683
|
+
}
|