nooa-bench 0.0.7__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.
- nooa_bench/__init__.py +19 -0
- nooa_bench/bench_agent.py +256 -0
- nooa_bench/protocol.py +191 -0
- nooa_bench/runner.py +235 -0
- nooa_bench/trace_analyzer.py +186 -0
- nooa_bench-0.0.7.dist-info/METADATA +29 -0
- nooa_bench-0.0.7.dist-info/RECORD +9 -0
- nooa_bench-0.0.7.dist-info/WHEEL +4 -0
- nooa_bench-0.0.7.dist-info/entry_points.txt +2 -0
nooa_bench/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Benchmark agent and Harbor runner for the NOOA framework.
|
|
4
|
+
|
|
5
|
+
Minimal reproducibility package for the NOOA tech report: the benchmark-agnostic
|
|
6
|
+
``BenchAgent`` (SWE-bench Verified, Terminal-Bench 2.0), the ``nemo-harbor``
|
|
7
|
+
container runner, and the trace analyzer used to extract the per-task token
|
|
8
|
+
statistics reported in the paper.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
# Maps --agent-type CLI values to dotted import paths: "module:ClassName"
|
|
14
|
+
AGENT_CLASSES: dict[str, str] = {
|
|
15
|
+
# Unified SWE-bench + Terminal-Bench agent (the tech report's BenchAgent)
|
|
16
|
+
"bench": "nooa_bench.bench_agent:BenchAgent",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
__all__ = ["AGENT_CLASSES"]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Generic benchmark agent for code and system tasks.
|
|
5
|
+
|
|
6
|
+
A single, non-specialized CodeAct agent that works on any task requiring shell
|
|
7
|
+
access inside a container. Not tuned for any particular benchmark -- the same
|
|
8
|
+
agent handles SWE-bench, Terminal-Bench, or any Harbor-compatible task.
|
|
9
|
+
|
|
10
|
+
Core contract:
|
|
11
|
+
- ``self.shell`` for persistent shell access (run/read/replace/write_file)
|
|
12
|
+
- ``self.repo`` for code navigation that returns ShellTools Match anchors
|
|
13
|
+
- ``self.todo`` for optional structured progress tracking
|
|
14
|
+
- Structured return: the agent must declare solution_description, evidence,
|
|
15
|
+
and command_to_verify when finishing -- forcing reflection before return.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from nooa import hidden as _hidden
|
|
21
|
+
|
|
22
|
+
_agentdoc_hidden_names = {"_hidden"}
|
|
23
|
+
|
|
24
|
+
with _hidden:
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
from nooa_cli.tools.repo_tools import RepoTools
|
|
30
|
+
from pydantic import BaseModel, Field
|
|
31
|
+
|
|
32
|
+
from nooa import Agent, CodeActStrategy, strategy
|
|
33
|
+
from nooa.agentdoc import doc, spec
|
|
34
|
+
from nooa.config import CodeActConfig
|
|
35
|
+
from nooa.context_blocks import DynamicContext
|
|
36
|
+
from nooa.tools.shell_tools import ShellTools
|
|
37
|
+
from nooa.tools.todo import TodoManager
|
|
38
|
+
from nooa.unifiedllm import FakeLLMClient
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from nooa.unifiedllm import UnifiedLLM
|
|
42
|
+
|
|
43
|
+
_logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
_OPTIONAL_TESTBED_ACTIVATE = (
|
|
46
|
+
"if [ -f /opt/miniconda3/etc/profile.d/conda.sh ]; then "
|
|
47
|
+
"[ -d /opt/harbor/cpython312/bin ] && export PATH=/opt/harbor/cpython312/bin:$PATH; "
|
|
48
|
+
"source /opt/miniconda3/etc/profile.d/conda.sh; "
|
|
49
|
+
"conda env list | awk '{print $1}' | grep -qx testbed && conda activate testbed || true; "
|
|
50
|
+
"fi"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TaskResult(BaseModel):
|
|
55
|
+
"""Structured result the agent must return when finishing a task."""
|
|
56
|
+
|
|
57
|
+
solution_description: str = Field(
|
|
58
|
+
description="What you did and why it solves the problem. Describe root cause and fix."
|
|
59
|
+
)
|
|
60
|
+
evidence: str = Field(
|
|
61
|
+
description=(
|
|
62
|
+
"Concrete evidence that the task is done: what tests passed, "
|
|
63
|
+
"what output was produced, what behavior changed. Not a guess -- "
|
|
64
|
+
"cite the actual shell output you observed."
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
command_to_verify: str = Field(
|
|
68
|
+
description="A shell command a verifier can run to confirm correctness (exit 0 on success)."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@_hidden
|
|
73
|
+
def _problem_statement(task_input: dict) -> str:
|
|
74
|
+
"""Extract the task text from supported Harbor/benchmark field names."""
|
|
75
|
+
for key in ("user_message", "problem_statement", "task_description"):
|
|
76
|
+
value = task_input.get(key)
|
|
77
|
+
if isinstance(value, str) and value.strip():
|
|
78
|
+
return value.strip()
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"task_input must include a non-empty user_message, problem_statement, or task_description"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BenchAgent(
|
|
85
|
+
Agent,
|
|
86
|
+
llm=FakeLLMClient(),
|
|
87
|
+
context={
|
|
88
|
+
"context_usage": DynamicContext(expr="self._context_usage_block()"),
|
|
89
|
+
"todo_status": DynamicContext(expr="self.todo.status()"),
|
|
90
|
+
"task": DynamicContext(expr="self.problem_statement"),
|
|
91
|
+
},
|
|
92
|
+
):
|
|
93
|
+
"""Generic agent for code and system tasks in containers.
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
r = await self.shell.run("command") # persistent shell
|
|
99
|
+
r = await self.shell.read("file.py", lines=(1,50)) # view -> Match
|
|
100
|
+
defs = await self.repo.symbols("src/", query="Handler") # definitions -> Match anchors
|
|
101
|
+
refs = await self.repo.refs("Handler", path="src/") # usages -> Match anchors
|
|
102
|
+
await self.shell.replace(defs[0], new_code) # edit at Match
|
|
103
|
+
await self.shell.write_file("file.py", content) # create/overwrite
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Workflow
|
|
107
|
+
|
|
108
|
+
1. **Understand** -- explore the codebase/environment, reproduce the issue
|
|
109
|
+
2. **Plan** -- write a plan based on todos
|
|
110
|
+
3. **Implement and verify** -- make the fix and run relevant tests
|
|
111
|
+
4. **Return** -- ``return_result(TaskResult(...))`` with evidence
|
|
112
|
+
|
|
113
|
+
## Return format
|
|
114
|
+
|
|
115
|
+
When you are done, you MUST return a ``TaskResult``:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
return_result(TaskResult(
|
|
119
|
+
solution_description="Root cause: missing URL-encoding in auth.py. Fixed with quote_plus().",
|
|
120
|
+
evidence="pytest tests/test_login.py passed (3 passed in 0.4s)",
|
|
121
|
+
command_to_verify="pytest tests/test_login.py -x",
|
|
122
|
+
))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use ``self.todo`` to track progress on multi-step tasks.
|
|
126
|
+
Mark todos done as you complete them.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
shell: ShellTools
|
|
130
|
+
repo: RepoTools
|
|
131
|
+
|
|
132
|
+
def _context_usage_block(self) -> str:
|
|
133
|
+
"""Return context-window usage plus a benchmark-agent compaction hint."""
|
|
134
|
+
if not self.context_stats:
|
|
135
|
+
return ""
|
|
136
|
+
return (
|
|
137
|
+
f"{self.context_stats.format()}\n"
|
|
138
|
+
"If event history is taking too much space, summarize older work "
|
|
139
|
+
"and call self.events.collapse(start_tag, end_tag, summary_text) "
|
|
140
|
+
"to replace it with a compact summary while keeping details accessible."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def __init__(self, llm: UnifiedLLM | None = None, **kwargs: Any) -> None:
|
|
144
|
+
super().__init__(llm=llm, **kwargs)
|
|
145
|
+
cwd = next((d for d in ("/testbed", "/app") if os.path.isdir(d)), os.getcwd())
|
|
146
|
+
self._install_python_tools(cwd)
|
|
147
|
+
self.todo = TodoManager()
|
|
148
|
+
self._seed_todos()
|
|
149
|
+
self.problem_statement = ""
|
|
150
|
+
# Base Agent hides context/events by default; BenchAgent's context_usage
|
|
151
|
+
# hint references them, so expose both APIs to the LLM here.
|
|
152
|
+
spec(self, "context", hidden=False)
|
|
153
|
+
spec(self, "events", hidden=False)
|
|
154
|
+
from nooa import Context
|
|
155
|
+
|
|
156
|
+
self.context_manager["python_tools"] = Context(doc(RepoTools, ShellTools), prefix=True)
|
|
157
|
+
self.context_manager["todo"] = Context(doc(type(self.todo)), prefix=True)
|
|
158
|
+
|
|
159
|
+
def _install_python_tools(self, cwd: str) -> None:
|
|
160
|
+
"""Install shell/repo tools rooted at the same working directory."""
|
|
161
|
+
self.shell = ShellTools(cwd=cwd, init_command=_OPTIONAL_TESTBED_ACTIVATE)
|
|
162
|
+
self.repo = RepoTools(root=cwd, session=self.shell.session)
|
|
163
|
+
|
|
164
|
+
def _seed_todos(self) -> None:
|
|
165
|
+
"""Preload the planning todo every benchmark task should start from."""
|
|
166
|
+
self.todo.add("Create a todo-based plan with clear dependencies")
|
|
167
|
+
|
|
168
|
+
async def _run_evaluation(self, task_input: dict) -> dict:
|
|
169
|
+
"""Entry point called by the Harbor runner."""
|
|
170
|
+
# Read task fields generically (Harbor adapters vary in field names).
|
|
171
|
+
self.problem_statement = _problem_statement(task_input)
|
|
172
|
+
instructions = task_input.get("system_prompt") or task_input.get("instructions") or ""
|
|
173
|
+
initial_obs = task_input.get("initial_observation") or ""
|
|
174
|
+
|
|
175
|
+
if instructions:
|
|
176
|
+
self.context["instructions"] = instructions
|
|
177
|
+
if initial_obs:
|
|
178
|
+
self.context["initial_observation"] = initial_obs
|
|
179
|
+
|
|
180
|
+
# Reset shell to the task working dir.
|
|
181
|
+
cwd = task_input.get("working_dir")
|
|
182
|
+
if cwd:
|
|
183
|
+
if not os.path.isdir(cwd):
|
|
184
|
+
raise ValueError(f"working_dir does not exist: {cwd!r}")
|
|
185
|
+
else:
|
|
186
|
+
cwd = next((d for d in ("/testbed", "/app") if os.path.isdir(d)), os.getcwd())
|
|
187
|
+
self._install_python_tools(cwd)
|
|
188
|
+
from nooa import Context
|
|
189
|
+
|
|
190
|
+
self.context_manager["todo"] = Context(doc(type(self.todo)), prefix=True)
|
|
191
|
+
self.todo.clear()
|
|
192
|
+
self._seed_todos()
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
result = await self._solve_task(self.problem_statement)
|
|
196
|
+
if isinstance(result, TaskResult):
|
|
197
|
+
return {
|
|
198
|
+
"response": result.command_to_verify,
|
|
199
|
+
"success": bool(result.solution_description),
|
|
200
|
+
"result": result.model_dump(),
|
|
201
|
+
}
|
|
202
|
+
# Fallback for non-structured returns
|
|
203
|
+
result_str = str(result) if result is not None else ""
|
|
204
|
+
return {"response": result_str, "success": True, "result": result}
|
|
205
|
+
except Exception as e:
|
|
206
|
+
_logger.error("BenchAgent failed: %s", e)
|
|
207
|
+
return {"response": "", "success": False, "error": str(e)}
|
|
208
|
+
|
|
209
|
+
@strategy(
|
|
210
|
+
CodeActStrategy(
|
|
211
|
+
config=CodeActConfig(
|
|
212
|
+
max_iterations=300, max_retries=10, text_only_stop_behavior="synthetic_comment"
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
async def _solve_task(self, description: str) -> TaskResult:
|
|
217
|
+
"""Solve the task.
|
|
218
|
+
|
|
219
|
+
You are an expert software engineer and system administrator working
|
|
220
|
+
inside a Linux container. Solve the task described below.
|
|
221
|
+
|
|
222
|
+
## Task
|
|
223
|
+
{description}
|
|
224
|
+
|
|
225
|
+
## Instructions
|
|
226
|
+
- Use ``await self.shell.run("command")`` to run shell commands.
|
|
227
|
+
- Use ``await self.shell.read("path")`` to view files.
|
|
228
|
+
- Use ``await self.repo.symbols(path, query="...")`` to find definitions.
|
|
229
|
+
- Use ``await self.repo.refs(name, path=".")`` to find usages.
|
|
230
|
+
- Use ``await self.shell.replace(...)`` to edit files or RepoTools matches.
|
|
231
|
+
- Use ``await self.shell.write_file(path, content)`` to create files.
|
|
232
|
+
- Use ``self.todo`` to track progress on multi-step work.
|
|
233
|
+
- You have root access; install packages as needed.
|
|
234
|
+
- Read task instructions carefully -- grading is strict and automated.
|
|
235
|
+
|
|
236
|
+
## Verification & Return
|
|
237
|
+
|
|
238
|
+
Before finishing, run the relevant tests to confirm your work is correct.
|
|
239
|
+
Then return a structured result explaining WHY you believe the task is done:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
return_result(TaskResult(
|
|
243
|
+
solution_description="The login handler didn't escape special chars in emails. Fixed by adding quote_plus() in auth.py:42.",
|
|
244
|
+
evidence="pytest tests/test_login.py -x passed: 5 passed in 1.2s",
|
|
245
|
+
command_to_verify="pytest tests/test_login.py -x",
|
|
246
|
+
))
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Workflow
|
|
250
|
+
|
|
251
|
+
1. Explore and understand the task/codebase
|
|
252
|
+
2. Write a plan based on todos
|
|
253
|
+
3. Implement the solution and run tests to verify
|
|
254
|
+
4. Return ``TaskResult(...)`` with concrete evidence
|
|
255
|
+
"""
|
|
256
|
+
...
|
nooa_bench/protocol.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Core protocol definitions for the task execution infrastructure.
|
|
5
|
+
|
|
6
|
+
This module defines the execution-engine and trace-analysis protocols along with
|
|
7
|
+
the usage-statistics data structures shared across the evaluation runner.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Protocol, TypeVar
|
|
13
|
+
|
|
14
|
+
# ===========================
|
|
15
|
+
# Layer 0/1/2 Protocols
|
|
16
|
+
# ===========================
|
|
17
|
+
# These protocols define the lower-level execution and analysis infrastructure
|
|
18
|
+
# that the task runner builds on.
|
|
19
|
+
|
|
20
|
+
R = TypeVar("R", covariant=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class EngineConfig:
|
|
25
|
+
"""Configuration for execution engines (Layer 0).
|
|
26
|
+
|
|
27
|
+
Generic configuration that all execution engines understand.
|
|
28
|
+
Engine-specific configs can extend this.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
max_concurrent: int = 10
|
|
32
|
+
timeout_seconds: float | None = None
|
|
33
|
+
enable_checkpointing: bool = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TaskState:
|
|
38
|
+
"""Checkpoint state for a single task.
|
|
39
|
+
|
|
40
|
+
Used by execution engines to resume from crashes or skip completed tasks.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
task_id: str
|
|
44
|
+
completed: bool
|
|
45
|
+
result: Any | None = None
|
|
46
|
+
error: str | None = None
|
|
47
|
+
timestamp: float | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ExecutionEngine(Protocol):
|
|
51
|
+
"""Protocol for swappable execution engines (Layer 0).
|
|
52
|
+
|
|
53
|
+
Different implementations provide different execution strategies:
|
|
54
|
+
- AsyncIOEngine: Async I/O with semaphore (for I/O-bound tasks like LLM APIs)
|
|
55
|
+
- MultiprocessEngine: Process pool (for CPU-bound tasks like local models)
|
|
56
|
+
- RayEngine: Distributed execution across cluster (for massive scale)
|
|
57
|
+
- NemoRunEngine: Slurm/cloud batch submission (for HPC clusters)
|
|
58
|
+
|
|
59
|
+
All engines share the same interface, allowing Layer 2 (Runner) to be
|
|
60
|
+
agnostic to the execution strategy.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
async def run_tasks(
|
|
64
|
+
self,
|
|
65
|
+
task_ids: list[str],
|
|
66
|
+
task_fn: Callable[[str], Awaitable[R]],
|
|
67
|
+
config: EngineConfig,
|
|
68
|
+
checkpoint_state: list[TaskState] | None = None,
|
|
69
|
+
on_task_complete: Callable[[str, R | Exception], None] | None = None,
|
|
70
|
+
) -> list[R]:
|
|
71
|
+
"""Execute tasks using engine-specific strategy.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
task_ids: List of task identifiers
|
|
75
|
+
task_fn: Async function that takes task_id and returns result
|
|
76
|
+
config: Engine configuration
|
|
77
|
+
checkpoint_state: Previous run state for resume (optional)
|
|
78
|
+
on_task_complete: Callback for each completed task (optional)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of results in same order as task_ids
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class ModelUsageStats:
|
|
88
|
+
"""Usage statistics for a single model."""
|
|
89
|
+
|
|
90
|
+
model_name: str
|
|
91
|
+
prompt_tokens: int = 0
|
|
92
|
+
completion_tokens: int = 0
|
|
93
|
+
total_tokens: int = 0
|
|
94
|
+
call_count: int = 0
|
|
95
|
+
latencies_ms: list[float] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def p95_latency_ms(self) -> float | None:
|
|
99
|
+
"""95th percentile latency in milliseconds."""
|
|
100
|
+
if not self.latencies_ms:
|
|
101
|
+
return None
|
|
102
|
+
sorted_latencies = sorted(self.latencies_ms)
|
|
103
|
+
idx = int(len(sorted_latencies) * 0.95)
|
|
104
|
+
return sorted_latencies[idx]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class TaskUsageStats:
|
|
109
|
+
"""Usage statistics extracted from a single task's trace."""
|
|
110
|
+
|
|
111
|
+
task_id: str
|
|
112
|
+
total_runtime_seconds: float
|
|
113
|
+
models_used: list[ModelUsageStats] = field(default_factory=list)
|
|
114
|
+
total_llm_calls: int = 0
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def total_tokens(self) -> int:
|
|
118
|
+
"""Total tokens across all models."""
|
|
119
|
+
return sum(m.total_tokens for m in self.models_used)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def total_prompt_tokens(self) -> int:
|
|
123
|
+
"""Total prompt tokens across all models."""
|
|
124
|
+
return sum(m.prompt_tokens for m in self.models_used)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def total_completion_tokens(self) -> int:
|
|
128
|
+
"""Total completion tokens across all models."""
|
|
129
|
+
return sum(m.completion_tokens for m in self.models_used)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class AggregateUsageStats:
|
|
134
|
+
"""Aggregate usage statistics across multiple tasks."""
|
|
135
|
+
|
|
136
|
+
num_tasks: int
|
|
137
|
+
per_task_stats: list[TaskUsageStats] = field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def total_tokens(self) -> int:
|
|
141
|
+
"""Total tokens across all tasks."""
|
|
142
|
+
return sum(s.total_tokens for s in self.per_task_stats)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def total_runtime_seconds(self) -> float:
|
|
146
|
+
"""Total runtime across all tasks."""
|
|
147
|
+
return sum(s.total_runtime_seconds for s in self.per_task_stats)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def models_breakdown(self) -> dict[str, ModelUsageStats]:
|
|
151
|
+
"""Aggregate stats per model across all tasks."""
|
|
152
|
+
by_model: dict[str, ModelUsageStats] = {}
|
|
153
|
+
|
|
154
|
+
for task_stats in self.per_task_stats:
|
|
155
|
+
for model_stats in task_stats.models_used:
|
|
156
|
+
if model_stats.model_name not in by_model:
|
|
157
|
+
by_model[model_stats.model_name] = ModelUsageStats(
|
|
158
|
+
model_name=model_stats.model_name
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
agg = by_model[model_stats.model_name]
|
|
162
|
+
agg.prompt_tokens += model_stats.prompt_tokens
|
|
163
|
+
agg.completion_tokens += model_stats.completion_tokens
|
|
164
|
+
agg.total_tokens += model_stats.total_tokens
|
|
165
|
+
agg.call_count += model_stats.call_count
|
|
166
|
+
agg.latencies_ms.extend(model_stats.latencies_ms)
|
|
167
|
+
|
|
168
|
+
return by_model
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TraceAnalyzer(Protocol):
|
|
172
|
+
"""Protocol for analyzing OTel traces to extract usage statistics.
|
|
173
|
+
|
|
174
|
+
Implementations read .jsonl trace files and extract:
|
|
175
|
+
- Token counts per model
|
|
176
|
+
- LLM call latencies
|
|
177
|
+
- Total runtime
|
|
178
|
+
|
|
179
|
+
This avoids duplicating trace data in IntermediateSteps.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def analyze_trace(self, trace_path: str) -> TaskUsageStats:
|
|
183
|
+
"""Analyze a single trace file and extract usage statistics.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
trace_path: Path to .jsonl trace file
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
TaskUsageStats with extracted metrics
|
|
190
|
+
"""
|
|
191
|
+
...
|
nooa_bench/runner.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Harbor agent runner for NOOA — executed inside the Harbor container.
|
|
5
|
+
|
|
6
|
+
Harbor invokes this as a CLI process::
|
|
7
|
+
|
|
8
|
+
nemo-harbor \\
|
|
9
|
+
--instruction '...' \\
|
|
10
|
+
--model 'anthropic/claude-opus-4-6' \\
|
|
11
|
+
--agent-type bench
|
|
12
|
+
|
|
13
|
+
The runner is benchmark-agnostic. Its only jobs are:
|
|
14
|
+
|
|
15
|
+
1. Instantiate the right agent class from ``--agent-type``.
|
|
16
|
+
2. Call ``agent._run_evaluation({"user_message": instruction})``.
|
|
17
|
+
3. Write ``result.json`` to ``/logs/agent/`` and answer text to ``/app/answer.txt``.
|
|
18
|
+
|
|
19
|
+
Benchmark-specific logic (system prompts, instruction parsing, data paths) lives
|
|
20
|
+
inside each agent class, not here.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import importlib
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("nooa_bench.runner")
|
|
37
|
+
|
|
38
|
+
# Harbor container path conventions
|
|
39
|
+
LOGS_DIR = Path("/logs/agent")
|
|
40
|
+
ARTIFACTS_DIR = Path("/logs/artifacts")
|
|
41
|
+
TRACES_DIR = ARTIFACTS_DIR / "traces"
|
|
42
|
+
ANSWER_FILE = Path("/app/answer.txt")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _setup_logging() -> None:
|
|
46
|
+
handlers: list[logging.Handler] = [logging.StreamHandler(sys.stderr)]
|
|
47
|
+
try:
|
|
48
|
+
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
handlers.append(logging.FileHandler(str(LOGS_DIR / "nooa_bench.log")))
|
|
50
|
+
except OSError:
|
|
51
|
+
# Outside a Harbor container /logs may not exist or be writable — stderr only.
|
|
52
|
+
pass
|
|
53
|
+
logging.basicConfig(
|
|
54
|
+
level=logging.INFO,
|
|
55
|
+
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
56
|
+
handlers=handlers,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _setup_tracing(model: str, agent_type: str) -> None:
|
|
61
|
+
"""Enable OTel tracing, publishing live to the viewer if reachable.
|
|
62
|
+
|
|
63
|
+
Detection order:
|
|
64
|
+
1. Probe ``OTLP_ENDPOINT`` env var, or ``http://localhost:5001`` by default.
|
|
65
|
+
2. If reachable → stream spans via the journal exporter with ``eval.model``
|
|
66
|
+
and ``eval.agent_type`` injected as resource attributes so the viewer can
|
|
67
|
+
display them without a separate ``import-harbor`` step.
|
|
68
|
+
3. If unreachable → fall back to JSONL files in the Harbor artifact
|
|
69
|
+
directory (``/logs/artifacts/traces/``), importable later via
|
|
70
|
+
``nemo-oo import-harbor``.
|
|
71
|
+
|
|
72
|
+
Note: Apptainer containers share the host network namespace, so
|
|
73
|
+
``localhost:5001`` inside the container resolves to the developer's host.
|
|
74
|
+
For Docker containers set ``OTLP_ENDPOINT=http://host.docker.internal:5001``.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
from nooa.tracing import (
|
|
78
|
+
enable_tracing,
|
|
79
|
+
probe_otlp_endpoint,
|
|
80
|
+
)
|
|
81
|
+
from nooa.tracing import exporters as nemo_exporters
|
|
82
|
+
except ImportError:
|
|
83
|
+
logger.warning("nooa.tracing not available, no tracing")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
endpoint = os.environ.get("OTLP_ENDPOINT", "http://localhost:5001/v1/traces")
|
|
87
|
+
|
|
88
|
+
if probe_otlp_endpoint(endpoint):
|
|
89
|
+
logger.info("OTLP endpoint reachable (%s) — streaming traces live", endpoint)
|
|
90
|
+
enable_tracing(
|
|
91
|
+
exporters=[nemo_exporters.journal(endpoint=endpoint)],
|
|
92
|
+
extra_resource_attrs={"eval.model": model, "eval.agent_type": agent_type},
|
|
93
|
+
)
|
|
94
|
+
logger.info("OTel tracing enabled → %s", endpoint)
|
|
95
|
+
else:
|
|
96
|
+
TRACES_DIR.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
enable_tracing(exporters=[nemo_exporters.jsonl(TRACES_DIR)])
|
|
98
|
+
logger.info("OTel tracing enabled → %s (OTLP not reachable)", TRACES_DIR)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _import_agent_class(agent_type: str) -> type:
|
|
102
|
+
from nooa_bench import AGENT_CLASSES
|
|
103
|
+
|
|
104
|
+
entry = AGENT_CLASSES.get(agent_type)
|
|
105
|
+
if entry is None:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Unknown agent_type: {agent_type!r}. Must be one of: {sorted(AGENT_CLASSES)}"
|
|
108
|
+
)
|
|
109
|
+
module_path, class_name = entry.rsplit(":", 1)
|
|
110
|
+
mod = importlib.import_module(module_path)
|
|
111
|
+
return getattr(mod, class_name)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _write_result(result: dict[str, Any], model: str, agent_type: str) -> None:
|
|
115
|
+
"""Write result metadata to LOGS_DIR/result.json."""
|
|
116
|
+
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
payload = {
|
|
118
|
+
"model": model,
|
|
119
|
+
"agent_type": agent_type,
|
|
120
|
+
"success": result.get("success", False),
|
|
121
|
+
"response": result.get("response", ""),
|
|
122
|
+
"error": result.get("error"),
|
|
123
|
+
"n_input_tokens": result.get("n_input_tokens"),
|
|
124
|
+
"n_output_tokens": result.get("n_output_tokens"),
|
|
125
|
+
}
|
|
126
|
+
out = LOGS_DIR / "result.json"
|
|
127
|
+
out.write_text(json.dumps(payload, indent=2))
|
|
128
|
+
logger.info("Result written → %s", out)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _write_answer(result: dict[str, Any]) -> None:
|
|
132
|
+
"""Write the agent's answer to /app/answer.txt for Harbor's verifier."""
|
|
133
|
+
answer = result.get("answer") or result.get("response", "")
|
|
134
|
+
if not answer:
|
|
135
|
+
logger.warning("No answer to write to %s", ANSWER_FILE)
|
|
136
|
+
return
|
|
137
|
+
try:
|
|
138
|
+
ANSWER_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
ANSWER_FILE.write_text(str(answer))
|
|
140
|
+
logger.info("Answer written → %s", ANSWER_FILE)
|
|
141
|
+
except OSError as e:
|
|
142
|
+
logger.warning("Could not write answer file %s: %s", ANSWER_FILE, e)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def _run(
|
|
146
|
+
instruction: str,
|
|
147
|
+
model: str,
|
|
148
|
+
agent_type: str,
|
|
149
|
+
api_base: str | None,
|
|
150
|
+
working_dir: str | None = None,
|
|
151
|
+
) -> int:
|
|
152
|
+
"""Async main: instantiate, wire, run. Returns exit code (0 = success)."""
|
|
153
|
+
from nooa.unifiedllm import get_llm_client
|
|
154
|
+
|
|
155
|
+
# Build LLM client — honour env-var overrides for local vLLM deployments.
|
|
156
|
+
llm_overrides: dict[str, str] = {}
|
|
157
|
+
if api_base:
|
|
158
|
+
llm_overrides["api_base"] = api_base
|
|
159
|
+
elif base_url := os.environ.get("OPENAI_BASE_URL"):
|
|
160
|
+
llm_overrides["api_base"] = base_url
|
|
161
|
+
if api_key := os.environ.get("OPENAI_API_KEY"):
|
|
162
|
+
llm_overrides["api_key"] = api_key
|
|
163
|
+
|
|
164
|
+
llm_client = get_llm_client(model, **llm_overrides)
|
|
165
|
+
|
|
166
|
+
# Instantiate agent.
|
|
167
|
+
AgentClass = _import_agent_class(agent_type)
|
|
168
|
+
agent: Any = AgentClass(llm=llm_client)
|
|
169
|
+
|
|
170
|
+
# All agents share the same interface: {"user_message": instruction}.
|
|
171
|
+
# Benchmark-specific parsing (system prompts, data paths, etc.) happens
|
|
172
|
+
# inside the agent's _run_evaluation method.
|
|
173
|
+
from nooa.runtime.token_usage import get_task_tokens, start_task_tokens
|
|
174
|
+
|
|
175
|
+
logger.info("Running agent %s (model=%s)...", agent_type, model)
|
|
176
|
+
start_task_tokens()
|
|
177
|
+
task_input: dict[str, Any] = {"user_message": instruction}
|
|
178
|
+
if working_dir:
|
|
179
|
+
task_input["working_dir"] = working_dir
|
|
180
|
+
result = await agent._run_evaluation(task_input)
|
|
181
|
+
result.update(get_task_tokens())
|
|
182
|
+
_write_result(result, model, agent_type)
|
|
183
|
+
_write_answer(result)
|
|
184
|
+
|
|
185
|
+
if result.get("success"):
|
|
186
|
+
logger.info("Agent completed successfully.")
|
|
187
|
+
return 0
|
|
188
|
+
else:
|
|
189
|
+
logger.error("Agent reported failure.")
|
|
190
|
+
return 1
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@click.command()
|
|
194
|
+
@click.option("--instruction", required=True, help="Task instruction / problem statement")
|
|
195
|
+
@click.option("--model", required=True, help="Model name in litellm format")
|
|
196
|
+
@click.option("--agent-type", default="bench", show_default=True, help="Agent variant to run")
|
|
197
|
+
@click.option("--working-dir", default=None, help="Working directory for the agent shell session")
|
|
198
|
+
@click.option("--api-base", default=None, help="Override API base URL")
|
|
199
|
+
def main(
|
|
200
|
+
instruction: str,
|
|
201
|
+
model: str,
|
|
202
|
+
agent_type: str,
|
|
203
|
+
working_dir: str | None,
|
|
204
|
+
api_base: str | None,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Run a NOOA agent on a task inside a Harbor container."""
|
|
207
|
+
_setup_logging()
|
|
208
|
+
logger.info("nooa-bench runner starting")
|
|
209
|
+
logger.info(" model: %s", model)
|
|
210
|
+
logger.info(" agent_type: %s", agent_type)
|
|
211
|
+
if api_base:
|
|
212
|
+
logger.info(" api_base: %s", api_base)
|
|
213
|
+
|
|
214
|
+
# Validate agent_type early so we get a clean error before any heavy imports.
|
|
215
|
+
from nooa_bench import AGENT_CLASSES
|
|
216
|
+
|
|
217
|
+
if agent_type not in AGENT_CLASSES:
|
|
218
|
+
logger.error(
|
|
219
|
+
"Unknown agent_type: %r. Must be one of: %s", agent_type, sorted(AGENT_CLASSES)
|
|
220
|
+
)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
_setup_tracing(model=model, agent_type=agent_type)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
exit_code = asyncio.run(_run(instruction, model, agent_type, api_base, working_dir))
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.exception("Runner failed with unhandled exception: %s", e)
|
|
229
|
+
exit_code = 1
|
|
230
|
+
|
|
231
|
+
sys.exit(exit_code)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Trace analyzer for extracting usage statistics from execution traces.
|
|
5
|
+
|
|
6
|
+
This module reads JSONL trace files produced by the agent framework and
|
|
7
|
+
extracts per-model token counts, LLM call latencies, and total runtime.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from nooa_bench.protocol import (
|
|
16
|
+
ModelUsageStats,
|
|
17
|
+
TaskUsageStats,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TraceAnalyzer:
|
|
24
|
+
"""
|
|
25
|
+
Analyzes OTel traces to extract usage statistics.
|
|
26
|
+
|
|
27
|
+
Implements the TraceAnalyzer protocol: reads .jsonl trace files and
|
|
28
|
+
extracts token counts per model, LLM call latencies, and total runtime.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def analyze_trace(self, trace_path: str) -> TaskUsageStats:
|
|
32
|
+
"""
|
|
33
|
+
Analyze a trace file and extract usage statistics.
|
|
34
|
+
|
|
35
|
+
Implements the TraceAnalyzer protocol to extract:
|
|
36
|
+
- Token counts per model
|
|
37
|
+
- LLM call latencies
|
|
38
|
+
- Total runtime
|
|
39
|
+
- Model usage breakdown
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
trace_path: Path to .jsonl trace file
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
TaskUsageStats with extracted metrics
|
|
46
|
+
"""
|
|
47
|
+
path = Path(trace_path)
|
|
48
|
+
if not path.exists():
|
|
49
|
+
# Return empty stats for missing trace
|
|
50
|
+
return TaskUsageStats(
|
|
51
|
+
task_id=path.stem,
|
|
52
|
+
total_runtime_seconds=0.0,
|
|
53
|
+
models_used=[],
|
|
54
|
+
total_llm_calls=0,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Track stats per model
|
|
58
|
+
model_stats: dict[str, ModelUsageStats] = {}
|
|
59
|
+
trace_start_time: float | None = None
|
|
60
|
+
trace_end_time: float | None = None
|
|
61
|
+
total_llm_calls = 0
|
|
62
|
+
|
|
63
|
+
with open(path) as f:
|
|
64
|
+
for line in f:
|
|
65
|
+
try:
|
|
66
|
+
span = json.loads(line.strip())
|
|
67
|
+
|
|
68
|
+
# Track overall trace timing
|
|
69
|
+
start_time = span.get("start_time_unix_nano", 0) / 1e9
|
|
70
|
+
end_time = span.get("end_time_unix_nano", 0) / 1e9
|
|
71
|
+
|
|
72
|
+
if trace_start_time is None or start_time < trace_start_time:
|
|
73
|
+
trace_start_time = start_time
|
|
74
|
+
if trace_end_time is None or end_time > trace_end_time:
|
|
75
|
+
trace_end_time = end_time
|
|
76
|
+
|
|
77
|
+
# Look for LLM spans
|
|
78
|
+
attrs = span.get("attributes", {})
|
|
79
|
+
span_name = span.get("name", "")
|
|
80
|
+
|
|
81
|
+
# Check if this is an LLM call span
|
|
82
|
+
if self._is_llm_span(span_name, attrs):
|
|
83
|
+
total_llm_calls += 1
|
|
84
|
+
|
|
85
|
+
# Extract model name
|
|
86
|
+
model_name = attrs.get(
|
|
87
|
+
"llm.model", attrs.get("gen_ai.request.model", "unknown")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Initialize model stats if not seen before
|
|
91
|
+
if model_name not in model_stats:
|
|
92
|
+
model_stats[model_name] = ModelUsageStats(model_name=model_name)
|
|
93
|
+
|
|
94
|
+
stats = model_stats[model_name]
|
|
95
|
+
|
|
96
|
+
# Extract token counts with validation
|
|
97
|
+
prompt_tokens = attrs.get(
|
|
98
|
+
"llm.token_count.prompt", attrs.get("gen_ai.usage.input_tokens", 0)
|
|
99
|
+
)
|
|
100
|
+
completion_tokens = attrs.get(
|
|
101
|
+
"llm.token_count.completion", attrs.get("gen_ai.usage.output_tokens", 0)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Validate and parse token counts
|
|
105
|
+
prompt_count = self._parse_token_count(prompt_tokens, "prompt_tokens", path)
|
|
106
|
+
completion_count = self._parse_token_count(
|
|
107
|
+
completion_tokens, "completion_tokens", path
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
stats.prompt_tokens += prompt_count
|
|
111
|
+
stats.completion_tokens += completion_count
|
|
112
|
+
stats.total_tokens += prompt_count + completion_count
|
|
113
|
+
stats.call_count += 1
|
|
114
|
+
|
|
115
|
+
# Calculate latency in milliseconds
|
|
116
|
+
if start_time > 0 and end_time > 0:
|
|
117
|
+
latency_ms = (end_time - start_time) * 1000
|
|
118
|
+
stats.latencies_ms.append(latency_ms)
|
|
119
|
+
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Calculate total runtime
|
|
124
|
+
runtime_seconds = 0.0
|
|
125
|
+
if trace_start_time and trace_end_time:
|
|
126
|
+
runtime_seconds = trace_end_time - trace_start_time
|
|
127
|
+
|
|
128
|
+
return TaskUsageStats(
|
|
129
|
+
task_id=path.stem,
|
|
130
|
+
total_runtime_seconds=runtime_seconds,
|
|
131
|
+
models_used=list(model_stats.values()),
|
|
132
|
+
total_llm_calls=total_llm_calls,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _is_llm_span(self, span_name: str, attributes: dict[str, Any]) -> bool:
|
|
136
|
+
"""Check if a span represents an LLM API call."""
|
|
137
|
+
# Check span name
|
|
138
|
+
llm_span_names = ["llm", "chat", "completion", "generation", "inference"]
|
|
139
|
+
if any(name in span_name.lower() for name in llm_span_names):
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
# Check for LLM-related attributes
|
|
143
|
+
llm_attrs = [
|
|
144
|
+
"llm.model",
|
|
145
|
+
"llm.token_count.prompt",
|
|
146
|
+
"llm.token_count.completion",
|
|
147
|
+
"gen_ai.request.model",
|
|
148
|
+
"gen_ai.usage.input_tokens",
|
|
149
|
+
"gen_ai.usage.output_tokens",
|
|
150
|
+
]
|
|
151
|
+
return any(attr in attributes for attr in llm_attrs)
|
|
152
|
+
|
|
153
|
+
def _parse_token_count(self, value: Any, field_name: str, trace_path: Path) -> int:
|
|
154
|
+
"""Parse a token count value with validation.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
value: The raw value from the trace (could be int, str, None, etc.)
|
|
158
|
+
field_name: Name of the field for logging
|
|
159
|
+
trace_path: Path to trace file for logging context
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Parsed integer token count, or 0 if invalid
|
|
163
|
+
"""
|
|
164
|
+
if value is None:
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
if isinstance(value, int):
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
if isinstance(value, float):
|
|
171
|
+
return int(value)
|
|
172
|
+
|
|
173
|
+
if isinstance(value, str):
|
|
174
|
+
try:
|
|
175
|
+
return int(value)
|
|
176
|
+
except ValueError:
|
|
177
|
+
logger.warning(
|
|
178
|
+
f"Invalid {field_name} value '{value}' in trace {trace_path.name}, using 0"
|
|
179
|
+
)
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
logger.warning(
|
|
183
|
+
f"Unexpected type {type(value).__name__} for {field_name} in trace "
|
|
184
|
+
f"{trace_path.name}, using 0"
|
|
185
|
+
)
|
|
186
|
+
return 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nooa-bench
|
|
3
|
+
Version: 0.0.7
|
|
4
|
+
Summary: Benchmark agent (BenchAgent) and Harbor runner for the NOOA framework — reproduces the tech report's SWE-bench and Terminal-Bench results
|
|
5
|
+
Project-URL: Homepage, https://github.com/NVIDIA-NeMo/labs-OO-Agents
|
|
6
|
+
Project-URL: Repository, https://github.com/NVIDIA-NeMo/labs-OO-Agents
|
|
7
|
+
Project-URL: Issues, https://github.com/NVIDIA-NeMo/labs-OO-Agents/issues
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Requires-Python: <3.14,>=3.12
|
|
10
|
+
Requires-Dist: click>=8.1
|
|
11
|
+
Requires-Dist: nooa
|
|
12
|
+
Requires-Dist: nooa-cli
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# nooa-bench
|
|
16
|
+
|
|
17
|
+
Benchmark agent (`BenchAgent`) and Harbor runner for
|
|
18
|
+
[NOOA](https://github.com/NVIDIA-NeMo/labs-OO-Agents). Reproduces the SWE-bench
|
|
19
|
+
and Terminal-Bench results from the NOOA tech report.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv add nooa-bench
|
|
23
|
+
nemo-harbor --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
See the [main repository](https://github.com/NVIDIA-NeMo/labs-OO-Agents) for
|
|
27
|
+
documentation.
|
|
28
|
+
|
|
29
|
+
Apache-2.0 licensed.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
nooa_bench/__init__.py,sha256=PnppDSvQWWJK2au5iCLEXXuT3EW57U7mNuuR8uDa_LU,776
|
|
2
|
+
nooa_bench/bench_agent.py,sha256=XThiC8G-GSFEIu7b9iLP9WhhPxFpHoLJgK38rDxfOnA,10243
|
|
3
|
+
nooa_bench/protocol.py,sha256=h3xbUHd986MXPXF2guQPtFSb1OU6aPdDId8QPcYHUS8,6076
|
|
4
|
+
nooa_bench/runner.py,sha256=TkFUg6GdwmtL1N_eTPtrp0gcFgfXR9GQRT-K6wByKW0,8365
|
|
5
|
+
nooa_bench/trace_analyzer.py,sha256=Fg6WXj_vJDcHiq8-WxW6Et5AzHs9c5eRqmwuJ7oDneA,6663
|
|
6
|
+
nooa_bench-0.0.7.dist-info/METADATA,sha256=Dxy7TjFB0-regKpqn0rpKo8fb8wqgDLzGy-YT9_DGwQ,944
|
|
7
|
+
nooa_bench-0.0.7.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
nooa_bench-0.0.7.dist-info/entry_points.txt,sha256=3J9ysVhArjnuOz0ZjXMXXF6t-TSvqt5zJDOUEGmsFJ4,55
|
|
9
|
+
nooa_bench-0.0.7.dist-info/RECORD,,
|