supervaizer 0.9.7__py3-none-any.whl → 0.10.0__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 (58) 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 +46 -7
  6. supervaizer/admin/static/js/job-start-form.js +373 -0
  7. supervaizer/admin/templates/agents.html +74 -0
  8. supervaizer/admin/templates/agents_grid.html +5 -3
  9. supervaizer/admin/templates/job_start_test.html +109 -0
  10. supervaizer/admin/templates/navigation.html +11 -1
  11. supervaizer/admin/templates/supervaize_instructions.html +212 -0
  12. supervaizer/agent.py +165 -25
  13. supervaizer/case.py +46 -14
  14. supervaizer/cli.py +248 -8
  15. supervaizer/common.py +45 -4
  16. supervaizer/deploy/__init__.py +16 -0
  17. supervaizer/deploy/cli.py +296 -0
  18. supervaizer/deploy/commands/__init__.py +9 -0
  19. supervaizer/deploy/commands/clean.py +294 -0
  20. supervaizer/deploy/commands/down.py +119 -0
  21. supervaizer/deploy/commands/local.py +460 -0
  22. supervaizer/deploy/commands/plan.py +167 -0
  23. supervaizer/deploy/commands/status.py +169 -0
  24. supervaizer/deploy/commands/up.py +281 -0
  25. supervaizer/deploy/docker.py +370 -0
  26. supervaizer/deploy/driver_factory.py +42 -0
  27. supervaizer/deploy/drivers/__init__.py +39 -0
  28. supervaizer/deploy/drivers/aws_app_runner.py +607 -0
  29. supervaizer/deploy/drivers/base.py +196 -0
  30. supervaizer/deploy/drivers/cloud_run.py +570 -0
  31. supervaizer/deploy/drivers/do_app_platform.py +504 -0
  32. supervaizer/deploy/health.py +404 -0
  33. supervaizer/deploy/state.py +210 -0
  34. supervaizer/deploy/templates/Dockerfile.template +44 -0
  35. supervaizer/deploy/templates/debug_env.py +69 -0
  36. supervaizer/deploy/templates/docker-compose.yml.template +37 -0
  37. supervaizer/deploy/templates/dockerignore.template +66 -0
  38. supervaizer/deploy/templates/entrypoint.sh +20 -0
  39. supervaizer/deploy/utils.py +41 -0
  40. supervaizer/examples/{controller-template.py → controller_template.py} +5 -4
  41. supervaizer/job.py +18 -5
  42. supervaizer/job_service.py +6 -5
  43. supervaizer/parameter.py +61 -1
  44. supervaizer/protocol/__init__.py +2 -2
  45. supervaizer/protocol/a2a/routes.py +1 -1
  46. supervaizer/routes.py +262 -12
  47. supervaizer/server.py +5 -11
  48. supervaizer/utils/__init__.py +16 -0
  49. supervaizer/utils/version_check.py +56 -0
  50. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/METADATA +105 -34
  51. supervaizer-0.10.0.dist-info/RECORD +76 -0
  52. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/WHEEL +1 -1
  53. supervaizer/protocol/acp/__init__.py +0 -21
  54. supervaizer/protocol/acp/model.py +0 -198
  55. supervaizer/protocol/acp/routes.py +0 -74
  56. supervaizer-0.9.7.dist-info/RECORD +0 -50
  57. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/entry_points.txt +0 -0
  58. {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.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
@@ -83,8 +84,7 @@ class AgentMethodField(BaseModel):
83
84
  description: str | None = Field(
84
85
  default=None, description="Description of the field - displayed in the UI"
85
86
  )
86
- # TODO: confirm the structure of choices (list[str] or list[tuple(str)) - How do we integrate it in Supervaize
87
- choices: list[str] | None = Field(
87
+ choices: list[tuple[str, str]] | list[str] | None = Field(
88
88
  default=None, description="For choice fields, list of [value, label] pairs"
89
89
  )
90
90
 
@@ -211,6 +211,11 @@ class AgentMethodAbstract(BaseModel):
211
211
  },
212
212
  }
213
213
 
214
+ nodes: CaseNodes | None = Field(
215
+ default=None,
216
+ description="The definition of the Case Nodes (=steps) for this method",
217
+ )
218
+
214
219
 
215
220
  class AgentMethod(AgentMethodAbstract):
216
221
  @property
@@ -244,32 +249,151 @@ class AgentMethod(AgentMethodAbstract):
244
249
  return type("EmptyFieldsModel", (BaseModel,), {"to_dict": lambda self: {}})
245
250
 
246
251
  field_annotations = {}
