plato-sdk-v2 2.8.1__py3-none-any.whl → 2.8.3__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/cli/pm.py CHANGED
@@ -22,12 +22,10 @@ from plato._generated.api.v1.simulator import (
22
22
  update_simulator_status,
23
23
  update_tag,
24
24
  )
25
- from plato._generated.api.v2.sessions import state as sessions_state
26
25
  from plato._generated.models import (
27
26
  AddReviewRequest,
28
27
  AppApiV1SimulatorRoutesUpdateSimulatorRequest,
29
28
  Authentication,
30
- Flow,
31
29
  Outcome,
32
30
  ReviewType,
33
31
  UpdateStatusRequest,
@@ -42,8 +40,10 @@ from plato.cli.utils import (
42
40
  require_sandbox_field,
43
41
  require_sandbox_state,
44
42
  )
43
+ from plato.v1.flow_executor import FlowExecutor
44
+ from plato.v1.models.flow import Flow
45
+ from plato.v1.sdk import Plato
45
46
  from plato.v2.async_.client import AsyncPlato
46
- from plato.v2.async_.flow_executor import FlowExecutor
47
47
  from plato.v2.types import Env
48
48
 
49
49
  # =============================================================================
@@ -318,25 +318,30 @@ def review_base(
318
318
  )
319
319
 
320
320
  async def _review_base():
321
+ import warnings
322
+
321
323
  base_url = _get_base_url()
322
- plato = AsyncPlato(api_key=api_key, base_url=base_url)
323
- session = None
324
+ # v1 SDK expects base_url to include /api suffix
325
+ v1_base_url = f"{base_url}/api"
326
+ # Suppress the deprecation warning from v1 Plato
327
+ with warnings.catch_warnings():
328
+ warnings.simplefilter("ignore", DeprecationWarning)
329
+ plato = Plato(api_key=api_key, base_url=v1_base_url)
330
+ env = None
324
331
  playwright = None
325
332
  browser = None
326
- login_result = None
327
333
 
328
334
  try:
329
- http_client = plato._http
330
-
331
335
  # simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
332
336
  assert simulator_name is not None, "simulator_name must be set"
333
337
 
334
- # Get simulator by name
335
- sim = await get_simulator_by_name.asyncio(
336
- client=http_client,
337
- name=simulator_name,
338
- x_api_key=api_key,
339
- )
338
+ # Get simulator by name using httpx for API calls
339
+ async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as http_client:
340
+ sim = await get_simulator_by_name.asyncio(
341
+ client=http_client,
342
+ name=simulator_name,
343
+ x_api_key=api_key,
344
+ )
340
345
  simulator_id = sim.id
341
346
  current_config = sim.config or {}
342
347
  current_status = current_config.get("status", "not_started")
@@ -357,26 +362,27 @@ def review_base(
357
362
 
358
363
  console.print(f"[cyan]Using artifact:[/cyan] {artifact_id}")
359
364
 
360
- # Try to create session with environment from artifact
365
+ # Try to create environment from artifact using v1 API
361
366
  try:
362
367
  console.print(f"[cyan]Creating {simulator_name} environment with artifact {artifact_id}...[/cyan]")
363
- session = await plato.sessions.create(
364
- envs=[Env.artifact(artifact_id)],
365
- timeout=300,
368
+ env = await plato.make_environment(
369
+ env_id=simulator_name,
370
+ artifact_id=artifact_id,
366
371
  )
367
- console.print(f"[green]✅ Session created: {session.session_id}[/green]")
372
+ console.print(f"[green]✅ Environment created: {env.id}[/green]")
373
+
374
+ # Wait for environment to be ready
375
+ console.print("[cyan]Waiting for environment to be ready...[/cyan]")
376
+ await env.wait_for_ready(timeout=300)
377
+ console.print("[green]✅ Environment ready![/green]")
368
378
 
369
379
  # Reset
370
380
  console.print("[cyan]Resetting environment...[/cyan]")
371
- await session.reset()
381
+ await env.reset()
372
382
  console.print("[green]✅ Environment reset complete![/green]")
373
383
 
374
- # Get public URL
375
- public_urls = await session.get_public_url()
376
- first_alias = session.envs[0].alias if session.envs else None
377
- public_url = public_urls.get(first_alias) if first_alias else None
378
- if not public_url and public_urls:
379
- public_url = list(public_urls.values())[0]
384
+ # Get public URL (v1 returns string directly)
385
+ public_url = await env.get_public_url()
380
386
  console.print(f"[cyan]Public URL:[/cyan] {public_url}")
381
387
 
382
388
  # Launch Playwright browser and login
@@ -452,70 +458,66 @@ def review_base(
452
458
  except Exception as e:
453
459
  console.print(f"[yellow]⚠️ Flow execution error: {e}[/yellow]")
454
460
  else:
455
- # Use default login via session.login()
461
+ # Use default login via env.login() (v1 API takes Page, not Browser)
456
462
  if fake_time:
457
463
  console.print("[yellow]⚠️ --clock with default login may not work correctly.[/yellow]")
458
464
  console.print("[yellow] Use --local with a flow file for reliable clock testing.[/yellow]")
459
465
  try:
460
- login_result = await session.login(browser, dataset="base")
461
- page = list(login_result.pages.values())[0] if login_result.pages else None
462
- console.print("[green]✅ Logged into environment[/green]")
463
- except Exception as e:
464
- console.print(f"[yellow]⚠️ Login error: {e}[/yellow]")
466
+ # Create page and navigate to public URL first
465
467
  page = await browser.new_page()
466
- # Install fake clock on fallback page
467
468
  if fake_time:
468
469
  await page.clock.install(time=fake_time)
469
470
  console.print(f"[green]✅ Fake clock installed: {fake_time.isoformat()}[/green]")
470
471
  if public_url:
471
472
  await page.goto(public_url)
473
+ # v1 login takes a Page and uses from_api=True to fetch flows from server
474
+ await env.login(page, dataset="base", from_api=True)
475
+ console.print("[green]✅ Logged into environment[/green]")
476
+ except Exception as e:
477
+ console.print(f"[yellow]⚠️ Login error: {e}[/yellow]")
478
+ # Page already created above, just navigate if not already done
479
+ if public_url and page:
480
+ try:
481
+ await page.goto(public_url)
482
+ except Exception:
483
+ pass
472
484
 
473
485
  # ALWAYS check state after login to verify no mutations
474
486
  console.print("\n[cyan]Checking environment state after login...[/cyan]")
475
487
  has_mutations = False
476
488
  has_errors = False
477
489
  try:
478
- state_response = await sessions_state.asyncio(
479
- client=http_client,
480
- session_id=session.session_id,
481
- merge_mutations=True,
482
- x_api_key=api_key,
483
- )
484
- if state_response and state_response.results:
485
- for jid, result in state_response.results.items():
486
- state_data = result.state if hasattr(result, "state") else result
487
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
488
-
489
- if isinstance(state_data, dict):
490
- # Check for error in state response
491
- if "error" in state_data:
492
- has_errors = True
493
- console.print("\n[bold red]❌ State API Error:[/bold red]")
494
- console.print(f"[red]{state_data['error']}[/red]")
495
- continue
496
-
497
- mutations = state_data.pop("mutations", [])
498
- console.print("\n[bold]State:[/bold]")
499
- console.print(json.dumps(state_data, indent=2, default=str))
500
- if mutations:
501
- has_mutations = True
502
- console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
503
- console.print(json.dumps(mutations, indent=2, default=str))
504
- else:
505
- console.print("\n[green]No mutations recorded[/green]")
506
- else:
507
- console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
508
-
509
- if has_errors:
510
- console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
511
- console.print("[yellow]The worker may not be properly connected.[/yellow]")
512
- elif has_mutations:
513
- console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
514
- console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
490
+ # v1 API: env.get_state() returns state dict directly
491
+ state_data = await env.get_state(merge_mutations=True)
492
+ console.print(f"\n[bold cyan]Environment {env.id}:[/bold cyan]")
493
+
494
+ if isinstance(state_data, dict):
495
+ # Check for error in state response (only if error has a truthy value)
496
+ if state_data.get("error"):
497
+ has_errors = True
498
+ console.print("\n[bold red]❌ State API Error:[/bold red]")
499
+ console.print(f"[red]{state_data['error']}[/red]")
515
500
  else:
516
- console.print("\n[bold green]✅ Login flow verified - no mutations created[/bold green]")
501
+ mutations = state_data.pop("mutations", [])
502
+ console.print("\n[bold]State:[/bold]")
503
+ console.print(json.dumps(state_data, indent=2, default=str))
504
+ if mutations:
505
+ has_mutations = True
506
+ console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
507
+ console.print(json.dumps(mutations, indent=2, default=str))
508
+ else:
509
+ console.print("\n[green]No mutations recorded[/green]")
517
510
  else:
518
- console.print("[yellow]No state data available[/yellow]")
511
+ console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
512
+
513
+ if has_errors:
514
+ console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
515
+ console.print("[yellow]The worker may not be properly connected.[/yellow]")
516
+ elif has_mutations:
517
+ console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
518
+ console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
519
+ else:
520
+ console.print("\n[bold green]✅ Login flow verified - no mutations created[/bold green]")
519
521
  except Exception as e:
520
522
  console.print(f"[red]❌ Error getting state: {e}[/red]")
521
523
 
@@ -562,37 +564,26 @@ def review_base(
562
564
  elif command in ["state", "s"]:
563
565
  console.print("\n[cyan]Getting environment state with mutations...[/cyan]")
564
566
  try:
565
- # Call API directly with merge_mutations=True to include mutations
566
- state_response = await sessions_state.asyncio(
567
- client=http_client,
568
- session_id=session.session_id,
569
- merge_mutations=True,
570
- x_api_key=api_key,
571
- )
572
- if state_response and state_response.results:
573
- for jid, result in state_response.results.items():
574
- state_data = result.state if hasattr(result, "state") else result
575
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
576
-
577
- if isinstance(state_data, dict):
578
- # Check for error in state response
579
- if "error" in state_data:
580
- console.print("\n[bold red]❌ State API Error:[/bold red]")
581
- console.print(f"[red]{state_data['error']}[/red]")
582
- continue
583
-
584
- mutations = state_data.pop("mutations", [])
585
- console.print("\n[bold]State:[/bold]")
586
- console.print(json.dumps(state_data, indent=2, default=str))
587
- if mutations:
588
- console.print(f"\n[bold]Mutations ({len(mutations)}):[/bold]")
589
- console.print(json.dumps(mutations, indent=2, default=str))
590
- else:
591
- console.print("\n[yellow]No mutations recorded[/yellow]")
567
+ # v1 API: env.get_state() returns state dict directly
568
+ state_data = await env.get_state(merge_mutations=True)
569
+ console.print(f"\n[bold cyan]Environment {env.id}:[/bold cyan]")
570
+
571
+ if isinstance(state_data, dict):
572
+ # Check for error in state response (only if error has a truthy value)
573
+ if state_data.get("error"):
574
+ console.print("\n[bold red]❌ State API Error:[/bold red]")
575
+ console.print(f"[red]{state_data['error']}[/red]")
576
+ else:
577
+ mutations = state_data.pop("mutations", [])
578
+ console.print("\n[bold]State:[/bold]")
579
+ console.print(json.dumps(state_data, indent=2, default=str))
580
+ if mutations:
581
+ console.print(f"\n[bold]Mutations ({len(mutations)}):[/bold]")
582
+ console.print(json.dumps(mutations, indent=2, default=str))
592
583
  else:
593
- console.print(json.dumps(state_data, indent=2, default=str))
584
+ console.print("\n[yellow]No mutations recorded[/yellow]")
594
585
  else:
595
- console.print("[yellow]No state data available[/yellow]")
586
+ console.print(json.dumps(state_data, indent=2, default=str))
596
587
  console.print()
597
588
  except Exception as e:
598
589
  console.print(f"[red]❌ Error getting state: {e}[/red]")
@@ -633,56 +624,60 @@ def review_base(
633
624
  validate_status_transition(current_status, "env_review_requested", "review base reject")
634
625
  new_status = "env_in_progress"
635
626
 
636
- # Update status
637
- await update_simulator_status.asyncio(
638
- client=http_client,
639
- simulator_id=simulator_id,
640
- body=UpdateStatusRequest(status=new_status),
641
- x_api_key=api_key,
642
- )
643
-
644
- # Add review if rejecting
645
- if outcome == "reject":
646
- comments = ""
647
- while not comments:
648
- comments = typer.prompt("Comments (required for reject)").strip()
649
- if not comments:
650
- console.print("[yellow]Comments are required when rejecting. Please provide feedback.[/yellow]")
651
-
652
- await add_simulator_review.asyncio(
653
- client=http_client,
627
+ # Create httpx client for API calls
628
+ async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as api_client:
629
+ # Update status
630
+ await update_simulator_status.asyncio(
631
+ client=api_client,
654
632
  simulator_id=simulator_id,
655
- body=AddReviewRequest(
656
- review_type=ReviewType.env,
657
- outcome=Outcome.reject,
658
- artifact_id=artifact_id,
659
- comments=comments,
660
- ),
633
+ body=UpdateStatusRequest(status=new_status),
661
634
  x_api_key=api_key,
662
635
  )
663
636
 
664
- console.print(f"[green]✅ Review submitted: {outcome}[/green]")
665
- console.print(f"[cyan]Status:[/cyan] {current_status} {new_status}")
666
-
667
- # If passed, automatically tag artifact as prod-latest
668
- if outcome == "pass" and artifact_id:
669
- console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
670
- try:
671
- # simulator_name and artifact_id are guaranteed to be set at this point
672
- assert simulator_name is not None
673
- await update_tag.asyncio(
674
- client=http_client,
675
- body=UpdateTagRequest(
676
- simulator_name=simulator_name,
637
+ # Add review if rejecting
638
+ if outcome == "reject":
639
+ comments = ""
640
+ while not comments:
641
+ comments = typer.prompt("Comments (required for reject)").strip()
642
+ if not comments:
643
+ console.print(
644
+ "[yellow]Comments are required when rejecting. Please provide feedback.[/yellow]"
645
+ )
646
+
647
+ await add_simulator_review.asyncio(
648
+ client=api_client,
649
+ simulator_id=simulator_id,
650
+ body=AddReviewRequest(
651
+ review_type=ReviewType.env,
652
+ outcome=Outcome.reject,
677
653
  artifact_id=artifact_id,
678
- tag_name="prod-latest",
679
- dataset="base",
654
+ comments=comments,
680
655
  ),
681
656
  x_api_key=api_key,
682
657
  )
683
- console.print(f"[green]✅ Tagged {artifact_id[:8]}... as prod-latest[/green]")
684
- except Exception as e:
685
- console.print(f"[yellow]⚠️ Could not tag as prod-latest: {e}[/yellow]")
658
+
659
+ console.print(f"[green]✅ Review submitted: {outcome}[/green]")
660
+ console.print(f"[cyan]Status:[/cyan] {current_status} {new_status}")
661
+
662
+ # If passed, automatically tag artifact as prod-latest
663
+ if outcome == "pass" and artifact_id:
664
+ console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
665
+ try:
666
+ # simulator_name and artifact_id are guaranteed to be set at this point
667
+ assert simulator_name is not None
668
+ await update_tag.asyncio(
669
+ client=api_client,
670
+ body=UpdateTagRequest(
671
+ simulator_name=simulator_name,
672
+ artifact_id=artifact_id,
673
+ tag_name="prod-latest",
674
+ dataset="base",
675
+ ),
676
+ x_api_key=api_key,
677
+ )
678
+ console.print(f"[green]✅ Tagged {artifact_id[:8]}... as prod-latest[/green]")
679
+ except Exception as e:
680
+ console.print(f"[yellow]⚠️ Could not tag as prod-latest: {e}[/yellow]")
686
681
 
687
682
  except typer.Exit:
688
683
  raise
@@ -693,8 +688,6 @@ def review_base(
693
688
  finally:
694
689
  # Cleanup
695
690
  try:
696
- if login_result and login_result.context:
697
- await login_result.context.close()
698
691
  if browser:
699
692
  await browser.close()
700
693
  if playwright:
@@ -702,13 +695,13 @@ def review_base(
702
695
  except Exception as e:
703
696
  console.print(f"[yellow]⚠️ Browser cleanup error: {e}[/yellow]")
704
697
 
705
- if session:
698
+ if env:
706
699
  try:
707
- console.print("[cyan]Shutting down session...[/cyan]")
708
- await session.close()
709
- console.print("[green]✅ Session shut down[/green]")
700
+ console.print("[cyan]Shutting down environment...[/cyan]")
701
+ await env.close()
702
+ console.print("[green]✅ Environment shut down[/green]")
710
703
  except Exception as e:
711
- console.print(f"[yellow]⚠️ Session cleanup error: {e}[/yellow]")
704
+ console.print(f"[yellow]⚠️ Environment cleanup error: {e}[/yellow]")
712
705
 
713
706
  try:
714
707
  await plato.close()
@@ -733,18 +726,25 @@ def review_data(
733
726
  help="Artifact UUID to review. If not provided, uses server's data_artifact_id.",
734
727
  ),
735
728
  ):
736
- """Launch browser with EnvGen Recorder extension for data review.
729
+ """
730
+ Launch browser with Data Review extension for data review.
737
731
 
738
- Opens Chrome with the EnvGen Recorder extension installed for reviewing
739
- data artifacts. The extension sidebar can be used to record and submit reviews.
740
- Press Control-C to exit when done.
732
+ Opens Chrome with the Data Review extension installed for reviewing
733
+ data artifacts. Close the browser when done.
741
734
 
742
- Requires simulator status: data_review_requested
735
+ SPECIFYING SIMULATOR AND ARTIFACT:
743
736
 
744
- Options:
745
- -s, --simulator: Simulator name. Supports colon notation for artifact:
746
- '-s sim' (uses server's data_artifact_id) or '-s sim:<uuid>'
747
- -a, --artifact: Explicit artifact UUID to review. Overrides server's value.
737
+ -s <simulator> Use server's data_artifact_id
738
+ -s <simulator> -a <artifact-uuid> Explicit artifact
739
+ -s <simulator>:<artifact-uuid> Colon notation (same as above)
740
+
741
+ EXAMPLES:
742
+
743
+ plato pm review data -s fathom
744
+ plato pm review data -s fathom -a e9c25ca5-1234-5678-9abc-def012345678
745
+ plato pm review data -s fathom:e9c25ca5-1234-5678-9abc-def012345678
746
+
747
+ Requires simulator status: data_review_requested
748
748
  """
749
749
  api_key = require_api_key()
750
750
 
@@ -753,8 +753,6 @@ def review_data(
753
753
  simulator, artifact, require_artifact=False, command_name="review data"
754
754
  )
755
755
 
756
- # Determine target URL based on simulator
757
- target_url = f"https://{simulator_name}.web.plato.so"
758
756
  console.print(f"[cyan]Simulator:[/cyan] {simulator_name}")
759
757
 
760
758
  # Fetch simulator config and get artifact ID if not provided
@@ -799,45 +797,75 @@ def review_data(
799
797
  console.print(f"[cyan]Artifact:[/cyan] {artifact_id}")
800
798
 
801
799
  # Find Chrome extension source
802
- package_dir = Path(__file__).resolve().parent.parent # v1/
800
+ package_dir = Path(__file__).resolve().parent.parent # plato/
803
801
  is_installed = "site-packages" in str(package_dir)
804
802
 
805
803
  if is_installed:
806
- extension_source_path = package_dir / "extensions" / "envgen-recorder-old"
804
+ extension_source_path = package_dir / "extensions" / "data-review"
807
805
  else:
808
- repo_root = package_dir.parent.parent.parent # plato-client/
809
- extension_source_path = repo_root / "extensions" / "envgen-recorder-old"
806
+ repo_root = package_dir.parent.parent # plato-client/
807
+ extension_source_path = repo_root / "extensions" / "data-review"
810
808
 
811
809
  # Fallback to env var
812
810
  if not extension_source_path.exists():
813
811
  plato_client_dir_env = os.getenv("PLATO_CLIENT_DIR")
814
812
  if plato_client_dir_env:
815
- env_path = Path(plato_client_dir_env) / "extensions" / "envgen-recorder-old"
813
+ env_path = Path(plato_client_dir_env) / "extensions" / "data-review"
816
814
  if env_path.exists():
817
815
  extension_source_path = env_path
818
816
 
819
817
  if not extension_source_path.exists():
820
- console.print("[red]❌ EnvGen Recorder extension not found[/red]")
818
+ console.print("[red]❌ Data Review extension not found[/red]")
821
819
  console.print(f"\n[yellow]Expected location:[/yellow] {extension_source_path}")
822
820
  raise typer.Exit(1)
823
821
 
824
822
  # Copy extension to temp directory
825
823
  temp_ext_dir = Path(tempfile.mkdtemp(prefix="plato-extension-"))
826
- extension_path = temp_ext_dir / "envgen-recorder"
824
+ extension_path = temp_ext_dir / "data-review"
827
825
 
828
826
  console.print("[cyan]Copying extension to temp directory...[/cyan]")
829
827
  shutil.copytree(extension_source_path, extension_path, dirs_exist_ok=False)
830
828
  console.print(f"[green]✅ Extension copied to: {extension_path}[/green]")
831
829
 
832
830
  async def _review_data():
831
+ base_url = _get_base_url()
832
+ plato = AsyncPlato(api_key=api_key, base_url=base_url)
833
+ session = None
833
834
  playwright = None
834
835
  browser = None
835
836
 
836
837
  try:
838
+ # Check if we have an artifact ID to create a session
839
+ if not artifact_id:
840
+ console.print("[red]❌ No artifact ID available. Cannot create session.[/red]")
841
+ console.print("[yellow]Specify artifact with: plato pm review data -s simulator:artifact_id[/yellow]")
842
+ raise typer.Exit(1)
843
+
844
+ # Create session with artifact
845
+ console.print(f"[cyan]Creating {simulator_name} environment with artifact {artifact_id}...[/cyan]")
846
+ session = await plato.sessions.create(
847
+ envs=[Env.artifact(artifact_id)],
848
+ timeout=300,
849
+ )
850
+ console.print(f"[green]✅ Session created: {session.session_id}[/green]")
851
+
852
+ # Reset environment
853
+ console.print("[cyan]Resetting environment...[/cyan]")
854
+ await session.reset()
855
+ console.print("[green]✅ Environment reset complete![/green]")
856
+
857
+ # Get public URL
858
+ public_urls = await session.get_public_url()
859
+ first_alias = session.envs[0].alias if session.envs else None
860
+ public_url = public_urls.get(first_alias) if first_alias else None
861
+ if not public_url and public_urls:
862
+ public_url = list(public_urls.values())[0]
863
+ console.print(f"[cyan]Public URL:[/cyan] {public_url}")
864
+
837
865
  user_data_dir = Path.home() / ".plato" / "chrome-data"
838
866
  user_data_dir.mkdir(parents=True, exist_ok=True)
839
867
 
840
- console.print("[cyan]Launching Chrome with EnvGen Recorder extension...[/cyan]")
868
+ console.print("[cyan]Launching Chrome with Data Review extension...[/cyan]")
841
869
 
842
870
  from playwright.async_api import async_playwright
843
871
 
@@ -878,13 +906,14 @@ def review_data(
878
906
  else:
879
907
  console.print("[yellow]⚠️ Could not find extension ID. Please set API key manually.[/yellow]")
880
908
 
881
- # Step 1: Navigate to target URL first
882
- console.print(f"[cyan]Navigating to {target_url}...[/cyan]")
909
+ # Navigate to public URL (user logs in manually with displayed credentials)
910
+ console.print("[cyan]Opening environment...[/cyan]")
883
911
  main_page = await browser.new_page()
884
- await main_page.goto(target_url, wait_until="domcontentloaded")
885
- console.print(f"[green]✅ Loaded: {target_url}[/green]")
912
+ if public_url:
913
+ await main_page.goto(public_url)
914
+ console.print(f"[green]✅ Loaded: {public_url}[/green]")
886
915
 
887
- # Step 2: Use options page to set API key
916
+ # Use options page to set API key
888
917
  if extension_id:
889
918
  options_page = await browser.new_page()
890
919
  try:
@@ -908,17 +937,16 @@ def review_data(
908
937
  await options_page.close()
909
938
 
910
939
  # Bring main page to front
911
- await main_page.bring_to_front()
940
+ if main_page:
941
+ await main_page.bring_to_front()
912
942
 
913
943
  console.print()
914
944
  console.print("[bold]Instructions:[/bold]")
915
- console.print(" 1. Click the EnvGen Recorder extension icon to open the sidebar")
916
- if simulator:
917
- console.print(f" 2. Click 'Configure Session' and enter '{simulator}' as the simulator name")
918
- else:
919
- console.print(" 2. Click 'Configure Session' and enter a simulator name")
920
- console.print(" 3. Use the extension to record and submit reviews")
921
- console.print(" 4. When done, press Control-C to exit")
945
+ console.print(" 1. Click the Data Review extension icon to open the sidebar")
946
+ console.print(f" 2. Enter '{simulator_name}' as the simulator name and click Start Review")
947
+ console.print(" 3. Take screenshots and add comments for any issues")
948
+ console.print(" 4. Select Pass or Reject and submit the review")
949
+ console.print(" 5. When done, press Control-C to exit")
922
950
 
923
951
  # Show recent review if available
924
952
  if recent_review:
@@ -931,10 +959,20 @@ def review_data(
931
959
  else:
932
960
  console.print(f"[bold green]📋 Most Recent Data Review: PASSED[/bold green] ({timestamp})")
933
961
 
934
- comments = recent_review.get("comments")
935
- if comments:
962
+ # Handle both old 'comments' field and new 'sim_comments' structure
963
+ sim_comments = recent_review.get("sim_comments")
964
+ if sim_comments:
936
965
  console.print("\n[yellow]Reviewer Comments:[/yellow]")
937
- console.print(f" {comments}")
966
+ for i, item in enumerate(sim_comments, 1):
967
+ comment_text = item.get("comment", "")
968
+ if comment_text:
969
+ console.print(f" {i}. {comment_text}")
970
+ else:
971
+ # Fallback to old comments field
972
+ comments = recent_review.get("comments")
973
+ if comments:
974
+ console.print("\n[yellow]Reviewer Comments:[/yellow]")
975
+ console.print(f" {comments}")
938
976
  console.print("=" * 60)
939
977
 
940
978
  console.print()
@@ -955,6 +993,8 @@ def review_data(
955
993
 
956
994
  finally:
957
995
  try:
996
+ if session:
997
+ await session.close()
958
998
  if browser:
959
999
  await browser.close()
960
1000
  if playwright:
@@ -962,7 +1002,7 @@ def review_data(
962
1002
  if temp_ext_dir.exists():
963
1003
  shutil.rmtree(temp_ext_dir, ignore_errors=True)
964
1004
  except Exception as e:
965
- console.print(f"[yellow]⚠️ Browser cleanup error: {e}[/yellow]")
1005
+ console.print(f"[yellow]⚠️ Cleanup error: {e}[/yellow]")
966
1006
 
967
1007
  handle_async(_review_data())
968
1008
 
plato/cli/sandbox.py CHANGED
@@ -407,8 +407,9 @@ def sandbox_snapshot(
407
407
 
408
408
  Captures VM state and database for later restoration.
409
409
 
410
- Example:
411
- plato sandbox snapshot
410
+ Examples:
411
+ plato sandbox snapshot # Uses mode from state.json
412
+ plato sandbox snapshot --mode config # Override to pass local plato-config.yml and flows to artifact
412
413
  """
413
414
  with sandbox_context(working_dir, json_output, verbose) as (client, out):
414
415
  out.console.print("Creating snapshot...")