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.
- supervaizer/__init__.py +11 -2
- supervaizer/__version__.py +1 -1
- supervaizer/account.py +4 -0
- supervaizer/account_service.py +7 -1
- supervaizer/admin/routes.py +46 -7
- supervaizer/admin/static/js/job-start-form.js +373 -0
- supervaizer/admin/templates/agents.html +74 -0
- supervaizer/admin/templates/agents_grid.html +5 -3
- supervaizer/admin/templates/job_start_test.html +109 -0
- supervaizer/admin/templates/navigation.html +11 -1
- supervaizer/admin/templates/supervaize_instructions.html +212 -0
- supervaizer/agent.py +165 -25
- supervaizer/case.py +46 -14
- supervaizer/cli.py +248 -8
- supervaizer/common.py +45 -4
- supervaizer/deploy/__init__.py +16 -0
- supervaizer/deploy/cli.py +296 -0
- supervaizer/deploy/commands/__init__.py +9 -0
- supervaizer/deploy/commands/clean.py +294 -0
- supervaizer/deploy/commands/down.py +119 -0
- supervaizer/deploy/commands/local.py +460 -0
- supervaizer/deploy/commands/plan.py +167 -0
- supervaizer/deploy/commands/status.py +169 -0
- supervaizer/deploy/commands/up.py +281 -0
- supervaizer/deploy/docker.py +370 -0
- supervaizer/deploy/driver_factory.py +42 -0
- supervaizer/deploy/drivers/__init__.py +39 -0
- supervaizer/deploy/drivers/aws_app_runner.py +607 -0
- supervaizer/deploy/drivers/base.py +196 -0
- supervaizer/deploy/drivers/cloud_run.py +570 -0
- supervaizer/deploy/drivers/do_app_platform.py +504 -0
- supervaizer/deploy/health.py +404 -0
- supervaizer/deploy/state.py +210 -0
- supervaizer/deploy/templates/Dockerfile.template +44 -0
- supervaizer/deploy/templates/debug_env.py +69 -0
- supervaizer/deploy/templates/docker-compose.yml.template +37 -0
- supervaizer/deploy/templates/dockerignore.template +66 -0
- supervaizer/deploy/templates/entrypoint.sh +20 -0
- supervaizer/deploy/utils.py +41 -0
- supervaizer/examples/{controller-template.py → controller_template.py} +5 -4
- supervaizer/job.py +18 -5
- supervaizer/job_service.py +6 -5
- supervaizer/parameter.py +61 -1
- supervaizer/protocol/__init__.py +2 -2
- supervaizer/protocol/a2a/routes.py +1 -1
- supervaizer/routes.py +262 -12
- supervaizer/server.py +5 -11
- supervaizer/utils/__init__.py +16 -0
- supervaizer/utils/version_check.py +56 -0
- {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/METADATA +105 -34
- supervaizer-0.10.0.dist-info/RECORD +76 -0
- {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/WHEEL +1 -1
- supervaizer/protocol/acp/__init__.py +0 -21
- supervaizer/protocol/acp/model.py +0 -198
- supervaizer/protocol/acp/routes.py +0 -74
- supervaizer-0.9.7.dist-info/RECORD +0 -50
- {supervaizer-0.9.7.dist-info → supervaizer-0.10.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
311
|
+
"valid": True,
|
|
312
|
+
"message": "Method has no field definitions",
|
|
313
|
+
"errors": [],
|
|
314
|
+
"invalid_fields": {},
|
|
262
315
|
}
|
|
263
316
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
|
|
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
|
|
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":
|
|
112
|
+
"payload": serialized_payload,
|
|
110
113
|
"is_final": self.is_final,
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
|
|
114
|
-
class
|
|
117
|
+
class CaseNodeType(Enum):
|
|
115
118
|
"""
|
|
116
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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(
|
|
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
|
|