247
- field_defaults: Dict[str, None] = {}
248
252
  for field in self.fields:
249
253
  field_name = field.name
250
254
  field_type = field.type
251
- is_required = field.required
255
+
256
+ # Convert Python types to proper typing annotations
257
+ if field_type is str:
258
+ annotation_type: type = str
259
+ elif field_type is int:
260
+ annotation_type = int
261
+ elif field_type is bool:
262
+ annotation_type = bool
263
+ elif field_type is list:
264
+ annotation_type = list
265
+ elif field_type is dict:
266
+ annotation_type = dict
267
+ elif field_type is float:
268
+ annotation_type = float
269
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ is list:
270
+ # Handle generic list types like list[str]
271
+ annotation_type = list
272
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ is dict:
273
+ # Handle generic dict types like dict[str, Any]
274
+ annotation_type = dict
275
+ else:
276
+ # Default to Any for unknown types
277
+ annotation_type = Any
278
+
279
+ # Make field optional if not required
252
280
  field_annotations[field_name] = (
253
- field_type if is_required else Optional[field_type]
281
+ annotation_type if field.required else Optional[annotation_type]
254
282
  )
255
- if not is_required:
256
- field_defaults[field_name] = None
257
283
 
258
- def to_dict(self: BaseModel) -> Dict[str, Any]:
284
+ # Create the dynamic model with proper module information
285
+ model_dict = {
286
+ "__module__": "supervaizer.agent",
287
+ "__annotations__": field_annotations,
288
+ "to_dict": lambda self: {
289
+ k: getattr(self, k)
290
+ for k in field_annotations.keys()
291
+ if hasattr(self, k)
292
+ },
293
+ }
294
+
295
+ return type("DynamicFieldsModel", (BaseModel,), model_dict)
296
+
297
+ def validate_method_fields(self, job_fields: Dict[str, Any]) -> Dict[str, Any]:
298
+ """Validate job fields against the method's field definitions.
299
+
300
+ Args:
301
+ job_fields: Dictionary of field names and values to validate
302
+
303
+ Returns:
304
+ Dictionary with validation results:
305
+ - "valid": bool - whether all fields are valid
306
+ - "errors": List[str] - list of validation error messages
307
+ - "invalid_fields": Dict[str, str] - field name to error message mapping
308
+ """
309
+ if self.fields is None:
259
310
  return {
260
- field_name: getattr(self, field_name)
261
- for field_name in self.__annotations__
311
+ "valid": True,
312
+ "message": "Method has no field definitions",
313
+ "errors": [],
314
+ "invalid_fields": {},
262
315
  }
263
316
 
264
- return type(
265
- "DynamicFieldsModel",
266
- (BaseModel,),
267
- {
268
- "__annotations__": field_annotations,
269
- "to_dict": to_dict,
270
- **field_defaults,
271
- },
272
- )
317
+ if len(self.fields) == 0:
318
+ return {
319
+ "valid": True,
320
+ "message": "Method fields validated successfully",
321
+ "errors": [],
322
+ "invalid_fields": {},
323
+ }
324
+
325
+ errors = []
326
+ invalid_fields = {}
327
+
328
+ # First check for missing required fields
329
+ for field in self.fields:
330
+ if field.required and field.name not in job_fields:
331
+ error_msg = f"Required field '{field.name}' is missing"
332
+ errors.append(error_msg)
333
+ invalid_fields[field.name] = error_msg
334
+
335
+ # Then validate the provided fields
336
+ for field_name, field_value in job_fields.items():
337
+ # Find the field definition
338
+ field_def = next((f for f in self.fields if f.name == field_name), None)
339
+ if not field_def:
340
+ error_msg = f"Unknown field '{field_name}'"
341
+ errors.append(error_msg)
342
+ invalid_fields[field_name] = error_msg
343
+ continue
344
+
345
+ # Skip validation for None values (optional fields)
346
+ if field_value is None:
347
+ continue
348
+
349
+ # Type validation
350
+ expected_type = field_def.type
351
+ if expected_type:
352
+ try:
353
+ # Handle special cases for type validation
354
+ if expected_type is str:
355
+ if not isinstance(field_value, str):
356
+ error_msg = f"Field '{field_name}' must be a string, got {type(field_value).__name__}"
357
+ errors.append(error_msg)
358
+ invalid_fields[field_name] = error_msg
359
+ elif expected_type is int:
360
+ if not isinstance(field_value, int):
361
+ error_msg = f"Field '{field_name}' must be an integer, got {type(field_value).__name__}"
362
+ errors.append(error_msg)
363
+ invalid_fields[field_name] = error_msg
364
+ elif expected_type is bool:
365
+ if not isinstance(field_value, bool):
366
+ error_msg = f"Field '{field_name}' must be a boolean, got {type(field_value).__name__}"
367
+ errors.append(error_msg)
368
+ invalid_fields[field_name] = error_msg
369
+ elif expected_type is list:
370
+ if not isinstance(field_value, list):
371
+ error_msg = f"Field '{field_name}' must be a list, got {type(field_value).__name__}"
372
+ errors.append(error_msg)
373
+ invalid_fields[field_name] = error_msg
374
+ elif expected_type is dict:
375
+ if not isinstance(field_value, dict):
376
+ error_msg = f"Field '{field_name}' must be a dictionary, got {type(field_value).__name__}"
377
+ errors.append(error_msg)
378
+ invalid_fields[field_name] = error_msg
379
+ elif expected_type is float:
380
+ if not isinstance(field_value, (int, float)):
381
+ error_msg = f"Field '{field_name}' must be a number, got {type(field_value).__name__}"
382
+ errors.append(error_msg)
383
+ invalid_fields[field_name] = error_msg
384
+ except Exception as e:
385
+ error_msg = f"Field '{field_name}' validation failed: {str(e)}"
386
+ errors.append(error_msg)
387
+ invalid_fields[field_name] = error_msg
388
+
389
+ return {
390
+ "valid": len(errors) == 0,
391
+ "message": "Method fields validated successfully"
392
+ if len(errors) == 0
393
+ else "Method field validation failed",
394
+ "errors": errors,
395
+ "invalid_fields": invalid_fields,
396
+ }
273
397
 
