plato-sdk-v2 2.0.50__py3-none-any.whl → 2.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. plato/__init__.py +7 -6
  2. plato/_generated/__init__.py +1 -1
  3. plato/_generated/api/v1/env/evaluate_session.py +3 -3
  4. plato/_generated/api/v1/env/log_state_mutation.py +4 -4
  5. plato/_generated/api/v1/sandbox/checkpoint_vm.py +3 -3
  6. plato/_generated/api/v1/sandbox/save_vm_snapshot.py +3 -3
  7. plato/_generated/api/v1/sandbox/setup_sandbox.py +8 -8
  8. plato/_generated/api/v1/session/__init__.py +2 -0
  9. plato/_generated/api/v1/session/get_sessions_for_archival.py +100 -0
  10. plato/_generated/api/v1/testcases/__init__.py +6 -2
  11. plato/_generated/api/v1/testcases/get_mutation_groups_for_testcase.py +98 -0
  12. plato/_generated/api/v1/testcases/{get_next_output_testcase_for_scoring.py → get_next_testcase_for_scoring.py} +23 -10
  13. plato/_generated/api/v1/testcases/get_testcase_metadata_for_scoring.py +74 -0
  14. plato/_generated/api/v2/__init__.py +2 -1
  15. plato/_generated/api/v2/jobs/__init__.py +4 -0
  16. plato/_generated/api/v2/jobs/checkpoint.py +3 -3
  17. plato/_generated/api/v2/jobs/disk_snapshot.py +3 -3
  18. plato/_generated/api/v2/jobs/log_for_job.py +4 -39
  19. plato/_generated/api/v2/jobs/make.py +4 -4
  20. plato/_generated/api/v2/jobs/setup_sandbox.py +97 -0
  21. plato/_generated/api/v2/jobs/snapshot.py +3 -3
  22. plato/_generated/api/v2/jobs/snapshot_store.py +91 -0
  23. plato/_generated/api/v2/sessions/__init__.py +4 -0
  24. plato/_generated/api/v2/sessions/checkpoint.py +3 -3
  25. plato/_generated/api/v2/sessions/disk_snapshot.py +3 -3
  26. plato/_generated/api/v2/sessions/evaluate.py +3 -3
  27. plato/_generated/api/v2/sessions/log_job_mutation.py +4 -39
  28. plato/_generated/api/v2/sessions/make.py +4 -4
  29. plato/_generated/api/v2/sessions/setup_sandbox.py +98 -0
  30. plato/_generated/api/v2/sessions/snapshot.py +3 -3
  31. plato/_generated/api/v2/sessions/snapshot_store.py +94 -0
  32. plato/_generated/api/v2/user/__init__.py +7 -0
  33. plato/_generated/api/v2/user/get_current_user.py +76 -0
  34. plato/_generated/models/__init__.py +174 -23
  35. plato/_sims_generator/__init__.py +19 -4
  36. plato/_sims_generator/instruction.py +203 -0
  37. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  38. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  39. plato/agents/__init__.py +107 -517
  40. plato/agents/base.py +145 -0
  41. plato/agents/build.py +61 -0
  42. plato/agents/config.py +160 -0
  43. plato/agents/logging.py +401 -0
  44. plato/agents/runner.py +161 -0
  45. plato/agents/trajectory.py +266 -0
  46. plato/chronos/__init__.py +37 -0
  47. plato/chronos/api/__init__.py +3 -0
  48. plato/chronos/api/agents/__init__.py +13 -0
  49. plato/chronos/api/agents/create_agent.py +63 -0
  50. plato/chronos/api/agents/delete_agent.py +61 -0
  51. plato/chronos/api/agents/get_agent.py +62 -0
  52. plato/chronos/api/agents/get_agent_schema.py +72 -0
  53. plato/chronos/api/agents/get_agent_versions.py +62 -0
  54. plato/chronos/api/agents/list_agents.py +57 -0
  55. plato/chronos/api/agents/lookup_agent.py +74 -0
  56. plato/chronos/api/auth/__init__.py +9 -0
  57. plato/chronos/api/auth/debug_auth_api_auth_debug_get.py +43 -0
  58. plato/chronos/api/auth/get_auth_status_api_auth_status_get.py +61 -0
  59. plato/chronos/api/auth/get_current_user_route_api_auth_me_get.py +60 -0
  60. plato/chronos/api/callback/__init__.py +11 -0
  61. plato/chronos/api/callback/push_agent_logs.py +61 -0
  62. plato/chronos/api/callback/update_agent_status.py +57 -0
  63. plato/chronos/api/callback/upload_artifacts.py +59 -0
  64. plato/chronos/api/callback/upload_logs_zip.py +57 -0
  65. plato/chronos/api/callback/upload_trajectory.py +57 -0
  66. plato/chronos/api/default/__init__.py +7 -0
  67. plato/chronos/api/default/health.py +43 -0
  68. plato/chronos/api/jobs/__init__.py +7 -0
  69. plato/chronos/api/jobs/launch_job.py +63 -0
  70. plato/chronos/api/registry/__init__.py +19 -0
  71. plato/chronos/api/registry/get_agent_schema_api_registry_agents__agent_name__schema_get.py +62 -0
  72. plato/chronos/api/registry/get_agent_versions_api_registry_agents__agent_name__versions_get.py +52 -0
  73. plato/chronos/api/registry/get_world_schema_api_registry_worlds__package_name__schema_get.py +68 -0
  74. plato/chronos/api/registry/get_world_versions_api_registry_worlds__package_name__versions_get.py +52 -0
  75. plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +44 -0
  76. plato/chronos/api/registry/list_registry_worlds_api_registry_worlds_get.py +44 -0
  77. plato/chronos/api/runtimes/__init__.py +11 -0
  78. plato/chronos/api/runtimes/create_runtime.py +63 -0
  79. plato/chronos/api/runtimes/delete_runtime.py +61 -0
  80. plato/chronos/api/runtimes/get_runtime.py +62 -0
  81. plato/chronos/api/runtimes/list_runtimes.py +57 -0
  82. plato/chronos/api/runtimes/test_runtime.py +67 -0
  83. plato/chronos/api/secrets/__init__.py +11 -0
  84. plato/chronos/api/secrets/create_secret.py +63 -0
  85. plato/chronos/api/secrets/delete_secret.py +61 -0
  86. plato/chronos/api/secrets/get_secret.py +62 -0
  87. plato/chronos/api/secrets/list_secrets.py +57 -0
  88. plato/chronos/api/secrets/update_secret.py +68 -0
  89. plato/chronos/api/sessions/__init__.py +10 -0
  90. plato/chronos/api/sessions/get_session.py +62 -0
  91. plato/chronos/api/sessions/get_session_logs.py +72 -0
  92. plato/chronos/api/sessions/get_session_logs_download.py +62 -0
  93. plato/chronos/api/sessions/list_sessions.py +57 -0
  94. plato/chronos/api/status/__init__.py +8 -0
  95. plato/chronos/api/status/get_status_api_status_get.py +44 -0
  96. plato/chronos/api/status/get_version_info_api_version_get.py +44 -0
  97. plato/chronos/api/templates/__init__.py +11 -0
  98. plato/chronos/api/templates/create_template.py +63 -0
  99. plato/chronos/api/templates/delete_template.py +61 -0
  100. plato/chronos/api/templates/get_template.py +62 -0
  101. plato/chronos/api/templates/list_templates.py +57 -0
  102. plato/chronos/api/templates/update_template.py +68 -0
  103. plato/chronos/api/trajectories/__init__.py +8 -0
  104. plato/chronos/api/trajectories/get_trajectory.py +62 -0
  105. plato/chronos/api/trajectories/list_trajectories.py +62 -0
  106. plato/chronos/api/worlds/__init__.py +10 -0
  107. plato/chronos/api/worlds/create_world.py +63 -0
  108. plato/chronos/api/worlds/delete_world.py +61 -0
  109. plato/chronos/api/worlds/get_world.py +62 -0
  110. plato/chronos/api/worlds/list_worlds.py +57 -0
  111. plato/chronos/client.py +171 -0
  112. plato/chronos/errors.py +141 -0
  113. plato/chronos/models/__init__.py +647 -0
  114. plato/chronos/py.typed +0 -0
  115. plato/sims/cli.py +299 -123
  116. plato/sims/registry.py +77 -4
  117. plato/v1/cli/agent.py +88 -84
  118. plato/v1/cli/main.py +2 -0
  119. plato/v1/cli/pm.py +441 -119
  120. plato/v1/cli/sandbox.py +747 -191
  121. plato/v1/cli/sim.py +11 -0
  122. plato/v1/cli/verify.py +1269 -0
  123. plato/v1/cli/world.py +3 -0
  124. plato/v1/flow_executor.py +21 -17
  125. plato/v1/models/env.py +11 -11
  126. plato/v1/sdk.py +2 -2
  127. plato/v1/sync_env.py +11 -11
  128. plato/v1/sync_flow_executor.py +21 -17
  129. plato/v1/sync_sdk.py +4 -2
  130. plato/v2/__init__.py +2 -0
  131. plato/v2/async_/environment.py +20 -1
  132. plato/v2/async_/session.py +54 -3
  133. plato/v2/sync/environment.py +2 -1
  134. plato/v2/sync/session.py +52 -2
  135. plato/worlds/README.md +218 -0
  136. plato/worlds/__init__.py +54 -18
  137. plato/worlds/base.py +304 -93
  138. plato/worlds/config.py +239 -73
  139. plato/worlds/runner.py +391 -80
  140. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -3
  141. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +143 -68
  142. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +1 -0
  143. plato/_generated/api/v2/interfaces/__init__.py +0 -27
  144. plato/_generated/api/v2/interfaces/v2_interface_browser_create.py +0 -68
  145. plato/_generated/api/v2/interfaces/v2_interface_cdp_url.py +0 -65
  146. plato/_generated/api/v2/interfaces/v2_interface_click.py +0 -64
  147. plato/_generated/api/v2/interfaces/v2_interface_close.py +0 -59
  148. plato/_generated/api/v2/interfaces/v2_interface_computer_create.py +0 -68
  149. plato/_generated/api/v2/interfaces/v2_interface_cursor.py +0 -64
  150. plato/_generated/api/v2/interfaces/v2_interface_key.py +0 -68
  151. plato/_generated/api/v2/interfaces/v2_interface_screenshot.py +0 -65
  152. plato/_generated/api/v2/interfaces/v2_interface_scroll.py +0 -70
  153. plato/_generated/api/v2/interfaces/v2_interface_type.py +0 -64
  154. plato/world/__init__.py +0 -44
  155. plato/world/base.py +0 -267
  156. plato/world/config.py +0 -139
  157. plato/world/types.py +0 -47
  158. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/WHEEL +0 -0
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
- result["params"].append("client.httpx")
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
- result["params"].append(f"{param_name}: {type_str}")
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
- result["params"].append(f"{param_name}: {type_str} = {default_repr}")
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:
@@ -725,7 +727,7 @@ def _format_response_structure(
725
727
  return lines
726
728
 
727
729
 
728
- def cmd_info(sim_name: str) -> None:
730
+ def cmd_info(sim_name: str, job_id: str | None = None) -> None:
729
731
  """Show detailed information about a sim."""
730
732
  try:
731
733
  info = registry.get_sim_info(sim_name)
@@ -750,6 +752,15 @@ def cmd_info(sim_name: str) -> None:
750
752
  except ImportError:
751
753
  pass
752
754
 
755
+ # Handle instruction-based sims differently
756
+ if info.sim_type == "instruction":
757
+ _cmd_info_instruction(info, job_id)
758
+ else:
759
+ _cmd_info_api(info)
760
+
761
+
762
+ def _cmd_info_api(info) -> None:
763
+ """Show info for API-based sim."""
753
764
  if info.auth:
754
765
  print(f"Auth Type: {info.auth.type}")
755
766
 
@@ -768,6 +779,71 @@ def cmd_info(sim_name: str) -> None:
768
779
  print(f" client = await {info.name}.AsyncClient.create({base_url_example})")
769
780
 
770
781
 
782
+ def _cmd_info_instruction(info, job_id: str | None) -> None:
783
+ """Show info for instruction-based sim."""
784
+ print("Type: Instruction-based (no API client)")
785
+ print()
786
+
787
+ # Show available services
788
+ if info.services:
789
+ print("Services:")
790
+ for name, svc in info.services.items():
791
+ port = svc.get("port", "?")
792
+ desc = svc.get("description", "")
793
+ if job_id:
794
+ url = f"https://{job_id}--{port}.connect.plato.so"
795
+ print(f" {name}: {url}")
796
+ if desc:
797
+ print(f" {desc}")
798
+ else:
799
+ print(f" {name}: port {port}")
800
+ if desc:
801
+ print(f" {desc}")
802
+ print()
803
+
804
+ # Show env vars
805
+ if info.env_vars:
806
+ print("Environment Variables:")
807
+ for name, var_config in info.env_vars.items():
808
+ desc = var_config.get("description", "")
809
+ if "template" in var_config:
810
+ print(f" {name}: (from service URL)")
811
+ elif "default" in var_config:
812
+ print(f" {name}={var_config['default']}")
813
+ else:
814
+ print(f" {name}")
815
+ if desc:
816
+ print(f" {desc}")
817
+ print()
818
+
819
+ # Show instructions
820
+ if info.instructions:
821
+ instructions = info.instructions
822
+ if job_id and info.services:
823
+ # Build service URLs from job_id and replace placeholders
824
+ for svc_name, svc_config in info.services.items():
825
+ port = svc_config.get("port", 80)
826
+ svc_url = f"https://{job_id}--{port}.connect.plato.so"
827
+ instructions = instructions.replace(f"{{service:{svc_name}}}", svc_url)
828
+
829
+ print("Instructions:")
830
+ print(instructions)
831
+ print()
832
+
833
+ # Show usage
834
+ print("Usage:")
835
+ print(f" from plato.sims import {info.name}")
836
+ print()
837
+ print(" # Get service URLs from job ID")
838
+ print(f" service_urls = {info.name}.get_service_urls(job_id)")
839
+ print()
840
+ print(" # Get formatted instructions")
841
+ print(f" instructions = {info.name}.get_instructions(service_urls)")
842
+ print()
843
+ print(" # Get environment variables to set")
844
+ print(f" env_vars = {info.name}.get_env_vars(service_urls)")
845
+
846
+
771
847
  def cmd_endpoints(
772
848
  sim_name: str,
773
849
  spec_name: str | None = None,
@@ -950,144 +1026,204 @@ def cmd_publish(
950
1026
  pkg_dir = build_dir / "src" / "plato" / "sims" / service_name
951
1027
  pkg_dir.mkdir(parents=True)
952
1028
 
953
- # Find OpenAPI spec
954
1029
  config_dir = config_file.parent
955
- spec_file = None
956
1030
 
957
- # Priority 1: specs_dir (new format)
1031
+ # Check for instruction-based sim first
1032
+ instructions_file = None
958
1033
  if specs_dir:
959
- specs_path = config_dir / specs_dir
960
- for candidate in ["openapi.json", "openapi.yaml", "openapi.yml"]:
961
- candidate_path = specs_path / candidate
962
- if candidate_path.exists():
963
- spec_file = candidate_path
964
- break
965
- # Priority 2: spec_path (legacy format)
966
- elif spec_path:
967
- spec_file = config_dir / spec_path
968
- # Priority 3: Try common locations in root
969
- else:
970
- for candidate in ["openapi.yaml", "openapi.yml", "openapi.json", "spec.yaml", "spec.json"]:
971
- candidate_path = config_dir / candidate
972
- if candidate_path.exists():
973
- spec_file = candidate_path
974
- break
1034
+ instructions_path = config_dir / specs_dir / "instructions.yaml"
1035
+ if instructions_path.exists():
1036
+ instructions_file = instructions_path
1037
+ if not instructions_file:
1038
+ instructions_path = config_dir / "instructions.yaml"
1039
+ if instructions_path.exists():
1040
+ instructions_file = instructions_path
1041
+
1042
+ if instructions_file:
1043
+ # Generate instruction-based SDK
1044
+ print(f"Using instructions config: {instructions_file}")
1045
+ print("Generating instruction-based SDK...")
1046
+ try:
1047
+ import plato
1048
+ from plato._sims_generator import InstructionConfig, InstructionGenerator
975
1049
 
976
- if not spec_file or not spec_file.exists():
977
- print(
978
- "Error: OpenAPI spec not found. Set 'sdk.specs_dir' or 'sdk.spec_path' in plato-config.yml",
979
- file=sys.stderr,
980
- )
981
- sys.exit(1)
1050
+ generator_version = getattr(plato, "__version__", None)
1051
+ print(f" Generator version: {generator_version}")
982
1052
 
983
- print(f"Using OpenAPI spec: {spec_file}")
1053
+ instruction_config = InstructionConfig.from_yaml(instructions_file)
1054
+ # Override version from plato-config.yml if set
1055
+ instruction_config.version = version
1056
+
1057
+ generator = InstructionGenerator(
1058
+ config=instruction_config,
1059
+ output_path=pkg_dir,
1060
+ package_name=service_name,
1061
+ generator_version=generator_version,
1062
+ )
1063
+ generator.generate()
1064
+ print(f" Generated instruction-based SDK to: {pkg_dir}")
1065
+
1066
+ except ImportError as e:
1067
+ print(f"Error: Missing dependency for SDK generation: {e}", file=sys.stderr)
1068
+ print("Install with: uv add plato-sdk-v2", file=sys.stderr)
1069
+ sys.exit(1)
1070
+ except Exception as e:
1071
+ print(f"Error generating instruction SDK: {e}", file=sys.stderr)
1072
+ sys.exit(1)
984
1073
 
985
- # Load auth config if provided
986
- auth_yaml = None
987
- if auth_config_path:
988
- auth_file = config_dir / auth_config_path
989
- if auth_file.exists():
990
- auth_yaml = auth_file
991
1074
  else:
992
- # Try specs_dir first, then root
993
- search_dirs = []
994
- if specs_dir:
995
- search_dirs.append(config_dir / specs_dir)
996
- search_dirs.append(config_dir)
1075
+ # Generate OpenAPI-based SDK
1076
+ # Find OpenAPI spec
1077
+ spec_file = None
997
1078
 
998
- for search_dir in search_dirs:
999
- for candidate in ["auth.yaml", "auth.yml"]:
1000
- candidate_path = search_dir / candidate
1079
+ # Priority 1: specs_dir (new format)
1080
+ if specs_dir:
1081
+ specs_path = config_dir / specs_dir
1082
+ for candidate in ["openapi.json", "openapi.yaml", "openapi.yml"]:
1083
+ candidate_path = specs_path / candidate
1001
1084
  if candidate_path.exists():
1002
- auth_yaml = candidate_path
1085
+ spec_file = candidate_path
1086
+ break
1087
+ # Priority 2: spec_path (legacy format)
1088
+ elif spec_path:
1089
+ spec_file = config_dir / spec_path
1090
+ # Priority 3: Try common locations in root
1091
+ else:
1092
+ for candidate in ["openapi.yaml", "openapi.yml", "openapi.json", "spec.yaml", "spec.json"]:
1093
+ candidate_path = config_dir / candidate
1094
+ if candidate_path.exists():
1095
+ spec_file = candidate_path
1003
1096
  break
1004
- if auth_yaml:
1005
- break
1006
-
1007
- # Generate SDK code
1008
- print("Generating SDK code...")
1009
- try:
1010
- import plato
1011
- from plato._sims_generator import AuthConfig, PythonGenerator, parse_openapi
1012
1097
 
1013
- # Get current plato-sdk-v2 version
1014
- generator_version = getattr(plato, "__version__", None)
1015
- print(f" Generator version: {generator_version}")
1098
+ if not spec_file or not spec_file.exists():
1099
+ print(
1100
+ "Error: OpenAPI spec or instructions.yaml not found. "
1101
+ "Set 'sdk.specs_dir' or 'sdk.spec_path' in plato-config.yml",
1102
+ file=sys.stderr,
1103
+ )
1104
+ sys.exit(1)
1016
1105
 
1017
- # Check if .generator-version exists and matches
1018
- if specs_dir:
1019
- generator_version_file = config_dir / specs_dir / ".generator-version"
1020
- if generator_version_file.exists():
1021
- expected_version = generator_version_file.read_text().strip()
1022
- if expected_version and generator_version and expected_version != generator_version:
1023
- print(
1024
- f"Error: Generator version mismatch. "
1025
- f"Expected {expected_version} (from .generator-version), "
1026
- f"but running {generator_version}",
1027
- file=sys.stderr,
1028
- )
1029
- print(
1030
- f" Run: uvx --from 'plato-sdk-v2=={expected_version}' plato sims publish",
1031
- file=sys.stderr,
1032
- )
1033
- print(
1034
- f" Or update .generator-version to {generator_version} to use current version",
1035
- file=sys.stderr,
1036
- )
1037
- sys.exit(1)
1038
-
1039
- # Write/update .generator-version to specs dir
1040
- if generator_version:
1041
- generator_version_file.write_text(f"{generator_version}\n")
1042
- print(f" Updated {generator_version_file}")
1043
-
1044
- # Load spec
1045
- with open(spec_file) as f:
1046
- if spec_file.suffix == ".json":
1047
- spec = json.load(f)
1048
- else:
1049
- spec = yaml.safe_load(f)
1106
+ print(f"Using OpenAPI spec: {spec_file}")
1050
1107
 
1051
- # Load auth config
1052
- if auth_yaml and auth_yaml.exists():
1053
- auth = AuthConfig.from_yaml(auth_yaml)
1108
+ # Load auth config if provided
1109
+ auth_yaml = None
1110
+ if auth_config_path:
1111
+ auth_file = config_dir / auth_config_path
1112
+ if auth_file.exists():
1113
+ auth_yaml = auth_file
1054
1114
  else:
1055
- # Default auth config
1056
- auth = AuthConfig(
1057
- type="basic",
1058
- env_prefix=service_name.upper(),
1059
- )
1115
+ # Try specs_dir first, then root
1116
+ search_dirs = []
1117
+ if specs_dir:
1118
+ search_dirs.append(config_dir / specs_dir)
1119
+ search_dirs.append(config_dir)
1120
+
1121
+ for search_dir in search_dirs:
1122
+ for candidate in ["auth.yaml", "auth.yml"]:
1123
+ candidate_path = search_dir / candidate
1124
+ if candidate_path.exists():
1125
+ auth_yaml = candidate_path
1126
+ break
1127
+ if auth_yaml:
1128
+ break
1060
1129
 
1061
- # Parse and generate
1062
- api = parse_openapi(spec)
1063
- print(f" Parsed {len(api.endpoints)} endpoints")
1064
-
1065
- generator = PythonGenerator(
1066
- api=api,
1067
- output_path=pkg_dir,
1068
- spec=spec,
1069
- package_name=service_name,
1070
- auth_config=auth,
1071
- generator_version=generator_version,
1072
- )
1073
- generator.generate()
1074
- print(f" Generated to: {pkg_dir}")
1130
+ # Generate SDK code
1131
+ print("Generating SDK code...")
1132
+ try:
1133
+ import plato
1134
+ from plato._sims_generator import AuthConfig, PythonGenerator, parse_openapi
1135
+
1136
+ # Get current plato-sdk-v2 version
1137
+ generator_version = getattr(plato, "__version__", None)
1138
+ print(f" Generator version: {generator_version}")
1139
+
1140
+ # Check if .generator-version exists and matches
1141
+ if specs_dir:
1142
+ generator_version_file = config_dir / specs_dir / ".generator-version"
1143
+ if generator_version_file.exists():
1144
+ expected_version = generator_version_file.read_text().strip()
1145
+ if expected_version and generator_version and expected_version != generator_version:
1146
+ print(
1147
+ f"Error: Generator version mismatch. "
1148
+ f"Expected {expected_version} (from .generator-version), "
1149
+ f"but running {generator_version}",
1150
+ file=sys.stderr,
1151
+ )
1152
+ print(
1153
+ f" Run: uvx --from 'plato-sdk-v2=={expected_version}' plato sims publish",
1154
+ file=sys.stderr,
1155
+ )
1156
+ print(
1157
+ f" Or update .generator-version to {generator_version} to use current version",
1158
+ file=sys.stderr,
1159
+ )
1160
+ sys.exit(1)
1161
+
1162
+ # Write/update .generator-version to specs dir
1163
+ if generator_version:
1164
+ generator_version_file.write_text(f"{generator_version}\n")
1165
+ print(f" Updated {generator_version_file}")
1166
+
1167
+ # Load spec (spec_file is guaranteed non-None after check above)
1168
+ assert spec_file is not None
1169
+ with open(spec_file) as f:
1170
+ if spec_file.suffix == ".json":
1171
+ spec = json.load(f)
1172
+ else:
1173
+ spec = yaml.safe_load(f)
1174
+
1175
+ # Load auth config
1176
+ if auth_yaml and auth_yaml.exists():
1177
+ auth = AuthConfig.from_yaml(auth_yaml)
1178
+ else:
1179
+ # Default auth config
1180
+ auth = AuthConfig(
1181
+ type="basic",
1182
+ env_prefix=service_name.upper(),
1183
+ )
1184
+
1185
+ # Parse and generate
1186
+ api = parse_openapi(spec)
1187
+ print(f" Parsed {len(api.endpoints)} endpoints")
1188
+
1189
+ generator = PythonGenerator(
1190
+ api=api,
1191
+ output_path=pkg_dir,
1192
+ spec=spec,
1193
+ package_name=service_name,
1194
+ auth_config=auth,
1195
+ generator_version=generator_version,
1196
+ )
1197
+ generator.generate()
1198
+ print(f" Generated to: {pkg_dir}")
1075
1199
 
1076
- except ImportError as e:
1077
- print(f"Error: Missing dependency for SDK generation: {e}", file=sys.stderr)
1078
- print("Install with: uv add plato-sdk-v2", file=sys.stderr)
1079
- sys.exit(1)
1080
- except Exception as e:
1081
- print(f"Error generating SDK: {e}", file=sys.stderr)
1082
- sys.exit(1)
1200
+ except ImportError as e:
1201
+ print(f"Error: Missing dependency for SDK generation: {e}", file=sys.stderr)
1202
+ print("Install with: uv add plato-sdk-v2", file=sys.stderr)
1203
+ sys.exit(1)
1204
+ except Exception as e:
1205
+ print(f"Error generating SDK: {e}", file=sys.stderr)
1206
+ sys.exit(1)
1083
1207
 
1084
1208
  # Create pyproject.toml with namespace package structure
1209
+ # Instruction sims need pyyaml; API sims need httpx and pydantic
1210
+ if instructions_file:
1211
+ dependencies = '["pyyaml>=6.0.0"]'
1212
+ # Include yaml file in package data
1213
+ extra_config = f"""
1214
+ [tool.hatch.build.targets.wheel.force-include]
1215
+ "src/plato/sims/{service_name}/instructions.yaml" = "plato/sims/{service_name}/instructions.yaml"
1216
+ """
1217
+ else:
1218
+ dependencies = '["httpx>=0.25.0", "pydantic>=2.0.0"]'
1219
+ extra_config = ""
1220
+
1085
1221
  pyproject_content = f'''[project]
1086
1222
  name = "{package_name}"
1087
1223
  version = "{version}"
1088
1224
  description = "{description}"
1089
1225
  requires-python = ">=3.10"
1090
- dependencies = ["httpx>=0.25.0", "pydantic>=2.0.0"]
1226
+ dependencies = {dependencies}
1091
1227
 
1092
1228
  [build-system]
1093
1229
  requires = ["hatchling"]
@@ -1095,11 +1231,38 @@ build-backend = "hatchling.build"
1095
1231
 
1096
1232
  [tool.hatch.build.targets.wheel]
1097
1233
  packages = ["src/plato"]
1098
- '''
1234
+ {extra_config}'''
1099
1235
  (build_dir / "pyproject.toml").write_text(pyproject_content)
1100
1236
 
1101
1237
  # Create README
1102
- readme_content = f"""# {package_name}
1238
+ if instructions_file:
1239
+ readme_content = f"""# {package_name}
1240
+
1241
+ Auto-generated instruction-based SDK for the {service_name} simulator.
1242
+
1243
+ ## Installation
1244
+
1245
+ ```bash
1246
+ uv add {package_name} --index-url https://plato.so/api/v2/pypi/{repo}/simple/
1247
+ ```
1248
+
1249
+ ## Usage
1250
+
1251
+ ```python
1252
+ from plato.sims.{service_name} import get_instructions, get_service_urls, setup_env
1253
+
1254
+ # Get service URLs from job ID
1255
+ service_urls = get_service_urls(job_id)
1256
+
1257
+ # Get formatted instructions
1258
+ instructions = get_instructions(service_urls)
1259
+
1260
+ # Set up environment variables
1261
+ setup_env(service_urls)
1262
+ ```
1263
+ """
1264
+ else:
1265
+ readme_content = f"""# {package_name}
1103
1266
 
1104
1267
  Auto-generated SDK for the {service_name} simulator.
1105
1268
 
@@ -1228,9 +1391,22 @@ def main(args: list[str] | None = None) -> None:
1228
1391
  elif command == "info":
1229
1392
  if len(args) < 2:
1230
1393
  print("Error: sim name required", file=sys.stderr)
1231
- print("Usage: plato sims info <sim_name>", file=sys.stderr)
1394
+ print("Usage: plato sims info <sim_name> [--job-id JOB_ID]", file=sys.stderr)
1232
1395
  sys.exit(1)
1233
- cmd_info(args[1])
1396
+
1397
+ sim_name = args[1]
1398
+ job_id = None
1399
+
1400
+ # Parse optional --job-id flag
1401
+ i = 2
1402
+ while i < len(args):
1403
+ if args[i] == "--job-id" and i + 1 < len(args):
1404
+ job_id = args[i + 1]
1405
+ i += 2
1406
+ else:
1407
+ i += 1
1408
+
1409
+ cmd_info(sim_name, job_id)
1234
1410
 
1235
1411
  elif command == "endpoints":
1236
1412
  if len(args) < 2:
plato/sims/registry.py CHANGED
@@ -38,6 +38,11 @@ class SimInfo:
38
38
  description: str | None
39
39
  auth: AuthRequirement | None
40
40
  base_url_suffix: str | None # e.g., "/api/v1" for EspoCRM
41
+ # New fields for instruction-based sims
42
+ sim_type: str = "api" # "api" or "instruction"
43
+ services: dict[str, dict[str, Any]] | None = None # {"main": {"port": 4566, "description": "..."}}
44
+ env_vars: dict[str, dict[str, Any]] | None = None # {"AWS_ENDPOINT_URL": {"template": "...", "description": "..."}}
45
+ instructions: str | None = None # Markdown instructions template
41
46
 
42
47
 
43
48
  class SimsRegistry:
@@ -76,8 +81,9 @@ class SimsRegistry:
76
81
  # Try to import the module to verify it's a valid sim
77
82
  mod = importlib.import_module(modname)
78
83
 
79
- # Check if it looks like a sim (has Client or AsyncClient)
80
- if hasattr(mod, "Client") or hasattr(mod, "AsyncClient"):
84
+ # Check if it looks like an API sim (has Client or AsyncClient)
85
+ # or an instruction sim (has SERVICES)
86
+ if hasattr(mod, "Client") or hasattr(mod, "AsyncClient") or hasattr(mod, "SERVICES"):
81
87
  self._installed_sims[short_name] = mod
82
88
  except ImportError:
83
89
  continue
@@ -98,8 +104,10 @@ class SimsRegistry:
98
104
  # Check local specs dir if provided
99
105
  if self.specs_dir and self.specs_dir.exists():
100
106
  for d in self.specs_dir.iterdir():
101
- if d.is_dir() and (d / "auth.yaml").exists():
102
- sims.add(d.name)
107
+ if d.is_dir():
108
+ # Check for API sim (auth.yaml) or instruction sim (instructions.yaml)
109
+ if (d / "auth.yaml").exists() or (d / "instructions.yaml").exists():
110
+ sims.add(d.name)
103
111
 
104
112
  return sorted(sims)
105
113
 
@@ -120,6 +128,11 @@ class SimsRegistry:
120
128
  except Exception:
121
129
  version = getattr(mod, "__version__", "unknown")
122
130
 
131
+ # Check if this is an instruction-based sim
132
+ if hasattr(mod, "SERVICES"):
133
+ return self._get_instruction_sim_info(name, mod, version)
134
+
135
+ # It's an API-based sim
123
136
  # Try to load auth config from package
124
137
  auth = None
125
138
  try:
@@ -141,12 +154,18 @@ class SimsRegistry:
141
154
  description=mod.__doc__,
142
155
  auth=auth,
143
156
  base_url_suffix=base_url_suffix,
157
+ sim_type="api",
144
158
  )
145
159
 
146
160
  # Fall back to local specs dir
147
161
  if self.specs_dir:
148
162
  sim_dir = self.specs_dir / name
149
163
  auth_path = sim_dir / "auth.yaml"
164
+ instructions_path = sim_dir / "instructions.yaml"
165
+
166
+ # Check for instruction-based sim first
167
+ if instructions_path.exists():
168
+ return self._load_instruction_sim_from_file(name, instructions_path)
150
169
 
151
170
  if auth_path.exists():
152
171
  auth = self._load_auth(auth_path)
@@ -157,10 +176,64 @@ class SimsRegistry:
157
176
  description=None,
158
177
  auth=auth,
159
178
  base_url_suffix=None,
179
+ sim_type="api",
160
180
  )
161
181
 
162
182
  raise ValueError(f"Sim '{name}' not found")
163
183
 
184
+ def _get_instruction_sim_info(self, name: str, mod: Any, version: str) -> SimInfo:
185
+ """Get SimInfo for an instruction-based sim from its module."""
186
+ import importlib.resources
187
+
188
+ services = getattr(mod, "SERVICES", {})
189
+
190
+ # Try to load full config from bundled instructions.yaml
191
+ env_vars_config: dict[str, dict[str, Any]] = {}
192
+ instructions_text = ""
193
+ title = name.title()
194
+ description = mod.__doc__
195
+
196
+ try:
197
+ config_text = importlib.resources.files(mod.__name__).joinpath("instructions.yaml").read_text()
198
+ config_data = yaml.safe_load(config_text)
199
+ env_vars_config = config_data.get("env_vars", {})
200
+ instructions_text = config_data.get("instructions", "")
201
+ title = config_data.get("title", name.title())
202
+ description = config_data.get("description", mod.__doc__)
203
+ except Exception:
204
+ pass
205
+
206
+ return SimInfo(
207
+ name=name,
208
+ title=title,
209
+ version=version,
210
+ description=description,
211
+ auth=None, # Instruction sims don't use auth config
212
+ base_url_suffix=None,
213
+ sim_type="instruction",
214
+ services=services,
215
+ env_vars=env_vars_config,
216
+ instructions=instructions_text,
217
+ )
218
+
219
+ def _load_instruction_sim_from_file(self, name: str, instructions_path: Path) -> SimInfo:
220
+ """Load instruction sim info from instructions.yaml file."""
221
+ with open(instructions_path) as f:
222
+ data = yaml.safe_load(f)
223
+
224
+ return SimInfo(
225
+ name=name,
226
+ title=data.get("title", name.title()),
227
+ version=data.get("version", "unknown"),
228
+ description=data.get("description"),
229
+ auth=None,
230
+ base_url_suffix=None,
231
+ sim_type="instruction",
232
+ services=data.get("services", {}),
233
+ env_vars=data.get("env_vars", {}),
234
+ instructions=data.get("instructions", ""),
235
+ )
236
+
164
237
  def _load_auth(self, auth_path: Path) -> AuthRequirement:
165
238
  """Load auth requirements from auth.yaml."""
166
239
  with open(auth_path) as f: