apsimo 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""ChannelRegistry — resolves per-person delivery channels from multiple sources.
|
|
2
|
+
|
|
3
|
+
Resolution priority (highest first):
|
|
4
|
+
1. Environment variables (COLONY_CHANNEL_*)
|
|
5
|
+
2. JSON config file ({COLONY_STATE_DIR}/data/channels.json)
|
|
6
|
+
3. Contact handles (phone → chat platform DM inference, configurable mapping)
|
|
7
|
+
4. Home channel fallback (WHATSAPP_HOME_CHANNEL, TELEGRAM_HOME_CHANNEL, etc.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Channel:
|
|
26
|
+
"""A resolved delivery channel."""
|
|
27
|
+
|
|
28
|
+
platform: str # "whatsapp", "telegram", "discord", ...
|
|
29
|
+
chat_id: str # platform-specific chat identifier
|
|
30
|
+
channel_type: str # "dm" | "home" | "work" | "custom"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ChannelRegistry:
|
|
34
|
+
"""Resolves per-person delivery channels from multiple sources."""
|
|
35
|
+
|
|
36
|
+
# Default gateway-to-platform mapping (override via COLONY_CHANNEL_GATEWAY_MAP)
|
|
37
|
+
DEFAULT_GATEWAY_MAP = {
|
|
38
|
+
"imessage": "whatsapp",
|
|
39
|
+
"sms": "whatsapp",
|
|
40
|
+
"telegram": "telegram",
|
|
41
|
+
"signal": "signal",
|
|
42
|
+
# "email" excluded — not a chat platform
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
env_channels: Dict[str, Dict[str, Channel]],
|
|
48
|
+
json_channels: Dict[str, Dict[str, Channel]],
|
|
49
|
+
fallback_channels: Dict[str, Channel],
|
|
50
|
+
handle_inference: bool,
|
|
51
|
+
gateway_map: Dict[str, str],
|
|
52
|
+
contacts_store: Optional[Any] = None,
|
|
53
|
+
owner_contact_id: Optional[str] = None,
|
|
54
|
+
channel_store: Optional[Any] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._env = env_channels
|
|
57
|
+
self._json = json_channels
|
|
58
|
+
self._fallback = fallback_channels
|
|
59
|
+
self._handle_inference = handle_inference
|
|
60
|
+
self._gateway_map = gateway_map
|
|
61
|
+
self._contacts_store = contacts_store
|
|
62
|
+
self._channel_store = channel_store
|
|
63
|
+
self._json_path_value: str = ""
|
|
64
|
+
self._owner_contact_id = self._normalize_person_id(owner_contact_id) if owner_contact_id else None
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
# Public API
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def resolve(
|
|
71
|
+
self,
|
|
72
|
+
person_id: str,
|
|
73
|
+
channel_type: str = "home",
|
|
74
|
+
) -> Optional[Channel]:
|
|
75
|
+
"""Resolve a delivery channel for ``person_id`` and ``channel_type``.
|
|
76
|
+
|
|
77
|
+
Resolution order:
|
|
78
|
+
1. Env vars (COLONY_CHANNEL_DM_* / COLONY_CHANNEL_HOME_*)
|
|
79
|
+
2. JSON config (contacts section)
|
|
80
|
+
3. JSON fallback
|
|
81
|
+
4. Contact handle inference (DM only)
|
|
82
|
+
5. Home channel env vars (home only)
|
|
83
|
+
|
|
84
|
+
Returns ``None`` if no channel can be resolved.
|
|
85
|
+
"""
|
|
86
|
+
normalized_id = self._normalize_person_id(person_id)
|
|
87
|
+
|
|
88
|
+
# 1. Env vars (highest priority)
|
|
89
|
+
env_person = self._env.get(normalized_id, {})
|
|
90
|
+
if channel_type in env_person:
|
|
91
|
+
return env_person[channel_type]
|
|
92
|
+
|
|
93
|
+
# 1b. Owner alias — if person_id matches owner contact ID, also check "owner"
|
|
94
|
+
if self._owner_contact_id and normalized_id == self._owner_contact_id:
|
|
95
|
+
env_owner = self._env.get("owner", {})
|
|
96
|
+
if channel_type in env_owner:
|
|
97
|
+
return env_owner[channel_type]
|
|
98
|
+
|
|
99
|
+
# 2. JSON config (contacts section)
|
|
100
|
+
json_person = self._json.get(normalized_id, {})
|
|
101
|
+
if channel_type in json_person:
|
|
102
|
+
return json_person[channel_type]
|
|
103
|
+
|
|
104
|
+
# 2b. Owner alias for JSON
|
|
105
|
+
if self._owner_contact_id and normalized_id == self._owner_contact_id:
|
|
106
|
+
json_owner = self._json.get("owner", {})
|
|
107
|
+
if channel_type in json_owner:
|
|
108
|
+
return json_owner[channel_type]
|
|
109
|
+
|
|
110
|
+
# 3. JSON fallback
|
|
111
|
+
if channel_type in self._fallback:
|
|
112
|
+
return self._fallback[channel_type]
|
|
113
|
+
|
|
114
|
+
# 4. Contact handle inference (DM only)
|
|
115
|
+
if channel_type == "dm" and self._handle_inference and self._contacts_store:
|
|
116
|
+
inferred = self._infer_from_handles(normalized_id)
|
|
117
|
+
if inferred:
|
|
118
|
+
return inferred
|
|
119
|
+
|
|
120
|
+
# 5. Registered channels (dynamic): a live registered channel that declares a
|
|
121
|
+
# home_chat_id can serve as the home route. Freshest last_seen_at wins, so
|
|
122
|
+
# routing follows the surface that is actually alive. Explicit env/JSON
|
|
123
|
+
# config above still outranks this (operator intent beats observation).
|
|
124
|
+
if channel_type == "home":
|
|
125
|
+
store_home = self._resolve_home_from_store()
|
|
126
|
+
if store_home:
|
|
127
|
+
return store_home
|
|
128
|
+
|
|
129
|
+
# 6. Home channel env vars (home only, lowest priority)
|
|
130
|
+
if channel_type == "home":
|
|
131
|
+
home = self._resolve_home_from_env()
|
|
132
|
+
if home:
|
|
133
|
+
return home
|
|
134
|
+
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
async def resolve_exact_verified_dm(
|
|
138
|
+
self,
|
|
139
|
+
person_id: str,
|
|
140
|
+
*,
|
|
141
|
+
platform: str,
|
|
142
|
+
) -> Optional[Channel]:
|
|
143
|
+
"""Resolve one canonical contact's exact verified platform handle.
|
|
144
|
+
|
|
145
|
+
This is the asynchronous, fail-closed path for a delivery boundary
|
|
146
|
+
that must address a specific contact. Unlike legacy handle inference,
|
|
147
|
+
it does not map a phone-bearing handle from one gateway to another and
|
|
148
|
+
it never falls back to a home/shared channel. Authority is deliberately
|
|
149
|
+
out of scope: the caller's policy boundary must still decide whether a
|
|
150
|
+
message may be sent.
|
|
151
|
+
"""
|
|
152
|
+
if self._contacts_store is None:
|
|
153
|
+
return None
|
|
154
|
+
canonical_id = str(person_id or "").strip()
|
|
155
|
+
exact_platform = str(platform or "").strip().lower()
|
|
156
|
+
if not canonical_id or not exact_platform:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
get_contact = getattr(self._contacts_store, "get", None)
|
|
160
|
+
get_handles = getattr(self._contacts_store, "get_handles", None)
|
|
161
|
+
if not callable(get_contact) or not callable(get_handles):
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
contact = get_contact(canonical_id)
|
|
165
|
+
if inspect.isawaitable(contact):
|
|
166
|
+
contact = await contact
|
|
167
|
+
if (
|
|
168
|
+
contact is None
|
|
169
|
+
or str(getattr(contact, "contact_id", "")) != canonical_id
|
|
170
|
+
or getattr(contact, "deleted_at", None)
|
|
171
|
+
):
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
handles = get_handles(canonical_id)
|
|
175
|
+
if inspect.isawaitable(handles):
|
|
176
|
+
handles = await handles
|
|
177
|
+
except Exception:
|
|
178
|
+
logger.debug(
|
|
179
|
+
"Exact verified DM resolution failed for %s",
|
|
180
|
+
canonical_id,
|
|
181
|
+
exc_info=True,
|
|
182
|
+
)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
candidates: set[str] = set()
|
|
186
|
+
for handle in handles or ():
|
|
187
|
+
handle_contact = getattr(handle, "contact_id", canonical_id)
|
|
188
|
+
if str(handle_contact) != canonical_id:
|
|
189
|
+
logger.warning(
|
|
190
|
+
"Exact DM resolution rejected a cross-contact handle record"
|
|
191
|
+
)
|
|
192
|
+
return None
|
|
193
|
+
gateway = str(getattr(handle, "gateway", "") or "").strip().lower()
|
|
194
|
+
if gateway != exact_platform or getattr(handle, "verified", None) is not True:
|
|
195
|
+
continue
|
|
196
|
+
address = getattr(handle, "address", None)
|
|
197
|
+
if not isinstance(address, str) or not address:
|
|
198
|
+
continue
|
|
199
|
+
candidates.add(address)
|
|
200
|
+
|
|
201
|
+
if len(candidates) != 1:
|
|
202
|
+
if candidates:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"Exact verified DM resolution is ambiguous for contact %s on %s",
|
|
205
|
+
canonical_id,
|
|
206
|
+
exact_platform,
|
|
207
|
+
)
|
|
208
|
+
return None
|
|
209
|
+
return Channel(
|
|
210
|
+
platform=exact_platform,
|
|
211
|
+
chat_id=next(iter(candidates)),
|
|
212
|
+
channel_type="dm",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _resolve_home_from_store(self) -> Optional[Channel]:
|
|
216
|
+
"""Home route from the channel registration DB (Framework Part 4)."""
|
|
217
|
+
if self._channel_store is None:
|
|
218
|
+
return None
|
|
219
|
+
try:
|
|
220
|
+
candidates = [
|
|
221
|
+
ch for ch in self._channel_store.list_active()
|
|
222
|
+
if getattr(ch.manifest, "home_chat_id", None)
|
|
223
|
+
]
|
|
224
|
+
if not candidates:
|
|
225
|
+
return None
|
|
226
|
+
candidates.sort(key=lambda c: c.last_seen_at or c.registered_at, reverse=True)
|
|
227
|
+
best = candidates[0]
|
|
228
|
+
platform = best.manifest.platform_hint or best.gateway_family
|
|
229
|
+
return Channel(
|
|
230
|
+
platform=platform,
|
|
231
|
+
chat_id=best.manifest.home_chat_id,
|
|
232
|
+
channel_type="home",
|
|
233
|
+
)
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.debug("channel-store home resolution failed", exc_info=True)
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def reload(self) -> None:
|
|
239
|
+
"""Re-read env vars and JSON config without restarting the sidecar.
|
|
240
|
+
|
|
241
|
+
This is a partial reload — contact handles are not re-fetched.
|
|
242
|
+
"""
|
|
243
|
+
env_prefix = "COLONY_CHANNEL_"
|
|
244
|
+
self._env, self._json, self._fallback = self._load_sources(
|
|
245
|
+
json_path=self._json_path(),
|
|
246
|
+
env_prefix=env_prefix,
|
|
247
|
+
)
|
|
248
|
+
raw = os.environ.get("COLONY_OWNER_CONTACT_ID", "")
|
|
249
|
+
self._owner_contact_id = self._normalize_person_id(raw) if raw else None
|
|
250
|
+
logger.info("ChannelRegistry reloaded")
|
|
251
|
+
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
# Class methods
|
|
254
|
+
# ------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def load(
|
|
258
|
+
cls,
|
|
259
|
+
json_path: Optional[str] = None,
|
|
260
|
+
env_prefix: str = "COLONY_CHANNEL_",
|
|
261
|
+
handle_inference: Optional[bool] = None,
|
|
262
|
+
gateway_map: Optional[Dict[str, str]] = None,
|
|
263
|
+
contacts_store: Optional[Any] = None,
|
|
264
|
+
channel_store: Optional[Any] = None,
|
|
265
|
+
) -> "ChannelRegistry":
|
|
266
|
+
"""Load a ChannelRegistry from all configured sources.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
json_path: Path to channels.json. Defaults to {COLONY_STATE_DIR}/data/channels.json.
|
|
270
|
+
env_prefix: Prefix for env var scanning.
|
|
271
|
+
handle_inference: Enable contact handle inference. Defaults to env var COLONY_CHANNEL_INFER_FROM_HANDLES.
|
|
272
|
+
gateway_map: Override gateway-to-platform mapping. Defaults to env var COLONY_CHANNEL_GATEWAY_MAP.
|
|
273
|
+
contacts_store: Optional contact store for handle inference.
|
|
274
|
+
"""
|
|
275
|
+
if json_path is None:
|
|
276
|
+
state_dir = os.environ.get("COLONY_STATE_DIR", str(Path.home() / ".colony"))
|
|
277
|
+
json_path = os.path.join(state_dir, "data", "channels.json")
|
|
278
|
+
|
|
279
|
+
if handle_inference is None:
|
|
280
|
+
handle_inference = os.environ.get("COLONY_CHANNEL_INFER_FROM_HANDLES", "true").lower() == "true"
|
|
281
|
+
|
|
282
|
+
if gateway_map is None:
|
|
283
|
+
raw_map = os.environ.get("COLONY_CHANNEL_GATEWAY_MAP", "")
|
|
284
|
+
if raw_map:
|
|
285
|
+
try:
|
|
286
|
+
gateway_map = json.loads(raw_map)
|
|
287
|
+
except json.JSONDecodeError:
|
|
288
|
+
logger.warning("Invalid COLONY_CHANNEL_GATEWAY_MAP JSON, using defaults")
|
|
289
|
+
gateway_map = dict(cls.DEFAULT_GATEWAY_MAP)
|
|
290
|
+
else:
|
|
291
|
+
gateway_map = dict(cls.DEFAULT_GATEWAY_MAP)
|
|
292
|
+
|
|
293
|
+
# Normalize gateway_map
|
|
294
|
+
if gateway_map is None:
|
|
295
|
+
gateway_map = dict(cls.DEFAULT_GATEWAY_MAP)
|
|
296
|
+
|
|
297
|
+
env_channels, json_channels, fallback = cls._load_sources(json_path, env_prefix)
|
|
298
|
+
|
|
299
|
+
owner_contact_id = os.environ.get("COLONY_OWNER_CONTACT_ID", "")
|
|
300
|
+
|
|
301
|
+
registry = cls(
|
|
302
|
+
env_channels=env_channels,
|
|
303
|
+
json_channels=json_channels,
|
|
304
|
+
fallback_channels=fallback,
|
|
305
|
+
handle_inference=handle_inference,
|
|
306
|
+
gateway_map=gateway_map,
|
|
307
|
+
contacts_store=contacts_store,
|
|
308
|
+
owner_contact_id=owner_contact_id or None,
|
|
309
|
+
channel_store=channel_store,
|
|
310
|
+
)
|
|
311
|
+
registry._json_path_value = json_path # stash for reload()
|
|
312
|
+
|
|
313
|
+
logger.info(
|
|
314
|
+
"ChannelRegistry loaded: %d env entries, %d JSON entries, inference=%s",
|
|
315
|
+
sum(len(v) for v in env_channels.values()),
|
|
316
|
+
sum(len(v) for v in json_channels.values()),
|
|
317
|
+
handle_inference,
|
|
318
|
+
)
|
|
319
|
+
return registry
|
|
320
|
+
|
|
321
|
+
# ------------------------------------------------------------------
|
|
322
|
+
# Private helpers
|
|
323
|
+
# ------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def _normalize_person_id(person_id: str) -> str:
|
|
327
|
+
"""Normalize a person ID: lowercase, spaces → underscores."""
|
|
328
|
+
return person_id.lower().strip().replace(" ", "_")
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def _load_sources(
|
|
332
|
+
cls,
|
|
333
|
+
json_path: str,
|
|
334
|
+
env_prefix: str,
|
|
335
|
+
) -> tuple[Dict[str, Dict[str, Channel]], Dict[str, Dict[str, Channel]], Dict[str, Channel]]:
|
|
336
|
+
"""Load channels from env vars and JSON file."""
|
|
337
|
+
env_channels = cls._load_from_env(env_prefix)
|
|
338
|
+
json_channels, fallback = cls._load_from_json(json_path)
|
|
339
|
+
return env_channels, json_channels, fallback
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def _load_from_env(
|
|
343
|
+
cls,
|
|
344
|
+
prefix: str,
|
|
345
|
+
) -> Dict[str, Dict[str, Channel]]:
|
|
346
|
+
"""Scan env vars matching COLONY_CHANNEL_DM_* and COLONY_CHANNEL_HOME_*."""
|
|
347
|
+
channels: Dict[str, Dict[str, Channel]] = {}
|
|
348
|
+
dm_prefix = f"{prefix}DM_"
|
|
349
|
+
home_prefix = f"{prefix}HOME_"
|
|
350
|
+
|
|
351
|
+
for key, value in os.environ.items():
|
|
352
|
+
person_id: Optional[str] = None
|
|
353
|
+
channel_type: Optional[str] = None
|
|
354
|
+
|
|
355
|
+
if key.startswith(dm_prefix):
|
|
356
|
+
person_id = key[len(dm_prefix):]
|
|
357
|
+
channel_type = "dm"
|
|
358
|
+
elif key.startswith(home_prefix):
|
|
359
|
+
# HOME_ could be global (HOME) or per-person (HOME_owner)
|
|
360
|
+
remainder = key[len(home_prefix):]
|
|
361
|
+
if remainder:
|
|
362
|
+
person_id = remainder
|
|
363
|
+
else:
|
|
364
|
+
person_id = "__global__"
|
|
365
|
+
channel_type = "home"
|
|
366
|
+
elif key == f"{prefix}HOME":
|
|
367
|
+
person_id = "__global__"
|
|
368
|
+
channel_type = "home"
|
|
369
|
+
else:
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
if not value:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
platform, chat_id = cls._parse_channel_value(value)
|
|
376
|
+
if platform and chat_id:
|
|
377
|
+
normalized_id = cls._normalize_person_id(person_id)
|
|
378
|
+
if normalized_id not in channels:
|
|
379
|
+
channels[normalized_id] = {}
|
|
380
|
+
channels[normalized_id][channel_type] = Channel(
|
|
381
|
+
platform=platform,
|
|
382
|
+
chat_id=chat_id,
|
|
383
|
+
channel_type=channel_type,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return channels
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def _load_from_json(
|
|
390
|
+
cls,
|
|
391
|
+
path: str,
|
|
392
|
+
) -> tuple[Dict[str, Dict[str, Channel]], Dict[str, Channel]]:
|
|
393
|
+
"""Load channels from JSON config file."""
|
|
394
|
+
channels: Dict[str, Dict[str, Channel]] = {}
|
|
395
|
+
fallback: Dict[str, Channel] = {}
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
if not Path(path).exists():
|
|
399
|
+
return channels, fallback
|
|
400
|
+
with open(path, "r") as f:
|
|
401
|
+
data = json.load(f)
|
|
402
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
403
|
+
logger.warning("Failed to load channels.json: %s", exc)
|
|
404
|
+
return channels, fallback
|
|
405
|
+
|
|
406
|
+
# contacts section
|
|
407
|
+
for person_id, person_channels in data.get("contacts", {}).items():
|
|
408
|
+
normalized_id = cls._normalize_person_id(person_id)
|
|
409
|
+
channels[normalized_id] = {}
|
|
410
|
+
for channel_type, info in person_channels.items():
|
|
411
|
+
platform = info.get("platform", "")
|
|
412
|
+
chat_id = info.get("chat_id", "")
|
|
413
|
+
if platform and chat_id:
|
|
414
|
+
channels[normalized_id][channel_type] = Channel(
|
|
415
|
+
platform=platform,
|
|
416
|
+
chat_id=chat_id,
|
|
417
|
+
channel_type=channel_type,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# fallback section
|
|
421
|
+
for channel_type, info in data.get("fallback", {}).items():
|
|
422
|
+
platform = info.get("platform", "")
|
|
423
|
+
chat_id = info.get("chat_id", "")
|
|
424
|
+
if platform and chat_id:
|
|
425
|
+
fallback[channel_type] = Channel(
|
|
426
|
+
platform=platform,
|
|
427
|
+
chat_id=chat_id,
|
|
428
|
+
channel_type=channel_type,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return channels, fallback
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def _parse_channel_value(cls, value: str) -> tuple[Optional[str], Optional[str]]:
|
|
435
|
+
"""Parse a channel value like 'telegram:@username' or 'whatsapp:+1555...'."""
|
|
436
|
+
if not value:
|
|
437
|
+
return None, None
|
|
438
|
+
if ":" in value:
|
|
439
|
+
platform, chat_id = value.split(":", 1)
|
|
440
|
+
return platform.strip().lower(), chat_id.strip()
|
|
441
|
+
# If no platform prefix, assume it's a chat ID and platform is unknown
|
|
442
|
+
return None, value.strip()
|
|
443
|
+
|
|
444
|
+
def _resolve_home_from_env(self) -> Optional[Channel]:
|
|
445
|
+
"""Scan for {PLATFORM}_HOME_CHANNEL env vars."""
|
|
446
|
+
pattern = re.compile(r"^(\w+)_HOME_CHANNEL$")
|
|
447
|
+
for key in sorted(os.environ.keys()):
|
|
448
|
+
match = pattern.match(key)
|
|
449
|
+
if match:
|
|
450
|
+
platform = match.group(1).lower()
|
|
451
|
+
chat_id = os.environ[key]
|
|
452
|
+
if chat_id:
|
|
453
|
+
return Channel(
|
|
454
|
+
platform=platform,
|
|
455
|
+
chat_id=chat_id,
|
|
456
|
+
channel_type="home",
|
|
457
|
+
)
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
def _infer_from_handles(self, person_id: str) -> Optional[Channel]:
|
|
461
|
+
"""Infer DM channel from contact handles.
|
|
462
|
+
|
|
463
|
+
Gracefully skips when the store's ``get_handles`` is async and we're
|
|
464
|
+
called from a synchronous ``resolve()`` context (e.g. inside an
|
|
465
|
+
already-running event loop). In those cases env-vars / JSON config
|
|
466
|
+
should already have covered the owner.
|
|
467
|
+
"""
|
|
468
|
+
if self._contacts_store is None:
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
method = getattr(self._contacts_store, "get_handles", None)
|
|
472
|
+
if method is None:
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
# Avoid creating an unawaited coroutine object — check asyncness first
|
|
476
|
+
if inspect.iscoroutinefunction(method):
|
|
477
|
+
logger.debug(
|
|
478
|
+
"Skipping handle inference for %s — async store called from sync context",
|
|
479
|
+
person_id,
|
|
480
|
+
)
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
handles = method(person_id)
|
|
485
|
+
except Exception:
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
if not handles:
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
for handle in handles:
|
|
492
|
+
gateway = getattr(handle, "gateway", "")
|
|
493
|
+
address = getattr(handle, "address", "")
|
|
494
|
+
if not gateway or not address:
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
platform = self._gateway_map.get(gateway)
|
|
498
|
+
if not platform:
|
|
499
|
+
continue
|
|
500
|
+
|
|
501
|
+
# Normalize phone numbers
|
|
502
|
+
if platform in ("whatsapp", "signal"):
|
|
503
|
+
address = self._normalize_phone(address)
|
|
504
|
+
|
|
505
|
+
return Channel(
|
|
506
|
+
platform=platform,
|
|
507
|
+
chat_id=address,
|
|
508
|
+
channel_type="dm",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def _normalize_phone(address: str) -> str:
|
|
515
|
+
"""Normalize a phone number address."""
|
|
516
|
+
# Strip spaces, ensure leading +
|
|
517
|
+
cleaned = address.strip().replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
|
|
518
|
+
if cleaned.startswith("++"):
|
|
519
|
+
cleaned = cleaned[1:]
|
|
520
|
+
if not cleaned.startswith("+") and cleaned.isdigit():
|
|
521
|
+
cleaned = "+" + cleaned
|
|
522
|
+
return cleaned
|
|
523
|
+
|
|
524
|
+
def _json_path(self) -> str:
|
|
525
|
+
"""Return the JSON config path (stashed during load)."""
|
|
526
|
+
return getattr(self, "_json_path_value", "")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Initiative delivery classification.
|
|
2
|
+
|
|
3
|
+
Splits initiative *types* into two disjoint routing classes:
|
|
4
|
+
|
|
5
|
+
* **reach-out** -- user-facing initiatives whose natural outcome is a message
|
|
6
|
+
to a person (a follow-up, a relationship touch, an introduction...). These
|
|
7
|
+
are delivered through the guarded proactive-delivery path so the host agent
|
|
8
|
+
can compose and send them in its own voice, under the delivery rate limiter
|
|
9
|
+
and approval gates.
|
|
10
|
+
* **internal** -- everything else (self-maintenance, health, research
|
|
11
|
+
bookkeeping, data quality, capability gaps...). These are processed in-place
|
|
12
|
+
by the sidecar's own execution backend and never messaged to a person.
|
|
13
|
+
|
|
14
|
+
The reach-out set is a small, conservative default and is fully overridable
|
|
15
|
+
from the environment so a deployment can widen or narrow it without a code
|
|
16
|
+
change. No deployment-specific identifiers live here.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from typing import FrozenSet
|
|
23
|
+
|
|
24
|
+
# Conservative default: types that inherently mean "contact a person".
|
|
25
|
+
# Anything not in this set is treated as internal / self-maintenance.
|
|
26
|
+
_DEFAULT_REACHOUT_TYPES: FrozenSet[str] = frozenset({
|
|
27
|
+
"follow_up",
|
|
28
|
+
"relationship",
|
|
29
|
+
"introduction",
|
|
30
|
+
"scheduling",
|
|
31
|
+
"commitment",
|
|
32
|
+
"calendar",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
# Env override: comma-separated list of initiative types. When set (non-empty)
|
|
36
|
+
# it fully replaces the default set.
|
|
37
|
+
_ENV_VAR = "COLONY_REACHOUT_TYPES"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def reachout_types() -> FrozenSet[str]:
|
|
41
|
+
"""Return the set of initiative types routed to proactive delivery."""
|
|
42
|
+
raw = os.environ.get(_ENV_VAR, "").strip()
|
|
43
|
+
if raw:
|
|
44
|
+
return frozenset(t.strip() for t in raw.split(",") if t.strip())
|
|
45
|
+
return _DEFAULT_REACHOUT_TYPES
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_reachout(initiative_type: str) -> bool:
|
|
49
|
+
"""True if this initiative type should be delivered to a person."""
|
|
50
|
+
return bool(initiative_type) and initiative_type in reachout_types()
|