supervaizer 0.10.5__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 +97 -0
- supervaizer/__version__.py +10 -0
- supervaizer/account.py +308 -0
- supervaizer/account_service.py +93 -0
- supervaizer/admin/routes.py +1293 -0
- supervaizer/admin/static/js/job-start-form.js +373 -0
- supervaizer/admin/templates/agent_detail.html +145 -0
- supervaizer/admin/templates/agents.html +249 -0
- supervaizer/admin/templates/agents_grid.html +82 -0
- supervaizer/admin/templates/base.html +233 -0
- supervaizer/admin/templates/case_detail.html +230 -0
- supervaizer/admin/templates/cases_list.html +182 -0
- supervaizer/admin/templates/cases_table.html +134 -0
- supervaizer/admin/templates/console.html +389 -0
- supervaizer/admin/templates/dashboard.html +153 -0
- supervaizer/admin/templates/job_detail.html +192 -0
- supervaizer/admin/templates/job_start_test.html +109 -0
- supervaizer/admin/templates/jobs_list.html +180 -0
- supervaizer/admin/templates/jobs_table.html +122 -0
- supervaizer/admin/templates/navigation.html +163 -0
- supervaizer/admin/templates/recent_activity.html +81 -0
- supervaizer/admin/templates/server.html +105 -0
- supervaizer/admin/templates/server_status_cards.html +121 -0
- supervaizer/admin/templates/supervaize_instructions.html +212 -0
- supervaizer/agent.py +956 -0
- supervaizer/case.py +432 -0
- supervaizer/cli.py +395 -0
- supervaizer/common.py +324 -0
- supervaizer/deploy/__init__.py +16 -0
- supervaizer/deploy/cli.py +305 -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 +377 -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 +52 -0
- supervaizer/event.py +181 -0
- supervaizer/examples/controller_template.py +196 -0
- supervaizer/instructions.py +145 -0
- supervaizer/job.py +392 -0
- supervaizer/job_service.py +156 -0
- supervaizer/lifecycle.py +417 -0
- supervaizer/parameter.py +233 -0
- supervaizer/protocol/__init__.py +11 -0
- supervaizer/protocol/a2a/__init__.py +21 -0
- supervaizer/protocol/a2a/model.py +227 -0
- supervaizer/protocol/a2a/routes.py +99 -0
- supervaizer/py.typed +1 -0
- supervaizer/routes.py +917 -0
- supervaizer/server.py +553 -0
- supervaizer/server_utils.py +54 -0
- supervaizer/storage.py +462 -0
- supervaizer/telemetry.py +81 -0
- supervaizer/utils/__init__.py +16 -0
- supervaizer/utils/version_check.py +56 -0
- supervaizer-0.10.5.dist-info/METADATA +317 -0
- supervaizer-0.10.5.dist-info/RECORD +76 -0
- supervaizer-0.10.5.dist-info/WHEEL +4 -0
- supervaizer-0.10.5.dist-info/entry_points.txt +2 -0
- supervaizer-0.10.5.dist-info/licenses/LICENSE.md +346 -0
supervaizer/agent.py
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
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
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
ClassVar,
|
|
15
|
+
Dict,
|
|
16
|
+
List,
|
|
17
|
+
Optional,
|
|
18
|
+
TypeVar,
|
|
19
|
+
)
|
|
20
|
+
import shortuuid
|
|
21
|
+
from pydantic import BaseModel, field_validator, Field
|
|
22
|
+
from rich import inspect, print
|
|
23
|
+
from slugify import slugify
|
|
24
|
+
from supervaizer.__version__ import VERSION
|
|
25
|
+
from supervaizer.common import ApiSuccess, SvBaseModel, log
|
|
26
|
+
from supervaizer.event import JobStartConfirmationEvent
|
|
27
|
+
from supervaizer.job import Job, JobContext, JobResponse
|
|
28
|
+
from supervaizer.job_service import service_job_finished
|
|
29
|
+
from supervaizer.lifecycle import EntityStatus
|
|
30
|
+
from supervaizer.parameter import ParametersSetup
|
|
31
|
+
from supervaizer.case import CaseNodes
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from supervaizer.server import Server
|
|
35
|
+
|
|
36
|
+
insp = inspect
|
|
37
|
+
prnt = print
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FieldTypeEnum(str, Enum):
|
|
43
|
+
CHAR = "CharField"
|
|
44
|
+
INT = "IntegerField"
|
|
45
|
+
BOOL = "BooleanField"
|
|
46
|
+
CHOICE = "ChoiceField"
|
|
47
|
+
MULTICHOICE = "MultipleChoiceField"
|
|
48
|
+
DATE = "DateField"
|
|
49
|
+
DATETIME = "DateTimeField"
|
|
50
|
+
FLOAT = "FloatField"
|
|
51
|
+
EMAIL = "EmailField"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AgentMethodField(BaseModel):
|
|
55
|
+
"""
|
|
56
|
+
Represents a field specification for generating forms/UI in the Supervaize platform.
|
|
57
|
+
|
|
58
|
+
Fields are used to define user input parameters that will be collected through
|
|
59
|
+
the UI and passed as kwargs to the AgentMethod.method. They follow Django forms
|
|
60
|
+
field definitions for consistency.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
- [Django Widgets](https://docs.djangoproject.com/en/5.2/ref/forms/widgets/)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
** field_type - available field types ** [Django Field classes](https://docs.djangoproject.com/en/5.2/ref/forms/fields/#built-in-field-classes)
|
|
67
|
+
|
|
68
|
+
- `CharField` - Text input
|
|
69
|
+
- `IntegerField` - Number input
|
|
70
|
+
- `BooleanField` - Checkbox
|
|
71
|
+
- `ChoiceField` - Dropdown with options
|
|
72
|
+
- `MultipleChoiceField` - Multi-select
|
|
73
|
+
- `JSONField` - JSON data input
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
name: str = Field(description="The name of the field - displayed in the UI")
|
|
78
|
+
type: Any = Field(
|
|
79
|
+
description="Python type of the field for pydantic validation - note , ChoiceField and MultipleChoiceField are a list[str]"
|
|
80
|
+
)
|
|
81
|
+
field_type: FieldTypeEnum = Field(
|
|
82
|
+
default=FieldTypeEnum.CHAR, description="Field type for persistence"
|
|
83
|
+
)
|
|
84
|
+
description: str | None = Field(
|
|
85
|
+
default=None, description="Description of the field - displayed in the UI"
|
|
86
|
+
)
|
|
87
|
+
choices: list[tuple[str, str]] | list[str] | None = Field(
|
|
88
|
+
default=None, description="For choice fields, list of [value, label] pairs"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
default: Any = Field(
|
|
92
|
+
default=None, description="Default value for the field - displayed in the UI"
|
|
93
|
+
)
|
|
94
|
+
widget: str | None = Field(
|
|
95
|
+
default=None,
|
|
96
|
+
description="UI widget to use (e.g. RadioSelect, TextInput) - as a django widget name",
|
|
97
|
+
)
|
|
98
|
+
required: bool = Field(
|
|
99
|
+
default=False, description="Whether field is required for form submission"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
model_config = {
|
|
103
|
+
"reference_group": "Core",
|
|
104
|
+
"json_schema_extra": {
|
|
105
|
+
"examples": [
|
|
106
|
+
{
|
|
107
|
+
"name": "color",
|
|
108
|
+
"type": "list[str]",
|
|
109
|
+
"field_type": "MultipleChoiceField",
|
|
110
|
+
"choices": [["B", "Blue"], ["R", "Red"], ["G", "Green"]],
|
|
111
|
+
"widget": "RadioSelect",
|
|
112
|
+
"required": True,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "age",
|
|
116
|
+
"type": "int",
|
|
117
|
+
"field_type": "IntegerField",
|
|
118
|
+
"widget": "NumberInput",
|
|
119
|
+
"required": False,
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AgentJobContextBase(BaseModel):
|
|
127
|
+
"""
|
|
128
|
+
Base model for agent job context parameters
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
job_context: JobContext
|
|
132
|
+
job_fields: Dict[str, Any]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class AgentMethodAbstract(BaseModel):
|
|
136
|
+
"""
|
|
137
|
+
Represents a method that can be called on an agent.
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
name: Display name of the method
|
|
141
|
+
method: Name of the actual method in the project's codebase that will be called with the provided parameters
|
|
142
|
+
params: see below
|
|
143
|
+
fields: see below
|
|
144
|
+
description: Optional description of what the method does
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
1. params : Dictionary format
|
|
148
|
+
A simple key-value dictionary of parameters what will be passed to the
|
|
149
|
+
AgentMethod.method as kwargs.
|
|
150
|
+
Example:
|
|
151
|
+
{
|
|
152
|
+
"verbose": True,
|
|
153
|
+
"timeout": 60,
|
|
154
|
+
"max_retries": 3
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
2. fields : Form fields format
|
|
158
|
+
These are the values that will be requested from the user in the Supervaize UI
|
|
159
|
+
and also passed as kwargs to the AgentMethod.method.
|
|
160
|
+
A list of field specifications for generating forms/UI, following the
|
|
161
|
+
django.forms.fields definition
|
|
162
|
+
see : https://docs.djangoproject.com/en/5.1/ref/forms/fields/
|
|
163
|
+
Each field is a dictionary with properties like:
|
|
164
|
+
- name: Field identifier
|
|
165
|
+
- type: Python type of the field for pydantic validation - note , ChoiceField and MultipleChoiceField are a list[str]
|
|
166
|
+
- field_type: Field type (one of: CharField, IntegerField, BooleanField, ChoiceField, MultipleChoiceField)
|
|
167
|
+
- choices: For choice fields, list of [value, label] pairs
|
|
168
|
+
- default: (optional) Default value for the field
|
|
169
|
+
- widget: UI widget to use (e.g. RadioSelect, TextInput)
|
|
170
|
+
- required: Whether field is required
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
name: str = Field(description="The name of the method")
|
|
177
|
+
method: str = Field(
|
|
178
|
+
description="The name of the method in the project's codebase that will be called with the provided parameters"
|
|
179
|
+
)
|
|
180
|
+
params: Dict[str, Any] | None = Field(
|
|
181
|
+
default=None,
|
|
182
|
+
description="A simple key-value dictionary of parameters what will be passed to the AgentMethod.method as kwargs",
|
|
183
|
+
)
|
|
184
|
+
fields: List[AgentMethodField] | None = Field(
|
|
185
|
+
default=None,
|
|
186
|
+
description="A list of field specifications for generating forms/UI, following the django.forms.fields definition",
|
|
187
|
+
)
|
|
188
|
+
description: str | None = Field(
|
|
189
|
+
default=None, description="Optional description of what the method does"
|
|
190
|
+
)
|
|
191
|
+
is_async: bool = Field(
|
|
192
|
+
default=False, description="Whether the method is asynchronous"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
model_config = {
|
|
196
|
+
"reference_group": "Core",
|
|
197
|
+
"example_dict": {
|
|
198
|
+
"name": "start",
|
|
199
|
+
"method": "example_agent.example_synchronous_job_start",
|
|
200
|
+
"params": {"action": "start"},
|
|
201
|
+
"fields": [
|
|
202
|
+
{
|
|
203
|
+
"name": "Company to research",
|
|
204
|
+
"type": str,
|
|
205
|
+
"field_type": "CharField",
|
|
206
|
+
"max_length": 100,
|
|
207
|
+
"required": True,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
"description": "Start the collection of new competitor summary",
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
nodes: CaseNodes | None = Field(
|
|
215
|
+
default=None,
|
|
216
|
+
description="The definition of the Case Nodes (=steps) for this method",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class AgentMethod(AgentMethodAbstract):
|
|
221
|
+
@property
|
|
222
|
+
def fields_definitions(self) -> list[Dict[str, Any]]:
|
|
223
|
+
"""
|
|
224
|
+
Returns a list of the fields with the type key as a string
|
|
225
|
+
Used for the API response.
|
|
226
|
+
"""
|
|
227
|
+
if self.fields:
|
|
228
|
+
result = []
|
|
229
|
+
for field in self.fields:
|
|
230
|
+
d = {k: v for k, v in field.__dict__.items() if k != "type"}
|
|
231
|
+
# type as string
|
|
232
|
+
type_val = field.type
|
|
233
|
+
if hasattr(type_val, "__name__"):
|
|
234
|
+
d["type"] = type_val.__name__
|
|
235
|
+
elif hasattr(type_val, "_name") and type_val._name:
|
|
236
|
+
d["type"] = type_val._name
|
|
237
|
+
else:
|
|
238
|
+
d["type"] = str(type_val)
|
|
239
|
+
result.append(d)
|
|
240
|
+
return result
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def fields_annotations(self) -> type[BaseModel]:
|
|
245
|
+
"""
|
|
246
|
+
Creates and returns a dynamic Pydantic model class based on the field definitions.
|
|
247
|
+
"""
|
|
248
|
+
if not self.fields:
|
|
249
|
+
return type("EmptyFieldsModel", (BaseModel,), {"to_dict": lambda self: {}})
|
|
250
|
+
|
|
251
|
+
field_annotations = {}
|
|
252
|
+
for field in self.fields:
|
|
253
|
+
field_name = field.name
|
|
254
|
+
field_type = field.type
|
|
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
|
|
280
|
+
field_annotations[field_name] = (
|
|
281
|
+
annotation_type if field.required else Optional[annotation_type]
|
|
282
|
+
)
|
|
283
|
+
|
|
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:
|
|
310
|
+
return {
|
|
311
|
+
"valid": True,
|
|
312
|
+
"message": "Method has no field definitions",
|
|
313
|
+
"errors": [],
|
|
314
|
+
"invalid_fields": {},
|
|
315
|
+
}
|
|
316
|
+
|
|
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
|
+
}
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def job_model(self) -> type[AgentJobContextBase]:
|
|
400
|
+
"""
|
|
401
|
+
Creates and returns a dynamic Pydantic model class combining job context and job fields.
|
|
402
|
+
"""
|
|
403
|
+
fields_model = self.fields_annotations
|
|
404
|
+
|
|
405
|
+
return type(
|
|
406
|
+
"AgentJobAbstract",
|
|
407
|
+
(AgentJobContextBase,),
|
|
408
|
+
{
|
|
409
|
+
"__annotations__": {
|
|
410
|
+
"job_context": JobContext,
|
|
411
|
+
"job_fields": fields_model,
|
|
412
|
+
"encrypted_agent_parameters": str | None,
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Returns a JSON-serializable dictionary representation of the AgentMethod.
|
|
421
|
+
"""
|
|
422
|
+
return {
|
|
423
|
+
"name": self.name,
|
|
424
|
+
"method": str(self.method),
|
|
425
|
+
"params": self.params,
|
|
426
|
+
"fields": self.fields_definitions,
|
|
427
|
+
"description": self.description,
|
|
428
|
+
"nodes": self.nodes.registration_info if self.nodes else None,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class AgentMethodParams(BaseModel):
|
|
433
|
+
"""
|
|
434
|
+
Method parameters for agent operations.
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
params: Dict[str, Any] = Field(
|
|
439
|
+
default_factory=dict,
|
|
440
|
+
description="A simple key-value dictionary of parameters what will be passed to the AgentMethod.method as kwargs",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class AgentCustomMethodParams(AgentMethodParams):
|
|
445
|
+
method_name: str
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class AgentMethodsAbstract(BaseModel):
|
|
449
|
+
job_start: AgentMethod
|
|
450
|
+
job_stop: AgentMethod | None = None
|
|
451
|
+
job_status: AgentMethod | None = None
|
|
452
|
+
human_answer: AgentMethod | None = None
|
|
453
|
+
chat: AgentMethod | None = None
|
|
454
|
+
custom: dict[str, AgentMethod] | None = None
|
|
455
|
+
|
|
456
|
+
@field_validator("custom")
|
|
457
|
+
@classmethod
|
|
458
|
+
def validate_custom_method_keys(
|
|
459
|
+
cls, value: dict[str, AgentMethod]
|
|
460
|
+
) -> dict[str, AgentMethod]:
|
|
461
|
+
"""Validate that custom method keys are valid slug-like values suitable for endpoints."""
|
|
462
|
+
if value:
|
|
463
|
+
for key in value.keys():
|
|
464
|
+
# Check if key is a valid slug format
|
|
465
|
+
if not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", key):
|
|
466
|
+
raise ValueError(
|
|
467
|
+
f"Custom method key '{key}' is not a valid slug. "
|
|
468
|
+
f"Keys must contain only lowercase letters, numbers, and hyphens, "
|
|
469
|
+
f"and cannot start or end with a hyphen. "
|
|
470
|
+
f"Examples: 'backup', 'health-check', 'sync-data'"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Additional checks for endpoint safety
|
|
474
|
+
if len(key) > 50:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"Custom method key '{key}' is too long (max 50 characters)"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return value
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class AgentMethods(AgentMethodsAbstract):
|
|
483
|
+
@property
|
|
484
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
485
|
+
return {
|
|
486
|
+
"job_start": self.job_start.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,
|
|
494
|
+
"chat": self.chat.registration_info if self.chat else None,
|
|
495
|
+
"custom": {
|
|
496
|
+
name: method.registration_info
|
|
497
|
+
for name, method in (self.custom or {}).items()
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class AgentAbstract(SvBaseModel):
|
|
503
|
+
"""
|
|
504
|
+
Agent model for the Supervaize Control API.
|
|
505
|
+
|
|
506
|
+
This represents an agent that can be registered with the Supervaize Control API.
|
|
507
|
+
It contains metadata about the agent like name, version, description etc. as well as
|
|
508
|
+
the methods it supports and any parameter configurations.
|
|
509
|
+
|
|
510
|
+
The agent ID is automatically generated from the name and must match.
|
|
511
|
+
|
|
512
|
+
Example:
|
|
513
|
+
```python
|
|
514
|
+
Agent(
|
|
515
|
+
name="Email AI Agent",
|
|
516
|
+
author="@parthshr370", # Author of the agent
|
|
517
|
+
developer="@alain_sv", # Developer of the controller
|
|
518
|
+
maintainer="@aintainer",
|
|
519
|
+
editor="AI Editor",
|
|
520
|
+
version="1.0.0",
|
|
521
|
+
description="AI-powered email processing agent that can fetch, analyze, generate responses, and send/draft emails",
|
|
522
|
+
tags=["email", "ai", "automation", "communication"],
|
|
523
|
+
methods=AgentMethods(
|
|
524
|
+
job_start=process_email_method, # Job start method
|
|
525
|
+
job_stop=job_stop, # Job stop method
|
|
526
|
+
job_status=job_status, # Job status method
|
|
527
|
+
chat=None,
|
|
528
|
+
custom=None,
|
|
529
|
+
),
|
|
530
|
+
parameters_setup=ParametersSetup.from_list([
|
|
531
|
+
Parameter(
|
|
532
|
+
name="IMAP_USERNAME",
|
|
533
|
+
description="IMAP username for email access",
|
|
534
|
+
is_environment=True,
|
|
535
|
+
is_secret=False,
|
|
536
|
+
),
|
|
537
|
+
Parameter(
|
|
538
|
+
name="IMAP_PASSWORD",
|
|
539
|
+
description="IMAP password for email access",
|
|
540
|
+
is_environment=True,
|
|
541
|
+
is_secret=True,
|
|
542
|
+
),
|
|
543
|
+
]),
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
supervaizer_VERSION: ClassVar[str] = VERSION
|
|
549
|
+
name: str = Field(description="Display name of the agent")
|
|
550
|
+
id: str = Field(description="Unique ID generated from name")
|
|
551
|
+
author: Optional[str] = Field(default=None, description="Author of the agent")
|
|
552
|
+
developer: Optional[str] = Field(
|
|
553
|
+
default=None, description="Developer of the controller integration"
|
|
554
|
+
)
|
|
555
|
+
maintainer: Optional[str] = Field(
|
|
556
|
+
default=None, description="Maintainer of the integration"
|
|
557
|
+
)
|
|
558
|
+
editor: Optional[str] = Field(
|
|
559
|
+
default=None, description="Editor (usually a company)"
|
|
560
|
+
)
|
|
561
|
+
version: str = Field(default="", description="Version string")
|
|
562
|
+
description: str = Field(
|
|
563
|
+
default="", description="Description of what the agent does"
|
|
564
|
+
)
|
|
565
|
+
tags: list[str] | None = Field(
|
|
566
|
+
default=None, description="Tags for categorizing the agent"
|
|
567
|
+
)
|
|
568
|
+
methods: AgentMethods | None = Field(
|
|
569
|
+
default=None, description="Methods supported by this agent"
|
|
570
|
+
)
|
|
571
|
+
parameters_setup: ParametersSetup | None = Field(
|
|
572
|
+
default=None, description="Parameter configuration"
|
|
573
|
+
)
|
|
574
|
+
server_agent_id: str | None = Field(
|
|
575
|
+
default=None, description="ID assigned by server - Do not set this manually"
|
|
576
|
+
)
|
|
577
|
+
server_agent_status: str | None = Field(
|
|
578
|
+
default=None, description="Current status on server - Do not set this manually"
|
|
579
|
+
)
|
|
580
|
+
server_agent_onboarding_status: str | None = Field(
|
|
581
|
+
default=None, description="Onboarding status - Do not set this manually"
|
|
582
|
+
)
|
|
583
|
+
server_encrypted_parameters: str | None = Field(
|
|
584
|
+
default=None,
|
|
585
|
+
description="Encrypted parameters from server - Do not set this manually",
|
|
586
|
+
)
|
|
587
|
+
max_execution_time: int = Field(
|
|
588
|
+
default=60 * 60,
|
|
589
|
+
description="Maximum execution time in seconds, defaults to 1 hour",
|
|
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
|
+
)
|
|
599
|
+
|
|
600
|
+
model_config = {
|
|
601
|
+
"reference_group": "Core",
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class Agent(AgentAbstract):
|
|
606
|
+
def __init__(
|
|
607
|
+
self,
|
|
608
|
+
name: str,
|
|
609
|
+
id: str | None = None,
|
|
610
|
+
author: Optional[str] = None,
|
|
611
|
+
developer: Optional[str] = None,
|
|
612
|
+
maintainer: Optional[str] = None,
|
|
613
|
+
editor: Optional[str] = None,
|
|
614
|
+
version: str = "",
|
|
615
|
+
description: str = "",
|
|
616
|
+
tags: list[str] | None = None,
|
|
617
|
+
methods: AgentMethods | None = None,
|
|
618
|
+
parameters_setup: ParametersSetup | None = None,
|
|
619
|
+
server_agent_id: str | None = None,
|
|
620
|
+
server_agent_status: str | None = None,
|
|
621
|
+
server_agent_onboarding_status: str | None = None,
|
|
622
|
+
server_encrypted_parameters: str | None = None,
|
|
623
|
+
max_execution_time: int = 60 * 60, # 1 hour (in seconds)
|
|
624
|
+
**kwargs: Any,
|
|
625
|
+
) -> None:
|
|
626
|
+
"""
|
|
627
|
+
This represents an agent that can be registered with the Supervaize Control API.
|
|
628
|
+
It contains metadata about the agent like name, version, description etc. as well as
|
|
629
|
+
the methods it supports and any parameter configurations.
|
|
630
|
+
|
|
631
|
+
The agent ID is automatically generated from the name and must match.
|
|
632
|
+
|
|
633
|
+
Attributes:
|
|
634
|
+
name (str): Display name of the agent
|
|
635
|
+
id (str): Unique ID generated from name
|
|
636
|
+
author (str, optional): Original author
|
|
637
|
+
developer (str, optional): Current developer
|
|
638
|
+
maintainer (str, optional): Current maintainer
|
|
639
|
+
editor (str, optional): Current editor
|
|
640
|
+
version (str): Version string
|
|
641
|
+
description (str): Description of what the agent does
|
|
642
|
+
tags (list[str], optional): Tags for categorizing the agent
|
|
643
|
+
methods (AgentMethods): Methods supported by this agent
|
|
644
|
+
parameters_setup (ParametersSetup, optional): Parameter configuration
|
|
645
|
+
server_agent_id (str, optional): ID assigned by server
|
|
646
|
+
server_agent_status (str, optional): Current status on server
|
|
647
|
+
server_agent_onboarding_status (str, optional): Onboarding status
|
|
648
|
+
server_encrypted_parameters (str, optional): Encrypted parameters from server
|
|
649
|
+
max_execution_time (int): Maximum execution time in seconds, defaults to 1 hour
|
|
650
|
+
|
|
651
|
+
Tested in tests/test_agent.py
|
|
652
|
+
"""
|
|
653
|
+
# Validate or generate agent ID
|
|
654
|
+
agent_id = id or shortuuid.uuid(name=name)
|
|
655
|
+
if id is not None and id != shortuuid.uuid(name=name):
|
|
656
|
+
raise ValueError("Agent ID does not match")
|
|
657
|
+
|
|
658
|
+
# Initialize using Pydantic's mechanism
|
|
659
|
+
super().__init__(
|
|
660
|
+
name=name,
|
|
661
|
+
id=agent_id,
|
|
662
|
+
author=author,
|
|
663
|
+
developer=developer,
|
|
664
|
+
maintainer=maintainer,
|
|
665
|
+
editor=editor,
|
|
666
|
+
version=version,
|
|
667
|
+
description=description,
|
|
668
|
+
tags=tags,
|
|
669
|
+
methods=methods,
|
|
670
|
+
parameters_setup=parameters_setup,
|
|
671
|
+
server_agent_id=server_agent_id,
|
|
672
|
+
server_agent_status=server_agent_status,
|
|
673
|
+
server_agent_onboarding_status=server_agent_onboarding_status,
|
|
674
|
+
server_encrypted_parameters=server_encrypted_parameters,
|
|
675
|
+
max_execution_time=max_execution_time,
|
|
676
|
+
**kwargs,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def __str__(self) -> str:
|
|
680
|
+
return f"{self.name} ({self.id})"
|
|
681
|
+
|
|
682
|
+
@property
|
|
683
|
+
def slug(self) -> str:
|
|
684
|
+
return slugify(self.name)
|
|
685
|
+
|
|
686
|
+
@property
|
|
687
|
+
def path(self) -> str:
|
|
688
|
+
return f"/agents/{self.slug}"
|
|
689
|
+
|
|
690
|
+
@property
|
|
691
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
692
|
+
"""Returns registration info for the agent"""
|
|
693
|
+
return {
|
|
694
|
+
"name": self.name,
|
|
695
|
+
"id": f"{self.id}",
|
|
696
|
+
"author": self.author,
|
|
697
|
+
"developer": self.developer,
|
|
698
|
+
"maintainer": self.maintainer,
|
|
699
|
+
"editor": self.editor,
|
|
700
|
+
"version": self.version,
|
|
701
|
+
"description": self.description,
|
|
702
|
+
"api_path": self.path,
|
|
703
|
+
"slug": self.slug,
|
|
704
|
+
"tags": self.tags,
|
|
705
|
+
"methods": self.methods.registration_info if self.methods else {},
|
|
706
|
+
"parameters_setup": self.parameters_setup.registration_info
|
|
707
|
+
if self.parameters_setup
|
|
708
|
+
else None,
|
|
709
|
+
"server_agent_id": f"{self.server_agent_id}",
|
|
710
|
+
"server_agent_status": self.server_agent_status,
|
|
711
|
+
"server_agent_onboarding_status": self.server_agent_onboarding_status,
|
|
712
|
+
"server_encrypted_parameters": self.server_encrypted_parameters,
|
|
713
|
+
"max_execution_time": self.max_execution_time,
|
|
714
|
+
"instructions_path": self.instructions_path,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
def update_agent_from_server(self, server: "Server") -> Optional["Agent"]:
|
|
718
|
+
"""
|
|
719
|
+
Update agent attributes and parameters from server registration information.
|
|
720
|
+
Example of agent_registration data is available in mock_api_responses.py
|
|
721
|
+
|
|
722
|
+
Server is used to decrypt parameters if needed
|
|
723
|
+
Tested in tests/test_agent.py/test_agent_update_agent_from_server
|
|
724
|
+
"""
|
|
725
|
+
if server.supervisor_account:
|
|
726
|
+
if self.server_agent_id:
|
|
727
|
+
# Get agent by ID from SaaS Server
|
|
728
|
+
from_server = server.supervisor_account.get_agent_by(
|
|
729
|
+
agent_id=self.server_agent_id
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
else:
|
|
733
|
+
# Get agent by name from SaaS Server
|
|
734
|
+
from_server = server.supervisor_account.get_agent_by(
|
|
735
|
+
agent_slug=self.slug
|
|
736
|
+
)
|
|
737
|
+
else:
|
|
738
|
+
return None
|
|
739
|
+
if not isinstance(from_server, ApiSuccess):
|
|
740
|
+
log.error(f"[Agent update_agent_from_server] Failed : {from_server}")
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
agent_from_server = from_server.detail
|
|
744
|
+
server_agent_id = agent_from_server.get("id") if agent_from_server else None
|
|
745
|
+
|
|
746
|
+
# This should never happen, but just in case
|
|
747
|
+
if self.server_agent_id and self.server_agent_id != server_agent_id:
|
|
748
|
+
message = f"Agent ID mismatch: {self.server_agent_id} != {server_agent_id}"
|
|
749
|
+
raise ValueError(message)
|
|
750
|
+
|
|
751
|
+
# Update agent attributes
|
|
752
|
+
self.server_agent_id = server_agent_id
|
|
753
|
+
self.server_agent_status = (
|
|
754
|
+
agent_from_server.get("status") if agent_from_server else None
|
|
755
|
+
)
|
|
756
|
+
self.server_agent_onboarding_status = (
|
|
757
|
+
agent_from_server.get("onboarding_status") if agent_from_server else None
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# If agent is configured, get encrypted parameters
|
|
761
|
+
if self.server_agent_onboarding_status == "configured":
|
|
762
|
+
log.debug(
|
|
763
|
+
f"[Agent configured] getting encrypted parameters for {self.name}"
|
|
764
|
+
)
|
|
765
|
+
server_encrypted_parameters = (
|
|
766
|
+
agent_from_server.get("parameters_encrypted")
|
|
767
|
+
if agent_from_server
|
|
768
|
+
else None
|
|
769
|
+
)
|
|
770
|
+
self.update_parameters_from_server(server, server_encrypted_parameters)
|
|
771
|
+
else:
|
|
772
|
+
log.debug("[Agent not onboarded] skipping encrypted parameters")
|
|
773
|
+
|
|
774
|
+
return self
|
|
775
|
+
|
|
776
|
+
def update_parameters_from_server(
|
|
777
|
+
self, server: "Server", server_encrypted_parameters: str | None
|
|
778
|
+
) -> None:
|
|
779
|
+
if server_encrypted_parameters and self.parameters_setup:
|
|
780
|
+
self.server_encrypted_parameters = server_encrypted_parameters
|
|
781
|
+
decrypted = server.decrypt(server_encrypted_parameters)
|
|
782
|
+
self.parameters_setup.update_values_from_server(json.loads(decrypted))
|
|
783
|
+
else:
|
|
784
|
+
log.debug("[No encrypted parameters] for {self.name}")
|
|
785
|
+
|
|
786
|
+
def _execute(self, action: str, params: Dict[str, Any] = {}) -> JobResponse:
|
|
787
|
+
"""
|
|
788
|
+
Execute an agent method and return a JobResponse
|
|
789
|
+
"""
|
|
790
|
+
|
|
791
|
+
module_name, func_name = action.rsplit(".", 1)
|
|
792
|
+
module = __import__(module_name, fromlist=[func_name])
|
|
793
|
+
method = getattr(module, func_name)
|
|
794
|
+
log.debug(f"[Agent method] {method.__name__} with params {params}")
|
|
795
|
+
result = method(**params)
|
|
796
|
+
if not isinstance(result, JobResponse):
|
|
797
|
+
raise TypeError(
|
|
798
|
+
f"Method {func_name} must return a JobResponse object, got {type(result).__name__}"
|
|
799
|
+
)
|
|
800
|
+
return result
|
|
801
|
+
|
|
802
|
+
def job_start(
|
|
803
|
+
self,
|
|
804
|
+
job: Job,
|
|
805
|
+
job_fields: Dict[str, Any],
|
|
806
|
+
context: JobContext,
|
|
807
|
+
server: "Server",
|
|
808
|
+
method_name: str = "job_start",
|
|
809
|
+
) -> Job:
|
|
810
|
+
"""Execute the agent's start method in the background
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
job (Job): The job instance to execute
|
|
814
|
+
job_fields (dict): The job-specific parameters
|
|
815
|
+
context (SupervaizeContextModel): The context of the job
|
|
816
|
+
Returns:
|
|
817
|
+
Job: The updated job instance
|
|
818
|
+
"""
|
|
819
|
+
if not self.methods:
|
|
820
|
+
raise ValueError("Agent methods not defined")
|
|
821
|
+
log.debug(
|
|
822
|
+
f"[Agent job_start] Run <{self.methods.job_start.method}> - Job <{job.id}>"
|
|
823
|
+
)
|
|
824
|
+
event = JobStartConfirmationEvent(
|
|
825
|
+
job=job,
|
|
826
|
+
account=server.supervisor_account,
|
|
827
|
+
)
|
|
828
|
+
if server.supervisor_account is not None:
|
|
829
|
+
server.supervisor_account.send_event(sender=job, event=event)
|
|
830
|
+
else:
|
|
831
|
+
log.warning(
|
|
832
|
+
f"[Agent job_start] No supervisor account defined for server, skipping event send for job {job.id}"
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
# Mark job as in progress when execution starts
|
|
836
|
+
job.add_response(
|
|
837
|
+
JobResponse(
|
|
838
|
+
job_id=job.id,
|
|
839
|
+
status=EntityStatus.IN_PROGRESS,
|
|
840
|
+
message="Starting job execution",
|
|
841
|
+
payload=None,
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# Execute the method
|
|
846
|
+
if method_name == "job_start":
|
|
847
|
+
action = self.methods.job_start
|
|
848
|
+
else:
|
|
849
|
+
if not self.methods.custom:
|
|
850
|
+
raise ValueError(f"Custom method {method_name} not found")
|
|
851
|
+
action = self.methods.custom[method_name]
|
|
852
|
+
|
|
853
|
+
action_method = action.method
|
|
854
|
+
method_params = action.params or {}
|
|
855
|
+
params = (
|
|
856
|
+
method_params
|
|
857
|
+
| {"fields": job_fields}
|
|
858
|
+
| {"context": context}
|
|
859
|
+
| {"agent_parameters": job.agent_parameters}
|
|
860
|
+
)
|
|
861
|
+
log.debug(
|
|
862
|
+
f"[Agent job_start] action_method : {action_method} - params : {params}"
|
|
863
|
+
)
|
|
864
|
+
try:
|
|
865
|
+
if self.methods.job_start.is_async:
|
|
866
|
+
# TODO: Implement async job execution & test
|
|
867
|
+
raise NotImplementedError(
|
|
868
|
+
"[Agent job_start] Async job execution is not implemented"
|
|
869
|
+
)
|
|
870
|
+
started = self._execute(action_method, params)
|
|
871
|
+
job_response = JobResponse(
|
|
872
|
+
job_id=job.id,
|
|
873
|
+
status=EntityStatus.IN_PROGRESS,
|
|
874
|
+
message="Job started ",
|
|
875
|
+
payload={"intermediary_deliverable": started},
|
|
876
|
+
)
|
|
877
|
+
else:
|
|
878
|
+
job_response = self._execute(action_method, params)
|
|
879
|
+
if (
|
|
880
|
+
job_response.status == EntityStatus.COMPLETED
|
|
881
|
+
or job_response.status == EntityStatus.FAILED
|
|
882
|
+
or job_response.status == EntityStatus.CANCELLED
|
|
883
|
+
or job_response.status == EntityStatus.CANCELLING
|
|
884
|
+
):
|
|
885
|
+
job.add_response(job_response)
|
|
886
|
+
service_job_finished(job, server=server)
|
|
887
|
+
elif job_response.status == EntityStatus.AWAITING:
|
|
888
|
+
log.debug(
|
|
889
|
+
f"[Agent job_start] Job is awaiting input, adding response : Job {job.id} status {job_response} §SAS02"
|
|
890
|
+
)
|
|
891
|
+
job.add_response(job_response)
|
|
892
|
+
else:
|
|
893
|
+
log.warning(
|
|
894
|
+
f"[Agent job_start] Job is not a terminal status, skipping job finish : Job {job.id} status {job_response} §SAS01"
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
except Exception as e:
|
|
898
|
+
# Handle any execution errors
|
|
899
|
+
error_msg = f"Job execution failed: {str(e)}"
|
|
900
|
+
log.error(f"[Agent job_start] Job failed : {job.id} - {error_msg}")
|
|
901
|
+
job_response = JobResponse(
|
|
902
|
+
job_id=job.id,
|
|
903
|
+
status=EntityStatus.FAILED,
|
|
904
|
+
message=error_msg,
|
|
905
|
+
payload=None,
|
|
906
|
+
error=e,
|
|
907
|
+
)
|
|
908
|
+
job.add_response(job_response)
|
|
909
|
+
raise
|
|
910
|
+
return job
|
|
911
|
+
|
|
912
|
+
def job_stop(self, params: Dict[str, Any] = {}) -> Any:
|
|
913
|
+
if not self.methods or not self.methods.job_stop:
|
|
914
|
+
raise ValueError("Agent methods not defined")
|
|
915
|
+
method = self.methods.job_stop.method
|
|
916
|
+
return self._execute(method, params)
|
|
917
|
+
|
|
918
|
+
def job_status(self, params: Dict[str, Any] = {}) -> Any:
|
|
919
|
+
if not self.methods or not self.methods.job_status:
|
|
920
|
+
raise ValueError("Agent methods not defined")
|
|
921
|
+
method = self.methods.job_status.method
|
|
922
|
+
return self._execute(method, params)
|
|
923
|
+
|
|
924
|
+
def chat(self, context: str, message: str) -> Any:
|
|
925
|
+
if not self.methods or not self.methods.chat:
|
|
926
|
+
raise ValueError("Chat method not configured")
|
|
927
|
+
method = self.methods.chat.method
|
|
928
|
+
params = {"context": context, "message": message}
|
|
929
|
+
return self._execute(method, params)
|
|
930
|
+
|
|
931
|
+
@property
|
|
932
|
+
def custom_methods_names(self) -> list[str] | None:
|
|
933
|
+
if self.methods and self.methods.custom:
|
|
934
|
+
return list(self.methods.custom.keys())
|
|
935
|
+
return None
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
class AgentResponse(BaseModel):
|
|
939
|
+
"""Response model for agent endpoints - values provided by Agent.registration_info"""
|
|
940
|
+
|
|
941
|
+
name: str
|
|
942
|
+
id: str
|
|
943
|
+
author: Optional[str] = None
|
|
944
|
+
developer: Optional[str] = None
|
|
945
|
+
maintainer: Optional[str] = None
|
|
946
|
+
editor: Optional[str] = None
|
|
947
|
+
version: str
|
|
948
|
+
api_path: str
|
|
949
|
+
description: str
|
|
950
|
+
tags: Optional[list[str]] = None
|
|
951
|
+
methods: Optional[AgentMethods] = None
|
|
952
|
+
parameters_setup: Optional[List[Dict[str, Any]]] = None
|
|
953
|
+
server_agent_id: Optional[str] = None
|
|
954
|
+
server_agent_status: Optional[str] = None
|
|
955
|
+
server_agent_onboarding_status: Optional[str] = None
|
|
956
|
+
server_encrypted_parameters: Optional[str] = None
|