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,279 +1,279 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Stage 4 — 从 Agent 日志统计效率指标(首响应、轮次、总耗时、总 Token)。
|
|
4
|
-
|
|
5
|
-
「首响应」= 从 Evaluate 开始到第一次 LLM API 返回的耗时(日志行 `first token`)。
|
|
6
|
-
|
|
7
|
-
兼容 CodeGenie / Trae 等 Agent 日志格式:
|
|
8
|
-
- Evaluate N:
|
|
9
|
-
- first token
|
|
10
|
-
- the N turn
|
|
11
|
-
- totalTokenCount = N
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
python scripts/qa_eval/stats_efficiency.py -l scripts/qa_eval/log/agent-with-builtin.log
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import argparse
|
|
20
|
-
import re
|
|
21
|
-
import sys
|
|
22
|
-
from datetime import datetime
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from statistics import mean
|
|
25
|
-
|
|
26
|
-
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
27
|
-
DEFAULT_LOG = _SCRIPT_DIR / "log" / "agent-with-builtin.log"
|
|
28
|
-
|
|
29
|
-
TIME_FMT = "%Y-%m-%d %H:%M:%S.%f"
|
|
30
|
-
|
|
31
|
-
TIME_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)")
|
|
32
|
-
EVALUATE_PATTERN = re.compile(r"Evaluate\s+(\d+):")
|
|
33
|
-
FIRST_TOKEN_PATTERN = re.compile(r"first token")
|
|
34
|
-
TURN_PATTERN = re.compile(r"the\s+(\d+)\s+turn")
|
|
35
|
-
TOKEN_PATTERN = re.compile(r"totalTokenCount\s*=\s*(\d+)")
|
|
36
|
-
PEAK_RSS_PATTERN = re.compile(r"peakRssMb\s*=\s*([\d.]+)")
|
|
37
|
-
AVG_RSS_PATTERN = re.compile(r"avgRssMb\s*=\s*([\d.]+)")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def parse_agent_log(log_file: Path) -> list[dict]:
|
|
41
|
-
tasks: list[dict] = []
|
|
42
|
-
current_task: dict | None = None
|
|
43
|
-
last_log_time: datetime | None = None
|
|
44
|
-
|
|
45
|
-
with log_file.open("r", encoding="utf-8", errors="replace") as f:
|
|
46
|
-
for line in f:
|
|
47
|
-
line = line.strip()
|
|
48
|
-
if not line:
|
|
49
|
-
continue
|
|
50
|
-
|
|
51
|
-
time_match = TIME_PATTERN.search(line)
|
|
52
|
-
if not time_match:
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
log_time = datetime.strptime(time_match.group(1), TIME_FMT)
|
|
57
|
-
except ValueError:
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
last_log_time = log_time
|
|
61
|
-
|
|
62
|
-
eval_match = EVALUATE_PATTERN.search(line)
|
|
63
|
-
if eval_match:
|
|
64
|
-
if current_task:
|
|
65
|
-
current_task["end_time"] = log_time
|
|
66
|
-
tasks.append(current_task)
|
|
67
|
-
|
|
68
|
-
current_task = {
|
|
69
|
-
"id": int(eval_match.group(1)),
|
|
70
|
-
"start_time": log_time,
|
|
71
|
-
"first_token_time": None,
|
|
72
|
-
"first_api_response_time": None,
|
|
73
|
-
"max_turn": 0,
|
|
74
|
-
"end_time": None,
|
|
75
|
-
"total_tokens": 0,
|
|
76
|
-
"peak_rss_mb": None,
|
|
77
|
-
"avg_rss_mb": None,
|
|
78
|
-
}
|
|
79
|
-
continue
|
|
80
|
-
|
|
81
|
-
if current_task is None:
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
if FIRST_TOKEN_PATTERN.search(line):
|
|
85
|
-
current_task["first_token_time"] = log_time
|
|
86
|
-
continue
|
|
87
|
-
|
|
88
|
-
turn_match = TURN_PATTERN.search(line)
|
|
89
|
-
if turn_match:
|
|
90
|
-
turn = int(turn_match.group(1))
|
|
91
|
-
current_task["max_turn"] = max(current_task["max_turn"], turn)
|
|
92
|
-
|
|
93
|
-
token_match = TOKEN_PATTERN.search(line)
|
|
94
|
-
if token_match:
|
|
95
|
-
if current_task["first_api_response_time"] is None:
|
|
96
|
-
current_task["first_api_response_time"] = log_time
|
|
97
|
-
current_task["total_tokens"] += int(token_match.group(1))
|
|
98
|
-
|
|
99
|
-
peak_match = PEAK_RSS_PATTERN.search(line)
|
|
100
|
-
if peak_match:
|
|
101
|
-
current_task["peak_rss_mb"] = float(peak_match.group(1))
|
|
102
|
-
|
|
103
|
-
avg_match = AVG_RSS_PATTERN.search(line)
|
|
104
|
-
if avg_match:
|
|
105
|
-
current_task["avg_rss_mb"] = float(avg_match.group(1))
|
|
106
|
-
|
|
107
|
-
if current_task:
|
|
108
|
-
current_task["end_time"] = last_log_time
|
|
109
|
-
tasks.append(current_task)
|
|
110
|
-
|
|
111
|
-
return tasks
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _memory_from_rows(rows: list[dict]) -> tuple[list[float], list[float]]:
|
|
115
|
-
peaks: list[float] = []
|
|
116
|
-
avgs: list[float] = []
|
|
117
|
-
for row in rows:
|
|
118
|
-
mem = row.get("agent_memory_mb") or {}
|
|
119
|
-
if mem.get("peak_rss_mb") is not None:
|
|
120
|
-
peaks.append(float(mem["peak_rss_mb"]))
|
|
121
|
-
if mem.get("avg_rss_mb") is not None:
|
|
122
|
-
avgs.append(float(mem["avg_rss_mb"]))
|
|
123
|
-
return peaks, avgs
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def summarize_jsonl_usage(rows: list[dict]) -> dict:
|
|
127
|
-
"""Fallback when agent .log is missing: aggregate agent_usage from JSONL rows."""
|
|
128
|
-
tokens: list[int] = []
|
|
129
|
-
for row in rows:
|
|
130
|
-
usage = row.get("agent_usage") or {}
|
|
131
|
-
total = usage.get("total_tokens")
|
|
132
|
-
if isinstance(total, (int, float)):
|
|
133
|
-
tokens.append(int(total))
|
|
134
|
-
peaks, avgs = _memory_from_rows(rows)
|
|
135
|
-
return {
|
|
136
|
-
"task_count": len(rows),
|
|
137
|
-
"avg_first_token_s": None,
|
|
138
|
-
"avg_turns": None,
|
|
139
|
-
"avg_duration_s": None,
|
|
140
|
-
"avg_tokens": mean(tokens) if tokens else None,
|
|
141
|
-
"total_tokens": sum(tokens),
|
|
142
|
-
"avg_peak_rss_mb": mean(peaks) if peaks else None,
|
|
143
|
-
"max_peak_rss_mb": max(peaks) if peaks else None,
|
|
144
|
-
"avg_avg_rss_mb": mean(avgs) if avgs else None,
|
|
145
|
-
"source": "jsonl",
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
FIRST_TOKEN_MIN_S = 0.05
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def effective_first_response_time(task: dict) -> datetime | None:
|
|
153
|
-
"""首响应时间:优先用 first token 行;若与 Evaluate 几乎同时(旧日志 bug)则回退到首次 API 返回。"""
|
|
154
|
-
start = task.get("start_time")
|
|
155
|
-
ft = task.get("first_token_time")
|
|
156
|
-
if start and ft:
|
|
157
|
-
if (ft - start).total_seconds() >= FIRST_TOKEN_MIN_S:
|
|
158
|
-
return ft
|
|
159
|
-
return task.get("first_api_response_time") or ft
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def summarize_tasks(tasks: list[dict], *, print_report: bool = True) -> dict:
|
|
163
|
-
first_token_costs: list[float] = []
|
|
164
|
-
turns: list[int] = []
|
|
165
|
-
durations: list[float] = []
|
|
166
|
-
tokens_list: list[int] = []
|
|
167
|
-
peak_rss: list[float] = []
|
|
168
|
-
avg_rss: list[float] = []
|
|
169
|
-
|
|
170
|
-
if print_report:
|
|
171
|
-
print("=" * 100)
|
|
172
|
-
print("任务明细")
|
|
173
|
-
print("=" * 100)
|
|
174
|
-
|
|
175
|
-
for task in tasks:
|
|
176
|
-
first_token_seconds = None
|
|
177
|
-
response_time = effective_first_response_time(task)
|
|
178
|
-
if response_time and task["start_time"]:
|
|
179
|
-
first_token_seconds = (response_time - task["start_time"]).total_seconds()
|
|
180
|
-
first_token_costs.append(first_token_seconds)
|
|
181
|
-
|
|
182
|
-
turns.append(task["max_turn"])
|
|
183
|
-
|
|
184
|
-
duration_seconds = 0.0
|
|
185
|
-
if task["end_time"] and task["start_time"]:
|
|
186
|
-
duration_seconds = (task["end_time"] - task["start_time"]).total_seconds()
|
|
187
|
-
durations.append(duration_seconds)
|
|
188
|
-
|
|
189
|
-
tokens_list.append(task["total_tokens"])
|
|
190
|
-
if task.get("peak_rss_mb") is not None:
|
|
191
|
-
peak_rss.append(float(task["peak_rss_mb"]))
|
|
192
|
-
if task.get("avg_rss_mb") is not None:
|
|
193
|
-
avg_rss.append(float(task["avg_rss_mb"]))
|
|
194
|
-
|
|
195
|
-
if print_report:
|
|
196
|
-
first_display = f"{first_token_seconds:.2f}s" if first_token_seconds is not None else "N/A"
|
|
197
|
-
mem_display = (
|
|
198
|
-
f"{task['peak_rss_mb']:.0f}MB"
|
|
199
|
-
if task.get("peak_rss_mb") is not None
|
|
200
|
-
else "N/A"
|
|
201
|
-
)
|
|
202
|
-
print(
|
|
203
|
-
f"任务 {task['id']:>3} | "
|
|
204
|
-
f"首响应: {first_display:<8} | "
|
|
205
|
-
f"轮次: {task['max_turn']:>3} | "
|
|
206
|
-
f"总时间: {duration_seconds:.2f}s | "
|
|
207
|
-
f"总Token: {task['total_tokens']:<6} | "
|
|
208
|
-
f"峰值内存: {mem_display}"
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
summary = {
|
|
212
|
-
"task_count": len(tasks),
|
|
213
|
-
"avg_first_token_s": mean(first_token_costs) if first_token_costs else None,
|
|
214
|
-
"avg_turns": mean(turns) if turns else None,
|
|
215
|
-
"avg_duration_s": mean(durations) if durations else None,
|
|
216
|
-
"avg_tokens": mean(tokens_list) if tokens_list else None,
|
|
217
|
-
"total_tokens": sum(tokens_list),
|
|
218
|
-
"avg_peak_rss_mb": mean(peak_rss) if peak_rss else None,
|
|
219
|
-
"max_peak_rss_mb": max(peak_rss) if peak_rss else None,
|
|
220
|
-
"avg_avg_rss_mb": mean(avg_rss) if avg_rss else None,
|
|
221
|
-
"source": "log",
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if print_report:
|
|
225
|
-
print("\n" + "=" * 100)
|
|
226
|
-
print("统计结果")
|
|
227
|
-
print("=" * 100)
|
|
228
|
-
print(f"任务总数: {summary['task_count']}")
|
|
229
|
-
|
|
230
|
-
if summary["avg_first_token_s"] is not None:
|
|
231
|
-
print(f"平均首响应时间: {summary['avg_first_token_s']:.2f} 秒")
|
|
232
|
-
if summary["avg_turns"] is not None:
|
|
233
|
-
print(f"平均轮次: {summary['avg_turns']:.2f}")
|
|
234
|
-
if summary["avg_duration_s"] is not None:
|
|
235
|
-
print(f"平均总时间: {summary['avg_duration_s'] / 60:.2f} min ({summary['avg_duration_s']:.2f} s)")
|
|
236
|
-
if summary["avg_tokens"] is not None:
|
|
237
|
-
print(f"平均总 Token: {summary['avg_tokens'] / 1000:.2f} k")
|
|
238
|
-
print(f"合计总 Token: {summary['total_tokens']}")
|
|
239
|
-
if summary["avg_peak_rss_mb"] is not None:
|
|
240
|
-
print(f"平均峰值内存: {summary['avg_peak_rss_mb']:.1f} MB")
|
|
241
|
-
if summary["max_peak_rss_mb"] is not None:
|
|
242
|
-
print(f"最大峰值内存: {summary['max_peak_rss_mb']:.1f} MB")
|
|
243
|
-
if summary["avg_avg_rss_mb"] is not None:
|
|
244
|
-
print(f"平均内存占用: {summary['avg_avg_rss_mb']:.1f} MB")
|
|
245
|
-
|
|
246
|
-
print("=" * 100)
|
|
247
|
-
return summary
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def summarize_log(log_file: Path, *, print_report: bool = True) -> dict | None:
|
|
251
|
-
if not log_file.is_file():
|
|
252
|
-
return None
|
|
253
|
-
tasks = parse_agent_log(log_file)
|
|
254
|
-
if not tasks:
|
|
255
|
-
return None
|
|
256
|
-
return summarize_tasks(tasks, print_report=print_report)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def main() -> int:
|
|
260
|
-
parser = argparse.ArgumentParser(description="Agent 日志效率统计")
|
|
261
|
-
parser.add_argument("--log", "-l", type=str, default=str(DEFAULT_LOG), help="Agent 日志路径")
|
|
262
|
-
args = parser.parse_args()
|
|
263
|
-
|
|
264
|
-
log_file = Path(args.log)
|
|
265
|
-
if not log_file.is_file():
|
|
266
|
-
print(f"错误: 日志文件 {log_file} 不存在")
|
|
267
|
-
return 1
|
|
268
|
-
|
|
269
|
-
tasks = parse_agent_log(log_file)
|
|
270
|
-
if not tasks:
|
|
271
|
-
print("警告: 日志中未解析到任何 Evaluate 任务段")
|
|
272
|
-
return 1
|
|
273
|
-
|
|
274
|
-
summarize_tasks(tasks)
|
|
275
|
-
return 0
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if __name__ == "__main__":
|
|
279
|
-
raise SystemExit(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Stage 4 — 从 Agent 日志统计效率指标(首响应、轮次、总耗时、总 Token)。
|
|
4
|
+
|
|
5
|
+
「首响应」= 从 Evaluate 开始到第一次 LLM API 返回的耗时(日志行 `first token`)。
|
|
6
|
+
|
|
7
|
+
兼容 CodeGenie / Trae 等 Agent 日志格式:
|
|
8
|
+
- Evaluate N:
|
|
9
|
+
- first token
|
|
10
|
+
- the N turn
|
|
11
|
+
- totalTokenCount = N
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python scripts/qa_eval/stats_efficiency.py -l scripts/qa_eval/log/agent-with-builtin.log
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from statistics import mean
|
|
25
|
+
|
|
26
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
27
|
+
DEFAULT_LOG = _SCRIPT_DIR / "log" / "agent-with-builtin.log"
|
|
28
|
+
|
|
29
|
+
TIME_FMT = "%Y-%m-%d %H:%M:%S.%f"
|
|
30
|
+
|
|
31
|
+
TIME_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)")
|
|
32
|
+
EVALUATE_PATTERN = re.compile(r"Evaluate\s+(\d+):")
|
|
33
|
+
FIRST_TOKEN_PATTERN = re.compile(r"first token")
|
|
34
|
+
TURN_PATTERN = re.compile(r"the\s+(\d+)\s+turn")
|
|
35
|
+
TOKEN_PATTERN = re.compile(r"totalTokenCount\s*=\s*(\d+)")
|
|
36
|
+
PEAK_RSS_PATTERN = re.compile(r"peakRssMb\s*=\s*([\d.]+)")
|
|
37
|
+
AVG_RSS_PATTERN = re.compile(r"avgRssMb\s*=\s*([\d.]+)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_agent_log(log_file: Path) -> list[dict]:
|
|
41
|
+
tasks: list[dict] = []
|
|
42
|
+
current_task: dict | None = None
|
|
43
|
+
last_log_time: datetime | None = None
|
|
44
|
+
|
|
45
|
+
with log_file.open("r", encoding="utf-8", errors="replace") as f:
|
|
46
|
+
for line in f:
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if not line:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
time_match = TIME_PATTERN.search(line)
|
|
52
|
+
if not time_match:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
log_time = datetime.strptime(time_match.group(1), TIME_FMT)
|
|
57
|
+
except ValueError:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
last_log_time = log_time
|
|
61
|
+
|
|
62
|
+
eval_match = EVALUATE_PATTERN.search(line)
|
|
63
|
+
if eval_match:
|
|
64
|
+
if current_task:
|
|
65
|
+
current_task["end_time"] = log_time
|
|
66
|
+
tasks.append(current_task)
|
|
67
|
+
|
|
68
|
+
current_task = {
|
|
69
|
+
"id": int(eval_match.group(1)),
|
|
70
|
+
"start_time": log_time,
|
|
71
|
+
"first_token_time": None,
|
|
72
|
+
"first_api_response_time": None,
|
|
73
|
+
"max_turn": 0,
|
|
74
|
+
"end_time": None,
|
|
75
|
+
"total_tokens": 0,
|
|
76
|
+
"peak_rss_mb": None,
|
|
77
|
+
"avg_rss_mb": None,
|
|
78
|
+
}
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if current_task is None:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
if FIRST_TOKEN_PATTERN.search(line):
|
|
85
|
+
current_task["first_token_time"] = log_time
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
turn_match = TURN_PATTERN.search(line)
|
|
89
|
+
if turn_match:
|
|
90
|
+
turn = int(turn_match.group(1))
|
|
91
|
+
current_task["max_turn"] = max(current_task["max_turn"], turn)
|
|
92
|
+
|
|
93
|
+
token_match = TOKEN_PATTERN.search(line)
|
|
94
|
+
if token_match:
|
|
95
|
+
if current_task["first_api_response_time"] is None:
|
|
96
|
+
current_task["first_api_response_time"] = log_time
|
|
97
|
+
current_task["total_tokens"] += int(token_match.group(1))
|
|
98
|
+
|
|
99
|
+
peak_match = PEAK_RSS_PATTERN.search(line)
|
|
100
|
+
if peak_match:
|
|
101
|
+
current_task["peak_rss_mb"] = float(peak_match.group(1))
|
|
102
|
+
|
|
103
|
+
avg_match = AVG_RSS_PATTERN.search(line)
|
|
104
|
+
if avg_match:
|
|
105
|
+
current_task["avg_rss_mb"] = float(avg_match.group(1))
|
|
106
|
+
|
|
107
|
+
if current_task:
|
|
108
|
+
current_task["end_time"] = last_log_time
|
|
109
|
+
tasks.append(current_task)
|
|
110
|
+
|
|
111
|
+
return tasks
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _memory_from_rows(rows: list[dict]) -> tuple[list[float], list[float]]:
|
|
115
|
+
peaks: list[float] = []
|
|
116
|
+
avgs: list[float] = []
|
|
117
|
+
for row in rows:
|
|
118
|
+
mem = row.get("agent_memory_mb") or {}
|
|
119
|
+
if mem.get("peak_rss_mb") is not None:
|
|
120
|
+
peaks.append(float(mem["peak_rss_mb"]))
|
|
121
|
+
if mem.get("avg_rss_mb") is not None:
|
|
122
|
+
avgs.append(float(mem["avg_rss_mb"]))
|
|
123
|
+
return peaks, avgs
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def summarize_jsonl_usage(rows: list[dict]) -> dict:
|
|
127
|
+
"""Fallback when agent .log is missing: aggregate agent_usage from JSONL rows."""
|
|
128
|
+
tokens: list[int] = []
|
|
129
|
+
for row in rows:
|
|
130
|
+
usage = row.get("agent_usage") or {}
|
|
131
|
+
total = usage.get("total_tokens")
|
|
132
|
+
if isinstance(total, (int, float)):
|
|
133
|
+
tokens.append(int(total))
|
|
134
|
+
peaks, avgs = _memory_from_rows(rows)
|
|
135
|
+
return {
|
|
136
|
+
"task_count": len(rows),
|
|
137
|
+
"avg_first_token_s": None,
|
|
138
|
+
"avg_turns": None,
|
|
139
|
+
"avg_duration_s": None,
|
|
140
|
+
"avg_tokens": mean(tokens) if tokens else None,
|
|
141
|
+
"total_tokens": sum(tokens),
|
|
142
|
+
"avg_peak_rss_mb": mean(peaks) if peaks else None,
|
|
143
|
+
"max_peak_rss_mb": max(peaks) if peaks else None,
|
|
144
|
+
"avg_avg_rss_mb": mean(avgs) if avgs else None,
|
|
145
|
+
"source": "jsonl",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
FIRST_TOKEN_MIN_S = 0.05
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def effective_first_response_time(task: dict) -> datetime | None:
|
|
153
|
+
"""首响应时间:优先用 first token 行;若与 Evaluate 几乎同时(旧日志 bug)则回退到首次 API 返回。"""
|
|
154
|
+
start = task.get("start_time")
|
|
155
|
+
ft = task.get("first_token_time")
|
|
156
|
+
if start and ft:
|
|
157
|
+
if (ft - start).total_seconds() >= FIRST_TOKEN_MIN_S:
|
|
158
|
+
return ft
|
|
159
|
+
return task.get("first_api_response_time") or ft
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def summarize_tasks(tasks: list[dict], *, print_report: bool = True) -> dict:
|
|
163
|
+
first_token_costs: list[float] = []
|
|
164
|
+
turns: list[int] = []
|
|
165
|
+
durations: list[float] = []
|
|
166
|
+
tokens_list: list[int] = []
|
|
167
|
+
peak_rss: list[float] = []
|
|
168
|
+
avg_rss: list[float] = []
|
|
169
|
+
|
|
170
|
+
if print_report:
|
|
171
|
+
print("=" * 100)
|
|
172
|
+
print("任务明细")
|
|
173
|
+
print("=" * 100)
|
|
174
|
+
|
|
175
|
+
for task in tasks:
|
|
176
|
+
first_token_seconds = None
|
|
177
|
+
response_time = effective_first_response_time(task)
|
|
178
|
+
if response_time and task["start_time"]:
|
|
179
|
+
first_token_seconds = (response_time - task["start_time"]).total_seconds()
|
|
180
|
+
first_token_costs.append(first_token_seconds)
|
|
181
|
+
|
|
182
|
+
turns.append(task["max_turn"])
|
|
183
|
+
|
|
184
|
+
duration_seconds = 0.0
|
|
185
|
+
if task["end_time"] and task["start_time"]:
|
|
186
|
+
duration_seconds = (task["end_time"] - task["start_time"]).total_seconds()
|
|
187
|
+
durations.append(duration_seconds)
|
|
188
|
+
|
|
189
|
+
tokens_list.append(task["total_tokens"])
|
|
190
|
+
if task.get("peak_rss_mb") is not None:
|
|
191
|
+
peak_rss.append(float(task["peak_rss_mb"]))
|
|
192
|
+
if task.get("avg_rss_mb") is not None:
|
|
193
|
+
avg_rss.append(float(task["avg_rss_mb"]))
|
|
194
|
+
|
|
195
|
+
if print_report:
|
|
196
|
+
first_display = f"{first_token_seconds:.2f}s" if first_token_seconds is not None else "N/A"
|
|
197
|
+
mem_display = (
|
|
198
|
+
f"{task['peak_rss_mb']:.0f}MB"
|
|
199
|
+
if task.get("peak_rss_mb") is not None
|
|
200
|
+
else "N/A"
|
|
201
|
+
)
|
|
202
|
+
print(
|
|
203
|
+
f"任务 {task['id']:>3} | "
|
|
204
|
+
f"首响应: {first_display:<8} | "
|
|
205
|
+
f"轮次: {task['max_turn']:>3} | "
|
|
206
|
+
f"总时间: {duration_seconds:.2f}s | "
|
|
207
|
+
f"总Token: {task['total_tokens']:<6} | "
|
|
208
|
+
f"峰值内存: {mem_display}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
summary = {
|
|
212
|
+
"task_count": len(tasks),
|
|
213
|
+
"avg_first_token_s": mean(first_token_costs) if first_token_costs else None,
|
|
214
|
+
"avg_turns": mean(turns) if turns else None,
|
|
215
|
+
"avg_duration_s": mean(durations) if durations else None,
|
|
216
|
+
"avg_tokens": mean(tokens_list) if tokens_list else None,
|
|
217
|
+
"total_tokens": sum(tokens_list),
|
|
218
|
+
"avg_peak_rss_mb": mean(peak_rss) if peak_rss else None,
|
|
219
|
+
"max_peak_rss_mb": max(peak_rss) if peak_rss else None,
|
|
220
|
+
"avg_avg_rss_mb": mean(avg_rss) if avg_rss else None,
|
|
221
|
+
"source": "log",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if print_report:
|
|
225
|
+
print("\n" + "=" * 100)
|
|
226
|
+
print("统计结果")
|
|
227
|
+
print("=" * 100)
|
|
228
|
+
print(f"任务总数: {summary['task_count']}")
|
|
229
|
+
|
|
230
|
+
if summary["avg_first_token_s"] is not None:
|
|
231
|
+
print(f"平均首响应时间: {summary['avg_first_token_s']:.2f} 秒")
|
|
232
|
+
if summary["avg_turns"] is not None:
|
|
233
|
+
print(f"平均轮次: {summary['avg_turns']:.2f}")
|
|
234
|
+
if summary["avg_duration_s"] is not None:
|
|
235
|
+
print(f"平均总时间: {summary['avg_duration_s'] / 60:.2f} min ({summary['avg_duration_s']:.2f} s)")
|
|
236
|
+
if summary["avg_tokens"] is not None:
|
|
237
|
+
print(f"平均总 Token: {summary['avg_tokens'] / 1000:.2f} k")
|
|
238
|
+
print(f"合计总 Token: {summary['total_tokens']}")
|
|
239
|
+
if summary["avg_peak_rss_mb"] is not None:
|
|
240
|
+
print(f"平均峰值内存: {summary['avg_peak_rss_mb']:.1f} MB")
|
|
241
|
+
if summary["max_peak_rss_mb"] is not None:
|
|
242
|
+
print(f"最大峰值内存: {summary['max_peak_rss_mb']:.1f} MB")
|
|
243
|
+
if summary["avg_avg_rss_mb"] is not None:
|
|
244
|
+
print(f"平均内存占用: {summary['avg_avg_rss_mb']:.1f} MB")
|
|
245
|
+
|
|
246
|
+
print("=" * 100)
|
|
247
|
+
return summary
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def summarize_log(log_file: Path, *, print_report: bool = True) -> dict | None:
|
|
251
|
+
if not log_file.is_file():
|
|
252
|
+
return None
|
|
253
|
+
tasks = parse_agent_log(log_file)
|
|
254
|
+
if not tasks:
|
|
255
|
+
return None
|
|
256
|
+
return summarize_tasks(tasks, print_report=print_report)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main() -> int:
|
|
260
|
+
parser = argparse.ArgumentParser(description="Agent 日志效率统计")
|
|
261
|
+
parser.add_argument("--log", "-l", type=str, default=str(DEFAULT_LOG), help="Agent 日志路径")
|
|
262
|
+
args = parser.parse_args()
|
|
263
|
+
|
|
264
|
+
log_file = Path(args.log)
|
|
265
|
+
if not log_file.is_file():
|
|
266
|
+
print(f"错误: 日志文件 {log_file} 不存在")
|
|
267
|
+
return 1
|
|
268
|
+
|
|
269
|
+
tasks = parse_agent_log(log_file)
|
|
270
|
+
if not tasks:
|
|
271
|
+
print("警告: 日志中未解析到任何 Evaluate 任务段")
|
|
272
|
+
return 1
|
|
273
|
+
|
|
274
|
+
summarize_tasks(tasks)
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
raise SystemExit(main())
|