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,136 @@
|
|
|
1
|
+
"""GraphBaselineStore — person behavioral baselines stored on Neo4j Person nodes.
|
|
2
|
+
|
|
3
|
+
Uses Welford's online algorithm for O(1) incremental mean/std updates.
|
|
4
|
+
No cache, no TTL — Neo4j is the single source of truth.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import math
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from apsimo.intelligence.mind_model.signal_collector import (
|
|
16
|
+
BaselineStore,
|
|
17
|
+
PersonBaseline,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Default preferred hours when insufficient data exists
|
|
23
|
+
_DEFAULT_PREFERRED_HOURS = [9, 10, 11, 14, 15, 16]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PersonBaselineImpl:
|
|
28
|
+
"""Concrete PersonBaseline implementation."""
|
|
29
|
+
length_mean: float
|
|
30
|
+
length_std: float
|
|
31
|
+
preferred_hours: List[int]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GraphBaselineStore:
|
|
35
|
+
"""BaselineStore backed by Person node properties in Neo4j.
|
|
36
|
+
|
|
37
|
+
Baselines live as properties on the :Person node — no new nodes,
|
|
38
|
+
no new infrastructure. Welford's algorithm gives O(1) incremental
|
|
39
|
+
updates for mean and standard deviation without storing raw history.
|
|
40
|
+
|
|
41
|
+
Falls back to neutral defaults when Neo4j is unavailable.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, graph) -> None:
|
|
45
|
+
self._graph = graph
|
|
46
|
+
|
|
47
|
+
async def get(self, person_id: str) -> PersonBaselineImpl:
|
|
48
|
+
"""Read baseline from Person node. Returns empty baseline on miss."""
|
|
49
|
+
try:
|
|
50
|
+
row = await self._graph._run_get_baseline(person_id)
|
|
51
|
+
if row is None:
|
|
52
|
+
return PersonBaselineImpl(
|
|
53
|
+
length_mean=0.0,
|
|
54
|
+
length_std=0.0,
|
|
55
|
+
preferred_hours=list(_DEFAULT_PREFERRED_HOURS),
|
|
56
|
+
)
|
|
57
|
+
return PersonBaselineImpl(
|
|
58
|
+
length_mean=row.get("length_mean") or 0.0,
|
|
59
|
+
length_std=row.get("length_std") or 0.0,
|
|
60
|
+
preferred_hours=self._preferred_hours(
|
|
61
|
+
row.get("hour_histogram")
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
logger.debug("GraphBaselineStore.get failed for %s: %s", person_id, exc)
|
|
66
|
+
return PersonBaselineImpl(
|
|
67
|
+
length_mean=0.0,
|
|
68
|
+
length_std=0.0,
|
|
69
|
+
preferred_hours=list(_DEFAULT_PREFERRED_HOURS),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def record_message(self, person_id: str, length: int, hour: int) -> None:
|
|
73
|
+
"""Incrementally update baseline on the Person node via Welford."""
|
|
74
|
+
try:
|
|
75
|
+
current = await self._graph._run_get_baseline(person_id)
|
|
76
|
+
count = (current or {}).get("msg_count", 0) or 0
|
|
77
|
+
mean = (current or {}).get("length_mean", 0.0) or 0.0
|
|
78
|
+
m2 = (current or {}).get("length_m2", 0.0) or 0.0
|
|
79
|
+
hist_raw = (current or {}).get("hour_histogram")
|
|
80
|
+
|
|
81
|
+
# Parse histogram
|
|
82
|
+
if hist_raw:
|
|
83
|
+
try:
|
|
84
|
+
histogram = json.loads(hist_raw)
|
|
85
|
+
if len(histogram) != 24:
|
|
86
|
+
histogram = [0] * 24
|
|
87
|
+
except (json.JSONDecodeError, TypeError):
|
|
88
|
+
histogram = [0] * 24
|
|
89
|
+
else:
|
|
90
|
+
histogram = [0] * 24
|
|
91
|
+
|
|
92
|
+
# Welford update
|
|
93
|
+
count += 1
|
|
94
|
+
delta = length - mean
|
|
95
|
+
mean += delta / count
|
|
96
|
+
delta2 = length - mean
|
|
97
|
+
m2 += delta * delta2
|
|
98
|
+
std = math.sqrt(m2 / count) if count > 1 else 0.0
|
|
99
|
+
|
|
100
|
+
# Hour histogram
|
|
101
|
+
histogram[hour] = histogram[hour] + 1
|
|
102
|
+
|
|
103
|
+
# Write back
|
|
104
|
+
await self._graph._run_update_baseline(
|
|
105
|
+
person_id=person_id,
|
|
106
|
+
msg_count=count,
|
|
107
|
+
length_mean=mean,
|
|
108
|
+
length_m2=m2,
|
|
109
|
+
length_std=std,
|
|
110
|
+
hour_histogram=json.dumps(histogram),
|
|
111
|
+
)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.debug("GraphBaselineStore.record_message failed for %s: %s", person_id, exc)
|
|
114
|
+
|
|
115
|
+
async def store_signal(self, signal: "Signal") -> None:
|
|
116
|
+
"""Persist a signal to the graph. Best-effort — logs on failure."""
|
|
117
|
+
try:
|
|
118
|
+
await self._graph.store_signal(signal)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
logger.debug("GraphBaselineStore.store_signal failed: %s", exc)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _preferred_hours(histogram_json: Optional[str]) -> List[int]:
|
|
124
|
+
"""Hours above the median activity level."""
|
|
125
|
+
if not histogram_json:
|
|
126
|
+
return list(_DEFAULT_PREFERRED_HOURS)
|
|
127
|
+
try:
|
|
128
|
+
counts = json.loads(histogram_json)
|
|
129
|
+
except (json.JSONDecodeError, TypeError):
|
|
130
|
+
return list(_DEFAULT_PREFERRED_HOURS)
|
|
131
|
+
if len(counts) != 24 or sum(counts) < 5:
|
|
132
|
+
return list(_DEFAULT_PREFERRED_HOURS)
|
|
133
|
+
median = sorted(counts)[12]
|
|
134
|
+
if median == 0:
|
|
135
|
+
return list(_DEFAULT_PREFERRED_HOURS)
|
|
136
|
+
return [h for h, c in enumerate(counts) if c >= median]
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Colony's interaction signal collection — zero LLM, pure computation.
|
|
2
|
+
|
|
3
|
+
Colony extracts behavioral signals from every message it exchanges across connected
|
|
4
|
+
gateways. These signals build Colony's own picture of each person it interacts with:
|
|
5
|
+
how they communicate, when they're engaged, their emotional tone. Colony's relational
|
|
6
|
+
intelligence is built from this stream of first-hand observations, not from the
|
|
7
|
+
owner's address book or stated opinions.
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import List, Optional, Protocol
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Signal:
|
|
20
|
+
signal_type: str
|
|
21
|
+
raw_value: float
|
|
22
|
+
normalized_value: float
|
|
23
|
+
timestamp: datetime
|
|
24
|
+
person_id: str
|
|
25
|
+
source: str # "message", "reaction", "call"
|
|
26
|
+
context: Optional[dict] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaselineStore(Protocol):
|
|
30
|
+
"""Protocol for baseline store dependency."""
|
|
31
|
+
async def get(self, person_id: str) -> "PersonBaseline": ...
|
|
32
|
+
async def record_message(self, person_id: str, length: int, hour: int) -> None: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ColonyGraph(Protocol):
|
|
36
|
+
"""Protocol for graph dependency."""
|
|
37
|
+
async def store_signal(self, signal: Signal) -> None: ...
|
|
38
|
+
async def get_recent_signals(self, person_id: str, hours: int, signal_type: Optional[str] = None) -> List[Signal]: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PersonBaseline(Protocol):
|
|
42
|
+
"""Protocol for person baseline."""
|
|
43
|
+
length_mean: float
|
|
44
|
+
length_std: float
|
|
45
|
+
preferred_hours: List[int]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Message(Protocol):
|
|
49
|
+
"""Protocol for message input."""
|
|
50
|
+
sender_id: str
|
|
51
|
+
content: str
|
|
52
|
+
timestamp: datetime
|
|
53
|
+
reply_to_id: Optional[str] = None
|
|
54
|
+
has_media: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SignalCollector:
|
|
58
|
+
"""Extract behavioral signals from Colony's own interactions — zero LLM, pure computation.
|
|
59
|
+
|
|
60
|
+
Each message Colony sends or receives is an observation. SignalCollector turns
|
|
61
|
+
those observations into quantified signals (frequency, sentiment, latency, etc.)
|
|
62
|
+
that feed Colony's mind model and relationship scoring. Colony builds its social
|
|
63
|
+
intelligence from this first-hand evidence, independent of the owner's stated views.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
POSITIVE_WORDS = {"happy", "great", "love", "thanks", "awesome", "excited",
|
|
67
|
+
"good", "wonderful", "amazing", "fantastic", "perfect"}
|
|
68
|
+
NEGATIVE_WORDS = {"sad", "angry", "frustrated", "sorry", "bad", "worried",
|
|
69
|
+
"terrible", "awful", "horrible", "hate", "upset"}
|
|
70
|
+
NEGATION_WORDS = {"not", "no", "never", "don't", "doesn't", "didn't", "wasn't"}
|
|
71
|
+
EMOJI_PATTERN = re.compile(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U00002702-\U000027B0]')
|
|
72
|
+
|
|
73
|
+
def __init__(self, baseline_store: BaselineStore, graph: ColonyGraph, metrics=None):
|
|
74
|
+
self.baselines = baseline_store
|
|
75
|
+
self.graph = graph
|
|
76
|
+
self._metrics = metrics # Optional ColonyMetricsCollector
|
|
77
|
+
|
|
78
|
+
async def ingest_raw(self, signal_data: dict) -> None:
|
|
79
|
+
"""Ingest a pre-computed signal from an external source."""
|
|
80
|
+
try:
|
|
81
|
+
from apsimo.models.signal import Signal, SignalType, SignalStrength
|
|
82
|
+
sig_type = SignalType.ENGAGEMENT_DEPTH # default
|
|
83
|
+
raw_type = signal_data.get("type", "")
|
|
84
|
+
for st in SignalType:
|
|
85
|
+
if st.value == raw_type or st.name.lower() == raw_type.lower():
|
|
86
|
+
sig_type = st
|
|
87
|
+
break
|
|
88
|
+
sig = Signal(
|
|
89
|
+
id=signal_data.get("id", f"raw-{time.monotonic_ns()}"),
|
|
90
|
+
person_id=signal_data.get("person_id", ""),
|
|
91
|
+
signal_type=sig_type,
|
|
92
|
+
value=float(signal_data.get("value", 0.5)),
|
|
93
|
+
source=signal_data.get("source", "api"),
|
|
94
|
+
context=signal_data.get("data"),
|
|
95
|
+
)
|
|
96
|
+
await self.baselines.store_signal(sig)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning("ingest_raw failed: %s", e)
|
|
99
|
+
|
|
100
|
+
async def collect(self, message: Message) -> List[Signal]:
|
|
101
|
+
"""Extract all signals from a single message in Colony's interaction stream."""
|
|
102
|
+
t0 = time.monotonic()
|
|
103
|
+
signals = []
|
|
104
|
+
person_id = message.sender_id
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
baseline = await self.baselines.get(person_id)
|
|
108
|
+
except (LookupError, AttributeError, OSError, RuntimeError):
|
|
109
|
+
baseline = None
|
|
110
|
+
|
|
111
|
+
# 1. Message length (Z-score normalized)
|
|
112
|
+
length = len(message.content)
|
|
113
|
+
if baseline and baseline.length_std > 0:
|
|
114
|
+
z_score = (length - baseline.length_mean) / baseline.length_std
|
|
115
|
+
else:
|
|
116
|
+
z_score = 0.0
|
|
117
|
+
signals.append(Signal(
|
|
118
|
+
signal_type="message_length",
|
|
119
|
+
raw_value=length,
|
|
120
|
+
normalized_value=z_score,
|
|
121
|
+
timestamp=message.timestamp,
|
|
122
|
+
person_id=person_id,
|
|
123
|
+
source="message",
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# 2. Sentiment (lexicon-based, no LLM)
|
|
127
|
+
sentiment = self._compute_sentiment(message.content)
|
|
128
|
+
signals.append(Signal(
|
|
129
|
+
signal_type="sentiment",
|
|
130
|
+
raw_value=sentiment,
|
|
131
|
+
normalized_value=sentiment, # Already -1 to 1
|
|
132
|
+
timestamp=message.timestamp,
|
|
133
|
+
person_id=person_id,
|
|
134
|
+
source="message",
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
# 3. Emoji usage
|
|
138
|
+
emoji_count = len(self.EMOJI_PATTERN.findall(message.content))
|
|
139
|
+
emoji_per_100 = (emoji_count / max(len(message.content), 1)) * 100
|
|
140
|
+
signals.append(Signal(
|
|
141
|
+
signal_type="emoji_usage",
|
|
142
|
+
raw_value=emoji_count,
|
|
143
|
+
normalized_value=emoji_per_100,
|
|
144
|
+
timestamp=message.timestamp,
|
|
145
|
+
person_id=person_id,
|
|
146
|
+
source="message",
|
|
147
|
+
))
|
|
148
|
+
|
|
149
|
+
# 4. Media usage
|
|
150
|
+
signals.append(Signal(
|
|
151
|
+
signal_type="media_usage",
|
|
152
|
+
raw_value=1.0 if message.has_media else 0.0,
|
|
153
|
+
normalized_value=1.0 if message.has_media else 0.0,
|
|
154
|
+
timestamp=message.timestamp,
|
|
155
|
+
person_id=person_id,
|
|
156
|
+
source="message",
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
# 5. Time of day
|
|
160
|
+
hour = message.timestamp.hour
|
|
161
|
+
if baseline and hasattr(baseline, 'preferred_hours') and baseline.preferred_hours:
|
|
162
|
+
if hour in baseline.preferred_hours:
|
|
163
|
+
tod_bucket = 0 # preferred
|
|
164
|
+
elif 8 <= hour <= 22:
|
|
165
|
+
tod_bucket = 1 # off-hours but reasonable
|
|
166
|
+
else:
|
|
167
|
+
tod_bucket = 2 # late-night
|
|
168
|
+
else:
|
|
169
|
+
if 8 <= hour <= 22:
|
|
170
|
+
tod_bucket = 0
|
|
171
|
+
elif 6 <= hour < 8 or 22 < hour <= 24:
|
|
172
|
+
tod_bucket = 1
|
|
173
|
+
else:
|
|
174
|
+
tod_bucket = 2
|
|
175
|
+
signals.append(Signal(
|
|
176
|
+
signal_type="time_of_day",
|
|
177
|
+
raw_value=float(hour),
|
|
178
|
+
normalized_value=float(tod_bucket),
|
|
179
|
+
timestamp=message.timestamp,
|
|
180
|
+
person_id=person_id,
|
|
181
|
+
source="message",
|
|
182
|
+
))
|
|
183
|
+
|
|
184
|
+
# 6. Message frequency (24h rolling)
|
|
185
|
+
recent_count = await self._get_message_frequency(person_id, hours=24)
|
|
186
|
+
signals.append(Signal(
|
|
187
|
+
signal_type="message_frequency",
|
|
188
|
+
raw_value=float(recent_count + 1), # +1 for this message
|
|
189
|
+
normalized_value=min(float(recent_count + 1) / 10.0, 10.0), # Cap at 10
|
|
190
|
+
timestamp=message.timestamp,
|
|
191
|
+
person_id=person_id,
|
|
192
|
+
source="message",
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
# 7. Response latency (if replying)
|
|
196
|
+
if message.reply_to_id:
|
|
197
|
+
latency = await self._compute_latency(message)
|
|
198
|
+
if latency is not None:
|
|
199
|
+
signals.append(Signal(
|
|
200
|
+
signal_type="response_latency",
|
|
201
|
+
raw_value=latency,
|
|
202
|
+
normalized_value=min(latency / 60.0, 24.0), # Hours, cap at 24
|
|
203
|
+
timestamp=message.timestamp,
|
|
204
|
+
person_id=person_id,
|
|
205
|
+
source="message",
|
|
206
|
+
))
|
|
207
|
+
|
|
208
|
+
# 8. Initiation ratio (30-day window)
|
|
209
|
+
init_ratio = await self._compute_initiation_ratio(person_id, days=30)
|
|
210
|
+
signals.append(Signal(
|
|
211
|
+
signal_type="initiation_ratio",
|
|
212
|
+
raw_value=init_ratio,
|
|
213
|
+
normalized_value=init_ratio, # Already 0-1
|
|
214
|
+
timestamp=message.timestamp,
|
|
215
|
+
person_id=person_id,
|
|
216
|
+
source="message",
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
# Persist all signals to graph
|
|
220
|
+
for sig in signals:
|
|
221
|
+
try:
|
|
222
|
+
await self.graph.store_signal(sig)
|
|
223
|
+
except (OSError, RuntimeError) as exc:
|
|
224
|
+
logger.debug("SignalCollector: failed to store signal %s: %s", sig.signal_type, exc)
|
|
225
|
+
|
|
226
|
+
# Update baseline with this message
|
|
227
|
+
try:
|
|
228
|
+
if hasattr(self.baselines, 'record_message'):
|
|
229
|
+
await self.baselines.record_message(person_id, length, hour)
|
|
230
|
+
except (OSError, RuntimeError, AttributeError) as exc:
|
|
231
|
+
logger.debug("SignalCollector: failed to update baseline for %s: %s", person_id, exc)
|
|
232
|
+
|
|
233
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
234
|
+
logger.info("Collected %d signals for %s in %.1fms", len(signals), person_id, elapsed_ms)
|
|
235
|
+
if self._metrics is not None:
|
|
236
|
+
try:
|
|
237
|
+
self._metrics.record_signal_collection(
|
|
238
|
+
latency_ms=elapsed_ms,
|
|
239
|
+
signal_count=len(signals),
|
|
240
|
+
signal_types=[s.signal_type for s in signals],
|
|
241
|
+
)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
logger.debug("Metrics recording failed: %s", exc)
|
|
244
|
+
|
|
245
|
+
return signals
|
|
246
|
+
|
|
247
|
+
def _compute_sentiment(self, text: str) -> float:
|
|
248
|
+
"""Lexicon-based sentiment with negation handling."""
|
|
249
|
+
raw_words = text.lower().split()
|
|
250
|
+
# Strip punctuation so "great!" matches "great", etc.
|
|
251
|
+
words = [w.strip(".,!?;:\"'()[]{}") for w in raw_words]
|
|
252
|
+
pos_count = 0
|
|
253
|
+
neg_count = 0
|
|
254
|
+
|
|
255
|
+
for i, word in enumerate(words):
|
|
256
|
+
# Check for negation in previous 3 words
|
|
257
|
+
negated = False
|
|
258
|
+
for j in range(max(0, i - 3), i):
|
|
259
|
+
if words[j] in self.NEGATION_WORDS:
|
|
260
|
+
negated = True
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
if word in self.POSITIVE_WORDS:
|
|
264
|
+
if negated:
|
|
265
|
+
neg_count += 0.5 # Negated positive = weak negative
|
|
266
|
+
else:
|
|
267
|
+
pos_count += 1
|
|
268
|
+
elif word in self.NEGATIVE_WORDS:
|
|
269
|
+
if negated:
|
|
270
|
+
pos_count += 0.5 # Negated negative = weak positive
|
|
271
|
+
else:
|
|
272
|
+
neg_count += 1
|
|
273
|
+
|
|
274
|
+
total = pos_count + neg_count
|
|
275
|
+
if total == 0:
|
|
276
|
+
return 0.0
|
|
277
|
+
return (pos_count - neg_count) / total
|
|
278
|
+
|
|
279
|
+
async def _get_message_frequency(self, person_id: str, hours: int = 24) -> int:
|
|
280
|
+
"""Count messages in the last N hours."""
|
|
281
|
+
try:
|
|
282
|
+
signals = await self.graph.get_recent_signals(
|
|
283
|
+
person_id, hours=hours, signal_type="message_length"
|
|
284
|
+
)
|
|
285
|
+
return len(signals)
|
|
286
|
+
except (OSError, RuntimeError):
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
async def _compute_latency(self, message: Message) -> Optional[float]:
|
|
290
|
+
"""Compute response latency in minutes for a reply message.
|
|
291
|
+
|
|
292
|
+
Uses the most recent prior signal as a proxy for the original
|
|
293
|
+
message timestamp, since Signal.context doesn't carry message IDs.
|
|
294
|
+
Returns None if no prior signals exist or on error.
|
|
295
|
+
"""
|
|
296
|
+
if not message.reply_to_id:
|
|
297
|
+
return None
|
|
298
|
+
try:
|
|
299
|
+
# Fetch recent signals ordered by timestamp DESC; the most recent
|
|
300
|
+
# signal before this message is the best proxy for what's being
|
|
301
|
+
# replied to.
|
|
302
|
+
prior_signals = await self.graph.get_recent_signals(
|
|
303
|
+
message.sender_id, hours=24 * 7, signal_type="message_length"
|
|
304
|
+
)
|
|
305
|
+
for sig in prior_signals:
|
|
306
|
+
if sig.timestamp < message.timestamp:
|
|
307
|
+
delta = (message.timestamp - sig.timestamp).total_seconds() / 60.0
|
|
308
|
+
return max(0.0, delta)
|
|
309
|
+
except (OSError, RuntimeError):
|
|
310
|
+
pass
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
async def _compute_initiation_ratio(self, person_id: str, days: int = 30) -> float:
|
|
314
|
+
"""Compute conversation initiation ratio over past N days.
|
|
315
|
+
|
|
316
|
+
A new conversation starts when there is a 4-hour gap since the
|
|
317
|
+
previous message from this person. Since we only track signals
|
|
318
|
+
from this person (not Colony's outbound messages), we approximate
|
|
319
|
+
the initiation ratio as (person-initiated conversations) / total.
|
|
320
|
+
A signal with ``source="inbound"`` means the person sent it;
|
|
321
|
+
``source="outbound"`` means Colony sent first. If direction info
|
|
322
|
+
is unavailable (all ``source="message"``), we fall back to the
|
|
323
|
+
ratio of conversation-starts to total messages as an activity proxy.
|
|
324
|
+
|
|
325
|
+
Returns a value in [0, 1]. Returns 0.5 when there is insufficient data.
|
|
326
|
+
"""
|
|
327
|
+
GAP_HOURS = 4
|
|
328
|
+
try:
|
|
329
|
+
signals = await self.graph.get_recent_signals(
|
|
330
|
+
person_id, hours=days * 24, signal_type="message_length"
|
|
331
|
+
)
|
|
332
|
+
if not signals:
|
|
333
|
+
return 0.5
|
|
334
|
+
|
|
335
|
+
signals.sort(key=lambda s: s.timestamp)
|
|
336
|
+
conversation_starts = 0
|
|
337
|
+
person_starts = 0
|
|
338
|
+
prev_ts = None
|
|
339
|
+
has_direction = any(s.source in ("inbound", "outbound") for s in signals)
|
|
340
|
+
|
|
341
|
+
for sig in signals:
|
|
342
|
+
is_new_convo = (
|
|
343
|
+
prev_ts is None
|
|
344
|
+
or (sig.timestamp - prev_ts).total_seconds() > GAP_HOURS * 3600
|
|
345
|
+
)
|
|
346
|
+
if is_new_convo:
|
|
347
|
+
conversation_starts += 1
|
|
348
|
+
if has_direction:
|
|
349
|
+
if sig.source == "inbound":
|
|
350
|
+
person_starts += 1
|
|
351
|
+
else:
|
|
352
|
+
# No direction info — count all conversation starts
|
|
353
|
+
# as person-initiated (best available approximation)
|
|
354
|
+
person_starts += 1
|
|
355
|
+
prev_ts = sig.timestamp
|
|
356
|
+
|
|
357
|
+
if conversation_starts == 0:
|
|
358
|
+
return 0.5
|
|
359
|
+
return person_starts / conversation_starts
|
|
360
|
+
except (OSError, RuntimeError):
|
|
361
|
+
return 0.5
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Relationships package — trust tiers and relationship scoring."""
|
|
2
|
+
|
|
3
|
+
from .trust_tiers import TrustTier, TrustTierManager
|
|
4
|
+
from .scorer import ScoreWeights, RelationshipScorer
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"TrustTier",
|
|
8
|
+
"TrustTierManager",
|
|
9
|
+
"ScoreWeights",
|
|
10
|
+
"RelationshipScorer",
|
|
11
|
+
]
|