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 +226 -186
- plato/cli/sandbox.py +3 -2
- plato/v1/cli/pm.py +10 -1252
- plato/v2/sync/sandbox.py +47 -30
- {plato_sdk_v2-2.8.1.dist-info → plato_sdk_v2-2.8.3.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.8.1.dist-info → plato_sdk_v2-2.8.3.dist-info}/RECORD +8 -8
- {plato_sdk_v2-2.8.1.dist-info → plato_sdk_v2-2.8.3.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.8.1.dist-info → plato_sdk_v2-2.8.3.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
368
|
+
env = await plato.make_environment(
|
|
369
|
+
env_id=simulator_name,
|
|
370
|
+
artifact_id=artifact_id,
|
|
366
371
|
)
|
|
367
|
-
console.print(f"[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
|
|
381
|
+
await env.reset()
|
|
372
382
|
console.print("[green]✅ Environment reset complete![/green]")
|
|
373
383
|
|
|
374
|
-
# Get public URL
|
|
375
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
console.print(f"
|
|
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
|
-
|
|
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]
|
|
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
|
-
#
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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(
|
|
584
|
+
console.print("\n[yellow]No mutations recorded[/yellow]")
|
|
594
585
|
else:
|
|
595
|
-
console.print(
|
|
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
|
-
#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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=
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
dataset="base",
|
|
654
|
+
comments=comments,
|
|
680
655
|
),
|
|
681
656
|
x_api_key=api_key,
|
|
682
657
|
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
698
|
+
if env:
|
|
706
699
|
try:
|
|
707
|
-
console.print("[cyan]Shutting down
|
|
708
|
-
await
|
|
709
|
-
console.print("[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]⚠️
|
|
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
|
-
"""
|
|
729
|
+
"""
|
|
730
|
+
Launch browser with Data Review extension for data review.
|
|
737
731
|
|
|
738
|
-
Opens Chrome with the
|
|
739
|
-
data artifacts.
|
|
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
|
-
|
|
735
|
+
SPECIFYING SIMULATOR AND ARTIFACT:
|
|
743
736
|
|
|
744
|
-
|
|
745
|
-
-s
|
|
746
|
-
|
|
747
|
-
|
|
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 #
|
|
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" / "
|
|
804
|
+
extension_source_path = package_dir / "extensions" / "data-review"
|
|
807
805
|
else:
|
|
808
|
-
repo_root = package_dir.parent.parent
|
|
809
|
-
extension_source_path = repo_root / "extensions" / "
|
|
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" / "
|
|
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]❌
|
|
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 / "
|
|
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
|
|
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
|
-
#
|
|
882
|
-
console.print(
|
|
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
|
-
|
|
885
|
-
|
|
912
|
+
if public_url:
|
|
913
|
+
await main_page.goto(public_url)
|
|
914
|
+
console.print(f"[green]✅ Loaded: {public_url}[/green]")
|
|
886
915
|
|
|
887
|
-
#
|
|
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
|
-
|
|
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
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
|
935
|
-
|
|
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
|
-
|
|
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]⚠️
|
|
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
|
-
|
|
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...")
|