mindcode 0.2.0__py3-none-any.whl
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.
- mindcode/__init__.py +5 -0
- mindcode/__main__.py +4 -0
- mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/__pycache__/_version.cpython-313.pyc +0 -0
- mindcode/__pycache__/config.cpython-313.pyc +0 -0
- mindcode/__pycache__/policy.cpython-313.pyc +0 -0
- mindcode/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/__pycache__/render.cpython-313.pyc +0 -0
- mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
- mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
- mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
- mindcode/_version.py +11 -0
- mindcode/approval.py +105 -0
- mindcode/bench/__init__.py +19 -0
- mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__init__.py +22 -0
- mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/base.py +45 -0
- mindcode/bench/adapters/mini_bfcl.py +18 -0
- mindcode/bench/adapters/mini_gaia.py +18 -0
- mindcode/bench/adapters/mini_terminal.py +18 -0
- mindcode/bench/adapters/swe_bench.py +86 -0
- mindcode/bench/adapters/terminal_bench.py +107 -0
- mindcode/bench/fake_tools.py +73 -0
- mindcode/bench/jsonutil.py +20 -0
- mindcode/bench/paths.py +10 -0
- mindcode/bench/predictions.py +65 -0
- mindcode/bench/report.py +48 -0
- mindcode/bench/runner.py +250 -0
- mindcode/bench/schema.py +78 -0
- mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/scorers/base.py +22 -0
- mindcode/bench/scorers/composite.py +57 -0
- mindcode/bench/scorers/exact.py +30 -0
- mindcode/bench/scorers/json_call.py +44 -0
- mindcode/bench/scorers/swe.py +29 -0
- mindcode/bench/scorers/terminal.py +43 -0
- mindcode/bench/scorers/terminal_bench.py +25 -0
- mindcode/bench/trace.py +34 -0
- mindcode/bench/workspace.py +127 -0
- mindcode/cli/__init__.py +112 -0
- mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
- mindcode/cli/_shared.py +43 -0
- mindcode/cli/commands/__init__.py +1 -0
- mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/cli/commands/bench.py +365 -0
- mindcode/cli/commands/chat.py +86 -0
- mindcode/cli/commands/config_cmd.py +197 -0
- mindcode/cli/commands/remote.py +146 -0
- mindcode/cli/commands/shell.py +108 -0
- mindcode/cli/commands/status.py +37 -0
- mindcode/cli/commands/task.py +417 -0
- mindcode/cli/commands/terminal.py +124 -0
- mindcode/cli/shell/__init__.py +5 -0
- mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
- mindcode/cli/shell/completion.py +72 -0
- mindcode/cli/shell/menu.py +88 -0
- mindcode/cli/shell/repl.py +597 -0
- mindcode/cli/shell/slash.py +480 -0
- mindcode/cli/shell/startup.py +55 -0
- mindcode/cli/shell/tui.py +897 -0
- mindcode/config.py +173 -0
- mindcode/mcp.py +205 -0
- mindcode/policy.py +173 -0
- mindcode/remote.py +216 -0
- mindcode/render.py +458 -0
- mindcode/runtime.py +541 -0
- mindcode/skills.py +154 -0
- mindcode/subagents.py +43 -0
- mindcode/tasking.py +281 -0
- mindcode/terminal/__init__.py +1 -0
- mindcode/terminal/__main__.py +17 -0
- mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/terminal_core.py +264 -0
- mindcode-0.2.0.dist-info/METADATA +244 -0
- mindcode-0.2.0.dist-info/RECORD +116 -0
- mindcode-0.2.0.dist-info/WHEEL +5 -0
- mindcode-0.2.0.dist-info/entry_points.txt +2 -0
- mindcode-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mindagent.core.artifact import validate_artifact_path, write_artifact_text
|
|
8
|
+
|
|
9
|
+
from .schema import BenchmarkCase, BenchmarkResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PATCH_PATTERN = re.compile(
|
|
13
|
+
r"(?:diff[\w_\.\ /\-]+\n)?---\s+a/(?:.*?)\n"
|
|
14
|
+
r"\+\+\+\s+b/(?:.*?)(?=diff\ |---\ a/|\Z)",
|
|
15
|
+
re.DOTALL,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_patch(text: str | None) -> str:
|
|
20
|
+
if not text:
|
|
21
|
+
return ""
|
|
22
|
+
matches = PATCH_PATTERN.findall(text)
|
|
23
|
+
if matches:
|
|
24
|
+
return "\n".join(match.rstrip() for match in matches).strip() + "\n"
|
|
25
|
+
stripped = text.strip()
|
|
26
|
+
if stripped.startswith("--- a/") and "\n+++ b/" in stripped:
|
|
27
|
+
return stripped + "\n"
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def append_swe_prediction(
|
|
32
|
+
run_dir: Path,
|
|
33
|
+
case: BenchmarkCase,
|
|
34
|
+
result: BenchmarkResult,
|
|
35
|
+
model_patch: str,
|
|
36
|
+
model_name: str,
|
|
37
|
+
) -> None:
|
|
38
|
+
prediction = {
|
|
39
|
+
"instance_id": case.case_id,
|
|
40
|
+
"model_name_or_path": model_name,
|
|
41
|
+
"model_patch": model_patch,
|
|
42
|
+
}
|
|
43
|
+
path = run_dir / "predictions.jsonl"
|
|
44
|
+
path = validate_artifact_path(run_dir, path)
|
|
45
|
+
write_artifact_text(
|
|
46
|
+
path,
|
|
47
|
+
json.dumps(prediction, ensure_ascii=False) + "\n",
|
|
48
|
+
root=run_dir,
|
|
49
|
+
append=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
manifest_path = run_dir / "swe_predictions.json"
|
|
53
|
+
manifest_path = validate_artifact_path(run_dir, manifest_path)
|
|
54
|
+
manifest = {
|
|
55
|
+
"predictions_path": str(path),
|
|
56
|
+
"instances": [case.case_id],
|
|
57
|
+
}
|
|
58
|
+
if manifest_path.exists():
|
|
59
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
60
|
+
manifest["instances"].append(case.case_id)
|
|
61
|
+
write_artifact_text(
|
|
62
|
+
manifest_path,
|
|
63
|
+
json.dumps(manifest, ensure_ascii=False, indent=2),
|
|
64
|
+
root=run_dir,
|
|
65
|
+
)
|
mindcode/bench/report.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .schema import BenchmarkResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_summary(results: list[BenchmarkResult]) -> dict[str, Any]:
|
|
10
|
+
total = len(results)
|
|
11
|
+
passed = sum(1 for result in results if result.passed)
|
|
12
|
+
by_category: dict[str, dict[str, Any]] = defaultdict(
|
|
13
|
+
lambda: {"total": 0, "passed": 0, "pass_rate": 0.0}
|
|
14
|
+
)
|
|
15
|
+
for result in results:
|
|
16
|
+
category = str(result.metrics.get("category", "uncategorized"))
|
|
17
|
+
item = by_category[category]
|
|
18
|
+
item["total"] += 1
|
|
19
|
+
item["passed"] += int(result.passed)
|
|
20
|
+
|
|
21
|
+
for item in by_category.values():
|
|
22
|
+
item["pass_rate"] = item["passed"] / item["total"]
|
|
23
|
+
|
|
24
|
+
latency_values = [
|
|
25
|
+
float(result.metrics.get("latency_s", 0.0)) for result in results
|
|
26
|
+
]
|
|
27
|
+
return {
|
|
28
|
+
"total": total,
|
|
29
|
+
"passed": passed,
|
|
30
|
+
"failed": total - passed,
|
|
31
|
+
"pass_rate": passed / total if total else 0.0,
|
|
32
|
+
"avg_latency_s": (
|
|
33
|
+
sum(latency_values) / len(latency_values)
|
|
34
|
+
if latency_values
|
|
35
|
+
else 0.0
|
|
36
|
+
),
|
|
37
|
+
"by_category": dict(sorted(by_category.items())),
|
|
38
|
+
"failed_cases": [
|
|
39
|
+
{
|
|
40
|
+
"case_id": result.case_id,
|
|
41
|
+
"score": result.score,
|
|
42
|
+
"failure_phase": result.metrics.get("failure_phase"),
|
|
43
|
+
"failure_reason": result.metrics.get("failure_reason"),
|
|
44
|
+
}
|
|
45
|
+
for result in results
|
|
46
|
+
if not result.passed
|
|
47
|
+
],
|
|
48
|
+
}
|
mindcode/bench/runner.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mindagent.core import AgentResult, safe_run_id_filename
|
|
9
|
+
from mindagent.core.artifact import (
|
|
10
|
+
ensure_artifact_directory,
|
|
11
|
+
validate_artifact_path,
|
|
12
|
+
write_artifact_text,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ..config import load_config
|
|
16
|
+
from ..policy import ApprovalMode
|
|
17
|
+
from ..runtime import build_runtime
|
|
18
|
+
from .adapters import built_in_adapters
|
|
19
|
+
from .fake_tools import build_fake_tools
|
|
20
|
+
from .jsonutil import to_jsonable
|
|
21
|
+
from .predictions import append_swe_prediction, extract_patch
|
|
22
|
+
from .report import build_summary
|
|
23
|
+
from .schema import BenchmarkCase, BenchmarkResult, BenchmarkRunConfig, CaseTrace
|
|
24
|
+
from .trace import BenchmarkTraceRecorder
|
|
25
|
+
from .workspace import prepare_case_workspace, workspace_diff
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MindcodeBenchmarkRunner:
|
|
29
|
+
def __init__(self, benchmark_root: Path):
|
|
30
|
+
self.benchmark_root = benchmark_root
|
|
31
|
+
self.adapters = built_in_adapters()
|
|
32
|
+
|
|
33
|
+
def list_suites(self) -> list[tuple[str, int]]:
|
|
34
|
+
suites: list[tuple[str, int]] = []
|
|
35
|
+
for name, adapter in sorted(self.adapters.items()):
|
|
36
|
+
try:
|
|
37
|
+
suites.append((name, len(adapter.load_cases(self.benchmark_root))))
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
suites.append((name, 0))
|
|
40
|
+
return suites
|
|
41
|
+
|
|
42
|
+
async def run(self, config: BenchmarkRunConfig) -> list[BenchmarkResult]:
|
|
43
|
+
if config.concurrency != 1:
|
|
44
|
+
raise ValueError("第一版 benchmark runner 仅支持 concurrency=1")
|
|
45
|
+
adapter = self.adapters.get(config.suite)
|
|
46
|
+
if adapter is None:
|
|
47
|
+
available = ", ".join(sorted(self.adapters))
|
|
48
|
+
raise ValueError(f"未知 suite: {config.suite}; 可用: {available}")
|
|
49
|
+
|
|
50
|
+
cases = _select_cases(
|
|
51
|
+
adapter.load_cases(self.benchmark_root),
|
|
52
|
+
config.case_ids,
|
|
53
|
+
config.limit,
|
|
54
|
+
)
|
|
55
|
+
scorer = adapter.build_scorer()
|
|
56
|
+
output_root = ensure_artifact_directory(config.output_root)
|
|
57
|
+
run_dir = _run_output_dir(config.output_root, config.run_id)
|
|
58
|
+
traces_dir = run_dir / "traces"
|
|
59
|
+
workspaces_root = run_dir / "workspaces"
|
|
60
|
+
ensure_artifact_directory(run_dir, root=output_root)
|
|
61
|
+
ensure_artifact_directory(traces_dir, root=output_root)
|
|
62
|
+
ensure_artifact_directory(workspaces_root, root=output_root)
|
|
63
|
+
config_path = validate_artifact_path(
|
|
64
|
+
output_root,
|
|
65
|
+
run_dir / "config.json",
|
|
66
|
+
)
|
|
67
|
+
write_artifact_text(
|
|
68
|
+
config_path,
|
|
69
|
+
json.dumps(to_jsonable(config), ensure_ascii=False, indent=2),
|
|
70
|
+
root=output_root,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
results: list[BenchmarkResult] = []
|
|
74
|
+
results_path = run_dir / "results.jsonl"
|
|
75
|
+
for case in cases:
|
|
76
|
+
result = await self._run_case(
|
|
77
|
+
case=case,
|
|
78
|
+
config=config,
|
|
79
|
+
scorer=scorer,
|
|
80
|
+
workspaces_root=workspaces_root,
|
|
81
|
+
traces_dir=traces_dir,
|
|
82
|
+
)
|
|
83
|
+
results.append(result)
|
|
84
|
+
write_artifact_text(
|
|
85
|
+
results_path,
|
|
86
|
+
json.dumps(to_jsonable(result), ensure_ascii=False) + "\n",
|
|
87
|
+
root=output_root,
|
|
88
|
+
append=True,
|
|
89
|
+
)
|
|
90
|
+
summary_path = validate_artifact_path(
|
|
91
|
+
output_root,
|
|
92
|
+
run_dir / "summary.json",
|
|
93
|
+
)
|
|
94
|
+
write_artifact_text(
|
|
95
|
+
summary_path,
|
|
96
|
+
json.dumps(build_summary(results), ensure_ascii=False, indent=2),
|
|
97
|
+
root=output_root,
|
|
98
|
+
)
|
|
99
|
+
return results
|
|
100
|
+
|
|
101
|
+
async def _run_case(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
case: BenchmarkCase,
|
|
105
|
+
config: BenchmarkRunConfig,
|
|
106
|
+
scorer,
|
|
107
|
+
workspaces_root: Path,
|
|
108
|
+
traces_dir: Path,
|
|
109
|
+
) -> BenchmarkResult:
|
|
110
|
+
started_at = time.time()
|
|
111
|
+
workspace = prepare_case_workspace(workspaces_root, case)
|
|
112
|
+
recorder = BenchmarkTraceRecorder()
|
|
113
|
+
run_result: AgentResult | None = None
|
|
114
|
+
error: str | None = None
|
|
115
|
+
runtime = None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
mindcode_config = load_config()
|
|
119
|
+
provider_config = mindcode_config.get_provider(config.provider)
|
|
120
|
+
runtime = build_runtime(
|
|
121
|
+
workspace,
|
|
122
|
+
provider_config=provider_config,
|
|
123
|
+
approve_mode=ApprovalMode(config.approve_mode),
|
|
124
|
+
model=config.model,
|
|
125
|
+
stream=False,
|
|
126
|
+
event_handler=recorder,
|
|
127
|
+
max_steps=config.max_steps,
|
|
128
|
+
step_timeout_s=config.step_timeout_s,
|
|
129
|
+
total_timeout_s=config.total_timeout_s,
|
|
130
|
+
extra_tools=build_fake_tools(case)
|
|
131
|
+
if case.suite == "mini_bfcl"
|
|
132
|
+
else None,
|
|
133
|
+
)
|
|
134
|
+
run_result = await runtime.run(
|
|
135
|
+
case.prompt,
|
|
136
|
+
metadata={
|
|
137
|
+
"benchmark_suite": case.suite,
|
|
138
|
+
"benchmark_case_id": case.case_id,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
except Exception as exc: # pragma: no cover - exercised by real runs
|
|
142
|
+
error = f"{type(exc).__name__}: {exc}"
|
|
143
|
+
finally:
|
|
144
|
+
if runtime is not None:
|
|
145
|
+
try:
|
|
146
|
+
await runtime.close(close_dependencies=True)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
if error is None:
|
|
149
|
+
error = f"{type(exc).__name__}: {exc}"
|
|
150
|
+
|
|
151
|
+
trace = CaseTrace(
|
|
152
|
+
case=case,
|
|
153
|
+
config=config,
|
|
154
|
+
agent_events=recorder.agent_events,
|
|
155
|
+
observations=to_jsonable(run_result.observations)
|
|
156
|
+
if run_result
|
|
157
|
+
else [],
|
|
158
|
+
tool_calls=recorder.tool_calls,
|
|
159
|
+
workspace_diff=workspace_diff(case, workspace),
|
|
160
|
+
)
|
|
161
|
+
score = scorer.score(case, run_result, workspace, trace)
|
|
162
|
+
if score.failure_phase or score.failure_reason:
|
|
163
|
+
trace.failure_analysis = {
|
|
164
|
+
"phase": score.failure_phase,
|
|
165
|
+
"reason": score.failure_reason,
|
|
166
|
+
}
|
|
167
|
+
trace_path = _case_trace_path(traces_dir, case.case_id)
|
|
168
|
+
completed_at = time.time()
|
|
169
|
+
metrics = {
|
|
170
|
+
**score.metrics,
|
|
171
|
+
"category": case.category,
|
|
172
|
+
"latency_s": completed_at - started_at,
|
|
173
|
+
"steps": run_result.steps if run_result else 0,
|
|
174
|
+
"tool_calls": len(recorder.tool_calls),
|
|
175
|
+
"agent_events": len(recorder.agent_events),
|
|
176
|
+
"run_outcome": run_result.outcome.value if run_result else None,
|
|
177
|
+
"failure_phase": score.failure_phase,
|
|
178
|
+
"failure_reason": score.failure_reason,
|
|
179
|
+
}
|
|
180
|
+
result = BenchmarkResult(
|
|
181
|
+
case_id=case.case_id,
|
|
182
|
+
suite=case.suite,
|
|
183
|
+
passed=score.passed,
|
|
184
|
+
score=score.score,
|
|
185
|
+
final_answer=run_result.final_answer if run_result else None,
|
|
186
|
+
error=error or (run_result.error if run_result else None),
|
|
187
|
+
started_at=started_at,
|
|
188
|
+
completed_at=completed_at,
|
|
189
|
+
metrics=metrics,
|
|
190
|
+
trace_path=str(trace_path),
|
|
191
|
+
workspace_path=str(workspace),
|
|
192
|
+
)
|
|
193
|
+
if case.suite == "swe_bench":
|
|
194
|
+
patch = trace.workspace_diff or extract_patch(result.final_answer)
|
|
195
|
+
append_swe_prediction(
|
|
196
|
+
traces_dir.parent,
|
|
197
|
+
case,
|
|
198
|
+
result,
|
|
199
|
+
patch,
|
|
200
|
+
config.model or config.provider,
|
|
201
|
+
)
|
|
202
|
+
trace_payload = {
|
|
203
|
+
"case": to_jsonable(case),
|
|
204
|
+
"config": to_jsonable(config),
|
|
205
|
+
"result": to_jsonable(result),
|
|
206
|
+
"agent_events": trace.agent_events,
|
|
207
|
+
"observations": trace.observations,
|
|
208
|
+
"tool_calls": trace.tool_calls,
|
|
209
|
+
"workspace_diff": trace.workspace_diff,
|
|
210
|
+
"failure_analysis": trace.failure_analysis,
|
|
211
|
+
}
|
|
212
|
+
write_artifact_text(
|
|
213
|
+
trace_path,
|
|
214
|
+
json.dumps(trace_payload, ensure_ascii=False, indent=2),
|
|
215
|
+
root=traces_dir.parent,
|
|
216
|
+
)
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def run_benchmark(config: BenchmarkRunConfig, benchmark_root: Path) -> list[BenchmarkResult]:
|
|
221
|
+
return asyncio.run(MindcodeBenchmarkRunner(benchmark_root).run(config))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _case_trace_path(traces_dir: Path, case_id: str) -> Path:
|
|
225
|
+
"""Keep per-case trace artifacts contained by the configured directory."""
|
|
226
|
+
path = traces_dir / f"{safe_run_id_filename(case_id)}.json"
|
|
227
|
+
return validate_artifact_path(traces_dir, path)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _run_output_dir(output_root: Path, run_id: str) -> Path:
|
|
231
|
+
"""Keep the benchmark run directory inside the configured output root."""
|
|
232
|
+
path = output_root / safe_run_id_filename(run_id)
|
|
233
|
+
return validate_artifact_path(output_root, path)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _select_cases(
|
|
237
|
+
cases: list[BenchmarkCase],
|
|
238
|
+
case_ids: tuple[str, ...],
|
|
239
|
+
limit: int | None,
|
|
240
|
+
) -> list[BenchmarkCase]:
|
|
241
|
+
selected = cases
|
|
242
|
+
if case_ids:
|
|
243
|
+
requested = set(case_ids)
|
|
244
|
+
selected = [case for case in selected if case.case_id in requested]
|
|
245
|
+
missing = sorted(requested - {case.case_id for case in selected})
|
|
246
|
+
if missing:
|
|
247
|
+
raise ValueError("未知 case: " + ", ".join(missing))
|
|
248
|
+
if limit is not None:
|
|
249
|
+
selected = selected[:limit]
|
|
250
|
+
return selected
|
mindcode/bench/schema.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class BenchmarkFile:
|
|
10
|
+
path: str
|
|
11
|
+
content: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class BenchmarkCase:
|
|
16
|
+
case_id: str
|
|
17
|
+
suite: str
|
|
18
|
+
category: str
|
|
19
|
+
prompt: str
|
|
20
|
+
expected: Any | None = None
|
|
21
|
+
files: tuple[BenchmarkFile, ...] = ()
|
|
22
|
+
tools: tuple[str, ...] = ()
|
|
23
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ScoreResult:
|
|
28
|
+
passed: bool
|
|
29
|
+
score: float
|
|
30
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
failure_phase: str | None = None
|
|
32
|
+
failure_reason: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class BenchmarkResult:
|
|
37
|
+
case_id: str
|
|
38
|
+
suite: str
|
|
39
|
+
passed: bool
|
|
40
|
+
score: float
|
|
41
|
+
final_answer: str | None
|
|
42
|
+
error: str | None
|
|
43
|
+
started_at: float
|
|
44
|
+
completed_at: float
|
|
45
|
+
metrics: dict[str, Any]
|
|
46
|
+
trace_path: str
|
|
47
|
+
workspace_path: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class BenchmarkRunConfig:
|
|
52
|
+
suite: str
|
|
53
|
+
run_id: str
|
|
54
|
+
provider: str
|
|
55
|
+
model: str | None
|
|
56
|
+
approve_mode: str
|
|
57
|
+
workspace_root: Path
|
|
58
|
+
output_root: Path
|
|
59
|
+
limit: int | None = None
|
|
60
|
+
case_ids: tuple[str, ...] = ()
|
|
61
|
+
concurrency: int = 1
|
|
62
|
+
max_steps: int = 50
|
|
63
|
+
step_timeout_s: float | None = 300.0
|
|
64
|
+
total_timeout_s: float | None = 1800.0
|
|
65
|
+
enable_subagents: bool = False
|
|
66
|
+
enable_background_tasks: bool = False
|
|
67
|
+
seed: int | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CaseTrace:
|
|
72
|
+
case: BenchmarkCase
|
|
73
|
+
config: BenchmarkRunConfig
|
|
74
|
+
agent_events: list[dict[str, Any]] = field(default_factory=list)
|
|
75
|
+
observations: list[dict[str, Any]] = field(default_factory=list)
|
|
76
|
+
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
77
|
+
workspace_diff: str = ""
|
|
78
|
+
failure_analysis: dict[str, Any] = field(default_factory=dict)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from mindagent.core import AgentResult
|
|
7
|
+
|
|
8
|
+
from ..schema import BenchmarkCase, CaseTrace, ScoreResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Scorer(Protocol):
|
|
12
|
+
def score(
|
|
13
|
+
self,
|
|
14
|
+
case: BenchmarkCase,
|
|
15
|
+
run_result: AgentResult | None,
|
|
16
|
+
workspace: Path,
|
|
17
|
+
trace: CaseTrace,
|
|
18
|
+
) -> ScoreResult: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_text(value: Any) -> str:
|
|
22
|
+
return str(value or "").strip()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mindagent.core import AgentResult
|
|
6
|
+
|
|
7
|
+
from ..schema import BenchmarkCase, CaseTrace, ScoreResult
|
|
8
|
+
from .base import Scorer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CompositeScorer:
|
|
12
|
+
def __init__(self, scorers: list[Scorer]):
|
|
13
|
+
self.scorers = scorers
|
|
14
|
+
|
|
15
|
+
def score(
|
|
16
|
+
self,
|
|
17
|
+
case: BenchmarkCase,
|
|
18
|
+
run_result: AgentResult | None,
|
|
19
|
+
workspace: Path,
|
|
20
|
+
trace: CaseTrace,
|
|
21
|
+
) -> ScoreResult:
|
|
22
|
+
results = [
|
|
23
|
+
scorer.score(case, run_result, workspace, trace)
|
|
24
|
+
for scorer in self.scorers
|
|
25
|
+
]
|
|
26
|
+
passed = all(result.passed for result in results)
|
|
27
|
+
score = (
|
|
28
|
+
sum(result.score for result in results) / len(results)
|
|
29
|
+
if results
|
|
30
|
+
else 0.0
|
|
31
|
+
)
|
|
32
|
+
first_failure = next(
|
|
33
|
+
(result for result in results if not result.passed),
|
|
34
|
+
None,
|
|
35
|
+
)
|
|
36
|
+
return ScoreResult(
|
|
37
|
+
passed=passed,
|
|
38
|
+
score=score,
|
|
39
|
+
metrics={
|
|
40
|
+
"components": [
|
|
41
|
+
{
|
|
42
|
+
"passed": result.passed,
|
|
43
|
+
"score": result.score,
|
|
44
|
+
"failure_phase": result.failure_phase,
|
|
45
|
+
"failure_reason": result.failure_reason,
|
|
46
|
+
"metrics": result.metrics,
|
|
47
|
+
}
|
|
48
|
+
for result in results
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
failure_phase=first_failure.failure_phase
|
|
52
|
+
if first_failure
|
|
53
|
+
else None,
|
|
54
|
+
failure_reason=first_failure.failure_reason
|
|
55
|
+
if first_failure
|
|
56
|
+
else None,
|
|
57
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mindagent.core import AgentResult
|
|
6
|
+
|
|
7
|
+
from ..schema import BenchmarkCase, CaseTrace, ScoreResult
|
|
8
|
+
from .base import normalize_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExactAnswerScorer:
|
|
12
|
+
def score(
|
|
13
|
+
self,
|
|
14
|
+
case: BenchmarkCase,
|
|
15
|
+
run_result: AgentResult | None,
|
|
16
|
+
workspace: Path,
|
|
17
|
+
trace: CaseTrace,
|
|
18
|
+
) -> ScoreResult:
|
|
19
|
+
expected = normalize_text(case.expected).casefold()
|
|
20
|
+
actual = normalize_text(
|
|
21
|
+
run_result.final_answer if run_result else None
|
|
22
|
+
).casefold()
|
|
23
|
+
passed = bool(expected) and expected == actual
|
|
24
|
+
return ScoreResult(
|
|
25
|
+
passed=passed,
|
|
26
|
+
score=1.0 if passed else 0.0,
|
|
27
|
+
metrics={"expected": case.expected, "actual": actual},
|
|
28
|
+
failure_phase=None if passed else "final_format",
|
|
29
|
+
failure_reason=None if passed else "answer_mismatch",
|
|
30
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mindagent.core import AgentResult
|
|
7
|
+
|
|
8
|
+
from ..schema import BenchmarkCase, CaseTrace, ScoreResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonToolCallScorer:
|
|
12
|
+
def score(
|
|
13
|
+
self,
|
|
14
|
+
case: BenchmarkCase,
|
|
15
|
+
run_result: AgentResult | None,
|
|
16
|
+
workspace: Path,
|
|
17
|
+
trace: CaseTrace,
|
|
18
|
+
) -> ScoreResult:
|
|
19
|
+
expected = case.expected if isinstance(case.expected, dict) else {}
|
|
20
|
+
expected_calls = expected.get("tool_calls", [])
|
|
21
|
+
actual_calls = _compact_calls(trace.tool_calls)
|
|
22
|
+
passed = actual_calls == expected_calls
|
|
23
|
+
return ScoreResult(
|
|
24
|
+
passed=passed,
|
|
25
|
+
score=1.0 if passed else 0.0,
|
|
26
|
+
metrics={
|
|
27
|
+
"expected_tool_calls": expected_calls,
|
|
28
|
+
"actual_tool_calls": actual_calls,
|
|
29
|
+
},
|
|
30
|
+
failure_phase=None if passed else "tool_selection",
|
|
31
|
+
failure_reason=None if passed else "tool_calls_mismatch",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _compact_calls(calls: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
36
|
+
compacted: list[dict[str, Any]] = []
|
|
37
|
+
for call in calls:
|
|
38
|
+
compacted.append(
|
|
39
|
+
{
|
|
40
|
+
"name": call.get("name"),
|
|
41
|
+
"arguments": call.get("arguments", {}),
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return compacted
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mindagent.core import AgentResult
|
|
6
|
+
|
|
7
|
+
from ..predictions import extract_patch
|
|
8
|
+
from ..schema import BenchmarkCase, CaseTrace, ScoreResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PatchGeneratedScorer:
|
|
12
|
+
def score(
|
|
13
|
+
self,
|
|
14
|
+
case: BenchmarkCase,
|
|
15
|
+
run_result: AgentResult | None,
|
|
16
|
+
workspace: Path,
|
|
17
|
+
trace: CaseTrace,
|
|
18
|
+
) -> ScoreResult:
|
|
19
|
+
patch = trace.workspace_diff or extract_patch(
|
|
20
|
+
run_result.final_answer if run_result else None
|
|
21
|
+
)
|
|
22
|
+
passed = bool(patch.strip())
|
|
23
|
+
return ScoreResult(
|
|
24
|
+
passed=passed,
|
|
25
|
+
score=1.0 if passed else 0.0,
|
|
26
|
+
metrics={"patch_bytes": len(patch.encode("utf-8"))},
|
|
27
|
+
failure_phase=None if passed else "workspace_state",
|
|
28
|
+
failure_reason=None if passed else "empty_patch",
|
|
29
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mindagent.core import AgentResult
|
|
6
|
+
from mindagent.core.artifact import validate_artifact_path
|
|
7
|
+
|
|
8
|
+
from ..schema import BenchmarkCase, CaseTrace, ScoreResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileStateScorer:
|
|
12
|
+
def score(
|
|
13
|
+
self,
|
|
14
|
+
case: BenchmarkCase,
|
|
15
|
+
run_result: AgentResult | None,
|
|
16
|
+
workspace: Path,
|
|
17
|
+
trace: CaseTrace,
|
|
18
|
+
) -> ScoreResult:
|
|
19
|
+
expected = case.expected if isinstance(case.expected, dict) else {}
|
|
20
|
+
expected_files = expected.get("files", {})
|
|
21
|
+
failures: list[str] = []
|
|
22
|
+
for rel_path, expected_content in expected_files.items():
|
|
23
|
+
target = workspace / rel_path
|
|
24
|
+
try:
|
|
25
|
+
validate_artifact_path(workspace, target)
|
|
26
|
+
except ValueError:
|
|
27
|
+
failures.append(f"unsafe:{rel_path}")
|
|
28
|
+
continue
|
|
29
|
+
if not target.exists():
|
|
30
|
+
failures.append(f"missing:{rel_path}")
|
|
31
|
+
continue
|
|
32
|
+
actual = target.read_text(encoding="utf-8")
|
|
33
|
+
if actual != expected_content:
|
|
34
|
+
failures.append(f"content_mismatch:{rel_path}")
|
|
35
|
+
|
|
36
|
+
passed = not failures
|
|
37
|
+
return ScoreResult(
|
|
38
|
+
passed=passed,
|
|
39
|
+
score=1.0 if passed else 0.0,
|
|
40
|
+
metrics={"file_failures": failures},
|
|
41
|
+
failure_phase=None if passed else "workspace_state",
|
|
42
|
+
failure_reason=None if passed else ",".join(failures),
|
|
43
|
+
)
|