newmeasure-cli 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/.gitignore +4 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/PKG-INFO +26 -1
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/README.md +25 -0
- newmeasure_cli-0.2.1/cli/api_client.py +38 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/admin.py +6 -2
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/login.py +5 -2
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/result.py +3 -2
- newmeasure_cli-0.2.1/cli/commands/run.py +281 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/upload.py +3 -1
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/config.py +2 -0
- newmeasure_cli-0.2.1/cli/errors.py +45 -0
- newmeasure_cli-0.2.1/cli/main.py +169 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/pyproject.toml +2 -2
- newmeasure_cli-0.2.0/cli/api_client.py +0 -19
- newmeasure_cli-0.2.0/cli/commands/run.py +0 -120
- newmeasure_cli-0.2.0/cli/main.py +0 -98
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/__init__.py +0 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/__init__.py +0 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/artifacts.py +0 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/runs.py +0 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/commands/tasks.py +0 -0
- {newmeasure_cli-0.2.0 → newmeasure_cli-0.2.1}/cli/progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: newmeasure-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: NewMeasure eval CLI
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: httpx>=0.28
|
|
@@ -68,6 +68,31 @@ newm result <run-id>
|
|
|
68
68
|
> [!NOTE]
|
|
69
69
|
> Task IDs persist across uploads (the first upload writes `task_id` into `task.toml`). Each execution gets a fresh trial ID.
|
|
70
70
|
|
|
71
|
+
## Preview environments and saved logins
|
|
72
|
+
|
|
73
|
+
In the app, open **Manage logins** from the Run dialog to create a personal CLI token, then:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
newm login --evals
|
|
77
|
+
newm logins
|
|
78
|
+
newm run t_abc123 --url https://my-branch.solstice.co --n-trials 3
|
|
79
|
+
newm run tasks/ --url https://my-branch.solstice.co --login "Preview team"
|
|
80
|
+
newm run t_abc123 --prompt-credentials --save-login "My preview"
|
|
81
|
+
# Add --share-login to make the saved login available to your organization.
|
|
82
|
+
newm run t_abc123 --from-run e_previous --login "My preview"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The personal CLI token supplements your customer API key (`newm login`), identifying whose saved
|
|
86
|
+
logins to use. Tokens expire after 90 days and can be revoked from Manage logins. Omit `--login`
|
|
87
|
+
to use the NewMeasure account. Saved logins are personal unless shared; `newm logins` shows who
|
|
88
|
+
saved each one. Passwords are prompted privately and never accepted as command arguments.
|
|
89
|
+
|
|
90
|
+
Trials are limited to **1–5 per task** in both the app and CLI. Every launch creates a separate
|
|
91
|
+
entry on Runs. `--from-run` links the original and inherits its environment unless `--url` is set;
|
|
92
|
+
it does not reuse the original login. Without either option the task's staging URL is used.
|
|
93
|
+
Solstice previews must use HTTPS on a subdomain of `solstice.co`, without user info, query strings
|
|
94
|
+
or fragments. The override applies only to the prepared run, never to your task files.
|
|
95
|
+
|
|
71
96
|
## Browser tasks
|
|
72
97
|
|
|
73
98
|
Browser tasks test a web product: an AI agent uses it in a cloud browser. Put the application login in a local env file, using the field names the task declares in `credential_fields`:
|
|
@@ -57,6 +57,31 @@ newm result <run-id>
|
|
|
57
57
|
> [!NOTE]
|
|
58
58
|
> Task IDs persist across uploads (the first upload writes `task_id` into `task.toml`). Each execution gets a fresh trial ID.
|
|
59
59
|
|
|
60
|
+
## Preview environments and saved logins
|
|
61
|
+
|
|
62
|
+
In the app, open **Manage logins** from the Run dialog to create a personal CLI token, then:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
newm login --evals
|
|
66
|
+
newm logins
|
|
67
|
+
newm run t_abc123 --url https://my-branch.solstice.co --n-trials 3
|
|
68
|
+
newm run tasks/ --url https://my-branch.solstice.co --login "Preview team"
|
|
69
|
+
newm run t_abc123 --prompt-credentials --save-login "My preview"
|
|
70
|
+
# Add --share-login to make the saved login available to your organization.
|
|
71
|
+
newm run t_abc123 --from-run e_previous --login "My preview"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The personal CLI token supplements your customer API key (`newm login`), identifying whose saved
|
|
75
|
+
logins to use. Tokens expire after 90 days and can be revoked from Manage logins. Omit `--login`
|
|
76
|
+
to use the NewMeasure account. Saved logins are personal unless shared; `newm logins` shows who
|
|
77
|
+
saved each one. Passwords are prompted privately and never accepted as command arguments.
|
|
78
|
+
|
|
79
|
+
Trials are limited to **1–5 per task** in both the app and CLI. Every launch creates a separate
|
|
80
|
+
entry on Runs. `--from-run` links the original and inherits its environment unless `--url` is set;
|
|
81
|
+
it does not reuse the original login. Without either option the task's staging URL is used.
|
|
82
|
+
Solstice previews must use HTTPS on a subdomain of `solstice.co`, without user info, query strings
|
|
83
|
+
or fragments. The override applies only to the prepared run, never to your task files.
|
|
84
|
+
|
|
60
85
|
## Browser tasks
|
|
61
86
|
|
|
62
87
|
Browser tasks test a web product: an AI agent uses it in a cloud browser. Put the application login in a local env file, using the field names the task declares in `credential_fields`:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from cli.config import get
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def client() -> httpx.Client:
|
|
10
|
+
api_url = get("api_url")
|
|
11
|
+
token = get("token")
|
|
12
|
+
if not token:
|
|
13
|
+
typer.echo("Not logged in. Run: newm login")
|
|
14
|
+
raise typer.Exit(1)
|
|
15
|
+
return httpx.Client(
|
|
16
|
+
base_url=api_url,
|
|
17
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
18
|
+
timeout=300,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def evals_client() -> httpx.Client:
|
|
23
|
+
token = get("evals_token")
|
|
24
|
+
if not token:
|
|
25
|
+
raise typer.BadParameter(
|
|
26
|
+
"Connect your app account with newm login --evals (create a token at /settings/runs in the app)"
|
|
27
|
+
)
|
|
28
|
+
url = get("evals_url")
|
|
29
|
+
from urllib.parse import urlsplit
|
|
30
|
+
|
|
31
|
+
p = urlsplit(url)
|
|
32
|
+
if p.scheme != "https" and p.hostname not in ("localhost", "127.0.0.1", "::1"):
|
|
33
|
+
raise typer.BadParameter("CLI access requires HTTPS")
|
|
34
|
+
return httpx.Client(
|
|
35
|
+
base_url=url.rstrip("/") + "/",
|
|
36
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
37
|
+
timeout=300,
|
|
38
|
+
)
|
|
@@ -16,10 +16,14 @@ admin_app = typer.Typer(help="Admin commands")
|
|
|
16
16
|
def create_token(
|
|
17
17
|
customer: str = typer.Option(..., "--customer", help="Customer name"),
|
|
18
18
|
agent_url: str = typer.Option(..., "--agent-url", help="Customer's agent URL"),
|
|
19
|
-
agent_protocol: str = typer.Option(
|
|
19
|
+
agent_protocol: str = typer.Option(
|
|
20
|
+
..., "--agent-protocol", help="Protocol (eve, mastra)"
|
|
21
|
+
),
|
|
20
22
|
agent_auth_user: Optional[str] = typer.Option(None, "--agent-auth-user"),
|
|
21
23
|
agent_auth_pass: Optional[str] = typer.Option(None, "--agent-auth-pass"),
|
|
22
|
-
mcp_token: Optional[str] = typer.Option(
|
|
24
|
+
mcp_token: Optional[str] = typer.Option(
|
|
25
|
+
None, "--mcp-token", help="Comma-separated MCP tokens for routing"
|
|
26
|
+
),
|
|
23
27
|
):
|
|
24
28
|
"""Create a new customer API token (requires admin auth)."""
|
|
25
29
|
import httpx
|
|
@@ -6,11 +6,14 @@ from rich.prompt import Prompt
|
|
|
6
6
|
from cli import config
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def login():
|
|
9
|
+
def login(evals: bool = False):
|
|
10
10
|
"""Store API token."""
|
|
11
11
|
cfg = config.load()
|
|
12
12
|
|
|
13
|
-
cfg["token"] = Prompt.ask(
|
|
13
|
+
cfg["evals_token" if evals else "token"] = Prompt.ask(
|
|
14
|
+
"Personal CLI token from Manage logins in the app" if evals else "API Token",
|
|
15
|
+
password=True,
|
|
16
|
+
)
|
|
14
17
|
|
|
15
18
|
config.save(cfg)
|
|
16
19
|
typer.echo(f"Saved to {config.CONFIG_PATH}")
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import typer
|
|
4
3
|
from rich.console import Console
|
|
5
4
|
from rich.table import Table
|
|
6
5
|
|
|
@@ -44,7 +43,9 @@ def result(run_id: str):
|
|
|
44
43
|
reward = f"{t['reward']:.2f}" if t.get("reward") is not None else "-"
|
|
45
44
|
dur = f"{t['duration_sec']:.1f}s" if t.get("duration_sec") else "-"
|
|
46
45
|
err = (t.get("error") or "")[:50]
|
|
47
|
-
table.add_row(
|
|
46
|
+
table.add_row(
|
|
47
|
+
t["id"], t["status"], reward, dur, t.get("last_stage") or "-", err
|
|
48
|
+
)
|
|
48
49
|
|
|
49
50
|
console.print(table)
|
|
50
51
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from dotenv import dotenv_values
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from cli.api_client import client, evals_client
|
|
14
|
+
from cli.commands.upload import upload
|
|
15
|
+
from cli.progress import progress_lines
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def task_dirs(target: Path) -> list[Path]:
|
|
21
|
+
"""The task folders TARGET names: itself, or every task folder in it (or in its tasks/)."""
|
|
22
|
+
if not target.is_dir():
|
|
23
|
+
return []
|
|
24
|
+
if (target / "task.toml").is_file():
|
|
25
|
+
return [target]
|
|
26
|
+
for root in (target, target / "tasks"):
|
|
27
|
+
found = sorted(p.parent for p in root.glob("*/task.toml"))
|
|
28
|
+
if found:
|
|
29
|
+
return found
|
|
30
|
+
raise typer.BadParameter(f"No task folders (with task.toml) in {target}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _task_id(task_dir: Path) -> str:
|
|
34
|
+
upload(task_dir)
|
|
35
|
+
task_id = tomllib.loads((task_dir / "task.toml").read_text()).get("task_id")
|
|
36
|
+
if not task_id:
|
|
37
|
+
typer.echo(f"Failed to get task_id after uploading {task_dir}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
return task_id
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _credentials(c, tasks: list[dict], credentials_file: Path | None) -> dict:
|
|
43
|
+
"""The fields the tasks need, from the local file; never sent over remote plain HTTP."""
|
|
44
|
+
url = urlparse(str(c.base_url))
|
|
45
|
+
if url.scheme != "https" and url.hostname not in {"127.0.0.1", "localhost", "::1"}:
|
|
46
|
+
raise typer.BadParameter(
|
|
47
|
+
"Application credentials require an HTTPS NewMeasure API URL"
|
|
48
|
+
)
|
|
49
|
+
if credentials_file is None or not credentials_file.is_file():
|
|
50
|
+
raise typer.BadParameter(
|
|
51
|
+
"Browser runs require --credentials-file pointing to a local environment file"
|
|
52
|
+
)
|
|
53
|
+
values = dotenv_values(credentials_file, interpolate=False)
|
|
54
|
+
fields = sorted({f for t in tasks for f in t.get("credential_fields", [])})
|
|
55
|
+
if any(not values.get(field) for field in fields):
|
|
56
|
+
raise typer.BadParameter(f"Credential file must define: {', '.join(fields)}")
|
|
57
|
+
return {field: values[field] for field in fields}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _who(where: Path) -> str:
|
|
61
|
+
"""Who is starting the run, for the evals app: git's user.email where the tasks are."""
|
|
62
|
+
try:
|
|
63
|
+
got = subprocess.run(
|
|
64
|
+
["git", "-C", str(where), "config", "user.email"],
|
|
65
|
+
check=False,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=5,
|
|
69
|
+
)
|
|
70
|
+
except (OSError, subprocess.SubprocessError):
|
|
71
|
+
return ""
|
|
72
|
+
return got.stdout.strip()[:200]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def task_kind(task_dir: Path) -> str:
|
|
76
|
+
"""The task's kind from [metadata.evals] kind in task.toml: "banner", or "email" when unset."""
|
|
77
|
+
config = tomllib.loads((task_dir / "task.toml").read_text())
|
|
78
|
+
return config.get("metadata", {}).get("evals", {}).get("kind") or "email"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run(
|
|
82
|
+
target: str,
|
|
83
|
+
n_trials: int = 1,
|
|
84
|
+
credentials_file: Path | None = None,
|
|
85
|
+
wait: bool = True,
|
|
86
|
+
kind: str | None = None,
|
|
87
|
+
url: str | None = None,
|
|
88
|
+
saved_login: str | None = None,
|
|
89
|
+
prompt_credentials: bool = False,
|
|
90
|
+
save_login: str | None = None,
|
|
91
|
+
share_login: bool = False,
|
|
92
|
+
parent: str | None = None,
|
|
93
|
+
):
|
|
94
|
+
"""Run an eval. TARGET is a task ID (t_...), a task folder, or a folder of task folders (such
|
|
95
|
+
as tasks/). Browser tasks run together as one eval run, graded and shown in the evals app.
|
|
96
|
+
KIND keeps only task folders of that kind (email or banner)."""
|
|
97
|
+
if not 1 <= n_trials <= 5:
|
|
98
|
+
raise typer.BadParameter("Trials must be 1 to 5")
|
|
99
|
+
if sum(bool(x) for x in (credentials_file, saved_login, prompt_credentials)) > 1:
|
|
100
|
+
raise typer.BadParameter("Choose only one login source")
|
|
101
|
+
if save_login and not (credentials_file or prompt_credentials):
|
|
102
|
+
raise typer.BadParameter("Saving a login requires credentials")
|
|
103
|
+
if share_login and not save_login:
|
|
104
|
+
raise typer.BadParameter("--share-login requires --save-login")
|
|
105
|
+
dirs = task_dirs(Path(target))
|
|
106
|
+
if kind:
|
|
107
|
+
if not dirs:
|
|
108
|
+
raise typer.BadParameter(
|
|
109
|
+
"--kind needs a task folder or a folder of task folders"
|
|
110
|
+
)
|
|
111
|
+
dirs = [d for d in dirs if task_kind(d) == kind]
|
|
112
|
+
if not dirs:
|
|
113
|
+
raise typer.BadParameter(f"No {kind} tasks in {target}")
|
|
114
|
+
task_ids = [_task_id(d) for d in dirs] if dirs else [target]
|
|
115
|
+
c = client()
|
|
116
|
+
tasks = []
|
|
117
|
+
for task_id in task_ids:
|
|
118
|
+
response = c.get(f"/api/v1/tasks/{task_id}")
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
tasks.append(response.json())
|
|
121
|
+
if not all(t.get("runner") == "browser" for t in tasks):
|
|
122
|
+
if len(tasks) > 1:
|
|
123
|
+
raise typer.BadParameter("Only browser tasks can run together")
|
|
124
|
+
if (
|
|
125
|
+
credentials_file
|
|
126
|
+
or url
|
|
127
|
+
or saved_login
|
|
128
|
+
or prompt_credentials
|
|
129
|
+
or save_login
|
|
130
|
+
or parent
|
|
131
|
+
):
|
|
132
|
+
raise typer.BadParameter(
|
|
133
|
+
"This task does not accept per-run browser credentials"
|
|
134
|
+
)
|
|
135
|
+
return _run_one(c, task_ids[0], n_trials, wait)
|
|
136
|
+
|
|
137
|
+
use_app = bool(
|
|
138
|
+
not credentials_file
|
|
139
|
+
or saved_login
|
|
140
|
+
or prompt_credentials
|
|
141
|
+
or save_login
|
|
142
|
+
or parent
|
|
143
|
+
)
|
|
144
|
+
values = _credentials(c, tasks, credentials_file) if credentials_file else {}
|
|
145
|
+
if prompt_credentials:
|
|
146
|
+
from rich.prompt import Prompt
|
|
147
|
+
|
|
148
|
+
values = {
|
|
149
|
+
"username": Prompt.ask("Username"),
|
|
150
|
+
"password": Prompt.ask("Password", password=True),
|
|
151
|
+
}
|
|
152
|
+
body = {"task_ids": task_ids, "n_trials": n_trials, "target_url": url or ""}
|
|
153
|
+
if use_app:
|
|
154
|
+
body.update(
|
|
155
|
+
login=saved_login or ("custom" if values else "default"),
|
|
156
|
+
**values,
|
|
157
|
+
save_login=save_login or "",
|
|
158
|
+
share_login=share_login,
|
|
159
|
+
parent_eval_id=parent or "",
|
|
160
|
+
)
|
|
161
|
+
with evals_client() as ec:
|
|
162
|
+
resp = ec.post("cli/evals", json=body)
|
|
163
|
+
else:
|
|
164
|
+
# Existing scripts using explicit files continue to work without a personal token.
|
|
165
|
+
body.update(
|
|
166
|
+
credentials=values,
|
|
167
|
+
source="cli",
|
|
168
|
+
created_by=_who(dirs[0] if dirs else Path.cwd()),
|
|
169
|
+
)
|
|
170
|
+
resp = c.post("/api/v1/evals", json=body)
|
|
171
|
+
values.clear()
|
|
172
|
+
body.clear()
|
|
173
|
+
resp.raise_for_status()
|
|
174
|
+
started = resp.json()
|
|
175
|
+
eval_id, runs = started["eval_id"], started["runs"]
|
|
176
|
+
plural = "s" if len(runs) != 1 else ""
|
|
177
|
+
console.print(
|
|
178
|
+
f"Eval run started: [bold]{eval_id}[/bold] ({len(runs)} task{plural})"
|
|
179
|
+
)
|
|
180
|
+
url = started.get("results_url")
|
|
181
|
+
if not wait:
|
|
182
|
+
if url:
|
|
183
|
+
console.print(f"Results: {url}")
|
|
184
|
+
return
|
|
185
|
+
if len(runs) == 1:
|
|
186
|
+
try:
|
|
187
|
+
_wait_run(c, runs[0]["run_id"])
|
|
188
|
+
finally:
|
|
189
|
+
if url:
|
|
190
|
+
console.print(f"Results: {url}")
|
|
191
|
+
return
|
|
192
|
+
_wait_eval(c, eval_id, url)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _run_one(c, task_id: str, n_trials: int, wait: bool) -> None:
|
|
196
|
+
"""One task of a hosted HTTP agent (not graded by the evals app)."""
|
|
197
|
+
resp = c.post("/api/v1/runs", json={"task_id": task_id, "n_trials": n_trials})
|
|
198
|
+
resp.raise_for_status()
|
|
199
|
+
run_id = resp.json()["run_id"]
|
|
200
|
+
console.print(f"Run started: [bold]{run_id}[/bold]")
|
|
201
|
+
if wait:
|
|
202
|
+
_wait_run(c, run_id)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _wait_run(c, run_id: str) -> None:
|
|
206
|
+
console.print("Waiting for results...")
|
|
207
|
+
shown = 0
|
|
208
|
+
while True:
|
|
209
|
+
time.sleep(3)
|
|
210
|
+
resp = c.get(f"/api/v1/runs/{run_id}")
|
|
211
|
+
resp.raise_for_status()
|
|
212
|
+
data = resp.json()
|
|
213
|
+
status = data["status"]
|
|
214
|
+
lines = progress_lines(data.get("progress", []))
|
|
215
|
+
for line in lines[shown:]:
|
|
216
|
+
console.print(line)
|
|
217
|
+
shown = len(lines)
|
|
218
|
+
|
|
219
|
+
if status == "completed":
|
|
220
|
+
lines, all_failed = summarize(data)
|
|
221
|
+
for line in lines:
|
|
222
|
+
console.print(line)
|
|
223
|
+
if all_failed:
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
break
|
|
226
|
+
elif status == "failed":
|
|
227
|
+
console.print("[red]Run failed.[/red]")
|
|
228
|
+
raise typer.Exit(1)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _wait_eval(c, eval_id: str, url: str | None) -> None:
|
|
232
|
+
"""Print each task as it finishes, then every task's result and where to see the scores."""
|
|
233
|
+
console.print("Waiting for results (each task is printed as it finishes)...")
|
|
234
|
+
done: set[str] = set()
|
|
235
|
+
while True:
|
|
236
|
+
time.sleep(10)
|
|
237
|
+
resp = c.get(f"/api/v1/evals/{eval_id}")
|
|
238
|
+
resp.raise_for_status()
|
|
239
|
+
data = resp.json()
|
|
240
|
+
for r in data["runs"]:
|
|
241
|
+
if r["status"] in ("completed", "failed") and r["id"] not in done:
|
|
242
|
+
done.add(r["id"])
|
|
243
|
+
console.print(eval_line(r))
|
|
244
|
+
if data["status"] in ("completed", "failed"):
|
|
245
|
+
break
|
|
246
|
+
ok = sum(r["status"] == "completed" for r in data["runs"])
|
|
247
|
+
console.print(f"Done. {ok} of {len(data['runs'])} tasks completed.")
|
|
248
|
+
if url:
|
|
249
|
+
console.print(f"Scores: {url}")
|
|
250
|
+
if ok == 0:
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def eval_line(r: dict) -> str:
|
|
255
|
+
name = r["task"].rsplit("/", 1)[-1]
|
|
256
|
+
if r["status"] == "failed":
|
|
257
|
+
return f" [red]{name}: failed[/red]"
|
|
258
|
+
reward = r.get("avg_reward")
|
|
259
|
+
return f" {name}: " + (
|
|
260
|
+
f"reward {reward:.2f}" if reward is not None else "no reward"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def summarize(data: dict) -> tuple[list[str], bool]:
|
|
265
|
+
"""Final lines for a completed run, and whether every trial failed."""
|
|
266
|
+
trials = data.get("trials", [])
|
|
267
|
+
n = len(trials)
|
|
268
|
+
failed = [t for t in trials if t.get("status") == "failed"]
|
|
269
|
+
plural = "s" if n != 1 else ""
|
|
270
|
+
avg = data.get("avg_reward")
|
|
271
|
+
head = f"Done. {n} trial{plural}" + (
|
|
272
|
+
f", avg reward = {avg:.2f}" if avg is not None else ""
|
|
273
|
+
)
|
|
274
|
+
if not failed:
|
|
275
|
+
return [head + "."], False
|
|
276
|
+
lines = [f"[red]{head}. {len(failed)} of {n} trial{plural} failed:[/red]"]
|
|
277
|
+
for t in failed:
|
|
278
|
+
why = t.get("error") or "no error recorded"
|
|
279
|
+
stage = f" (last stage: {t['last_stage']})" if t.get("last_stage") else ""
|
|
280
|
+
lines.append(f" {t['id']}: {why}{stage}")
|
|
281
|
+
return lines, len(failed) == n
|
|
@@ -116,7 +116,9 @@ def upload_browser(task_dir: Path, config: dict):
|
|
|
116
116
|
if path.is_symlink():
|
|
117
117
|
raise typer.BadParameter("Task files must not be symlinks")
|
|
118
118
|
info = zipfile.ZipInfo(path.relative_to(task_dir).as_posix())
|
|
119
|
-
info.external_attr = (
|
|
119
|
+
info.external_attr = (
|
|
120
|
+
path.stat().st_mode & 0o777
|
|
121
|
+
) << 16 # keep test.sh executable
|
|
120
122
|
archive.writestr(info, path.read_bytes(), zipfile.ZIP_DEFLATED)
|
|
121
123
|
if not (task_dir / "product.md").exists():
|
|
122
124
|
# Shared task context is packaged by the CLI, never resolved on the server.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def describe(response: httpx.Response) -> str:
|
|
7
|
+
"""The server's reason for an error response, as one line."""
|
|
8
|
+
try:
|
|
9
|
+
body = response.json()
|
|
10
|
+
except ValueError:
|
|
11
|
+
return f"{response.reason_phrase or 'Request failed'} from {response.url.host}"
|
|
12
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
13
|
+
if isinstance(detail, str) and detail:
|
|
14
|
+
return detail
|
|
15
|
+
if isinstance(detail, list) and detail:
|
|
16
|
+
parts = []
|
|
17
|
+
for item in detail:
|
|
18
|
+
if not isinstance(item, dict):
|
|
19
|
+
parts.append(str(item))
|
|
20
|
+
continue
|
|
21
|
+
loc = ".".join(str(p) for p in item.get("loc", []) if p != "body")
|
|
22
|
+
msg = item.get("msg", "invalid")
|
|
23
|
+
parts.append(f"{loc}: {msg}" if loc else msg)
|
|
24
|
+
return "; ".join(parts)
|
|
25
|
+
return response.reason_phrase or "Request failed"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def http_error_message(exc: httpx.HTTPStatusError) -> str:
|
|
29
|
+
response = exc.response
|
|
30
|
+
request = exc.request
|
|
31
|
+
message = (
|
|
32
|
+
f"Error: {describe(response)} "
|
|
33
|
+
f"(HTTP {response.status_code} on {request.method} {request.url.path})"
|
|
34
|
+
)
|
|
35
|
+
if response.status_code == 401:
|
|
36
|
+
message += "\nCheck your API token: newm login"
|
|
37
|
+
return message
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def transport_error_message(exc: httpx.TransportError) -> str:
|
|
41
|
+
try:
|
|
42
|
+
target = f"{exc.request.url.scheme}://{exc.request.url.host}"
|
|
43
|
+
except RuntimeError:
|
|
44
|
+
target = "the NewMeasure API"
|
|
45
|
+
return f"Error: couldn't reach {target} ({type(exc).__name__}: {exc})"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from cli.commands.admin import admin_app
|
|
11
|
+
from cli.commands.artifacts import artifacts as artifacts_cmd
|
|
12
|
+
from cli.commands.login import login as login_cmd
|
|
13
|
+
from cli.commands.result import result as result_cmd
|
|
14
|
+
from cli.commands.run import run as run_cmd
|
|
15
|
+
from cli.commands.runs import list_runs
|
|
16
|
+
from cli.commands.tasks import list_tasks
|
|
17
|
+
from cli.commands.upload import upload as upload_cmd
|
|
18
|
+
from cli.errors import http_error_message, transport_error_message
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="newm",
|
|
22
|
+
help="NewMeasure eval CLI",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
app.add_typer(admin_app, name="admin")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def login(
|
|
30
|
+
evals: bool = typer.Option(
|
|
31
|
+
False, "--evals", help="Connect your personal app account"
|
|
32
|
+
),
|
|
33
|
+
):
|
|
34
|
+
"""Store API token."""
|
|
35
|
+
login_cmd(evals)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def upload(task_dir: Path = typer.Argument(..., help="Path to task directory")):
|
|
40
|
+
"""Validate and upload a task."""
|
|
41
|
+
upload_cmd(task_dir)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def run(
|
|
46
|
+
target: str = typer.Argument(
|
|
47
|
+
...,
|
|
48
|
+
help="Task ID (t_...), a task folder, or a folder of task folders (e.g. tasks/)",
|
|
49
|
+
),
|
|
50
|
+
n_trials: int = typer.Option(
|
|
51
|
+
1, "--n-trials", min=1, max=5, help="Trials per task (1–5)"
|
|
52
|
+
),
|
|
53
|
+
credentials_file: Path | None = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--credentials-file",
|
|
56
|
+
help="Per-run application credentials (not uploaded as a file)",
|
|
57
|
+
),
|
|
58
|
+
url: str | None = typer.Option(None, "--url", help="HTTPS target URL for this run"),
|
|
59
|
+
saved_login: str | None = typer.Option(
|
|
60
|
+
None, "--login", help="Saved login name or ID"
|
|
61
|
+
),
|
|
62
|
+
prompt_credentials: bool = typer.Option(
|
|
63
|
+
False,
|
|
64
|
+
"--prompt-credentials",
|
|
65
|
+
help="Prompt for application username and password",
|
|
66
|
+
),
|
|
67
|
+
save_login: str | None = typer.Option(
|
|
68
|
+
None, "--save-login", help="Save the supplied login under this name"
|
|
69
|
+
),
|
|
70
|
+
share_login: bool = typer.Option(
|
|
71
|
+
False, "--share-login", help="Share the saved login with your organization"
|
|
72
|
+
),
|
|
73
|
+
parent: str | None = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--from-run",
|
|
76
|
+
help="Link to an original run; reuse its environment unless --url is set",
|
|
77
|
+
),
|
|
78
|
+
wait: bool = typer.Option(
|
|
79
|
+
True, "--wait/--no-wait", help="Wait for results after submission"
|
|
80
|
+
),
|
|
81
|
+
kind: str | None = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--kind",
|
|
84
|
+
help="Only tasks of this kind: email or banner (kind in task.toml under metadata.evals)",
|
|
85
|
+
),
|
|
86
|
+
):
|
|
87
|
+
"""Run an eval. Several browser tasks run together as one eval run."""
|
|
88
|
+
run_cmd(
|
|
89
|
+
target=target,
|
|
90
|
+
n_trials=n_trials,
|
|
91
|
+
credentials_file=credentials_file,
|
|
92
|
+
wait=wait,
|
|
93
|
+
kind=kind,
|
|
94
|
+
url=url,
|
|
95
|
+
saved_login=saved_login,
|
|
96
|
+
prompt_credentials=prompt_credentials,
|
|
97
|
+
save_login=save_login,
|
|
98
|
+
share_login=share_login,
|
|
99
|
+
parent=parent,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def artifacts(
|
|
105
|
+
run_id: str,
|
|
106
|
+
trial_id: str,
|
|
107
|
+
output: Path | None = typer.Option(
|
|
108
|
+
None, "--output", help="Download artifacts to this directory"
|
|
109
|
+
),
|
|
110
|
+
):
|
|
111
|
+
"""List or download a trial's stored artifacts."""
|
|
112
|
+
artifacts_cmd(run_id, trial_id, output)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def tasks():
|
|
117
|
+
"""List uploaded tasks."""
|
|
118
|
+
list_tasks()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command()
|
|
122
|
+
def runs(task_id: str | None = typer.Argument(None, help="Filter by task ID")):
|
|
123
|
+
"""List runs."""
|
|
124
|
+
list_runs(task_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def result(run_id: str = typer.Argument(..., help="Run ID")):
|
|
129
|
+
"""Get detailed run results."""
|
|
130
|
+
result_cmd(run_id)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command(
|
|
134
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
135
|
+
)
|
|
136
|
+
def harbor(ctx: typer.Context):
|
|
137
|
+
"""Passthrough to Harbor CLI (power users)."""
|
|
138
|
+
cmd = ["harbor", *ctx.args]
|
|
139
|
+
sys.exit(subprocess.call(cmd))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> None:
|
|
143
|
+
"""Entry point: show the server's reason for a failed request, not a traceback."""
|
|
144
|
+
try:
|
|
145
|
+
app()
|
|
146
|
+
except httpx.HTTPStatusError as exc:
|
|
147
|
+
typer.secho(http_error_message(exc), err=True, fg=typer.colors.RED)
|
|
148
|
+
raise SystemExit(1) from None
|
|
149
|
+
except httpx.TransportError as exc:
|
|
150
|
+
typer.secho(transport_error_message(exc), err=True, fg=typer.colors.RED)
|
|
151
|
+
raise SystemExit(1) from None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command("logins")
|
|
155
|
+
def logins():
|
|
156
|
+
"""List the same personal and shared logins as the app."""
|
|
157
|
+
from cli.api_client import evals_client
|
|
158
|
+
|
|
159
|
+
with evals_client() as c:
|
|
160
|
+
r = c.get("run-logins")
|
|
161
|
+
r.raise_for_status()
|
|
162
|
+
for entry in r.json()["logins"]:
|
|
163
|
+
typer.echo(
|
|
164
|
+
f"{entry['id']} {entry['name']} {'Organization' if entry['shared'] else 'Personal'} Saved by {entry['saved_by']}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "newmeasure-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "NewMeasure eval CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -25,4 +25,4 @@ packages = ["cli"]
|
|
|
25
25
|
include = ["/cli", "/README.md", "/pyproject.toml"]
|
|
26
26
|
|
|
27
27
|
[project.scripts]
|
|
28
|
-
newm = "cli.main:
|
|
28
|
+
newm = "cli.main:main"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
import typer
|
|
5
|
-
|
|
6
|
-
from cli.config import get
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def client() -> httpx.Client:
|
|
10
|
-
api_url = get("api_url")
|
|
11
|
-
token = get("token")
|
|
12
|
-
if not token:
|
|
13
|
-
typer.echo("Not logged in. Run: newm login")
|
|
14
|
-
raise typer.Exit(1)
|
|
15
|
-
return httpx.Client(
|
|
16
|
-
base_url=api_url,
|
|
17
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
18
|
-
timeout=300,
|
|
19
|
-
)
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from urllib.parse import urlparse
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
from dotenv import dotenv_values
|
|
9
|
-
from rich.console import Console
|
|
10
|
-
|
|
11
|
-
from cli.api_client import client
|
|
12
|
-
from cli.progress import progress_lines
|
|
13
|
-
from cli.commands.upload import upload
|
|
14
|
-
|
|
15
|
-
console = Console()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def run(
|
|
19
|
-
target: str,
|
|
20
|
-
n_trials: int = 1,
|
|
21
|
-
credentials_file: Path | None = None,
|
|
22
|
-
wait: bool = True,
|
|
23
|
-
):
|
|
24
|
-
"""Run an eval. TARGET is a task ID (t_...) or a local task directory path."""
|
|
25
|
-
task_id = target
|
|
26
|
-
target_path = Path(target)
|
|
27
|
-
if target_path.is_dir():
|
|
28
|
-
upload(target_path)
|
|
29
|
-
import tomllib
|
|
30
|
-
|
|
31
|
-
toml_text = (target_path / "task.toml").read_text()
|
|
32
|
-
task_id = tomllib.loads(toml_text).get("task_id")
|
|
33
|
-
if not task_id:
|
|
34
|
-
typer.echo("Failed to get task_id after upload")
|
|
35
|
-
raise typer.Exit(1)
|
|
36
|
-
|
|
37
|
-
c = client()
|
|
38
|
-
body = {
|
|
39
|
-
"task_id": task_id,
|
|
40
|
-
"n_trials": n_trials,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
task_response = c.get(f"/api/v1/tasks/{task_id}")
|
|
44
|
-
task_response.raise_for_status()
|
|
45
|
-
task = task_response.json()
|
|
46
|
-
if task.get("runner") == "browser":
|
|
47
|
-
url = urlparse(str(c.base_url))
|
|
48
|
-
if url.scheme != "https" and url.hostname not in {
|
|
49
|
-
"127.0.0.1",
|
|
50
|
-
"localhost",
|
|
51
|
-
"::1",
|
|
52
|
-
}:
|
|
53
|
-
raise typer.BadParameter(
|
|
54
|
-
"Application credentials require an HTTPS NewMeasure API URL"
|
|
55
|
-
)
|
|
56
|
-
if credentials_file is None or not credentials_file.is_file():
|
|
57
|
-
raise typer.BadParameter(
|
|
58
|
-
"Browser runs require --credentials-file pointing to a local environment file"
|
|
59
|
-
)
|
|
60
|
-
values = dotenv_values(credentials_file, interpolate=False)
|
|
61
|
-
fields = task.get("credential_fields", [])
|
|
62
|
-
if any(not values.get(field) for field in fields):
|
|
63
|
-
raise typer.BadParameter(
|
|
64
|
-
f"Credential file must define: {', '.join(fields)}"
|
|
65
|
-
)
|
|
66
|
-
body["credentials"] = {field: values[field] for field in fields}
|
|
67
|
-
elif credentials_file:
|
|
68
|
-
raise typer.BadParameter(
|
|
69
|
-
"This task does not accept per-run browser credentials"
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
resp = c.post("/api/v1/runs", json=body)
|
|
73
|
-
body.pop("credentials", None)
|
|
74
|
-
resp.raise_for_status()
|
|
75
|
-
run_id = resp.json()["run_id"]
|
|
76
|
-
console.print(f"Run started: [bold]{run_id}[/bold]")
|
|
77
|
-
if not wait:
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
console.print("Waiting for results...")
|
|
81
|
-
shown = 0
|
|
82
|
-
while True:
|
|
83
|
-
time.sleep(3)
|
|
84
|
-
resp = c.get(f"/api/v1/runs/{run_id}")
|
|
85
|
-
resp.raise_for_status()
|
|
86
|
-
data = resp.json()
|
|
87
|
-
status = data["status"]
|
|
88
|
-
lines = progress_lines(data.get("progress", []))
|
|
89
|
-
for line in lines[shown:]:
|
|
90
|
-
console.print(line)
|
|
91
|
-
shown = len(lines)
|
|
92
|
-
|
|
93
|
-
if status == "completed":
|
|
94
|
-
lines, all_failed = summarize(data)
|
|
95
|
-
for line in lines:
|
|
96
|
-
console.print(line)
|
|
97
|
-
if all_failed:
|
|
98
|
-
raise typer.Exit(1)
|
|
99
|
-
break
|
|
100
|
-
elif status == "failed":
|
|
101
|
-
console.print("[red]Run failed.[/red]")
|
|
102
|
-
raise typer.Exit(1)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def summarize(data: dict) -> tuple[list[str], bool]:
|
|
106
|
-
"""Final lines for a completed run, and whether every trial failed."""
|
|
107
|
-
trials = data.get("trials", [])
|
|
108
|
-
n = len(trials)
|
|
109
|
-
failed = [t for t in trials if t.get("status") == "failed"]
|
|
110
|
-
plural = "s" if n != 1 else ""
|
|
111
|
-
avg = data.get("avg_reward")
|
|
112
|
-
head = f"Done. {n} trial{plural}" + (f", avg reward = {avg:.2f}" if avg is not None else "")
|
|
113
|
-
if not failed:
|
|
114
|
-
return [head + "."], False
|
|
115
|
-
lines = [f"[red]{head}. {len(failed)} of {n} trial{plural} failed:[/red]"]
|
|
116
|
-
for t in failed:
|
|
117
|
-
why = t.get("error") or "no error recorded"
|
|
118
|
-
stage = f" (last stage: {t['last_stage']})" if t.get("last_stage") else ""
|
|
119
|
-
lines.append(f" {t['id']}: {why}{stage}")
|
|
120
|
-
return lines, len(failed) == n
|
newmeasure_cli-0.2.0/cli/main.py
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
import typer
|
|
9
|
-
|
|
10
|
-
from cli.commands.admin import admin_app
|
|
11
|
-
from cli.commands.artifacts import artifacts as artifacts_cmd
|
|
12
|
-
from cli.commands.login import login as login_cmd
|
|
13
|
-
from cli.commands.result import result as result_cmd
|
|
14
|
-
from cli.commands.run import run as run_cmd
|
|
15
|
-
from cli.commands.runs import list_runs
|
|
16
|
-
from cli.commands.tasks import list_tasks
|
|
17
|
-
from cli.commands.upload import upload as upload_cmd
|
|
18
|
-
|
|
19
|
-
app = typer.Typer(
|
|
20
|
-
name="newm",
|
|
21
|
-
help="NewMeasure eval CLI",
|
|
22
|
-
no_args_is_help=True,
|
|
23
|
-
)
|
|
24
|
-
app.add_typer(admin_app, name="admin")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@app.command()
|
|
28
|
-
def login():
|
|
29
|
-
"""Store API token."""
|
|
30
|
-
login_cmd()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@app.command()
|
|
34
|
-
def upload(task_dir: Path = typer.Argument(..., help="Path to task directory")):
|
|
35
|
-
"""Validate and upload a task."""
|
|
36
|
-
upload_cmd(task_dir)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@app.command()
|
|
40
|
-
def run(
|
|
41
|
-
target: str = typer.Argument(..., help="Task ID (t_...) or local task directory"),
|
|
42
|
-
n_trials: int = typer.Option(1, "--n-trials", help="Number of trials"),
|
|
43
|
-
credentials_file: Optional[Path] = typer.Option(
|
|
44
|
-
None,
|
|
45
|
-
"--credentials-file",
|
|
46
|
-
help="Per-run application credentials (not uploaded as a file)",
|
|
47
|
-
),
|
|
48
|
-
wait: bool = typer.Option(
|
|
49
|
-
True, "--wait/--no-wait", help="Wait for results after submission"
|
|
50
|
-
),
|
|
51
|
-
):
|
|
52
|
-
"""Run an eval against your agent."""
|
|
53
|
-
run_cmd(
|
|
54
|
-
target=target, n_trials=n_trials, credentials_file=credentials_file, wait=wait
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@app.command()
|
|
59
|
-
def artifacts(
|
|
60
|
-
run_id: str,
|
|
61
|
-
trial_id: str,
|
|
62
|
-
output: Optional[Path] = typer.Option(
|
|
63
|
-
None, "--output", help="Download artifacts to this directory"
|
|
64
|
-
),
|
|
65
|
-
):
|
|
66
|
-
"""List or download a trial's stored artifacts."""
|
|
67
|
-
artifacts_cmd(run_id, trial_id, output)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@app.command()
|
|
71
|
-
def tasks():
|
|
72
|
-
"""List uploaded tasks."""
|
|
73
|
-
list_tasks()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@app.command()
|
|
77
|
-
def runs(task_id: Optional[str] = typer.Argument(None, help="Filter by task ID")):
|
|
78
|
-
"""List runs."""
|
|
79
|
-
list_runs(task_id)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@app.command()
|
|
83
|
-
def result(run_id: str = typer.Argument(..., help="Run ID")):
|
|
84
|
-
"""Get detailed run results."""
|
|
85
|
-
result_cmd(run_id)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@app.command(
|
|
89
|
-
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
90
|
-
)
|
|
91
|
-
def harbor(ctx: typer.Context):
|
|
92
|
-
"""Passthrough to Harbor CLI (power users)."""
|
|
93
|
-
cmd = ["harbor", *ctx.args]
|
|
94
|
-
sys.exit(subprocess.call(cmd))
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if __name__ == "__main__":
|
|
98
|
-
app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|