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,670 @@
|
|
|
1
|
+
"""Colony Vector Store — embedding providers and pipeline.
|
|
2
|
+
|
|
3
|
+
Provides hardware-agnostic embedding via swappable backends:
|
|
4
|
+
- CUDAEmbeddingProvider (NVIDIA GPU via sentence-transformers)
|
|
5
|
+
- CPUEmbeddingProvider (CPU via sentence-transformers)
|
|
6
|
+
- MLXEmbeddingProvider (Apple Silicon via PyTorch MPS — legacy)
|
|
7
|
+
- NativeMLXEmbeddingProvider (Apple Silicon via native MLX framework)
|
|
8
|
+
|
|
9
|
+
EmbeddingPipeline wraps a provider and adds LRU caching + latency monitoring.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import functools
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
from apsimo.vector.config import EmbeddingConfig
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Abstract provider
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
class EmbeddingProvider(ABC):
|
|
32
|
+
"""Abstract interface for all embedding backends."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
35
|
+
self._config = config
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def dimensions(self) -> int:
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def embed(self, text: str) -> list[float]:
|
|
44
|
+
"""Embed a single text string. Returns float32 vector."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
49
|
+
"""Embed a batch of texts. Returns list of float32 vectors."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
async def warmup(self) -> None:
|
|
54
|
+
"""Load model weights, allocate device memory, run one dummy batch."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def close(self) -> None:
|
|
59
|
+
"""Release resources."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# CUDA provider
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
class CUDAEmbeddingProvider(EmbeddingProvider):
|
|
68
|
+
"""NVIDIA GPU embedding via sentence-transformers."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
71
|
+
super().__init__(config)
|
|
72
|
+
self._model = None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def dimensions(self) -> int:
|
|
76
|
+
return self._config.dimensions
|
|
77
|
+
|
|
78
|
+
async def warmup(self) -> None:
|
|
79
|
+
loop = asyncio.get_running_loop()
|
|
80
|
+
self._model = await loop.run_in_executor(None, self._load_model)
|
|
81
|
+
# Warm up with a dummy encode
|
|
82
|
+
await loop.run_in_executor(None, self._model.encode, ["warmup"])
|
|
83
|
+
|
|
84
|
+
def _load_model(self):
|
|
85
|
+
from sentence_transformers import SentenceTransformer
|
|
86
|
+
|
|
87
|
+
device = self._config.device or "cuda"
|
|
88
|
+
kwargs: dict = {"device": device}
|
|
89
|
+
if self._config.cache_dir:
|
|
90
|
+
kwargs["cache_folder"] = self._config.cache_dir
|
|
91
|
+
model = SentenceTransformer(self._config.model_id, **kwargs)
|
|
92
|
+
return model
|
|
93
|
+
|
|
94
|
+
async def embed(self, text: str) -> list[float]:
|
|
95
|
+
loop = asyncio.get_running_loop()
|
|
96
|
+
result = await loop.run_in_executor(
|
|
97
|
+
None, functools.partial(self._model.encode, [text], normalize_embeddings=True)
|
|
98
|
+
)
|
|
99
|
+
return result[0].tolist()
|
|
100
|
+
|
|
101
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
102
|
+
if not texts:
|
|
103
|
+
return []
|
|
104
|
+
loop = asyncio.get_running_loop()
|
|
105
|
+
result = await loop.run_in_executor(
|
|
106
|
+
None,
|
|
107
|
+
functools.partial(
|
|
108
|
+
self._model.encode,
|
|
109
|
+
texts,
|
|
110
|
+
batch_size=self._config.max_batch_size,
|
|
111
|
+
normalize_embeddings=True,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
return result.tolist()
|
|
115
|
+
|
|
116
|
+
async def close(self) -> None:
|
|
117
|
+
self._model = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# CPU provider
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
class CPUEmbeddingProvider(EmbeddingProvider):
|
|
125
|
+
"""CPU-only embedding via sentence-transformers."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
128
|
+
super().__init__(config)
|
|
129
|
+
self._model = None
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def dimensions(self) -> int:
|
|
133
|
+
return self._config.dimensions
|
|
134
|
+
|
|
135
|
+
async def warmup(self) -> None:
|
|
136
|
+
loop = asyncio.get_running_loop()
|
|
137
|
+
self._model = await loop.run_in_executor(None, self._load_model)
|
|
138
|
+
await loop.run_in_executor(None, self._model.encode, ["warmup"])
|
|
139
|
+
|
|
140
|
+
def _load_model(self):
|
|
141
|
+
from sentence_transformers import SentenceTransformer
|
|
142
|
+
|
|
143
|
+
kwargs: dict = {"device": "cpu"}
|
|
144
|
+
if self._config.cache_dir:
|
|
145
|
+
kwargs["cache_folder"] = self._config.cache_dir
|
|
146
|
+
model = SentenceTransformer(self._config.model_id, **kwargs)
|
|
147
|
+
return model
|
|
148
|
+
|
|
149
|
+
async def embed(self, text: str) -> list[float]:
|
|
150
|
+
loop = asyncio.get_running_loop()
|
|
151
|
+
result = await loop.run_in_executor(
|
|
152
|
+
None, functools.partial(self._model.encode, [text], normalize_embeddings=True)
|
|
153
|
+
)
|
|
154
|
+
return result[0].tolist()
|
|
155
|
+
|
|
156
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
157
|
+
if not texts:
|
|
158
|
+
return []
|
|
159
|
+
loop = asyncio.get_running_loop()
|
|
160
|
+
result = await loop.run_in_executor(
|
|
161
|
+
None,
|
|
162
|
+
functools.partial(
|
|
163
|
+
self._model.encode,
|
|
164
|
+
texts,
|
|
165
|
+
batch_size=self._config.max_batch_size,
|
|
166
|
+
normalize_embeddings=True,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
return result.tolist()
|
|
170
|
+
|
|
171
|
+
async def close(self) -> None:
|
|
172
|
+
self._model = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# MLX provider (Apple Silicon)
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
class MLXEmbeddingProvider(EmbeddingProvider):
|
|
180
|
+
"""Apple Silicon embedding via sentence-transformers with PyTorch MPS backend.
|
|
181
|
+
|
|
182
|
+
NOTE: This is the legacy provider. For true native MLX performance,
|
|
183
|
+
use ``NativeMLXEmbeddingProvider`` (provider="native_mlx").
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
187
|
+
super().__init__(config)
|
|
188
|
+
self._model = None
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def dimensions(self) -> int:
|
|
192
|
+
return self._config.dimensions
|
|
193
|
+
|
|
194
|
+
async def warmup(self) -> None:
|
|
195
|
+
# PyTorch MPS backend deadlocks when model initialization runs in
|
|
196
|
+
# asyncio's ThreadPoolExecutor during FastAPI lifespan startup.
|
|
197
|
+
# Load synchronously in the main thread — startup is not serving
|
|
198
|
+
# requests yet, so brief blocking is acceptable.
|
|
199
|
+
# See: https://github.com/Aevonix/ColonyAI/issues/17
|
|
200
|
+
self._model = self._load_model()
|
|
201
|
+
self._model.encode(["warmup"])
|
|
202
|
+
|
|
203
|
+
def _load_model(self):
|
|
204
|
+
from sentence_transformers import SentenceTransformer
|
|
205
|
+
|
|
206
|
+
# sentence-transformers auto-detects MPS on Apple Silicon
|
|
207
|
+
device = self._config.device or "mps"
|
|
208
|
+
kwargs: dict = {"device": device}
|
|
209
|
+
if self._config.cache_dir:
|
|
210
|
+
kwargs["cache_folder"] = self._config.cache_dir
|
|
211
|
+
model = SentenceTransformer(self._config.model_id, **kwargs)
|
|
212
|
+
return model
|
|
213
|
+
|
|
214
|
+
async def embed(self, text: str) -> list[float]:
|
|
215
|
+
loop = asyncio.get_running_loop()
|
|
216
|
+
result = await loop.run_in_executor(
|
|
217
|
+
None, functools.partial(self._model.encode, [text], normalize_embeddings=True)
|
|
218
|
+
)
|
|
219
|
+
return result[0].tolist()
|
|
220
|
+
|
|
221
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
222
|
+
if not texts:
|
|
223
|
+
return []
|
|
224
|
+
loop = asyncio.get_running_loop()
|
|
225
|
+
result = await loop.run_in_executor(
|
|
226
|
+
None,
|
|
227
|
+
functools.partial(
|
|
228
|
+
self._model.encode,
|
|
229
|
+
texts,
|
|
230
|
+
batch_size=self._config.max_batch_size,
|
|
231
|
+
normalize_embeddings=True,
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
return result.tolist()
|
|
235
|
+
|
|
236
|
+
async def close(self) -> None:
|
|
237
|
+
self._model = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Native MLX provider (Apple Silicon via true MLX framework)
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
class NativeMLXEmbeddingProvider(EmbeddingProvider):
|
|
245
|
+
"""Apple Silicon embedding via the native MLX framework (mlx-embeddings).
|
|
246
|
+
|
|
247
|
+
Uses ``mlx_embeddings`` to load models directly into MLX arrays, avoiding
|
|
248
|
+
the PyTorch MPS overhead. Supports both original HuggingFace models
|
|
249
|
+
(converted on-the-fly) and pre-converted ``mlx-community`` checkpoints.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
253
|
+
super().__init__(config)
|
|
254
|
+
self._model = None
|
|
255
|
+
self._tokenizer = None
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def dimensions(self) -> int:
|
|
259
|
+
return self._config.dimensions
|
|
260
|
+
|
|
261
|
+
async def warmup(self) -> None:
|
|
262
|
+
# MLX model loading can trigger lazy weight evaluation; keep it
|
|
263
|
+
# synchronous in the main thread just like the MPS provider.
|
|
264
|
+
self._model, self._tokenizer = self._load_model()
|
|
265
|
+
self._warmup_inference()
|
|
266
|
+
|
|
267
|
+
def _load_model(self):
|
|
268
|
+
from mlx_embeddings import load
|
|
269
|
+
|
|
270
|
+
kwargs: dict = {"lazy": False}
|
|
271
|
+
if self._config.cache_dir:
|
|
272
|
+
kwargs["cache_dir"] = self._config.cache_dir
|
|
273
|
+
model, tokenizer = load(self._config.model_id, **kwargs)
|
|
274
|
+
return model, tokenizer
|
|
275
|
+
|
|
276
|
+
def _warmup_inference(self):
|
|
277
|
+
from mlx_embeddings import generate
|
|
278
|
+
|
|
279
|
+
generate(self._model, self._tokenizer, "warmup", max_length=512)
|
|
280
|
+
|
|
281
|
+
async def embed(self, text: str) -> list[float]:
|
|
282
|
+
loop = asyncio.get_running_loop()
|
|
283
|
+
result = await loop.run_in_executor(
|
|
284
|
+
None,
|
|
285
|
+
functools.partial(
|
|
286
|
+
self._infer, [text], max_length=512
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
return result[0]
|
|
290
|
+
|
|
291
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
292
|
+
if not texts:
|
|
293
|
+
return []
|
|
294
|
+
loop = asyncio.get_running_loop()
|
|
295
|
+
result = await loop.run_in_executor(
|
|
296
|
+
None,
|
|
297
|
+
functools.partial(
|
|
298
|
+
self._infer, texts, max_length=512
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
def _infer(self, texts, max_length: int = 512):
|
|
304
|
+
from mlx_embeddings import generate
|
|
305
|
+
|
|
306
|
+
outputs = generate(self._model, self._tokenizer, texts, max_length=max_length)
|
|
307
|
+
# text_embeds is an mx.array of shape (batch, dims)
|
|
308
|
+
return outputs.text_embeds.tolist()
|
|
309
|
+
|
|
310
|
+
async def close(self) -> None:
|
|
311
|
+
self._model = None
|
|
312
|
+
self._tokenizer = None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# Provider factory
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
def make_provider(config: EmbeddingConfig) -> EmbeddingProvider:
|
|
320
|
+
"""Instantiate the correct provider for the given config.
|
|
321
|
+
|
|
322
|
+
Supported providers: cuda, cpu, mlx, native_mlx, openai_api.
|
|
323
|
+
"""
|
|
324
|
+
from apsimo.vector.openai_provider import OpenAIAPIEmbeddingProvider
|
|
325
|
+
|
|
326
|
+
providers = {
|
|
327
|
+
"cuda": CUDAEmbeddingProvider,
|
|
328
|
+
"cpu": CPUEmbeddingProvider,
|
|
329
|
+
"mlx": MLXEmbeddingProvider,
|
|
330
|
+
"native_mlx": NativeMLXEmbeddingProvider,
|
|
331
|
+
"openai_api": OpenAIAPIEmbeddingProvider,
|
|
332
|
+
}
|
|
333
|
+
provider_cls = providers.get(config.provider)
|
|
334
|
+
if provider_cls is None:
|
|
335
|
+
if config.provider == "skip":
|
|
336
|
+
logger.info("Embedding provider set to 'skip' — embeddings disabled")
|
|
337
|
+
return None
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"Unknown embedding provider {config.provider!r}. "
|
|
340
|
+
f"Choose from: {', '.join(providers)}, skip"
|
|
341
|
+
)
|
|
342
|
+
return provider_cls(config)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
# Embedding pipeline (caching + latency monitoring)
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
class EmbeddingPipeline:
|
|
350
|
+
"""Wraps an EmbeddingProvider with LRU caching and latency monitoring.
|
|
351
|
+
|
|
352
|
+
Supports both text-only and multimodal embedding. When multimodal is
|
|
353
|
+
enabled, the pipeline holds both a text provider and a multimodal provider.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
LATENCY_WARN_MS = 500.0 # Raised for Qwen3-Embedding-8B on CUDA
|
|
357
|
+
|
|
358
|
+
def __init__(
|
|
359
|
+
self,
|
|
360
|
+
provider: EmbeddingProvider,
|
|
361
|
+
cache_size: int = 4096,
|
|
362
|
+
multimodal_provider: Any = None, # MultimodalEmbeddingProvider
|
|
363
|
+
image_store: Any = None, # LocalImageStore or EmbedOnlyStore
|
|
364
|
+
) -> None:
|
|
365
|
+
self._provider = provider
|
|
366
|
+
self._multimodal_provider = multimodal_provider
|
|
367
|
+
self._image_store = image_store
|
|
368
|
+
self._cache: dict[str, list[float]] = {}
|
|
369
|
+
self._cache_order: list[str] = []
|
|
370
|
+
self._cache_size = cache_size
|
|
371
|
+
self._multimodal_enabled = multimodal_provider is not None
|
|
372
|
+
self.query_instruction = os.environ.get('COLONY_EMBED_QUERY_INSTRUCTION',
|
|
373
|
+
'Instruct: Given a search query, retrieve relevant memories that answer it\nQuery: ')
|
|
374
|
+
self._index_identity = None
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def is_multimodal(self) -> bool:
|
|
378
|
+
"""True when multimodal embedding is active."""
|
|
379
|
+
return self._multimodal_enabled
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def modalities(self) -> list[str]:
|
|
383
|
+
"""Supported modalities."""
|
|
384
|
+
if self._multimodal_enabled and self._multimodal_provider:
|
|
385
|
+
return self._multimodal_provider.modalities
|
|
386
|
+
return ["text"]
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def dimensions(self) -> int:
|
|
390
|
+
return self._provider.dimensions
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def index_identity(self):
|
|
394
|
+
from apsimo.vector.indexes import EmbeddingIdentity
|
|
395
|
+
return self._index_identity or EmbeddingIdentity.from_pipeline(self)
|
|
396
|
+
|
|
397
|
+
async def warmup(self) -> None:
|
|
398
|
+
"""Delegate warmup to the underlying provider."""
|
|
399
|
+
await self._provider.warmup()
|
|
400
|
+
from apsimo.vector.indexes import EmbeddingIdentity
|
|
401
|
+
self._index_identity = EmbeddingIdentity.from_pipeline(self)
|
|
402
|
+
|
|
403
|
+
def _check_identity(self):
|
|
404
|
+
if self._index_identity is not None:
|
|
405
|
+
from apsimo.vector.indexes import EmbeddingIdentity, IncompatibleIndex
|
|
406
|
+
if EmbeddingIdentity.from_pipeline(self) != self._index_identity:
|
|
407
|
+
raise IncompatibleIndex('Embedding configuration changed; use a new pipeline and index generation')
|
|
408
|
+
|
|
409
|
+
async def embed_query(self, query: str) -> list[float]:
|
|
410
|
+
return await self.embed(self.query_instruction + query)
|
|
411
|
+
|
|
412
|
+
async def health_check(self) -> dict[str, Any]:
|
|
413
|
+
"""Verify the embedder is loaded and producing valid output.
|
|
414
|
+
|
|
415
|
+
Returns a dict with keys:
|
|
416
|
+
provider, model, dims, latency_ms, status ("ok"|"error"),
|
|
417
|
+
and optionally error (str) if status is "error".
|
|
418
|
+
"""
|
|
419
|
+
import math
|
|
420
|
+
|
|
421
|
+
model_id = ""
|
|
422
|
+
provider_name = ""
|
|
423
|
+
dims = 0
|
|
424
|
+
if hasattr(self._provider, "_config"):
|
|
425
|
+
model_id = self._provider._config.model_id
|
|
426
|
+
provider_name = self._provider._config.provider
|
|
427
|
+
dims = self._provider._config.dimensions
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
if self._provider is None:
|
|
431
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": 0, "status": "error", "error": "provider not initialized"}
|
|
432
|
+
|
|
433
|
+
t0 = time.monotonic()
|
|
434
|
+
vector = await self._provider.embed("colony health check")
|
|
435
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
436
|
+
|
|
437
|
+
# Validate vector
|
|
438
|
+
if not vector:
|
|
439
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": elapsed_ms, "status": "error", "error": "empty vector returned"}
|
|
440
|
+
if len(vector) != dims:
|
|
441
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": elapsed_ms, "status": "error", "error": f"dimension mismatch: expected {dims}, got {len(vector)}"}
|
|
442
|
+
if any(math.isnan(v) or math.isinf(v) for v in vector):
|
|
443
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": elapsed_ms, "status": "error", "error": "vector contains NaN or Inf"}
|
|
444
|
+
if all(v == 0.0 for v in vector):
|
|
445
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": elapsed_ms, "status": "error", "error": "vector is all zeros"}
|
|
446
|
+
|
|
447
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": round(elapsed_ms, 1), "status": "ok"}
|
|
448
|
+
except Exception as exc:
|
|
449
|
+
return {"provider": provider_name, "model": model_id, "dims": dims, "latency_ms": 0, "status": "error", "error": str(exc)}
|
|
450
|
+
|
|
451
|
+
async def embed(self, text: str) -> list[float]:
|
|
452
|
+
"""Single embed with LRU caching and latency monitoring."""
|
|
453
|
+
self._check_identity()
|
|
454
|
+
cached = self._cache.get(text)
|
|
455
|
+
if cached is not None:
|
|
456
|
+
return cached
|
|
457
|
+
|
|
458
|
+
t0 = time.monotonic()
|
|
459
|
+
vector = await self._provider.embed(text)
|
|
460
|
+
elapsed_ms = (time.monotonic() - t0) * 1000
|
|
461
|
+
|
|
462
|
+
if elapsed_ms > self.LATENCY_WARN_MS:
|
|
463
|
+
logger.warning(
|
|
464
|
+
"Embedding latency %.0fms exceeds %.0fms threshold — "
|
|
465
|
+
"check COLONY_EMBED_PROVIDER and model config",
|
|
466
|
+
elapsed_ms,
|
|
467
|
+
self.LATENCY_WARN_MS,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
self._put_cache(text, vector)
|
|
471
|
+
return vector
|
|
472
|
+
|
|
473
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
474
|
+
"""Explicit batch embed — bypasses auto-batch window."""
|
|
475
|
+
self._check_identity()
|
|
476
|
+
if not texts:
|
|
477
|
+
return []
|
|
478
|
+
|
|
479
|
+
# Check cache for each; collect uncached
|
|
480
|
+
results: list[Optional[list[float]]] = []
|
|
481
|
+
uncached_indices: list[int] = []
|
|
482
|
+
uncached_texts: list[str] = []
|
|
483
|
+
|
|
484
|
+
for i, text in enumerate(texts):
|
|
485
|
+
cached = self._cache.get(text)
|
|
486
|
+
if cached is not None:
|
|
487
|
+
results.append(cached)
|
|
488
|
+
else:
|
|
489
|
+
results.append(None)
|
|
490
|
+
uncached_indices.append(i)
|
|
491
|
+
uncached_texts.append(text)
|
|
492
|
+
|
|
493
|
+
if uncached_texts:
|
|
494
|
+
vectors = await self._provider.embed_batch(uncached_texts)
|
|
495
|
+
for idx, vec in zip(uncached_indices, vectors):
|
|
496
|
+
results[idx] = vec
|
|
497
|
+
self._put_cache(uncached_texts[uncached_indices.index(idx)], vec)
|
|
498
|
+
|
|
499
|
+
return results # type: ignore[return-value]
|
|
500
|
+
|
|
501
|
+
# ------------------------------------------------------------------
|
|
502
|
+
# Multimodal methods
|
|
503
|
+
# ------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
async def embed_image(self, source: str | bytes, mime_type: str = "", caption: str = "") -> tuple[list[float], dict[str, Any]]:
|
|
506
|
+
"""Embed a single image. Returns (vector, metadata).
|
|
507
|
+
|
|
508
|
+
Parameters
|
|
509
|
+
----------
|
|
510
|
+
source : str | bytes
|
|
511
|
+
File path, URL, base64 string, or raw bytes.
|
|
512
|
+
mime_type : str
|
|
513
|
+
Optional MIME type hint.
|
|
514
|
+
caption : str
|
|
515
|
+
Optional user-provided caption.
|
|
516
|
+
|
|
517
|
+
Returns
|
|
518
|
+
-------
|
|
519
|
+
tuple[list[float], dict]
|
|
520
|
+
Vector and metadata dict with image_hash, image_ref, caption,
|
|
521
|
+
thumbnail_ref, width, height, exif.
|
|
522
|
+
"""
|
|
523
|
+
if not self._multimodal_enabled or not self._multimodal_provider:
|
|
524
|
+
raise ValueError("Multimodal not enabled. Set COLONY_MULTIMODAL=true and restart.")
|
|
525
|
+
|
|
526
|
+
from apsimo.vector.multimodal_types import EmbedInput, Modality
|
|
527
|
+
from apsimo.vector.image_preprocess import (
|
|
528
|
+
compute_image_hash, extract_exif, load_image, resize_image,
|
|
529
|
+
strip_gps_exif, validate_image,
|
|
530
|
+
)
|
|
531
|
+
from apsimo.vector.safety_image import check_image, ImageCheckLevel
|
|
532
|
+
|
|
533
|
+
# Load image
|
|
534
|
+
data, detected_mime = await load_image(source, mime_type)
|
|
535
|
+
mime = detected_mime or "image/jpeg"
|
|
536
|
+
|
|
537
|
+
# Validate
|
|
538
|
+
errors = validate_image(data)
|
|
539
|
+
if errors:
|
|
540
|
+
raise ValueError(f"Invalid image: {'; '.join(errors)}")
|
|
541
|
+
|
|
542
|
+
# Safety check
|
|
543
|
+
check_level = ImageCheckLevel(os.environ.get("COLONY_IMAGE_CHECK", "basic"))
|
|
544
|
+
result = await check_image(data, mime, level=check_level)
|
|
545
|
+
if not result.safe:
|
|
546
|
+
raise ValueError(f"Image check failed: {result.reason}")
|
|
547
|
+
|
|
548
|
+
# Resize if needed
|
|
549
|
+
data, width, height = resize_image(data)
|
|
550
|
+
|
|
551
|
+
# Strip GPS from stored image if configured
|
|
552
|
+
strip_gps = os.environ.get("COLONY_STRIP_EXIF_GPS", "true").lower() == "true"
|
|
553
|
+
stored_data = strip_gps_exif(data) if strip_gps else data
|
|
554
|
+
|
|
555
|
+
# Extract EXIF before stripping
|
|
556
|
+
exif = extract_exif(data)
|
|
557
|
+
|
|
558
|
+
# Compute hash
|
|
559
|
+
img_hash = compute_image_hash(data)
|
|
560
|
+
|
|
561
|
+
# Build ImageInput
|
|
562
|
+
from apsimo.vector.multimodal_types import ImageInput
|
|
563
|
+
image_input = ImageInput(
|
|
564
|
+
data=data, mime_type=mime, width=width, height=height,
|
|
565
|
+
original_path=source if isinstance(source, str) and not source.startswith("data:") else "",
|
|
566
|
+
image_hash=img_hash, exif=exif, caption=caption,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Store image
|
|
570
|
+
image_ref = ""
|
|
571
|
+
thumbnail_ref = ""
|
|
572
|
+
if self._image_store:
|
|
573
|
+
stored = await self._image_store.store(image_input)
|
|
574
|
+
image_ref = stored.path
|
|
575
|
+
thumbnail_ref = stored.thumbnail_path
|
|
576
|
+
|
|
577
|
+
# Auto-caption if no caption provided
|
|
578
|
+
if not caption:
|
|
579
|
+
from apsimo.vector.caption import caption_image
|
|
580
|
+
llm_cfg = getattr(self, "_llm_config", None)
|
|
581
|
+
caption = await caption_image(image_input, llm_config=llm_cfg)
|
|
582
|
+
|
|
583
|
+
# Embed
|
|
584
|
+
vector = await self._multimodal_provider.embed_image(image_input)
|
|
585
|
+
|
|
586
|
+
metadata = {
|
|
587
|
+
"modality": "image",
|
|
588
|
+
"image_hash": img_hash,
|
|
589
|
+
"image_ref": image_ref,
|
|
590
|
+
"thumbnail_ref": thumbnail_ref,
|
|
591
|
+
"caption": caption,
|
|
592
|
+
"width": width,
|
|
593
|
+
"height": height,
|
|
594
|
+
"model_id": self._multimodal_provider.model_id,
|
|
595
|
+
"embedded_at": time.time(),
|
|
596
|
+
}
|
|
597
|
+
# Add EXIF data
|
|
598
|
+
if exif.get("captured_at"):
|
|
599
|
+
metadata["captured_at"] = exif["captured_at"]
|
|
600
|
+
if exif.get("gps_lat") and exif.get("gps_lon"):
|
|
601
|
+
metadata["gps_lat"] = exif["gps_lat"]
|
|
602
|
+
metadata["gps_lon"] = exif["gps_lon"]
|
|
603
|
+
|
|
604
|
+
return vector, metadata
|
|
605
|
+
|
|
606
|
+
async def embed_images(
|
|
607
|
+
self,
|
|
608
|
+
sources: list[str | bytes],
|
|
609
|
+
mime_types: list[str] | None = None,
|
|
610
|
+
captions: list[str] | None = None,
|
|
611
|
+
) -> list[tuple[list[float], dict[str, Any]]]:
|
|
612
|
+
"""Embed multiple images. Returns list of (vector, metadata) tuples."""
|
|
613
|
+
mimes = mime_types or [""] * len(sources)
|
|
614
|
+
caps = captions or [""] * len(sources)
|
|
615
|
+
results = []
|
|
616
|
+
for src, mime, cap in zip(sources, mimes, caps):
|
|
617
|
+
vec, meta = await self.embed_image(src, mime_type=mime, caption=cap)
|
|
618
|
+
results.append((vec, meta))
|
|
619
|
+
return results
|
|
620
|
+
|
|
621
|
+
async def embed_mixed(self, items: list[dict]) -> list[tuple[list[float], dict[str, Any]]]:
|
|
622
|
+
"""Embed a mix of text and image inputs.
|
|
623
|
+
|
|
624
|
+
Each item: {"type": "text", "content": "..."} or
|
|
625
|
+
{"type": "image", "content": "<path/url/base64>", "mime_type": "...", "caption": "..."}
|
|
626
|
+
"""
|
|
627
|
+
results = []
|
|
628
|
+
for item in items:
|
|
629
|
+
item_type = item.get("type", "text")
|
|
630
|
+
content = item.get("content", "")
|
|
631
|
+
if item_type == "text":
|
|
632
|
+
vec = await self.embed(content)
|
|
633
|
+
meta = {"modality": "text", "model_id": self._provider._config.model_id, "embedded_at": time.time()}
|
|
634
|
+
results.append((vec, meta))
|
|
635
|
+
elif item_type == "image":
|
|
636
|
+
vec, meta = await self.embed_image(
|
|
637
|
+
content,
|
|
638
|
+
mime_type=item.get("mime_type", ""),
|
|
639
|
+
caption=item.get("caption", ""),
|
|
640
|
+
)
|
|
641
|
+
results.append((vec, meta))
|
|
642
|
+
else:
|
|
643
|
+
raise ValueError(f"Unsupported input type: {item_type}")
|
|
644
|
+
return results
|
|
645
|
+
|
|
646
|
+
def set_llm_config(self, config: dict[str, Any]) -> None:
|
|
647
|
+
"""Set LLM config for auto-captioning."""
|
|
648
|
+
self._llm_config = config
|
|
649
|
+
|
|
650
|
+
@property
|
|
651
|
+
def embed_fn(self):
|
|
652
|
+
"""Drop-in callable for ColonyGraph.set_embed_fn()."""
|
|
653
|
+
return self.embed
|
|
654
|
+
|
|
655
|
+
async def close(self) -> None:
|
|
656
|
+
"""Release underlying provider resources."""
|
|
657
|
+
await self._provider.close()
|
|
658
|
+
if self._multimodal_provider:
|
|
659
|
+
await self._multimodal_provider.close()
|
|
660
|
+
self._cache.clear()
|
|
661
|
+
self._cache_order.clear()
|
|
662
|
+
|
|
663
|
+
def _put_cache(self, key: str, value: list[float]) -> None:
|
|
664
|
+
if key in self._cache:
|
|
665
|
+
return
|
|
666
|
+
if len(self._cache_order) >= self._cache_size:
|
|
667
|
+
evict = self._cache_order.pop(0)
|
|
668
|
+
self._cache.pop(evict, None)
|
|
669
|
+
self._cache[key] = value
|
|
670
|
+
self._cache_order.append(key)
|