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.
- package/LICENSE +21 -21
- package/README.md +268 -243
- package/dist/arkts/ohos-api-index.d.ts +15 -0
- package/dist/arkts/ohos-api-index.d.ts.map +1 -0
- package/dist/arkts/ohos-api-index.js +190 -0
- package/dist/arkts/ohos-api-index.js.map +1 -0
- package/dist/arkts/ohos-sdk-input.d.ts +36 -0
- package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
- package/dist/arkts/ohos-sdk-input.js +214 -0
- package/dist/arkts/ohos-sdk-input.js.map +1 -0
- package/dist/bin/command-supervision.d.ts +12 -0
- package/dist/bin/command-supervision.d.ts.map +1 -0
- package/dist/bin/command-supervision.js +86 -0
- package/dist/bin/command-supervision.js.map +1 -0
- package/dist/bin/homegraph.d.ts +1 -1
- package/dist/bin/homegraph.js +971 -71
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/index.d.ts +20 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +39 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +58 -19
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +14 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +314 -143
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +172 -152
- package/dist/directory.d.ts +32 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +88 -5
- package/dist/directory.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.d.ts +2 -0
- package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
- package/dist/extraction/arkts-batch-worker.js +28 -0
- package/dist/extraction/arkts-batch-worker.js.map +1 -0
- package/dist/extraction/context.d.ts +11 -0
- package/dist/extraction/context.d.ts.map +1 -1
- package/dist/extraction/context.js +20 -0
- package/dist/extraction/context.js.map +1 -1
- package/dist/extraction/index.d.ts +16 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +619 -380
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
- package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
- package/dist/extraction/languages/arkts-state-decorators.js +26 -0
- package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
- package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
- package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
- package/dist/extraction/languages/arkts-viewtree.js +21 -6
- package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +98 -2
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1290 -61
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +56 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +198 -1
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
- package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
- package/dist/extraction/languages/ohos-api-consumer.js +283 -0
- package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
- package/dist/extraction/parse-pool.d.ts +126 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +319 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +17 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +21 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +198 -27
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +76 -17
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -4
- package/dist/index.js.map +1 -1
- 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/daemon-paths.d.ts +30 -3
- package/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/dist/mcp/daemon-paths.js +50 -10
- package/dist/mcp/daemon-paths.js.map +1 -1
- package/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/dist/mcp/daemon-registry.js +7 -3
- package/dist/mcp/daemon-registry.js.map +1 -1
- package/dist/mcp/daemon.d.ts +48 -0
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +196 -32
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/engine.d.ts +17 -0
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +73 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +25 -43
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/ppid-watchdog.d.ts +18 -0
- package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
- package/dist/mcp/ppid-watchdog.js +37 -0
- package/dist/mcp/ppid-watchdog.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +25 -0
- package/dist/mcp/query-cache.d.ts.map +1 -0
- package/dist/mcp/query-cache.js +191 -0
- package/dist/mcp/query-cache.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +94 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +297 -0
- package/dist/mcp/query-pool.js.map +1 -0
- package/dist/mcp/query-worker.d.ts +24 -0
- package/dist/mcp/query-worker.d.ts.map +1 -0
- package/dist/mcp/query-worker.js +87 -0
- package/dist/mcp/query-worker.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +5 -7
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +72 -74
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +94 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +877 -71
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-config.d.ts +20 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +42 -2
- package/dist/project-config.js.map +1 -1
- package/dist/reasoning/login.js +1 -1
- package/dist/reasoning/login.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +765 -79
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +72 -11
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +32 -0
- package/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/dist/resolution/cooperative-yield.js +42 -0
- package/dist/resolution/cooperative-yield.js.map +1 -0
- package/dist/resolution/index.d.ts +11 -2
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +72 -4
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +22 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +317 -20
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/spec/config.d.ts +39 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +304 -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 +24 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +128 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +74 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +324 -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 +33 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +119 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +117 -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 +24 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +42 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +567 -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 +230 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +155 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +440 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/llm/client.d.ts +29 -0
- package/dist/spec/llm/client.d.ts.map +1 -0
- package/dist/spec/llm/client.js +123 -0
- package/dist/spec/llm/client.js.map +1 -0
- package/dist/spec/llm/index.d.ts +3 -0
- package/dist/spec/llm/index.d.ts.map +1 -0
- package/dist/spec/llm/index.js +11 -0
- package/dist/spec/llm/index.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +13 -0
- package/dist/spec/llm/prompts.d.ts.map +1 -0
- package/dist/spec/llm/prompts.js +75 -0
- package/dist/spec/llm/prompts.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 +103 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +307 -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 +178 -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 +167 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +463 -0
- package/dist/spec/utils.js.map +1 -0
- package/dist/sync/worktree.d.ts +9 -0
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +40 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.d.ts +2 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.js +19 -2
- package/dist/ui/shimmer-progress.js.map +1 -1
- package/dist/upgrade/index.js +1 -1
- package/dist/upgrade/index.js.map +1 -1
- 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/repro-concurrent-explore.mjs +119 -0
- package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
- 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/exp_boundary_eval/README.md +247 -0
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
- package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
- package/scripts/exp_boundary_eval/_utils.py +1116 -0
- package/scripts/exp_boundary_eval/analyze.py +1313 -0
- package/scripts/exp_boundary_eval/data/agents.json +109 -0
- package/scripts/exp_boundary_eval/data/experiments.json +140 -0
- package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
- package/scripts/exp_boundary_eval/run_all.py +378 -0
- package/scripts/exp_boundary_eval/run_one.py +165 -0
- package/scripts/exp_boundary_eval/run_session.py +158 -0
- package/scripts/exp_boundary_eval/setup.py +120 -0
- package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
- package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
- package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
- 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 +268 -246
- package/scripts/ohos-sdk-publish.mjs +133 -0
- 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,526 +1,526 @@
|
|
|
1
|
-
"""Multi-turn QA agent: with homegraph tools vs without (grep/read only). DashScope Qwen."""
|
|
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 time
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from openai import OpenAI
|
|
18
|
-
except ImportError:
|
|
19
|
-
OpenAI = None # type: ignore[misc, assignment]
|
|
20
|
-
|
|
21
|
-
from memory_monitor import sample_memory
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
TIME_FMT = "%Y-%m-%d %H:%M:%S.%f"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _arm_short(arm: str) -> str:
|
|
29
|
-
return "WITH" if arm == "with" else "WITHOUT"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def print_agent_progress(
|
|
33
|
-
arm: str,
|
|
34
|
-
index: int,
|
|
35
|
-
total: int,
|
|
36
|
-
item_id: str,
|
|
37
|
-
message: str,
|
|
38
|
-
*,
|
|
39
|
-
indent: int = 0,
|
|
40
|
-
) -> None:
|
|
41
|
-
prefix = " " * indent
|
|
42
|
-
print(f"{prefix}[{_arm_short(arm)} {index}/{total}] {item_id} — {message}", flush=True)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def print_agent_turn(task_id: int, turn: int, max_turns: int, message: str = "请求 LLM…") -> None:
|
|
46
|
-
print(f" · 第 {turn}/{max_turns} 轮 {message}", flush=True)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _log_line(log_file: Path | None, msg: str) -> None:
|
|
50
|
-
if log_file is None:
|
|
51
|
-
return
|
|
52
|
-
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
-
with log_file.open("a", encoding="utf-8") as f:
|
|
54
|
-
f.write(f"{datetime.now().strftime(TIME_FMT)} {msg}\n")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _log_memory(log_file: Path | None, mem: dict[str, float | None]) -> None:
|
|
58
|
-
if mem.get("peak_rss_mb") is not None:
|
|
59
|
-
_log_line(log_file, f"peakRssMb = {mem['peak_rss_mb']}")
|
|
60
|
-
if mem.get("avg_rss_mb") is not None:
|
|
61
|
-
_log_line(log_file, f"avgRssMb = {mem['avg_rss_mb']}")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def find_homegraph_bin(explicit: str | None = None) -> str:
|
|
65
|
-
if explicit:
|
|
66
|
-
p = Path(explicit)
|
|
67
|
-
if p.is_file() and os.access(p, os.X_OK):
|
|
68
|
-
return str(p.resolve())
|
|
69
|
-
raise FileNotFoundError(f"homegraph binary not found: {explicit}")
|
|
70
|
-
|
|
71
|
-
# Prefer local dev build
|
|
72
|
-
repo_root = Path(__file__).resolve().parents[2]
|
|
73
|
-
for local in (
|
|
74
|
-
repo_root / "dist" / "bin" / "homegraph.js",
|
|
75
|
-
repo_root / "dist" / "bin" / "homegraph.js",
|
|
76
|
-
):
|
|
77
|
-
if local.is_file():
|
|
78
|
-
return f"node {local.resolve()}"
|
|
79
|
-
|
|
80
|
-
for name in ("homegraph", "homegraph"):
|
|
81
|
-
found = shutil.which(name)
|
|
82
|
-
if found:
|
|
83
|
-
return found
|
|
84
|
-
raise FileNotFoundError(
|
|
85
|
-
"homegraph/homegraph not on PATH.\n"
|
|
86
|
-
" 在 homegraph 仓库里: npm run build\n"
|
|
87
|
-
" 或: python scripts/qa_eval/run_pipeline.py ab --homegraph-bin 'node /path/to/dist/bin/homegraph.js'"
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _hg_cmd(hg_bin: str, args: list[str]) -> list[str]:
|
|
92
|
-
if hg_bin.startswith("node "):
|
|
93
|
-
return hg_bin.split(" ", 1) + args
|
|
94
|
-
return [hg_bin, *args]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def require_index(repo: Path) -> None:
|
|
98
|
-
for name in (".homegraph", ".homegraph"):
|
|
99
|
-
d = repo / name
|
|
100
|
-
if (d / "homegraph.db").exists() or (d / "homegraph.db").exists():
|
|
101
|
-
return
|
|
102
|
-
raise RuntimeError(f"仓库未索引: {repo}\n请先运行: homegraph sync {repo}")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def tool_block(name: str, body: str) -> str:
|
|
106
|
-
return f"---\n{name}\n{body.strip()}\n---"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def homegraph_query(repo: Path, query: str, *, hg_bin: str, limit: int = 8) -> str:
|
|
110
|
-
proc = subprocess.run(
|
|
111
|
-
_hg_cmd(hg_bin, ["query", query, "-p", str(repo.resolve()), "-l", str(limit), "-j"]),
|
|
112
|
-
capture_output=True,
|
|
113
|
-
text=True,
|
|
114
|
-
timeout=120,
|
|
115
|
-
)
|
|
116
|
-
if proc.returncode != 0:
|
|
117
|
-
return f"(homegraph query failed: {(proc.stderr or proc.stdout)[-400:]})"
|
|
118
|
-
try:
|
|
119
|
-
hits = json.loads(proc.stdout)
|
|
120
|
-
except json.JSONDecodeError:
|
|
121
|
-
return proc.stdout[:4000]
|
|
122
|
-
lines: list[str] = []
|
|
123
|
-
for i, hit in enumerate(hits[:limit], 1):
|
|
124
|
-
node = hit.get("node") or {}
|
|
125
|
-
lines.append(
|
|
126
|
-
f"{i}. [{node.get('kind')}] {node.get('qualifiedName', node.get('name'))}\n"
|
|
127
|
-
f" file: {node.get('filePath')}:{node.get('startLine')}-{node.get('endLine')}\n"
|
|
128
|
-
f" sig: {node.get('signature', '')}"
|
|
129
|
-
)
|
|
130
|
-
return "\n".join(lines) if lines else "(no hits)"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def read_file(repo: Path, rel_path: str, start: int = 1, end: int | None = None, context: int = 0) -> str:
|
|
134
|
-
path = (repo / rel_path).resolve()
|
|
135
|
-
if not path.is_file() or not str(path).startswith(str(repo.resolve())):
|
|
136
|
-
return f"(file not found: {rel_path})"
|
|
137
|
-
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
138
|
-
if end is None:
|
|
139
|
-
end = min(len(lines), start + 80)
|
|
140
|
-
lo = max(1, start - context) - 1
|
|
141
|
-
hi = min(len(lines), end + context)
|
|
142
|
-
body = "\n".join(f"{i + 1:4}| {lines[i]}" for i in range(lo, hi))
|
|
143
|
-
return f"// {rel_path}:{start}-{end}\n{body}"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def find_rg_binary() -> str | None:
|
|
147
|
-
found = shutil.which("rg")
|
|
148
|
-
if found:
|
|
149
|
-
return found
|
|
150
|
-
candidates = [
|
|
151
|
-
Path.home() / ".cursor-server/bin/*/node_modules/@vscode/ripgrep/bin/rg",
|
|
152
|
-
]
|
|
153
|
-
import glob
|
|
154
|
-
|
|
155
|
-
for pattern in candidates:
|
|
156
|
-
for path in glob.glob(str(pattern)):
|
|
157
|
-
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
158
|
-
return path
|
|
159
|
-
return None
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def search_text(repo: Path, pattern: str, max_hits: int = 20) -> str:
|
|
163
|
-
rg = find_rg_binary()
|
|
164
|
-
if not rg:
|
|
165
|
-
return "(rg not found on PATH; install: sudo apt install ripgrep)"
|
|
166
|
-
proc = subprocess.run(
|
|
167
|
-
[rg, "-n", "--no-heading", "-m", str(max_hits), pattern, str(repo)],
|
|
168
|
-
capture_output=True,
|
|
169
|
-
text=True,
|
|
170
|
-
timeout=60,
|
|
171
|
-
)
|
|
172
|
-
out = (proc.stdout or proc.stderr or "").strip()
|
|
173
|
-
return out[:6000] if out else "(no matches)"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def find_files(repo: Path, pattern: str, max_hits: int = 30) -> str:
|
|
177
|
-
"""Find files by glob. Invalid patterns return a hint instead of crashing the agent."""
|
|
178
|
-
pattern = pattern.strip().lstrip("/")
|
|
179
|
-
if not pattern:
|
|
180
|
-
return "(empty pattern)"
|
|
181
|
-
|
|
182
|
-
hits: list[str] = []
|
|
183
|
-
try:
|
|
184
|
-
# pathlib rglob: ** must be a whole path segment (e.g. **/*.ets, not */**/x)
|
|
185
|
-
if "**" in pattern and not pattern.startswith("**"):
|
|
186
|
-
# Fallback: treat as substring search via rg --files | rg
|
|
187
|
-
rg = shutil.which("rg")
|
|
188
|
-
if rg:
|
|
189
|
-
proc = subprocess.run(
|
|
190
|
-
[rg, "--files", str(repo)],
|
|
191
|
-
capture_output=True,
|
|
192
|
-
text=True,
|
|
193
|
-
timeout=60,
|
|
194
|
-
)
|
|
195
|
-
needle = pattern.replace("**", "").replace("*", "")
|
|
196
|
-
for line in (proc.stdout or "").splitlines():
|
|
197
|
-
if needle and needle.lower() in line.lower():
|
|
198
|
-
hits.append(line)
|
|
199
|
-
if len(hits) >= max_hits:
|
|
200
|
-
break
|
|
201
|
-
return "\n".join(hits) if hits else "(no files; try SearchText instead)"
|
|
202
|
-
return "(invalid glob: use pattern like **/*.ets or use SearchText)"
|
|
203
|
-
|
|
204
|
-
for p in sorted(repo.rglob(pattern)):
|
|
205
|
-
if p.is_file() and ".git" not in p.parts:
|
|
206
|
-
hits.append(str(p.relative_to(repo)))
|
|
207
|
-
if len(hits) >= max_hits:
|
|
208
|
-
break
|
|
209
|
-
except ValueError as e:
|
|
210
|
-
return f"(invalid glob pattern '{pattern}': {e}. Try **/*.ets or SearchText)"
|
|
211
|
-
except OSError as e:
|
|
212
|
-
return f"(find failed: {e})"
|
|
213
|
-
|
|
214
|
-
return "\n".join(hits) if hits else "(no files)"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
WITH_TOOLS = [
|
|
218
|
-
{
|
|
219
|
-
"type": "function",
|
|
220
|
-
"function": {
|
|
221
|
-
"name": "HomegraphQuery",
|
|
222
|
-
"description": "Search symbols in the repo via homegraph index (preferred for finding functions/classes/files).",
|
|
223
|
-
"parameters": {
|
|
224
|
-
"type": "object",
|
|
225
|
-
"properties": {
|
|
226
|
-
"query": {"type": "string", "description": "Symbol name or search terms"},
|
|
227
|
-
"limit": {"type": "integer", "description": "Max hits", "default": 8},
|
|
228
|
-
},
|
|
229
|
-
"required": ["query"],
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
"type": "function",
|
|
235
|
-
"function": {
|
|
236
|
-
"name": "ReadFile",
|
|
237
|
-
"description": "Read source file lines from the repo.",
|
|
238
|
-
"parameters": {
|
|
239
|
-
"type": "object",
|
|
240
|
-
"properties": {
|
|
241
|
-
"path": {"type": "string"},
|
|
242
|
-
"start_line": {"type": "integer", "default": 1},
|
|
243
|
-
"end_line": {"type": "integer"},
|
|
244
|
-
},
|
|
245
|
-
"required": ["path"],
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
]
|
|
250
|
-
|
|
251
|
-
WITHOUT_TOOLS = [
|
|
252
|
-
{
|
|
253
|
-
"type": "function",
|
|
254
|
-
"function": {
|
|
255
|
-
"name": "SearchText",
|
|
256
|
-
"description": "Ripgrep search in the repo (like grep).",
|
|
257
|
-
"parameters": {
|
|
258
|
-
"type": "object",
|
|
259
|
-
"properties": {"pattern": {"type": "string"}},
|
|
260
|
-
"required": ["pattern"],
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
"type": "function",
|
|
266
|
-
"function": {
|
|
267
|
-
"name": "ReadFile",
|
|
268
|
-
"description": "Read source file lines from the repo.",
|
|
269
|
-
"parameters": {
|
|
270
|
-
"type": "object",
|
|
271
|
-
"properties": {
|
|
272
|
-
"path": {"type": "string"},
|
|
273
|
-
"start_line": {"type": "integer", "default": 1},
|
|
274
|
-
"end_line": {"type": "integer"},
|
|
275
|
-
},
|
|
276
|
-
"required": ["path"],
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
"type": "function",
|
|
282
|
-
"function": {
|
|
283
|
-
"name": "FindFiles",
|
|
284
|
-
"description": "Glob find files under repo. Use patterns like *.ets or **/BenchmarkHub*.ets (not */**/x).",
|
|
285
|
-
"parameters": {
|
|
286
|
-
"type": "object",
|
|
287
|
-
"properties": {"pattern": {"type": "string"}},
|
|
288
|
-
"required": ["pattern"],
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
]
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def execute_tool(
|
|
296
|
-
repo: Path,
|
|
297
|
-
name: str,
|
|
298
|
-
args: dict[str, Any],
|
|
299
|
-
*,
|
|
300
|
-
arm: str,
|
|
301
|
-
hg_bin: str,
|
|
302
|
-
) -> str:
|
|
303
|
-
if name == "HomegraphQuery":
|
|
304
|
-
if arm != "with":
|
|
305
|
-
return "(homegraph disabled in without arm)"
|
|
306
|
-
return homegraph_query(repo, args["query"], hg_bin=hg_bin, limit=int(args.get("limit") or 8))
|
|
307
|
-
if name == "ReadFile":
|
|
308
|
-
return read_file(
|
|
309
|
-
repo,
|
|
310
|
-
args["path"],
|
|
311
|
-
start=int(args.get("start_line") or 1),
|
|
312
|
-
end=int(args["end_line"]) if args.get("end_line") else None,
|
|
313
|
-
)
|
|
314
|
-
if name == "SearchText":
|
|
315
|
-
return search_text(repo, args["pattern"])
|
|
316
|
-
if name == "FindFiles":
|
|
317
|
-
return find_files(repo, args["pattern"])
|
|
318
|
-
return f"(unknown tool: {name})"
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def run_agent_on_query(
|
|
322
|
-
repo: Path,
|
|
323
|
-
query: str,
|
|
324
|
-
*,
|
|
325
|
-
arm: str,
|
|
326
|
-
api_key: str,
|
|
327
|
-
base_url: str,
|
|
328
|
-
model: str,
|
|
329
|
-
hg_bin: str,
|
|
330
|
-
log_file: Path | None = None,
|
|
331
|
-
task_id: int = 1,
|
|
332
|
-
max_turns: int = 8,
|
|
333
|
-
timeout_sec: int = 600,
|
|
334
|
-
extra_body: dict | None = None,
|
|
335
|
-
) -> dict[str, Any]:
|
|
336
|
-
if OpenAI is None:
|
|
337
|
-
raise RuntimeError("pip install openai")
|
|
338
|
-
|
|
339
|
-
client = OpenAI(api_key=api_key, base_url=base_url, timeout=timeout_sec)
|
|
340
|
-
tools = WITH_TOOLS if arm == "with" else WITHOUT_TOOLS
|
|
341
|
-
backend = "agent-with-homegraph" if arm == "with" else "agent-grep-read"
|
|
342
|
-
|
|
343
|
-
if arm == "with":
|
|
344
|
-
system = (
|
|
345
|
-
"你是鸿蒙 ArkTS 代码分析 Agent。可用 HomegraphQuery 检索符号、ReadFile 读源码。"
|
|
346
|
-
"先检索再读文件,基于仓库事实作答。回答简洁准确,中文。"
|
|
347
|
-
)
|
|
348
|
-
else:
|
|
349
|
-
system = (
|
|
350
|
-
"你是鸿蒙 ArkTS 代码分析 Agent。可用 SearchText(rg)、FindFiles、ReadFile 探索仓库。"
|
|
351
|
-
"没有 homegraph,请主动搜索和读文件后再答。回答简洁准确,中文。"
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
messages: list[dict[str, Any]] = [
|
|
355
|
-
{"role": "system", "content": system},
|
|
356
|
-
{"role": "user", "content": query},
|
|
357
|
-
]
|
|
358
|
-
tool_trace: list[str] = []
|
|
359
|
-
total_tokens = 0
|
|
360
|
-
t0 = time.time()
|
|
361
|
-
_log_line(log_file, f"Evaluate {task_id}:")
|
|
362
|
-
|
|
363
|
-
with sample_memory() as mem_sampler:
|
|
364
|
-
for turn in range(1, max_turns + 1):
|
|
365
|
-
_log_line(log_file, f"the {turn} turn")
|
|
366
|
-
print_agent_turn(task_id, turn, max_turns)
|
|
367
|
-
create_kwargs: dict[str, Any] = {
|
|
368
|
-
"model": model,
|
|
369
|
-
"messages": messages,
|
|
370
|
-
"tools": tools,
|
|
371
|
-
"temperature": 0.2,
|
|
372
|
-
}
|
|
373
|
-
if extra_body:
|
|
374
|
-
create_kwargs["extra_body"] = extra_body
|
|
375
|
-
resp = client.chat.completions.create(**create_kwargs)
|
|
376
|
-
if turn == 1:
|
|
377
|
-
_log_line(log_file, "first token")
|
|
378
|
-
if resp.usage:
|
|
379
|
-
total_tokens += int(resp.usage.total_tokens or 0)
|
|
380
|
-
_log_line(log_file, f"totalTokenCount = {resp.usage.total_tokens}")
|
|
381
|
-
|
|
382
|
-
msg = resp.choices[0].message
|
|
383
|
-
if msg.tool_calls:
|
|
384
|
-
names = ", ".join(tc.function.name for tc in msg.tool_calls)
|
|
385
|
-
print_agent_turn(task_id, turn, max_turns, f"工具: {names}")
|
|
386
|
-
messages.append(msg.model_dump())
|
|
387
|
-
for tc in msg.tool_calls:
|
|
388
|
-
fn = tc.function
|
|
389
|
-
try:
|
|
390
|
-
fn_args = json.loads(fn.arguments or "{}")
|
|
391
|
-
except json.JSONDecodeError:
|
|
392
|
-
fn_args = {}
|
|
393
|
-
result = execute_tool(repo, fn.name, fn_args, arm=arm, hg_bin=hg_bin)
|
|
394
|
-
arg_str = json.dumps(fn_args, ensure_ascii=False)
|
|
395
|
-
tool_trace.append(tool_block(fn.name, f"args: {arg_str}\nresult:\n{result[:3000]}"))
|
|
396
|
-
messages.append(
|
|
397
|
-
{
|
|
398
|
-
"role": "tool",
|
|
399
|
-
"tool_call_id": tc.id,
|
|
400
|
-
"content": result[:8000],
|
|
401
|
-
}
|
|
402
|
-
)
|
|
403
|
-
continue
|
|
404
|
-
|
|
405
|
-
answer = (msg.content or "").strip()
|
|
406
|
-
duration_ms = int((time.time() - t0) * 1000)
|
|
407
|
-
trace_text = "\n\n".join(tool_trace)
|
|
408
|
-
output = f"{trace_text}\n\n{answer}" if trace_text else answer
|
|
409
|
-
usage = resp.usage
|
|
410
|
-
mem = mem_sampler.last_stats
|
|
411
|
-
_log_memory(log_file, mem)
|
|
412
|
-
meta: dict[str, Any] = {
|
|
413
|
-
"output_answer": output,
|
|
414
|
-
"agent_status": "success" if answer else "error",
|
|
415
|
-
"agent_backend": backend,
|
|
416
|
-
"agent_host": "builtin",
|
|
417
|
-
"agent_model": model,
|
|
418
|
-
"agent_turns": turn,
|
|
419
|
-
"agent_duration_ms": duration_ms,
|
|
420
|
-
"ab_arm": "with-homegraph" if arm == "with" else "without-homegraph",
|
|
421
|
-
"agent_memory_mb": mem,
|
|
422
|
-
}
|
|
423
|
-
if usage:
|
|
424
|
-
meta["agent_usage"] = {
|
|
425
|
-
"prompt_tokens": usage.prompt_tokens,
|
|
426
|
-
"completion_tokens": usage.completion_tokens,
|
|
427
|
-
"total_tokens": usage.total_tokens,
|
|
428
|
-
}
|
|
429
|
-
if not answer:
|
|
430
|
-
meta["agent_error"] = "empty final answer"
|
|
431
|
-
return meta
|
|
432
|
-
|
|
433
|
-
mem = mem_sampler.last_stats
|
|
434
|
-
_log_memory(log_file, mem)
|
|
435
|
-
return {
|
|
436
|
-
"output_answer": "\n\n".join(tool_trace),
|
|
437
|
-
"agent_status": "error",
|
|
438
|
-
"agent_error": f"max turns ({max_turns}) exceeded",
|
|
439
|
-
"agent_backend": backend,
|
|
440
|
-
"agent_host": "builtin",
|
|
441
|
-
"agent_model": model,
|
|
442
|
-
"ab_arm": "with-homegraph" if arm == "with" else "without-homegraph",
|
|
443
|
-
"agent_memory_mb": mem,
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def run_agent_dataset(
|
|
448
|
-
repo: Path,
|
|
449
|
-
dataset: list[dict[str, Any]],
|
|
450
|
-
*,
|
|
451
|
-
arm: str,
|
|
452
|
-
output: Path,
|
|
453
|
-
log_file: Path | None,
|
|
454
|
-
api_key: str,
|
|
455
|
-
base_url: str,
|
|
456
|
-
model: str,
|
|
457
|
-
hg_bin: str,
|
|
458
|
-
max_turns: int = 8,
|
|
459
|
-
extra_body: dict | None = None,
|
|
460
|
-
) -> list[dict[str, Any]]:
|
|
461
|
-
if arm == "with":
|
|
462
|
-
require_index(repo)
|
|
463
|
-
hg = find_homegraph_bin(hg_bin) if arm == "with" else ""
|
|
464
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
465
|
-
if log_file:
|
|
466
|
-
log_file.write_text("", encoding="utf-8")
|
|
467
|
-
|
|
468
|
-
results: list[dict[str, Any]] = []
|
|
469
|
-
auth_failed = False
|
|
470
|
-
total = len(dataset)
|
|
471
|
-
print(f" → {_arm_short(arm)} 臂:共 {total} 题", flush=True)
|
|
472
|
-
with output.open("w", encoding="utf-8") as f:
|
|
473
|
-
for i, item in enumerate(dataset, 1):
|
|
474
|
-
q = str(item["query"])
|
|
475
|
-
item_id = str(item.get("id") or i)
|
|
476
|
-
print_agent_progress(arm, i, total, item_id, "开始…")
|
|
477
|
-
logger.info("[%s] %s/%s %s", arm, i, total, item_id)
|
|
478
|
-
try:
|
|
479
|
-
agent_meta = run_agent_on_query(
|
|
480
|
-
repo,
|
|
481
|
-
q,
|
|
482
|
-
arm=arm,
|
|
483
|
-
api_key=api_key,
|
|
484
|
-
base_url=base_url,
|
|
485
|
-
model=model,
|
|
486
|
-
hg_bin=hg,
|
|
487
|
-
log_file=log_file,
|
|
488
|
-
task_id=i,
|
|
489
|
-
max_turns=max_turns,
|
|
490
|
-
extra_body=extra_body,
|
|
491
|
-
)
|
|
492
|
-
except Exception as e:
|
|
493
|
-
err = str(e)
|
|
494
|
-
logger.error("Agent failed %s: %s", item.get("id"), e)
|
|
495
|
-
if "401" in err or "invalid_api_key" in err or "Incorrect API key" in err:
|
|
496
|
-
auth_failed = True
|
|
497
|
-
agent_meta = {
|
|
498
|
-
"output_answer": "",
|
|
499
|
-
"agent_status": "error",
|
|
500
|
-
"agent_error": err,
|
|
501
|
-
"agent_backend": "agent-with-homegraph" if arm == "with" else "agent-grep-read",
|
|
502
|
-
}
|
|
503
|
-
row = {**item, **agent_meta}
|
|
504
|
-
results.append(row)
|
|
505
|
-
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
506
|
-
f.flush()
|
|
507
|
-
if agent_meta.get("agent_status") == "success":
|
|
508
|
-
turns = agent_meta.get("agent_turns", "?")
|
|
509
|
-
dur_ms = agent_meta.get("agent_duration_ms")
|
|
510
|
-
dur_s = f"{dur_ms / 1000:.1f}s" if isinstance(dur_ms, (int, float)) else "?"
|
|
511
|
-
print_agent_progress(arm, i, total, item_id, f"完成 ({turns} 轮, {dur_s})")
|
|
512
|
-
else:
|
|
513
|
-
err = str(agent_meta.get("agent_error") or agent_meta.get("agent_status") or "error")
|
|
514
|
-
print_agent_progress(arm, i, total, item_id, f"失败: {err[:100]}")
|
|
515
|
-
if auth_failed:
|
|
516
|
-
raise RuntimeError(
|
|
517
|
-
"LLM API 鉴权失败 (401)。智谱 Key 请用:\n"
|
|
518
|
-
" export ZHIPU_API_KEY='your-id.your-secret'\n"
|
|
519
|
-
" python scripts/qa_eval/run_pipeline.py ab --provider zhipu\n"
|
|
520
|
-
"DashScope Key 请用:\n"
|
|
521
|
-
" export DASHSCOPE_API_KEY='sk-...'\n"
|
|
522
|
-
" python scripts/qa_eval/run_pipeline.py ab --provider dashscope"
|
|
523
|
-
) from None
|
|
524
|
-
ok = sum(1 for r in results if r.get("agent_status") == "success")
|
|
525
|
-
print(f" → {_arm_short(arm)} 臂结束:{ok}/{total} 成功", flush=True)
|
|
526
|
-
return results
|
|
1
|
+
"""Multi-turn QA agent: with homegraph tools vs without (grep/read only). DashScope Qwen."""
|
|
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 time
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from openai import OpenAI
|
|
18
|
+
except ImportError:
|
|
19
|
+
OpenAI = None # type: ignore[misc, assignment]
|
|
20
|
+
|
|
21
|
+
from memory_monitor import sample_memory
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
TIME_FMT = "%Y-%m-%d %H:%M:%S.%f"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _arm_short(arm: str) -> str:
|
|
29
|
+
return "WITH" if arm == "with" else "WITHOUT"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_agent_progress(
|
|
33
|
+
arm: str,
|
|
34
|
+
index: int,
|
|
35
|
+
total: int,
|
|
36
|
+
item_id: str,
|
|
37
|
+
message: str,
|
|
38
|
+
*,
|
|
39
|
+
indent: int = 0,
|
|
40
|
+
) -> None:
|
|
41
|
+
prefix = " " * indent
|
|
42
|
+
print(f"{prefix}[{_arm_short(arm)} {index}/{total}] {item_id} — {message}", flush=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_agent_turn(task_id: int, turn: int, max_turns: int, message: str = "请求 LLM…") -> None:
|
|
46
|
+
print(f" · 第 {turn}/{max_turns} 轮 {message}", flush=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _log_line(log_file: Path | None, msg: str) -> None:
|
|
50
|
+
if log_file is None:
|
|
51
|
+
return
|
|
52
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
with log_file.open("a", encoding="utf-8") as f:
|
|
54
|
+
f.write(f"{datetime.now().strftime(TIME_FMT)} {msg}\n")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _log_memory(log_file: Path | None, mem: dict[str, float | None]) -> None:
|
|
58
|
+
if mem.get("peak_rss_mb") is not None:
|
|
59
|
+
_log_line(log_file, f"peakRssMb = {mem['peak_rss_mb']}")
|
|
60
|
+
if mem.get("avg_rss_mb") is not None:
|
|
61
|
+
_log_line(log_file, f"avgRssMb = {mem['avg_rss_mb']}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_homegraph_bin(explicit: str | None = None) -> str:
|
|
65
|
+
if explicit:
|
|
66
|
+
p = Path(explicit)
|
|
67
|
+
if p.is_file() and os.access(p, os.X_OK):
|
|
68
|
+
return str(p.resolve())
|
|
69
|
+
raise FileNotFoundError(f"homegraph binary not found: {explicit}")
|
|
70
|
+
|
|
71
|
+
# Prefer local dev build when testing unreleased changes
|
|
72
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
73
|
+
for local in (
|
|
74
|
+
repo_root / "dist" / "bin" / "homegraph.js",
|
|
75
|
+
repo_root / "dist" / "bin" / "homegraph.js",
|
|
76
|
+
):
|
|
77
|
+
if local.is_file():
|
|
78
|
+
return f"node {local.resolve()}"
|
|
79
|
+
|
|
80
|
+
for name in ("homegraph", "homegraph"):
|
|
81
|
+
found = shutil.which(name)
|
|
82
|
+
if found:
|
|
83
|
+
return found
|
|
84
|
+
raise FileNotFoundError(
|
|
85
|
+
"homegraph/homegraph not on PATH.\n"
|
|
86
|
+
" 在 homegraph 仓库里: npm run build\n"
|
|
87
|
+
" 或: python scripts/qa_eval/run_pipeline.py ab --homegraph-bin 'node /path/to/dist/bin/homegraph.js'"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _hg_cmd(hg_bin: str, args: list[str]) -> list[str]:
|
|
92
|
+
if hg_bin.startswith("node "):
|
|
93
|
+
return hg_bin.split(" ", 1) + args
|
|
94
|
+
return [hg_bin, *args]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def require_index(repo: Path) -> None:
|
|
98
|
+
for name in (".homegraph", ".homegraph"):
|
|
99
|
+
d = repo / name
|
|
100
|
+
if (d / "homegraph.db").exists() or (d / "homegraph.db").exists():
|
|
101
|
+
return
|
|
102
|
+
raise RuntimeError(f"仓库未索引: {repo}\n请先运行: homegraph sync {repo}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def tool_block(name: str, body: str) -> str:
|
|
106
|
+
return f"---\n{name}\n{body.strip()}\n---"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def homegraph_query(repo: Path, query: str, *, hg_bin: str, limit: int = 8) -> str:
|
|
110
|
+
proc = subprocess.run(
|
|
111
|
+
_hg_cmd(hg_bin, ["query", query, "-p", str(repo.resolve()), "-l", str(limit), "-j"]),
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
timeout=120,
|
|
115
|
+
)
|
|
116
|
+
if proc.returncode != 0:
|
|
117
|
+
return f"(homegraph query failed: {(proc.stderr or proc.stdout)[-400:]})"
|
|
118
|
+
try:
|
|
119
|
+
hits = json.loads(proc.stdout)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
return proc.stdout[:4000]
|
|
122
|
+
lines: list[str] = []
|
|
123
|
+
for i, hit in enumerate(hits[:limit], 1):
|
|
124
|
+
node = hit.get("node") or {}
|
|
125
|
+
lines.append(
|
|
126
|
+
f"{i}. [{node.get('kind')}] {node.get('qualifiedName', node.get('name'))}\n"
|
|
127
|
+
f" file: {node.get('filePath')}:{node.get('startLine')}-{node.get('endLine')}\n"
|
|
128
|
+
f" sig: {node.get('signature', '')}"
|
|
129
|
+
)
|
|
130
|
+
return "\n".join(lines) if lines else "(no hits)"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_file(repo: Path, rel_path: str, start: int = 1, end: int | None = None, context: int = 0) -> str:
|
|
134
|
+
path = (repo / rel_path).resolve()
|
|
135
|
+
if not path.is_file() or not str(path).startswith(str(repo.resolve())):
|
|
136
|
+
return f"(file not found: {rel_path})"
|
|
137
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
138
|
+
if end is None:
|
|
139
|
+
end = min(len(lines), start + 80)
|
|
140
|
+
lo = max(1, start - context) - 1
|
|
141
|
+
hi = min(len(lines), end + context)
|
|
142
|
+
body = "\n".join(f"{i + 1:4}| {lines[i]}" for i in range(lo, hi))
|
|
143
|
+
return f"// {rel_path}:{start}-{end}\n{body}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def find_rg_binary() -> str | None:
|
|
147
|
+
found = shutil.which("rg")
|
|
148
|
+
if found:
|
|
149
|
+
return found
|
|
150
|
+
candidates = [
|
|
151
|
+
Path.home() / ".cursor-server/bin/*/node_modules/@vscode/ripgrep/bin/rg",
|
|
152
|
+
]
|
|
153
|
+
import glob
|
|
154
|
+
|
|
155
|
+
for pattern in candidates:
|
|
156
|
+
for path in glob.glob(str(pattern)):
|
|
157
|
+
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
158
|
+
return path
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def search_text(repo: Path, pattern: str, max_hits: int = 20) -> str:
|
|
163
|
+
rg = find_rg_binary()
|
|
164
|
+
if not rg:
|
|
165
|
+
return "(rg not found on PATH; install: sudo apt install ripgrep)"
|
|
166
|
+
proc = subprocess.run(
|
|
167
|
+
[rg, "-n", "--no-heading", "-m", str(max_hits), pattern, str(repo)],
|
|
168
|
+
capture_output=True,
|
|
169
|
+
text=True,
|
|
170
|
+
timeout=60,
|
|
171
|
+
)
|
|
172
|
+
out = (proc.stdout or proc.stderr or "").strip()
|
|
173
|
+
return out[:6000] if out else "(no matches)"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def find_files(repo: Path, pattern: str, max_hits: int = 30) -> str:
|
|
177
|
+
"""Find files by glob. Invalid patterns return a hint instead of crashing the agent."""
|
|
178
|
+
pattern = pattern.strip().lstrip("/")
|
|
179
|
+
if not pattern:
|
|
180
|
+
return "(empty pattern)"
|
|
181
|
+
|
|
182
|
+
hits: list[str] = []
|
|
183
|
+
try:
|
|
184
|
+
# pathlib rglob: ** must be a whole path segment (e.g. **/*.ets, not */**/x)
|
|
185
|
+
if "**" in pattern and not pattern.startswith("**"):
|
|
186
|
+
# Fallback: treat as substring search via rg --files | rg
|
|
187
|
+
rg = shutil.which("rg")
|
|
188
|
+
if rg:
|
|
189
|
+
proc = subprocess.run(
|
|
190
|
+
[rg, "--files", str(repo)],
|
|
191
|
+
capture_output=True,
|
|
192
|
+
text=True,
|
|
193
|
+
timeout=60,
|
|
194
|
+
)
|
|
195
|
+
needle = pattern.replace("**", "").replace("*", "")
|
|
196
|
+
for line in (proc.stdout or "").splitlines():
|
|
197
|
+
if needle and needle.lower() in line.lower():
|
|
198
|
+
hits.append(line)
|
|
199
|
+
if len(hits) >= max_hits:
|
|
200
|
+
break
|
|
201
|
+
return "\n".join(hits) if hits else "(no files; try SearchText instead)"
|
|
202
|
+
return "(invalid glob: use pattern like **/*.ets or use SearchText)"
|
|
203
|
+
|
|
204
|
+
for p in sorted(repo.rglob(pattern)):
|
|
205
|
+
if p.is_file() and ".git" not in p.parts:
|
|
206
|
+
hits.append(str(p.relative_to(repo)))
|
|
207
|
+
if len(hits) >= max_hits:
|
|
208
|
+
break
|
|
209
|
+
except ValueError as e:
|
|
210
|
+
return f"(invalid glob pattern '{pattern}': {e}. Try **/*.ets or SearchText)"
|
|
211
|
+
except OSError as e:
|
|
212
|
+
return f"(find failed: {e})"
|
|
213
|
+
|
|
214
|
+
return "\n".join(hits) if hits else "(no files)"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
WITH_TOOLS = [
|
|
218
|
+
{
|
|
219
|
+
"type": "function",
|
|
220
|
+
"function": {
|
|
221
|
+
"name": "HomegraphQuery",
|
|
222
|
+
"description": "Search symbols in the repo via homegraph index (preferred for finding functions/classes/files).",
|
|
223
|
+
"parameters": {
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": {
|
|
226
|
+
"query": {"type": "string", "description": "Symbol name or search terms"},
|
|
227
|
+
"limit": {"type": "integer", "description": "Max hits", "default": 8},
|
|
228
|
+
},
|
|
229
|
+
"required": ["query"],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"type": "function",
|
|
235
|
+
"function": {
|
|
236
|
+
"name": "ReadFile",
|
|
237
|
+
"description": "Read source file lines from the repo.",
|
|
238
|
+
"parameters": {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"path": {"type": "string"},
|
|
242
|
+
"start_line": {"type": "integer", "default": 1},
|
|
243
|
+
"end_line": {"type": "integer"},
|
|
244
|
+
},
|
|
245
|
+
"required": ["path"],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
WITHOUT_TOOLS = [
|
|
252
|
+
{
|
|
253
|
+
"type": "function",
|
|
254
|
+
"function": {
|
|
255
|
+
"name": "SearchText",
|
|
256
|
+
"description": "Ripgrep search in the repo (like grep).",
|
|
257
|
+
"parameters": {
|
|
258
|
+
"type": "object",
|
|
259
|
+
"properties": {"pattern": {"type": "string"}},
|
|
260
|
+
"required": ["pattern"],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"type": "function",
|
|
266
|
+
"function": {
|
|
267
|
+
"name": "ReadFile",
|
|
268
|
+
"description": "Read source file lines from the repo.",
|
|
269
|
+
"parameters": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"path": {"type": "string"},
|
|
273
|
+
"start_line": {"type": "integer", "default": 1},
|
|
274
|
+
"end_line": {"type": "integer"},
|
|
275
|
+
},
|
|
276
|
+
"required": ["path"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"type": "function",
|
|
282
|
+
"function": {
|
|
283
|
+
"name": "FindFiles",
|
|
284
|
+
"description": "Glob find files under repo. Use patterns like *.ets or **/BenchmarkHub*.ets (not */**/x).",
|
|
285
|
+
"parameters": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"properties": {"pattern": {"type": "string"}},
|
|
288
|
+
"required": ["pattern"],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def execute_tool(
|
|
296
|
+
repo: Path,
|
|
297
|
+
name: str,
|
|
298
|
+
args: dict[str, Any],
|
|
299
|
+
*,
|
|
300
|
+
arm: str,
|
|
301
|
+
hg_bin: str,
|
|
302
|
+
) -> str:
|
|
303
|
+
if name == "HomegraphQuery":
|
|
304
|
+
if arm != "with":
|
|
305
|
+
return "(homegraph disabled in without arm)"
|
|
306
|
+
return homegraph_query(repo, args["query"], hg_bin=hg_bin, limit=int(args.get("limit") or 8))
|
|
307
|
+
if name == "ReadFile":
|
|
308
|
+
return read_file(
|
|
309
|
+
repo,
|
|
310
|
+
args["path"],
|
|
311
|
+
start=int(args.get("start_line") or 1),
|
|
312
|
+
end=int(args["end_line"]) if args.get("end_line") else None,
|
|
313
|
+
)
|
|
314
|
+
if name == "SearchText":
|
|
315
|
+
return search_text(repo, args["pattern"])
|
|
316
|
+
if name == "FindFiles":
|
|
317
|
+
return find_files(repo, args["pattern"])
|
|
318
|
+
return f"(unknown tool: {name})"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def run_agent_on_query(
|
|
322
|
+
repo: Path,
|
|
323
|
+
query: str,
|
|
324
|
+
*,
|
|
325
|
+
arm: str,
|
|
326
|
+
api_key: str,
|
|
327
|
+
base_url: str,
|
|
328
|
+
model: str,
|
|
329
|
+
hg_bin: str,
|
|
330
|
+
log_file: Path | None = None,
|
|
331
|
+
task_id: int = 1,
|
|
332
|
+
max_turns: int = 8,
|
|
333
|
+
timeout_sec: int = 600,
|
|
334
|
+
extra_body: dict | None = None,
|
|
335
|
+
) -> dict[str, Any]:
|
|
336
|
+
if OpenAI is None:
|
|
337
|
+
raise RuntimeError("pip install openai")
|
|
338
|
+
|
|
339
|
+
client = OpenAI(api_key=api_key, base_url=base_url, timeout=timeout_sec)
|
|
340
|
+
tools = WITH_TOOLS if arm == "with" else WITHOUT_TOOLS
|
|
341
|
+
backend = "agent-with-homegraph" if arm == "with" else "agent-grep-read"
|
|
342
|
+
|
|
343
|
+
if arm == "with":
|
|
344
|
+
system = (
|
|
345
|
+
"你是鸿蒙 ArkTS 代码分析 Agent。可用 HomegraphQuery 检索符号、ReadFile 读源码。"
|
|
346
|
+
"先检索再读文件,基于仓库事实作答。回答简洁准确,中文。"
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
system = (
|
|
350
|
+
"你是鸿蒙 ArkTS 代码分析 Agent。可用 SearchText(rg)、FindFiles、ReadFile 探索仓库。"
|
|
351
|
+
"没有 homegraph,请主动搜索和读文件后再答。回答简洁准确,中文。"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
messages: list[dict[str, Any]] = [
|
|
355
|
+
{"role": "system", "content": system},
|
|
356
|
+
{"role": "user", "content": query},
|
|
357
|
+
]
|
|
358
|
+
tool_trace: list[str] = []
|
|
359
|
+
total_tokens = 0
|
|
360
|
+
t0 = time.time()
|
|
361
|
+
_log_line(log_file, f"Evaluate {task_id}:")
|
|
362
|
+
|
|
363
|
+
with sample_memory() as mem_sampler:
|
|
364
|
+
for turn in range(1, max_turns + 1):
|
|
365
|
+
_log_line(log_file, f"the {turn} turn")
|
|
366
|
+
print_agent_turn(task_id, turn, max_turns)
|
|
367
|
+
create_kwargs: dict[str, Any] = {
|
|
368
|
+
"model": model,
|
|
369
|
+
"messages": messages,
|
|
370
|
+
"tools": tools,
|
|
371
|
+
"temperature": 0.2,
|
|
372
|
+
}
|
|
373
|
+
if extra_body:
|
|
374
|
+
create_kwargs["extra_body"] = extra_body
|
|
375
|
+
resp = client.chat.completions.create(**create_kwargs)
|
|
376
|
+
if turn == 1:
|
|
377
|
+
_log_line(log_file, "first token")
|
|
378
|
+
if resp.usage:
|
|
379
|
+
total_tokens += int(resp.usage.total_tokens or 0)
|
|
380
|
+
_log_line(log_file, f"totalTokenCount = {resp.usage.total_tokens}")
|
|
381
|
+
|
|
382
|
+
msg = resp.choices[0].message
|
|
383
|
+
if msg.tool_calls:
|
|
384
|
+
names = ", ".join(tc.function.name for tc in msg.tool_calls)
|
|
385
|
+
print_agent_turn(task_id, turn, max_turns, f"工具: {names}")
|
|
386
|
+
messages.append(msg.model_dump())
|
|
387
|
+
for tc in msg.tool_calls:
|
|
388
|
+
fn = tc.function
|
|
389
|
+
try:
|
|
390
|
+
fn_args = json.loads(fn.arguments or "{}")
|
|
391
|
+
except json.JSONDecodeError:
|
|
392
|
+
fn_args = {}
|
|
393
|
+
result = execute_tool(repo, fn.name, fn_args, arm=arm, hg_bin=hg_bin)
|
|
394
|
+
arg_str = json.dumps(fn_args, ensure_ascii=False)
|
|
395
|
+
tool_trace.append(tool_block(fn.name, f"args: {arg_str}\nresult:\n{result[:3000]}"))
|
|
396
|
+
messages.append(
|
|
397
|
+
{
|
|
398
|
+
"role": "tool",
|
|
399
|
+
"tool_call_id": tc.id,
|
|
400
|
+
"content": result[:8000],
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
answer = (msg.content or "").strip()
|
|
406
|
+
duration_ms = int((time.time() - t0) * 1000)
|
|
407
|
+
trace_text = "\n\n".join(tool_trace)
|
|
408
|
+
output = f"{trace_text}\n\n{answer}" if trace_text else answer
|
|
409
|
+
usage = resp.usage
|
|
410
|
+
mem = mem_sampler.last_stats
|
|
411
|
+
_log_memory(log_file, mem)
|
|
412
|
+
meta: dict[str, Any] = {
|
|
413
|
+
"output_answer": output,
|
|
414
|
+
"agent_status": "success" if answer else "error",
|
|
415
|
+
"agent_backend": backend,
|
|
416
|
+
"agent_host": "builtin",
|
|
417
|
+
"agent_model": model,
|
|
418
|
+
"agent_turns": turn,
|
|
419
|
+
"agent_duration_ms": duration_ms,
|
|
420
|
+
"ab_arm": "with-homegraph" if arm == "with" else "without-homegraph",
|
|
421
|
+
"agent_memory_mb": mem,
|
|
422
|
+
}
|
|
423
|
+
if usage:
|
|
424
|
+
meta["agent_usage"] = {
|
|
425
|
+
"prompt_tokens": usage.prompt_tokens,
|
|
426
|
+
"completion_tokens": usage.completion_tokens,
|
|
427
|
+
"total_tokens": usage.total_tokens,
|
|
428
|
+
}
|
|
429
|
+
if not answer:
|
|
430
|
+
meta["agent_error"] = "empty final answer"
|
|
431
|
+
return meta
|
|
432
|
+
|
|
433
|
+
mem = mem_sampler.last_stats
|
|
434
|
+
_log_memory(log_file, mem)
|
|
435
|
+
return {
|
|
436
|
+
"output_answer": "\n\n".join(tool_trace),
|
|
437
|
+
"agent_status": "error",
|
|
438
|
+
"agent_error": f"max turns ({max_turns}) exceeded",
|
|
439
|
+
"agent_backend": backend,
|
|
440
|
+
"agent_host": "builtin",
|
|
441
|
+
"agent_model": model,
|
|
442
|
+
"ab_arm": "with-homegraph" if arm == "with" else "without-homegraph",
|
|
443
|
+
"agent_memory_mb": mem,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def run_agent_dataset(
|
|
448
|
+
repo: Path,
|
|
449
|
+
dataset: list[dict[str, Any]],
|
|
450
|
+
*,
|
|
451
|
+
arm: str,
|
|
452
|
+
output: Path,
|
|
453
|
+
log_file: Path | None,
|
|
454
|
+
api_key: str,
|
|
455
|
+
base_url: str,
|
|
456
|
+
model: str,
|
|
457
|
+
hg_bin: str,
|
|
458
|
+
max_turns: int = 8,
|
|
459
|
+
extra_body: dict | None = None,
|
|
460
|
+
) -> list[dict[str, Any]]:
|
|
461
|
+
if arm == "with":
|
|
462
|
+
require_index(repo)
|
|
463
|
+
hg = find_homegraph_bin(hg_bin) if arm == "with" else ""
|
|
464
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
465
|
+
if log_file:
|
|
466
|
+
log_file.write_text("", encoding="utf-8")
|
|
467
|
+
|
|
468
|
+
results: list[dict[str, Any]] = []
|
|
469
|
+
auth_failed = False
|
|
470
|
+
total = len(dataset)
|
|
471
|
+
print(f" → {_arm_short(arm)} 臂:共 {total} 题", flush=True)
|
|
472
|
+
with output.open("w", encoding="utf-8") as f:
|
|
473
|
+
for i, item in enumerate(dataset, 1):
|
|
474
|
+
q = str(item["query"])
|
|
475
|
+
item_id = str(item.get("id") or i)
|
|
476
|
+
print_agent_progress(arm, i, total, item_id, "开始…")
|
|
477
|
+
logger.info("[%s] %s/%s %s", arm, i, total, item_id)
|
|
478
|
+
try:
|
|
479
|
+
agent_meta = run_agent_on_query(
|
|
480
|
+
repo,
|
|
481
|
+
q,
|
|
482
|
+
arm=arm,
|
|
483
|
+
api_key=api_key,
|
|
484
|
+
base_url=base_url,
|
|
485
|
+
model=model,
|
|
486
|
+
hg_bin=hg,
|
|
487
|
+
log_file=log_file,
|
|
488
|
+
task_id=i,
|
|
489
|
+
max_turns=max_turns,
|
|
490
|
+
extra_body=extra_body,
|
|
491
|
+
)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
err = str(e)
|
|
494
|
+
logger.error("Agent failed %s: %s", item.get("id"), e)
|
|
495
|
+
if "401" in err or "invalid_api_key" in err or "Incorrect API key" in err:
|
|
496
|
+
auth_failed = True
|
|
497
|
+
agent_meta = {
|
|
498
|
+
"output_answer": "",
|
|
499
|
+
"agent_status": "error",
|
|
500
|
+
"agent_error": err,
|
|
501
|
+
"agent_backend": "agent-with-homegraph" if arm == "with" else "agent-grep-read",
|
|
502
|
+
}
|
|
503
|
+
row = {**item, **agent_meta}
|
|
504
|
+
results.append(row)
|
|
505
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
506
|
+
f.flush()
|
|
507
|
+
if agent_meta.get("agent_status") == "success":
|
|
508
|
+
turns = agent_meta.get("agent_turns", "?")
|
|
509
|
+
dur_ms = agent_meta.get("agent_duration_ms")
|
|
510
|
+
dur_s = f"{dur_ms / 1000:.1f}s" if isinstance(dur_ms, (int, float)) else "?"
|
|
511
|
+
print_agent_progress(arm, i, total, item_id, f"完成 ({turns} 轮, {dur_s})")
|
|
512
|
+
else:
|
|
513
|
+
err = str(agent_meta.get("agent_error") or agent_meta.get("agent_status") or "error")
|
|
514
|
+
print_agent_progress(arm, i, total, item_id, f"失败: {err[:100]}")
|
|
515
|
+
if auth_failed:
|
|
516
|
+
raise RuntimeError(
|
|
517
|
+
"LLM API 鉴权失败 (401)。智谱 Key 请用:\n"
|
|
518
|
+
" export ZHIPU_API_KEY='your-id.your-secret'\n"
|
|
519
|
+
" python scripts/qa_eval/run_pipeline.py ab --provider zhipu\n"
|
|
520
|
+
"DashScope Key 请用:\n"
|
|
521
|
+
" export DASHSCOPE_API_KEY='sk-...'\n"
|
|
522
|
+
" python scripts/qa_eval/run_pipeline.py ab --provider dashscope"
|
|
523
|
+
) from None
|
|
524
|
+
ok = sum(1 for r in results if r.get("agent_status") == "success")
|
|
525
|
+
print(f" → {_arm_short(arm)} 臂结束:{ok}/{total} 成功", flush=True)
|
|
526
|
+
return results
|