igel-qe-core 1.0.0

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 (394) hide show
  1. package/dist/cli/index.js +38 -0
  2. package/dist/mcp/server.js +104 -0
  3. package/knowledge_base/__init__.py +3 -0
  4. package/knowledge_base/__pycache__/__init__.cpython-314.pyc +0 -0
  5. package/knowledge_base/__pycache__/__init__.cpython-37.pyc +0 -0
  6. package/knowledge_base/__pycache__/__init__.cpython-38.pyc +0 -0
  7. package/knowledge_base/__pycache__/confidence.cpython-37.pyc +0 -0
  8. package/knowledge_base/__pycache__/config.cpython-314.pyc +0 -0
  9. package/knowledge_base/__pycache__/config.cpython-37.pyc +0 -0
  10. package/knowledge_base/__pycache__/config.cpython-38.pyc +0 -0
  11. package/knowledge_base/__pycache__/context_fusion.cpython-314.pyc +0 -0
  12. package/knowledge_base/__pycache__/context_fusion.cpython-37.pyc +0 -0
  13. package/knowledge_base/__pycache__/feedback_analytics.cpython-37.pyc +0 -0
  14. package/knowledge_base/__pycache__/feedback_applier.cpython-37.pyc +0 -0
  15. package/knowledge_base/__pycache__/feedback_cli.cpython-37.pyc +0 -0
  16. package/knowledge_base/__pycache__/feedback_curator.cpython-37.pyc +0 -0
  17. package/knowledge_base/__pycache__/feedback_handler.cpython-37.pyc +0 -0
  18. package/knowledge_base/__pycache__/generate.cpython-37.pyc +0 -0
  19. package/knowledge_base/__pycache__/generate_qcapps134.cpython-37.pyc +0 -0
  20. package/knowledge_base/__pycache__/input_classifier.cpython-314.pyc +0 -0
  21. package/knowledge_base/__pycache__/input_classifier.cpython-37.pyc +0 -0
  22. package/knowledge_base/__pycache__/kg_extractor.cpython-37.pyc +0 -0
  23. package/knowledge_base/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  24. package/knowledge_base/__pycache__/knowledge_graph.cpython-37.pyc +0 -0
  25. package/knowledge_base/__pycache__/manual_tc_parser.cpython-314.pyc +0 -0
  26. package/knowledge_base/__pycache__/manual_tc_parser.cpython-37.pyc +0 -0
  27. package/knowledge_base/__pycache__/precedence.cpython-37.pyc +0 -0
  28. package/knowledge_base/__pycache__/prompts.cpython-314.pyc +0 -0
  29. package/knowledge_base/__pycache__/prompts.cpython-37.pyc +0 -0
  30. package/knowledge_base/__pycache__/reranker.cpython-314.pyc +0 -0
  31. package/knowledge_base/__pycache__/reranker.cpython-37.pyc +0 -0
  32. package/knowledge_base/__pycache__/retrieval_debug.cpython-314.pyc +0 -0
  33. package/knowledge_base/__pycache__/retrieval_debug.cpython-37.pyc +0 -0
  34. package/knowledge_base/__pycache__/retriever.cpython-314.pyc +0 -0
  35. package/knowledge_base/__pycache__/retriever.cpython-37.pyc +0 -0
  36. package/knowledge_base/__pycache__/server.cpython-37.pyc +0 -0
  37. package/knowledge_base/__pycache__/tc_transformer.cpython-314.pyc +0 -0
  38. package/knowledge_base/__pycache__/tc_transformer.cpython-37.pyc +0 -0
  39. package/knowledge_base/__pycache__/test_generator.cpython-314.pyc +0 -0
  40. package/knowledge_base/__pycache__/test_generator.cpython-37.pyc +0 -0
  41. package/knowledge_base/agents/__init__.py +6 -0
  42. package/knowledge_base/agents/__pycache__/__init__.cpython-37.pyc +0 -0
  43. package/knowledge_base/agents/__pycache__/__init__.cpython-38.pyc +0 -0
  44. package/knowledge_base/agents/__pycache__/fusion_agent.cpython-37.pyc +0 -0
  45. package/knowledge_base/agents/__pycache__/fusion_agent.cpython-38.pyc +0 -0
  46. package/knowledge_base/agents/__pycache__/graph.cpython-37.pyc +0 -0
  47. package/knowledge_base/agents/__pycache__/graph.cpython-38.pyc +0 -0
  48. package/knowledge_base/agents/__pycache__/healing_agent.cpython-37.pyc +0 -0
  49. package/knowledge_base/agents/__pycache__/intent_agent.cpython-37.pyc +0 -0
  50. package/knowledge_base/agents/__pycache__/intent_agent.cpython-38.pyc +0 -0
  51. package/knowledge_base/agents/__pycache__/kg_agent.cpython-37.pyc +0 -0
  52. package/knowledge_base/agents/__pycache__/kg_agent.cpython-38.pyc +0 -0
  53. package/knowledge_base/agents/__pycache__/memory_agent.cpython-37.pyc +0 -0
  54. package/knowledge_base/agents/__pycache__/memory_agent.cpython-38.pyc +0 -0
  55. package/knowledge_base/agents/__pycache__/rca_agent.cpython-37.pyc +0 -0
  56. package/knowledge_base/agents/__pycache__/reflection_agent.cpython-37.pyc +0 -0
  57. package/knowledge_base/agents/__pycache__/reflection_agent.cpython-38.pyc +0 -0
  58. package/knowledge_base/agents/__pycache__/regression_agent.cpython-37.pyc +0 -0
  59. package/knowledge_base/agents/__pycache__/retrieval_agent.cpython-37.pyc +0 -0
  60. package/knowledge_base/agents/__pycache__/retrieval_agent.cpython-38.pyc +0 -0
  61. package/knowledge_base/agents/__pycache__/script_agent.cpython-37.pyc +0 -0
  62. package/knowledge_base/agents/__pycache__/script_agent.cpython-38.pyc +0 -0
  63. package/knowledge_base/agents/__pycache__/state.cpython-37.pyc +0 -0
  64. package/knowledge_base/agents/__pycache__/state.cpython-38.pyc +0 -0
  65. package/knowledge_base/agents/__pycache__/temporal_agent.cpython-37.pyc +0 -0
  66. package/knowledge_base/agents/__pycache__/temporal_agent.cpython-38.pyc +0 -0
  67. package/knowledge_base/agents/__pycache__/validation_agent.cpython-37.pyc +0 -0
  68. package/knowledge_base/agents/__pycache__/validation_agent.cpython-38.pyc +0 -0
  69. package/knowledge_base/agents/automation_design_agent.py +123 -0
  70. package/knowledge_base/agents/code_intelligence_agent.py +147 -0
  71. package/knowledge_base/agents/confidence_agent.py +94 -0
  72. package/knowledge_base/agents/execution_validation_agent.py +129 -0
  73. package/knowledge_base/agents/feedback_agent.py +56 -0
  74. package/knowledge_base/agents/fusion_agent.py +113 -0
  75. package/knowledge_base/agents/graph.py +152 -0
  76. package/knowledge_base/agents/healing_agent.py +62 -0
  77. package/knowledge_base/agents/intent_agent.py +76 -0
  78. package/knowledge_base/agents/kg_agent.py +91 -0
  79. package/knowledge_base/agents/memory_agent.py +62 -0
  80. package/knowledge_base/agents/rca_agent.py +53 -0
  81. package/knowledge_base/agents/reflection_agent.py +62 -0
  82. package/knowledge_base/agents/regression_agent.py +81 -0
  83. package/knowledge_base/agents/requirement_agent.py +71 -0
  84. package/knowledge_base/agents/retrieval_agent.py +66 -0
  85. package/knowledge_base/agents/scenario_agent.py +81 -0
  86. package/knowledge_base/agents/script_agent.py +121 -0
  87. package/knowledge_base/agents/script_generator_agent.py +77 -0
  88. package/knowledge_base/agents/state.py +53 -0
  89. package/knowledge_base/agents/temporal_agent.py +48 -0
  90. package/knowledge_base/agents/testcase_agent.py +111 -0
  91. package/knowledge_base/agents/validation_agent.py +49 -0
  92. package/knowledge_base/agents/workflow_engine.py +87 -0
  93. package/knowledge_base/alembic/README.md +104 -0
  94. package/knowledge_base/alembic/__pycache__/env.cpython-37.pyc +0 -0
  95. package/knowledge_base/alembic/__pycache__/env.cpython-38.pyc +0 -0
  96. package/knowledge_base/alembic/env.py +76 -0
  97. package/knowledge_base/alembic/script.py.mako +5 -0
  98. package/knowledge_base/alembic/versions/.gitkeep +26 -0
  99. package/knowledge_base/alembic/versions/001_phase0_feedback.py +122 -0
  100. package/knowledge_base/alembic/versions/002_feedback_promotions.py +60 -0
  101. package/knowledge_base/alembic/versions/003_enterprise_hardening.py +74 -0
  102. package/knowledge_base/alembic/versions/__pycache__/001_phase0_feedback.cpython-37.pyc +0 -0
  103. package/knowledge_base/alembic/versions/__pycache__/001_phase0_feedback.cpython-38.pyc +0 -0
  104. package/knowledge_base/alembic/versions/__pycache__/002_feedback_promotions.cpython-37.pyc +0 -0
  105. package/knowledge_base/alembic/versions/__pycache__/002_feedback_promotions.cpython-38.pyc +0 -0
  106. package/knowledge_base/api/__init__.py +1 -0
  107. package/knowledge_base/api/__pycache__/__init__.cpython-37.pyc +0 -0
  108. package/knowledge_base/api/__pycache__/__init__.cpython-38.pyc +0 -0
  109. package/knowledge_base/api/__pycache__/middleware.cpython-38.pyc +0 -0
  110. package/knowledge_base/api/__pycache__/server.cpython-37.pyc +0 -0
  111. package/knowledge_base/api/__pycache__/server.cpython-38.pyc +0 -0
  112. package/knowledge_base/api/middleware.py +78 -0
  113. package/knowledge_base/api/server.py +778 -0
  114. package/knowledge_base/chunking/__init__.py +0 -0
  115. package/knowledge_base/chunking/__pycache__/__init__.cpython-37.pyc +0 -0
  116. package/knowledge_base/chunking/__pycache__/__init__.cpython-38.pyc +0 -0
  117. package/knowledge_base/chunking/__pycache__/chunker.cpython-37.pyc +0 -0
  118. package/knowledge_base/chunking/__pycache__/chunker.cpython-38.pyc +0 -0
  119. package/knowledge_base/chunking/__pycache__/validators.cpython-37.pyc +0 -0
  120. package/knowledge_base/chunking/__pycache__/validators.cpython-38.pyc +0 -0
  121. package/knowledge_base/chunking/chunker.py +402 -0
  122. package/knowledge_base/chunking/validators.py +144 -0
  123. package/knowledge_base/cli/__init__.py +1 -0
  124. package/knowledge_base/cli/__pycache__/__init__.cpython-37.pyc +0 -0
  125. package/knowledge_base/cli/__pycache__/__init__.cpython-38.pyc +0 -0
  126. package/knowledge_base/cli/__pycache__/feedback_cli.cpython-37.pyc +0 -0
  127. package/knowledge_base/cli/__pycache__/feedback_cli.cpython-38.pyc +0 -0
  128. package/knowledge_base/cli/__pycache__/generate.cpython-37.pyc +0 -0
  129. package/knowledge_base/cli/__pycache__/generate.cpython-38.pyc +0 -0
  130. package/knowledge_base/cli/__pycache__/generate_qcapps134.cpython-37.pyc +0 -0
  131. package/knowledge_base/cli/feedback_cli.py +121 -0
  132. package/knowledge_base/cli/generate.py +308 -0
  133. package/knowledge_base/cli/generate_qcapps134.py +391 -0
  134. package/knowledge_base/config/.env +100 -0
  135. package/knowledge_base/config/.env.example +75 -0
  136. package/knowledge_base/config/__init__.py +5 -0
  137. package/knowledge_base/config/__pycache__/__init__.cpython-37.pyc +0 -0
  138. package/knowledge_base/config/__pycache__/__init__.cpython-38.pyc +0 -0
  139. package/knowledge_base/config/__pycache__/settings.cpython-37.pyc +0 -0
  140. package/knowledge_base/config/__pycache__/settings.cpython-38.pyc +0 -0
  141. package/knowledge_base/config/settings.py +280 -0
  142. package/knowledge_base/connectors/__init__.py +5 -0
  143. package/knowledge_base/connectors/__pycache__/__init__.cpython-37.pyc +0 -0
  144. package/knowledge_base/connectors/__pycache__/__init__.cpython-38.pyc +0 -0
  145. package/knowledge_base/connectors/__pycache__/base.cpython-37.pyc +0 -0
  146. package/knowledge_base/connectors/__pycache__/base.cpython-38.pyc +0 -0
  147. package/knowledge_base/connectors/__pycache__/bitbucket_connector.cpython-37.pyc +0 -0
  148. package/knowledge_base/connectors/__pycache__/bitbucket_connector.cpython-38.pyc +0 -0
  149. package/knowledge_base/connectors/__pycache__/community_connector.cpython-38.pyc +0 -0
  150. package/knowledge_base/connectors/__pycache__/confluence_connector.cpython-37.pyc +0 -0
  151. package/knowledge_base/connectors/__pycache__/confluence_connector.cpython-38.pyc +0 -0
  152. package/knowledge_base/connectors/__pycache__/jira_connector.cpython-37.pyc +0 -0
  153. package/knowledge_base/connectors/__pycache__/jira_connector.cpython-38.pyc +0 -0
  154. package/knowledge_base/connectors/__pycache__/kb_igel_connector.cpython-38.pyc +0 -0
  155. package/knowledge_base/connectors/__pycache__/playwright_fetcher.cpython-38.pyc +0 -0
  156. package/knowledge_base/connectors/__pycache__/query_bridge.cpython-38.pyc +0 -0
  157. package/knowledge_base/connectors/__pycache__/search_backends.cpython-38.pyc +0 -0
  158. package/knowledge_base/connectors/__pycache__/serviceNow_connector.cpython-37.pyc +0 -0
  159. package/knowledge_base/connectors/__pycache__/serviceNow_connector.cpython-38.pyc +0 -0
  160. package/knowledge_base/connectors/__pycache__/sync_scheduler.cpython-37.pyc +0 -0
  161. package/knowledge_base/connectors/__pycache__/sync_scheduler.cpython-38.pyc +0 -0
  162. package/knowledge_base/connectors/__pycache__/teamcity_connector.cpython-37.pyc +0 -0
  163. package/knowledge_base/connectors/__pycache__/teamcity_connector.cpython-38.pyc +0 -0
  164. package/knowledge_base/connectors/__pycache__/testRail_connector.cpython-37.pyc +0 -0
  165. package/knowledge_base/connectors/__pycache__/testRail_connector.cpython-38.pyc +0 -0
  166. package/knowledge_base/connectors/__pycache__/web_fetcher.cpython-38.pyc +0 -0
  167. package/knowledge_base/connectors/base.py +44 -0
  168. package/knowledge_base/connectors/bitbucket_connector.py +25 -0
  169. package/knowledge_base/connectors/community_connector.py +112 -0
  170. package/knowledge_base/connectors/confluence_connector.py +165 -0
  171. package/knowledge_base/connectors/jira_connector.py +174 -0
  172. package/knowledge_base/connectors/kb_igel_connector.py +145 -0
  173. package/knowledge_base/connectors/playwright_fetcher.py +50 -0
  174. package/knowledge_base/connectors/query_bridge.py +170 -0
  175. package/knowledge_base/connectors/search_backends.py +368 -0
  176. package/knowledge_base/connectors/serviceNow_connector.py +25 -0
  177. package/knowledge_base/connectors/sync_scheduler.py +111 -0
  178. package/knowledge_base/connectors/teamcity_connector.py +25 -0
  179. package/knowledge_base/connectors/testRail_connector.py +25 -0
  180. package/knowledge_base/connectors/web_fetcher.py +280 -0
  181. package/knowledge_base/core/__init__.py +8 -0
  182. package/knowledge_base/core/__pycache__/__init__.cpython-37.pyc +0 -0
  183. package/knowledge_base/core/__pycache__/__init__.cpython-38.pyc +0 -0
  184. package/knowledge_base/core/__pycache__/cache.cpython-38.pyc +0 -0
  185. package/knowledge_base/core/__pycache__/circuit_breaker.cpython-38.pyc +0 -0
  186. package/knowledge_base/core/__pycache__/confidence.cpython-37.pyc +0 -0
  187. package/knowledge_base/core/__pycache__/confidence.cpython-38.pyc +0 -0
  188. package/knowledge_base/core/__pycache__/logging_setup.cpython-38.pyc +0 -0
  189. package/knowledge_base/core/__pycache__/metrics.cpython-38.pyc +0 -0
  190. package/knowledge_base/core/__pycache__/precedence.cpython-37.pyc +0 -0
  191. package/knowledge_base/core/__pycache__/precedence.cpython-38.pyc +0 -0
  192. package/knowledge_base/core/__pycache__/request_context.cpython-38.pyc +0 -0
  193. package/knowledge_base/core/cache.py +98 -0
  194. package/knowledge_base/core/circuit_breaker.py +33 -0
  195. package/knowledge_base/core/confidence.py +254 -0
  196. package/knowledge_base/core/logging_setup.py +39 -0
  197. package/knowledge_base/core/metrics.py +63 -0
  198. package/knowledge_base/core/precedence.py +354 -0
  199. package/knowledge_base/core/request_context.py +54 -0
  200. package/knowledge_base/db/__init__.py +0 -0
  201. package/knowledge_base/db/__pycache__/__init__.cpython-314.pyc +0 -0
  202. package/knowledge_base/db/__pycache__/__init__.cpython-37.pyc +0 -0
  203. package/knowledge_base/db/__pycache__/__init__.cpython-38.pyc +0 -0
  204. package/knowledge_base/db/__pycache__/client.cpython-314.pyc +0 -0
  205. package/knowledge_base/db/__pycache__/client.cpython-37.pyc +0 -0
  206. package/knowledge_base/db/__pycache__/client.cpython-38.pyc +0 -0
  207. package/knowledge_base/db/__pycache__/connector_sync.cpython-38.pyc +0 -0
  208. package/knowledge_base/db/__pycache__/feedback_schema.cpython-37.pyc +0 -0
  209. package/knowledge_base/db/__pycache__/feedback_schema.cpython-38.pyc +0 -0
  210. package/knowledge_base/db/__pycache__/kg_introspect.cpython-314.pyc +0 -0
  211. package/knowledge_base/db/__pycache__/kg_introspect.cpython-37.pyc +0 -0
  212. package/knowledge_base/db/__pycache__/kg_introspect.cpython-38.pyc +0 -0
  213. package/knowledge_base/db/__pycache__/schema.cpython-37.pyc +0 -0
  214. package/knowledge_base/db/__pycache__/schema.cpython-38.pyc +0 -0
  215. package/knowledge_base/db/client.py +100 -0
  216. package/knowledge_base/db/connector_sync.py +43 -0
  217. package/knowledge_base/db/feedback_schema.py +548 -0
  218. package/knowledge_base/db/igel_schema.py +150 -0
  219. package/knowledge_base/db/kg_introspect.py +116 -0
  220. package/knowledge_base/db/migrations.py +22 -0
  221. package/knowledge_base/db/schema.py +649 -0
  222. package/knowledge_base/deploy/Dockerfile +23 -0
  223. package/knowledge_base/deploy/feedback_automation.sh +18 -0
  224. package/knowledge_base/deploy/firecrawl-docker.sh +50 -0
  225. package/knowledge_base/deploy/igel-testgen.service +26 -0
  226. package/knowledge_base/deploy/install_reranker.sh +92 -0
  227. package/knowledge_base/deploy/requirements.txt +68 -0
  228. package/knowledge_base/deploy/run_server.sh +35 -0
  229. package/knowledge_base/docs/TEST_GENERATION_GUIDE.md +174 -0
  230. package/knowledge_base/embeddings/__init__.py +0 -0
  231. package/knowledge_base/embeddings/__pycache__/__init__.cpython-314.pyc +0 -0
  232. package/knowledge_base/embeddings/__pycache__/__init__.cpython-37.pyc +0 -0
  233. package/knowledge_base/embeddings/__pycache__/__init__.cpython-38.pyc +0 -0
  234. package/knowledge_base/embeddings/__pycache__/azure_embedder.cpython-314.pyc +0 -0
  235. package/knowledge_base/embeddings/__pycache__/azure_embedder.cpython-37.pyc +0 -0
  236. package/knowledge_base/embeddings/__pycache__/azure_embedder.cpython-38.pyc +0 -0
  237. package/knowledge_base/embeddings/azure_embedder.py +99 -0
  238. package/knowledge_base/eval/__init__.py +33 -0
  239. package/knowledge_base/eval/__main__.py +172 -0
  240. package/knowledge_base/eval/__pycache__/__init__.cpython-37.pyc +0 -0
  241. package/knowledge_base/eval/__pycache__/__init__.cpython-38.pyc +0 -0
  242. package/knowledge_base/eval/__pycache__/__main__.cpython-37.pyc +0 -0
  243. package/knowledge_base/eval/__pycache__/baseline.cpython-37.pyc +0 -0
  244. package/knowledge_base/eval/__pycache__/baseline.cpython-38.pyc +0 -0
  245. package/knowledge_base/eval/__pycache__/harness.cpython-37.pyc +0 -0
  246. package/knowledge_base/eval/__pycache__/harness.cpython-38.pyc +0 -0
  247. package/knowledge_base/eval/__pycache__/scorers.cpython-37.pyc +0 -0
  248. package/knowledge_base/eval/__pycache__/scorers.cpython-38.pyc +0 -0
  249. package/knowledge_base/eval/baseline.py +213 -0
  250. package/knowledge_base/eval/harness.py +400 -0
  251. package/knowledge_base/eval/queries_50.json +302 -0
  252. package/knowledge_base/eval/scorers.py +494 -0
  253. package/knowledge_base/exemplars/README.md +7 -0
  254. package/knowledge_base/exemplars/__pycache__/pytest_reference_snippet.cpython-37.pyc +0 -0
  255. package/knowledge_base/exemplars/gold_manual_concise_snippet.md +8 -0
  256. package/knowledge_base/exemplars/gold_manual_detailed_snippet.md +7 -0
  257. package/knowledge_base/exemplars/pytest_reference_snippet.py +37 -0
  258. package/knowledge_base/feedback/__init__.py +10 -0
  259. package/knowledge_base/feedback/__pycache__/__init__.cpython-37.pyc +0 -0
  260. package/knowledge_base/feedback/__pycache__/__init__.cpython-38.pyc +0 -0
  261. package/knowledge_base/feedback/__pycache__/analytics.cpython-37.pyc +0 -0
  262. package/knowledge_base/feedback/__pycache__/analytics.cpython-38.pyc +0 -0
  263. package/knowledge_base/feedback/__pycache__/applier.cpython-37.pyc +0 -0
  264. package/knowledge_base/feedback/__pycache__/applier.cpython-38.pyc +0 -0
  265. package/knowledge_base/feedback/__pycache__/curator.cpython-37.pyc +0 -0
  266. package/knowledge_base/feedback/__pycache__/curator.cpython-38.pyc +0 -0
  267. package/knowledge_base/feedback/__pycache__/handler.cpython-37.pyc +0 -0
  268. package/knowledge_base/feedback/__pycache__/handler.cpython-38.pyc +0 -0
  269. package/knowledge_base/feedback/analytics.py +72 -0
  270. package/knowledge_base/feedback/applier.py +93 -0
  271. package/knowledge_base/feedback/curator.py +49 -0
  272. package/knowledge_base/feedback/handler.py +151 -0
  273. package/knowledge_base/generation/__init__.py +5 -0
  274. package/knowledge_base/generation/__pycache__/__init__.cpython-37.pyc +0 -0
  275. package/knowledge_base/generation/__pycache__/__init__.cpython-38.pyc +0 -0
  276. package/knowledge_base/generation/__pycache__/input_classifier.cpython-37.pyc +0 -0
  277. package/knowledge_base/generation/__pycache__/input_classifier.cpython-38.pyc +0 -0
  278. package/knowledge_base/generation/__pycache__/manual_tc_parser.cpython-37.pyc +0 -0
  279. package/knowledge_base/generation/__pycache__/manual_tc_parser.cpython-38.pyc +0 -0
  280. package/knowledge_base/generation/__pycache__/prompts.cpython-37.pyc +0 -0
  281. package/knowledge_base/generation/__pycache__/prompts.cpython-38.pyc +0 -0
  282. package/knowledge_base/generation/__pycache__/tc_transformer.cpython-37.pyc +0 -0
  283. package/knowledge_base/generation/__pycache__/test_generator.cpython-37.pyc +0 -0
  284. package/knowledge_base/generation/__pycache__/test_generator.cpython-38.pyc +0 -0
  285. package/knowledge_base/generation/input_classifier.py +148 -0
  286. package/knowledge_base/generation/manual_tc_parser.py +180 -0
  287. package/knowledge_base/generation/prompts.py +755 -0
  288. package/knowledge_base/generation/tc_transformer.py +368 -0
  289. package/knowledge_base/generation/test_generator.py +553 -0
  290. package/knowledge_base/graph_db/__init__.py +5 -0
  291. package/knowledge_base/graph_db/__pycache__/__init__.cpython-37.pyc +0 -0
  292. package/knowledge_base/graph_db/__pycache__/__init__.cpython-38.pyc +0 -0
  293. package/knowledge_base/graph_db/__pycache__/graphiti_client.cpython-37.pyc +0 -0
  294. package/knowledge_base/graph_db/__pycache__/kg_traversal.cpython-37.pyc +0 -0
  295. package/knowledge_base/graph_db/__pycache__/migrate_kg.cpython-37.pyc +0 -0
  296. package/knowledge_base/graph_db/__pycache__/neo4j_client.cpython-37.pyc +0 -0
  297. package/knowledge_base/graph_db/__pycache__/neo4j_client.cpython-38.pyc +0 -0
  298. package/knowledge_base/graph_db/__pycache__/schema.cpython-37.pyc +0 -0
  299. package/knowledge_base/graph_db/__pycache__/temporal_ingest.cpython-37.pyc +0 -0
  300. package/knowledge_base/graph_db/graphiti_client.py +127 -0
  301. package/knowledge_base/graph_db/kg_traversal.py +192 -0
  302. package/knowledge_base/graph_db/migrate_kg.py +93 -0
  303. package/knowledge_base/graph_db/neo4j_client.py +116 -0
  304. package/knowledge_base/graph_db/schema.py +22 -0
  305. package/knowledge_base/graph_db/temporal_ingest.py +59 -0
  306. package/knowledge_base/ingestion/__init__.py +1 -0
  307. package/knowledge_base/ingestion/__pycache__/__init__.cpython-37.pyc +0 -0
  308. package/knowledge_base/ingestion/__pycache__/__init__.cpython-38.pyc +0 -0
  309. package/knowledge_base/ingestion/__pycache__/ingest.cpython-37.pyc +0 -0
  310. package/knowledge_base/ingestion/__pycache__/ingest.cpython-38.pyc +0 -0
  311. package/knowledge_base/ingestion/__pycache__/ingest_images.cpython-37.pyc +0 -0
  312. package/knowledge_base/ingestion/__pycache__/ingest_images.cpython-38.pyc +0 -0
  313. package/knowledge_base/ingestion/__pycache__/kg_extractor.cpython-37.pyc +0 -0
  314. package/knowledge_base/ingestion/__pycache__/kg_extractor.cpython-38.pyc +0 -0
  315. package/knowledge_base/ingestion/__pycache__/web_ingest.cpython-38.pyc +0 -0
  316. package/knowledge_base/ingestion/ingest.py +346 -0
  317. package/knowledge_base/ingestion/ingest_images.py +330 -0
  318. package/knowledge_base/ingestion/kg_extractor.py +358 -0
  319. package/knowledge_base/ingestion/web_ingest.py +83 -0
  320. package/knowledge_base/intelligence/__init__.py +1 -0
  321. package/knowledge_base/intelligence/__pycache__/__init__.cpython-37.pyc +0 -0
  322. package/knowledge_base/intelligence/__pycache__/execution_ingest.cpython-37.pyc +0 -0
  323. package/knowledge_base/intelligence/__pycache__/predictive_engine.cpython-37.pyc +0 -0
  324. package/knowledge_base/intelligence/__pycache__/risk_scorer.cpython-37.pyc +0 -0
  325. package/knowledge_base/intelligence/execution_ingest.py +34 -0
  326. package/knowledge_base/intelligence/predictive_engine.py +7 -0
  327. package/knowledge_base/intelligence/risk_scorer.py +8 -0
  328. package/knowledge_base/memory/__init__.py +6 -0
  329. package/knowledge_base/memory/__pycache__/__init__.cpython-37.pyc +0 -0
  330. package/knowledge_base/memory/__pycache__/__init__.cpython-38.pyc +0 -0
  331. package/knowledge_base/memory/__pycache__/cognee_client.cpython-37.pyc +0 -0
  332. package/knowledge_base/memory/__pycache__/cognee_client.cpython-38.pyc +0 -0
  333. package/knowledge_base/memory/__pycache__/ingest_memory.cpython-37.pyc +0 -0
  334. package/knowledge_base/memory/__pycache__/memory_types.cpython-37.pyc +0 -0
  335. package/knowledge_base/memory/__pycache__/memory_types.cpython-38.pyc +0 -0
  336. package/knowledge_base/memory/cognee_client.py +164 -0
  337. package/knowledge_base/memory/ingest_memory.py +60 -0
  338. package/knowledge_base/memory/memory_types.py +23 -0
  339. package/knowledge_base/models/test_requirement.py +84 -0
  340. package/knowledge_base/parsers/__init__.py +0 -0
  341. package/knowledge_base/parsers/__pycache__/__init__.cpython-37.pyc +0 -0
  342. package/knowledge_base/parsers/__pycache__/__init__.cpython-38.pyc +0 -0
  343. package/knowledge_base/parsers/__pycache__/csv_parser.cpython-37.pyc +0 -0
  344. package/knowledge_base/parsers/__pycache__/csv_parser.cpython-38.pyc +0 -0
  345. package/knowledge_base/parsers/__pycache__/doc_parser.cpython-37.pyc +0 -0
  346. package/knowledge_base/parsers/__pycache__/doc_parser.cpython-38.pyc +0 -0
  347. package/knowledge_base/parsers/__pycache__/html_parser.cpython-38.pyc +0 -0
  348. package/knowledge_base/parsers/__pycache__/image_extractor.cpython-37.pyc +0 -0
  349. package/knowledge_base/parsers/__pycache__/image_extractor.cpython-38.pyc +0 -0
  350. package/knowledge_base/parsers/__pycache__/markdown_parser.cpython-37.pyc +0 -0
  351. package/knowledge_base/parsers/__pycache__/markdown_parser.cpython-38.pyc +0 -0
  352. package/knowledge_base/parsers/__pycache__/pdf_parser.cpython-37.pyc +0 -0
  353. package/knowledge_base/parsers/__pycache__/pdf_parser.cpython-38.pyc +0 -0
  354. package/knowledge_base/parsers/csv_parser.py +126 -0
  355. package/knowledge_base/parsers/doc_parser.py +176 -0
  356. package/knowledge_base/parsers/html_parser.py +151 -0
  357. package/knowledge_base/parsers/image_extractor.py +370 -0
  358. package/knowledge_base/parsers/markdown_parser.py +116 -0
  359. package/knowledge_base/parsers/pdf_parser.py +178 -0
  360. package/knowledge_base/retrieval/__init__.py +1 -0
  361. package/knowledge_base/retrieval/__pycache__/__init__.cpython-37.pyc +0 -0
  362. package/knowledge_base/retrieval/__pycache__/__init__.cpython-38.pyc +0 -0
  363. package/knowledge_base/retrieval/__pycache__/context_fusion.cpython-37.pyc +0 -0
  364. package/knowledge_base/retrieval/__pycache__/context_fusion.cpython-38.pyc +0 -0
  365. package/knowledge_base/retrieval/__pycache__/knowledge_graph.cpython-37.pyc +0 -0
  366. package/knowledge_base/retrieval/__pycache__/knowledge_graph.cpython-38.pyc +0 -0
  367. package/knowledge_base/retrieval/__pycache__/reranker.cpython-37.pyc +0 -0
  368. package/knowledge_base/retrieval/__pycache__/reranker.cpython-38.pyc +0 -0
  369. package/knowledge_base/retrieval/__pycache__/retrieval_debug.cpython-37.pyc +0 -0
  370. package/knowledge_base/retrieval/__pycache__/retrieval_debug.cpython-38.pyc +0 -0
  371. package/knowledge_base/retrieval/__pycache__/retriever.cpython-37.pyc +0 -0
  372. package/knowledge_base/retrieval/__pycache__/retriever.cpython-38.pyc +0 -0
  373. package/knowledge_base/retrieval/__pycache__/unified_query.cpython-38.pyc +0 -0
  374. package/knowledge_base/retrieval/__pycache__/web_retriever.cpython-38.pyc +0 -0
  375. package/knowledge_base/retrieval/context_fusion.py +264 -0
  376. package/knowledge_base/retrieval/knowledge_graph.py +267 -0
  377. package/knowledge_base/retrieval/reranker.py +308 -0
  378. package/knowledge_base/retrieval/retrieval_debug.py +69 -0
  379. package/knowledge_base/retrieval/retriever.py +885 -0
  380. package/knowledge_base/retrieval/unified_query.py +189 -0
  381. package/knowledge_base/retrieval/web_retriever.py +337 -0
  382. package/knowledge_base/validators/__init__.py +1 -0
  383. package/knowledge_base/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  384. package/knowledge_base/validators/__pycache__/__init__.cpython-37.pyc +0 -0
  385. package/knowledge_base/validators/__pycache__/step_fidelity.cpython-314.pyc +0 -0
  386. package/knowledge_base/validators/__pycache__/step_fidelity.cpython-37.pyc +0 -0
  387. package/knowledge_base/validators/step_fidelity.py +88 -0
  388. package/knowledge_base/vision/__init__.py +0 -0
  389. package/knowledge_base/vision/__pycache__/__init__.cpython-37.pyc +0 -0
  390. package/knowledge_base/vision/__pycache__/__init__.cpython-38.pyc +0 -0
  391. package/knowledge_base/vision/__pycache__/image_describer.cpython-37.pyc +0 -0
  392. package/knowledge_base/vision/__pycache__/image_describer.cpython-38.pyc +0 -0
  393. package/knowledge_base/vision/image_describer.py +202 -0
  394. package/package.json +31 -0
