marvisx-cli 0.1.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 (587) hide show
  1. core/api/__init__.py +0 -0
  2. core/api/agents/__init__.py +0 -0
  3. core/api/agents/session_health.py +59 -0
  4. core/api/agents/session_manager.py +206 -0
  5. core/api/bin/marvisx-state-hook.py +182 -0
  6. core/api/config.py +533 -0
  7. core/api/db.py +1516 -0
  8. core/api/dependencies/__init__.py +0 -0
  9. core/api/dependencies/tenant.py +34 -0
  10. core/api/main.py +1641 -0
  11. core/api/mcp/__init__.py +8 -0
  12. core/api/mcp/_adapter.py +184 -0
  13. core/api/mcp/server.py +58 -0
  14. core/api/mcp/tools/__init__.py +59 -0
  15. core/api/mcp/tools/brain.py +599 -0
  16. core/api/mcp/tools/graph.py +380 -0
  17. core/api/mcp/tools/handoffs.py +112 -0
  18. core/api/mcp/tools/ingest.py +326 -0
  19. core/api/mcp/tools/learnings.py +144 -0
  20. core/api/mcp/tools/projects.py +99 -0
  21. core/api/mcp/tools/pull_requests.py +173 -0
  22. core/api/mcp/tools/safety.py +111 -0
  23. core/api/mcp/tools/search.py +79 -0
  24. core/api/mcp/tools/tasks.py +258 -0
  25. core/api/middleware/__init__.py +0 -0
  26. core/api/middleware/tool_call_audit.py +111 -0
  27. core/api/models/__init__.py +346 -0
  28. core/api/models/auth.py +48 -0
  29. core/api/models/brain.py +1006 -0
  30. core/api/models/common.py +76 -0
  31. core/api/models/costs.py +91 -0
  32. core/api/models/graph.py +66 -0
  33. core/api/models/graph_cosmo.py +125 -0
  34. core/api/models/graph_pr_impact.py +257 -0
  35. core/api/models/graph_ux.py +141 -0
  36. core/api/models/inbox.py +230 -0
  37. core/api/models/ingest_keys.py +108 -0
  38. core/api/models/kg.py +41 -0
  39. core/api/models/llm_config.py +56 -0
  40. core/api/models/monitoring.py +234 -0
  41. core/api/models/projects.py +161 -0
  42. core/api/models/search.py +42 -0
  43. core/api/models/sessions.py +322 -0
  44. core/api/models/tasks.py +184 -0
  45. core/api/models/teams.py +63 -0
  46. core/api/models/users.py +184 -0
  47. core/api/observability/__init__.py +0 -0
  48. core/api/observability/tracing.py +92 -0
  49. core/api/paths.py +26 -0
  50. core/api/rate_limit.py +24 -0
  51. core/api/rbac.py +112 -0
  52. core/api/routers/__init__.py +0 -0
  53. core/api/routers/_adapter.py +24 -0
  54. core/api/routers/admin_pr_impact.py +230 -0
  55. core/api/routers/admin_settings.py +147 -0
  56. core/api/routers/agent.py +1079 -0
  57. core/api/routers/agent_tokens.py +276 -0
  58. core/api/routers/app_settings.py +112 -0
  59. core/api/routers/audit.py +89 -0
  60. core/api/routers/auth.py +586 -0
  61. core/api/routers/bench.py +161 -0
  62. core/api/routers/brain.py +881 -0
  63. core/api/routers/brain_directions.py +527 -0
  64. core/api/routers/ci_checks.py +140 -0
  65. core/api/routers/comments.py +273 -0
  66. core/api/routers/costs.py +148 -0
  67. core/api/routers/docs_coverage.py +217 -0
  68. core/api/routers/docs_governance.py +63 -0
  69. core/api/routers/documents.py +318 -0
  70. core/api/routers/files.py +163 -0
  71. core/api/routers/finder.py +987 -0
  72. core/api/routers/graph.py +836 -0
  73. core/api/routers/handoffs.py +156 -0
  74. core/api/routers/inbox.py +496 -0
  75. core/api/routers/ingest_api_keys.py +205 -0
  76. core/api/routers/ingest_triage.py +1227 -0
  77. core/api/routers/judge.py +306 -0
  78. core/api/routers/kg.py +336 -0
  79. core/api/routers/learnings.py +253 -0
  80. core/api/routers/llm_config.py +130 -0
  81. core/api/routers/monitoring.py +347 -0
  82. core/api/routers/notifications.py +125 -0
  83. core/api/routers/pr_impact.py +315 -0
  84. core/api/routers/projects.py +1061 -0
  85. core/api/routers/pull_requests.py +312 -0
  86. core/api/routers/push.py +67 -0
  87. core/api/routers/raci.py +228 -0
  88. core/api/routers/search.py +125 -0
  89. core/api/routers/sessions.py +3100 -0
  90. core/api/routers/settings.py +90 -0
  91. core/api/routers/share_repo.py +68 -0
  92. core/api/routers/status_updates.py +96 -0
  93. core/api/routers/tags.py +45 -0
  94. core/api/routers/tasks.py +526 -0
  95. core/api/routers/teams.py +425 -0
  96. core/api/routers/terminal.py +105 -0
  97. core/api/routers/users.py +331 -0
  98. core/api/routers/webhooks.py +330 -0
  99. core/api/runtime_settings.py +84 -0
  100. core/api/security.py +652 -0
  101. core/api/services/__init__.py +0 -0
  102. core/api/services/audit.py +58 -0
  103. core/api/services/auto_approval.py +82 -0
  104. core/api/services/brain/__init__.py +52 -0
  105. core/api/services/brain/baseline.py +230 -0
  106. core/api/services/brain/capabilities.py +75 -0
  107. core/api/services/brain/cascade_rollup.py +388 -0
  108. core/api/services/brain/compound_bridge.py +215 -0
  109. core/api/services/brain/cycle.py +1242 -0
  110. core/api/services/brain/cycle_snapshot.py +371 -0
  111. core/api/services/brain/digest_collector.py +147 -0
  112. core/api/services/brain/direction.py +421 -0
  113. core/api/services/brain/drift.py +356 -0
  114. core/api/services/brain/drift_router.py +409 -0
  115. core/api/services/brain/edge_metrics.py +79 -0
  116. core/api/services/brain/events_reader.py +222 -0
  117. core/api/services/brain/findings.py +1379 -0
  118. core/api/services/brain/findings_reader.py +1006 -0
  119. core/api/services/brain/jobs.py +733 -0
  120. core/api/services/brain/journal.py +206 -0
  121. core/api/services/brain/knowledge_forms.py +92 -0
  122. core/api/services/brain/llm/__init__.py +37 -0
  123. core/api/services/brain/llm/_runner.py +62 -0
  124. core/api/services/brain/llm/base.py +70 -0
  125. core/api/services/brain/llm/cache.py +99 -0
  126. core/api/services/brain/llm/constants.py +46 -0
  127. core/api/services/brain/llm/direction_alignment.py +289 -0
  128. core/api/services/brain/llm/factory.py +132 -0
  129. core/api/services/brain/llm/finding_reasoning.py +98 -0
  130. core/api/services/brain/llm/finding_summary.py +92 -0
  131. core/api/services/brain/llm/grounding.py +46 -0
  132. core/api/services/brain/llm/journal_polish.py +96 -0
  133. core/api/services/brain/llm/local_gateway.py +426 -0
  134. core/api/services/brain/llm/parsers.py +71 -0
  135. core/api/services/brain/llm/router_glue.py +422 -0
  136. core/api/services/brain/memory_ops.py +1677 -0
  137. core/api/services/brain/models.py +140 -0
  138. core/api/services/brain/owner_hint.py +211 -0
  139. core/api/services/brain/recap.py +307 -0
  140. core/api/services/brain/rules/__init__.py +65 -0
  141. core/api/services/brain/rules/_signals.py +205 -0
  142. core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
  143. core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
  144. core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
  145. core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
  146. core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
  147. core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
  148. core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
  149. core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
  150. core/api/services/brain/runs_reader.py +485 -0
  151. core/api/services/brain/scope.py +79 -0
  152. core/api/services/brain/sources/__init__.py +46 -0
  153. core/api/services/brain/sources/base.py +86 -0
  154. core/api/services/brain/sources/git_kg.py +393 -0
  155. core/api/services/brain/sources/handoffs.py +157 -0
  156. core/api/services/brain/sources/ingestor.py +130 -0
  157. core/api/services/brain/sources/learnings.py +121 -0
  158. core/api/services/brain/sources/pir_tasks.py +245 -0
  159. core/api/services/brain/watermarks.py +147 -0
  160. core/api/services/brain/ws_emitter.py +170 -0
  161. core/api/services/cc_tasks_reader.py +76 -0
  162. core/api/services/ci_service.py +263 -0
  163. core/api/services/claude_metrics.py +796 -0
  164. core/api/services/codex_metrics.py +364 -0
  165. core/api/services/conversation_reader.py +102 -0
  166. core/api/services/cost_service.py +243 -0
  167. core/api/services/crypto.py +147 -0
  168. core/api/services/docs_governance/__init__.py +1 -0
  169. core/api/services/docs_governance/confidence.py +230 -0
  170. core/api/services/docs_governance/config.py +87 -0
  171. core/api/services/docs_governance/enrichment.py +65 -0
  172. core/api/services/docs_governance/frontmatter_validator.py +83 -0
  173. core/api/services/docs_governance/hard_gates.py +221 -0
  174. core/api/services/docs_governance/triage_orchestrator.py +98 -0
  175. core/api/services/embedding_internal.py +395 -0
  176. core/api/services/embedding_service.py +832 -0
  177. core/api/services/event_dispatcher.py +167 -0
  178. core/api/services/events.py +70 -0
  179. core/api/services/git_ops.py +621 -0
  180. core/api/services/graph_cosmo_service.py +440 -0
  181. core/api/services/graph_ranker.py +306 -0
  182. core/api/services/graph_service.py +1589 -0
  183. core/api/services/inbox.py +800 -0
  184. core/api/services/inbox_digest.py +221 -0
  185. core/api/services/inbox_digest_deep_research.py +80 -0
  186. core/api/services/inbox_digest_jobs.py +595 -0
  187. core/api/services/inbox_gmail_sync.py +167 -0
  188. core/api/services/inbox_llm_classifier.py +906 -0
  189. core/api/services/inbox_source_identity.py +116 -0
  190. core/api/services/inbox_sources.py +456 -0
  191. core/api/services/inbox_taxonomy.py +195 -0
  192. core/api/services/inbox_tldr.py +1079 -0
  193. core/api/services/inbox_triage.py +899 -0
  194. core/api/services/ingest/__init__.py +13 -0
  195. core/api/services/ingest/api_key_auth.py +136 -0
  196. core/api/services/ingest/auto_approve.py +120 -0
  197. core/api/services/ingest/classifier.py +138 -0
  198. core/api/services/ingest/confidence.py +173 -0
  199. core/api/services/ingest/dispatch.py +88 -0
  200. core/api/services/ingest/embedding_router.py +272 -0
  201. core/api/services/ingest/events.py +33 -0
  202. core/api/services/ingest/ignore_patterns.py +79 -0
  203. core/api/services/ingest/image_probe.py +218 -0
  204. core/api/services/ingest/ingress.py +263 -0
  205. core/api/services/ingest/insert_saga.py +793 -0
  206. core/api/services/ingest/llm/__init__.py +13 -0
  207. core/api/services/ingest/llm/anthropic_haiku.py +23 -0
  208. core/api/services/ingest/llm/base.py +59 -0
  209. core/api/services/ingest/llm/byok_provider.py +130 -0
  210. core/api/services/ingest/llm/classification_context.py +301 -0
  211. core/api/services/ingest/llm/config_store.py +246 -0
  212. core/api/services/ingest/llm/factory.py +24 -0
  213. core/api/services/ingest/llm/kg_enricher.py +306 -0
  214. core/api/services/ingest/llm/local_gateway.py +821 -0
  215. core/api/services/ingest/llm/local_vllm.py +23 -0
  216. core/api/services/ingest/llm/openai_nano.py +349 -0
  217. core/api/services/ingest/lock_advisory.py +57 -0
  218. core/api/services/ingest/parser_router.py +1756 -0
  219. core/api/services/ingest/parsers/__init__.py +1 -0
  220. core/api/services/ingest/parsers/docling_parser.py +142 -0
  221. core/api/services/ingest/parsers/docparse_gateway.py +178 -0
  222. core/api/services/ingest/parsers/docx_parser.py +127 -0
  223. core/api/services/ingest/parsers/folder_unpacker.py +85 -0
  224. core/api/services/ingest/parsers/gateway_aux.py +147 -0
  225. core/api/services/ingest/parsers/image_parser.py +251 -0
  226. core/api/services/ingest/parsers/internal_markdown.py +89 -0
  227. core/api/services/ingest/parsers/ocr_gateway.py +117 -0
  228. core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
  229. core/api/services/ingest/parsers/pdf_types.py +13 -0
  230. core/api/services/ingest/parsers/transcript_parser.py +445 -0
  231. core/api/services/ingest/parsers/vision_gateway.py +186 -0
  232. core/api/services/ingest/parsers/xlsx_parser.py +91 -0
  233. core/api/services/ingest/parsers/zip_unpacker.py +126 -0
  234. core/api/services/ingest/preflight.py +393 -0
  235. core/api/services/ingest/retry_voyage.py +88 -0
  236. core/api/services/ingest/routing_policy.py +307 -0
  237. core/api/services/ingest/serializers/__init__.py +1 -0
  238. core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
  239. core/api/services/ingest/skip_log.py +74 -0
  240. core/api/services/ingest/watcher.py +637 -0
  241. core/api/services/kg/__init__.py +0 -0
  242. core/api/services/kg/audit.py +49 -0
  243. core/api/services/kg/hybrid_search.py +691 -0
  244. core/api/services/kg/lens.py +339 -0
  245. core/api/services/kg/pr_impact.py +770 -0
  246. core/api/services/kg/queries.py +152 -0
  247. core/api/services/kg/ranking.py +89 -0
  248. core/api/services/kg/rrf.py +143 -0
  249. core/api/services/kg_watcher_control.py +161 -0
  250. core/api/services/local_llm/__init__.py +19 -0
  251. core/api/services/local_llm/async_client.py +385 -0
  252. core/api/services/local_llm/client.py +173 -0
  253. core/api/services/local_llm/url_validator.py +44 -0
  254. core/api/services/metrics_collector.py +646 -0
  255. core/api/services/metrics_providers.py +65 -0
  256. core/api/services/model_registry.py +266 -0
  257. core/api/services/model_router.py +137 -0
  258. core/api/services/n8n_client.py +77 -0
  259. core/api/services/newsletter_llm_gateway.py +66 -0
  260. core/api/services/notification_service.py +134 -0
  261. core/api/services/openai_responses.py +55 -0
  262. core/api/services/opencode_metrics.py +375 -0
  263. core/api/services/opencode_sessions.py +173 -0
  264. core/api/services/pii_redactor.py +138 -0
  265. core/api/services/pr_impact_pipeline/__init__.py +21 -0
  266. core/api/services/pr_impact_pipeline/differ.py +421 -0
  267. core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
  268. core/api/services/pr_impact_pipeline/gc.py +93 -0
  269. core/api/services/pr_impact_pipeline/languages.py +192 -0
  270. core/api/services/pr_impact_pipeline/parser.py +178 -0
  271. core/api/services/pr_impact_pipeline/writer.py +394 -0
  272. core/api/services/pr_service.py +1393 -0
  273. core/api/services/project_paths.py +70 -0
  274. core/api/services/project_status_updates.py +265 -0
  275. core/api/services/providers.py +276 -0
  276. core/api/services/push_service.py +170 -0
  277. core/api/services/reminder_service.py +89 -0
  278. core/api/services/runas.py +41 -0
  279. core/api/services/salience_service.py +69 -0
  280. core/api/services/security_collector.py +281 -0
  281. core/api/services/session_catalog.py +385 -0
  282. core/api/services/session_metrics_service.py +301 -0
  283. core/api/services/session_ops.py +272 -0
  284. core/api/services/session_state.py +173 -0
  285. core/api/services/share_links.py +222 -0
  286. core/api/services/task_transitions.py +146 -0
  287. core/api/services/terminal_metrics.py +462 -0
  288. core/api/services/terminal_metrics_dump.py +203 -0
  289. core/api/services/tmux.py +1205 -0
  290. core/api/services/webhook_service.py +422 -0
  291. core/api/services/workspace_sync.py +164 -0
  292. core/api/templates/__init__.py +1 -0
  293. core/api/templates/markdown_share.py +164 -0
  294. core/api/terminal.py +1031 -0
  295. core/api/tests/__init__.py +0 -0
  296. core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
  297. core/api/tests/test_audit_permissions.py +133 -0
  298. core/api/tests/test_backfill_session_conversations.py +90 -0
  299. core/api/tests/test_backfill_working_seconds_msg.py +129 -0
  300. core/api/tests/test_claude_metrics.py +326 -0
  301. core/api/tests/test_codex_metrics.py +189 -0
  302. core/api/tests/test_finder_paths.py +74 -0
  303. core/api/tests/test_git_ops_merge.py +155 -0
  304. core/api/tests/test_learnings_check_search.py +81 -0
  305. core/api/tests/test_metrics_providers.py +133 -0
  306. core/api/tests/test_migration_087.py +164 -0
  307. core/api/tests/test_migration_088.py +94 -0
  308. core/api/tests/test_migration_089.py +116 -0
  309. core/api/tests/test_openai_responses.py +24 -0
  310. core/api/tests/test_opencode_metrics.py +740 -0
  311. core/api/tests/test_opencode_sessions.py +321 -0
  312. core/api/tests/test_pr_workflow_e2e.py +457 -0
  313. core/api/tests/test_projects_handoffs.py +31 -0
  314. core/api/tests/test_providers.py +138 -0
  315. core/api/tests/test_safety_bridge.py +347 -0
  316. core/api/tests/test_session_catalog.py +142 -0
  317. core/api/tests/test_session_conversations.py +512 -0
  318. core/api/tests/test_session_metrics_service.py +270 -0
  319. core/api/tests/test_session_resume_paths.py +548 -0
  320. core/api/tests/test_session_theme_mode_migration.py +56 -0
  321. core/api/tests/test_sessions_rbac.py +131 -0
  322. core/api/tests/test_share_edit.py +398 -0
  323. core/api/tests/test_share_repo.py +200 -0
  324. core/api/tests/test_terminal_session_manager.py +98 -0
  325. core/api/tests/test_terminal_upload.py +34 -0
  326. core/api/tests/test_tmux.py +272 -0
  327. core/api/tests/test_workspace_sync.py +186 -0
  328. core/api/tests/test_ws_ticket_in_memory.py +73 -0
  329. core/api/use_cases/__init__.py +11 -0
  330. core/api/use_cases/_context.py +89 -0
  331. core/api/use_cases/_errors.py +62 -0
  332. core/api/use_cases/_roles.py +16 -0
  333. core/api/use_cases/audit.py +171 -0
  334. core/api/use_cases/brain.py +1232 -0
  335. core/api/use_cases/costs.py +249 -0
  336. core/api/use_cases/graph.py +1153 -0
  337. core/api/use_cases/handoffs.py +506 -0
  338. core/api/use_cases/ingest_triage.py +1229 -0
  339. core/api/use_cases/learnings.py +538 -0
  340. core/api/use_cases/projects.py +705 -0
  341. core/api/use_cases/pull_requests.py +415 -0
  342. core/api/use_cases/search.py +926 -0
  343. core/api/use_cases/tasks.py +1495 -0
  344. core/api/visibility.py +141 -0
  345. core/cli/__init__.py +5 -0
  346. core/cli/_index_source.py +632 -0
  347. core/cli/_runtime_ctx.py +160 -0
  348. core/cli/_transmute.py +241 -0
  349. core/cli/marvis_doctor.py +704 -0
  350. core/cli/marvis_feedback.py +396 -0
  351. core/cli/marvis_governance.py +315 -0
  352. core/cli/marvis_hooks.py +515 -0
  353. core/cli/marvis_init.py +757 -0
  354. core/cli/marvis_mcp.py +401 -0
  355. core/cli/marvis_runtime.py +855 -0
  356. core/cli/marvis_telemetry.py +228 -0
  357. core/scripts/_drift_check.py +716 -0
  358. core/scripts/_frontmatter.py +66 -0
  359. core/scripts/_graph_writer.py +189 -0
  360. core/scripts/ast_parser.py +1553 -0
  361. core/scripts/install_hooks/__init__.py +1 -0
  362. core/scripts/install_hooks/_config.sh +109 -0
  363. core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
  364. core/scripts/install_hooks/block-db-direct-write.sh +23 -0
  365. core/scripts/install_hooks/block-push-no-task.sh +23 -0
  366. core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
  367. core/scripts/install_hooks/block-subtree-push.sh +23 -0
  368. core/scripts/install_hooks/config.json +53 -0
  369. core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
  370. core/scripts/install_hooks/enforce-worktree.sh +23 -0
  371. core/scripts/install_hooks/quality-gate.sh +170 -0
  372. core/scripts/install_hooks/safety_bridge.py +968 -0
  373. core/scripts/install_hooks/secret-scan.sh +23 -0
  374. core/scripts/migrate_spike_node_ids.py +122 -0
  375. core/scripts/populate_artifacts.py +2198 -0
  376. core/scripts/populate_cross_project.py +2457 -0
  377. core/scripts/populate_inbox_nodes.py +357 -0
  378. core/scripts/populate_pr_impact.py +267 -0
  379. core/scripts/populate_project_nodes.py +603 -0
  380. core/scripts/populate_touch_counter.py +337 -0
  381. core/scripts/reparse_failed.py +57 -0
  382. core/scripts/safety_bridge.py +968 -0
  383. core/telemetry/__init__.py +9 -0
  384. core/telemetry/client.py +405 -0
  385. core/telemetry/schema.py +122 -0
  386. core/wizard/__init__.py +65 -0
  387. core/wizard/byok_vault.py +147 -0
  388. core/wizard/defaults.py +58 -0
  389. core/wizard/state.py +117 -0
  390. core/wizard/steps.py +70 -0
  391. core/wizard/validation.py +136 -0
  392. marvisx_cli-0.1.0.dist-info/METADATA +201 -0
  393. marvisx_cli-0.1.0.dist-info/RECORD +587 -0
  394. marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
  395. marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
  396. marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
  397. marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
  398. migrations/001_initial.sql +33 -0
  399. migrations/002_tasks.sql +30 -0
  400. migrations/003_session_management.sql +7 -0
  401. migrations/004_projects_comments.sql +65 -0
  402. migrations/005_session_intelligence.sql +15 -0
  403. migrations/006_settings.sql +12 -0
  404. migrations/007_task_scoring.sql +12 -0
  405. migrations/008_cost_tracking.sql +31 -0
  406. migrations/009_session_card_metrics.sql +3 -0
  407. migrations/010_monitoring.sql +55 -0
  408. migrations/012_agent_api.sql +21 -0
  409. migrations/013_session_complete.sql +8 -0
  410. migrations/015_pull_requests.sql +43 -0
  411. migrations/015_pull_requests_down.sql +5 -0
  412. migrations/016_users_raci.sql +116 -0
  413. migrations/017_task_cost_entries.sql +87 -0
  414. migrations/018_agents.sql +73 -0
  415. migrations/018_agents_down.sql +13 -0
  416. migrations/019_review_feedback.sql +18 -0
  417. migrations/020_pr_commit_sha.sql +4 -0
  418. migrations/021_webhook_events.sql +18 -0
  419. migrations/022_devx_agent_managed.sql +11 -0
  420. migrations/022_devx_agent_managed_down.sql +6 -0
  421. migrations/023_devx_p1_gate.sql +7 -0
  422. migrations/023_devx_p1_gate_down.sql +3 -0
  423. migrations/024_chat_messages.sql +16 -0
  424. migrations/024_pr_conversation_id.sql +8 -0
  425. migrations/024_task_indexes.sql +21 -0
  426. migrations/024_task_indexes_down.sql +7 -0
  427. migrations/025_audit_log.sql +17 -0
  428. migrations/026_agent_tokens.sql +20 -0
  429. migrations/027_teams_auth_phase_b.sql +35 -0
  430. migrations/028_learnings.sql +23 -0
  431. migrations/029_team_roles.sql +14 -0
  432. migrations/030_finder_pins.sql +10 -0
  433. migrations/031_pr_deploy_status.sql +9 -0
  434. migrations/032_task_reminders.sql +7 -0
  435. migrations/033_events_retry_count.sql +6 -0
  436. migrations/033_session_owner.sql +9 -0
  437. migrations/034_notifications.sql +38 -0
  438. migrations/035_shared_links.sql +15 -0
  439. migrations/036_session_index_upgrade.sql +29 -0
  440. migrations/037_pr_approval.sql +15 -0
  441. migrations/038_pr_submitted_by.sql +6 -0
  442. migrations/039_push_subscriptions.sql +17 -0
  443. migrations/040_semantic_search.sql +16 -0
  444. migrations/041_workspaces.sql +63 -0
  445. migrations/042_oidc_providers.sql +24 -0
  446. migrations/043_ci_checks.sql +31 -0
  447. migrations/044_agent_metrics.sql +30 -0
  448. migrations/045_documents_doc_type.sql +5 -0
  449. migrations/046_salience.sql +13 -0
  450. migrations/047_seed_missing_agents.sql +6 -0
  451. migrations/048_fix_agent_paths_roles.sql +5 -0
  452. migrations/049_agent_role_and_learnings_schema.sql +3 -0
  453. migrations/050_session_provider.sql +2 -0
  454. migrations/051_session_launch_profile.sql +4 -0
  455. migrations/052_session_theme_mode.sql +2 -0
  456. migrations/052_task_kind.sql +4 -0
  457. migrations/053_inbox_items.sql +31 -0
  458. migrations/054_inbox_triage_contract.sql +30 -0
  459. migrations/055_inbox_topic_treatment.sql +12 -0
  460. migrations/056_inbox_treatment_read_save.sql +57 -0
  461. migrations/057_session_theme_mode_backfill.sql +4 -0
  462. migrations/058_inbox_item_status_lifecycle.sql +13 -0
  463. migrations/059_inbox_tldr_and_source_scores.sql +18 -0
  464. migrations/060_newsletter.sql +16 -0
  465. migrations/061_inbox_redesign.sql +69 -0
  466. migrations/062_fix_inbox_sources_backfill.sql +37 -0
  467. migrations/063_task_completion_mode.sql +23 -0
  468. migrations/064_judge_mode_setting.sql +4 -0
  469. migrations/065_knowledge_graph_spike.sql +40 -0
  470. migrations/066_digest_ranking_inputs.sql +10 -0
  471. migrations/066_kg_artifact_nodes.sql +129 -0
  472. migrations/067_inbox_digest_selections.sql +28 -0
  473. migrations/067_kg_temporal.sql +53 -0
  474. migrations/068_inbox_digest_app_settings.sql +9 -0
  475. migrations/068_kg_touch_counter.sql +52 -0
  476. migrations/069_kg_doc_types.sql +117 -0
  477. migrations/070_digest_ranking_inputs_recovery.sql +3 -0
  478. migrations/071_inbox_digest_selections_recovery.sql +3 -0
  479. migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
  480. migrations/073_kg_cross_project.sql +216 -0
  481. migrations/073_kg_cross_project_down.sql +77 -0
  482. migrations/074_kg_infra_types.sql +208 -0
  483. migrations/074_kg_infra_types_down.sql +80 -0
  484. migrations/075_kg_file_state_recovery.sql +35 -0
  485. migrations/075_kg_file_state_recovery_down.sql +5 -0
  486. migrations/076_kg_watcher_state.sql +33 -0
  487. migrations/076_kg_watcher_state_down.sql +3 -0
  488. migrations/077_kg_doc_types_extend.sql +226 -0
  489. migrations/077_kg_doc_types_extend_down.sql +80 -0
  490. migrations/078_kg_fts5.sql +102 -0
  491. migrations/078_kg_fts5_down.sql +14 -0
  492. migrations/079_kg_missing_indexes.sql +31 -0
  493. migrations/079_kg_missing_indexes_down.sql +10 -0
  494. migrations/080_kg_fts5_extended.sql +232 -0
  495. migrations/080_kg_fts5_extended_down.sql +25 -0
  496. migrations/081_kg_lens_indexes.sql +9 -0
  497. migrations/081_kg_lens_indexes_down.sql +3 -0
  498. migrations/082_kg_pins.sql +26 -0
  499. migrations/082_kg_pins_down.sql +14 -0
  500. migrations/083_kg_graph_nodes_degree.sql +20 -0
  501. migrations/083_kg_graph_nodes_degree_down.sql +15 -0
  502. migrations/084_drop_legacy_scheduler_tables.sql +58 -0
  503. migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
  504. migrations/085_kg_edge_resolves_to.sql +142 -0
  505. migrations/085_kg_edge_resolves_to_down.sql +66 -0
  506. migrations/086_project_status_updates_feed.sql +20 -0
  507. migrations/086_project_status_updates_feed_down.sql +36 -0
  508. migrations/087_session_metrics_dual.sql +50 -0
  509. migrations/087_session_metrics_dual_down.sql +21 -0
  510. migrations/088_rename_context_pct_legacy.sql +23 -0
  511. migrations/088_rename_context_pct_legacy_down.sql +8 -0
  512. migrations/089_session_metrics_equivalent_cost.sql +26 -0
  513. migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
  514. migrations/090_kg_inbox_node_type.sql +26 -0
  515. migrations/090_kg_inbox_node_type_down.sql +20 -0
  516. migrations/091_kg_inbox_node_type_check.sql +265 -0
  517. migrations/091_kg_inbox_node_type_check_down.sql +129 -0
  518. migrations/092_sessions_activity_state_ts.sql +29 -0
  519. migrations/092_sessions_activity_state_ts_down.sql +14 -0
  520. migrations/093_sessions_activity_state_column.sql +29 -0
  521. migrations/093_sessions_activity_state_column_down.sql +10 -0
  522. migrations/094_ingest_pending.sql +55 -0
  523. migrations/094_ingest_pending_down.sql +15 -0
  524. migrations/095_kg_intent_first.sql +77 -0
  525. migrations/095_kg_intent_first_down.sql +25 -0
  526. migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
  527. migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
  528. migrations/097_ingest_change_history.sql +37 -0
  529. migrations/097_ingest_change_history_down.sql +13 -0
  530. migrations/098_kg_node_type_business.sql +254 -0
  531. migrations/098_kg_node_type_business_down.sql +195 -0
  532. migrations/099_kg_edges_restore_weight.sql +58 -0
  533. migrations/099_kg_edges_restore_weight_down.sql +12 -0
  534. migrations/100_kg_enriched_at.sql +25 -0
  535. migrations/100_kg_enriched_at_down.sql +12 -0
  536. migrations/101_local_llm_shadow_comparisons.sql +66 -0
  537. migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
  538. migrations/102_promote_llm_costs.sql +69 -0
  539. migrations/102_promote_llm_costs_down.sql +19 -0
  540. migrations/103_ingest_skipped_log.sql +46 -0
  541. migrations/103_ingest_skipped_log_down.sql +15 -0
  542. migrations/120_docs_governance.sql +50 -0
  543. migrations/120_docs_governance_down.sql +11 -0
  544. migrations/121_notification_event_fk_cleanup.sql +21 -0
  545. migrations/121_notification_event_fk_cleanup_down.sql +10 -0
  546. migrations/122_docs_drift_history.sql +34 -0
  547. migrations/122_docs_drift_history_down.sql +15 -0
  548. migrations/123_ingest_parser_waiting_status.sql +69 -0
  549. migrations/123_ingest_parser_waiting_status_down.sql +69 -0
  550. migrations/124_heypocket_recordings.sql +63 -0
  551. migrations/124_heypocket_recordings_down.sql +13 -0
  552. migrations/125_kg_node_type_record.sql +219 -0
  553. migrations/125_kg_node_type_record_down.sql +205 -0
  554. migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
  555. migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
  556. migrations/127_brain_v1_substrate.sql +200 -0
  557. migrations/127_brain_v1_substrate_down.sql +32 -0
  558. migrations/128_brain_drift_signals.sql +157 -0
  559. migrations/128_brain_drift_signals_down.sql +23 -0
  560. migrations/129_brain_memory_operations.sql +232 -0
  561. migrations/129_brain_memory_operations_down.sql +27 -0
  562. migrations/130_brain_findings.sql +258 -0
  563. migrations/130_brain_findings_down.sql +29 -0
  564. migrations/132_kg_pr_modifies.sql +242 -0
  565. migrations/132_kg_pr_modifies_down.sql +99 -0
  566. migrations/133_brain_v1_2_direction_schema.sql +476 -0
  567. migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
  568. migrations/134_brain_journal_narrative_polished.sql +8 -0
  569. migrations/134_brain_journal_narrative_polished_down.sql +6 -0
  570. migrations/135_kg_edges_provider.sql +21 -0
  571. migrations/136_documents_fts.sql +56 -0
  572. migrations/137_promote_llm_costs.sql +59 -0
  573. migrations/137_promote_llm_costs_down.sql +19 -0
  574. migrations/138_ingest_api_keys.sql +39 -0
  575. migrations/138_ingest_api_keys_down.sql +8 -0
  576. migrations/139_ingest_pending_ingress.sql +91 -0
  577. migrations/139_ingest_pending_ingress_down.sql +73 -0
  578. migrations/140_ingest_idempotency_quota.sql +45 -0
  579. migrations/140_ingest_idempotency_quota_down.sql +9 -0
  580. migrations/141_ingest_pending_metadata.sql +16 -0
  581. migrations/141_ingest_pending_metadata_down.sql +7 -0
  582. migrations/142_llm_function_config.sql +36 -0
  583. migrations/142_llm_function_config_down.sql +8 -0
  584. migrations/143_kg_code_embeddings.sql +25 -0
  585. migrations/143_kg_code_embeddings_down.sql +5 -0
  586. migrations/__init__.py +4 -0
  587. projects/_template/project.yaml +46 -0