274
398
  @property
275
399
  def job_model(self) -> type[AgentJobContextBase]:
@@ -301,6 +425,7 @@ class AgentMethod(AgentMethodAbstract):
301
425
  "params": self.params,
302
426
  "fields": self.fields_definitions,
303
427
  "description": self.description,
428
+ "nodes": self.nodes.registration_info if self.nodes else None,
304
429
  }
305
430
 
306
431
 
@@ -322,8 +447,9 @@ class AgentCustomMethodParams(AgentMethodParams):
322
447
 
323
448
  class AgentMethodsAbstract(BaseModel):
324
449
  job_start: AgentMethod
325
- job_stop: AgentMethod
326
- job_status: AgentMethod
450
+ job_stop: AgentMethod | None = None
451
+ job_status: AgentMethod | None = None
452
+ human_answer: AgentMethod | None = None
327
453
  chat: AgentMethod | None = None
328
454
  custom: dict[str, AgentMethod] | None = None
329
455
 
@@ -358,8 +484,13 @@ class AgentMethods(AgentMethodsAbstract):
358
484
  def registration_info(self) -> Dict[str, Any]:
359
485
  return {
360
486
  "job_start": self.job_start.registration_info,
361
- "job_stop": self.job_stop.registration_info,
362
- "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,
363
494
  "chat": self.chat.registration_info if self.chat else None,
364
495
  "custom": {
365
496
  name: method.registration_info
@@ -457,6 +588,14 @@ class AgentAbstract(SvBaseModel):
457
588
  default=60 * 60,
458
589
  description="Maximum execution time in seconds, defaults to 1 hour",
459
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
+ )
460
599
 
461
600
  model_config = {
462
601
  "reference_group": "Core",
@@ -572,6 +711,7 @@ class Agent(AgentAbstract):
572
711
  "server_agent_onboarding_status": self.server_agent_onboarding_status,
573
712
  "server_encrypted_parameters": self.server_encrypted_parameters,
574
713
  "max_execution_time": self.max_execution_time,
714
+ "instructions_path": self.instructions_path,
575
715
  }
576
716
 
577
717
  def update_agent_from_server(self, server: "Server") -> Optional["Agent"]:
@@ -770,13 +910,13 @@ class Agent(AgentAbstract):
770
910
  return job
771
911
 
772
912
  def job_stop(self, params: Dict[str, Any] = {}) -> Any:
773
- if not self.methods:
913
+ if not self.methods or not self.methods.job_stop:
774
914
  raise ValueError("Agent methods not defined")
775
915
  method = self.methods.job_stop.method
776
916
  return self._execute(method, params)
777
917
 
778
918
  def job_status(self, params: Dict[str, Any] = {}) -> Any:
779
- if not self.methods:
919
+ if not self.methods or not self.methods.job_status:
780
920
  raise ValueError("Agent methods not defined")
781
921
  method = self.methods.job_status.method
782
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