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.
Files changed (116) hide show
  1. mindcode/__init__.py +5 -0
  2. mindcode/__main__.py +4 -0
  3. mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
  4. mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
  5. mindcode/__pycache__/_version.cpython-313.pyc +0 -0
  6. mindcode/__pycache__/config.cpython-313.pyc +0 -0
  7. mindcode/__pycache__/policy.cpython-313.pyc +0 -0
  8. mindcode/__pycache__/remote.cpython-313.pyc +0 -0
  9. mindcode/__pycache__/render.cpython-313.pyc +0 -0
  10. mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
  11. mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
  12. mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
  13. mindcode/_version.py +11 -0
  14. mindcode/approval.py +105 -0
  15. mindcode/bench/__init__.py +19 -0
  16. mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
  17. mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
  18. mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
  19. mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
  20. mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
  21. mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
  22. mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
  23. mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
  24. mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
  25. mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
  26. mindcode/bench/adapters/__init__.py +22 -0
  27. mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  28. mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
  29. mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
  30. mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
  31. mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
  32. mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
  33. mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  34. mindcode/bench/adapters/base.py +45 -0
  35. mindcode/bench/adapters/mini_bfcl.py +18 -0
  36. mindcode/bench/adapters/mini_gaia.py +18 -0
  37. mindcode/bench/adapters/mini_terminal.py +18 -0
  38. mindcode/bench/adapters/swe_bench.py +86 -0
  39. mindcode/bench/adapters/terminal_bench.py +107 -0
  40. mindcode/bench/fake_tools.py +73 -0
  41. mindcode/bench/jsonutil.py +20 -0
  42. mindcode/bench/paths.py +10 -0
  43. mindcode/bench/predictions.py +65 -0
  44. mindcode/bench/report.py +48 -0
  45. mindcode/bench/runner.py +250 -0
  46. mindcode/bench/schema.py +78 -0
  47. mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
  48. mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
  49. mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
  50. mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
  51. mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
  52. mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
  53. mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  54. mindcode/bench/scorers/base.py +22 -0
  55. mindcode/bench/scorers/composite.py +57 -0
  56. mindcode/bench/scorers/exact.py +30 -0
  57. mindcode/bench/scorers/json_call.py +44 -0
  58. mindcode/bench/scorers/swe.py +29 -0
  59. mindcode/bench/scorers/terminal.py +43 -0
  60. mindcode/bench/scorers/terminal_bench.py +25 -0
  61. mindcode/bench/trace.py +34 -0
  62. mindcode/bench/workspace.py +127 -0
  63. mindcode/cli/__init__.py +112 -0
  64. mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  65. mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
  66. mindcode/cli/_shared.py +43 -0
  67. mindcode/cli/commands/__init__.py +1 -0
  68. mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  69. mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
  70. mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
  71. mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
  72. mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
  73. mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
  74. mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
  75. mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
  76. mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
  77. mindcode/cli/commands/bench.py +365 -0
  78. mindcode/cli/commands/chat.py +86 -0
  79. mindcode/cli/commands/config_cmd.py +197 -0
  80. mindcode/cli/commands/remote.py +146 -0
  81. mindcode/cli/commands/shell.py +108 -0
  82. mindcode/cli/commands/status.py +37 -0
  83. mindcode/cli/commands/task.py +417 -0
  84. mindcode/cli/commands/terminal.py +124 -0
  85. mindcode/cli/shell/__init__.py +5 -0
  86. mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
  87. mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
  88. mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
  89. mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
  90. mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
  91. mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
  92. mindcode/cli/shell/completion.py +72 -0
  93. mindcode/cli/shell/menu.py +88 -0
  94. mindcode/cli/shell/repl.py +597 -0
  95. mindcode/cli/shell/slash.py +480 -0
  96. mindcode/cli/shell/startup.py +55 -0
  97. mindcode/cli/shell/tui.py +897 -0
  98. mindcode/config.py +173 -0
  99. mindcode/mcp.py +205 -0
  100. mindcode/policy.py +173 -0
  101. mindcode/remote.py +216 -0
  102. mindcode/render.py +458 -0
  103. mindcode/runtime.py +541 -0
  104. mindcode/skills.py +154 -0
  105. mindcode/subagents.py +43 -0
  106. mindcode/tasking.py +281 -0
  107. mindcode/terminal/__init__.py +1 -0
  108. mindcode/terminal/__main__.py +17 -0
  109. mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
  110. mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
  111. mindcode/terminal_core.py +264 -0
  112. mindcode-0.2.0.dist-info/METADATA +244 -0
  113. mindcode-0.2.0.dist-info/RECORD +116 -0
  114. mindcode-0.2.0.dist-info/WHEEL +5 -0
  115. mindcode-0.2.0.dist-info/entry_points.txt +2 -0
  116. 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
+ )
@@ -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
+ }
@@ -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
@@ -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)
@@ -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
+ )