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,389 @@
|
|
|
1
|
+
"""RelationshipProfiler -- per-person standing and attributed approach guidance.
|
|
2
|
+
|
|
3
|
+
The analysis half of docs/RELATIONSHIPS.md: once attribution flows (the
|
|
4
|
+
ParticipantResolver), every store already accumulates per-person signal.
|
|
5
|
+
This module composes those signals into one compact RelationshipBrief per
|
|
6
|
+
contact and derives concrete approach guidance the agent can act on
|
|
7
|
+
(preferred channel, best time to reach, engagement style, cautions).
|
|
8
|
+
|
|
9
|
+
The profiler composes source-backed stated preferences and separate contact
|
|
10
|
+
affect. Historical numeric personality and closeness fields do not govern
|
|
11
|
+
approach guidance. Briefs are cached in
|
|
12
|
+
``colony-relationships.db`` and refreshed by the autonomy phase when a
|
|
13
|
+
contact accrues new interactions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import sqlite3
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import asdict, dataclass, field
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_EXCLUDED_IDS = ("system", "default")
|
|
31
|
+
_MOOD_CAUTION = "recent mood is negative; lead carefully"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def profile_min_interactions() -> int:
|
|
35
|
+
try:
|
|
36
|
+
return max(1, int(os.environ.get(
|
|
37
|
+
"COLONY_RELATIONSHIP_PROFILE_MIN_INTERACTIONS", "5")))
|
|
38
|
+
except ValueError:
|
|
39
|
+
return 5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def approach_guidance_enabled() -> bool:
|
|
43
|
+
return os.environ.get(
|
|
44
|
+
"COLONY_APPROACH_GUIDANCE", "true").strip().lower() != "false"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RelationshipBrief:
|
|
49
|
+
contact_id: str
|
|
50
|
+
display_name: str = ""
|
|
51
|
+
trust_tier: str = "unknown"
|
|
52
|
+
interaction_count: int = 0
|
|
53
|
+
last_interaction_at: str = ""
|
|
54
|
+
relationship_score: Optional[float] = None
|
|
55
|
+
channels: Dict[str, int] = field(default_factory=dict)
|
|
56
|
+
preferred_channel: str = ""
|
|
57
|
+
best_hours_local: List[int] = field(default_factory=list)
|
|
58
|
+
timezone: str = ""
|
|
59
|
+
affect_valence: Optional[float] = None
|
|
60
|
+
affect_trend: str = ""
|
|
61
|
+
psyche_guidance: List[str] = field(default_factory=list)
|
|
62
|
+
psyche_motivators: List[str] = field(default_factory=list)
|
|
63
|
+
rapport_topics: List[str] = field(default_factory=list)
|
|
64
|
+
rapport_projection_digest: str = ""
|
|
65
|
+
cautions: List[str] = field(default_factory=list)
|
|
66
|
+
profiled_at: float = 0.0
|
|
67
|
+
interactions_at_profile: int = 0
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
70
|
+
return asdict(self)
|
|
71
|
+
|
|
72
|
+
def render(self, *, include_approach: bool = True, include_affect: bool = True) -> str:
|
|
73
|
+
"""Compact prompt-injectable text form."""
|
|
74
|
+
lines: List[str] = []
|
|
75
|
+
head = f"{self.display_name or self.contact_id} ({self.trust_tier})"
|
|
76
|
+
stats = f"{self.interaction_count} interactions"
|
|
77
|
+
if self.last_interaction_at:
|
|
78
|
+
stats += f", last {self.last_interaction_at[:10]}"
|
|
79
|
+
if self.preferred_channel:
|
|
80
|
+
stats += f", mostly via {self.preferred_channel}"
|
|
81
|
+
lines.append(f"{head}: {stats}.")
|
|
82
|
+
if include_affect and (self.affect_trend or self.affect_valence is not None):
|
|
83
|
+
mood = []
|
|
84
|
+
if self.affect_valence is not None:
|
|
85
|
+
mood.append(f"valence {self.affect_valence:+.2f}")
|
|
86
|
+
if self.affect_trend:
|
|
87
|
+
mood.append(self.affect_trend)
|
|
88
|
+
lines.append("Recent mood: " + ", ".join(mood) + ".")
|
|
89
|
+
if self.rapport_topics:
|
|
90
|
+
lines.append("Rapport topics: " + ", ".join(self.rapport_topics[:5]) + ".")
|
|
91
|
+
if include_approach and approach_guidance_enabled():
|
|
92
|
+
if self.psyche_guidance:
|
|
93
|
+
lines.append("Approach: " + " ".join(self.psyche_guidance[:4]))
|
|
94
|
+
if self.psyche_motivators:
|
|
95
|
+
lines.append("Motivated by: " + ", ".join(self.psyche_motivators[:3]) + ".")
|
|
96
|
+
if self.best_hours_local:
|
|
97
|
+
hrs = ", ".join(f"{h:02d}:00" for h in self.best_hours_local[:2])
|
|
98
|
+
tzs = f" ({self.timezone})" if self.timezone else ""
|
|
99
|
+
lines.append(f"Usually reachable around {hrs}{tzs}.")
|
|
100
|
+
for c in [item for item in self.cautions if include_affect or item != _MOOD_CAUTION][:2]:
|
|
101
|
+
lines.append("Caution: " + c)
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RelationshipProfiler:
|
|
106
|
+
def __init__(self, *, contacts_store: Any, comms_log: Any = None,
|
|
107
|
+
affect_store: Any = None, facts_store: Any = None,
|
|
108
|
+
engagement_store: Any = None,
|
|
109
|
+
p8_runtime: Any = None,
|
|
110
|
+
db_path: Optional[str] = None) -> None:
|
|
111
|
+
self._contacts = contacts_store
|
|
112
|
+
self._comms = comms_log
|
|
113
|
+
self._affect = affect_store
|
|
114
|
+
self._facts = facts_store
|
|
115
|
+
self._engagement = engagement_store
|
|
116
|
+
self._p8 = p8_runtime
|
|
117
|
+
self._lock = threading.RLock()
|
|
118
|
+
self._conn = sqlite3.connect(str(db_path) if db_path else ":memory:",
|
|
119
|
+
check_same_thread=False)
|
|
120
|
+
self._conn.row_factory = sqlite3.Row
|
|
121
|
+
with self._lock:
|
|
122
|
+
self._conn.execute(
|
|
123
|
+
"""CREATE TABLE IF NOT EXISTS relationship_briefs (
|
|
124
|
+
contact_id TEXT PRIMARY KEY,
|
|
125
|
+
brief_json TEXT NOT NULL,
|
|
126
|
+
profiled_at REAL NOT NULL,
|
|
127
|
+
interactions_at_profile INTEGER DEFAULT 0
|
|
128
|
+
)""")
|
|
129
|
+
self._conn.commit()
|
|
130
|
+
|
|
131
|
+
# -- cache --------------------------------------------------------------
|
|
132
|
+
def cached(
|
|
133
|
+
self,
|
|
134
|
+
contact_id: str,
|
|
135
|
+
*,
|
|
136
|
+
viewer: Any = None,
|
|
137
|
+
) -> Optional[RelationshipBrief]:
|
|
138
|
+
with self._lock:
|
|
139
|
+
row = self._conn.execute(
|
|
140
|
+
"SELECT brief_json FROM relationship_briefs WHERE contact_id=?",
|
|
141
|
+
(contact_id,)).fetchone()
|
|
142
|
+
if row is None:
|
|
143
|
+
return None
|
|
144
|
+
try:
|
|
145
|
+
brief = RelationshipBrief(**json.loads(row["brief_json"]))
|
|
146
|
+
self._refresh_approach(brief)
|
|
147
|
+
# Rapport topics are content-derived. Cached/autonomy reads carry
|
|
148
|
+
# no authority and therefore stay contentless. A request boundary
|
|
149
|
+
# may supply its server-sealed viewer; re-project on every such read
|
|
150
|
+
# to honor deletion, expiry, update, scope, and confidence changes.
|
|
151
|
+
if self._p8 is not None:
|
|
152
|
+
topics, digest = [], ""
|
|
153
|
+
if viewer is not None:
|
|
154
|
+
try:
|
|
155
|
+
topics, digest = self._projected_rapport(
|
|
156
|
+
brief.contact_id, viewer=viewer)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
brief.rapport_topics = topics
|
|
160
|
+
brief.rapport_projection_digest = digest
|
|
161
|
+
return brief
|
|
162
|
+
except Exception:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _save(self, brief: RelationshipBrief) -> None:
|
|
166
|
+
payload = brief.to_dict()
|
|
167
|
+
# Current source-backed projections are read at use time, including
|
|
168
|
+
# erasure reconciliation. The interaction cache does not own copies.
|
|
169
|
+
for key in ("affect_valence", "affect_trend", "psyche_guidance", "psyche_motivators"):
|
|
170
|
+
payload.pop(key, None)
|
|
171
|
+
payload["cautions"] = [c for c in payload["cautions"] if c != _MOOD_CAUTION]
|
|
172
|
+
if self._p8 is not None:
|
|
173
|
+
# The cache is an availability optimization, never an authority
|
|
174
|
+
# receipt. Persist only non-P8 profile fields.
|
|
175
|
+
payload["rapport_topics"] = []
|
|
176
|
+
payload["rapport_projection_digest"] = ""
|
|
177
|
+
with self._lock:
|
|
178
|
+
self._conn.execute(
|
|
179
|
+
"""INSERT INTO relationship_briefs
|
|
180
|
+
(contact_id, brief_json, profiled_at, interactions_at_profile)
|
|
181
|
+
VALUES (?, ?, ?, ?)
|
|
182
|
+
ON CONFLICT(contact_id) DO UPDATE SET
|
|
183
|
+
brief_json=excluded.brief_json,
|
|
184
|
+
profiled_at=excluded.profiled_at,
|
|
185
|
+
interactions_at_profile=excluded.interactions_at_profile""",
|
|
186
|
+
(brief.contact_id, json.dumps(payload),
|
|
187
|
+
brief.profiled_at, brief.interactions_at_profile))
|
|
188
|
+
self._conn.commit()
|
|
189
|
+
|
|
190
|
+
def snapshot(self) -> List[Dict[str, Any]]:
|
|
191
|
+
with self._lock:
|
|
192
|
+
rows = self._conn.execute(
|
|
193
|
+
"SELECT contact_id, profiled_at, interactions_at_profile "
|
|
194
|
+
"FROM relationship_briefs ORDER BY profiled_at DESC").fetchall()
|
|
195
|
+
return [dict(r) for r in rows]
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _rapport_topics(rows: Any) -> List[str]:
|
|
199
|
+
topics: Dict[str, int] = {}
|
|
200
|
+
for row in rows or []:
|
|
201
|
+
text = str((row or {}).get("fact", ""))
|
|
202
|
+
for word in text.split():
|
|
203
|
+
word = word.strip(".,!?;:\"'()").lower()
|
|
204
|
+
if len(word) >= 5 and word.isalpha():
|
|
205
|
+
topics[word] = topics.get(word, 0) + 1
|
|
206
|
+
return [word for word, count in sorted(
|
|
207
|
+
topics.items(), key=lambda item: -item[1]
|
|
208
|
+
) if count >= 2][:6]
|
|
209
|
+
|
|
210
|
+
def _projected_rapport(
|
|
211
|
+
self,
|
|
212
|
+
contact_id: str,
|
|
213
|
+
*,
|
|
214
|
+
viewer: Any,
|
|
215
|
+
) -> tuple[List[str], str]:
|
|
216
|
+
if viewer is None:
|
|
217
|
+
return [], ""
|
|
218
|
+
projection = self._p8.project_shared_facts(
|
|
219
|
+
viewer,
|
|
220
|
+
now=datetime.now(timezone.utc),
|
|
221
|
+
subject_person_id=contact_id,
|
|
222
|
+
max_facts=50,
|
|
223
|
+
max_total_chars=24_000,
|
|
224
|
+
)
|
|
225
|
+
rows = ({"fact": fact.content} for fact in projection.facts)
|
|
226
|
+
return self._rapport_topics(rows), projection.audit_digest
|
|
227
|
+
|
|
228
|
+
# -- profiling ------------------------------------------------------------
|
|
229
|
+
async def profile(
|
|
230
|
+
self,
|
|
231
|
+
contact_id: str,
|
|
232
|
+
*,
|
|
233
|
+
viewer: Any = None,
|
|
234
|
+
) -> Optional[RelationshipBrief]:
|
|
235
|
+
"""Compose a fresh brief for one contact (None for non-persons)."""
|
|
236
|
+
if not contact_id or contact_id in _EXCLUDED_IDS:
|
|
237
|
+
return None
|
|
238
|
+
try:
|
|
239
|
+
contact = await self._contacts.get(contact_id)
|
|
240
|
+
except Exception:
|
|
241
|
+
contact = None
|
|
242
|
+
if contact is None:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
brief = RelationshipBrief(
|
|
246
|
+
contact_id=contact_id,
|
|
247
|
+
display_name=str(contact.display_name or ""),
|
|
248
|
+
trust_tier=str(contact.trust_tier or "unknown"),
|
|
249
|
+
interaction_count=int(getattr(contact, "interaction_count", 0) or 0),
|
|
250
|
+
last_interaction_at=str(getattr(contact, "last_interaction_at", "") or ""),
|
|
251
|
+
relationship_score=None, # Legacy scalar does not describe this relationship.
|
|
252
|
+
timezone=str(getattr(contact, "timezone", "") or ""),
|
|
253
|
+
profiled_at=time.time(),
|
|
254
|
+
)
|
|
255
|
+
brief.interactions_at_profile = brief.interaction_count
|
|
256
|
+
|
|
257
|
+
# Channel usage + best hours (comms provenance).
|
|
258
|
+
if self._comms is not None:
|
|
259
|
+
try:
|
|
260
|
+
stats = self._comms.stats(contact_id)
|
|
261
|
+
brief.channels = stats.get("channels", {})
|
|
262
|
+
if brief.channels:
|
|
263
|
+
brief.preferred_channel = max(
|
|
264
|
+
brief.channels, key=brief.channels.get)
|
|
265
|
+
hours = stats.get("hours_utc") or []
|
|
266
|
+
if sum(hours) >= 5:
|
|
267
|
+
offset = self._tz_offset_hours(brief.timezone)
|
|
268
|
+
ranked = sorted(range(24), key=lambda h: -hours[h])
|
|
269
|
+
brief.best_hours_local = [
|
|
270
|
+
(h + offset) % 24 for h in ranked[:2] if hours[h] > 0]
|
|
271
|
+
except Exception:
|
|
272
|
+
logger.debug("comms stats failed for %s", contact_id,
|
|
273
|
+
exc_info=True)
|
|
274
|
+
|
|
275
|
+
self._refresh_approach(brief)
|
|
276
|
+
|
|
277
|
+
# Rapport topics from shared facts. Once P8 is attached, raw rows are
|
|
278
|
+
# never a fallback and autonomy has no implicit recipient authority.
|
|
279
|
+
# Only a caller-supplied, server-sealed viewer may project content.
|
|
280
|
+
if self._p8 is not None or self._facts is not None:
|
|
281
|
+
try:
|
|
282
|
+
if self._p8 is not None:
|
|
283
|
+
if viewer is not None:
|
|
284
|
+
topics, digest = self._projected_rapport(
|
|
285
|
+
contact_id, viewer=viewer)
|
|
286
|
+
brief.rapport_topics = topics
|
|
287
|
+
brief.rapport_projection_digest = digest
|
|
288
|
+
else:
|
|
289
|
+
facts = self._facts.list_facts(
|
|
290
|
+
contact_id=contact_id, limit=50)
|
|
291
|
+
rows = (
|
|
292
|
+
facts.get("facts", facts)
|
|
293
|
+
if isinstance(facts, dict) else facts
|
|
294
|
+
)
|
|
295
|
+
brief.rapport_topics = self._rapport_topics(rows)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
# Cadence caution: silent for much longer than their usual gap.
|
|
300
|
+
try:
|
|
301
|
+
if brief.interaction_count >= 5 and brief.last_interaction_at:
|
|
302
|
+
last = datetime.fromisoformat(
|
|
303
|
+
brief.last_interaction_at.replace("Z", "+00:00"))
|
|
304
|
+
if last.tzinfo is None:
|
|
305
|
+
last = last.replace(tzinfo=timezone.utc)
|
|
306
|
+
silent_days = (datetime.now(timezone.utc) - last).days
|
|
307
|
+
if silent_days >= 30:
|
|
308
|
+
brief.cautions.append(
|
|
309
|
+
f"no contact in {silent_days} days")
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
self._save(brief)
|
|
314
|
+
return brief
|
|
315
|
+
|
|
316
|
+
def _refresh_approach(self, brief: RelationshipBrief) -> None:
|
|
317
|
+
"""Reproject changing source-backed advice without a new interaction."""
|
|
318
|
+
# Old cache rows can contain copied guidance. Clear it before reading
|
|
319
|
+
# the current stores, so an unavailable source cannot serve old advice.
|
|
320
|
+
brief.affect_valence = None
|
|
321
|
+
brief.relationship_score = None
|
|
322
|
+
brief.affect_trend = ""
|
|
323
|
+
brief.psyche_guidance = []
|
|
324
|
+
brief.psyche_motivators = []
|
|
325
|
+
brief.cautions = [c for c in brief.cautions if c != _MOOD_CAUTION]
|
|
326
|
+
if self._affect is not None:
|
|
327
|
+
try:
|
|
328
|
+
st = self._affect.get_state(brief.contact_id) or {}
|
|
329
|
+
if st.get("event_count"):
|
|
330
|
+
valence = round(float(st.get("current_valence", 0.0)), 3)
|
|
331
|
+
trend = str(st.get("trend", "") or "")
|
|
332
|
+
brief.affect_valence, brief.affect_trend = valence, trend
|
|
333
|
+
if valence <= -0.3 or trend == "declining":
|
|
334
|
+
brief.cautions.append(_MOOD_CAUTION)
|
|
335
|
+
except Exception:
|
|
336
|
+
logger.debug("current relationship affect unavailable", exc_info=True)
|
|
337
|
+
if self._engagement is not None:
|
|
338
|
+
try:
|
|
339
|
+
from apsimo.tom.engagement import build_guidance
|
|
340
|
+
prof = self._engagement.get_profile(brief.contact_id) or {}
|
|
341
|
+
guidance = [ln.lstrip("- ").strip()
|
|
342
|
+
for ln in build_guidance(prof).splitlines() if ln.strip()][:6]
|
|
343
|
+
brief.psyche_guidance = guidance
|
|
344
|
+
except Exception:
|
|
345
|
+
logger.debug("current relationship engagement unavailable", exc_info=True)
|
|
346
|
+
|
|
347
|
+
async def refresh_due(self, *, limit: int = 20) -> Dict[str, Any]:
|
|
348
|
+
"""(Re)profile contacts that accrued enough new interactions since
|
|
349
|
+
their last profile. Called by the autonomy phase."""
|
|
350
|
+
report = {"profiled": 0, "skipped": 0, "errors": 0}
|
|
351
|
+
min_new = profile_min_interactions()
|
|
352
|
+
try:
|
|
353
|
+
contacts = await self._contacts.list(limit=500)
|
|
354
|
+
except Exception:
|
|
355
|
+
logger.debug("profiler contact list failed", exc_info=True)
|
|
356
|
+
return report
|
|
357
|
+
done = 0
|
|
358
|
+
for c in contacts or []:
|
|
359
|
+
if done >= limit:
|
|
360
|
+
break
|
|
361
|
+
cid = getattr(c, "contact_id", None)
|
|
362
|
+
n = int(getattr(c, "interaction_count", 0) or 0)
|
|
363
|
+
if not cid or cid in _EXCLUDED_IDS or n < min_new:
|
|
364
|
+
continue
|
|
365
|
+
prior = self.cached(cid)
|
|
366
|
+
if prior is not None and (
|
|
367
|
+
n - prior.interactions_at_profile) < min_new:
|
|
368
|
+
report["skipped"] += 1
|
|
369
|
+
continue
|
|
370
|
+
try:
|
|
371
|
+
if await self.profile(cid) is not None:
|
|
372
|
+
report["profiled"] += 1
|
|
373
|
+
done += 1
|
|
374
|
+
except Exception:
|
|
375
|
+
report["errors"] += 1
|
|
376
|
+
logger.debug("profile failed for %s", cid, exc_info=True)
|
|
377
|
+
return report
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _tz_offset_hours(tz_name: str) -> int:
|
|
381
|
+
if not tz_name:
|
|
382
|
+
return 0
|
|
383
|
+
try:
|
|
384
|
+
from datetime import datetime
|
|
385
|
+
from zoneinfo import ZoneInfo
|
|
386
|
+
off = datetime.now(ZoneInfo(tz_name)).utcoffset()
|
|
387
|
+
return int(off.total_seconds() // 3600) if off else 0
|
|
388
|
+
except Exception:
|
|
389
|
+
return 0
|