@@ -0,0 +1,395 @@
1
+ """Self-hosted embedding client for Granite 97M multilingual models.
2
+
3
+ Torch-free engine: runs ``ibm-granite/granite-embedding-97m-multilingual-r2``
4
+ (384-dim, ModernBert) directly via ``onnxruntime`` + the Rust ``tokenizers``
5
+ package + ``huggingface_hub``. No ``sentence-transformers`` / ``optimum`` /
6
+ ``torch`` — those drag in ~1GB of wheels even when inference runs on ONNX.
7
+
8
+ The public shape mirrors the previous client so ``embedding_service.py`` and the
9
+ MCP/search layers are untouched (no-fork): ``embed_texts(texts, input_type,
10
+ batch_size) -> list[list[float]]`` plus ``is_available()`` / ``can_attempt_load``
11
+ / ``active_model_name`` / ``dimensions`` / ``cache_folder``. Model loading is
12
+ lazy: constructing the client only does inexpensive env + RAM checks; the ONNX
13
+ session and tokenizer are created on the first embed (or warmed in
14
+ ``is_available()``).
15
+
16
+ Pooling is **CLS** (``last_hidden_state[:, 0]``) then L2-normalize — NOT mean.
17
+ This matches the model's ``1_Pooling/config.json`` (``pooling_mode_cls_token=true``).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import importlib
23
+ import logging
24
+ import math
25
+ import os
26
+ import threading
27
+ import time
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ logger = logging.getLogger("marvisx.embedding_internal")
32
+
33
+ DEFAULT_MODEL = "ibm-granite/granite-embedding-97m-multilingual-r2"
34
+ DEFAULT_RAM_THRESHOLD_MB = 4096
35
+ EXPECTED_DIMENSIONS = 384
36
+ _MB = 1024 * 1024
37
+
38
+ # --- Pinned model revision (review delta #2) --------------------------------
39
+ # Pin the exact commit so the model graph + tokenizer never drift silently under
40
+ # us (a future re-export would break the cosine>=0.999 invariant the search
41
+ # pipeline depends on). The equivalence fixture is generated from THIS revision.
42
+ # https://huggingface.co/ibm-granite/granite-embedding-97m-multilingual-r2
43
+ MODEL_REVISION = "835ad14087e140460703cf0fae09f97d469d65c2"
44
+
45
+ # --- ONNX graph selection ---------------------------------------------------
46
+ # The HF repo ships two ONNX graphs: onnx/model.onnx (fp32, ~390MB) and
47
+ # onnx/model_quint8_avx2.onnx (int8, ~98MB). There is no fp16 export in the repo.
48
+ # Precision was measured empirically via tests/test_embedding_onnx_equivalence.py
49
+ # (review delta #7): fp32 holds cosine>=0.999 vs sentence-transformers on the
50
+ # IT+EN code+prose corpus; int8 drops below it (1-3 recall points at risk), so
51
+ # fp32 is the default. Override with EMBEDDING_ONNX_FILE for benchmarking only.
52
+ _ONNX_FILE_DEFAULT = "onnx/model.onnx"
53
+
54
+ # --- Tokenizer constants (review delta #3) ----------------------------------
55
+ # Hardcoded with a source comment rather than parsing config at runtime (a
56
+ # parser + fallback is the worst of both worlds for a single fixed model). The
57
+ # equivalence test catches any drift from these values.
58
+ # Source: config.json (pad_token_id=179935, cls_token_id=179934),
59
+ # tokenizer_config.json (pad_token="<|endoftext|>", model_max_length=32768),
60
+ # special_tokens_map.json (pad_token "<|endoftext|>").
61
+ PAD_ID = 179935
62
+ PAD_TOKEN = "<|endoftext|>"
63
+ CLS_TOKEN_ID = 179934 # added by the tokenizer template, never injected by hand
64
+
65
+ # Truncation length: MUST match what sentence-transformers used, or long inputs
66
+ # get a different CLS vector => cosine<0.999 (verified: truncating at 512 dropped
67
+ # two >512-token corpus strings to cos~0.994). sentence-transformers reads this
68
+ # model's truncation from sentence_bert_config.json => max_seq_length=32768 (NOT
69
+ # the 512 the plan assumed; this ModernBert R2 model has a 32768 context window,
70
+ # matching config.json max_position_embeddings). Hardcode 32768 to mirror ST.
71
+ # Source: sentence_bert_config.json (max_seq_length=32768), config.json
72
+ # (max_position_embeddings=32768). Padding is to the per-batch max, not MAX_LEN,
73
+ # so short inputs stay cheap; only genuinely long docs pay the longer sequence.
74
+ MAX_LEN = 32768
75
+
76
+ # Only fetch the ONNX graph + tokenizer/config — never *.safetensors / *.bin
77
+ # (those are the torch weights we are eliminating).
78
+ _ALLOW_PATTERNS = [
79
+ "onnx/model.onnx",
80
+ "onnx/model.onnx_data", # external-data sidecar if the graph is split (safe no-op if absent)
81
+ "onnx/model_quint8_avx2.onnx",
82
+ "tokenizer.json",
83
+ "tokenizer_config.json",
84
+ "special_tokens_map.json",
85
+ "config.json",
86
+ "1_Pooling/config.json",
87
+ "config_sentence_transformers.json",
88
+ ]
89
+
90
+
91
+ class GraniteEmbeddingClient:
92
+ """Lazy torch-free onnxruntime client for local Granite embeddings.
93
+
94
+ Granite is the only local model. Machines below the RAM floor are NOT
95
+ silently downgraded to a weaker model: the client emits an actionable
96
+ warning pointing at the hardware requirement and proceeds with Granite.
97
+
98
+ Thread-safety (review delta #4): the API is async and offloads embeds via
99
+ ``run_in_executor``, so multiple threads may call ``embed_texts`` / trigger
100
+ the lazy load concurrently. The lazy load is guarded by ``_load_lock`` (two
101
+ threads must not download/instantiate twice). ``InferenceSession.run`` is
102
+ thread-safe, but a ``tokenizers.Tokenizer`` configured with
103
+ ``enable_padding``/``enable_truncation`` mutates internal state and is NOT
104
+ thread-safe, so each embed call tokenizes under ``_encode_lock``.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ model_name: str = DEFAULT_MODEL,
110
+ device: str = "cpu",
111
+ ) -> None:
112
+ self.model_name = model_name
113
+ self.device = device
114
+ self._active_model_name = model_name
115
+ # _model holds the loaded engine tuple (session, tokenizer); None until
116
+ # the lazy load succeeds. This is what is_available() gates on, mirroring
117
+ # the previous contract (review delta #5).
118
+ self._model: tuple[Any, Any] | None = None
119
+ self._backend: str | None = None
120
+ self._dimensions: int | None = None
121
+ self._load_error: BaseException | None = None
122
+ self._load_lock = threading.Lock()
123
+ self._encode_lock = threading.Lock()
124
+ self._cache_folder = Path(
125
+ os.environ.get("HF_HOME", "~/.cache/huggingface")
126
+ ).expanduser()
127
+ self._ram_threshold_mb = _int_from_env(
128
+ "EMBEDDING_RAM_THRESHOLD_MB", DEFAULT_RAM_THRESHOLD_MB
129
+ )
130
+ self._onnx_file = os.environ.get("EMBEDDING_ONNX_FILE", _ONNX_FILE_DEFAULT)
131
+
132
+ available_mb = _available_memory_mb()
133
+ if available_mb is not None and available_mb < self._ram_threshold_mb:
134
+ logger.warning(
135
+ "Granite embedding may be RAM-constrained: available RAM %.0fMB is "
136
+ "below the %dMB floor. The local engine requires more memory; see the "
137
+ "hardware requirement (run `marvis doctor`). Proceeding with %s — no "
138
+ "silent downgrade to a weaker model.",
139
+ available_mb,
140
+ self._ram_threshold_mb,
141
+ model_name,
142
+ )
143
+
144
+ @property
145
+ def active_model_name(self) -> str:
146
+ """Return the active Granite model name."""
147
+
148
+ return self._active_model_name
149
+
150
+ @property
151
+ def cache_folder(self) -> Path:
152
+ """Return the Hugging Face cache folder used for the model snapshot."""
153
+
154
+ return self._cache_folder
155
+
156
+ @property
157
+ def can_attempt_load(self) -> bool:
158
+ """Return false only after a model load crash has been recorded."""
159
+
160
+ return self._load_error is None
161
+
162
+ def is_available(self) -> bool:
163
+ """Return true once the model has loaded successfully.
164
+
165
+ Eagerly warms the model (review delta #6): the first
166
+ ``InferenceSession.__init__`` + a dummy run cost 1-3s; paying it here
167
+ moves the cold-start out of the first real request. A load failure is
168
+ swallowed (recorded in ``_load_error``) so callers can degrade to the
169
+ keyword lane instead of crashing — matching the previous contract.
170
+ """
171
+
172
+ if self._model is not None and self._load_error is None:
173
+ return True
174
+ if self._load_error is not None:
175
+ return False
176
+ try:
177
+ self._get_model()
178
+ except Exception: # noqa: BLE001 — recorded in _load_error, degrade gracefully
179
+ return False
180
+ return self._model is not None and self._load_error is None
181
+
182
+ @property
183
+ def dimensions(self) -> int:
184
+ """Native output dimensions for Granite."""
185
+
186
+ return self._dimensions or EXPECTED_DIMENSIONS
187
+
188
+ def embed_texts(
189
+ self,
190
+ texts: list[str],
191
+ input_type: str = "document",
192
+ batch_size: int = 16,
193
+ ) -> list[list[float]]:
194
+ """Embed texts and return L2-normalized native 384-dimensional vectors.
195
+
196
+ ``input_type`` is a no-op (kept for interface compatibility): the model's
197
+ ``config_sentence_transformers.json`` declares empty query/document
198
+ prompts, so sentence-transformers adds no prefix either.
199
+ """
200
+
201
+ if not texts:
202
+ return []
203
+
204
+ self._get_model()
205
+ try:
206
+ raw_vectors = self._encode(texts, batch_size)
207
+ vectors = [_normalize_l2(row) for row in raw_vectors]
208
+ except Exception:
209
+ logger.exception(
210
+ "Granite embedding failed model=%s backend=%s input_type=%s",
211
+ self._active_model_name,
212
+ self._backend or "unknown",
213
+ input_type,
214
+ )
215
+ raise
216
+
217
+ if vectors:
218
+ self._dimensions = len(vectors[0])
219
+ return vectors
220
+
221
+ def _get_model(self) -> tuple[Any, Any]:
222
+ if self._model is not None:
223
+ return self._model
224
+ with self._load_lock:
225
+ # Re-check under the lock: a concurrent caller may have loaded it.
226
+ if self._model is not None:
227
+ return self._model
228
+ if self._load_error is not None:
229
+ raise RuntimeError(
230
+ f"Embedding model load previously failed: {self._active_model_name}"
231
+ ) from self._load_error
232
+
233
+ start = time.perf_counter()
234
+ try:
235
+ session, tokenizer = self._load_onnx_session()
236
+ except Exception as exc:
237
+ self._load_error = exc
238
+ logger.exception(
239
+ "Failed to load embedding model model=%s device=%s cache=%s",
240
+ self._active_model_name,
241
+ self.device,
242
+ self._cache_folder,
243
+ )
244
+ raise RuntimeError(
245
+ f"Embedding model unavailable: {self._active_model_name}"
246
+ ) from exc
247
+
248
+ self._model = (session, tokenizer)
249
+ self._backend = "onnxruntime"
250
+ self._dimensions = _session_output_dim(session) or EXPECTED_DIMENSIONS
251
+ if self._dimensions != EXPECTED_DIMENSIONS:
252
+ logger.warning(
253
+ "Granite ONNX output dim=%d != expected %d",
254
+ self._dimensions,
255
+ EXPECTED_DIMENSIONS,
256
+ )
257
+ logger.info(
258
+ "Embedding model loaded model=%s backend=%s device=%s dims=%d "
259
+ "onnx=%s cache=%s load_ms=%d",
260
+ self._active_model_name,
261
+ self._backend,
262
+ self.device,
263
+ self._dimensions,
264
+ self._onnx_file,
265
+ self._cache_folder,
266
+ int((time.perf_counter() - start) * 1000),
267
+ )
268
+ return self._model
269
+
270
+ def _load_onnx_session(self) -> tuple[Any, Any]:
271
+ ort = importlib.import_module("onnxruntime")
272
+ tokenizers_mod = importlib.import_module("tokenizers")
273
+ hub = importlib.import_module("huggingface_hub")
274
+
275
+ local = hub.snapshot_download(
276
+ self._active_model_name,
277
+ revision=MODEL_REVISION, # pinned (delta #2)
278
+ cache_dir=str(self._cache_folder),
279
+ allow_patterns=_ALLOW_PATTERNS,
280
+ )
281
+ local_path = Path(local)
282
+
283
+ tokenizer = tokenizers_mod.Tokenizer.from_file(str(local_path / "tokenizer.json"))
284
+ tokenizer.enable_truncation(max_length=MAX_LEN)
285
+ tokenizer.enable_padding(pad_id=PAD_ID, pad_token=PAD_TOKEN)
286
+
287
+ sess_options = ort.SessionOptions()
288
+ # intra_op = physical cores by default; inter_op=1 (single graph, batched).
289
+ threads = _int_from_env("EMBEDDING_ORT_THREADS", _physical_cores())
290
+ if threads > 0:
291
+ sess_options.intra_op_num_threads = threads
292
+ sess_options.inter_op_num_threads = 1
293
+ sess_options.graph_optimization_level = (
294
+ ort.GraphOptimizationLevel.ORT_ENABLE_ALL
295
+ )
296
+ # Persist the optimized graph next to the model so subsequent cold-starts
297
+ # skip re-optimization (review delta #6). Best-effort: ignore if unwritable.
298
+ try:
299
+ opt_path = local_path / "onnx" / (
300
+ Path(self._onnx_file).name + ".ort_optimized.onnx"
301
+ )
302
+ sess_options.optimized_model_filepath = str(opt_path)
303
+ except Exception: # noqa: BLE001
304
+ pass
305
+
306
+ session = ort.InferenceSession(
307
+ str(local_path / self._onnx_file),
308
+ sess_options=sess_options,
309
+ providers=["CPUExecutionProvider"],
310
+ )
311
+
312
+ # Warm-up dummy run so the first real request does not pay graph init.
313
+ try:
314
+ self._run_session(session, tokenizer, ["warmup"])
315
+ except Exception: # noqa: BLE001 — warmup is best-effort
316
+ logger.debug("Granite ONNX warm-up run failed (non-fatal)", exc_info=True)
317
+
318
+ return session, tokenizer
319
+
320
+ def _encode(self, texts: list[str], batch_size: int) -> list[list[float]]:
321
+ session, tokenizer = self._model # type: ignore[misc]
322
+ out: list[list[float]] = []
323
+ for start in range(0, len(texts), max(1, batch_size)):
324
+ chunk = texts[start : start + max(1, batch_size)]
325
+ out.extend(self._run_session(session, tokenizer, chunk))
326
+ return out
327
+
328
+ def _run_session(
329
+ self, session: Any, tokenizer: Any, chunk: list[str]
330
+ ) -> list[list[float]]:
331
+ import numpy as np # local import keeps module import cheap
332
+
333
+ # Tokenizer state (padding/truncation) is mutable + not thread-safe:
334
+ # encode_batch under a lock; padding is to the max of THIS batch.
335
+ with self._encode_lock:
336
+ encodings = tokenizer.encode_batch(chunk)
337
+ ids = np.array([e.ids for e in encodings], dtype=np.int64)
338
+ mask = np.array([e.attention_mask for e in encodings], dtype=np.int64)
339
+ hidden = session.run(
340
+ ["last_hidden_state"],
341
+ {"input_ids": ids, "attention_mask": mask},
342
+ )[0]
343
+ # CLS pooling — first token. NOT mean pooling (delta from the old client).
344
+ cls = hidden[:, 0]
345
+ return [[float(v) for v in row] for row in cls]
346
+
347
+
348
+ def _physical_cores() -> int:
349
+ try:
350
+ psutil = importlib.import_module("psutil")
351
+ cores = psutil.cpu_count(logical=False)
352
+ if cores:
353
+ return int(cores)
354
+ except Exception: # noqa: BLE001
355
+ pass
356
+ return os.cpu_count() or 1
357
+
358
+
359
+ def _session_output_dim(session: Any) -> int | None:
360
+ try:
361
+ for output in session.get_outputs():
362
+ if output.name == "last_hidden_state":
363
+ shape = output.shape
364
+ if shape and isinstance(shape[-1], int):
365
+ return int(shape[-1])
366
+ except Exception: # noqa: BLE001
367
+ return None
368
+ return None
369
+
370
+
371
+ def _available_memory_mb() -> float | None:
372
+ try:
373
+ psutil = importlib.import_module("psutil")
374
+ except ImportError:
375
+ logger.warning("psutil is not installed; skipping embedding RAM gate")
376
+ return None
377
+ return float(psutil.virtual_memory().available) / _MB
378
+
379
+
380
+ def _int_from_env(name: str, default: int) -> int:
381
+ raw = os.environ.get(name)
382
+ if not raw:
383
+ return default
384
+ try:
385
+ return int(raw)
386
+ except ValueError:
387
+ logger.warning("Invalid %s=%r; using %d", name, raw, default)
388
+ return default
389
+
390
+
391
+ def _normalize_l2(vector: list[float]) -> list[float]:
392
+ norm = math.sqrt(sum(value * value for value in vector))
393
+ if norm == 0.0:
394
+ return [0.0 for _ in vector]
395
+ return [value / norm for value in vector]