homegraph 1.1.2 → 1.1.3
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.
- package/LICENSE +21 -21
- package/README.md +243 -243
- package/dist/bin/homegraph.js +371 -9
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/migrations.js +18 -18
- package/dist/db/queries.js +140 -140
- package/dist/db/schema.sql +152 -152
- package/dist/directory.js +5 -5
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/installer/instructions-template.js +9 -9
- package/dist/installer/targets/shared.d.ts +5 -6
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +5 -6
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/server-instructions.js +69 -69
- package/dist/mcp/tools.d.ts +17 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +192 -22
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/spec/config.d.ts +43 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +275 -0
- package/dist/spec/config.js.map +1 -0
- package/dist/spec/db/commit-node.d.ts +23 -0
- package/dist/spec/db/commit-node.d.ts.map +1 -0
- package/dist/spec/db/commit-node.js +62 -0
- package/dist/spec/db/commit-node.js.map +1 -0
- package/dist/spec/db/fragment-node.d.ts +23 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +120 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +60 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +285 -0
- package/dist/spec/db/fts.js.map +1 -0
- package/dist/spec/db/index.d.ts +13 -0
- package/dist/spec/db/index.d.ts.map +1 -0
- package/dist/spec/db/index.js +50 -0
- package/dist/spec/db/index.js.map +1 -0
- package/dist/spec/db/relations.d.ts +55 -0
- package/dist/spec/db/relations.d.ts.map +1 -0
- package/dist/spec/db/relations.js +158 -0
- package/dist/spec/db/relations.js.map +1 -0
- package/dist/spec/db/schema.d.ts +30 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +87 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +107 -0
- package/dist/spec/db/spec-node.d.ts +41 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -0
- package/dist/spec/db/spec-node.js +114 -0
- package/dist/spec/db/spec-node.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +13 -0
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
- package/dist/spec/evolve/impact-locator.js +25 -0
- package/dist/spec/evolve/impact-locator.js.map +1 -0
- package/dist/spec/evolve/llm-client.d.ts +50 -0
- package/dist/spec/evolve/llm-client.d.ts.map +1 -0
- package/dist/spec/evolve/llm-client.js +176 -0
- package/dist/spec/evolve/llm-client.js.map +1 -0
- package/dist/spec/evolve/logic-checker.d.ts +12 -0
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
- package/dist/spec/evolve/logic-checker.js +48 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +40 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +314 -0
- package/dist/spec/evolve/pipeline.js.map +1 -0
- package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
- package/dist/spec/evolve/spec-rewriter.js +254 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +58 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +215 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/mining/diff-parser.d.ts +33 -0
- package/dist/spec/mining/diff-parser.d.ts.map +1 -0
- package/dist/spec/mining/diff-parser.js +166 -0
- package/dist/spec/mining/diff-parser.js.map +1 -0
- package/dist/spec/mining/git-scanner.d.ts +91 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +266 -0
- package/dist/spec/mining/git-scanner.js.map +1 -0
- package/dist/spec/mining/pipeline.d.ts +53 -0
- package/dist/spec/mining/pipeline.d.ts.map +1 -0
- package/dist/spec/mining/pipeline.js +165 -0
- package/dist/spec/mining/pipeline.js.map +1 -0
- package/dist/spec/mining/scope-resolver.d.ts +45 -0
- package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
- package/dist/spec/mining/scope-resolver.js +103 -0
- package/dist/spec/mining/scope-resolver.js.map +1 -0
- package/dist/spec/mining/spec-extractor.d.ts +69 -0
- package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
- package/dist/spec/mining/spec-extractor.js +369 -0
- package/dist/spec/mining/spec-extractor.js.map +1 -0
- package/dist/spec/types.d.ts +149 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +15 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/utils.d.ts +166 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +461 -0
- package/dist/spec/utils.js.map +1 -0
- package/package.json +58 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- package/scripts/agent-eval/ab-adoption.sh +91 -91
- package/scripts/agent-eval/ab-hook.sh +86 -86
- package/scripts/agent-eval/ab-impl.sh +78 -78
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
- package/scripts/agent-eval/ab-sufficiency.sh +78 -78
- package/scripts/agent-eval/arms-F.sh +21 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
- package/scripts/agent-eval/offload-eval-setup.sh +24 -24
- package/scripts/agent-eval/offload-eval-styles.sh +71 -71
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
- package/scripts/agent-eval/offload-eval.md +76 -76
- package/scripts/agent-eval/parse-arms.mjs +116 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -38
- package/scripts/agent-eval/run-agent.sh +34 -34
- package/scripts/agent-eval/run-all.sh +69 -69
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +246 -246
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +407 -404
- package/scripts/qa_eval/_test_deveco_probe.py +41 -41
- package/scripts/qa_eval/agent_runner.py +526 -526
- package/scripts/qa_eval/data/.gitignore +4 -4
- package/scripts/qa_eval/data/test-set.jsonl +2 -22
- package/scripts/qa_eval/eval_metrics.py +274 -233
- package/scripts/qa_eval/external_agent.py +976 -671
- package/scripts/qa_eval/llm_config.py +92 -92
- package/scripts/qa_eval/memory_monitor.py +132 -132
- package/scripts/qa_eval/my_answer_accuracy.py +187 -187
- package/scripts/qa_eval/requirements.txt +2 -2
- package/scripts/qa_eval/run_pipeline.py +804 -711
- package/scripts/qa_eval/stats_efficiency.py +279 -279
- package/scripts/qa_eval/stats_scores.py +207 -207
|
@@ -1,671 +1,976 @@
|
|
|
1
|
-
"""Run qa_eval dataset through real Agent hosts: Claude Code CLI, DevEco Code (opencode CLI)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
|
-
import shutil
|
|
10
|
-
import subprocess
|
|
11
|
-
import tempfile
|
|
12
|
-
import time
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
from memory_monitor import sample_memory
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
for line in raw.splitlines():
|
|
238
|
-
line = line.strip()
|
|
239
|
-
if not line:
|
|
240
|
-
continue
|
|
241
|
-
try:
|
|
242
|
-
ev = json.loads(line)
|
|
243
|
-
except json.JSONDecodeError:
|
|
244
|
-
continue
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
"
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
"
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"
|
|
551
|
-
"
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1
|
+
"""Run qa_eval dataset through real Agent hosts: Claude Code CLI, DevEco Code (opencode CLI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from memory_monitor import sample_memory
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_QA_EVAL_DIR = Path(__file__).resolve().parent
|
|
22
|
+
|
|
23
|
+
TIME_FMT = "%Y-%m-%d %H:%M:%S.%f"
|
|
24
|
+
|
|
25
|
+
HOST_CLAUDE = "claude-code"
|
|
26
|
+
HOST_DEVECO = "deveco-code"
|
|
27
|
+
SUPPORTED_HOSTS = (HOST_CLAUDE, HOST_DEVECO)
|
|
28
|
+
|
|
29
|
+
# DevEco / opencode MCP tool names (server "homegraph" may prefix once or twice).
|
|
30
|
+
_HOMEGRAPH_TOOL_RE = re.compile(r"homegraph(?:_homegraph)?_(?:explore|node|search|callers|callees)", re.I)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def trace_tool_names(output_answer: str) -> list[str]:
|
|
34
|
+
return re.findall(r"---\n([^\n]+)\n", str(output_answer or ""))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_homegraph_tool(name: str) -> bool:
|
|
38
|
+
n = str(name or "").strip()
|
|
39
|
+
if not n:
|
|
40
|
+
return False
|
|
41
|
+
if _HOMEGRAPH_TOOL_RE.search(n):
|
|
42
|
+
return True
|
|
43
|
+
return n.lower() in ("homegraph_explore", "homegraph_node")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def output_used_homegraph(output_answer: str) -> bool:
|
|
47
|
+
return any(is_homegraph_tool(n) for n in trace_tool_names(output_answer))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _tools_from_session_export(raw: str) -> list[str]:
|
|
51
|
+
tools: list[str] = []
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(raw.strip())
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
return tools
|
|
56
|
+
if not isinstance(data, dict):
|
|
57
|
+
return tools
|
|
58
|
+
for msg in data.get("messages") or []:
|
|
59
|
+
if not isinstance(msg, dict):
|
|
60
|
+
continue
|
|
61
|
+
for part in msg.get("parts") or []:
|
|
62
|
+
if not isinstance(part, dict) or part.get("type") != "tool":
|
|
63
|
+
continue
|
|
64
|
+
t = part.get("tool")
|
|
65
|
+
if t:
|
|
66
|
+
tools.append(str(t))
|
|
67
|
+
return tools
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _deveco_with_query(query: str) -> str:
|
|
71
|
+
return (
|
|
72
|
+
"【homegraph 评测臂】请优先用 MCP 工具 homegraph_explore(query 用原问题里的符号名,"
|
|
73
|
+
"如 getColorString);只回答题目问的那一个定义所在文件,不要列举其它同名函数。"
|
|
74
|
+
"若 homegraph 结果不足,再用 grep/read 补充。"
|
|
75
|
+
f"问题:{query}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_DEVECO_WITH_AGENT_PROMPT = (
|
|
80
|
+
"你是鸿蒙 ArkTS 代码仓库问答 Agent(homegraph 评测臂)。"
|
|
81
|
+
"回答代码定位/理解问题时,优先调用 MCP 工具 homegraph_explore;"
|
|
82
|
+
"仅当 homegraph 查不到或信息不够时,再用 grep/read/glob。"
|
|
83
|
+
"基于仓库事实作答,中文简洁准确。"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_DEVECO_WITHOUT_AGENT_PROMPT = (
|
|
87
|
+
"你是鸿蒙 ArkTS 代码仓库问答 Agent(baseline 臂,无 homegraph)。"
|
|
88
|
+
"用 grep、read 等内置工具探索仓库后作答,中文简洁准确。"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _strip_ansi(text: str) -> str:
|
|
93
|
+
return re.sub(r"\x1b\[[0-9;]*m", "", text or "")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _extract_deveco_json_errors(text: str) -> str | None:
|
|
97
|
+
"""Parse deveco --format json error lines into a short message."""
|
|
98
|
+
messages: list[str] = []
|
|
99
|
+
for line in _strip_ansi(text).splitlines():
|
|
100
|
+
line = line.strip()
|
|
101
|
+
if not line.startswith("{"):
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
ev = json.loads(line)
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
continue
|
|
107
|
+
if ev.get("type") != "error":
|
|
108
|
+
continue
|
|
109
|
+
err = ev.get("error") or {}
|
|
110
|
+
data = err.get("data") or {}
|
|
111
|
+
msg = data.get("message") or err.get("message") or err.get("name")
|
|
112
|
+
if msg and msg not in messages:
|
|
113
|
+
messages.append(str(msg))
|
|
114
|
+
if not messages:
|
|
115
|
+
return None
|
|
116
|
+
return "; ".join(messages)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _cli_error_summary(stderr: str, stdout: str, *, max_len: int = 400) -> str:
|
|
120
|
+
"""Pick a short user-facing error from CLI stderr/stdout."""
|
|
121
|
+
for raw in (stderr, stdout):
|
|
122
|
+
deveco_err = _extract_deveco_json_errors(raw)
|
|
123
|
+
if deveco_err:
|
|
124
|
+
return deveco_err[:max_len]
|
|
125
|
+
for raw in (stderr, stdout):
|
|
126
|
+
clean = _strip_ansi(raw).strip()
|
|
127
|
+
if not clean:
|
|
128
|
+
continue
|
|
129
|
+
for line in clean.splitlines():
|
|
130
|
+
line = line.strip()
|
|
131
|
+
if not line or line.lower().startswith("error:"):
|
|
132
|
+
continue
|
|
133
|
+
if len(line) > 20:
|
|
134
|
+
return line[:max_len]
|
|
135
|
+
if clean:
|
|
136
|
+
return clean[:max_len]
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def auth_failure_reason(text: str) -> str | None:
|
|
141
|
+
"""Return a user-facing reason if output looks like an auth/login failure."""
|
|
142
|
+
t = _strip_ansi(text or "").lower()
|
|
143
|
+
if any(h in t for h in ("not logged in", "please run /login")) or (
|
|
144
|
+
"/login" in t and "run" in t
|
|
145
|
+
):
|
|
146
|
+
return "Claude Code 未登录,请先运行: claude login"
|
|
147
|
+
if "credentials cannot be decrypted" in t or "saved provider credentials are unavailable" in t:
|
|
148
|
+
return (
|
|
149
|
+
"DevEco Code 本地凭证无法解密。请运行: deveco providers reset,"
|
|
150
|
+
"然后在 TUI 中重新配置模型,或执行 deveco providers login"
|
|
151
|
+
)
|
|
152
|
+
if "deveco providers reset" in t or "deveco auth reset" in t:
|
|
153
|
+
return (
|
|
154
|
+
"DevEco Code 未配置或凭证无效。请运行: deveco providers reset,"
|
|
155
|
+
"然后重新登录/配置 provider"
|
|
156
|
+
)
|
|
157
|
+
if "model not found" in t:
|
|
158
|
+
return (
|
|
159
|
+
"DevEco 模型名无效。deveco 需要 provider/model 格式(如 zhipuai/glm-4.5-flash),"
|
|
160
|
+
"可用 `deveco models` 查看;勿把 Judge 的 --model 直接传给 deveco"
|
|
161
|
+
)
|
|
162
|
+
return "API Key 无效"
|
|
163
|
+
if "authentication required" in t or "unauthorized" in t:
|
|
164
|
+
return "未授权,请检查登录或 API Key"
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _log_line(log_file: Path | None, msg: str) -> None:
|
|
169
|
+
if log_file is None:
|
|
170
|
+
return
|
|
171
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
with log_file.open("a", encoding="utf-8") as f:
|
|
173
|
+
f.write(f"{datetime.now().strftime(TIME_FMT)} {msg}\n")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _log_memory(log_file: Path | None, mem: dict[str, float | None]) -> None:
|
|
177
|
+
if mem.get("peak_rss_mb") is not None:
|
|
178
|
+
_log_line(log_file, f"peakRssMb = {mem['peak_rss_mb']}")
|
|
179
|
+
if mem.get("avg_rss_mb") is not None:
|
|
180
|
+
_log_line(log_file, f"avgRssMb = {mem['avg_rss_mb']}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def find_claude_cli() -> str:
|
|
184
|
+
found = shutil.which("claude")
|
|
185
|
+
if not found:
|
|
186
|
+
raise FileNotFoundError(
|
|
187
|
+
"未找到 claude CLI。请安装 Claude Code 并确保 `claude` 在 PATH 中。"
|
|
188
|
+
)
|
|
189
|
+
return found
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def find_deveco_cli() -> str:
|
|
193
|
+
for name in ("deveco", "opencode"):
|
|
194
|
+
found = shutil.which(name)
|
|
195
|
+
if found:
|
|
196
|
+
return found
|
|
197
|
+
raise FileNotFoundError(
|
|
198
|
+
"未找到 DevEco Code / opencode CLI。请安装 DevEco Code 或 opencode 并加入 PATH。"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _popen_capture(cmd: list[str], *, cwd: str) -> subprocess.Popen:
|
|
203
|
+
"""Run CLI with UTF-8 stdout/stderr (Windows default locale is often GBK)."""
|
|
204
|
+
return subprocess.Popen(
|
|
205
|
+
cmd,
|
|
206
|
+
cwd=cwd,
|
|
207
|
+
stdout=subprocess.PIPE,
|
|
208
|
+
stderr=subprocess.PIPE,
|
|
209
|
+
text=True,
|
|
210
|
+
encoding="utf-8",
|
|
211
|
+
errors="replace",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def write_mcp_config(path: Path, *, hg_command: str, hg_args: list[str]) -> None:
|
|
216
|
+
path.write_text(
|
|
217
|
+
json.dumps({"mcpServers": {"homegraph": {"command": hg_command, "args": hg_args}}}, indent=2)
|
|
218
|
+
+ "\n",
|
|
219
|
+
encoding="utf-8",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _split_hg_bin(hg_bin: str) -> tuple[str, list[str]]:
|
|
224
|
+
if hg_bin.startswith("node "):
|
|
225
|
+
parts = hg_bin.split(" ", 1)
|
|
226
|
+
return parts[0], parts[1].split() + ["serve", "--mcp"]
|
|
227
|
+
return hg_bin, ["serve", "--mcp"]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def parse_claude_stream_json(raw: str) -> dict[str, Any]:
|
|
231
|
+
tool_trace: list[str] = []
|
|
232
|
+
answer_parts: list[str] = []
|
|
233
|
+
total_tokens = 0
|
|
234
|
+
max_turn = 0
|
|
235
|
+
duration_ms = 0
|
|
236
|
+
|
|
237
|
+
for line in raw.splitlines():
|
|
238
|
+
line = line.strip()
|
|
239
|
+
if not line:
|
|
240
|
+
continue
|
|
241
|
+
try:
|
|
242
|
+
ev = json.loads(line)
|
|
243
|
+
except json.JSONDecodeError:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
if ev.get("type") == "assistant" and ev.get("message", {}).get("content"):
|
|
247
|
+
max_turn += 1
|
|
248
|
+
for block in ev["message"]["content"]:
|
|
249
|
+
if block.get("type") == "text" and block.get("text"):
|
|
250
|
+
answer_parts.append(str(block["text"]))
|
|
251
|
+
if block.get("type") == "tool_use":
|
|
252
|
+
name = block.get("name", "?")
|
|
253
|
+
inp = block.get("input") or {}
|
|
254
|
+
tool_trace.append(
|
|
255
|
+
f"---\n{name}\nargs: {json.dumps(inp, ensure_ascii=False)[:500]}\n---"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if ev.get("type") == "result":
|
|
259
|
+
usage = ev.get("usage") or {}
|
|
260
|
+
total_tokens = int(
|
|
261
|
+
(usage.get("input_tokens") or 0)
|
|
262
|
+
+ (usage.get("cache_read_input_tokens") or 0)
|
|
263
|
+
+ (usage.get("cache_creation_input_tokens") or 0)
|
|
264
|
+
+ (usage.get("output_tokens") or 0)
|
|
265
|
+
)
|
|
266
|
+
duration_ms = int(ev.get("duration_ms") or 0)
|
|
267
|
+
if ev.get("num_turns"):
|
|
268
|
+
max_turn = max(max_turn, int(ev["num_turns"]))
|
|
269
|
+
if ev.get("result") and not answer_parts:
|
|
270
|
+
answer_parts.append(str(ev["result"]))
|
|
271
|
+
|
|
272
|
+
answer = "\n".join(answer_parts).strip()
|
|
273
|
+
output = "\n\n".join(tool_trace + ([answer] if answer else []))
|
|
274
|
+
auth_err = auth_failure_reason(answer) or auth_failure_reason(raw)
|
|
275
|
+
if auth_err:
|
|
276
|
+
return {
|
|
277
|
+
"output_answer": output,
|
|
278
|
+
"agent_status": "error",
|
|
279
|
+
"agent_error": auth_err,
|
|
280
|
+
"agent_turns": max_turn,
|
|
281
|
+
"agent_duration_ms": duration_ms,
|
|
282
|
+
"agent_usage": {"total_tokens": total_tokens} if total_tokens else {},
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
"output_answer": output,
|
|
286
|
+
"agent_status": "success" if answer else "error",
|
|
287
|
+
"agent_turns": max_turn,
|
|
288
|
+
"agent_duration_ms": duration_ms,
|
|
289
|
+
"agent_usage": {"total_tokens": total_tokens} if total_tokens else {},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def parse_opencode_json_events(raw: str) -> dict[str, Any]:
|
|
294
|
+
tool_trace: list[str] = []
|
|
295
|
+
answer_parts: list[str] = []
|
|
296
|
+
total_tokens = 0
|
|
297
|
+
max_turn = 0
|
|
298
|
+
duration_ms = 0
|
|
299
|
+
session_id = ""
|
|
300
|
+
stream_errors: list[str] = []
|
|
301
|
+
|
|
302
|
+
def note_session(ev: dict[str, Any], part: dict[str, Any]) -> None:
|
|
303
|
+
nonlocal session_id
|
|
304
|
+
sid = ev.get("sessionID") or ev.get("sessionId") or part.get("sessionID") or part.get("sessionId")
|
|
305
|
+
if sid:
|
|
306
|
+
session_id = str(sid)
|
|
307
|
+
|
|
308
|
+
def append_tool(name: str, inp: Any, outp: Any = "") -> None:
|
|
309
|
+
payload = json.dumps(inp, ensure_ascii=False)[:500] if inp else ""
|
|
310
|
+
block = f"---\n{name}\nargs: {payload}\n---"
|
|
311
|
+
if outp:
|
|
312
|
+
out_s = str(outp)
|
|
313
|
+
block = f"---\n{name}\nargs: {payload}\noutput: {out_s[:800]}\n---"
|
|
314
|
+
tool_trace.append(block)
|
|
315
|
+
|
|
316
|
+
for line in raw.splitlines():
|
|
317
|
+
line = line.strip()
|
|
318
|
+
if not line:
|
|
319
|
+
continue
|
|
320
|
+
try:
|
|
321
|
+
ev = json.loads(line)
|
|
322
|
+
except json.JSONDecodeError:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
ev_type = ev.get("type") or ev.get("event")
|
|
326
|
+
part = ev.get("part") if isinstance(ev.get("part"), dict) else {}
|
|
327
|
+
part_type = part.get("type") or ""
|
|
328
|
+
note_session(ev, part)
|
|
329
|
+
|
|
330
|
+
if ev_type == "error":
|
|
331
|
+
err = ev.get("error") or {}
|
|
332
|
+
data = err.get("data") or {}
|
|
333
|
+
msg = data.get("message") or err.get("message") or err.get("name")
|
|
334
|
+
if msg:
|
|
335
|
+
stream_errors.append(str(msg))
|
|
336
|
+
|
|
337
|
+
if ev_type == "text" and part.get("text"):
|
|
338
|
+
text = str(part["text"]).strip()
|
|
339
|
+
if text:
|
|
340
|
+
answer_parts.append(text)
|
|
341
|
+
max_turn += 1
|
|
342
|
+
elif ev_type == "text" and ev.get("text"):
|
|
343
|
+
text = str(ev["text"]).strip()
|
|
344
|
+
if text:
|
|
345
|
+
answer_parts.append(text)
|
|
346
|
+
max_turn += 1
|
|
347
|
+
|
|
348
|
+
if ev_type == "tool_use":
|
|
349
|
+
max_turn += 1
|
|
350
|
+
state = part.get("state") if isinstance(part.get("state"), dict) else {}
|
|
351
|
+
name = part.get("tool") or part.get("name") or "?"
|
|
352
|
+
append_tool(str(name), state.get("input") or part.get("input"), state.get("output"))
|
|
353
|
+
elif ev_type in ("tool", "tool_call") or part_type in (
|
|
354
|
+
"tool",
|
|
355
|
+
"tool-invocation",
|
|
356
|
+
"tool_use",
|
|
357
|
+
"tool-call",
|
|
358
|
+
):
|
|
359
|
+
max_turn += 1
|
|
360
|
+
name = part.get("tool") or part.get("name") or ev.get("tool") or "?"
|
|
361
|
+
inp = part.get("input") or part.get("args") or part.get("state") or ev.get("input") or {}
|
|
362
|
+
if isinstance(inp, dict) and "input" in inp:
|
|
363
|
+
outp = inp.get("output")
|
|
364
|
+
inp = inp.get("input") or inp
|
|
365
|
+
else:
|
|
366
|
+
outp = ""
|
|
367
|
+
append_tool(str(name), inp, outp)
|
|
368
|
+
|
|
369
|
+
if ev_type in ("message", "assistant"):
|
|
370
|
+
content = ev.get("content") or ev.get("text") or ev.get("message")
|
|
371
|
+
if isinstance(content, str) and content.strip():
|
|
372
|
+
answer_parts.append(content.strip())
|
|
373
|
+
max_turn += 1
|
|
374
|
+
elif isinstance(content, list):
|
|
375
|
+
max_turn += 1
|
|
376
|
+
for block in content:
|
|
377
|
+
if isinstance(block, dict):
|
|
378
|
+
if block.get("type") == "text" and block.get("text"):
|
|
379
|
+
answer_parts.append(str(block["text"]))
|
|
380
|
+
if block.get("type") == "tool_use":
|
|
381
|
+
append_tool(
|
|
382
|
+
str(block.get("name", "?")),
|
|
383
|
+
block.get("input") or {},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
usage = ev.get("usage") or ev.get("tokens")
|
|
387
|
+
if isinstance(usage, dict):
|
|
388
|
+
total = usage.get("total") or usage.get("total_tokens")
|
|
389
|
+
if total:
|
|
390
|
+
total_tokens = max(total_tokens, int(total))
|
|
391
|
+
|
|
392
|
+
if ev_type == "step_finish":
|
|
393
|
+
tokens = part.get("tokens") if isinstance(part.get("tokens"), dict) else {}
|
|
394
|
+
step_total = tokens.get("total") or tokens.get("total_tokens")
|
|
395
|
+
if step_total:
|
|
396
|
+
total_tokens = max(total_tokens, int(step_total))
|
|
397
|
+
|
|
398
|
+
if ev.get("duration_ms"):
|
|
399
|
+
duration_ms = max(duration_ms, int(ev["duration_ms"]))
|
|
400
|
+
|
|
401
|
+
part_time = part.get("time") if isinstance(part.get("time"), dict) else {}
|
|
402
|
+
if part_time.get("start") and part_time.get("end"):
|
|
403
|
+
duration_ms = max(duration_ms, int(part_time["end"]) - int(part_time["start"]))
|
|
404
|
+
|
|
405
|
+
answer = "\n".join(answer_parts).strip()
|
|
406
|
+
output = "\n\n".join(tool_trace + ([answer] if answer else []))
|
|
407
|
+
base = {
|
|
408
|
+
"output_answer": output,
|
|
409
|
+
"agent_turns": max_turn,
|
|
410
|
+
"agent_duration_ms": duration_ms,
|
|
411
|
+
"agent_usage": {"total_tokens": total_tokens} if total_tokens else {},
|
|
412
|
+
"deveco_session_id": session_id or None,
|
|
413
|
+
}
|
|
414
|
+
auth_err = auth_failure_reason(answer) or auth_failure_reason(raw)
|
|
415
|
+
if auth_err:
|
|
416
|
+
return {
|
|
417
|
+
**base,
|
|
418
|
+
"agent_status": "error",
|
|
419
|
+
"agent_error": auth_err,
|
|
420
|
+
}
|
|
421
|
+
if stream_errors and not answer:
|
|
422
|
+
return {
|
|
423
|
+
**base,
|
|
424
|
+
"agent_status": "error",
|
|
425
|
+
"agent_error": "; ".join(stream_errors),
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
**base,
|
|
429
|
+
"agent_status": "success" if answer else "error",
|
|
430
|
+
"agent_error": None if answer else "deveco 未返回可解析的文本回答",
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _meaningful_text(text: str) -> bool:
|
|
435
|
+
return bool(str(text or "").strip())
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _extract_text_from_export(raw: str) -> list[str]:
|
|
439
|
+
"""Pull assistant answer text from `deveco export` (session JSON, JSONL, or array)."""
|
|
440
|
+
texts: list[str] = []
|
|
441
|
+
|
|
442
|
+
def collect_stream_event(ev: dict[str, Any]) -> None:
|
|
443
|
+
if ev.get("type") == "text":
|
|
444
|
+
t = ev.get("text")
|
|
445
|
+
if not t and isinstance(ev.get("part"), dict):
|
|
446
|
+
t = ev["part"].get("text")
|
|
447
|
+
if _meaningful_text(str(t or "")):
|
|
448
|
+
texts.append(str(t).strip())
|
|
449
|
+
return
|
|
450
|
+
part = ev.get("part") if isinstance(ev.get("part"), dict) else {}
|
|
451
|
+
if part.get("type") == "text" and _meaningful_text(str(part.get("text") or "")):
|
|
452
|
+
texts.append(str(part["text"]).strip())
|
|
453
|
+
|
|
454
|
+
def collect_session_export(data: dict[str, Any]) -> None:
|
|
455
|
+
for msg in data.get("messages") or []:
|
|
456
|
+
if not isinstance(msg, dict):
|
|
457
|
+
continue
|
|
458
|
+
info = msg.get("info") if isinstance(msg.get("info"), dict) else {}
|
|
459
|
+
if info.get("role") != "assistant":
|
|
460
|
+
continue
|
|
461
|
+
for part in msg.get("parts") or []:
|
|
462
|
+
if not isinstance(part, dict):
|
|
463
|
+
continue
|
|
464
|
+
if part.get("type") == "text" and _meaningful_text(str(part.get("text") or "")):
|
|
465
|
+
texts.append(str(part["text"]).strip())
|
|
466
|
+
|
|
467
|
+
stripped = raw.strip()
|
|
468
|
+
if not stripped:
|
|
469
|
+
return texts
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
data = json.loads(stripped)
|
|
473
|
+
except json.JSONDecodeError:
|
|
474
|
+
for line in stripped.splitlines():
|
|
475
|
+
line = line.strip()
|
|
476
|
+
if not line.startswith("{"):
|
|
477
|
+
continue
|
|
478
|
+
try:
|
|
479
|
+
collect_stream_event(json.loads(line))
|
|
480
|
+
except json.JSONDecodeError:
|
|
481
|
+
continue
|
|
482
|
+
return texts
|
|
483
|
+
|
|
484
|
+
if isinstance(data, dict) and isinstance(data.get("messages"), list):
|
|
485
|
+
collect_session_export(data)
|
|
486
|
+
return texts
|
|
487
|
+
if isinstance(data, list):
|
|
488
|
+
for ev in data:
|
|
489
|
+
if isinstance(ev, dict):
|
|
490
|
+
collect_stream_event(ev)
|
|
491
|
+
return texts
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _final_answer_from_export(raw: str) -> str:
|
|
495
|
+
"""Best-effort final assistant text from exported session."""
|
|
496
|
+
texts = _extract_text_from_export(raw)
|
|
497
|
+
return texts[-1] if texts else ""
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def export_deveco_session(
|
|
501
|
+
session_id: str,
|
|
502
|
+
dest: Path,
|
|
503
|
+
*,
|
|
504
|
+
cwd: str,
|
|
505
|
+
cli: str | None = None,
|
|
506
|
+
timeout_sec: int = 60,
|
|
507
|
+
) -> bool:
|
|
508
|
+
"""Save `deveco export <session_id>` to dest. Returns True if file written."""
|
|
509
|
+
if not session_id:
|
|
510
|
+
return False
|
|
511
|
+
exe = cli or find_deveco_cli()
|
|
512
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
513
|
+
proc = subprocess.run(
|
|
514
|
+
[exe, "export", session_id],
|
|
515
|
+
cwd=cwd,
|
|
516
|
+
capture_output=True,
|
|
517
|
+
text=True,
|
|
518
|
+
encoding="utf-8",
|
|
519
|
+
errors="replace",
|
|
520
|
+
timeout=timeout_sec,
|
|
521
|
+
)
|
|
522
|
+
body = (proc.stdout or "").strip()
|
|
523
|
+
if proc.returncode != 0 or not body:
|
|
524
|
+
logger.warning(
|
|
525
|
+
"deveco export %s failed (rc=%s): %s",
|
|
526
|
+
session_id,
|
|
527
|
+
proc.returncode,
|
|
528
|
+
(proc.stderr or "")[:200],
|
|
529
|
+
)
|
|
530
|
+
return False
|
|
531
|
+
dest.write_text(body + "\n", encoding="utf-8")
|
|
532
|
+
return True
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def supplement_from_session_export(parsed: dict[str, Any], export_path: Path) -> dict[str, Any]:
|
|
536
|
+
"""If stdout missed final text, recover from exported session file."""
|
|
537
|
+
if not export_path.is_file():
|
|
538
|
+
return parsed
|
|
539
|
+
raw = export_path.read_text(encoding="utf-8")
|
|
540
|
+
answer = _final_answer_from_export(raw)
|
|
541
|
+
if not answer:
|
|
542
|
+
return parsed
|
|
543
|
+
existing = str(parsed.get("output_answer") or "")
|
|
544
|
+
if answer not in existing:
|
|
545
|
+
parsed = {**parsed, "output_answer": (existing + "\n\n" + answer).strip() if existing else answer}
|
|
546
|
+
if parsed.get("agent_status") != "success":
|
|
547
|
+
parsed = {
|
|
548
|
+
**parsed,
|
|
549
|
+
"agent_status": "success",
|
|
550
|
+
"agent_error": None,
|
|
551
|
+
"agent_answer_source": "session_export",
|
|
552
|
+
}
|
|
553
|
+
return parsed
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def verify_claude_login() -> None:
|
|
557
|
+
"""Fail fast before burning the whole dataset on 'Not logged in'."""
|
|
558
|
+
claude = find_claude_cli()
|
|
559
|
+
proc = subprocess.run(
|
|
560
|
+
[claude, "-p", "ping", "--output-format", "text", "--max-turns", "1"],
|
|
561
|
+
capture_output=True,
|
|
562
|
+
text=True,
|
|
563
|
+
timeout=45,
|
|
564
|
+
cwd=os.getcwd(),
|
|
565
|
+
)
|
|
566
|
+
combined = f"{proc.stdout}\n{proc.stderr}"
|
|
567
|
+
reason = auth_failure_reason(combined)
|
|
568
|
+
if reason:
|
|
569
|
+
raise RuntimeError(f"{reason}\n(探测输出: {combined.strip()[:200]})")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def run_claude_query(
|
|
573
|
+
repo: Path,
|
|
574
|
+
query: str,
|
|
575
|
+
*,
|
|
576
|
+
arm: str,
|
|
577
|
+
hg_bin: str,
|
|
578
|
+
log_file: Path | None,
|
|
579
|
+
task_id: int,
|
|
580
|
+
timeout_sec: int = 600,
|
|
581
|
+
) -> dict[str, Any]:
|
|
582
|
+
claude = find_claude_cli()
|
|
583
|
+
backend = f"claude-code-{'with' if arm == 'with' else 'without'}-homegraph"
|
|
584
|
+
|
|
585
|
+
with tempfile.TemporaryDirectory(prefix="qa-eval-mcp-") as tmp:
|
|
586
|
+
mcp_path = Path(tmp) / "mcp.json"
|
|
587
|
+
if arm == "with":
|
|
588
|
+
cmd, base_args = _split_hg_bin(hg_bin)
|
|
589
|
+
write_mcp_config(
|
|
590
|
+
mcp_path,
|
|
591
|
+
hg_command=cmd,
|
|
592
|
+
hg_args=[*base_args, "--path", str(repo.resolve())],
|
|
593
|
+
)
|
|
594
|
+
mcp_args = ["--strict-mcp-config", "--mcp-config", str(mcp_path)]
|
|
595
|
+
else:
|
|
596
|
+
mcp_path.write_text('{"mcpServers":{}}\n', encoding="utf-8")
|
|
597
|
+
mcp_args = ["--strict-mcp-config", "--mcp-config", str(mcp_path)]
|
|
598
|
+
|
|
599
|
+
cmd = [
|
|
600
|
+
claude,
|
|
601
|
+
"-p",
|
|
602
|
+
query,
|
|
603
|
+
"--output-format",
|
|
604
|
+
"stream-json",
|
|
605
|
+
"--verbose",
|
|
606
|
+
"--permission-mode",
|
|
607
|
+
"bypassPermissions",
|
|
608
|
+
*mcp_args,
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
_log_line(log_file, f"Evaluate {task_id}:")
|
|
612
|
+
_log_line(log_file, "the 1 turn")
|
|
613
|
+
t0 = time.time()
|
|
614
|
+
|
|
615
|
+
proc = _popen_capture(cmd, cwd=str(repo))
|
|
616
|
+
with sample_memory(proc.pid) as sampler:
|
|
617
|
+
try:
|
|
618
|
+
stdout, stderr = proc.communicate(timeout=timeout_sec)
|
|
619
|
+
except subprocess.TimeoutExpired:
|
|
620
|
+
proc.kill()
|
|
621
|
+
stdout, stderr = proc.communicate()
|
|
622
|
+
mem = sampler.last_stats
|
|
623
|
+
_log_memory(log_file, mem)
|
|
624
|
+
return {
|
|
625
|
+
"output_answer": "",
|
|
626
|
+
"agent_status": "error",
|
|
627
|
+
"agent_error": f"timeout after {timeout_sec}s",
|
|
628
|
+
"agent_backend": backend,
|
|
629
|
+
"agent_memory_mb": mem,
|
|
630
|
+
}
|
|
631
|
+
mem = sampler.last_stats
|
|
632
|
+
|
|
633
|
+
if proc.returncode != 0 and not (stdout or "").strip():
|
|
634
|
+
_log_memory(log_file, mem)
|
|
635
|
+
return {
|
|
636
|
+
"output_answer": (stderr or "")[:2000],
|
|
637
|
+
"agent_status": "error",
|
|
638
|
+
"agent_error": f"claude exit {proc.returncode}",
|
|
639
|
+
"agent_backend": backend,
|
|
640
|
+
"agent_memory_mb": mem,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
_log_line(log_file, "first token")
|
|
644
|
+
combined = f"{stdout}\n{stderr}"
|
|
645
|
+
auth_err = auth_failure_reason(combined)
|
|
646
|
+
if auth_err:
|
|
647
|
+
_log_memory(log_file, mem)
|
|
648
|
+
return {
|
|
649
|
+
"output_answer": (stdout or stderr or "")[:2000],
|
|
650
|
+
"agent_status": "error",
|
|
651
|
+
"agent_error": auth_err,
|
|
652
|
+
"agent_backend": backend,
|
|
653
|
+
"agent_host": HOST_CLAUDE,
|
|
654
|
+
"agent_memory_mb": mem,
|
|
655
|
+
}
|
|
656
|
+
parsed = parse_claude_stream_json(stdout or "")
|
|
657
|
+
tokens = (parsed.get("agent_usage") or {}).get("total_tokens") or 0
|
|
658
|
+
if tokens:
|
|
659
|
+
_log_line(log_file, f"totalTokenCount = {tokens}")
|
|
660
|
+
_log_memory(log_file, mem)
|
|
661
|
+
|
|
662
|
+
wall_ms = int((time.time() - t0) * 1000)
|
|
663
|
+
_log_line(log_file, f"completed ({wall_ms}ms)")
|
|
664
|
+
return {
|
|
665
|
+
**parsed,
|
|
666
|
+
"agent_duration_ms": wall_ms,
|
|
667
|
+
"agent_backend": backend,
|
|
668
|
+
"agent_host": HOST_CLAUDE,
|
|
669
|
+
"ab_arm": "with-homegraph" if arm == "with" else "without-homegraph",
|
|
670
|
+
"agent_memory_mb": mem,
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _deveco_project_config_dir(repo: Path) -> Path:
|
|
675
|
+
"""Project-level deveco config (read before ~/.config/deveco/deveco.jsonc)."""
|
|
676
|
+
return repo / ".deveco"
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _write_deveco_project_mcp(repo: Path, *, arm: str, hg_bin: str) -> Path:
|
|
680
|
+
"""Write repo/.deveco/deveco.jsonc — MCP, permissions, and agent prompt for qa_eval A/B."""
|
|
681
|
+
config_dir = _deveco_project_config_dir(repo)
|
|
682
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
683
|
+
config_path = config_dir / "deveco.jsonc"
|
|
684
|
+
body: dict[str, Any] = {"$schema": "https://opencode.ai/config.json"}
|
|
685
|
+
if arm == "with":
|
|
686
|
+
if not hg_bin:
|
|
687
|
+
from agent_runner import find_homegraph_bin
|
|
688
|
+
|
|
689
|
+
hg_bin = find_homegraph_bin(None)
|
|
690
|
+
cmd, base_args = _split_hg_bin(hg_bin)
|
|
691
|
+
body["mcp"] = {
|
|
692
|
+
"homegraph": {
|
|
693
|
+
"type": "local",
|
|
694
|
+
"command": [cmd, *base_args, "--path", str(repo.resolve())],
|
|
695
|
+
"enabled": True,
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
body["agent"] = {
|
|
699
|
+
"build": {
|
|
700
|
+
"prompt": _DEVECO_WITH_AGENT_PROMPT,
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else:
|
|
704
|
+
body["mcp"] = {}
|
|
705
|
+
body["permission"] = {
|
|
706
|
+
"homegraph_*": "deny",
|
|
707
|
+
"homegraph_homegraph_*": "deny",
|
|
708
|
+
}
|
|
709
|
+
body["agent"] = {
|
|
710
|
+
"build": {
|
|
711
|
+
"prompt": _DEVECO_WITHOUT_AGENT_PROMPT,
|
|
712
|
+
"permission": {
|
|
713
|
+
"homegraph_*": "deny",
|
|
714
|
+
"homegraph_homegraph_*": "deny",
|
|
715
|
+
},
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
config_path.write_text(json.dumps(body, indent=2) + "\n", encoding="utf-8")
|
|
719
|
+
return config_path
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
DEFAULT_DEVECO_MODEL = "zhipuai/glm-4.5-flash"
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def run_deveco_query(
|
|
726
|
+
repo: Path,
|
|
727
|
+
query: str,
|
|
728
|
+
*,
|
|
729
|
+
arm: str,
|
|
730
|
+
hg_bin: str,
|
|
731
|
+
log_file: Path | None,
|
|
732
|
+
task_id: int,
|
|
733
|
+
item_id: str | None = None,
|
|
734
|
+
trace_dir: Path | None = None,
|
|
735
|
+
model: str | None = None,
|
|
736
|
+
timeout_sec: int = 600,
|
|
737
|
+
deveco_attach: str | None = None,
|
|
738
|
+
) -> dict[str, Any]:
|
|
739
|
+
cli = find_deveco_cli()
|
|
740
|
+
backend = f"deveco-code-{'with' if arm == 'with' else 'without'}-homegraph"
|
|
741
|
+
qid = item_id or str(task_id)
|
|
742
|
+
title = f"qa-eval-{arm}-{qid}"
|
|
743
|
+
prompt = _deveco_with_query(query) if arm == "with" else query
|
|
744
|
+
|
|
745
|
+
run_cmd = [
|
|
746
|
+
cli,
|
|
747
|
+
"run",
|
|
748
|
+
prompt,
|
|
749
|
+
"--format",
|
|
750
|
+
"json",
|
|
751
|
+
"--dir",
|
|
752
|
+
str(repo),
|
|
753
|
+
"--title",
|
|
754
|
+
title,
|
|
755
|
+
"--skip-agreement",
|
|
756
|
+
"--dangerously-skip-permissions",
|
|
757
|
+
]
|
|
758
|
+
if deveco_attach:
|
|
759
|
+
run_cmd.extend(["--attach", deveco_attach])
|
|
760
|
+
if model:
|
|
761
|
+
run_cmd.extend(["--model", model])
|
|
762
|
+
else:
|
|
763
|
+
run_cmd.extend(["--model", DEFAULT_DEVECO_MODEL])
|
|
764
|
+
|
|
765
|
+
_log_line(log_file, f"Evaluate {task_id}:")
|
|
766
|
+
_log_line(log_file, "the 1 turn")
|
|
767
|
+
t0 = time.time()
|
|
768
|
+
|
|
769
|
+
proc = _popen_capture(run_cmd, cwd=str(repo))
|
|
770
|
+
with sample_memory(proc.pid) as sampler:
|
|
771
|
+
try:
|
|
772
|
+
stdout, stderr = proc.communicate(timeout=timeout_sec)
|
|
773
|
+
except subprocess.TimeoutExpired:
|
|
774
|
+
proc.kill()
|
|
775
|
+
stdout, stderr = proc.communicate()
|
|
776
|
+
mem = sampler.last_stats
|
|
777
|
+
_log_memory(log_file, mem)
|
|
778
|
+
wall_ms = int((time.time() - t0) * 1000)
|
|
779
|
+
return {
|
|
780
|
+
"output_answer": "",
|
|
781
|
+
"agent_status": "error",
|
|
782
|
+
"agent_error": f"timeout after {timeout_sec}s",
|
|
783
|
+
"agent_backend": backend,
|
|
784
|
+
"agent_duration_ms": wall_ms,
|
|
785
|
+
"agent_memory_mb": mem,
|
|
786
|
+
}
|
|
787
|
+
mem = sampler.last_stats
|
|
788
|
+
combined = f"{stdout or ''}\n{stderr or ''}"
|
|
789
|
+
wall_ms = int((time.time() - t0) * 1000)
|
|
790
|
+
|
|
791
|
+
if proc.returncode != 0:
|
|
792
|
+
auth_err = auth_failure_reason(combined)
|
|
793
|
+
detail = auth_err or _cli_error_summary(stderr, stdout)
|
|
794
|
+
err_msg = detail or f"{cli} exit {proc.returncode}"
|
|
795
|
+
_log_memory(log_file, mem)
|
|
796
|
+
return {
|
|
797
|
+
"output_answer": _strip_ansi(stderr or stdout or "")[:2000],
|
|
798
|
+
"agent_status": "error",
|
|
799
|
+
"agent_error": err_msg,
|
|
800
|
+
"agent_backend": backend,
|
|
801
|
+
"agent_host": HOST_DEVECO,
|
|
802
|
+
"agent_duration_ms": wall_ms,
|
|
803
|
+
"agent_memory_mb": mem,
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
out = (stdout or "").strip()
|
|
807
|
+
err = (stderr or "").strip()
|
|
808
|
+
parsed = parse_opencode_json_events(out if out else err)
|
|
809
|
+
session_id = parsed.get("deveco_session_id")
|
|
810
|
+
trace_file: Path | None = None
|
|
811
|
+
tools_used: list[str] = trace_tool_names(str(parsed.get("output_answer") or ""))
|
|
812
|
+
if session_id and trace_dir is not None:
|
|
813
|
+
safe_sid = re.sub(r"[^\w.-]", "_", str(session_id))
|
|
814
|
+
trace_file = trace_dir / f"{qid}-{safe_sid}.json"
|
|
815
|
+
if export_deveco_session(str(session_id), trace_file, cwd=str(repo), cli=cli):
|
|
816
|
+
try:
|
|
817
|
+
rel = trace_file.relative_to(_QA_EVAL_DIR)
|
|
818
|
+
except ValueError:
|
|
819
|
+
rel = trace_file
|
|
820
|
+
parsed["agent_trace_file"] = str(rel).replace("\\", "/")
|
|
821
|
+
parsed = supplement_from_session_export(parsed, trace_file)
|
|
822
|
+
export_tools = _tools_from_session_export(trace_file.read_text(encoding="utf-8"))
|
|
823
|
+
for t in export_tools:
|
|
824
|
+
if t not in tools_used:
|
|
825
|
+
tools_used.append(t)
|
|
826
|
+
_log_line(log_file, f"session export → {parsed.get('agent_trace_file')}")
|
|
827
|
+
else:
|
|
828
|
+
parsed["agent_trace_file"] = None
|
|
829
|
+
if session_id:
|
|
830
|
+
_log_line(log_file, f"sessionID = {session_id}")
|
|
831
|
+
if tools_used:
|
|
832
|
+
_log_line(log_file, f"tools = {', '.join(tools_used)}")
|
|
833
|
+
used_hg = any(is_homegraph_tool(t) for t in tools_used)
|
|
834
|
+
parsed["agent_tools_used"] = tools_used
|
|
835
|
+
parsed["agent_used_homegraph"] = used_hg
|
|
836
|
+
|
|
837
|
+
tokens = (parsed.get("agent_usage") or {}).get("total_tokens") or 0
|
|
838
|
+
if tokens:
|
|
839
|
+
_log_line(log_file, f"totalTokenCount = {tokens}")
|
|
840
|
+
_log_memory(log_file, mem)
|
|
841
|
+
_log_line(log_file, f"completed ({wall_ms}ms)")
|
|
842
|
+
|
|
843
|
+
result = {
|
|
844
|
+
**parsed,
|
|
845
|
+
"agent_duration_ms": wall_ms,
|
|
846
|
+
"agent_backend": backend,
|
|
847
|
+
"agent_host": HOST_DEVECO,
|
|
848
|
+
"ab_arm": "with-homegraph" if arm == "with" else "without-homegraph",
|
|
849
|
+
"agent_memory_mb": mem,
|
|
850
|
+
}
|
|
851
|
+
if result.get("agent_status") != "success" and not result.get("agent_error"):
|
|
852
|
+
result["agent_error"] = _extract_deveco_json_errors(combined) or "deveco 未返回可解析的文本回答"
|
|
853
|
+
return result
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def run_external_dataset(
|
|
857
|
+
host: str,
|
|
858
|
+
repo: Path,
|
|
859
|
+
dataset: list[dict[str, Any]],
|
|
860
|
+
*,
|
|
861
|
+
arm: str,
|
|
862
|
+
output: Path,
|
|
863
|
+
log_file: Path | None,
|
|
864
|
+
hg_bin: str,
|
|
865
|
+
model: str | None = None,
|
|
866
|
+
deveco_attach: str | None = None,
|
|
867
|
+
) -> list[dict[str, Any]]:
|
|
868
|
+
if host not in SUPPORTED_HOSTS:
|
|
869
|
+
raise ValueError(f"unknown agent host: {host}")
|
|
870
|
+
|
|
871
|
+
from agent_runner import find_homegraph_bin, print_agent_progress, require_index, _arm_short
|
|
872
|
+
|
|
873
|
+
if arm == "with":
|
|
874
|
+
require_index(repo)
|
|
875
|
+
hg = find_homegraph_bin(hg_bin) if arm == "with" else ""
|
|
876
|
+
|
|
877
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
878
|
+
if log_file:
|
|
879
|
+
log_file.write_text("", encoding="utf-8")
|
|
880
|
+
|
|
881
|
+
results: list[dict[str, Any]] = []
|
|
882
|
+
total = len(dataset)
|
|
883
|
+
print(f" → [{host}] {_arm_short(arm)} 臂:共 {total} 题", flush=True)
|
|
884
|
+
if host == HOST_DEVECO:
|
|
885
|
+
_write_deveco_project_mcp(repo, arm=arm, hg_bin=hg)
|
|
886
|
+
trace_root = output.parent / "traces" / f"{arm}-deveco"
|
|
887
|
+
if deveco_attach:
|
|
888
|
+
print(f" → deveco attach: {deveco_attach}", flush=True)
|
|
889
|
+
print(
|
|
890
|
+
" → 提示: 若 serve 启动报 ServeError,多为端口占用;"
|
|
891
|
+
"可 netstat -ano | findstr :4096 后 taskkill,或不加 --deveco-attach 直接跑",
|
|
892
|
+
flush=True,
|
|
893
|
+
)
|
|
894
|
+
print(f" → 轨迹目录: {trace_root}", flush=True)
|
|
895
|
+
if arm == "with":
|
|
896
|
+
print(" → WITH 臂: 优先 homegraph_explore,不足时可 grep/read", flush=True)
|
|
897
|
+
else:
|
|
898
|
+
trace_root = None
|
|
899
|
+
|
|
900
|
+
auth_abort: str | None = None
|
|
901
|
+
with output.open("w", encoding="utf-8") as f:
|
|
902
|
+
for i, item in enumerate(dataset, 1):
|
|
903
|
+
if auth_abort:
|
|
904
|
+
meta = {
|
|
905
|
+
"output_answer": "",
|
|
906
|
+
"agent_status": "error",
|
|
907
|
+
"agent_error": auth_abort,
|
|
908
|
+
"agent_backend": f"{host}-{arm}",
|
|
909
|
+
"agent_host": host,
|
|
910
|
+
}
|
|
911
|
+
print_agent_progress(arm, i, total, str(item.get("id") or i), f"跳过: {auth_abort[:80]}")
|
|
912
|
+
row = {**item, **meta}
|
|
913
|
+
results.append(row)
|
|
914
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
915
|
+
f.flush()
|
|
916
|
+
continue
|
|
917
|
+
q = str(item["query"])
|
|
918
|
+
item_id = str(item.get("id") or i)
|
|
919
|
+
print_agent_progress(arm, i, total, item_id, f"[{host}] 开始…")
|
|
920
|
+
logger.info("[%s/%s] %s/%s %s", host, arm, i, total, item_id)
|
|
921
|
+
try:
|
|
922
|
+
common = dict(
|
|
923
|
+
arm=arm,
|
|
924
|
+
hg_bin=hg,
|
|
925
|
+
log_file=log_file,
|
|
926
|
+
task_id=i,
|
|
927
|
+
)
|
|
928
|
+
if host == HOST_CLAUDE:
|
|
929
|
+
meta = run_claude_query(repo, q, **common)
|
|
930
|
+
else:
|
|
931
|
+
meta = run_deveco_query(
|
|
932
|
+
repo,
|
|
933
|
+
q,
|
|
934
|
+
model=model,
|
|
935
|
+
deveco_attach=deveco_attach,
|
|
936
|
+
item_id=item_id,
|
|
937
|
+
trace_dir=trace_root,
|
|
938
|
+
**common,
|
|
939
|
+
)
|
|
940
|
+
except Exception as e:
|
|
941
|
+
logger.error("External agent failed %s: %s", item.get("id"), e)
|
|
942
|
+
meta = {
|
|
943
|
+
"output_answer": "",
|
|
944
|
+
"agent_status": "error",
|
|
945
|
+
"agent_error": str(e),
|
|
946
|
+
"agent_backend": f"{host}-{arm}",
|
|
947
|
+
"agent_host": host,
|
|
948
|
+
}
|
|
949
|
+
row = {**item, **meta}
|
|
950
|
+
results.append(row)
|
|
951
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
952
|
+
f.flush()
|
|
953
|
+
if meta.get("agent_status") == "success":
|
|
954
|
+
dur_ms = meta.get("agent_duration_ms")
|
|
955
|
+
dur_s = f"{dur_ms / 1000:.1f}s" if isinstance(dur_ms, (int, float)) else "?"
|
|
956
|
+
extra = ""
|
|
957
|
+
if host == HOST_DEVECO and arm == "with":
|
|
958
|
+
extra = " ✓homegraph" if meta.get("agent_used_homegraph") else " ⚠未用homegraph"
|
|
959
|
+
print_agent_progress(arm, i, total, item_id, f"[{host}] 完成 ({dur_s}){extra}")
|
|
960
|
+
else:
|
|
961
|
+
err = str(meta.get("agent_error") or meta.get("agent_status") or "error")
|
|
962
|
+
extra = ""
|
|
963
|
+
if host == HOST_DEVECO and arm == "with" and meta.get("agent_tools_used"):
|
|
964
|
+
extra = f" tools={','.join(meta['agent_tools_used'][:4])}"
|
|
965
|
+
print_agent_progress(arm, i, total, item_id, f"[{host}] 失败: {err[:100]}{extra}")
|
|
966
|
+
if i == 1 and meta.get("agent_error") and (
|
|
967
|
+
"未登录" in str(meta["agent_error"])
|
|
968
|
+
or "DevEco Code" in str(meta["agent_error"])
|
|
969
|
+
or "DevEco 模型" in str(meta["agent_error"])
|
|
970
|
+
or "Model not found" in str(meta["agent_error"])
|
|
971
|
+
):
|
|
972
|
+
auth_abort = str(meta["agent_error"])
|
|
973
|
+
print(f"\n ✗ {auth_abort} — 后续题目跳过", flush=True)
|
|
974
|
+
ok = sum(1 for r in results if r.get("agent_status") == "success")
|
|
975
|
+
print(f" → [{host}] {_arm_short(arm)} 臂结束:{ok}/{total} 成功", flush=True)
|
|
976
|
+
return results
|