homegraph 1.1.2 → 1.2.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 (361) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +268 -243
  3. package/dist/arkts/ohos-api-index.d.ts +15 -0
  4. package/dist/arkts/ohos-api-index.d.ts.map +1 -0
  5. package/dist/arkts/ohos-api-index.js +190 -0
  6. package/dist/arkts/ohos-api-index.js.map +1 -0
  7. package/dist/arkts/ohos-sdk-input.d.ts +36 -0
  8. package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
  9. package/dist/arkts/ohos-sdk-input.js +214 -0
  10. package/dist/arkts/ohos-sdk-input.js.map +1 -0
  11. package/dist/bin/command-supervision.d.ts +12 -0
  12. package/dist/bin/command-supervision.d.ts.map +1 -0
  13. package/dist/bin/command-supervision.js +86 -0
  14. package/dist/bin/command-supervision.js.map +1 -0
  15. package/dist/bin/homegraph.d.ts +1 -1
  16. package/dist/bin/homegraph.js +971 -71
  17. package/dist/bin/homegraph.js.map +1 -1
  18. package/dist/db/index.d.ts +20 -0
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +39 -0
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/migrations.d.ts +1 -1
  23. package/dist/db/migrations.d.ts.map +1 -1
  24. package/dist/db/migrations.js +58 -19
  25. package/dist/db/migrations.js.map +1 -1
  26. package/dist/db/queries.d.ts +14 -0
  27. package/dist/db/queries.d.ts.map +1 -1
  28. package/dist/db/queries.js +314 -143
  29. package/dist/db/queries.js.map +1 -1
  30. package/dist/db/schema.sql +172 -152
  31. package/dist/directory.d.ts +32 -0
  32. package/dist/directory.d.ts.map +1 -1
  33. package/dist/directory.js +88 -5
  34. package/dist/directory.js.map +1 -1
  35. package/dist/extraction/arkts-batch-worker.d.ts +2 -0
  36. package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
  37. package/dist/extraction/arkts-batch-worker.js +28 -0
  38. package/dist/extraction/arkts-batch-worker.js.map +1 -0
  39. package/dist/extraction/context.d.ts +11 -0
  40. package/dist/extraction/context.d.ts.map +1 -1
  41. package/dist/extraction/context.js +20 -0
  42. package/dist/extraction/context.js.map +1 -1
  43. package/dist/extraction/index.d.ts +16 -2
  44. package/dist/extraction/index.d.ts.map +1 -1
  45. package/dist/extraction/index.js +619 -380
  46. package/dist/extraction/index.js.map +1 -1
  47. package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
  48. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
  49. package/dist/extraction/languages/arkts-state-decorators.js +26 -0
  50. package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
  51. package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
  52. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  53. package/dist/extraction/languages/arkts-viewtree.js +21 -6
  54. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  55. package/dist/extraction/languages/arkts.d.ts +98 -2
  56. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  57. package/dist/extraction/languages/arkts.js +1290 -61
  58. package/dist/extraction/languages/arkts.js.map +1 -1
  59. package/dist/extraction/languages/c-cpp.d.ts +56 -0
  60. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  61. package/dist/extraction/languages/c-cpp.js +198 -1
  62. package/dist/extraction/languages/c-cpp.js.map +1 -1
  63. package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
  64. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
  65. package/dist/extraction/languages/ohos-api-consumer.js +283 -0
  66. package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
  67. package/dist/extraction/parse-pool.d.ts +126 -0
  68. package/dist/extraction/parse-pool.d.ts.map +1 -0
  69. package/dist/extraction/parse-pool.js +319 -0
  70. package/dist/extraction/parse-pool.js.map +1 -0
  71. package/dist/extraction/tree-sitter-types.d.ts +17 -0
  72. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  73. package/dist/extraction/tree-sitter.d.ts +21 -0
  74. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  75. package/dist/extraction/tree-sitter.js +198 -27
  76. package/dist/extraction/tree-sitter.js.map +1 -1
  77. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  78. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  79. package/dist/graph/traversal.d.ts.map +1 -1
  80. package/dist/graph/traversal.js +76 -17
  81. package/dist/graph/traversal.js.map +1 -1
  82. package/dist/index.d.ts +25 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +78 -4
  85. package/dist/index.js.map +1 -1
  86. package/dist/installer/instructions-template.js +9 -9
  87. package/dist/installer/targets/shared.d.ts +5 -6
  88. package/dist/installer/targets/shared.d.ts.map +1 -1
  89. package/dist/installer/targets/shared.js +5 -6
  90. package/dist/installer/targets/shared.js.map +1 -1
  91. package/dist/mcp/daemon-paths.d.ts +30 -3
  92. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  93. package/dist/mcp/daemon-paths.js +50 -10
  94. package/dist/mcp/daemon-paths.js.map +1 -1
  95. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  96. package/dist/mcp/daemon-registry.js +7 -3
  97. package/dist/mcp/daemon-registry.js.map +1 -1
  98. package/dist/mcp/daemon.d.ts +48 -0
  99. package/dist/mcp/daemon.d.ts.map +1 -1
  100. package/dist/mcp/daemon.js +196 -32
  101. package/dist/mcp/daemon.js.map +1 -1
  102. package/dist/mcp/engine.d.ts +17 -0
  103. package/dist/mcp/engine.d.ts.map +1 -1
  104. package/dist/mcp/engine.js +73 -1
  105. package/dist/mcp/engine.js.map +1 -1
  106. package/dist/mcp/index.d.ts.map +1 -1
  107. package/dist/mcp/index.js +25 -43
  108. package/dist/mcp/index.js.map +1 -1
  109. package/dist/mcp/liveness-watchdog.js +16 -16
  110. package/dist/mcp/ppid-watchdog.d.ts +18 -0
  111. package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
  112. package/dist/mcp/ppid-watchdog.js +37 -0
  113. package/dist/mcp/ppid-watchdog.js.map +1 -1
  114. package/dist/mcp/query-cache.d.ts +25 -0
  115. package/dist/mcp/query-cache.d.ts.map +1 -0
  116. package/dist/mcp/query-cache.js +191 -0
  117. package/dist/mcp/query-cache.js.map +1 -0
  118. package/dist/mcp/query-pool.d.ts +94 -0
  119. package/dist/mcp/query-pool.d.ts.map +1 -0
  120. package/dist/mcp/query-pool.js +297 -0
  121. package/dist/mcp/query-pool.js.map +1 -0
  122. package/dist/mcp/query-worker.d.ts +24 -0
  123. package/dist/mcp/query-worker.d.ts.map +1 -0
  124. package/dist/mcp/query-worker.js +87 -0
  125. package/dist/mcp/query-worker.js.map +1 -0
  126. package/dist/mcp/server-instructions.d.ts +5 -7
  127. package/dist/mcp/server-instructions.d.ts.map +1 -1
  128. package/dist/mcp/server-instructions.js +72 -74
  129. package/dist/mcp/server-instructions.js.map +1 -1
  130. package/dist/mcp/tools.d.ts +94 -4
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +877 -71
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/project-config.d.ts +20 -0
  135. package/dist/project-config.d.ts.map +1 -1
  136. package/dist/project-config.js +42 -2
  137. package/dist/project-config.js.map +1 -1
  138. package/dist/reasoning/login.js +1 -1
  139. package/dist/reasoning/login.js.map +1 -1
  140. package/dist/reasoning/reasoner.js +32 -32
  141. package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
  142. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  143. package/dist/resolution/c-fnptr-synthesizer.js +765 -79
  144. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  145. package/dist/resolution/callback-synthesizer.d.ts +1 -1
  146. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  147. package/dist/resolution/callback-synthesizer.js +72 -11
  148. package/dist/resolution/callback-synthesizer.js.map +1 -1
  149. package/dist/resolution/cooperative-yield.d.ts +32 -0
  150. package/dist/resolution/cooperative-yield.d.ts.map +1 -0
  151. package/dist/resolution/cooperative-yield.js +42 -0
  152. package/dist/resolution/cooperative-yield.js.map +1 -0
  153. package/dist/resolution/index.d.ts +11 -2
  154. package/dist/resolution/index.d.ts.map +1 -1
  155. package/dist/resolution/index.js +72 -4
  156. package/dist/resolution/index.js.map +1 -1
  157. package/dist/resolution/name-matcher.d.ts +22 -0
  158. package/dist/resolution/name-matcher.d.ts.map +1 -1
  159. package/dist/resolution/name-matcher.js +317 -20
  160. package/dist/resolution/name-matcher.js.map +1 -1
  161. package/dist/spec/config.d.ts +39 -0
  162. package/dist/spec/config.d.ts.map +1 -0
  163. package/dist/spec/config.js +304 -0
  164. package/dist/spec/config.js.map +1 -0
  165. package/dist/spec/db/commit-node.d.ts +23 -0
  166. package/dist/spec/db/commit-node.d.ts.map +1 -0
  167. package/dist/spec/db/commit-node.js +62 -0
  168. package/dist/spec/db/commit-node.js.map +1 -0
  169. package/dist/spec/db/fragment-node.d.ts +24 -0
  170. package/dist/spec/db/fragment-node.d.ts.map +1 -0
  171. package/dist/spec/db/fragment-node.js +128 -0
  172. package/dist/spec/db/fragment-node.js.map +1 -0
  173. package/dist/spec/db/fts.d.ts +74 -0
  174. package/dist/spec/db/fts.d.ts.map +1 -0
  175. package/dist/spec/db/fts.js +324 -0
  176. package/dist/spec/db/fts.js.map +1 -0
  177. package/dist/spec/db/index.d.ts +13 -0
  178. package/dist/spec/db/index.d.ts.map +1 -0
  179. package/dist/spec/db/index.js +50 -0
  180. package/dist/spec/db/index.js.map +1 -0
  181. package/dist/spec/db/relations.d.ts +55 -0
  182. package/dist/spec/db/relations.d.ts.map +1 -0
  183. package/dist/spec/db/relations.js +158 -0
  184. package/dist/spec/db/relations.js.map +1 -0
  185. package/dist/spec/db/schema.d.ts +33 -0
  186. package/dist/spec/db/schema.d.ts.map +1 -0
  187. package/dist/spec/db/schema.js +119 -0
  188. package/dist/spec/db/schema.js.map +1 -0
  189. package/dist/spec/db/schema.sql +117 -0
  190. package/dist/spec/db/spec-node.d.ts +41 -0
  191. package/dist/spec/db/spec-node.d.ts.map +1 -0
  192. package/dist/spec/db/spec-node.js +114 -0
  193. package/dist/spec/db/spec-node.js.map +1 -0
  194. package/dist/spec/evolve/impact-locator.d.ts +13 -0
  195. package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
  196. package/dist/spec/evolve/impact-locator.js +25 -0
  197. package/dist/spec/evolve/impact-locator.js.map +1 -0
  198. package/dist/spec/evolve/llm-client.d.ts +50 -0
  199. package/dist/spec/evolve/llm-client.d.ts.map +1 -0
  200. package/dist/spec/evolve/llm-client.js +176 -0
  201. package/dist/spec/evolve/llm-client.js.map +1 -0
  202. package/dist/spec/evolve/logic-checker.d.ts +12 -0
  203. package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
  204. package/dist/spec/evolve/logic-checker.js +24 -0
  205. package/dist/spec/evolve/logic-checker.js.map +1 -0
  206. package/dist/spec/evolve/pipeline.d.ts +42 -0
  207. package/dist/spec/evolve/pipeline.d.ts.map +1 -0
  208. package/dist/spec/evolve/pipeline.js +567 -0
  209. package/dist/spec/evolve/pipeline.js.map +1 -0
  210. package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
  211. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
  212. package/dist/spec/evolve/spec-rewriter.js +230 -0
  213. package/dist/spec/evolve/spec-rewriter.js.map +1 -0
  214. package/dist/spec/graph/queries.d.ts +155 -0
  215. package/dist/spec/graph/queries.d.ts.map +1 -0
  216. package/dist/spec/graph/queries.js +440 -0
  217. package/dist/spec/graph/queries.js.map +1 -0
  218. package/dist/spec/llm/client.d.ts +29 -0
  219. package/dist/spec/llm/client.d.ts.map +1 -0
  220. package/dist/spec/llm/client.js +123 -0
  221. package/dist/spec/llm/client.js.map +1 -0
  222. package/dist/spec/llm/index.d.ts +3 -0
  223. package/dist/spec/llm/index.d.ts.map +1 -0
  224. package/dist/spec/llm/index.js +11 -0
  225. package/dist/spec/llm/index.js.map +1 -0
  226. package/dist/spec/llm/prompts.d.ts +13 -0
  227. package/dist/spec/llm/prompts.d.ts.map +1 -0
  228. package/dist/spec/llm/prompts.js +75 -0
  229. package/dist/spec/llm/prompts.js.map +1 -0
  230. package/dist/spec/mining/diff-parser.d.ts +33 -0
  231. package/dist/spec/mining/diff-parser.d.ts.map +1 -0
  232. package/dist/spec/mining/diff-parser.js +166 -0
  233. package/dist/spec/mining/diff-parser.js.map +1 -0
  234. package/dist/spec/mining/git-scanner.d.ts +103 -0
  235. package/dist/spec/mining/git-scanner.d.ts.map +1 -0
  236. package/dist/spec/mining/git-scanner.js +307 -0
  237. package/dist/spec/mining/git-scanner.js.map +1 -0
  238. package/dist/spec/mining/pipeline.d.ts +53 -0
  239. package/dist/spec/mining/pipeline.d.ts.map +1 -0
  240. package/dist/spec/mining/pipeline.js +178 -0
  241. package/dist/spec/mining/pipeline.js.map +1 -0
  242. package/dist/spec/mining/scope-resolver.d.ts +45 -0
  243. package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
  244. package/dist/spec/mining/scope-resolver.js +103 -0
  245. package/dist/spec/mining/scope-resolver.js.map +1 -0
  246. package/dist/spec/mining/spec-extractor.d.ts +69 -0
  247. package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
  248. package/dist/spec/mining/spec-extractor.js +369 -0
  249. package/dist/spec/mining/spec-extractor.js.map +1 -0
  250. package/dist/spec/types.d.ts +149 -0
  251. package/dist/spec/types.d.ts.map +1 -0
  252. package/dist/spec/types.js +15 -0
  253. package/dist/spec/types.js.map +1 -0
  254. package/dist/spec/utils.d.ts +167 -0
  255. package/dist/spec/utils.d.ts.map +1 -0
  256. package/dist/spec/utils.js +463 -0
  257. package/dist/spec/utils.js.map +1 -0
  258. package/dist/sync/worktree.d.ts +9 -0
  259. package/dist/sync/worktree.d.ts.map +1 -1
  260. package/dist/sync/worktree.js +40 -0
  261. package/dist/sync/worktree.js.map +1 -1
  262. package/dist/types.d.ts +6 -1
  263. package/dist/types.d.ts.map +1 -1
  264. package/dist/ui/shimmer-progress.d.ts +2 -0
  265. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  266. package/dist/ui/shimmer-progress.js +19 -2
  267. package/dist/ui/shimmer-progress.js.map +1 -1
  268. package/dist/upgrade/index.js +1 -1
  269. package/dist/upgrade/index.js.map +1 -1
  270. package/package.json +58 -57
  271. package/scripts/add-lang/bench.sh +60 -60
  272. package/scripts/add-lang/check-grammar.mjs +75 -75
  273. package/scripts/add-lang/dump-ast.mjs +103 -103
  274. package/scripts/add-lang/verify-extraction.mjs +70 -70
  275. package/scripts/agent-eval/ab-adoption.sh +91 -91
  276. package/scripts/agent-eval/ab-hook.sh +86 -86
  277. package/scripts/agent-eval/ab-impl.sh +78 -78
  278. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  279. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  280. package/scripts/agent-eval/arms-F.sh +21 -21
  281. package/scripts/agent-eval/arms-matrix.sh +37 -37
  282. package/scripts/agent-eval/audit.sh +68 -68
  283. package/scripts/agent-eval/bench-readme.sh +28 -28
  284. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  285. package/scripts/agent-eval/block-read-hook.sh +19 -19
  286. package/scripts/agent-eval/hook-settings.json +15 -15
  287. package/scripts/agent-eval/itrun.sh +120 -120
  288. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  289. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  290. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  291. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  292. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  293. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  294. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  295. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  296. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  297. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  298. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  299. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  300. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  301. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  302. package/scripts/agent-eval/offload-eval.md +76 -76
  303. package/scripts/agent-eval/parse-arms.mjs +116 -116
  304. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  305. package/scripts/agent-eval/parse-run.mjs +45 -45
  306. package/scripts/agent-eval/parse-session.mjs +93 -93
  307. package/scripts/agent-eval/probe-context.mjs +21 -21
  308. package/scripts/agent-eval/probe-explore.mjs +40 -40
  309. package/scripts/agent-eval/probe-node.mjs +20 -20
  310. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  311. package/scripts/agent-eval/probe-trace.mjs +20 -20
  312. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  313. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  314. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  315. package/scripts/agent-eval/run-agent.sh +34 -34
  316. package/scripts/agent-eval/run-all.sh +69 -69
  317. package/scripts/agent-eval/run-arms.sh +56 -56
  318. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  319. package/scripts/build-bundle.sh +118 -118
  320. package/scripts/exp_boundary_eval/README.md +247 -0
  321. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  322. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  323. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  324. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  325. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  326. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  327. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
  328. package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
  329. package/scripts/exp_boundary_eval/_utils.py +1116 -0
  330. package/scripts/exp_boundary_eval/analyze.py +1313 -0
  331. package/scripts/exp_boundary_eval/data/agents.json +109 -0
  332. package/scripts/exp_boundary_eval/data/experiments.json +140 -0
  333. package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
  334. package/scripts/exp_boundary_eval/run_all.py +378 -0
  335. package/scripts/exp_boundary_eval/run_one.py +165 -0
  336. package/scripts/exp_boundary_eval/run_session.py +158 -0
  337. package/scripts/exp_boundary_eval/setup.py +120 -0
  338. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
  339. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
  340. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
  341. package/scripts/extract-release-notes.mjs +130 -130
  342. package/scripts/local-install.sh +41 -41
  343. package/scripts/npm-sdk.js +75 -75
  344. package/scripts/npm-shim.js +268 -246
  345. package/scripts/ohos-sdk-publish.mjs +133 -0
  346. package/scripts/pack-npm.sh +119 -119
  347. package/scripts/prepare-release.mjs +270 -270
  348. package/scripts/qa_eval/README.md +407 -404
  349. package/scripts/qa_eval/_test_deveco_probe.py +41 -41
  350. package/scripts/qa_eval/agent_runner.py +526 -526
  351. package/scripts/qa_eval/data/.gitignore +4 -4
  352. package/scripts/qa_eval/data/test-set.jsonl +2 -22
  353. package/scripts/qa_eval/eval_metrics.py +274 -233
  354. package/scripts/qa_eval/external_agent.py +976 -671
  355. package/scripts/qa_eval/llm_config.py +92 -92
  356. package/scripts/qa_eval/memory_monitor.py +132 -132
  357. package/scripts/qa_eval/my_answer_accuracy.py +187 -187
  358. package/scripts/qa_eval/requirements.txt +2 -2
  359. package/scripts/qa_eval/run_pipeline.py +804 -711
  360. package/scripts/qa_eval/stats_efficiency.py +279 -279
  361. package/scripts/qa_eval/stats_scores.py +207 -207
