supervaizer 0.9.8__py3-none-any.whl → 0.10.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.
Files changed (56) hide show
  1. supervaizer/__init__.py +11 -2
  2. supervaizer/__version__.py +1 -1
  3. supervaizer/account.py +4 -0
  4. supervaizer/account_service.py +7 -1
  5. supervaizer/admin/routes.py +24 -8
  6. supervaizer/admin/templates/agents.html +74 -0
  7. supervaizer/admin/templates/agents_grid.html +5 -3
  8. supervaizer/admin/templates/navigation.html +11 -1
  9. supervaizer/admin/templates/supervaize_instructions.html +212 -0
  10. supervaizer/agent.py +28 -6
  11. supervaizer/case.py +46 -14
  12. supervaizer/cli.py +247 -7
  13. supervaizer/common.py +45 -4
  14. supervaizer/deploy/__init__.py +16 -0
  15. supervaizer/deploy/cli.py +296 -0
  16. supervaizer/deploy/commands/__init__.py +9 -0
  17. supervaizer/deploy/commands/clean.py +294 -0
  18. supervaizer/deploy/commands/down.py +119 -0
  19. supervaizer/deploy/commands/local.py +460 -0
  20. supervaizer/deploy/commands/plan.py +167 -0
  21. supervaizer/deploy/commands/status.py +169 -0
  22. supervaizer/deploy/commands/up.py +281 -0
  23. supervaizer/deploy/docker.py +378 -0
  24. supervaizer/deploy/driver_factory.py +42 -0
  25. supervaizer/deploy/drivers/__init__.py +39 -0
  26. supervaizer/deploy/drivers/aws_app_runner.py +607 -0
  27. supervaizer/deploy/drivers/base.py +196 -0
  28. supervaizer/deploy/drivers/cloud_run.py +570 -0
  29. supervaizer/deploy/drivers/do_app_platform.py +504 -0
  30. supervaizer/deploy/health.py +404 -0
  31. supervaizer/deploy/state.py +210 -0
  32. supervaizer/deploy/templates/Dockerfile.template +44 -0
  33. supervaizer/deploy/templates/debug_env.py +69 -0
  34. supervaizer/deploy/templates/docker-compose.yml.template +37 -0
  35. supervaizer/deploy/templates/dockerignore.template +66 -0
  36. supervaizer/deploy/templates/entrypoint.sh +20 -0
  37. supervaizer/deploy/utils.py +52 -0
  38. supervaizer/examples/controller_template.py +1 -1
  39. supervaizer/job.py +18 -5
  40. supervaizer/job_service.py +6 -5
  41. supervaizer/parameter.py +13 -1
  42. supervaizer/protocol/__init__.py +2 -2
  43. supervaizer/protocol/a2a/routes.py +1 -1
  44. supervaizer/routes.py +141 -17
  45. supervaizer/server.py +5 -11
  46. supervaizer/utils/__init__.py +16 -0
  47. supervaizer/utils/version_check.py +56 -0
  48. {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/METADATA +105 -34
  49. supervaizer-0.10.1.dist-info/RECORD +76 -0
  50. {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/WHEEL +1 -1
  51. supervaizer/protocol/acp/__init__.py +0 -21
  52. supervaizer/protocol/acp/model.py +0 -198
  53. supervaizer/protocol/acp/routes.py +0 -74
  54. supervaizer-0.9.8.dist-info/RECORD +0 -52
  55. {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/entry_points.txt +0 -0
  56. {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/licenses/LICENSE.md +0 -0
supervaizer/agent.py CHANGED
@@ -28,6 +28,7 @@ from supervaizer.job import Job, JobContext, JobResponse
28
28
  from supervaizer.job_service import service_job_finished
29
29
  from supervaizer.lifecycle import EntityStatus
30
30
  from supervaizer.parameter import ParametersSetup
31
+ from supervaizer.case import CaseNodes
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  from supervaizer.server import Server
@@ -210,6 +211,11 @@ class AgentMethodAbstract(BaseModel):
210
211
  },
211
212
  }
212
213
 
214
+ nodes: CaseNodes | None = Field(
215
+ default=None,
216
+ description="The definition of the Case Nodes (=steps) for this method",
217
+ )
218
+
213
219
 
214
220
  class AgentMethod(AgentMethodAbstract):
215
221
  @property
@@ -419,6 +425,7 @@ class AgentMethod(AgentMethodAbstract):
419
425
  "params": self.params,
420
426
  "fields": self.fields_definitions,
421
427
  "description": self.description,
428
+ "nodes": self.nodes.registration_info if self.nodes else None,
422
429
  }
423
430
 
424
431
 
@@ -440,8 +447,9 @@ class AgentCustomMethodParams(AgentMethodParams):
440
447
 
441
448
  class AgentMethodsAbstract(BaseModel):
442
449
  job_start: AgentMethod
443
- job_stop: AgentMethod
444
- job_status: AgentMethod
450
+ job_stop: AgentMethod | None = None
451
+ job_status: AgentMethod | None = None
452
+ human_answer: AgentMethod | None = None
445
453
  chat: AgentMethod | None = None
446
454
  custom: dict[str, AgentMethod] | None = None
447
455
 
@@ -476,8 +484,13 @@ class AgentMethods(AgentMethodsAbstract):
476
484
  def registration_info(self) -> Dict[str, Any]:
477
485
  return {
478
486
  "job_start": self.job_start.registration_info,
479
- "job_stop": self.job_stop.registration_info,
480
- "job_status": self.job_status.registration_info,
487
+ "job_stop": self.job_stop.registration_info if self.job_stop else None,
488
+ "job_status": self.job_status.registration_info
489
+ if self.job_status
490
+ else None,
491
+ "human_answer": self.human_answer.registration_info
492
+ if self.human_answer
493
+ else None,
481
494
  "chat": self.chat.registration_info if self.chat else None,
482
495
  "custom": {
483
496
  name: method.registration_info
@@ -575,6 +588,14 @@ class AgentAbstract(SvBaseModel):
575
588
  default=60 * 60,
576
589
  description="Maximum execution time in seconds, defaults to 1 hour",
577
590
  )
591
+ supervaize_instructions_template_path: Optional[str] = Field(
592
+ default=None,
593
+ description="Optional path to a custom template file for supervaize_instructions.html page",
594
+ )
595
+ instructions_path: str = Field(
596
+ default="supervaize_instructions.html",
597
+ description="Path where the supervaize instructions page is served (relative to agent path)",
598
+ )
578
599
 
579
600
  model_config = {
580
601
  "reference_group": "Core",
@@ -690,6 +711,7 @@ class Agent(AgentAbstract):
690
711
  "server_agent_onboarding_status": self.server_agent_onboarding_status,
691
712
  "server_encrypted_parameters": self.server_encrypted_parameters,
692
713
  "max_execution_time": self.max_execution_time,
714
+ "instructions_path": self.instructions_path,
693
715
  }
694
716
 
695
717
  def update_agent_from_server(self, server: "Server") -> Optional["Agent"]:
@@ -888,13 +910,13 @@ class Agent(AgentAbstract):
888
910
  return job
889
911
 
890
912
  def job_stop(self, params: Dict[str, Any] = {}) -> Any:
891
- if not self.methods:
913
+ if not self.methods or not self.methods.job_stop:
892
914
  raise ValueError("Agent methods not defined")
893
915
  method = self.methods.job_stop.method
894
916
  return self._execute(method, params)
895
917
 
896
918
  def job_status(self, params: Dict[str, Any] = {}) -> Any:
897
- if not self.methods:
919
+ if not self.methods or not self.methods.job_status:
898
920
  raise ValueError("Agent methods not defined")
899
921
  method = self.methods.job_status.method
900
922
  return self._execute(method, params)
supervaizer/case.py CHANGED
@@ -10,9 +10,9 @@ from enum import Enum
10
10
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
11
11
 
12
12
  import shortuuid
13
- from pydantic import ConfigDict
14
- from typing_extensions import deprecated
15
-
13
+ from pydantic import ConfigDict, Field
14
+ from pydantic.json_schema import SkipJsonSchema
15
+ from typing import Callable
16
16
  from supervaizer.common import SvBaseModel, log, singleton
17
17
  from supervaizer.lifecycle import EntityEvents, EntityStatus
18
18
  from supervaizer.storage import PersistentEntityLifecycle, StorageManager
@@ -55,7 +55,6 @@ class CaseNodeUpdate(SvBaseModel):
55
55
  payload (Dict[str, Any]): Additional data for the update - when a question is requested to the user, the payload is the question
56
56
  is_final (bool): Whether this is the final update. Default to False
57
57
  index (int): Index of the node to update. This is set by Case.update()
58
-
59
58
  error (Optional[str]): Error message if any. Default to None
60
59
 
61
60
  When payload contains a question (supervaizer_form):
@@ -101,24 +100,32 @@ class CaseNodeUpdate(SvBaseModel):
101
100
  @property
102
101
  def registration_info(self) -> Dict[str, Any]:
103
102
  """Returns registration info for the case node update"""
103
+ # Serialize payload to convert type objects to strings for JSON serialization
104
+ serialized_payload = (
105
+ self.serialize_value(self.payload) if self.payload else None
106
+ )
104
107
  return {
105
108
  "index": self.index,
106
109
  "name": self.name,
107
110
  "error": self.error,
108
111
  "cost": self.cost,
109
- "payload": self.payload,
112
+ "payload": serialized_payload,
110
113
  "is_final": self.is_final,
111
114
  }
112
115
 
113
116
 
114
- class CaseNoteType(Enum):
117
+ class CaseNodeType(Enum):
115
118
  """
116
- CaseNoteType is an enum that represents the type of a case note.
119
+ CaseNodeType is an enum that represents the type of a case note.
117
120
  """
118
121
 
119
122
  CHAT = "chat"
120
123
  TRIGGER = "trigger"
121
124
  NOTIFICATION = "notification"
125
+ STATUS_UPDATE = "status_update"
126
+ INTERMEDIARY_DELIVERY = "intermediary_delivery"
127
+ HITL = "human_in_the_loop"
128
+ DELIVERABLE = "deliverable"
122
129
  VALIDATION = "validation"
123
130
  DELIVERY = "delivery"
124
131
  ERROR = "error"
@@ -126,22 +133,43 @@ class CaseNoteType(Enum):
126
133
  INFO = "info"
127
134
 
128
135
 
129
- @deprecated("Not used")
130
136
  class CaseNode(SvBaseModel):
137
+ model_config = ConfigDict(arbitrary_types_allowed=True)
138
+
131
139
  name: str
132
- description: str
133
- type: CaseNoteType
140
+ type: CaseNodeType
141
+ factory: SkipJsonSchema[Callable[..., CaseNodeUpdate]] = Field(
142
+ exclude=True, repr=False
143
+ ) # Exclude from JSON schema generation and representation
144
+ description: str | None = None
145
+ can_be_confirmed: bool = False # Whether the user can decide that this node needs to be confirmed. This must be set in the job definition.
134
146
 
135
- class Config:
136
- arbitrary_types_allowed = True
147
+ def __call__(self, *args: Any, **kwargs: Any) -> CaseNodeUpdate:
148
+ """Make it callable directly."""
149
+ return self.factory(*args, **kwargs)
137
150
 
138
151
  @property
139
152
  def registration_info(self) -> Dict[str, Any]:
140
153
  """Returns registration info for the case node"""
141
154
  return {
142
155
  "name": self.name,
143
- "description": self.description,
144
156
  "type": self.type.value,
157
+ "description": self.description,
158
+ "can_be_confirmed": self.can_be_confirmed,
159
+ }
160
+
161
+
162
+ class CaseNodes(SvBaseModel):
163
+ nodes: List[CaseNode] = []
164
+
165
+ def get(self, name: str) -> CaseNode | None:
166
+ return next((node for node in self.nodes if node.name == name), None)
167
+
168
+ @property
169
+ def registration_info(self) -> Dict[str, Any]:
170
+ """Returns registration info for the case nodes"""
171
+ return {
172
+ "nodes": [node.registration_info for node in self.nodes],
145
173
  }
146
174
 
147
175
 
@@ -216,7 +244,11 @@ class Case(CaseAbstractModel):
216
244
  storage = StorageManager()
217
245
  storage.save_object("Case", self.to_dict)
218
246
 
219
- def receive_human_input(self, **kwargs: Any) -> None:
247
+ def receive_human_input(
248
+ self, updateCaseNode: CaseNodeUpdate, **kwargs: Any
249
+ ) -> None:
250
+ # Add the update to the case (this handles index, send_update_case, and persistence)
251
+ self.update(updateCaseNode)
220
252
  # Transition from AWAITING to IN_PROGRESS
221
253
  from supervaizer.storage import PersistentEntityLifecycle
222
254
 
supervaizer/cli.py CHANGED
@@ -4,6 +4,7 @@
4
4
  # If a copy of the MPL was not distributed with this file, you can obtain one at
5
5
  # https://mozilla.org/MPL/2.0/.
6
6
 
7
+ import asyncio
7
8
  import os
8
9
  import shutil
9
10
  import signal
@@ -12,16 +13,94 @@ import sys
12
13
  from typing import Any
13
14
  from pathlib import Path
14
15
  from typing import Optional
16
+ from supervaizer.deploy.cli import deploy_app
15
17
 
16
18
  import typer
17
19
  from rich.console import Console
20
+ from rich.prompt import Confirm
18
21
 
19
22
  from supervaizer.__version__ import VERSION
23
+ from supervaizer.utils.version_check import check_is_latest_version
24
+
25
+ console = Console()
26
+
27
+ # Cache version status to avoid multiple checks
28
+ _version_status: tuple[bool, str | None] | None = None
29
+
30
+
31
+ def _check_version() -> tuple[bool, str | None]:
32
+ """Check version status, caching the result."""
33
+ global _version_status
34
+ if _version_status is None:
35
+ try:
36
+ _version_status = asyncio.run(check_is_latest_version())
37
+ except Exception:
38
+ # On error, assume we're on latest to avoid false positives
39
+ _version_status = (True, None)
40
+ return _version_status
41
+
42
+
43
+ def _display_version_info() -> None:
44
+ """Display version information."""
45
+ is_latest, latest_version = _check_version()
46
+ console.print(f"Supervaizer v{VERSION}")
47
+ if latest_version:
48
+ if is_latest:
49
+ console.print(f"[green]✓[/] Up to date (latest: v{latest_version})")
50
+ else:
51
+ console.print(f"[yellow]⚠[/] Latest available: v{latest_version}")
52
+ console.print("Update with: [bold]pip install --upgrade supervaizer[/]")
53
+ else:
54
+ console.print("(Unable to check for latest version)")
55
+
56
+
57
+ def _display_update_warning() -> None:
58
+ """Display update warning for commands."""
59
+ is_latest, latest_version = _check_version()
60
+ if latest_version and not is_latest:
61
+ console.print(
62
+ f"\n[bold yellow]⚠ Warning:[/] You are running Supervaizer v{VERSION}, "
63
+ f"but v{latest_version} is available.\n"
64
+ f"Update with: [bold]pip install --upgrade supervaizer[/]\n",
65
+ style="yellow",
66
+ )
67
+
20
68
 
21
69
  app = typer.Typer(
22
- help=f"Supervaizer Controller CLI v{VERSION} - Documentation @ https://docs.supervaize.com"
70
+ help=f"Supervaizer Controller CLI v{VERSION} - Documentation @ https://doc.supervaize.com/docs/category/supervaizer-controller"
23
71
  )
24
- console = Console()
72
+
73
+
74
+ @app.callback(invoke_without_command=True)
75
+ def main_callback(
76
+ ctx: typer.Context,
77
+ version: bool = typer.Option(
78
+ False, "--version", "-v", help="Show version information and exit"
79
+ ),
80
+ ) -> None:
81
+ """CLI callback that runs before any command."""
82
+ # Handle --version option
83
+ if version:
84
+ _display_version_info()
85
+ raise typer.Exit()
86
+
87
+ # Show version status in help or as warning for commands
88
+ if ctx.invoked_subcommand is None:
89
+ # For help display, show version status
90
+ _display_version_info()
91
+ else:
92
+ # For actual commands, show warning if not latest
93
+ _display_update_warning()
94
+
95
+
96
+ # Add deploy subcommand
97
+ app.add_typer(
98
+ deploy_app, name="deploy", help="Deploy Supervaizer agents to cloud platforms"
99
+ )
100
+
101
+ # Create scaffold subcommand group
102
+ scaffold_app = typer.Typer(help="Scaffold commands for creating project files")
103
+ app.add_typer(scaffold_app, name="scaffold", invoke_without_command=True)
25
104
 
26
105
 
27
106
  @app.command()
@@ -98,8 +177,45 @@ def start(
98
177
  process.wait()
99
178
 
100
179
 
101
- @app.command()
180
+ def _create_instructions_file(
181
+ output_dir: Path, force: bool = False, silent: bool = False
182
+ ) -> Path:
183
+ """Create supervaize_instructions.html file in the given directory.
184
+
185
+ Args:
186
+ output_dir: Directory where to create the instructions file
187
+ force: If True, overwrite existing file
188
+ silent: If True, don't show warnings if file already exists (just skip)
189
+ """
190
+ instructions_path = output_dir / "supervaize_instructions.html"
191
+
192
+ # Check if file already exists
193
+ if instructions_path.exists() and not force:
194
+ if not silent:
195
+ console.print(
196
+ f"[bold yellow]Warning:[/] {instructions_path} already exists"
197
+ )
198
+ console.print(
199
+ "Use [bold]--force[/] to overwrite it, or run [bold]supervaizer refresh-instructions[/]"
200
+ )
201
+ return instructions_path
202
+
203
+ # Get the path to the admin templates directory
204
+ admin_templates_dir = Path(__file__).parent / "admin" / "templates"
205
+ template_file = admin_templates_dir / "supervaize_instructions.html"
206
+
207
+ if not template_file.exists():
208
+ console.print("[bold red]Error:[/] Template file not found")
209
+ sys.exit(1)
210
+
211
+ # Copy the template file
212
+ shutil.copy(template_file, instructions_path)
213
+ return instructions_path
214
+
215
+
216
+ @scaffold_app.callback(invoke_without_command=True)
102
217
  def scaffold(
218
+ ctx: typer.Context,
103
219
  output_path: str = typer.Option(
104
220
  os.environ.get("SUPERVAIZER_OUTPUT_PATH", "supervaizer_control.py"),
105
221
  help="Path to save the script",
@@ -109,7 +225,10 @@ def scaffold(
109
225
  help="Overwrite existing file",
110
226
  ),
111
227
  ) -> None:
112
- """Create a draft supervaizer_control.py script."""
228
+ """Create a draft supervaizer_control.py script and supervaize_instructions.html."""
229
+ # Only run if no subcommand was invoked
230
+ if ctx.invoked_subcommand is not None:
231
+ return
113
232
  # Check if file already exists
114
233
  if os.path.exists(output_path) and not force:
115
234
  console.print(f"[bold red]Error:[/] {output_path} already exists")
@@ -129,18 +248,139 @@ def scaffold(
129
248
  console.print(
130
249
  f"[bold green]Success:[/] Created an example file at [bold blue]{output_path}[/]"
131
250
  )
251
+
252
+ # Create instructions file in the same directory (silently if it already exists)
253
+ output_dir = Path(output_path).parent
254
+ instructions_path = output_dir / "supervaize_instructions.html"
255
+ instructions_existed = instructions_path.exists()
256
+ _create_instructions_file(output_dir, force=force, silent=True)
257
+ # Only show success message if we actually created the file (didn't exist before or force was used)
258
+ if not instructions_existed or force:
259
+ console.print(
260
+ f"[bold green]Success:[/] Created instructions template at [bold blue]{instructions_path}[/]"
261
+ )
262
+
132
263
  console.print(
133
264
  "1. Copy this file to [bold]supervaizer_control.py[/] and edit it to configure your agent(s)"
134
265
  )
135
266
  console.print(
136
- "2. (Optional) Get your API from [bold]supervaizer.com and setup your environment variables"
267
+ "2. Customize [bold]supervaize_instructions.html[/] to match your agent's documentation"
137
268
  )
138
269
  console.print(
139
- "3. Run [bold]supervaizer start[/] to start the supervaizer controller"
270
+ "3. (Optional) Get your API from [bold]supervaizer.com and setup your environment variables"
140
271
  )
141
- console.print("4. Open [bold]http://localhost:8000/docs[/] to explore the API")
272
+ console.print(
273
+ "4. Run [bold]supervaizer start[/] to start the supervaizer controller"
274
+ )
275
+ console.print("5. Open [bold]http://localhost:8000/docs[/] to explore the API")
142
276
  sys.exit(0)
143
277
 
144
278
 
279
+ @scaffold_app.command(name="instructions")
280
+ def scaffold_instructions(
281
+ control_file: Optional[str] = typer.Option(
282
+ None,
283
+ help="Path to supervaizer_control.py (default: auto-detect)",
284
+ ),
285
+ output_path: Optional[str] = typer.Option(
286
+ None,
287
+ help="Path to save supervaize_instructions.html (default: same directory as control file)",
288
+ ),
289
+ force: bool = typer.Option(
290
+ False,
291
+ help="Overwrite existing file",
292
+ ),
293
+ ) -> None:
294
+ """Create supervaize_instructions.html file."""
295
+ # Determine control file path
296
+ if control_file is None:
297
+ control_file = (
298
+ os.environ.get("SUPERVAIZER_SCRIPT_PATH") or "supervaizer_control.py"
299
+ )
300
+
301
+ control_path = Path(control_file)
302
+
303
+ # Determine output directory
304
+ if output_path is None:
305
+ output_dir = control_path.parent
306
+ instructions_path = output_dir / "supervaize_instructions.html"
307
+ else:
308
+ instructions_path = Path(output_path)
309
+ output_dir = instructions_path.parent
310
+
311
+ # Check if control file exists (informational)
312
+ if not control_path.exists():
313
+ console.print(f"[bold yellow]Warning:[/] Control file {control_file} not found")
314
+ console.print("Creating instructions file anyway...")
315
+
316
+ # Create instructions file
317
+ _create_instructions_file(output_dir, force=force)
318
+ console.print(
319
+ f"[bold green]Success:[/] Created instructions template at [bold blue]{instructions_path}[/]"
320
+ )
321
+ console.print(
322
+ "Customize this file to match your agent's documentation and instructions."
323
+ )
324
+
325
+
326
+ @scaffold_app.command(name="refresh-instructions")
327
+ def refresh_instructions(
328
+ control_file: Optional[str] = typer.Option(
329
+ None,
330
+ help="Path to supervaizer_control.py (default: auto-detect)",
331
+ ),
332
+ output_path: Optional[str] = typer.Option(
333
+ None,
334
+ help="Path to supervaize_instructions.html (default: same directory as control file)",
335
+ ),
336
+ force: bool = typer.Option(
337
+ False,
338
+ help="Skip confirmation prompt",
339
+ ),
340
+ ) -> None:
341
+ """Refresh/update supervaize_instructions.html file."""
342
+ # Determine control file path
343
+ if control_file is None:
344
+ control_file = (
345
+ os.environ.get("SUPERVAIZER_SCRIPT_PATH") or "supervaizer_control.py"
346
+ )
347
+
348
+ control_path = Path(control_file)
349
+
350
+ # Determine output path
351
+ if output_path is None:
352
+ output_dir = control_path.parent
353
+ instructions_path = output_dir / "supervaize_instructions.html"
354
+ else:
355
+ instructions_path = Path(output_path)
356
+
357
+ # Check if instructions file exists
358
+ if instructions_path.exists():
359
+ if not force:
360
+ console.print(
361
+ f"[bold yellow]Warning:[/] {instructions_path} already exists"
362
+ )
363
+ if not Confirm.ask(
364
+ "Delete existing file and create a fresh template?",
365
+ default=False,
366
+ ):
367
+ console.print("[bold]Cancelled.[/]")
368
+ sys.exit(0)
369
+
370
+ # Delete existing file
371
+ instructions_path.unlink()
372
+ console.print(f"[bold]Deleted[/] existing {instructions_path}")
373
+
374
+ # Create new instructions file
375
+ output_dir = instructions_path.parent
376
+ _create_instructions_file(output_dir, force=True)
377
+ console.print(
378
+ f"[bold green]Success:[/] Created fresh instructions template at [bold blue]{instructions_path}[/]"
379
+ )
380
+ console.print(
381
+ "Customize this file to match your agent's documentation and instructions."
382
+ )
383
+
384
+
145
385
  if __name__ == "__main__":
146
386
  app()
supervaizer/common.py CHANGED
@@ -28,19 +28,44 @@ class SvBaseModel(BaseModel):
28
28
  Base model for all Supervaize models.
29
29
  """
30
30
 
31
+ @staticmethod
32
+ def serialize_value(value: Any) -> Any:
33
+ """Recursively serialize values, converting type objects and datetimes to strings."""
34
+ from datetime import datetime
35
+
36
+ if isinstance(value, type):
37
+ # Convert type objects to their string name
38
+ return value.__name__
39
+ elif isinstance(value, datetime):
40
+ # Convert datetime to ISO format string
41
+ return value.isoformat()
42
+ elif isinstance(value, dict):
43
+ # Recursively process dictionaries
44
+ return {k: SvBaseModel.serialize_value(v) for k, v in value.items()}
45
+ elif isinstance(value, (list, tuple)):
46
+ # Recursively process lists and tuples
47
+ return [SvBaseModel.serialize_value(item) for item in value]
48
+ else:
49
+ # Return value as-is for other types
50
+ return value
51
+
31
52
  @property
32
53
  def to_dict(self) -> Dict[str, Any]:
33
54
  """
34
55
  Convert the model to a dictionary.
35
56
 
36
- Note: Using mode="json" to handle datetime serialization.
57
+ Note: Handles datetime serialization and type objects by converting them
58
+ to their string representation.
37
59
  Tested in tests/test_common.test_sv_base_model_json_conversion
38
60
  """
39
- return self.model_dump(mode="json")
61
+ # Use mode="python" to avoid Pydantic's JSON serialization errors with type objects
62
+ # Then post-process to handle type objects and datetimes
63
+ data = self.model_dump(mode="python")
64
+ return self.serialize_value(data)
40
65
 
41
66
  @property
42
67
  def to_json(self) -> str:
43
- return self.model_dump_json()
68
+ return json.dumps(self.to_dict)
44
69
 
45
70
 
46
71
  class ApiResult:
@@ -251,8 +276,24 @@ def decrypt_value(encrypted_value: str, private_key: rsa.RSAPrivateKey) -> str:
251
276
  ValueError: If decryption fails
252
277
  """
253
278
 
279
+ # Basic validation
280
+ if not encrypted_value:
281
+ raise ValueError("Empty encrypted value")
282
+
283
+ # Clean the string
284
+ encrypted_value = encrypted_value.strip()
285
+
254
286
  # Decode base64
255
- combined = base64.b64decode(encrypted_value)
287
+ try:
288
+ combined = base64.b64decode(encrypted_value)
289
+ except Exception as e:
290
+ raise ValueError(f"Base64 decode failed: {str(e)}")
291
+
292
+ # Validate combined data structure
293
+ if len(combined) < 272:
294
+ raise ValueError(
295
+ f"Invalid encrypted data structure: too short ({len(combined)} bytes)"
296
+ )
256
297
 
257
298
  # Extract components - first 256 bytes are RSA encrypted key
258
299
  encrypted_key = combined[:256] # RSA-2048 output is 256 bytes
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
2
+ #
3
+ # This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ # If a copy of the MPL was not distributed with this file, you can obtain one at
5
+ # https://mozilla.org/MPL/2.0/.
6
+
7
+ """
8
+ Supervaizer Deployment CLI
9
+
10
+ This module provides automated deployment capabilities for Supervaizer agents
11
+ to cloud platforms including GCP Cloud Run, AWS App Runner, and DigitalOcean App Platform.
12
+ """
13
+
14
+ from supervaizer.__version__ import VERSION
15
+
16
+ __version__ = VERSION