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,565 @@
1
+ """Reranker pipeline — scores and re-ranks retrieved documents.
2
+
3
+ Uses the same provider pattern as the embedding pipeline:
4
+ CUDA (sentence-transformers) for GPU, CPU for fallback,
5
+ and an OpenAI-compatible API provider for remote reranking.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import functools
12
+ import logging
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass
15
+ from typing import Optional
16
+
17
+ from apsimo.vector.tiers import ModelSpec
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class RerankResult:
24
+ """A single reranked document with score."""
25
+
26
+ index: int # Original index in the input list
27
+ score: float # Relevance score (higher = more relevant)
28
+ text: str # The document text
29
+
30
+
31
+ class RerankerProvider(ABC):
32
+ """Base class for reranker providers."""
33
+
34
+ def __init__(self, model_id: str) -> None:
35
+ self._model_id = model_id
36
+ self._model = None
37
+
38
+ def calibration_metadata(self) -> dict:
39
+ """Configuration identity for optional deployment-calibrated cutoffs.
40
+
41
+ A model name is not an immutable weight revision. The caller may add
42
+ an observed revision; otherwise that uncertainty remains explicit.
43
+ """
44
+ from urllib.parse import urlsplit, urlunsplit
45
+ import hashlib
46
+ endpoint = urlsplit(getattr(self, "_base_url", ""))
47
+ host = endpoint.hostname or ""
48
+ if endpoint.port:
49
+ host += f":{endpoint.port}"
50
+ prompt_style = getattr(self, "_prompt_style", "")
51
+ format_version = "raw-v1"
52
+ if prompt_style == "qwen3":
53
+ format_version = hashlib.sha256((QWEN3_RERANK_PREFIX + QWEN3_RERANK_INSTRUCTION + QWEN3_RERANK_SUFFIX).encode()).hexdigest()
54
+ return {
55
+ "provider": type(self).__name__, "model": self._model_id,
56
+ "endpoint": urlunsplit((endpoint.scheme, host, endpoint.path, "", "")),
57
+ "prompt_style": prompt_style,
58
+ "format_version": format_version,
59
+ "weights_revision": "unverified",
60
+ }
61
+
62
+ @abstractmethod
63
+ async def warmup(self) -> None: ...
64
+
65
+ @abstractmethod
66
+ async def rerank(
67
+ self,
68
+ query: str,
69
+ documents: list[str],
70
+ top_k: int = 10,
71
+ ) -> list[RerankResult]: ...
72
+
73
+
74
+ class CUDARerankerProvider(RerankerProvider):
75
+ """NVIDIA GPU reranker via sentence-transformers CrossEncoder."""
76
+
77
+ async def warmup(self) -> None:
78
+ loop = asyncio.get_running_loop()
79
+ self._model = await loop.run_in_executor(None, self._load_model)
80
+
81
+ def _load_model(self):
82
+ from sentence_transformers import CrossEncoder
83
+ return CrossEncoder(self._model_id, device="cuda")
84
+
85
+ async def rerank(
86
+ self,
87
+ query: str,
88
+ documents: list[str],
89
+ top_k: int = 10,
90
+ ) -> list[RerankResult]:
91
+ if not documents:
92
+ return []
93
+
94
+ pairs = [(query, doc) for doc in documents]
95
+ loop = asyncio.get_running_loop()
96
+ scores = await loop.run_in_executor(
97
+ None,
98
+ functools.partial(self._model.predict, pairs),
99
+ )
100
+
101
+ results = [
102
+ RerankResult(index=i, score=float(scores[i]), text=documents[i])
103
+ for i in range(len(documents))
104
+ ]
105
+ results.sort(key=lambda r: r.score, reverse=True)
106
+ return results[:top_k]
107
+
108
+
109
+ class CPURerankerProvider(RerankerProvider):
110
+ """CPU reranker via sentence-transformers CrossEncoder."""
111
+
112
+ async def warmup(self) -> None:
113
+ loop = asyncio.get_running_loop()
114
+ self._model = await loop.run_in_executor(None, self._load_model)
115
+
116
+ def _load_model(self):
117
+ from sentence_transformers import CrossEncoder
118
+ return CrossEncoder(self._model_id, device="cpu")
119
+
120
+ async def rerank(
121
+ self,
122
+ query: str,
123
+ documents: list[str],
124
+ top_k: int = 10,
125
+ ) -> list[RerankResult]:
126
+ if not documents:
127
+ return []
128
+
129
+ pairs = [(query, doc) for doc in documents]
130
+ loop = asyncio.get_running_loop()
131
+ scores = await loop.run_in_executor(
132
+ None,
133
+ functools.partial(self._model.predict, pairs),
134
+ )
135
+
136
+ results = [
137
+ RerankResult(index=i, score=float(scores[i]), text=documents[i])
138
+ for i in range(len(documents))
139
+ ]
140
+ results.sort(key=lambda r: r.score, reverse=True)
141
+ return results[:top_k]
142
+
143
+
144
+ class MLXRerankerProvider(RerankerProvider):
145
+ """Apple Silicon reranker via sentence-transformers CrossEncoder with PyTorch MPS.
146
+
147
+ NOTE: This is the legacy provider. For true native MLX performance,
148
+ use ``NativeMLXRerankerProvider`` (provider="native_mlx").
149
+
150
+ NOTE: warmup() runs synchronously in the main thread to avoid
151
+ PyTorch MPS deadlocks in asyncio.run_in_executor (Issue #17).
152
+ """
153
+
154
+ async def warmup(self) -> None:
155
+ self._model = self._load_model()
156
+
157
+ def _load_model(self):
158
+ from sentence_transformers import CrossEncoder
159
+ return CrossEncoder(self._model_id, device="mps")
160
+
161
+ async def rerank(
162
+ self,
163
+ query: str,
164
+ documents: list[str],
165
+ top_k: int = 10,
166
+ ) -> list[RerankResult]:
167
+ if not documents:
168
+ return []
169
+
170
+ pairs = [(query, doc) for doc in documents]
171
+ loop = asyncio.get_running_loop()
172
+ scores = await loop.run_in_executor(
173
+ None,
174
+ functools.partial(self._model.predict, pairs),
175
+ )
176
+
177
+ results = [
178
+ RerankResult(index=i, score=float(scores[i]), text=documents[i])
179
+ for i in range(len(documents))
180
+ ]
181
+ results.sort(key=lambda r: r.score, reverse=True)
182
+ return results[:top_k]
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # Native MLX reranker (Apple Silicon via true MLX framework)
187
+ # ---------------------------------------------------------------------------
188
+
189
+ class NativeMLXRerankerProvider(RerankerProvider):
190
+ """Apple Silicon reranker via the native MLX framework (mlx-lm).
191
+
192
+ Loads the original HuggingFace CrossEncoder model with ``mlx_lm`` and
193
+ extracts logits for the true/false classification tokens. This avoids
194
+ the PyTorch MPS overhead entirely.
195
+
196
+ The model must expose a ``config_sentence_transformers.json`` with
197
+ ``true_token_id`` and ``false_token_id`` (standard for sentence-transformers
198
+ CrossEncoder checkpoints). If absent, the provider falls back to logits
199
+ for generic "yes"/"no" tokens.
200
+ """
201
+
202
+ def __init__(self, model_id: str) -> None:
203
+ super().__init__(model_id)
204
+ self._model = None
205
+ self._tokenizer = None
206
+ self._true_token_id: int | None = None
207
+ self._false_token_id: int | None = None
208
+
209
+ async def warmup(self) -> None:
210
+ self._model, self._tokenizer = self._load_model()
211
+
212
+ def _load_model(self):
213
+ from mlx_lm import load
214
+ import json
215
+ from pathlib import Path
216
+ from huggingface_hub import try_to_load_from_cache
217
+
218
+ model, tokenizer = load(self._model_id, lazy=False)
219
+ self._instruction = None
220
+
221
+ # Load instruction / prompt from sentence-transformers config
222
+ try:
223
+ cache_path = try_to_load_from_cache(
224
+ self._model_id, "config_sentence_transformers.json"
225
+ )
226
+ if cache_path and Path(cache_path).exists():
227
+ with open(cache_path) as f:
228
+ st_cfg = json.load(f)
229
+ else:
230
+ from huggingface_hub import snapshot_download
231
+ model_path = Path(snapshot_download(self._model_id, allow_patterns=["config_sentence_transformers.json"]))
232
+ with open(model_path / "config_sentence_transformers.json") as f:
233
+ st_cfg = json.load(f)
234
+
235
+ prompts = st_cfg.get("prompts", {})
236
+ default_name = st_cfg.get("default_prompt_name")
237
+ if default_name and default_name in prompts:
238
+ self._instruction = prompts[default_name]
239
+ elif prompts:
240
+ self._instruction = next(iter(prompts.values()))
241
+ except Exception:
242
+ pass
243
+
244
+ # Attempt to load sentence-transformers config for token IDs
245
+ try:
246
+ # Check local cache first, then download
247
+ cache_path = try_to_load_from_cache(
248
+ self._model_id, "config_sentence_transformers.json"
249
+ )
250
+ if cache_path and Path(cache_path).exists():
251
+ with open(cache_path) as f:
252
+ st_cfg = json.load(f)
253
+ else:
254
+ # Try to find in the model directory
255
+ from huggingface_hub import snapshot_download
256
+ model_path = Path(snapshot_download(self._model_id, allow_patterns=["config_sentence_transformers.json"]))
257
+ with open(model_path / "config_sentence_transformers.json") as f:
258
+ st_cfg = json.load(f)
259
+
260
+ # Look for LogitScore module config
261
+ logit_cfg = st_cfg.get("1_LogitScore", {})
262
+ if not logit_cfg:
263
+ # Some models store it directly in the root
264
+ logit_cfg = st_cfg
265
+ self._true_token_id = logit_cfg.get("true_token_id")
266
+ self._false_token_id = logit_cfg.get("false_token_id")
267
+ except Exception:
268
+ pass
269
+
270
+ # Qwen3 rerankers store token IDs in 1_LogitScore/config.json
271
+ # rather than inside config_sentence_transformers.json.
272
+ if self._true_token_id is None or self._false_token_id is None:
273
+ try:
274
+ cache_path = try_to_load_from_cache(
275
+ self._model_id, "1_LogitScore/config.json"
276
+ )
277
+ if cache_path and Path(cache_path).exists():
278
+ with open(cache_path) as f:
279
+ logit_cfg = json.load(f)
280
+ else:
281
+ from huggingface_hub import snapshot_download
282
+
283
+ model_path = Path(
284
+ snapshot_download(
285
+ self._model_id, allow_patterns=["1_LogitScore/config.json"]
286
+ )
287
+ )
288
+ with open(model_path / "1_LogitScore" / "config.json") as f:
289
+ logit_cfg = json.load(f)
290
+ self._true_token_id = logit_cfg.get("true_token_id")
291
+ self._false_token_id = logit_cfg.get("false_token_id")
292
+ except Exception:
293
+ pass
294
+
295
+ return model, tokenizer
296
+
297
+ async def rerank(
298
+ self,
299
+ query: str,
300
+ documents: list[str],
301
+ top_k: int = 10,
302
+ ) -> list[RerankResult]:
303
+ if not documents:
304
+ return []
305
+
306
+ loop = asyncio.get_running_loop()
307
+ scores = await loop.run_in_executor(
308
+ None,
309
+ functools.partial(self._score_documents, query, documents),
310
+ )
311
+
312
+ results = [
313
+ RerankResult(index=i, score=float(scores[i]), text=documents[i])
314
+ for i in range(len(documents))
315
+ ]
316
+ results.sort(key=lambda r: r.score, reverse=True)
317
+ return results[:top_k]
318
+
319
+ def _score_documents(self, query: str, documents: list[str]) -> list[float]:
320
+ import mlx.core as mx
321
+
322
+ scores: list[float] = []
323
+ for doc in documents:
324
+ # Use chat template if available (Qwen3 rerankers require this)
325
+ if (
326
+ hasattr(self._tokenizer, "apply_chat_template")
327
+ and self._tokenizer.chat_template
328
+ ):
329
+ instruction = (
330
+ self._instruction
331
+ or "Given a web search query, retrieve relevant passages that answer the query"
332
+ )
333
+ messages = [
334
+ {"role": "system", "content": instruction},
335
+ {"role": "query", "content": query},
336
+ {"role": "document", "content": doc},
337
+ ]
338
+ prompt = self._tokenizer.apply_chat_template(
339
+ messages, tokenize=False, add_generation_prompt=True
340
+ )
341
+ else:
342
+ # Fallback for models without a chat template
343
+ prompt = f"{query}\n{doc}"
344
+
345
+ tokens = mx.array(self._tokenizer.encode(prompt))
346
+ logits = self._model(tokens[None, :])
347
+ last_logits = logits[0, -1, :]
348
+
349
+ if self._true_token_id is not None and self._false_token_id is not None:
350
+ true_score = last_logits[self._true_token_id].item()
351
+ false_score = last_logits[self._false_token_id].item()
352
+ scores.append(true_score - false_score)
353
+ else:
354
+ # Fallback: use max logit as relevance proxy
355
+ scores.append(last_logits.max().item())
356
+
357
+ return scores
358
+
359
+
360
+ # Qwen3-Reranker scores relevance from yes/no token logits, which are only
361
+ # calibrated when the request is wrapped in the model's instruction template
362
+ # (vLLM's /v1/rerank does not apply it server-side — raw strings score noise).
363
+ QWEN3_RERANK_PREFIX = (
364
+ "<|im_start|>system\nJudge whether the Document meets the requirements "
365
+ 'based on the Query and the Instruct provided. Note that the answer can '
366
+ 'only be "yes" or "no".<|im_end|>\n<|im_start|>user\n'
367
+ )
368
+ QWEN3_RERANK_SUFFIX = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
369
+ QWEN3_RERANK_INSTRUCTION = (
370
+ "Given a web search query, retrieve relevant passages that answer the query"
371
+ )
372
+
373
+
374
+ def format_qwen3_rerank(query: str, documents: list[str]) -> tuple[str, list[str]]:
375
+ """Wrap a query and documents in the Qwen3-Reranker instruction template."""
376
+ wrapped_query = (
377
+ f"{QWEN3_RERANK_PREFIX}<Instruct>: {QWEN3_RERANK_INSTRUCTION}"
378
+ f"\n<Query>: {query}\n"
379
+ )
380
+ wrapped_docs = [f"<Document>: {d}{QWEN3_RERANK_SUFFIX}" for d in documents]
381
+ return wrapped_query, wrapped_docs
382
+
383
+
384
+ class OpenAIAPIRerankerProvider(RerankerProvider):
385
+ """Reranker via an OpenAI-compatible API endpoint."""
386
+
387
+ def __init__(self, model_id: str) -> None:
388
+ super().__init__(model_id)
389
+ self._base_url: str = ""
390
+ self._api_key: str = ""
391
+ self._prompt_style: str = ""
392
+
393
+ def configure(self, base_url: str, api_key: str, prompt_style: str = "") -> None:
394
+ self._base_url = base_url.rstrip("/")
395
+ self._api_key = api_key
396
+ self._prompt_style = prompt_style
397
+
398
+ async def warmup(self) -> None:
399
+ pass # No local model to warm up
400
+
401
+ async def rerank(
402
+ self,
403
+ query: str,
404
+ documents: list[str],
405
+ top_k: int = 10,
406
+ ) -> list[RerankResult]:
407
+ if not documents:
408
+ return []
409
+
410
+ # Use the Jina/Cohere rerank API format (widely supported)
411
+ import httpx
412
+
413
+ send_query, send_docs = query, documents
414
+ if self._prompt_style == "qwen3":
415
+ send_query, send_docs = format_qwen3_rerank(query, documents)
416
+
417
+ url = f"{self._base_url}/v1/rerank"
418
+ headers = {
419
+ "Authorization": f"Bearer {self._api_key}",
420
+ "Content-Type": "application/json",
421
+ }
422
+ payload = {
423
+ "model": self._model_id,
424
+ "query": send_query,
425
+ "documents": send_docs,
426
+ "top_n": top_k,
427
+ }
428
+
429
+ async with httpx.AsyncClient(timeout=30) as client:
430
+ response = await client.post(url, json=payload, headers=headers)
431
+ response.raise_for_status()
432
+ data = response.json()
433
+
434
+ results = []
435
+ for item in data.get("results", []):
436
+ idx = item.get("index", 0)
437
+ results.append(RerankResult(
438
+ index=idx,
439
+ score=item.get("relevance_score", 0.0),
440
+ text=documents[idx],
441
+ ))
442
+
443
+ return results
444
+
445
+
446
+ def make_reranker_provider(
447
+ spec: ModelSpec,
448
+ gpu_type: str = "none",
449
+ api_base_url: str | None = None,
450
+ api_key: str | None = None,
451
+ ) -> Optional[RerankerProvider]:
452
+ """Create the appropriate reranker provider based on spec and hardware.
453
+
454
+ Returns None if the spec is None (tier doesn't have a reranker).
455
+ """
456
+ if spec is None:
457
+ return None
458
+
459
+ # Check if this is a multimodal reranker
460
+ if "image" in spec.modalities:
461
+ return _make_multimodal_reranker(spec, gpu_type, api_base_url, api_key)
462
+
463
+ if api_base_url and api_key:
464
+ provider = OpenAIAPIRerankerProvider(spec.model_id)
465
+ provider.configure(api_base_url, api_key)
466
+ return provider
467
+
468
+ if gpu_type == "cuda":
469
+ return CUDARerankerProvider(spec.model_id)
470
+ elif gpu_type == "native_mlx":
471
+ return NativeMLXRerankerProvider(spec.model_id)
472
+ elif gpu_type == "mlx":
473
+ return MLXRerankerProvider(spec.model_id)
474
+ else:
475
+ return CPURerankerProvider(spec.model_id)
476
+
477
+
478
+ def _make_multimodal_reranker(
479
+ spec: ModelSpec,
480
+ gpu_type: str = "none",
481
+ api_base_url: str | None = None,
482
+ api_key: str | None = None,
483
+ ) -> RerankerProvider:
484
+ """Create a multimodal reranker provider.
485
+
486
+ For multimodal rerankers (e.g. jina-reranker-m0), uses the CrossEncoder
487
+ interface but supports image+text pairs. Falls back to text-only reranking
488
+ on image captions when a true multimodal cross-encoder isn't available.
489
+ """
490
+ if api_base_url and api_key:
491
+ provider = OpenAIAPIRerankerProvider(spec.model_id)
492
+ provider.configure(api_base_url, api_key)
493
+ return provider
494
+
495
+ # Multimodal rerankers use the same CrossEncoder interface
496
+ # but with image+text pair support
497
+ if gpu_type == "cuda":
498
+ return CUDAMultimodalRerankerProvider(spec.model_id)
499
+ else:
500
+ return CPUMultimodalRerankerProvider(spec.model_id)
501
+
502
+
503
+ class CUDAMultimodalRerankerProvider(RerankerProvider):
504
+ """Multimodal reranker on NVIDIA GPU.
505
+
506
+ Uses CrossEncoder with vision-language models that support
507
+ text+image pairs. For image documents, uses caption as text
508
+ representation.
509
+ """
510
+
511
+ async def warmup(self) -> None:
512
+ loop = asyncio.get_running_loop()
513
+ self._model = await loop.run_in_executor(None, self._load_model)
514
+
515
+ def _load_model(self):
516
+ from sentence_transformers import CrossEncoder
517
+ return CrossEncoder(self._model_id, device="cuda")
518
+
519
+ async def rerank(
520
+ self,
521
+ query: str,
522
+ documents: list[str],
523
+ top_k: int = 10,
524
+ ) -> list[RerankResult]:
525
+ if not documents:
526
+ return []
527
+ pairs = [(query, doc) for doc in documents]
528
+ loop = asyncio.get_running_loop()
529
+ scores = await loop.run_in_executor(None, self._model.predict, pairs)
530
+ results = [
531
+ RerankResult(index=i, score=float(s), text=documents[i])
532
+ for i, s in enumerate(scores)
533
+ ]
534
+ results.sort(key=lambda r: r.score, reverse=True)
535
+ return results[:top_k]
536
+
537
+
538
+ class CPUMultimodalRerankerProvider(RerankerProvider):
539
+ """Multimodal reranker on CPU."""
540
+
541
+ async def warmup(self) -> None:
542
+ loop = asyncio.get_running_loop()
543
+ self._model = await loop.run_in_executor(None, self._load_model)
544
+
545
+ def _load_model(self):
546
+ from sentence_transformers import CrossEncoder
547
+ return CrossEncoder(self._model_id, device="cpu")
548
+
549
+ async def rerank(
550
+ self,
551
+ query: str,
552
+ documents: list[str],
553
+ top_k: int = 10,
554
+ ) -> list[RerankResult]:
555
+ if not documents:
556
+ return []
557
+ pairs = [(query, doc) for doc in documents]
558
+ loop = asyncio.get_running_loop()
559
+ scores = await loop.run_in_executor(None, self._model.predict, pairs)
560
+ results = [
561
+ RerankResult(index=i, score=float(s), text=documents[i])
562
+ for i, s in enumerate(scores)
563
+ ]
564
+ results.sort(key=lambda r: r.score, reverse=True)
565
+ return results[:top_k]