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,654 @@
|
|
|
1
|
+
"""Neo4j storage backend for the Colony World Model.
|
|
2
|
+
|
|
3
|
+
Implements the same interface as SQLiteBackend but backed by Neo4j,
|
|
4
|
+
which provides native graph traversal (Cypher) for relationship queries,
|
|
5
|
+
neighborhood traversal, and path finding.
|
|
6
|
+
|
|
7
|
+
Requires: ``neo4j`` driver (pip install neo4j)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
import secrets
|
|
16
|
+
import time
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from ..constants import (
|
|
21
|
+
CAUSAL_RELATIONSHIP_TYPES,
|
|
22
|
+
ENTITY_ID_PREFIX,
|
|
23
|
+
RELATIONSHIP_ID_PREFIX,
|
|
24
|
+
OBSERVATION_ID_PREFIX,
|
|
25
|
+
MERGE_PROPOSAL_ID_PREFIX,
|
|
26
|
+
MERGE_AUDIT_ID_PREFIX,
|
|
27
|
+
)
|
|
28
|
+
from ..entities import BaseEntity, ENTITY_CLASS_MAP, entity_from_dict
|
|
29
|
+
from ..relationships import WorldRelationship
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Query-only causal guard (H2.5): untyped (generic) reads NEVER return
|
|
34
|
+
# causal edges; only /world/causal/* and explicitly typed queries do.
|
|
35
|
+
_CAUSAL_EXCLUSION = sorted(CAUSAL_RELATIONSHIP_TYPES)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _generate_id(prefix: str) -> str:
|
|
39
|
+
ts = int(time.time() * 1000)
|
|
40
|
+
rand = secrets.token_hex(6)
|
|
41
|
+
return f"{prefix}-{ts}-{rand}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _entity_to_props(entity: BaseEntity) -> Dict[str, Any]:
|
|
45
|
+
"""Convert an entity dataclass to a flat dict for Neo4j node properties."""
|
|
46
|
+
import dataclasses
|
|
47
|
+
props = {}
|
|
48
|
+
for f in dataclasses.fields(entity):
|
|
49
|
+
val = getattr(entity, f.name)
|
|
50
|
+
if val is None:
|
|
51
|
+
continue
|
|
52
|
+
if isinstance(val, (list, dict)):
|
|
53
|
+
props[f.name] = json.dumps(val)
|
|
54
|
+
elif isinstance(val, datetime):
|
|
55
|
+
props[f.name] = val.isoformat()
|
|
56
|
+
else:
|
|
57
|
+
props[f.name] = val
|
|
58
|
+
return props
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _props_to_entity(props: Dict[str, Any]) -> BaseEntity:
|
|
62
|
+
"""Convert Neo4j node properties back to an entity dataclass."""
|
|
63
|
+
data = dict(props)
|
|
64
|
+
for key in ("aliases", "external_ids", "properties"):
|
|
65
|
+
if key in data and isinstance(data[key], str):
|
|
66
|
+
try:
|
|
67
|
+
data[key] = json.loads(data[key])
|
|
68
|
+
except (json.JSONDecodeError, TypeError):
|
|
69
|
+
data[key] = [] if key == "aliases" else {}
|
|
70
|
+
for key in ("first_seen", "last_seen", "created_at", "updated_at"):
|
|
71
|
+
if key in data and isinstance(data[key], str):
|
|
72
|
+
try:
|
|
73
|
+
data[key] = datetime.fromisoformat(data[key])
|
|
74
|
+
except (ValueError, TypeError):
|
|
75
|
+
data[key] = None
|
|
76
|
+
return entity_from_dict(data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _rel_to_props(rel: WorldRelationship) -> Dict[str, Any]:
|
|
80
|
+
"""Convert a WorldRelationship to a flat dict for Neo4j edge properties."""
|
|
81
|
+
import dataclasses
|
|
82
|
+
props = {}
|
|
83
|
+
for f in dataclasses.fields(rel):
|
|
84
|
+
val = getattr(rel, f.name)
|
|
85
|
+
if val is None:
|
|
86
|
+
continue
|
|
87
|
+
if isinstance(val, dict):
|
|
88
|
+
props[f.name] = json.dumps(val)
|
|
89
|
+
else:
|
|
90
|
+
props[f.name] = val
|
|
91
|
+
return props
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _props_to_rel(props: Dict[str, Any]) -> WorldRelationship:
|
|
95
|
+
"""Convert Neo4j edge properties back to a WorldRelationship."""
|
|
96
|
+
import dataclasses
|
|
97
|
+
data = dict(props)
|
|
98
|
+
if "properties" in data and isinstance(data["properties"], str):
|
|
99
|
+
try:
|
|
100
|
+
data["properties"] = json.loads(data["properties"])
|
|
101
|
+
except (json.JSONDecodeError, TypeError):
|
|
102
|
+
data["properties"] = {}
|
|
103
|
+
valid = {f.name for f in dataclasses.fields(WorldRelationship)}
|
|
104
|
+
return WorldRelationship(**{k: v for k, v in data.items() if k in valid})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _sanitize_rel_type(rel_type: str) -> str:
|
|
108
|
+
"""Sanitize a relationship type for use in Cypher (alphanumeric + underscore)."""
|
|
109
|
+
safe = re.sub(r'[^A-Z_0-9]', '', rel_type.upper())
|
|
110
|
+
return safe or "WM_RELATED_TO"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _node_to_dict(node) -> Dict[str, Any]:
|
|
114
|
+
"""Convert a Neo4j Node to a plain dict. Handles both Node objects and pre-converted dicts."""
|
|
115
|
+
if isinstance(node, dict):
|
|
116
|
+
return node
|
|
117
|
+
try:
|
|
118
|
+
return dict(node.items())
|
|
119
|
+
except (TypeError, AttributeError):
|
|
120
|
+
return dict(node)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _rel_to_dict(rel) -> Dict[str, Any]:
|
|
124
|
+
"""Convert a Neo4j Relationship to a plain dict.
|
|
125
|
+
|
|
126
|
+
When using result.data(), relationships come as tuples:
|
|
127
|
+
(start_node_dict, rel_type_str, end_node_dict)
|
|
128
|
+
When using result.single(), they come as Relationship objects.
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(rel, tuple):
|
|
131
|
+
# result.data() format: (start_node_dict, rel_type_str, end_node_dict)
|
|
132
|
+
# This is unreliable — callers should use properties(r) instead
|
|
133
|
+
return {}
|
|
134
|
+
if isinstance(rel, dict):
|
|
135
|
+
return rel
|
|
136
|
+
try:
|
|
137
|
+
return dict(rel.items())
|
|
138
|
+
except (TypeError, AttributeError):
|
|
139
|
+
return dict(rel)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Neo4jBackend:
|
|
143
|
+
"""Neo4j-backed storage for the Colony World Model.
|
|
144
|
+
|
|
145
|
+
Node labels: ``Entity`` (all entity types, with ``entity_type`` as property)
|
|
146
|
+
Relationship types: prefixed ``WM_`` types from constants
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, uri: str, database: str = "neo4j",
|
|
150
|
+
username: str = "neo4j", password: str = "") -> None:
|
|
151
|
+
self._uri = uri
|
|
152
|
+
self._database = database
|
|
153
|
+
self._username = username
|
|
154
|
+
self._password = password
|
|
155
|
+
self._driver = None
|
|
156
|
+
|
|
157
|
+
async def connect(self) -> None:
|
|
158
|
+
try:
|
|
159
|
+
from neo4j import AsyncGraphDatabase
|
|
160
|
+
except ImportError:
|
|
161
|
+
raise ImportError("neo4j driver not installed. Run: pip install neo4j")
|
|
162
|
+
|
|
163
|
+
self._driver = AsyncGraphDatabase.driver(
|
|
164
|
+
self._uri,
|
|
165
|
+
auth=(self._username, self._password),
|
|
166
|
+
)
|
|
167
|
+
async with self._driver.session(database=self._database) as session:
|
|
168
|
+
result = await session.run("RETURN 1")
|
|
169
|
+
await result.consume()
|
|
170
|
+
logger.info("Neo4j backend connected: %s (db=%s)", self._uri, self._database)
|
|
171
|
+
await self._apply_schema()
|
|
172
|
+
|
|
173
|
+
async def close(self) -> None:
|
|
174
|
+
if self._driver:
|
|
175
|
+
await self._driver.close()
|
|
176
|
+
self._driver = None
|
|
177
|
+
|
|
178
|
+
async def _apply_schema(self) -> None:
|
|
179
|
+
"""Create indexes and constraints for optimal query performance."""
|
|
180
|
+
async with self._driver.session(database=self._database) as session:
|
|
181
|
+
for cypher in [
|
|
182
|
+
"CREATE CONSTRAINT entity_id_unique IF NOT EXISTS FOR (e:Entity) REQUIRE e.id IS UNIQUE",
|
|
183
|
+
"CREATE INDEX entity_type_index IF NOT EXISTS FOR (e:Entity) ON (e.entity_type)",
|
|
184
|
+
"CREATE INDEX entity_name_index IF NOT EXISTS FOR (e:Entity) ON (e.name)",
|
|
185
|
+
"CREATE FULLTEXT INDEX entity_search IF NOT EXISTS FOR (e:Entity) ON EACH [e.name, e.aliases]",
|
|
186
|
+
]:
|
|
187
|
+
try:
|
|
188
|
+
result = await session.run(cypher)
|
|
189
|
+
await result.consume()
|
|
190
|
+
except Exception:
|
|
191
|
+
pass # May already exist
|
|
192
|
+
|
|
193
|
+
# ── Entity reads ──────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
async def get_entity(self, entity_id: str) -> Optional[BaseEntity]:
|
|
196
|
+
async with self._driver.session(database=self._database) as session:
|
|
197
|
+
result = await session.run(
|
|
198
|
+
"MATCH (e:Entity {id: $id}) RETURN e", id=entity_id,
|
|
199
|
+
)
|
|
200
|
+
record = await result.single()
|
|
201
|
+
if record is None:
|
|
202
|
+
return None
|
|
203
|
+
return _props_to_entity(_node_to_dict(record["e"]))
|
|
204
|
+
|
|
205
|
+
async def get_entity_by_external_id(
|
|
206
|
+
self, key: str, value: str
|
|
207
|
+
) -> Optional[BaseEntity]:
|
|
208
|
+
async with self._driver.session(database=self._database) as session:
|
|
209
|
+
result = await session.run(
|
|
210
|
+
"MATCH (e:Entity) WHERE e.external_ids CONTAINS $kv RETURN e",
|
|
211
|
+
kv=json.dumps({key: value}),
|
|
212
|
+
)
|
|
213
|
+
record = await result.single()
|
|
214
|
+
if record is None:
|
|
215
|
+
return None
|
|
216
|
+
return _props_to_entity(_node_to_dict(record["e"]))
|
|
217
|
+
|
|
218
|
+
async def find_entities(
|
|
219
|
+
self,
|
|
220
|
+
query: str,
|
|
221
|
+
entity_type: Optional[str] = None,
|
|
222
|
+
min_confidence: float = 0.30,
|
|
223
|
+
limit: int = 20,
|
|
224
|
+
) -> List[BaseEntity]:
|
|
225
|
+
safe_query = re.sub(r'[^\w\s]', '', query[:200]).strip()
|
|
226
|
+
if not safe_query:
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
async with self._driver.session(database=self._database) as session:
|
|
230
|
+
try:
|
|
231
|
+
cypher = (
|
|
232
|
+
"CALL db.index.fulltext.queryNodes('entity_search', $query) "
|
|
233
|
+
"YIELD node, score "
|
|
234
|
+
"WHERE node.confidence >= $min_conf "
|
|
235
|
+
)
|
|
236
|
+
params: Dict[str, Any] = {"query": safe_query, "min_conf": min_confidence, "limit": limit}
|
|
237
|
+
if entity_type:
|
|
238
|
+
cypher += "AND node.entity_type = $etype "
|
|
239
|
+
params["etype"] = entity_type
|
|
240
|
+
cypher += "RETURN node LIMIT $limit"
|
|
241
|
+
result = await session.run(cypher, params)
|
|
242
|
+
records = await result.data()
|
|
243
|
+
return [_props_to_entity(_node_to_dict(r["node"])) for r in records]
|
|
244
|
+
except Exception:
|
|
245
|
+
cypher = (
|
|
246
|
+
"MATCH (e:Entity) "
|
|
247
|
+
"WHERE e.name CONTAINS $query AND e.confidence >= $min_conf "
|
|
248
|
+
)
|
|
249
|
+
params = {"query": safe_query, "min_conf": min_confidence, "limit": limit}
|
|
250
|
+
if entity_type:
|
|
251
|
+
cypher += "AND e.entity_type = $etype "
|
|
252
|
+
params["etype"] = entity_type
|
|
253
|
+
cypher += "RETURN e LIMIT $limit"
|
|
254
|
+
result = await session.run(cypher, params)
|
|
255
|
+
records = await result.data()
|
|
256
|
+
return [_props_to_entity(_node_to_dict(r["e"])) for r in records]
|
|
257
|
+
|
|
258
|
+
# ── Entity writes ─────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
|
|
261
|
+
props = _entity_to_props(entity)
|
|
262
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
263
|
+
if "created_at" not in props:
|
|
264
|
+
props["created_at"] = now
|
|
265
|
+
props["updated_at"] = now
|
|
266
|
+
|
|
267
|
+
async with self._driver.session(database=self._database) as session:
|
|
268
|
+
result = await session.run(
|
|
269
|
+
"MERGE (e:Entity {id: $id}) SET e += $props RETURN e",
|
|
270
|
+
id=entity.id, props=props,
|
|
271
|
+
)
|
|
272
|
+
record = await result.single()
|
|
273
|
+
return _props_to_entity(_node_to_dict(record["e"]))
|
|
274
|
+
|
|
275
|
+
async def update_entity_property(
|
|
276
|
+
self, entity_id: str, property_key: str, property_value: Any, confidence: float,
|
|
277
|
+
) -> None:
|
|
278
|
+
async with self._driver.session(database=self._database) as session:
|
|
279
|
+
result = await session.run(
|
|
280
|
+
"MATCH (e:Entity {id: $id}) "
|
|
281
|
+
"SET e.confidence = $conf, e.updated_at = $now",
|
|
282
|
+
id=entity_id, conf=confidence,
|
|
283
|
+
now=datetime.now(timezone.utc).isoformat(),
|
|
284
|
+
)
|
|
285
|
+
await result.consume()
|
|
286
|
+
|
|
287
|
+
async def add_entity_alias(self, entity_id: str, alias: str) -> None:
|
|
288
|
+
async with self._driver.session(database=self._database) as session:
|
|
289
|
+
result = await session.run(
|
|
290
|
+
"MATCH (e:Entity {id: $id}) "
|
|
291
|
+
"SET e.aliases = CASE WHEN e.aliases IS NULL THEN [$alias] "
|
|
292
|
+
" WHEN NOT $alias IN e.aliases THEN e.aliases + [$alias] "
|
|
293
|
+
" ELSE e.aliases END, "
|
|
294
|
+
" e.updated_at = $now",
|
|
295
|
+
id=entity_id, alias=alias,
|
|
296
|
+
now=datetime.now(timezone.utc).isoformat(),
|
|
297
|
+
)
|
|
298
|
+
await result.consume()
|
|
299
|
+
|
|
300
|
+
async def delete_entity(self, entity_id: str) -> None:
|
|
301
|
+
async with self._driver.session(database=self._database) as session:
|
|
302
|
+
result = await session.run(
|
|
303
|
+
"MATCH (e:Entity {id: $id}) DETACH DELETE e", id=entity_id,
|
|
304
|
+
)
|
|
305
|
+
await result.consume()
|
|
306
|
+
|
|
307
|
+
# ── Relationship reads ────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
async def get_relationship(self, rel_id: str) -> Optional[WorldRelationship]:
|
|
310
|
+
async with self._driver.session(database=self._database) as session:
|
|
311
|
+
result = await session.run(
|
|
312
|
+
"MATCH ()-[r]->() WHERE r.id = $id RETURN r", id=rel_id,
|
|
313
|
+
)
|
|
314
|
+
record = await result.single()
|
|
315
|
+
if record is None:
|
|
316
|
+
return None
|
|
317
|
+
return _props_to_rel(_rel_to_dict(record["r"]))
|
|
318
|
+
|
|
319
|
+
async def query_relationships(
|
|
320
|
+
self,
|
|
321
|
+
source_id: Optional[str] = None,
|
|
322
|
+
target_id: Optional[str] = None,
|
|
323
|
+
relationship_type: Optional[str] = None,
|
|
324
|
+
target_types: Optional[List[str]] = None,
|
|
325
|
+
active_only: bool = False,
|
|
326
|
+
min_confidence: float = 0.30,
|
|
327
|
+
limit: int = 100,
|
|
328
|
+
) -> List[WorldRelationship]:
|
|
329
|
+
clauses = []
|
|
330
|
+
params: Dict[str, Any] = {"min_conf": min_confidence, "limit": limit}
|
|
331
|
+
|
|
332
|
+
if source_id:
|
|
333
|
+
clauses.append("(s:Entity {id: $src})-[r]->(t:Entity)")
|
|
334
|
+
params["src"] = source_id
|
|
335
|
+
elif target_id:
|
|
336
|
+
clauses.append("(s:Entity)-[r]->(t:Entity {id: $tgt})")
|
|
337
|
+
params["tgt"] = target_id
|
|
338
|
+
else:
|
|
339
|
+
clauses.append("(s:Entity)-[r]->(t:Entity)")
|
|
340
|
+
|
|
341
|
+
where_parts = ["r.confidence >= $min_conf"]
|
|
342
|
+
if active_only:
|
|
343
|
+
where_parts.append("r.valid_to IS NULL")
|
|
344
|
+
if target_types:
|
|
345
|
+
where_parts.append("t.entity_type IN $tgt_types")
|
|
346
|
+
params["tgt_types"] = target_types
|
|
347
|
+
if not relationship_type:
|
|
348
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
349
|
+
where_parts.append("NOT type(r) IN $causal_excl")
|
|
350
|
+
params["causal_excl"] = _CAUSAL_EXCLUSION
|
|
351
|
+
|
|
352
|
+
match_clause = clauses[0] if not relationship_type else \
|
|
353
|
+
clauses[0].replace("-[r]->", f"-[r:{_sanitize_rel_type(relationship_type)}]->")
|
|
354
|
+
|
|
355
|
+
cypher = f"MATCH {match_clause} WHERE {' AND '.join(where_parts)} RETURN s.id AS source_id, t.id AS target_id, type(r) AS rel_type, properties(r) AS rel_props LIMIT $limit"
|
|
356
|
+
|
|
357
|
+
async with self._driver.session(database=self._database) as session:
|
|
358
|
+
result = await session.run(cypher, params)
|
|
359
|
+
records = await result.data()
|
|
360
|
+
rels = []
|
|
361
|
+
for r in records:
|
|
362
|
+
props = r["rel_props"] or {}
|
|
363
|
+
props["source_id"] = r["source_id"]
|
|
364
|
+
props["target_id"] = r["target_id"]
|
|
365
|
+
props.setdefault("relationship_type", r["rel_type"])
|
|
366
|
+
rels.append(_props_to_rel(props))
|
|
367
|
+
return rels
|
|
368
|
+
|
|
369
|
+
async def query_at_time(
|
|
370
|
+
self, entity_id: str, as_of: str,
|
|
371
|
+
relationship_types: Optional[List[str]] = None,
|
|
372
|
+
) -> List[WorldRelationship]:
|
|
373
|
+
clauses = [
|
|
374
|
+
"r.confidence >= 0.30",
|
|
375
|
+
"(r.valid_from IS NULL OR r.valid_from <= $as_of)",
|
|
376
|
+
"(r.valid_to IS NULL OR r.valid_to > $as_of)",
|
|
377
|
+
]
|
|
378
|
+
params = {"id": entity_id, "as_of": as_of}
|
|
379
|
+
if not relationship_types:
|
|
380
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
381
|
+
clauses.append("NOT type(r) IN $causal_excl")
|
|
382
|
+
params["causal_excl"] = _CAUSAL_EXCLUSION
|
|
383
|
+
|
|
384
|
+
rel_match = "-[r]->" if not relationship_types else \
|
|
385
|
+
"-[r:" + "|".join(_sanitize_rel_type(t) for t in relationship_types) + "]->"
|
|
386
|
+
|
|
387
|
+
cypher = (
|
|
388
|
+
f"MATCH (s:Entity {{id: $id}}){rel_match}(t:Entity) "
|
|
389
|
+
f"WHERE {' AND '.join(clauses)} RETURN s.id AS source_id, t.id AS target_id, type(r) AS rel_type, properties(r) AS rel_props"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
async with self._driver.session(database=self._database) as session:
|
|
393
|
+
result = await session.run(cypher, params)
|
|
394
|
+
records = await result.data()
|
|
395
|
+
rels = []
|
|
396
|
+
for r in records:
|
|
397
|
+
props = r["rel_props"] or {}
|
|
398
|
+
props["source_id"] = r["source_id"]
|
|
399
|
+
props["target_id"] = r["target_id"]
|
|
400
|
+
props.setdefault("relationship_type", r["rel_type"])
|
|
401
|
+
rels.append(_props_to_rel(props))
|
|
402
|
+
return rels
|
|
403
|
+
|
|
404
|
+
async def get_neighbors(
|
|
405
|
+
self, entity_id: str, min_confidence: float = 0.30,
|
|
406
|
+
relationship_types: Optional[List[str]] = None,
|
|
407
|
+
) -> List[Tuple[BaseEntity, WorldRelationship]]:
|
|
408
|
+
# Bidirectional: both outgoing and incoming relationships
|
|
409
|
+
causal_where = ""
|
|
410
|
+
if not relationship_types:
|
|
411
|
+
rel_type_filter = ""
|
|
412
|
+
# Untyped read: causal edges are query-only, exclude them.
|
|
413
|
+
causal_where = " AND NOT type(r) IN $causal_excl"
|
|
414
|
+
else:
|
|
415
|
+
rel_type_filter = ":" + "|".join(_sanitize_rel_type(t) for t in relationship_types)
|
|
416
|
+
|
|
417
|
+
# Outgoing: (center)-[r]->(other)
|
|
418
|
+
cypher_out = (
|
|
419
|
+
f"MATCH (s:Entity {{id: $id}})-[r{rel_type_filter}]->(t:Entity) "
|
|
420
|
+
f"WHERE r.confidence >= $min_conf{causal_where} "
|
|
421
|
+
"RETURN s.id AS source_id, t.id AS target_id, "
|
|
422
|
+
"type(r) AS rel_type, properties(r) AS rel_props, t"
|
|
423
|
+
)
|
|
424
|
+
# Incoming: (other)-[r]->(center)
|
|
425
|
+
cypher_in = (
|
|
426
|
+
f"MATCH (t:Entity)-[r{rel_type_filter}]->(s:Entity {{id: $id}}) "
|
|
427
|
+
f"WHERE r.confidence >= $min_conf{causal_where} "
|
|
428
|
+
"RETURN t.id AS source_id, s.id AS target_id, "
|
|
429
|
+
"type(r) AS rel_type, properties(r) AS rel_props, t"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
neighbors = []
|
|
433
|
+
seen_ids = set()
|
|
434
|
+
async with self._driver.session(database=self._database) as session:
|
|
435
|
+
for cypher in [cypher_out, cypher_in]:
|
|
436
|
+
result = await session.run(cypher, id=entity_id, min_conf=min_confidence,
|
|
437
|
+
causal_excl=_CAUSAL_EXCLUSION)
|
|
438
|
+
records = await result.data()
|
|
439
|
+
for rec in records:
|
|
440
|
+
nid = rec["target_id"] if rec["source_id"] == entity_id else rec["source_id"]
|
|
441
|
+
if nid in seen_ids or nid == entity_id:
|
|
442
|
+
continue
|
|
443
|
+
seen_ids.add(nid)
|
|
444
|
+
entity = _props_to_entity(_node_to_dict(rec["t"]))
|
|
445
|
+
props = rec["rel_props"] or {}
|
|
446
|
+
props["source_id"] = rec["source_id"]
|
|
447
|
+
props["target_id"] = rec["target_id"]
|
|
448
|
+
props.setdefault("relationship_type", rec["rel_type"])
|
|
449
|
+
rel = _props_to_rel(props)
|
|
450
|
+
neighbors.append((entity, rel))
|
|
451
|
+
return neighbors
|
|
452
|
+
|
|
453
|
+
# ── Relationship writes ───────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
async def upsert_relationship(self, rel: WorldRelationship) -> WorldRelationship:
|
|
456
|
+
props = _rel_to_props(rel)
|
|
457
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
458
|
+
if "created_at" not in props:
|
|
459
|
+
props["created_at"] = now
|
|
460
|
+
props["updated_at"] = now
|
|
461
|
+
|
|
462
|
+
safe_type = _sanitize_rel_type(rel.relationship_type)
|
|
463
|
+
|
|
464
|
+
async with self._driver.session(database=self._database) as session:
|
|
465
|
+
existing_result = await session.run(
|
|
466
|
+
"MATCH ()-[r]->() WHERE r.id = $id RETURN r", id=rel.id,
|
|
467
|
+
)
|
|
468
|
+
existing_record = await existing_result.single()
|
|
469
|
+
|
|
470
|
+
if existing_record:
|
|
471
|
+
result = await session.run(
|
|
472
|
+
"MATCH ()-[r]->() WHERE r.id = $id SET r += $props RETURN r",
|
|
473
|
+
id=rel.id, props=props,
|
|
474
|
+
)
|
|
475
|
+
await result.consume()
|
|
476
|
+
else:
|
|
477
|
+
result = await session.run(
|
|
478
|
+
f"MATCH (s:Entity {{id: $src}}), (t:Entity {{id: $tgt}}) "
|
|
479
|
+
f"CREATE (s)-[r:{safe_type}]->(t) SET r += $props",
|
|
480
|
+
src=rel.source_id, tgt=rel.target_id, props=props,
|
|
481
|
+
)
|
|
482
|
+
await result.consume()
|
|
483
|
+
|
|
484
|
+
return rel
|
|
485
|
+
|
|
486
|
+
async def close_relationship(self, rel_id: str, valid_to: str) -> None:
|
|
487
|
+
async with self._driver.session(database=self._database) as session:
|
|
488
|
+
result = await session.run(
|
|
489
|
+
"MATCH ()-[r]->() WHERE r.id = $id "
|
|
490
|
+
"SET r.valid_to = $valid_to, r.updated_at = $now",
|
|
491
|
+
id=rel_id, valid_to=valid_to,
|
|
492
|
+
now=datetime.now(timezone.utc).isoformat(),
|
|
493
|
+
)
|
|
494
|
+
await result.consume()
|
|
495
|
+
|
|
496
|
+
# ── Observations ──────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
async def add_observation(
|
|
499
|
+
self, entity_id: Optional[str], relationship_id: Optional[str],
|
|
500
|
+
observation: str, source: str,
|
|
501
|
+
) -> str:
|
|
502
|
+
obs_id = _generate_id(OBSERVATION_ID_PREFIX)
|
|
503
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
504
|
+
|
|
505
|
+
async with self._driver.session(database=self._database) as session:
|
|
506
|
+
if entity_id:
|
|
507
|
+
result = await session.run(
|
|
508
|
+
"MATCH (e:Entity {id: $eid}) "
|
|
509
|
+
"CREATE (o:Observation {id: $id, text: $text, source: $source, "
|
|
510
|
+
" entity_id: $eid, created_at: $now}) "
|
|
511
|
+
"CREATE (e)-[:HAS_OBSERVATION]->(o)",
|
|
512
|
+
eid=entity_id, id=obs_id, text=observation,
|
|
513
|
+
source=source, now=now,
|
|
514
|
+
)
|
|
515
|
+
elif relationship_id:
|
|
516
|
+
result = await session.run(
|
|
517
|
+
"MATCH ()-[r]->() WHERE r.id = $rid "
|
|
518
|
+
"CREATE (o:Observation {id: $id, text: $text, source: $source, "
|
|
519
|
+
" relationship_id: $rid, created_at: $now})",
|
|
520
|
+
rid=relationship_id, id=obs_id, text=observation,
|
|
521
|
+
source=source, now=now,
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
result = await session.run(
|
|
525
|
+
"CREATE (o:Observation {id: $id, text: $text, source: $source, "
|
|
526
|
+
" created_at: $now})",
|
|
527
|
+
id=obs_id, text=observation, source=source, now=now,
|
|
528
|
+
)
|
|
529
|
+
await result.consume()
|
|
530
|
+
return obs_id
|
|
531
|
+
|
|
532
|
+
# ── Merge proposals ───────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
async def create_merge_proposal(
|
|
535
|
+
self, winner_id: str, loser_id: str, reason: str, confidence: float,
|
|
536
|
+
) -> str:
|
|
537
|
+
proposal_id = _generate_id(MERGE_PROPOSAL_ID_PREFIX)
|
|
538
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
539
|
+
|
|
540
|
+
async with self._driver.session(database=self._database) as session:
|
|
541
|
+
result = await session.run(
|
|
542
|
+
"CREATE (mp:MergeProposal {id: $id, winner_id: $winner, loser_id: $loser, "
|
|
543
|
+
" reason: $reason, confidence: $conf, status: 'pending', created_at: $now})",
|
|
544
|
+
id=proposal_id, winner=winner_id, loser=loser_id,
|
|
545
|
+
reason=reason, conf=confidence, now=now,
|
|
546
|
+
)
|
|
547
|
+
await result.consume()
|
|
548
|
+
return proposal_id
|
|
549
|
+
|
|
550
|
+
async def get_merge_proposal(self, proposal_id: str) -> Optional[Dict[str, Any]]:
|
|
551
|
+
async with self._driver.session(database=self._database) as session:
|
|
552
|
+
result = await session.run(
|
|
553
|
+
"MATCH (mp:MergeProposal {id: $id}) RETURN mp", id=proposal_id,
|
|
554
|
+
)
|
|
555
|
+
record = await result.single()
|
|
556
|
+
if record is None:
|
|
557
|
+
return None
|
|
558
|
+
return _node_to_dict(record["mp"])
|
|
559
|
+
|
|
560
|
+
async def update_merge_proposal_status(self, proposal_id: str, status: str) -> None:
|
|
561
|
+
async with self._driver.session(database=self._database) as session:
|
|
562
|
+
result = await session.run(
|
|
563
|
+
"MATCH (mp:MergeProposal {id: $id}) SET mp.status = $status",
|
|
564
|
+
id=proposal_id, status=status,
|
|
565
|
+
)
|
|
566
|
+
await result.consume()
|
|
567
|
+
|
|
568
|
+
async def get_pending_merge_proposals(self) -> List[Dict[str, Any]]:
|
|
569
|
+
async with self._driver.session(database=self._database) as session:
|
|
570
|
+
result = await session.run(
|
|
571
|
+
"MATCH (mp:MergeProposal {status: 'pending'}) RETURN mp"
|
|
572
|
+
)
|
|
573
|
+
records = await result.data()
|
|
574
|
+
return [_node_to_dict(r["mp"]) for r in records]
|
|
575
|
+
|
|
576
|
+
async def execute_merge(
|
|
577
|
+
self, winner_id: str, loser_id: str, merge_properties: bool = True,
|
|
578
|
+
) -> None:
|
|
579
|
+
async with self._driver.session(database=self._database) as session:
|
|
580
|
+
if merge_properties:
|
|
581
|
+
result = await session.run(
|
|
582
|
+
"MATCH (w:Entity {id: $winner}), (l:Entity {id: $loser}) "
|
|
583
|
+
"WITH w, l "
|
|
584
|
+
"SET w.aliases = CASE WHEN w.aliases IS NULL THEN l.aliases "
|
|
585
|
+
" ELSE apoc.coll.union(w.aliases, l.aliases) END",
|
|
586
|
+
winner=winner_id, loser=loser_id,
|
|
587
|
+
)
|
|
588
|
+
await result.consume()
|
|
589
|
+
|
|
590
|
+
# Re-point incoming relationships
|
|
591
|
+
result = await session.run(
|
|
592
|
+
"MATCH (other)-[r]->(loser:Entity {id: $loser}) "
|
|
593
|
+
"WITH other, r, loser, type(r) AS rtype, properties(r) AS rprops "
|
|
594
|
+
"MATCH (winner:Entity {id: $winner}) "
|
|
595
|
+
"CALL apoc.create.relationship(other, rtype, rprops, winner) YIELD rel "
|
|
596
|
+
"DELETE r",
|
|
597
|
+
loser=loser_id, winner=winner_id,
|
|
598
|
+
)
|
|
599
|
+
await result.consume()
|
|
600
|
+
|
|
601
|
+
# Re-point outgoing relationships
|
|
602
|
+
result = await session.run(
|
|
603
|
+
"MATCH (loser:Entity {id: $loser})-[r]->(other) "
|
|
604
|
+
"WITH loser, r, other, type(r) AS rtype, properties(r) AS rprops "
|
|
605
|
+
"MATCH (winner:Entity {id: $winner}) "
|
|
606
|
+
"CALL apoc.create.relationship(winner, rtype, rprops, other) YIELD rel "
|
|
607
|
+
"DELETE r",
|
|
608
|
+
loser=loser_id, winner=winner_id,
|
|
609
|
+
)
|
|
610
|
+
await result.consume()
|
|
611
|
+
|
|
612
|
+
# Delete loser
|
|
613
|
+
result = await session.run(
|
|
614
|
+
"MATCH (l:Entity {id: $loser}) DETACH DELETE l", loser=loser_id,
|
|
615
|
+
)
|
|
616
|
+
await result.consume()
|
|
617
|
+
|
|
618
|
+
# ── Stats ─────────────────────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
621
|
+
async with self._driver.session(database=self._database) as session:
|
|
622
|
+
type_result = await session.run(
|
|
623
|
+
"MATCH (e:Entity) WHERE e.entity_type IS NOT NULL RETURN e.entity_type AS type, count(e) AS cnt"
|
|
624
|
+
)
|
|
625
|
+
type_records = await type_result.data()
|
|
626
|
+
entities_by_type = {r["type"]: r["cnt"] for r in type_records}
|
|
627
|
+
|
|
628
|
+
total_result = await session.run("MATCH (e:Entity) RETURN count(e) AS total")
|
|
629
|
+
total_record = await total_result.single()
|
|
630
|
+
|
|
631
|
+
rel_result = await session.run("MATCH ()-[r]->() RETURN count(r) AS total")
|
|
632
|
+
rel_record = await rel_result.single()
|
|
633
|
+
|
|
634
|
+
active_result = await session.run(
|
|
635
|
+
"MATCH ()-[r]->() WHERE r.valid_to IS NULL RETURN count(r) AS total"
|
|
636
|
+
)
|
|
637
|
+
active_record = await active_result.single()
|
|
638
|
+
|
|
639
|
+
obs_result = await session.run("MATCH (o:Observation) RETURN count(o) AS total")
|
|
640
|
+
obs_record = await obs_result.single()
|
|
641
|
+
|
|
642
|
+
merge_result = await session.run(
|
|
643
|
+
"MATCH (mp:MergeProposal {status: 'pending'}) RETURN count(mp) AS total"
|
|
644
|
+
)
|
|
645
|
+
merge_record = await merge_result.single()
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
"total_entities": total_record["total"] if total_record else 0,
|
|
649
|
+
"entities_by_type": entities_by_type,
|
|
650
|
+
"total_relationships": rel_record["total"] if rel_record else 0,
|
|
651
|
+
"active_relationships": active_record["total"] if active_record else 0,
|
|
652
|
+
"total_observations": obs_record["total"] if obs_record else 0,
|
|
653
|
+
"merge_proposals_pending": merge_record["total"] if merge_record else 0,
|
|
654
|
+
}
|