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,405 @@
|
|
|
1
|
+
"""MemoryConsolidator — deduplicate and merge near-duplicate memories in Neo4j.
|
|
2
|
+
|
|
3
|
+
Runs as a background task (not in the hot path). Called by the autonomy loop
|
|
4
|
+
once per hour (configurable). Operates entirely on Memory nodes and their edges.
|
|
5
|
+
|
|
6
|
+
Algorithm:
|
|
7
|
+
1. Retrieve all Memory nodes modified in the last N hours
|
|
8
|
+
2. Compute pairwise cosine similarity using stored embeddings
|
|
9
|
+
3. For pairs with similarity > threshold (default 0.92):
|
|
10
|
+
a. Keep the node with higher strength; add sources of both; update last_seen
|
|
11
|
+
b. Redirect all edges from the merged node to the survivor
|
|
12
|
+
c. Mark the merged node with MERGED_INTO edge and delete it
|
|
13
|
+
4. Detect conflicting facts (same entity, contradictory attribute values):
|
|
14
|
+
create a CONFLICTS_WITH edge and flag for human review
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import asyncio
|
|
21
|
+
import math
|
|
22
|
+
import os
|
|
23
|
+
import uuid
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Result types
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ConflictPair:
|
|
37
|
+
"""Two memories that assert contradictory facts."""
|
|
38
|
+
memory_id_a: str
|
|
39
|
+
memory_id_b: str
|
|
40
|
+
entity_name: str
|
|
41
|
+
reason: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ConsolidationResult:
|
|
46
|
+
"""Outcome of one MemoryConsolidator.run() call."""
|
|
47
|
+
pairs_examined: int = 0
|
|
48
|
+
pairs_merged: int = 0
|
|
49
|
+
conflicts_detected: int = 0
|
|
50
|
+
errors: List[str] = field(default_factory=list)
|
|
51
|
+
duration_ms: float = 0.0
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def reduction_ratio(self) -> float:
|
|
55
|
+
"""Fraction of examined pairs that were merged."""
|
|
56
|
+
if self.pairs_examined == 0:
|
|
57
|
+
return 0.0
|
|
58
|
+
return self.pairs_merged / self.pairs_examined
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Helpers
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
import numpy as _np
|
|
67
|
+
except Exception: # numpy is present in practice; the pure
|
|
68
|
+
_np = None # python path remains as a fallback
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _cosine_similarity(a: List[float], b: List[float]) -> float:
|
|
72
|
+
"""Cosine similarity between two equal-length vectors. Returns 0 on error.
|
|
73
|
+
|
|
74
|
+
Uses numpy when available: at embedding dimensions in the thousands the
|
|
75
|
+
pure-python path is ~100x slower and, run pairwise on the event loop, was
|
|
76
|
+
observed starving the whole API (accept-queue exhaustion)."""
|
|
77
|
+
if not a or not b or len(a) != len(b):
|
|
78
|
+
return 0.0
|
|
79
|
+
if _np is not None:
|
|
80
|
+
va = _np.asarray(a, dtype=_np.float32)
|
|
81
|
+
vb = _np.asarray(b, dtype=_np.float32)
|
|
82
|
+
na = float(_np.linalg.norm(va)); nb = float(_np.linalg.norm(vb))
|
|
83
|
+
if na == 0.0 or nb == 0.0:
|
|
84
|
+
return 0.0
|
|
85
|
+
return float(va.dot(vb) / (na * nb))
|
|
86
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
87
|
+
mag_a = math.sqrt(sum(x * x for x in a))
|
|
88
|
+
mag_b = math.sqrt(sum(y * y for y in b))
|
|
89
|
+
if mag_a == 0.0 or mag_b == 0.0:
|
|
90
|
+
return 0.0
|
|
91
|
+
return dot / (mag_a * mag_b)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _jaccard_similarity(text_a: str, text_b: str) -> float:
|
|
95
|
+
"""Token-level Jaccard similarity — fast fallback when embeddings are absent."""
|
|
96
|
+
tokens_a = set(text_a.lower().split())
|
|
97
|
+
tokens_b = set(text_b.lower().split())
|
|
98
|
+
if not tokens_a and not tokens_b:
|
|
99
|
+
return 1.0
|
|
100
|
+
if not tokens_a or not tokens_b:
|
|
101
|
+
return 0.0
|
|
102
|
+
return len(tokens_a & tokens_b) / len(tokens_a | tokens_b)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Main class
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
class MemoryConsolidator:
|
|
110
|
+
"""Deduplicate and merge near-duplicate memories in Neo4j.
|
|
111
|
+
|
|
112
|
+
Designed to be used with ColonyGraph but accepts any object that exposes
|
|
113
|
+
an ``async def execute(query, **params)`` method returning a list of
|
|
114
|
+
record dicts — this makes unit-testing straightforward without Neo4j.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
graph_client: ColonyGraph instance (or compatible mock)
|
|
118
|
+
similarity_threshold: Cosine/Jaccard score above which two memories
|
|
119
|
+
are considered duplicates (default: 0.92)
|
|
120
|
+
lookback_hours: Only examine memories touched in the last N hours
|
|
121
|
+
(default: 24)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
graph_client: Any,
|
|
127
|
+
similarity_threshold: float = 0.92,
|
|
128
|
+
lookback_hours: int = 24,
|
|
129
|
+
max_merge_ratio: float = 0.5,
|
|
130
|
+
params: Any = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
self.graph = graph_client
|
|
133
|
+
self.similarity_threshold = similarity_threshold
|
|
134
|
+
self.lookback_hours = lookback_hours
|
|
135
|
+
self._max_merge_ratio = max_merge_ratio
|
|
136
|
+
# Optional AdaptiveParamStore: when wired, the merge threshold is
|
|
137
|
+
# re-resolved at the start of every run, so a meta-learning
|
|
138
|
+
# adjustment takes effect without a restart (bounds enforced by the
|
|
139
|
+
# store, hard floor 0.85).
|
|
140
|
+
self._params = params
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Public API
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
async def run(self) -> ConsolidationResult:
|
|
147
|
+
"""Execute one consolidation pass. Never raises — errors are captured."""
|
|
148
|
+
import time
|
|
149
|
+
start = time.monotonic()
|
|
150
|
+
result = ConsolidationResult()
|
|
151
|
+
|
|
152
|
+
if self._params is not None:
|
|
153
|
+
try:
|
|
154
|
+
from apsimo.self_model.params import (
|
|
155
|
+
PARAM_CONSOLIDATION_THRESHOLD,
|
|
156
|
+
)
|
|
157
|
+
self.similarity_threshold = float(self._params.get(
|
|
158
|
+
PARAM_CONSOLIDATION_THRESHOLD,
|
|
159
|
+
default=self.similarity_threshold))
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.debug("adaptive threshold resolve failed; keeping %s",
|
|
162
|
+
self.similarity_threshold, exc_info=True)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
candidates = await self._fetch_recent_memories()
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
result.errors.append(f"fetch_recent_memories: {exc}")
|
|
168
|
+
result.duration_ms = (time.monotonic() - start) * 1000
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
# Pairwise comparison (O(n²) — acceptable for typical batch sizes < 1000)
|
|
172
|
+
# Safety cap: merge at most max_merge_ratio of candidates per pass
|
|
173
|
+
# to prevent catastrophic information loss from runaway merges.
|
|
174
|
+
n = len(candidates)
|
|
175
|
+
max_merges = max(1, int(n * self._max_merge_ratio))
|
|
176
|
+
for i in range(n):
|
|
177
|
+
for j in range(i + 1, n):
|
|
178
|
+
a, b = candidates[i], candidates[j]
|
|
179
|
+
result.pairs_examined += 1
|
|
180
|
+
if result.pairs_examined % 512 == 0:
|
|
181
|
+
await asyncio.sleep(0) # breathe: never starve the event loop
|
|
182
|
+
sim = self._similarity(a, b)
|
|
183
|
+
if sim >= self.similarity_threshold:
|
|
184
|
+
if result.pairs_merged >= max_merges:
|
|
185
|
+
logger.warning(
|
|
186
|
+
"Consolidation safety cap reached: merged %d/%d (%.0f%% limit)",
|
|
187
|
+
result.pairs_merged, n, self._max_merge_ratio * 100,
|
|
188
|
+
)
|
|
189
|
+
break
|
|
190
|
+
try:
|
|
191
|
+
await self._merge_pair(a, b)
|
|
192
|
+
result.pairs_merged += 1
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
result.errors.append(
|
|
195
|
+
f"merge_pair({a.get('id')}, {b.get('id')}): {exc}"
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
continue
|
|
199
|
+
break # Break outer loop when safety cap reached
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
conflicts = await self._detect_conflicts()
|
|
203
|
+
result.conflicts_detected = len(conflicts)
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
result.errors.append(f"detect_conflicts: {exc}")
|
|
206
|
+
|
|
207
|
+
result.duration_ms = (time.monotonic() - start) * 1000
|
|
208
|
+
logger.info(
|
|
209
|
+
"MemoryConsolidator: examined=%d merged=%d conflicts=%d errors=%d (%.1fms)",
|
|
210
|
+
result.pairs_examined,
|
|
211
|
+
result.pairs_merged,
|
|
212
|
+
result.conflicts_detected,
|
|
213
|
+
len(result.errors),
|
|
214
|
+
result.duration_ms,
|
|
215
|
+
)
|
|
216
|
+
try:
|
|
217
|
+
from apsimo.events.broadcaster import emit as _emit
|
|
218
|
+
_emit("memory_consolidated", {
|
|
219
|
+
"examined": result.pairs_examined,
|
|
220
|
+
"merged": result.pairs_merged,
|
|
221
|
+
"conflicts": result.conflicts_detected,
|
|
222
|
+
"duration_ms": result.duration_ms,
|
|
223
|
+
})
|
|
224
|
+
except Exception:
|
|
225
|
+
logger.debug("memory_consolidated broadcast failed", exc_info=True)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
# Private helpers
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
async def _fetch_recent_memories(self) -> List[Dict[str, Any]]:
|
|
233
|
+
"""Return Memory nodes touched within lookback_hours."""
|
|
234
|
+
query = (
|
|
235
|
+
"MATCH (m:Memory) "
|
|
236
|
+
"WHERE m.accessed_at >= datetime() - duration({hours: $hours}) "
|
|
237
|
+
" AND NOT m.epistemic_state IN ['stale', 'superseded', 'deprecated', 'archived'] "
|
|
238
|
+
"RETURN m.id AS id, m.content AS content, m.embedding AS embedding, "
|
|
239
|
+
"m.strength AS strength, m.effective_confidence AS effective_confidence, "
|
|
240
|
+
"m.type AS type, m.sources AS sources, m.epistemic_state AS epistemic_state "
|
|
241
|
+
"ORDER BY m.effective_confidence DESC, m.strength DESC "
|
|
242
|
+
"LIMIT $cap"
|
|
243
|
+
)
|
|
244
|
+
# O(n^2) pass: the candidate set MUST be bounded. A bulk import once
|
|
245
|
+
# produced thousands of same-day memories and the unbounded pass spun
|
|
246
|
+
# for hours at 100% CPU (COLONY_CONSOLIDATION_BATCH to tune).
|
|
247
|
+
try:
|
|
248
|
+
cap = max(50, int(os.environ.get("COLONY_CONSOLIDATION_BATCH", "400")))
|
|
249
|
+
except ValueError:
|
|
250
|
+
cap = 400
|
|
251
|
+
rows = await self._execute(query, hours=self.lookback_hours, cap=cap)
|
|
252
|
+
return rows if rows else []
|
|
253
|
+
|
|
254
|
+
def _similarity(self, a: Dict[str, Any], b: Dict[str, Any]) -> float:
|
|
255
|
+
"""Return the best similarity score between two memory records."""
|
|
256
|
+
emb_a = a.get("embedding") or []
|
|
257
|
+
emb_b = b.get("embedding") or []
|
|
258
|
+
if emb_a and emb_b and len(emb_a) == len(emb_b):
|
|
259
|
+
return _cosine_similarity(emb_a, emb_b)
|
|
260
|
+
# Fall back to Jaccard on raw content
|
|
261
|
+
return _jaccard_similarity(
|
|
262
|
+
a.get("content", ""), b.get("content", "")
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def _merge_pair(self, keep_rec: Dict[str, Any], merge_rec: Dict[str, Any]) -> None:
|
|
266
|
+
"""Merge *merge_rec* into *keep_rec*: highest-strength node survives."""
|
|
267
|
+
from apsimo.intelligence.graph.client import EpistemicState
|
|
268
|
+
|
|
269
|
+
# Determine survivor by effective_confidence → strength → recency
|
|
270
|
+
keep_conf = keep_rec.get("effective_confidence") or keep_rec.get("strength") or 0.0
|
|
271
|
+
merge_conf = merge_rec.get("effective_confidence") or merge_rec.get("strength") or 0.0
|
|
272
|
+
if merge_conf > keep_conf:
|
|
273
|
+
keep_rec, merge_rec = merge_rec, keep_rec
|
|
274
|
+
keep_conf, merge_conf = merge_conf, keep_conf
|
|
275
|
+
|
|
276
|
+
keep_id = keep_rec["id"]
|
|
277
|
+
merge_id = merge_rec["id"]
|
|
278
|
+
|
|
279
|
+
# 1. Re-point all outgoing MENTIONS relationships from merge → keep
|
|
280
|
+
await self._execute(
|
|
281
|
+
"""
|
|
282
|
+
MATCH (m:Memory {id: $merge_id})-[r:MENTIONS]->(e:Entity)
|
|
283
|
+
MATCH (k:Memory {id: $keep_id})
|
|
284
|
+
MERGE (k)-[nr:MENTIONS]->(e)
|
|
285
|
+
SET nr.created_at = coalesce(r.created_at, datetime())
|
|
286
|
+
DELETE r
|
|
287
|
+
""",
|
|
288
|
+
keep_id=keep_id,
|
|
289
|
+
merge_id=merge_id,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# 3. Re-point ABOUT relationships
|
|
293
|
+
await self._execute(
|
|
294
|
+
"""
|
|
295
|
+
MATCH (m:Memory {id: $merge_id})-[r:ABOUT]->(p:Person)
|
|
296
|
+
MATCH (k:Memory {id: $keep_id})
|
|
297
|
+
MERGE (k)-[nr:ABOUT]->(p)
|
|
298
|
+
SET nr.created_at = coalesce(r.created_at, datetime())
|
|
299
|
+
DELETE r
|
|
300
|
+
""",
|
|
301
|
+
keep_id=keep_id,
|
|
302
|
+
merge_id=merge_id,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# 4. Create MERGED_INTO edge
|
|
306
|
+
await self._execute(
|
|
307
|
+
"""
|
|
308
|
+
MATCH (k:Memory {id: $keep_id}), (m:Memory {id: $merge_id})
|
|
309
|
+
MERGE (m)-[r:MERGED_INTO]->(k)
|
|
310
|
+
SET r.merged_at = datetime()
|
|
311
|
+
""",
|
|
312
|
+
keep_id=keep_id,
|
|
313
|
+
merge_id=merge_id,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# 5. Merge provenance and update survivor
|
|
317
|
+
merge_sources = merge_rec.get("sources") or []
|
|
318
|
+
merge_provenance = merge_rec.get("provenance") or []
|
|
319
|
+
await self._execute(
|
|
320
|
+
"""
|
|
321
|
+
MATCH (k:Memory {id: $keep_id})
|
|
322
|
+
SET k.sources = CASE WHEN k.sources IS NULL THEN $sources
|
|
323
|
+
ELSE k.sources + [x IN $sources WHERE NOT x IN k.sources] END,
|
|
324
|
+
k.provenance = coalesce(k.provenance, []) + $merge_provenance + [$merged_id],
|
|
325
|
+
k.accessed_at = datetime(),
|
|
326
|
+
k.epistemic_state = CASE
|
|
327
|
+
WHEN k.epistemic_state = 'inferred' AND $merge_state IN ['observed', 'corroborated', 'verified']
|
|
328
|
+
THEN $merge_state
|
|
329
|
+
ELSE k.epistemic_state
|
|
330
|
+
END
|
|
331
|
+
""",
|
|
332
|
+
keep_id=keep_id,
|
|
333
|
+
sources=merge_sources,
|
|
334
|
+
merge_provenance=merge_provenance,
|
|
335
|
+
merged_id=merge_id,
|
|
336
|
+
merge_state=merge_rec.get("epistemic_state", "inferred"),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# 6. Remove :Memory label from merged node (preserves MERGED_INTO audit edge)
|
|
340
|
+
await self._execute(
|
|
341
|
+
"MATCH (m:Memory {id: $merge_id}) REMOVE m:Memory",
|
|
342
|
+
merge_id=merge_id,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
logger.debug("Merged memory %s → %s (MERGED_INTO + provenance recorded)", merge_id, keep_id)
|
|
346
|
+
|
|
347
|
+
async def _detect_conflicts(self) -> List[ConflictPair]:
|
|
348
|
+
"""Find Memory pairs that assert contradictory facts about the same entity."""
|
|
349
|
+
# Look for memories sharing an entity where content contradicts
|
|
350
|
+
# (simple heuristic: both contain the entity name + negation words)
|
|
351
|
+
query = (
|
|
352
|
+
"MATCH (m1:Memory)-[:MENTIONS]->(e:Entity)<-[:MENTIONS]-(m2:Memory) "
|
|
353
|
+
"WHERE id(m1) < id(m2) "
|
|
354
|
+
"AND NOT (m1)-[:MERGED_INTO]-() AND NOT (m2)-[:MERGED_INTO]-() "
|
|
355
|
+
"RETURN m1.id AS id_a, m2.id AS id_b, e.name AS entity, "
|
|
356
|
+
" m1.content AS content_a, m2.content AS content_b"
|
|
357
|
+
)
|
|
358
|
+
rows = await self._execute(query)
|
|
359
|
+
if not rows:
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
conflicts: List[ConflictPair] = []
|
|
363
|
+
negation_words = {"not", "never", "no", "isn't", "wasn't", "doesn't", "can't", "won't"}
|
|
364
|
+
|
|
365
|
+
for row in rows:
|
|
366
|
+
ca = set((row.get("content_a") or "").lower().split())
|
|
367
|
+
cb = set((row.get("content_b") or "").lower().split())
|
|
368
|
+
# Conflict heuristic: one sentence has negation that the other lacks
|
|
369
|
+
neg_a = bool(ca & negation_words)
|
|
370
|
+
neg_b = bool(cb & negation_words)
|
|
371
|
+
if neg_a != neg_b:
|
|
372
|
+
conflict = ConflictPair(
|
|
373
|
+
memory_id_a=row["id_a"],
|
|
374
|
+
memory_id_b=row["id_b"],
|
|
375
|
+
entity_name=row.get("entity", ""),
|
|
376
|
+
reason="negation asymmetry",
|
|
377
|
+
)
|
|
378
|
+
conflicts.append(conflict)
|
|
379
|
+
# Flag with CONFLICTS_WITH edge
|
|
380
|
+
try:
|
|
381
|
+
await self._execute(
|
|
382
|
+
"MATCH (m1:Memory {id: $id_a}), (m2:Memory {id: $id_b}) "
|
|
383
|
+
"MERGE (m1)-[:CONFLICTS_WITH]->(m2)",
|
|
384
|
+
id_a=row["id_a"],
|
|
385
|
+
id_b=row["id_b"],
|
|
386
|
+
)
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
logger.debug("Could not create CONFLICTS_WITH edge: %s", exc)
|
|
389
|
+
|
|
390
|
+
return conflicts
|
|
391
|
+
|
|
392
|
+
async def _execute(self, query: str, **params: Any) -> List[Dict[str, Any]]:
|
|
393
|
+
"""Execute a Cypher query via the graph client.
|
|
394
|
+
|
|
395
|
+
Supports ColonyGraph (which exposes an AsyncDriver) or any mock that
|
|
396
|
+
implements ``async def execute(query, **params) -> list``.
|
|
397
|
+
"""
|
|
398
|
+
if hasattr(self.graph, "execute"):
|
|
399
|
+
result = await self.graph.execute(query, **params)
|
|
400
|
+
return result if result is not None else []
|
|
401
|
+
|
|
402
|
+
# ColonyGraph path: use the driver session directly
|
|
403
|
+
async with self.graph.driver.session(database=self.graph.database) as session:
|
|
404
|
+
result = await session.run(query, **params)
|
|
405
|
+
return [dict(record) async for record in result]
|