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,417 @@
|
|
|
1
|
+
"""Colony Vector — multimodal embedding providers.
|
|
2
|
+
|
|
3
|
+
CUDA, CPU, and API-based providers for text+image embedding.
|
|
4
|
+
All providers produce vectors in the same embedding space for a given model,
|
|
5
|
+
enabling cross-modal search (text query → image result, and vice versa).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import io
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
from apsimo.vector.config import EmbeddingConfig
|
|
18
|
+
from apsimo.vector.multimodal_types import EmbedInput, EmbedResult, ImageInput, Modality
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MultimodalEmbeddingProvider(ABC):
|
|
24
|
+
"""Base class for multimodal embedding providers."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
27
|
+
self._config = config
|
|
28
|
+
self._model = None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def model_id(self) -> str:
|
|
32
|
+
return self._config.model_id
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def dimensions(self) -> int:
|
|
36
|
+
return self._config.dimensions
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def modalities(self) -> list[str]:
|
|
40
|
+
return ["text", "image"]
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def embed_text(self, text: str) -> list[float]:
|
|
44
|
+
"""Embed a single text string."""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def embed_image(self, image: ImageInput) -> list[float]:
|
|
48
|
+
"""Embed a single image."""
|
|
49
|
+
|
|
50
|
+
async def embed_batch_text(self, texts: list[str]) -> list[list[float]]:
|
|
51
|
+
"""Embed multiple texts. Default: sequential call."""
|
|
52
|
+
return [await self.embed_text(t) for t in texts]
|
|
53
|
+
|
|
54
|
+
async def embed_batch_image(self, images: list[ImageInput]) -> list[list[float]]:
|
|
55
|
+
"""Embed multiple images. Default: sequential with small batches."""
|
|
56
|
+
results = []
|
|
57
|
+
# Images are memory-heavy — process in small batches
|
|
58
|
+
batch_size = 4
|
|
59
|
+
for i in range(0, len(images), batch_size):
|
|
60
|
+
batch = images[i : i + batch_size]
|
|
61
|
+
for img in batch:
|
|
62
|
+
vec = await self.embed_image(img)
|
|
63
|
+
results.append(vec)
|
|
64
|
+
return results
|
|
65
|
+
|
|
66
|
+
async def embed_mixed(self, items: list[EmbedInput]) -> list[list[float]]:
|
|
67
|
+
"""Embed a mix of text and image inputs."""
|
|
68
|
+
results: list[Optional[list[float]]] = [None] * len(items)
|
|
69
|
+
|
|
70
|
+
# Process texts and images in parallel batches
|
|
71
|
+
text_indices = [i for i, item in enumerate(items) if item.modality == Modality.TEXT]
|
|
72
|
+
image_indices = [i for i, item in enumerate(items) if item.modality == Modality.IMAGE]
|
|
73
|
+
|
|
74
|
+
if text_indices:
|
|
75
|
+
texts = [items[i].content for i in text_indices]
|
|
76
|
+
text_vecs = await self.embed_batch_text(texts)
|
|
77
|
+
for idx, vec in zip(text_indices, text_vecs):
|
|
78
|
+
results[idx] = vec
|
|
79
|
+
|
|
80
|
+
if image_indices:
|
|
81
|
+
# Load images from content (path, URL, or base64)
|
|
82
|
+
image_inputs = []
|
|
83
|
+
for i in image_indices:
|
|
84
|
+
img_input = await self._load_image_input(items[i])
|
|
85
|
+
image_inputs.append(img_input)
|
|
86
|
+
image_vecs = await self.embed_batch_image(image_inputs)
|
|
87
|
+
for idx, vec in zip(image_indices, image_vecs):
|
|
88
|
+
results[idx] = vec
|
|
89
|
+
|
|
90
|
+
return [r for r in results if r is not None]
|
|
91
|
+
|
|
92
|
+
async def _load_image_input(self, item: EmbedInput) -> ImageInput:
|
|
93
|
+
"""Convert an EmbedInput with image modality to ImageInput."""
|
|
94
|
+
from apsimo.vector.image_preprocess import (
|
|
95
|
+
compute_image_hash,
|
|
96
|
+
extract_exif,
|
|
97
|
+
load_image,
|
|
98
|
+
resize_image,
|
|
99
|
+
validate_image,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
data, mime = await load_image(item.content, item.mime_type)
|
|
103
|
+
|
|
104
|
+
# Validate
|
|
105
|
+
errors = validate_image(data)
|
|
106
|
+
if errors:
|
|
107
|
+
raise ValueError(f"Invalid image: {'; '.join(errors)}")
|
|
108
|
+
|
|
109
|
+
# Resize if needed
|
|
110
|
+
data, width, height = resize_image(data)
|
|
111
|
+
|
|
112
|
+
# Extract EXIF
|
|
113
|
+
exif = extract_exif(data)
|
|
114
|
+
|
|
115
|
+
# Compute hash
|
|
116
|
+
img_hash = compute_image_hash(data)
|
|
117
|
+
|
|
118
|
+
return ImageInput(
|
|
119
|
+
data=data,
|
|
120
|
+
mime_type=mime or "image/jpeg",
|
|
121
|
+
width=width,
|
|
122
|
+
height=height,
|
|
123
|
+
original_path=item.content if not item.content.startswith("data:") else "",
|
|
124
|
+
image_hash=img_hash,
|
|
125
|
+
exif=exif,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def warmup(self) -> None:
|
|
129
|
+
"""Load model weights. Default: embed a test string."""
|
|
130
|
+
try:
|
|
131
|
+
_ = await self.embed_text("colony multimodal warmup")
|
|
132
|
+
logger.info("Multimodal provider warmed up: %s", self.model_id)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
logger.warning("Multimodal warmup failed for %s: %s", self.model_id, exc)
|
|
135
|
+
|
|
136
|
+
async def close(self) -> None:
|
|
137
|
+
"""Release model resources."""
|
|
138
|
+
self._model = None
|
|
139
|
+
|
|
140
|
+
async def health_check(self) -> dict[str, Any]:
|
|
141
|
+
"""Verify provider is producing valid output for both modalities."""
|
|
142
|
+
import math
|
|
143
|
+
|
|
144
|
+
result: dict[str, Any] = {
|
|
145
|
+
"provider": self._config.provider,
|
|
146
|
+
"model": self.model_id,
|
|
147
|
+
"dims": self.dimensions,
|
|
148
|
+
"modalities": self.modalities,
|
|
149
|
+
"status": "error",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# Text embedding check
|
|
154
|
+
t0 = time.monotonic()
|
|
155
|
+
text_vec = await self.embed_text("colony health check")
|
|
156
|
+
text_ms = (time.monotonic() - t0) * 1000
|
|
157
|
+
|
|
158
|
+
if len(text_vec) != self.dimensions:
|
|
159
|
+
result["error"] = f"text dimension mismatch: expected {self.dimensions}, got {len(text_vec)}"
|
|
160
|
+
return result
|
|
161
|
+
if any(math.isnan(v) or math.isinf(v) for v in text_vec):
|
|
162
|
+
result["error"] = "text vector contains NaN or Inf"
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
result["text_latency_ms"] = round(text_ms, 1)
|
|
166
|
+
|
|
167
|
+
# Create a minimal test image (1x1 white JPEG)
|
|
168
|
+
test_image = _test_image_jpeg()
|
|
169
|
+
from apsimo.vector.image_preprocess import compute_image_hash
|
|
170
|
+
test_input = ImageInput(
|
|
171
|
+
data=test_image,
|
|
172
|
+
mime_type="image/jpeg",
|
|
173
|
+
image_hash=compute_image_hash(test_image),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
t0 = time.monotonic()
|
|
177
|
+
img_vec = await self.embed_image(test_input)
|
|
178
|
+
img_ms = (time.monotonic() - t0) * 1000
|
|
179
|
+
|
|
180
|
+
if len(img_vec) != self.dimensions:
|
|
181
|
+
result["error"] = f"image dimension mismatch: expected {self.dimensions}, got {len(img_vec)}"
|
|
182
|
+
return result
|
|
183
|
+
if any(math.isnan(v) or math.isinf(v) for v in img_vec):
|
|
184
|
+
result["error"] = "image vector contains NaN or Inf"
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
result["image_latency_ms"] = round(img_ms, 1)
|
|
188
|
+
result["status"] = "ok"
|
|
189
|
+
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
result["error"] = str(exc)
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _test_image_jpeg() -> bytes:
|
|
197
|
+
"""Generate a minimal 1x1 white JPEG for health checks."""
|
|
198
|
+
# Smallest valid JPEG
|
|
199
|
+
return bytes([
|
|
200
|
+
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
|
|
201
|
+
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
|
|
202
|
+
0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,
|
|
203
|
+
0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
|
|
204
|
+
0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
|
|
205
|
+
0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
|
|
206
|
+
0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
|
|
207
|
+
0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01,
|
|
208
|
+
0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00,
|
|
209
|
+
0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
210
|
+
0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
211
|
+
0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03,
|
|
212
|
+
0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
|
|
213
|
+
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
|
|
214
|
+
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
|
|
215
|
+
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72,
|
|
216
|
+
0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
|
|
217
|
+
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45,
|
|
218
|
+
0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
|
|
219
|
+
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
|
|
220
|
+
0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
|
221
|
+
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3,
|
|
222
|
+
0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
|
|
223
|
+
0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
|
|
224
|
+
0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
|
|
225
|
+
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4,
|
|
226
|
+
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
|
|
227
|
+
0x00, 0x00, 0x3F, 0x00, 0x7B, 0x94, 0x11, 0x00, 0x00, 0x00, 0x00, 0xFF,
|
|
228
|
+
0xD9,
|
|
229
|
+
])
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# CUDA Provider
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class CUDAMultimodalProvider(MultimodalEmbeddingProvider):
|
|
238
|
+
"""Multimodal embedding on NVIDIA GPU via sentence-transformers or transformers."""
|
|
239
|
+
|
|
240
|
+
async def embed_text(self, text: str) -> list[float]:
|
|
241
|
+
return await asyncio.to_thread(self._sync_embed_text, text)
|
|
242
|
+
|
|
243
|
+
async def embed_image(self, image: ImageInput) -> list[float]:
|
|
244
|
+
return await asyncio.to_thread(self._sync_embed_image, image)
|
|
245
|
+
|
|
246
|
+
def _ensure_model(self):
|
|
247
|
+
if self._model is not None:
|
|
248
|
+
return
|
|
249
|
+
from sentence_transformers import SentenceTransformer
|
|
250
|
+
self._model = SentenceTransformer(self._config.model_id, device="cuda")
|
|
251
|
+
|
|
252
|
+
def _sync_embed_text(self, text: str) -> list[float]:
|
|
253
|
+
self._ensure_model()
|
|
254
|
+
vec = self._model.encode(text, normalize_embeddings=True)
|
|
255
|
+
return vec.tolist()
|
|
256
|
+
|
|
257
|
+
def _sync_embed_image(self, image: ImageInput) -> list[float]:
|
|
258
|
+
self._ensure_model()
|
|
259
|
+
from PIL import Image as PILImage
|
|
260
|
+
img = PILImage.open(io.BytesIO(image.data))
|
|
261
|
+
try:
|
|
262
|
+
vec = self._model.encode(img, normalize_embeddings=True)
|
|
263
|
+
except (TypeError, AttributeError):
|
|
264
|
+
# Some models don't accept PIL Image directly — try with image_processor
|
|
265
|
+
try:
|
|
266
|
+
from transformers import AutoProcessor
|
|
267
|
+
processor = AutoProcessor.from_pretrained(self._config.model_id)
|
|
268
|
+
inputs = processor(images=img, return_tensors="pt").to("cuda")
|
|
269
|
+
import torch
|
|
270
|
+
with torch.no_grad():
|
|
271
|
+
outputs = self._model.get_text_features(**inputs) if hasattr(self._model, "get_text_features") else self._model(**inputs)
|
|
272
|
+
if hasattr(outputs, "image_embeds"):
|
|
273
|
+
vec = outputs.image_embeds[0].cpu().numpy()
|
|
274
|
+
elif hasattr(outputs, "last_hidden_state"):
|
|
275
|
+
vec = outputs.last_hidden_state[0].mean(dim=0).cpu().numpy()
|
|
276
|
+
else:
|
|
277
|
+
vec = outputs[0].cpu().numpy() if isinstance(outputs, (list, tuple)) else outputs.cpu().numpy()
|
|
278
|
+
import numpy as np
|
|
279
|
+
norm = np.linalg.norm(vec)
|
|
280
|
+
if norm > 0:
|
|
281
|
+
vec = vec / norm
|
|
282
|
+
vec = vec.tolist()
|
|
283
|
+
except Exception as exc:
|
|
284
|
+
raise ValueError(f"Model {self._config.model_id} does not support image embedding: {exc}") from exc
|
|
285
|
+
return vec if isinstance(vec, list) else vec.tolist()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# CPU Provider
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class CPUMultimodalProvider(MultimodalEmbeddingProvider):
|
|
294
|
+
"""Multimodal embedding on CPU. Slow but works everywhere."""
|
|
295
|
+
|
|
296
|
+
async def embed_text(self, text: str) -> list[float]:
|
|
297
|
+
return await asyncio.to_thread(self._sync_embed_text, text)
|
|
298
|
+
|
|
299
|
+
async def embed_image(self, image: ImageInput) -> list[float]:
|
|
300
|
+
return await asyncio.to_thread(self._sync_embed_image, image)
|
|
301
|
+
|
|
302
|
+
def _ensure_model(self):
|
|
303
|
+
if self._model is not None:
|
|
304
|
+
return
|
|
305
|
+
from sentence_transformers import SentenceTransformer
|
|
306
|
+
self._model = SentenceTransformer(self._config.model_id, device="cpu")
|
|
307
|
+
|
|
308
|
+
def _sync_embed_text(self, text: str) -> list[float]:
|
|
309
|
+
self._ensure_model()
|
|
310
|
+
vec = self._model.encode(text, normalize_embeddings=True)
|
|
311
|
+
return vec.tolist()
|
|
312
|
+
|
|
313
|
+
def _sync_embed_image(self, image: ImageInput) -> list[float]:
|
|
314
|
+
self._ensure_model()
|
|
315
|
+
from PIL import Image as PILImage
|
|
316
|
+
img = PILImage.open(io.BytesIO(image.data))
|
|
317
|
+
try:
|
|
318
|
+
vec = self._model.encode(img, normalize_embeddings=True)
|
|
319
|
+
except (TypeError, AttributeError):
|
|
320
|
+
# Some models don't accept PIL Image directly
|
|
321
|
+
try:
|
|
322
|
+
from transformers import AutoProcessor
|
|
323
|
+
processor = AutoProcessor.from_pretrained(self._config.model_id)
|
|
324
|
+
inputs = processor(images=img, return_tensors="pt")
|
|
325
|
+
import torch
|
|
326
|
+
with torch.no_grad():
|
|
327
|
+
outputs = self._model.get_text_features(**inputs) if hasattr(self._model, "get_text_features") else self._model(**inputs)
|
|
328
|
+
if hasattr(outputs, "image_embeds"):
|
|
329
|
+
vec = outputs.image_embeds[0].cpu().numpy()
|
|
330
|
+
elif hasattr(outputs, "last_hidden_state"):
|
|
331
|
+
vec = outputs.last_hidden_state[0].mean(dim=0).cpu().numpy()
|
|
332
|
+
else:
|
|
333
|
+
vec = outputs[0].cpu().numpy() if isinstance(outputs, (list, tuple)) else outputs.cpu().numpy()
|
|
334
|
+
import numpy as np
|
|
335
|
+
norm = np.linalg.norm(vec)
|
|
336
|
+
if norm > 0:
|
|
337
|
+
vec = vec / norm
|
|
338
|
+
vec = vec.tolist()
|
|
339
|
+
except Exception as exc:
|
|
340
|
+
raise ValueError(f"Model {self._config.model_id} does not support image embedding: {exc}") from exc
|
|
341
|
+
return vec if isinstance(vec, list) else vec.tolist()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# API Provider (Jina, or any OpenAI-compatible endpoint with image support)
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class APIMultimodalProvider(MultimodalEmbeddingProvider):
|
|
350
|
+
"""Multimodal embedding via API (Jina, or OpenAI-compatible with image support).
|
|
351
|
+
|
|
352
|
+
Inherits API key from host configuration.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
356
|
+
super().__init__(config)
|
|
357
|
+
if not config.base_url:
|
|
358
|
+
raise ValueError("Multimodal API embeddings require an explicit endpoint")
|
|
359
|
+
self._base_url = config.base_url.rstrip("/")
|
|
360
|
+
if not self._base_url.endswith("/v1"):
|
|
361
|
+
self._base_url += "/v1"
|
|
362
|
+
self._api_key = config.api_key or ""
|
|
363
|
+
|
|
364
|
+
async def embed_text(self, text: str) -> list[float]:
|
|
365
|
+
import httpx
|
|
366
|
+
|
|
367
|
+
async with httpx.AsyncClient(timeout=60, trust_env=False, follow_redirects=False) as client:
|
|
368
|
+
resp = await client.post(
|
|
369
|
+
f"{self._base_url}/embeddings",
|
|
370
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
371
|
+
json={
|
|
372
|
+
"model": self._config.model_id,
|
|
373
|
+
"input": [{"text": text}],
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
resp.raise_for_status()
|
|
377
|
+
data = resp.json()
|
|
378
|
+
return data["data"][0]["embedding"]
|
|
379
|
+
|
|
380
|
+
async def embed_image(self, image: ImageInput) -> list[float]:
|
|
381
|
+
import base64
|
|
382
|
+
import httpx
|
|
383
|
+
|
|
384
|
+
b64 = base64.b64encode(image.data).decode()
|
|
385
|
+
|
|
386
|
+
async with httpx.AsyncClient(timeout=60, trust_env=False, follow_redirects=False) as client:
|
|
387
|
+
resp = await client.post(
|
|
388
|
+
f"{self._base_url}/embeddings",
|
|
389
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
390
|
+
json={
|
|
391
|
+
"model": self._config.model_id,
|
|
392
|
+
"input": [{"image": b64}],
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
resp.raise_for_status()
|
|
396
|
+
data = resp.json()
|
|
397
|
+
return data["data"][0]["embedding"]
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# Factory
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def make_multimodal_provider(config: EmbeddingConfig) -> MultimodalEmbeddingProvider:
|
|
406
|
+
"""Create a multimodal embedding provider based on config."""
|
|
407
|
+
provider = config.provider.lower()
|
|
408
|
+
|
|
409
|
+
if provider == "cuda":
|
|
410
|
+
return CUDAMultimodalProvider(config)
|
|
411
|
+
elif provider == "cpu":
|
|
412
|
+
return CPUMultimodalProvider(config)
|
|
413
|
+
elif provider in ("openai_api", "api", "jina"):
|
|
414
|
+
return APIMultimodalProvider(config)
|
|
415
|
+
else:
|
|
416
|
+
logger.warning("Unknown multimodal provider '%s', falling back to CPU", provider)
|
|
417
|
+
return CPUMultimodalProvider(config)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Colony Vector — multimodal embedding types and input definitions.
|
|
2
|
+
|
|
3
|
+
Modality-agnostic design: supports text, image, and future modalities
|
|
4
|
+
(audio, video) without hardcoding assumptions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Optional, Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Modality(str, Enum):
|
|
15
|
+
"""Supported input modalities for embedding."""
|
|
16
|
+
|
|
17
|
+
TEXT = "text"
|
|
18
|
+
IMAGE = "image"
|
|
19
|
+
# Future: AUDIO = "audio", VIDEO = "video"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class EmbedInput:
|
|
24
|
+
"""A single embedding input — text, image, or future modalities.
|
|
25
|
+
|
|
26
|
+
The `content` field interpretation depends on `modality`:
|
|
27
|
+
- text: raw text string
|
|
28
|
+
- image: file path, URL, or base64-encoded string
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
modality: Modality = Modality.TEXT
|
|
32
|
+
content: str = ""
|
|
33
|
+
mime_type: str = "" # e.g. "image/jpeg", "image/png" — auto-detected if empty
|
|
34
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, Any]:
|
|
37
|
+
return {
|
|
38
|
+
"modality": self.modality.value,
|
|
39
|
+
"content": self.content[:200] + "..." if len(self.content) > 200 else self.content,
|
|
40
|
+
"mime_type": self.mime_type,
|
|
41
|
+
"metadata": self.metadata,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ImageInput:
|
|
47
|
+
"""Normalized image input for the embedding pipeline.
|
|
48
|
+
|
|
49
|
+
Constructed by image_preprocess from raw inputs (path, URL, bytes, base64).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
data: bytes # Raw image bytes (JPEG or PNG)
|
|
53
|
+
mime_type: str = "image/jpeg"
|
|
54
|
+
width: int = 0
|
|
55
|
+
height: int = 0
|
|
56
|
+
original_path: str = "" # Source path/URL for reference
|
|
57
|
+
image_hash: str = "" # SHA-256 of raw bytes
|
|
58
|
+
exif: dict[str, Any] = field(default_factory=dict) # Extracted EXIF data
|
|
59
|
+
caption: str = "" # User-provided or auto-generated caption
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class EmbedResult:
|
|
64
|
+
"""Result of embedding a single input."""
|
|
65
|
+
|
|
66
|
+
vector: list[float]
|
|
67
|
+
modality: Modality
|
|
68
|
+
model_id: str
|
|
69
|
+
dims: int
|
|
70
|
+
image_hash: str = "" # Set for image inputs
|
|
71
|
+
caption: str = "" # Set for image inputs
|
|
72
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class MultimodalSearchResult:
|
|
77
|
+
"""Search result with modality awareness."""
|
|
78
|
+
|
|
79
|
+
id: str
|
|
80
|
+
score: float
|
|
81
|
+
text: str
|
|
82
|
+
modality: Modality = Modality.TEXT
|
|
83
|
+
image_ref: str = ""
|
|
84
|
+
image_hash: str = ""
|
|
85
|
+
caption: str = ""
|
|
86
|
+
thumbnail_ref: str = ""
|
|
87
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""OpenAI-compatible API embedding provider.
|
|
2
|
+
|
|
3
|
+
Calls an OpenAI-compatible ``/v1/embeddings`` endpoint using the
|
|
4
|
+
host's API key. This lets Colony inherit embeddings from whichever
|
|
5
|
+
LLM provider the host is configured with — no separate API key needed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import math
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from apsimo.vector.config import EmbeddingConfig
|
|
16
|
+
from apsimo.vector.embedder import EmbeddingProvider
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OpenAIAPIEmbeddingProvider(EmbeddingProvider):
|
|
22
|
+
"""Embedding provider that calls an OpenAI-compatible API endpoint.
|
|
23
|
+
|
|
24
|
+
Uses ``httpx`` for async HTTP requests. Inherits the API key and
|
|
25
|
+
base URL from the host configuration (same key used for LLM calls).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: EmbeddingConfig) -> None:
|
|
29
|
+
super().__init__(config)
|
|
30
|
+
self._base_url: str = ""
|
|
31
|
+
self._api_key: str = ""
|
|
32
|
+
self._served_model: str = ""
|
|
33
|
+
|
|
34
|
+
def configure(
|
|
35
|
+
self,
|
|
36
|
+
base_url: str,
|
|
37
|
+
api_key: str,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Set the API endpoint and key (called by the host plugin)."""
|
|
40
|
+
if self._served_model and (base_url.rstrip('/') != self._base_url or api_key != self._api_key):
|
|
41
|
+
raise ValueError('Create a new embedding provider before changing an active endpoint')
|
|
42
|
+
self._base_url = base_url.rstrip("/")
|
|
43
|
+
self._api_key = api_key
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def dimensions(self) -> int:
|
|
47
|
+
return self._config.dimensions
|
|
48
|
+
|
|
49
|
+
async def warmup(self) -> None:
|
|
50
|
+
"""Verify connectivity with a test embedding."""
|
|
51
|
+
if not self._base_url:
|
|
52
|
+
logger.warning("OpenAI API embedder not configured — set base_url")
|
|
53
|
+
return
|
|
54
|
+
result = await self.embed("warmup")
|
|
55
|
+
logger.info(
|
|
56
|
+
"OpenAI API embedder warmed up (dims=%d)", len(result),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
async def embed(self, text: str) -> list[float]:
|
|
60
|
+
"""Embed a single text via the API."""
|
|
61
|
+
results = await self.embed_batch([text])
|
|
62
|
+
return results[0]
|
|
63
|
+
|
|
64
|
+
async def close(self) -> None:
|
|
65
|
+
"""No resources to release for API provider."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
69
|
+
"""Embed a batch of texts via the API."""
|
|
70
|
+
if not texts:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
import httpx
|
|
74
|
+
|
|
75
|
+
url = self._base_url + ('/embeddings' if self._base_url.endswith('/v1') else '/v1/embeddings')
|
|
76
|
+
headers = {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
}
|
|
79
|
+
if self._api_key:
|
|
80
|
+
headers['Authorization'] = f'Bearer {self._api_key}'
|
|
81
|
+
payload: dict[str, Any] = {
|
|
82
|
+
"model": self._config.model_id,
|
|
83
|
+
"input": texts,
|
|
84
|
+
}
|
|
85
|
+
# NOTE: do not send "dimensions" — non-matryoshka models (e.g.
|
|
86
|
+
# Qwen3-Embedding-8B, native 4096) reject it with HTTP 400 on vllm.
|
|
87
|
+
|
|
88
|
+
# Resilience (v0.21.1): the embedding endpoint is often remote (e.g. an
|
|
89
|
+
# SSH-tunnelled vLLM). Transient blips were silently failing memory writes
|
|
90
|
+
# AND recall (the agent then "loses" its memory). Retry with backoff.
|
|
91
|
+
last_exc: Exception | None = None
|
|
92
|
+
for attempt in range(3):
|
|
93
|
+
try:
|
|
94
|
+
async with httpx.AsyncClient(timeout=30, trust_env=False, follow_redirects=False) as client:
|
|
95
|
+
response = await client.post(url, json=payload, headers=headers)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
data = response.json()
|
|
98
|
+
rows = data.get('data', [])
|
|
99
|
+
if not isinstance(rows, list) or len(rows) != len(texts):
|
|
100
|
+
raise ValueError('Embedding response count does not match its inputs')
|
|
101
|
+
if any('index' in row for row in rows):
|
|
102
|
+
if sorted(row.get('index', -1) for row in rows) != list(range(len(texts))):
|
|
103
|
+
raise ValueError('Embedding response indexes do not match its inputs')
|
|
104
|
+
rows = sorted(rows, key=lambda row: row['index'])
|
|
105
|
+
vectors = [row['embedding'] for row in rows]
|
|
106
|
+
if any(len(vector) != self.dimensions or not all(math.isfinite(value) for value in vector)
|
|
107
|
+
or not any(vector) for vector in vectors):
|
|
108
|
+
raise ValueError('Embedding response contains an invalid vector or dimension')
|
|
109
|
+
served = str(data.get('model') or 'unknown')
|
|
110
|
+
if self._served_model and self._served_model != served:
|
|
111
|
+
raise ValueError('Embedding endpoint changed its reported model; rebuild with a new provider snapshot')
|
|
112
|
+
self._served_model = served
|
|
113
|
+
return vectors
|
|
114
|
+
except (httpx.HTTPError, OSError) as exc:
|
|
115
|
+
last_exc = exc
|
|
116
|
+
if attempt < 2:
|
|
117
|
+
await asyncio.sleep(0.5 * (attempt + 1))
|
|
118
|
+
logger.warning("Embedding failed after 3 attempts: %s", last_exc)
|
|
119
|
+
raise last_exc # type: ignore[misc]
|
apsimo/vector/query.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Colony Vector Store — query and result types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class VectorResult:
|
|
11
|
+
"""A single result from vector search."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
score: float # cosine similarity in [0, 1]
|
|
15
|
+
text: str # original embedded text
|
|
16
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class VectorQuery:
|
|
21
|
+
"""Parameters for a vector similarity search."""
|
|
22
|
+
|
|
23
|
+
vector: list[float]
|
|
24
|
+
limit: int = 10
|
|
25
|
+
filter: Optional[str] = None # LanceDB SQL filter expression
|
|
26
|
+
min_score: float = 0.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class VectorItem:
|
|
31
|
+
"""An item to insert into the vector store."""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
text: str
|
|
35
|
+
vector: list[float]
|
|
36
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class HybridQuery:
|
|
41
|
+
"""Combined dense + sparse (keyword) query — future use."""
|
|
42
|
+
|
|
43
|
+
vector: list[float]
|
|
44
|
+
text: str # keyword query for BM25 sparse search
|
|
45
|
+
limit: int = 10
|
|
46
|
+
filter: Optional[str] = None
|
|
47
|
+
min_score: float = 0.0
|
|
48
|
+
dense_weight: float = 0.7
|
|
49
|
+
sparse_weight: float = 0.3
|