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/common.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
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 base64
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import traceback
|
|
12
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
13
|
+
|
|
14
|
+
import demjson3
|
|
15
|
+
from cryptography.hazmat.primitives import hashes
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
17
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
18
|
+
from loguru import logger
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
log = logger.bind(module="supervaize")
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SvBaseModel(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Base model for all Supervaize models.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Convert the model to a dictionary.
|
|
35
|
+
|
|
36
|
+
Note: Using mode="json" to handle datetime serialization.
|
|
37
|
+
Tested in tests/test_common.test_sv_base_model_json_conversion
|
|
38
|
+
"""
|
|
39
|
+
return self.model_dump(mode="json")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def to_json(self) -> str:
|
|
43
|
+
return self.model_dump_json()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ApiResult:
|
|
47
|
+
def __init__(self, message: str, detail: Optional[Dict[str, Any]], code: str):
|
|
48
|
+
self.message = message
|
|
49
|
+
self.code = str(code)
|
|
50
|
+
self.detail = detail
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return f"{self.json_return}"
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return f"{self.__class__.__name__} ({self.message})"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def dict(self) -> Dict[str, Any]:
|
|
60
|
+
return {key: value for key, value in self.__dict__.items()}
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def json_return(self) -> str:
|
|
64
|
+
return json.dumps(self.dict)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ApiSuccess(ApiResult):
|
|
68
|
+
"""
|
|
69
|
+
ApiSuccess is a class that extends ApiResult.
|
|
70
|
+
It is used to return a success response from the API.
|
|
71
|
+
|
|
72
|
+
If the detail is a string, it is decoded as a JSON object: Expects a JSON object with a
|
|
73
|
+
key "object" and a value of the JSON object to return.
|
|
74
|
+
If the detail is a dictionary, it is used as is.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Tested in tests/test_common.py
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self, message: str, detail: Optional[Dict[str, Any] | str], code: int = 200
|
|
82
|
+
):
|
|
83
|
+
log_message = "✅ "
|
|
84
|
+
if isinstance(detail, str):
|
|
85
|
+
result = demjson3.decode(detail, return_errors=True)
|
|
86
|
+
detail = {"object": result.object}
|
|
87
|
+
id = result.object.get("id") or None
|
|
88
|
+
if id is not None:
|
|
89
|
+
log_message += f"{message} : {id}"
|
|
90
|
+
else:
|
|
91
|
+
log_message += f"{message}"
|
|
92
|
+
else:
|
|
93
|
+
id = None
|
|
94
|
+
detail = detail
|
|
95
|
+
log_message += f"{message}"
|
|
96
|
+
|
|
97
|
+
super().__init__(
|
|
98
|
+
message=message,
|
|
99
|
+
detail=detail,
|
|
100
|
+
code=str(code),
|
|
101
|
+
)
|
|
102
|
+
self.id: Optional[str] = id
|
|
103
|
+
self.log_message = log_message
|
|
104
|
+
log.debug(f"[API Success] {self.log_message}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ApiError(ApiResult):
|
|
108
|
+
"""
|
|
109
|
+
ApiError is a class that extends ApiResult.
|
|
110
|
+
It can be used to return an error response from the API.
|
|
111
|
+
Note : not really useful for the moment, as API errors raise exception.
|
|
112
|
+
|
|
113
|
+
Tested in tests/test_common.py
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
message: str,
|
|
119
|
+
code: str = "",
|
|
120
|
+
detail: Optional[Dict[str, Any]] = None,
|
|
121
|
+
exception: Optional[Exception] = None,
|
|
122
|
+
url: str = "",
|
|
123
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
124
|
+
):
|
|
125
|
+
super().__init__(message, detail, code)
|
|
126
|
+
self.exception = exception
|
|
127
|
+
self.url = url
|
|
128
|
+
self.payload = payload
|
|
129
|
+
self.log_message = f"❌ {message} : {self.exception}"
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def dict(self) -> Dict[str, Any]:
|
|
133
|
+
if self.exception:
|
|
134
|
+
exception_dict: Dict[str, Any] = {
|
|
135
|
+
"type": type(self.exception).__name__,
|
|
136
|
+
"message": str(self.exception),
|
|
137
|
+
"traceback": traceback.format_exc(),
|
|
138
|
+
"attributes": {},
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
response := hasattr(self.exception, "response")
|
|
142
|
+
and self.exception.response
|
|
143
|
+
):
|
|
144
|
+
self.code = str(response.status_code) or ""
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
response_text = self.exception.response.text
|
|
148
|
+
exception_dict["response"] = json.loads(response_text)
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
pass
|
|
151
|
+
for attr in dir(self.exception):
|
|
152
|
+
try:
|
|
153
|
+
if (
|
|
154
|
+
not attr.startswith("__")
|
|
155
|
+
and not callable(attribute := getattr(self.exception, attr))
|
|
156
|
+
and getattr(self.exception, attr)
|
|
157
|
+
):
|
|
158
|
+
try:
|
|
159
|
+
exception_dict["attributes"][attr] = json.loads(
|
|
160
|
+
str(attribute)
|
|
161
|
+
)
|
|
162
|
+
except json.JSONDecodeError:
|
|
163
|
+
pass
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
result: Dict[str, Any] = {
|
|
168
|
+
"message": self.message,
|
|
169
|
+
"code": self.code,
|
|
170
|
+
"url": self.url,
|
|
171
|
+
"payload": self.payload,
|
|
172
|
+
"detail": self.detail,
|
|
173
|
+
}
|
|
174
|
+
if self.exception:
|
|
175
|
+
result["exception"] = exception_dict
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def singleton(cls: type[T]) -> Callable[..., T]:
|
|
180
|
+
"""Decorator to create a singleton class
|
|
181
|
+
Tested in tests/test_common.py
|
|
182
|
+
"""
|
|
183
|
+
instances: Dict[type[T], T] = {}
|
|
184
|
+
|
|
185
|
+
def get_instance(*args: Any, **kwargs: Any) -> T:
|
|
186
|
+
if cls not in instances:
|
|
187
|
+
instances[cls] = cls(*args, **kwargs)
|
|
188
|
+
return instances[cls]
|
|
189
|
+
|
|
190
|
+
return get_instance
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def encrypt_value(value_to_encrypt: str, public_key: rsa.RSAPublicKey) -> str:
|
|
194
|
+
"""Encrypt using hybrid RSA+AES encryption to handle messages of any size.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
value_to_encrypt (str): Value to encrypt
|
|
198
|
+
public_key (rsa.RSAPublicKey): RSA public key
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
str: Base64 encoded encrypted value containing both the encrypted AES key and encrypted data
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If encryption fails
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
# Generate random AES key and IV
|
|
208
|
+
aes_key = os.urandom(32) # 256-bit key
|
|
209
|
+
iv = os.urandom(16)
|
|
210
|
+
|
|
211
|
+
# Encrypt the AES key with RSA
|
|
212
|
+
encrypted_key = public_key.encrypt(
|
|
213
|
+
aes_key,
|
|
214
|
+
padding.OAEP(
|
|
215
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
216
|
+
algorithm=hashes.SHA256(),
|
|
217
|
+
label=None,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Create AES cipher
|
|
222
|
+
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
|
|
223
|
+
encryptor = cipher.encryptor()
|
|
224
|
+
|
|
225
|
+
# Pad data to block size
|
|
226
|
+
value_bytes = value_to_encrypt.encode("utf-8")
|
|
227
|
+
pad_length = 16 - (len(value_bytes) % 16)
|
|
228
|
+
value_bytes += bytes([pad_length]) * pad_length
|
|
229
|
+
|
|
230
|
+
# Encrypt data with AES
|
|
231
|
+
encrypted_data = encryptor.update(value_bytes) + encryptor.finalize()
|
|
232
|
+
|
|
233
|
+
# Combine encrypted key, IV and data
|
|
234
|
+
combined = encrypted_key + iv + encrypted_data
|
|
235
|
+
|
|
236
|
+
# Return base64 encoded result
|
|
237
|
+
return base64.b64encode(combined).decode("utf-8")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def decrypt_value(encrypted_value: str, private_key: rsa.RSAPrivateKey) -> str:
|
|
241
|
+
"""Decrypt using hybrid RSA+AES decryption.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
encrypted_value (str): Base64 encoded encrypted value
|
|
245
|
+
private_key (rsa.RSAPrivateKey): RSA private key
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
str: Decrypted value as string
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If decryption fails
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
# Decode base64
|
|
255
|
+
combined = base64.b64decode(encrypted_value)
|
|
256
|
+
|
|
257
|
+
# Extract components - first 256 bytes are RSA encrypted key
|
|
258
|
+
encrypted_key = combined[:256] # RSA-2048 output is 256 bytes
|
|
259
|
+
iv = combined[256:272] # 16 bytes IV
|
|
260
|
+
encrypted_data = combined[272:] # Rest is encrypted data
|
|
261
|
+
|
|
262
|
+
# Decrypt AES key
|
|
263
|
+
aes_key = private_key.decrypt(
|
|
264
|
+
encrypted_key,
|
|
265
|
+
padding.OAEP(
|
|
266
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
267
|
+
algorithm=hashes.SHA256(),
|
|
268
|
+
label=None,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Create AES cipher
|
|
273
|
+
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
|
|
274
|
+
decryptor = cipher.decryptor()
|
|
275
|
+
|
|
276
|
+
# Decrypt data
|
|
277
|
+
decrypted_padded = decryptor.update(encrypted_data) + decryptor.finalize()
|
|
278
|
+
|
|
279
|
+
# Remove padding
|
|
280
|
+
pad_length = decrypted_padded[-1]
|
|
281
|
+
decrypted = decrypted_padded[:-pad_length]
|
|
282
|
+
|
|
283
|
+
return decrypted.decode("utf-8")
|
supervaizer/event.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict
|
|
9
|
+
|
|
10
|
+
from supervaizer.__version__ import VERSION
|
|
11
|
+
from supervaizer.common import SvBaseModel
|
|
12
|
+
from supervaizer.lifecycle import EntityStatus
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from supervaizer.agent import Agent
|
|
16
|
+
from supervaizer.case import Case, CaseNodeUpdate
|
|
17
|
+
from supervaizer.job import Job
|
|
18
|
+
from supervaizer.server import Server
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventType(str, Enum):
|
|
22
|
+
AGENT_REGISTER = "agent.register"
|
|
23
|
+
SERVER_REGISTER = "server.register"
|
|
24
|
+
AGENT_WAKEUP = "agent.wakeup"
|
|
25
|
+
AGENT_SEND_ANOMALY = "agent.anomaly"
|
|
26
|
+
INTERMEDIARY = "agent.intermediary"
|
|
27
|
+
JOB_START_CONFIRMATION = "agent.job.start.confirmation"
|
|
28
|
+
JOB_END = "agent.job.end"
|
|
29
|
+
JOB_STATUS = "agent.job.status"
|
|
30
|
+
JOB_RESULT = "agent.job.result"
|
|
31
|
+
JOB_ERROR = "agent.job.error"
|
|
32
|
+
CASE_START = "agent.case.start"
|
|
33
|
+
CASE_END = "agent.case.end"
|
|
34
|
+
CASE_STATUS = "agent.case.status"
|
|
35
|
+
CASE_RESULT = "agent.case.result"
|
|
36
|
+
CASE_UPDATE = "agent.case.update"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AbstractEvent(SvBaseModel):
|
|
40
|
+
supervaizer_VERSION: ClassVar[str] = VERSION
|
|
41
|
+
source: Dict[str, Any]
|
|
42
|
+
account: Any # Use Any to avoid Pydantic type resolution issues
|
|
43
|
+
type: EventType
|
|
44
|
+
object_type: str
|
|
45
|
+
details: Dict[str, Any]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Event(AbstractEvent):
|
|
49
|
+
"""Base class for all events in the Supervaize Control system.
|
|
50
|
+
|
|
51
|
+
Events represent messages sent from agents to the control system to communicate
|
|
52
|
+
status, anomalies, deliverables and other information.
|
|
53
|
+
|
|
54
|
+
Inherits from AbstractEvent which defines the core event attributes:
|
|
55
|
+
- source: The source/origin of the event (e.g. agent/server URI)
|
|
56
|
+
- type: The EventType enum indicating the event category
|
|
57
|
+
- account: The account that the event belongs to
|
|
58
|
+
- details: A dictionary containing event-specific details
|
|
59
|
+
|
|
60
|
+
Tests in tests/test_event.py
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
64
|
+
super().__init__(**kwargs)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def payload(self) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Returns the payload for the event.
|
|
70
|
+
This must be a dictionary that can be serialized to JSON to be sent in the request body.
|
|
71
|
+
"""
|
|
72
|
+
return {
|
|
73
|
+
"source": self.source,
|
|
74
|
+
"workspace": f"{self.account.workspace_id}",
|
|
75
|
+
"event_type": f"{self.type.value}",
|
|
76
|
+
"object_type": self.object_type,
|
|
77
|
+
"details": self.details,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AgentRegisterEvent(Event):
|
|
82
|
+
"""Event sent when an agent registers with the control system.
|
|
83
|
+
|
|
84
|
+
Test in tests/test_agent_register_event.py
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
agent: "Agent",
|
|
90
|
+
account: Any, # Use Any to avoid type resolution issues
|
|
91
|
+
polling: bool = True,
|
|
92
|
+
) -> None:
|
|
93
|
+
super().__init__(
|
|
94
|
+
type=EventType.AGENT_REGISTER,
|
|
95
|
+
account=account,
|
|
96
|
+
source={"agent": agent.slug},
|
|
97
|
+
object_type="agent",
|
|
98
|
+
details=agent.registration_info | {"polling": polling},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ServerRegisterEvent(Event):
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
account: Any, # Use Any to avoid type resolution issues
|
|
106
|
+
server: "Server",
|
|
107
|
+
) -> None:
|
|
108
|
+
super().__init__(
|
|
109
|
+
type=EventType.SERVER_REGISTER,
|
|
110
|
+
source={"server": server.uri},
|
|
111
|
+
account=account,
|
|
112
|
+
object_type="server",
|
|
113
|
+
details=server.registration_info,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class JobStartConfirmationEvent(Event):
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
job: "Job",
|
|
121
|
+
account: Any, # Use Any to avoid type resolution issues
|
|
122
|
+
) -> None:
|
|
123
|
+
super().__init__(
|
|
124
|
+
type=EventType.JOB_START_CONFIRMATION,
|
|
125
|
+
account=account,
|
|
126
|
+
source={"job": job.id},
|
|
127
|
+
object_type="job",
|
|
128
|
+
details=job.registration_info,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class JobFinishedEvent(Event):
|
|
133
|
+
def __init__(self, job: "Job", account: Any) -> None:
|
|
134
|
+
# Check if job has responses, otherwise use the job's current status
|
|
135
|
+
if job.responses:
|
|
136
|
+
details = job.responses[-1].status
|
|
137
|
+
else:
|
|
138
|
+
details = job.status
|
|
139
|
+
|
|
140
|
+
event_type = (
|
|
141
|
+
EventType.JOB_END
|
|
142
|
+
if details == EntityStatus.COMPLETED
|
|
143
|
+
else EventType.JOB_ERROR
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
super().__init__(
|
|
147
|
+
type=event_type,
|
|
148
|
+
account=account,
|
|
149
|
+
source={"job": job.id},
|
|
150
|
+
object_type="job",
|
|
151
|
+
details=job.registration_info,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class CaseStartEvent(Event):
|
|
156
|
+
def __init__(
|
|
157
|
+
self, case: "Case", account: Any
|
|
158
|
+
) -> None: # Use Any to avoid type resolution issues
|
|
159
|
+
super().__init__(
|
|
160
|
+
type=EventType.CASE_START,
|
|
161
|
+
account=account,
|
|
162
|
+
source={"job": case.job_id, "case": case.id},
|
|
163
|
+
object_type="case",
|
|
164
|
+
details=case.registration_info,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CaseUpdateEvent(Event):
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
case: "Case",
|
|
172
|
+
account: Any,
|
|
173
|
+
update: "CaseNodeUpdate",
|
|
174
|
+
) -> None:
|
|
175
|
+
super().__init__(
|
|
176
|
+
type=EventType.CASE_UPDATE,
|
|
177
|
+
account=account,
|
|
178
|
+
source={"job": case.job_id, "case": case.id},
|
|
179
|
+
object_type="case",
|
|
180
|
+
details=update.registration_info,
|
|
181
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
# This is an example file.
|
|
8
|
+
# It must be copied / renamed to supervaizer_control.py
|
|
9
|
+
# and edited to configure your agent(s)
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shortuuid
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from supervaizer import (
|
|
16
|
+
Agent,
|
|
17
|
+
AgentMethod,
|
|
18
|
+
AgentMethods,
|
|
19
|
+
Parameter,
|
|
20
|
+
ParametersSetup,
|
|
21
|
+
Server,
|
|
22
|
+
)
|
|
23
|
+
from supervaizer.account import Account
|
|
24
|
+
|
|
25
|
+
# Create a console with default style set to yellow
|
|
26
|
+
console = Console(style="yellow")
|
|
27
|
+
|
|
28
|
+
# Public url of your hosted agent (including port if needed)
|
|
29
|
+
# Use loca.lt or ngrok to get a public url during development.
|
|
30
|
+
# This can be setup from environment variables.
|
|
31
|
+
# SUPERVAIZER_HOST and SUPERVAIZER_PORT
|
|
32
|
+
DEV_PUBLIC_URL = "https://myagent-dev.loca.lt"
|
|
33
|
+
# Public url of your hosted agent
|
|
34
|
+
PROD_PUBLIC_URL = "https://myagent.cloud-hosting.net:8001"
|
|
35
|
+
|
|
36
|
+
# Define the parameters and secrets expected by the agent
|
|
37
|
+
agent_parameters: ParametersSetup | None = ParametersSetup.from_list(
|
|
38
|
+
[
|
|
39
|
+
Parameter(
|
|
40
|
+
name="OPEN_API_KEY",
|
|
41
|
+
description="OpenAPI Key",
|
|
42
|
+
is_environment=True,
|
|
43
|
+
is_secret=True,
|
|
44
|
+
),
|
|
45
|
+
Parameter(
|
|
46
|
+
name="SERPER_API",
|
|
47
|
+
description="Server API key updated",
|
|
48
|
+
is_environment=True,
|
|
49
|
+
is_secret=True,
|
|
50
|
+
),
|
|
51
|
+
Parameter(
|
|
52
|
+
name="COMPETITOR_SUMMARY_URL",
|
|
53
|
+
description="Competitor Summary URL",
|
|
54
|
+
is_environment=True,
|
|
55
|
+
is_secret=False,
|
|
56
|
+
),
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Define the method used to start a job
|
|
61
|
+
job_start_method: AgentMethod = AgentMethod(
|
|
62
|
+
name="start", # This is required
|
|
63
|
+
method="example_agent.example_synchronous_job_start", # Path to the main function in dotted notation.
|
|
64
|
+
is_async=False, # Only use sync methods for the moment
|
|
65
|
+
params={"action": "start"}, # If default parameters must be passed to the function.
|
|
66
|
+
fields=[
|
|
67
|
+
{
|
|
68
|
+
"name": "Company to research", # Field name - displayed in the UI
|
|
69
|
+
"type": str, # Python type of the field for pydantic validation - note , ChoiceField and MultipleChoiceField are a list[str]
|
|
70
|
+
"field_type": "CharField", # Field type for persistence.
|
|
71
|
+
"description": "Company to research", # Optional - Description of the field - displayed in the UI
|
|
72
|
+
"default": "Google", # Optional - Default value for the field
|
|
73
|
+
"required": True, # Whether the field is required
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "Max number of results",
|
|
77
|
+
"type": int,
|
|
78
|
+
"field_type": "IntegerField",
|
|
79
|
+
"required": True,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "Subscribe to updates",
|
|
83
|
+
"type": bool,
|
|
84
|
+
"field_type": "BooleanField",
|
|
85
|
+
"required": False,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "Type of research",
|
|
89
|
+
"type": str,
|
|
90
|
+
"field_type": "ChoiceField",
|
|
91
|
+
"choices": [["A", "Advanced"], ["R", "Restricted"]],
|
|
92
|
+
"widget": "RadioSelect",
|
|
93
|
+
"required": True,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "Details of research",
|
|
97
|
+
"type": str,
|
|
98
|
+
"field_type": "CharField",
|
|
99
|
+
"widget": "Textarea",
|
|
100
|
+
"required": False,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "List of countries",
|
|
104
|
+
"type": list[str],
|
|
105
|
+
"field_type": "MultipleChoiceField",
|
|
106
|
+
"choices": [
|
|
107
|
+
["PA", "Panama"],
|
|
108
|
+
["PG", "Papua New Guinea"],
|
|
109
|
+
["PY", "Paraguay"],
|
|
110
|
+
["PE", "Peru"],
|
|
111
|
+
["PH", "Philippines"],
|
|
112
|
+
["PN", "Pitcairn"],
|
|
113
|
+
["PL", "Poland"],
|
|
114
|
+
],
|
|
115
|
+
"required": True,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "languages",
|
|
119
|
+
"type": list[str],
|
|
120
|
+
"field_type": "MultipleChoiceField",
|
|
121
|
+
"choices": [["en", "English"], ["fr", "French"], ["es", "Spanish"]],
|
|
122
|
+
"required": False,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
description="Start the collection of new competitor summary",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
job_stop_method: AgentMethod = AgentMethod(
|
|
129
|
+
name="stop",
|
|
130
|
+
method="control.stop",
|
|
131
|
+
params={"action": "stop"},
|
|
132
|
+
description="Stop the agent",
|
|
133
|
+
)
|
|
134
|
+
job_status_method: AgentMethod = AgentMethod(
|
|
135
|
+
name="status",
|
|
136
|
+
method="hello.mystatus",
|
|
137
|
+
params={"status": "statusvalue"},
|
|
138
|
+
description="Get the status of the agent",
|
|
139
|
+
)
|
|
140
|
+
custom_method: AgentMethod = AgentMethod(
|
|
141
|
+
name="custom",
|
|
142
|
+
method="control.custom",
|
|
143
|
+
params={"action": "custom"},
|
|
144
|
+
description="Custom method",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
custom_method2: AgentMethod = AgentMethod(
|
|
148
|
+
name="custom2",
|
|
149
|
+
method="control.custom2",
|
|
150
|
+
params={"action": "custom2"},
|
|
151
|
+
description="Custom method",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
agent_name = "competitor_summary"
|
|
156
|
+
|
|
157
|
+
# Define the Agent
|
|
158
|
+
agent: Agent = Agent(
|
|
159
|
+
name=agent_name,
|
|
160
|
+
id=shortuuid.uuid(f"{agent_name}"),
|
|
161
|
+
author="John Doe", # Author of the agent
|
|
162
|
+
developer="Developer", # Developer of the controller integration
|
|
163
|
+
maintainer="Ive Maintained", # Maintainer of the integration
|
|
164
|
+
editor="DevAiExperts", # Editor (usually a company)
|
|
165
|
+
version="1.3", # Version string
|
|
166
|
+
description="This is a test agent",
|
|
167
|
+
tags=["testtag", "testtag2"],
|
|
168
|
+
methods=AgentMethods(
|
|
169
|
+
job_start=job_start_method,
|
|
170
|
+
job_stop=job_stop_method,
|
|
171
|
+
job_status=job_status_method,
|
|
172
|
+
chat=None,
|
|
173
|
+
custom={"custom1": custom_method, "custom2": custom_method2},
|
|
174
|
+
),
|
|
175
|
+
parameters_setup=agent_parameters,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
account: Account = Account(
|
|
179
|
+
workspace_id=os.getenv("SUPERVAIZE_WORKSPACE_ID"), # From supervaize.com
|
|
180
|
+
api_key=os.getenv("SUPERVAIZE_API_KEY"), # From supervaize
|
|
181
|
+
api_url=os.getenv("SUPERVAIZE_API_URL"), # From supervaize
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Define the supervaizer server capabilities
|
|
185
|
+
sv_server: Server = Server(
|
|
186
|
+
agents=[agent],
|
|
187
|
+
a2a_endpoints=True, # Enable A2A endpoints
|
|
188
|
+
acp_endpoints=True, # Enable ACP endpoints
|
|
189
|
+
supervisor_account=account, # Account of the supervisor from Supervaize
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
# Start the supervaize server
|
|
195
|
+
sv_server.launch(log_level="DEBUG")
|