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.
Files changed (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. 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