supervaizer 0.9.6__tar.gz → 0.9.8__tar.gz

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.
Files changed (52) hide show
  1. {supervaizer-0.9.6 → supervaizer-0.9.8}/PKG-INFO +1 -1
  2. {supervaizer-0.9.6 → supervaizer-0.9.8}/pyproject.toml +1 -1
  3. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/__version__.py +1 -1
  4. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/routes.py +23 -0
  5. supervaizer-0.9.8/src/supervaizer/admin/static/js/job-start-form.js +373 -0
  6. supervaizer-0.9.8/src/supervaizer/admin/templates/job_start_test.html +109 -0
  7. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/agent.py +139 -21
  8. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/cli.py +17 -6
  9. supervaizer-0.9.6/src/supervaizer/examples/controller-template.py → supervaizer-0.9.8/src/supervaizer/examples/controller_template.py +4 -3
  10. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/parameter.py +48 -0
  11. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/routes.py +131 -5
  12. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/server.py +7 -2
  13. {supervaizer-0.9.6 → supervaizer-0.9.8}/.gitignore +0 -0
  14. {supervaizer-0.9.6 → supervaizer-0.9.8}/LICENSE.md +0 -0
  15. {supervaizer-0.9.6 → supervaizer-0.9.8}/README.md +0 -0
  16. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/__init__.py +0 -0
  17. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/account.py +0 -0
  18. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/account_service.py +0 -0
  19. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/agent_detail.html +0 -0
  20. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/agents.html +0 -0
  21. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/agents_grid.html +0 -0
  22. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/base.html +0 -0
  23. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/case_detail.html +0 -0
  24. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/cases_list.html +0 -0
  25. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/cases_table.html +0 -0
  26. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/console.html +0 -0
  27. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/dashboard.html +0 -0
  28. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/job_detail.html +0 -0
  29. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/jobs_list.html +0 -0
  30. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/jobs_table.html +0 -0
  31. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/navigation.html +0 -0
  32. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/recent_activity.html +0 -0
  33. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/server.html +0 -0
  34. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/admin/templates/server_status_cards.html +0 -0
  35. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/case.py +0 -0
  36. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/common.py +0 -0
  37. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/event.py +0 -0
  38. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/instructions.py +0 -0
  39. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/job.py +0 -0
  40. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/job_service.py +0 -0
  41. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/lifecycle.py +0 -0
  42. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/__init__.py +0 -0
  43. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/a2a/__init__.py +0 -0
  44. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/a2a/model.py +0 -0
  45. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/a2a/routes.py +0 -0
  46. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/acp/__init__.py +0 -0
  47. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/acp/model.py +0 -0
  48. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/protocol/acp/routes.py +0 -0
  49. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/py.typed +0 -0
  50. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/server_utils.py +0 -0
  51. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/storage.py +0 -0
  52. {supervaizer-0.9.6 → supervaizer-0.9.8}/src/supervaizer/telemetry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supervaizer
3
- Version: 0.9.6
3
+ Version: 0.9.8
4
4
  Summary: Controller system for Supervaize
5
5
  Project-URL: Homepage, https://supervaize.com
6
6
  Project-URL: Repository, https://github.com/supervaize/supervaizer
@@ -70,7 +70,7 @@ markers = [
70
70
  "db: calls to the database",
71
71
  "current: test under development",
72
72
  ]
73
- addopts = "--cov=supervaizer --cov-report=term --cov-report=html"
73
+ #addopts = "--cov=supervaizer --cov-report=term --cov-report=html"
74
74
  asyncio_default_fixture_loop_scope = "function"
75
75
  testpaths = ["tests"]
76
76
  pythonpath = ["src"]
@@ -5,6 +5,6 @@
5
5
  # https://mozilla.org/MPL/2.0/.
6
6
 
7
7
 
8
- VERSION = "0.9.6"
8
+ VERSION = "0.9.8"
9
9
  API_VERSION = "v1"
10
10
  TELEMETRY_VERSION = "v1"
@@ -345,6 +345,29 @@ def create_admin_routes() -> APIRouter:
345
345
  status_code=503, detail="Server information unavailable"
346
346
  ) from e
347
347
 
348
+ @router.get("/job-start-test", response_class=HTMLResponse)
349
+ async def admin_job_start_test_page(request: Request) -> Response:
350
+ """Job start form test page."""
351
+ return templates.TemplateResponse(
352
+ "job_start_test.html",
353
+ {
354
+ "request": request,
355
+ "api_version": API_VERSION,
356
+ "api_key": os.getenv("SUPERVAIZER_API_KEY"),
357
+ },
358
+ )
359
+
360
+ @router.get("/static/js/job-start-form.js")
361
+ async def serve_job_start_form_js() -> Response:
362
+ """Serve the JobStartForm JavaScript file."""
363
+ js_file_path = Path(__file__).parent / "static" / "js" / "job-start-form.js"
364
+ if js_file_path.exists():
365
+ with open(js_file_path, "r") as f:
366
+ content = f.read()
367
+ return Response(content=content, media_type="application/javascript")
368
+ else:
369
+ raise HTTPException(status_code=404, detail="JavaScript file not found")
370
+
348
371
  @router.get("/console", response_class=HTMLResponse)
349
372
  async def admin_console_page(request: Request) -> Response:
