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/tom/asymmetry.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Knowledge-asymmetry engine (tom2, H3.2) — daily, default OFF.
|
|
2
|
+
|
|
3
|
+
Compares the per-contact epistemic rows in SharedFactsStore and derives
|
|
4
|
+
second-order inferences:
|
|
5
|
+
|
|
6
|
+
* ``knows`` — a contact knows the facts recorded for them;
|
|
7
|
+
* ``unaware_of`` — a contact has no equivalent of a (confident) fact
|
|
8
|
+
another contact holds.
|
|
9
|
+
|
|
10
|
+
Fact TEXT is compared in memory only; what persists (live mode) are
|
|
11
|
+
Tom2Store rows holding fact IDS — the refs-not-content invariant is the
|
|
12
|
+
store's hard write-time pin, this engine simply never has a reason to
|
|
13
|
+
violate it.
|
|
14
|
+
|
|
15
|
+
Modes (COLONY_TOM2): off (default, engine is inert) | shadow (counts only,
|
|
16
|
+
writes nothing) | live (writes owner-only inference rows).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any, Dict, List
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# A cross-contact "unaware_of" is an inference, not an observation: born
|
|
29
|
+
# modest, and only derived from reasonably confident source facts.
|
|
30
|
+
_UNAWARE_CONFIDENCE = 0.4
|
|
31
|
+
_MIN_SOURCE_CONFIDENCE = 0.6
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def tom2_mode() -> str:
|
|
35
|
+
from apsimo.util.autonomy_preset import resolve
|
|
36
|
+
return resolve("COLONY_TOM2", ("off", "shadow", "live"), "off")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def tom2_context_enabled() -> bool:
|
|
40
|
+
"""COLONY_TOM2_CONTEXT (default 0, H3.3): may tom2 inferences be
|
|
41
|
+
injected into the OWNER'S assembled context? Injection is additionally
|
|
42
|
+
keyed to the owner's contact identity at assembly time — this flag can
|
|
43
|
+
never widen the audience, only turn the owner section on."""
|
|
44
|
+
return os.environ.get("COLONY_TOM2_CONTEXT", "0").strip().lower() in (
|
|
45
|
+
"1", "true", "yes", "on")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _norm(text: Any) -> str:
|
|
49
|
+
return re.sub(r"\s+", " ", str(text or "").strip().lower())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AsymmetryEngine:
|
|
53
|
+
def __init__(self, facts_store: Any, tom2_store: Any,
|
|
54
|
+
max_facts: int = 500,
|
|
55
|
+
max_rows_per_contact: int = 50) -> None:
|
|
56
|
+
self._facts = facts_store
|
|
57
|
+
self._tom2 = tom2_store
|
|
58
|
+
self._max_facts = max_facts
|
|
59
|
+
self._max_rows = max_rows_per_contact
|
|
60
|
+
self.last_report: Dict[str, Any] = {}
|
|
61
|
+
|
|
62
|
+
def run(self) -> Dict[str, Any]:
|
|
63
|
+
mode = tom2_mode()
|
|
64
|
+
report: Dict[str, Any] = {"mode": mode, "contacts": 0, "knows": 0,
|
|
65
|
+
"unaware_of": 0, "written": 0}
|
|
66
|
+
self.last_report = report
|
|
67
|
+
if mode == "off" or self._facts is None or self._tom2 is None:
|
|
68
|
+
return report
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
facts: List[Dict[str, Any]] = self._facts.list_facts(
|
|
72
|
+
limit=self._max_facts).get("facts", [])
|
|
73
|
+
except Exception:
|
|
74
|
+
logger.warning("tom2 asymmetry: facts unavailable", exc_info=True)
|
|
75
|
+
return report
|
|
76
|
+
|
|
77
|
+
by_contact: Dict[str, List[Dict[str, Any]]] = {}
|
|
78
|
+
for f in facts:
|
|
79
|
+
cid = str(f.get("contact_id") or "").strip()
|
|
80
|
+
if cid and f.get("id"):
|
|
81
|
+
by_contact.setdefault(cid, []).append(f)
|
|
82
|
+
contacts = sorted(by_contact)
|
|
83
|
+
report["contacts"] = len(contacts)
|
|
84
|
+
|
|
85
|
+
for cid in contacts:
|
|
86
|
+
own = by_contact[cid]
|
|
87
|
+
own_texts = {_norm(f.get("fact")) for f in own}
|
|
88
|
+
|
|
89
|
+
# First-order grounding: a contact knows their own rows.
|
|
90
|
+
for f in own:
|
|
91
|
+
report["knows"] += 1
|
|
92
|
+
if mode == "live":
|
|
93
|
+
self._write(cid, "knows", f["id"],
|
|
94
|
+
float(f.get("confidence") or 0.5), report)
|
|
95
|
+
|
|
96
|
+
# Cross-contact asymmetry: confident facts held elsewhere with
|
|
97
|
+
# no textual equivalent in this contact's rows. The text match
|
|
98
|
+
# happens HERE, in memory; only ids are ever written.
|
|
99
|
+
written_rows = 0
|
|
100
|
+
for other in contacts:
|
|
101
|
+
if other == cid:
|
|
102
|
+
continue
|
|
103
|
+
for f in by_contact[other]:
|
|
104
|
+
if float(f.get("confidence") or 0.0) < _MIN_SOURCE_CONFIDENCE:
|
|
105
|
+
continue
|
|
106
|
+
if _norm(f.get("fact")) in own_texts:
|
|
107
|
+
continue
|
|
108
|
+
report["unaware_of"] += 1
|
|
109
|
+
if mode == "live" and written_rows < self._max_rows:
|
|
110
|
+
if self._write(cid, "unaware_of", f["id"],
|
|
111
|
+
_UNAWARE_CONFIDENCE, report):
|
|
112
|
+
written_rows += 1
|
|
113
|
+
|
|
114
|
+
logger.info(
|
|
115
|
+
"tom2 asymmetry[%s]: contacts=%d knows=%d unaware_of=%d written=%d",
|
|
116
|
+
mode, report["contacts"], report["knows"], report["unaware_of"],
|
|
117
|
+
report["written"])
|
|
118
|
+
return report
|
|
119
|
+
|
|
120
|
+
def _write(self, contact_id: str, kind: str, fact_ref: str,
|
|
121
|
+
confidence: float, report: Dict[str, Any]) -> bool:
|
|
122
|
+
try:
|
|
123
|
+
self._tom2.record_inference(
|
|
124
|
+
contact_id=contact_id, kind=kind, fact_ref=str(fact_ref),
|
|
125
|
+
confidence=confidence)
|
|
126
|
+
report["written"] += 1
|
|
127
|
+
return True
|
|
128
|
+
except Exception:
|
|
129
|
+
# A refused write (privacy pin) must never break the sweep.
|
|
130
|
+
logger.debug("tom2 inference write refused", exc_info=True)
|
|
131
|
+
return False
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Level-2 eligibility pipeline (L2.1) — ordered, first-fail-wins.
|
|
2
|
+
|
|
3
|
+
Decides whether ONE tom2 inference may be considered for level-2 rendering
|
|
4
|
+
to ONE reader in ONE conversation. Every check must pass, in order; the
|
|
5
|
+
first failure names itself (owner observability) and ends the evaluation.
|
|
6
|
+
Any internal error is a failure — never an exemption.
|
|
7
|
+
|
|
8
|
+
Order (each check exists for a specific threat):
|
|
9
|
+
|
|
10
|
+
1. subject-scope — the subject is a real, resolvable contact and
|
|
11
|
+
not the reader themself.
|
|
12
|
+
2. subject-owner — the subject is never the owner (T9: "the owner
|
|
13
|
+
hasn't heard X" must not be narrated to anyone).
|
|
14
|
+
3. subject-present — the subject was NOT sighted in this conversation
|
|
15
|
+
window (T5: never model someone into the room
|
|
16
|
+
they are standing in).
|
|
17
|
+
4. ref-visibility — the H3.5 double gate, delegated VERBATIM to
|
|
18
|
+
tom.tom2.render_inference_for_contact: every ref
|
|
19
|
+
must resolve to a fact row owned by the reader.
|
|
20
|
+
This is the structural guarantee that level 2
|
|
21
|
+
can never introduce new fact content.
|
|
22
|
+
5. mutual-knowledge — M1: reader and subject demonstrably know of each
|
|
23
|
+
other (a shared conversation sighting within
|
|
24
|
+
COLONY_TOM2_MUTUAL_WINDOW_DAYS, default 30).
|
|
25
|
+
6. tier-floors — M2: reader tier >= trusted, subject tier >=
|
|
26
|
+
regular.
|
|
27
|
+
7. approval — M3: the owner has approved this (reader,
|
|
28
|
+
subject) pair (injected hook; required unless
|
|
29
|
+
COLONY_TOM2_L2_APPROVAL=off).
|
|
30
|
+
8. budget — M7: the exposure ledger has budget for this
|
|
31
|
+
rendering (injected hook; no hook = no budget).
|
|
32
|
+
|
|
33
|
+
This module is dark: nothing live calls it until the wiring tranche.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
import os
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
|
|
42
|
+
|
|
43
|
+
from apsimo.gate.env_risk import _tier_rank, env_risk_window_hours
|
|
44
|
+
from apsimo.identity.participants import SYSTEM_CONTACT_ID
|
|
45
|
+
from apsimo.tom.tom2 import render_inference_for_contact
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
CHECKS = ("subject-scope", "subject-owner", "subject-present",
|
|
50
|
+
"ref-visibility", "mutual-knowledge", "tier-floors",
|
|
51
|
+
"approval", "budget")
|
|
52
|
+
|
|
53
|
+
#: Tier floors (M2): who may RECEIVE a level-2 line, who may be its SUBJECT.
|
|
54
|
+
READER_FLOOR = "trusted"
|
|
55
|
+
SUBJECT_FLOOR = "regular"
|
|
56
|
+
|
|
57
|
+
HookResult = Union[bool, Awaitable[bool]]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def l2_approval_mode() -> str:
|
|
61
|
+
"""COLONY_TOM2_L2_APPROVAL: 'required' (default) or 'off'. Any other
|
|
62
|
+
value reads as 'required' (fail closed)."""
|
|
63
|
+
raw = os.environ.get("COLONY_TOM2_L2_APPROVAL", "required").strip().lower()
|
|
64
|
+
return "off" if raw == "off" else "required"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def mutual_window_days() -> float:
|
|
68
|
+
"""COLONY_TOM2_MUTUAL_WINDOW_DAYS (default 30): how recent the reader/
|
|
69
|
+
subject co-sighting must be to count as mutual knowledge."""
|
|
70
|
+
try:
|
|
71
|
+
v = float(os.environ.get("COLONY_TOM2_MUTUAL_WINDOW_DAYS", "30"))
|
|
72
|
+
return v if v > 0 else 30.0
|
|
73
|
+
except (TypeError, ValueError):
|
|
74
|
+
return 30.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class EligibilityDecision:
|
|
79
|
+
eligible: bool
|
|
80
|
+
failed_check: Optional[str] = None
|
|
81
|
+
detail: str = ""
|
|
82
|
+
checks_passed: List[str] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
85
|
+
return {"eligible": self.eligible, "failed_check": self.failed_check,
|
|
86
|
+
"detail": self.detail,
|
|
87
|
+
"checks_passed": list(self.checks_passed)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _fail(check: str, detail: str,
|
|
91
|
+
passed: List[str]) -> EligibilityDecision:
|
|
92
|
+
return EligibilityDecision(eligible=False, failed_check=check,
|
|
93
|
+
detail=detail, checks_passed=list(passed))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def _hook_ok(hook: Any, *args: Any) -> bool:
|
|
97
|
+
"""Run an injected hook; anything but a clean truthy answer is False."""
|
|
98
|
+
if hook is None:
|
|
99
|
+
return False
|
|
100
|
+
try:
|
|
101
|
+
r = hook(*args)
|
|
102
|
+
if hasattr(r, "__await__"):
|
|
103
|
+
r = await r
|
|
104
|
+
return bool(r)
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.debug("eligibility hook failed (=> ineligible)", exc_info=True)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def evaluate_inference(
|
|
111
|
+
inference: Dict[str, Any],
|
|
112
|
+
*,
|
|
113
|
+
reader_contact_id: str,
|
|
114
|
+
conversation_key: str,
|
|
115
|
+
facts_store: Any,
|
|
116
|
+
contacts_store: Any,
|
|
117
|
+
presence_store: Any,
|
|
118
|
+
owner_id: Optional[str] = None,
|
|
119
|
+
approval_check: Optional[Callable[[str, str], HookResult]] = None,
|
|
120
|
+
budget_check: Optional[Callable[[str, str, str], HookResult]] = None,
|
|
121
|
+
) -> EligibilityDecision:
|
|
122
|
+
"""Run the full ordered pipeline for one inference. Never raises."""
|
|
123
|
+
try:
|
|
124
|
+
return await _evaluate(inference,
|
|
125
|
+
reader_contact_id=reader_contact_id,
|
|
126
|
+
conversation_key=conversation_key,
|
|
127
|
+
facts_store=facts_store,
|
|
128
|
+
contacts_store=contacts_store,
|
|
129
|
+
presence_store=presence_store,
|
|
130
|
+
owner_id=owner_id,
|
|
131
|
+
approval_check=approval_check,
|
|
132
|
+
budget_check=budget_check)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
logger.debug("eligibility evaluation failed (=> ineligible): %s",
|
|
135
|
+
exc, exc_info=True)
|
|
136
|
+
return _fail("error", f"{type(exc).__name__}", [])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _evaluate(inference, *, reader_contact_id, conversation_key,
|
|
140
|
+
facts_store, contacts_store, presence_store, owner_id,
|
|
141
|
+
approval_check, budget_check) -> EligibilityDecision:
|
|
142
|
+
passed: List[str] = []
|
|
143
|
+
reader = str(reader_contact_id or "").strip()
|
|
144
|
+
subject = str((inference or {}).get("contact_id") or "").strip()
|
|
145
|
+
|
|
146
|
+
# 1. subject-scope
|
|
147
|
+
if not isinstance(inference, dict) or not subject:
|
|
148
|
+
return _fail("subject-scope", "no subject", passed)
|
|
149
|
+
if not reader or reader == SYSTEM_CONTACT_ID:
|
|
150
|
+
return _fail("subject-scope", "no reader", passed)
|
|
151
|
+
if subject == reader or subject == SYSTEM_CONTACT_ID:
|
|
152
|
+
return _fail("subject-scope", "subject is reader/system", passed)
|
|
153
|
+
if contacts_store is None:
|
|
154
|
+
return _fail("subject-scope", "contacts store missing", passed)
|
|
155
|
+
subject_contact = await contacts_store.get(subject)
|
|
156
|
+
if subject_contact is None:
|
|
157
|
+
return _fail("subject-scope", "subject unresolvable", passed)
|
|
158
|
+
passed.append("subject-scope")
|
|
159
|
+
|
|
160
|
+
# 2. subject-owner (T9). Owner identity unknown fails closed: we cannot
|
|
161
|
+
# PROVE the subject is not the owner.
|
|
162
|
+
if owner_id is None:
|
|
163
|
+
from apsimo.identity.resolver import get_owner_contact_id
|
|
164
|
+
owner_id = get_owner_contact_id()
|
|
165
|
+
owner = str(owner_id or "").strip()
|
|
166
|
+
if not owner:
|
|
167
|
+
return _fail("subject-owner", "owner identity unset", passed)
|
|
168
|
+
if subject == owner:
|
|
169
|
+
return _fail("subject-owner", "subject is the owner", passed)
|
|
170
|
+
passed.append("subject-owner")
|
|
171
|
+
|
|
172
|
+
# 3. subject-present (T5). A broken/missing presence store cannot prove
|
|
173
|
+
# absence — fail closed.
|
|
174
|
+
if presence_store is None:
|
|
175
|
+
return _fail("subject-present", "presence store missing", passed)
|
|
176
|
+
try:
|
|
177
|
+
present = presence_store.is_present(
|
|
178
|
+
conversation_key, subject, window_hours=env_risk_window_hours())
|
|
179
|
+
except Exception:
|
|
180
|
+
return _fail("subject-present", "presence read failed", passed)
|
|
181
|
+
if present:
|
|
182
|
+
return _fail("subject-present", "subject sighted in conversation",
|
|
183
|
+
passed)
|
|
184
|
+
passed.append("subject-present")
|
|
185
|
+
|
|
186
|
+
# 4. ref-visibility — the H3.5 gate, verbatim. None = ineligible,
|
|
187
|
+
# whether from partial visibility, a missing fact, or the master
|
|
188
|
+
# COLONY_TOM2_CROSS_CONTEXT flag being off.
|
|
189
|
+
if render_inference_for_contact(inference, facts_store, reader) is None:
|
|
190
|
+
return _fail("ref-visibility", "H3.5 gate refused", passed)
|
|
191
|
+
passed.append("ref-visibility")
|
|
192
|
+
|
|
193
|
+
# 5. mutual-knowledge (M1)
|
|
194
|
+
try:
|
|
195
|
+
mutual = presence_store.cooccurred(reader, subject,
|
|
196
|
+
within_days=mutual_window_days())
|
|
197
|
+
except Exception:
|
|
198
|
+
return _fail("mutual-knowledge", "cooccurrence read failed", passed)
|
|
199
|
+
if not mutual:
|
|
200
|
+
return _fail("mutual-knowledge",
|
|
201
|
+
"no recent shared conversation", passed)
|
|
202
|
+
passed.append("mutual-knowledge")
|
|
203
|
+
|
|
204
|
+
# 6. tier-floors (M2)
|
|
205
|
+
reader_contact = await contacts_store.get(reader)
|
|
206
|
+
if reader_contact is None:
|
|
207
|
+
return _fail("tier-floors", "reader unresolvable", passed)
|
|
208
|
+
if _tier_rank(getattr(reader_contact, "trust_tier", "")) <\
|
|
209
|
+
_tier_rank(READER_FLOOR):
|
|
210
|
+
return _fail("tier-floors", "reader below trusted", passed)
|
|
211
|
+
if _tier_rank(getattr(subject_contact, "trust_tier", "")) <\
|
|
212
|
+
_tier_rank(SUBJECT_FLOOR):
|
|
213
|
+
return _fail("tier-floors", "subject below regular", passed)
|
|
214
|
+
passed.append("tier-floors")
|
|
215
|
+
|
|
216
|
+
# 7. approval (M3)
|
|
217
|
+
if l2_approval_mode() == "off":
|
|
218
|
+
passed.append("approval")
|
|
219
|
+
elif await _hook_ok(approval_check, reader, subject):
|
|
220
|
+
passed.append("approval")
|
|
221
|
+
else:
|
|
222
|
+
return _fail("approval", "pair not approved by owner", passed)
|
|
223
|
+
|
|
224
|
+
# 8. budget (M7): no ledger, no budget.
|
|
225
|
+
fact_ref = str((inference or {}).get("fact_ref") or "")
|
|
226
|
+
if not await _hook_ok(budget_check, reader, subject, fact_ref):
|
|
227
|
+
return _fail("budget", "no exposure budget", passed)
|
|
228
|
+
passed.append("budget")
|
|
229
|
+
|
|
230
|
+
return EligibilityDecision(eligible=True, checks_passed=passed)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def eligible_inferences(rows: List[Dict[str, Any]], *,
|
|
234
|
+
limit: int = 5, **kwargs: Any
|
|
235
|
+
) -> List[Dict[str, Any]]:
|
|
236
|
+
"""Filter inference rows through the pipeline (order preserved), up to
|
|
237
|
+
``limit`` eligible results. Errors inside any evaluation drop that row."""
|
|
238
|
+
out: List[Dict[str, Any]] = []
|
|
239
|
+
limit = max(0, int(limit))
|
|
240
|
+
if limit == 0:
|
|
241
|
+
return out
|
|
242
|
+
for row in rows or []:
|
|
243
|
+
decision = await evaluate_inference(row, **kwargs)
|
|
244
|
+
if decision.eligible:
|
|
245
|
+
out.append(row)
|
|
246
|
+
if len(out) >= limit:
|
|
247
|
+
break
|
|
248
|
+
return out
|
apsimo/tom/engagement.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Read current attributed engagement observations; retain numeric history only.
|
|
2
|
+
|
|
3
|
+
Canonical source claims own preferences; AppraisalStore projects them. Legacy
|
|
4
|
+
aggregates remain inspectable but no longer produce psychological certainty.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import sqlite3
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Numeric dimensions. OCEAN are signed (-1 low .. +1 high); style are 0..1.
|
|
17
|
+
OCEAN = ("openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism")
|
|
18
|
+
STYLE = ("formality", "directness", "warmth", "verbosity", "emoji_ok", "humor")
|
|
19
|
+
_ALL_DIMS = OCEAN + STYLE
|
|
20
|
+
_QUAL_KEYS = ("motivators", "topics", "avoid")
|
|
21
|
+
_QUAL_CAP = 8
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _now() -> str:
|
|
25
|
+
return datetime.now(timezone.utc).isoformat()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from .source_lineage import SourceLinkedStore
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EngagementStore(SourceLinkedStore):
|
|
32
|
+
"""SQLite-backed evolving engagement profile per contact."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, db_path: str, *, source_ledger=None) -> None:
|
|
35
|
+
self._db_path = str(db_path)
|
|
36
|
+
self._source_ledger = source_ledger
|
|
37
|
+
self._conn = sqlite3.connect(self._db_path)
|
|
38
|
+
self._conn.row_factory = sqlite3.Row
|
|
39
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
40
|
+
self._conn.executescript(
|
|
41
|
+
"""
|
|
42
|
+
CREATE TABLE IF NOT EXISTS engagement_profiles (
|
|
43
|
+
contact_id TEXT PRIMARY KEY,
|
|
44
|
+
dims_json TEXT NOT NULL DEFAULT '{}',
|
|
45
|
+
qual_json TEXT NOT NULL DEFAULT '{}',
|
|
46
|
+
observation_count INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
updated_at TEXT NOT NULL
|
|
48
|
+
);
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
# Capture old aggregate profiles once, without inventing source links.
|
|
52
|
+
# New observations are separate so deletion can replay their EMA exactly.
|
|
53
|
+
with self._conn:
|
|
54
|
+
self._conn.execute('BEGIN IMMEDIATE')
|
|
55
|
+
first = not self._conn.execute("SELECT 1 FROM sqlite_master WHERE name='engagement_baselines'").fetchone()
|
|
56
|
+
self._conn.execute("""CREATE TABLE IF NOT EXISTS engagement_baselines (
|
|
57
|
+
contact_id TEXT PRIMARY KEY, dims_json TEXT NOT NULL, qual_json TEXT NOT NULL,
|
|
58
|
+
observation_count INTEGER NOT NULL, updated_at TEXT NOT NULL,
|
|
59
|
+
evidence_basis TEXT NOT NULL DEFAULT 'legacy_unlinked')""")
|
|
60
|
+
self._conn.execute("""CREATE TABLE IF NOT EXISTS engagement_observations (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, contact_id TEXT NOT NULL,
|
|
62
|
+
observation_json TEXT NOT NULL, source_lineage_json TEXT, created_at TEXT NOT NULL)""")
|
|
63
|
+
self._conn.execute('CREATE INDEX IF NOT EXISTS idx_engagement_observation_contact ON engagement_observations(contact_id)')
|
|
64
|
+
if first:
|
|
65
|
+
self._conn.execute("""INSERT INTO engagement_baselines
|
|
66
|
+
(contact_id,dims_json,qual_json,observation_count,updated_at)
|
|
67
|
+
SELECT contact_id,dims_json,qual_json,observation_count,updated_at FROM engagement_profiles""")
|
|
68
|
+
|
|
69
|
+
def purge_erased_sources(self, turn_ids=None, *, contact_id=None) -> int:
|
|
70
|
+
sql = 'SELECT id,contact_id,source_lineage_json FROM engagement_observations WHERE source_lineage_json IS NOT NULL'
|
|
71
|
+
rows = self._conn.execute(sql + (' AND contact_id=?' if contact_id else ''),
|
|
72
|
+
(contact_id,) if contact_id else ()).fetchall()
|
|
73
|
+
invalid = self._invalid_sources(rows, turn_ids)
|
|
74
|
+
with self._conn:
|
|
75
|
+
self._conn.executemany('DELETE FROM engagement_observations WHERE id=?', [(row['id'],) for row in invalid])
|
|
76
|
+
for person in {row['contact_id'] for row in invalid}:
|
|
77
|
+
self._recompute_profile(person)
|
|
78
|
+
return len(invalid)
|
|
79
|
+
|
|
80
|
+
# -- read ---------------------------------------------------------------
|
|
81
|
+
def _row(self, contact_id: str) -> Optional[sqlite3.Row]:
|
|
82
|
+
return self._conn.execute(
|
|
83
|
+
"SELECT * FROM engagement_profiles WHERE contact_id=?", (contact_id,)
|
|
84
|
+
).fetchone()
|
|
85
|
+
|
|
86
|
+
def get_profile(self, contact_id: str) -> Dict[str, Any]:
|
|
87
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
88
|
+
row = self._row(contact_id)
|
|
89
|
+
baseline = self._conn.execute('SELECT observation_count FROM engagement_baselines WHERE contact_id=?', (contact_id,)).fetchone()
|
|
90
|
+
dims_raw = json.loads(row["dims_json"] or "{}") if row else {}
|
|
91
|
+
observations = []
|
|
92
|
+
if self._source_ledger is not None:
|
|
93
|
+
from apsimo.identity import get_owner_contact_id
|
|
94
|
+
from apsimo.self_model.appraisals import AppraisalStore
|
|
95
|
+
observations = AppraisalStore(self._source_ledger, owner_id=get_owner_contact_id()).view(
|
|
96
|
+
contact_id, viewer_contact_id=contact_id)['records']
|
|
97
|
+
return {
|
|
98
|
+
"contact_id": contact_id, "dims": {}, "qual": {},
|
|
99
|
+
"observations": observations, "observation_count": len(observations),
|
|
100
|
+
"legacy_profile": {"dims": dims_raw, "qual": json.loads(row["qual_json"] or "{}") if row else {},
|
|
101
|
+
"observation_count": row["observation_count"] if row else 0, "governing": False},
|
|
102
|
+
"updated_at": row["updated_at"] if row else None,
|
|
103
|
+
"legacy_unlinked_observations": baseline[0] if baseline else 0,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# -- write --------------------------------------------------------------
|
|
107
|
+
def update_from_observation(
|
|
108
|
+
self,
|
|
109
|
+
contact_id: str,
|
|
110
|
+
ocean: Optional[Dict[str, Any]] = None,
|
|
111
|
+
style: Optional[Dict[str, Any]] = None,
|
|
112
|
+
motivators: Optional[List[str]] = None,
|
|
113
|
+
topics: Optional[List[str]] = None,
|
|
114
|
+
avoid: Optional[List[str]] = None,
|
|
115
|
+
source_lineage: Optional[Dict[str, Any]] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Fold one observation into the contact's evolving profile (EMA per dim)."""
|
|
118
|
+
if not contact_id:
|
|
119
|
+
return
|
|
120
|
+
if source_lineage is not None and not self._source_visible(contact_id, source_lineage):
|
|
121
|
+
from apsimo.turns.idempotency import SourceErased
|
|
122
|
+
raise SourceErased('source_erased')
|
|
123
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
124
|
+
observation = dict(ocean=ocean, style=style, motivators=motivators, topics=topics, avoid=avoid)
|
|
125
|
+
with self._conn:
|
|
126
|
+
self._conn.execute("""INSERT INTO engagement_observations
|
|
127
|
+
(contact_id,observation_json,source_lineage_json,created_at) VALUES (?,?,?,?)""",
|
|
128
|
+
(contact_id, json.dumps(observation), json.dumps(source_lineage) if source_lineage is not None else None, _now()))
|
|
129
|
+
row = self._row(contact_id)
|
|
130
|
+
dims = json.loads(row['dims_json']) if row else {}
|
|
131
|
+
qual = json.loads(row['qual_json']) if row else {}
|
|
132
|
+
self._fold(dims, qual, **observation)
|
|
133
|
+
self._write_profile(contact_id, dims, qual, (row['observation_count'] if row else 0) + 1, _now())
|
|
134
|
+
# Erasure can commit while the projection database is being written.
|
|
135
|
+
if source_lineage is not None and not self._source_visible(contact_id, source_lineage):
|
|
136
|
+
self.purge_erased_sources(contact_id=contact_id)
|
|
137
|
+
from apsimo.turns.idempotency import SourceErased
|
|
138
|
+
raise SourceErased('source_erased')
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _fold(dims, qual, *, ocean=None, style=None, motivators=None, topics=None, avoid=None):
|
|
142
|
+
obs = dict(ocean or {})
|
|
143
|
+
obs.update(style or {})
|
|
144
|
+
for dim, val in obs.items():
|
|
145
|
+
if dim not in _ALL_DIMS or val is None:
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
val = float(val)
|
|
149
|
+
except (TypeError, ValueError):
|
|
150
|
+
continue
|
|
151
|
+
lo = -1.0 if dim in OCEAN else 0.0
|
|
152
|
+
val = max(lo, min(1.0, val))
|
|
153
|
+
cur = dims.get(dim)
|
|
154
|
+
if cur is None:
|
|
155
|
+
dims[dim] = {"v": round(val, 4), "n": 1}
|
|
156
|
+
else:
|
|
157
|
+
n = cur["n"] + 1
|
|
158
|
+
alpha = max(0.15, 1.0 / n) # responsive early, stabilises later
|
|
159
|
+
v = cur["v"] + alpha * (val - cur["v"])
|
|
160
|
+
dims[dim] = {"v": round(v, 4), "n": n}
|
|
161
|
+
|
|
162
|
+
for key, items in (("motivators", motivators), ("topics", topics), ("avoid", avoid)):
|
|
163
|
+
if not items:
|
|
164
|
+
continue
|
|
165
|
+
existing = list(qual.get(key, []))
|
|
166
|
+
seen = {s.lower() for s in existing}
|
|
167
|
+
for it in items:
|
|
168
|
+
it = (it or "").strip()
|
|
169
|
+
if it and it.lower() not in seen:
|
|
170
|
+
existing.append(it)
|
|
171
|
+
seen.add(it.lower())
|
|
172
|
+
qual[key] = existing[-_QUAL_CAP:] # keep most recent
|
|
173
|
+
|
|
174
|
+
def _recompute_profile(self, contact_id):
|
|
175
|
+
baseline = self._conn.execute('SELECT * FROM engagement_baselines WHERE contact_id=?', (contact_id,)).fetchone()
|
|
176
|
+
rows = self._conn.execute('SELECT * FROM engagement_observations WHERE contact_id=? ORDER BY id', (contact_id,)).fetchall()
|
|
177
|
+
if not baseline and not rows:
|
|
178
|
+
self._conn.execute('DELETE FROM engagement_profiles WHERE contact_id=?', (contact_id,))
|
|
179
|
+
return
|
|
180
|
+
dims = json.loads(baseline['dims_json']) if baseline else {}
|
|
181
|
+
qual = json.loads(baseline['qual_json']) if baseline else {}
|
|
182
|
+
count = baseline['observation_count'] if baseline else 0
|
|
183
|
+
updated = baseline['updated_at'] if baseline else None
|
|
184
|
+
for row in rows:
|
|
185
|
+
self._fold(dims, qual, **json.loads(row['observation_json']))
|
|
186
|
+
count += 1
|
|
187
|
+
updated = row['created_at']
|
|
188
|
+
self._write_profile(contact_id, dims, qual, count, updated)
|
|
189
|
+
|
|
190
|
+
def _write_profile(self, contact_id, dims, qual, count, updated):
|
|
191
|
+
self._conn.execute(
|
|
192
|
+
"""INSERT INTO engagement_profiles (contact_id, dims_json, qual_json, observation_count, updated_at)
|
|
193
|
+
VALUES (?,?,?,?,?)
|
|
194
|
+
ON CONFLICT(contact_id) DO UPDATE SET
|
|
195
|
+
dims_json=excluded.dims_json, qual_json=excluded.qual_json,
|
|
196
|
+
observation_count=excluded.observation_count, updated_at=excluded.updated_at""",
|
|
197
|
+
(contact_id, json.dumps(dims), json.dumps(qual), count, updated),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def purge(self, contact_id: str) -> None:
|
|
201
|
+
with self._conn:
|
|
202
|
+
self._conn.execute("DELETE FROM engagement_profiles WHERE contact_id=?", (contact_id,))
|
|
203
|
+
self._conn.execute("DELETE FROM engagement_observations WHERE contact_id=?", (contact_id,))
|
|
204
|
+
self._conn.execute("DELETE FROM engagement_baselines WHERE contact_id=?", (contact_id,))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def build_guidance(profile: Dict[str, Any]) -> str:
|
|
208
|
+
"""Only current attributed preferences; numeric legacy profiles never govern."""
|
|
209
|
+
lines = []
|
|
210
|
+
for item in profile.get('observations', [])[:4]:
|
|
211
|
+
if item.get('kind') != 'preference' or item.get('status') != 'current' or not item.get('sources'):
|
|
212
|
+
continue
|
|
213
|
+
lines.append('Stated preference (attributed, correctable): ' + str(item.get('text', '')))
|
|
214
|
+
return '\n'.join(lines)
|