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,171 @@
|
|
|
1
|
+
"""Insight Deliverer — route insights to appropriate delivery channels.
|
|
2
|
+
|
|
3
|
+
Routes based on:
|
|
4
|
+
- Urgency (high → push notification, low → digest)
|
|
5
|
+
- User preferences (push enabled/disabled, preferred channels)
|
|
6
|
+
- Channel availability
|
|
7
|
+
- Time of day (avoid late-night pushes for non-critical insights)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeliveryChannel(str, Enum):
|
|
22
|
+
"""Available delivery channels for insights.
|
|
23
|
+
|
|
24
|
+
PUSH: Immediate notification (WhatsApp, push notification)
|
|
25
|
+
DIGEST: Batched into daily/weekly summary
|
|
26
|
+
IN_APP: Show in next conversation naturally
|
|
27
|
+
EMAIL: Email notification for lower-urgency items
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
PUSH = "push"
|
|
31
|
+
DIGEST = "digest"
|
|
32
|
+
IN_APP = "in_app"
|
|
33
|
+
EMAIL = "email"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class DeliveryDecision:
|
|
38
|
+
"""Routing decision for an insight.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
insight_id: ID of the insight being routed
|
|
42
|
+
channel: Selected delivery channel
|
|
43
|
+
reason: Why this channel was chosen
|
|
44
|
+
scheduled_for: ISO timestamp for digest delivery (None for immediate)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
insight_id: str
|
|
48
|
+
channel: DeliveryChannel
|
|
49
|
+
reason: str
|
|
50
|
+
scheduled_for: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@runtime_checkable
|
|
54
|
+
class DelivererEventBus(Protocol):
|
|
55
|
+
"""Protocol for event emission from the deliverer."""
|
|
56
|
+
|
|
57
|
+
async def emit_async(self, event: Any) -> None: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@runtime_checkable
|
|
61
|
+
class Deliverable(Protocol):
|
|
62
|
+
"""Protocol for objects that can be delivered as insights."""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def id(self) -> str: ...
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def description(self) -> str: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InsightDeliverer:
|
|
72
|
+
"""Route insights to appropriate delivery channels.
|
|
73
|
+
|
|
74
|
+
Makes routing decisions based on urgency level and user preferences.
|
|
75
|
+
High-urgency insights get pushed immediately; low-urgency insights
|
|
76
|
+
queue for the next digest.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
event_bus: Event bus for emitting delivery events
|
|
80
|
+
user_preferences: Dict of user delivery preferences
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
event_bus: DelivererEventBus,
|
|
86
|
+
user_preferences: Optional[Dict[str, Any]] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self.events = event_bus
|
|
89
|
+
self.preferences = user_preferences or {}
|
|
90
|
+
|
|
91
|
+
async def deliver(
|
|
92
|
+
self,
|
|
93
|
+
insight: Deliverable,
|
|
94
|
+
urgency: float = 0.5,
|
|
95
|
+
) -> DeliveryDecision:
|
|
96
|
+
"""Determine delivery channel and route an insight.
|
|
97
|
+
|
|
98
|
+
Routing logic:
|
|
99
|
+
- urgency >= 0.8 and push enabled → PUSH
|
|
100
|
+
- urgency >= 0.5 → IN_APP (show in conversation)
|
|
101
|
+
- urgency < 0.5 → DIGEST (queue for summary)
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
insight: The insight to deliver
|
|
105
|
+
urgency: Urgency level between 0 and 1
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
DeliveryDecision describing channel and reasoning.
|
|
109
|
+
"""
|
|
110
|
+
if urgency >= 0.8 and self.preferences.get("push_enabled", True):
|
|
111
|
+
channel = DeliveryChannel.PUSH
|
|
112
|
+
reason = "High urgency, push enabled"
|
|
113
|
+
await self._send_push(insight)
|
|
114
|
+
elif urgency >= 0.5:
|
|
115
|
+
channel = DeliveryChannel.IN_APP
|
|
116
|
+
reason = "Moderate urgency, show in conversation"
|
|
117
|
+
else:
|
|
118
|
+
channel = DeliveryChannel.DIGEST
|
|
119
|
+
reason = "Low urgency, queue for digest"
|
|
120
|
+
|
|
121
|
+
decision = DeliveryDecision(
|
|
122
|
+
insight_id=insight.id,
|
|
123
|
+
channel=channel,
|
|
124
|
+
reason=reason,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
logger.info(
|
|
128
|
+
"Insight %s routed to %s: %s",
|
|
129
|
+
insight.id,
|
|
130
|
+
channel.value,
|
|
131
|
+
reason,
|
|
132
|
+
)
|
|
133
|
+
return decision
|
|
134
|
+
|
|
135
|
+
async def deliver_batch(
|
|
136
|
+
self,
|
|
137
|
+
insights: List[tuple[Deliverable, float]],
|
|
138
|
+
) -> List[DeliveryDecision]:
|
|
139
|
+
"""Route multiple insights with their urgency levels.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
insights: List of (insight, urgency) tuples
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of DeliveryDecision objects.
|
|
146
|
+
"""
|
|
147
|
+
decisions = []
|
|
148
|
+
for insight, urgency in insights:
|
|
149
|
+
decision = await self.deliver(insight, urgency)
|
|
150
|
+
decisions.append(decision)
|
|
151
|
+
return decisions
|
|
152
|
+
|
|
153
|
+
async def _send_push(self, insight: Deliverable) -> None:
|
|
154
|
+
"""Send an immediate push notification for a high-urgency insight."""
|
|
155
|
+
if self.events is None:
|
|
156
|
+
logger.warning("No event bus configured; insight %s not delivered via push", insight.id)
|
|
157
|
+
return
|
|
158
|
+
try:
|
|
159
|
+
from apsimo.events.types import InsightPushEvent
|
|
160
|
+
event = InsightPushEvent(
|
|
161
|
+
id=str(uuid.uuid4()),
|
|
162
|
+
source="insight_deliverer",
|
|
163
|
+
insight_id=insight.id,
|
|
164
|
+
description=insight.description,
|
|
165
|
+
urgency=1.0,
|
|
166
|
+
delivery_channel="push",
|
|
167
|
+
)
|
|
168
|
+
await self.events.emit_async(event)
|
|
169
|
+
logger.debug("Push event emitted for insight %s", insight.id)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.warning("Failed to emit push event for insight %s: %s", insight.id, e)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""SQLite-backed store for dismissed-insight overlays.
|
|
2
|
+
|
|
3
|
+
Insights themselves are re-computed each call by ConnectionDiscoverer —
|
|
4
|
+
we don't persist the insights. This store tracks which insight IDs the
|
|
5
|
+
user has dismissed so ``list_insights`` can filter them out.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
import logging
|
|
12
|
+
import sqlite3
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Iterable, Iterator, Set
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InsightStore:
|
|
21
|
+
"""Persistent record of dismissed insight IDs."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, db_path: Path | str) -> None:
|
|
24
|
+
self._db_path = str(db_path)
|
|
25
|
+
Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
with self._connect() as conn:
|
|
27
|
+
conn.execute(
|
|
28
|
+
"""
|
|
29
|
+
CREATE TABLE IF NOT EXISTS dismissed_insights (
|
|
30
|
+
insight_id TEXT PRIMARY KEY,
|
|
31
|
+
dismissed_at TEXT NOT NULL
|
|
32
|
+
)
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@contextmanager
|
|
37
|
+
def _connect(self) -> Iterator[sqlite3.Connection]:
|
|
38
|
+
connection = sqlite3.connect(self._db_path)
|
|
39
|
+
try:
|
|
40
|
+
with connection:
|
|
41
|
+
yield connection
|
|
42
|
+
finally:
|
|
43
|
+
connection.close()
|
|
44
|
+
|
|
45
|
+
def dismiss(self, insight_id: str) -> None:
|
|
46
|
+
"""Mark the given insight as dismissed. Idempotent."""
|
|
47
|
+
with self._connect() as conn:
|
|
48
|
+
conn.execute(
|
|
49
|
+
"""
|
|
50
|
+
INSERT INTO dismissed_insights (insight_id, dismissed_at)
|
|
51
|
+
VALUES (?, ?)
|
|
52
|
+
ON CONFLICT(insight_id) DO NOTHING
|
|
53
|
+
""",
|
|
54
|
+
(insight_id, datetime.now(timezone.utc).isoformat()),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def is_dismissed(self, insight_id: str) -> bool:
|
|
58
|
+
with self._connect() as conn:
|
|
59
|
+
row = conn.execute(
|
|
60
|
+
"SELECT 1 FROM dismissed_insights WHERE insight_id=? LIMIT 1",
|
|
61
|
+
(insight_id,),
|
|
62
|
+
).fetchone()
|
|
63
|
+
return row is not None
|
|
64
|
+
|
|
65
|
+
def list_dismissed(self) -> Set[str]:
|
|
66
|
+
with self._connect() as conn:
|
|
67
|
+
rows = conn.execute(
|
|
68
|
+
"SELECT insight_id FROM dismissed_insights"
|
|
69
|
+
).fetchall()
|
|
70
|
+
return {r[0] for r in rows}
|
|
71
|
+
|
|
72
|
+
def undismiss(self, insight_id: str) -> bool:
|
|
73
|
+
"""Remove a dismissal. Returns True if a row was removed."""
|
|
74
|
+
with self._connect() as conn:
|
|
75
|
+
cur = conn.execute(
|
|
76
|
+
"DELETE FROM dismissed_insights WHERE insight_id=?",
|
|
77
|
+
(insight_id,),
|
|
78
|
+
)
|
|
79
|
+
return cur.rowcount > 0
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Insight Validator — ensure insights meet quality thresholds before surfacing.
|
|
2
|
+
|
|
3
|
+
Validates insights against configurable criteria:
|
|
4
|
+
- Minimum evidence count (don't surface guesses)
|
|
5
|
+
- Confidence threshold (minimum certainty)
|
|
6
|
+
- Actionability check (prefer actionable insights)
|
|
7
|
+
- Data recency (stale data degrades trust)
|
|
8
|
+
|
|
9
|
+
Only insights passing all checks should be delivered to the user.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ValidationResult:
|
|
24
|
+
"""Result of insight validation.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
valid: Whether the insight passed all checks
|
|
28
|
+
reasons: List of failure reasons (empty if valid)
|
|
29
|
+
evidence_count: Number of supporting evidence items
|
|
30
|
+
confidence: The insight's confidence score
|
|
31
|
+
data_age_hours: Age of the oldest supporting data in hours
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
valid: bool
|
|
35
|
+
reasons: List[str]
|
|
36
|
+
evidence_count: int
|
|
37
|
+
confidence: float
|
|
38
|
+
data_age_hours: float
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class Validatable(Protocol):
|
|
43
|
+
"""Protocol for objects that can be validated as insights.
|
|
44
|
+
|
|
45
|
+
Insight objects must expose confidence, supporting evidence,
|
|
46
|
+
and optionally actionability flags.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def confidence(self) -> float: ...
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def supporting_evidence(self) -> List[str]: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class ValidatorGraphClient(Protocol):
|
|
58
|
+
"""Protocol for graph access needed by insight validation."""
|
|
59
|
+
|
|
60
|
+
async def recall(
|
|
61
|
+
self,
|
|
62
|
+
query: str,
|
|
63
|
+
limit: int = 10,
|
|
64
|
+
min_strength: float = 0.1,
|
|
65
|
+
) -> List[Dict[str, Any]]: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InsightValidator:
|
|
69
|
+
"""Validate insights before surfacing to the user.
|
|
70
|
+
|
|
71
|
+
Applies configurable quality gates. An insight must pass all
|
|
72
|
+
checks to be considered valid for delivery.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
min_evidence: Minimum number of supporting evidence items
|
|
76
|
+
min_confidence: Minimum confidence score (0-1)
|
|
77
|
+
max_data_age_hours: Maximum age of supporting data in hours
|
|
78
|
+
graph: Optional graph client for evidence timestamp lookups
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
min_evidence: int = 2,
|
|
84
|
+
min_confidence: float = 0.6,
|
|
85
|
+
max_data_age_hours: float = 168.0, # 1 week
|
|
86
|
+
graph: Optional[ValidatorGraphClient] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self.min_evidence = min_evidence
|
|
89
|
+
self.min_confidence = min_confidence
|
|
90
|
+
self.max_data_age_hours = max_data_age_hours
|
|
91
|
+
self._graph = graph
|
|
92
|
+
|
|
93
|
+
async def validate(self, insight: Validatable) -> ValidationResult:
|
|
94
|
+
"""Check if an insight meets all quality thresholds.
|
|
95
|
+
|
|
96
|
+
Evaluates evidence count, confidence, and data recency.
|
|
97
|
+
Returns a ValidationResult with pass/fail and detailed reasons.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
insight: The insight to validate
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
ValidationResult indicating whether the insight is fit
|
|
104
|
+
for delivery, with failure reasons if not.
|
|
105
|
+
"""
|
|
106
|
+
reasons: List[str] = []
|
|
107
|
+
|
|
108
|
+
# Evidence count
|
|
109
|
+
evidence_count = len(insight.supporting_evidence)
|
|
110
|
+
if evidence_count < self.min_evidence:
|
|
111
|
+
reasons.append(
|
|
112
|
+
f"Insufficient evidence ({evidence_count} < {self.min_evidence})"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Confidence check
|
|
116
|
+
confidence = insight.confidence
|
|
117
|
+
if confidence < self.min_confidence:
|
|
118
|
+
reasons.append(
|
|
119
|
+
f"Low confidence ({confidence:.2f} < {self.min_confidence})"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Data recency
|
|
123
|
+
data_age = await self._compute_data_age(insight)
|
|
124
|
+
if data_age > self.max_data_age_hours:
|
|
125
|
+
reasons.append(
|
|
126
|
+
f"Stale data ({data_age:.0f}h > {self.max_data_age_hours:.0f}h)"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
valid = len(reasons) == 0
|
|
130
|
+
|
|
131
|
+
return ValidationResult(
|
|
132
|
+
valid=valid,
|
|
133
|
+
reasons=reasons,
|
|
134
|
+
evidence_count=evidence_count,
|
|
135
|
+
confidence=confidence,
|
|
136
|
+
data_age_hours=data_age,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def validate_batch(
|
|
140
|
+
self,
|
|
141
|
+
insights: List[Validatable],
|
|
142
|
+
) -> List[tuple[Any, ValidationResult]]:
|
|
143
|
+
"""Validate multiple insights, returning paired results.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
insights: List of insights to validate
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of (insight, ValidationResult) tuples.
|
|
150
|
+
"""
|
|
151
|
+
results = []
|
|
152
|
+
for insight in insights:
|
|
153
|
+
result = await self.validate(insight)
|
|
154
|
+
results.append((insight, result))
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
async def _compute_data_age(self, insight: Validatable) -> float:
|
|
158
|
+
"""Calculate the age of the oldest supporting data in hours."""
|
|
159
|
+
if not insight.supporting_evidence or self._graph is None:
|
|
160
|
+
return 0.0 # no evidence or no graph = treat as fresh
|
|
161
|
+
|
|
162
|
+
oldest_hours = 0.0
|
|
163
|
+
now = datetime.now(timezone.utc)
|
|
164
|
+
for evidence_id in insight.supporting_evidence:
|
|
165
|
+
try:
|
|
166
|
+
records = await self._graph.recall(
|
|
167
|
+
evidence_id, limit=1, min_strength=0.0
|
|
168
|
+
)
|
|
169
|
+
if records:
|
|
170
|
+
created_at = records[0].get("created_at")
|
|
171
|
+
if created_at is not None:
|
|
172
|
+
if hasattr(created_at, 'to_native'):
|
|
173
|
+
created_at = created_at.to_native()
|
|
174
|
+
elif isinstance(created_at, str):
|
|
175
|
+
created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
176
|
+
if isinstance(created_at, datetime):
|
|
177
|
+
if created_at.tzinfo is None:
|
|
178
|
+
created_at = created_at.replace(tzinfo=timezone.utc)
|
|
179
|
+
age = (now - created_at).total_seconds() / 3600
|
|
180
|
+
oldest_hours = max(oldest_hours, age)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.debug("Could not fetch data age for evidence %s: %s", evidence_id, e)
|
|
183
|
+
return oldest_hours
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Novelty Scorer — rate how surprising or useful an insight is.
|
|
2
|
+
|
|
3
|
+
Scores connections and insights based on:
|
|
4
|
+
- Historical frequency (has this been observed before?)
|
|
5
|
+
- Domain distance (cross-domain insights score higher)
|
|
6
|
+
- Confidence delta (unexpected findings score higher)
|
|
7
|
+
|
|
8
|
+
Higher novelty scores indicate insights the user is less likely to
|
|
9
|
+
already know, making them more valuable to surface.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class NoveltyScore:
|
|
23
|
+
"""Novelty assessment for an insight or connection.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
score: Overall novelty score between 0 and 1
|
|
27
|
+
reasons: Human-readable explanations for the score
|
|
28
|
+
historical_frequency: Number of prior observations of similar connections
|
|
29
|
+
domain_distance: How far apart the involved domains are (0-1)
|
|
30
|
+
confidence_delta: How much the confidence deviates from expectation
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
score: float
|
|
34
|
+
reasons: List[str]
|
|
35
|
+
historical_frequency: int
|
|
36
|
+
domain_distance: float
|
|
37
|
+
confidence_delta: float
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class ScoringGraphClient(Protocol):
|
|
42
|
+
"""Protocol for graph access needed by novelty scoring."""
|
|
43
|
+
|
|
44
|
+
async def recall(
|
|
45
|
+
self,
|
|
46
|
+
query: str,
|
|
47
|
+
limit: int = 10,
|
|
48
|
+
min_strength: float = 0.1,
|
|
49
|
+
) -> List[Dict[str, Any]]: ...
|
|
50
|
+
|
|
51
|
+
async def run_query(
|
|
52
|
+
self,
|
|
53
|
+
cypher: str,
|
|
54
|
+
params: dict,
|
|
55
|
+
) -> List[Dict[str, Any]]: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@runtime_checkable
|
|
59
|
+
class Connectable(Protocol):
|
|
60
|
+
"""Protocol for objects that have source/target domain attributes."""
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def source_domain(self) -> str: ...
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def target_domain(self) -> str: ...
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def confidence(self) -> float: ...
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def description(self) -> str: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Semantic distance matrix between domains.
|
|
76
|
+
# Higher values mean domains are more distant (cross-domain insights
|
|
77
|
+
# spanning distant domains are scored as more novel).
|
|
78
|
+
DOMAIN_DISTANCES: Dict[tuple[str, str], float] = {
|
|
79
|
+
("work", "relationships"): 0.6,
|
|
80
|
+
("health", "work"): 0.8,
|
|
81
|
+
("health", "finance"): 0.9,
|
|
82
|
+
("work", "finance"): 0.4,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class NoveltyScorer:
|
|
87
|
+
"""Score how novel or useful a connection or insight is.
|
|
88
|
+
|
|
89
|
+
Combines historical frequency, domain distance, and confidence
|
|
90
|
+
deviation into a single novelty score. Lower frequency + higher
|
|
91
|
+
distance + higher confidence delta = more novel.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
graph_client: Graph database client for historical lookups
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, graph_client: ScoringGraphClient) -> None:
|
|
98
|
+
self.graph = graph_client
|
|
99
|
+
|
|
100
|
+
async def score_connection(self, connection: Connectable) -> NoveltyScore:
|
|
101
|
+
"""Score novelty of a discovered connection.
|
|
102
|
+
|
|
103
|
+
Computes a weighted composite of three factors:
|
|
104
|
+
- Frequency factor (40%): inverse of historical frequency
|
|
105
|
+
- Domain distance (30%): semantic distance between domains
|
|
106
|
+
- Confidence delta (30%): deviation from expected confidence
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
connection: A connection or insight with domain attributes
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
NoveltyScore with composite score and explanations.
|
|
113
|
+
"""
|
|
114
|
+
freq = await self._get_historical_frequency(connection)
|
|
115
|
+
distance = await self._compute_domain_distance(
|
|
116
|
+
connection.source_domain,
|
|
117
|
+
connection.target_domain,
|
|
118
|
+
)
|
|
119
|
+
delta = await self._calculate_confidence_delta(connection)
|
|
120
|
+
|
|
121
|
+
# Composite score: lower frequency + higher distance + higher delta = more novel
|
|
122
|
+
frequency_factor = 1.0 / (1.0 + freq)
|
|
123
|
+
score = frequency_factor * 0.4 + distance * 0.3 + delta * 0.3
|
|
124
|
+
|
|
125
|
+
reasons: List[str] = []
|
|
126
|
+
if freq == 0:
|
|
127
|
+
reasons.append("Never observed before")
|
|
128
|
+
if distance > 0.5:
|
|
129
|
+
reasons.append("Cross-domain connection")
|
|
130
|
+
if delta > 0.3:
|
|
131
|
+
reasons.append("Unexpected confidence level")
|
|
132
|
+
|
|
133
|
+
return NoveltyScore(
|
|
134
|
+
score=min(1.0, score),
|
|
135
|
+
reasons=reasons,
|
|
136
|
+
historical_frequency=freq,
|
|
137
|
+
domain_distance=distance,
|
|
138
|
+
confidence_delta=delta,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def _get_historical_frequency(self, connection: Connectable) -> int:
|
|
142
|
+
"""Count how many similar connections exist in the graph."""
|
|
143
|
+
source_domain = connection.source_domain
|
|
144
|
+
target_domain = connection.target_domain
|
|
145
|
+
|
|
146
|
+
# Try run_query first (direct Cypher)
|
|
147
|
+
if hasattr(self.graph, 'run_query'):
|
|
148
|
+
try:
|
|
149
|
+
cypher = """
|
|
150
|
+
MATCH (m:Memory {type: "connection"})
|
|
151
|
+
WHERE (m.metadata.source_domain = $source_domain
|
|
152
|
+
AND m.metadata.target_domain = $target_domain)
|
|
153
|
+
OR (m.metadata.source_domain = $target_domain
|
|
154
|
+
AND m.metadata.target_domain = $source_domain)
|
|
155
|
+
RETURN count(m) AS frequency
|
|
156
|
+
"""
|
|
157
|
+
records = await self.graph.run_query(cypher, {
|
|
158
|
+
"source_domain": source_domain,
|
|
159
|
+
"target_domain": target_domain,
|
|
160
|
+
})
|
|
161
|
+
if records:
|
|
162
|
+
return int(records[0].get("frequency", 0))
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.debug("_get_historical_frequency query failed: %s", e)
|
|
165
|
+
|
|
166
|
+
# Fallback: use recall with domain pair query
|
|
167
|
+
try:
|
|
168
|
+
query = f"{source_domain} {target_domain} connection pattern"
|
|
169
|
+
results = await self.graph.recall(query, limit=50, min_strength=0.1)
|
|
170
|
+
return len(results)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.debug("_get_historical_frequency recall fallback failed: %s", e)
|
|
173
|
+
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
async def _compute_domain_distance(self, source: str, target: str) -> float:
|
|
177
|
+
"""Compute domain distance from graph confidence data.
|
|
178
|
+
|
|
179
|
+
Queries the graph for mean confidence of historical connections between
|
|
180
|
+
the domain pair. Distance is derived as ``1 - mean_confidence``: high-
|
|
181
|
+
confidence connections indicate domains are closely related (low
|
|
182
|
+
distance); low-confidence connections indicate distant domains (high
|
|
183
|
+
distance). Falls back to ``_calculate_domain_distance`` when the graph
|
|
184
|
+
returns no data.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
source: Source domain name.
|
|
188
|
+
target: Target domain name.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Distance between 0 (same / closely related) and 1 (maximally distant).
|
|
192
|
+
"""
|
|
193
|
+
if source == target:
|
|
194
|
+
return 0.0
|
|
195
|
+
|
|
196
|
+
if hasattr(self.graph, "run_query"):
|
|
197
|
+
try:
|
|
198
|
+
cypher = """
|
|
199
|
+
MATCH (m:Memory {type: "connection"})
|
|
200
|
+
WHERE ((m.metadata.source_domain = $source_domain
|
|
201
|
+
AND m.metadata.target_domain = $target_domain)
|
|
202
|
+
OR (m.metadata.source_domain = $target_domain
|
|
203
|
+
AND m.metadata.target_domain = $source_domain))
|
|
204
|
+
AND m.metadata.confidence IS NOT NULL
|
|
205
|
+
RETURN avg(toFloat(m.metadata.confidence)) AS mean_confidence
|
|
206
|
+
"""
|
|
207
|
+
records = await self.graph.run_query(
|
|
208
|
+
cypher,
|
|
209
|
+
{"source_domain": source, "target_domain": target},
|
|
210
|
+
)
|
|
211
|
+
if records and records[0].get("mean_confidence") is not None:
|
|
212
|
+
mean_conf = float(records[0]["mean_confidence"])
|
|
213
|
+
return max(0.0, min(1.0, 1.0 - mean_conf))
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.debug("_compute_domain_distance query failed: %s", e)
|
|
216
|
+
|
|
217
|
+
return self._calculate_domain_distance(source, target)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _calculate_domain_distance(source: str, target: str) -> float:
|
|
221
|
+
"""Calculate semantic distance between two domains.
|
|
222
|
+
|
|
223
|
+
Uses a precomputed distance matrix. Unknown pairs default to 0.5.
|
|
224
|
+
Same-domain connections have distance 0.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
source: Source domain name
|
|
228
|
+
target: Target domain name
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Distance between 0 (same domain) and 1 (maximally distant).
|
|
232
|
+
"""
|
|
233
|
+
if source == target:
|
|
234
|
+
return 0.0
|
|
235
|
+
return DOMAIN_DISTANCES.get(
|
|
236
|
+
(source, target),
|
|
237
|
+
DOMAIN_DISTANCES.get((target, source), 0.5),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def _calculate_confidence_delta(self, connection: Connectable) -> float:
|
|
241
|
+
"""Measure how unexpected the connection's confidence level is."""
|
|
242
|
+
source_domain = connection.source_domain
|
|
243
|
+
target_domain = connection.target_domain
|
|
244
|
+
|
|
245
|
+
# Try run_query first
|
|
246
|
+
if hasattr(self.graph, 'run_query'):
|
|
247
|
+
try:
|
|
248
|
+
cypher = """
|
|
249
|
+
MATCH (m:Memory {type: "connection"})
|
|
250
|
+
WHERE ((m.metadata.source_domain = $source_domain
|
|
251
|
+
AND m.metadata.target_domain = $target_domain)
|
|
252
|
+
OR (m.metadata.source_domain = $target_domain
|
|
253
|
+
AND m.metadata.target_domain = $source_domain))
|
|
254
|
+
AND m.metadata.confidence IS NOT NULL
|
|
255
|
+
RETURN avg(toFloat(m.metadata.confidence)) AS mean_confidence
|
|
256
|
+
"""
|
|
257
|
+
records = await self.graph.run_query(cypher, {
|
|
258
|
+
"source_domain": source_domain,
|
|
259
|
+
"target_domain": target_domain,
|
|
260
|
+
})
|
|
261
|
+
if records and records[0].get("mean_confidence") is not None:
|
|
262
|
+
mean_conf = float(records[0]["mean_confidence"])
|
|
263
|
+
return abs(connection.confidence - mean_conf)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.debug("_calculate_confidence_delta query failed: %s", e)
|
|
266
|
+
|
|
267
|
+
return 0.0
|