@@ -0,0 +1,151 @@
1
+ """Parse HTML pages (kb.igel.com, community sites) into Section objects for chunking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from html import unescape
8
+ from typing import Optional
9
+ from urllib.parse import urljoin, urlparse
10
+
11
+ from knowledge_base.parsers.markdown_parser import Section
12
+
13
+
14
+ @dataclass
15
+ class ParsedWebPage:
16
+ url: str
17
+ title: str
18
+ sections: list[Section]
19
+ raw_text: str
20
+ source_type: str = "kb_web"
21
+
22
+
23
+ def _strip_tags(html: str) -> str:
24
+ text = re.sub(r"(?is)<(script|style|nav|footer|header|aside)[^>]*>.*?</\1>", " ", html)
25
+ text = re.sub(r"(?is)<br\s*/?>", "\n", text)
26
+ text = re.sub(r"(?is)</?(p|div|li|h[1-6]|tr|td|th)[^>]*>", "\n", text)
27
+ text = re.sub(r"<[^>]+>", " ", text)
28
+ text = unescape(text)
29
+ text = re.sub(r"[ \t]+\n", "\n", text)
30
+ text = re.sub(r"\n{3,}", "\n\n", text)
31
+ return text.strip()
32
+
33
+
34
+ def _extract_title(html: str, fallback: str) -> str:
35
+ m = re.search(r"(?is)<title[^>]*>(.*?)</title>", html)
36
+ if m:
37
+ title = _strip_tags(m.group(1)).strip()
38
+ if title:
39
+ return title[:200]
40
+ for tag in ("h1", "h2"):
41
+ m = re.search(rf"(?is)<{tag}[^>]*>(.*?)</{tag}>", html)
42
+ if m:
43
+ title = _strip_tags(m.group(1)).strip()
44
+ if title:
45
+ return title[:200]
46
+ return fallback
47
+
48
+
49
+ def _extract_main_html(html: str) -> str:
50
+ """Best-effort main content extraction for Confluence / Scroll Sites pages."""
51
+ for pattern in (
52
+ r'(?is)<main[^>]*>(.*?)</main>',
53
+ r'(?is)<article[^>]*>(.*?)</article>',
54
+ r'(?is)<div[^>]*id="main-content"[^>]*>(.*?)</div>',
55
+ r'(?is)<div[^>]*class="[^"]*wiki-content[^"]*"[^>]*>(.*?)</div>',
56
+ r'(?is)<div[^>]*class="[^"]*page-content[^"]*"[^>]*>(.*?)</div>',
57
+ r'(?is)<div[^>]*class="[^"]*content[^"]*"[^>]*>(.*?)</div>',
58
+ ):
59
+ m = re.search(pattern, html)
60
+ if m and len(m.group(1).strip()) > 200:
61
+ return m.group(1)
62
+ return html
63
+
64
+
65
+ def _split_headings(text: str, page_title: str, url: str) -> list[Section]:
66
+ lines = text.split("\n")
67
+ sections: list[Section] = []
68
+ current_title = page_title
69
+ current_lines: list[str] = []
70
+
71
+ for line in lines:
72
+ heading = re.match(r"^(#{1,4})\s+(.+)", line.strip())
73
+ if heading:
74
+ content = "\n".join(current_lines).strip()
75
+ if content:
76
+ sections.append(
77
+ Section(
78
+ title=current_title,
79
+ level=len(heading.group(1)),
80
+ content=content,
81
+ page_hint=0,
82
+ metadata={"canonical_url": url, "source_type": "kb_web"},
83
+ )
84
+ )
85
+ current_title = heading.group(2).strip()
86
+ current_lines = []
87
+ else:
88
+ current_lines.append(line)
89
+
90
+ content = "\n".join(current_lines).strip()
91
+ if content:
92
+ sections.append(
93
+ Section(
94
+ title=current_title,
95
+ level=1,
96
+ content=content,
97
+ page_hint=0,
98
+ metadata={"canonical_url": url, "source_type": "kb_web"},
99
+ )
100
+ )
101
+
102
+ if not sections and text.strip():
103
+ sections.append(
104
+ Section(
105
+ title=page_title,
106
+ level=1,
107
+ content=text.strip(),
108
+ page_hint=0,
109
+ metadata={"canonical_url": url, "source_type": "kb_web"},
110
+ )
111
+ )
112
+ return sections
113
+
114
+
115
+ def parse_html_page(html: str, url: str, *, source_type: str = "kb_web") -> ParsedWebPage:
116
+ """Convert an HTML page into structured sections."""
117
+ title = _extract_title(html, fallback=urlparse(url).path.split("/")[-1] or url)
118
+ main_html = _extract_main_html(html)
119
+ text = _strip_tags(main_html)
120
+
121
+ # Promote obvious headings (ALL CAPS lines or numbered sections)
122
+ promoted: list[str] = []
123
+ for line in text.split("\n"):
124
+ stripped = line.strip()
125
+ if re.match(r"^[A-Z][A-Z0-9\s\-/&]{8,}$", stripped):
126
+ promoted.append(f"## {stripped.title()}")
127
+ elif re.match(r"^\d+\.\s+[A-Z]", stripped):
128
+ promoted.append(stripped)
129
+ else:
130
+ promoted.append(line)
131
+ text = "\n".join(promoted)
132
+
133
+ sections = _split_headings(text, title, url)
134
+ for sec in sections:
135
+ sec.metadata["canonical_url"] = url
136
+ sec.metadata["source_type"] = source_type
137
+
138
+ return ParsedWebPage(url=url, title=title, sections=sections, raw_text=text, source_type=source_type)
139
+
140
+
141
+ def resolve_links(html: str, base_url: str) -> list[str]:
142
+ """Extract same-domain links from HTML for crawl expansion."""
143
+ base_host = urlparse(base_url).netloc
144
+ links: list[str] = []
145
+ for href in re.findall(r'(?is)href=["\']([^"\']+)["\']', html):
146
+ if href.startswith("#") or href.startswith("mailto:"):
147
+ continue
148
+ full = urljoin(base_url, href)
149
+ if urlparse(full).netloc == base_host and full not in links:
150
+ links.append(full.split("#")[0])
151
+ return links[:50]
@@ -0,0 +1,370 @@
1
+ """
2
+ Image extraction from PDFs, DOCX, and DOC files.
3
+
4
+ Each extracted image carries enough context to be linked back to its source
5
+ document and the nearest text section:
6
+ - bytes (raw, byte-perfect)
7
+ - format / dimensions
8
+ - page_number (PDF) or paragraph_index (DOCX)
9
+ - bbox on page (PDF only)
10
+ - surrounding_text (paragraphs above + below)
11
+ - caption (if heuristically detectable)
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import io
17
+ import logging
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class ExtractedImage:
26
+ sequence_index: int
27
+ image_bytes: bytes
28
+ image_format: str # "png" | "jpeg" | "jpg" | ...
29
+ width: int | None
30
+ height: int | None
31
+ page_number: int | None # 1-based for PDFs (first occurrence)
32
+ bbox: dict | None # {x0,y0,x1,y1} for PDFs (first occurrence)
33
+ surrounding_text: str # paragraph(s) above + below the FIRST occurrence
34
+ caption: str | None
35
+ occurrence_pages: list[int] = field(default_factory=list) # all pages this image appears on
36
+ is_recurring: bool = False # likely header/logo/footer if appears on >25% of pages
37
+ image_hash: str = field(init=False)
38
+
39
+ def __post_init__(self):
40
+ self.image_hash = hashlib.sha256(self.image_bytes).hexdigest()
41
+
42
+
43
+ # ── PDFs (PyMuPDF) ────────────────────────────────────────────────────────────
44
+
45
+ # Filter: skip tiny decorative images and giant page-wide background images
46
+ MIN_IMAGE_BYTES = 2 * 1024 # 2 KB
47
+ MIN_DIMENSION = 50 # pixels
48
+ MAX_DIMENSION = 8000 # pixels (filter out scanned-page backgrounds)
49
+
50
+
51
+ def extract_images_from_pdf(pdf_path: Path) -> list[ExtractedImage]:
52
+ """Extract unique images from a PDF.
53
+
54
+ Critical: same logo / header / footer often appears on every page.
55
+ We dedupe by sha256 across the WHOLE document, keep the first occurrence's
56
+ page_number/bbox/surrounding_text, and record all pages it appears on so the
57
+ describer can flag recurring headers.
58
+ """
59
+ import fitz # PyMuPDF
60
+
61
+ try:
62
+ doc = fitz.open(str(pdf_path))
63
+ except Exception as e:
64
+ logger.error("PyMuPDF could not open %s: %s", pdf_path.name, e)
65
+ return []
66
+
67
+ page_count = len(doc)
68
+
69
+ # First pass: build (hash → list of occurrences) and (xref → bytes/meta)
70
+ # An "occurrence" = the page it appeared on, plus first-seen bbox + page text.
71
+ # We process pages in order so the FIRST occurrence becomes the canonical one.
72
+ occurrences: dict[str, dict] = {} # hash -> {bytes, ext, w, h, first_page, first_bbox, first_page_text, pages: [int]}
73
+
74
+ try:
75
+ for page_idx in range(page_count):
76
+ page = doc[page_idx]
77
+ page_num = page_idx + 1
78
+ page_text = page.get_text("text") or ""
79
+
80
+ seen_in_page: set[str] = set()
81
+ for img_info in page.get_images(full=True):
82
+ xref = img_info[0]
83
+ try:
84
+ info = doc.extract_image(xref)
85
+ except Exception as e:
86
+ logger.warning("extract_image failed (xref=%s) in %s: %s", xref, pdf_path.name, e)
87
+ continue
88
+
89
+ img_bytes = info.get("image")
90
+ ext = (info.get("ext") or "png").lower()
91
+ w = info.get("width")
92
+ h = info.get("height")
93
+
94
+ if not img_bytes or len(img_bytes) < MIN_IMAGE_BYTES:
95
+ continue
96
+ if w and h:
97
+ if w < MIN_DIMENSION or h < MIN_DIMENSION:
98
+ continue
99
+ if w > MAX_DIMENSION or h > MAX_DIMENSION:
100
+ continue
101
+
102
+ hsh = hashlib.sha256(img_bytes).hexdigest()
103
+ if hsh in seen_in_page:
104
+ continue
105
+ seen_in_page.add(hsh)
106
+
107
+ if hsh in occurrences:
108
+ # Already recorded — just track the page
109
+ occurrences[hsh]["pages"].append(page_num)
110
+ continue
111
+
112
+ # First time seeing this image — record canonical position
113
+ bbox = None
114
+ try:
115
+ rects = page.get_image_rects(xref)
116
+ if rects:
117
+ r = rects[0]
118
+ bbox = {"x0": float(r.x0), "y0": float(r.y0),
119
+ "x1": float(r.x1), "y1": float(r.y1)}
120
+ except Exception:
121
+ pass
122
+
123
+ occurrences[hsh] = {
124
+ "bytes": img_bytes,
125
+ "ext": ext,
126
+ "w": w, "h": h,
127
+ "first_page": page_num,
128
+ "first_page_idx": page_idx,
129
+ "first_bbox": bbox,
130
+ "first_page_text": page_text,
131
+ "pages": [page_num],
132
+ }
133
+ finally:
134
+ # Defer doc.close until after we read previous/next page text below
135
+ pass
136
+
137
+ # Second pass: build ExtractedImage per unique hash, with surrounding_text
138
+ # composed from the FIRST page's neighbors. Mark recurring if appears on
139
+ # > 25% of pages.
140
+ images: list[ExtractedImage] = []
141
+ seq = 0
142
+ recurring_threshold = max(3, int(page_count * 0.25))
143
+
144
+ try:
145
+ # Sort by first occurrence so sequence_index reflects reading order
146
+ for hsh, occ in sorted(occurrences.items(), key=lambda kv: (kv[1]["first_page"], kv[1]["pages"][0])):
147
+ page_idx = occ["first_page_idx"]
148
+ page_num = occ["first_page"]
149
+ page_text = occ["first_page_text"]
150
+
151
+ surrounding_parts = []
152
+ if page_idx > 0:
153
+ try:
154
+ prev = doc[page_idx - 1].get_text("text") or ""
155
+ if prev:
156
+ surrounding_parts.append(prev[-600:].strip())
157
+ except Exception:
158
+ pass
159
+ if page_text.strip():
160
+ surrounding_parts.append(page_text.strip()[:1500])
161
+ if page_idx + 1 < page_count:
162
+ try:
163
+ nxt = doc[page_idx + 1].get_text("text") or ""
164
+ if nxt:
165
+ surrounding_parts.append(nxt[:600].strip())
166
+ except Exception:
167
+ pass
168
+ surrounding_text = "\n\n".join(surrounding_parts)
169
+
170
+ caption = _find_caption(page_text, occ["first_bbox"])
171
+ occurrence_pages = sorted(set(occ["pages"]))
172
+ is_recurring = len(occurrence_pages) >= recurring_threshold and page_count >= 4
173
+
174
+ seq += 1
175
+ images.append(ExtractedImage(
176
+ sequence_index=seq,
177
+ image_bytes=occ["bytes"],
178
+ image_format=occ["ext"],
179
+ width=occ["w"],
180
+ height=occ["h"],
181
+ page_number=page_num,
182
+ bbox=occ["first_bbox"],
183
+ surrounding_text=surrounding_text,
184
+ caption=caption,
185
+ occurrence_pages=occurrence_pages,
186
+ is_recurring=is_recurring,
187
+ ))
188
+ finally:
189
+ doc.close()
190
+
191
+ return images
192
+
193
+
194
+ def _find_caption(page_text: str, bbox: dict | None) -> str | None:
195
+ import re
196
+ if not page_text:
197
+ return None
198
+ # Heuristic: find a "Figure N: ..." or "Fig. N ..." or "Table N: ..." line
199
+ m = re.search(r"^(?:Figure|Fig\.|Table)\s*\d+[:.]?\s*[^\n]{0,200}",
200
+ page_text, flags=re.MULTILINE)
201
+ if m:
202
+ return m.group(0).strip()
203
+ return None
204
+
205
+
206
+ # ── DOCX (python-docx + relationships) ───────────────────────────────────────
207
+
208
+ # Inline shapes / images live in the document's relationships.
209
+ # We iterate paragraphs in order; whenever a run contains a drawing element
210
+ # referencing an image relationship, we record that image at that point.
211
+
212
+ def extract_images_from_docx(docx_path: Path) -> list[ExtractedImage]:
213
+ from docx import Document
214
+ from docx.oxml.ns import qn
215
+
216
+ images: list[ExtractedImage] = []
217
+ seq = 0
218
+
219
+ try:
220
+ doc = Document(str(docx_path))
221
+ except Exception as e:
222
+ logger.error("python-docx could not open %s: %s", docx_path.name, e)
223
+ return []
224
+
225
+ # Build a flat ordered list of (paragraph_index, paragraph_text, image_rels[])
226
+ paragraphs = doc.paragraphs
227
+
228
+ # First pass: linearize into events
229
+ events: list[tuple[str, int, str | list]] = []
230
+ # event types: ("text", para_idx, text), ("image", para_idx, [rel_id, ...])
231
+ for p_idx, para in enumerate(paragraphs):
232
+ text = para.text.strip()
233
+ if text:
234
+ events.append(("text", p_idx, text))
235
+
236
+ # Find image relationship ids inside this paragraph
237
+ rel_ids: list[str] = []
238
+ for run in para.runs:
239
+ for blip in run._element.iter(qn("a:blip")):
240
+ rid = blip.get(qn("r:embed"))
241
+ if rid:
242
+ rel_ids.append(rid)
243
+ if rel_ids:
244
+ events.append(("image", p_idx, rel_ids))
245
+
246
+ # Second pass: emit ExtractedImage at each "image" event with surrounding text
247
+ rels = doc.part.related_parts
248
+ for ev_idx, ev in enumerate(events):
249
+ if ev[0] != "image":
250
+ continue
251
+ _, p_idx, rel_ids = ev
252
+
253
+ # Surrounding text: 2 prior text events + 2 following text events
254
+ before = [e[2] for e in events[max(0, ev_idx - 6):ev_idx] if e[0] == "text"][-3:]
255
+ after = [e[2] for e in events[ev_idx + 1: ev_idx + 7] if e[0] == "text"][:3]
256
+ surrounding_text = "\n\n".join(before + after)
257
+
258
+ # Caption: heuristic — paragraph immediately after that starts with Figure/Table
259
+ caption = None
260
+ for e in events[ev_idx + 1: ev_idx + 4]:
261
+ if e[0] == "text":
262
+ t = e[2]
263
+ if t[:7].lower() in ("figure ", "fig. ", "table ") or \
264
+ t.lower().startswith(("figure", "fig.", "table")):
265
+ caption = t[:200]
266
+ break
267
+
268
+ for rel_id in rel_ids:
269
+ part = rels.get(rel_id)
270
+ if part is None or not hasattr(part, "blob"):
271
+ continue
272
+ img_bytes: bytes = part.blob
273
+ if not img_bytes or len(img_bytes) < MIN_IMAGE_BYTES:
274
+ continue
275
+
276
+ # Try to determine format + dimensions via Pillow
277
+ ext = "png"
278
+ w = h = None
279
+ try:
280
+ from PIL import Image
281
+ im = Image.open(io.BytesIO(img_bytes))
282
+ ext = (im.format or "png").lower()
283
+ if ext == "jpeg":
284
+ ext = "jpg"
285
+ w, h = im.size
286
+ if w < MIN_DIMENSION or h < MIN_DIMENSION:
287
+ continue
288
+ if w > MAX_DIMENSION or h > MAX_DIMENSION:
289
+ continue
290
+ except Exception:
291
+ pass
292
+
293
+ seq += 1
294
+ images.append(ExtractedImage(
295
+ sequence_index=seq,
296
+ image_bytes=img_bytes,
297
+ image_format=ext,
298
+ width=w,
299
+ height=h,
300
+ page_number=None, # DOCX has no page numbers without rendering
301
+ bbox=None,
302
+ surrounding_text=surrounding_text,
303
+ caption=caption,
304
+ ))
305
+
306
+ return images
307
+
308
+
309
+ # ── DOC binary (Word legacy) — convert to DOCX first via Word COM if available ──
310
+
311
+ def extract_images_from_doc(doc_path: Path) -> list[ExtractedImage]:
312
+ """Best-effort: convert .doc to .docx via Word COM, then reuse DOCX extractor.
313
+ Returns [] if Microsoft Word isn't available on this host."""
314
+ import tempfile
315
+
316
+ try:
317
+ import win32com.client # type: ignore
318
+ import pythoncom # type: ignore
319
+ except ImportError:
320
+ logger.info("pywin32 not available — cannot extract images from .doc: %s", doc_path.name)
321
+ return []
322
+
323
+ pythoncom.CoInitialize()
324
+ word = None
325
+ try:
326
+ word = win32com.client.Dispatch("Word.Application")
327
+ word.Visible = False
328
+ except Exception as e:
329
+ logger.info("Microsoft Word COM unavailable — skipping images for %s (%s)", doc_path.name, e)
330
+ return []
331
+
332
+ tmp = Path(tempfile.gettempdir()) / f"{doc_path.stem}_converted.docx"
333
+ try:
334
+ d = word.Documents.Open(str(doc_path.resolve()))
335
+ # WdSaveFormat.wdFormatXMLDocument = 12 (.docx)
336
+ d.SaveAs(str(tmp), FileFormat=12)
337
+ d.Close(False)
338
+ word.Quit()
339
+ except Exception as e:
340
+ logger.warning("Could not convert %s -> docx: %s", doc_path.name, e)
341
+ try:
342
+ if word is not None:
343
+ word.Quit()
344
+ except Exception:
345
+ pass
346
+ return []
347
+
348
+ if not tmp.exists():
349
+ return []
350
+
351
+ images = extract_images_from_docx(tmp)
352
+ try:
353
+ tmp.unlink()
354
+ except Exception:
355
+ pass
356
+ return images
357
+
358
+
359
+ # ── Dispatcher ────────────────────────────────────────────────────────────────
360
+
361
+ def extract_images(file_path: Path) -> list[ExtractedImage]:
362
+ suffix = file_path.suffix.lower()
363
+ if suffix == ".pdf":
364
+ return extract_images_from_pdf(file_path)
365
+ if suffix == ".docx":
366
+ return extract_images_from_docx(file_path)
367
+ if suffix == ".doc":
368
+ return extract_images_from_doc(file_path)
369
+ # CSV, MD, MHT, etc. — no embedded images
370
+ return []
@@ -0,0 +1,116 @@
1
+ """
2
+ Parses pre-converted Markdown files from marker-pdf output.
3
+ Priority: if a PDF already has a markdown_output folder, use it — it's higher quality
4
+ than raw pdfplumber extraction because it's been processed with Azure OpenAI LLM.
5
+ """
6
+ from __future__ import annotations
7
+ import re
8
+ from pathlib import Path
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class Section:
14
+ title: str
15
+ level: int # 1=H1, 2=H2, 3=H3, 0=no heading
16
+ content: str
17
+ page_hint: int = 0 # approximate page (from image filenames or context)
18
+ metadata: dict = field(default_factory=dict)
19
+
20
+
21
+ def parse_markdown_file(md_path: Path) -> list[Section]:
22
+ text = md_path.read_text(encoding="utf-8", errors="replace")
23
+ return _split_into_sections(text, str(md_path.name))
24
+
25
+
26
+ def _split_into_sections(text: str, source_name: str) -> list[Section]:
27
+ # Strip HTML comments (marker-pdf source headers)
28
+ text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL).strip()
29
+
30
+ lines = text.split("\n")
31
+ sections: list[Section] = []
32
+ current_title = source_name
33
+ current_level = 0
34
+ current_lines: list[str] = []
35
+ page_hint = 1
36
+
37
+ for line in lines:
38
+ heading_match = re.match(r"^(#{1,4})\s+(.+)", line)
39
+ if heading_match:
40
+ # Flush previous section
41
+ content = "\n".join(current_lines).strip()
42
+ if content:
43
+ sections.append(Section(
44
+ title=current_title,
45
+ level=current_level,
46
+ content=content,
47
+ page_hint=page_hint,
48
+ ))
49
+ current_title = heading_match.group(2).strip()
50
+ current_level = len(heading_match.group(1))
51
+ current_lines = []
52
+ else:
53
+ # Track page hints from image references like images/page_5_img_1.png
54
+ page_ref = re.search(r"page_(\d+)", line)
55
+ if page_ref:
56
+ page_hint = int(page_ref.group(1))
57
+ current_lines.append(line)
58
+
59
+ # Flush last section
60
+ content = "\n".join(current_lines).strip()
61
+ if content:
62
+ sections.append(Section(
63
+ title=current_title,
64
+ level=current_level,
65
+ content=content,
66
+ page_hint=page_hint,
67
+ ))
68
+
69
+ # Merge tiny orphan sections (heading-only, < 50 chars) into next sibling
70
+ return _merge_orphan_sections(sections)
71
+
72
+
73
+ def _merge_orphan_sections(sections: list[Section]) -> list[Section]:
74
+ merged: list[Section] = []
75
+ i = 0
76
+ while i < len(sections):
77
+ sec = sections[i]
78
+ # If this section has almost no content and there's a next section at same/lower level
79
+ if len(sec.content) < 60 and i + 1 < len(sections):
80
+ next_sec = sections[i + 1]
81
+ # Prepend this section's content into next section with heading as context
82
+ next_sec.content = f"**{sec.title}**\n{sec.content}\n\n{next_sec.content}".strip()
83
+ i += 1
84
+ continue
85
+ merged.append(sec)
86
+ i += 1
87
+ return merged
88
+
89
+
90
+ def find_markdown_for_pdf(pdf_filename: str, markdown_output_dir: Path) -> Path | None:
91
+ """
92
+ Given a PDF filename, find its pre-converted markdown file in markdown_output/.
93
+ Returns the .md file path if found, else None.
94
+ """
95
+ stem = Path(pdf_filename).stem
96
+ # Normalize: spaces → underscores, remove parens/commas (matches convert_pdfs.py logic)
97
+ safe_dir = stem.replace(" ", "_").replace("(", "").replace(")", "").replace(",", "")
98
+ md_path = markdown_output_dir / safe_dir / f"{stem}.md"
99
+
100
+ if md_path.exists():
101
+ return md_path
102
+
103
+ # Also try with cleaned filename
104
+ for folder in markdown_output_dir.iterdir():
105
+ if folder.is_dir():
106
+ candidate = folder / f"{folder.name}.md"
107
+ if candidate.exists() and _fuzzy_match(stem, folder.name):
108
+ return candidate
109
+
110
+ return None
111
+
112
+
113
+ def _fuzzy_match(a: str, b: str) -> bool:
114
+ """Simple match ignoring spaces, underscores, case."""
115
+ norm = lambda s: s.lower().replace("_", "").replace(" ", "").replace("-", "")
116
+ return norm(a) in norm(b) or norm(b) in norm(a)