supervaizer 0.9.7__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.
- {supervaizer-0.9.7 → supervaizer-0.9.8}/PKG-INFO +1 -1
- {supervaizer-0.9.7 → supervaizer-0.9.8}/pyproject.toml +1 -1
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/__version__.py +1 -1
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/routes.py +23 -0
- supervaizer-0.9.8/src/supervaizer/admin/static/js/job-start-form.js +373 -0
- supervaizer-0.9.8/src/supervaizer/admin/templates/job_start_test.html +109 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/agent.py +137 -19
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/cli.py +1 -1
- supervaizer-0.9.7/src/supervaizer/examples/controller-template.py → supervaizer-0.9.8/src/supervaizer/examples/controller_template.py +4 -3
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/parameter.py +48 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/routes.py +131 -5
- {supervaizer-0.9.7 → supervaizer-0.9.8}/.gitignore +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/LICENSE.md +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/README.md +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/__init__.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/account.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/account_service.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/agent_detail.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/agents.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/agents_grid.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/base.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/case_detail.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/cases_list.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/cases_table.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/console.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/dashboard.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/job_detail.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/jobs_list.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/jobs_table.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/navigation.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/recent_activity.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/server.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/server_status_cards.html +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/case.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/common.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/event.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/instructions.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/job.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/job_service.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/lifecycle.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/__init__.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/a2a/__init__.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/a2a/model.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/a2a/routes.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/acp/__init__.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/acp/model.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/protocol/acp/routes.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/py.typed +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/server.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/server_utils.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/storage.py +0 -0
- {supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/telemetry.py +0 -0
|
@@ -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"]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
313
|
+
"valid": True,
|
|
314
|
+
"message": "Method fields validated successfully",
|
|
315
|
+
"errors": [],
|
|
316
|
+
"invalid_fields": {},
|
|
262
317
|
}
|
|
263
318
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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]:
|
|
@@ -118,7 +118,7 @@ def scaffold(
|
|
|
118
118
|
|
|
119
119
|
# Get the path to the examples directory
|
|
120
120
|
examples_dir = Path(__file__).parent / "examples"
|
|
121
|
-
example_file = examples_dir / "
|
|
121
|
+
example_file = examples_dir / "controller_template.py"
|
|
122
122
|
|
|
123
123
|
if not example_file.exists():
|
|
124
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")
|
|
180
|
-
api_key=os.getenv("SUPERVAIZE_API_KEY")
|
|
181
|
-
api_url=os.getenv("SUPERVAIZE_API_URL")
|
|
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
|
-
|
|
419
|
-
|
|
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 =
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/recent_activity.html
RENAMED
|
File without changes
|
|
File without changes
|
{supervaizer-0.9.7 → supervaizer-0.9.8}/src/supervaizer/admin/templates/server_status_cards.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|