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,251 @@
1
+ """Standalone image parser for Universal Ingestion phase 1."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import logging
6
+ import re
7
+ from functools import lru_cache
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from PIL import Image
12
+
13
+ from core.api.services.ingest.parsers.docparse_gateway import parse_docparse_with_gateway
14
+ from core.api.services.ingest.parsers.gateway_aux import MissingGatewayConfig
15
+ from core.api.services.ingest.parsers.gateway_aux import settings as gateway_settings
16
+ from core.api.services.ingest.parsers.ocr_gateway import parse_ocr_with_gateway
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ SUPPORTED_IMAGE_SUFFIXES = frozenset({".png", ".jpg", ".jpeg", ".webp"})
21
+ UNSUPPORTED_PHASE1_SUFFIXES = frozenset({".avif", ".heic", ".heif"})
22
+ EXIF_GPS_TAG = 0x8825
23
+ EXIF_ORIENTATION_TAG = 0x0112
24
+ IMAGE_MIME_BY_SUFFIX = {
25
+ ".jpeg": "image/jpeg",
26
+ ".jpg": "image/jpeg",
27
+ ".png": "image/png",
28
+ ".webp": "image/webp",
29
+ }
30
+ _HTML_TAG_RE = re.compile(r"<[^>]+>")
31
+ _IMAGE_MARKUP_RE = re.compile(r"(<\s*img\b|!\[[^\]]*\]\()", re.IGNORECASE)
32
+ _MARKDOWN_IMAGE_RE = re.compile(r"!\[[^\]]*\]\([^)]*\)")
33
+
34
+
35
+ def _validate_image_suffix(image_path: Path) -> None:
36
+ suffix = image_path.suffix.lower()
37
+ if suffix in UNSUPPORTED_PHASE1_SUFFIXES:
38
+ raise ValueError(f"unsupported_phase1_image_ext: {suffix}")
39
+ if suffix not in SUPPORTED_IMAGE_SUFFIXES:
40
+ raise ValueError(f"unsupported_image_ext: {suffix}")
41
+
42
+
43
+ def _image_result(
44
+ image_path: Path,
45
+ *,
46
+ exif_result: dict[str, Any],
47
+ ocr_result: dict[str, Any],
48
+ ) -> dict[str, Any]:
49
+ post_redact_sha = hashlib.sha256(image_path.read_bytes()).hexdigest()
50
+
51
+ structure = {
52
+ "kind": "image",
53
+ "format": exif_result["format"],
54
+ "width": exif_result["width"],
55
+ "height": exif_result["height"],
56
+ "mode": exif_result["mode"],
57
+ "exif_redacted": exif_result["exif_redacted"],
58
+ "exif_removed": exif_result["exif_removed"],
59
+ "sha256_post_redact": post_redact_sha,
60
+ "ocr_confidence_avg": ocr_result.get("confidence_avg", 0.0),
61
+ "ocr_lines": ocr_result.get("lines") or [],
62
+ }
63
+ if ocr_result.get("raw") is not None:
64
+ structure["ocr_raw"] = ocr_result["raw"]
65
+ return {
66
+ "frontmatter": {},
67
+ "text": str(ocr_result.get("extracted_text") or ""),
68
+ "structure": structure,
69
+ "parser_used": str(ocr_result.get("parser_used") or "none"),
70
+ "extracted_text": str(ocr_result.get("extracted_text") or ""),
71
+ "exif_redacted": exif_result["exif_redacted"],
72
+ "exif_removed": exif_result["exif_removed"],
73
+ "sha256_post_redact": post_redact_sha,
74
+ "ocr_confidence_avg": ocr_result.get("confidence_avg", 0.0),
75
+ }
76
+
77
+
78
+ def parse_image(path: str | Path) -> dict[str, Any]:
79
+ """Parse a standalone image and redact privacy-sensitive EXIF metadata."""
80
+ image_path = Path(path)
81
+ _validate_image_suffix(image_path)
82
+ exif_result = _strip_exif_privacy(image_path)
83
+ return _image_result(
84
+ image_path,
85
+ exif_result=exif_result,
86
+ ocr_result=_ocr_with_fallback(image_path),
87
+ )
88
+
89
+
90
+ async def parse_image_with_gateway(
91
+ path: str | Path,
92
+ mime_type: str | None = None,
93
+ *,
94
+ prefer_docparse: bool | None = None,
95
+ docparse_mode: str | None = None,
96
+ ) -> dict[str, Any]:
97
+ """Parse an image with the Mac Gateway OCR endpoint when configured."""
98
+ image_path = Path(path)
99
+ _validate_image_suffix(image_path)
100
+ exif_result = _strip_exif_privacy(image_path)
101
+ upload_mime = mime_type or IMAGE_MIME_BY_SUFFIX[image_path.suffix.lower()]
102
+ cfg = gateway_settings()
103
+ docparse_enabled = getattr(cfg, "ingest_docparse_enabled", False) and getattr(
104
+ cfg, "ingest_docparse_images_enabled", True
105
+ )
106
+ if prefer_docparse is not None:
107
+ docparse_enabled = bool(prefer_docparse)
108
+ if docparse_enabled:
109
+ try:
110
+ docparse_result = await parse_docparse_with_gateway(
111
+ image_path,
112
+ upload_mime,
113
+ mode=docparse_mode,
114
+ )
115
+ if _docparse_result_is_image_only(docparse_result):
116
+ logger.warning(
117
+ "tier-docparse returned image-only markup; falling back to OCR: path=%s",
118
+ image_path,
119
+ )
120
+ else:
121
+ return _image_docparse_result(
122
+ image_path,
123
+ exif_result=exif_result,
124
+ docparse_result=docparse_result,
125
+ )
126
+ except MissingGatewayConfig:
127
+ logger.warning("tier-docparse not configured; falling back to OCR")
128
+
129
+ try:
130
+ ocr_result = await parse_ocr_with_gateway(
131
+ image_path,
132
+ upload_mime,
133
+ )
134
+ except MissingGatewayConfig:
135
+ ocr_result = _ocr_with_fallback(image_path)
136
+ return _image_result(
137
+ image_path,
138
+ exif_result=exif_result,
139
+ ocr_result=ocr_result,
140
+ )
141
+
142
+
143
+ def _docparse_result_is_image_only(docparse_result: dict[str, Any]) -> bool:
144
+ text = str(docparse_result.get("text") or "")
145
+ markdown = str(docparse_result.get("markdown") or "")
146
+ raw = f"{text}\n{markdown}"
147
+ if not _IMAGE_MARKUP_RE.search(raw):
148
+ return False
149
+ meaningful = _meaningful_docparse_text(raw)
150
+ if len(meaningful) >= 40:
151
+ return False
152
+ return True
153
+
154
+
155
+ def _meaningful_docparse_text(raw: str) -> str:
156
+ without_markdown_images = _MARKDOWN_IMAGE_RE.sub(" ", raw)
157
+ without_html = _HTML_TAG_RE.sub(" ", without_markdown_images)
158
+ tokens = re.findall(r"\b[^\W\d_]{2,}\b", without_html, flags=re.UNICODE)
159
+ return " ".join(tokens)
160
+
161
+
162
+ def _image_docparse_result(
163
+ image_path: Path,
164
+ *,
165
+ exif_result: dict[str, Any],
166
+ docparse_result: dict[str, Any],
167
+ ) -> dict[str, Any]:
168
+ text = str(docparse_result.get("text") or docparse_result.get("markdown") or "")
169
+ structure = {
170
+ "kind": "image",
171
+ "format": exif_result["format"],
172
+ "width": exif_result["width"],
173
+ "height": exif_result["height"],
174
+ "mode": exif_result["mode"],
175
+ "exif_redacted": exif_result["exif_redacted"],
176
+ "exif_removed": exif_result["exif_removed"],
177
+ "sha256_post_redact": hashlib.sha256(image_path.read_bytes()).hexdigest(),
178
+ "docparse_backend": "tier_docparse",
179
+ "page_count": docparse_result.get("page_count"),
180
+ "elements_count": len(docparse_result.get("elements") or []),
181
+ "elements": docparse_result.get("elements") or [],
182
+ "pages": docparse_result.get("pages") or [],
183
+ "metadata": docparse_result.get("metadata") or {},
184
+ }
185
+ return {
186
+ "frontmatter": {},
187
+ "text": text,
188
+ "structure": structure,
189
+ "parser_used": "tier_docparse",
190
+ "extracted_text": text,
191
+ "exif_redacted": exif_result["exif_redacted"],
192
+ "exif_removed": exif_result["exif_removed"],
193
+ "sha256_post_redact": structure["sha256_post_redact"],
194
+ }
195
+
196
+
197
+ def _strip_exif_privacy(path: Path) -> dict[str, Any]:
198
+ """Remove GPS and non-orientation EXIF while preserving display orientation."""
199
+ try:
200
+ with Image.open(path) as image:
201
+ exif = image.getexif()
202
+ removed = False
203
+ if exif:
204
+ orientation = exif.get(EXIF_ORIENTATION_TAG)
205
+ sanitized = Image.Exif()
206
+ if orientation is not None:
207
+ sanitized[EXIF_ORIENTATION_TAG] = orientation
208
+ removed = set(exif.keys()) != set(sanitized.keys()) or any(
209
+ exif.get(key) != sanitized.get(key) for key in sanitized.keys()
210
+ )
211
+ if EXIF_GPS_TAG in exif:
212
+ removed = True
213
+ if removed:
214
+ image.save(path, exif=sanitized.tobytes() if sanitized else b"")
215
+ return {
216
+ "format": image.format,
217
+ "width": image.width,
218
+ "height": image.height,
219
+ "mode": image.mode,
220
+ "exif_redacted": True,
221
+ "exif_removed": removed,
222
+ }
223
+ except Exception as exc:
224
+ raise ValueError(f"invalid_image: {exc}") from exc
225
+
226
+
227
+ def _ocr_with_fallback(path: Path) -> dict[str, Any]:
228
+ """Run Tesseract OCR locally (PaddleOCR-VL via Mac Gateway tier-ocr upstream)."""
229
+ try:
230
+ import pytesseract
231
+
232
+ with Image.open(path) as image:
233
+ try:
234
+ text = pytesseract.image_to_string(image, lang="ita+eng")
235
+ except Exception:
236
+ logger.warning("Tesseract ita+eng failed, retrying default language", exc_info=True)
237
+ text = pytesseract.image_to_string(image)
238
+ return {
239
+ "extracted_text": str(text),
240
+ "parser_used": "tesseract",
241
+ "confidence_avg": 0.0,
242
+ "lines": [],
243
+ }
244
+ except Exception as exc:
245
+ logger.warning("No OCR backend available for image parser: %s", exc)
246
+ return {
247
+ "extracted_text": "",
248
+ "parser_used": "none",
249
+ "confidence_avg": 0.0,
250
+ "lines": [],
251
+ }
@@ -0,0 +1,89 @@
1
+ """Internal Markdown parser used by phase-1 ingestion."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import yaml
10
+ from pydantic import BaseModel, Field, ValidationError, field_validator
11
+
12
+
13
+ MAX_MARKDOWN_BYTES = 5 * 1024 * 1024
14
+ _FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---\s*(?:\n|\Z)", re.DOTALL)
15
+ _HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.MULTILINE)
16
+
17
+
18
+ class MarkdownFrontmatter(BaseModel):
19
+ type: str | None = Field(default=None, max_length=60)
20
+ title: str | None = Field(default=None, max_length=300)
21
+ tags: list[str] = Field(default_factory=list, max_length=20)
22
+
23
+ @field_validator("tags", mode="before")
24
+ @classmethod
25
+ def normalize_tags(cls, value: Any) -> list[str]:
26
+ if value is None:
27
+ return []
28
+ if isinstance(value, str):
29
+ return [value]
30
+ if isinstance(value, list):
31
+ return [str(v) for v in value if v is not None]
32
+ return []
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class MarkdownParseResult:
37
+ frontmatter: dict[str, Any]
38
+ text: str
39
+ structure: dict[str, Any]
40
+
41
+
42
+ def _split_frontmatter(raw: str) -> tuple[dict[str, Any], str]:
43
+ match = _FRONTMATTER_RE.match(raw)
44
+ if not match:
45
+ return {}, raw
46
+
47
+ frontmatter_raw = match.group(1)
48
+ body = raw[match.end() :]
49
+ loaded = yaml.safe_load(frontmatter_raw) or {}
50
+ if not isinstance(loaded, dict):
51
+ loaded = {}
52
+
53
+ try:
54
+ model = MarkdownFrontmatter.model_validate(loaded)
55
+ except ValidationError:
56
+ model = MarkdownFrontmatter()
57
+ normalized = model.model_dump()
58
+ extra = {k: v for k, v in loaded.items() if k not in normalized}
59
+ return {**extra, **normalized}, body
60
+
61
+
62
+ def _extract_headings(text: str) -> list[dict[str, Any]]:
63
+ headings: list[dict[str, Any]] = []
64
+ for match in _HEADING_RE.finditer(text):
65
+ headings.append(
66
+ {
67
+ "level": len(match.group(1)),
68
+ "text": match.group(2).strip()[:300],
69
+ }
70
+ )
71
+ return headings[:100]
72
+
73
+
74
+ def parse_markdown_file(path: Path) -> MarkdownParseResult:
75
+ size = path.stat().st_size
76
+ if size > MAX_MARKDOWN_BYTES:
77
+ raise ValueError(f"Markdown file too large: {size} bytes")
78
+
79
+ raw = path.read_text(encoding="utf-8", errors="replace")
80
+ frontmatter, body = _split_frontmatter(raw)
81
+ return MarkdownParseResult(
82
+ frontmatter=frontmatter,
83
+ text=body,
84
+ structure={
85
+ "headings": _extract_headings(body),
86
+ "bytes": size,
87
+ "line_count": body.count("\n") + (1 if body else 0),
88
+ },
89
+ )
@@ -0,0 +1,117 @@
1
+ """OCR parser backed by the Mac Gateway AUX /v1/ocr endpoint."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from core.api.services.ingest.parsers.gateway_aux import (
10
+ auth_headers,
11
+ aux_base_url,
12
+ request_gateway_with_retries,
13
+ settings,
14
+ )
15
+
16
+
17
+ def _normalize_lines(raw: Any) -> list[dict[str, Any]]:
18
+ if not isinstance(raw, list):
19
+ return []
20
+ lines: list[dict[str, Any]] = []
21
+ for item in raw:
22
+ if not isinstance(item, dict):
23
+ continue
24
+ text = str(item.get("text") or item.get("content") or "").strip()
25
+ if not text:
26
+ continue
27
+ line: dict[str, Any] = {"text": text}
28
+ confidence = item.get("confidence", item.get("score"))
29
+ if confidence is not None:
30
+ try:
31
+ line["confidence"] = float(confidence)
32
+ except (TypeError, ValueError):
33
+ pass
34
+ if item.get("bbox") is not None:
35
+ line["bbox"] = item["bbox"]
36
+ if item.get("page") is not None:
37
+ line["page"] = item["page"]
38
+ lines.append(line)
39
+ return lines
40
+
41
+
42
+ def _confidence_avg(lines: list[dict[str, Any]], payload: dict[str, Any]) -> float:
43
+ explicit = payload.get("confidence_avg", payload.get("avg_confidence"))
44
+ if explicit is not None:
45
+ try:
46
+ return float(explicit)
47
+ except (TypeError, ValueError):
48
+ pass
49
+ values = [
50
+ float(line["confidence"])
51
+ for line in lines
52
+ if isinstance(line.get("confidence"), (int, float))
53
+ ]
54
+ return sum(values) / len(values) if values else 0.0
55
+
56
+
57
+ def _text_from_payload(payload: dict[str, Any], lines: list[dict[str, Any]]) -> str:
58
+ direct = payload.get("text") or payload.get("extracted_text")
59
+ if direct:
60
+ return str(direct).strip()
61
+ return "\n".join(str(line["text"]) for line in lines).strip()
62
+
63
+
64
+ async def parse_ocr_with_gateway(path: Path, mime_type: str) -> dict[str, Any]:
65
+ cfg = settings()
66
+ size = path.stat().st_size
67
+ if size > int(cfg.ingest_ocr_max_bytes):
68
+ raise ValueError(f"File too large for OCR: {size} bytes")
69
+
70
+ timeout = httpx.Timeout(
71
+ connect=5.0,
72
+ read=float(cfg.ingest_ocr_timeout_seconds),
73
+ write=60.0,
74
+ pool=5.0,
75
+ )
76
+ async with httpx.AsyncClient(
77
+ base_url=f"{aux_base_url()}/",
78
+ timeout=timeout,
79
+ limits=httpx.Limits(max_connections=4, max_keepalive_connections=2),
80
+ ) as client:
81
+ async def post_ocr() -> httpx.Response:
82
+ with path.open("rb") as handle:
83
+ return await client.post(
84
+ "ocr",
85
+ headers=auth_headers(),
86
+ files={"file": (path.name, handle, mime_type)},
87
+ )
88
+
89
+ response = await request_gateway_with_retries(
90
+ post_ocr,
91
+ service_name="tier-ocr",
92
+ max_attempts=5,
93
+ )
94
+
95
+ if response.status_code in {401, 403}:
96
+ raise RuntimeError("tier-ocr authorization failed")
97
+ if response.status_code == 429:
98
+ raise RuntimeError("tier-ocr rate limited")
99
+ if response.status_code >= 500:
100
+ raise RuntimeError(f"tier-ocr unavailable: HTTP {response.status_code}")
101
+ response.raise_for_status()
102
+ payload = response.json()
103
+ if not isinstance(payload, dict):
104
+ raise RuntimeError("tier-ocr returned invalid payload")
105
+
106
+ lines = _normalize_lines(payload.get("lines"))
107
+ text = _text_from_payload(payload, lines)
108
+ if not text:
109
+ raise RuntimeError("tier-ocr returned empty text")
110
+
111
+ return {
112
+ "extracted_text": text,
113
+ "parser_used": "tier_ocr",
114
+ "confidence_avg": _confidence_avg(lines, payload),
115
+ "lines": lines,
116
+ "raw": payload,
117
+ }
@@ -0,0 +1,112 @@
1
+ """OCR fallback for scanned PDFs."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import logging
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from functools import lru_cache
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from core.api.services.ingest.parsers.gateway_aux import MissingGatewayConfig
12
+ from core.api.services.ingest.parsers.ocr_gateway import parse_ocr_with_gateway
13
+ from core.api.services.ingest.parsers.pdf_types import PdfParseResult
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ OCR_TIMEOUT_SECONDS = 180
18
+ OCR_WORKERS = 2
19
+
20
+
21
+ @lru_cache(maxsize=1)
22
+ def _executor() -> ThreadPoolExecutor:
23
+ return ThreadPoolExecutor(max_workers=OCR_WORKERS, thread_name_prefix="pdf-ocr")
24
+
25
+
26
+ @lru_cache(maxsize=1)
27
+ def _rapidocr_converter() -> Any:
28
+ try:
29
+ from docling.datamodel.base_models import InputFormat
30
+ from docling.datamodel.pipeline_options import (
31
+ PdfPipelineOptions,
32
+ RapidOcrOptions,
33
+ )
34
+ from docling.document_converter import DocumentConverter, PdfFormatOption
35
+ except ImportError as exc:
36
+ raise RuntimeError("Docling with RapidOCR is required for PDF OCR fallback") from exc
37
+
38
+ pipeline_options = PdfPipelineOptions(
39
+ do_ocr=True,
40
+ ocr_options=RapidOcrOptions(
41
+ backend="onnxruntime",
42
+ force_full_page_ocr=True,
43
+ lang=["english"],
44
+ ),
45
+ )
46
+ return DocumentConverter(
47
+ allowed_formats=[InputFormat.PDF],
48
+ format_options={
49
+ InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options),
50
+ },
51
+ )
52
+
53
+
54
+ def _ocr_sync(file_path: Path) -> PdfParseResult:
55
+ result = _rapidocr_converter().convert(str(file_path))
56
+ document = result.document
57
+ markdown = str(document.export_to_markdown() or "").strip()
58
+ structure = _safe_doc_dict(document)
59
+ pages = getattr(document, "pages", None)
60
+ page_count = len(pages) if pages is not None else None
61
+ if page_count is not None:
62
+ structure.setdefault("page_count", page_count)
63
+ structure.update(
64
+ {
65
+ "ocr_backend": "rapidocr",
66
+ "ocr_engine": "docling_rapidocr",
67
+ }
68
+ )
69
+ return PdfParseResult(
70
+ frontmatter={},
71
+ text=markdown,
72
+ structure=structure,
73
+ parser_used="rapidocr_fallback",
74
+ )
75
+
76
+
77
+ def _safe_doc_dict(document: Any) -> dict[str, Any]:
78
+ if hasattr(document, "export_to_dict"):
79
+ value = document.export_to_dict()
80
+ return value if isinstance(value, dict) else {"document": value}
81
+ if hasattr(document, "dict"):
82
+ value = document.dict()
83
+ return value if isinstance(value, dict) else {"document": value}
84
+ return {}
85
+
86
+
87
+ async def parse_pdf_ocr(file_path: Path) -> PdfParseResult:
88
+ try:
89
+ ocr = await parse_ocr_with_gateway(file_path, "application/pdf")
90
+ raw = ocr.get("raw") or {}
91
+ return PdfParseResult(
92
+ frontmatter={},
93
+ text=str(ocr["extracted_text"]),
94
+ structure={
95
+ "pages": raw.get("pages"),
96
+ "ocr_backend": "tier_ocr",
97
+ "ocr_confidence_avg": ocr.get("confidence_avg", 0.0),
98
+ "ocr_lines": ocr.get("lines") or [],
99
+ "ocr_raw": raw,
100
+ },
101
+ parser_used="tier_ocr",
102
+ )
103
+ except MissingGatewayConfig:
104
+ pass
105
+
106
+ loop = asyncio.get_running_loop()
107
+ future = loop.run_in_executor(_executor(), _ocr_sync, file_path)
108
+ try:
109
+ return await asyncio.wait_for(future, timeout=OCR_TIMEOUT_SECONDS)
110
+ except asyncio.TimeoutError as exc:
111
+ future.cancel()
112
+ raise TimeoutError(f"OCR timeout {OCR_TIMEOUT_SECONDS}s") from exc
@@ -0,0 +1,13 @@
1
+ """Shared PDF parser result types."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class PdfParseResult:
10
+ frontmatter: dict[str, Any]
11
+ text: str
12
+ structure: dict[str, Any]
13
+ parser_used: str