plato-sdk-v2 2.1.11__py3-none-any.whl → 2.3.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.
plato/v1/cli/main.py CHANGED
@@ -11,6 +11,7 @@ from dotenv import load_dotenv
11
11
  from plato.v1.cli.agent import agent_app
12
12
  from plato.v1.cli.pm import pm_app
13
13
  from plato.v1.cli.sandbox import sandbox_app
14
+ from plato.v1.cli.sim import sim_app
14
15
  from plato.v1.cli.utils import console
15
16
  from plato.v1.cli.world import world_app
16
17
 
@@ -69,6 +70,7 @@ app = typer.Typer(help="[bold blue]Plato CLI[/bold blue] - Manage Plato environm
69
70
  # Register sub-apps
70
71
  app.add_typer(sandbox_app, name="sandbox")
71
72
  app.add_typer(pm_app, name="pm")
73
+ app.add_typer(sim_app, name="sim")
72
74
  app.add_typer(agent_app, name="agent")
73
75
  app.add_typer(world_app, name="world")
74
76
 
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
- # If skip_review, check state and exit without interactive loop
407
- if skip_review:
408
- console.print("\n[cyan]Checking environment state after login...[/cyan]")
409
- try:
410
- state_response = await sessions_state.asyncio(
411
- client=http_client,
412
- session_id=session.session_id,
413
- merge_mutations=True,
414
- x_api_key=api_key,
415
- )
416
- if state_response and state_response.results:
417
- has_mutations = False
418
- for jid, result in state_response.results.items():
419
- state_data = result.state if hasattr(result, "state") else result
420
- if isinstance(state_data, dict):
421
- mutations = state_data.pop("mutations", [])
422
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
423
- console.print("\n[bold]State:[/bold]")
424
- console.print(json.dumps(state_data, indent=2, default=str))
425
- if mutations:
426
- has_mutations = True
427
- console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
428
- console.print(json.dumps(mutations, indent=2, default=str))
429
- else:
430
- console.print("\n[green]No mutations recorded[/green]")
431
-
432
- if has_mutations:
433
- console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
434
- console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
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
- "\n[bold green]✅ Login flow verified - no mutations created[/bold green]"
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("[yellow]No state data available[/yellow]")
441
- except Exception as e:
442
- console.print(f"[red] Error getting state: {e}[/red]")
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,
plato/v1/cli/sandbox.py CHANGED
@@ -17,12 +17,8 @@ from urllib.parse import quote
17
17
 
18
18
  import typer
19
19
  import yaml
20
- from playwright.async_api import async_playwright
21
20
  from rich.logging import RichHandler
22
21
 
23
- # UUID pattern for detecting artifact IDs in colon notation
24
- 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)
25
-
26
22
  from plato._generated.api.v1.gitea import (
27
23
  create_simulator_repository,
28
24
  get_accessible_simulators,
@@ -74,11 +70,16 @@ from plato.v1.cli.utils import (
74
70
  require_sandbox_state,
75
71
  save_sandbox_state,
76
72
  )
73
+ from plato.v1.cli.verify import sandbox_verify_app
77
74
  from plato.v2.async_.flow_executor import FlowExecutor
78
75
  from plato.v2.sync.client import Plato as PlatoV2
79
76
  from plato.v2.types import Env, SimConfigCompute
80
77
 
78
+ # UUID pattern for detecting artifact IDs in colon notation
79
+ 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)
80
+
81
81
  sandbox_app = typer.Typer(help="Manage sandboxes for simulator development")
82
+ sandbox_app.add_typer(sandbox_verify_app, name="verify")
82
83
 
83
84
 
84
85
  def format_public_url_with_router_target(public_url: str | None, service_name: str | None) -> str | None:
@@ -423,9 +424,10 @@ def sandbox_start(
423
424
  listeners=listeners_dict,
424
425
  )
425
426
 
427
+ dataset_value = dataset_name or state_extras.get("dataset", "base")
426
428
  setup_request = AppSchemasBuildModelsSetupSandboxRequest(
427
429
  service=sim_name or "",
428
- dataset=dataset_name or state_extras.get("dataset", "base") or "",
430
+ dataset=str(dataset_value) if dataset_value else "",
429
431
  plato_dataset_config=dataset_config_obj,
430
432
  ssh_public_key=ssh_public_key,
431
433
  )
