agentversion 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentversion/__init__.py +31 -0
- agentversion/_shared.py +23 -0
- agentversion/cli.py +407 -0
- agentversion/compatibility.py +258 -0
- agentversion/constants.py +8 -0
- agentversion/dataset.py +248 -0
- agentversion/decision.py +249 -0
- agentversion/diff.py +740 -0
- agentversion/hasher.py +162 -0
- agentversion/ids.py +324 -0
- agentversion/manifest.py +405 -0
- agentversion/py.typed +0 -0
- agentversion/refs.py +128 -0
- agentversion/replay.py +166 -0
- agentversion/validator.py +346 -0
- agentversion-0.1.0.dist-info/METADATA +252 -0
- agentversion-0.1.0.dist-info/RECORD +20 -0
- agentversion-0.1.0.dist-info/WHEEL +4 -0
- agentversion-0.1.0.dist-info/entry_points.txt +2 -0
- agentversion-0.1.0.dist-info/licenses/LICENSE +190 -0
agentversion/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""AgentVersion — reference implementation.
|
|
2
|
+
|
|
3
|
+
An open specification for versioning agent runtimes and keeping datasets valid.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError
|
|
7
|
+
from importlib.metadata import version as _pkg_version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = _pkg_version("agentversion")
|
|
11
|
+
except PackageNotFoundError: # editable install before metadata is registered
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
|
13
|
+
|
|
14
|
+
from agentversion.constants import SPEC_VERSION
|
|
15
|
+
from agentversion.hasher import hash_manifest, hash_surface
|
|
16
|
+
from agentversion.manifest import AgentManifest
|
|
17
|
+
from agentversion.validator import (
|
|
18
|
+
ValidationResult,
|
|
19
|
+
validate_manifest,
|
|
20
|
+
validate_manifest_file,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SPEC_VERSION",
|
|
25
|
+
"AgentManifest",
|
|
26
|
+
"ValidationResult",
|
|
27
|
+
"hash_manifest",
|
|
28
|
+
"hash_surface",
|
|
29
|
+
"validate_manifest",
|
|
30
|
+
"validate_manifest_file",
|
|
31
|
+
]
|
agentversion/_shared.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared types used across replay, dataset, and other spec modules.
|
|
2
|
+
|
|
3
|
+
Internal module — do not import from outside the package. Public re-exports
|
|
4
|
+
live in ``agentversion.replay`` and ``agentversion.dataset``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Message(BaseModel):
|
|
15
|
+
"""A message in a conversation or trace.
|
|
16
|
+
|
|
17
|
+
Used by both replay inputs and dataset step inputs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
role: Literal["system", "developer", "user", "assistant", "tool"]
|
|
21
|
+
content: str | None = None
|
|
22
|
+
content_ref: str | None = None
|
|
23
|
+
name: str | None = None
|
agentversion/cli.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""CLI entry point for agentversion."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from agentversion import __version__
|
|
13
|
+
from agentversion.constants import SPEC_VERSION
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _print_id_issues(data: dict[str, Any], kind: str) -> int:
|
|
19
|
+
"""Run ID checks and print them. Returns the number of errors (so the
|
|
20
|
+
caller can ``raise SystemExit`` if non-zero)."""
|
|
21
|
+
from agentversion.ids import check_object_ids
|
|
22
|
+
|
|
23
|
+
errors = 0
|
|
24
|
+
for sev, code, message, path in check_object_ids(data, kind=kind):
|
|
25
|
+
icon = "[red]✗[/red]" if sev == "error" else "[yellow]⚠[/yellow]"
|
|
26
|
+
console.print(f" {icon} [{code}] at {path}: {message}")
|
|
27
|
+
if sev == "error":
|
|
28
|
+
errors += 1
|
|
29
|
+
return errors
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
@click.version_option(version=__version__, prog_name="agentversion")
|
|
34
|
+
def cli() -> None:
|
|
35
|
+
"""AgentVersion — CLI tools.
|
|
36
|
+
|
|
37
|
+
An open specification for versioning agent runtimes
|
|
38
|
+
and keeping datasets valid.
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cli.command()
|
|
44
|
+
@click.argument("manifest_file", type=click.Path(exists=True))
|
|
45
|
+
def validate(manifest_file: str) -> None:
|
|
46
|
+
"""Validate a manifest file against the spec."""
|
|
47
|
+
from agentversion.validator import Severity, validate_manifest_file
|
|
48
|
+
|
|
49
|
+
result = validate_manifest_file(manifest_file)
|
|
50
|
+
|
|
51
|
+
if result.valid and not result.warnings:
|
|
52
|
+
console.print(f"[green]✓[/green] {manifest_file} is valid")
|
|
53
|
+
if result.manifest:
|
|
54
|
+
console.print(f" agent: [bold]{result.manifest.agent_name}[/bold]")
|
|
55
|
+
console.print(f" version: {result.manifest.version_label}")
|
|
56
|
+
console.print(f" hash: {result.manifest.identity.overall_hash[:24]}...")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
for issue in result.issues:
|
|
60
|
+
if issue.severity == Severity.ERROR:
|
|
61
|
+
icon = "[red]✗[/red]"
|
|
62
|
+
elif issue.severity == Severity.WARNING:
|
|
63
|
+
icon = "[yellow]⚠[/yellow]"
|
|
64
|
+
else:
|
|
65
|
+
icon = "[blue]ℹ[/blue]"
|
|
66
|
+
|
|
67
|
+
path_str = f" at {issue.path}" if issue.path else ""
|
|
68
|
+
console.print(f" {icon} [{issue.code}]{path_str}: {issue.message}")
|
|
69
|
+
|
|
70
|
+
if result.valid:
|
|
71
|
+
console.print(f"\n[green]✓[/green] {manifest_file} is valid (with {len(result.warnings)} warning(s))")
|
|
72
|
+
else:
|
|
73
|
+
console.print(f"\n[red]✗[/red] {manifest_file} is invalid ({len(result.errors)} error(s))")
|
|
74
|
+
raise SystemExit(1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cli.command()
|
|
78
|
+
@click.argument("old_manifest", type=click.Path(exists=True))
|
|
79
|
+
@click.argument("new_manifest", type=click.Path(exists=True))
|
|
80
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
81
|
+
@click.option("--fail-on-breaking", is_flag=True, help="Exit with code 1 if breaking changes found")
|
|
82
|
+
@click.option("--compat", is_flag=True, help="Include compatibility recommendation")
|
|
83
|
+
def diff(old_manifest: str, new_manifest: str, output_json: bool, fail_on_breaking: bool, compat: bool) -> None:
|
|
84
|
+
"""Diff two manifest files and classify changes."""
|
|
85
|
+
from agentversion.compatibility import classify_compatibility
|
|
86
|
+
from agentversion.diff import diff_manifests
|
|
87
|
+
|
|
88
|
+
with open(old_manifest) as f:
|
|
89
|
+
old_data = json.load(f)
|
|
90
|
+
with open(new_manifest) as f:
|
|
91
|
+
new_data = json.load(f)
|
|
92
|
+
|
|
93
|
+
result = diff_manifests(old_data, new_data)
|
|
94
|
+
|
|
95
|
+
if output_json:
|
|
96
|
+
output = json.loads(result.model_dump_json())
|
|
97
|
+
if compat:
|
|
98
|
+
report = classify_compatibility(result)
|
|
99
|
+
output["compatibility"] = json.loads(report.model_dump_json())
|
|
100
|
+
console.print_json(json.dumps(output, indent=2))
|
|
101
|
+
else:
|
|
102
|
+
if not result.changed_surfaces:
|
|
103
|
+
console.print("[green]✓[/green] No changes detected")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
table = Table(title="Manifest Diff")
|
|
107
|
+
table.add_column("Surface", style="bold")
|
|
108
|
+
table.add_column("Change Type")
|
|
109
|
+
table.add_column("Details")
|
|
110
|
+
|
|
111
|
+
for change in result.changed_surfaces:
|
|
112
|
+
style = "red" if change.change_type == "breaking" else "green"
|
|
113
|
+
table.add_row(
|
|
114
|
+
change.surface,
|
|
115
|
+
f"[{style}]{change.change_type}[/{style}]",
|
|
116
|
+
"\n".join(change.details),
|
|
117
|
+
)
|
|
118
|
+
console.print(table)
|
|
119
|
+
console.print(
|
|
120
|
+
f"\n Breaking: {result.summary.breaking_surfaces} "
|
|
121
|
+
f"Non-breaking: {result.summary.non_breaking_surfaces}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if compat:
|
|
125
|
+
report = classify_compatibility(result)
|
|
126
|
+
console.print(f"\n Recommendation: [bold]{report.recommended_decision}[/bold]")
|
|
127
|
+
console.print(f" {report.summary}")
|
|
128
|
+
|
|
129
|
+
if fail_on_breaking and result.summary.breaking_surfaces > 0:
|
|
130
|
+
raise SystemExit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@cli.command()
|
|
134
|
+
def init() -> None:
|
|
135
|
+
"""Initialize a new manifest file interactively."""
|
|
136
|
+
from agentversion.hasher import compute_and_set_hashes
|
|
137
|
+
from agentversion.ids import mint_id
|
|
138
|
+
|
|
139
|
+
agent_name = click.prompt("Agent name", type=str)
|
|
140
|
+
version_label = click.prompt("Version label", default="v1")
|
|
141
|
+
provider = click.prompt("Model provider (e.g. openai, anthropic, google)", type=str)
|
|
142
|
+
model = click.prompt("Model name (e.g. gpt-4o, claude-opus-4, gemini-2.0-flash)", type=str)
|
|
143
|
+
|
|
144
|
+
manifest = {
|
|
145
|
+
"spec_version": SPEC_VERSION,
|
|
146
|
+
"kind": "agent_manifest",
|
|
147
|
+
"manifest_id": mint_id("agent_manifest"),
|
|
148
|
+
"agent_name": agent_name,
|
|
149
|
+
"version_label": version_label,
|
|
150
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
151
|
+
"description": f"Manifest for {agent_name} {version_label}",
|
|
152
|
+
"tags": [],
|
|
153
|
+
"identity": {
|
|
154
|
+
"overall_hash": "PLACEHOLDER",
|
|
155
|
+
"hash_algorithm": "jcs-sha256",
|
|
156
|
+
},
|
|
157
|
+
"contract": {
|
|
158
|
+
"prompt_stack": {
|
|
159
|
+
"system_prompt": {
|
|
160
|
+
"id": f"prompt_system_{agent_name}",
|
|
161
|
+
"version": "1",
|
|
162
|
+
"hash": "sha256:REPLACE_WITH_ACTUAL_HASH",
|
|
163
|
+
},
|
|
164
|
+
"reasoning_policy": "hidden",
|
|
165
|
+
},
|
|
166
|
+
"model_runtime": {
|
|
167
|
+
"provider": provider,
|
|
168
|
+
"model": model,
|
|
169
|
+
},
|
|
170
|
+
"tool_registry": {
|
|
171
|
+
"registry_version": "1",
|
|
172
|
+
"registry_hash": "sha256:REPLACE_WITH_ACTUAL_HASH",
|
|
173
|
+
"tools": [],
|
|
174
|
+
},
|
|
175
|
+
"workflow": {
|
|
176
|
+
"graph_name": f"{agent_name}-graph",
|
|
177
|
+
"graph_version": "1",
|
|
178
|
+
},
|
|
179
|
+
"output_contract": {
|
|
180
|
+
"version": "1",
|
|
181
|
+
"schema_hash": "sha256:REPLACE_WITH_ACTUAL_HASH",
|
|
182
|
+
"format": "text",
|
|
183
|
+
"strict": False,
|
|
184
|
+
},
|
|
185
|
+
"guardrails": None,
|
|
186
|
+
},
|
|
187
|
+
"extensions": {},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
compute_and_set_hashes(manifest)
|
|
191
|
+
|
|
192
|
+
output_file = f"{agent_name}-manifest.json"
|
|
193
|
+
with open(output_file, "w") as f:
|
|
194
|
+
json.dump(manifest, f, indent=2)
|
|
195
|
+
console.print(f"[green]✓[/green] Created {output_file}")
|
|
196
|
+
console.print(f" hash: {manifest['identity']['overall_hash']}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@cli.command()
|
|
200
|
+
@click.argument("manifest_file", type=click.Path(exists=True))
|
|
201
|
+
def hash(manifest_file: str) -> None:
|
|
202
|
+
"""Compute the canonical hash of a manifest."""
|
|
203
|
+
from agentversion.hasher import hash_manifest
|
|
204
|
+
|
|
205
|
+
with open(manifest_file) as f:
|
|
206
|
+
data = json.load(f)
|
|
207
|
+
|
|
208
|
+
computed_hash = hash_manifest(data)
|
|
209
|
+
console.print(computed_hash)
|
|
210
|
+
|
|
211
|
+
existing = data.get("identity", {}).get("overall_hash")
|
|
212
|
+
if existing and existing != computed_hash:
|
|
213
|
+
console.print(f"[yellow]⚠[/yellow] Declared hash differs: {existing}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@cli.command()
|
|
217
|
+
@click.argument("manifest_file", type=click.Path(exists=True))
|
|
218
|
+
@click.option("--to", "target_version", required=True, help="Target spec version (e.g. 1.1.0)")
|
|
219
|
+
@click.option("--in-place", is_flag=True, help="Rewrite the input file instead of printing to stdout")
|
|
220
|
+
def upgrade(manifest_file: str, target_version: str, in_place: bool) -> None:
|
|
221
|
+
"""Upgrade a manifest to a newer spec version.
|
|
222
|
+
|
|
223
|
+
Within a major version there are no field-level migrations required, so
|
|
224
|
+
this is an identity upgrade: parse, set spec_version, re-emit. Refuses to
|
|
225
|
+
downgrade or cross major-version boundaries.
|
|
226
|
+
"""
|
|
227
|
+
import re
|
|
228
|
+
|
|
229
|
+
semver_re = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
230
|
+
if not semver_re.match(target_version):
|
|
231
|
+
console.print(f"[red]✗[/red] --to must be MAJOR.MINOR.PATCH (got {target_version!r})")
|
|
232
|
+
raise SystemExit(2)
|
|
233
|
+
|
|
234
|
+
with open(manifest_file) as f:
|
|
235
|
+
data = json.load(f)
|
|
236
|
+
|
|
237
|
+
current = data.get("spec_version", "0.0.0")
|
|
238
|
+
m_cur = semver_re.match(current)
|
|
239
|
+
m_tgt = semver_re.match(target_version)
|
|
240
|
+
if not m_cur:
|
|
241
|
+
console.print(f"[red]✗[/red] manifest has invalid spec_version {current!r}")
|
|
242
|
+
raise SystemExit(2)
|
|
243
|
+
|
|
244
|
+
cur_tuple = tuple(int(x) for x in m_cur.groups())
|
|
245
|
+
tgt_tuple = tuple(int(x) for x in m_tgt.groups()) # type: ignore[union-attr]
|
|
246
|
+
|
|
247
|
+
if tgt_tuple < cur_tuple:
|
|
248
|
+
console.print(f"[red]✗[/red] refuse to downgrade ({current} → {target_version})")
|
|
249
|
+
raise SystemExit(2)
|
|
250
|
+
if tgt_tuple[0] != cur_tuple[0]:
|
|
251
|
+
console.print(
|
|
252
|
+
f"[red]✗[/red] cross-major upgrade not supported "
|
|
253
|
+
f"({current} → {target_version}); see CHANGELOG for the migration path"
|
|
254
|
+
)
|
|
255
|
+
raise SystemExit(2)
|
|
256
|
+
|
|
257
|
+
data["spec_version"] = target_version
|
|
258
|
+
|
|
259
|
+
if in_place:
|
|
260
|
+
with open(manifest_file, "w") as f:
|
|
261
|
+
json.dump(data, f, indent=2)
|
|
262
|
+
console.print(f"[green]✓[/green] {manifest_file}: {current} → {target_version}")
|
|
263
|
+
else:
|
|
264
|
+
console.print_json(json.dumps(data, indent=2))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# -- Sub-command groups for other spec objects --
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@cli.group()
|
|
271
|
+
def decision() -> None:
|
|
272
|
+
"""Compatibility decision commands."""
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@decision.command("validate")
|
|
277
|
+
@click.argument("decision_file", type=click.Path(exists=True))
|
|
278
|
+
def decision_validate(decision_file: str) -> None:
|
|
279
|
+
"""Validate a compatibility decision file."""
|
|
280
|
+
from agentversion.decision import CompatibilityDecision
|
|
281
|
+
|
|
282
|
+
with open(decision_file) as f:
|
|
283
|
+
data = json.load(f)
|
|
284
|
+
try:
|
|
285
|
+
d = CompatibilityDecision.model_validate(data)
|
|
286
|
+
console.print(f"[green]✓[/green] Valid compatibility decision: {d.decision}")
|
|
287
|
+
console.print(f" subject: {d.subject.type}/{d.subject.id}")
|
|
288
|
+
if d.reason_codes:
|
|
289
|
+
console.print(f" reasons: {', '.join(d.reason_codes)}")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
console.print(f"[red]✗[/red] Invalid compatibility decision: {e}")
|
|
292
|
+
raise SystemExit(1)
|
|
293
|
+
|
|
294
|
+
if _print_id_issues(data, "compatibility_decision") > 0:
|
|
295
|
+
raise SystemExit(1)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@decision.command("generate")
|
|
299
|
+
@click.argument("old_manifest", type=click.Path(exists=True))
|
|
300
|
+
@click.argument("new_manifest", type=click.Path(exists=True))
|
|
301
|
+
@click.option(
|
|
302
|
+
"--subject-type",
|
|
303
|
+
type=click.Choice(["task", "episode", "step", "dataset_item"]),
|
|
304
|
+
default="episode",
|
|
305
|
+
help="Subject type",
|
|
306
|
+
)
|
|
307
|
+
@click.option("--subject-id", default="ep_unknown", help="Subject ID")
|
|
308
|
+
def decision_generate(old_manifest: str, new_manifest: str, subject_type: str, subject_id: str) -> None:
|
|
309
|
+
"""Auto-generate a compatibility decision from two manifests."""
|
|
310
|
+
from datetime import timezone
|
|
311
|
+
|
|
312
|
+
from agentversion.compatibility import classify_compatibility
|
|
313
|
+
from agentversion.decision import CompatibilityDecision, DecisionSubject
|
|
314
|
+
from agentversion.diff import diff_manifests
|
|
315
|
+
|
|
316
|
+
with open(old_manifest) as f:
|
|
317
|
+
old_data = json.load(f)
|
|
318
|
+
with open(new_manifest) as f:
|
|
319
|
+
new_data = json.load(f)
|
|
320
|
+
|
|
321
|
+
diff_result = diff_manifests(old_data, new_data)
|
|
322
|
+
report = classify_compatibility(diff_result)
|
|
323
|
+
|
|
324
|
+
cd = CompatibilityDecision(
|
|
325
|
+
decision_id=f"cdc_auto_{subject_id}",
|
|
326
|
+
subject=DecisionSubject(
|
|
327
|
+
type=cast(Literal["task", "episode", "step", "dataset_item"], subject_type),
|
|
328
|
+
id=subject_id,
|
|
329
|
+
),
|
|
330
|
+
old_manifest_id=old_data.get("manifest_id", "unknown"),
|
|
331
|
+
target_manifest_id=new_data.get("manifest_id", "unknown"),
|
|
332
|
+
decision=report.recommended_decision,
|
|
333
|
+
reason_codes=report.reason_codes,
|
|
334
|
+
created_at=datetime.now(timezone.utc),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
console.print_json(cd.model_dump_json(indent=2))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@cli.group()
|
|
341
|
+
def replay() -> None:
|
|
342
|
+
"""Replay job commands."""
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@replay.command("validate")
|
|
347
|
+
@click.argument("job_file", type=click.Path(exists=True))
|
|
348
|
+
def replay_validate(job_file: str) -> None:
|
|
349
|
+
"""Validate a replay job file."""
|
|
350
|
+
from agentversion.replay import ReplayJob
|
|
351
|
+
|
|
352
|
+
with open(job_file) as f:
|
|
353
|
+
data = json.load(f)
|
|
354
|
+
try:
|
|
355
|
+
job = ReplayJob.model_validate(data)
|
|
356
|
+
console.print(f"[green]✓[/green] Valid replay job: {job.replay_job_id}")
|
|
357
|
+
console.print(f" mode: {job.mode} priority: {job.priority}")
|
|
358
|
+
console.print(f" target: {job.target_manifest_id}")
|
|
359
|
+
except Exception as e:
|
|
360
|
+
console.print(f"[red]✗[/red] Invalid replay job: {e}")
|
|
361
|
+
raise SystemExit(1)
|
|
362
|
+
|
|
363
|
+
if _print_id_issues(data, "replay_job") > 0:
|
|
364
|
+
raise SystemExit(1)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@cli.group()
|
|
368
|
+
def dataset() -> None:
|
|
369
|
+
"""Dataset commands."""
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@dataset.command("validate")
|
|
374
|
+
@click.argument("dataset_file", type=click.Path(exists=True))
|
|
375
|
+
def dataset_validate(dataset_file: str) -> None:
|
|
376
|
+
"""Validate a dataset file (task, episode, step, or snapshot)."""
|
|
377
|
+
from agentversion.dataset import DatasetSnapshot, Episode, Step, Task
|
|
378
|
+
|
|
379
|
+
kind_map: dict[str, type[BaseModel]] = {
|
|
380
|
+
"task": Task,
|
|
381
|
+
"episode": Episode,
|
|
382
|
+
"step": Step,
|
|
383
|
+
"dataset_snapshot": DatasetSnapshot,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
with open(dataset_file) as f:
|
|
387
|
+
data = json.load(f)
|
|
388
|
+
|
|
389
|
+
kind = data.get("kind")
|
|
390
|
+
if kind not in kind_map:
|
|
391
|
+
console.print(f"[red]✗[/red] Unknown kind: {kind!r} (expected one of {list(kind_map.keys())})")
|
|
392
|
+
raise SystemExit(1)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
model_cls = kind_map[kind]
|
|
396
|
+
obj = model_cls.model_validate(data)
|
|
397
|
+
console.print(f"[green]✓[/green] Valid {kind}: {getattr(obj, f'{kind}_id', 'ok')}")
|
|
398
|
+
except Exception as e:
|
|
399
|
+
console.print(f"[red]✗[/red] Invalid {kind}: {e}")
|
|
400
|
+
raise SystemExit(1)
|
|
401
|
+
|
|
402
|
+
if _print_id_issues(data, kind) > 0:
|
|
403
|
+
raise SystemExit(1)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
if __name__ == "__main__":
|
|
407
|
+
cli()
|