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