agentbox-rl 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.
- agentbox/__init__.py +52 -0
- agentbox/agent/__init__.py +6 -0
- agentbox/agent/agent.py +59 -0
- agentbox/agent/loop.py +201 -0
- agentbox/agent/prompts.py +44 -0
- agentbox/benchmark/__init__.py +29 -0
- agentbox/benchmark/compare.py +51 -0
- agentbox/benchmark/hash.py +59 -0
- agentbox/benchmark/loader.py +152 -0
- agentbox/benchmark/report.py +175 -0
- agentbox/benchmark/runner.py +131 -0
- agentbox/benchmark/schema.py +198 -0
- agentbox/benchmark/setup_check.py +57 -0
- agentbox/cli/__init__.py +1 -0
- agentbox/cli/bench.py +571 -0
- agentbox/cli/generate.py +314 -0
- agentbox/cli/main.py +276 -0
- agentbox/cli/traj.py +82 -0
- agentbox/config.py +84 -0
- agentbox/config_load.py +107 -0
- agentbox/errors.py +79 -0
- agentbox/jobs/__init__.py +15 -0
- agentbox/jobs/artifacts.py +65 -0
- agentbox/jobs/cache.py +39 -0
- agentbox/jobs/types.py +67 -0
- agentbox/metrics/__init__.py +5 -0
- agentbox/metrics/aggregate.py +63 -0
- agentbox/model/__init__.py +17 -0
- agentbox/model/base.py +32 -0
- agentbox/model/errors.py +5 -0
- agentbox/model/mock.py +43 -0
- agentbox/model/openai_compat.py +153 -0
- agentbox/model/probe.py +186 -0
- agentbox/py.typed +0 -0
- agentbox/runner/__init__.py +6 -0
- agentbox/runner/parallel.py +194 -0
- agentbox/runner/rollout.py +251 -0
- agentbox/sandbox/__init__.py +15 -0
- agentbox/sandbox/docker_backend.py +158 -0
- agentbox/sandbox/images.py +29 -0
- agentbox/sandbox/manager.py +265 -0
- agentbox/sandbox/paths.py +65 -0
- agentbox/sandbox/prune.py +42 -0
- agentbox/sandbox/snapshot.py +43 -0
- agentbox/sandbox/types.py +67 -0
- agentbox/tasks/__init__.py +18 -0
- agentbox/tasks/bulk.py +67 -0
- agentbox/tasks/filter.py +100 -0
- agentbox/tasks/generate/__init__.py +42 -0
- agentbox/tasks/generate/batch.py +308 -0
- agentbox/tasks/generate/constraints.py +41 -0
- agentbox/tasks/generate/dedup.py +109 -0
- agentbox/tasks/generate/generator.py +665 -0
- agentbox/tasks/generate/llm_validate.py +325 -0
- agentbox/tasks/generate/lm.py +79 -0
- agentbox/tasks/generate/prompts.py +7 -0
- agentbox/tasks/generate/review.py +111 -0
- agentbox/tasks/generate/signatures.py +178 -0
- agentbox/tasks/generate/static_qc.py +236 -0
- agentbox/tasks/generate/strip_impl.py +71 -0
- agentbox/tasks/generate/validate.py +212 -0
- agentbox/tasks/loader.py +15 -0
- agentbox/tasks/rewards.py +47 -0
- agentbox/tasks/schema.py +79 -0
- agentbox/tasks/seeder.py +78 -0
- agentbox/tasks/verifier.py +65 -0
- agentbox/tools/__init__.py +16 -0
- agentbox/tools/base.py +54 -0
- agentbox/tools/builtins.py +31 -0
- agentbox/tools/decorator.py +89 -0
- agentbox/tools/executor.py +87 -0
- agentbox/tools/file_tools.py +162 -0
- agentbox/tools/registry.py +99 -0
- agentbox/tools/shell.py +63 -0
- agentbox/tools/tests_tool.py +35 -0
- agentbox/trajectory/__init__.py +12 -0
- agentbox/trajectory/formats/__init__.py +6 -0
- agentbox/trajectory/formats/art.py +68 -0
- agentbox/trajectory/formats/jsonl.py +17 -0
- agentbox/trajectory/recorder.py +76 -0
- agentbox/trajectory/render.py +334 -0
- agentbox/trajectory/schema.py +124 -0
- agentbox/types.py +42 -0
- agentbox/version.py +3 -0
- agentbox_rl-0.2.0.dist-info/METADATA +427 -0
- agentbox_rl-0.2.0.dist-info/RECORD +89 -0
- agentbox_rl-0.2.0.dist-info/WHEEL +4 -0
- agentbox_rl-0.2.0.dist-info/entry_points.txt +2 -0
- agentbox_rl-0.2.0.dist-info/licenses/LICENSE +176 -0
agentbox/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""AgentBox — local agentic virtual environment for trajectory building."""
|
|
2
|
+
|
|
3
|
+
from agentbox.agent.agent import Agent
|
|
4
|
+
from agentbox.benchmark import (
|
|
5
|
+
BenchmarkReport,
|
|
6
|
+
BenchmarkRunConfig,
|
|
7
|
+
BenchmarkRunner,
|
|
8
|
+
BenchmarkSuite,
|
|
9
|
+
ModelUnderTest,
|
|
10
|
+
compare_reports,
|
|
11
|
+
create_suite_from_tasks,
|
|
12
|
+
load_suite,
|
|
13
|
+
)
|
|
14
|
+
from agentbox.config import AgentConfig, ModelConfig, RolloutConfig, SandboxConfig
|
|
15
|
+
from agentbox.metrics import AggregateMetrics, aggregate_trajectories
|
|
16
|
+
from agentbox.runner.parallel import ParallelResult, ParallelRunner
|
|
17
|
+
from agentbox.runner.rollout import Rollout
|
|
18
|
+
from agentbox.sandbox.manager import SandboxManager
|
|
19
|
+
from agentbox.tasks.schema import Task, VerifierSpec
|
|
20
|
+
from agentbox.tools.base import BaseTool
|
|
21
|
+
from agentbox.tools.decorator import tool
|
|
22
|
+
from agentbox.trajectory.schema import Trajectory
|
|
23
|
+
from agentbox.version import __version__
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
"Agent",
|
|
28
|
+
"AgentConfig",
|
|
29
|
+
"AggregateMetrics",
|
|
30
|
+
"BaseTool",
|
|
31
|
+
"BenchmarkReport",
|
|
32
|
+
"BenchmarkRunConfig",
|
|
33
|
+
"BenchmarkRunner",
|
|
34
|
+
"BenchmarkSuite",
|
|
35
|
+
"ModelConfig",
|
|
36
|
+
"ModelUnderTest",
|
|
37
|
+
"ParallelResult",
|
|
38
|
+
"ParallelRunner",
|
|
39
|
+
"Rollout",
|
|
40
|
+
"RolloutConfig",
|
|
41
|
+
"SandboxConfig",
|
|
42
|
+
"SandboxManager",
|
|
43
|
+
"Task",
|
|
44
|
+
"Trajectory",
|
|
45
|
+
"VerifierSpec",
|
|
46
|
+
"aggregate_trajectories",
|
|
47
|
+
"compare_reports",
|
|
48
|
+
"create_suite_from_tasks",
|
|
49
|
+
"load_suite",
|
|
50
|
+
"tool",
|
|
51
|
+
]
|
|
52
|
+
|
agentbox/agent/agent.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""High-level Agent facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agentbox.config import AgentConfig, ModelConfig
|
|
9
|
+
from agentbox.model.base import ModelClient
|
|
10
|
+
from agentbox.model.openai_compat import OpenAICompatClient
|
|
11
|
+
from agentbox.tools.base import BaseTool
|
|
12
|
+
from agentbox.types import ToolMode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Agent:
|
|
16
|
+
"""Ergonomic agent configuration for rollouts."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
model: str | ModelConfig | ModelClient,
|
|
21
|
+
*,
|
|
22
|
+
base_url: str | None = None,
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
tools: ToolMode | list[str] | str = ToolMode.STRUCTURED,
|
|
25
|
+
custom_tools: Sequence[BaseTool] | None = None,
|
|
26
|
+
builtins: bool = True,
|
|
27
|
+
max_steps: int = 40,
|
|
28
|
+
temperature: float = 0.7,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
if isinstance(model, str):
|
|
32
|
+
self.model_config = ModelConfig(
|
|
33
|
+
model=model,
|
|
34
|
+
base_url=base_url,
|
|
35
|
+
api_key=api_key,
|
|
36
|
+
temperature=temperature,
|
|
37
|
+
)
|
|
38
|
+
self.model_client: ModelClient = OpenAICompatClient(self.model_config)
|
|
39
|
+
elif isinstance(model, ModelConfig):
|
|
40
|
+
self.model_config = model
|
|
41
|
+
self.model_client = OpenAICompatClient(model)
|
|
42
|
+
else:
|
|
43
|
+
self.model_config = ModelConfig(model=getattr(model, "model", "unknown"))
|
|
44
|
+
self.model_client = model
|
|
45
|
+
|
|
46
|
+
tool_mode: ToolMode | list[str]
|
|
47
|
+
if isinstance(tools, str) and tools in {m.value for m in ToolMode}:
|
|
48
|
+
tool_mode = ToolMode(tools)
|
|
49
|
+
else:
|
|
50
|
+
tool_mode = tools # type: ignore[assignment]
|
|
51
|
+
|
|
52
|
+
agent_kwargs = {
|
|
53
|
+
"tools": tool_mode,
|
|
54
|
+
"custom_tools": list(custom_tools or []),
|
|
55
|
+
"builtins": builtins,
|
|
56
|
+
"max_steps": max_steps,
|
|
57
|
+
**kwargs,
|
|
58
|
+
}
|
|
59
|
+
self.config = AgentConfig(**agent_kwargs)
|
agentbox/agent/loop.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Multi-turn agent loop with OpenAI tool calling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from agentbox.config import AgentConfig
|
|
14
|
+
from agentbox.model.base import ModelClient, ModelResponse
|
|
15
|
+
from agentbox.tools.base import ToolContext
|
|
16
|
+
from agentbox.tools.executor import ToolExecutor
|
|
17
|
+
from agentbox.trajectory.schema import Message, ToolCallRecord
|
|
18
|
+
from agentbox.types import FinalStatus, MessageRole
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentLoopResult(BaseModel):
|
|
22
|
+
messages: list[Message]
|
|
23
|
+
tool_call_records: list[ToolCallRecord]
|
|
24
|
+
final_status: FinalStatus
|
|
25
|
+
steps: int
|
|
26
|
+
model_calls: int
|
|
27
|
+
stop_reason: str
|
|
28
|
+
error: str | None = None
|
|
29
|
+
usage_totals: dict[str, int] = Field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentLoop:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
model: ModelClient,
|
|
36
|
+
tools: ToolExecutor,
|
|
37
|
+
config: AgentConfig,
|
|
38
|
+
*,
|
|
39
|
+
on_step: Callable[[int, Message], Awaitable[None] | None] | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.model = model
|
|
42
|
+
self.tools = tools
|
|
43
|
+
self.config = config
|
|
44
|
+
self.on_step = on_step
|
|
45
|
+
|
|
46
|
+
async def run(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
system_prompt: str,
|
|
50
|
+
user_prompt: str,
|
|
51
|
+
ctx: ToolContext,
|
|
52
|
+
episode_deadline: float | None = None,
|
|
53
|
+
) -> AgentLoopResult:
|
|
54
|
+
messages: list[Message] = [
|
|
55
|
+
Message(role=MessageRole.SYSTEM, content=system_prompt),
|
|
56
|
+
Message(role=MessageRole.USER, content=user_prompt),
|
|
57
|
+
]
|
|
58
|
+
tool_records: list[ToolCallRecord] = []
|
|
59
|
+
usage_totals = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
|
60
|
+
model_calls = 0
|
|
61
|
+
step = 0
|
|
62
|
+
schemas = self.tools.schemas()
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
while step < self.config.max_steps:
|
|
66
|
+
if episode_deadline is not None and time.monotonic() >= episode_deadline:
|
|
67
|
+
return AgentLoopResult(
|
|
68
|
+
messages=messages,
|
|
69
|
+
tool_call_records=tool_records,
|
|
70
|
+
final_status=FinalStatus.TIMEOUT,
|
|
71
|
+
steps=step,
|
|
72
|
+
model_calls=model_calls,
|
|
73
|
+
stop_reason="timeout",
|
|
74
|
+
usage_totals=usage_totals,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
openai_messages = [m.to_openai_dict() for m in messages]
|
|
78
|
+
try:
|
|
79
|
+
response = await asyncio.wait_for(
|
|
80
|
+
self.model.complete(
|
|
81
|
+
openai_messages,
|
|
82
|
+
tools=schemas or None,
|
|
83
|
+
tool_choice="auto",
|
|
84
|
+
),
|
|
85
|
+
timeout=self.config.step_timeout_s,
|
|
86
|
+
)
|
|
87
|
+
except TimeoutError:
|
|
88
|
+
return AgentLoopResult(
|
|
89
|
+
messages=messages,
|
|
90
|
+
tool_call_records=tool_records,
|
|
91
|
+
final_status=FinalStatus.TIMEOUT,
|
|
92
|
+
steps=step,
|
|
93
|
+
model_calls=model_calls,
|
|
94
|
+
stop_reason="timeout",
|
|
95
|
+
error=f"step timed out after {self.config.step_timeout_s}s",
|
|
96
|
+
usage_totals=usage_totals,
|
|
97
|
+
)
|
|
98
|
+
model_calls += 1
|
|
99
|
+
self._accumulate_usage(usage_totals, response)
|
|
100
|
+
|
|
101
|
+
assistant = self._to_assistant_message(response)
|
|
102
|
+
messages.append(assistant)
|
|
103
|
+
|
|
104
|
+
if self.on_step:
|
|
105
|
+
maybe = self.on_step(step, assistant)
|
|
106
|
+
if maybe is not None:
|
|
107
|
+
await maybe
|
|
108
|
+
|
|
109
|
+
if not response.tool_calls:
|
|
110
|
+
return AgentLoopResult(
|
|
111
|
+
messages=messages,
|
|
112
|
+
tool_call_records=tool_records,
|
|
113
|
+
final_status=FinalStatus.SUCCESS, # provisional; verifier decides reward
|
|
114
|
+
steps=step,
|
|
115
|
+
model_calls=model_calls,
|
|
116
|
+
stop_reason="final_answer",
|
|
117
|
+
usage_totals=usage_totals,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
ctx.step = step
|
|
121
|
+
try:
|
|
122
|
+
tool_messages = await asyncio.wait_for(
|
|
123
|
+
self.tools.execute_many(
|
|
124
|
+
response.tool_calls,
|
|
125
|
+
ctx,
|
|
126
|
+
parallel=self.config.parallel_tool_calls,
|
|
127
|
+
),
|
|
128
|
+
timeout=self.config.step_timeout_s,
|
|
129
|
+
)
|
|
130
|
+
except TimeoutError:
|
|
131
|
+
return AgentLoopResult(
|
|
132
|
+
messages=messages,
|
|
133
|
+
tool_call_records=tool_records,
|
|
134
|
+
final_status=FinalStatus.TIMEOUT,
|
|
135
|
+
steps=step,
|
|
136
|
+
model_calls=model_calls,
|
|
137
|
+
stop_reason="timeout",
|
|
138
|
+
error=f"tool batch timed out after {self.config.step_timeout_s}s",
|
|
139
|
+
usage_totals=usage_totals,
|
|
140
|
+
)
|
|
141
|
+
for tc, tm in zip(response.tool_calls, tool_messages, strict=True):
|
|
142
|
+
try:
|
|
143
|
+
args = json.loads(tc.function.arguments or "{}")
|
|
144
|
+
except json.JSONDecodeError:
|
|
145
|
+
args = {"_raw": tc.function.arguments}
|
|
146
|
+
is_error = bool(
|
|
147
|
+
tm.content and str(tm.content).startswith("ERROR:")
|
|
148
|
+
)
|
|
149
|
+
tool_records.append(
|
|
150
|
+
ToolCallRecord(
|
|
151
|
+
step=step,
|
|
152
|
+
tool_call_id=tc.id,
|
|
153
|
+
name=tc.function.name,
|
|
154
|
+
arguments=args if isinstance(args, dict) else {},
|
|
155
|
+
result=tm.content or "",
|
|
156
|
+
is_error=is_error,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
messages.extend(tool_messages)
|
|
160
|
+
step += 1
|
|
161
|
+
|
|
162
|
+
return AgentLoopResult(
|
|
163
|
+
messages=messages,
|
|
164
|
+
tool_call_records=tool_records,
|
|
165
|
+
final_status=FinalStatus.MAX_STEPS,
|
|
166
|
+
steps=step,
|
|
167
|
+
model_calls=model_calls,
|
|
168
|
+
stop_reason="max_steps",
|
|
169
|
+
usage_totals=usage_totals,
|
|
170
|
+
)
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
return AgentLoopResult(
|
|
173
|
+
messages=messages,
|
|
174
|
+
tool_call_records=tool_records,
|
|
175
|
+
final_status=FinalStatus.ERROR,
|
|
176
|
+
steps=step,
|
|
177
|
+
model_calls=model_calls,
|
|
178
|
+
stop_reason="error",
|
|
179
|
+
error=str(exc),
|
|
180
|
+
usage_totals=usage_totals,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _to_assistant_message(self, response: ModelResponse) -> Message:
|
|
184
|
+
return Message(
|
|
185
|
+
role=MessageRole.ASSISTANT,
|
|
186
|
+
content=response.content,
|
|
187
|
+
tool_calls=response.tool_calls or None,
|
|
188
|
+
reasoning_content=response.reasoning_content
|
|
189
|
+
if self.config.include_thinking
|
|
190
|
+
else None,
|
|
191
|
+
raw=response.raw_choice,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _accumulate_usage(
|
|
196
|
+
totals: dict[str, int], response: ModelResponse
|
|
197
|
+
) -> None:
|
|
198
|
+
if not response.usage:
|
|
199
|
+
return
|
|
200
|
+
for key in ("prompt_tokens", "completion_tokens", "total_tokens"):
|
|
201
|
+
totals[key] = totals.get(key, 0) + int(response.usage.get(key, 0))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Default system prompts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agentbox.types import ToolMode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
STRUCTURED_SYSTEM_PROMPT = """\
|
|
9
|
+
You are a coding agent working inside an isolated Linux container.
|
|
10
|
+
Your workspace is {workspace_dir}.
|
|
11
|
+
|
|
12
|
+
You solve the user's task using the provided tools. Prefer small, correct edits.
|
|
13
|
+
When you are done, respond with a brief final message and NO tool calls.
|
|
14
|
+
Do not invent file paths. Always verify your work with tests or commands when possible.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
SHELL_SYSTEM_PROMPT = """\
|
|
18
|
+
You are a coding agent working inside an isolated Linux container.
|
|
19
|
+
Your workspace is {workspace_dir}.
|
|
20
|
+
|
|
21
|
+
You only have the run_command tool. Use standard Unix tools to inspect and edit files.
|
|
22
|
+
When you are done, respond with a brief final message and NO tool calls.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render_system_prompt(
|
|
27
|
+
*,
|
|
28
|
+
workspace_dir: str = "/workspace",
|
|
29
|
+
mode: ToolMode | str = ToolMode.STRUCTURED,
|
|
30
|
+
extra: str | None = None,
|
|
31
|
+
override: str | None = None,
|
|
32
|
+
) -> str:
|
|
33
|
+
if override:
|
|
34
|
+
base = override.format(workspace_dir=workspace_dir)
|
|
35
|
+
else:
|
|
36
|
+
template = (
|
|
37
|
+
SHELL_SYSTEM_PROMPT
|
|
38
|
+
if mode in (ToolMode.SHELL, "shell")
|
|
39
|
+
else STRUCTURED_SYSTEM_PROMPT
|
|
40
|
+
)
|
|
41
|
+
base = template.format(workspace_dir=workspace_dir)
|
|
42
|
+
if extra:
|
|
43
|
+
base = base.rstrip() + "\n\n" + extra.strip()
|
|
44
|
+
return base
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Native real-rollout benchmark suites."""
|
|
2
|
+
|
|
3
|
+
from agentbox.benchmark.compare import compare_reports
|
|
4
|
+
from agentbox.benchmark.loader import create_suite_from_tasks, load_suite, save_suite
|
|
5
|
+
from agentbox.benchmark.runner import BenchmarkRunner
|
|
6
|
+
from agentbox.benchmark.schema import (
|
|
7
|
+
BenchmarkReport,
|
|
8
|
+
BenchmarkRunConfig,
|
|
9
|
+
BenchmarkSuite,
|
|
10
|
+
BenchmarkSuiteManifest,
|
|
11
|
+
ModelUnderTest,
|
|
12
|
+
ScoringConfig,
|
|
13
|
+
SetupCheckSpec,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BenchmarkReport",
|
|
18
|
+
"BenchmarkRunConfig",
|
|
19
|
+
"BenchmarkRunner",
|
|
20
|
+
"BenchmarkSuite",
|
|
21
|
+
"BenchmarkSuiteManifest",
|
|
22
|
+
"ModelUnderTest",
|
|
23
|
+
"ScoringConfig",
|
|
24
|
+
"SetupCheckSpec",
|
|
25
|
+
"compare_reports",
|
|
26
|
+
"create_suite_from_tasks",
|
|
27
|
+
"load_suite",
|
|
28
|
+
"save_suite",
|
|
29
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Compare two benchmark reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agentbox.benchmark.schema import BenchmarkReport, ComparisonResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compare_reports(
|
|
9
|
+
a: BenchmarkReport,
|
|
10
|
+
b: BenchmarkReport,
|
|
11
|
+
*,
|
|
12
|
+
require_hash_match: bool = False,
|
|
13
|
+
) -> ComparisonResult:
|
|
14
|
+
warnings: list[str] = []
|
|
15
|
+
if a.suite_id != b.suite_id:
|
|
16
|
+
warnings.append(f"suite_id differs: {a.suite_id!r} vs {b.suite_id!r}")
|
|
17
|
+
hash_match = (a.suite_content_hash or "") == (b.suite_content_hash or "")
|
|
18
|
+
if not hash_match:
|
|
19
|
+
warnings.append(
|
|
20
|
+
f"content_hash differs: {a.suite_content_hash!r} vs {b.suite_content_hash!r}"
|
|
21
|
+
)
|
|
22
|
+
if require_hash_match and not hash_match:
|
|
23
|
+
warnings.append("require_hash_match=True: comparison may be unfair")
|
|
24
|
+
|
|
25
|
+
a_models = {m.model_id: m for m in a.models}
|
|
26
|
+
b_models = {m.model_id: m for m in b.models}
|
|
27
|
+
deltas: list[dict] = []
|
|
28
|
+
|
|
29
|
+
for mid in sorted(set(a_models) | set(b_models)):
|
|
30
|
+
ma, mb = a_models.get(mid), b_models.get(mid)
|
|
31
|
+
if ma is None or mb is None:
|
|
32
|
+
warnings.append(f"model {mid!r} missing in one report")
|
|
33
|
+
continue
|
|
34
|
+
deltas.append(
|
|
35
|
+
{
|
|
36
|
+
"model_id": mid,
|
|
37
|
+
"success_rate_a": ma.aggregate.success_rate,
|
|
38
|
+
"success_rate_b": mb.aggregate.success_rate,
|
|
39
|
+
"delta_success_rate": mb.aggregate.success_rate - ma.aggregate.success_rate,
|
|
40
|
+
"mean_steps_a": ma.aggregate.mean_steps,
|
|
41
|
+
"mean_steps_b": mb.aggregate.mean_steps,
|
|
42
|
+
"delta_mean_steps": mb.aggregate.mean_steps - ma.aggregate.mean_steps,
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return ComparisonResult(
|
|
47
|
+
suite_id=a.suite_id,
|
|
48
|
+
hash_match=hash_match,
|
|
49
|
+
warnings=warnings,
|
|
50
|
+
deltas=deltas,
|
|
51
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Stable content hashing for benchmark suite integrity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agentbox.benchmark.schema import BenchmarkSuiteManifest
|
|
10
|
+
from agentbox.tasks.schema import Task
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _canonical_json(obj: Any) -> str:
|
|
14
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _task_payload(task: Task) -> dict[str, Any]:
|
|
18
|
+
return {
|
|
19
|
+
"task_id": task.task_id,
|
|
20
|
+
"description": task.description,
|
|
21
|
+
"starter_files": task.starter_files,
|
|
22
|
+
"setup_commands": task.setup_commands,
|
|
23
|
+
"verifier": task.verifier.model_dump(mode="json"),
|
|
24
|
+
"metadata": task.metadata,
|
|
25
|
+
"system_prompt_extra": task.system_prompt_extra,
|
|
26
|
+
"allowed_tools": task.allowed_tools,
|
|
27
|
+
"max_steps": task.max_steps,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fairness_config_payload(manifest: BenchmarkSuiteManifest) -> dict[str, Any]:
|
|
32
|
+
"""Config fields that affect evaluation fairness (exclude free-form notes)."""
|
|
33
|
+
agent = manifest.agent.model_dump(
|
|
34
|
+
mode="json",
|
|
35
|
+
exclude={"custom_tools"}, # non-serializable / not used in suite freeze
|
|
36
|
+
)
|
|
37
|
+
return {
|
|
38
|
+
"suite_id": manifest.suite_id,
|
|
39
|
+
"version": manifest.version,
|
|
40
|
+
"sandbox": manifest.sandbox.model_dump(mode="json"),
|
|
41
|
+
"agent": agent,
|
|
42
|
+
"n_per_task": manifest.n_per_task,
|
|
43
|
+
"seed": manifest.seed,
|
|
44
|
+
"scoring": manifest.scoring.model_dump(mode="json"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_suite_content_hash(
|
|
49
|
+
tasks: list[Task],
|
|
50
|
+
manifest: BenchmarkSuiteManifest,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""SHA-256 hex over tasks + fairness-critical suite config."""
|
|
53
|
+
ordered = sorted(tasks, key=lambda t: t.task_id)
|
|
54
|
+
payload = {
|
|
55
|
+
"config": _fairness_config_payload(manifest),
|
|
56
|
+
"tasks": [_task_payload(t) for t in ordered],
|
|
57
|
+
}
|
|
58
|
+
raw = _canonical_json(payload).encode("utf-8")
|
|
59
|
+
return hashlib.sha256(raw).hexdigest()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Load and save benchmark suite directory packs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from agentbox.benchmark.schema import BenchmarkSuite, BenchmarkSuiteManifest
|
|
10
|
+
from agentbox.config import AgentConfig, SandboxConfig
|
|
11
|
+
from agentbox.errors import BenchmarkError, TaskValidationError
|
|
12
|
+
from agentbox.tasks.bulk import load_task_dataset
|
|
13
|
+
from agentbox.tasks.schema import Task
|
|
14
|
+
from agentbox.types import ToolMode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_suite(path: str | Path) -> BenchmarkSuite:
|
|
18
|
+
"""Load suite.json + tasks from a directory pack."""
|
|
19
|
+
root = Path(path).resolve()
|
|
20
|
+
manifest_path = root / "suite.json"
|
|
21
|
+
if not manifest_path.is_file():
|
|
22
|
+
raise BenchmarkError(f"suite.json not found in {root}")
|
|
23
|
+
try:
|
|
24
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
25
|
+
manifest = BenchmarkSuiteManifest.model_validate(data)
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
raise BenchmarkError(f"Invalid suite.json: {exc}") from exc
|
|
28
|
+
|
|
29
|
+
tasks_dir = root / manifest.tasks_path
|
|
30
|
+
if not tasks_dir.exists():
|
|
31
|
+
raise BenchmarkError(f"tasks path not found: {tasks_dir}")
|
|
32
|
+
|
|
33
|
+
tasks = load_task_dataset(tasks_dir)
|
|
34
|
+
if not tasks:
|
|
35
|
+
# try single-level task.json dirs only already covered; try direct json
|
|
36
|
+
for p in sorted(tasks_dir.glob("*.json")):
|
|
37
|
+
try:
|
|
38
|
+
tasks.append(Task.from_json(p))
|
|
39
|
+
except TaskValidationError:
|
|
40
|
+
continue
|
|
41
|
+
if not tasks:
|
|
42
|
+
raise BenchmarkError(f"No tasks found under {tasks_dir}")
|
|
43
|
+
|
|
44
|
+
# Fairness: benchmarks should not carry custom_tools blobs
|
|
45
|
+
agent = manifest.agent.model_copy(update={"custom_tools": [], "drop_tools_prob": 0.0})
|
|
46
|
+
manifest = manifest.model_copy(update={"agent": agent})
|
|
47
|
+
return BenchmarkSuite(root=root, manifest=manifest, tasks=tasks)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def save_suite(suite: BenchmarkSuite, path: str | Path) -> Path:
|
|
51
|
+
"""Write suite pack to directory (suite.json + tasks/)."""
|
|
52
|
+
root = Path(path).resolve()
|
|
53
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
tasks_dir = root / suite.manifest.tasks_path
|
|
55
|
+
tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
for task in suite.tasks:
|
|
58
|
+
task_dir = tasks_dir / task.task_id
|
|
59
|
+
task_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
# Write starter files into files/ and strip from task.json for cleanliness
|
|
61
|
+
files_dir = task_dir / "files"
|
|
62
|
+
if task.starter_files:
|
|
63
|
+
files_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
for rel, content in task.starter_files.items():
|
|
65
|
+
fp = files_dir / rel
|
|
66
|
+
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
fp.write_text(content, encoding="utf-8")
|
|
68
|
+
slim = task.model_copy(update={"starter_files": {}})
|
|
69
|
+
slim.save_json(task_dir / "task.json")
|
|
70
|
+
|
|
71
|
+
# Serialize agent without custom_tools
|
|
72
|
+
m = suite.manifest.model_copy(deep=True)
|
|
73
|
+
m.agent = m.agent.model_copy(update={"custom_tools": []})
|
|
74
|
+
(root / "suite.json").write_text(
|
|
75
|
+
m.model_dump_json(indent=2),
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
|
78
|
+
return root
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_suite_from_tasks(
|
|
82
|
+
tasks_src: str | Path,
|
|
83
|
+
dest: str | Path,
|
|
84
|
+
*,
|
|
85
|
+
suite_id: str,
|
|
86
|
+
name: str,
|
|
87
|
+
version: str = "1.0.0",
|
|
88
|
+
description: str = "",
|
|
89
|
+
sandbox: SandboxConfig | None = None,
|
|
90
|
+
agent: AgentConfig | None = None,
|
|
91
|
+
n_per_task: int = 1,
|
|
92
|
+
concurrency: int = 8,
|
|
93
|
+
freeze: bool = True,
|
|
94
|
+
) -> BenchmarkSuite:
|
|
95
|
+
"""Create a suite directory from a tasks folder or dataset."""
|
|
96
|
+
src = Path(tasks_src)
|
|
97
|
+
tasks = load_task_dataset(src)
|
|
98
|
+
if not tasks and src.is_dir():
|
|
99
|
+
for p in sorted(src.rglob("task.json")):
|
|
100
|
+
try:
|
|
101
|
+
tasks.append(Task.from_dir(p.parent))
|
|
102
|
+
except TaskValidationError:
|
|
103
|
+
continue
|
|
104
|
+
for p in sorted(src.glob("*.json")):
|
|
105
|
+
try:
|
|
106
|
+
tasks.append(Task.from_json(p))
|
|
107
|
+
except TaskValidationError:
|
|
108
|
+
continue
|
|
109
|
+
# de-dupe
|
|
110
|
+
seen: set[str] = set()
|
|
111
|
+
unique: list[Task] = []
|
|
112
|
+
for t in tasks:
|
|
113
|
+
if t.task_id in seen:
|
|
114
|
+
continue
|
|
115
|
+
seen.add(t.task_id)
|
|
116
|
+
unique.append(t)
|
|
117
|
+
if not unique:
|
|
118
|
+
raise BenchmarkError(f"No tasks found in {src}")
|
|
119
|
+
|
|
120
|
+
manifest = BenchmarkSuiteManifest(
|
|
121
|
+
suite_id=suite_id,
|
|
122
|
+
name=name,
|
|
123
|
+
description=description,
|
|
124
|
+
version=version,
|
|
125
|
+
sandbox=sandbox or SandboxConfig(),
|
|
126
|
+
agent=agent
|
|
127
|
+
or AgentConfig(
|
|
128
|
+
tools=ToolMode.STRUCTURED,
|
|
129
|
+
max_steps=40,
|
|
130
|
+
drop_tools_prob=0.0,
|
|
131
|
+
custom_tools=[],
|
|
132
|
+
),
|
|
133
|
+
n_per_task=n_per_task,
|
|
134
|
+
concurrency=concurrency,
|
|
135
|
+
)
|
|
136
|
+
suite = BenchmarkSuite(manifest=manifest, tasks=unique)
|
|
137
|
+
if freeze:
|
|
138
|
+
suite = suite.freeze()
|
|
139
|
+
save_suite(suite, dest)
|
|
140
|
+
suite.root = Path(dest).resolve()
|
|
141
|
+
return suite
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def snapshot_suite(suite: BenchmarkSuite, dest: str | Path) -> Path:
|
|
145
|
+
"""Copy frozen suite into results for audit."""
|
|
146
|
+
dest_p = Path(dest)
|
|
147
|
+
if suite.root and suite.root.is_dir():
|
|
148
|
+
if dest_p.exists():
|
|
149
|
+
shutil.rmtree(dest_p)
|
|
150
|
+
shutil.copytree(suite.root, dest_p)
|
|
151
|
+
return dest_p
|
|
152
|
+
return save_suite(suite, dest_p)
|