supervaizer 0.9.6__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 +88 -0
- supervaizer/__version__.py +10 -0
- supervaizer/account.py +304 -0
- supervaizer/account_service.py +87 -0
- supervaizer/admin/routes.py +1254 -0
- supervaizer/admin/templates/agent_detail.html +145 -0
- supervaizer/admin/templates/agents.html +175 -0
- supervaizer/admin/templates/agents_grid.html +80 -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/jobs_list.html +180 -0
- supervaizer/admin/templates/jobs_table.html +122 -0
- supervaizer/admin/templates/navigation.html +153 -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/agent.py +816 -0
- supervaizer/case.py +400 -0
- supervaizer/cli.py +135 -0
- supervaizer/common.py +283 -0
- supervaizer/event.py +181 -0
- supervaizer/examples/controller-template.py +195 -0
- supervaizer/instructions.py +145 -0
- supervaizer/job.py +379 -0
- supervaizer/job_service.py +155 -0
- supervaizer/lifecycle.py +417 -0
- supervaizer/parameter.py +173 -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/protocol/acp/__init__.py +21 -0
- supervaizer/protocol/acp/model.py +198 -0
- supervaizer/protocol/acp/routes.py +74 -0
- supervaizer/py.typed +1 -0
- supervaizer/routes.py +667 -0
- supervaizer/server.py +554 -0
- supervaizer/server_utils.py +54 -0
- supervaizer/storage.py +436 -0
- supervaizer/telemetry.py +81 -0
- supervaizer-0.9.6.dist-info/METADATA +245 -0
- supervaizer-0.9.6.dist-info/RECORD +50 -0
- supervaizer-0.9.6.dist-info/WHEEL +4 -0
- supervaizer-0.9.6.dist-info/entry_points.txt +2 -0
- supervaizer-0.9.6.dist-info/licenses/LICENSE.md +346 -0
supervaizer/agent.py
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
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
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from supervaizer.server import Server
|
|
34
|
+
|
|
35
|
+
insp = inspect
|
|
36
|
+
prnt = print
|
|
37
|
+
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FieldTypeEnum(str, Enum):
|
|
42
|
+
CHAR = "CharField"
|
|
43
|
+
INT = "IntegerField"
|
|
44
|
+
BOOL = "BooleanField"
|
|
45
|
+
CHOICE = "ChoiceField"
|
|
46
|
+
MULTICHOICE = "MultipleChoiceField"
|
|
47
|
+
DATE = "DateField"
|
|
48
|
+
DATETIME = "DateTimeField"
|
|
49
|
+
FLOAT = "FloatField"
|
|
50
|
+
EMAIL = "EmailField"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AgentMethodField(BaseModel):
|
|
54
|
+
"""
|
|
55
|
+
Represents a field specification for generating forms/UI in the Supervaize platform.
|
|
56
|
+
|
|
57
|
+
Fields are used to define user input parameters that will be collected through
|
|
58
|
+
the UI and passed as kwargs to the AgentMethod.method. They follow Django forms
|
|
59
|
+
field definitions for consistency.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
- [Django Widgets](https://docs.djangoproject.com/en/5.2/ref/forms/widgets/)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
** field_type - available field types ** [Django Field classes](https://docs.djangoproject.com/en/5.2/ref/forms/fields/#built-in-field-classes)
|
|
66
|
+
|
|
67
|
+
- `CharField` - Text input
|
|
68
|
+
- `IntegerField` - Number input
|
|
69
|
+
- `BooleanField` - Checkbox
|
|
70
|
+
- `ChoiceField` - Dropdown with options
|
|
71
|
+
- `MultipleChoiceField` - Multi-select
|
|
72
|
+
- `JSONField` - JSON data input
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name: str = Field(description="The name of the field - displayed in the UI")
|
|
77
|
+
type: Any = Field(
|
|
78
|
+
description="Python type of the field for pydantic validation - note , ChoiceField and MultipleChoiceField are a list[str]"
|
|
79
|
+
)
|
|
80
|
+
field_type: FieldTypeEnum = Field(
|
|
81
|
+
default=FieldTypeEnum.CHAR, description="Field type for persistence"
|
|
82
|
+
)
|
|
83
|
+
description: str | None = Field(
|
|
84
|
+
default=None, description="Description of the field - displayed in the UI"
|
|
85
|
+
)
|
|
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(
|
|
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
|
+
|
|
215
|
+
class AgentMethod(AgentMethodAbstract):
|
|
216
|
+
@property
|
|
217
|
+
def fields_definitions(self) -> list[Dict[str, Any]]:
|
|
218
|
+
"""
|
|
219
|
+
Returns a list of the fields with the type key as a string
|
|
220
|
+
Used for the API response.
|
|
221
|
+
"""
|
|
222
|
+
if self.fields:
|
|
223
|
+
result = []
|
|
224
|
+
for field in self.fields:
|
|
225
|
+
d = {k: v for k, v in field.__dict__.items() if k != "type"}
|
|
226
|
+
# type as string
|
|
227
|
+
type_val = field.type
|
|
228
|
+
if hasattr(type_val, "__name__"):
|
|
229
|
+
d["type"] = type_val.__name__
|
|
230
|
+
elif hasattr(type_val, "_name") and type_val._name:
|
|
231
|
+
d["type"] = type_val._name
|
|
232
|
+
else:
|
|
233
|
+
d["type"] = str(type_val)
|
|
234
|
+
result.append(d)
|
|
235
|
+
return result
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def fields_annotations(self) -> type[BaseModel]:
|
|
240
|
+
"""
|
|
241
|
+
Creates and returns a dynamic Pydantic model class based on the field definitions.
|
|
242
|
+
"""
|
|
243
|
+
if not self.fields:
|
|
244
|
+
return type("EmptyFieldsModel", (BaseModel,), {"to_dict": lambda self: {}})
|
|
245
|
+
|
|
246
|
+
field_annotations = {}
|
|
247
|
+
field_defaults: Dict[str, None] = {}
|
|
248
|
+
for field in self.fields:
|
|
249
|
+
field_name = field.name
|
|
250
|
+
field_type = field.type
|
|
251
|
+
is_required = field.required
|
|
252
|
+
field_annotations[field_name] = (
|
|
253
|
+
field_type if is_required else Optional[field_type]
|
|
254
|
+
)
|
|
255
|
+
if not is_required:
|
|
256
|
+
field_defaults[field_name] = None
|
|
257
|
+
|
|
258
|
+
def to_dict(self: BaseModel) -> Dict[str, Any]:
|
|
259
|
+
return {
|
|
260
|
+
field_name: getattr(self, field_name)
|
|
261
|
+
for field_name in self.__annotations__
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return type(
|
|
265
|
+
"DynamicFieldsModel",
|
|
266
|
+
(BaseModel,),
|
|
267
|
+
{
|
|
268
|
+
"__annotations__": field_annotations,
|
|
269
|
+
"to_dict": to_dict,
|
|
270
|
+
**field_defaults,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def job_model(self) -> type[AgentJobContextBase]:
|
|
276
|
+
"""
|
|
277
|
+
Creates and returns a dynamic Pydantic model class combining job context and job fields.
|
|
278
|
+
"""
|
|
279
|
+
fields_model = self.fields_annotations
|
|
280
|
+
|
|
281
|
+
return type(
|
|
282
|
+
"AgentJobAbstract",
|
|
283
|
+
(AgentJobContextBase,),
|
|
284
|
+
{
|
|
285
|
+
"__annotations__": {
|
|
286
|
+
"job_context": JobContext,
|
|
287
|
+
"job_fields": fields_model,
|
|
288
|
+
"encrypted_agent_parameters": str | None,
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
295
|
+
"""
|
|
296
|
+
Returns a JSON-serializable dictionary representation of the AgentMethod.
|
|
297
|
+
"""
|
|
298
|
+
return {
|
|
299
|
+
"name": self.name,
|
|
300
|
+
"method": str(self.method),
|
|
301
|
+
"params": self.params,
|
|
302
|
+
"fields": self.fields_definitions,
|
|
303
|
+
"description": self.description,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class AgentMethodParams(BaseModel):
|
|
308
|
+
"""
|
|
309
|
+
Method parameters for agent operations.
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
params: Dict[str, Any] = Field(
|
|
314
|
+
default_factory=dict,
|
|
315
|
+
description="A simple key-value dictionary of parameters what will be passed to the AgentMethod.method as kwargs",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class AgentCustomMethodParams(AgentMethodParams):
|
|
320
|
+
method_name: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class AgentMethodsAbstract(BaseModel):
|
|
324
|
+
job_start: AgentMethod
|
|
325
|
+
job_stop: AgentMethod
|
|
326
|
+
job_status: AgentMethod
|
|
327
|
+
chat: AgentMethod | None = None
|
|
328
|
+
custom: dict[str, AgentMethod] | None = None
|
|
329
|
+
|
|
330
|
+
@field_validator("custom")
|
|
331
|
+
@classmethod
|
|
332
|
+
def validate_custom_method_keys(
|
|
333
|
+
cls, value: dict[str, AgentMethod]
|
|
334
|
+
) -> dict[str, AgentMethod]:
|
|
335
|
+
"""Validate that custom method keys are valid slug-like values suitable for endpoints."""
|
|
336
|
+
if value:
|
|
337
|
+
for key in value.keys():
|
|
338
|
+
# Check if key is a valid slug format
|
|
339
|
+
if not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", key):
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"Custom method key '{key}' is not a valid slug. "
|
|
342
|
+
f"Keys must contain only lowercase letters, numbers, and hyphens, "
|
|
343
|
+
f"and cannot start or end with a hyphen. "
|
|
344
|
+
f"Examples: 'backup', 'health-check', 'sync-data'"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Additional checks for endpoint safety
|
|
348
|
+
if len(key) > 50:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"Custom method key '{key}' is too long (max 50 characters)"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return value
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class AgentMethods(AgentMethodsAbstract):
|
|
357
|
+
@property
|
|
358
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
359
|
+
return {
|
|
360
|
+
"job_start": self.job_start.registration_info,
|
|
361
|
+
"job_stop": self.job_stop.registration_info,
|
|
362
|
+
"job_status": self.job_status.registration_info,
|
|
363
|
+
"chat": self.chat.registration_info if self.chat else None,
|
|
364
|
+
"custom": {
|
|
365
|
+
name: method.registration_info
|
|
366
|
+
for name, method in (self.custom or {}).items()
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class AgentAbstract(SvBaseModel):
|
|
372
|
+
"""
|
|
373
|
+
Agent model for the Supervaize Control API.
|
|
374
|
+
|
|
375
|
+
This represents an agent that can be registered with the Supervaize Control API.
|
|
376
|
+
It contains metadata about the agent like name, version, description etc. as well as
|
|
377
|
+
the methods it supports and any parameter configurations.
|
|
378
|
+
|
|
379
|
+
The agent ID is automatically generated from the name and must match.
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
```python
|
|
383
|
+
Agent(
|
|
384
|
+
name="Email AI Agent",
|
|
385
|
+
author="@parthshr370", # Author of the agent
|
|
386
|
+
developer="@alain_sv", # Developer of the controller
|
|
387
|
+
maintainer="@aintainer",
|
|
388
|
+
editor="AI Editor",
|
|
389
|
+
version="1.0.0",
|
|
390
|
+
description="AI-powered email processing agent that can fetch, analyze, generate responses, and send/draft emails",
|
|
391
|
+
tags=["email", "ai", "automation", "communication"],
|
|
392
|
+
methods=AgentMethods(
|
|
393
|
+
job_start=process_email_method, # Job start method
|
|
394
|
+
job_stop=job_stop, # Job stop method
|
|
395
|
+
job_status=job_status, # Job status method
|
|
396
|
+
chat=None,
|
|
397
|
+
custom=None,
|
|
398
|
+
),
|
|
399
|
+
parameters_setup=ParametersSetup.from_list([
|
|
400
|
+
Parameter(
|
|
401
|
+
name="IMAP_USERNAME",
|
|
402
|
+
description="IMAP username for email access",
|
|
403
|
+
is_environment=True,
|
|
404
|
+
is_secret=False,
|
|
405
|
+
),
|
|
406
|
+
Parameter(
|
|
407
|
+
name="IMAP_PASSWORD",
|
|
408
|
+
description="IMAP password for email access",
|
|
409
|
+
is_environment=True,
|
|
410
|
+
is_secret=True,
|
|
411
|
+
),
|
|
412
|
+
]),
|
|
413
|
+
)
|
|
414
|
+
```
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
supervaizer_VERSION: ClassVar[str] = VERSION
|
|
418
|
+
name: str = Field(description="Display name of the agent")
|
|
419
|
+
id: str = Field(description="Unique ID generated from name")
|
|
420
|
+
author: Optional[str] = Field(default=None, description="Author of the agent")
|
|
421
|
+
developer: Optional[str] = Field(
|
|
422
|
+
default=None, description="Developer of the controller integration"
|
|
423
|
+
)
|
|
424
|
+
maintainer: Optional[str] = Field(
|
|
425
|
+
default=None, description="Maintainer of the integration"
|
|
426
|
+
)
|
|
427
|
+
editor: Optional[str] = Field(
|
|
428
|
+
default=None, description="Editor (usually a company)"
|
|
429
|
+
)
|
|
430
|
+
version: str = Field(default="", description="Version string")
|
|
431
|
+
description: str = Field(
|
|
432
|
+
default="", description="Description of what the agent does"
|
|
433
|
+
)
|
|
434
|
+
tags: list[str] | None = Field(
|
|
435
|
+
default=None, description="Tags for categorizing the agent"
|
|
436
|
+
)
|
|
437
|
+
methods: AgentMethods | None = Field(
|
|
438
|
+
default=None, description="Methods supported by this agent"
|
|
439
|
+
)
|
|
440
|
+
parameters_setup: ParametersSetup | None = Field(
|
|
441
|
+
default=None, description="Parameter configuration"
|
|
442
|
+
)
|
|
443
|
+
server_agent_id: str | None = Field(
|
|
444
|
+
default=None, description="ID assigned by server - Do not set this manually"
|
|
445
|
+
)
|
|
446
|
+
server_agent_status: str | None = Field(
|
|
447
|
+
default=None, description="Current status on server - Do not set this manually"
|
|
448
|
+
)
|
|
449
|
+
server_agent_onboarding_status: str | None = Field(
|
|
450
|
+
default=None, description="Onboarding status - Do not set this manually"
|
|
451
|
+
)
|
|
452
|
+
server_encrypted_parameters: str | None = Field(
|
|
453
|
+
default=None,
|
|
454
|
+
description="Encrypted parameters from server - Do not set this manually",
|
|
455
|
+
)
|
|
456
|
+
max_execution_time: int = Field(
|
|
457
|
+
default=60 * 60,
|
|
458
|
+
description="Maximum execution time in seconds, defaults to 1 hour",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
model_config = {
|
|
462
|
+
"reference_group": "Core",
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class Agent(AgentAbstract):
|
|
467
|
+
def __init__(
|
|
468
|
+
self,
|
|
469
|
+
name: str,
|
|
470
|
+
id: str | None = None,
|
|
471
|
+
author: Optional[str] = None,
|
|
472
|
+
developer: Optional[str] = None,
|
|
473
|
+
maintainer: Optional[str] = None,
|
|
474
|
+
editor: Optional[str] = None,
|
|
475
|
+
version: str = "",
|
|
476
|
+
description: str = "",
|
|
477
|
+
tags: list[str] | None = None,
|
|
478
|
+
methods: AgentMethods | None = None,
|
|
479
|
+
parameters_setup: ParametersSetup | None = None,
|
|
480
|
+
server_agent_id: str | None = None,
|
|
481
|
+
server_agent_status: str | None = None,
|
|
482
|
+
server_agent_onboarding_status: str | None = None,
|
|
483
|
+
server_encrypted_parameters: str | None = None,
|
|
484
|
+
max_execution_time: int = 60 * 60, # 1 hour (in seconds)
|
|
485
|
+
**kwargs: Any,
|
|
486
|
+
) -> None:
|
|
487
|
+
"""
|
|
488
|
+
This represents an agent that can be registered with the Supervaize Control API.
|
|
489
|
+
It contains metadata about the agent like name, version, description etc. as well as
|
|
490
|
+
the methods it supports and any parameter configurations.
|
|
491
|
+
|
|
492
|
+
The agent ID is automatically generated from the name and must match.
|
|
493
|
+
|
|
494
|
+
Attributes:
|
|
495
|
+
name (str): Display name of the agent
|
|
496
|
+
id (str): Unique ID generated from name
|
|
497
|
+
author (str, optional): Original author
|
|
498
|
+
developer (str, optional): Current developer
|
|
499
|
+
maintainer (str, optional): Current maintainer
|
|
500
|
+
editor (str, optional): Current editor
|
|
501
|
+
version (str): Version string
|
|
502
|
+
description (str): Description of what the agent does
|
|
503
|
+
tags (list[str], optional): Tags for categorizing the agent
|
|
504
|
+
methods (AgentMethods): Methods supported by this agent
|
|
505
|
+
parameters_setup (ParametersSetup, optional): Parameter configuration
|
|
506
|
+
server_agent_id (str, optional): ID assigned by server
|
|
507
|
+
server_agent_status (str, optional): Current status on server
|
|
508
|
+
server_agent_onboarding_status (str, optional): Onboarding status
|
|
509
|
+
server_encrypted_parameters (str, optional): Encrypted parameters from server
|
|
510
|
+
max_execution_time (int): Maximum execution time in seconds, defaults to 1 hour
|
|
511
|
+
|
|
512
|
+
Tested in tests/test_agent.py
|
|
513
|
+
"""
|
|
514
|
+
# Validate or generate agent ID
|
|
515
|
+
agent_id = id or shortuuid.uuid(name=name)
|
|
516
|
+
if id is not None and id != shortuuid.uuid(name=name):
|
|
517
|
+
raise ValueError("Agent ID does not match")
|
|
518
|
+
|
|
519
|
+
# Initialize using Pydantic's mechanism
|
|
520
|
+
super().__init__(
|
|
521
|
+
name=name,
|
|
522
|
+
id=agent_id,
|
|
523
|
+
author=author,
|
|
524
|
+
developer=developer,
|
|
525
|
+
maintainer=maintainer,
|
|
526
|
+
editor=editor,
|
|
527
|
+
version=version,
|
|
528
|
+
description=description,
|
|
529
|
+
tags=tags,
|
|
530
|
+
methods=methods,
|
|
531
|
+
parameters_setup=parameters_setup,
|
|
532
|
+
server_agent_id=server_agent_id,
|
|
533
|
+
server_agent_status=server_agent_status,
|
|
534
|
+
server_agent_onboarding_status=server_agent_onboarding_status,
|
|
535
|
+
server_encrypted_parameters=server_encrypted_parameters,
|
|
536
|
+
max_execution_time=max_execution_time,
|
|
537
|
+
**kwargs,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def __str__(self) -> str:
|
|
541
|
+
return f"{self.name} ({self.id})"
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def slug(self) -> str:
|
|
545
|
+
return slugify(self.name)
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def path(self) -> str:
|
|
549
|
+
return f"/agents/{self.slug}"
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
553
|
+
"""Returns registration info for the agent"""
|
|
554
|
+
return {
|
|
555
|
+
"name": self.name,
|
|
556
|
+
"id": self.id,
|
|
557
|
+
"author": self.author,
|
|
558
|
+
"developer": self.developer,
|
|
559
|
+
"maintainer": self.maintainer,
|
|
560
|
+
"editor": self.editor,
|
|
561
|
+
"version": self.version,
|
|
562
|
+
"description": self.description,
|
|
563
|
+
"api_path": self.path,
|
|
564
|
+
"slug": self.slug,
|
|
565
|
+
"tags": self.tags,
|
|
566
|
+
"methods": self.methods.registration_info if self.methods else {},
|
|
567
|
+
"parameters_setup": self.parameters_setup.registration_info
|
|
568
|
+
if self.parameters_setup
|
|
569
|
+
else None,
|
|
570
|
+
"server_agent_id": self.server_agent_id,
|
|
571
|
+
"server_agent_status": self.server_agent_status,
|
|
572
|
+
"server_agent_onboarding_status": self.server_agent_onboarding_status,
|
|
573
|
+
"server_encrypted_parameters": self.server_encrypted_parameters,
|
|
574
|
+
"max_execution_time": self.max_execution_time,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
def update_agent_from_server(self, server: "Server") -> Optional["Agent"]:
|
|
578
|
+
"""
|
|
579
|
+
Update agent attributes and parameters from server registration information.
|
|
580
|
+
Example of agent_registration data is available in mock_api_responses.py
|
|
581
|
+
|
|
582
|
+
Server is used to decrypt parameters if needed
|
|
583
|
+
Tested in tests/test_agent.py/test_agent_update_agent_from_server
|
|
584
|
+
"""
|
|
585
|
+
if server.supervisor_account:
|
|
586
|
+
if self.server_agent_id:
|
|
587
|
+
# Get agent by ID from SaaS Server
|
|
588
|
+
from_server = server.supervisor_account.get_agent_by(
|
|
589
|
+
agent_id=self.server_agent_id
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
else:
|
|
593
|
+
# Get agent by name from SaaS Server
|
|
594
|
+
from_server = server.supervisor_account.get_agent_by(
|
|
595
|
+
agent_slug=self.slug
|
|
596
|
+
)
|
|
597
|
+
else:
|
|
598
|
+
return None
|
|
599
|
+
if not isinstance(from_server, ApiSuccess):
|
|
600
|
+
log.error(f"[Agent update_agent_from_server] Failed : {from_server}")
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
agent_from_server = from_server.detail
|
|
604
|
+
server_agent_id = agent_from_server.get("id") if agent_from_server else None
|
|
605
|
+
|
|
606
|
+
# This should never happen, but just in case
|
|
607
|
+
if self.server_agent_id and self.server_agent_id != server_agent_id:
|
|
608
|
+
message = f"Agent ID mismatch: {self.server_agent_id} != {server_agent_id}"
|
|
609
|
+
raise ValueError(message)
|
|
610
|
+
|
|
611
|
+
# Update agent attributes
|
|
612
|
+
self.server_agent_id = server_agent_id
|
|
613
|
+
self.server_agent_status = (
|
|
614
|
+
agent_from_server.get("status") if agent_from_server else None
|
|
615
|
+
)
|
|
616
|
+
self.server_agent_onboarding_status = (
|
|
617
|
+
agent_from_server.get("onboarding_status") if agent_from_server else None
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# If agent is configured, get encrypted parameters
|
|
621
|
+
if self.server_agent_onboarding_status == "configured":
|
|
622
|
+
log.debug(
|
|
623
|
+
f"[Agent configured] getting encrypted parameters for {self.name}"
|
|
624
|
+
)
|
|
625
|
+
server_encrypted_parameters = (
|
|
626
|
+
agent_from_server.get("parameters_encrypted")
|
|
627
|
+
if agent_from_server
|
|
628
|
+
else None
|
|
629
|
+
)
|
|
630
|
+
self.update_parameters_from_server(server, server_encrypted_parameters)
|
|
631
|
+
else:
|
|
632
|
+
log.debug("[Agent not onboarded] skipping encrypted parameters")
|
|
633
|
+
|
|
634
|
+
return self
|
|
635
|
+
|
|
636
|
+
def update_parameters_from_server(
|
|
637
|
+
self, server: "Server", server_encrypted_parameters: str | None
|
|
638
|
+
) -> None:
|
|
639
|
+
if server_encrypted_parameters and self.parameters_setup:
|
|
640
|
+
self.server_encrypted_parameters = server_encrypted_parameters
|
|
641
|
+
decrypted = server.decrypt(server_encrypted_parameters)
|
|
642
|
+
self.parameters_setup.update_values_from_server(json.loads(decrypted))
|
|
643
|
+
else:
|
|
644
|
+
log.debug("[No encrypted parameters] for {self.name}")
|
|
645
|
+
|
|
646
|
+
def _execute(self, action: str, params: Dict[str, Any] = {}) -> JobResponse:
|
|
647
|
+
"""
|
|
648
|
+
Execute an agent method and return a JobResponse
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
module_name, func_name = action.rsplit(".", 1)
|
|
652
|
+
module = __import__(module_name, fromlist=[func_name])
|
|
653
|
+
method = getattr(module, func_name)
|
|
654
|
+
log.debug(f"[Agent method] {method.__name__} with params {params}")
|
|
655
|
+
result = method(**params)
|
|
656
|
+
if not isinstance(result, JobResponse):
|
|
657
|
+
raise TypeError(
|
|
658
|
+
f"Method {func_name} must return a JobResponse object, got {type(result).__name__}"
|
|
659
|
+
)
|
|
660
|
+
return result
|
|
661
|
+
|
|
662
|
+
def job_start(
|
|
663
|
+
self,
|
|
664
|
+
job: Job,
|
|
665
|
+
job_fields: Dict[str, Any],
|
|
666
|
+
context: JobContext,
|
|
667
|
+
server: "Server",
|
|
668
|
+
method_name: str = "job_start",
|
|
669
|
+
) -> Job:
|
|
670
|
+
"""Execute the agent's start method in the background
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
job (Job): The job instance to execute
|
|
674
|
+
job_fields (dict): The job-specific parameters
|
|
675
|
+
context (SupervaizeContextModel): The context of the job
|
|
676
|
+
Returns:
|
|
677
|
+
Job: The updated job instance
|
|
678
|
+
"""
|
|
679
|
+
if not self.methods:
|
|
680
|
+
raise ValueError("Agent methods not defined")
|
|
681
|
+
log.debug(
|
|
682
|
+
f"[Agent job_start] Run <{self.methods.job_start.method}> - Job <{job.id}>"
|
|
683
|
+
)
|
|
684
|
+
event = JobStartConfirmationEvent(
|
|
685
|
+
job=job,
|
|
686
|
+
account=server.supervisor_account,
|
|
687
|
+
)
|
|
688
|
+
if server.supervisor_account is not None:
|
|
689
|
+
server.supervisor_account.send_event(sender=job, event=event)
|
|
690
|
+
else:
|
|
691
|
+
log.warning(
|
|
692
|
+
f"[Agent job_start] No supervisor account defined for server, skipping event send for job {job.id}"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Mark job as in progress when execution starts
|
|
696
|
+
job.add_response(
|
|
697
|
+
JobResponse(
|
|
698
|
+
job_id=job.id,
|
|
699
|
+
status=EntityStatus.IN_PROGRESS,
|
|
700
|
+
message="Starting job execution",
|
|
701
|
+
payload=None,
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# Execute the method
|
|
706
|
+
if method_name == "job_start":
|
|
707
|
+
action = self.methods.job_start
|
|
708
|
+
else:
|
|
709
|
+
if not self.methods.custom:
|
|
710
|
+
raise ValueError(f"Custom method {method_name} not found")
|
|
711
|
+
action = self.methods.custom[method_name]
|
|
712
|
+
|
|
713
|
+
action_method = action.method
|
|
714
|
+
method_params = action.params or {}
|
|
715
|
+
params = (
|
|
716
|
+
method_params
|
|
717
|
+
| {"fields": job_fields}
|
|
718
|
+
| {"context": context}
|
|
719
|
+
| {"agent_parameters": job.agent_parameters}
|
|
720
|
+
)
|
|
721
|
+
log.debug(
|
|
722
|
+
f"[Agent job_start] action_method : {action_method} - params : {params}"
|
|
723
|
+
)
|
|
724
|
+
try:
|
|
725
|
+
if self.methods.job_start.is_async:
|
|
726
|
+
# TODO: Implement async job execution & test
|
|
727
|
+
raise NotImplementedError(
|
|
728
|
+
"[Agent job_start] Async job execution is not implemented"
|
|
729
|
+
)
|
|
730
|
+
started = self._execute(action_method, params)
|
|
731
|
+
job_response = JobResponse(
|
|
732
|
+
job_id=job.id,
|
|
733
|
+
status=EntityStatus.IN_PROGRESS,
|
|
734
|
+
message="Job started ",
|
|
735
|
+
payload={"intermediary_deliverable": started},
|
|
736
|
+
)
|
|
737
|
+
else:
|
|
738
|
+
job_response = self._execute(action_method, params)
|
|
739
|
+
if (
|
|
740
|
+
job_response.status == EntityStatus.COMPLETED
|
|
741
|
+
or job_response.status == EntityStatus.FAILED
|
|
742
|
+
or job_response.status == EntityStatus.CANCELLED
|
|
743
|
+
or job_response.status == EntityStatus.CANCELLING
|
|
744
|
+
):
|
|
745
|
+
job.add_response(job_response)
|
|
746
|
+
service_job_finished(job, server=server)
|
|
747
|
+
elif job_response.status == EntityStatus.AWAITING:
|
|
748
|
+
log.debug(
|
|
749
|
+
f"[Agent job_start] Job is awaiting input, adding response : Job {job.id} status {job_response} §SAS02"
|
|
750
|
+
)
|
|
751
|
+
job.add_response(job_response)
|
|
752
|
+
else:
|
|
753
|
+
log.warning(
|
|
754
|
+
f"[Agent job_start] Job is not a terminal status, skipping job finish : Job {job.id} status {job_response} §SAS01"
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
except Exception as e:
|
|
758
|
+
# Handle any execution errors
|
|
759
|
+
error_msg = f"Job execution failed: {str(e)}"
|
|
760
|
+
log.error(f"[Agent job_start] Job failed : {job.id} - {error_msg}")
|
|
761
|
+
job_response = JobResponse(
|
|
762
|
+
job_id=job.id,
|
|
763
|
+
status=EntityStatus.FAILED,
|
|
764
|
+
message=error_msg,
|
|
765
|
+
payload=None,
|
|
766
|
+
error=e,
|
|
767
|
+
)
|
|
768
|
+
job.add_response(job_response)
|
|
769
|
+
raise
|
|
770
|
+
return job
|
|
771
|
+
|
|
772
|
+
def job_stop(self, params: Dict[str, Any] = {}) -> Any:
|
|
773
|
+
if not self.methods:
|
|
774
|
+
raise ValueError("Agent methods not defined")
|
|
775
|
+
method = self.methods.job_stop.method
|
|
776
|
+
return self._execute(method, params)
|
|
777
|
+
|
|
778
|
+
def job_status(self, params: Dict[str, Any] = {}) -> Any:
|
|
779
|
+
if not self.methods:
|
|
780
|
+
raise ValueError("Agent methods not defined")
|
|
781
|
+
method = self.methods.job_status.method
|
|
782
|
+
return self._execute(method, params)
|
|
783
|
+
|
|
784
|
+
def chat(self, context: str, message: str) -> Any:
|
|
785
|
+
if not self.methods or not self.methods.chat:
|
|
786
|
+
raise ValueError("Chat method not configured")
|
|
787
|
+
method = self.methods.chat.method
|
|
788
|
+
params = {"context": context, "message": message}
|
|
789
|
+
return self._execute(method, params)
|
|
790
|
+
|
|
791
|
+
@property
|
|
792
|
+
def custom_methods_names(self) -> list[str] | None:
|
|
793
|
+
if self.methods and self.methods.custom:
|
|
794
|
+
return list(self.methods.custom.keys())
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
class AgentResponse(BaseModel):
|
|
799
|
+
"""Response model for agent endpoints - values provided by Agent.registration_info"""
|
|
800
|
+
|
|
801
|
+
name: str
|
|
802
|
+
id: str
|
|
803
|
+
author: Optional[str] = None
|
|
804
|
+
developer: Optional[str] = None
|
|
805
|
+
maintainer: Optional[str] = None
|
|
806
|
+
editor: Optional[str] = None
|
|
807
|
+
version: str
|
|
808
|
+
api_path: str
|
|
809
|
+
description: str
|
|
810
|
+
tags: Optional[list[str]] = None
|
|
811
|
+
methods: Optional[AgentMethods] = None
|
|
812
|
+
parameters_setup: Optional[List[Dict[str, Any]]] = None
|
|
813
|
+
server_agent_id: Optional[str] = None
|
|
814
|
+
server_agent_status: Optional[str] = None
|
|
815
|
+
server_agent_onboarding_status: Optional[str] = None
|
|
816
|
+
server_encrypted_parameters: Optional[str] = None
|