plato-sdk-v2 2.1.17__py3-none-any.whl → 2.2.1__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/_sims_generator/instruction.py +4 -3
- plato/agents/trajectory.py +4 -4
- plato/chronos/models/__init__.py +1 -1
- plato/sims/cli.py +7 -4
- plato/sims/registry.py +0 -1
- plato/v1/cli/agent.py +10 -0
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +80 -47
- plato/v1/cli/sandbox.py +45 -12
- plato/v1/cli/sim.py +11 -0
- plato/v1/cli/verify.py +1269 -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/async_/session.py +4 -4
- plato/v2/sync/session.py +4 -4
- plato/worlds/config.py +2 -2
- {plato_sdk_v2-2.1.17.dist-info → plato_sdk_v2-2.2.1.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.1.17.dist-info → plato_sdk_v2-2.2.1.dist-info}/RECORD +25 -23
- {plato_sdk_v2-2.1.17.dist-info → plato_sdk_v2-2.2.1.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.1.17.dist-info → plato_sdk_v2-2.2.1.dist-info}/entry_points.txt +0 -0
|
@@ -157,7 +157,8 @@ class InstructionGenerator:
|
|
|
157
157
|
def _copy_instructions_yaml(self):
|
|
158
158
|
"""Bundle the instructions.yaml config with the package."""
|
|
159
159
|
# We'll generate a cleaned version of the config
|
|
160
|
-
|
|
160
|
+
env_vars: dict[str, dict[str, str | int]] = {}
|
|
161
|
+
config_data: dict[str, str | dict] = {
|
|
161
162
|
"type": "instruction",
|
|
162
163
|
"env_prefix": self.config.env_prefix,
|
|
163
164
|
"title": self.config.title,
|
|
@@ -166,7 +167,7 @@ class InstructionGenerator:
|
|
|
166
167
|
"services": {
|
|
167
168
|
name: {"port": svc.port, "description": svc.description} for name, svc in self.config.services.items()
|
|
168
169
|
},
|
|
169
|
-
"env_vars":
|
|
170
|
+
"env_vars": env_vars,
|
|
170
171
|
"instructions": self.config.instructions,
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -178,7 +179,7 @@ class InstructionGenerator:
|
|
|
178
179
|
var_data["default"] = var.default
|
|
179
180
|
if var.env_key:
|
|
180
181
|
var_data["env_key"] = var.env_key
|
|
181
|
-
|
|
182
|
+
env_vars[name] = var_data
|
|
182
183
|
|
|
183
184
|
with open(self.output / "instructions.yaml", "w") as f:
|
|
184
185
|
yaml.dump(config_data, f, default_flow_style=False, sort_keys=False)
|
plato/agents/trajectory.py
CHANGED
|
@@ -9,7 +9,7 @@ Spec: https://harborframework.com/docs/trajectory-format
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from datetime import datetime
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
13
|
from typing import Any, Literal
|
|
14
14
|
|
|
15
15
|
from pydantic import BaseModel, Field
|
|
@@ -115,7 +115,7 @@ class Step(BaseModel):
|
|
|
115
115
|
"""Create a user step."""
|
|
116
116
|
return cls(
|
|
117
117
|
step_id=step_id,
|
|
118
|
-
timestamp=datetime.
|
|
118
|
+
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
119
119
|
source="user",
|
|
120
120
|
message=message,
|
|
121
121
|
**kwargs,
|
|
@@ -136,7 +136,7 @@ class Step(BaseModel):
|
|
|
136
136
|
"""Create an agent step."""
|
|
137
137
|
return cls(
|
|
138
138
|
step_id=step_id,
|
|
139
|
-
timestamp=datetime.
|
|
139
|
+
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
140
140
|
source="agent",
|
|
141
141
|
message=message,
|
|
142
142
|
model_name=model_name,
|
|
@@ -152,7 +152,7 @@ class Step(BaseModel):
|
|
|
152
152
|
"""Create a system step."""
|
|
153
153
|
return cls(
|
|
154
154
|
step_id=step_id,
|
|
155
|
-
timestamp=datetime.
|
|
155
|
+
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
156
156
|
source="system",
|
|
157
157
|
message=message,
|
|
158
158
|
**kwargs,
|
plato/chronos/models/__init__.py
CHANGED
plato/sims/cli.py
CHANGED
|
@@ -241,9 +241,11 @@ def _get_function_info(
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
# Extract parameters
|
|
244
|
+
params_list = result["params"]
|
|
245
|
+
assert isinstance(params_list, list) # type narrowing for ty
|
|
244
246
|
for param_name, param in sig.parameters.items():
|
|
245
247
|
if param_name == "client":
|
|
246
|
-
|
|
248
|
+
params_list.append("client.httpx")
|
|
247
249
|
continue
|
|
248
250
|
|
|
249
251
|
# Get type from hints or annotation
|
|
@@ -251,10 +253,10 @@ def _get_function_info(
|
|
|
251
253
|
type_str = _format_type_annotation(param_type)
|
|
252
254
|
|
|
253
255
|
if param.default is inspect.Parameter.empty:
|
|
254
|
-
|
|
256
|
+
params_list.append(f"{param_name}: {type_str}")
|
|
255
257
|
else:
|
|
256
258
|
default_repr = repr(param.default) if param.default is not None else "None"
|
|
257
|
-
|
|
259
|
+
params_list.append(f"{param_name}: {type_str} = {default_repr}")
|
|
258
260
|
|
|
259
261
|
# Check if this is the body parameter
|
|
260
262
|
if param_name == "body" and param_type is not inspect.Parameter.empty:
|
|
@@ -1162,7 +1164,8 @@ def cmd_publish(
|
|
|
1162
1164
|
generator_version_file.write_text(f"{generator_version}\n")
|
|
1163
1165
|
print(f" Updated {generator_version_file}")
|
|
1164
1166
|
|
|
1165
|
-
# Load spec
|
|
1167
|
+
# Load spec (spec_file is guaranteed non-None after check above)
|
|
1168
|
+
assert spec_file is not None
|
|
1166
1169
|
with open(spec_file) as f:
|
|
1167
1170
|
if spec_file.suffix == ".json":
|
|
1168
1171
|
spec = json.load(f)
|
plato/sims/registry.py
CHANGED
|
@@ -186,7 +186,6 @@ class SimsRegistry:
|
|
|
186
186
|
import importlib.resources
|
|
187
187
|
|
|
188
188
|
services = getattr(mod, "SERVICES", {})
|
|
189
|
-
env_prefix = getattr(mod, "ENV_PREFIX", "")
|
|
190
189
|
|
|
191
190
|
# Try to load full config from bundled instructions.yaml
|
|
192
191
|
env_vars_config: dict[str, dict[str, Any]] = {}
|
plato/v1/cli/agent.py
CHANGED
|
@@ -7,11 +7,15 @@ import shutil
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
12
|
import typer
|
|
12
13
|
|
|
13
14
|
from plato.v1.cli.utils import console, require_api_key
|
|
14
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from plato.agents.build import BuildConfig
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
def _extract_schemas(pkg_path: Path, package_name: str) -> tuple[dict | None, dict | None, dict | None]:
|
|
17
21
|
"""Extract Config, BuildConfig, and SecretsConfig schemas from the agent package.
|
|
@@ -325,6 +329,9 @@ def _publish_agent_image(
|
|
|
325
329
|
|
|
326
330
|
# Stream build output
|
|
327
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)
|
|
328
335
|
for line in iter(process.stdout.readline, ""):
|
|
329
336
|
line = line.rstrip()
|
|
330
337
|
if line:
|
|
@@ -562,6 +569,8 @@ def _publish_package(path: str, repo: str, dry_run: bool = False):
|
|
|
562
569
|
upload_url = f"{api_url}/v2/pypi/{repo}/"
|
|
563
570
|
console.print(f"\n[cyan]Uploading to {upload_url}...[/cyan]")
|
|
564
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"
|
|
565
574
|
try:
|
|
566
575
|
result = subprocess.run(
|
|
567
576
|
[
|
|
@@ -577,6 +586,7 @@ def _publish_package(path: str, repo: str, dry_run: bool = False):
|
|
|
577
586
|
],
|
|
578
587
|
capture_output=True,
|
|
579
588
|
text=True,
|
|
589
|
+
check=False,
|
|
580
590
|
)
|
|
581
591
|
|
|
582
592
|
if result.returncode == 0:
|
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
|
@@ -7,18 +7,11 @@ import re
|
|
|
7
7
|
import shutil
|
|
8
8
|
import tempfile
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
11
10
|
|
|
12
11
|
import httpx
|
|
13
12
|
import typer
|
|
14
13
|
from rich.table import Table
|
|
15
14
|
|
|
16
|
-
# UUID pattern for detecting artifact IDs in sim:artifact notation
|
|
17
|
-
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)
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
15
|
from plato._generated.api.v1.env import get_simulator_by_name, get_simulators
|
|
23
16
|
from plato._generated.api.v1.organization import get_organization_members
|
|
24
17
|
from plato._generated.api.v1.simulator import (
|
|
@@ -46,9 +39,17 @@ from plato.v1.cli.utils import (
|
|
|
46
39
|
require_sandbox_field,
|
|
47
40
|
require_sandbox_state,
|
|
48
41
|
)
|
|
42
|
+
from plato.v1.cli.verify import pm_verify_app
|
|
49
43
|
from plato.v2.async_.client import AsyncPlato
|
|
50
44
|
from plato.v2.types import Env
|
|
51
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
|
+
|
|
52
53
|
# =============================================================================
|
|
53
54
|
# APP STRUCTURE
|
|
54
55
|
# =============================================================================
|
|
@@ -61,6 +62,7 @@ submit_app = typer.Typer(help="Submit simulator artifacts for review")
|
|
|
61
62
|
pm_app.add_typer(list_app, name="list")
|
|
62
63
|
pm_app.add_typer(review_app, name="review")
|
|
63
64
|
pm_app.add_typer(submit_app, name="submit")
|
|
65
|
+
pm_app.add_typer(pm_verify_app, name="verify")
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
# =============================================================================
|
|
@@ -343,6 +345,9 @@ def review_base(
|
|
|
343
345
|
try:
|
|
344
346
|
http_client = plato._http
|
|
345
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
|
+
|
|
346
351
|
# Get simulator by name
|
|
347
352
|
sim = await get_simulator_by_name.asyncio(
|
|
348
353
|
client=http_client,
|
|
@@ -356,7 +361,7 @@ def review_base(
|
|
|
356
361
|
console.print(f"[cyan]Current status:[/cyan] {current_status}")
|
|
357
362
|
|
|
358
363
|
# Use provided artifact ID or fall back to base_artifact_id from server config
|
|
359
|
-
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")
|
|
360
365
|
if not artifact_id:
|
|
361
366
|
console.print("[red]❌ No artifact ID provided.[/red]")
|
|
362
367
|
console.print(
|
|
@@ -408,44 +413,57 @@ def review_base(
|
|
|
408
413
|
if public_url:
|
|
409
414
|
await page.goto(public_url)
|
|
410
415
|
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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]")
|
|
440
449
|
else:
|
|
441
|
-
console.print(
|
|
442
|
-
|
|
443
|
-
|
|
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]")
|
|
444
458
|
else:
|
|
445
|
-
console.print("[
|
|
446
|
-
|
|
447
|
-
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]")
|
|
448
464
|
|
|
465
|
+
# If skip_review, exit without interactive loop
|
|
466
|
+
if skip_review:
|
|
449
467
|
console.print("\n[cyan]Skipping interactive review (--skip-review)[/cyan]")
|
|
450
468
|
return
|
|
451
469
|
|
|
@@ -497,9 +515,16 @@ def review_base(
|
|
|
497
515
|
if state_response and state_response.results:
|
|
498
516
|
for jid, result in state_response.results.items():
|
|
499
517
|
state_data = result.state if hasattr(result, "state") else result
|
|
518
|
+
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
519
|
+
|
|
500
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
|
+
|
|
501
527
|
mutations = state_data.pop("mutations", [])
|
|
502
|
-
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
503
528
|
console.print("\n[bold]State:[/bold]")
|
|
504
529
|
console.print(json.dumps(state_data, indent=2, default=str))
|
|
505
530
|
if mutations:
|
|
@@ -508,7 +533,6 @@ def review_base(
|
|
|
508
533
|
else:
|
|
509
534
|
console.print("\n[yellow]No mutations recorded[/yellow]")
|
|
510
535
|
else:
|
|
511
|
-
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
512
536
|
console.print(json.dumps(state_data, indent=2, default=str))
|
|
513
537
|
else:
|
|
514
538
|
console.print("[yellow]No state data available[/yellow]")
|
|
@@ -584,9 +608,11 @@ def review_base(
|
|
|
584
608
|
console.print(f"[cyan]Status:[/cyan] {current_status} → {new_status}")
|
|
585
609
|
|
|
586
610
|
# If passed, automatically tag artifact as prod-latest
|
|
587
|
-
if outcome == "pass":
|
|
611
|
+
if outcome == "pass" and artifact_id:
|
|
588
612
|
console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
|
|
589
613
|
try:
|
|
614
|
+
# simulator_name and artifact_id are guaranteed to be set at this point
|
|
615
|
+
assert simulator_name is not None
|
|
590
616
|
await update_tag.asyncio(
|
|
591
617
|
client=http_client,
|
|
592
618
|
body=UpdateTagRequest(
|
|
@@ -686,6 +712,9 @@ def review_data(
|
|
|
686
712
|
|
|
687
713
|
async def _fetch_artifact_info():
|
|
688
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
|
+
|
|
689
718
|
base_url = _get_base_url()
|
|
690
719
|
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
|
|
691
720
|
try:
|
|
@@ -1087,6 +1116,10 @@ def submit_data(
|
|
|
1087
1116
|
)
|
|
1088
1117
|
|
|
1089
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
|
+
|
|
1090
1123
|
base_url = _get_base_url()
|
|
1091
1124
|
|
|
1092
1125
|
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
|
|
@@ -1117,7 +1150,7 @@ def submit_data(
|
|
|
1117
1150
|
x_api_key=api_key,
|
|
1118
1151
|
)
|
|
1119
1152
|
|
|
1120
|
-
# Set data_artifact_id via tag update
|
|
1153
|
+
# Set data_artifact_id via tag update (simulator_name and artifact_id already asserted above)
|
|
1121
1154
|
try:
|
|
1122
1155
|
await update_tag.asyncio(
|
|
1123
1156
|
client=client,
|
plato/v1/cli/sandbox.py
CHANGED
|
@@ -13,19 +13,12 @@ import tempfile
|
|
|
13
13
|
import time
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import TYPE_CHECKING
|
|
17
16
|
from urllib.parse import quote
|
|
18
17
|
|
|
19
18
|
import typer
|
|
20
19
|
import yaml
|
|
21
20
|
from rich.logging import RichHandler
|
|
22
21
|
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
# UUID pattern for detecting artifact IDs in colon notation
|
|
27
|
-
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)
|
|
28
|
-
|
|
29
22
|
from plato._generated.api.v1.gitea import (
|
|
30
23
|
create_simulator_repository,
|
|
31
24
|
get_accessible_simulators,
|
|
@@ -77,11 +70,16 @@ from plato.v1.cli.utils import (
|
|
|
77
70
|
require_sandbox_state,
|
|
78
71
|
save_sandbox_state,
|
|
79
72
|
)
|
|
73
|
+
from plato.v1.cli.verify import sandbox_verify_app
|
|
80
74
|
from plato.v2.async_.flow_executor import FlowExecutor
|
|
81
75
|
from plato.v2.sync.client import Plato as PlatoV2
|
|
82
76
|
from plato.v2.types import Env, SimConfigCompute
|
|
83
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
|
+
|
|
84
81
|
sandbox_app = typer.Typer(help="Manage sandboxes for simulator development")
|
|
82
|
+
sandbox_app.add_typer(sandbox_verify_app, name="verify")
|
|
85
83
|
|
|
86
84
|
|
|
87
85
|
def format_public_url_with_router_target(public_url: str | None, service_name: str | None) -> str | None:
|
|
@@ -426,9 +424,10 @@ def sandbox_start(
|
|
|
426
424
|
listeners=listeners_dict,
|
|
427
425
|
)
|
|
428
426
|
|
|
427
|
+
dataset_value = dataset_name or state_extras.get("dataset", "base")
|
|
429
428
|
setup_request = AppSchemasBuildModelsSetupSandboxRequest(
|
|
430
429
|
service=sim_name or "",
|
|
431
|
-
dataset=
|
|
430
|
+
dataset=str(dataset_value) if dataset_value else "",
|
|
432
431
|
plato_dataset_config=dataset_config_obj,
|
|
433
432
|
ssh_public_key=ssh_public_key,
|
|
434
433
|
)
|
|
@@ -1050,6 +1049,9 @@ def sandbox_start_worker(
|
|
|
1050
1049
|
console.print(f"[cyan]Waiting for worker to be ready (timeout: {wait_timeout}s)...[/cyan]")
|
|
1051
1050
|
|
|
1052
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)
|
|
1053
1055
|
start_time = time.time()
|
|
1054
1056
|
poll_interval = 10 # seconds between polls
|
|
1055
1057
|
worker_ready = False
|
|
@@ -1533,9 +1535,21 @@ def sandbox_flow(
|
|
|
1533
1535
|
console.print(f"[red]❌ Failed to fetch flows from API: {e}[/red]")
|
|
1534
1536
|
raise typer.Exit(1) from e
|
|
1535
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
|
+
|
|
1536
1546
|
console.print(f"[cyan]URL: {url}[/cyan]")
|
|
1537
1547
|
console.print(f"[cyan]Flow name: {flow_name}[/cyan]")
|
|
1538
1548
|
|
|
1549
|
+
# Capture for closure (narrowed types)
|
|
1550
|
+
_url: str = url
|
|
1551
|
+
_flow_obj: Flow = flow_obj
|
|
1552
|
+
|
|
1539
1553
|
async def _run():
|
|
1540
1554
|
from playwright.async_api import async_playwright
|
|
1541
1555
|
|
|
@@ -1544,8 +1558,8 @@ def sandbox_flow(
|
|
|
1544
1558
|
async with async_playwright() as p:
|
|
1545
1559
|
browser = await p.chromium.launch(headless=False)
|
|
1546
1560
|
page = await browser.new_page()
|
|
1547
|
-
await page.goto(
|
|
1548
|
-
executor = FlowExecutor(page,
|
|
1561
|
+
await page.goto(_url)
|
|
1562
|
+
executor = FlowExecutor(page, _flow_obj, screenshots_dir, log=_flow_logger)
|
|
1549
1563
|
await executor.execute()
|
|
1550
1564
|
console.print("[green]✅ Flow executed successfully[/green]")
|
|
1551
1565
|
except Exception as e:
|
|
@@ -1606,8 +1620,23 @@ def sandbox_state_cmd(
|
|
|
1606
1620
|
def check_mutations(result_dict: dict) -> tuple[bool, bool, str | None]:
|
|
1607
1621
|
"""Check if result has mutations or errors. Returns (has_mutations, has_error, error_msg)."""
|
|
1608
1622
|
if isinstance(result_dict, dict):
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
|
1611
1640
|
mutations = result_dict.get("mutations", [])
|
|
1612
1641
|
if mutations:
|
|
1613
1642
|
return True, False, None
|
|
@@ -2009,6 +2038,10 @@ def sandbox_start_services(
|
|
|
2009
2038
|
if not json_output:
|
|
2010
2039
|
console.print("[cyan]Step 3: Getting/creating repository...[/cyan]")
|
|
2011
2040
|
|
|
2041
|
+
if sim_id is None:
|
|
2042
|
+
console.print("[red]❌ Simulator ID not available[/red]")
|
|
2043
|
+
raise typer.Exit(1)
|
|
2044
|
+
|
|
2012
2045
|
if has_repo:
|
|
2013
2046
|
repo = get_simulator_repository.sync(client=client, simulator_id=sim_id, x_api_key=api_key)
|
|
2014
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.")
|