haliosai-cli 2.0.4__tar.gz → 2.0.6__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.
- {haliosai_cli-2.0.4/haliosai_cli.egg-info → haliosai_cli-2.0.6}/PKG-INFO +12 -5
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/README.md +11 -4
- haliosai_cli-2.0.6/halios_cli/_version.py +1 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_eval.py +32 -3
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_optimize.py +45 -8
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_project.py +39 -9
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_support.py +83 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_trace.py +25 -5
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6/haliosai_cli.egg-info}/PKG-INFO +12 -5
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/tests/test_cli_surface.py +92 -2
- haliosai_cli-2.0.4/halios_cli/_version.py +0 -1
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/LICENSE +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/MANIFEST.in +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/__init__.py +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli.py +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_auth.py +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/cli_scenario.py +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/py.typed +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/schemas/__init__.py +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/schemas/eval.schema.json +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/halios_cli/schemas/scenarios.schema.json +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/haliosai_cli.egg-info/SOURCES.txt +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/haliosai_cli.egg-info/dependency_links.txt +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/haliosai_cli.egg-info/entry_points.txt +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/haliosai_cli.egg-info/requires.txt +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/haliosai_cli.egg-info/top_level.txt +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/pyproject.toml +0 -0
- {haliosai_cli-2.0.4 → haliosai_cli-2.0.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haliosai-cli
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.6
|
|
4
4
|
Summary: Halios CLI for coding-agent evaluations, guardrails, and OpenTelemetry evidence
|
|
5
5
|
Author-email: HaliosLabs <support@halios.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -65,10 +65,13 @@ npx skills add HaliosAI/halios --skill halios
|
|
|
65
65
|
Prompt your agent to set up evaluations for your project:
|
|
66
66
|
|
|
67
67
|
```text
|
|
68
|
-
"Set up evals for this agent: inspect the repository, create realistic test scenarios and checks, and run
|
|
68
|
+
"Set up evals for this agent: inspect the repository, create realistic test scenarios and checks, run one smoke test, and summarize before asking whether to run the full suite."
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Your agent will inspect your application entrypoint, configure standard OpenTelemetry, draft
|
|
71
|
+
Your agent will inspect your application entrypoint, configure standard OpenTelemetry, draft
|
|
72
|
+
scenarios in `.halios/`, and run one smoke scenario. It stops with a verification summary before
|
|
73
|
+
asking whether to run the full suite and how many trials per scenario to use. The summary includes
|
|
74
|
+
direct Halios UI links for reviewing the materialized suite, smoke run, and trace evidence.
|
|
72
75
|
|
|
73
76
|
---
|
|
74
77
|
|
|
@@ -78,15 +81,19 @@ If you prefer to drive evaluations directly from the command line or CI:
|
|
|
78
81
|
|
|
79
82
|
```bash
|
|
80
83
|
# Recommended: Install with uv tool
|
|
81
|
-
uv tool install 'haliosai-cli>=2.0.
|
|
84
|
+
uv tool install 'haliosai-cli>=2.0.6'
|
|
82
85
|
|
|
83
86
|
# Or install with pipx
|
|
84
|
-
pipx install 'haliosai-cli>=2.0.
|
|
87
|
+
pipx install 'haliosai-cli>=2.0.6'
|
|
85
88
|
|
|
86
89
|
# See available commands and usage
|
|
87
90
|
halios --help
|
|
88
91
|
```
|
|
89
92
|
|
|
93
|
+
The API and UI use the same origin by default. For a self-hosted deployment with separate origins,
|
|
94
|
+
set `HALIOS_UI_URL` so structured review links point to the web application without changing the
|
|
95
|
+
API endpoint used by `HALIOS_BASE_URL`.
|
|
96
|
+
|
|
90
97
|
---
|
|
91
98
|
|
|
92
99
|
## How It Works
|
|
@@ -28,10 +28,13 @@ npx skills add HaliosAI/halios --skill halios
|
|
|
28
28
|
Prompt your agent to set up evaluations for your project:
|
|
29
29
|
|
|
30
30
|
```text
|
|
31
|
-
"Set up evals for this agent: inspect the repository, create realistic test scenarios and checks, and run
|
|
31
|
+
"Set up evals for this agent: inspect the repository, create realistic test scenarios and checks, run one smoke test, and summarize before asking whether to run the full suite."
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Your agent will inspect your application entrypoint, configure standard OpenTelemetry, draft
|
|
34
|
+
Your agent will inspect your application entrypoint, configure standard OpenTelemetry, draft
|
|
35
|
+
scenarios in `.halios/`, and run one smoke scenario. It stops with a verification summary before
|
|
36
|
+
asking whether to run the full suite and how many trials per scenario to use. The summary includes
|
|
37
|
+
direct Halios UI links for reviewing the materialized suite, smoke run, and trace evidence.
|
|
35
38
|
|
|
36
39
|
---
|
|
37
40
|
|
|
@@ -41,15 +44,19 @@ If you prefer to drive evaluations directly from the command line or CI:
|
|
|
41
44
|
|
|
42
45
|
```bash
|
|
43
46
|
# Recommended: Install with uv tool
|
|
44
|
-
uv tool install 'haliosai-cli>=2.0.
|
|
47
|
+
uv tool install 'haliosai-cli>=2.0.6'
|
|
45
48
|
|
|
46
49
|
# Or install with pipx
|
|
47
|
-
pipx install 'haliosai-cli>=2.0.
|
|
50
|
+
pipx install 'haliosai-cli>=2.0.6'
|
|
48
51
|
|
|
49
52
|
# See available commands and usage
|
|
50
53
|
halios --help
|
|
51
54
|
```
|
|
52
55
|
|
|
56
|
+
The API and UI use the same origin by default. For a self-hosted deployment with separate origins,
|
|
57
|
+
set `HALIOS_UI_URL` so structured review links point to the web application without changing the
|
|
58
|
+
API endpoint used by `HALIOS_BASE_URL`.
|
|
59
|
+
|
|
53
60
|
---
|
|
54
61
|
|
|
55
62
|
## How It Works
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.6"
|
|
@@ -24,8 +24,10 @@ from jsonschema import Draft202012Validator
|
|
|
24
24
|
|
|
25
25
|
from .cli_support import (
|
|
26
26
|
ApiClient,
|
|
27
|
+
emit_review_links,
|
|
27
28
|
evaluation_suite_digest,
|
|
28
29
|
git_provenance,
|
|
30
|
+
halios_ui_links,
|
|
29
31
|
load_project_config,
|
|
30
32
|
load_yaml,
|
|
31
33
|
resolve_credentials,
|
|
@@ -354,8 +356,8 @@ def _eval_quality_gaps(eval_plan: dict[str, Any], scenarios: list[dict[str, Any]
|
|
|
354
356
|
):
|
|
355
357
|
gaps.append(
|
|
356
358
|
f"Scenario '{scenario_id}': simulator_context is empty for a multi-turn scenario; "
|
|
357
|
-
"populate private user facts/preferences/state in simulator_context so the
|
|
358
|
-
"can reveal them dynamically upon agent request"
|
|
359
|
+
"populate private user facts/preferences/state in simulator_context so the "
|
|
360
|
+
"simulator can reveal them dynamically upon agent request"
|
|
359
361
|
)
|
|
360
362
|
return list(dict.fromkeys(gaps))
|
|
361
363
|
|
|
@@ -801,9 +803,28 @@ def _raise_for_failed_run(report: dict[str, Any], run_id: str) -> None:
|
|
|
801
803
|
message = str(error.get("message") or trial.get("outcome") or trial.get("state"))
|
|
802
804
|
details.append(f"{trial.get('scenario_id') or trial.get('id')}: {message}")
|
|
803
805
|
suffix = f" ({'; '.join(details)})" if details else ""
|
|
806
|
+
run_url = str((report.get("links") or {}).get("evaluation_run") or "")
|
|
807
|
+
review = f" Review in Halios: {run_url}." if run_url else ""
|
|
804
808
|
raise typer.BadParameter(
|
|
805
809
|
f"Evaluation run {run_id} failed{suffix}. "
|
|
806
|
-
f"Inspect `halios eval report {run_id} --failures --json`."
|
|
810
|
+
f"Inspect `halios eval report {run_id} --failures --json`.{review}"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _representative_trace_id(report: dict[str, Any]) -> str | None:
|
|
815
|
+
for trial in report.get("trials") or []:
|
|
816
|
+
if isinstance(trial, dict) and trial.get("trace_id"):
|
|
817
|
+
return str(trial["trace_id"])
|
|
818
|
+
return None
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _evaluation_links(*, base_url: str, agent_id: str, report: dict[str, Any]) -> dict[str, str]:
|
|
822
|
+
return halios_ui_links(
|
|
823
|
+
base_url,
|
|
824
|
+
agent_id,
|
|
825
|
+
include_evaluations=True,
|
|
826
|
+
run_tag=str(report.get("run_tag") or "") or None,
|
|
827
|
+
trace_id=_representative_trace_id(report),
|
|
807
828
|
)
|
|
808
829
|
|
|
809
830
|
|
|
@@ -1028,6 +1049,9 @@ def run(
|
|
|
1028
1049
|
f"Evaluation run {run_id} did not finish within {timeout}s. "
|
|
1029
1050
|
f"Inspect `halios eval report {run_id} --failures --json`."
|
|
1030
1051
|
)
|
|
1052
|
+
report["links"] = _evaluation_links(
|
|
1053
|
+
base_url=credentials.ui_base_url, agent_id=agent_id, report=report
|
|
1054
|
+
)
|
|
1031
1055
|
_raise_for_failed_run(report, run_id)
|
|
1032
1056
|
if expected_roots:
|
|
1033
1057
|
report["telemetry_verification"] = _verify_simulation_telemetry(
|
|
@@ -1047,6 +1071,7 @@ def run(
|
|
|
1047
1071
|
if telemetry.get("verified"):
|
|
1048
1072
|
typer.echo(f"Telemetry: verified ({telemetry['trace_count']} traces)")
|
|
1049
1073
|
typer.echo(f"Run {run_id}: pass@{k}={pass_at_k:.1%} gate={gate}")
|
|
1074
|
+
emit_review_links(report["links"])
|
|
1050
1075
|
if not report.get("gate_passed"):
|
|
1051
1076
|
raise typer.Exit(2)
|
|
1052
1077
|
|
|
@@ -1072,6 +1097,9 @@ def report(
|
|
|
1072
1097
|
**result,
|
|
1073
1098
|
"trials": [item for item in result.get("trials", []) if not item.get("passed")],
|
|
1074
1099
|
}
|
|
1100
|
+
result["links"] = _evaluation_links(
|
|
1101
|
+
base_url=credentials.ui_base_url, agent_id=agent_id, report=result
|
|
1102
|
+
)
|
|
1075
1103
|
if json_output:
|
|
1076
1104
|
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
1077
1105
|
else:
|
|
@@ -1086,3 +1114,4 @@ def report(
|
|
|
1086
1114
|
f"trial_failures={int(result.get('evaluation_failed_count') or 0)} "
|
|
1087
1115
|
f"revision={result.get('report_revision')}{delta}"
|
|
1088
1116
|
)
|
|
1117
|
+
emit_review_links(result["links"])
|
|
@@ -8,7 +8,14 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
-
from .cli_support import
|
|
11
|
+
from .cli_support import (
|
|
12
|
+
ApiClient,
|
|
13
|
+
atomic_write_text,
|
|
14
|
+
emit_review_links,
|
|
15
|
+
halios_ui_links,
|
|
16
|
+
load_project_config,
|
|
17
|
+
resolve_credentials,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
app = typer.Typer(
|
|
14
21
|
help="Guide, record, and verify coding-agent prompt optimization.",
|
|
@@ -27,7 +34,16 @@ def _emit(value: dict[str, Any], json_output: bool) -> None:
|
|
|
27
34
|
if json_output:
|
|
28
35
|
typer.echo(json.dumps(value, indent=2, sort_keys=True, default=str))
|
|
29
36
|
else:
|
|
30
|
-
|
|
37
|
+
links = value.get("links")
|
|
38
|
+
typer.echo(
|
|
39
|
+
json.dumps(
|
|
40
|
+
{key: item for key, item in value.items() if key != "links"},
|
|
41
|
+
indent=2,
|
|
42
|
+
default=str,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
if isinstance(links, dict):
|
|
46
|
+
emit_review_links(links)
|
|
31
47
|
|
|
32
48
|
|
|
33
49
|
def _resolve_baseline(api: ApiClient, agent_id: str, explicit_run_id: str | None) -> dict[str, Any]:
|
|
@@ -149,6 +165,12 @@ def start(
|
|
|
149
165
|
f"`halios optimize record {run_id} --evaluation-run <run-id> "
|
|
150
166
|
f"--prompt-file {prompt_file}`."
|
|
151
167
|
),
|
|
168
|
+
"links": halios_ui_links(
|
|
169
|
+
credentials.ui_base_url,
|
|
170
|
+
agent_id,
|
|
171
|
+
run_tag=str(baseline.get("run_tag") or "") or None,
|
|
172
|
+
optimization_run_id=run_id,
|
|
173
|
+
),
|
|
152
174
|
}
|
|
153
175
|
_emit(result, json_output)
|
|
154
176
|
|
|
@@ -156,9 +178,10 @@ def start(
|
|
|
156
178
|
@app.command("guidance")
|
|
157
179
|
def guidance(run_id: str, json_output: bool = typer.Option(False, "--json")) -> None:
|
|
158
180
|
"""Return the next bounded edit contract and negative memory for a coding agent."""
|
|
159
|
-
|
|
181
|
+
agent_id, credentials = _context()
|
|
160
182
|
with ApiClient(credentials) as api:
|
|
161
183
|
result = api.request("POST", f"/api/v1/optimization-runs/{run_id}/next-action")
|
|
184
|
+
result["links"] = halios_ui_links(credentials.ui_base_url, agent_id, optimization_run_id=run_id)
|
|
162
185
|
_emit(result, json_output)
|
|
163
186
|
|
|
164
187
|
|
|
@@ -170,7 +193,7 @@ def record(
|
|
|
170
193
|
json_output: bool = typer.Option(False, "--json"),
|
|
171
194
|
) -> None:
|
|
172
195
|
"""Record one candidate using evidence from the unchanged canonical eval suite."""
|
|
173
|
-
|
|
196
|
+
agent_id, credentials = _context()
|
|
174
197
|
prompt_after = prompt_file.read_text(encoding="utf-8")
|
|
175
198
|
with ApiClient(credentials) as api:
|
|
176
199
|
run = api.request("GET", f"/api/v1/optimization-runs/{run_id}")
|
|
@@ -229,6 +252,12 @@ def record(
|
|
|
229
252
|
if accepted
|
|
230
253
|
else "Revert the rejected prompt edit, inspect next_action, and try one different edit."
|
|
231
254
|
),
|
|
255
|
+
"links": halios_ui_links(
|
|
256
|
+
credentials.ui_base_url,
|
|
257
|
+
agent_id,
|
|
258
|
+
run_tag=str(report.get("run_tag") or "") or None,
|
|
259
|
+
optimization_run_id=run_id,
|
|
260
|
+
),
|
|
232
261
|
}
|
|
233
262
|
_emit(result, json_output)
|
|
234
263
|
if not accepted:
|
|
@@ -242,7 +271,7 @@ def apply_candidate(
|
|
|
242
271
|
json_output: bool = typer.Option(False, "--json"),
|
|
243
272
|
) -> None:
|
|
244
273
|
"""Retrieve one backend-approved prompt candidate for repository application."""
|
|
245
|
-
|
|
274
|
+
agent_id, credentials = _context()
|
|
246
275
|
with ApiClient(credentials) as api:
|
|
247
276
|
handoff = api.request(
|
|
248
277
|
"POST",
|
|
@@ -257,6 +286,11 @@ def apply_candidate(
|
|
|
257
286
|
"Run `halios eval run --json` after applying the prompt, then use "
|
|
258
287
|
f"`halios optimize verify {handoff['optimization_run_id']} --evaluation-run <run-id>`."
|
|
259
288
|
)
|
|
289
|
+
handoff["links"] = halios_ui_links(
|
|
290
|
+
credentials.ui_base_url,
|
|
291
|
+
agent_id,
|
|
292
|
+
optimization_run_id=str(handoff["optimization_run_id"]),
|
|
293
|
+
)
|
|
260
294
|
if json_output:
|
|
261
295
|
_emit(handoff, True)
|
|
262
296
|
elif output:
|
|
@@ -272,13 +306,14 @@ def verify_candidate(
|
|
|
272
306
|
json_output: bool = typer.Option(False, "--json"),
|
|
273
307
|
) -> None:
|
|
274
308
|
"""Verify the applied candidate against the frozen baseline and unchanged suite."""
|
|
275
|
-
|
|
309
|
+
agent_id, credentials = _context()
|
|
276
310
|
with ApiClient(credentials) as api:
|
|
277
311
|
result = api.request(
|
|
278
312
|
"POST",
|
|
279
313
|
f"/api/v1/optimization-runs/{run_id}/verify",
|
|
280
314
|
json={"evaluation_run_id": evaluation_run_id},
|
|
281
315
|
)
|
|
316
|
+
result["links"] = halios_ui_links(credentials.ui_base_url, agent_id, optimization_run_id=run_id)
|
|
282
317
|
_emit(result, json_output)
|
|
283
318
|
if not result.get("passed"):
|
|
284
319
|
raise typer.Exit(2)
|
|
@@ -294,15 +329,17 @@ def list_runs(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
|
294
329
|
|
|
295
330
|
@app.command("status")
|
|
296
331
|
def status(run_id: str, json_output: bool = typer.Option(False, "--json")) -> None:
|
|
297
|
-
|
|
332
|
+
agent_id, credentials = _context()
|
|
298
333
|
with ApiClient(credentials) as api:
|
|
299
334
|
result = api.request("GET", f"/api/v1/optimization-runs/{run_id}")
|
|
335
|
+
result["links"] = halios_ui_links(credentials.ui_base_url, agent_id, optimization_run_id=run_id)
|
|
300
336
|
_emit(result, json_output)
|
|
301
337
|
|
|
302
338
|
|
|
303
339
|
@app.command("cancel")
|
|
304
340
|
def cancel(run_id: str, json_output: bool = typer.Option(False, "--json")) -> None:
|
|
305
|
-
|
|
341
|
+
agent_id, credentials = _context()
|
|
306
342
|
with ApiClient(credentials) as api:
|
|
307
343
|
result = api.request("POST", f"/api/v1/optimization-runs/{run_id}/cancel")
|
|
344
|
+
result["links"] = halios_ui_links(credentials.ui_base_url, agent_id, optimization_run_id=run_id)
|
|
308
345
|
_emit(result, json_output)
|
|
@@ -17,8 +17,10 @@ import typer
|
|
|
17
17
|
from .cli_support import (
|
|
18
18
|
ApiClient,
|
|
19
19
|
ApiError,
|
|
20
|
+
emit_review_links,
|
|
20
21
|
evaluation_suite_digest,
|
|
21
22
|
git_provenance,
|
|
23
|
+
halios_ui_links,
|
|
22
24
|
load_project_config,
|
|
23
25
|
load_yaml,
|
|
24
26
|
preserve_suite_recovery,
|
|
@@ -92,6 +94,7 @@ def init(
|
|
|
92
94
|
command: str = typer.Option(
|
|
93
95
|
"", "--command", help="Project adapter command; may be added later by the coding agent."
|
|
94
96
|
),
|
|
97
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
95
98
|
) -> None:
|
|
96
99
|
"""Create a fresh agent, or explicitly link one by UUID, and initialize the checkout."""
|
|
97
100
|
root = pathlib.Path.cwd().resolve()
|
|
@@ -167,16 +170,37 @@ def init(
|
|
|
167
170
|
created.append(".halios/scenarios.yml")
|
|
168
171
|
|
|
169
172
|
verb = "Created" if created_agent else "Linked"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
links = halios_ui_links(credentials.ui_base_url, str(resolved_agent["id"]))
|
|
174
|
+
result = {
|
|
175
|
+
"created": created_agent,
|
|
176
|
+
"agent": {
|
|
177
|
+
"id": str(resolved_agent["id"]),
|
|
178
|
+
"name": str(resolved_agent["name"]),
|
|
179
|
+
"slug": str(resolved_agent["slug"]),
|
|
180
|
+
},
|
|
181
|
+
"suite": {
|
|
182
|
+
"revision": int(suite.get("revision") or 0),
|
|
183
|
+
"check_count": len((suite.get("eval") or {}).get("checks") or []),
|
|
184
|
+
"scenario_count": len((suite.get("scenarios") or {}).get("scenarios") or []),
|
|
185
|
+
},
|
|
186
|
+
"created_files": created,
|
|
187
|
+
"links": links,
|
|
188
|
+
}
|
|
189
|
+
if json_output:
|
|
190
|
+
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
191
|
+
else:
|
|
192
|
+
typer.echo(f"{verb} Halios agent: {resolved_agent['name']}")
|
|
193
|
+
typer.echo(f"Agent ID: {resolved_agent['id']}")
|
|
194
|
+
if not created_agent:
|
|
195
|
+
typer.echo(
|
|
196
|
+
f"Existing evaluation suite: revision {suite['revision']}, "
|
|
197
|
+
f"{result['suite']['check_count']} checks, "
|
|
198
|
+
f"{result['suite']['scenario_count']} scenarios"
|
|
199
|
+
)
|
|
174
200
|
typer.echo(
|
|
175
|
-
|
|
176
|
-
f"{len((suite.get('eval') or {}).get('checks') or [])} checks, "
|
|
177
|
-
f"{len((suite.get('scenarios') or {}).get('scenarios') or [])} scenarios"
|
|
201
|
+
"Created: " + ", ".join(created) if created else "Project was already initialized."
|
|
178
202
|
)
|
|
179
|
-
|
|
203
|
+
emit_review_links(links)
|
|
180
204
|
|
|
181
205
|
|
|
182
206
|
def _apply_suite_response(root: pathlib.Path, response: dict[str, Any]) -> None:
|
|
@@ -244,6 +268,7 @@ def configure(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
|
244
268
|
"revision": response["revision"],
|
|
245
269
|
"digest": response.get("digest"),
|
|
246
270
|
"verification": verification,
|
|
271
|
+
"links": halios_ui_links(credentials.ui_base_url, agent_id, include_suite=True),
|
|
247
272
|
}
|
|
248
273
|
if json_output:
|
|
249
274
|
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
@@ -253,6 +278,7 @@ def configure(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
|
253
278
|
f"{verification['check_count']} checks, {verification['rule_count']} rules, "
|
|
254
279
|
f"{verification['rubric_count']} rubrics, {verification['scenario_count']} scenarios"
|
|
255
280
|
)
|
|
281
|
+
emit_review_links(result["links"])
|
|
256
282
|
|
|
257
283
|
|
|
258
284
|
@app.command("refresh")
|
|
@@ -261,10 +287,12 @@ def refresh() -> None:
|
|
|
261
287
|
root, config = load_project_config()
|
|
262
288
|
agent_id = str((config.get("agent") or {}).get("id") or "")
|
|
263
289
|
profile = str(config.get("profile") or "default")
|
|
264
|
-
|
|
290
|
+
credentials = resolve_credentials(profile, agent_id)
|
|
291
|
+
with ApiClient(credentials) as api:
|
|
265
292
|
response = api.request("GET", f"/api/v1/agents/{agent_id}/evaluation-suite")
|
|
266
293
|
_apply_suite_response(root, response)
|
|
267
294
|
typer.echo(f"Refreshed evaluation suite revision {response['revision']} from Halios")
|
|
295
|
+
emit_review_links(halios_ui_links(credentials.ui_base_url, agent_id, include_suite=True))
|
|
268
296
|
|
|
269
297
|
|
|
270
298
|
@app.command("check")
|
|
@@ -345,6 +373,7 @@ def check(
|
|
|
345
373
|
"git": {"branch": branch, "commit": commit, "dirty": dirty == "true"},
|
|
346
374
|
"evaluation_ai": evaluation_status,
|
|
347
375
|
"suite": {"verified": True, "revision": suite["revision"]},
|
|
376
|
+
"links": halios_ui_links(credentials.ui_base_url, agent_id, include_suite=True),
|
|
348
377
|
}
|
|
349
378
|
if json_output:
|
|
350
379
|
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
@@ -355,6 +384,7 @@ def check(
|
|
|
355
384
|
typer.echo(f"Git: {branch}@{commit} dirty={dirty}")
|
|
356
385
|
typer.echo(f"Evaluation AI: {evaluation_status}")
|
|
357
386
|
typer.echo(f"Evaluation suite: verified (revision {suite['revision']})")
|
|
387
|
+
emit_review_links(result["links"])
|
|
358
388
|
|
|
359
389
|
|
|
360
390
|
@app.command("instrumentation")
|
|
@@ -10,6 +10,7 @@ import platform
|
|
|
10
10
|
import re
|
|
11
11
|
import subprocess
|
|
12
12
|
import tempfile
|
|
13
|
+
import urllib.parse
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from datetime import datetime, timezone
|
|
15
16
|
from typing import Any
|
|
@@ -31,6 +32,77 @@ def normalize_url(value: str) -> str:
|
|
|
31
32
|
return (value if "://" in value else f"http://{value}").rstrip("/")
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def _ui_origin(base_url: str) -> str:
|
|
36
|
+
"""Return a credential-free origin suitable for links shown to users."""
|
|
37
|
+
parsed = urllib.parse.urlsplit(normalize_url(base_url))
|
|
38
|
+
hostname = parsed.hostname
|
|
39
|
+
if parsed.scheme not in {"http", "https"} or not hostname:
|
|
40
|
+
raise typer.BadParameter(f"Invalid Halios base URL: {base_url}")
|
|
41
|
+
rendered_host = f"[{hostname}]" if ":" in hostname else hostname
|
|
42
|
+
if parsed.port is not None:
|
|
43
|
+
rendered_host = f"{rendered_host}:{parsed.port}"
|
|
44
|
+
return urllib.parse.urlunsplit((parsed.scheme, rendered_host, "", "", ""))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def halios_ui_links(
|
|
48
|
+
base_url: str,
|
|
49
|
+
agent_id: str,
|
|
50
|
+
*,
|
|
51
|
+
include_suite: bool = False,
|
|
52
|
+
include_evaluations: bool = False,
|
|
53
|
+
run_tag: str | None = None,
|
|
54
|
+
trace_id: str | None = None,
|
|
55
|
+
optimization_run_id: str | None = None,
|
|
56
|
+
) -> dict[str, str]:
|
|
57
|
+
"""Build stable, secret-free Halios UI review links for durable resources."""
|
|
58
|
+
if not agent_id:
|
|
59
|
+
return {}
|
|
60
|
+
origin = _ui_origin(base_url)
|
|
61
|
+
encoded_agent = urllib.parse.quote(agent_id, safe="")
|
|
62
|
+
agent_path = f"{origin}/agents/{encoded_agent}"
|
|
63
|
+
links = {"agent": agent_path}
|
|
64
|
+
if include_suite:
|
|
65
|
+
links["scenarios"] = (
|
|
66
|
+
f"{agent_path}/evaluations?{urllib.parse.urlencode({'tab': 'scenarios'})}"
|
|
67
|
+
)
|
|
68
|
+
links["rules"] = f"{agent_path}/evaluations?{urllib.parse.urlencode({'tab': 'rules'})}"
|
|
69
|
+
if include_evaluations or run_tag:
|
|
70
|
+
links["evaluations"] = f"{agent_path}/evaluations"
|
|
71
|
+
if run_tag:
|
|
72
|
+
encoded_run_tag = urllib.parse.quote(run_tag, safe="")
|
|
73
|
+
links["evaluation_run"] = f"{agent_path}/evaluations/{encoded_run_tag}"
|
|
74
|
+
links["evaluation_traces"] = (
|
|
75
|
+
f"{agent_path}/traces?{urllib.parse.urlencode({'eval_run': run_tag})}"
|
|
76
|
+
)
|
|
77
|
+
if trace_id:
|
|
78
|
+
links["trace"] = f"{agent_path}/traces/{urllib.parse.quote(trace_id, safe='')}"
|
|
79
|
+
if optimization_run_id:
|
|
80
|
+
encoded_run_id = urllib.parse.quote(optimization_run_id, safe="")
|
|
81
|
+
links["optimization_run"] = f"{agent_path}/optimize/{encoded_run_id}"
|
|
82
|
+
return links
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_LINK_LABELS = {
|
|
86
|
+
"agent": "Agent overview",
|
|
87
|
+
"scenarios": "Scenarios",
|
|
88
|
+
"rules": "Rules and rubrics",
|
|
89
|
+
"evaluations": "Evaluation runs",
|
|
90
|
+
"evaluation_run": "Evaluation run",
|
|
91
|
+
"evaluation_traces": "Run traces",
|
|
92
|
+
"trace": "Trace evidence",
|
|
93
|
+
"optimization_run": "Optimization run",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def emit_review_links(links: dict[str, str]) -> None:
|
|
98
|
+
"""Print a compact human-review handoff without making UI usage mandatory."""
|
|
99
|
+
if not links:
|
|
100
|
+
return
|
|
101
|
+
typer.echo("Review in Halios:")
|
|
102
|
+
for key, url in links.items():
|
|
103
|
+
typer.echo(f"- {_LINK_LABELS.get(key, key.replace('_', ' ').title())}: {url}")
|
|
104
|
+
|
|
105
|
+
|
|
34
106
|
def credentials_path() -> pathlib.Path:
|
|
35
107
|
if os.getenv("HALIOS_CONFIG_HOME"):
|
|
36
108
|
root = pathlib.Path(os.environ["HALIOS_CONFIG_HOME"])
|
|
@@ -77,6 +149,7 @@ def save_profile(
|
|
|
77
149
|
*,
|
|
78
150
|
base_url: str,
|
|
79
151
|
api_key: str,
|
|
152
|
+
ui_base_url: str | None = None,
|
|
80
153
|
organization_id: str | None = None,
|
|
81
154
|
api_key_id: int | None = None,
|
|
82
155
|
expires_at: str | None = None,
|
|
@@ -87,6 +160,7 @@ def save_profile(
|
|
|
87
160
|
profiles[profile] = {
|
|
88
161
|
**previous,
|
|
89
162
|
"base_url": normalize_url(base_url),
|
|
163
|
+
"ui_base_url": normalize_url(ui_base_url or base_url),
|
|
90
164
|
"api_key": api_key,
|
|
91
165
|
"organization_id": organization_id,
|
|
92
166
|
"api_key_id": api_key_id,
|
|
@@ -117,6 +191,7 @@ def save_agent_ingest_token(profile: str, agent_id: str, token: str) -> None:
|
|
|
117
191
|
class Credentials:
|
|
118
192
|
profile: str
|
|
119
193
|
base_url: str
|
|
194
|
+
ui_base_url: str
|
|
120
195
|
api_key: str
|
|
121
196
|
organization_id: str | None
|
|
122
197
|
otlp_token: str | None = None
|
|
@@ -133,6 +208,9 @@ def stored_profile_credentials(profile: str = "default") -> Credentials | None:
|
|
|
133
208
|
return Credentials(
|
|
134
209
|
profile=profile,
|
|
135
210
|
base_url=normalize_url(str(entry.get("base_url") or DEFAULT_BASE_URL)),
|
|
211
|
+
ui_base_url=normalize_url(
|
|
212
|
+
str(entry.get("ui_base_url") or entry.get("base_url") or DEFAULT_BASE_URL)
|
|
213
|
+
),
|
|
136
214
|
api_key=str(entry["api_key"]),
|
|
137
215
|
organization_id=entry.get("organization_id"),
|
|
138
216
|
api_key_id=raw_key_id if isinstance(raw_key_id, int) else None,
|
|
@@ -150,6 +228,10 @@ def resolve_credentials(profile: str = "default", agent_id: str | None = None) -
|
|
|
150
228
|
base_url = normalize_url(
|
|
151
229
|
os.getenv("HALIOS_BASE_URL") or str(entry.get("base_url") or DEFAULT_BASE_URL)
|
|
152
230
|
)
|
|
231
|
+
ui_base_url = normalize_url(
|
|
232
|
+
os.getenv("HALIOS_UI_URL")
|
|
233
|
+
or str(entry.get("ui_base_url") or entry.get("base_url") or base_url)
|
|
234
|
+
)
|
|
153
235
|
# INTENT: CI runners need both control-plane and agent-scoped ingest credentials without
|
|
154
236
|
# writing a persistent profile to the ephemeral filesystem.
|
|
155
237
|
token = os.getenv("HALIOS_OTLP_TOKEN")
|
|
@@ -160,6 +242,7 @@ def resolve_credentials(profile: str = "default", agent_id: str | None = None) -
|
|
|
160
242
|
return Credentials(
|
|
161
243
|
profile=profile,
|
|
162
244
|
base_url=base_url,
|
|
245
|
+
ui_base_url=ui_base_url,
|
|
163
246
|
api_key=env_key or str(entry.get("api_key") or ""),
|
|
164
247
|
organization_id=entry.get("organization_id"),
|
|
165
248
|
otlp_token=token,
|
|
@@ -7,7 +7,13 @@ import re
|
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
|
-
from .cli_support import
|
|
10
|
+
from .cli_support import (
|
|
11
|
+
ApiClient,
|
|
12
|
+
emit_review_links,
|
|
13
|
+
halios_ui_links,
|
|
14
|
+
load_project_config,
|
|
15
|
+
resolve_credentials,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
app = typer.Typer(help="Inspect trace evidence and production failures.", no_args_is_help=True)
|
|
13
19
|
|
|
@@ -23,7 +29,17 @@ def _emit(value: object, json_output: bool) -> None:
|
|
|
23
29
|
if json_output:
|
|
24
30
|
typer.echo(json.dumps(value, indent=2, sort_keys=True, default=str))
|
|
25
31
|
else:
|
|
26
|
-
|
|
32
|
+
links = value.get("links") if isinstance(value, dict) else None
|
|
33
|
+
display_value = (
|
|
34
|
+
{key: item for key, item in value.items() if key != "links"}
|
|
35
|
+
if isinstance(value, dict)
|
|
36
|
+
else value
|
|
37
|
+
)
|
|
38
|
+
items = (
|
|
39
|
+
display_value.get("data", display_value)
|
|
40
|
+
if isinstance(display_value, dict)
|
|
41
|
+
else display_value
|
|
42
|
+
)
|
|
27
43
|
if isinstance(items, list):
|
|
28
44
|
for item in items:
|
|
29
45
|
if isinstance(item, dict):
|
|
@@ -33,7 +49,9 @@ def _emit(value: object, json_output: bool) -> None:
|
|
|
33
49
|
)
|
|
34
50
|
)
|
|
35
51
|
else:
|
|
36
|
-
typer.echo(json.dumps(
|
|
52
|
+
typer.echo(json.dumps(display_value, indent=2, default=str))
|
|
53
|
+
if isinstance(links, dict):
|
|
54
|
+
emit_review_links(links)
|
|
37
55
|
|
|
38
56
|
|
|
39
57
|
@app.command("list")
|
|
@@ -57,7 +75,7 @@ def show(
|
|
|
57
75
|
include: str = typer.Option("spans,checks", "--include"),
|
|
58
76
|
json_output: bool = typer.Option(False, "--json"),
|
|
59
77
|
) -> None:
|
|
60
|
-
|
|
78
|
+
agent_id, credentials = _context()
|
|
61
79
|
with ApiClient(credentials) as api:
|
|
62
80
|
result = api.request("GET", f"/api/v1/traces/{trace_id}")
|
|
63
81
|
allowed = {item.strip() for item in include.split(",") if item.strip()}
|
|
@@ -66,6 +84,7 @@ def show(
|
|
|
66
84
|
result.pop("spans", None)
|
|
67
85
|
if "checks" not in allowed:
|
|
68
86
|
result.pop("check_executions", None)
|
|
87
|
+
result["links"] = halios_ui_links(credentials.ui_base_url, agent_id, trace_id=trace_id)
|
|
69
88
|
_emit(result, json_output)
|
|
70
89
|
|
|
71
90
|
|
|
@@ -122,7 +141,7 @@ def verify(
|
|
|
122
141
|
json_output: bool = typer.Option(False, "--json"),
|
|
123
142
|
) -> None:
|
|
124
143
|
"""Fail closed unless a stored runtime trace has usable standard OTel evidence."""
|
|
125
|
-
|
|
144
|
+
agent_id, credentials = _context()
|
|
126
145
|
with ApiClient(credentials) as api:
|
|
127
146
|
detail = api.request("GET", f"/api/v1/traces/{trace_id}")
|
|
128
147
|
spans = detail.get("spans") or []
|
|
@@ -169,6 +188,7 @@ def verify(
|
|
|
169
188
|
"span_count": len(spans),
|
|
170
189
|
"root_count": len(roots),
|
|
171
190
|
"issues": issues,
|
|
191
|
+
"links": halios_ui_links(credentials.ui_base_url, agent_id, trace_id=trace_id),
|
|
172
192
|
}
|
|
173
193
|
_emit(result, json_output)
|
|
174
194
|
if issues:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haliosai-cli
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.6
|
|
4
4
|
Summary: Halios CLI for coding-agent evaluations, guardrails, and OpenTelemetry evidence
|
|
5
5
|
Author-email: HaliosLabs <support@halios.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -65,10 +65,13 @@ npx skills add HaliosAI/halios --skill halios
|
|
|
65
65
|
Prompt your agent to set up evaluations for your project:
|
|
66
66
|
|
|
67
67
|
```text
|
|
68
|
-
"Set up evals for this agent: inspect the repository, create realistic test scenarios and checks, and run
|
|
68
|
+
"Set up evals for this agent: inspect the repository, create realistic test scenarios and checks, run one smoke test, and summarize before asking whether to run the full suite."
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Your agent will inspect your application entrypoint, configure standard OpenTelemetry, draft
|
|
71
|
+
Your agent will inspect your application entrypoint, configure standard OpenTelemetry, draft
|
|
72
|
+
scenarios in `.halios/`, and run one smoke scenario. It stops with a verification summary before
|
|
73
|
+
asking whether to run the full suite and how many trials per scenario to use. The summary includes
|
|
74
|
+
direct Halios UI links for reviewing the materialized suite, smoke run, and trace evidence.
|
|
72
75
|
|
|
73
76
|
---
|
|
74
77
|
|
|
@@ -78,15 +81,19 @@ If you prefer to drive evaluations directly from the command line or CI:
|
|
|
78
81
|
|
|
79
82
|
```bash
|
|
80
83
|
# Recommended: Install with uv tool
|
|
81
|
-
uv tool install 'haliosai-cli>=2.0.
|
|
84
|
+
uv tool install 'haliosai-cli>=2.0.6'
|
|
82
85
|
|
|
83
86
|
# Or install with pipx
|
|
84
|
-
pipx install 'haliosai-cli>=2.0.
|
|
87
|
+
pipx install 'haliosai-cli>=2.0.6'
|
|
85
88
|
|
|
86
89
|
# See available commands and usage
|
|
87
90
|
halios --help
|
|
88
91
|
```
|
|
89
92
|
|
|
93
|
+
The API and UI use the same origin by default. For a self-hosted deployment with separate origins,
|
|
94
|
+
set `HALIOS_UI_URL` so structured review links point to the web application without changing the
|
|
95
|
+
API endpoint used by `HALIOS_BASE_URL`.
|
|
96
|
+
|
|
90
97
|
---
|
|
91
98
|
|
|
92
99
|
## How It Works
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import sys
|
|
5
|
+
from types import SimpleNamespace
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
import typer
|
|
6
10
|
from typer.testing import CliRunner
|
|
7
11
|
|
|
8
|
-
from halios_cli import cli_support, cli_trace
|
|
12
|
+
from halios_cli import cli_project, cli_support, cli_trace
|
|
9
13
|
from halios_cli.cli import app
|
|
10
14
|
from halios_cli.cli_eval import _invoke_adapter, _otlp_root_payload, _scenario_schema_errors
|
|
11
15
|
|
|
@@ -30,7 +34,8 @@ class FakeApiClient:
|
|
|
30
34
|
|
|
31
35
|
def _patch_trace_context(monkeypatch) -> None:
|
|
32
36
|
FakeApiClient.calls = []
|
|
33
|
-
|
|
37
|
+
credentials = SimpleNamespace(ui_base_url="https://app.halios.ai")
|
|
38
|
+
monkeypatch.setattr(cli_trace, "_context", lambda: ("agent-id", credentials))
|
|
34
39
|
monkeypatch.setattr(cli_trace, "ApiClient", FakeApiClient)
|
|
35
40
|
|
|
36
41
|
|
|
@@ -46,12 +51,96 @@ def test_ci_credentials_accept_agent_scoped_otlp_token_from_environment(monkeypa
|
|
|
46
51
|
monkeypatch.setenv("HALIOS_API_KEY", "halios_control_plane_key")
|
|
47
52
|
monkeypatch.setenv("HALIOS_OTLP_TOKEN", "halios_agent_ingest_token")
|
|
48
53
|
monkeypatch.setenv("HALIOS_BASE_URL", "https://api.halios.ai")
|
|
54
|
+
monkeypatch.setenv("HALIOS_UI_URL", "https://app.halios.ai")
|
|
49
55
|
|
|
50
56
|
credentials = cli_support.resolve_credentials(agent_id="agent-id")
|
|
51
57
|
|
|
52
58
|
assert credentials.api_key == "halios_control_plane_key"
|
|
53
59
|
assert credentials.otlp_token == "halios_agent_ingest_token"
|
|
54
60
|
assert credentials.base_url == "https://api.halios.ai"
|
|
61
|
+
assert credentials.ui_base_url == "https://app.halios.ai"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_ui_links_are_encoded_and_strip_credentials() -> None:
|
|
65
|
+
links = cli_support.halios_ui_links(
|
|
66
|
+
"https://user:secret@app.halios.ai/api?token=do-not-copy",
|
|
67
|
+
"agent/id",
|
|
68
|
+
include_suite=True,
|
|
69
|
+
run_tag="run:hello world",
|
|
70
|
+
trace_id="1" * 32,
|
|
71
|
+
optimization_run_id="opt/id",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert links["agent"] == "https://app.halios.ai/agents/agent%2Fid"
|
|
75
|
+
assert links["scenarios"].endswith("/evaluations?tab=scenarios")
|
|
76
|
+
assert links["rules"].endswith("/evaluations?tab=rules")
|
|
77
|
+
assert links["evaluation_run"].endswith("/evaluations/run%3Ahello%20world")
|
|
78
|
+
assert links["evaluation_traces"].endswith("/traces?eval_run=run%3Ahello+world")
|
|
79
|
+
assert links["optimization_run"].endswith("/optimize/opt%2Fid")
|
|
80
|
+
assert "secret" not in "\n".join(links.values())
|
|
81
|
+
assert "token" not in "\n".join(links.values())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_ui_links_require_agent_identity() -> None:
|
|
85
|
+
assert cli_support.halios_ui_links("https://app.halios.ai", "") == {}
|
|
86
|
+
|
|
87
|
+
with pytest.raises(typer.BadParameter, match="Invalid Halios base URL"):
|
|
88
|
+
cli_support.halios_ui_links("ftp://app.halios.ai", "agent-id")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_project_init_json_returns_agent_review_link(monkeypatch, tmp_path) -> None:
|
|
92
|
+
agent_id = "11111111-1111-4111-8111-111111111111"
|
|
93
|
+
credentials = SimpleNamespace(
|
|
94
|
+
base_url="https://api.halios.ai",
|
|
95
|
+
ui_base_url="https://app.halios.ai",
|
|
96
|
+
otlp_token="stored-token",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
class InitApi:
|
|
100
|
+
def __init__(self, _credentials: object) -> None:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
def __enter__(self):
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def __exit__(self, *_args):
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def request(self, method: str, path: str, **_kwargs):
|
|
110
|
+
if method == "GET" and path == "/api/v1/ai/capability":
|
|
111
|
+
return {"evaluation_available": True}
|
|
112
|
+
if method == "POST" and path == "/api/v1/agents":
|
|
113
|
+
return {"id": agent_id, "name": "Support agent", "slug": "support-agent"}
|
|
114
|
+
if path == f"/api/v1/agents/{agent_id}/evaluation-suite":
|
|
115
|
+
return {
|
|
116
|
+
"revision": 0,
|
|
117
|
+
"digest": "",
|
|
118
|
+
"eval": {},
|
|
119
|
+
"scenarios": {"version": 1, "scenarios": []},
|
|
120
|
+
}
|
|
121
|
+
raise AssertionError(f"Unexpected request: {method} {path}")
|
|
122
|
+
|
|
123
|
+
monkeypatch.chdir(tmp_path)
|
|
124
|
+
monkeypatch.setattr(cli_project, "resolve_credentials", lambda *_args: credentials)
|
|
125
|
+
monkeypatch.setattr(cli_project, "ApiClient", InitApi)
|
|
126
|
+
|
|
127
|
+
result = CliRunner().invoke(
|
|
128
|
+
app,
|
|
129
|
+
[
|
|
130
|
+
"project",
|
|
131
|
+
"init",
|
|
132
|
+
"--agent",
|
|
133
|
+
"Support agent",
|
|
134
|
+
"--command",
|
|
135
|
+
"python adapter.py",
|
|
136
|
+
"--json",
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert result.exit_code == 0
|
|
141
|
+
payload = json.loads(result.output)
|
|
142
|
+
assert payload["agent"]["id"] == agent_id
|
|
143
|
+
assert payload["links"]["agent"] == f"https://app.halios.ai/agents/{agent_id}"
|
|
55
144
|
|
|
56
145
|
|
|
57
146
|
def test_trace_list_uses_backend_traffic_scope(monkeypatch) -> None:
|
|
@@ -100,6 +189,7 @@ def test_trace_verify_accepts_complete_root(monkeypatch) -> None:
|
|
|
100
189
|
result = CliRunner().invoke(app, ["trace", "verify", trace_id, "--json"])
|
|
101
190
|
assert result.exit_code == 0
|
|
102
191
|
assert '"verified": true' in result.output
|
|
192
|
+
assert '"trace": "https://app.halios.ai/agents/agent-id/traces/' in result.output
|
|
103
193
|
|
|
104
194
|
|
|
105
195
|
def test_cli_root_span_uses_standard_genai_message_parts() -> None:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|