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,824 @@
|
|
|
1
|
+
"""Colony Briefing System — data aggregators.
|
|
2
|
+
|
|
3
|
+
Thin adapter layer that pulls structured data from Colony intelligence
|
|
4
|
+
modules and formats it for the BriefingComposer. Each aggregator uses
|
|
5
|
+
stub/default behaviour when the underlying subsystem is unavailable,
|
|
6
|
+
so briefings always generate even in partially-configured environments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import zoneinfo
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import date as _date, datetime, timedelta as _timedelta, timezone
|
|
18
|
+
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from .models import CalendarEvent
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Domain value objects
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RelationshipChange:
|
|
32
|
+
contact_name: str
|
|
33
|
+
change_type: str # "new" | "dormant" | "sentiment_shift" | "tier_change"
|
|
34
|
+
description: str
|
|
35
|
+
trust_tier: str # "info" | "casual" | "close" | "inner_circle"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GoalSummary:
|
|
40
|
+
goal_id: str
|
|
41
|
+
title: str
|
|
42
|
+
status: str # "overdue" | "blocked" | "completing_soon"
|
|
43
|
+
due_at: Optional[datetime] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GoalCompletionStats:
|
|
48
|
+
total_initiated: int
|
|
49
|
+
total_completed: int
|
|
50
|
+
completion_rate: float # 0.0–1.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class AnomalySummary:
|
|
55
|
+
anomaly_id: str
|
|
56
|
+
severity: str # "critical" | "warning" | "info"
|
|
57
|
+
description: str
|
|
58
|
+
detected_at: datetime
|
|
59
|
+
source: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class HealthSnapshot:
|
|
64
|
+
sleep_score: Optional[float] = None # 0.0–1.0
|
|
65
|
+
readiness: Optional[float] = None
|
|
66
|
+
notable: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class CrossDomainInsight:
|
|
71
|
+
insight_id: str
|
|
72
|
+
description: str
|
|
73
|
+
confidence: float
|
|
74
|
+
domains: List[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Aggregator protocols (allow injection of real or fake implementations)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@runtime_checkable
|
|
83
|
+
class RelationshipAggregatorProtocol(Protocol):
|
|
84
|
+
def get_notable_changes(
|
|
85
|
+
self, since: datetime, min_delta: float = 0.15
|
|
86
|
+
) -> List[RelationshipChange]: ...
|
|
87
|
+
|
|
88
|
+
def get_neglected_contacts(
|
|
89
|
+
self, days_since_contact: int = 14, limit: int = 5
|
|
90
|
+
) -> List[str]: ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@runtime_checkable
|
|
94
|
+
class CalendarAggregatorProtocol(Protocol):
|
|
95
|
+
def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]: ...
|
|
96
|
+
|
|
97
|
+
def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]: ...
|
|
98
|
+
|
|
99
|
+
def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]: ...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@runtime_checkable
|
|
103
|
+
class GoalAggregatorProtocol(Protocol):
|
|
104
|
+
def get_overdue_goals(self) -> List[GoalSummary]: ...
|
|
105
|
+
|
|
106
|
+
def get_blocked_goals(self) -> List[GoalSummary]: ...
|
|
107
|
+
|
|
108
|
+
def get_completing_soon(self, hours: float = 4.0) -> List[GoalSummary]: ...
|
|
109
|
+
|
|
110
|
+
def get_week_completion_stats(
|
|
111
|
+
self, period_start: datetime, period_end: datetime
|
|
112
|
+
) -> GoalCompletionStats: ...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@runtime_checkable
|
|
116
|
+
class AnomalyAggregatorProtocol(Protocol):
|
|
117
|
+
def get_active_anomalies(self, min_severity: str = "warning") -> List[AnomalySummary]: ...
|
|
118
|
+
|
|
119
|
+
def get_new_since(
|
|
120
|
+
self, since: datetime, min_severity: str = "warning"
|
|
121
|
+
) -> List[AnomalySummary]: ...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@runtime_checkable
|
|
125
|
+
class MindModelAggregatorProtocol(Protocol):
|
|
126
|
+
def get_health_snapshot(self) -> Optional[HealthSnapshot]: ...
|
|
127
|
+
|
|
128
|
+
def get_predicted_load(self, date: str) -> Optional[float]: ...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@runtime_checkable
|
|
132
|
+
class SynthesisAggregatorProtocol(Protocol):
|
|
133
|
+
def get_high_confidence_insights(
|
|
134
|
+
self,
|
|
135
|
+
min_confidence: float = 0.80,
|
|
136
|
+
since: Optional[datetime] = None,
|
|
137
|
+
limit: int = 3,
|
|
138
|
+
) -> List[CrossDomainInsight]: ...
|
|
139
|
+
|
|
140
|
+
def get_weekly_patterns(
|
|
141
|
+
self, period_start: datetime, period_end: datetime
|
|
142
|
+
) -> List[str]: ...
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Default stub implementations (used when subsystems are not configured)
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class StubRelationshipAggregator:
|
|
151
|
+
def get_notable_changes(
|
|
152
|
+
self, since: datetime, min_delta: float = 0.15
|
|
153
|
+
) -> List[RelationshipChange]:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def get_neglected_contacts(
|
|
157
|
+
self, days_since_contact: int = 14, limit: int = 5
|
|
158
|
+
) -> List[str]:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class StubCalendarAggregator:
|
|
163
|
+
def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class StubGoalAggregator:
|
|
174
|
+
def get_overdue_goals(self) -> List[GoalSummary]:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
def get_blocked_goals(self) -> List[GoalSummary]:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
def get_completing_soon(self, hours: float = 4.0) -> List[GoalSummary]:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
def get_week_completion_stats(
|
|
184
|
+
self, period_start: datetime, period_end: datetime
|
|
185
|
+
) -> GoalCompletionStats:
|
|
186
|
+
return GoalCompletionStats(total_initiated=0, total_completed=0, completion_rate=0.0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class StubAnomalyAggregator:
|
|
190
|
+
def get_active_anomalies(self, min_severity: str = "warning") -> List[AnomalySummary]:
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
def get_new_since(
|
|
194
|
+
self, since: datetime, min_severity: str = "warning"
|
|
195
|
+
) -> List[AnomalySummary]:
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class StubMindModelAggregator:
|
|
200
|
+
def get_health_snapshot(self) -> Optional[HealthSnapshot]:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def get_predicted_load(self, date: str) -> Optional[float]:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class StubSynthesisAggregator:
|
|
208
|
+
def get_high_confidence_insights(
|
|
209
|
+
self,
|
|
210
|
+
min_confidence: float = 0.80,
|
|
211
|
+
since: Optional[datetime] = None,
|
|
212
|
+
limit: int = 3,
|
|
213
|
+
) -> List[CrossDomainInsight]:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
def get_weekly_patterns(
|
|
217
|
+
self, period_start: datetime, period_end: datetime
|
|
218
|
+
) -> List[str]:
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# Async → sync bridge
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
_ASYNC_BRIDGE_POOL: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
|
228
|
+
_ASYNC_BRIDGE_LOCK = threading.Lock()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _async_bridge_pool() -> concurrent.futures.ThreadPoolExecutor:
|
|
232
|
+
"""Lazily create one shared bridge pool, instead of a new executor per call.
|
|
233
|
+
|
|
234
|
+
A fresh ``ThreadPoolExecutor`` per ``_run_async`` thrashed thread+loop
|
|
235
|
+
create/destroy under tactical-briefing bursts (a composer makes 5–8 of these
|
|
236
|
+
calls per briefing). One small bounded pool is reused for the whole process.
|
|
237
|
+
"""
|
|
238
|
+
global _ASYNC_BRIDGE_POOL
|
|
239
|
+
if _ASYNC_BRIDGE_POOL is None:
|
|
240
|
+
with _ASYNC_BRIDGE_LOCK:
|
|
241
|
+
if _ASYNC_BRIDGE_POOL is None:
|
|
242
|
+
_ASYNC_BRIDGE_POOL = concurrent.futures.ThreadPoolExecutor(
|
|
243
|
+
max_workers=2, thread_name_prefix="briefing-async"
|
|
244
|
+
)
|
|
245
|
+
return _ASYNC_BRIDGE_POOL
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _run_async(coro: Any) -> Any:
|
|
249
|
+
"""Run an async coroutine from a synchronous call-site.
|
|
250
|
+
|
|
251
|
+
Uses ``asyncio.run()`` when no event loop is active (the normal production
|
|
252
|
+
path where briefing generation is synchronous). Falls back to a shared
|
|
253
|
+
worker pool when a loop is already running (e.g. inside pytest-asyncio
|
|
254
|
+
tests), so we never deadlock by trying to nest ``asyncio.run()``.
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
asyncio.get_running_loop()
|
|
258
|
+
# Already inside a running loop — offload to the shared bridge pool.
|
|
259
|
+
return _async_bridge_pool().submit(asyncio.run, coro).result()
|
|
260
|
+
except RuntimeError:
|
|
261
|
+
# No running loop — safe to call asyncio.run directly.
|
|
262
|
+
return asyncio.run(coro)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Real implementations
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class RelationshipAggregator:
|
|
271
|
+
"""Real implementation: wraps RelationshipScorer and queries Neo4j graph data.
|
|
272
|
+
|
|
273
|
+
Score-change history is read from ``ScoreEvent`` nodes written by
|
|
274
|
+
``RelationshipScorer.record_score_change()``. Neglected contacts are
|
|
275
|
+
found via a direct graph query so that the aggregator is usable without
|
|
276
|
+
running a full scorer refresh cycle.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
# Scores live on a 0-100 scale; min_delta is expressed as a 0-1 fraction,
|
|
280
|
+
# so we multiply by 100 to get the point-delta threshold.
|
|
281
|
+
_SCORE_CHANGES_CYPHER = """
|
|
282
|
+
MATCH (p:Person)-[:SCORE_CHANGED]->(se:ScoreEvent)
|
|
283
|
+
WHERE se.createdAt >= datetime($since_iso)
|
|
284
|
+
AND abs(se.delta) >= $delta_threshold
|
|
285
|
+
RETURN p.name AS name, p.tier AS current_tier,
|
|
286
|
+
se.delta AS delta, se.reason AS reason, se.tier AS new_tier
|
|
287
|
+
ORDER BY abs(se.delta) DESC
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
_NEGLECTED_CYPHER = """
|
|
291
|
+
MATCH (p:Person)
|
|
292
|
+
WHERE p.tier IN ['inner_circle', 'trusted', 'regular']
|
|
293
|
+
AND (
|
|
294
|
+
p.lastInteraction IS NULL
|
|
295
|
+
OR p.lastInteraction < datetime() - duration({days: $days_threshold})
|
|
296
|
+
)
|
|
297
|
+
RETURN p.name AS name
|
|
298
|
+
ORDER BY p.lastInteraction ASC
|
|
299
|
+
LIMIT $limit
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(self, scorer: Any, graph: Any) -> None:
|
|
303
|
+
self._scorer = scorer
|
|
304
|
+
self._graph = graph
|
|
305
|
+
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
# Protocol methods
|
|
308
|
+
# ------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def get_notable_changes(
|
|
311
|
+
self, since: datetime, min_delta: float = 0.15
|
|
312
|
+
) -> List[RelationshipChange]:
|
|
313
|
+
try:
|
|
314
|
+
return _run_async(self._notable_changes_async(since, min_delta))
|
|
315
|
+
except Exception:
|
|
316
|
+
logger.exception("RelationshipAggregator.get_notable_changes failed")
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
def get_neglected_contacts(
|
|
320
|
+
self, days_since_contact: int = 14, limit: int = 5
|
|
321
|
+
) -> List[str]:
|
|
322
|
+
try:
|
|
323
|
+
return _run_async(self._neglected_contacts_async(days_since_contact, limit))
|
|
324
|
+
except Exception:
|
|
325
|
+
logger.exception("RelationshipAggregator.get_neglected_contacts failed")
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
# ------------------------------------------------------------------
|
|
329
|
+
# Async helpers (isolated for testability)
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
async def _notable_changes_async(
|
|
333
|
+
self, since: datetime, min_delta: float
|
|
334
|
+
) -> List[RelationshipChange]:
|
|
335
|
+
delta_threshold = min_delta * 100.0
|
|
336
|
+
rows = await self._query_score_changes(since.isoformat(), delta_threshold)
|
|
337
|
+
result: List[RelationshipChange] = []
|
|
338
|
+
for row in rows:
|
|
339
|
+
name: str = row.get("name") or "Unknown"
|
|
340
|
+
tier: str = row.get("new_tier") or row.get("current_tier") or "peripheral"
|
|
341
|
+
delta: float = float(row.get("delta") or 0.0)
|
|
342
|
+
reason: str = row.get("reason") or ""
|
|
343
|
+
|
|
344
|
+
if reason == "new_contact":
|
|
345
|
+
change_type = "new"
|
|
346
|
+
elif delta < 0:
|
|
347
|
+
change_type = "dormant"
|
|
348
|
+
else:
|
|
349
|
+
change_type = "tier_change"
|
|
350
|
+
|
|
351
|
+
description = (
|
|
352
|
+
f"Score increased by {delta:.1f} points"
|
|
353
|
+
if delta >= 0
|
|
354
|
+
else f"Score decreased by {abs(delta):.1f} points"
|
|
355
|
+
)
|
|
356
|
+
result.append(RelationshipChange(
|
|
357
|
+
contact_name=name,
|
|
358
|
+
change_type=change_type,
|
|
359
|
+
description=description,
|
|
360
|
+
trust_tier=tier,
|
|
361
|
+
))
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
async def _neglected_contacts_async(
|
|
365
|
+
self, days_since_contact: int, limit: int
|
|
366
|
+
) -> List[str]:
|
|
367
|
+
rows = await self._query_neglected(days_since_contact, limit)
|
|
368
|
+
return [r["name"] for r in rows if r.get("name")]
|
|
369
|
+
|
|
370
|
+
async def _query_score_changes(
|
|
371
|
+
self, since_iso: str, delta_threshold: float
|
|
372
|
+
) -> List[Dict[str, Any]]:
|
|
373
|
+
async with self._graph.driver.session(database=self._graph.database) as session:
|
|
374
|
+
result = await session.run(
|
|
375
|
+
self._SCORE_CHANGES_CYPHER,
|
|
376
|
+
since_iso=since_iso,
|
|
377
|
+
delta_threshold=delta_threshold,
|
|
378
|
+
)
|
|
379
|
+
return [dict(r) async for r in result]
|
|
380
|
+
|
|
381
|
+
async def _query_neglected(
|
|
382
|
+
self, days_threshold: int, limit: int
|
|
383
|
+
) -> List[Dict[str, Any]]:
|
|
384
|
+
async with self._graph.driver.session(database=self._graph.database) as session:
|
|
385
|
+
result = await session.run(
|
|
386
|
+
self._NEGLECTED_CYPHER,
|
|
387
|
+
days_threshold=days_threshold,
|
|
388
|
+
limit=limit,
|
|
389
|
+
)
|
|
390
|
+
return [dict(r) async for r in result]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class AnomalyDetectorAggregator:
|
|
394
|
+
"""Real implementation over the AnomalyDetector the autonomy registry
|
|
395
|
+
holds (its observation state is in-memory on that ONE instance, so the
|
|
396
|
+
detector is resolved lazily at call time — never constructed fresh here,
|
|
397
|
+
which would always be empty)."""
|
|
398
|
+
|
|
399
|
+
# Severity labels over the detector's 0-1 float scale.
|
|
400
|
+
_SEVERITY_FLOOR = {"critical": 0.85, "warning": 0.6, "info": 0.3}
|
|
401
|
+
|
|
402
|
+
def __init__(self, detector_provider: Optional[Any] = None) -> None:
|
|
403
|
+
self._provider = detector_provider or self._registry_detector
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _registry_detector():
|
|
407
|
+
try:
|
|
408
|
+
from apsimo.api.routers import host as _h
|
|
409
|
+
loop = getattr(_h, "_autonomy_loop", None)
|
|
410
|
+
registry = getattr(loop, "_registry", None)
|
|
411
|
+
return getattr(registry, "anomalies", None)
|
|
412
|
+
except Exception:
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
def _label(cls, severity: float) -> str:
|
|
417
|
+
if severity >= cls._SEVERITY_FLOOR["critical"]:
|
|
418
|
+
return "critical"
|
|
419
|
+
if severity >= cls._SEVERITY_FLOOR["warning"]:
|
|
420
|
+
return "warning"
|
|
421
|
+
return "info"
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _aware(dt: Optional[datetime]) -> datetime:
|
|
425
|
+
# the detector stamps naive LOCAL datetimes
|
|
426
|
+
if dt is None:
|
|
427
|
+
return datetime.now(timezone.utc)
|
|
428
|
+
if dt.tzinfo is None:
|
|
429
|
+
return dt.astimezone()
|
|
430
|
+
return dt
|
|
431
|
+
|
|
432
|
+
def _summaries(self, min_severity: str) -> List[AnomalySummary]:
|
|
433
|
+
detector = self._provider() if callable(self._provider) else self._provider
|
|
434
|
+
if detector is None:
|
|
435
|
+
return []
|
|
436
|
+
floor = self._SEVERITY_FLOOR.get(min_severity, 0.6)
|
|
437
|
+
try:
|
|
438
|
+
recent = detector.get_recent(min_severity=floor, limit=20) or []
|
|
439
|
+
except Exception:
|
|
440
|
+
logger.exception("AnomalyDetectorAggregator get_recent failed")
|
|
441
|
+
return []
|
|
442
|
+
out = []
|
|
443
|
+
for a in recent:
|
|
444
|
+
out.append(AnomalySummary(
|
|
445
|
+
anomaly_id=getattr(a, "id", ""),
|
|
446
|
+
severity=self._label(float(getattr(a, "severity", 0.0))),
|
|
447
|
+
description=getattr(a, "description", ""),
|
|
448
|
+
detected_at=self._aware(getattr(a, "detected_at", None)),
|
|
449
|
+
source=getattr(getattr(a, "type", None), "value",
|
|
450
|
+
str(getattr(a, "type", "anomaly"))),
|
|
451
|
+
))
|
|
452
|
+
return out
|
|
453
|
+
|
|
454
|
+
def get_active_anomalies(self, min_severity: str = "warning") -> List[AnomalySummary]:
|
|
455
|
+
return self._summaries(min_severity)
|
|
456
|
+
|
|
457
|
+
def get_new_since(self, since: datetime,
|
|
458
|
+
min_severity: str = "warning") -> List[AnomalySummary]:
|
|
459
|
+
if since is not None and since.tzinfo is None:
|
|
460
|
+
since = since.replace(tzinfo=timezone.utc)
|
|
461
|
+
return [a for a in self._summaries(min_severity)
|
|
462
|
+
if since is None or a.detected_at >= since]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class DiscovererSynthesisAggregator:
|
|
466
|
+
"""Real implementation over the ConnectionDiscoverer + InsightStore.
|
|
467
|
+
|
|
468
|
+
Connections are recomputed on demand (the discoverer persists nothing);
|
|
469
|
+
dismissed insight ids are filtered out. Connections carry no reliable
|
|
470
|
+
timestamps, so `since` cannot be honored and is ignored — the confidence
|
|
471
|
+
floor and dismissal filter are the honest selectors."""
|
|
472
|
+
|
|
473
|
+
def __init__(self, discoverer_provider: Optional[Any] = None,
|
|
474
|
+
insight_store_provider: Optional[Any] = None) -> None:
|
|
475
|
+
self._discoverer = discoverer_provider or self._host_discoverer
|
|
476
|
+
self._insights = insight_store_provider or self._host_insight_store
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _host_discoverer():
|
|
480
|
+
try:
|
|
481
|
+
from apsimo.api.routers import host as _h
|
|
482
|
+
return getattr(_h, "_connection_discoverer", None)
|
|
483
|
+
except Exception:
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
@staticmethod
|
|
487
|
+
def _host_insight_store():
|
|
488
|
+
try:
|
|
489
|
+
from apsimo.api.routers import host as _h
|
|
490
|
+
return getattr(_h, "_insight_store", None)
|
|
491
|
+
except Exception:
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
def _connections(self, min_confidence: float) -> List[Any]:
|
|
495
|
+
disc = self._discoverer() if callable(self._discoverer) else self._discoverer
|
|
496
|
+
if disc is None:
|
|
497
|
+
return []
|
|
498
|
+
try:
|
|
499
|
+
conns = _run_async(disc.discover_connections(
|
|
500
|
+
min_confidence=min_confidence)) or []
|
|
501
|
+
except Exception:
|
|
502
|
+
logger.exception("DiscovererSynthesisAggregator discovery failed")
|
|
503
|
+
return []
|
|
504
|
+
store = self._insights() if callable(self._insights) else self._insights
|
|
505
|
+
if store is not None:
|
|
506
|
+
try:
|
|
507
|
+
dismissed = store.list_dismissed()
|
|
508
|
+
conns = [c for c in conns
|
|
509
|
+
if getattr(c, "id", None) not in dismissed]
|
|
510
|
+
except Exception:
|
|
511
|
+
logger.debug("dismissed-insight filter failed", exc_info=True)
|
|
512
|
+
return conns
|
|
513
|
+
|
|
514
|
+
def get_high_confidence_insights(
|
|
515
|
+
self,
|
|
516
|
+
min_confidence: float = 0.80,
|
|
517
|
+
since: Optional[datetime] = None,
|
|
518
|
+
limit: int = 3,
|
|
519
|
+
) -> List[CrossDomainInsight]:
|
|
520
|
+
out = []
|
|
521
|
+
for c in self._connections(min_confidence)[:limit]:
|
|
522
|
+
out.append(CrossDomainInsight(
|
|
523
|
+
insight_id=getattr(c, "id", ""),
|
|
524
|
+
description=getattr(c, "description", ""),
|
|
525
|
+
confidence=float(getattr(c, "confidence", 0.0)),
|
|
526
|
+
domains=[d for d in (getattr(c, "source_domain", None),
|
|
527
|
+
getattr(c, "target_domain", None)) if d],
|
|
528
|
+
))
|
|
529
|
+
return out
|
|
530
|
+
|
|
531
|
+
def get_weekly_patterns(self, period_start: datetime,
|
|
532
|
+
period_end: datetime) -> List[str]:
|
|
533
|
+
# Recurring connections (seen more than once) read as patterns.
|
|
534
|
+
return [getattr(c, "description", "")
|
|
535
|
+
for c in self._connections(min_confidence=0.6)
|
|
536
|
+
if getattr(c, "observation_count", 1) > 1][:5]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class GoalEngineAggregator:
|
|
540
|
+
"""Real implementation backed by the GoalEngine (goals are owner-scoped;
|
|
541
|
+
no person filter). Overdue/blocked/completing-soon read live goal state;
|
|
542
|
+
weekly stats come from created/completed timestamps in the window."""
|
|
543
|
+
|
|
544
|
+
def __init__(self, engine: Any) -> None:
|
|
545
|
+
self._engine = engine
|
|
546
|
+
|
|
547
|
+
def _goals(self, status: Optional[str] = None, limit: int = 200) -> List[Any]:
|
|
548
|
+
try:
|
|
549
|
+
return self._engine.list_goals(status=status, limit=limit) or []
|
|
550
|
+
except Exception:
|
|
551
|
+
logger.exception("GoalEngineAggregator list failed")
|
|
552
|
+
return []
|
|
553
|
+
|
|
554
|
+
@staticmethod
|
|
555
|
+
def _aware(dt: Optional[datetime]) -> Optional[datetime]:
|
|
556
|
+
if dt is not None and dt.tzinfo is None:
|
|
557
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
558
|
+
return dt
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _summary(g: Any, status: str) -> GoalSummary:
|
|
562
|
+
return GoalSummary(goal_id=getattr(g, "goal_id", ""),
|
|
563
|
+
title=getattr(g, "title", ""),
|
|
564
|
+
status=status,
|
|
565
|
+
due_at=getattr(g, "deadline", None))
|
|
566
|
+
|
|
567
|
+
def get_overdue_goals(self) -> List[GoalSummary]:
|
|
568
|
+
out = []
|
|
569
|
+
for g in self._goals(status="active"):
|
|
570
|
+
try:
|
|
571
|
+
if getattr(g, "deadline", None) is not None and g.is_overdue():
|
|
572
|
+
out.append(self._summary(g, "overdue"))
|
|
573
|
+
except Exception:
|
|
574
|
+
continue
|
|
575
|
+
return out
|
|
576
|
+
|
|
577
|
+
def get_blocked_goals(self) -> List[GoalSummary]:
|
|
578
|
+
return [self._summary(g, "blocked") for g in self._goals(status="blocked")]
|
|
579
|
+
|
|
580
|
+
def get_completing_soon(self, hours: float = 4.0) -> List[GoalSummary]:
|
|
581
|
+
now = datetime.now(timezone.utc)
|
|
582
|
+
out = []
|
|
583
|
+
for g in self._goals(status="active"):
|
|
584
|
+
dl = self._aware(getattr(g, "deadline", None))
|
|
585
|
+
if dl is None:
|
|
586
|
+
continue
|
|
587
|
+
delta = (dl - now).total_seconds()
|
|
588
|
+
if 0 <= delta <= hours * 3600:
|
|
589
|
+
out.append(self._summary(g, "completing_soon"))
|
|
590
|
+
return out
|
|
591
|
+
|
|
592
|
+
def get_week_completion_stats(
|
|
593
|
+
self, period_start: datetime, period_end: datetime
|
|
594
|
+
) -> GoalCompletionStats:
|
|
595
|
+
period_start = self._aware(period_start)
|
|
596
|
+
period_end = self._aware(period_end)
|
|
597
|
+
initiated = completed = 0
|
|
598
|
+
for g in self._goals(status=None, limit=500):
|
|
599
|
+
ca = self._aware(getattr(g, "created_at", None))
|
|
600
|
+
if ca is not None and period_start <= ca <= period_end:
|
|
601
|
+
initiated += 1
|
|
602
|
+
done = self._aware(getattr(g, "completed_at", None))
|
|
603
|
+
if done is not None and period_start <= done <= period_end:
|
|
604
|
+
completed += 1
|
|
605
|
+
# A goal completed in-window but created before it can push the raw
|
|
606
|
+
# ratio past 1.0; the field's contract is 0.0-1.0, so clamp.
|
|
607
|
+
rate = min(1.0, completed / initiated) if initiated else 0.0
|
|
608
|
+
return GoalCompletionStats(total_initiated=initiated,
|
|
609
|
+
total_completed=completed,
|
|
610
|
+
completion_rate=rate)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class ConnectorCalendarAggregator:
|
|
614
|
+
"""Real calendar sections from the ICS calendar CONNECTOR (the only
|
|
615
|
+
calendar source that exists in-repo). Fetches the configured feed on
|
|
616
|
+
demand — briefings run once or twice a day, so a fresh fetch per compose
|
|
617
|
+
is fine and never serves stale events. Gated on the connector's own
|
|
618
|
+
COLONY_CONNECTOR_CALENDAR_ENABLED so it can't surprise-fetch.
|
|
619
|
+
|
|
620
|
+
Prep-needed heuristic: two or more attendees (the ICS payload carries no
|
|
621
|
+
video-link field)."""
|
|
622
|
+
|
|
623
|
+
_PREP_ATTENDEE_THRESHOLD = 2
|
|
624
|
+
|
|
625
|
+
def __init__(self, connector: Optional[Any] = None) -> None:
|
|
626
|
+
# accepts one connector, a list of connectors, or None (resolve all
|
|
627
|
+
# enabled calendar instances — base + per-account — at call time)
|
|
628
|
+
if connector is None:
|
|
629
|
+
self._connectors = None
|
|
630
|
+
elif isinstance(connector, (list, tuple)):
|
|
631
|
+
self._connectors = list(connector)
|
|
632
|
+
else:
|
|
633
|
+
self._connectors = [connector]
|
|
634
|
+
|
|
635
|
+
def _get_connectors(self):
|
|
636
|
+
if self._connectors is not None:
|
|
637
|
+
return [c for c in self._connectors if getattr(c, "enabled", True)]
|
|
638
|
+
try:
|
|
639
|
+
from apsimo.connectors.base import ConnectorConfig
|
|
640
|
+
from apsimo.connectors.caldav_calendar import CalendarConnector
|
|
641
|
+
accounts = [a.strip() for a in
|
|
642
|
+
ConnectorConfig("calendar").get("ACCOUNTS", "")
|
|
643
|
+
.replace(";", ",").split(",") if a.strip()]
|
|
644
|
+
if accounts:
|
|
645
|
+
out = []
|
|
646
|
+
for a in accounts:
|
|
647
|
+
c = CalendarConnector(account=a)
|
|
648
|
+
c._default_enabled = True
|
|
649
|
+
out.append(c)
|
|
650
|
+
return [c for c in out if c.enabled]
|
|
651
|
+
c = CalendarConnector()
|
|
652
|
+
return [c] if c.enabled else []
|
|
653
|
+
except Exception:
|
|
654
|
+
return []
|
|
655
|
+
|
|
656
|
+
def _events_between(self, start_utc: datetime,
|
|
657
|
+
end_utc: datetime, tz: str) -> List[CalendarEvent]:
|
|
658
|
+
observations = []
|
|
659
|
+
for conn in self._get_connectors():
|
|
660
|
+
try:
|
|
661
|
+
observations.extend(conn.poll() or [])
|
|
662
|
+
except Exception:
|
|
663
|
+
logger.exception("ConnectorCalendarAggregator poll failed (%s)",
|
|
664
|
+
getattr(conn, "name", "?"))
|
|
665
|
+
try:
|
|
666
|
+
zone = zoneinfo.ZoneInfo(tz)
|
|
667
|
+
except Exception:
|
|
668
|
+
zone = timezone.utc
|
|
669
|
+
from apsimo.connectors.caldav_calendar import _ics_ts
|
|
670
|
+
dated: List[Any] = []
|
|
671
|
+
for obs in observations:
|
|
672
|
+
when = datetime.fromtimestamp(obs.ts, tz=timezone.utc)
|
|
673
|
+
if not (start_utc <= when < end_utc):
|
|
674
|
+
continue
|
|
675
|
+
payload = obs.payload or {}
|
|
676
|
+
attendees = list(payload.get("attendees") or [])
|
|
677
|
+
end_raw = payload.get("end") or ""
|
|
678
|
+
duration = 30
|
|
679
|
+
if end_raw:
|
|
680
|
+
dur = (_ics_ts(end_raw) - obs.ts) / 60.0
|
|
681
|
+
if 0 < dur <= 24 * 60:
|
|
682
|
+
duration = int(dur)
|
|
683
|
+
dated.append((when, CalendarEvent(
|
|
684
|
+
time=when.astimezone(zone).strftime("%H:%M"),
|
|
685
|
+
title=payload.get("summary") or "(untitled)",
|
|
686
|
+
participants=attendees,
|
|
687
|
+
prep_needed=len(attendees) >= self._PREP_ATTENDEE_THRESHOLD,
|
|
688
|
+
location=payload.get("location") or None,
|
|
689
|
+
duration_minutes=duration,
|
|
690
|
+
)))
|
|
691
|
+
# sort by the REAL datetime — an HH:MM string sort scrambles any
|
|
692
|
+
# window longer than one day (Tue 08:00 before Mon 09:00)
|
|
693
|
+
dated.sort(key=lambda pair: pair[0])
|
|
694
|
+
return [e for _, e in dated]
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def _day_bounds(date: str, tz: str, days: int = 1):
|
|
698
|
+
try:
|
|
699
|
+
zone = zoneinfo.ZoneInfo(tz)
|
|
700
|
+
except Exception:
|
|
701
|
+
zone = timezone.utc
|
|
702
|
+
day = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=zone)
|
|
703
|
+
start = day.astimezone(timezone.utc)
|
|
704
|
+
return start, start + _timedelta(days=days)
|
|
705
|
+
|
|
706
|
+
def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]:
|
|
707
|
+
try:
|
|
708
|
+
start, end = self._day_bounds(date, timezone)
|
|
709
|
+
return self._events_between(start, end, timezone)
|
|
710
|
+
except Exception:
|
|
711
|
+
logger.exception("ConnectorCalendarAggregator.get_today_events failed")
|
|
712
|
+
return []
|
|
713
|
+
|
|
714
|
+
def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]:
|
|
715
|
+
return [e for e in events if e.prep_needed]
|
|
716
|
+
|
|
717
|
+
def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]:
|
|
718
|
+
try:
|
|
719
|
+
start, end = self._day_bounds(start_date, timezone, days=7)
|
|
720
|
+
return self._events_between(start, end, timezone)
|
|
721
|
+
except Exception:
|
|
722
|
+
logger.exception("ConnectorCalendarAggregator.get_upcoming_week failed")
|
|
723
|
+
return []
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class CalendarAggregator:
|
|
727
|
+
"""Real implementation: wraps CalendarIntegration for the briefing pipeline.
|
|
728
|
+
|
|
729
|
+
``CalendarIntegration`` is async; this class bridges the sync protocol
|
|
730
|
+
interface using ``_run_async()``. Events are converted from the
|
|
731
|
+
``EventData`` representation used by the integration layer to the
|
|
732
|
+
``CalendarEvent`` model used by briefings.
|
|
733
|
+
|
|
734
|
+
Prep-needed heuristic: an event needs prep when it has a video link *or*
|
|
735
|
+
has two or more attendees.
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
_PREP_ATTENDEE_THRESHOLD = 2
|
|
739
|
+
|
|
740
|
+
def __init__(self, calendar: Any) -> None:
|
|
741
|
+
self._calendar = calendar
|
|
742
|
+
|
|
743
|
+
# ------------------------------------------------------------------
|
|
744
|
+
# Protocol methods
|
|
745
|
+
# ------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]:
|
|
748
|
+
try:
|
|
749
|
+
return _run_async(self._today_events_async(date, timezone))
|
|
750
|
+
except Exception:
|
|
751
|
+
logger.exception("CalendarAggregator.get_today_events failed")
|
|
752
|
+
return []
|
|
753
|
+
|
|
754
|
+
def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]:
|
|
755
|
+
return [e for e in events if e.prep_needed]
|
|
756
|
+
|
|
757
|
+
def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]:
|
|
758
|
+
try:
|
|
759
|
+
return _run_async(self._upcoming_week_async(start_date, timezone))
|
|
760
|
+
except Exception:
|
|
761
|
+
logger.exception("CalendarAggregator.get_upcoming_week failed")
|
|
762
|
+
return []
|
|
763
|
+
|
|
764
|
+
# ------------------------------------------------------------------
|
|
765
|
+
# Async helpers (isolated for testability)
|
|
766
|
+
# ------------------------------------------------------------------
|
|
767
|
+
|
|
768
|
+
async def _today_events_async(
|
|
769
|
+
self, date_str: str, tz_name: str
|
|
770
|
+
) -> List[CalendarEvent]:
|
|
771
|
+
target_date = _date.fromisoformat(date_str)
|
|
772
|
+
tz_info = _resolve_tz(tz_name)
|
|
773
|
+
# Fetch 2 days to safely cover timezone-offset edge cases.
|
|
774
|
+
raw_events = await self._calendar.list_events(days=2, max_results=100)
|
|
775
|
+
items: List[tuple] = []
|
|
776
|
+
for ev in raw_events:
|
|
777
|
+
ev_local = ev.start.astimezone(tz_info)
|
|
778
|
+
if ev_local.date() != target_date:
|
|
779
|
+
continue
|
|
780
|
+
items.append((ev_local, _to_calendar_event(ev, tz_info)))
|
|
781
|
+
items.sort(key=lambda x: x[0])
|
|
782
|
+
return [item[1] for item in items]
|
|
783
|
+
|
|
784
|
+
async def _upcoming_week_async(
|
|
785
|
+
self, start_date: str, tz_name: str
|
|
786
|
+
) -> List[CalendarEvent]:
|
|
787
|
+
target_start = _date.fromisoformat(start_date)
|
|
788
|
+
target_end = target_start + _timedelta(days=7)
|
|
789
|
+
tz_info = _resolve_tz(tz_name)
|
|
790
|
+
raw_events = await self._calendar.list_events(days=8, max_results=100)
|
|
791
|
+
items: List[tuple] = []
|
|
792
|
+
for ev in raw_events:
|
|
793
|
+
ev_local = ev.start.astimezone(tz_info)
|
|
794
|
+
if not (target_start <= ev_local.date() < target_end):
|
|
795
|
+
continue
|
|
796
|
+
items.append((ev_local, _to_calendar_event(ev, tz_info)))
|
|
797
|
+
items.sort(key=lambda x: x[0])
|
|
798
|
+
return [item[1] for item in items]
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
# ---------------------------------------------------------------------------
|
|
802
|
+
# Module-level helpers shared by CalendarAggregator
|
|
803
|
+
# ---------------------------------------------------------------------------
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _resolve_tz(tz_name: str) -> Any:
|
|
807
|
+
try:
|
|
808
|
+
return zoneinfo.ZoneInfo(tz_name)
|
|
809
|
+
except Exception:
|
|
810
|
+
logger.warning("Unknown timezone %r, falling back to UTC", tz_name)
|
|
811
|
+
return timezone.utc
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _to_calendar_event(ev: Any, tz_info: Any) -> CalendarEvent:
|
|
815
|
+
prep_needed = bool(ev.video_link or len(ev.attendees) >= 2)
|
|
816
|
+
ev_local = ev.start.astimezone(tz_info)
|
|
817
|
+
return CalendarEvent(
|
|
818
|
+
time=ev_local.strftime("%H:%M"),
|
|
819
|
+
title=ev.title,
|
|
820
|
+
participants=list(ev.attendees),
|
|
821
|
+
prep_needed=prep_needed,
|
|
822
|
+
location=ev.location,
|
|
823
|
+
duration_minutes=int(ev.duration_minutes),
|
|
824
|
+
)
|