devcouncil 0.3.1 → 0.4.1

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 (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,290 @@
1
+ """Semantic cache with FAISS backend, TTL, LRU eviction, and multi-gate validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import threading
8
+ import time
9
+ import uuid
10
+ from collections import OrderedDict
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Protocol
13
+
14
+ import faiss
15
+ import numpy as np
16
+ from numpy.typing import NDArray
17
+
18
+ from .config import CacheConfig
19
+ from .embeddings import EmbeddingService, FloatVector
20
+
21
+ FloatMatrix = NDArray[np.float32]
22
+
23
+
24
+ class EmbedderProtocol(Protocol):
25
+ def embed_one(self, text: str) -> FloatVector: ...
26
+
27
+
28
+ @dataclass
29
+ class CacheEntry:
30
+ entry_id: str
31
+ query_text: str
32
+ query_hash: str
33
+ response_text: str
34
+ embedding: FloatVector
35
+ namespace: str
36
+ intent: str
37
+ created_at: float
38
+ last_accessed: float
39
+ access_count: int = 0
40
+ metadata: dict[str, Any] = field(default_factory=dict)
41
+
42
+ def is_expired(self, ttl_seconds: int, now: float | None = None) -> bool:
43
+ now = now or time.time()
44
+ return (now - self.created_at) > ttl_seconds
45
+
46
+
47
+ @dataclass
48
+ class CacheLookupResult:
49
+ hit: bool
50
+ response: str | None = None
51
+ similarity: float = 0.0
52
+ entry_id: str | None = None
53
+ reason: str = "miss"
54
+
55
+
56
+ class SemanticCache:
57
+ """
58
+ FAISS IndexFlatIP + LRU metadata store.
59
+ IndexFlatIP with normalized vectors == cosine similarity.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ config: CacheConfig | None = None,
65
+ embedder: EmbedderProtocol | None = None,
66
+ dimension: int = 384,
67
+ ) -> None:
68
+ self.config = config or CacheConfig()
69
+ self.embedder = embedder or EmbeddingService.get_instance()
70
+ self.dimension = dimension
71
+ self._index: faiss.Index = faiss.IndexFlatIP(dimension)
72
+ self._entries: OrderedDict[str, CacheEntry] = OrderedDict()
73
+ self._id_to_faiss_row: dict[str, int] = {}
74
+ self._faiss_row_to_id: dict[int, str] = {}
75
+ self._lock = threading.RLock()
76
+ self._stats = {"hits": 0, "misses": 0, "evictions": 0, "false_positive_forced": 0}
77
+
78
+ @staticmethod
79
+ def _normalize_query(text: str) -> str:
80
+ return " ".join(text.lower().split())
81
+
82
+ @staticmethod
83
+ def _hash_query(text: str) -> str:
84
+ normalized = SemanticCache._normalize_query(text)
85
+ return hashlib.sha256(normalized.encode()).hexdigest()
86
+
87
+ def _evict_lru(self) -> None:
88
+ """Remove oldest-accessed entry when at capacity."""
89
+ if len(self._entries) < self.config.max_entries:
90
+ return
91
+ self._entries.popitem(last=False)
92
+ self._stats["evictions"] += 1
93
+ # Note: FAISS IndexFlatIP does not support deletion;
94
+ # stale rows are ignored during search (production: use IndexIDMap2)
95
+
96
+ def _search_faiss(
97
+ self, query_vec: FloatVector, k: int = 5
98
+ ) -> list[tuple[str, float]]:
99
+ if self._index.ntotal == 0:
100
+ return []
101
+
102
+ q = query_vec.reshape(1, -1).astype(np.float32)
103
+ scores, indices = self._index.search(q, min(k, self._index.ntotal))
104
+ results: list[tuple[str, float]] = []
105
+ now = time.time()
106
+
107
+ for score, idx in zip(scores[0], indices[0]):
108
+ if idx < 0:
109
+ continue
110
+ entry_id = self._faiss_row_to_id.get(int(idx))
111
+ if entry_id is None:
112
+ continue
113
+ entry = self._entries.get(entry_id)
114
+ if entry is None:
115
+ continue
116
+ if entry.is_expired(self.config.ttl_seconds, now):
117
+ continue
118
+ if entry.namespace != self.config.namespace:
119
+ continue
120
+ results.append((entry_id, float(score)))
121
+ return results
122
+
123
+ def lookup(
124
+ self,
125
+ query: str,
126
+ query_embedding: FloatVector | None = None,
127
+ intent: str | None = None,
128
+ force_miss: bool = False,
129
+ ) -> CacheLookupResult:
130
+ with self._lock:
131
+ query_hash = self._hash_query(query)
132
+
133
+ # Exact hash short-circuit
134
+ for entry in self._entries.values():
135
+ if (
136
+ entry.query_hash == query_hash
137
+ and entry.namespace == self.config.namespace
138
+ and not entry.is_expired(self.config.ttl_seconds)
139
+ ):
140
+ if intent is not None and entry.intent != intent:
141
+ self._stats["misses"] += 1
142
+ return CacheLookupResult(hit=False, similarity=1.0, reason="intent_mismatch")
143
+ entry.last_accessed = time.time()
144
+ entry.access_count += 1
145
+ self._entries.move_to_end(entry.entry_id)
146
+ self._stats["hits"] += 1
147
+ return CacheLookupResult(
148
+ hit=True,
149
+ response=entry.response_text,
150
+ similarity=1.0,
151
+ entry_id=entry.entry_id,
152
+ reason="exact_hash",
153
+ )
154
+
155
+ if force_miss:
156
+ self._stats["misses"] += 1
157
+ return CacheLookupResult(hit=False, reason="forced_exploration")
158
+
159
+ vec = query_embedding if query_embedding is not None else self.embedder.embed_one(query)
160
+ candidates = self._search_faiss(vec, k=5)
161
+
162
+ if not candidates:
163
+ self._stats["misses"] += 1
164
+ return CacheLookupResult(hit=False, reason="empty_index")
165
+
166
+ best_id, best_sim = candidates[0]
167
+
168
+ # OOD gate
169
+ if best_sim < self.config.ood_threshold:
170
+ self._stats["misses"] += 1
171
+ return CacheLookupResult(hit=False, similarity=best_sim, reason="ood")
172
+
173
+ # Similarity gate
174
+ if best_sim < self.config.similarity_threshold:
175
+ self._stats["misses"] += 1
176
+ return CacheLookupResult(hit=False, similarity=best_sim, reason="below_threshold")
177
+
178
+ # Margin gate (ambiguous neighborhood)
179
+ if len(candidates) >= 2:
180
+ second_sim = candidates[1][1]
181
+ if (best_sim - second_sim) < self.config.margin_threshold:
182
+ self._stats["misses"] += 1
183
+ return CacheLookupResult(
184
+ hit=False,
185
+ similarity=best_sim,
186
+ reason="insufficient_margin",
187
+ )
188
+
189
+ entry = self._entries[best_id]
190
+
191
+ # Intent consistency gate
192
+ if intent is not None and entry.intent != intent:
193
+ self._stats["misses"] += 1
194
+ return CacheLookupResult(hit=False, similarity=best_sim, reason="intent_mismatch")
195
+
196
+ entry.last_accessed = time.time()
197
+ entry.access_count += 1
198
+ self._entries.move_to_end(entry.entry_id)
199
+ self._stats["hits"] += 1
200
+ return CacheLookupResult(
201
+ hit=True,
202
+ response=entry.response_text,
203
+ similarity=best_sim,
204
+ entry_id=entry.entry_id,
205
+ reason="semantic_hit",
206
+ )
207
+
208
+ def put(
209
+ self,
210
+ query: str,
211
+ response: str,
212
+ query_embedding: FloatVector | None = None,
213
+ intent: str = "general",
214
+ metadata: dict[str, Any] | None = None,
215
+ ) -> str:
216
+ with self._lock:
217
+ self._evict_lru()
218
+ vec = query_embedding if query_embedding is not None else self.embedder.embed_one(query)
219
+ entry_id = str(uuid.uuid4())
220
+ now = time.time()
221
+ entry = CacheEntry(
222
+ entry_id=entry_id,
223
+ query_text=query,
224
+ query_hash=self._hash_query(query),
225
+ response_text=response,
226
+ embedding=vec,
227
+ namespace=self.config.namespace,
228
+ intent=intent,
229
+ created_at=now,
230
+ last_accessed=now,
231
+ metadata=metadata or {},
232
+ )
233
+ row = self._index.ntotal
234
+ self._index.add(vec.reshape(1, -1).astype(np.float32))
235
+ self._id_to_faiss_row[entry_id] = row
236
+ self._faiss_row_to_id[row] = entry_id
237
+ self._entries[entry_id] = entry
238
+ return entry_id
239
+
240
+ @property
241
+ def hit_rate(self) -> float:
242
+ total = self._stats["hits"] + self._stats["misses"]
243
+ return self._stats["hits"] / total if total else 0.0
244
+
245
+ def stats(self) -> dict[str, Any]:
246
+ return {**self._stats, "hit_rate": self.hit_rate, "size": len(self._entries)}
247
+
248
+ def persist(self, path: str) -> None:
249
+ with self._lock:
250
+ faiss.write_index(self._index, f"{path}.faiss")
251
+ serializable = {
252
+ eid: {
253
+ "query_text": e.query_text,
254
+ "query_hash": e.query_hash,
255
+ "response_text": e.response_text,
256
+ "embedding": e.embedding.tolist(),
257
+ "namespace": e.namespace,
258
+ "intent": e.intent,
259
+ "created_at": e.created_at,
260
+ "last_accessed": e.last_accessed,
261
+ "access_count": e.access_count,
262
+ "metadata": e.metadata,
263
+ }
264
+ for eid, e in self._entries.items()
265
+ }
266
+ with open(f"{path}.json", "w", encoding="utf-8") as f:
267
+ json.dump({"entries": serializable, "row_map": self._faiss_row_to_id}, f)
268
+
269
+ def load(self, path: str) -> None:
270
+ with self._lock:
271
+ self._index = faiss.read_index(f"{path}.faiss")
272
+ with open(f"{path}.json", encoding="utf-8") as f:
273
+ data = json.load(f)
274
+ self._entries.clear()
275
+ self._faiss_row_to_id = {int(k): v for k, v in data["row_map"].items()}
276
+ self._id_to_faiss_row = {v: int(k) for k, v in self._faiss_row_to_id.items()}
277
+ for eid, raw in data["entries"].items():
278
+ self._entries[eid] = CacheEntry(
279
+ entry_id=eid,
280
+ query_text=raw["query_text"],
281
+ query_hash=raw["query_hash"],
282
+ response_text=raw["response_text"],
283
+ embedding=np.array(raw["embedding"], dtype=np.float32),
284
+ namespace=raw["namespace"],
285
+ intent=raw["intent"],
286
+ created_at=raw["created_at"],
287
+ last_accessed=raw["last_accessed"],
288
+ access_count=raw["access_count"],
289
+ metadata=raw.get("metadata", {}),
290
+ )
@@ -0,0 +1,137 @@
1
+ """Semantic RAG context compression with MMR diversification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+ from .config import CompressorConfig
11
+ from .embeddings import EmbeddingService, FloatVector
12
+
13
+
14
+ @dataclass
15
+ class CompressedContext:
16
+ text: str
17
+ chunks_used: int
18
+ chunks_total: int
19
+ estimated_tokens: int
20
+ scores: list[float]
21
+
22
+
23
+ class SemanticCompressor:
24
+ def __init__(
25
+ self,
26
+ config: CompressorConfig | None = None,
27
+ embedder: EmbeddingService | None = None,
28
+ ) -> None:
29
+ self.config = config or CompressorConfig()
30
+ self.embedder = embedder or EmbeddingService.get_instance()
31
+
32
+ @staticmethod
33
+ def _estimate_tokens(text: str) -> int:
34
+ # Rough heuristic: ~4 chars per token for English
35
+ return max(1, len(text) // 4)
36
+
37
+ def chunk_document(self, document: str) -> list[str]:
38
+ """Split document into overlapping word-based chunks."""
39
+ words = document.split()
40
+ chunk_words = self.config.chunk_token_size # treating as word proxy
41
+ overlap = self.config.chunk_overlap
42
+ if not words:
43
+ return []
44
+
45
+ chunks: list[str] = []
46
+ start = 0
47
+ while start < len(words):
48
+ end = min(len(words), start + chunk_words)
49
+ chunks.append(" ".join(words[start:end]))
50
+ if end >= len(words):
51
+ break
52
+ start = max(start + 1, end - overlap)
53
+ return chunks
54
+
55
+ def _mmr_select(
56
+ self,
57
+ query_vec: FloatVector,
58
+ chunk_vecs: NDArray[np.float32],
59
+ chunk_texts: list[str],
60
+ token_budget: int,
61
+ ) -> tuple[list[int], list[float]]:
62
+ """Maximal Marginal Relevance selection under token budget."""
63
+ n = len(chunk_texts)
64
+ if n == 0:
65
+ return [], []
66
+
67
+ relevance = chunk_vecs @ query_vec
68
+ selected: list[int] = []
69
+ scores: list[float] = []
70
+ tokens_used = 0
71
+ lam = self.config.mmr_lambda
72
+
73
+ candidate_mask = relevance >= self.config.min_chunk_score
74
+ candidates = [i for i in range(n) if candidate_mask[i]]
75
+
76
+ while candidates and len(selected) < self.config.top_k:
77
+ best_idx = -1
78
+ best_mmr = -float("inf")
79
+
80
+ for i in candidates:
81
+ chunk_tokens = self._estimate_tokens(chunk_texts[i])
82
+ if tokens_used + chunk_tokens > token_budget and selected:
83
+ continue
84
+
85
+ rel = float(relevance[i])
86
+ redundancy = 0.0
87
+ if selected:
88
+ redundancy = max(
89
+ float(chunk_vecs[i] @ chunk_vecs[j]) for j in selected
90
+ )
91
+ mmr = lam * rel - (1.0 - lam) * redundancy
92
+ if mmr > best_mmr:
93
+ best_mmr = mmr
94
+ best_idx = i
95
+
96
+ if best_idx < 0:
97
+ break
98
+
99
+ selected.append(best_idx)
100
+ scores.append(float(relevance[best_idx]))
101
+ tokens_used += self._estimate_tokens(chunk_texts[best_idx])
102
+ candidates.remove(best_idx)
103
+
104
+ if tokens_used >= token_budget:
105
+ break
106
+
107
+ return selected, scores
108
+
109
+ def compress(
110
+ self,
111
+ query: str,
112
+ documents: list[str],
113
+ query_embedding: FloatVector | None = None,
114
+ token_budget: int | None = None,
115
+ ) -> CompressedContext:
116
+ budget = token_budget or self.config.token_budget
117
+ all_chunks: list[str] = []
118
+ for doc in documents:
119
+ all_chunks.extend(self.chunk_document(doc))
120
+
121
+ if not all_chunks:
122
+ return CompressedContext(text="", chunks_used=0, chunks_total=0, estimated_tokens=0, scores=[])
123
+
124
+ q_vec = query_embedding if query_embedding is not None else self.embedder.embed_one(query)
125
+ chunk_vecs = self.embedder.embed(all_chunks)
126
+
127
+ selected_indices, scores = self._mmr_select(q_vec, chunk_vecs, all_chunks, budget)
128
+ selected_chunks = [all_chunks[i] for i in selected_indices]
129
+ combined = "\n\n---\n\n".join(selected_chunks)
130
+
131
+ return CompressedContext(
132
+ text=combined,
133
+ chunks_used=len(selected_chunks),
134
+ chunks_total=len(all_chunks),
135
+ estimated_tokens=self._estimate_tokens(combined),
136
+ scores=scores,
137
+ )
@@ -0,0 +1,75 @@
1
+ """Central configuration for the semantic layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Literal
8
+
9
+
10
+ class ModelTier(str, Enum):
11
+ SMALL = "small" # 1B-3B parameters
12
+ LARGE = "large" # 8B-70B parameters
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class EmbeddingConfig:
17
+ model_name: str = "sentence-transformers/all-MiniLM-L6-v2"
18
+ dimension: int = 384
19
+ device: Literal["cpu", "cuda"] = "cpu" # CPU avoids VRAM contention with LLM
20
+ batch_size: int = 32
21
+ normalize: bool = True
22
+
23
+
24
+ @dataclass
25
+ class CacheConfig:
26
+ backend: Literal["faiss", "chroma"] = "faiss"
27
+ similarity_threshold: float = 0.92
28
+ ood_threshold: float = 0.75
29
+ margin_threshold: float = 0.03
30
+ ttl_seconds: int = 3600
31
+ max_entries: int = 10_000
32
+ namespace: str = "default"
33
+ exploration_rate: float = 0.02 # force miss for FPR calibration
34
+
35
+
36
+ @dataclass
37
+ class RouterConfig:
38
+ complexity_threshold: float = 0.45
39
+ small_model: str = "qwen2.5:1.5b"
40
+ large_model: str = "llama3.1:8b"
41
+ weights: dict[str, float] = field(
42
+ default_factory=lambda: {
43
+ "length": 0.25,
44
+ "structure": 0.30,
45
+ "embed_disp": 0.25,
46
+ "domain": 0.20,
47
+ }
48
+ )
49
+
50
+
51
+ @dataclass
52
+ class CompressorConfig:
53
+ token_budget: int = 2048
54
+ top_k: int = 8
55
+ chunk_token_size: int = 256
56
+ chunk_overlap: int = 32
57
+ min_chunk_score: float = 0.35
58
+ mmr_lambda: float = 0.7
59
+
60
+
61
+ @dataclass
62
+ class LLMConfig:
63
+ backend: Literal["ollama", "llama_cpp", "hf"] = "ollama"
64
+ base_url: str = "http://localhost:11434"
65
+ timeout_seconds: float = 120.0
66
+
67
+
68
+ @dataclass
69
+ class SemanticLayerConfig:
70
+ embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
71
+ cache: CacheConfig = field(default_factory=CacheConfig)
72
+ router: RouterConfig = field(default_factory=RouterConfig)
73
+ compressor: CompressorConfig = field(default_factory=CompressorConfig)
74
+ llm: LLMConfig = field(default_factory=LLMConfig)
75
+ latency_budget_ms: float = 15.0
@@ -0,0 +1,69 @@
1
+ """Shared embedding service — singleton, low-latency, thread-safe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from typing import Sequence, cast
8
+
9
+ import numpy as np
10
+ from numpy.typing import NDArray
11
+
12
+ from .config import EmbeddingConfig
13
+
14
+ FloatVector = NDArray[np.float32]
15
+ FloatMatrix = NDArray[np.float32]
16
+
17
+
18
+ class EmbeddingService:
19
+ """Lazy-loaded sentence-transformers embedder with L2 normalization."""
20
+
21
+ _instance: EmbeddingService | None = None
22
+ _lock = threading.Lock()
23
+
24
+ def __init__(self, config: EmbeddingConfig | None = None) -> None:
25
+ self.config = config or EmbeddingConfig()
26
+ self._model = None
27
+ self._model_lock = threading.Lock()
28
+
29
+ @classmethod
30
+ def get_instance(cls, config: EmbeddingConfig | None = None) -> EmbeddingService:
31
+ with cls._lock:
32
+ if cls._instance is None:
33
+ cls._instance = cls(config)
34
+ return cls._instance
35
+
36
+ def _load_model(self) -> None:
37
+ if self._model is not None:
38
+ return
39
+ with self._model_lock:
40
+ if self._model is not None:
41
+ return
42
+ from sentence_transformers import SentenceTransformer
43
+
44
+ self._model = SentenceTransformer(
45
+ self.config.model_name,
46
+ device=self.config.device,
47
+ )
48
+
49
+ def embed(self, texts: Sequence[str]) -> FloatMatrix:
50
+ """Embed a batch of texts. Returns (N, dim) float32 array."""
51
+ self._load_model()
52
+ assert self._model is not None
53
+
54
+ t0 = time.perf_counter()
55
+ vectors: FloatMatrix = self._model.encode(
56
+ list(texts),
57
+ batch_size=self.config.batch_size,
58
+ convert_to_numpy=True,
59
+ normalize_embeddings=self.config.normalize,
60
+ show_progress_bar=False,
61
+ ).astype(np.float32)
62
+ elapsed_ms = (time.perf_counter() - t0) * 1000
63
+ if elapsed_ms > 10:
64
+ # Log in production; kept silent here for brevity
65
+ pass
66
+ return vectors
67
+
68
+ def embed_one(self, text: str) -> FloatVector:
69
+ return cast(FloatVector, self.embed([text])[0])
@@ -0,0 +1,99 @@
1
+ """Unified adapter for Ollama, llama.cpp, and Hugging Face backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ from typing import Any, cast
7
+
8
+ import httpx
9
+
10
+ from .config import LLMConfig
11
+
12
+
13
+ class LLMBackend(abc.ABC):
14
+ @abc.abstractmethod
15
+ def generate(self, model: str, prompt: str, system: str | None = None) -> str:
16
+ ...
17
+
18
+ def close(self) -> None:
19
+ """Release backend resources if any."""
20
+
21
+
22
+ class OllamaBackend(LLMBackend):
23
+ def __init__(self, config: LLMConfig | None = None) -> None:
24
+ self.config = config or LLMConfig()
25
+ self._client = httpx.Client(base_url=self.config.base_url, timeout=self.config.timeout_seconds)
26
+
27
+ def generate(self, model: str, prompt: str, system: str | None = None) -> str:
28
+ payload: dict[str, Any] = {
29
+ "model": model,
30
+ "prompt": prompt,
31
+ "stream": False,
32
+ }
33
+ if system:
34
+ payload["system"] = system
35
+ resp = self._client.post("/api/generate", json=payload)
36
+ resp.raise_for_status()
37
+ return cast(str, resp.json()["response"])
38
+
39
+ def close(self) -> None:
40
+ self._client.close()
41
+
42
+
43
+ class LlamaCppBackend(LLMBackend):
44
+ """OpenAI-compatible llama.cpp server (/v1/chat/completions)."""
45
+
46
+ def __init__(self, config: LLMConfig | None = None) -> None:
47
+ self.config = config or LLMConfig(base_url="http://localhost:8080")
48
+ self._client = httpx.Client(base_url=self.config.base_url, timeout=self.config.timeout_seconds)
49
+
50
+ def generate(self, model: str, prompt: str, system: str | None = None) -> str:
51
+ messages = []
52
+ if system:
53
+ messages.append({"role": "system", "content": system})
54
+ messages.append({"role": "user", "content": prompt})
55
+ resp = self._client.post(
56
+ "/v1/chat/completions",
57
+ json={"model": model, "messages": messages, "stream": False},
58
+ )
59
+ resp.raise_for_status()
60
+ return cast(str, resp.json()["choices"][0]["message"]["content"])
61
+
62
+ def close(self) -> None:
63
+ self._client.close()
64
+
65
+
66
+ class HuggingFaceBackend(LLMBackend):
67
+ """Local HF pipeline — lazy load to avoid cold-start in semantic layer process."""
68
+
69
+ def __init__(self, config: LLMConfig | None = None) -> None:
70
+ self.config = config or LLMConfig()
71
+ self._pipelines: dict[str, Any] = {}
72
+
73
+ def _get_pipeline(self, model: str) -> Any:
74
+ if model not in self._pipelines:
75
+ from transformers import pipeline
76
+
77
+ self._pipelines[model] = pipeline(
78
+ "text-generation",
79
+ model=model,
80
+ device_map="auto",
81
+ )
82
+ return self._pipelines[model]
83
+
84
+ def generate(self, model: str, prompt: str, system: str | None = None) -> str:
85
+ pipe = self._get_pipeline(model)
86
+ full_prompt = f"{system}\n\n{prompt}" if system else prompt
87
+ out = pipe(full_prompt, max_new_tokens=512, do_sample=False)
88
+ return cast(str, out[0]["generated_text"])[len(full_prompt):]
89
+
90
+
91
+ def create_backend(config: LLMConfig | None = None) -> LLMBackend:
92
+ cfg = config or LLMConfig()
93
+ if cfg.backend == "ollama":
94
+ return OllamaBackend(cfg)
95
+ if cfg.backend == "llama_cpp":
96
+ return LlamaCppBackend(cfg)
97
+ if cfg.backend == "hf":
98
+ return HuggingFaceBackend(cfg)
99
+ raise ValueError(f"Unknown backend: {cfg.backend}")