apsimo 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
apsimo/util/temporal.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Temporal reference frame for Colony (v0.21.0).
|
|
2
|
+
|
|
3
|
+
The single source of truth for "now" and timezone handling across the sidecar.
|
|
4
|
+
Everything that needs to reason about wall-clock time — context assembly, the
|
|
5
|
+
timeline tool, briefings, autonomy triggers — should go through this module so
|
|
6
|
+
the agent gets a consistent, editable sense of time.
|
|
7
|
+
|
|
8
|
+
Three independent, editable timezones (per the design):
|
|
9
|
+
* AGENT timezone — where the Colony agent "lives" (its home reference frame).
|
|
10
|
+
Editable; defaults to COLONY_AGENT_TIMEZONE, then the host
|
|
11
|
+
system tz, then UTC.
|
|
12
|
+
* CONTACT timezone — a per-contact field (contacts.timezone). Editable per
|
|
13
|
+
contact. Lets the agent reason "it's 9am my time / 3pm
|
|
14
|
+
theirs".
|
|
15
|
+
* COMMUNICATION tz — resolved per turn: an explicit override on the turn, else
|
|
16
|
+
the contact's tz, else the agent tz.
|
|
17
|
+
|
|
18
|
+
Stdlib only (datetime + zoneinfo). No third-party deps.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
from datetime import datetime, timezone, timedelta
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
try: # py3.9+
|
|
32
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
33
|
+
except Exception: # pragma: no cover - very old python
|
|
34
|
+
ZoneInfo = None # type: ignore
|
|
35
|
+
class ZoneInfoNotFoundError(Exception): # type: ignore
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
UTC = timezone.utc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------- #
|
|
44
|
+
# State (editable agent / default-contact timezone) #
|
|
45
|
+
# --------------------------------------------------------------------------- #
|
|
46
|
+
|
|
47
|
+
def _state_dir() -> Path:
|
|
48
|
+
return Path(os.environ.get("COLONY_STATE_DIR", os.path.expanduser("~/.colony")))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _state_path() -> Path:
|
|
52
|
+
return _state_dir() / "temporal.json"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_state() -> dict:
|
|
56
|
+
try:
|
|
57
|
+
with open(_state_path()) as f:
|
|
58
|
+
return json.load(f) or {}
|
|
59
|
+
except Exception:
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _save_state(state: dict) -> None:
|
|
64
|
+
try:
|
|
65
|
+
p = _state_path()
|
|
66
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
tmp = p.with_suffix(".json.tmp")
|
|
68
|
+
with open(tmp, "w") as f:
|
|
69
|
+
json.dump(state, f, indent=2)
|
|
70
|
+
os.replace(tmp, p)
|
|
71
|
+
except Exception as exc: # pragma: no cover
|
|
72
|
+
logger.warning("Failed to persist temporal state: %s", exc)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_valid_timezone(tz: Optional[str]) -> bool:
|
|
76
|
+
if not tz:
|
|
77
|
+
return False
|
|
78
|
+
if tz.upper() == "UTC":
|
|
79
|
+
return True
|
|
80
|
+
if ZoneInfo is None:
|
|
81
|
+
return False
|
|
82
|
+
try:
|
|
83
|
+
ZoneInfo(tz)
|
|
84
|
+
return True
|
|
85
|
+
except (ZoneInfoNotFoundError, ValueError, KeyError):
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _system_timezone() -> Optional[str]:
|
|
90
|
+
"""Best-effort IANA name of the host's local timezone."""
|
|
91
|
+
# /etc/localtime symlink → .../zoneinfo/<Area>/<City>
|
|
92
|
+
try:
|
|
93
|
+
p = Path("/etc/localtime")
|
|
94
|
+
if p.is_symlink():
|
|
95
|
+
target = os.readlink(p)
|
|
96
|
+
if "zoneinfo/" in target:
|
|
97
|
+
name = target.split("zoneinfo/", 1)[1]
|
|
98
|
+
if is_valid_timezone(name):
|
|
99
|
+
return name
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
tz = os.environ.get("TZ")
|
|
103
|
+
if is_valid_timezone(tz):
|
|
104
|
+
return tz
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def agent_timezone() -> str:
|
|
109
|
+
"""The agent's home timezone (IANA name). Always returns a usable value."""
|
|
110
|
+
env = os.environ.get("COLONY_AGENT_TIMEZONE")
|
|
111
|
+
if is_valid_timezone(env):
|
|
112
|
+
return env # type: ignore[return-value]
|
|
113
|
+
stored = _load_state().get("agent_timezone")
|
|
114
|
+
if is_valid_timezone(stored):
|
|
115
|
+
return stored
|
|
116
|
+
sys_tz = _system_timezone()
|
|
117
|
+
if sys_tz:
|
|
118
|
+
return sys_tz
|
|
119
|
+
return "UTC"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def set_agent_timezone(tz: str) -> str:
|
|
123
|
+
"""Persist the agent home timezone. Raises ValueError if invalid."""
|
|
124
|
+
if not is_valid_timezone(tz):
|
|
125
|
+
raise ValueError(f"Invalid IANA timezone: {tz!r}")
|
|
126
|
+
state = _load_state()
|
|
127
|
+
state["agent_timezone"] = tz
|
|
128
|
+
_save_state(state)
|
|
129
|
+
return tz
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def default_contact_timezone() -> Optional[str]:
|
|
133
|
+
"""Fallback tz for contacts that have none set (optional)."""
|
|
134
|
+
env = os.environ.get("COLONY_DEFAULT_CONTACT_TIMEZONE")
|
|
135
|
+
if is_valid_timezone(env):
|
|
136
|
+
return env # type: ignore[return-value]
|
|
137
|
+
stored = _load_state().get("default_contact_timezone")
|
|
138
|
+
return stored if is_valid_timezone(stored) else None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def set_default_contact_timezone(tz: Optional[str]) -> Optional[str]:
|
|
142
|
+
if tz is not None and not is_valid_timezone(tz):
|
|
143
|
+
raise ValueError(f"Invalid IANA timezone: {tz!r}")
|
|
144
|
+
state = _load_state()
|
|
145
|
+
state["default_contact_timezone"] = tz
|
|
146
|
+
_save_state(state)
|
|
147
|
+
return tz
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve_communication_timezone(
|
|
151
|
+
contact_tz: Optional[str] = None,
|
|
152
|
+
override_tz: Optional[str] = None,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Per-communication tz: explicit override → contact → default → agent."""
|
|
155
|
+
for cand in (override_tz, contact_tz, default_contact_timezone()):
|
|
156
|
+
if is_valid_timezone(cand):
|
|
157
|
+
return cand # type: ignore[return-value]
|
|
158
|
+
return agent_timezone()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------------- #
|
|
162
|
+
# Now / conversion #
|
|
163
|
+
# --------------------------------------------------------------------------- #
|
|
164
|
+
|
|
165
|
+
def _zone(tz: str):
|
|
166
|
+
if tz.upper() == "UTC" or ZoneInfo is None:
|
|
167
|
+
return UTC
|
|
168
|
+
try:
|
|
169
|
+
return ZoneInfo(tz)
|
|
170
|
+
except Exception:
|
|
171
|
+
return UTC
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def now_utc() -> datetime:
|
|
175
|
+
return datetime.now(UTC)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def now_in(tz: str) -> datetime:
|
|
179
|
+
return now_utc().astimezone(_zone(tz))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def to_zone(dt: datetime, tz: str) -> datetime:
|
|
183
|
+
if dt.tzinfo is None:
|
|
184
|
+
dt = dt.replace(tzinfo=UTC)
|
|
185
|
+
return dt.astimezone(_zone(tz))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_iso(value) -> Optional[datetime]:
|
|
189
|
+
"""Parse an ISO-8601 string (or pass through a datetime) → aware UTC dt."""
|
|
190
|
+
if value is None:
|
|
191
|
+
return None
|
|
192
|
+
if isinstance(value, datetime):
|
|
193
|
+
return value if value.tzinfo else value.replace(tzinfo=UTC)
|
|
194
|
+
try:
|
|
195
|
+
s = str(value).strip().replace("Z", "+00:00")
|
|
196
|
+
dt = datetime.fromisoformat(s)
|
|
197
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=UTC)
|
|
198
|
+
except Exception:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# --------------------------------------------------------------------------- #
|
|
203
|
+
# Human-friendly formatting #
|
|
204
|
+
# --------------------------------------------------------------------------- #
|
|
205
|
+
|
|
206
|
+
def part_of_day(dt: datetime) -> str:
|
|
207
|
+
h = dt.hour
|
|
208
|
+
if h < 5:
|
|
209
|
+
return "the middle of the night"
|
|
210
|
+
if h < 9:
|
|
211
|
+
return "early morning"
|
|
212
|
+
if h < 12:
|
|
213
|
+
return "morning"
|
|
214
|
+
if h < 14:
|
|
215
|
+
return "midday"
|
|
216
|
+
if h < 18:
|
|
217
|
+
return "afternoon"
|
|
218
|
+
if h < 22:
|
|
219
|
+
return "evening"
|
|
220
|
+
return "night"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def format_clock(dt: datetime, tz: Optional[str] = None) -> str:
|
|
224
|
+
"""e.g. 'Thu, Jun 12, 12:37 AM EDT'."""
|
|
225
|
+
if tz:
|
|
226
|
+
dt = to_zone(dt, tz)
|
|
227
|
+
elif dt.tzinfo is None:
|
|
228
|
+
dt = dt.replace(tzinfo=UTC)
|
|
229
|
+
label = dt.strftime("%Z") or "UTC"
|
|
230
|
+
# %-I is platform-specific; strip a leading zero portably.
|
|
231
|
+
hm = dt.strftime("%I:%M %p").lstrip("0")
|
|
232
|
+
return f"{dt.strftime('%a, %b %d')}, {hm} {label}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def humanize_delta(ts, ref: Optional[datetime] = None) -> str:
|
|
236
|
+
"""'6h ago', 'in 2d', 'just now'. Accepts datetime or ISO string."""
|
|
237
|
+
dt = parse_iso(ts)
|
|
238
|
+
if dt is None:
|
|
239
|
+
return "unknown"
|
|
240
|
+
ref = ref or now_utc()
|
|
241
|
+
if ref.tzinfo is None:
|
|
242
|
+
ref = ref.replace(tzinfo=UTC)
|
|
243
|
+
delta = ref - dt
|
|
244
|
+
secs = delta.total_seconds()
|
|
245
|
+
future = secs < 0
|
|
246
|
+
secs = abs(secs)
|
|
247
|
+
|
|
248
|
+
if secs < 45:
|
|
249
|
+
return "just now"
|
|
250
|
+
if secs < 90 * 60:
|
|
251
|
+
val, unit = round(secs / 60), "m"
|
|
252
|
+
elif secs < 36 * 3600:
|
|
253
|
+
val, unit = round(secs / 3600), "h"
|
|
254
|
+
elif secs < 14 * 86400:
|
|
255
|
+
val, unit = round(secs / 86400), "d"
|
|
256
|
+
elif secs < 8 * 7 * 86400:
|
|
257
|
+
val, unit = round(secs / (7 * 86400)), "w"
|
|
258
|
+
elif secs < 365 * 86400:
|
|
259
|
+
val, unit = round(secs / (30 * 86400)), "mo"
|
|
260
|
+
else:
|
|
261
|
+
val, unit = round(secs / (365 * 86400)), "y"
|
|
262
|
+
return f"in {val}{unit}" if future else f"{val}{unit} ago"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def bucket(ts, tz: Optional[str] = None) -> str:
|
|
266
|
+
"""Coarse calendar bucket in the given tz: today / yesterday / this week..."""
|
|
267
|
+
dt = parse_iso(ts)
|
|
268
|
+
if dt is None:
|
|
269
|
+
return "unknown"
|
|
270
|
+
tz = tz or agent_timezone()
|
|
271
|
+
local = to_zone(dt, tz)
|
|
272
|
+
today = now_in(tz).date()
|
|
273
|
+
d = (today - local.date()).days
|
|
274
|
+
if d < 0:
|
|
275
|
+
return "upcoming"
|
|
276
|
+
if d == 0:
|
|
277
|
+
return "today"
|
|
278
|
+
if d == 1:
|
|
279
|
+
return "yesterday"
|
|
280
|
+
if d < 7:
|
|
281
|
+
return "earlier this week"
|
|
282
|
+
if d < 14:
|
|
283
|
+
return "last week"
|
|
284
|
+
if d < 31:
|
|
285
|
+
return f"{d // 7}w ago"
|
|
286
|
+
if d < 365:
|
|
287
|
+
return f"{d // 30}mo ago"
|
|
288
|
+
return f"{d // 365}y ago"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
_REL_RE = re.compile(r"^\s*(\d+)\s*([smhdw])\s*$", re.IGNORECASE)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def parse_relative_since(value, default_hours: float = 24.0) -> str:
|
|
295
|
+
"""Turn a window spec into an ISO 'since' timestamp (UTC).
|
|
296
|
+
|
|
297
|
+
Accepts relative durations ('30m', '24h', '7d', '2w'), the words
|
|
298
|
+
'today'/'yesterday', or an absolute ISO date/time. Falls back to
|
|
299
|
+
``default_hours`` ago when empty/unparseable.
|
|
300
|
+
"""
|
|
301
|
+
if not value:
|
|
302
|
+
return (now_utc() - timedelta(hours=default_hours)).isoformat()
|
|
303
|
+
s = str(value).strip()
|
|
304
|
+
m = _REL_RE.match(s)
|
|
305
|
+
if m:
|
|
306
|
+
n = int(m.group(1))
|
|
307
|
+
mult = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}[m.group(2).lower()]
|
|
308
|
+
return (now_utc() - timedelta(seconds=n * mult)).isoformat()
|
|
309
|
+
low = s.lower()
|
|
310
|
+
if low in ("today", "yesterday"):
|
|
311
|
+
local = now_in(agent_timezone())
|
|
312
|
+
if low == "yesterday":
|
|
313
|
+
local = local - timedelta(days=1)
|
|
314
|
+
start = local.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
315
|
+
return start.astimezone(UTC).isoformat()
|
|
316
|
+
dt = parse_iso(s)
|
|
317
|
+
if dt is not None:
|
|
318
|
+
return dt.isoformat()
|
|
319
|
+
return (now_utc() - timedelta(hours=default_hours)).isoformat()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def hours_since(ts, ref: Optional[datetime] = None) -> Optional[float]:
|
|
323
|
+
dt = parse_iso(ts)
|
|
324
|
+
if dt is None:
|
|
325
|
+
return None
|
|
326
|
+
ref = ref or now_utc()
|
|
327
|
+
if ref.tzinfo is None:
|
|
328
|
+
ref = ref.replace(tzinfo=UTC)
|
|
329
|
+
return (ref - dt).total_seconds() / 3600.0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def describe_now(agent_tz: Optional[str] = None,
|
|
333
|
+
contact_tz: Optional[str] = None,
|
|
334
|
+
contact_label: str = "their") -> str:
|
|
335
|
+
"""One-line 'now' anchor, optionally with the contact's local time too."""
|
|
336
|
+
atz = agent_tz or agent_timezone()
|
|
337
|
+
a_local = now_in(atz)
|
|
338
|
+
line = f"{format_clock(a_local)} — {part_of_day(a_local)} (your local time, {atz})"
|
|
339
|
+
if contact_tz and is_valid_timezone(contact_tz) and contact_tz != atz:
|
|
340
|
+
c_local = now_in(contact_tz)
|
|
341
|
+
poss = contact_label if contact_label.endswith("s") else f"{contact_label}'s"
|
|
342
|
+
line += f"\nFor {poss} side it is {format_clock(c_local)} — {part_of_day(c_local)} ({contact_tz})."
|
|
343
|
+
return line
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Colony Vector Store — local LanceDB-backed vector search.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
- VectorStore — LanceDB wrapper, CRUD + ANN search
|
|
5
|
+
- EmbeddingPipeline — provider + caching + latency monitoring
|
|
6
|
+
- Collection — enum of named vector collections
|
|
7
|
+
- get_store() — retrieve the singleton VectorStore instance
|
|
8
|
+
- get_pipeline() — retrieve the singleton EmbeddingPipeline instance
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from apsimo.vector.collections import Collection
|
|
12
|
+
from apsimo.vector.config import EmbeddingConfig, HardwareProfile, VectorStoreConfig
|
|
13
|
+
from apsimo.vector.embedder import (
|
|
14
|
+
CPUEmbeddingProvider,
|
|
15
|
+
CUDAEmbeddingProvider,
|
|
16
|
+
EmbeddingPipeline,
|
|
17
|
+
EmbeddingProvider,
|
|
18
|
+
MLXEmbeddingProvider,
|
|
19
|
+
make_provider,
|
|
20
|
+
)
|
|
21
|
+
from apsimo.vector.query import HybridQuery, VectorItem, VectorQuery, VectorResult
|
|
22
|
+
|
|
23
|
+
# Lazy import — VectorStore depends on pyarrow/lancedb which may not be installed
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
_store: Any = None
|
|
26
|
+
_pipeline: Optional[EmbeddingPipeline] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _VectorStore():
|
|
30
|
+
"""Lazy import for VectorStore."""
|
|
31
|
+
from apsimo.vector.store import VectorStore
|
|
32
|
+
return VectorStore
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_store(store: Any) -> None:
|
|
36
|
+
global _store
|
|
37
|
+
_store = store
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def set_pipeline(pipeline: EmbeddingPipeline) -> None:
|
|
41
|
+
global _pipeline
|
|
42
|
+
_pipeline = pipeline
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_store() -> Any:
|
|
46
|
+
"""Retrieve the singleton VectorStore, or None if not configured."""
|
|
47
|
+
return _store
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_pipeline() -> Optional[EmbeddingPipeline]:
|
|
51
|
+
"""Retrieve the singleton EmbeddingPipeline, or None if not configured."""
|
|
52
|
+
return _pipeline
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"Collection",
|
|
57
|
+
"EmbeddingConfig",
|
|
58
|
+
"EmbeddingPipeline",
|
|
59
|
+
"EmbeddingProvider",
|
|
60
|
+
"CUDAEmbeddingProvider",
|
|
61
|
+
"CPUEmbeddingProvider",
|
|
62
|
+
"MLXEmbeddingProvider",
|
|
63
|
+
"HardwareProfile",
|
|
64
|
+
"HybridQuery",
|
|
65
|
+
"VectorItem",
|
|
66
|
+
"VectorQuery",
|
|
67
|
+
"VectorResult",
|
|
68
|
+
"VectorStore",
|
|
69
|
+
"VectorStoreConfig",
|
|
70
|
+
"get_pipeline",
|
|
71
|
+
"get_store",
|
|
72
|
+
"make_provider",
|
|
73
|
+
"set_pipeline",
|
|
74
|
+
"set_store",
|
|
75
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Colony Vector Store — backfill and re-embedding.
|
|
2
|
+
|
|
3
|
+
Re-embeds all vectors in one or more collections using the current
|
|
4
|
+
embedding pipeline. Skips rows already tagged with the current model
|
|
5
|
+
(idempotent). Marks every re-embedded vector with model_id and
|
|
6
|
+
embedded_at metadata.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BackfillResult:
|
|
23
|
+
"""Result of a backfill run."""
|
|
24
|
+
|
|
25
|
+
total: int = 0
|
|
26
|
+
processed: int = 0
|
|
27
|
+
failed: int = 0
|
|
28
|
+
skipped: int = 0 # already had current model_id
|
|
29
|
+
duration_s: float = 0.0
|
|
30
|
+
errors: list[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def backfill(
|
|
34
|
+
store,
|
|
35
|
+
pipeline,
|
|
36
|
+
collection: Optional[str] = None,
|
|
37
|
+
batch_size: int = 64,
|
|
38
|
+
current_model_id: Optional[str] = None,
|
|
39
|
+
) -> BackfillResult:
|
|
40
|
+
"""Re-embed vectors in the store using the current pipeline.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
store : VectorStore
|
|
45
|
+
The LanceDB vector store.
|
|
46
|
+
pipeline : EmbeddingPipeline
|
|
47
|
+
The current embedding pipeline.
|
|
48
|
+
collection : str, optional
|
|
49
|
+
Specific collection to backfill. If None, backfill all.
|
|
50
|
+
batch_size : int
|
|
51
|
+
Number of rows to embed per batch.
|
|
52
|
+
current_model_id : str, optional
|
|
53
|
+
The model_id of the current embedder. Rows with this model_id
|
|
54
|
+
in their metadata are skipped (idempotent).
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
BackfillResult
|
|
59
|
+
"""
|
|
60
|
+
from apsimo.vector.collections import Collection
|
|
61
|
+
|
|
62
|
+
start = time.monotonic()
|
|
63
|
+
result = BackfillResult()
|
|
64
|
+
|
|
65
|
+
# Resolve current model_id from pipeline if not provided
|
|
66
|
+
if current_model_id is None:
|
|
67
|
+
if hasattr(pipeline, "_provider") and hasattr(pipeline._provider, "_config"):
|
|
68
|
+
current_model_id = pipeline._provider._config.model_id
|
|
69
|
+
else:
|
|
70
|
+
current_model_id = ""
|
|
71
|
+
|
|
72
|
+
# Determine collections to process
|
|
73
|
+
if collection is not None:
|
|
74
|
+
try:
|
|
75
|
+
collections = [Collection(collection)]
|
|
76
|
+
except ValueError:
|
|
77
|
+
result.errors.append(f"Unknown collection: {collection}")
|
|
78
|
+
result.duration_s = time.monotonic() - start
|
|
79
|
+
return result
|
|
80
|
+
else:
|
|
81
|
+
collections = list(Collection)
|
|
82
|
+
|
|
83
|
+
for col in collections:
|
|
84
|
+
try:
|
|
85
|
+
col_result = await _backfill_collection(
|
|
86
|
+
store, pipeline, col, batch_size, current_model_id
|
|
87
|
+
)
|
|
88
|
+
result.total += col_result.total
|
|
89
|
+
result.processed += col_result.processed
|
|
90
|
+
result.failed += col_result.failed
|
|
91
|
+
result.skipped += col_result.skipped
|
|
92
|
+
result.errors.extend(col_result.errors)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
msg = f"Collection {col.value} backfill failed: {exc}"
|
|
95
|
+
logger.error(msg)
|
|
96
|
+
result.errors.append(msg)
|
|
97
|
+
|
|
98
|
+
result.duration_s = time.monotonic() - start
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def _backfill_collection(
|
|
103
|
+
store,
|
|
104
|
+
pipeline,
|
|
105
|
+
collection,
|
|
106
|
+
batch_size: int,
|
|
107
|
+
current_model_id: str,
|
|
108
|
+
) -> BackfillResult:
|
|
109
|
+
"""Backfill a single collection."""
|
|
110
|
+
result = BackfillResult()
|
|
111
|
+
|
|
112
|
+
# Scan all rows
|
|
113
|
+
try:
|
|
114
|
+
rows = await store.scan_all(collection)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
result.errors.append(f"scan failed: {exc}")
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
result.total = len(rows)
|
|
120
|
+
|
|
121
|
+
# Separate into skip vs re-embed
|
|
122
|
+
to_embed: list[dict] = []
|
|
123
|
+
for row in rows:
|
|
124
|
+
meta_str = row.get("metadata", "{}")
|
|
125
|
+
try:
|
|
126
|
+
meta = json.loads(meta_str) if isinstance(meta_str, str) else (meta_str or {})
|
|
127
|
+
except (json.JSONDecodeError, TypeError):
|
|
128
|
+
meta = {}
|
|
129
|
+
|
|
130
|
+
if current_model_id and meta.get("model_id") == current_model_id:
|
|
131
|
+
result.skipped += 1
|
|
132
|
+
continue
|
|
133
|
+
to_embed.append(row)
|
|
134
|
+
|
|
135
|
+
# Batch re-embed
|
|
136
|
+
now = time.time()
|
|
137
|
+
for i in range(0, len(to_embed), batch_size):
|
|
138
|
+
batch = to_embed[i : i + batch_size]
|
|
139
|
+
texts = [row.get("text", "") for row in batch]
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
vectors = await pipeline.embed_batch(texts)
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
result.failed += len(batch)
|
|
145
|
+
result.errors.append(f"embed batch failed: {exc}")
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Upsert each row with new vector + metadata
|
|
149
|
+
for row, vector in zip(batch, vectors):
|
|
150
|
+
try:
|
|
151
|
+
meta_str = row.get("metadata", "{}")
|
|
152
|
+
try:
|
|
153
|
+
meta = json.loads(meta_str) if isinstance(meta_str, str) else (meta_str or {})
|
|
154
|
+
except (json.JSONDecodeError, TypeError):
|
|
155
|
+
meta = {}
|
|
156
|
+
meta["model_id"] = current_model_id
|
|
157
|
+
meta["embedded_at"] = now
|
|
158
|
+
|
|
159
|
+
await store.update(
|
|
160
|
+
collection,
|
|
161
|
+
id=row["id"],
|
|
162
|
+
text=row.get("text", ""),
|
|
163
|
+
vector=vector,
|
|
164
|
+
metadata=meta,
|
|
165
|
+
)
|
|
166
|
+
result.processed += 1
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
result.failed += 1
|
|
169
|
+
result.errors.append(f"update {row.get('id', '?')} failed: {exc}")
|
|
170
|
+
|
|
171
|
+
return result
|
apsimo/vector/caption.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Colony Vector — auto-captioning for images.
|
|
2
|
+
|
|
3
|
+
Generates text captions for images using the host's LLM (if configured)
|
|
4
|
+
or falls back to EXIF/filename-based descriptions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from apsimo.vector.multimodal_types import ImageInput
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def caption_image(
|
|
18
|
+
image: ImageInput,
|
|
19
|
+
llm_config: Optional[dict[str, Any]] = None,
|
|
20
|
+
max_tokens: int = 50,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Generate a caption for an image.
|
|
23
|
+
|
|
24
|
+
Tries LLM-based captioning first (if credentials available),
|
|
25
|
+
falls back to EXIF/metadata-based caption.
|
|
26
|
+
"""
|
|
27
|
+
# Try LLM captioning
|
|
28
|
+
if llm_config and llm_config.get("api_key"):
|
|
29
|
+
try:
|
|
30
|
+
caption = await _llm_caption(image, llm_config, max_tokens)
|
|
31
|
+
if caption:
|
|
32
|
+
return caption
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
logger.warning("LLM captioning failed: %s — falling back to metadata", exc)
|
|
35
|
+
|
|
36
|
+
# Fallback: metadata-based caption
|
|
37
|
+
return _metadata_caption(image)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _llm_caption(
|
|
41
|
+
image: ImageInput,
|
|
42
|
+
llm_config: dict[str, Any],
|
|
43
|
+
max_tokens: int,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Use a vision LLM to generate a caption."""
|
|
46
|
+
import base64
|
|
47
|
+
import httpx
|
|
48
|
+
|
|
49
|
+
api_key = llm_config.get("api_key", "")
|
|
50
|
+
base_url = llm_config.get("base_url", "https://open.bigmodel.cn/api/paas/v4")
|
|
51
|
+
model = llm_config.get("vision_model", llm_config.get("model", "glm-4v-flash"))
|
|
52
|
+
|
|
53
|
+
b64 = base64.b64encode(image.data).decode()
|
|
54
|
+
|
|
55
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
56
|
+
resp = await client.post(
|
|
57
|
+
f"{base_url}/chat/completions",
|
|
58
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
59
|
+
json={
|
|
60
|
+
"model": model,
|
|
61
|
+
"messages": [
|
|
62
|
+
{
|
|
63
|
+
"role": "user",
|
|
64
|
+
"content": [
|
|
65
|
+
{"type": "text", "text": f"Describe this image in {max_tokens} characters or less. Be concise and factual."},
|
|
66
|
+
{"type": "image_url", "image_url": {"url": f"data:{image.mime_type};base64,{b64}"}},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"max_tokens": max_tokens,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
resp.raise_for_status()
|
|
74
|
+
data = resp.json()
|
|
75
|
+
return data["choices"][0]["message"]["content"].strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _metadata_caption(image: ImageInput) -> str:
|
|
79
|
+
"""Generate a basic caption from image metadata."""
|
|
80
|
+
parts: list[str] = []
|
|
81
|
+
|
|
82
|
+
# Use user-provided caption if available
|
|
83
|
+
if image.caption:
|
|
84
|
+
return image.caption
|
|
85
|
+
|
|
86
|
+
# Dimensions
|
|
87
|
+
if image.width and image.height:
|
|
88
|
+
parts.append(f"{image.width}x{image.height}")
|
|
89
|
+
|
|
90
|
+
# MIME type
|
|
91
|
+
fmt = image.mime_type.split("/")[-1].upper() if image.mime_type else "Image"
|
|
92
|
+
parts.insert(0, fmt)
|
|
93
|
+
|
|
94
|
+
# EXIF data
|
|
95
|
+
exif = image.exif
|
|
96
|
+
if exif.get("captured_at"):
|
|
97
|
+
parts.append(f"taken {exif['captured_at']}")
|
|
98
|
+
if exif.get("camera_make") or exif.get("camera_model"):
|
|
99
|
+
camera = f"{exif.get('camera_make', '')} {exif.get('camera_model', '')}".strip()
|
|
100
|
+
parts.append(f"camera: {camera}")
|
|
101
|
+
|
|
102
|
+
# GPS
|
|
103
|
+
if exif.get("gps_lat") and exif.get("gps_lon"):
|
|
104
|
+
parts.append(f"location: {exif['gps_lat']},{exif['gps_lon']}")
|
|
105
|
+
|
|
106
|
+
# File path hint
|
|
107
|
+
if image.original_path:
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
name = Path(image.original_path).stem
|
|
110
|
+
if name and not name.startswith("data:"):
|
|
111
|
+
parts.insert(0, name.replace("-", " ").replace("_", " "))
|
|
112
|
+
|
|
113
|
+
caption = " — ".join(parts) if parts else "Image"
|
|
114
|
+
return caption[:200] # Cap length
|