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 @@
1
+ """File parsers for universal ingestion."""
@@ -0,0 +1,142 @@
1
+ """PDF parser using Docling as the primary extractor."""
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.ocr_pdf_parser import parse_pdf_ocr
12
+ from core.api.services.ingest.parsers.docparse_gateway import parse_pdf_docparse
13
+ from core.api.services.ingest.parsers.gateway_aux import MissingGatewayConfig, settings
14
+ from core.api.services.ingest.parsers.pdf_types import PdfParseResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ DOCLING_TIMEOUT_SECONDS = 120
19
+ DOCLING_WORKERS = 3
20
+ MIN_TEXT_FOR_OCR_FALLBACK = 100
21
+
22
+
23
+ @lru_cache(maxsize=1)
24
+ def _executor() -> ThreadPoolExecutor:
25
+ return ThreadPoolExecutor(max_workers=DOCLING_WORKERS, thread_name_prefix="docling")
26
+
27
+
28
+ @lru_cache(maxsize=1)
29
+ def _converter():
30
+ from docling.document_converter import DocumentConverter
31
+
32
+ return DocumentConverter()
33
+
34
+
35
+ def _is_encrypted(file_path: Path) -> bool:
36
+ try:
37
+ from pypdf import PdfReader
38
+
39
+ return bool(PdfReader(str(file_path)).is_encrypted)
40
+ except ImportError:
41
+ logger.warning("pypdf not installed; skipping PDF encryption preflight")
42
+ return False
43
+ except Exception:
44
+ logger.warning("PDF encryption preflight failed for %s", file_path, exc_info=True)
45
+ return False
46
+
47
+
48
+ def _safe_doc_dict(document: Any) -> dict[str, Any]:
49
+ if hasattr(document, "export_to_dict"):
50
+ value = document.export_to_dict()
51
+ return value if isinstance(value, dict) else {"document": value}
52
+ if hasattr(document, "dict"):
53
+ value = document.dict()
54
+ return value if isinstance(value, dict) else {"document": value}
55
+ return {}
56
+
57
+
58
+ def _count_tables(document: Any, structure: dict[str, Any]) -> int:
59
+ tables = getattr(document, "tables", None)
60
+ if isinstance(tables, list):
61
+ return len(tables)
62
+ if hasattr(document, "iterate_items"):
63
+ try:
64
+ return sum(
65
+ 1
66
+ for item in document.iterate_items()
67
+ if str(getattr(item, "label", "")).lower() == "table"
68
+ )
69
+ except Exception:
70
+ logger.debug("Docling table iteration failed", exc_info=True)
71
+ return _count_table_like_entries(structure)
72
+
73
+
74
+ def _count_table_like_entries(value: Any) -> int:
75
+ if isinstance(value, dict):
76
+ count = 1 if str(value.get("label", "")).lower() == "table" else 0
77
+ return count + sum(_count_table_like_entries(item) for item in value.values())
78
+ if isinstance(value, list):
79
+ return sum(_count_table_like_entries(item) for item in value)
80
+ return 0
81
+
82
+
83
+ def _convert_with_docling(file_path: Path) -> PdfParseResult:
84
+ result = _converter().convert(str(file_path))
85
+ document = result.document
86
+ markdown = document.export_to_markdown()
87
+ structure = _safe_doc_dict(document)
88
+ structure.setdefault("tables_count", _count_tables(document, structure))
89
+ pages = getattr(document, "pages", None)
90
+ if pages is not None:
91
+ structure.setdefault("page_count", len(pages))
92
+ return PdfParseResult(
93
+ frontmatter={},
94
+ text=markdown,
95
+ structure=structure,
96
+ parser_used="docling",
97
+ )
98
+
99
+
100
+ def _convert_sync(file_path: Path) -> PdfParseResult:
101
+ return _convert_with_docling(file_path)
102
+
103
+
104
+ async def parse_pdf_file(
105
+ file_path: Path,
106
+ *,
107
+ allow_docparse: bool = True,
108
+ docparse_mode: str | None = None,
109
+ ) -> PdfParseResult:
110
+ """Parse a PDF and run OCR fallback when Docling finds too little text."""
111
+ if _is_encrypted(file_path):
112
+ raise ValueError("PDF encrypted, requires password")
113
+
114
+ cfg = settings()
115
+ if allow_docparse and getattr(cfg, "ingest_docparse_enabled", False) and getattr(
116
+ cfg, "ingest_docparse_pdfs_enabled", True
117
+ ):
118
+ try:
119
+ return await parse_pdf_docparse(file_path, mode=docparse_mode)
120
+ except MissingGatewayConfig:
121
+ logger.warning("tier-docparse not configured; falling back to Docling")
122
+
123
+ loop = asyncio.get_running_loop()
124
+ future = loop.run_in_executor(_executor(), _convert_sync, file_path)
125
+ try:
126
+ parsed = await asyncio.wait_for(future, timeout=DOCLING_TIMEOUT_SECONDS)
127
+ except asyncio.TimeoutError as exc:
128
+ future.cancel()
129
+ raise TimeoutError(f"parse timeout {DOCLING_TIMEOUT_SECONDS}s") from exc
130
+ except Exception:
131
+ logger.warning(
132
+ "Docling parse failed; trying OCR fallback: %s",
133
+ file_path,
134
+ exc_info=True,
135
+ )
136
+ return await parse_pdf_ocr(file_path)
137
+
138
+ if len(parsed.text.strip()) >= MIN_TEXT_FOR_OCR_FALLBACK:
139
+ return parsed
140
+
141
+ logger.info("Docling extracted <100 chars, trying OCR fallback: %s", file_path)
142
+ return await parse_pdf_ocr(file_path)
@@ -0,0 +1,178 @@
1
+ """Document parser backed by the Mac Gateway AUX /v1/docparse endpoint."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from core.api.services.ingest.parsers.gateway_aux import (
12
+ auth_headers,
13
+ aux_base_url,
14
+ request_gateway_with_retries,
15
+ settings,
16
+ )
17
+ from core.api.services.ingest.parsers.pdf_types import PdfParseResult
18
+
19
+
20
+ async def parse_docparse_with_gateway(
21
+ path: Path,
22
+ mime_type: str,
23
+ *,
24
+ mode: str | None = None,
25
+ ) -> dict[str, Any]:
26
+ cfg = settings()
27
+ size = path.stat().st_size
28
+ if size > int(cfg.ingest_docparse_max_bytes):
29
+ raise ValueError(f"File too large for docparse: {size} bytes")
30
+
31
+ timeout_seconds = float(cfg.ingest_docparse_timeout_seconds)
32
+ timeout = httpx.Timeout(
33
+ connect=5.0,
34
+ read=timeout_seconds,
35
+ write=60.0,
36
+ pool=5.0,
37
+ )
38
+ async with httpx.AsyncClient(
39
+ base_url=f"{aux_base_url()}/",
40
+ timeout=timeout,
41
+ limits=httpx.Limits(max_connections=2, max_keepalive_connections=1),
42
+ ) as client:
43
+ async def post_docparse() -> httpx.Response:
44
+ with path.open("rb") as handle:
45
+ return await client.post(
46
+ "docparse",
47
+ headers=auth_headers(),
48
+ data={
49
+ "output_format": "markdown",
50
+ "mode": mode or cfg.ingest_docparse_mode,
51
+ },
52
+ files={"file": (path.name, handle, mime_type)},
53
+ )
54
+
55
+ response = await request_gateway_with_retries(
56
+ post_docparse,
57
+ service_name="tier-docparse",
58
+ )
59
+ payload = await _resolve_docparse_response(
60
+ client,
61
+ response,
62
+ timeout_seconds=timeout_seconds,
63
+ )
64
+
65
+ text = str(payload.get("text") or "").strip()
66
+ markdown = str(payload.get("markdown") or "").strip()
67
+ if not text and markdown:
68
+ text = markdown
69
+ if not text:
70
+ raise RuntimeError("tier-docparse returned empty text")
71
+
72
+ return {
73
+ "text": text,
74
+ "markdown": markdown,
75
+ "page_count": payload.get("page_count"),
76
+ "elements": payload.get("elements") or [],
77
+ "pages": payload.get("pages") or [],
78
+ "metadata": payload.get("metadata") or {},
79
+ "raw": payload,
80
+ }
81
+
82
+
83
+ async def parse_pdf_docparse(path: Path, *, mode: str | None = None) -> PdfParseResult:
84
+ parsed = await parse_docparse_with_gateway(path, "application/pdf", mode=mode)
85
+ return PdfParseResult(
86
+ frontmatter={},
87
+ text=parsed["markdown"] or parsed["text"],
88
+ structure={
89
+ "page_count": parsed.get("page_count"),
90
+ "elements_count": len(parsed.get("elements") or []),
91
+ "elements": parsed.get("elements") or [],
92
+ "pages": parsed.get("pages") or [],
93
+ "metadata": parsed.get("metadata") or {},
94
+ "docparse_backend": "tier_docparse",
95
+ },
96
+ parser_used="tier_docparse",
97
+ )
98
+
99
+
100
+ async def _resolve_docparse_response(
101
+ client: httpx.AsyncClient,
102
+ response: httpx.Response,
103
+ *,
104
+ timeout_seconds: float,
105
+ ) -> dict[str, Any]:
106
+ if response.status_code in {401, 403}:
107
+ raise RuntimeError("tier-docparse authorization failed")
108
+ if response.status_code == 429:
109
+ raise RuntimeError("tier-docparse rate limited")
110
+ if response.status_code >= 500:
111
+ raise RuntimeError(f"tier-docparse unavailable: HTTP {response.status_code}")
112
+ response.raise_for_status()
113
+ payload = response.json()
114
+ if not isinstance(payload, dict):
115
+ raise RuntimeError("tier-docparse returned invalid payload")
116
+ if response.status_code != 202:
117
+ return payload
118
+
119
+ status_url = str(payload.get("status_url") or "").strip()
120
+ if not status_url:
121
+ raise RuntimeError("tier-docparse async response missing status_url")
122
+ return await _poll_docparse_job(client, status_url, timeout_seconds=timeout_seconds)
123
+
124
+
125
+ async def _poll_docparse_job(
126
+ client: httpx.AsyncClient,
127
+ status_url: str,
128
+ *,
129
+ timeout_seconds: float,
130
+ ) -> dict[str, Any]:
131
+ deadline = time.monotonic() + timeout_seconds
132
+ request_url = _status_request_url(client, status_url)
133
+ while time.monotonic() < deadline:
134
+ async def get_status() -> httpx.Response:
135
+ return await client.get(request_url, headers=auth_headers())
136
+
137
+ response = await request_gateway_with_retries(
138
+ get_status,
139
+ service_name="tier-docparse-poll",
140
+ )
141
+ if response.status_code in {401, 403}:
142
+ raise RuntimeError("tier-docparse poll authorization failed")
143
+ if response.status_code == 404:
144
+ raise RuntimeError("tier-docparse job not found")
145
+ if response.status_code >= 500:
146
+ raise RuntimeError(f"tier-docparse poll unavailable: HTTP {response.status_code}")
147
+ response.raise_for_status()
148
+ status_payload = response.json()
149
+ if not isinstance(status_payload, dict):
150
+ raise RuntimeError("tier-docparse poll returned invalid payload")
151
+
152
+ status = status_payload.get("status")
153
+ if status == "done":
154
+ result = status_payload.get("result")
155
+ if not isinstance(result, dict):
156
+ raise RuntimeError("tier-docparse completed without result")
157
+ return result
158
+ if status == "failed":
159
+ error = status_payload.get("error") or {}
160
+ message = error.get("message") if isinstance(error, dict) else None
161
+ raise RuntimeError(f"tier-docparse job failed: {message or 'unknown error'}")
162
+
163
+ retry_after = response.headers.get("Retry-After")
164
+ try:
165
+ sleep_for = float(retry_after) if retry_after else 2.0
166
+ except ValueError:
167
+ sleep_for = 2.0
168
+ await asyncio.sleep(max(0.2, min(sleep_for, 5.0)))
169
+
170
+ raise TimeoutError(f"tier-docparse job timed out after {timeout_seconds:g}s")
171
+
172
+
173
+ def _status_request_url(client: httpx.AsyncClient, status_url: str) -> str:
174
+ if status_url.startswith(("http://", "https://")):
175
+ return status_url
176
+ if status_url.startswith("/"):
177
+ return str(client.base_url.copy_with(path=status_url, query=None, fragment=None))
178
+ return status_url
@@ -0,0 +1,127 @@
1
+ """Minimal local DOCX parser for Ingestor 2.0."""
2
+ from __future__ import annotations
3
+
4
+ import zipfile
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from xml.etree import ElementTree
8
+
9
+ from core.api.config import settings
10
+ from core.api.services.ingest.parsers.internal_markdown import MarkdownParseResult
11
+
12
+ DOCX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
13
+ WORD_NS = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
14
+
15
+
16
+ def parse_docx(path: Path) -> MarkdownParseResult:
17
+ """Extract paragraphs and tables from an Office Open XML document.
18
+
19
+ This deliberately avoids a broad conversion dependency. It is enough for
20
+ classification evidence and triage preview, while legacy binary `.doc`
21
+ remains unsupported.
22
+ """
23
+ size = path.stat().st_size
24
+ if size > int(settings.ingest_docx_max_bytes):
25
+ raise ValueError(f"DOCX file too large: {size} bytes")
26
+ if path.suffix.lower() != ".docx":
27
+ raise ValueError(f"Unsupported Word extension: {path.suffix or '<none>'}")
28
+
29
+ try:
30
+ with zipfile.ZipFile(path) as archive:
31
+ document_xml = archive.read("word/document.xml")
32
+ except KeyError as exc:
33
+ raise ValueError("Invalid DOCX: word/document.xml missing") from exc
34
+ except zipfile.BadZipFile as exc:
35
+ raise ValueError("Invalid DOCX container") from exc
36
+
37
+ root = ElementTree.fromstring(document_xml)
38
+ paragraphs: list[str] = []
39
+ tables: list[list[list[str]]] = []
40
+
41
+ for child in root.findall(".//w:body/*", WORD_NS):
42
+ tag = _local_name(child.tag)
43
+ if tag == "p":
44
+ text = _paragraph_text(child)
45
+ if text:
46
+ paragraphs.append(text)
47
+ elif tag == "tbl":
48
+ table = _table_rows(child)
49
+ if table:
50
+ tables.append(table)
51
+
52
+ markdown = _to_markdown(path, paragraphs, tables)
53
+ return MarkdownParseResult(
54
+ frontmatter=_frontmatter(path, markdown),
55
+ text=markdown,
56
+ structure={
57
+ "kind": "docx",
58
+ "bytes": size,
59
+ "paragraph_count": len(paragraphs),
60
+ "table_count": len(tables),
61
+ "table_cell_count": sum(len(row) for table in tables for row in table),
62
+ },
63
+ )
64
+
65
+
66
+ def _paragraph_text(node: ElementTree.Element) -> str:
67
+ return "".join(
68
+ text_node.text or ""
69
+ for text_node in node.findall(".//w:t", WORD_NS)
70
+ if text_node.text
71
+ ).strip()
72
+
73
+
74
+ def _table_rows(node: ElementTree.Element) -> list[list[str]]:
75
+ rows: list[list[str]] = []
76
+ for row in node.findall(".//w:tr", WORD_NS):
77
+ cells = [_paragraph_text(cell) for cell in row.findall("./w:tc", WORD_NS)]
78
+ if any(cells):
79
+ rows.append(cells)
80
+ return rows
81
+
82
+
83
+ def _to_markdown(path: Path, paragraphs: list[str], tables: list[list[list[str]]]) -> str:
84
+ parts = [f"# {path.stem}"]
85
+ for paragraph in paragraphs:
86
+ parts.extend(["", paragraph])
87
+ for index, table in enumerate(tables, start=1):
88
+ parts.extend(["", f"## Table {index}", "", *_table_markdown(table)])
89
+ return "\n".join(parts).strip() + "\n"
90
+
91
+
92
+ def _table_markdown(rows: list[list[str]]) -> list[str]:
93
+ if not rows:
94
+ return []
95
+ width = max(len(row) for row in rows)
96
+ normalized = [row + [""] * (width - len(row)) for row in rows]
97
+ header = normalized[0]
98
+ body = normalized[1:] or [[""] * width]
99
+ out = [
100
+ "| " + " | ".join(_cell(cell) for cell in header) + " |",
101
+ "| " + " | ".join("---" for _ in header) + " |",
102
+ ]
103
+ for row in body:
104
+ out.append("| " + " | ".join(_cell(cell) for cell in row) + " |")
105
+ return out
106
+
107
+
108
+ def _cell(value: str) -> str:
109
+ return (value or "").replace("|", "\\|").strip()
110
+
111
+
112
+ def _frontmatter(path: Path, markdown: str) -> dict[str, Any]:
113
+ lowered = markdown.lower()
114
+ tags = ["docx"]
115
+ doc_type = "file"
116
+ if any(term in lowered for term in {"contratto", "agreement", "clausola", "firma"}):
117
+ doc_type = "contract"
118
+ tags.append("contract")
119
+ return {
120
+ "type": doc_type,
121
+ "title": path.stem,
122
+ "tags": tags,
123
+ }
124
+
125
+
126
+ def _local_name(tag: str) -> str:
127
+ return tag.rsplit("}", 1)[-1]
@@ -0,0 +1,85 @@
1
+ """Folder traversal helpers for Universal Ingestion containers."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path, PurePosixPath
6
+
7
+ from core.api.services.ingest.ignore_patterns import (
8
+ is_ignored_directory_name,
9
+ should_ignore,
10
+ )
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FolderFile:
15
+ path: Path
16
+ relative_path: PurePosixPath
17
+ size_bytes: int
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class FolderSkipped:
22
+ path: str
23
+ reason: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FolderScanResult:
28
+ files: list[FolderFile]
29
+ skipped: list[FolderSkipped]
30
+
31
+
32
+ def scan_folder_files(root: Path, *, max_depth: int = 5) -> FolderScanResult:
33
+ root = root.resolve()
34
+ files: list[FolderFile] = []
35
+ skipped: list[FolderSkipped] = []
36
+ visited_dirs: set[Path] = set()
37
+
38
+ def walk(directory: Path, rel_dir: PurePosixPath, depth: int) -> None:
39
+ if depth > max_depth:
40
+ skipped.append(FolderSkipped(str(rel_dir), "max-depth"))
41
+ return
42
+ try:
43
+ resolved_dir = directory.resolve()
44
+ except OSError:
45
+ skipped.append(FolderSkipped(str(rel_dir), "unreadable"))
46
+ return
47
+ if resolved_dir in visited_dirs:
48
+ skipped.append(FolderSkipped(str(rel_dir), "symlink-loop"))
49
+ return
50
+ visited_dirs.add(resolved_dir)
51
+
52
+ try:
53
+ children = sorted(directory.iterdir(), key=lambda item: item.name.casefold())
54
+ except OSError:
55
+ skipped.append(FolderSkipped(str(rel_dir), "unreadable"))
56
+ return
57
+
58
+ for child in children:
59
+ rel_path = rel_dir / child.name
60
+ if child.is_symlink():
61
+ skipped.append(FolderSkipped(str(rel_path), "symlink"))
62
+ continue
63
+ if child.is_dir():
64
+ if is_ignored_directory_name(child.name):
65
+ skipped.append(FolderSkipped(str(rel_path), "ignored-directory"))
66
+ continue
67
+ walk(child, rel_path, depth + 1)
68
+ continue
69
+ if not child.is_file():
70
+ skipped.append(FolderSkipped(str(rel_path), "not-file"))
71
+ continue
72
+
73
+ try:
74
+ size_bytes = child.stat().st_size
75
+ except OSError:
76
+ skipped.append(FolderSkipped(str(rel_path), "unreadable"))
77
+ continue
78
+ ignore_reason = should_ignore(rel_path, file_size_bytes=size_bytes)
79
+ if ignore_reason:
80
+ skipped.append(FolderSkipped(str(rel_path), ignore_reason))
81
+ continue
82
+ files.append(FolderFile(child, rel_path, size_bytes))
83
+
84
+ walk(root, PurePosixPath("."), 0)
85
+ return FolderScanResult(files=files, skipped=skipped)
@@ -0,0 +1,147 @@
1
+ """Shared helpers for Mac Gateway AUX endpoints used by ingest parsers."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import logging
6
+ from collections.abc import Awaitable, Callable
7
+ from datetime import datetime, timezone
8
+ from email.utils import parsedate_to_datetime
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ from core.api import config as _config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ RETRYABLE_STATUS_CODES = {408, 409, 425, 429, 500, 502, 503, 504}
18
+ RETRY_AFTER_CAP_SECONDS = 30.0
19
+
20
+
21
+ class MissingGatewayConfig(RuntimeError):
22
+ """Raised when AUX endpoints are not configured in this environment."""
23
+
24
+
25
+ def settings():
26
+ return _config.settings
27
+
28
+
29
+ def secret_value(value: Any) -> str:
30
+ if hasattr(value, "get_secret_value"):
31
+ return str(value.get_secret_value())
32
+ return str(value or "")
33
+
34
+
35
+ def gateway_api_key() -> str:
36
+ cfg = settings()
37
+ if getattr(cfg, "pir_env", "") == "test":
38
+ raise MissingGatewayConfig("AUX gateway is disabled under PIR_ENV=test")
39
+ api_key = secret_value(getattr(cfg, "ingest_llm_gateway_api_key", None))
40
+ if not api_key:
41
+ raise MissingGatewayConfig("INGEST_LLM_GATEWAY_API_KEY is not configured")
42
+ return api_key
43
+
44
+
45
+ def gateway_agent_name() -> str:
46
+ cfg = settings()
47
+ return str(getattr(cfg, "ingest_llm_gateway_agent_name", "") or "marvisx-ingester")
48
+
49
+
50
+ def gateway_priority() -> str:
51
+ cfg = settings()
52
+ priority = str(getattr(cfg, "ingest_llm_gateway_priority", "") or "batch").strip()
53
+ return priority if priority in {"interactive", "batch", "background"} else "batch"
54
+
55
+
56
+ def aux_base_url() -> str:
57
+ cfg = settings()
58
+ if getattr(cfg, "pir_env", "") == "test":
59
+ raise MissingGatewayConfig("AUX gateway is disabled under PIR_ENV=test")
60
+ base_url = cfg.llm_gateway_aux_base_url or cfg.llm_gateway_base_url
61
+ if not base_url:
62
+ raise MissingGatewayConfig("LLM_GATEWAY_BASE_URL is not configured")
63
+
64
+ base_url = base_url.rstrip("/")
65
+ if cfg.llm_gateway_aux_base_url:
66
+ return base_url
67
+
68
+ # Production chat calls use LiteLLM on :4000. The ASR/OCR AUX proxy lives on
69
+ # :9100 on the same Mac; public CF URLs already route by path and stay as-is.
70
+ if base_url.startswith("http://100.103.221.55:4000"):
71
+ return base_url.replace(":4000", ":9100", 1)
72
+ return base_url
73
+
74
+
75
+ def auth_headers() -> dict[str, str]:
76
+ return {
77
+ "Authorization": f"Bearer {gateway_api_key()}",
78
+ "X-Agent-Name": gateway_agent_name(),
79
+ "X-Priority": gateway_priority(),
80
+ }
81
+
82
+
83
+ def is_retryable_gateway_response(response: httpx.Response) -> bool:
84
+ return response.status_code in RETRYABLE_STATUS_CODES
85
+
86
+
87
+ def retry_delay_seconds(response: httpx.Response | None, attempt: int) -> float:
88
+ retry_after = response.headers.get("Retry-After") if response is not None else None
89
+ if retry_after:
90
+ try:
91
+ return min(max(float(retry_after), 0.0), RETRY_AFTER_CAP_SECONDS)
92
+ except ValueError:
93
+ try:
94
+ parsed = parsedate_to_datetime(retry_after)
95
+ if parsed.tzinfo is None:
96
+ parsed = parsed.replace(tzinfo=timezone.utc)
97
+ delta = (parsed - datetime.now(timezone.utc)).total_seconds()
98
+ return min(max(delta, 0.0), RETRY_AFTER_CAP_SECONDS)
99
+ except (TypeError, ValueError):
100
+ pass
101
+ return min(0.5 * attempt, 2.0)
102
+
103
+
104
+ async def request_gateway_with_retries(
105
+ operation: Callable[[], Awaitable[httpx.Response]],
106
+ *,
107
+ service_name: str,
108
+ max_attempts: int = 3,
109
+ ) -> httpx.Response:
110
+ for attempt in range(1, max_attempts + 1):
111
+ response: httpx.Response | None = None
112
+ try:
113
+ response = await operation()
114
+ except (
115
+ httpx.ConnectError,
116
+ httpx.ConnectTimeout,
117
+ httpx.PoolTimeout,
118
+ httpx.ReadTimeout,
119
+ httpx.RemoteProtocolError,
120
+ httpx.WriteError,
121
+ httpx.WriteTimeout,
122
+ ) as exc:
123
+ if attempt == max_attempts:
124
+ raise RuntimeError(
125
+ f"{service_name} unavailable after {max_attempts} attempts"
126
+ ) from exc
127
+ logger.warning(
128
+ "%s transient request error on attempt %s/%s: %s",
129
+ service_name,
130
+ attempt,
131
+ max_attempts,
132
+ exc.__class__.__name__,
133
+ )
134
+ else:
135
+ if not is_retryable_gateway_response(response) or attempt == max_attempts:
136
+ return response
137
+ logger.warning(
138
+ "%s transient HTTP %s on attempt %s/%s",
139
+ service_name,
140
+ response.status_code,
141
+ attempt,
142
+ max_attempts,
143
+ )
144
+
145
+ await asyncio.sleep(retry_delay_seconds(response, attempt))
146
+
147
+ raise RuntimeError(f"{service_name} retry loop exited unexpectedly")