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/exposure.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Tom2 exposure ledger + budgets (L2.3) — refs only, never content.
|
|
2
|
+
|
|
3
|
+
Every level-2 rendering, when the system eventually wires one, must be
|
|
4
|
+
written here FIRST and budgeted against what was already shown. The ledger
|
|
5
|
+
slows aggregation (T6): a reader cannot harvest the epistemic map by asking
|
|
6
|
+
often, because budgets bind per (reader, subject) pair, per reader, and
|
|
7
|
+
globally per rolling 24h window.
|
|
8
|
+
|
|
9
|
+
PRIVACY INVARIANT (same pin as Tom2Store, enforced at write time and
|
|
10
|
+
regression-locked by a raw-DB regex test): a ledger row carries contact
|
|
11
|
+
ids, an opaque fact ref, and an opaque conversation key — cross-contact
|
|
12
|
+
fact TEXT never enters this table. A leak of the ledger reveals that
|
|
13
|
+
topology was rendered, never what it said.
|
|
14
|
+
|
|
15
|
+
Budget semantics are fail-closed: malformed env values read as budget 0
|
|
16
|
+
(nothing renders), and any storage error answers "no budget".
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sqlite3
|
|
25
|
+
import threading
|
|
26
|
+
import uuid
|
|
27
|
+
from datetime import datetime, timedelta, timezone
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from apsimo.tom.tom2 import _validate_ref
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
#: Opaque conversation key: compact, no whitespace — prose is refused so
|
|
35
|
+
#: message text cannot be smuggled through the key field.
|
|
36
|
+
_CONV_KEY_RE = re.compile(r"^[A-Za-z0-9_.:+@-]{1,128}$")
|
|
37
|
+
|
|
38
|
+
#: Shipped budget defaults — deliberately tight; a deployment widens them
|
|
39
|
+
#: explicitly. Malformed values fail closed to 0.
|
|
40
|
+
DEFAULT_PAIR_DAY = 1
|
|
41
|
+
DEFAULT_READER_DAY = 3
|
|
42
|
+
DEFAULT_GLOBAL_DAY = 10
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _budget(name: str, default: int) -> int:
|
|
46
|
+
raw = os.environ.get(name)
|
|
47
|
+
if raw is None or not str(raw).strip():
|
|
48
|
+
return default
|
|
49
|
+
try:
|
|
50
|
+
return max(0, int(str(raw).strip()))
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
logger.warning("%s=%r is malformed — failing closed to budget 0",
|
|
53
|
+
name, raw)
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def budget_pair_day() -> int:
|
|
58
|
+
"""COLONY_TOM2_BUDGET_PAIR_DAY (default 1): renderings per (reader,
|
|
59
|
+
subject) pair per rolling 24h."""
|
|
60
|
+
return _budget("COLONY_TOM2_BUDGET_PAIR_DAY", DEFAULT_PAIR_DAY)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def budget_reader_day() -> int:
|
|
64
|
+
"""COLONY_TOM2_BUDGET_READER_DAY (default 3): renderings per reader
|
|
65
|
+
per rolling 24h."""
|
|
66
|
+
return _budget("COLONY_TOM2_BUDGET_READER_DAY", DEFAULT_READER_DAY)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def budget_global_day() -> int:
|
|
70
|
+
"""COLONY_TOM2_BUDGET_GLOBAL_DAY (default 10): renderings across all
|
|
71
|
+
readers per rolling 24h."""
|
|
72
|
+
return _budget("COLONY_TOM2_BUDGET_GLOBAL_DAY", DEFAULT_GLOBAL_DAY)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _now() -> datetime:
|
|
76
|
+
return datetime.now(tz=timezone.utc)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _validate_conv_key(key: Any) -> str:
|
|
80
|
+
key = str(key or "").strip()
|
|
81
|
+
if not _CONV_KEY_RE.match(key):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"exposure conversation key {key[:40]!r} is not an opaque id "
|
|
84
|
+
"(refs-not-content invariant)")
|
|
85
|
+
return key
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _validate_cid(cid: Any, what: str) -> str:
|
|
89
|
+
cid = str(cid or "").strip()
|
|
90
|
+
if not _CONV_KEY_RE.match(cid):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"exposure {what} {cid[:40]!r} is not an opaque contact id "
|
|
93
|
+
"(refs-not-content invariant)")
|
|
94
|
+
return cid
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Tom2ExposureStore:
|
|
98
|
+
"""SQLite-backed ledger of level-2 renderings (refs-not-content)."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
|
101
|
+
self._lock = threading.RLock()
|
|
102
|
+
self._conn = sqlite3.connect(db_path or ":memory:",
|
|
103
|
+
check_same_thread=False)
|
|
104
|
+
self._conn.row_factory = sqlite3.Row
|
|
105
|
+
with self._lock:
|
|
106
|
+
self._conn.execute("""
|
|
107
|
+
CREATE TABLE IF NOT EXISTS tom2_exposures (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
reader_contact_id TEXT NOT NULL,
|
|
110
|
+
subject_contact_id TEXT NOT NULL,
|
|
111
|
+
fact_ref TEXT NOT NULL,
|
|
112
|
+
conversation_key TEXT NOT NULL,
|
|
113
|
+
created_at TEXT NOT NULL
|
|
114
|
+
)""")
|
|
115
|
+
self._conn.execute(
|
|
116
|
+
"CREATE INDEX IF NOT EXISTS idx_tom2_exp_reader "
|
|
117
|
+
"ON tom2_exposures(reader_contact_id, created_at)")
|
|
118
|
+
self._conn.commit()
|
|
119
|
+
|
|
120
|
+
# -- writes -------------------------------------------------------------
|
|
121
|
+
def record_exposure(self, *, reader_contact_id: str,
|
|
122
|
+
subject_contact_id: str, fact_ref: str,
|
|
123
|
+
conversation_key: str) -> Dict[str, Any]:
|
|
124
|
+
"""Append one rendering to the ledger. Raises on any privacy-pin
|
|
125
|
+
violation (prose in any field) — never best-effort."""
|
|
126
|
+
row = {
|
|
127
|
+
"id": str(uuid.uuid4()),
|
|
128
|
+
"reader_contact_id": _validate_cid(reader_contact_id, "reader"),
|
|
129
|
+
"subject_contact_id": _validate_cid(subject_contact_id,
|
|
130
|
+
"subject"),
|
|
131
|
+
"fact_ref": _validate_ref(fact_ref),
|
|
132
|
+
"conversation_key": _validate_conv_key(conversation_key),
|
|
133
|
+
"created_at": _now().isoformat(),
|
|
134
|
+
}
|
|
135
|
+
with self._lock:
|
|
136
|
+
self._conn.execute(
|
|
137
|
+
"INSERT INTO tom2_exposures (id, reader_contact_id, "
|
|
138
|
+
"subject_contact_id, fact_ref, conversation_key, created_at)"
|
|
139
|
+
" VALUES (?,?,?,?,?,?)", list(row.values()))
|
|
140
|
+
self._conn.commit()
|
|
141
|
+
return row
|
|
142
|
+
|
|
143
|
+
# -- budgets --------------------------------------------------------------
|
|
144
|
+
def _count(self, where: str, params: List[Any], hours: float) -> int:
|
|
145
|
+
cutoff = (_now() - timedelta(hours=hours)).isoformat()
|
|
146
|
+
with self._lock:
|
|
147
|
+
r = self._conn.execute(
|
|
148
|
+
f"SELECT COUNT(*) n FROM tom2_exposures WHERE {where} "
|
|
149
|
+
"AND created_at >= ?", params + [cutoff]).fetchone()
|
|
150
|
+
return int(r["n"])
|
|
151
|
+
|
|
152
|
+
def budget_ok(self, reader_contact_id: str, subject_contact_id: str,
|
|
153
|
+
fact_ref: str = "") -> bool:
|
|
154
|
+
"""True when rendering one more line for (reader, subject) stays
|
|
155
|
+
inside ALL budgets (pair, reader, global; rolling 24h). Any error
|
|
156
|
+
answers False — a broken ledger cannot vouch for capacity."""
|
|
157
|
+
try:
|
|
158
|
+
reader = str(reader_contact_id or "").strip()
|
|
159
|
+
subject = str(subject_contact_id or "").strip()
|
|
160
|
+
if not reader or not subject:
|
|
161
|
+
return False
|
|
162
|
+
if self._count(
|
|
163
|
+
"reader_contact_id=? AND subject_contact_id=?",
|
|
164
|
+
[reader, subject], 24.0) >= budget_pair_day():
|
|
165
|
+
return False
|
|
166
|
+
if self._count("reader_contact_id=?", [reader],
|
|
167
|
+
24.0) >= budget_reader_day():
|
|
168
|
+
return False
|
|
169
|
+
if self._count("1=1", [], 24.0) >= budget_global_day():
|
|
170
|
+
return False
|
|
171
|
+
return True
|
|
172
|
+
except Exception:
|
|
173
|
+
logger.debug("exposure budget check failed (=> no budget)",
|
|
174
|
+
exc_info=True)
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# -- owner reads ----------------------------------------------------------
|
|
178
|
+
def recent(self, *, reader_contact_id: Optional[str] = None,
|
|
179
|
+
subject_contact_id: Optional[str] = None,
|
|
180
|
+
limit: int = 50) -> List[Dict[str, Any]]:
|
|
181
|
+
clauses, params = ["1=1"], []
|
|
182
|
+
if reader_contact_id:
|
|
183
|
+
clauses.append("reader_contact_id=?")
|
|
184
|
+
params.append(reader_contact_id)
|
|
185
|
+
if subject_contact_id:
|
|
186
|
+
clauses.append("subject_contact_id=?")
|
|
187
|
+
params.append(subject_contact_id)
|
|
188
|
+
params.append(max(1, min(500, int(limit))))
|
|
189
|
+
with self._lock:
|
|
190
|
+
rows = self._conn.execute(
|
|
191
|
+
f"SELECT * FROM tom2_exposures WHERE {' AND '.join(clauses)}"
|
|
192
|
+
" ORDER BY created_at DESC LIMIT ?", params).fetchall()
|
|
193
|
+
return [dict(r) for r in rows]
|
|
194
|
+
|
|
195
|
+
def counts(self) -> Dict[str, Any]:
|
|
196
|
+
"""Aggregate observability (numbers only)."""
|
|
197
|
+
with self._lock:
|
|
198
|
+
total = self._conn.execute(
|
|
199
|
+
"SELECT COUNT(*) n FROM tom2_exposures").fetchone()["n"]
|
|
200
|
+
readers = self._conn.execute(
|
|
201
|
+
"SELECT COUNT(DISTINCT reader_contact_id) n "
|
|
202
|
+
"FROM tom2_exposures").fetchone()["n"]
|
|
203
|
+
pairs = self._conn.execute(
|
|
204
|
+
"SELECT COUNT(*) n FROM (SELECT DISTINCT reader_contact_id,"
|
|
205
|
+
" subject_contact_id FROM tom2_exposures)").fetchone()["n"]
|
|
206
|
+
last24 = self._count("1=1", [], 24.0)
|
|
207
|
+
return {"total": int(total), "readers": int(readers),
|
|
208
|
+
"pairs": int(pairs), "last_24h": last24}
|
|
209
|
+
|
|
210
|
+
def close(self) -> None:
|
|
211
|
+
try:
|
|
212
|
+
self._conn.close()
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
apsimo/tom/extractor.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""LLM-backed extraction for Theory of Mind.
|
|
2
|
+
|
|
3
|
+
Extracts affect signals and shared facts from conversation turns.
|
|
4
|
+
Uses the LLM router (same as world model entity extraction) — Colony
|
|
5
|
+
does NOT build its own LLM client.
|
|
6
|
+
|
|
7
|
+
Throttled per contact and operation: max 1 extraction per
|
|
8
|
+
COLONY_TOM_EXTRACTION_THROTTLE_MINUTES (default 5 minutes).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from apsimo.beliefs.promotion import PROMOTION_PROMPT, promotion_metadata
|
|
21
|
+
from apsimo.util.model_output import final_text
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
MAX_INPUT_CHARS = 4_000
|
|
26
|
+
MAX_RESPONSE_TOKENS = int(os.environ.get("COLONY_TOM_MAX_TOKENS", "2048")) # reasoning models (mimo) need room; 512 returns empty content
|
|
27
|
+
THROTTLE_MINUTES = int(os.environ.get("COLONY_TOM_EXTRACTION_THROTTLE_MINUTES", "5"))
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Affect extraction prompt
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
_AFFECT_SYSTEM_PROMPT = (
|
|
34
|
+
"You are an emotion analysis engine. Read the conversation and return a "
|
|
35
|
+
"JSON object with the speaker's emotional state. Keys: "
|
|
36
|
+
'"valence" (float -1.0 to 1.0, negative=unhappy, positive=happy), '
|
|
37
|
+
'"arousal" (float 0.0 to 1.0, 0=calm, 1=intense), '
|
|
38
|
+
'"trigger" (string, brief excerpt causing the reading), '
|
|
39
|
+
'"confidence" (float 0.0 to 1.0). '
|
|
40
|
+
"Return ONLY the JSON object. If the conversation is neutral, "
|
|
41
|
+
'return {"valence": 0.0, "arousal": 0.3, "trigger": null, "confidence": 0.5}. '
|
|
42
|
+
"Do not add prose, code fences, or explanation."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Fact extraction prompt
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_FACT_SYSTEM_PROMPT = (
|
|
50
|
+
"You are a knowledge extraction engine. Read the conversation and identify "
|
|
51
|
+
"what the contact now KNOWS or BELIEVES as a result of this conversation. "
|
|
52
|
+
"Return a JSON array of objects. Each object has keys: "
|
|
53
|
+
'"evidence" (an exact contiguous quotation of at most 500 characters from the conversation), '
|
|
54
|
+
'"source" (one of: told_by_contact, told_to_contact, shared_context, inferred), '
|
|
55
|
+
'"confidence" (float 0.0 to 1.0). '
|
|
56
|
+
"Keep the speaker's actual words and attribution. This is an estimate of what "
|
|
57
|
+
"was communicated, not proof it is true or that the contact accepted it. "
|
|
58
|
+
"Do not infer that the contact knows something merely because it was in the "
|
|
59
|
+
"assistant's response. Return at most 6 items. "
|
|
60
|
+
"Return ONLY the JSON array. Return [] if no useful new knowledge. "
|
|
61
|
+
"Do not add prose, code fences, or explanation."
|
|
62
|
+
) + '\n' + PROMOTION_PROMPT
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TomExtractor:
|
|
68
|
+
"""LLM-backed ToM extraction from conversation turns."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, llm_router: Any) -> None:
|
|
71
|
+
self._router = llm_router
|
|
72
|
+
self._last_extraction: Dict[str, str] = {} # contact_id → ISO timestamp
|
|
73
|
+
self._last_facts: Dict[str, str] = {} # affect must not suppress fact extraction
|
|
74
|
+
|
|
75
|
+
async def extract_affect(
|
|
76
|
+
self,
|
|
77
|
+
conversation_text: str,
|
|
78
|
+
contact_id: str,
|
|
79
|
+
*,
|
|
80
|
+
session_id: Optional[str] = None,
|
|
81
|
+
) -> Optional[Dict[str, Any]]:
|
|
82
|
+
"""Extract affect from a conversation snippet.
|
|
83
|
+
|
|
84
|
+
Returns dict with valence, arousal, trigger, confidence or None.
|
|
85
|
+
Throttled per contact.
|
|
86
|
+
"""
|
|
87
|
+
if not self._can_extract(contact_id):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
snippet = (conversation_text or "").strip()
|
|
91
|
+
if not snippet:
|
|
92
|
+
return None
|
|
93
|
+
if len(snippet) > MAX_INPUT_CHARS:
|
|
94
|
+
snippet = snippet[:MAX_INPUT_CHARS]
|
|
95
|
+
|
|
96
|
+
user_prompt = f"Conversation:\n---\n{snippet}\n---\n\nAnalyze the speaker's emotional state."
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
resp = await self._router.complete(
|
|
100
|
+
messages=[
|
|
101
|
+
{"role": "system", "content": _AFFECT_SYSTEM_PROMPT},
|
|
102
|
+
{"role": "user", "content": user_prompt},
|
|
103
|
+
],
|
|
104
|
+
context={"task": "tom_affect_extraction", "max_tokens": MAX_RESPONSE_TOKENS},
|
|
105
|
+
)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.warning("ToM affect extraction LLM call failed: %s", exc)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
content = final_text(resp)
|
|
112
|
+
except ValueError:
|
|
113
|
+
return None
|
|
114
|
+
result = _parse_affect_json(content)
|
|
115
|
+
if result is not None:
|
|
116
|
+
self._mark_extracted(contact_id)
|
|
117
|
+
result["source"] = "inferred"
|
|
118
|
+
result["contact_id"] = contact_id
|
|
119
|
+
if session_id:
|
|
120
|
+
result["session_id"] = session_id
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
async def extract_facts(
|
|
124
|
+
self,
|
|
125
|
+
conversation_text: str,
|
|
126
|
+
contact_id: str,
|
|
127
|
+
*,
|
|
128
|
+
session_id: Optional[str] = None,
|
|
129
|
+
) -> List[Dict[str, Any]]:
|
|
130
|
+
"""Extract shared facts from a conversation snippet.
|
|
131
|
+
|
|
132
|
+
Returns list of dicts with fact, source, confidence.
|
|
133
|
+
Throttled per contact.
|
|
134
|
+
"""
|
|
135
|
+
if not self._can_extract(contact_id, operation="facts"):
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
snippet = (conversation_text or "").strip()
|
|
139
|
+
if not snippet:
|
|
140
|
+
return []
|
|
141
|
+
if len(snippet) > MAX_INPUT_CHARS:
|
|
142
|
+
snippet = snippet[:MAX_INPUT_CHARS]
|
|
143
|
+
|
|
144
|
+
user_prompt = f"Conversation:\n---\n{snippet}\n---\n\nWhat does the contact now know?"
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
resp = await self._router.complete(
|
|
148
|
+
messages=[
|
|
149
|
+
{"role": "system", "content": _FACT_SYSTEM_PROMPT},
|
|
150
|
+
{"role": "user", "content": user_prompt},
|
|
151
|
+
],
|
|
152
|
+
context={"task": "tom_fact_extraction", "max_tokens": MAX_RESPONSE_TOKENS},
|
|
153
|
+
)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
logger.warning("ToM fact extraction LLM call failed: %s", exc)
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
content = final_text(resp)
|
|
160
|
+
except ValueError:
|
|
161
|
+
return []
|
|
162
|
+
results = _parse_fact_array(content, conversation_text=snippet)
|
|
163
|
+
self._mark_extracted(contact_id, operation="facts")
|
|
164
|
+
for r in results:
|
|
165
|
+
r["contact_id"] = contact_id
|
|
166
|
+
r['model_provenance'] = {
|
|
167
|
+
'function_role': getattr(resp, 'function_role', '') or 'extraction',
|
|
168
|
+
'model_id': getattr(resp, 'model_id', 'unknown'),
|
|
169
|
+
'config_revision': getattr(resp, 'config_revision', '') or 'unknown',
|
|
170
|
+
'weight_revision': getattr(resp, 'model_revision', '') or 'unknown'}
|
|
171
|
+
return results
|
|
172
|
+
|
|
173
|
+
async def extract_engagement(
|
|
174
|
+
self,
|
|
175
|
+
conversation_text: str,
|
|
176
|
+
contact_id: str,
|
|
177
|
+
*,
|
|
178
|
+
session_id: Optional[str] = None,
|
|
179
|
+
) -> Optional[Dict[str, Any]]:
|
|
180
|
+
"""Retired numeric personality pass; canonical appraisal worker owns this.
|
|
181
|
+
|
|
182
|
+
Retain the method for older host callers during a paired upgrade. It
|
|
183
|
+
makes no model call and never creates count-derived trait certainty.
|
|
184
|
+
"""
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def _can_extract(self, contact_id: str, *, operation: str = "affect") -> bool:
|
|
188
|
+
"""Check the operation's per-contact throttle."""
|
|
189
|
+
timestamps = self._last_facts if operation == "facts" else self._last_extraction
|
|
190
|
+
last = timestamps.get(contact_id)
|
|
191
|
+
if last is None:
|
|
192
|
+
return True
|
|
193
|
+
try:
|
|
194
|
+
last_dt = datetime.fromisoformat(last)
|
|
195
|
+
elapsed = (datetime.now(timezone.utc) - last_dt).total_seconds() / 60
|
|
196
|
+
return elapsed >= THROTTLE_MINUTES
|
|
197
|
+
except (ValueError, TypeError):
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
def _mark_extracted(self, contact_id: str, *, operation: str = "affect") -> None:
|
|
201
|
+
timestamps = self._last_facts if operation == "facts" else self._last_extraction
|
|
202
|
+
timestamps[contact_id] = datetime.now(timezone.utc).isoformat()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Parsing helpers
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
_CODE_FENCE = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _strip_code_fence(text: str) -> str:
|
|
213
|
+
return _CODE_FENCE.sub("", text)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _parse_affect_json(raw: str) -> Optional[Dict[str, Any]]:
|
|
217
|
+
"""Parse LLM response into affect dict."""
|
|
218
|
+
if not raw:
|
|
219
|
+
return None
|
|
220
|
+
candidate = _strip_code_fence(raw).strip()
|
|
221
|
+
try:
|
|
222
|
+
parsed = json.loads(candidate)
|
|
223
|
+
except json.JSONDecodeError:
|
|
224
|
+
# Try to find JSON object in response
|
|
225
|
+
match = re.search(r"\{.*\}", candidate, re.DOTALL)
|
|
226
|
+
if not match:
|
|
227
|
+
return None
|
|
228
|
+
try:
|
|
229
|
+
parsed = json.loads(match.group(0))
|
|
230
|
+
except json.JSONDecodeError:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
if not isinstance(parsed, dict):
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
valence = parsed.get("valence", 0.0)
|
|
237
|
+
arousal = parsed.get("arousal", 0.3)
|
|
238
|
+
confidence = parsed.get("confidence", 0.5)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
valence = max(-1.0, min(1.0, float(valence)))
|
|
242
|
+
arousal = max(0.0, min(1.0, float(arousal)))
|
|
243
|
+
confidence = max(0.0, min(1.0, float(confidence)))
|
|
244
|
+
except (TypeError, ValueError):
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
# Skip neutral readings — not worth storing
|
|
248
|
+
if abs(valence) < 0.1 and abs(arousal - 0.3) < 0.1:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"valence": valence,
|
|
253
|
+
"arousal": arousal,
|
|
254
|
+
"trigger": parsed.get("trigger"),
|
|
255
|
+
"confidence": confidence,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _parse_fact_array(raw: str, *, conversation_text: str = "") -> List[Dict[str, Any]]:
|
|
260
|
+
"""Accept useful quotations; unsupported output remains conversation history."""
|
|
261
|
+
if not raw:
|
|
262
|
+
return []
|
|
263
|
+
candidate = _strip_code_fence(raw).strip()
|
|
264
|
+
try:
|
|
265
|
+
parsed = json.loads(candidate)
|
|
266
|
+
except json.JSONDecodeError:
|
|
267
|
+
match = re.search(r"\[.*\]", candidate, re.DOTALL)
|
|
268
|
+
if not match:
|
|
269
|
+
return []
|
|
270
|
+
try:
|
|
271
|
+
parsed = json.loads(match.group(0))
|
|
272
|
+
except json.JSONDecodeError:
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
if isinstance(parsed, dict):
|
|
276
|
+
parsed = parsed.get("facts") or parsed.get("items") or []
|
|
277
|
+
if not isinstance(parsed, list) or len(parsed) > 6:
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
facts: List[Dict[str, Any]] = []
|
|
281
|
+
for item in parsed:
|
|
282
|
+
if not isinstance(item, dict):
|
|
283
|
+
continue
|
|
284
|
+
fact_text = item.get("evidence")
|
|
285
|
+
quality = promotion_metadata(item)
|
|
286
|
+
if (not isinstance(fact_text, str) or not fact_text.strip()
|
|
287
|
+
or len(fact_text) > 500 or fact_text not in conversation_text or quality is None):
|
|
288
|
+
continue
|
|
289
|
+
source = item.get("source", "inferred")
|
|
290
|
+
if source not in ("told_by_contact", "told_to_contact", "shared_context", "inferred"):
|
|
291
|
+
source = "inferred"
|
|
292
|
+
confidence = item.get("confidence", 0.7)
|
|
293
|
+
try:
|
|
294
|
+
confidence = max(0.0, min(1.0, float(confidence)))
|
|
295
|
+
except (TypeError, ValueError):
|
|
296
|
+
confidence = 0.7
|
|
297
|
+
facts.append({
|
|
298
|
+
"fact": fact_text.strip(),
|
|
299
|
+
"source": source,
|
|
300
|
+
"confidence": confidence,
|
|
301
|
+
"memory_quality": quality,
|
|
302
|
+
})
|
|
303
|
+
unique = {}
|
|
304
|
+
for fact in facts:
|
|
305
|
+
unique.setdefault((fact['source'], fact['fact']), fact)
|
|
306
|
+
return list(unique.values())
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Pure P8 fact adapters with an explicit server-authority boundary.
|
|
2
|
+
|
|
3
|
+
Models and request bodies may propose only fact content and confidence. Fact
|
|
4
|
+
identity, source, subject, audience, shareability, evidence, and freshness are
|
|
5
|
+
accepted solely through the typed ``ServerFactAuthorityV1`` input. Unknown
|
|
6
|
+
untrusted fields are rejected rather than silently ignored so a future caller
|
|
7
|
+
cannot accidentally make a body/model field authoritative during refactoring.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any, Mapping
|
|
16
|
+
|
|
17
|
+
from apsimo.tom.visibility import (
|
|
18
|
+
FactCandidateV1,
|
|
19
|
+
FactVisibilityV1,
|
|
20
|
+
content_digest,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FactAuthorityBoundaryError(ValueError):
|
|
25
|
+
"""Untrusted input attempted to cross the fact-authority boundary."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _iso(value: datetime | str, *, field: str) -> str:
|
|
29
|
+
if isinstance(value, datetime):
|
|
30
|
+
parsed = value
|
|
31
|
+
else:
|
|
32
|
+
try:
|
|
33
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
34
|
+
except (TypeError, ValueError) as exc:
|
|
35
|
+
raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc
|
|
36
|
+
if parsed.tzinfo is None:
|
|
37
|
+
raise ValueError(f"{field} must include a timezone")
|
|
38
|
+
return parsed.astimezone(timezone.utc).isoformat()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class FactPayloadV1:
|
|
43
|
+
"""The complete set of fields an untrusted fact producer may propose."""
|
|
44
|
+
|
|
45
|
+
content: str
|
|
46
|
+
confidence: float
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
if not isinstance(self.content, str) or not self.content.strip():
|
|
50
|
+
raise ValueError("fact content is required")
|
|
51
|
+
if isinstance(self.confidence, bool):
|
|
52
|
+
raise ValueError("fact confidence must be finite and between zero and one")
|
|
53
|
+
try:
|
|
54
|
+
confidence = float(self.confidence)
|
|
55
|
+
except (TypeError, ValueError) as exc:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"fact confidence must be finite and between zero and one") from exc
|
|
58
|
+
if not math.isfinite(confidence) or not 0.0 <= confidence <= 1.0:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"fact confidence must be finite and between zero and one")
|
|
61
|
+
object.__setattr__(self, "confidence", confidence)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_untrusted(
|
|
65
|
+
cls,
|
|
66
|
+
fields: Mapping[str, Any],
|
|
67
|
+
*,
|
|
68
|
+
origin: str,
|
|
69
|
+
) -> "FactPayloadV1":
|
|
70
|
+
"""Whitelist content/confidence and reject every other input field."""
|
|
71
|
+
|
|
72
|
+
label = str(origin or "untrusted").strip().lower() or "untrusted"
|
|
73
|
+
if not isinstance(fields, Mapping):
|
|
74
|
+
raise FactAuthorityBoundaryError(
|
|
75
|
+
f"{label} fact payload must be a structured mapping")
|
|
76
|
+
allowed = {"content", "confidence"}
|
|
77
|
+
rejected = sorted(str(key) for key in fields if key not in allowed)
|
|
78
|
+
if rejected:
|
|
79
|
+
raise FactAuthorityBoundaryError(
|
|
80
|
+
f"{label} field {rejected[0]} cannot supply fact authority")
|
|
81
|
+
missing = sorted(allowed.difference(fields))
|
|
82
|
+
if missing:
|
|
83
|
+
raise ValueError(f"{label} fact payload is missing {missing[0]}")
|
|
84
|
+
return cls(
|
|
85
|
+
content=fields["content"],
|
|
86
|
+
confidence=fields["confidence"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class ServerFactAuthorityV1:
|
|
92
|
+
"""Structured authority derived from authenticated server-side state."""
|
|
93
|
+
|
|
94
|
+
fact_ref: str
|
|
95
|
+
source_ref: str
|
|
96
|
+
subject_person_id: str
|
|
97
|
+
viewer_scope: str
|
|
98
|
+
shareability: str
|
|
99
|
+
observed_at: datetime | str
|
|
100
|
+
fresh_until: datetime | str
|
|
101
|
+
evidence_refs: tuple[str, ...]
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
object.__setattr__(
|
|
105
|
+
self, "observed_at", _iso(self.observed_at, field="observed_at"))
|
|
106
|
+
object.__setattr__(
|
|
107
|
+
self, "fresh_until", _iso(self.fresh_until, field="fresh_until"))
|
|
108
|
+
object.__setattr__(self, "evidence_refs", tuple(self.evidence_refs))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_fact_candidate(
|
|
112
|
+
*,
|
|
113
|
+
authority: ServerFactAuthorityV1,
|
|
114
|
+
payload: FactPayloadV1,
|
|
115
|
+
) -> FactCandidateV1:
|
|
116
|
+
"""Combine typed server authority with a non-authoritative fact payload."""
|
|
117
|
+
|
|
118
|
+
if not isinstance(authority, ServerFactAuthorityV1):
|
|
119
|
+
raise FactAuthorityBoundaryError(
|
|
120
|
+
"fact visibility authority must be a server-derived structured input")
|
|
121
|
+
if not isinstance(payload, FactPayloadV1):
|
|
122
|
+
raise FactAuthorityBoundaryError(
|
|
123
|
+
"fact content must be a bounded FactPayloadV1 input")
|
|
124
|
+
visibility = FactVisibilityV1(
|
|
125
|
+
fact_ref=authority.fact_ref,
|
|
126
|
+
content_digest=content_digest(payload.content),
|
|
127
|
+
source_ref=authority.source_ref,
|
|
128
|
+
subject_person_id=authority.subject_person_id,
|
|
129
|
+
viewer_scope=authority.viewer_scope,
|
|
130
|
+
shareability=authority.shareability,
|
|
131
|
+
confidence=payload.confidence,
|
|
132
|
+
observed_at=authority.observed_at,
|
|
133
|
+
fresh_until=authority.fresh_until,
|
|
134
|
+
evidence_refs=authority.evidence_refs,
|
|
135
|
+
)
|
|
136
|
+
return FactCandidateV1(content=payload.content, visibility=visibility)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"FactAuthorityBoundaryError",
|
|
141
|
+
"FactPayloadV1",
|
|
142
|
+
"ServerFactAuthorityV1",
|
|
143
|
+
"build_fact_candidate",
|
|
144
|
+
]
|