@@ -1,711 +1,804 @@
1
- #!/usr/bin/env python3
2
- """
3
- QA Eval A/B 一条龙:Agent 跑题 → Judge 打分 → 完整对比报告。
4
-
5
- Usage:
6
- export DASHSCOPE_API_KEY=sk-...
7
- # 或智谱: export ZHIPU_API_KEY='id.secret'
8
- python scripts/qa_eval/run_pipeline.py ab -r /path/to/your/repo
9
- python scripts/qa_eval/run_pipeline.py ab -r /path/to/your/repo --provider zhipu
10
-
11
- # 只重打报告、不重跑 Agent/Judge(无需 --repo):
12
- python scripts/qa_eval/run_pipeline.py ab --no-agent --no-judge
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import argparse
18
- import io
19
- import json
20
- import os
21
- import statistics
22
- import subprocess
23
- import sys
24
- from collections import defaultdict
25
- from contextlib import redirect_stdout
26
- from pathlib import Path
27
-
28
- _SCRIPT_DIR = Path(__file__).resolve().parent
29
- DATA_DIR = _SCRIPT_DIR / "data"
30
- LOG_DIR = _SCRIPT_DIR / "log"
31
- DEFAULT_REPORT = DATA_DIR / "report-builtin.txt"
32
- DEFAULT_DATASET = DATA_DIR / "test-set.jsonl"
33
- DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
34
- DEFAULT_MODEL = "qwen3-235b-a22b-instruct-2507"
35
-
36
- if str(_SCRIPT_DIR) not in sys.path:
37
- sys.path.insert(0, str(_SCRIPT_DIR))
38
-
39
- from agent_runner import run_agent_dataset # noqa: E402
40
- from external_agent import HOST_CLAUDE, HOST_DEVECO, SUPPORTED_HOSTS, run_external_dataset, verify_claude_login # noqa: E402
41
- from llm_config import PROVIDER_DASHSCOPE, PROVIDER_ZHIPU, provider_help, resolve_llm_config # noqa: E402
42
- from my_answer_accuracy import extract_json_blocks_answerbyCOT, remove_tool_calls # noqa: E402
43
- from stats_efficiency import parse_agent_log, summarize_jsonl_usage, summarize_tasks # noqa: E402
44
- from stats_scores import compute_stats_from_rows, print_statistics # noqa: E402
45
-
46
- AGENT_HOST_BUILTIN = "builtin"
47
- ALL_AGENT_HOSTS = (AGENT_HOST_BUILTIN, HOST_CLAUDE, HOST_DEVECO)
48
-
49
- # 各宿主产出文件后缀 report-builtin.txt / report-claude.txt / report-deveco.txt
50
- HOST_FILE_SUFFIX: dict[str, str] = {
51
- AGENT_HOST_BUILTIN: "builtin",
52
- HOST_CLAUDE: "claude",
53
- HOST_DEVECO: "deveco",
54
- }
55
-
56
-
57
- def require_repo_arg(repo: str | None, *, no_agent: bool) -> Path | None:
58
- """Return resolved repo path; required unless --no-agent (report-only)."""
59
- if no_agent:
60
- return Path(repo).expanduser().resolve() if repo else None
61
- if not repo:
62
- print("错误: 请指定被测仓库路径 --repo / -r(跑 Agent 时必填)")
63
- return None
64
- path = Path(repo).expanduser().resolve()
65
- if not path.is_dir():
66
- print(f"错误: 仓库不存在: {path}")
67
- return None
68
- return path
69
-
70
-
71
- def host_tag(agent_host: str) -> str:
72
- suffix = HOST_FILE_SUFFIX.get(agent_host, agent_host.replace("-code", ""))
73
- return f"-{suffix}"
74
-
75
-
76
- def paths_for_host(agent_host: str, *, log_dir: Path = LOG_DIR, data_dir: Path = DATA_DIR) -> dict[str, Path]:
77
- tag = host_tag(agent_host)
78
- log_dir.mkdir(parents=True, exist_ok=True)
79
- return {
80
- "with_jsonl": log_dir / f"result-with{tag}.jsonl",
81
- "without_jsonl": log_dir / f"result-without{tag}.jsonl",
82
- "with_scored": log_dir / f"result-with{tag}-scored.jsonl",
83
- "without_scored": log_dir / f"result-without{tag}-scored.jsonl",
84
- "with_log": log_dir / f"agent-with{tag}.log",
85
- "without_log": log_dir / f"agent-without{tag}.log",
86
- "report": data_dir / f"report{tag}.txt",
87
- }
88
-
89
-
90
- def load_jsonl(path: Path) -> list[dict]:
91
- rows: list[dict] = []
92
- with path.open("r", encoding="utf-8") as f:
93
- for line in f:
94
- line = line.strip()
95
- if line:
96
- rows.append(json.loads(line))
97
- return rows
98
-
99
-
100
- def index_by_id(rows: list[dict]) -> dict[str, dict]:
101
- return {str(r.get("id", i)): r for i, r in enumerate(rows)}
102
-
103
-
104
- def fmt_num(value: float | None, *, digits: int = 2) -> str:
105
- if value is None:
106
- return "N/A"
107
- return f"{value:.{digits}f}"
108
-
109
-
110
- def fmt_delta(a: float | None, b: float | None, *, digits: int = 2) -> str:
111
- if a is None or b is None:
112
- return "N/A"
113
- sign = "+" if a - b >= 0 else ""
114
- return f"{sign}{a - b:.{digits}f}"
115
-
116
-
117
- def clip(text: str, n: int = 56) -> str:
118
- text = " ".join(text.split())
119
- return text if len(text) <= n else text[: n - 1] + "…"
120
-
121
-
122
- def clean_answer(raw: str) -> str:
123
- return remove_tool_calls(extract_json_blocks_answerbyCOT(str(raw or "")))
124
-
125
-
126
- def row_tokens(row: dict) -> int | None:
127
- usage = row.get("agent_usage") or {}
128
- total = usage.get("total_tokens")
129
- return int(total) if isinstance(total, (int, float)) else None
130
-
131
-
132
- def resolve_api_key() -> str | None:
133
- try:
134
- return resolve_llm_config(provider=os.environ.get("QA_EVAL_PROVIDER")).api_key
135
- except RuntimeError:
136
- return None
137
-
138
-
139
- def run_judge(agent_jsonl: Path, scored_jsonl: Path, *, workers: int, llm) -> int:
140
- if not agent_jsonl.is_file():
141
- print(f"错误: Agent 产出不存在,无法 Judge: {agent_jsonl}")
142
- return 1
143
- n = sum(1 for line in agent_jsonl.open(encoding="utf-8") if line.strip())
144
- print(f"\n>>> Judge ({llm.provider}):{agent_jsonl.name},共 {n} ", flush=True)
145
- cmd = [
146
- sys.executable,
147
- str(_SCRIPT_DIR / "eval_metrics.py"),
148
- "-i",
149
- str(agent_jsonl),
150
- "-o",
151
- str(scored_jsonl),
152
- "-w",
153
- str(workers),
154
- "-m",
155
- llm.model,
156
- "--base-url",
157
- llm.base_url,
158
- "--provider",
159
- llm.provider,
160
- ]
161
- print(f"\n>>> {' '.join(cmd)}\n")
162
- return subprocess.call(cmd)
163
-
164
-
165
- def run_judge_if_needed(agent_jsonl: Path, scored_jsonl: Path, *, workers: int, llm) -> int:
166
- return run_judge(agent_jsonl, scored_jsonl, workers=workers, llm=llm)
167
-
168
-
169
- def print_category_table(rows_with: list[dict], rows_without: list[dict]) -> None:
170
- by_cat: dict[str, list[tuple[float, float]]] = defaultdict(list)
171
- wo = index_by_id(rows_without)
172
- for r in rows_with:
173
- rid = str(r.get("id"))
174
- if rid not in wo:
175
- continue
176
- if r.get("evaluation_status") != "success" or wo[rid].get("evaluation_status") != "success":
177
- continue
178
- cat = str(r.get("category_l1") or "未分类")
179
- by_cat[cat].append((float(r["answer_accuracy_score"]), float(wo[rid]["answer_accuracy_score"])))
180
-
181
- print("\n" + "=" * 88)
182
- print("按类别准确率")
183
- print("=" * 88)
184
- print(f"{'类别':<12}{'题数':>6}{'with均分':>12}{'without均分':>14}{'Δ':>10}")
185
- print("-" * 88)
186
- for cat in sorted(by_cat):
187
- pairs = by_cat[cat]
188
- w_mean = statistics.mean(p[0] for p in pairs)
189
- wo_mean = statistics.mean(p[1] for p in pairs)
190
- print(f"{cat:<12}{len(pairs):>6}{w_mean:>12.4f}{wo_mean:>14.4f}{w_mean - wo_mean:>+10.4f}")
191
- print("=" * 88)
192
-
193
-
194
- def print_per_item_table(rows_with: list[dict], rows_without: list[dict]) -> None:
195
- wo = index_by_id(rows_without)
196
- wins = ties = losses = 0
197
-
198
- print("\n" + "=" * 120)
199
- print("逐题对比(query → with / without 得分与答案摘要)")
200
- print("=" * 120)
201
- print(
202
- f"{'ID':<5}{'类':<6}{'with':>6}{'wo':>6}{'Δ':>7} {'问题':<28} {'with答案':<32} {'without答案'}"
203
- )
204
- print("-" * 120)
205
-
206
- for r in rows_with:
207
- rid = str(r.get("id", ""))
208
- w = wo.get(rid, {})
209
- sw = float(r["answer_accuracy_score"]) if r.get("evaluation_status") == "success" else None
210
- swo = float(w["answer_accuracy_score"]) if w.get("evaluation_status") == "success" else None
211
- if sw is not None and swo is not None:
212
- if sw > swo:
213
- wins += 1
214
- elif sw < swo:
215
- losses += 1
216
- else:
217
- ties += 1
218
- cat = str(r.get("category_l1") or "")[:4]
219
- q = clip(str(r.get("query", "")), 26)
220
- aw = clip(clean_answer(str(r.get("output_answer", ""))), 30)
221
- awo = clip(clean_answer(str(w.get("output_answer", ""))), 30)
222
- print(
223
- f"{rid:<5}{cat:<6}"
224
- f"{fmt_num(sw, digits=2):>6}"
225
- f"{fmt_num(swo, digits=2):>6}"
226
- f"{fmt_delta(sw, swo, digits=2):>7} "
227
- f"{q:<28} {aw:<32} {awo}"
228
- )
229
-
230
- print("-" * 120)
231
- print(f"with 更高: {wins} | 持平: {ties} | without 更高: {losses}")
232
- print("=" * 120)
233
-
234
-
235
- def print_efficiency_section(
236
- label: str, log_path: Path | None, rows: list[dict], *, print_report: bool = True
237
- ) -> dict:
238
- if print_report:
239
- print(f"\n{'=' * 88}")
240
- print(f"{label} 效率明细")
241
- print("=" * 88)
242
-
243
- if log_path and log_path.is_file():
244
- tasks = parse_agent_log(log_path)
245
- if tasks:
246
- summary = summarize_tasks(tasks, print_report=print_report)
247
- summary["source"] = f"日志 {log_path}"
248
- return summary
249
-
250
- if print_report:
251
- print(f"日志: {log_path} (不存在或未解析到 Evaluate 段)")
252
- print("\n从 JSONL agent_usage 逐题 Token:")
253
- print(f"{'ID':<6}{'Token':>10}{'agent_backend':>20}")
254
- print("-" * 40)
255
- tokens: list[int] = []
256
- for r in rows:
257
- tok = row_tokens(r)
258
- if tok is not None:
259
- tokens.append(tok)
260
- if print_report:
261
- print(
262
- f"{str(r.get('id', '')):<6}"
263
- f"{tok if tok is not None else 'N/A':>10}"
264
- f"{str(r.get('agent_backend', '')):>20}"
265
- )
266
- summary = summarize_jsonl_usage(rows)
267
- summary["source"] = "JSONL agent_usage"
268
- if print_report:
269
- print("-" * 40)
270
- print(f"平均 Token: {summary['avg_tokens']:.0f}" if summary.get("avg_tokens") else "平均 Token: N/A")
271
- print(f"合计 Token: {summary.get('total_tokens', 0)}")
272
- print("=" * 88)
273
- return summary
274
-
275
-
276
- def print_full_ab_report(
277
- *,
278
- with_scored: Path,
279
- without_scored: Path,
280
- with_log: Path | None,
281
- without_log: Path | None,
282
- rows_with: list[dict],
283
- rows_without: list[dict],
284
- ) -> None:
285
- stats_with = compute_stats_from_rows(rows_with)
286
- stats_without = compute_stats_from_rows(rows_without)
287
-
288
- print("\n")
289
- print("#" * 88)
290
- print("#" + " " * 30 + "A/B 完整评测报告" + " " * 30 + "#")
291
- print("#" * 88)
292
-
293
- print("\n【输入文件】")
294
- print(f" with scored : {with_scored}")
295
- print(f" without scored: {without_scored}")
296
- print(f" with log : {with_log} {'(存在)' if with_log and with_log.is_file() else '(无)'}")
297
- print(f" without log : {without_log} {'(存在)' if without_log and without_log.is_file() else '(无)'}")
298
-
299
- eff_with = print_efficiency_section("WITH homegraph", with_log, rows_with)
300
- eff_without = print_efficiency_section("WITHOUT homegraph", without_log, rows_without)
301
-
302
- print_statistics(stats_with, with_scored, title="【WITH homegraph】准确率统计")
303
- print_statistics(stats_without, without_scored, title="【WITHOUT homegraph】准确率统计")
304
-
305
- print_category_table(rows_with, rows_without)
306
- print_per_item_table(rows_with, rows_without)
307
-
308
- w = 22
309
- print("\n" + "=" * 88)
310
- print("【A/B 汇总表】")
311
- print("=" * 88)
312
- print(f"{'指标':<24}{'with':>{w}}{'without':>{w}}{'Δ (with−without)':>{w}}")
313
- print("-" * 88)
314
-
315
- def sm(s: dict | None) -> float | None:
316
- return float(s["mean"]) if s and s.get("valid_scores") else None
317
-
318
- def sd(s: dict | None) -> float | None:
319
- return float(s["median"]) if s and s.get("valid_scores") else None
320
-
321
- print(
322
- f"{'准确率 均值':<24}"
323
- f"{fmt_num(sm(stats_with), digits=4):>{w}}"
324
- f"{fmt_num(sm(stats_without), digits=4):>{w}}"
325
- f"{fmt_delta(sm(stats_with), sm(stats_without), digits=4):>{w}}"
326
- )
327
- print(
328
- f"{'准确率 中位数':<24}"
329
- f"{fmt_num(sd(stats_with), digits=4):>{w}}"
330
- f"{fmt_num(sd(stats_without), digits=4):>{w}}"
331
- f"{fmt_delta(sd(stats_with), sd(stats_without), digits=4):>{w}}"
332
- )
333
- jw = f"{stats_with['success_samples']}/{stats_with['total_samples']}"
334
- jo = f"{stats_without['success_samples']}/{stats_without['total_samples']}"
335
- print(
336
- f"{'Judge 成功/总数':<24}"
337
- f"{jw:>{w}}"
338
- f"{jo:>{w}}"
339
- f"{'—':>{w}}"
340
- )
341
- print("-" * 88)
342
- print(
343
- f"{'平均轮次':<24}"
344
- f"{fmt_num(eff_with.get('avg_turns')):>{w}}"
345
- f"{fmt_num(eff_without.get('avg_turns')):>{w}}"
346
- f"{fmt_delta(eff_with.get('avg_turns'), eff_without.get('avg_turns')):>{w}}"
347
- )
348
- print(
349
- f"{'平均耗时 ()':<24}"
350
- f"{fmt_num(eff_with.get('avg_duration_s')):>{w}}"
351
- f"{fmt_num(eff_without.get('avg_duration_s')):>{w}}"
352
- f"{fmt_delta(eff_with.get('avg_duration_s'), eff_without.get('avg_duration_s')):>{w}}"
353
- )
354
- tw = eff_with.get("avg_tokens")
355
- two = eff_without.get("avg_tokens")
356
- print(
357
- f"{'平均 Token (k)':<24}"
358
- f"{fmt_num(tw / 1000 if tw else None, digits=2):>{w}}"
359
- f"{fmt_num(two / 1000 if two else None, digits=2):>{w}}"
360
- f"{fmt_delta(tw / 1000 if tw else None, two / 1000 if two else None, digits=2):>{w}}"
361
- )
362
- print(
363
- f"{'平均首响应 (秒)':<24}"
364
- f"{fmt_num(eff_with.get('avg_first_token_s')):>{w}}"
365
- f"{fmt_num(eff_without.get('avg_first_token_s')):>{w}}"
366
- f"{fmt_delta(eff_with.get('avg_first_token_s'), eff_without.get('avg_first_token_s')):>{w}}"
367
- )
368
- print(
369
- f"{'平均峰值内存 (MB)':<24}"
370
- f"{fmt_num(eff_with.get('avg_peak_rss_mb')):>{w}}"
371
- f"{fmt_num(eff_without.get('avg_peak_rss_mb')):>{w}}"
372
- f"{fmt_delta(eff_with.get('avg_peak_rss_mb'), eff_without.get('avg_peak_rss_mb')):>{w}}"
373
- )
374
- print(
375
- f"{'最大峰值内存 (MB)':<24}"
376
- f"{fmt_num(eff_with.get('max_peak_rss_mb')):>{w}}"
377
- f"{fmt_num(eff_without.get('max_peak_rss_mb')):>{w}}"
378
- f"{fmt_delta(eff_with.get('max_peak_rss_mb'), eff_without.get('max_peak_rss_mb')):>{w}}"
379
- )
380
- print("-" * 88)
381
- print("效率数据来源:")
382
- print(f" with : {eff_with.get('source', '?')}")
383
- print(f" without : {eff_without.get('source', '?')}")
384
- print("=" * 88)
385
-
386
-
387
- def run_agent_stage(
388
- *,
389
- agent_host: str,
390
- repo: Path,
391
- dataset: list[dict],
392
- with_jsonl: Path,
393
- without_jsonl: Path,
394
- with_log: Path | None,
395
- without_log: Path | None,
396
- llm,
397
- hg_bin: str | None,
398
- max_turns: int,
399
- deveco_model: str | None = None,
400
- ) -> None:
401
- if agent_host == HOST_CLAUDE:
402
- verify_claude_login()
403
- for arm, out, log in (
404
- ("with", with_jsonl, with_log),
405
- ("without", without_jsonl, without_log),
406
- ):
407
- label = "WITH homegraph" if arm == "with" else "WITHOUT (grep/read only)"
408
- print(f"\n>>> Agent [{agent_host}] [{label}] → {out}")
409
- if agent_host == AGENT_HOST_BUILTIN:
410
- run_agent_dataset(
411
- repo,
412
- dataset,
413
- arm=arm,
414
- output=out,
415
- log_file=log,
416
- api_key=llm.api_key,
417
- base_url=llm.base_url,
418
- model=llm.model,
419
- hg_bin=hg_bin,
420
- max_turns=max_turns,
421
- extra_body=llm.extra_body,
422
- )
423
- else:
424
- run_external_dataset(
425
- agent_host,
426
- repo,
427
- dataset,
428
- arm=arm,
429
- output=out,
430
- log_file=log,
431
- hg_bin=hg_bin or "",
432
- model=deveco_model if agent_host == HOST_DEVECO else None,
433
- )
434
-
435
-
436
- def cmd_ab(args: argparse.Namespace) -> int:
437
- if args.agent_host == "all":
438
- return cmd_hosts(args)
439
-
440
- agent_host = args.agent_host
441
- paths = paths_for_host(agent_host)
442
- if args.with_scored:
443
- with_scored = Path(args.with_scored)
444
- else:
445
- with_scored = paths["with_scored"]
446
- if args.without_scored:
447
- without_scored = Path(args.without_scored)
448
- else:
449
- without_scored = paths["without_scored"]
450
- if args.with_jsonl:
451
- with_jsonl = Path(args.with_jsonl)
452
- else:
453
- with_jsonl = paths["with_jsonl"]
454
- if args.without_jsonl:
455
- without_jsonl = Path(args.without_jsonl)
456
- else:
457
- without_jsonl = paths["without_jsonl"]
458
- with_log = Path(args.with_log) if args.with_log else paths["with_log"]
459
- without_log = Path(args.without_log) if args.without_log else paths["without_log"]
460
- repo = require_repo_arg(getattr(args, "repo", None), no_agent=args.no_agent)
461
- if repo is None and not args.no_agent:
462
- return 1
463
- dataset_path = Path(args.dataset).expanduser().resolve()
464
-
465
- try:
466
- llm = resolve_llm_config(
467
- provider=getattr(args, "provider", None),
468
- model=getattr(args, "model", None),
469
- base_url=getattr(args, "base_url", None),
470
- )
471
- except RuntimeError as e:
472
- print(f"错误: {e}")
473
- return 1
474
-
475
- if not args.no_agent:
476
- if not dataset_path.is_file():
477
- print(f"错误: 测试集不存在: {dataset_path}")
478
- return 1
479
-
480
- dataset = load_jsonl(dataset_path)
481
- print("=" * 60)
482
- print(f"Stage 1 — Agent 跑题 ({len(dataset)} 条) host={agent_host}")
483
- print(f" LLM : {llm.provider}")
484
- print(f" 仓库 : {repo}")
485
- print(f" 测试集 : {dataset_path}")
486
- print(f" 报告 : {paths['report']}")
487
- print("=" * 60)
488
-
489
- assert repo is not None
490
- run_agent_stage(
491
- agent_host=agent_host,
492
- repo=repo,
493
- dataset=dataset,
494
- with_jsonl=with_jsonl,
495
- without_jsonl=without_jsonl,
496
- with_log=with_log,
497
- without_log=without_log,
498
- llm=llm,
499
- hg_bin=args.homegraph_bin,
500
- max_turns=args.max_turns,
501
- deveco_model=getattr(args, "deveco_model", None),
502
- )
503
-
504
- if not args.no_judge:
505
- print("\n" + "=" * 60)
506
- print(f"Stage 2 — Judge 打分 ({llm.provider})")
507
- print("=" * 60)
508
- if run_judge(with_jsonl, with_scored, workers=args.workers, llm=llm) != 0:
509
- return 1
510
- if run_judge(without_jsonl, without_scored, workers=args.workers, llm=llm) != 0:
511
- return 1
512
-
513
- if not with_scored.is_file() or not without_scored.is_file():
514
- print("错误: 缺少 scored 结果。请去掉 --no-agent --no-judge 完整跑一遍,或提供 scored 文件。")
515
- return 1
516
-
517
- rows_with = load_jsonl(with_scored)
518
- rows_without = load_jsonl(without_scored)
519
-
520
- print("\n" + "=" * 60)
521
- print("Stage 3–4 — 效率统计 + A/B 对比报告")
522
- print("=" * 60)
523
-
524
- eff_with = print_efficiency_section("WITH homegraph", with_log, rows_with)
525
- eff_without = print_efficiency_section("WITHOUT homegraph", without_log, rows_without)
526
-
527
- report_path = Path(args.report).expanduser().resolve() if args.report else paths["report"]
528
- report_path.parent.mkdir(parents=True, exist_ok=True)
529
-
530
- buf = io.StringIO()
531
- with redirect_stdout(buf):
532
- print_full_ab_report(
533
- with_scored=with_scored,
534
- without_scored=without_scored,
535
- with_log=with_log,
536
- without_log=without_log,
537
- rows_with=rows_with,
538
- rows_without=rows_without,
539
- )
540
-
541
- report_path.write_text(buf.getvalue(), encoding="utf-8")
542
- print(f"\nA/B 完整报告已写入: {report_path}")
543
- return 0
544
-
545
-
546
- def cmd_score(args: argparse.Namespace) -> int:
547
- input_jsonl = Path(args.input)
548
- scored = Path(args.scored) if args.scored else input_jsonl.with_name(input_jsonl.stem + "-scored.jsonl")
549
- cmd = [
550
- sys.executable,
551
- str(_SCRIPT_DIR / "eval_metrics.py"),
552
- "-i",
553
- str(input_jsonl),
554
- "-o",
555
- str(scored),
556
- "-w",
557
- str(args.workers),
558
- ]
559
- if getattr(args, "provider", None):
560
- cmd.extend(["--provider", args.provider])
561
- if args.model:
562
- cmd.extend(["-m", args.model])
563
- rc = subprocess.call(cmd)
564
- if rc != 0:
565
- return rc
566
- rows = load_jsonl(scored)
567
- print_statistics(compute_stats_from_rows(rows), scored)
568
- if args.log:
569
- print_efficiency_section("单路", Path(args.log), rows)
570
- return 0
571
-
572
-
573
- def cmd_hosts(args: argparse.Namespace) -> int:
574
- """Run full A/B for each Agent host (builtin, claude-code, deveco-code)."""
575
- repo = require_repo_arg(getattr(args, "repo", None), no_agent=args.no_agent)
576
- if repo is None and not args.no_agent:
577
- return 1
578
-
579
- hosts_raw = getattr(args, "agent_hosts", None) or ",".join(ALL_AGENT_HOSTS)
580
- hosts = [h.strip() for h in hosts_raw.split(",") if h.strip()]
581
- unknown = [h for h in hosts if h not in ALL_AGENT_HOSTS]
582
- if unknown:
583
- print(f"错误: 未知 agent host: {unknown},可选: {', '.join(ALL_AGENT_HOSTS)}")
584
- return 1
585
-
586
- rc = 0
587
- for host in hosts:
588
- print("\n" + "#" * 88)
589
- print(f"# Agent host: {host}")
590
- print("#" * 88)
591
- host_args = argparse.Namespace(
592
- repo=args.repo,
593
- dataset=args.dataset,
594
- with_scored=None,
595
- without_scored=None,
596
- with_jsonl=None,
597
- without_jsonl=None,
598
- with_log=None,
599
- without_log=None,
600
- workers=args.workers,
601
- provider=getattr(args, "provider", None),
602
- model=args.model,
603
- base_url=args.base_url,
604
- homegraph_bin=args.homegraph_bin,
605
- max_turns=args.max_turns,
606
- no_agent=args.no_agent,
607
- no_judge=args.no_judge,
608
- report=None,
609
- agent_host=host,
610
- deveco_model=getattr(args, "deveco_model", None),
611
- )
612
- if cmd_ab(host_args) != 0:
613
- rc = 1
614
- if rc == 0:
615
- print("\n" + "=" * 60)
616
- print("全部宿主跑完,报告文件:")
617
- for host in hosts:
618
- p = paths_for_host(host)["report"]
619
- print(f" {host}: {p}")
620
- print("=" * 60)
621
- return rc
622
-
623
-
624
- def main() -> int:
625
- parser = argparse.ArgumentParser(description="QA Eval A/B — 完整打印全部对比结果")
626
- sub = parser.add_subparsers(dest="command", required=True)
627
-
628
- p_ab = sub.add_parser("ab", help="一条龙:Agent → Judge → 完整报告(默认会更新全部数据)")
629
- p_ab.add_argument(
630
- "--repo",
631
- "-r",
632
- default=None,
633
- help="被测仓库绝对路径(跑 Agent 时必填;仅 --no-agent 重打报告时可省略)",
634
- )
635
- p_ab.add_argument("--dataset", "-d", default=str(DEFAULT_DATASET))
636
- p_ab.add_argument(
637
- "--agent-host",
638
- default=AGENT_HOST_BUILTIN,
639
- choices=[*ALL_AGENT_HOSTS, "all"],
640
- help="Agent 宿主: builtin(Python+Qwen) | claude-code | deveco-code | all",
641
- )
642
- p_ab.add_argument("--with-scored", default=None)
643
- p_ab.add_argument("--without-scored", default=None)
644
- p_ab.add_argument("--with-jsonl", default=None)
645
- p_ab.add_argument("--without-jsonl", default=None)
646
- p_ab.add_argument("--with-log", default=None)
647
- p_ab.add_argument("--without-log", default=None)
648
- p_ab.add_argument("--workers", "-w", type=int, default=1)
649
- p_ab.add_argument(
650
- "--provider",
651
- choices=[PROVIDER_DASHSCOPE, PROVIDER_ZHIPU],
652
- default=None,
653
- help=provider_help(),
654
- )
655
- p_ab.add_argument("--model", "-m", default=None, help="LLM 模型(默认随 provider)")
656
- p_ab.add_argument("--base-url", default=None, help="OpenAI 兼容 API 端点(默认随 provider)")
657
- p_ab.add_argument("--homegraph-bin", default=None)
658
- p_ab.add_argument("--max-turns", type=int, default=8, help="Agent 最多工具轮次")
659
- p_ab.add_argument(
660
- "--deveco-model",
661
- default=None,
662
- help="DevEco Agent 模型(provider/model,如 zhipuai/glm-4.5-flash);默认用 DevEco 自身配置,不用 --model",
663
- )
664
- p_ab.add_argument("--no-agent", action="store_true", help="不跑 Agent,沿用已有 JSONL")
665
- p_ab.add_argument("--no-judge", action="store_true", help="不跑 Judge,沿用已有 scored")
666
- p_ab.add_argument(
667
- "--report",
668
- default=None,
669
- help="A/B 完整报告输出路径(默认 data/report-{host}.txt,如 report-deveco.txt)",
670
- )
671
- p_ab.set_defaults(func=cmd_ab)
672
-
673
- p_hosts = sub.add_parser("hosts", help="依次跑 builtin + claude-code + deveco-code 的 A/B")
674
- p_hosts.add_argument(
675
- "--repo",
676
- "-r",
677
- default=None,
678
- help="被测仓库绝对路径(跑 Agent 时必填;仅 --no-agent 重打报告时可省略)",
679
- )
680
- p_hosts.add_argument("--dataset", "-d", default=str(DEFAULT_DATASET))
681
- p_hosts.add_argument("--workers", "-w", type=int, default=1)
682
- p_hosts.add_argument("--provider", choices=[PROVIDER_DASHSCOPE, PROVIDER_ZHIPU], default=None)
683
- p_hosts.add_argument("--model", "-m", default=None)
684
- p_hosts.add_argument("--base-url", default=None)
685
- p_hosts.add_argument("--homegraph-bin", default=None)
686
- p_hosts.add_argument("--max-turns", type=int, default=8)
687
- p_hosts.add_argument("--deveco-model", default=None)
688
- p_hosts.add_argument("--no-agent", action="store_true")
689
- p_hosts.add_argument("--no-judge", action="store_true")
690
- p_hosts.add_argument(
691
- "--agent-hosts",
692
- default=",".join(ALL_AGENT_HOSTS),
693
- help="逗号分隔: builtin,claude-code,deveco-code",
694
- )
695
- p_hosts.set_defaults(func=cmd_hosts)
696
-
697
- p_score = sub.add_parser("score", help="单路:Agent JSONL → Judge → 统计")
698
- p_score.add_argument("--input", "-i", required=True)
699
- p_score.add_argument("--scored", "-o", default=None)
700
- p_score.add_argument("--log", "-l", default=None)
701
- p_score.add_argument("--workers", "-w", type=int, default=1)
702
- p_score.add_argument("--provider", choices=[PROVIDER_DASHSCOPE, PROVIDER_ZHIPU], default=None)
703
- p_score.add_argument("--model", "-m", default=None)
704
- p_score.set_defaults(func=cmd_score)
705
-
706
- args = parser.parse_args()
707
- return args.func(args)
708
-
709
-
710
- if __name__ == "__main__":
711
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ """
3
+ QA Eval A/B 一条龙:Agent 跑题 → Judge 打分 → 完整对比报告。
4
+
5
+ Usage:
6
+ export DASHSCOPE_API_KEY=sk-...
7
+ # 或智谱: export ZHIPU_API_KEY='id.secret'
8
+ python scripts/qa_eval/run_pipeline.py ab -r /path/to/your/repo
9
+ python scripts/qa_eval/run_pipeline.py ab -r /path/to/your/repo --provider zhipu
10
+
11
+ # 只重打报告、不重跑 Agent/Judge(无需 --repo):
12
+ python scripts/qa_eval/run_pipeline.py ab --no-agent --no-judge
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import io
19
+ import json
20
+ import os
21
+ import re
22
+ import shutil
23
+ import statistics
24
+ import subprocess
25
+ import sys
26
+ from collections import defaultdict
27
+ from contextlib import redirect_stdout
28
+ from pathlib import Path
29
+
30
+ _SCRIPT_DIR = Path(__file__).resolve().parent
31
+ DATA_DIR = _SCRIPT_DIR / "data"
32
+ LOG_DIR = _SCRIPT_DIR / "log"
33
+ DEFAULT_REPORT = DATA_DIR / "report-builtin.txt"
34
+ DEFAULT_DATASET = DATA_DIR / "test-set.jsonl"
35
+ DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
36
+ DEFAULT_MODEL = "qwen3-235b-a22b-instruct-2507"
37
+
38
+ if str(_SCRIPT_DIR) not in sys.path:
39
+ sys.path.insert(0, str(_SCRIPT_DIR))
40
+
41
+ from agent_runner import run_agent_dataset # noqa: E402
42
+ from external_agent import HOST_CLAUDE, HOST_DEVECO, SUPPORTED_HOSTS, run_external_dataset, verify_claude_login # noqa: E402
43
+ from llm_config import PROVIDER_DASHSCOPE, PROVIDER_ZHIPU, provider_help, resolve_llm_config # noqa: E402
44
+ from my_answer_accuracy import extract_json_blocks_answerbyCOT, remove_tool_calls # noqa: E402
45
+ from stats_efficiency import parse_agent_log, summarize_jsonl_usage, summarize_tasks # noqa: E402
46
+ from stats_scores import compute_stats_from_rows, print_statistics # noqa: E402
47
+
48
+ AGENT_HOST_BUILTIN = "builtin"
49
+ ALL_AGENT_HOSTS = (AGENT_HOST_BUILTIN, HOST_CLAUDE, HOST_DEVECO)
50
+
51
+ # 各宿主产出文件后缀 → report-builtin.txt / report-claude.txt / report-deveco.txt
52
+ HOST_FILE_SUFFIX: dict[str, str] = {
53
+ AGENT_HOST_BUILTIN: "builtin",
54
+ HOST_CLAUDE: "claude",
55
+ HOST_DEVECO: "deveco",
56
+ }
57
+
58
+
59
+ def require_repo_arg(repo: str | None, *, no_agent: bool) -> Path | None:
60
+ """Return resolved repo path; required unless --no-agent (report-only)."""
61
+ if no_agent:
62
+ return Path(repo).expanduser().resolve() if repo else None
63
+ if not repo:
64
+ print("错误: 请指定被测仓库路径 --repo / -r(跑 Agent 时必填)")
65
+ return None
66
+ path = Path(repo).expanduser().resolve()
67
+ if not path.is_dir():
68
+ print(f"错误: 仓库不存在: {path}")
69
+ return None
70
+ return path
71
+
72
+
73
+ def host_tag(agent_host: str) -> str:
74
+ suffix = HOST_FILE_SUFFIX.get(agent_host, agent_host.replace("-code", ""))
75
+ return f"-{suffix}"
76
+
77
+
78
+ def clear_log_dir(log_dir: Path) -> tuple[int, list[str]]:
79
+ """Remove files/subdirs under qa_eval log/ before a fresh run.
80
+
81
+ Skips paths locked by another process (common on Windows when a file is open
82
+ in the editor) instead of aborting the pipeline.
83
+ """
84
+ log_dir.mkdir(parents=True, exist_ok=True)
85
+ removed = 0
86
+ skipped: list[str] = []
87
+ for child in sorted(log_dir.iterdir(), key=lambda p: (p.is_dir(), p.name.lower())):
88
+ try:
89
+ if child.is_file():
90
+ child.unlink()
91
+ elif child.is_dir():
92
+ shutil.rmtree(child)
93
+ else:
94
+ continue
95
+ removed += 1
96
+ except OSError as e:
97
+ skipped.append(f"{child.name} ({e})")
98
+ return removed, skipped
99
+
100
+
101
+ def paths_for_host(agent_host: str, *, log_dir: Path = LOG_DIR, data_dir: Path = DATA_DIR) -> dict[str, Path]:
102
+ tag = host_tag(agent_host)
103
+ log_dir.mkdir(parents=True, exist_ok=True)
104
+ return {
105
+ "with_jsonl": log_dir / f"result-with{tag}.jsonl",
106
+ "without_jsonl": log_dir / f"result-without{tag}.jsonl",
107
+ "with_scored": log_dir / f"result-with{tag}-scored.jsonl",
108
+ "without_scored": log_dir / f"result-without{tag}-scored.jsonl",
109
+ "with_log": log_dir / f"agent-with{tag}.log",
110
+ "without_log": log_dir / f"agent-without{tag}.log",
111
+ "report": data_dir / f"report{tag}.txt",
112
+ }
113
+
114
+
115
+ def load_jsonl(path: Path) -> list[dict]:
116
+ rows: list[dict] = []
117
+ with path.open("r", encoding="utf-8") as f:
118
+ for line in f:
119
+ line = line.strip()
120
+ if line:
121
+ rows.append(json.loads(line))
122
+ return rows
123
+
124
+
125
+ def index_by_id(rows: list[dict]) -> dict[str, dict]:
126
+ return {str(r.get("id", i)): r for i, r in enumerate(rows)}
127
+
128
+
129
+ def fmt_num(value: float | None, *, digits: int = 2) -> str:
130
+ if value is None:
131
+ return "N/A"
132
+ return f"{value:.{digits}f}"
133
+
134
+
135
+ def fmt_delta(a: float | None, b: float | None, *, digits: int = 2) -> str:
136
+ if a is None or b is None:
137
+ return "N/A"
138
+ sign = "+" if a - b >= 0 else ""
139
+ return f"{sign}{a - b:.{digits}f}"
140
+
141
+
142
+ def clip(text: str, n: int = 56) -> str:
143
+ text = " ".join(text.split())
144
+ return text if len(text) <= n else text[: n - 1] + "…"
145
+
146
+
147
+ def clean_answer(raw: str) -> str:
148
+ return remove_tool_calls(extract_json_blocks_answerbyCOT(str(raw or "")))
149
+
150
+
151
+ def tool_chain_summary(raw: str, *, used_homegraph: bool | None = None) -> str:
152
+ tools = re.findall(r"---\n([^\n]+)\n", str(raw or ""))
153
+ chain = " → ".join(tools) if tools else "-"
154
+ if used_homegraph is True:
155
+ return f"{chain} [homegraph]"
156
+ if used_homegraph is False:
157
+ return f"{chain} [无homegraph]"
158
+ return chain
159
+
160
+
161
+ def print_trajectory_section(rows_with: list[dict], rows_without: list[dict]) -> None:
162
+ rows = [( "with", r) for r in rows_with] + [("without", r) for r in rows_without]
163
+ if not any(r.get("deveco_session_id") or r.get("agent_trace_file") for _, r in rows):
164
+ return
165
+ print("\n" + "=" * 120)
166
+ print("【Agent 轨迹 / DevEco Session】")
167
+ print("跑完后查看完整轨迹:")
168
+ print(" 1) JSON 文件:scripts/qa_eval/log/traces/<with|without>-deveco/<ID>-ses_*.json")
169
+ print(" 2) CLI:deveco export <session_id> 或 deveco session list 找标题 qa-eval-*")
170
+ print("-" * 120)
171
+ print(f"{'ID':<6}{'臂':<8}{'HG':<4}{'状态':<8}{'工具链':<28}{'session_id':<28}{'轨迹文件'}")
172
+ print("-" * 120)
173
+ for arm, r in rows:
174
+ rid = str(r.get("id", ""))
175
+ status = str(r.get("agent_status", ""))[:7]
176
+ hg = "Y" if r.get("agent_used_homegraph") else ("N" if arm == "with" else "-")
177
+ chain = clip(
178
+ tool_chain_summary(
179
+ str(r.get("output_answer", "")),
180
+ used_homegraph=r.get("agent_used_homegraph") if arm == "with" else None,
181
+ ),
182
+ 26,
183
+ )
184
+ sid = str(r.get("deveco_session_id") or "-")
185
+ trace = str(r.get("agent_trace_file") or "-")
186
+ print(f"{rid:<6}{arm:<8}{hg:<4}{status:<8}{chain:<28}{sid:<28}{trace}")
187
+ print("=" * 120)
188
+
189
+
190
+ def row_tokens(row: dict) -> int | None:
191
+ usage = row.get("agent_usage") or {}
192
+ total = usage.get("total_tokens")
193
+ return int(total) if isinstance(total, (int, float)) else None
194
+
195
+
196
+ def resolve_api_key() -> str | None:
197
+ try:
198
+ return resolve_llm_config(provider=os.environ.get("QA_EVAL_PROVIDER")).api_key
199
+ except RuntimeError:
200
+ return None
201
+
202
+
203
+ def run_judge(agent_jsonl: Path, scored_jsonl: Path, *, workers: int, llm) -> int:
204
+ if not agent_jsonl.is_file():
205
+ print(f"错误: Agent 产出不存在,无法 Judge: {agent_jsonl}")
206
+ return 1
207
+ n = sum(1 for line in agent_jsonl.open(encoding="utf-8") if line.strip())
208
+ print(f"\n>>> Judge ({llm.provider}):{agent_jsonl.name},共 {n} 条", flush=True)
209
+ cmd = [
210
+ sys.executable,
211
+ str(_SCRIPT_DIR / "eval_metrics.py"),
212
+ "-i",
213
+ str(agent_jsonl),
214
+ "-o",
215
+ str(scored_jsonl),
216
+ "-w",
217
+ str(workers),
218
+ "-m",
219
+ llm.model,
220
+ "--base-url",
221
+ llm.base_url,
222
+ "--provider",
223
+ llm.provider,
224
+ ]
225
+ print(f"\n>>> {' '.join(cmd)}\n")
226
+ return subprocess.call(cmd)
227
+
228
+
229
+ def run_judge_if_needed(agent_jsonl: Path, scored_jsonl: Path, *, workers: int, llm) -> int:
230
+ return run_judge(agent_jsonl, scored_jsonl, workers=workers, llm=llm)
231
+
232
+
233
+ def print_category_table(rows_with: list[dict], rows_without: list[dict]) -> None:
234
+ by_cat: dict[str, list[tuple[float, float]]] = defaultdict(list)
235
+ wo = index_by_id(rows_without)
236
+ for r in rows_with:
237
+ rid = str(r.get("id"))
238
+ if rid not in wo:
239
+ continue
240
+ if r.get("evaluation_status") != "success" or wo[rid].get("evaluation_status") != "success":
241
+ continue
242
+ cat = str(r.get("category_l1") or "未分类")
243
+ by_cat[cat].append((float(r["answer_accuracy_score"]), float(wo[rid]["answer_accuracy_score"])))
244
+
245
+ print("\n" + "=" * 88)
246
+ print("按类别准确率")
247
+ print("=" * 88)
248
+ print(f"{'类别':<12}{'题数':>6}{'with均分':>12}{'without均分':>14}{'Δ':>10}")
249
+ print("-" * 88)
250
+ for cat in sorted(by_cat):
251
+ pairs = by_cat[cat]
252
+ w_mean = statistics.mean(p[0] for p in pairs)
253
+ wo_mean = statistics.mean(p[1] for p in pairs)
254
+ print(f"{cat:<12}{len(pairs):>6}{w_mean:>12.4f}{wo_mean:>14.4f}{w_mean - wo_mean:>+10.4f}")
255
+ print("=" * 88)
256
+
257
+
258
+ def print_per_item_table(rows_with: list[dict], rows_without: list[dict]) -> None:
259
+ wo = index_by_id(rows_without)
260
+ wins = ties = losses = 0
261
+
262
+ print("\n" + "=" * 120)
263
+ print("逐题对比(query with / without 得分与答案摘要)")
264
+ print("=" * 120)
265
+ print(
266
+ f"{'ID':<5}{'类':<6}{'with':>6}{'wo':>6}{'Δ':>7} {'问题':<28} {'with答案':<32} {'without答案'}"
267
+ )
268
+ print("-" * 120)
269
+
270
+ for r in rows_with:
271
+ rid = str(r.get("id", ""))
272
+ w = wo.get(rid, {})
273
+ sw = float(r["answer_accuracy_score"]) if r.get("evaluation_status") == "success" else None
274
+ swo = float(w["answer_accuracy_score"]) if w.get("evaluation_status") == "success" else None
275
+ if sw is not None and swo is not None:
276
+ if sw > swo:
277
+ wins += 1
278
+ elif sw < swo:
279
+ losses += 1
280
+ else:
281
+ ties += 1
282
+ cat = str(r.get("category_l1") or "")[:4]
283
+ q = clip(str(r.get("query", "")), 26)
284
+ aw = clip(clean_answer(str(r.get("output_answer", ""))), 30)
285
+ awo = clip(clean_answer(str(w.get("output_answer", ""))), 30)
286
+ print(
287
+ f"{rid:<5}{cat:<6}"
288
+ f"{fmt_num(sw, digits=2):>6}"
289
+ f"{fmt_num(swo, digits=2):>6}"
290
+ f"{fmt_delta(sw, swo, digits=2):>7} "
291
+ f"{q:<28} {aw:<32} {awo}"
292
+ )
293
+
294
+ print("-" * 120)
295
+ print(f"with 更高: {wins} | 持平: {ties} | without 更高: {losses}")
296
+ print("=" * 120)
297
+
298
+
299
+ def print_efficiency_section(
300
+ label: str, log_path: Path | None, rows: list[dict], *, print_report: bool = True
301
+ ) -> dict:
302
+ if print_report:
303
+ print(f"\n{'=' * 88}")
304
+ print(f"{label} — 效率明细")
305
+ print("=" * 88)
306
+
307
+ if log_path and log_path.is_file():
308
+ tasks = parse_agent_log(log_path)
309
+ if tasks:
310
+ summary = summarize_tasks(tasks, print_report=print_report)
311
+ summary["source"] = f"日志 {log_path}"
312
+ return summary
313
+
314
+ if print_report:
315
+ print(f"日志: {log_path} (不存在或未解析到 Evaluate 段)")
316
+ print("\n从 JSONL agent_usage 逐题 Token:")
317
+ print(f"{'ID':<6}{'Token':>10}{'agent_backend':>20}")
318
+ print("-" * 40)
319
+ tokens: list[int] = []
320
+ for r in rows:
321
+ tok = row_tokens(r)
322
+ if tok is not None:
323
+ tokens.append(tok)
324
+ if print_report:
325
+ print(
326
+ f"{str(r.get('id', '')):<6}"
327
+ f"{tok if tok is not None else 'N/A':>10}"
328
+ f"{str(r.get('agent_backend', '')):>20}"
329
+ )
330
+ summary = summarize_jsonl_usage(rows)
331
+ summary["source"] = "JSONL agent_usage"
332
+ if print_report:
333
+ print("-" * 40)
334
+ print(f"平均 Token: {summary['avg_tokens']:.0f}" if summary.get("avg_tokens") else "平均 Token: N/A")
335
+ print(f"合计 Token: {summary.get('total_tokens', 0)}")
336
+ print("=" * 88)
337
+ return summary
338
+
339
+
340
+ def print_full_ab_report(
341
+ *,
342
+ with_scored: Path,
343
+ without_scored: Path,
344
+ with_log: Path | None,
345
+ without_log: Path | None,
346
+ rows_with: list[dict],
347
+ rows_without: list[dict],
348
+ ) -> None:
349
+ stats_with = compute_stats_from_rows(rows_with)
350
+ stats_without = compute_stats_from_rows(rows_without)
351
+
352
+ print("\n")
353
+ print("#" * 88)
354
+ print("#" + " " * 30 + "A/B 完整评测报告" + " " * 30 + "#")
355
+ print("#" * 88)
356
+
357
+ print("\n【输入文件】")
358
+ print(f" with scored : {with_scored}")
359
+ print(f" without scored: {without_scored}")
360
+ print(f" with log : {with_log} {'(存在)' if with_log and with_log.is_file() else '(无)'}")
361
+ print(f" without log : {without_log} {'(存在)' if without_log and without_log.is_file() else '(无)'}")
362
+
363
+ eff_with = print_efficiency_section("WITH homegraph", with_log, rows_with)
364
+ eff_without = print_efficiency_section("WITHOUT homegraph", without_log, rows_without)
365
+
366
+ print_statistics(stats_with, with_scored, title="【WITH homegraph】准确率统计")
367
+ print_statistics(stats_without, without_scored, title="【WITHOUT homegraph】准确率统计")
368
+
369
+ print_category_table(rows_with, rows_without)
370
+ print_per_item_table(rows_with, rows_without)
371
+ print_trajectory_section(rows_with, rows_without)
372
+
373
+ w = 22
374
+ print("\n" + "=" * 88)
375
+ print("【A/B 汇总表】")
376
+ print("=" * 88)
377
+ print(f"{'指标':<24}{'with':>{w}}{'without':>{w}}{'Δ (with−without)':>{w}}")
378
+ print("-" * 88)
379
+
380
+ def sm(s: dict | None) -> float | None:
381
+ return float(s["mean"]) if s and s.get("valid_scores") else None
382
+
383
+ def sd(s: dict | None) -> float | None:
384
+ return float(s["median"]) if s and s.get("valid_scores") else None
385
+
386
+ print(
387
+ f"{'准确率 均值':<24}"
388
+ f"{fmt_num(sm(stats_with), digits=4):>{w}}"
389
+ f"{fmt_num(sm(stats_without), digits=4):>{w}}"
390
+ f"{fmt_delta(sm(stats_with), sm(stats_without), digits=4):>{w}}"
391
+ )
392
+ print(
393
+ f"{'准确率 中位数':<24}"
394
+ f"{fmt_num(sd(stats_with), digits=4):>{w}}"
395
+ f"{fmt_num(sd(stats_without), digits=4):>{w}}"
396
+ f"{fmt_delta(sd(stats_with), sd(stats_without), digits=4):>{w}}"
397
+ )
398
+ jw = f"{stats_with['success_samples']}/{stats_with['total_samples']}"
399
+ jo = f"{stats_without['success_samples']}/{stats_without['total_samples']}"
400
+ print(
401
+ f"{'Judge 成功/总数':<24}"
402
+ f"{jw:>{w}}"
403
+ f"{jo:>{w}}"
404
+ f"{'—':>{w}}"
405
+ )
406
+ print("-" * 88)
407
+ print(
408
+ f"{'平均轮次':<24}"
409
+ f"{fmt_num(eff_with.get('avg_turns')):>{w}}"
410
+ f"{fmt_num(eff_without.get('avg_turns')):>{w}}"
411
+ f"{fmt_delta(eff_with.get('avg_turns'), eff_without.get('avg_turns')):>{w}}"
412
+ )
413
+ print(
414
+ f"{'平均耗时 (秒)':<24}"
415
+ f"{fmt_num(eff_with.get('avg_duration_s')):>{w}}"
416
+ f"{fmt_num(eff_without.get('avg_duration_s')):>{w}}"
417
+ f"{fmt_delta(eff_with.get('avg_duration_s'), eff_without.get('avg_duration_s')):>{w}}"
418
+ )
419
+ tw = eff_with.get("avg_tokens")
420
+ two = eff_without.get("avg_tokens")
421
+ print(
422
+ f"{'平均 Token (k)':<24}"
423
+ f"{fmt_num(tw / 1000 if tw else None, digits=2):>{w}}"
424
+ f"{fmt_num(two / 1000 if two else None, digits=2):>{w}}"
425
+ f"{fmt_delta(tw / 1000 if tw else None, two / 1000 if two else None, digits=2):>{w}}"
426
+ )
427
+ print(
428
+ f"{'平均首响应 (秒)':<24}"
429
+ f"{fmt_num(eff_with.get('avg_first_token_s')):>{w}}"
430
+ f"{fmt_num(eff_without.get('avg_first_token_s')):>{w}}"
431
+ f"{fmt_delta(eff_with.get('avg_first_token_s'), eff_without.get('avg_first_token_s')):>{w}}"
432
+ )
433
+ print(
434
+ f"{'平均峰值内存 (MB)':<24}"
435
+ f"{fmt_num(eff_with.get('avg_peak_rss_mb')):>{w}}"
436
+ f"{fmt_num(eff_without.get('avg_peak_rss_mb')):>{w}}"
437
+ f"{fmt_delta(eff_with.get('avg_peak_rss_mb'), eff_without.get('avg_peak_rss_mb')):>{w}}"
438
+ )
439
+ print(
440
+ f"{'最大峰值内存 (MB)':<24}"
441
+ f"{fmt_num(eff_with.get('max_peak_rss_mb')):>{w}}"
442
+ f"{fmt_num(eff_without.get('max_peak_rss_mb')):>{w}}"
443
+ f"{fmt_delta(eff_with.get('max_peak_rss_mb'), eff_without.get('max_peak_rss_mb')):>{w}}"
444
+ )
445
+ print("-" * 88)
446
+ print("效率数据来源:")
447
+ print(f" with : {eff_with.get('source', '?')}")
448
+ print(f" without : {eff_without.get('source', '?')}")
449
+ print("=" * 88)
450
+
451
+
452
+ def run_agent_stage(
453
+ *,
454
+ agent_host: str,
455
+ repo: Path,
456
+ dataset: list[dict],
457
+ with_jsonl: Path,
458
+ without_jsonl: Path,
459
+ with_log: Path | None,
460
+ without_log: Path | None,
461
+ llm,
462
+ hg_bin: str | None,
463
+ max_turns: int,
464
+ deveco_model: str | None = None,
465
+ deveco_attach: str | None = None,
466
+ ) -> None:
467
+ if agent_host == HOST_CLAUDE:
468
+ verify_claude_login()
469
+ for arm, out, log in (
470
+ ("with", with_jsonl, with_log),
471
+ ("without", without_jsonl, without_log),
472
+ ):
473
+ label = "WITH homegraph" if arm == "with" else "WITHOUT (grep/read only)"
474
+ print(f"\n>>> Agent [{agent_host}] [{label}] → {out}")
475
+ if agent_host == AGENT_HOST_BUILTIN:
476
+ run_agent_dataset(
477
+ repo,
478
+ dataset,
479
+ arm=arm,
480
+ output=out,
481
+ log_file=log,
482
+ api_key=llm.api_key,
483
+ base_url=llm.base_url,
484
+ model=llm.model,
485
+ hg_bin=hg_bin,
486
+ max_turns=max_turns,
487
+ extra_body=llm.extra_body,
488
+ )
489
+ else:
490
+ run_external_dataset(
491
+ agent_host,
492
+ repo,
493
+ dataset,
494
+ arm=arm,
495
+ output=out,
496
+ log_file=log,
497
+ hg_bin=hg_bin or "",
498
+ model=deveco_model if agent_host == HOST_DEVECO else None,
499
+ deveco_attach=deveco_attach if agent_host == HOST_DEVECO else None,
500
+ )
501
+
502
+
503
+ def cmd_ab(args: argparse.Namespace) -> int:
504
+ if args.agent_host == "all":
505
+ return cmd_hosts(args)
506
+
507
+ agent_host = args.agent_host
508
+ paths = paths_for_host(agent_host)
509
+ if args.with_scored:
510
+ with_scored = Path(args.with_scored)
511
+ else:
512
+ with_scored = paths["with_scored"]
513
+ if args.without_scored:
514
+ without_scored = Path(args.without_scored)
515
+ else:
516
+ without_scored = paths["without_scored"]
517
+ if args.with_jsonl:
518
+ with_jsonl = Path(args.with_jsonl)
519
+ else:
520
+ with_jsonl = paths["with_jsonl"]
521
+ if args.without_jsonl:
522
+ without_jsonl = Path(args.without_jsonl)
523
+ else:
524
+ without_jsonl = paths["without_jsonl"]
525
+ with_log = Path(args.with_log) if args.with_log else paths["with_log"]
526
+ without_log = Path(args.without_log) if args.without_log else paths["without_log"]
527
+ repo = require_repo_arg(getattr(args, "repo", None), no_agent=args.no_agent)
528
+ if repo is None and not args.no_agent:
529
+ return 1
530
+ dataset_path = Path(args.dataset).expanduser().resolve()
531
+
532
+ try:
533
+ llm = resolve_llm_config(
534
+ provider=getattr(args, "provider", None),
535
+ model=getattr(args, "model", None),
536
+ base_url=getattr(args, "base_url", None),
537
+ )
538
+ except RuntimeError as e:
539
+ print(f"错误: {e}")
540
+ return 1
541
+
542
+ if not args.no_agent:
543
+ if not dataset_path.is_file():
544
+ print(f"错误: 测试集不存在: {dataset_path}")
545
+ return 1
546
+
547
+ dataset = load_jsonl(dataset_path)
548
+ if not getattr(args, "keep_log", False):
549
+ removed, skipped = clear_log_dir(LOG_DIR)
550
+ print(f"已清空 log 目录: {LOG_DIR} ({removed} 项)", flush=True)
551
+ if skipped:
552
+ print("⚠ 以下项被占用,未删除(关闭占用进程后可手动删):", flush=True)
553
+ for item in skipped:
554
+ print(f" - {item}", flush=True)
555
+ print("=" * 60)
556
+ print(f"Stage 1 — Agent 跑题 ({len(dataset)} 条) host={agent_host}")
557
+ print(f" LLM : {llm.provider}")
558
+ print(f" 仓库 : {repo}")
559
+ print(f" 测试集 : {dataset_path}")
560
+ print(f" 报告 : {paths['report']}")
561
+ print("=" * 60)
562
+
563
+ assert repo is not None
564
+ run_agent_stage(
565
+ agent_host=agent_host,
566
+ repo=repo,
567
+ dataset=dataset,
568
+ with_jsonl=with_jsonl,
569
+ without_jsonl=without_jsonl,
570
+ with_log=with_log,
571
+ without_log=without_log,
572
+ llm=llm,
573
+ hg_bin=args.homegraph_bin,
574
+ max_turns=args.max_turns,
575
+ deveco_model=getattr(args, "deveco_model", None),
576
+ deveco_attach=getattr(args, "deveco_attach", None),
577
+ )
578
+
579
+ if not args.no_judge:
580
+ print("\n" + "=" * 60)
581
+ print(f"Stage 2 Judge 打分 ({llm.provider})")
582
+ print("=" * 60)
583
+ if run_judge(with_jsonl, with_scored, workers=args.workers, llm=llm) != 0:
584
+ return 1
585
+ if run_judge(without_jsonl, without_scored, workers=args.workers, llm=llm) != 0:
586
+ return 1
587
+
588
+ if not with_scored.is_file() or not without_scored.is_file():
589
+ print("错误: 缺少 scored 结果。请去掉 --no-agent --no-judge 完整跑一遍,或提供 scored 文件。")
590
+ return 1
591
+
592
+ rows_with = load_jsonl(with_scored)
593
+ rows_without = load_jsonl(without_scored)
594
+
595
+ print("\n" + "=" * 60)
596
+ print("Stage 3–4 — 效率统计 + A/B 对比报告")
597
+ print("=" * 60)
598
+
599
+ eff_with = print_efficiency_section("WITH homegraph", with_log, rows_with)
600
+ eff_without = print_efficiency_section("WITHOUT homegraph", without_log, rows_without)
601
+
602
+ report_path = Path(args.report).expanduser().resolve() if args.report else paths["report"]
603
+ report_path.parent.mkdir(parents=True, exist_ok=True)
604
+
605
+ buf = io.StringIO()
606
+ with redirect_stdout(buf):
607
+ print_full_ab_report(
608
+ with_scored=with_scored,
609
+ without_scored=without_scored,
610
+ with_log=with_log,
611
+ without_log=without_log,
612
+ rows_with=rows_with,
613
+ rows_without=rows_without,
614
+ )
615
+
616
+ report_path.write_text(buf.getvalue(), encoding="utf-8")
617
+ print(f"\nA/B 完整报告已写入: {report_path}")
618
+ return 0
619
+
620
+
621
+ def cmd_score(args: argparse.Namespace) -> int:
622
+ input_jsonl = Path(args.input)
623
+ scored = Path(args.scored) if args.scored else input_jsonl.with_name(input_jsonl.stem + "-scored.jsonl")
624
+ cmd = [
625
+ sys.executable,
626
+ str(_SCRIPT_DIR / "eval_metrics.py"),
627
+ "-i",
628
+ str(input_jsonl),
629
+ "-o",
630
+ str(scored),
631
+ "-w",
632
+ str(args.workers),
633
+ ]
634
+ if getattr(args, "provider", None):
635
+ cmd.extend(["--provider", args.provider])
636
+ if args.model:
637
+ cmd.extend(["-m", args.model])
638
+ rc = subprocess.call(cmd)
639
+ if rc != 0:
640
+ return rc
641
+ rows = load_jsonl(scored)
642
+ print_statistics(compute_stats_from_rows(rows), scored)
643
+ if args.log:
644
+ print_efficiency_section("单路", Path(args.log), rows)
645
+ return 0
646
+
647
+
648
+ def cmd_hosts(args: argparse.Namespace) -> int:
649
+ """Run full A/B for each Agent host (builtin, claude-code, deveco-code)."""
650
+ repo = require_repo_arg(getattr(args, "repo", None), no_agent=args.no_agent)
651
+ if repo is None and not args.no_agent:
652
+ return 1
653
+
654
+ hosts_raw = getattr(args, "agent_hosts", None) or ",".join(ALL_AGENT_HOSTS)
655
+ hosts = [h.strip() for h in hosts_raw.split(",") if h.strip()]
656
+ unknown = [h for h in hosts if h not in ALL_AGENT_HOSTS]
657
+ if unknown:
658
+ print(f"错误: 未知 agent host: {unknown},可选: {', '.join(ALL_AGENT_HOSTS)}")
659
+ return 1
660
+
661
+ rc = 0
662
+ for host in hosts:
663
+ print("\n" + "#" * 88)
664
+ print(f"# Agent host: {host}")
665
+ print("#" * 88)
666
+ host_args = argparse.Namespace(
667
+ repo=args.repo,
668
+ dataset=args.dataset,
669
+ with_scored=None,
670
+ without_scored=None,
671
+ with_jsonl=None,
672
+ without_jsonl=None,
673
+ with_log=None,
674
+ without_log=None,
675
+ workers=args.workers,
676
+ provider=getattr(args, "provider", None),
677
+ model=args.model,
678
+ base_url=args.base_url,
679
+ homegraph_bin=args.homegraph_bin,
680
+ max_turns=args.max_turns,
681
+ no_agent=args.no_agent,
682
+ no_judge=args.no_judge,
683
+ keep_log=getattr(args, "keep_log", False),
684
+ report=None,
685
+ agent_host=host,
686
+ deveco_model=getattr(args, "deveco_model", None),
687
+ deveco_attach=getattr(args, "deveco_attach", None),
688
+ )
689
+ if cmd_ab(host_args) != 0:
690
+ rc = 1
691
+ if rc == 0:
692
+ print("\n" + "=" * 60)
693
+ print("全部宿主跑完,报告文件:")
694
+ for host in hosts:
695
+ p = paths_for_host(host)["report"]
696
+ print(f" {host}: {p}")
697
+ print("=" * 60)
698
+ return rc
699
+
700
+
701
+ def main() -> int:
702
+ parser = argparse.ArgumentParser(description="QA Eval A/B — 完整打印全部对比结果")
703
+ sub = parser.add_subparsers(dest="command", required=True)
704
+
705
+ p_ab = sub.add_parser("ab", help="一条龙:Agent → Judge → 完整报告(默认会更新全部数据)")
706
+ p_ab.add_argument(
707
+ "--repo",
708
+ "-r",
709
+ default=None,
710
+ help="被测仓库绝对路径(跑 Agent 时必填;仅 --no-agent 重打报告时可省略)",
711
+ )
712
+ p_ab.add_argument("--dataset", "-d", default=str(DEFAULT_DATASET))
713
+ p_ab.add_argument(
714
+ "--agent-host",
715
+ default=AGENT_HOST_BUILTIN,
716
+ choices=[*ALL_AGENT_HOSTS, "all"],
717
+ help="Agent 宿主: builtin(Python+Qwen) | claude-code | deveco-code | all",
718
+ )
719
+ p_ab.add_argument("--with-scored", default=None)
720
+ p_ab.add_argument("--without-scored", default=None)
721
+ p_ab.add_argument("--with-jsonl", default=None)
722
+ p_ab.add_argument("--without-jsonl", default=None)
723
+ p_ab.add_argument("--with-log", default=None)
724
+ p_ab.add_argument("--without-log", default=None)
725
+ p_ab.add_argument("--workers", "-w", type=int, default=1)
726
+ p_ab.add_argument(
727
+ "--provider",
728
+ choices=[PROVIDER_DASHSCOPE, PROVIDER_ZHIPU],
729
+ default=None,
730
+ help=provider_help(),
731
+ )
732
+ p_ab.add_argument("--model", "-m", default=None, help="LLM 模型(默认随 provider)")
733
+ p_ab.add_argument("--base-url", default=None, help="OpenAI 兼容 API 端点(默认随 provider)")
734
+ p_ab.add_argument("--homegraph-bin", default=None)
735
+ p_ab.add_argument("--max-turns", type=int, default=8, help="Agent 最多工具轮次")
736
+ p_ab.add_argument(
737
+ "--deveco-model",
738
+ default=None,
739
+ help="DevEco Agent 模型(provider/model,如 zhipuai/glm-4.5-flash);默认用 DevEco 自身配置,不用 --model",
740
+ )
741
+ p_ab.add_argument(
742
+ "--deveco-attach",
743
+ default=os.environ.get("QA_EVAL_DEVECO_ATTACH"),
744
+ help="已运行的 deveco serve 地址(如 http://127.0.0.1:4096),复用进程避免每题冷启动",
745
+ )
746
+ p_ab.add_argument("--no-agent", action="store_true", help="不跑 Agent,沿用已有 JSONL")
747
+ p_ab.add_argument("--no-judge", action="store_true", help="不跑 Judge,沿用已有 scored")
748
+ p_ab.add_argument(
749
+ "--keep-log",
750
+ action="store_true",
751
+ help="保留 scripts/qa_eval/log 下旧文件(默认每次跑 Agent 前清空)",
752
+ )
753
+ p_ab.add_argument(
754
+ "--report",
755
+ default=None,
756
+ help="A/B 完整报告输出路径(默认 data/report-{host}.txt,如 report-deveco.txt)",
757
+ )
758
+ p_ab.set_defaults(func=cmd_ab)
759
+
760
+ p_hosts = sub.add_parser("hosts", help="依次跑 builtin + claude-code + deveco-code 的 A/B")
761
+ p_hosts.add_argument(
762
+ "--repo",
763
+ "-r",
764
+ default=None,
765
+ help="被测仓库绝对路径(跑 Agent 时必填;仅 --no-agent 重打报告时可省略)",
766
+ )
767
+ p_hosts.add_argument("--dataset", "-d", default=str(DEFAULT_DATASET))
768
+ p_hosts.add_argument("--workers", "-w", type=int, default=1)
769
+ p_hosts.add_argument("--provider", choices=[PROVIDER_DASHSCOPE, PROVIDER_ZHIPU], default=None)
770
+ p_hosts.add_argument("--model", "-m", default=None)
771
+ p_hosts.add_argument("--base-url", default=None)
772
+ p_hosts.add_argument("--homegraph-bin", default=None)
773
+ p_hosts.add_argument("--max-turns", type=int, default=8)
774
+ p_hosts.add_argument("--deveco-model", default=None)
775
+ p_hosts.add_argument(
776
+ "--deveco-attach",
777
+ default=os.environ.get("QA_EVAL_DEVECO_ATTACH"),
778
+ help="deveco serve 地址,见 ab --deveco-attach",
779
+ )
780
+ p_hosts.add_argument("--no-agent", action="store_true")
781
+ p_hosts.add_argument("--no-judge", action="store_true")
782
+ p_hosts.add_argument("--keep-log", action="store_true")
783
+ p_hosts.add_argument(
784
+ "--agent-hosts",
785
+ default=",".join(ALL_AGENT_HOSTS),
786
+ help="逗号分隔: builtin,claude-code,deveco-code",
787
+ )
788
+ p_hosts.set_defaults(func=cmd_hosts)
789
+
790
+ p_score = sub.add_parser("score", help="单路:Agent JSONL → Judge → 统计")
791
+ p_score.add_argument("--input", "-i", required=True)
792
+ p_score.add_argument("--scored", "-o", default=None)
793
+ p_score.add_argument("--log", "-l", default=None)
794
+ p_score.add_argument("--workers", "-w", type=int, default=1)
795
+ p_score.add_argument("--provider", choices=[PROVIDER_DASHSCOPE, PROVIDER_ZHIPU], default=None)
796
+ p_score.add_argument("--model", "-m", default=None)
797
+ p_score.set_defaults(func=cmd_score)
798
+
799
+ args = parser.parse_args()
800
+ return args.func(args)
801
+
802
+
803
+ if __name__ == "__main__":
804
+ raise SystemExit(main())