350
373
  """Interactive console page - publicly accessible, authentication handled by frontend."""
@@ -0,0 +1,373 @@
1
+ /**
2
+ * JobStartForm - Handles job parameter validation and submission
3
+ *
4
+ * This class provides methods to validate agent parameters and method fields
5
+ * before starting a job, using the new validation endpoints.
6
+ */
7
+
8
+ class JobStartForm {
9
+ constructor(agentPath) {
10
+ this.agentPath = agentPath;
11
+ this.form = null;
12
+ this.errorContainer = null;
13
+ this.initialize();
14
+ }
15
+
16
+ initialize() {
17
+ // Wait for DOM to be ready
18
+ if (document.readyState === 'loading') {
19
+ document.addEventListener('DOMContentLoaded', () => this.setupForm());
20
+ } else {
21
+ this.setupForm();
22
+ }
23
+ }
24
+
25
+ setupForm() {
26
+ // Find the form and error container
27
+ this.form = document.querySelector('form[data-job-start]');
28
+ this.errorContainer = document.getElementById('validation-errors');
29
+
30
+ if (!this.form) {
31
+ console.warn('JobStartForm: No form found with data-job-start attribute');
32
+ return;
33
+ }
34
+
35
+ if (!this.errorContainer) {
36
+ console.warn('JobStartForm: No validation-errors container found');
37
+ return;
38
+ }
39
+
40
+ // Add submit handler
41
+ this.form.addEventListener('submit', (e) => this.handleSubmit(e));
42
+
43
+ // Add validation on field change
44
+ this.form.addEventListener('change', () => this.clearErrors());
45
+
46
+ console.log('JobStartForm initialized for', this.agentPath);
47
+ }
48
+
49
+ async handleSubmit(event) {
50
+ event.preventDefault();
51
+
52
+ // Clear previous errors
53
+ this.clearErrors();
54
+
55
+ // Validate both agent parameters and method fields
56
+ const [agentParamsValid, methodFieldsValid] = await Promise.all([
57
+ this.validateAgentParameters(),
58
+ this.validateMethodFields()
59
+ ]);
60
+
61
+ if (agentParamsValid && methodFieldsValid) {
62
+ // All validation passed, submit the form
63
+ this.submitForm();
64
+ }
65
+ }
66
+
67
+ async validateAgentParameters() {
68
+ const encryptedParams = this.getEncryptedAgentParameters();
69
+
70
+ if (!encryptedParams) {
71
+ return true; // No agent parameters to validate
72
+ }
73
+
74
+ try {
75
+ const response = await fetch(
76
+ `${this.agentPath}/validate-agent-parameters`,
77
+ {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'X-API-Key': this.getApiKey()
82
+ },
83
+ body: JSON.stringify({
84
+ encrypted_agent_parameters: encryptedParams,
85
+ }),
86
+ }
87
+ );
88
+
89
+ const result = await response.json();
90
+
91
+ if (!result.valid) {
92
+ this.displayAgentParameterErrors(result);
93
+ return false;
94
+ }
95
+
96
+ return true;
97
+ } catch (error) {
98
+ console.error('Agent parameter validation failed:', error);
99
+ this.displayError('Agent parameter validation failed due to network error');
100
+ return false;
101
+ }
102
+ }
103
+
104
+ async validateMethodFields() {
105
+ const formData = this.getFormData();
106
+
107
+ try {
108
+ const response = await fetch(
109
+ `${this.agentPath}/validate-method-fields`,
110
+ {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ 'X-API-Key': this.getApiKey()
115
+ },
116
+ body: JSON.stringify({
117
+ method_name: 'job_start',
118
+ job_fields: formData,
119
+ }),
120
+ }
121
+ );
122
+
123
+ const result = await response.json();
124
+
125
+ if (!result.valid) {
126
+ this.displayMethodFieldErrors(result);
127
+ return false;
128
+ }
129
+
130
+ return true;
131
+ } catch (error) {
132
+ console.error('Method field validation failed:', error);
133
+ this.displayError('Method field validation failed due to network error');
134
+ return false;
135
+ }
136
+ }
137
+
138
+ getFormData() {
139
+ const formData = {};
140
+ const formElements = this.form.elements;
141
+
142
+ for (let element of formElements) {
143
+ if (element.name && element.type !== 'submit') {
144
+ if (element.type === 'checkbox') {
145
+ formData[element.name] = element.checked;
146
+ } else if (element.type === 'radio') {
147
+ if (element.checked) {
148
+ formData[element.name] = element.value;
149
+ }
150
+ } else if (element.type === 'select-multiple') {
151
+ formData[element.name] = Array.from(element.selectedOptions).map(option => option.value);
152
+ } else {
153
+ formData[element.name] = element.value;
154
+ }
155
+ }
156
+ }
157
+
158
+ return formData;
159
+ }
160
+
161
+ getEncryptedAgentParameters() {
162
+ // Look for encrypted agent parameters in hidden fields or data attributes
163
+ const encryptedField = this.form.querySelector('[name="encrypted_agent_parameters"]');
164
+ if (encryptedField) {
165
+ return encryptedField.value;
166
+ }
167
+
168
+ // Check if agent parameters are stored in data attributes
169
+ const agentParamsData = this.form.dataset.agentParameters;
170
+ if (agentParamsData) {
171
+ try {
172
+ return JSON.parse(agentParamsData);
173
+ } catch (e) {
174
+ console.warn('Failed to parse agent parameters from data attribute');
175
+ }
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ getApiKey() {
182
+ // Get API key from form data, meta tag, or global variable
183
+ const apiKeyField = this.form.querySelector('[name="api_key"]');
184
+ if (apiKeyField) {
185
+ return apiKeyField.value;
186
+ }
187
+
188
+ const metaApiKey = document.querySelector('meta[name="api-key"]');
189
+ if (metaApiKey) {
190
+ return metaApiKey.content;
191
+ }
192
+
193
+ // Check for global variable
194
+ if (typeof window.SUPERVAIZER_API_KEY !== 'undefined') {
195
+ return window.SUPERVAIZER_API_KEY;
196
+ }
197
+
198
+ return '';
199
+ }
200
+
201
+ displayAgentParameterErrors(validationResult) {
202
+ const errorSection = document.getElementById('agent-parameter-errors');
203
+ if (!errorSection) {
204
+ // Create error section if it doesn't exist
205
+ const section = document.createElement('div');
206
+ section.id = 'agent-parameter-errors';
207
+ section.className = 'mb-4';
208
+ this.errorContainer.appendChild(section);
209
+ }
210
+
211
+ const section = document.getElementById('agent-parameter-errors');
212
+ section.innerHTML = `
213
+ <div class="alert alert-warning">
214
+ <strong>Agent Configuration Issues:</strong> ${validationResult.message
215
+ }
216
+ <ul>
217
+ ${validationResult.errors
218
+ .map((error) => `<li>${error}</li>`)
219
+ .join('')}
220
+ </ul>
221
+ </div>
222
+ `;
223
+ }
224
+
225
+ displayMethodFieldErrors(validationResult) {
226
+ // Clear any existing field-specific errors
227
+ this.form.querySelectorAll('.is-invalid').forEach((field) => {
228
+ field.classList.remove('is-invalid');
229
+ });
230
+
231
+ // Display general error message
232
+ this.displayError(validationResult.message);
233
+
234
+ // Mark invalid fields and show specific error messages
235
+ Object.entries(validationResult.invalid_fields).forEach(([fieldName, errorMessage]) => {
236
+ const field = this.form.querySelector(`[name="${fieldName}"]`);
237
+ if (field) {
238
+ field.classList.add('is-invalid');
239
+
240
+ // Add error message below the field
241
+ let errorElement = field.parentNode.querySelector('.invalid-feedback');
242
+ if (!errorElement) {
243
+ errorElement = document.createElement('div');
244
+ errorElement.className = 'invalid-feedback';
245
+ field.parentNode.appendChild(errorElement);
246
+ }
247
+ errorElement.textContent = errorMessage;
248
+ }
249
+ });
250
+ }
251
+
252
+ displayError(message) {
253
+ this.errorContainer.innerHTML = `
254
+ <div class="alert alert-danger">
255
+ <strong>Validation Error:</strong> ${message}
256
+ </div>
257
+ `;
258
+ }
259
+
260
+ clearErrors() {
261
+ this.errorContainer.innerHTML = '';
262
+
263
+ // Clear agent parameter errors
264
+ const agentErrorSection = document.getElementById('agent-parameter-errors');
265
+ if (agentErrorSection) {
266
+ agentErrorSection.innerHTML = '';
267
+ }
268
+
269
+ // Clear field errors
270
+ this.form.querySelectorAll('.is-invalid').forEach((field) => {
271
+ field.classList.remove('is-invalid');
272
+ });
273
+
274
+ // Clear invalid feedback messages
275
+ this.form.querySelectorAll('.invalid-feedback').forEach((feedback) => {
276
+ feedback.remove();
277
+ });
278
+ }
279
+
280
+ async submitForm() {
281
+ try {
282
+ const formData = this.getFormData();
283
+ const encryptedParams = this.getEncryptedAgentParameters();
284
+
285
+ const requestBody = {
286
+ job_context: {
287
+ // Add any job context data here
288
+ },
289
+ job_fields: formData
290
+ };
291
+
292
+ if (encryptedParams) {
293
+ requestBody.encrypted_agent_parameters = encryptedParams;
294
+ }
295
+
296
+ const response = await fetch(`${this.agentPath}/jobs`, {
297
+ method: 'POST',
298
+ headers: {
299
+ 'Content-Type': 'application/json',
300
+ 'X-API-Key': this.getApiKey()
301
+ },
302
+ body: JSON.stringify(requestBody)
303
+ });
304
+
305
+ if (response.ok) {
306
+ const result = await response.json();
307
+ this.handleJobStarted(result);
308
+ } else {
309
+ const error = await response.json();
310
+ this.handleJobError(error);
311
+ }
312
+ } catch (error) {
313
+ console.error('Job submission failed:', error);
314
+ this.displayError('Failed to submit job due to network error');
315
+ }
316
+ }
317
+
318
+ handleJobStarted(jobData) {
319
+ // Clear form and show success message
320
+ this.form.reset();
321
+ this.clearErrors();
322
+
323
+ this.errorContainer.innerHTML = `
324
+ <div class="alert alert-success">
325
+ <strong>Job Started Successfully!</strong><br>
326
+ Job ID: ${jobData.id || 'Unknown'}<br>
327
+ Status: ${jobData.status || 'Unknown'}
328
+ </div>
329
+ `;
330
+
331
+ // Trigger any success callbacks
332
+ if (typeof this.onJobStarted === 'function') {
333
+ this.onJobStarted(jobData);
334
+ }
335
+ }
336
+
337
+ handleJobError(error) {
338
+ this.displayError(`Job submission failed: ${error.detail || error.message || 'Unknown error'}`);
339
+
340
+ // Trigger any error callbacks
341
+ if (typeof this.onJobError === 'function') {
342
+ this.onJobError(error);
343
+ }
344
+ }
345
+
346
+ // Public methods for external use
347
+ setOnJobStarted(callback) {
348
+ this.onJobStarted = callback;
349
+ }
350
+
351
+ setOnJobError(callback) {
352
+ this.onJobError = callback;
353
+ }
354
+
355
+ // Method to manually trigger validation
356
+ async validate() {
357
+ const [agentParamsValid, methodFieldsValid] = await Promise.all([
358
+ this.validateAgentParameters(),
359
+ this.validateMethodFields()
360
+ ]);
361
+ return agentParamsValid && methodFieldsValid;
362
+ }
363
+ }
364
+
365
+ // Export for module systems
366
+ if (typeof module !== 'undefined' && module.exports) {
367
+ module.exports = JobStartForm;
368
+ }
369
+
370
+ // Make available globally
371
+ if (typeof window !== 'undefined') {
372
+ window.JobStartForm = JobStartForm;
373
+ }
@@ -0,0 +1,109 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="max-w-4xl mx-auto">
5
+ <div class="bg-white shadow rounded-lg p-6">
6
+ <h1 class="text-2xl font-bold text-gray-900 mb-6">Job Start Form Test</h1>
7
+
8
+ <!-- Validation Errors Container -->
9
+ <div id="validation-errors" class="mb-6"></div>
10
+
11
+ <!-- Agent Parameter Errors Container -->
12
+ <div id="agent-parameter-errors" class="mb-6"></div>
13
+
14
+ <!-- Job Start Form -->
15
+ <form data-job-start method="POST" class="space-y-6">
16
+ <!-- Job Fields -->
17
+ <div class="space-y-4">
18
+ <h3 class="text-lg font-medium text-gray-900">Job Parameters</h3>
19
+
20
+ <div>
21
+ <label for="company_name" class="block text-sm font-medium text-gray-700">
22
+ Company Name *
23
+ </label>
24
+ <input
25
+ type="text"
26
+ id="company_name"
27
+ name="company_name"
28
+ required
29
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
30
+ placeholder="Enter company name"
31
+ >
32
+ </div>
33
+
34
+ <div>
35
+ <label for="max_results" class="block text-sm font-medium text-gray-700">
36
+ Max Results *
37
+ </label>
38
+ <input
39
+ type="number"
40
+ id="max_results"
41
+ name="max_results"
42
+ required
43
+ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
44
+ placeholder="Enter maximum results"
45
+ >
46
+ </div>
47
+
48
+ <div>
49
+ <label for="subscribe_updates" class="block text-sm font-medium text-gray-700">
50
+ Subscribe to Updates
51
+ </label>
52
+ <input
53
+ type="checkbox"
54
+ id="subscribe_updates"
55
+ name="subscribe_updates"
56
+ class="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
57
+ >
58
+ <span class="ml-2 text-sm text-gray-600">Receive email updates about this job</span>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Hidden Fields for Testing -->
63
+ <input type="hidden" name="encrypted_agent_parameters" value="test_encrypted_params">
64
+ <input type="hidden" name="api_key" value="{{ api_key }}">
65
+
66
+ <!-- Submit Button -->
67
+ <div class="flex justify-end">
68
+ <button
69
+ type="submit"
70
+ class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
71
+ >
72
+ Start Job
73
+ </button>
74
+ </div>
75
+ </form>
76
+ </div>
77
+ </div>
78
+
79
+ <script>
80
+ // Initialize JobStartForm when the page loads
81
+ document.addEventListener('DOMContentLoaded', function() {
82
+ // Wait for JobStartForm class to be available
83
+ function waitForJobStartForm() {
84
+ if (typeof window.JobStartForm !== 'undefined') {
85
+ console.log('JobStartForm class found, initializing...');
86
+
87
+ // Initialize the form with the current agent path
88
+ const form = new JobStartForm('/test-agent');
89
+
90
+ // Set up callbacks
91
+ form.setOnJobStarted(function(jobData) {
92
+ console.log('Job started successfully:', jobData);
93
+ });
94
+
95
+ form.setOnJobError(function(error) {
96
+ console.error('Job failed:', error);
97
+ });
98
+
99
+ console.log('JobStartForm initialized successfully');
100
+ } else {
101
+ console.log('JobStartForm class not ready yet, waiting...');
102
+ setTimeout(waitForJobStartForm, 100);
103
+ }
104
+ }
105
+
106
+ waitForJobStartForm();
107
+ });
108
+ </script>
109
+ {% endblock %}
@@ -83,8 +83,7 @@ class AgentMethodField(BaseModel):
83
83
  description: str | None = Field(
84
84
  default=None, description="Description of the field - displayed in the UI"
85
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(
86
+ choices: list[tuple[str, str]] | list[str] | None = Field(
88
87
  default=None, description="For choice fields, list of [value, label] pairs"
89
88
  )
90
89
 
@@ -244,32 +243,151 @@ class AgentMethod(AgentMethodAbstract):
244
243
  return type("EmptyFieldsModel", (BaseModel,), {"to_dict": lambda self: {}})
245
244
 
246
245
  field_annotations = {}
247
- field_defaults: Dict[str, None] = {}
248
246
  for field in self.fields:
249
247
  field_name = field.name
250
248
  field_type = field.type
251
- is_required = field.required
249
+
250
+ # Convert Python types to proper typing annotations
251
+ if field_type is str:
252
+ annotation_type: type = str
253
+ elif field_type is int:
254
+ annotation_type = int
255
+ elif field_type is bool:
256
+ annotation_type = bool
257
+ elif field_type is list:
258
+ annotation_type = list
259
+ elif field_type is dict:
260
+ annotation_type = dict
261
+ elif field_type is float:
262
+ annotation_type = float
263
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ is list:
264
+ # Handle generic list types like list[str]
265
+ annotation_type = list
266
+ elif hasattr(field_type, "__origin__") and field_type.__origin__ is dict:
267
+ # Handle generic dict types like dict[str, Any]
268
+ annotation_type = dict
269
+ else:
270
+ # Default to Any for unknown types
271
+ annotation_type = Any
272
+
273
+ # Make field optional if not required
252
274
  field_annotations[field_name] = (
253
- field_type if is_required else Optional[field_type]
275
+ annotation_type if field.required else Optional[annotation_type]
254
276
  )
255
- if not is_required:
256
- field_defaults[field_name] = None
257
277
 
258
- def to_dict(self: BaseModel) -> Dict[str, Any]:
278
+ # Create the dynamic model with proper module information
279
+ model_dict = {
280
+ "__module__": "supervaizer.agent",
281
+ "__annotations__": field_annotations,
282
+ "to_dict": lambda self: {
283
+ k: getattr(self, k)
284
+ for k in field_annotations.keys()
285
+ if hasattr(self, k)
286
+ },
287
+ }
288
+
289
+ return type("DynamicFieldsModel", (BaseModel,), model_dict)
290
+
291
+ def validate_method_fields(self, job_fields: Dict[str, Any]) -> Dict[str, Any]:
292
+ """Validate job fields against the method's field definitions.
293
+
294
+ Args:
295
+ job_fields: Dictionary of field names and values to validate
296
+
297
+ Returns:
298
+ Dictionary with validation results:
299
+ - "valid": bool - whether all fields are valid
300
+ - "errors": List[str] - list of validation error messages
301
+ - "invalid_fields": Dict[str, str] - field name to error message mapping
302
+ """
303
+ if self.fields is None:
304
+ return {
305
+ "valid": True,
306
+ "message": "Method has no field definitions",
307
+ "errors": [],
308
+ "invalid_fields": {},
309
+ }
310
+
311
+ if len(self.fields) == 0:
259
312
  return {
260
- field_name: getattr(self, field_name)
261
- for field_name in self.__annotations__
313
+ "valid": True,
314
+ "message": "Method fields validated successfully",
315
+ "errors": [],
316
+ "invalid_fields": {},
262
317
  }
263
318
 
264
- return type(
265
- "DynamicFieldsModel",
266
- (BaseModel,),
267
- {
268
- "__annotations__": field_annotations,
269
- "to_dict": to_dict,
270
- **field_defaults,
271
- },
272
- )
319
+ errors = []
320
+ invalid_fields = {}
321
+
322
+ # First check for missing required fields
323
+ for field in self.fields:
324
+ if field.required and field.name not in job_fields:
325
+ error_msg = f"Required field '{field.name}' is missing"
326
+ errors.append(error_msg)
327
+ invalid_fields[field.name] = error_msg
328
+
329
+ # Then validate the provided fields
330
+ for field_name, field_value in job_fields.items():
331
+ # Find the field definition
332
+ field_def = next((f for f in self.fields if f.name == field_name), None)
333
+ if not field_def:
334
+ error_msg = f"Unknown field '{field_name}'"
335
+ errors.append(error_msg)
336
+ invalid_fields[field_name] = error_msg
337
+ continue
338
+
339
+ # Skip validation for None values (optional fields)
340
+ if field_value is None:
341
+ continue
342
+
343
+ # Type validation
344
+ expected_type = field_def.type
345
+ if expected_type:
346
+ try:
347
+ # Handle special cases for type validation
348
+ if expected_type is str:
349
+ if not isinstance(field_value, str):
350
+ error_msg = f"Field '{field_name}' must be a string, got {type(field_value).__name__}"
351
+ errors.append(error_msg)
352
+ invalid_fields[field_name] = error_msg
353
+ elif expected_type is int:
354
+ if not isinstance(field_value, int):
355
+ error_msg = f"Field '{field_name}' must be an integer, got {type(field_value).__name__}"
356
+ errors.append(error_msg)
357
+ invalid_fields[field_name] = error_msg
358
+ elif expected_type is bool:
359
+ if not isinstance(field_value, bool):
360
+ error_msg = f"Field '{field_name}' must be a boolean, got {type(field_value).__name__}"
361
+ errors.append(error_msg)
362
+ invalid_fields[field_name] = error_msg
363
+ elif expected_type is list:
364
+ if not isinstance(field_value, list):
365
+ error_msg = f"Field '{field_name}' must be a list, got {type(field_value).__name__}"
366
+ errors.append(error_msg)
367
+ invalid_fields[field_name] = error_msg
368
+ elif expected_type is dict:
369
+ if not isinstance(field_value, dict):
370
+ error_msg = f"Field '{field_name}' must be a dictionary, got {type(field_value).__name__}"
371
+ errors.append(error_msg)
372
+ invalid_fields[field_name] = error_msg
373
+ elif expected_type is float:
374
+ if not isinstance(field_value, (int, float)):
375
+ error_msg = f"Field '{field_name}' must be a number, got {type(field_value).__name__}"
376
+ errors.append(error_msg)
377
+ invalid_fields[field_name] = error_msg
378
+ except Exception as e:
379
+ error_msg = f"Field '{field_name}' validation failed: {str(e)}"
380
+ errors.append(error_msg)
381
+ invalid_fields[field_name] = error_msg
382
+
383
+ return {
384
+ "valid": len(errors) == 0,
385
+ "message": "Method fields validated successfully"
386
+ if len(errors) == 0
387
+ else "Method field validation failed",
388
+ "errors": errors,
389
+ "invalid_fields": invalid_fields,
390
+ }
273
391
 
274
392
  @property
275
393
  def job_model(self) -> type[AgentJobContextBase]:
@@ -553,7 +671,7 @@ class Agent(AgentAbstract):
553
671
  """Returns registration info for the agent"""
554
672
  return {
555
673
  "name": self.name,
556
- "id": self.id,
674
+ "id": f"{self.id}",
557
675
  "author": self.author,
558
676
  "developer": self.developer,
559
677
  "maintainer": self.maintainer,
@@ -567,7 +685,7 @@ class Agent(AgentAbstract):
567
685
  "parameters_setup": self.parameters_setup.registration_info
568
686
  if self.parameters_setup
569
687
  else None,
570
- "server_agent_id": self.server_agent_id,
688
+ "server_agent_id": f"{self.server_agent_id}",
571
689
  "server_agent_status": self.server_agent_status,
572
690
  "server_agent_onboarding_status": self.server_agent_onboarding_status,
573
691
  "server_encrypted_parameters": self.server_encrypted_parameters,
@@ -6,7 +6,10 @@
6
6
 
7
7
  import os
8
8
  import shutil
9
+ import signal
10
+ import subprocess
9
11
  import sys
12
+ from typing import Any
10
13
  from pathlib import Path
11
14
  from typing import Optional
12
15
 
@@ -79,12 +82,20 @@ def start(
79
82
  console.print(f"[bold green]Starting Supervaizer Controller v{VERSION}[/]")
80
83
  console.print(f"Loading configuration from [bold]{script_path}[/]")
81
84
 
82
- # Execute the script
83
- with open(script_path, "r") as f:
84
- script_content = f.read()
85
+ # Execute the script in a new Python process with proper signal handling
85
86
 
86
- # Execute the script in the current global namespace
87
- exec(script_content, globals())
87
+ def signal_handler(signum: int, frame: Any) -> None:
88
+ # Send the signal to the subprocess
89
+ if "process" in globals():
90
+ globals()["process"].terminate()
91
+ sys.exit(0)
92
+
93
+ signal.signal(signal.SIGINT, signal_handler)
94
+ signal.signal(signal.SIGTERM, signal_handler)
95
+
96
+ process = subprocess.Popen([sys.executable, script_path])
97
+ globals()["process"] = process
98
+ process.wait()
88
99
 
89
100
 
90
101
  @app.command()
@@ -107,7 +118,7 @@ def scaffold(
107
118
 
108
119
  # Get the path to the examples directory
109
120
  examples_dir = Path(__file__).parent / "examples"
110
- example_file = examples_dir / "controller-template.py"
121
+ example_file = examples_dir / "controller_template.py"
111
122
 
112
123
  if not example_file.exists():
113
124
  console.print("[bold red]Error:[/] Example file not found")
@@ -175,10 +175,11 @@ agent: Agent = Agent(
175
175
  parameters_setup=agent_parameters,
176
176
  )
177
177
 
178
+ # For export purposes, use dummy values if environment variables are not set
178
179
  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
180
+ workspace_id=os.getenv("SUPERVAIZE_WORKSPACE_ID") or "dummy_workspace_id",
181
+ api_key=os.getenv("SUPERVAIZE_API_KEY") or "dummy_api_key",
182
+ api_url=os.getenv("SUPERVAIZE_API_URL") or "https://api.supervaize.com",
182
183
  )
183
184
 
184
185
  # Define the supervaizer server capabilities
@@ -171,3 +171,51 @@ class ParametersSetup(SvBaseModel):
171
171
  raise ValueError(message)
172
172
 
173
173
  return self
174
+
175
+ def validate_parameters(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
176
+ """Validate parameters against their expected types and return validation errors.
177
+
178
+ Args:
179
+ parameters: Dictionary of parameter names and values to validate
180
+
181
+ Returns:
182
+ Dictionary with validation results:
183
+ - "valid": bool - whether all parameters are valid
184
+ - "errors": List[str] - list of validation error messages
185
+ - "invalid_parameters": Dict[str, str] - parameter name to error message mapping
186
+ """
187
+ errors = []
188
+ invalid_parameters = {}
189
+
190
+ # First check for missing required parameters
191
+ for param_name, param_def in self.definitions.items():
192
+ if param_def.is_required and param_name not in parameters:
193
+ error_msg = f"Required parameter '{param_name}' is missing"
194
+ errors.append(error_msg)
195
+ invalid_parameters[param_name] = error_msg
196
+
197
+ # Then validate the provided parameters
198
+ for param_name, param_value in parameters.items():
199
+ if param_name not in self.definitions:
200
+ error_msg = f"Unknown parameter '{param_name}'"
201
+ errors.append(error_msg)
202
+ invalid_parameters[param_name] = error_msg
203
+ continue
204
+
205
+ param_def = self.definitions[param_name]
206
+
207
+ # Skip validation for None values (optional parameters)
208
+ if param_value is None:
209
+ continue
210
+
211
+ # Since Parameter values are always strings, validate that input parameters are strings
212
+ if not isinstance(param_value, str):
213
+ error_msg = f"Parameter '{param_name}' must be a string, got {type(param_value).__name__}"
214
+ errors.append(error_msg)
215
+ invalid_parameters[param_name] = error_msg
216
+
217
+ return {
218
+ "valid": len(errors) == 0,
219
+ "errors": errors,
220
+ "invalid_parameters": invalid_parameters,
221
+ }
@@ -383,6 +383,129 @@ def create_agent_route(server: "Server", agent: Agent) -> APIRouter:
383
383
  **agent.registration_info,
384
384
  )
385
385
 
386
+ @router.post(
387
+ "/validate-agent-parameters",
388
+ summary=f"Validate agent parameters for agent: {agent.name}",
389
+ description="Validate agent configuration parameters (secrets, API keys, etc.) before starting a job",
390
+ response_model=Dict[str, Any],
391
+ responses={
392
+ http_status.HTTP_200_OK: {"model": Dict[str, Any]},
393
+ http_status.HTTP_400_BAD_REQUEST: {"model": Dict[str, Any]},
394
+ http_status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponse},
395
+ },
396
+ dependencies=[Security(server.verify_api_key)],
397
+ )
398
+ @handle_route_errors()
399
+ async def validate_agent_parameters(
400
+ body_params: Any = Body(...),
401
+ agent: Agent = Depends(get_agent),
402
+ ) -> Dict[str, Any]:
403
+ """Validate agent parameters for this agent"""
404
+ log.info(
405
+ f"📥 POST /validate-agent-parameters [Validate agent parameters] {agent.name}"
406
+ )
407
+
408
+ if not agent.parameters_setup:
409
+ return {
410
+ "valid": True,
411
+ "message": "Agent has no parameter setup defined",
412
+ "errors": [],
413
+ "invalid_parameters": {},
414
+ }
415
+
416
+ encrypted_agent_parameters = body_params.get("encrypted_agent_parameters")
417
+
418
+ # Decrypt agent parameters if provided
419
+ agent_parameters: Dict[str, Any] = {}
420
+ if encrypted_agent_parameters:
421
+ try:
422
+ from supervaizer.common import decrypt_value
423
+ import json
424
+
425
+ agent_parameters_str = decrypt_value(
426
+ encrypted_agent_parameters, server.private_key
427
+ )
428
+ agent_parameters = (
429
+ json.loads(agent_parameters_str) if agent_parameters_str else {}
430
+ )
431
+ except Exception as e:
432
+ return {
433
+ "valid": False,
434
+ "message": f"Failed to decrypt agent parameters: {str(e)}",
435
+ "errors": [f"Decryption failed: {str(e)}"],
436
+ "invalid_parameters": {
437
+ "encrypted_agent_parameters": f"Decryption failed: {str(e)}"
438
+ },
439
+ }
440
+
441
+ # Validate agent parameters
442
+ validation_result = agent.parameters_setup.validate_parameters(agent_parameters)
443
+
444
+ return {
445
+ "valid": validation_result["valid"],
446
+ "message": "Agent parameters validated successfully"
447
+ if validation_result["valid"]
448
+ else "Agent parameter validation failed",
449
+ "errors": validation_result["errors"],
450
+ "invalid_parameters": validation_result["invalid_parameters"],
451
+ }
452
+
453
+ @router.post(
454
+ "/validate-method-fields",
455
+ summary=f"Validate method fields for agent: {agent.name}",
456
+ description="Validate job input fields against the method's field definitions before starting a job",
457
+ response_model=Dict[str, Any],
458
+ responses={
459
+ http_status.HTTP_200_OK: {"model": Dict[str, Any]},
460
+ http_status.HTTP_400_BAD_REQUEST: {"model": Dict[str, Any]},
461
+ http_status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponse},
462
+ },
463
+ dependencies=[Security(server.verify_api_key)],
464
+ )
465
+ @handle_route_errors()
466
+ async def validate_method_fields(
467
+ body_params: Any = Body(...),
468
+ agent: Agent = Depends(get_agent),
469
+ ) -> Dict[str, Any]:
470
+ """Validate method fields for this agent"""
471
+ log.info(
472
+ f"📥 POST /validate-method-fields [Validate method fields] {agent.name}"
473
+ )
474
+
475
+ method_name = body_params.get("method_name", "job_start")
476
+ job_fields = body_params.get("job_fields", {})
477
+
478
+ # Get the method to validate against
479
+ if not agent.methods:
480
+ return {
481
+ "valid": False,
482
+ "message": "Agent has no methods defined",
483
+ "errors": ["Agent has no methods defined"],
484
+ "invalid_fields": {},
485
+ }
486
+
487
+ if method_name == "job_start":
488
+ method = agent.methods.job_start
489
+ elif agent.methods.custom and method_name in agent.methods.custom:
490
+ method = agent.methods.custom[method_name]
491
+ else:
492
+ return {
493
+ "valid": False,
494
+ "message": f"Method '{method_name}' not found",
495
+ "errors": [f"Method '{method_name}' not found"],
496
+ "invalid_fields": {},
497
+ }
498
+
499
+ # Validate method fields
500
+ validation_result = method.validate_method_fields(job_fields)
501
+
502
+ return {
503
+ "valid": validation_result["valid"],
504
+ "message": validation_result["message"],
505
+ "errors": validation_result["errors"],
506
+ "invalid_fields": validation_result["invalid_fields"],
507
+ }
508
+
386
509
  if not agent.methods:
387
510
  raise ValueError(f"Agent {agent.name} has no methods defined")
388
511
 
@@ -400,6 +523,7 @@ def create_agent_route(server: "Server", agent: Agent) -> APIRouter:
400
523
  description=f"{agent.methods.job_start.description}",
401
524
  responses={
402
525
  http_status.HTTP_202_ACCEPTED: {"model": Job},
526
+ http_status.HTTP_400_BAD_REQUEST: {"model": Dict[str, Any]},
403
527
  http_status.HTTP_409_CONFLICT: {"model": ErrorResponse},
404
528
  http_status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponse},
405
529
  },
@@ -415,13 +539,12 @@ def create_agent_route(server: "Server", agent: Agent) -> APIRouter:
415
539
  ) -> Union[Job, JSONResponse]:
416
540
  """Start a new job for this agent"""
417
541
  log.info(f"📥 POST /jobs [Start job] {agent.name} with params {body_params}")
418
- sv_context: JobContext = body_params.job_context
419
- job_fields = body_params.job_fields.to_dict()
542
+
543
+ sv_context: JobContext = JobContext(**body_params["job_context"])
544
+ job_fields = body_params["job_fields"]
420
545
 
421
546
  # Get job encrypted parameters if available
422
- encrypted_agent_parameters = getattr(
423
- body_params, "encrypted_agent_parameters", None
424
- )
547
+ encrypted_agent_parameters = body_params.get("encrypted_agent_parameters")
425
548
 
426
549
  # Delegate job creation and scheduling to the service
427
550
  new_job = await service_job_start(
@@ -623,6 +746,7 @@ def create_agent_custom_routes(server: "Server", agent: Agent) -> APIRouter:
623
746
  response_model=JobResponse,
624
747
  responses={
625
748
  http_status.HTTP_202_ACCEPTED: {"model": JobResponse},
749
+ http_status.HTTP_400_BAD_REQUEST: {"model": Dict[str, Any]},
626
750
  http_status.HTTP_405_METHOD_NOT_ALLOWED: {"model": ErrorResponse},
627
751
  },
628
752
  dependencies=[Security(server.verify_api_key)],
@@ -637,6 +761,8 @@ def create_agent_custom_routes(server: "Server", agent: Agent) -> APIRouter:
637
761
  log.info(
638
762
  f"📥 POST /custom/{method_name} [custom job] {agent.name} with params {body_params}"
639
763
  )
764
+ log.info(f"body_params: {body_params}")
765
+
640
766
  sv_context: JobContext = body_params.job_context
641
767
  job_fields = body_params.job_fields.to_dict()
642
768
 
@@ -131,6 +131,8 @@ class ServerAbstract(SvBaseModel):
131
131
  The server can be configured with various endpoints (A2A, ACP, admin interface)
132
132
  and supports encryption/decryption of parameters using RSA keys.
133
133
 
134
+ Note that when the supervisor ccount is set, the A2A protocol is automatically activated to provide HEALTH CHECK endpoints.
135
+
134
136
  public_url: full url (including scheme and port) to use for outbound connections and registration.
135
137
  This is especially important in Docker environments where the binding
136
138
  address (0.0.0.0) can't be used for outbound connections. Set to
@@ -359,12 +361,15 @@ class Server(ServerAbstract):
359
361
 
360
362
  # Create routes
361
363
  if self.supervisor_account:
362
- log.info("[Server launch] 🚀 Deploy Supervaizer routes")
364
+ log.info(
365
+ "[Server launch] 🚀 Deploy Supervaizer routes - also activates A2A routes"
366
+ )
363
367
  self.app.include_router(create_default_routes(self))
364
368
  self.app.include_router(create_utils_routes(self))
365
369
  self.app.include_router(create_agents_routes(self))
370
+ self.a2a_endpoints = True # Needed by supervaize.
366
371
  if self.a2a_endpoints:
367
- log.info("[Server launch] 📢 Deploy A2A routes")
372
+ log.info("[Server launch] 📢 Deploy A2A routes ")
368
373
  self.app.include_router(create_a2a_routes(self))
369
374
  if self.acp_endpoints:
370
375
  log.info("[Server launch] 📢 Deploy ACP routes")
File without changes
File without changes
File without changes