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,885 @@
1
+ """
2
+ Hybrid Knowledge Base Retriever — Vector similarity + PostgreSQL full-text, merged with RRF.
3
+
4
+ Flow:
5
+ 1. Embed query via Azure text-embedding-3-large
6
+ 2. Run pgvector cosine similarity search (top_k * 2 candidates)
7
+ 3. Run PostgreSQL plainto_tsquery full-text search (top_k * 2 candidates)
8
+ 4. Merge both result sets with Reciprocal Rank Fusion (RRF, k=60)
9
+ 5. Deduplicate: keep best-scoring child per parent_chunk_id
10
+ 6. Bulk-fetch parent_chunk content (full section, 800-1200 tokens) for LLM context
11
+ 7. Return top_k SearchResult objects
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from dataclasses import dataclass, field
17
+ from time import perf_counter
18
+
19
+ import psycopg2
20
+
21
+ from knowledge_base.db.client import get_conn, init_pool
22
+ from knowledge_base.embeddings.azure_embedder import embed_single, format_for_pgvector
23
+ from knowledge_base.retrieval.knowledge_graph import (
24
+ KGContext,
25
+ classify_query_style,
26
+ retrieve_kg_context,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+ _POOL_INITIALISED = False
31
+
32
+
33
+ def _ensure_pool() -> None:
34
+ global _POOL_INITIALISED
35
+ if not _POOL_INITIALISED:
36
+ init_pool()
37
+ _POOL_INITIALISED = True
38
+
39
+
40
+ # ── Data Model ────────────────────────────────────────────────────────────────
41
+
42
+ @dataclass
43
+ class SearchResult:
44
+ child_chunk_id: str
45
+ parent_chunk_id: str
46
+ document_id: str
47
+ child_content: str
48
+ parent_content: str # full section text — sent to LLM as context
49
+ section_title: str
50
+ file_name: str
51
+ product: str
52
+ chunk_type: str
53
+ rrf_score: float
54
+ vector_rank: int | None = None
55
+ fts_rank: int | None = None
56
+ rerank_score: float | None = None
57
+ metadata: dict = field(default_factory=dict)
58
+
59
+
60
+ # ── Public API ────────────────────────────────────────────────────────────────
61
+
62
+ def retrieve(
63
+ query: str,
64
+ top_k: int = 8,
65
+ product: str | None = None,
66
+ chunk_type: str | None = None,
67
+ ) -> list[SearchResult]:
68
+ """
69
+ Hybrid search returning top_k SearchResult objects with full parent context loaded.
70
+
71
+ Args:
72
+ query: Natural language search query
73
+ top_k: Number of results to return (after deduplication)
74
+ product: Optional product filter — "UMS", "IGEL OS", "COSMOS", "ICG", "IMI"
75
+ chunk_type: Optional type filter — "procedure", "concept", "configuration", "table"
76
+
77
+ Returns:
78
+ List of SearchResult sorted by rrf_score descending. Empty list if nothing found.
79
+ """
80
+ if not query.strip():
81
+ logger.warning("retrieve() called with empty query")
82
+ return []
83
+
84
+ _ensure_pool()
85
+
86
+ candidate_limit = top_k * 3 # fetch extra candidates before dedup
87
+
88
+ # Step 1: embed query
89
+ query_vec = _embed_query(query)
90
+ vec_available = any(v != 0.0 for v in query_vec)
91
+
92
+ # Step 2: vector search (skip if embedding failed)
93
+ vector_rows: list[tuple] = []
94
+ if vec_available:
95
+ try:
96
+ vector_rows = _vector_search(query_vec, candidate_limit, product, chunk_type)
97
+ logger.debug("Vector search returned %d candidates", len(vector_rows))
98
+ except psycopg2.ProgrammingError as e:
99
+ if "vector" in str(e).lower():
100
+ logger.error(
101
+ "pgvector extension not available. Run: CREATE EXTENSION IF NOT EXISTS vector; "
102
+ "then re-run knowledge_base.ingestion.ingest"
103
+ )
104
+ else:
105
+ logger.error("Vector search failed: %s", e)
106
+
107
+ # Step 3: full-text search
108
+ fts_rows: list[tuple] = []
109
+ try:
110
+ fts_rows = _fts_search(query, candidate_limit, product, chunk_type)
111
+ logger.debug("FTS search returned %d candidates", len(fts_rows))
112
+ except Exception as e:
113
+ logger.error("FTS search failed: %s", e)
114
+
115
+ if not vector_rows and not fts_rows:
116
+ # If product filter was used and both returned nothing, retry without filter
117
+ if product:
118
+ logger.warning(
119
+ "No results for product='%s'. Retrying without product filter.", product
120
+ )
121
+ return retrieve(query, top_k=top_k, product=None, chunk_type=chunk_type)
122
+ logger.warning("No KB results found for query: %r", query)
123
+ return []
124
+
125
+ # Step 4: RRF merge
126
+ # Build lookup: child_id -> row metadata (from whichever result set has it)
127
+ row_meta: dict[str, tuple] = {}
128
+ for row in vector_rows + fts_rows:
129
+ child_id = row[0]
130
+ if child_id not in row_meta:
131
+ row_meta[child_id] = row
132
+
133
+ vector_ranks = {row[0]: idx + 1 for idx, row in enumerate(vector_rows)}
134
+ fts_ranks = {row[0]: idx + 1 for idx, row in enumerate(fts_rows)}
135
+ merged = _rrf_merge(vector_ranks, fts_ranks)
136
+
137
+ # Step 5: deduplicate by parent_chunk_id (keep highest-scored child per parent)
138
+ child_to_parent = {row[0]: row[1] for row in (vector_rows + fts_rows)}
139
+ merged_dedup = _deduplicate_by_parent(merged, child_to_parent)
140
+
141
+ # Take top candidates before fetching parents (avoid unnecessary DB calls)
142
+ top_candidates = merged_dedup[:top_k]
143
+
144
+ # Step 6: bulk-fetch parent content
145
+ parent_ids = list({child_to_parent[cid] for cid, _ in top_candidates if cid in child_to_parent})
146
+ parent_data = _fetch_parent_chunks(parent_ids) # {parent_id: (content, chunk_type)}
147
+
148
+ # Step 7: build SearchResult objects
149
+ results: list[SearchResult] = []
150
+ for child_id, rrf_score in top_candidates:
151
+ if child_id not in row_meta:
152
+ continue
153
+ row = row_meta[child_id]
154
+ # row shape: (child_id, parent_id, doc_id, child_content, section_title, file_name, product, rank_pos)
155
+ parent_id = row[1]
156
+ parent_content, parent_chunk_type = parent_data.get(parent_id, ("", "concept"))
157
+
158
+ results.append(SearchResult(
159
+ child_chunk_id=child_id,
160
+ parent_chunk_id=parent_id,
161
+ document_id=row[2],
162
+ child_content=row[3],
163
+ parent_content=parent_content,
164
+ section_title=row[4] or "",
165
+ file_name=row[5] or "",
166
+ product=row[6] or "IGEL",
167
+ chunk_type=parent_chunk_type or "concept",
168
+ rrf_score=rrf_score,
169
+ vector_rank=vector_ranks.get(child_id),
170
+ fts_rank=fts_ranks.get(child_id),
171
+ ))
172
+
173
+ logger.info(
174
+ "retrieve('%s') → %d results (vec=%d fts=%d dedup=%d)",
175
+ query[:60], len(results), len(vector_rows), len(fts_rows), len(merged_dedup),
176
+ )
177
+ return results
178
+
179
+
180
+ # ── Internal Helpers ──────────────────────────────────────────────────────────
181
+
182
+ def _embed_query(query: str) -> list[float]:
183
+ try:
184
+ return embed_single(query)
185
+ except Exception as e:
186
+ logger.warning("Embedding failed for query, falling back to FTS only: %s", e)
187
+ from knowledge_base.config import cfg
188
+ return [0.0] * cfg.EMBEDDING_DIM
189
+
190
+
191
+ def _vector_search(
192
+ query_vec: list[float],
193
+ limit: int,
194
+ product: str | None,
195
+ chunk_type: str | None,
196
+ ) -> list[tuple]:
197
+ vec_literal = format_for_pgvector(query_vec)
198
+
199
+ sql = """
200
+ SELECT
201
+ c.id::text,
202
+ c.parent_chunk_id::text,
203
+ c.document_id::text,
204
+ c.content,
205
+ p.section_title,
206
+ d.file_name,
207
+ d.product,
208
+ ROW_NUMBER() OVER (ORDER BY c.embedding <=> %s::vector) AS rank_pos
209
+ FROM kb_chunks c
210
+ JOIN kb_parent_chunks p ON c.parent_chunk_id = p.id
211
+ JOIN kb_documents d ON c.document_id = d.id
212
+ WHERE c.embedding IS NOT NULL
213
+ AND (%s IS NULL OR d.product ILIKE %s)
214
+ AND (%s IS NULL OR c.chunk_type = %s)
215
+ ORDER BY c.embedding <=> %s::vector
216
+ LIMIT %s
217
+ """
218
+ params = (
219
+ vec_literal,
220
+ product, f"%{product}%" if product else None,
221
+ chunk_type, chunk_type,
222
+ vec_literal,
223
+ limit,
224
+ )
225
+ with get_conn() as conn:
226
+ with conn.cursor() as cur:
227
+ cur.execute(sql, params)
228
+ return cur.fetchall()
229
+
230
+
231
+ def _fts_lang() -> str:
232
+ allowed = {"english", "german", "simple"}
233
+ lang = getattr(cfg, "FTS_LANGUAGE", "english").lower()
234
+ return lang if lang in allowed else "english"
235
+
236
+
237
+ def _fts_search(
238
+ query: str,
239
+ limit: int,
240
+ product: str | None,
241
+ chunk_type: str | None,
242
+ ) -> list[tuple]:
243
+ lang = _fts_lang()
244
+ sql = f"""
245
+ SELECT
246
+ c.id::text,
247
+ c.parent_chunk_id::text,
248
+ c.document_id::text,
249
+ c.content,
250
+ p.section_title,
251
+ d.file_name,
252
+ d.product,
253
+ ROW_NUMBER() OVER (
254
+ ORDER BY ts_rank_cd(
255
+ to_tsvector('{lang}', c.content),
256
+ plainto_tsquery('{lang}', %s)
257
+ ) DESC
258
+ ) AS rank_pos
259
+ FROM kb_chunks c
260
+ JOIN kb_parent_chunks p ON c.parent_chunk_id = p.id
261
+ JOIN kb_documents d ON c.document_id = d.id
262
+ WHERE to_tsvector('{lang}', c.content) @@ plainto_tsquery('{lang}', %s)
263
+ AND (%s IS NULL OR d.product ILIKE %s)
264
+ AND (%s IS NULL OR c.chunk_type = %s)
265
+ ORDER BY ts_rank_cd(
266
+ to_tsvector('{lang}', c.content),
267
+ plainto_tsquery('{lang}', %s)
268
+ ) DESC
269
+ LIMIT %s
270
+ """
271
+ params = (
272
+ query, query,
273
+ product, f"%{product}%" if product else None,
274
+ chunk_type, chunk_type,
275
+ query,
276
+ limit,
277
+ )
278
+ with get_conn() as conn:
279
+ with conn.cursor() as cur:
280
+ cur.execute(sql, params)
281
+ return cur.fetchall()
282
+
283
+
284
+ def _rrf_merge(
285
+ vector_ranks: dict[str, int],
286
+ fts_ranks: dict[str, int],
287
+ rrf_k: int = 60,
288
+ ) -> list[tuple[str, float]]:
289
+ all_ids = set(vector_ranks) | set(fts_ranks)
290
+ scores: dict[str, float] = {}
291
+ for cid in all_ids:
292
+ v_score = 1.0 / (vector_ranks.get(cid, 9999) + rrf_k)
293
+ f_score = 1.0 / (fts_ranks.get(cid, 9999) + rrf_k)
294
+ scores[cid] = v_score + f_score
295
+ return sorted(scores.items(), key=lambda x: x[1], reverse=True)
296
+
297
+
298
+ def _deduplicate_by_parent(
299
+ merged: list[tuple[str, float]],
300
+ child_to_parent: dict[str, str],
301
+ ) -> list[tuple[str, float]]:
302
+ seen_parents: set[str] = set()
303
+ deduped: list[tuple[str, float]] = []
304
+ for child_id, score in merged:
305
+ parent_id = child_to_parent.get(child_id, child_id)
306
+ if parent_id not in seen_parents:
307
+ seen_parents.add(parent_id)
308
+ deduped.append((child_id, score))
309
+ return deduped
310
+
311
+
312
+ def _fetch_parent_chunks(parent_ids: list[str]) -> dict[str, tuple[str, str]]:
313
+ if not parent_ids:
314
+ return {}
315
+ sql = """
316
+ SELECT id::text, content, chunk_type
317
+ FROM kb_parent_chunks
318
+ WHERE id = ANY(%s::uuid[])
319
+ """
320
+ with get_conn() as conn:
321
+ with conn.cursor() as cur:
322
+ cur.execute(sql, (parent_ids,))
323
+ return {row[0]: (row[1], row[2] or "concept") for row in cur.fetchall()}
324
+
325
+
326
+ @dataclass
327
+ class RetrievalBundle:
328
+ """V3 retrieval: fused chunks + KG + web + debug metadata."""
329
+ results: list[SearchResult]
330
+ kg_context: KGContext
331
+ image_block: str
332
+ image_hits: list["ImageHit"]
333
+ debug: "RetrievalDebugReport"
334
+ precedence_attribution: dict = field(default_factory=dict)
335
+ precedence_conflicts: list[dict] = field(default_factory=list)
336
+ web_hits: list = field(default_factory=list)
337
+ web_block: str = ""
338
+
339
+
340
+ def retrieve_v3(
341
+ query: str,
342
+ top_k: int = 8,
343
+ product: str | None = None,
344
+ chunk_type: str | None = None,
345
+ ) -> RetrievalBundle:
346
+ """
347
+ Hybrid retrieval with RRF, optional cross-encoder reranking, KG context, fusion, debug traces.
348
+ """
349
+ from knowledge_base.config import cfg
350
+ from knowledge_base.retrieval.context_fusion import fuse_retrieval_context
351
+ from knowledge_base.retrieval.reranker import RerankItem, rerank_passages
352
+ from knowledge_base.retrieval.retrieval_debug import ChunkScoreTrace, RetrievalDebugReport
353
+
354
+ timings: dict[str, float] = {}
355
+ debug = RetrievalDebugReport(
356
+ query=query,
357
+ query_classification=classify_query_style(query),
358
+ total_vector_candidates=0,
359
+ total_fts_candidates=0,
360
+ rrf_merged_count=0,
361
+ reranker_applied=(cfg.RERANKER_BACKEND or "").lower().strip() not in ("none", ""),
362
+ post_rerank_count=0,
363
+ kg_nodes_found=0,
364
+ kg_edges_traversed=0,
365
+ final_chunk_count=0,
366
+ total_tokens_used=0,
367
+ token_budget=int(cfg.CONTEXT_TOKEN_BUDGET),
368
+ traces=[],
369
+ timing_ms={},
370
+ )
371
+
372
+ if not query.strip():
373
+ return RetrievalBundle(
374
+ results=[],
375
+ kg_context=KGContext("", [], 0, 0),
376
+ image_block="",
377
+ image_hits=[],
378
+ debug=debug,
379
+ )
380
+
381
+ _ensure_pool()
382
+
383
+ cand_mult = max(3, getattr(cfg, "RETRIEVAL_CANDIDATE_MULTIPLIER", 6))
384
+ candidate_limit = max(top_k * cand_mult, top_k * 3)
385
+
386
+ t0 = perf_counter()
387
+ query_vec = _embed_query(query)
388
+ timings["embed_ms"] = (perf_counter() - t0) * 1000
389
+ vec_available = any(v != 0.0 for v in query_vec)
390
+
391
+ vector_rows: list[tuple] = []
392
+ if vec_available:
393
+ try:
394
+ t1 = perf_counter()
395
+ vector_rows = _vector_search(query_vec, candidate_limit, product, chunk_type)
396
+ timings["vector_ms"] = (perf_counter() - t1) * 1000
397
+ except psycopg2.ProgrammingError as e:
398
+ if "vector" in str(e).lower():
399
+ logger.error("pgvector not available — check extension: %s", e)
400
+ else:
401
+ logger.error("Vector search failed: %s", e)
402
+
403
+ fts_rows = []
404
+ try:
405
+ t2 = perf_counter()
406
+ fts_rows = _fts_search(query, candidate_limit, product, chunk_type)
407
+ timings["fts_ms"] = (perf_counter() - t2) * 1000
408
+ except Exception as e:
409
+ logger.error("FTS search failed: %s", e)
410
+
411
+ debug.total_vector_candidates = len(vector_rows)
412
+ debug.total_fts_candidates = len(fts_rows)
413
+
414
+ if not vector_rows and not fts_rows:
415
+ if product:
416
+ return retrieve_v3(query, top_k=top_k, product=None, chunk_type=chunk_type)
417
+
418
+ web_hits = []
419
+ web_block = ""
420
+ t_web = perf_counter()
421
+ if getattr(cfg, "WEB_RETRIEVAL_ENABLED", False):
422
+ try:
423
+ from knowledge_base.retrieval.web_retriever import retrieve_web_context, format_web_block
424
+
425
+ web_hits = retrieve_web_context(
426
+ query,
427
+ max_pages=getattr(cfg, "WEB_MAX_PAGES", 5),
428
+ local_top_score=None,
429
+ )
430
+ web_block = format_web_block(web_hits)
431
+ except Exception as e:
432
+ logger.warning("web retrieval skipped: %s", e)
433
+ timings["web_ms"] = (perf_counter() - t_web) * 1000
434
+
435
+ debug.final_chunk_count = 0
436
+ debug.total_tokens_used = int(len((web_block or "").encode("utf-8", errors="ignore")) / 3.8)
437
+ debug.timing_ms = timings
438
+
439
+ return RetrievalBundle(
440
+ results=[],
441
+ kg_context=KGContext("", [], 0, 0),
442
+ image_block="",
443
+ image_hits=[],
444
+ debug=debug,
445
+ web_hits=web_hits,
446
+ web_block=web_block,
447
+ )
448
+
449
+ row_meta: dict[str, tuple] = {}
450
+ for row in vector_rows + fts_rows:
451
+ cid = row[0]
452
+ if cid not in row_meta:
453
+ row_meta[cid] = row
454
+
455
+ vector_ranks = {row[0]: idx + 1 for idx, row in enumerate(vector_rows)}
456
+ fts_ranks = {row[0]: idx + 1 for idx, row in enumerate(fts_rows)}
457
+ merged = _rrf_merge(vector_ranks, fts_ranks)
458
+ debug.rrf_merged_count = len(merged)
459
+
460
+ child_to_parent = {row[0]: row[1] for row in (vector_rows + fts_rows)}
461
+ rrf_by_child = dict(merged)
462
+
463
+ rerank_take = min(len(merged), cfg.RERANKER_TOP_N)
464
+ pool_pairs = merged[:rerank_take]
465
+
466
+ # Prefetch parent content for richer rerank passages (section + parent + child)
467
+ pool_parent_ids = list(dict.fromkeys(
468
+ child_to_parent[cid] for cid, _ in pool_pairs if cid in child_to_parent
469
+ ))
470
+ parent_data_early = _fetch_parent_chunks(pool_parent_ids)
471
+
472
+ t_rr = perf_counter()
473
+ items = []
474
+ for child_id, _rrf_sc in pool_pairs:
475
+ rw = row_meta.get(child_id)
476
+ if not rw:
477
+ continue
478
+ pid = rw[1]
479
+ parent_text, _ptype = parent_data_early.get(pid, ("", "concept"))
480
+ meta = {
481
+ "file_path": rw[5] or "",
482
+ "product": rw[6] or "IGEL",
483
+ "section_title": rw[4] or "",
484
+ }
485
+ items.append(
486
+ RerankItem(
487
+ child_chunk_id=child_id,
488
+ passage=rw[3][:12000] if rw[3] else "",
489
+ parent_passage=parent_text[:8000] if parent_text else "",
490
+ section_title=rw[4] or "",
491
+ source_url=meta.get("file_path", ""),
492
+ metadata=meta,
493
+ )
494
+ )
495
+
496
+ ranked = rerank_passages(query, items)
497
+ score_map: dict[str, float] = {cid: sc for cid, sc in ranked}
498
+ timings["rerank_ms"] = (perf_counter() - t_rr) * 1000
499
+ debug.post_rerank_count = len(score_map)
500
+
501
+ merged_for_sort = sorted(
502
+ pool_pairs,
503
+ key=lambda p: (-score_map.get(p[0], -1e9), -(rrf_by_child.get(p[0], 0.0))),
504
+ )
505
+
506
+ merged_dedup = _deduplicate_by_parent(merged_for_sort, child_to_parent)
507
+ prefetch_k = max(top_k * 3, len(merged_dedup))
508
+ shortlist = merged_dedup[: min(prefetch_k, len(merged_dedup))]
509
+ parent_ids_short = [
510
+ child_to_parent[cid] for cid, _ in shortlist if cid in child_to_parent
511
+ ]
512
+ t_fetch = perf_counter()
513
+ parent_data = _fetch_parent_chunks(list(dict.fromkeys(parent_ids_short)))
514
+ timings["parents_ms"] = (perf_counter() - t_fetch) * 1000
515
+
516
+ traces: list[ChunkScoreTrace] = []
517
+ for cid, rrfs in merged_for_sort[:rerank_take]:
518
+ row = row_meta.get(cid)
519
+ if row is None:
520
+ continue
521
+ pid = row[1]
522
+ rs = score_map.get(cid)
523
+ traces.append(
524
+ ChunkScoreTrace(
525
+ child_chunk_id=cid,
526
+ parent_chunk_id=pid,
527
+ source_file=row[5] or "",
528
+ section_title=row[4] or "",
529
+ vector_rank=vector_ranks.get(cid),
530
+ fts_rank=fts_ranks.get(cid),
531
+ rrf_score=float(rrf_by_child.get(cid, rrfs)),
532
+ rerank_score=rs,
533
+ included=False,
534
+ exclusion_reason="",
535
+ )
536
+ )
537
+
538
+ refined = []
539
+ seen_parent_prefetch: set[str] = set()
540
+ for cid, rrfs in shortlist:
541
+ row = row_meta.get(cid)
542
+ if row is None:
543
+ continue
544
+ pid = row[1]
545
+ if pid in seen_parent_prefetch:
546
+ continue
547
+ seen_parent_prefetch.add(pid)
548
+ pc, ptype = parent_data.get(pid, ("", "concept"))
549
+ refined.append(
550
+ SearchResult(
551
+ child_chunk_id=cid,
552
+ parent_chunk_id=pid,
553
+ document_id=row[2],
554
+ child_content=row[3],
555
+ parent_content=pc,
556
+ section_title=row[4] or "",
557
+ file_name=row[5] or "",
558
+ product=row[6] or "IGEL",
559
+ chunk_type=ptype or "concept",
560
+ rrf_score=float(rrf_by_child.get(cid, rrfs)),
561
+ vector_rank=vector_ranks.get(cid),
562
+ fts_rank=fts_ranks.get(cid),
563
+ rerank_score=score_map.get(cid),
564
+ metadata={
565
+ "file_path": row[5] or "",
566
+ "product": row[6] or "IGEL",
567
+ "section_title": row[4] or "",
568
+ },
569
+ )
570
+ )
571
+
572
+ seed_parents = [r.parent_chunk_id for r in refined[:top_k]]
573
+ kg_ctx = KGContext("", [], 0, 0)
574
+ t_kg = perf_counter()
575
+ if cfg.KG_ENABLED:
576
+ kg_ctx = retrieve_kg_context(
577
+ query,
578
+ max_hops=cfg.KG_MAX_HOPS,
579
+ seed_parent_chunk_ids=seed_parents,
580
+ max_nodes=64,
581
+ )
582
+ timings["kg_ms"] = (perf_counter() - t_kg) * 1000
583
+ debug.kg_nodes_found = kg_ctx.node_count
584
+ debug.kg_edges_traversed = kg_ctx.edge_count
585
+
586
+ image_hits: list[ImageHit] = []
587
+ t_img = perf_counter()
588
+ if cfg.CONTEXT_INCLUDE_IMAGES:
589
+ try:
590
+ image_hits = retrieve_images(query, top_k=min(12, max(4, top_k)))
591
+ except Exception as e:
592
+ logger.warning("retrieve_images in v3 skipped: %s", e)
593
+ timings["images_ms"] = (perf_counter() - t_img) * 1000
594
+
595
+ web_hits = []
596
+ web_block = ""
597
+ t_web = perf_counter()
598
+ local_top_score = None
599
+ if score_map:
600
+ sorted_scores = sorted((float(s) for s in score_map.values()), reverse=True)
601
+ if len(sorted_scores) == 1:
602
+ local_top_score = sorted_scores[0]
603
+ else:
604
+ # Penalize uncertain rankings where top and runner-up are very close.
605
+ margin = max(0.0, sorted_scores[0] - sorted_scores[1])
606
+ local_top_score = min(1.0, (0.6 * sorted_scores[0]) + (0.4 * margin))
607
+ if getattr(cfg, "WEB_RETRIEVAL_ENABLED", False):
608
+ try:
609
+ from knowledge_base.retrieval.web_retriever import retrieve_web_context, format_web_block
610
+
611
+ web_hits = retrieve_web_context(
612
+ query,
613
+ max_pages=getattr(cfg, "WEB_MAX_PAGES", 5),
614
+ local_top_score=local_top_score,
615
+ )
616
+ web_block = format_web_block(web_hits)
617
+ except Exception as e:
618
+ logger.warning("web retrieval skipped: %s", e)
619
+ timings["web_ms"] = (perf_counter() - t_web) * 1000
620
+
621
+ fused, kg_trim, img_block, web_trim, rag_toks, precedence_attr, precedence_conflicts = fuse_retrieval_context(
622
+ refined,
623
+ kg_ctx.text_block,
624
+ image_hits,
625
+ token_budget=int(cfg.CONTEXT_TOKEN_BUDGET),
626
+ reserve_prompt=int(cfg.CONTEXT_RESERVE_PROMPT),
627
+ kg_ratio=float(cfg.CONTEXT_KG_BUDGET_RATIO),
628
+ image_ratio=float(cfg.CONTEXT_IMAGE_BUDGET_RATIO),
629
+ min_score=cfg.CONTEXT_MIN_SCORE,
630
+ max_chunks=int(cfg.CONTEXT_MAX_CHUNKS),
631
+ web_block=web_block,
632
+ web_ratio=float(getattr(cfg, "WEB_BUDGET_RATIO", 0.12)),
633
+ )
634
+ web_block = web_trim
635
+
636
+ fused_parent_ids = {r.parent_chunk_id for r in fused}
637
+ min_sc = cfg.CONTEXT_MIN_SCORE
638
+
639
+ traces_out: list[ChunkScoreTrace] = []
640
+ trace_by_child: dict[str, ChunkScoreTrace] = {}
641
+ for tr in traces:
642
+ trace_by_child.setdefault(tr.child_chunk_id, tr)
643
+
644
+ merged_ids_in_order = [p[0] for p in merged_for_sort]
645
+ for cid in merged_ids_in_order:
646
+ base = trace_by_child.get(cid)
647
+ if base is None:
648
+ continue
649
+ incl = base.parent_chunk_id in fused_parent_ids
650
+ if incl:
651
+ why = ""
652
+ elif min_sc is not None and min_sc > 0 and base.rerank_score is not None:
653
+ why = "below_threshold" if base.rerank_score < min_sc else "budget_exceeded"
654
+ else:
655
+ why = "budget_exceeded"
656
+
657
+ traces_out.append(
658
+ ChunkScoreTrace(
659
+ child_chunk_id=base.child_chunk_id,
660
+ parent_chunk_id=base.parent_chunk_id,
661
+ source_file=base.source_file,
662
+ section_title=base.section_title,
663
+ vector_rank=base.vector_rank,
664
+ fts_rank=base.fts_rank,
665
+ rrf_score=base.rrf_score,
666
+ rerank_score=base.rerank_score,
667
+ included=incl,
668
+ exclusion_reason=why if not incl else "",
669
+ )
670
+ )
671
+ if len(traces_out) >= 96:
672
+ break
673
+
674
+ kg_ctx_out = KGContext(
675
+ text_block=kg_trim,
676
+ node_ids=kg_ctx.node_ids,
677
+ node_count=kg_ctx.node_count,
678
+ edge_count=kg_ctx.edge_count,
679
+ )
680
+
681
+ est_total_tokens = int(
682
+ rag_toks
683
+ + len((kg_trim or "").encode("utf-8", errors="ignore")) / 3.8
684
+ + len((img_block or "").encode("utf-8", errors="ignore")) / 3.8
685
+ + len((web_block or "").encode("utf-8", errors="ignore")) / 3.8
686
+ )
687
+
688
+ debug.final_chunk_count = len(fused)
689
+ debug.total_tokens_used = max(est_total_tokens, 1)
690
+ debug.traces = traces_out
691
+ debug.timing_ms = timings
692
+
693
+ logger.info(
694
+ "retrieve_v3('%s') → fused=%d kg_nodes=%d rerank=%s",
695
+ query[:60],
696
+ len(fused),
697
+ kg_ctx.node_count,
698
+ debug.reranker_applied,
699
+ )
700
+
701
+ return RetrievalBundle(
702
+ results=fused,
703
+ kg_context=kg_ctx_out,
704
+ image_block=img_block,
705
+ image_hits=image_hits,
706
+ debug=debug,
707
+ precedence_attribution=precedence_attr,
708
+ precedence_conflicts=precedence_conflicts,
709
+ web_hits=web_hits,
710
+ web_block=web_block,
711
+ )
712
+
713
+
714
+ # ── Image retrieval ───────────────────────────────────────────────────────────
715
+
716
+ @dataclass
717
+ class ImageHit:
718
+ image_id: str
719
+ document_id: str
720
+ file_name: str
721
+ page_number: int | None
722
+ section_title: str
723
+ image_type: str
724
+ verbatim_text: str
725
+ semantic_description: str
726
+ test_relevance: str
727
+ score: float # cosine similarity (1 - distance)
728
+ fts_rank: int | None = None
729
+ parent_chunk_id: str | None = None
730
+ image_format: str = "png"
731
+ width: int | None = None
732
+ height: int | None = None
733
+ is_recurring: bool = False
734
+
735
+
736
+ def retrieve_images(
737
+ query: str,
738
+ top_k: int = 8,
739
+ document_id: str | None = None,
740
+ exclude_recurring: bool = True,
741
+ ) -> list[ImageHit]:
742
+ """Hybrid (vector + FTS) search over kb_images.
743
+
744
+ Returns image hits with FK to document + nearest section so the caller
745
+ knows exactly which doc/page each image is from.
746
+ """
747
+ if not query.strip():
748
+ return []
749
+ _ensure_pool()
750
+
751
+ candidate_limit = top_k * 3
752
+ query_vec = _embed_query(query)
753
+ vec_available = any(v != 0.0 for v in query_vec)
754
+
755
+ vector_rows: list[tuple] = []
756
+ fts_rows: list[tuple] = []
757
+
758
+ base_select = """
759
+ SELECT
760
+ i.id::text,
761
+ i.document_id::text,
762
+ d.file_name,
763
+ i.page_number,
764
+ COALESCE(p.section_title, '') AS section_title,
765
+ i.image_type,
766
+ COALESCE(i.verbatim_text, '') AS verbatim_text,
767
+ COALESCE(i.semantic_description, '') AS semantic_description,
768
+ COALESCE(i.test_relevance, '') AS test_relevance,
769
+ i.image_format,
770
+ i.width,
771
+ i.height,
772
+ i.parent_chunk_id::text,
773
+ COALESCE((i.metadata->>'is_recurring')::boolean, FALSE) AS is_recurring
774
+ """
775
+ where = """
776
+ WHERE i.description_embedding IS NOT NULL
777
+ AND (%s IS NULL OR i.document_id = %s::uuid)
778
+ AND (NOT %s OR COALESCE((i.metadata->>'is_recurring')::boolean, FALSE) = FALSE)
779
+ """
780
+
781
+ if vec_available:
782
+ try:
783
+ vec_literal = format_for_pgvector(query_vec)
784
+ with get_conn() as conn:
785
+ with conn.cursor() as cur:
786
+ sql = f"""
787
+ {base_select},
788
+ 1 - (i.description_embedding <=> %s::vector) AS score
789
+ FROM kb_images i
790
+ JOIN kb_documents d ON d.id = i.document_id
791
+ LEFT JOIN kb_parent_chunks p ON p.id = i.parent_chunk_id
792
+ {where}
793
+ ORDER BY i.description_embedding <=> %s::vector
794
+ LIMIT %s
795
+ """
796
+ cur.execute(sql, (
797
+ vec_literal,
798
+ document_id, document_id,
799
+ exclude_recurring,
800
+ vec_literal, candidate_limit,
801
+ ))
802
+ vector_rows = cur.fetchall()
803
+ except Exception as e:
804
+ logger.error("Image vector search failed: %s", e)
805
+
806
+ try:
807
+ with get_conn() as conn:
808
+ with conn.cursor() as cur:
809
+ sql = f"""
810
+ {base_select},
811
+ ts_rank_cd(
812
+ to_tsvector('english',
813
+ coalesce(i.verbatim_text,'') || ' ' ||
814
+ coalesce(i.semantic_description,'') || ' ' ||
815
+ coalesce(i.surrounding_text,'')),
816
+ plainto_tsquery('english', %s)
817
+ ) AS score
818
+ FROM kb_images i
819
+ JOIN kb_documents d ON d.id = i.document_id
820
+ LEFT JOIN kb_parent_chunks p ON p.id = i.parent_chunk_id
821
+ {where}
822
+ AND to_tsvector('english',
823
+ coalesce(i.verbatim_text,'') || ' ' ||
824
+ coalesce(i.semantic_description,'') || ' ' ||
825
+ coalesce(i.surrounding_text,''))
826
+ @@ plainto_tsquery('english', %s)
827
+ ORDER BY score DESC
828
+ LIMIT %s
829
+ """
830
+ cur.execute(sql, (
831
+ query,
832
+ document_id, document_id,
833
+ exclude_recurring,
834
+ query, candidate_limit,
835
+ ))
836
+ fts_rows = cur.fetchall()
837
+ except Exception as e:
838
+ logger.error("Image FTS search failed: %s", e)
839
+
840
+ # RRF merge of image hits (similar pattern to text)
841
+ vec_ranks = {r[0]: idx + 1 for idx, r in enumerate(vector_rows)}
842
+ fts_ranks_map = {r[0]: idx + 1 for idx, r in enumerate(fts_rows)}
843
+ all_ids = set(vec_ranks) | set(fts_ranks_map)
844
+ rrf_k = 60
845
+ scored = sorted(
846
+ ((iid, 1.0 / (vec_ranks.get(iid, 9999) + rrf_k) +
847
+ 1.0 / (fts_ranks_map.get(iid, 9999) + rrf_k))
848
+ for iid in all_ids),
849
+ key=lambda kv: kv[1], reverse=True,
850
+ )
851
+
852
+ row_meta: dict[str, tuple] = {}
853
+ for r in vector_rows + fts_rows:
854
+ row_meta.setdefault(r[0], r)
855
+
856
+ hits: list[ImageHit] = []
857
+ for iid, rrf_score in scored[:top_k]:
858
+ r = row_meta.get(iid)
859
+ if not r:
860
+ continue
861
+ hits.append(ImageHit(
862
+ image_id=r[0], document_id=r[1], file_name=r[2],
863
+ page_number=r[3], section_title=r[4],
864
+ image_type=r[5] or "unknown",
865
+ verbatim_text=r[6], semantic_description=r[7], test_relevance=r[8],
866
+ image_format=r[9] or "png", width=r[10], height=r[11],
867
+ parent_chunk_id=r[12], is_recurring=bool(r[13]),
868
+ score=rrf_score, fts_rank=fts_ranks_map.get(iid),
869
+ ))
870
+ return hits
871
+
872
+
873
+ def retrieve_image_bytes(image_id: str) -> tuple[bytes, str] | None:
874
+ """Fetch the raw image binary + format for a given image_id (e.g. for display)."""
875
+ _ensure_pool()
876
+ with get_conn() as conn:
877
+ with conn.cursor() as cur:
878
+ cur.execute(
879
+ "SELECT image_bytes, image_format FROM kb_images WHERE id = %s::uuid",
880
+ (image_id,),
881
+ )
882
+ row = cur.fetchone()
883
+ if not row:
884
+ return None
885
+ return bytes(row[0]), row[1]