@@ -1047,6 +1049,9 @@ def sandbox_start_worker(
1047
1049
  console.print(f"[cyan]Waiting for worker to be ready (timeout: {wait_timeout}s)...[/cyan]")
1048
1050
 
1049
1051
  session_id = state.get("session_id")
1052
+ if not session_id:
1053
+ console.print("[red]Session ID not found in .sandbox.yaml[/red]")
1054
+ raise typer.Exit(1)
1050
1055
  start_time = time.time()
1051
1056
  poll_interval = 10 # seconds between polls
1052
1057
  worker_ready = False
@@ -1530,17 +1535,31 @@ def sandbox_flow(
1530
1535
  console.print(f"[red]❌ Failed to fetch flows from API: {e}[/red]")
1531
1536
  raise typer.Exit(1) from e
1532
1537
 
1538
+ # At this point, url and flow_obj must be set (validated above)
1539
+ if not url:
1540
+ console.print("[red]❌ URL is not set[/red]")
1541
+ raise typer.Exit(1)
1542
+ if not flow_obj:
1543
+ console.print("[red]❌ Flow object could not be loaded[/red]")
1544
+ raise typer.Exit(1)
1545
+
1533
1546
  console.print(f"[cyan]URL: {url}[/cyan]")
1534
1547
  console.print(f"[cyan]Flow name: {flow_name}[/cyan]")
1535
1548
 
1549
+ # Capture for closure (narrowed types)
1550
+ _url: str = url
1551
+ _flow_obj: Flow = flow_obj
1552
+
1536
1553
  async def _run():
1554
+ from playwright.async_api import async_playwright
1555
+
1537
1556
  browser = None
1538
1557
  try:
1539
1558
  async with async_playwright() as p:
1540
1559
  browser = await p.chromium.launch(headless=False)
1541
1560
  page = await browser.new_page()
1542
- await page.goto(url)
1543
- executor = FlowExecutor(page, flow_obj, screenshots_dir, log=_flow_logger)
1561
+ await page.goto(_url)
1562
+ executor = FlowExecutor(page, _flow_obj, screenshots_dir, log=_flow_logger)
1544
1563
  await executor.execute()
1545
1564
  console.print("[green]✅ Flow executed successfully[/green]")
1546
1565
  except Exception as e:
@@ -1601,8 +1620,23 @@ def sandbox_state_cmd(
1601
1620
  def check_mutations(result_dict: dict) -> tuple[bool, bool, str | None]:
1602
1621
  """Check if result has mutations or errors. Returns (has_mutations, has_error, error_msg)."""
1603
1622
  if isinstance(result_dict, dict):
1604
- if "error" in result_dict:
1605
- return False, True, result_dict.get("error")
1623
+ # Check for state
1624
+ state = result_dict.get("state", {})
1625
+ if isinstance(state, dict):
1626
+ # Check for error wrapped in state (from API layer transformation)
1627
+ if "error" in state:
1628
+ return False, True, state["error"]
1629
+ # Check for db state
1630
+ db_state = state.get("db", {})
1631
+ if isinstance(db_state, dict):
1632
+ mutations = db_state.get("mutations", [])
1633
+ if mutations:
1634
+ return True, False, None
1635
+ # Also check audit_log_count
1636
+ audit_count = db_state.get("audit_log_count", 0)
1637
+ if audit_count > 0:
1638
+ return True, False, None
1639
+ # Check top-level mutations as fallback
1606
1640
  mutations = result_dict.get("mutations", [])
1607
1641
  if mutations:
1608
1642
  return True, False, None
@@ -2004,6 +2038,10 @@ def sandbox_start_services(
2004
2038
  if not json_output:
2005
2039
  console.print("[cyan]Step 3: Getting/creating repository...[/cyan]")
2006
2040
 
2041
+ if sim_id is None:
2042
+ console.print("[red]❌ Simulator ID not available[/red]")
2043
+ raise typer.Exit(1)
2044
+
2007
2045
  if has_repo:
2008
2046
  repo = get_simulator_repository.sync(client=client, simulator_id=sim_id, x_api_key=api_key)
2009
2047
  else:
plato/v1/cli/sim.py ADDED
@@ -0,0 +1,11 @@
1
+ """Plato CLI - Simulator commands (stub)."""
2
+
3
+ import typer
4
+
5
+ sim_app = typer.Typer(help="Simulator management commands (coming soon)")
6
+
7
+
8
+ @sim_app.command()
9
+ def list():
10
+ """List available simulators."""
11
+ typer.echo("Simulator list command not yet implemented.")