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
apsimo/telemetry.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Sidecar telemetry store for temporal health monitoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# v0.21.0 — persisted so last_*_at survive a restart (previously in-memory only,
|
|
17
|
+
# which reset silence/last-outreach reasoning to null on every restart).
|
|
18
|
+
_PERSIST_KEYS = (
|
|
19
|
+
"started_at", "last_sync_at", "last_tick_at",
|
|
20
|
+
"last_initiative_at", "last_prefetch_at", "last_agent_outreach_at",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _telemetry_path() -> Path:
|
|
25
|
+
return Path(os.environ.get("COLONY_STATE_DIR", os.path.expanduser("~/.colony"))) / "telemetry.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TelemetryStore:
|
|
30
|
+
started_at: Optional[datetime] = None
|
|
31
|
+
last_sync_at: Optional[datetime] = None
|
|
32
|
+
last_tick_at: Optional[datetime] = None
|
|
33
|
+
last_initiative_at: Optional[datetime] = None
|
|
34
|
+
last_prefetch_at: Optional[datetime] = None
|
|
35
|
+
last_agent_outreach_at: Optional[datetime] = None
|
|
36
|
+
#: "ok" normally; "unknown" when persisted state could not be restored
|
|
37
|
+
#: (corrupt/unreadable file) — the reset must be visible, never a
|
|
38
|
+
#: silently fresh-looking store.
|
|
39
|
+
state: str = "ok"
|
|
40
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
41
|
+
|
|
42
|
+
def load(self) -> None:
|
|
43
|
+
"""Restore persisted timestamps (except started_at) across restart."""
|
|
44
|
+
path = _telemetry_path()
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return # first boot — nothing persisted yet
|
|
47
|
+
try:
|
|
48
|
+
data = json.loads(path.read_text())
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
# A corrupt file silently resetting every timestamp to None used
|
|
51
|
+
# to produce a permanently unflaggable "ok". Make the reset loud.
|
|
52
|
+
logger.warning(
|
|
53
|
+
"telemetry state at %s is unreadable (%s) — timestamps reset; "
|
|
54
|
+
"temporal state is UNKNOWN until subsystems report again",
|
|
55
|
+
path, exc,
|
|
56
|
+
)
|
|
57
|
+
self.state = "unknown"
|
|
58
|
+
return
|
|
59
|
+
for key in _PERSIST_KEYS:
|
|
60
|
+
if key == "started_at":
|
|
61
|
+
continue # started_at is set fresh per process
|
|
62
|
+
val = data.get(key)
|
|
63
|
+
if val:
|
|
64
|
+
try:
|
|
65
|
+
setattr(self, key, datetime.fromisoformat(val))
|
|
66
|
+
except (ValueError, TypeError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def _persist(self) -> None:
|
|
70
|
+
try:
|
|
71
|
+
p = _telemetry_path()
|
|
72
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
data = {
|
|
74
|
+
k: (getattr(self, k).isoformat() if getattr(self, k) else None)
|
|
75
|
+
for k in _PERSIST_KEYS
|
|
76
|
+
}
|
|
77
|
+
tmp = p.with_suffix(".json.tmp")
|
|
78
|
+
tmp.write_text(json.dumps(data))
|
|
79
|
+
os.replace(tmp, p)
|
|
80
|
+
except Exception as exc: # pragma: no cover
|
|
81
|
+
logger.debug("telemetry persist failed: %s", exc)
|
|
82
|
+
|
|
83
|
+
async def touch(self, key: str) -> None:
|
|
84
|
+
async with self._lock:
|
|
85
|
+
setattr(self, key, datetime.now(timezone.utc))
|
|
86
|
+
self._persist()
|
|
87
|
+
|
|
88
|
+
async def silence_hours(self, key: str) -> Optional[float]:
|
|
89
|
+
"""Return hours since the last event of the given type."""
|
|
90
|
+
attr_map = {
|
|
91
|
+
"sync": "last_sync_at",
|
|
92
|
+
"tick": "last_tick_at",
|
|
93
|
+
"initiative": "last_initiative_at",
|
|
94
|
+
"prefetch": "last_prefetch_at",
|
|
95
|
+
}
|
|
96
|
+
attr = attr_map.get(key, key)
|
|
97
|
+
async with self._lock:
|
|
98
|
+
ts = getattr(self, attr, None)
|
|
99
|
+
if ts is None:
|
|
100
|
+
return None
|
|
101
|
+
return (datetime.now(timezone.utc) - ts).total_seconds() / 3600
|
|
102
|
+
|
|
103
|
+
async def stale_flags(self, thresholds: Dict[str, float]) -> List[str]:
|
|
104
|
+
flags = []
|
|
105
|
+
for key, threshold in thresholds.items():
|
|
106
|
+
silence = await self.silence_hours(key)
|
|
107
|
+
if silence is None:
|
|
108
|
+
# Never recorded. A subsystem that has not run once since
|
|
109
|
+
# process start, past its own staleness threshold, is stale —
|
|
110
|
+
# "never ran" must flag, not read as silence/ok.
|
|
111
|
+
async with self._lock:
|
|
112
|
+
started = self.started_at
|
|
113
|
+
if started is not None:
|
|
114
|
+
uptime_h = (
|
|
115
|
+
datetime.now(timezone.utc) - started
|
|
116
|
+
).total_seconds() / 3600
|
|
117
|
+
if uptime_h > threshold:
|
|
118
|
+
flags.append(f"{key}:never_ran")
|
|
119
|
+
continue
|
|
120
|
+
if silence > threshold:
|
|
121
|
+
flags.append(key)
|
|
122
|
+
return flags
|
|
123
|
+
|
|
124
|
+
async def to_dict(self, thresholds: Dict[str, float]) -> dict:
|
|
125
|
+
started = self.started_at.isoformat() if self.started_at else None
|
|
126
|
+
sync_at = self.last_sync_at.isoformat() if self.last_sync_at else None
|
|
127
|
+
tick_at = self.last_tick_at.isoformat() if self.last_tick_at else None
|
|
128
|
+
init_at = self.last_initiative_at.isoformat() if self.last_initiative_at else None
|
|
129
|
+
prefetch_at = self.last_prefetch_at.isoformat() if self.last_prefetch_at else None
|
|
130
|
+
outreach_at = self.last_agent_outreach_at.isoformat() if self.last_agent_outreach_at else None
|
|
131
|
+
silence = {}
|
|
132
|
+
for key in thresholds:
|
|
133
|
+
silence[key] = await self.silence_hours(key)
|
|
134
|
+
flags = await self.stale_flags(thresholds)
|
|
135
|
+
return {
|
|
136
|
+
"started_at": started,
|
|
137
|
+
"last_sync_at": sync_at,
|
|
138
|
+
"last_tick_at": tick_at,
|
|
139
|
+
"last_initiative_at": init_at,
|
|
140
|
+
"last_prefetch_at": prefetch_at,
|
|
141
|
+
"last_agent_outreach_at": outreach_at,
|
|
142
|
+
"silence_hours": silence,
|
|
143
|
+
"stale_flags": flags,
|
|
144
|
+
"state": self.state,
|
|
145
|
+
}
|
apsimo/tom/__init__.py
ADDED
apsimo/tom/affect.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Affect Tracker — per-contact emotional valence and arousal over time.
|
|
2
|
+
|
|
3
|
+
Stores discrete affect events and maintains a computed current state per
|
|
4
|
+
contact with exponential decay toward neutral.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import sqlite3
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Default decay: 5% per hour toward neutral.
|
|
19
|
+
DEFAULT_DECAY_FACTOR = 0.95
|
|
20
|
+
DEFAULT_AROUSAL_BASELINE = 0.3
|
|
21
|
+
DEFAULT_SUSTAINED_DECLINE_MAGNITUDE = 0.3
|
|
22
|
+
_DECAY_ONLY_SOURCES = frozenset({"decay"})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from .source_lineage import SourceLinkedStore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AffectStore(SourceLinkedStore):
|
|
29
|
+
"""SQLite-backed affect event store with computed state."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, db_path: str, *, decay_factor: float = DEFAULT_DECAY_FACTOR, source_ledger=None) -> None:
|
|
32
|
+
self._db_path = db_path
|
|
33
|
+
self._source_ledger = source_ledger
|
|
34
|
+
self._decay_factor = decay_factor
|
|
35
|
+
self._conn = sqlite3.connect(db_path)
|
|
36
|
+
self._conn.row_factory = sqlite3.Row
|
|
37
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
38
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
39
|
+
self._create_tables()
|
|
40
|
+
|
|
41
|
+
def _create_tables(self) -> None:
|
|
42
|
+
self._conn.executescript("""
|
|
43
|
+
CREATE TABLE IF NOT EXISTS affect_events (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
contact_id TEXT NOT NULL,
|
|
46
|
+
valence REAL NOT NULL,
|
|
47
|
+
arousal REAL NOT NULL DEFAULT 0.5,
|
|
48
|
+
source TEXT NOT NULL,
|
|
49
|
+
trigger TEXT,
|
|
50
|
+
timestamp TEXT NOT NULL,
|
|
51
|
+
session_id TEXT
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_affect_contact
|
|
55
|
+
ON affect_events(contact_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_affect_timestamp
|
|
57
|
+
ON affect_events(timestamp);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS affect_state (
|
|
60
|
+
contact_id TEXT PRIMARY KEY,
|
|
61
|
+
current_valence REAL NOT NULL DEFAULT 0.0,
|
|
62
|
+
current_arousal REAL NOT NULL DEFAULT 0.3,
|
|
63
|
+
trend TEXT NOT NULL DEFAULT 'stable',
|
|
64
|
+
last_event_id TEXT,
|
|
65
|
+
last_updated TEXT NOT NULL,
|
|
66
|
+
event_count INTEGER NOT NULL DEFAULT 0
|
|
67
|
+
);
|
|
68
|
+
""")
|
|
69
|
+
if 'source_lineage_json' not in {row[1] for row in self._conn.execute('PRAGMA table_info(affect_events)')}:
|
|
70
|
+
self._conn.execute('ALTER TABLE affect_events ADD COLUMN source_lineage_json TEXT')
|
|
71
|
+
self._conn.commit()
|
|
72
|
+
|
|
73
|
+
def purge_erased_sources(self, turn_ids=None, *, contact_id=None) -> int:
|
|
74
|
+
sql = 'SELECT id,contact_id,source_lineage_json FROM affect_events WHERE source_lineage_json IS NOT NULL'
|
|
75
|
+
rows = self._conn.execute(sql + (' AND contact_id=?' if contact_id else ''),
|
|
76
|
+
(contact_id,) if contact_id else ()).fetchall()
|
|
77
|
+
invalid = self._invalid_sources(rows, turn_ids)
|
|
78
|
+
with self._conn:
|
|
79
|
+
self._conn.executemany('DELETE FROM affect_events WHERE id=?', [(row['id'],) for row in invalid])
|
|
80
|
+
for person in {row['contact_id'] for row in invalid}:
|
|
81
|
+
self._recompute_state(person, commit=False)
|
|
82
|
+
return len(invalid)
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _decode(row):
|
|
86
|
+
event = dict(row)
|
|
87
|
+
raw = event.pop('source_lineage_json', None)
|
|
88
|
+
event['source_lineage'] = json.loads(raw) if raw else None
|
|
89
|
+
event['evidence_basis'] = 'canonical_source' if raw else 'unlinked_observation'
|
|
90
|
+
return event
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Event CRUD
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def create_event(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
contact_id: str,
|
|
100
|
+
valence: float,
|
|
101
|
+
arousal: float = 0.5,
|
|
102
|
+
source: str = "explicit",
|
|
103
|
+
trigger: Optional[str] = None,
|
|
104
|
+
session_id: Optional[str] = None,
|
|
105
|
+
timestamp: Optional[str] = None,
|
|
106
|
+
source_lineage: Optional[Dict[str, Any]] = None,
|
|
107
|
+
) -> Dict[str, Any]:
|
|
108
|
+
"""Record an affect event and update the contact's computed state.
|
|
109
|
+
|
|
110
|
+
Returns the created event dict.
|
|
111
|
+
"""
|
|
112
|
+
if source_lineage is not None and not self._source_visible(contact_id, source_lineage):
|
|
113
|
+
from apsimo.turns.idempotency import SourceErased
|
|
114
|
+
raise SourceErased('source_erased')
|
|
115
|
+
# Clamp valence and arousal to valid ranges.
|
|
116
|
+
valence = max(-1.0, min(1.0, valence))
|
|
117
|
+
arousal = max(0.0, min(1.0, arousal))
|
|
118
|
+
|
|
119
|
+
event_id = str(uuid.uuid4())
|
|
120
|
+
ts = timestamp or datetime.now(timezone.utc).isoformat()
|
|
121
|
+
|
|
122
|
+
with self._conn:
|
|
123
|
+
self._conn.execute(
|
|
124
|
+
"""INSERT INTO affect_events (id, contact_id, valence, arousal, source, trigger, timestamp, session_id, source_lineage_json)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
126
|
+
(event_id, contact_id, valence, arousal, source, trigger, ts, session_id,
|
|
127
|
+
json.dumps(source_lineage) if source_lineage is not None else None),
|
|
128
|
+
)
|
|
129
|
+
self._recompute_state(contact_id, commit=False)
|
|
130
|
+
|
|
131
|
+
event = self.get_event(event_id)
|
|
132
|
+
if event is None:
|
|
133
|
+
from apsimo.turns.idempotency import SourceErased
|
|
134
|
+
raise SourceErased('source_erased')
|
|
135
|
+
return event
|
|
136
|
+
|
|
137
|
+
def get_event(self, event_id: str) -> Optional[Dict[str, Any]]:
|
|
138
|
+
owner = self._conn.execute('SELECT contact_id FROM affect_events WHERE id=?', (event_id,)).fetchone()
|
|
139
|
+
if owner is None:
|
|
140
|
+
return None
|
|
141
|
+
self.purge_erased_sources(contact_id=owner['contact_id'])
|
|
142
|
+
row = self._conn.execute(
|
|
143
|
+
"SELECT * FROM affect_events WHERE id = ?", (event_id,)
|
|
144
|
+
).fetchone()
|
|
145
|
+
if row is None:
|
|
146
|
+
return None
|
|
147
|
+
return self._decode(row)
|
|
148
|
+
|
|
149
|
+
def list_events(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
contact_id: Optional[str] = None,
|
|
153
|
+
source: Optional[str] = None,
|
|
154
|
+
limit: int = 50,
|
|
155
|
+
offset: int = 0,
|
|
156
|
+
) -> List[Dict[str, Any]]:
|
|
157
|
+
"""List affect events with optional filters."""
|
|
158
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
159
|
+
clauses: List[str] = []
|
|
160
|
+
params: List[Any] = []
|
|
161
|
+
|
|
162
|
+
if contact_id is not None:
|
|
163
|
+
clauses.append("contact_id = ?")
|
|
164
|
+
params.append(contact_id)
|
|
165
|
+
if source is not None:
|
|
166
|
+
clauses.append("source = ?")
|
|
167
|
+
params.append(source)
|
|
168
|
+
|
|
169
|
+
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
170
|
+
rows = self._conn.execute(
|
|
171
|
+
f"SELECT * FROM affect_events{where} ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
|
172
|
+
params + [limit, offset],
|
|
173
|
+
).fetchall()
|
|
174
|
+
return [self._decode(r) for r in rows]
|
|
175
|
+
|
|
176
|
+
def count_events(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
contact_id: Optional[str] = None,
|
|
180
|
+
source: Optional[str] = None,
|
|
181
|
+
) -> int:
|
|
182
|
+
"""Total matching events, so paginated views can report a real total."""
|
|
183
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
184
|
+
clauses: List[str] = []
|
|
185
|
+
params: List[Any] = []
|
|
186
|
+
if contact_id is not None:
|
|
187
|
+
clauses.append("contact_id = ?")
|
|
188
|
+
params.append(contact_id)
|
|
189
|
+
if source is not None:
|
|
190
|
+
clauses.append("source = ?")
|
|
191
|
+
params.append(source)
|
|
192
|
+
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
193
|
+
return int(self._conn.execute(
|
|
194
|
+
f"SELECT COUNT(*) FROM affect_events{where}", params
|
|
195
|
+
).fetchone()[0])
|
|
196
|
+
|
|
197
|
+
def delete_event(self, event_id: str) -> bool:
|
|
198
|
+
"""Delete an affect event and recompute state. Returns True if deleted."""
|
|
199
|
+
event = self.get_event(event_id)
|
|
200
|
+
if event is None:
|
|
201
|
+
return False
|
|
202
|
+
self._conn.execute("DELETE FROM affect_events WHERE id = ?", (event_id,))
|
|
203
|
+
self._conn.commit()
|
|
204
|
+
self._recompute_state(event["contact_id"])
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# Computed state
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def get_state(self, contact_id: str) -> Dict[str, Any]:
|
|
212
|
+
"""Get the current affect state for a contact.
|
|
213
|
+
|
|
214
|
+
Applies time-based decay before returning.
|
|
215
|
+
"""
|
|
216
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
217
|
+
self._apply_decay(contact_id)
|
|
218
|
+
row = self._conn.execute(
|
|
219
|
+
"SELECT * FROM affect_state WHERE contact_id = ?", (contact_id,)
|
|
220
|
+
).fetchone()
|
|
221
|
+
if row is None:
|
|
222
|
+
return {
|
|
223
|
+
"contact_id": contact_id,
|
|
224
|
+
"current_valence": 0.0,
|
|
225
|
+
"current_arousal": DEFAULT_AROUSAL_BASELINE,
|
|
226
|
+
"trend": "stable",
|
|
227
|
+
"last_event_id": None,
|
|
228
|
+
"last_updated": None,
|
|
229
|
+
"event_count": 0,
|
|
230
|
+
}
|
|
231
|
+
return dict(row)
|
|
232
|
+
|
|
233
|
+
def get_all_states(self) -> List[Dict[str, Any]]:
|
|
234
|
+
"""Get affect states for all contacts with recorded events."""
|
|
235
|
+
self.purge_erased_sources()
|
|
236
|
+
rows = self._conn.execute("SELECT * FROM affect_state").fetchall()
|
|
237
|
+
results = []
|
|
238
|
+
for row in rows:
|
|
239
|
+
self._apply_decay(row["contact_id"])
|
|
240
|
+
results.append(self.get_state(row["contact_id"]))
|
|
241
|
+
return results
|
|
242
|
+
|
|
243
|
+
def detect_negative_spike(self, contact_id: str, threshold: float = -0.5) -> bool:
|
|
244
|
+
"""Check if the most recent event is a negative spike."""
|
|
245
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
246
|
+
row = self._conn.execute(
|
|
247
|
+
"SELECT valence FROM affect_events WHERE contact_id = ? ORDER BY timestamp DESC LIMIT 1",
|
|
248
|
+
(contact_id,),
|
|
249
|
+
).fetchone()
|
|
250
|
+
return row is not None and row["valence"] <= threshold
|
|
251
|
+
|
|
252
|
+
def detect_sustained_decline(
|
|
253
|
+
self,
|
|
254
|
+
contact_id: str,
|
|
255
|
+
min_events: int = 3,
|
|
256
|
+
min_magnitude: float = DEFAULT_SUSTAINED_DECLINE_MAGNITUDE,
|
|
257
|
+
) -> bool:
|
|
258
|
+
"""Check for a material decline supported by observed affect events.
|
|
259
|
+
|
|
260
|
+
Time decay changes the current state toward neutral but is not new
|
|
261
|
+
evidence about a contact. Recompute the trend from non-decay events
|
|
262
|
+
here so a stale/decayed state cannot trigger proactive initiative.
|
|
263
|
+
"""
|
|
264
|
+
state = self.get_state(contact_id)
|
|
265
|
+
if state["current_valence"] > -abs(min_magnitude):
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
rows = self._conn.execute(
|
|
269
|
+
"SELECT * FROM affect_events WHERE contact_id = ? ORDER BY timestamp ASC",
|
|
270
|
+
(contact_id,),
|
|
271
|
+
).fetchall()
|
|
272
|
+
observed = [row for row in rows if not self._is_decay_only(row)]
|
|
273
|
+
return len(observed) >= min_events and self._event_trend(observed) == "declining"
|
|
274
|
+
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
# Internal
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _is_decay_only(event: sqlite3.Row) -> bool:
|
|
281
|
+
return str(event["source"]).strip().lower() in _DECAY_ONLY_SOURCES
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _event_trend(rows: List[sqlite3.Row]) -> str:
|
|
285
|
+
"""Return the trend of the latest observed affect events."""
|
|
286
|
+
recent = rows[-5:] if len(rows) >= 5 else rows
|
|
287
|
+
if len(recent) < 2:
|
|
288
|
+
return "stable"
|
|
289
|
+
|
|
290
|
+
midpoint = len(recent) // 2
|
|
291
|
+
early_avg = sum(row["valence"] for row in recent[:midpoint]) / midpoint
|
|
292
|
+
late_avg = sum(row["valence"] for row in recent[midpoint:]) / (len(recent) - midpoint)
|
|
293
|
+
diff = late_avg - early_avg
|
|
294
|
+
if diff > 0.1:
|
|
295
|
+
return "improving"
|
|
296
|
+
if diff < -0.1:
|
|
297
|
+
return "declining"
|
|
298
|
+
return "stable"
|
|
299
|
+
|
|
300
|
+
def _apply_decay(self, contact_id: str) -> None:
|
|
301
|
+
"""Apply exponential decay to a contact's affect state."""
|
|
302
|
+
row = self._conn.execute(
|
|
303
|
+
"SELECT * FROM affect_state WHERE contact_id = ?", (contact_id,)
|
|
304
|
+
).fetchone()
|
|
305
|
+
if row is None or row["last_updated"] is None:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
last_updated = datetime.fromisoformat(row["last_updated"])
|
|
309
|
+
now = datetime.now(timezone.utc)
|
|
310
|
+
hours_elapsed = (now - last_updated).total_seconds() / 3600.0
|
|
311
|
+
|
|
312
|
+
if hours_elapsed <= 0:
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
decayed_valence = row["current_valence"] * (self._decay_factor ** hours_elapsed)
|
|
316
|
+
decayed_arousal = DEFAULT_AROUSAL_BASELINE + (row["current_arousal"] - DEFAULT_AROUSAL_BASELINE) * (self._decay_factor ** hours_elapsed)
|
|
317
|
+
|
|
318
|
+
# Round to avoid floating-point drift.
|
|
319
|
+
decayed_valence = round(decayed_valence, 4)
|
|
320
|
+
decayed_arousal = round(decayed_arousal, 4)
|
|
321
|
+
|
|
322
|
+
self._conn.execute(
|
|
323
|
+
"""UPDATE affect_state SET current_valence = ?, current_arousal = ?, last_updated = ?
|
|
324
|
+
WHERE contact_id = ?""",
|
|
325
|
+
(decayed_valence, decayed_arousal, now.isoformat(), contact_id),
|
|
326
|
+
)
|
|
327
|
+
self._conn.commit()
|
|
328
|
+
|
|
329
|
+
def _recompute_state(self, contact_id: str, *, commit=True) -> None:
|
|
330
|
+
"""Recompute the affect state from recent events.
|
|
331
|
+
|
|
332
|
+
Uses a weighted average with recency bias: more recent events
|
|
333
|
+
contribute more to the current state.
|
|
334
|
+
"""
|
|
335
|
+
rows = self._conn.execute(
|
|
336
|
+
"SELECT * FROM affect_events WHERE contact_id = ? ORDER BY timestamp ASC",
|
|
337
|
+
(contact_id,),
|
|
338
|
+
).fetchall()
|
|
339
|
+
|
|
340
|
+
if not rows:
|
|
341
|
+
self._conn.execute(
|
|
342
|
+
"DELETE FROM affect_state WHERE contact_id = ?", (contact_id,)
|
|
343
|
+
)
|
|
344
|
+
if commit:
|
|
345
|
+
self._conn.commit()
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Weighted average with exponential recency bias.
|
|
349
|
+
total_weight = 0.0
|
|
350
|
+
weighted_valence = 0.0
|
|
351
|
+
weighted_arousal = 0.0
|
|
352
|
+
weight = 1.0
|
|
353
|
+
|
|
354
|
+
for row in rows:
|
|
355
|
+
weighted_valence += row["valence"] * weight
|
|
356
|
+
weighted_arousal += row["arousal"] * weight
|
|
357
|
+
total_weight += weight
|
|
358
|
+
weight *= 0.9 # decay weight for older events
|
|
359
|
+
|
|
360
|
+
current_valence = round(weighted_valence / total_weight, 4)
|
|
361
|
+
current_arousal = round(weighted_arousal / total_weight, 4)
|
|
362
|
+
|
|
363
|
+
# Decay is a state projection, not a new observation. Keep it out of
|
|
364
|
+
# the event trend even if an imported/legacy row labels it explicitly.
|
|
365
|
+
observed = [row for row in rows if not self._is_decay_only(row)]
|
|
366
|
+
trend = self._event_trend(observed)
|
|
367
|
+
|
|
368
|
+
last_event = rows[-1]
|
|
369
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
370
|
+
|
|
371
|
+
self._conn.execute(
|
|
372
|
+
"""INSERT INTO affect_state (contact_id, current_valence, current_arousal, trend, last_event_id, last_updated, event_count)
|
|
373
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
374
|
+
ON CONFLICT(contact_id) DO UPDATE SET
|
|
375
|
+
current_valence = excluded.current_valence,
|
|
376
|
+
current_arousal = excluded.current_arousal,
|
|
377
|
+
trend = excluded.trend,
|
|
378
|
+
last_event_id = excluded.last_event_id,
|
|
379
|
+
last_updated = excluded.last_updated,
|
|
380
|
+
event_count = excluded.event_count""",
|
|
381
|
+
(contact_id, current_valence, current_arousal, trend, last_event["id"], now, len(rows)),
|
|
382
|
+
)
|
|
383
|
+
if commit:
|
|
384
|
+
self._conn.commit()
|
|
385
|
+
|
|
386
|
+
def close(self) -> None:
|
|
387
|
+
self._conn.close()
|