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/lifecycle.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
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
|
+
import logging
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Protocol, TypeVar
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EntityStatus(str, Enum):
|
|
19
|
+
"""Base status enum for workflow entities."""
|
|
20
|
+
|
|
21
|
+
STOPPED = "stopped"
|
|
22
|
+
IN_PROGRESS = "in_progress"
|
|
23
|
+
CANCELLING = "cancelling"
|
|
24
|
+
AWAITING = "awaiting"
|
|
25
|
+
COMPLETED = "completed"
|
|
26
|
+
FAILED = "failed"
|
|
27
|
+
CANCELLED = "cancelled"
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def status_stopped() -> list["EntityStatus"]:
|
|
31
|
+
return [
|
|
32
|
+
EntityStatus.STOPPED,
|
|
33
|
+
EntityStatus.CANCELLED,
|
|
34
|
+
EntityStatus.FAILED,
|
|
35
|
+
EntityStatus.COMPLETED,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_stopped(self) -> bool:
|
|
40
|
+
return self in EntityStatus.status_stopped()
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def status_running() -> list["EntityStatus"]:
|
|
44
|
+
return [
|
|
45
|
+
EntityStatus.IN_PROGRESS,
|
|
46
|
+
EntityStatus.CANCELLING,
|
|
47
|
+
EntityStatus.AWAITING,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_running(self) -> bool:
|
|
52
|
+
return self in EntityStatus.status_running()
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def status_anomaly() -> list["EntityStatus"]:
|
|
56
|
+
return [
|
|
57
|
+
EntityStatus.CANCELLING,
|
|
58
|
+
EntityStatus.CANCELLED,
|
|
59
|
+
EntityStatus.FAILED,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_anomaly(self) -> bool:
|
|
64
|
+
return self in EntityStatus.status_anomaly()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def label(self) -> str:
|
|
68
|
+
"""Get the display label for the enum value."""
|
|
69
|
+
return self.name.replace("_", " ").title()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EntityEvents(str, Enum):
|
|
73
|
+
"""Events that trigger transitions between entity states."""
|
|
74
|
+
|
|
75
|
+
START_WORK = "start_work"
|
|
76
|
+
SUCCESSFULLY_DONE = "successfully_done"
|
|
77
|
+
AWAITING_ON_INPUT = "awaiting_on_input"
|
|
78
|
+
CANCEL_REQUESTED = "cancel_requested"
|
|
79
|
+
ERROR_ENCOUNTERED = "error_encountered"
|
|
80
|
+
TIMEOUT_OR_ERROR = "timeout_or_error"
|
|
81
|
+
INPUT_RECEIVED = "input_received"
|
|
82
|
+
CANCEL_WHILE_WAITING = "cancel_while_waiting"
|
|
83
|
+
CANCEL_CONFIRMED = "cancel_confirmed"
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def label(self) -> str:
|
|
87
|
+
"""Get the display label for the enum value."""
|
|
88
|
+
return self.name.replace("_", " ").title()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Lifecycle:
|
|
92
|
+
"""
|
|
93
|
+
Defines valid state transitions for workflow entities.
|
|
94
|
+
|
|
95
|
+
From: https://agentcommunicationprotocol.dev/core-concepts/agent-lifecycle
|
|
96
|
+
```mermaid
|
|
97
|
+
stateDiagram-v2
|
|
98
|
+
[*] --> created
|
|
99
|
+
created --> in_progress : Start work
|
|
100
|
+
in_progress --> completed : Successfully done
|
|
101
|
+
in_progress --> awaiting : Awaiting on input
|
|
102
|
+
in_progress --> cancelling : Cancel requested
|
|
103
|
+
awaiting --> failed : Timeout or error
|
|
104
|
+
in_progress --> failed : Error encountered
|
|
105
|
+
awaiting --> in_progress : Input received
|
|
106
|
+
awaiting --> cancelling : Cancel while waiting
|
|
107
|
+
cancelling --> cancelled : Cancel confirmed
|
|
108
|
+
cancelled --> [*]
|
|
109
|
+
completed --> [*]
|
|
110
|
+
failed --> [*]
|
|
111
|
+
```
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# Event to transition mapping
|
|
115
|
+
EVENT_TRANSITIONS = {
|
|
116
|
+
EntityEvents.START_WORK: (EntityStatus.STOPPED, EntityStatus.IN_PROGRESS),
|
|
117
|
+
EntityEvents.SUCCESSFULLY_DONE: (
|
|
118
|
+
EntityStatus.IN_PROGRESS,
|
|
119
|
+
EntityStatus.COMPLETED,
|
|
120
|
+
),
|
|
121
|
+
EntityEvents.AWAITING_ON_INPUT: (
|
|
122
|
+
EntityStatus.IN_PROGRESS,
|
|
123
|
+
EntityStatus.AWAITING,
|
|
124
|
+
),
|
|
125
|
+
EntityEvents.CANCEL_REQUESTED: (
|
|
126
|
+
EntityStatus.IN_PROGRESS,
|
|
127
|
+
EntityStatus.CANCELLING,
|
|
128
|
+
),
|
|
129
|
+
EntityEvents.ERROR_ENCOUNTERED: (EntityStatus.IN_PROGRESS, EntityStatus.FAILED),
|
|
130
|
+
EntityEvents.TIMEOUT_OR_ERROR: (EntityStatus.AWAITING, EntityStatus.FAILED),
|
|
131
|
+
EntityEvents.INPUT_RECEIVED: (EntityStatus.AWAITING, EntityStatus.IN_PROGRESS),
|
|
132
|
+
EntityEvents.CANCEL_WHILE_WAITING: (
|
|
133
|
+
EntityStatus.AWAITING,
|
|
134
|
+
EntityStatus.CANCELLING,
|
|
135
|
+
),
|
|
136
|
+
EntityEvents.CANCEL_CONFIRMED: (
|
|
137
|
+
EntityStatus.CANCELLING,
|
|
138
|
+
EntityStatus.CANCELLED,
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def get_terminal_states(cls) -> List[EntityStatus]:
|
|
144
|
+
"""
|
|
145
|
+
Identify terminal states in the state machine.
|
|
146
|
+
|
|
147
|
+
A terminal state is a state that has no outgoing transitions.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
list: List of EntityStatus enum values representing terminal states
|
|
151
|
+
"""
|
|
152
|
+
# Get all states that appear as 'from_status' in transitions
|
|
153
|
+
states_with_outgoing = set(
|
|
154
|
+
from_status for _, (from_status, _) in cls.EVENT_TRANSITIONS.items()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Terminal states are those that have no outgoing transitions
|
|
158
|
+
terminal_states = [
|
|
159
|
+
status for status in EntityStatus if status not in states_with_outgoing
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
return terminal_states
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def get_start_states(cls) -> List[EntityStatus]:
|
|
166
|
+
"""
|
|
167
|
+
Identify start states in the state machine.
|
|
168
|
+
|
|
169
|
+
A start state is a state that can be entered directly at the beginning of
|
|
170
|
+
the workflow. In our case, this is determined by convention and by examining
|
|
171
|
+
which states don't appear as target states in any transition except their own.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
list: List of EntityStatus enum values representing start states
|
|
175
|
+
"""
|
|
176
|
+
# Get all states that appear as 'to_status' in transitions
|
|
177
|
+
target_states = set(
|
|
178
|
+
to_status for _, (_, to_status) in cls.EVENT_TRANSITIONS.items()
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Get all states that appear as 'from_status' in transitions
|
|
182
|
+
source_states = set(
|
|
183
|
+
from_status for _, (from_status, _) in cls.EVENT_TRANSITIONS.items()
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Find states that are source states but never target states
|
|
187
|
+
# (except for cycles where they might transition to themselves)
|
|
188
|
+
start_candidates = source_states.difference(target_states)
|
|
189
|
+
|
|
190
|
+
# If no clear start states are found based on the above logic,
|
|
191
|
+
# use STOPPED as the conventional start state
|
|
192
|
+
if not start_candidates:
|
|
193
|
+
return [EntityStatus.STOPPED]
|
|
194
|
+
|
|
195
|
+
return list(start_candidates)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def get_valid_transitions(
|
|
199
|
+
cls, current_status: EntityStatus
|
|
200
|
+
) -> Dict[EntityStatus, EntityEvents]:
|
|
201
|
+
"""Get valid transitions from the current status."""
|
|
202
|
+
result = {}
|
|
203
|
+
for event, (from_status, to_status) in cls.EVENT_TRANSITIONS.items():
|
|
204
|
+
if from_status == current_status:
|
|
205
|
+
result[to_status] = event
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def can_transition(cls, from_status: EntityStatus, to_status: EntityStatus) -> bool:
|
|
210
|
+
"""Check if transition from current status to target status is valid."""
|
|
211
|
+
for event, (event_from, event_to) in cls.EVENT_TRANSITIONS.items():
|
|
212
|
+
if event_from == from_status and event_to == to_status:
|
|
213
|
+
return True
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def get_transition_reason(
|
|
218
|
+
cls, from_status: EntityStatus, to_status: EntityStatus
|
|
219
|
+
) -> str:
|
|
220
|
+
"""Get the reason/description for a transition."""
|
|
221
|
+
for event, (event_from, event_to) in cls.EVENT_TRANSITIONS.items():
|
|
222
|
+
if event_from == from_status and event_to == to_status:
|
|
223
|
+
return event.value
|
|
224
|
+
return "Invalid transition"
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def get_status_from_event(
|
|
228
|
+
cls, current_status: EntityStatus, event: EntityEvents
|
|
229
|
+
) -> EntityStatus | None:
|
|
230
|
+
"""Get the target status for a given event from the current status."""
|
|
231
|
+
if event not in cls.EVENT_TRANSITIONS:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
from_status, to_status = cls.EVENT_TRANSITIONS[event]
|
|
235
|
+
if current_status == from_status:
|
|
236
|
+
return to_status
|
|
237
|
+
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def generate_valid_transitions_dict(
|
|
242
|
+
cls,
|
|
243
|
+
) -> Dict[EntityStatus, Dict[EntityStatus, EntityEvents]]:
|
|
244
|
+
"""
|
|
245
|
+
Generate a complete dictionary of all valid transitions in the format:
|
|
246
|
+
{
|
|
247
|
+
StatusA: {StatusB: EventAB, StatusC: EventAC},
|
|
248
|
+
StatusB: {StatusD: EventBD},
|
|
249
|
+
...
|
|
250
|
+
TerminalStatusX: {},
|
|
251
|
+
}
|
|
252
|
+
"""
|
|
253
|
+
# Initialize the result dictionary with all statuses
|
|
254
|
+
result: Dict[EntityStatus, Dict[EntityStatus, EntityEvents]] = {
|
|
255
|
+
status: {} for status in EntityStatus
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Populate with transitions from EVENT_TRANSITIONS
|
|
259
|
+
for event, (from_status, to_status) in cls.EVENT_TRANSITIONS.items():
|
|
260
|
+
result[from_status][to_status] = event
|
|
261
|
+
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def generate_mermaid_diagram(cls, steps_list: list[str]) -> str:
|
|
266
|
+
"""
|
|
267
|
+
Generate a Mermaid stateDiagram-v2 representation of the state machine.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
steps_list: List of steps to include in the diagram (get it from cls.mermaid_diagram_all_steps())
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
str: Mermaid markdown for the state diagram
|
|
274
|
+
"""
|
|
275
|
+
# Start with diagram header
|
|
276
|
+
mermaid = "```mermaid\nstateDiagram-v2\n"
|
|
277
|
+
# Start state
|
|
278
|
+
mermaid += "\n ".join(steps_list)
|
|
279
|
+
|
|
280
|
+
# Close the diagram
|
|
281
|
+
mermaid += "\n```"
|
|
282
|
+
|
|
283
|
+
return mermaid
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def mermaid_diagram_all_steps(cls) -> list[str]:
|
|
287
|
+
"""Get all steps for the Mermaid diagram."""
|
|
288
|
+
steps = cls.mermaid_start_state()
|
|
289
|
+
steps.extend(cls.mermaid_diagram_steps())
|
|
290
|
+
steps.extend(cls.mermaid_terminal_states())
|
|
291
|
+
return steps
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def mermaid_diagram_steps(cls) -> list[str]:
|
|
295
|
+
"""
|
|
296
|
+
Generate a list of steps for the Mermaid diagram.
|
|
297
|
+
"""
|
|
298
|
+
steps = []
|
|
299
|
+
for event, (from_status, to_status) in cls.EVENT_TRANSITIONS.items():
|
|
300
|
+
# Get the event display name for the transition label
|
|
301
|
+
event_display = str(event.label)
|
|
302
|
+
from_state = from_status.value
|
|
303
|
+
to_state = to_status.value
|
|
304
|
+
|
|
305
|
+
steps.append(f"{from_state} --> {to_state} : {event_display}")
|
|
306
|
+
|
|
307
|
+
return steps
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def mermaid_start_state(cls) -> list[str]:
|
|
311
|
+
"""Get the start state for the Mermaid diagram."""
|
|
312
|
+
return [f"[*] --> {state.value}" for state in cls.get_start_states()]
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def mermaid_terminal_states(cls) -> list[str]:
|
|
316
|
+
"""Get the terminal states for the Mermaid diagram."""
|
|
317
|
+
return [f"{state.value} --> [*]" for state in cls.get_terminal_states()]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Type aliases for backward compatibility
|
|
321
|
+
JobTransitions = Lifecycle
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class WorkflowEntity(Protocol):
|
|
325
|
+
"""Protocol that defines the interface required for an entity to work with lifecycle transitions."""
|
|
326
|
+
|
|
327
|
+
status: EntityStatus
|
|
328
|
+
finished_at: Any
|
|
329
|
+
id: Any
|
|
330
|
+
name: str
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
T = TypeVar("T", bound=WorkflowEntity)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class EntityLifecycle:
|
|
337
|
+
"""
|
|
338
|
+
Generic lifecycle manager for workflow entities like Job and Case.
|
|
339
|
+
Handles state transitions according to defined business rules.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def transition(entity: T, to_status: EntityStatus) -> tuple[bool, str]:
|
|
344
|
+
"""
|
|
345
|
+
Transition an entity to a new status if the transition is valid.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
entity: The entity object to transition (Job, Case, etc.)
|
|
349
|
+
to_status: The target EntityStatus to transition to
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
tuple[bool, str]: (True, "") if transition was successful,
|
|
353
|
+
(False, "error explanation") otherwise
|
|
354
|
+
|
|
355
|
+
Side effects:
|
|
356
|
+
- Updates the entity status
|
|
357
|
+
- Records the finished time if the entity is in a terminal state
|
|
358
|
+
|
|
359
|
+
Tested in apps.sv_entities.tests.test_lifecycle
|
|
360
|
+
"""
|
|
361
|
+
current_status = entity.status
|
|
362
|
+
|
|
363
|
+
# Check if transition is valid
|
|
364
|
+
if not Lifecycle.can_transition(current_status, to_status):
|
|
365
|
+
error_msg = (
|
|
366
|
+
f"Invalid transition: {current_status} → {to_status} "
|
|
367
|
+
f"for {entity.__class__.__name__} {entity.id} ({entity.name})"
|
|
368
|
+
)
|
|
369
|
+
log.warning(error_msg)
|
|
370
|
+
return False, error_msg
|
|
371
|
+
|
|
372
|
+
# Log the transition
|
|
373
|
+
reason = Lifecycle.get_transition_reason(current_status, to_status)
|
|
374
|
+
log.info(
|
|
375
|
+
f"{entity.__class__.__name__} {entity.id} ({entity.name}) "
|
|
376
|
+
f"transitioning: {current_status} → {to_status}. Reason: {reason}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Update the entity status
|
|
380
|
+
entity.status = to_status
|
|
381
|
+
|
|
382
|
+
# If transitioning to a terminal state, record the finished time
|
|
383
|
+
if to_status in [
|
|
384
|
+
EntityStatus.COMPLETED,
|
|
385
|
+
EntityStatus.CANCELLED,
|
|
386
|
+
EntityStatus.FAILED,
|
|
387
|
+
]:
|
|
388
|
+
if not entity.finished_at:
|
|
389
|
+
entity.finished_at = datetime.now()
|
|
390
|
+
|
|
391
|
+
return True, ""
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def handle_event(entity: T, event: EntityEvents) -> tuple[bool, str]:
|
|
395
|
+
"""
|
|
396
|
+
Handle an event by transitioning the entity to the appropriate status.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
entity: The entity object to transition
|
|
400
|
+
event: The event that occurred
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
tuple[bool, str]: (True, "") if handling was successful,
|
|
404
|
+
(False, "error explanation") otherwise
|
|
405
|
+
"""
|
|
406
|
+
current_status = entity.status
|
|
407
|
+
to_status = Lifecycle.get_status_from_event(current_status, event)
|
|
408
|
+
|
|
409
|
+
if not to_status:
|
|
410
|
+
error_msg = (
|
|
411
|
+
f"Invalid event {event} for current status {current_status} "
|
|
412
|
+
f"in {entity.__class__.__name__} {entity.id} ({entity.name})"
|
|
413
|
+
)
|
|
414
|
+
log.warning(error_msg)
|
|
415
|
+
return False, error_msg
|
|
416
|
+
|
|
417
|
+
return EntityLifecycle.transition(entity, to_status)
|
supervaizer/parameter.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
from pydantic import Field
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
from supervaizer.common import SvBaseModel, log
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ParameterAbstract(SvBaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Base model for agent parameters that defines configuration and metadata.
|
|
18
|
+
|
|
19
|
+
Parameters can be environment variables, secrets, or regular configuration values
|
|
20
|
+
that are used by agents during execution. The Supervaize platform uses this
|
|
21
|
+
model to manage parameter definitions and values.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name: str = Field(
|
|
25
|
+
description="The name of the parameter, as used in the agent code"
|
|
26
|
+
)
|
|
27
|
+
description: str | None = Field(
|
|
28
|
+
default=None,
|
|
29
|
+
description="The description of the parameter, used in the Supervaize UI",
|
|
30
|
+
)
|
|
31
|
+
is_environment: bool = Field(
|
|
32
|
+
default=False,
|
|
33
|
+
description="Whether the parameter is set as an environment variable",
|
|
34
|
+
)
|
|
35
|
+
value: str | None = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="The value of the parameter - provided by the Supervaize platform",
|
|
38
|
+
)
|
|
39
|
+
is_secret: bool = Field(
|
|
40
|
+
default=False,
|
|
41
|
+
description="Whether the parameter is a secret - hidden from the user in the Supervaize UI",
|
|
42
|
+
)
|
|
43
|
+
is_required: bool = Field(
|
|
44
|
+
default=False,
|
|
45
|
+
description="Whether the parameter is required, used in the Supervaize UI",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
model_config = {
|
|
49
|
+
"reference_group": "Core",
|
|
50
|
+
"example_dict": {
|
|
51
|
+
"name": "OPEN_API_KEY",
|
|
52
|
+
"description": "OpenAPI Key",
|
|
53
|
+
"is_environment": True,
|
|
54
|
+
"is_secret": True,
|
|
55
|
+
"is_required": True,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Parameter(ParameterAbstract):
|
|
61
|
+
@property
|
|
62
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Override the to_dict method to handle the value field.
|
|
65
|
+
"""
|
|
66
|
+
data = self.model_dump(mode="json")
|
|
67
|
+
if self.is_secret:
|
|
68
|
+
data["value"] = "********"
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def registration_info(self) -> Dict[str, Any]:
|
|
73
|
+
return {
|
|
74
|
+
"name": self.name,
|
|
75
|
+
"description": self.description,
|
|
76
|
+
"is_environment": self.is_environment,
|
|
77
|
+
"is_secret": self.is_secret,
|
|
78
|
+
"is_required": self.is_required,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def set_value(self, value: str) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Set the value of a parameter and update the environment variable if needed.
|
|
84
|
+
Note that environment is updated ONLY if set_value is called explicitly.
|
|
85
|
+
Tested in tests/test_parameter.py
|
|
86
|
+
"""
|
|
87
|
+
self.value = value
|
|
88
|
+
if self.is_environment:
|
|
89
|
+
os.environ[self.name] = value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ParametersSetup(SvBaseModel):
|
|
93
|
+
"""
|
|
94
|
+
ParametersSetup model for the Supervaize Control API.
|
|
95
|
+
|
|
96
|
+
This represents a collection of parameters that can be used by an agent.
|
|
97
|
+
It contains a dictionary of parameters, where the key is the parameter name
|
|
98
|
+
and the value is the parameter object.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
```python
|
|
102
|
+
ParametersSetup.from_list([
|
|
103
|
+
Parameter(name="parameter1", value="value1"),
|
|
104
|
+
Parameter(name="parameter2", value="value2", description="desc2"),
|
|
105
|
+
])
|
|
106
|
+
```
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
definitions: Dict[str, Parameter] = Field(
|
|
110
|
+
description="A dictionary of Parameters, where the key is the parameter name and the value is the parameter object.",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
model_config = {
|
|
114
|
+
"reference_group": "Core",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_list(
|
|
119
|
+
cls, parameter_list: List[Parameter | Dict[str, Any]] | None
|
|
120
|
+
) -> "ParametersSetup | None":
|
|
121
|
+
if not parameter_list:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
if parameter_list and isinstance(
|
|
125
|
+
parameter_list[0], dict
|
|
126
|
+
): # TODO: add test for this
|
|
127
|
+
parameter_list_casted = [
|
|
128
|
+
Parameter(**parameter)
|
|
129
|
+
for parameter in parameter_list
|
|
130
|
+
if isinstance(parameter, dict)
|
|
131
|
+
]
|
|
132
|
+
parameter_list = parameter_list_casted # type: ignore
|
|
133
|
+
return cls(
|
|
134
|
+
definitions={parameter.name: parameter for parameter in parameter_list} # type: ignore
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def value(self, name: str) -> str | None:
|
|
138
|
+
"""
|
|
139
|
+
Get the value of a parameter from the environment.
|
|
140
|
+
"""
|
|
141
|
+
parameter = self.definitions.get(name, None)
|
|
142
|
+
return parameter.value if parameter else None
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def registration_info(self) -> List[Dict[str, Any]]:
|
|
146
|
+
return [parameter.registration_info for parameter in self.definitions.values()]
|
|
147
|
+
|
|
148
|
+
def update_values_from_server(
|
|
149
|
+
self, server_parameters_setup: List[Dict[str, Any]]
|
|
150
|
+
) -> "ParametersSetup":
|
|
151
|
+
"""Update the values of the parameters from the server.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
server_parameters_setup (List[Dict[str, Any]]): The parameters from the server.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If the parameter is not found in the definitions.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
ParametersSetup: The updated parameters.
|
|
161
|
+
|
|
162
|
+
Tested in tests/test_parameter.test_parameters_setup_update_values_from_server
|
|
163
|
+
"""
|
|
164
|
+
for parameter in server_parameters_setup:
|
|
165
|
+
if parameter.get("name", None) in self.definitions.keys():
|
|
166
|
+
def_parameter = self.definitions[parameter["name"]]
|
|
167
|
+
def_parameter.set_value(parameter["value"])
|
|
168
|
+
else:
|
|
169
|
+
message = f"Parameter {parameter} not found in definitions"
|
|
170
|
+
log.error(message)
|
|
171
|
+
raise ValueError(message)
|
|
172
|
+
|
|
173
|
+
return self
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
"""Protocol implementations for SUPERVAIZER."""
|
|
8
|
+
|
|
9
|
+
from supervaizer.protocol import a2a, acp
|
|
10
|
+
|
|
11
|
+
__all__ = ["a2a", "acp"]
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
"""A2A protocol implementation for SUPERVAIZER."""
|
|
8
|
+
|
|
9
|
+
from supervaizer.protocol.a2a.model import (
|
|
10
|
+
create_agent_card,
|
|
11
|
+
create_agents_list,
|
|
12
|
+
create_health_data,
|
|
13
|
+
)
|
|
14
|
+
from supervaizer.protocol.a2a.routes import create_routes
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"create_agent_card",
|
|
18
|
+
"create_agents_list",
|
|
19
|
+
"create_health_data",
|
|
20
|
+
"create_routes",
|
|
21
|
+
]
|