plato-sdk-v2 2.0.64__py3-none-any.whl → 2.3.4__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.
- plato/__init__.py +0 -9
- plato/_sims_generator/__init__.py +19 -4
- plato/_sims_generator/instruction.py +203 -0
- plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
- plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
- plato/agents/__init__.py +99 -430
- plato/agents/base.py +145 -0
- plato/agents/build.py +61 -0
- plato/agents/config.py +160 -0
- plato/agents/logging.py +515 -0
- plato/agents/runner.py +191 -0
- plato/agents/trajectory.py +266 -0
- plato/chronos/models/__init__.py +1 -1
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +88 -84
- plato/v1/cli/pm.py +84 -44
- plato/v1/cli/sandbox.py +241 -61
- plato/v1/cli/ssh.py +16 -4
- plato/v1/cli/verify.py +685 -0
- plato/v1/cli/world.py +3 -0
- plato/v1/flow_executor.py +21 -17
- plato/v1/models/env.py +11 -11
- plato/v1/sdk.py +2 -2
- plato/v1/sync_env.py +11 -11
- plato/v1/sync_flow_executor.py +21 -17
- plato/v1/sync_sdk.py +4 -2
- plato/v2/__init__.py +2 -0
- plato/v2/async_/environment.py +31 -0
- plato/v2/async_/session.py +72 -4
- plato/v2/sync/environment.py +31 -0
- plato/v2/sync/session.py +72 -4
- plato/worlds/README.md +71 -56
- plato/worlds/__init__.py +56 -18
- plato/worlds/base.py +578 -93
- plato/worlds/config.py +276 -74
- plato/worlds/runner.py +475 -80
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/METADATA +3 -3
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/RECORD +41 -36
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/entry_points.txt +1 -0
- plato/agents/callback.py +0 -246
- plato/world/__init__.py +0 -44
- plato/world/base.py +0 -267
- plato/world/config.py +0 -139
- plato/world/types.py +0 -47
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/WHEEL +0 -0
plato/v1/cli/agent.py
CHANGED
|
@@ -6,13 +6,16 @@ import re
|
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
-
import tempfile
|
|
10
9
|
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
|
|
14
14
|
from plato.v1.cli.utils import console, require_api_key
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from plato.agents.build import BuildConfig
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
def _extract_schemas(pkg_path: Path, package_name: str) -> tuple[dict | None, dict | None, dict | None]:
|
|
18
21
|
"""Extract Config, BuildConfig, and SecretsConfig schemas from the agent package.
|
|
@@ -279,8 +282,7 @@ def _publish_agent_image(
|
|
|
279
282
|
description: str,
|
|
280
283
|
dry_run: bool,
|
|
281
284
|
schema_data: dict | None = None,
|
|
282
|
-
|
|
283
|
-
secrets_schema: dict | None = None,
|
|
285
|
+
build_config: "BuildConfig | None" = None,
|
|
284
286
|
) -> None:
|
|
285
287
|
"""Common logic for publishing an agent Docker image to ECR."""
|
|
286
288
|
import httpx
|
|
@@ -298,9 +300,27 @@ def _publish_agent_image(
|
|
|
298
300
|
console.print("[cyan]Building Docker image...[/cyan]")
|
|
299
301
|
local_tag = f"{agent_name}:{version}"
|
|
300
302
|
|
|
303
|
+
# Build docker command with build args from config
|
|
304
|
+
docker_cmd = ["docker", "build", "--progress=plain", "-t", local_tag]
|
|
305
|
+
|
|
306
|
+
# Add --target prod if Dockerfile has multi-stage builds
|
|
307
|
+
dockerfile_path = build_path / "Dockerfile"
|
|
308
|
+
if dockerfile_path.exists():
|
|
309
|
+
dockerfile_content = dockerfile_path.read_text()
|
|
310
|
+
if "FROM" in dockerfile_content and "AS prod" in dockerfile_content:
|
|
311
|
+
docker_cmd.extend(["--target", "prod"])
|
|
312
|
+
console.print("[cyan]Using target: prod[/cyan]")
|
|
313
|
+
|
|
314
|
+
# Add build args from build config's env dict
|
|
315
|
+
if build_config and build_config.env:
|
|
316
|
+
for key, value in build_config.env.items():
|
|
317
|
+
docker_cmd.extend(["--build-arg", f"{key}={value}"])
|
|
318
|
+
|
|
319
|
+
docker_cmd.append(str(build_path))
|
|
320
|
+
|
|
301
321
|
# Use Popen to stream output in real-time
|
|
302
322
|
process = subprocess.Popen(
|
|
303
|
-
|
|
323
|
+
docker_cmd,
|
|
304
324
|
stdout=subprocess.PIPE,
|
|
305
325
|
stderr=subprocess.STDOUT,
|
|
306
326
|
text=True,
|
|
@@ -309,6 +329,9 @@ def _publish_agent_image(
|
|
|
309
329
|
|
|
310
330
|
# Stream build output
|
|
311
331
|
build_output = []
|
|
332
|
+
if process.stdout is None:
|
|
333
|
+
console.print("[red]Error: Failed to capture Docker build output[/red]")
|
|
334
|
+
raise typer.Exit(1)
|
|
312
335
|
for line in iter(process.stdout.readline, ""):
|
|
313
336
|
line = line.rstrip()
|
|
314
337
|
if line:
|
|
@@ -405,10 +428,6 @@ def _publish_agent_image(
|
|
|
405
428
|
"description": description,
|
|
406
429
|
"config_schema": schema_data,
|
|
407
430
|
}
|
|
408
|
-
if template_variables:
|
|
409
|
-
registration_data["template_variables"] = template_variables
|
|
410
|
-
if secrets_schema:
|
|
411
|
-
registration_data["secrets_schema"] = secrets_schema
|
|
412
431
|
response = client.post(
|
|
413
432
|
"/v2/agents/register",
|
|
414
433
|
json=registration_data,
|
|
@@ -431,6 +450,11 @@ def _publish_agent_image(
|
|
|
431
450
|
console.print(f"[cyan]Artifact ID:[/cyan] {reg_result['artifact_id']}")
|
|
432
451
|
console.print(f"[cyan]Image:[/cyan] {ecr_image}")
|
|
433
452
|
|
|
453
|
+
# Clean up local images after successful push
|
|
454
|
+
console.print("[dim]Cleaning up local images...[/dim]")
|
|
455
|
+
subprocess.run(["docker", "rmi", local_tag], capture_output=True)
|
|
456
|
+
subprocess.run(["docker", "rmi", ecr_image], capture_output=True)
|
|
457
|
+
|
|
434
458
|
|
|
435
459
|
def _publish_package(path: str, repo: str, dry_run: bool = False):
|
|
436
460
|
"""
|
|
@@ -545,6 +569,8 @@ def _publish_package(path: str, repo: str, dry_run: bool = False):
|
|
|
545
569
|
upload_url = f"{api_url}/v2/pypi/{repo}/"
|
|
546
570
|
console.print(f"\n[cyan]Uploading to {upload_url}...[/cyan]")
|
|
547
571
|
|
|
572
|
+
# api_key is guaranteed to be set (checked earlier when not dry_run)
|
|
573
|
+
assert api_key is not None, "api_key must be set when not in dry_run mode"
|
|
548
574
|
try:
|
|
549
575
|
result = subprocess.run(
|
|
550
576
|
[
|
|
@@ -560,6 +586,7 @@ def _publish_package(path: str, repo: str, dry_run: bool = False):
|
|
|
560
586
|
],
|
|
561
587
|
capture_output=True,
|
|
562
588
|
text=True,
|
|
589
|
+
check=False,
|
|
563
590
|
)
|
|
564
591
|
|
|
565
592
|
if result.returncode == 0:
|
|
@@ -797,39 +824,10 @@ def agent_push(
|
|
|
797
824
|
console.print(f"[yellow]Failed:[/yellow] {', '.join(failed)}")
|
|
798
825
|
return
|
|
799
826
|
|
|
800
|
-
#
|
|
801
|
-
if target in HARBOR_AGENTS:
|
|
802
|
-
harbor_version = _get_harbor_version()
|
|
803
|
-
console.print(f"[cyan]Harbor agent:[/cyan] {target}")
|
|
804
|
-
console.print(f"[cyan]Harbor version:[/cyan] {harbor_version}")
|
|
805
|
-
|
|
806
|
-
install_script = _get_harbor_install_script(target)
|
|
807
|
-
if not install_script:
|
|
808
|
-
console.print(f"[red]Error: Could not get install script for '{target}'[/red]")
|
|
809
|
-
console.print("[yellow]Make sure harbor is installed: pip install 'plato-sdk-v2[agents]'[/yellow]")
|
|
810
|
-
raise typer.Exit(1)
|
|
811
|
-
|
|
812
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
813
|
-
tmppath = Path(tmpdir)
|
|
814
|
-
aux_files = HARBOR_AGENT_AUX_FILES.get(target, [])
|
|
815
|
-
(tmppath / "Dockerfile").write_text(_build_harbor_dockerfile(aux_files))
|
|
816
|
-
(tmppath / "install.sh").write_text(install_script)
|
|
817
|
-
_copy_harbor_aux_files(target, tmppath)
|
|
818
|
-
|
|
819
|
-
_publish_agent_image(
|
|
820
|
-
agent_name=target,
|
|
821
|
-
version=harbor_version,
|
|
822
|
-
build_path=tmppath,
|
|
823
|
-
description=f"Harbor agent: {target}",
|
|
824
|
-
dry_run=dry_run,
|
|
825
|
-
)
|
|
826
|
-
return
|
|
827
|
-
|
|
828
|
-
# Otherwise, treat as a path to custom agent
|
|
827
|
+
# Treat target as a path to agent directory
|
|
829
828
|
pkg_path = Path(target).resolve()
|
|
830
829
|
if not pkg_path.exists():
|
|
831
|
-
console.print(f"[red]Error: '{target}' is not a
|
|
832
|
-
console.print(f"\n[yellow]Harbor agents:[/yellow] {', '.join(HARBOR_AGENTS.keys())}")
|
|
830
|
+
console.print(f"[red]Error: '{target}' is not a valid path[/red]")
|
|
833
831
|
raise typer.Exit(1)
|
|
834
832
|
|
|
835
833
|
_push_single_agent(pkg_path, dry_run)
|
|
@@ -837,6 +835,8 @@ def agent_push(
|
|
|
837
835
|
|
|
838
836
|
def _push_single_agent(pkg_path: Path, dry_run: bool) -> None:
|
|
839
837
|
"""Push a single custom agent from a directory."""
|
|
838
|
+
from plato.agents.build import BuildConfig
|
|
839
|
+
|
|
840
840
|
# Check for Dockerfile
|
|
841
841
|
if not (pkg_path / "Dockerfile").exists():
|
|
842
842
|
console.print(f"[red]Error: No Dockerfile found at {pkg_path}[/red]")
|
|
@@ -866,56 +866,61 @@ def _push_single_agent(pkg_path: Path, dry_run: bool) -> None:
|
|
|
866
866
|
console.print("[red]Error: No version in pyproject.toml[/red]")
|
|
867
867
|
raise typer.Exit(1)
|
|
868
868
|
|
|
869
|
-
# Extract short name
|
|
869
|
+
# Extract short name (remove common prefixes)
|
|
870
870
|
short_name = package_name
|
|
871
|
-
for
|
|
872
|
-
if short_name.
|
|
873
|
-
short_name = short_name[len(
|
|
871
|
+
for suffix in ("-agent",):
|
|
872
|
+
if short_name.endswith(suffix):
|
|
873
|
+
short_name = short_name[: -len(suffix)]
|
|
874
874
|
break
|
|
875
875
|
|
|
876
|
-
#
|
|
877
|
-
|
|
878
|
-
|
|
876
|
+
# Load build config from pyproject.toml (optional, for build args)
|
|
877
|
+
build_config = None
|
|
878
|
+
try:
|
|
879
|
+
build_config = BuildConfig.from_pyproject(pkg_path)
|
|
880
|
+
if build_config.env:
|
|
881
|
+
console.print(f"[cyan]Build args:[/cyan] {list(build_config.env.keys())}")
|
|
882
|
+
except Exception:
|
|
883
|
+
pass # Build config is optional
|
|
879
884
|
|
|
880
|
-
#
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if schema_file.exists():
|
|
884
|
-
console.print(f"[cyan]Loading schema from {schema_file}[/cyan]")
|
|
885
|
-
try:
|
|
886
|
-
with open(schema_file) as f:
|
|
887
|
-
schema_data = json.load(f)
|
|
888
|
-
except Exception as e:
|
|
889
|
-
console.print(f"[yellow]Warning: Failed to load schema.json: {e}[/yellow]")
|
|
885
|
+
# Load schema from entry point defined in pyproject.toml
|
|
886
|
+
schema_data = None
|
|
887
|
+
entry_points_config = pyproject.get("project", {}).get("entry-points", {}).get("plato.agents", {})
|
|
890
888
|
|
|
891
|
-
if
|
|
892
|
-
console.print("[
|
|
889
|
+
if not entry_points_config:
|
|
890
|
+
console.print("[yellow]No plato.agents entry point in pyproject.toml - agent will have no schema[/yellow]")
|
|
893
891
|
else:
|
|
894
|
-
|
|
892
|
+
# Get the first (and typically only) entry point
|
|
893
|
+
# Format is: agent_name = "module_name:ClassName"
|
|
894
|
+
for ep_name, ep_value in entry_points_config.items():
|
|
895
|
+
try:
|
|
896
|
+
if ":" not in ep_value:
|
|
897
|
+
console.print(f"[yellow]Invalid entry point format '{ep_value}' - expected 'module:Class'[/yellow]")
|
|
898
|
+
continue
|
|
895
899
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
900
|
+
module_name, class_name = ep_value.split(":", 1)
|
|
901
|
+
|
|
902
|
+
# Add src/ to path and import
|
|
903
|
+
import sys
|
|
904
|
+
|
|
905
|
+
src_path = pkg_path / "src"
|
|
906
|
+
if src_path.exists():
|
|
907
|
+
sys.path.insert(0, str(src_path))
|
|
908
|
+
|
|
909
|
+
try:
|
|
910
|
+
module = __import__(module_name, fromlist=[class_name])
|
|
911
|
+
agent_cls = getattr(module, class_name)
|
|
912
|
+
schema_data = agent_cls.get_schema()
|
|
913
|
+
console.print(f"[green]Loaded schema from {class_name}[/green]")
|
|
914
|
+
break
|
|
915
|
+
finally:
|
|
916
|
+
if src_path.exists() and str(src_path) in sys.path:
|
|
917
|
+
sys.path.remove(str(src_path))
|
|
918
|
+
|
|
919
|
+
except Exception as e:
|
|
920
|
+
console.print(f"[yellow]Failed to load schema from entry point: {e}[/yellow]")
|
|
921
|
+
|
|
922
|
+
if not schema_data:
|
|
923
|
+
console.print("[yellow]No schema found (agent will have no config validation)[/yellow]")
|
|
919
924
|
|
|
920
925
|
_publish_agent_image(
|
|
921
926
|
agent_name=short_name,
|
|
@@ -924,8 +929,7 @@ def _push_single_agent(pkg_path: Path, dry_run: bool) -> None:
|
|
|
924
929
|
description=description or f"Custom agent: {short_name}",
|
|
925
930
|
dry_run=dry_run,
|
|
926
931
|
schema_data=schema_data,
|
|
927
|
-
|
|
928
|
-
secrets_schema=secrets_schema,
|
|
932
|
+
build_config=build_config,
|
|
929
933
|
)
|
|
930
934
|
|
|
931
935
|
|
plato/v1/cli/pm.py
CHANGED
|
@@ -10,10 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
import typer
|
|
13
|
-
|
|
14
|
-
# UUID pattern for detecting artifact IDs in sim:artifact notation
|
|
15
|
-
UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)
|
|
16
|
-
from playwright.async_api import async_playwright
|
|
17
13
|
from rich.table import Table
|
|
18
14
|
|
|
19
15
|
from plato._generated.api.v1.env import get_simulator_by_name, get_simulators
|
|
@@ -43,9 +39,17 @@ from plato.v1.cli.utils import (
|
|
|
43
39
|
require_sandbox_field,
|
|
44
40
|
require_sandbox_state,
|
|
45
41
|
)
|
|
42
|
+
from plato.v1.cli.verify import pm_verify_app
|
|
46
43
|
from plato.v2.async_.client import AsyncPlato
|
|
47
44
|
from plato.v2.types import Env
|
|
48
45
|
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# CONSTANTS
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
# UUID pattern for detecting artifact IDs in sim:artifact notation
|
|
51
|
+
UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)
|
|
52
|
+
|
|
49
53
|
# =============================================================================
|
|
50
54
|
# APP STRUCTURE
|
|
51
55
|
# =============================================================================
|
|
@@ -58,6 +62,7 @@ submit_app = typer.Typer(help="Submit simulator artifacts for review")
|
|
|
58
62
|
pm_app.add_typer(list_app, name="list")
|
|
59
63
|
pm_app.add_typer(review_app, name="review")
|
|
60
64
|
pm_app.add_typer(submit_app, name="submit")
|
|
65
|
+
pm_app.add_typer(pm_verify_app, name="verify")
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
# =============================================================================
|
|
@@ -340,6 +345,9 @@ def review_base(
|
|
|
340
345
|
try:
|
|
341
346
|
http_client = plato._http
|
|
342
347
|
|
|
348
|
+
# simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
|
|
349
|
+
assert simulator_name is not None, "simulator_name must be set"
|
|
350
|
+
|
|
343
351
|
# Get simulator by name
|
|
344
352
|
sim = await get_simulator_by_name.asyncio(
|
|
345
353
|
client=http_client,
|
|
@@ -353,7 +361,7 @@ def review_base(
|
|
|
353
361
|
console.print(f"[cyan]Current status:[/cyan] {current_status}")
|
|
354
362
|
|
|
355
363
|
# Use provided artifact ID or fall back to base_artifact_id from server config
|
|
356
|
-
artifact_id = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
|
|
364
|
+
artifact_id: str | None = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
|
|
357
365
|
if not artifact_id:
|
|
358
366
|
console.print("[red]❌ No artifact ID provided.[/red]")
|
|
359
367
|
console.print(
|
|
@@ -390,6 +398,8 @@ def review_base(
|
|
|
390
398
|
|
|
391
399
|
# Launch Playwright browser and login
|
|
392
400
|
console.print("[cyan]Launching browser and logging in...[/cyan]")
|
|
401
|
+
from playwright.async_api import async_playwright
|
|
402
|
+
|
|
393
403
|
playwright = await async_playwright().start()
|
|
394
404
|
browser = await playwright.chromium.launch(headless=False)
|
|
395
405
|
|
|
@@ -403,44 +413,57 @@ def review_base(
|
|
|
403
413
|
if public_url:
|
|
404
414
|
await page.goto(public_url)
|
|
405
415
|
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
416
|
+
# ALWAYS check state after login to verify no mutations
|
|
417
|
+
console.print("\n[cyan]Checking environment state after login...[/cyan]")
|
|
418
|
+
has_mutations = False
|
|
419
|
+
has_errors = False
|
|
420
|
+
try:
|
|
421
|
+
state_response = await sessions_state.asyncio(
|
|
422
|
+
client=http_client,
|
|
423
|
+
session_id=session.session_id,
|
|
424
|
+
merge_mutations=True,
|
|
425
|
+
x_api_key=api_key,
|
|
426
|
+
)
|
|
427
|
+
if state_response and state_response.results:
|
|
428
|
+
for jid, result in state_response.results.items():
|
|
429
|
+
state_data = result.state if hasattr(result, "state") else result
|
|
430
|
+
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
431
|
+
|
|
432
|
+
if isinstance(state_data, dict):
|
|
433
|
+
# Check for error in state response
|
|
434
|
+
if "error" in state_data:
|
|
435
|
+
has_errors = True
|
|
436
|
+
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
437
|
+
console.print(f"[red]{state_data['error']}[/red]")
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
mutations = state_data.pop("mutations", [])
|
|
441
|
+
console.print("\n[bold]State:[/bold]")
|
|
442
|
+
console.print(json.dumps(state_data, indent=2, default=str))
|
|
443
|
+
if mutations:
|
|
444
|
+
has_mutations = True
|
|
445
|
+
console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
|
|
446
|
+
console.print(json.dumps(mutations, indent=2, default=str))
|
|
447
|
+
else:
|
|
448
|
+
console.print("\n[green]No mutations recorded[/green]")
|
|
435
449
|
else:
|
|
436
|
-
console.print(
|
|
437
|
-
|
|
438
|
-
|
|
450
|
+
console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
|
|
451
|
+
|
|
452
|
+
if has_errors:
|
|
453
|
+
console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
|
|
454
|
+
console.print("[yellow]The worker may not be properly connected.[/yellow]")
|
|
455
|
+
elif has_mutations:
|
|
456
|
+
console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
|
|
457
|
+
console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
|
|
439
458
|
else:
|
|
440
|
-
console.print("[
|
|
441
|
-
|
|
442
|
-
console.print(
|
|
459
|
+
console.print("\n[bold green]✅ Login flow verified - no mutations created[/bold green]")
|
|
460
|
+
else:
|
|
461
|
+
console.print("[yellow]No state data available[/yellow]")
|
|
462
|
+
except Exception as e:
|
|
463
|
+
console.print(f"[red]❌ Error getting state: {e}[/red]")
|
|
443
464
|
|
|
465
|
+
# If skip_review, exit without interactive loop
|
|
466
|
+
if skip_review:
|
|
444
467
|
console.print("\n[cyan]Skipping interactive review (--skip-review)[/cyan]")
|
|
445
468
|
return
|
|
446
469
|
|
|
@@ -492,9 +515,16 @@ def review_base(
|
|
|
492
515
|
if state_response and state_response.results:
|
|
493
516
|
for jid, result in state_response.results.items():
|
|
494
517
|
state_data = result.state if hasattr(result, "state") else result
|
|
518
|
+
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
519
|
+
|
|
495
520
|
if isinstance(state_data, dict):
|
|
521
|
+
# Check for error in state response
|
|
522
|
+
if "error" in state_data:
|
|
523
|
+
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
524
|
+
console.print(f"[red]{state_data['error']}[/red]")
|
|
525
|
+
continue
|
|
526
|
+
|
|
496
527
|
mutations = state_data.pop("mutations", [])
|
|
497
|
-
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
498
528
|
console.print("\n[bold]State:[/bold]")
|
|
499
529
|
console.print(json.dumps(state_data, indent=2, default=str))
|
|
500
530
|
if mutations:
|
|
@@ -503,7 +533,6 @@ def review_base(
|
|
|
503
533
|
else:
|
|
504
534
|
console.print("\n[yellow]No mutations recorded[/yellow]")
|
|
505
535
|
else:
|
|
506
|
-
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
507
536
|
console.print(json.dumps(state_data, indent=2, default=str))
|
|
508
537
|
else:
|
|
509
538
|
console.print("[yellow]No state data available[/yellow]")
|
|
@@ -579,9 +608,11 @@ def review_base(
|
|
|
579
608
|
console.print(f"[cyan]Status:[/cyan] {current_status} → {new_status}")
|
|
580
609
|
|
|
581
610
|
# If passed, automatically tag artifact as prod-latest
|
|
582
|
-
if outcome == "pass":
|
|
611
|
+
if outcome == "pass" and artifact_id:
|
|
583
612
|
console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
|
|
584
613
|
try:
|
|
614
|
+
# simulator_name and artifact_id are guaranteed to be set at this point
|
|
615
|
+
assert simulator_name is not None
|
|
585
616
|
await update_tag.asyncio(
|
|
586
617
|
client=http_client,
|
|
587
618
|
body=UpdateTagRequest(
|
|
@@ -681,6 +712,9 @@ def review_data(
|
|
|
681
712
|
|
|
682
713
|
async def _fetch_artifact_info():
|
|
683
714
|
nonlocal artifact_id
|
|
715
|
+
# simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
|
|
716
|
+
assert simulator_name is not None, "simulator_name must be set"
|
|
717
|
+
|
|
684
718
|
base_url = _get_base_url()
|
|
685
719
|
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
|
|
686
720
|
try:
|
|
@@ -755,6 +789,8 @@ def review_data(
|
|
|
755
789
|
|
|
756
790
|
console.print("[cyan]Launching Chrome with EnvGen Recorder extension...[/cyan]")
|
|
757
791
|
|
|
792
|
+
from playwright.async_api import async_playwright
|
|
793
|
+
|
|
758
794
|
playwright = await async_playwright().start()
|
|
759
795
|
|
|
760
796
|
browser = await playwright.chromium.launch_persistent_context(
|
|
@@ -1080,6 +1116,10 @@ def submit_data(
|
|
|
1080
1116
|
)
|
|
1081
1117
|
|
|
1082
1118
|
async def _submit_data():
|
|
1119
|
+
# simulator_name and artifact_id are guaranteed set by parse_simulator_artifact with require_artifact=True
|
|
1120
|
+
assert simulator_name is not None, "simulator_name must be set"
|
|
1121
|
+
assert artifact_id is not None, "artifact_id must be set"
|
|
1122
|
+
|
|
1083
1123
|
base_url = _get_base_url()
|
|
1084
1124
|
|
|
1085
1125
|
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
|
|
@@ -1110,7 +1150,7 @@ def submit_data(
|
|
|
1110
1150
|
x_api_key=api_key,
|
|
1111
1151
|
)
|
|
1112
1152
|
|
|
1113
|
-
# Set data_artifact_id via tag update
|
|
1153
|
+
# Set data_artifact_id via tag update (simulator_name and artifact_id already asserted above)
|
|
1114
1154
|
try:
|
|
1115
1155
|
await update_tag.asyncio(
|
|
1116
1156
|
client=client,
|