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.
Files changed (50) hide show
  1. supervaizer/__init__.py +88 -0
  2. supervaizer/__version__.py +10 -0
  3. supervaizer/account.py +304 -0
  4. supervaizer/account_service.py +87 -0
  5. supervaizer/admin/routes.py +1254 -0
  6. supervaizer/admin/templates/agent_detail.html +145 -0
  7. supervaizer/admin/templates/agents.html +175 -0
  8. supervaizer/admin/templates/agents_grid.html +80 -0
  9. supervaizer/admin/templates/base.html +233 -0
  10. supervaizer/admin/templates/case_detail.html +230 -0
  11. supervaizer/admin/templates/cases_list.html +182 -0
  12. supervaizer/admin/templates/cases_table.html +134 -0
  13. supervaizer/admin/templates/console.html +389 -0
  14. supervaizer/admin/templates/dashboard.html +153 -0
  15. supervaizer/admin/templates/job_detail.html +192 -0
  16. supervaizer/admin/templates/jobs_list.html +180 -0
  17. supervaizer/admin/templates/jobs_table.html +122 -0
  18. supervaizer/admin/templates/navigation.html +153 -0
  19. supervaizer/admin/templates/recent_activity.html +81 -0
  20. supervaizer/admin/templates/server.html +105 -0
  21. supervaizer/admin/templates/server_status_cards.html +121 -0
  22. supervaizer/agent.py +816 -0
  23. supervaizer/case.py +400 -0
  24. supervaizer/cli.py +135 -0
  25. supervaizer/common.py +283 -0
  26. supervaizer/event.py +181 -0
  27. supervaizer/examples/controller-template.py +195 -0
  28. supervaizer/instructions.py +145 -0
  29. supervaizer/job.py +379 -0
  30. supervaizer/job_service.py +155 -0
  31. supervaizer/lifecycle.py +417 -0
  32. supervaizer/parameter.py +173 -0
  33. supervaizer/protocol/__init__.py +11 -0
  34. supervaizer/protocol/a2a/__init__.py +21 -0
  35. supervaizer/protocol/a2a/model.py +227 -0
  36. supervaizer/protocol/a2a/routes.py +99 -0
  37. supervaizer/protocol/acp/__init__.py +21 -0
  38. supervaizer/protocol/acp/model.py +198 -0
  39. supervaizer/protocol/acp/routes.py +74 -0
  40. supervaizer/py.typed +1 -0
  41. supervaizer/routes.py +667 -0
  42. supervaizer/server.py +554 -0
  43. supervaizer/server_utils.py +54 -0
  44. supervaizer/storage.py +436 -0
  45. supervaizer/telemetry.py +81 -0
  46. supervaizer-0.9.6.dist-info/METADATA +245 -0
  47. supervaizer-0.9.6.dist-info/RECORD +50 -0
  48. supervaizer-0.9.6.dist-info/WHEEL +4 -0
  49. supervaizer-0.9.6.dist-info/entry_points.txt +2 -0
  50. supervaizer-0.9.6.dist-info/licenses/LICENSE.md +346 -0
supervaizer/server.py ADDED
@@ -0,0 +1,554 @@
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 os
8
+ import secrets
9
+ import sys
10
+ import time
11
+ import uuid
12
+ from datetime import datetime
13
+ from typing import Any, ClassVar, Dict, List, Optional, TypeVar
14
+ from urllib.parse import urlunparse
15
+
16
+ from cryptography.hazmat.backends import default_backend
17
+ from cryptography.hazmat.primitives import serialization
18
+ from cryptography.hazmat.primitives.asymmetric import rsa
19
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
20
+ from fastapi import FastAPI, HTTPException, Request, Security, status
21
+ from fastapi.exceptions import RequestValidationError
22
+ from fastapi.responses import JSONResponse
23
+ from fastapi.security import APIKeyHeader
24
+ from pydantic import BaseModel, field_validator, Field
25
+ from rich import inspect
26
+
27
+ from supervaizer.__version__ import API_VERSION, VERSION
28
+ from supervaizer.account import Account
29
+ from supervaizer.admin.routes import create_admin_routes
30
+ from supervaizer.agent import Agent
31
+ from supervaizer.common import (
32
+ ApiResult,
33
+ ApiSuccess,
34
+ SvBaseModel,
35
+ decrypt_value,
36
+ encrypt_value,
37
+ log,
38
+ )
39
+ from supervaizer.instructions import display_instructions
40
+ from supervaizer.protocol.a2a import create_routes as create_a2a_routes
41
+ from supervaizer.protocol.acp import create_routes as create_acp_routes
42
+ from supervaizer.routes import (
43
+ create_agents_routes,
44
+ create_default_routes,
45
+ create_utils_routes,
46
+ get_server,
47
+ )
48
+ from supervaizer.storage import StorageManager, load_running_entities_on_startup
49
+
50
+ insp = inspect
51
+
52
+ T = TypeVar("T")
53
+
54
+ # Additional imports for server persistence
55
+
56
+
57
+ class ServerInfo(BaseModel):
58
+ """Complete server information for storage."""
59
+
60
+ id: str = "server_instance" # Fixed ID for singleton
61
+ host: str
62
+ port: int
63
+ api_version: str
64
+ environment: str
65
+ agents: List[Dict[str, str]]
66
+ start_time: float
67
+ created_at: str
68
+ updated_at: str
69
+
70
+
71
+ def save_server_info_to_storage(server_instance: "Server") -> None:
72
+ """Save server information to storage."""
73
+ try:
74
+ storage = StorageManager()
75
+
76
+ # Get agent information
77
+ agents = []
78
+ if hasattr(server_instance, "agents") and server_instance.agents:
79
+ for agent in server_instance.agents:
80
+ agents.append(
81
+ {
82
+ "name": agent.name,
83
+ "description": agent.description,
84
+ "version": agent.version,
85
+ }
86
+ )
87
+
88
+ # Create server info
89
+ server_info = ServerInfo(
90
+ host=getattr(server_instance, "host", "0.0.0.0"),
91
+ port=getattr(server_instance, "port", 8000),
92
+ api_version=API_VERSION,
93
+ environment=os.getenv("SUPERVAIZER_ENVIRONMENT", "development"),
94
+ agents=agents,
95
+ start_time=time.time(),
96
+ created_at=datetime.now().isoformat(),
97
+ updated_at=datetime.now().isoformat(),
98
+ )
99
+
100
+ # Save to storage
101
+ storage.save_object("ServerInfo", server_info.model_dump())
102
+
103
+ log.info(
104
+ f"[Server] Server info saved to storage: {server_info.host}:{server_info.port}"
105
+ )
106
+
107
+ except Exception as e:
108
+ log.error(f"[Server] Failed to save server info to storage: {e}")
109
+
110
+
111
+ def get_server_info_from_storage() -> Optional[ServerInfo]:
112
+ """Get server information from storage."""
113
+ storage = StorageManager()
114
+ server_data = storage.get_object_by_id("ServerInfo", "server_instance")
115
+
116
+ if server_data:
117
+ return ServerInfo.model_validate(server_data)
118
+ return None
119
+
120
+
121
+ class ServerAbstract(SvBaseModel):
122
+ """
123
+ API Server for the Supervaize Controller.
124
+
125
+ The server is a FastAPI application (see https://fastapi.tiangolo.com/ for details and advanced parameters)
126
+
127
+ This represents the main server instance that manages agents and provides
128
+ the API endpoints for the Supervaize Control API. It handles agent registration,
129
+ job execution, and communication with the Supervaize platform.
130
+
131
+ The server can be configured with various endpoints (A2A, ACP, admin interface)
132
+ and supports encryption/decryption of parameters using RSA keys.
133
+
134
+ public_url: full url (including scheme and port) to use for outbound connections and registration.
135
+ This is especially important in Docker environments where the binding
136
+ address (0.0.0.0) can't be used for outbound connections. Set to
137
+ 'host.docker.internal' for Docker or the appropriate service name
138
+ in container environments.
139
+ Examples:
140
+ - In Docker, set to 'http://host.docker.internal' to reach the host machine
141
+ - In Kubernetes, might be set to the service name or external DNS
142
+ If not provided, falls back to using the listening host.
143
+
144
+ """
145
+
146
+ supervaizer_VERSION: ClassVar[str] = VERSION
147
+ scheme: str = Field(description="URL scheme (http or https)")
148
+ host: str = Field(
149
+ description="Host to bind the server to (e.g., 0.0.0.0 for all interfaces)"
150
+ )
151
+ port: int = Field(description="Port to bind the server to")
152
+ environment: str = Field(description="Environment name (e.g., dev, staging, prod)")
153
+ mac_addr: str = Field(description="MAC address to use for server identification")
154
+ debug: bool = Field(description="Whether to enable debug mode")
155
+ agents: List[Agent] = Field(
156
+ description="List of agents to register with the server"
157
+ )
158
+ app: FastAPI = Field(description="FastAPI application instance")
159
+ reload: bool = Field(description="Whether to enable auto-reload")
160
+ supervisor_account: Optional[Account] = Field(
161
+ default=None,
162
+ description="Account of the supervisor - can be created at supervaize.com",
163
+ )
164
+ a2a_endpoints: bool = Field(
165
+ default=True, description="Whether to enable A2A endpoints"
166
+ )
167
+ acp_endpoints: bool = Field(
168
+ default=True, description="Whether to enable ACP endpoints"
169
+ )
170
+ private_key: RSAPrivateKey = Field(
171
+ description="RSA private key for secret parameters encryption - Used in server-to-agent communication - Not needed by user"
172
+ )
173
+ public_key: RSAPublicKey = Field(
174
+ description="RSA public key for secret parameters encryption - Used in agent-to-server communication - Not needed by user"
175
+ )
176
+ public_url: Optional[str] = Field(
177
+ default=None,
178
+ description="Public including scheme and port to use for inbound connections",
179
+ )
180
+ api_key: Optional[str] = Field(
181
+ default=None,
182
+ description="Force the API key to access the supervaizer endpoints - if not provided, a random key will be generated",
183
+ )
184
+ api_key_header: Optional[APIKeyHeader] = Field(
185
+ default=None, description="API key header for authentication"
186
+ )
187
+
188
+ model_config = {
189
+ "reference_group": "Core",
190
+ "arbitrary_types_allowed": True, # for FastAPI
191
+ "json_schema_extra": {
192
+ "examples": [
193
+ {
194
+ "agents": "[agent]",
195
+ "a2a_enabled": True,
196
+ "supervisor_account": None,
197
+ },
198
+ {
199
+ "scheme": "http",
200
+ "host": "0.0.0.0",
201
+ "port": 8000,
202
+ "environment": "dev",
203
+ "mac_addr": "00-11-22-33-44-55",
204
+ "debug": False,
205
+ "reload": False,
206
+ "a2a_endpoints": True,
207
+ "acp_endpoints": True,
208
+ },
209
+ ]
210
+ },
211
+ }
212
+
213
+ @field_validator("scheme")
214
+ def scheme_validator(cls, v: str) -> str:
215
+ if "://" in v:
216
+ raise ValueError(f"Scheme should not include '://': {v}")
217
+ return v
218
+
219
+ @field_validator("host")
220
+ def host_validator(cls, v: str) -> str:
221
+ if "://" in v:
222
+ raise ValueError(f"Host should not include '://': {v}")
223
+ return v
224
+
225
+ def get_agent_by_name(self, agent_name: str) -> Optional[Agent]:
226
+ for agent in self.agents:
227
+ if agent.name == agent_name:
228
+ return agent
229
+ return None
230
+
231
+
232
+ class Server(ServerAbstract):
233
+ def __init__(
234
+ self,
235
+ agents: List[Agent],
236
+ supervisor_account: Optional[Account] = None,
237
+ a2a_endpoints: bool = True,
238
+ acp_endpoints: bool = True,
239
+ admin_interface: bool = True,
240
+ scheme: str = "http",
241
+ environment: str = os.getenv("SUPERVAIZER_ENVIRONMENT", "dev"),
242
+ host: str = os.getenv("SUPERVAIZER_HOST", "0.0.0.0"),
243
+ port: int = int(os.getenv("SUPERVAIZER_PORT", 8000)),
244
+ debug: bool = False,
245
+ reload: bool = False,
246
+ mac_addr: str = "",
247
+ private_key: Optional[RSAPrivateKey] = None,
248
+ public_url: Optional[str] = os.getenv("SUPERVAIZER_PUBLIC_URL", None),
249
+ api_key: Optional[str] = os.getenv(
250
+ "SUPERVAIZER_API_KEY", secrets.token_urlsafe(32)
251
+ ),
252
+ **kwargs: Any,
253
+ ) -> None:
254
+ """Initialize the server with the given configuration.
255
+
256
+ Args:
257
+ agents: List of agents to register with the server
258
+ supervisor_account: Account of the supervisor
259
+ a2a_endpoints: Whether to enable A2A endpoints
260
+ acp_endpoints: Whether to enable ACP endpoints
261
+ admin_interface: Whether to enable admin interface
262
+ scheme: URL scheme (http or https)
263
+ environment: Environment name (e.g., dev, staging, prod)
264
+ host: Host to bind the server to (e.g., 0.0.0.0 for all interfaces)
265
+ port: Port to bind the server to
266
+ debug: Whether to enable debug mode
267
+ reload: Whether to enable auto-reload
268
+ mac_addr: MAC address to use for server identification
269
+ private_key: RSA private key for encryption
270
+
271
+ api_key: API key for securing endpoints
272
+
273
+ """
274
+ if not mac_addr:
275
+ node_id = uuid.getnode()
276
+ mac_addr = "-".join(
277
+ format(node_id, "012X")[i : i + 2] for i in range(0, 12, 2)
278
+ )
279
+
280
+ if private_key is None:
281
+ private_key = rsa.generate_private_key(
282
+ public_exponent=65537,
283
+ key_size=2048,
284
+ backend=default_backend(),
285
+ )
286
+
287
+ public_key = private_key.public_key()
288
+
289
+ # Create root app to handle version prefix
290
+ docs_url = "/docs" # Swagger UI
291
+ redoc_url = "/redoc" # ReDoc
292
+ openapi_url = "/openapi.json"
293
+
294
+ app = FastAPI(
295
+ debug=debug,
296
+ title="Supervaizer API",
297
+ description=(
298
+ f"API version: {API_VERSION} Controller version: {VERSION}\n\n"
299
+ "API for controlling and managing Supervaize agents. \n\nMore information at "
300
+ "[https://doc.supervaize.com](https://doc.supervaize.com)\n\n"
301
+ "## Authentication\n\n"
302
+ "Some endpoints require API key authentication. Protected endpoints expect "
303
+ "the API key in the X-API-Key header.\n\n"
304
+ f"[Swagger]({docs_url})\n"
305
+ f"[Redoc]({redoc_url})\n"
306
+ f"[OpenAPI]({openapi_url})\n"
307
+ ),
308
+ version=API_VERSION,
309
+ terms_of_service="https://supervaize.com/terms/",
310
+ contact={
311
+ "name": "Support Team",
312
+ "url": "https://supervaize.com/dev_contact/",
313
+ "email": "integration_support@supervaize.com",
314
+ },
315
+ license_info={
316
+ "name": "Mozilla Public License 2.0",
317
+ "url": "https://mozilla.org/MPL/2.0/",
318
+ },
319
+ docs_url=docs_url,
320
+ redoc_url=redoc_url,
321
+ openapi_url=openapi_url,
322
+ )
323
+
324
+ # Add exception handler for 422 validation errors
325
+ @app.exception_handler(RequestValidationError)
326
+ async def validation_exception_handler(
327
+ request: Request, exc: RequestValidationError
328
+ ) -> JSONResponse:
329
+ log.error(f"[422 Error] {exc.errors()}")
330
+ return JSONResponse(
331
+ status_code=422,
332
+ content={"detail": exc.errors(), "body": exc.body},
333
+ )
334
+
335
+ # Create API key header security
336
+ API_KEY_NAME = "X-API-Key"
337
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
338
+
339
+ super().__init__(
340
+ scheme=scheme,
341
+ host=host,
342
+ port=port,
343
+ environment=environment,
344
+ mac_addr=mac_addr,
345
+ debug=debug,
346
+ agents=agents,
347
+ app=app,
348
+ reload=reload,
349
+ supervisor_account=supervisor_account,
350
+ a2a_endpoints=a2a_endpoints,
351
+ acp_endpoints=acp_endpoints,
352
+ private_key=private_key,
353
+ public_key=public_key,
354
+ public_url=public_url,
355
+ api_key=api_key,
356
+ api_key_header=api_key_header,
357
+ **kwargs,
358
+ )
359
+
360
+ # Create routes
361
+ if self.supervisor_account:
362
+ log.info("[Server launch] 🚀 Deploy Supervaizer routes")
363
+ self.app.include_router(create_default_routes(self))
364
+ self.app.include_router(create_utils_routes(self))
365
+ self.app.include_router(create_agents_routes(self))
366
+ if self.a2a_endpoints:
367
+ log.info("[Server launch] 📢 Deploy A2A routes")
368
+ self.app.include_router(create_a2a_routes(self))
369
+ if self.acp_endpoints:
370
+ log.info("[Server launch] 📢 Deploy ACP routes")
371
+ self.app.include_router(create_acp_routes(self))
372
+
373
+ # Deploy admin routes if API key is available
374
+ if self.api_key and admin_interface:
375
+ log.info(
376
+ f"[Server launch] 💼 Deploy Admin interface @ {self.public_url}/admin"
377
+ )
378
+ self.app.include_router(create_admin_routes(), prefix="/admin")
379
+
380
+ # Save server info to storage for admin interface
381
+ save_server_info_to_storage(self)
382
+
383
+ # Load running entities from storage into memory
384
+ try:
385
+ load_running_entities_on_startup()
386
+ except Exception as e:
387
+ log.error(f"[Server launch] Failed to load running entities: {e}")
388
+ raise
389
+
390
+ # Override the get_server dependency to return this instance
391
+ async def get_current_server() -> "Server":
392
+ return self
393
+
394
+ # Update the dependency
395
+ self.app.dependency_overrides[get_server] = get_current_server
396
+
397
+ if api_key:
398
+ log.info("[Server launch] API Key authentication enabled")
399
+ # Print the API key if it was generated
400
+ if os.getenv("SUPERVAIZER_API_KEY") is None:
401
+ log.warning(f"[Server launch] Using auto-generated API key: {api_key}")
402
+ else:
403
+ log.info("[Server launch] API Key authentication disabled")
404
+
405
+ if not self.public_url:
406
+ self.public_url = f"{self.scheme}://{self.host}:{self.port}"
407
+
408
+ async def verify_api_key(
409
+ self, api_key: str = Security(APIKeyHeader(name="X-API-Key"))
410
+ ) -> bool:
411
+ """Verify that the API key is valid.
412
+
413
+ Args:
414
+ api_key: The API key from the request header
415
+
416
+ Returns:
417
+ True if the API key is valid
418
+
419
+ Raises:
420
+ HTTPException: If the API key is invalid or not provided when required
421
+ """
422
+ if self.api_key is None:
423
+ # API key authentication is disabled
424
+ return True
425
+
426
+ if api_key != self.api_key:
427
+ raise HTTPException(
428
+ status_code=status.HTTP_403_FORBIDDEN,
429
+ detail="Invalid API key",
430
+ headers={"WWW-Authenticate": "APIKey"},
431
+ )
432
+
433
+ return True
434
+
435
+ @property
436
+ def url(self) -> str:
437
+ """Get the server's local URL."""
438
+ return urlunparse((self.scheme, f"{self.host}:{self.port}", "", "", "", ""))
439
+
440
+ @property
441
+ def uri(self) -> str:
442
+ """Get the server's URI."""
443
+ return f"server:{self.mac_addr}"
444
+
445
+ @property
446
+ def registration_info(self) -> Dict[str, Any]:
447
+ """Get registration info for the server."""
448
+ assert self.public_key is not None, "Public key not initialized"
449
+ return {
450
+ "url": self.public_url,
451
+ "uri": self.uri,
452
+ "api_version": API_VERSION,
453
+ "environment": self.environment,
454
+ "public_key": str(
455
+ self.public_key.public_bytes(
456
+ encoding=serialization.Encoding.PEM,
457
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
458
+ ).decode("utf-8")
459
+ ),
460
+ "api_key": self.api_key,
461
+ "docs": {
462
+ "swagger": f"{self.public_url}{self.app.docs_url}",
463
+ "redoc": f"{self.public_url}{self.app.redoc_url}",
464
+ "openapi": f"{self.public_url}{self.app.openapi_url}",
465
+ },
466
+ "agents": [agent.registration_info for agent in self.agents],
467
+ }
468
+
469
+ def launch(self, log_level: Optional[str] = "INFO") -> None:
470
+ if log_level:
471
+ log.remove()
472
+ log.add(
473
+ sys.stderr,
474
+ colorize=True,
475
+ format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>|<level> {level}</level> | <level>{message}</level>",
476
+ level=log_level,
477
+ )
478
+
479
+ # Add log handler for admin streaming if API key is enabled
480
+ if self.api_key:
481
+
482
+ def log_queue_handler(message: Any) -> None:
483
+ record = message.record
484
+ try:
485
+ # Import here to avoid circular imports and ensure module is loaded
486
+ import supervaizer.admin.routes as admin_routes
487
+
488
+ admin_routes.add_log_to_queue(
489
+ timestamp=record["time"].isoformat(),
490
+ level=record["level"].name,
491
+ message=record["message"],
492
+ )
493
+ except ImportError:
494
+ # Silently ignore import errors to avoid breaking logging
495
+ pass
496
+ except Exception:
497
+ # Silently ignore other errors to avoid breaking logging
498
+ pass
499
+
500
+ # Add the handler with a specific format to avoid recursion
501
+ log.add(log_queue_handler, level=log_level, format="{message}")
502
+
503
+ log_level = (
504
+ log_level.lower()
505
+ ) # needs to be lower case of uvicorn and uppercase of loguru
506
+
507
+ log.info(
508
+ f"[Server launch] Starting Supervaize Controller API v{VERSION} - Log : {log_level} "
509
+ )
510
+
511
+ # self.instructions()
512
+ if self.supervisor_account:
513
+ # Register the server with the supervisor account
514
+ server_registration_result: ApiResult = (
515
+ self.supervisor_account.register_server(server=self)
516
+ )
517
+ assert isinstance(
518
+ server_registration_result, ApiSuccess
519
+ ) # If ApiError, exception should have been raised before
520
+ # Get the agent details from the server
521
+ for agent in self.agents:
522
+ updated_agent = agent.update_agent_from_server(self)
523
+ if updated_agent:
524
+ log.info(f"[Server launch] Updated agent {updated_agent.name}")
525
+
526
+ import uvicorn
527
+
528
+ uvicorn.run(
529
+ self.app,
530
+ host=self.host,
531
+ port=self.port,
532
+ reload=self.reload,
533
+ log_level=log_level,
534
+ )
535
+
536
+ def instructions(self) -> None:
537
+ server_url = f"http://{self.host}:{self.port}"
538
+ display_instructions(
539
+ server_url, f"Starting server on {server_url} \n Waiting for instructions.."
540
+ )
541
+
542
+ def decrypt(self, encrypted_parameters: str) -> str:
543
+ """Decrypt parameters using the server's private key."""
544
+ result = decrypt_value(encrypted_parameters, self.private_key)
545
+ if result is None:
546
+ raise ValueError("Failed to decrypt parameters")
547
+ return result
548
+
549
+ def encrypt(self, parameters: str) -> str:
550
+ """Encrypt parameters using the server's public key."""
551
+ result = encrypt_value(parameters, self.public_key)
552
+ if result is None:
553
+ raise ValueError("Failed to encrypt parameters")
554
+ return result
@@ -0,0 +1,54 @@
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 datetime import datetime
9
+ from enum import Enum
10
+
11
+ from fastapi.encoders import jsonable_encoder
12
+ from fastapi.responses import JSONResponse
13
+ from loguru import logger as log
14
+ from pydantic import BaseModel
15
+
16
+
17
+ class ErrorType(str, Enum):
18
+ """Enumeration of possible error types"""
19
+
20
+ JOB_NOT_FOUND = "job_not_found"
21
+ JOB_ALREADY_EXISTS = "job_already_exists"
22
+ AGENT_NOT_FOUND = "agent_not_found"
23
+ INVALID_REQUEST = "invalid_request"
24
+ INTERNAL_ERROR = "internal_error"
25
+ INVALID_PARAMETERS = "invalid_parameters"
26
+
27
+
28
+ class ErrorResponse(BaseModel):
29
+ """Standard error response model"""
30
+
31
+ error: str
32
+ error_type: ErrorType
33
+ detail: str | None = None
34
+ timestamp: datetime = datetime.now()
35
+ status_code: int
36
+
37
+
38
+ def create_error_response(
39
+ error_type: ErrorType, detail: str, status_code: int, traceback: str | None = None
40
+ ) -> JSONResponse:
41
+ """Helper function to create consistent error responses"""
42
+ error_response = ErrorResponse(
43
+ error=error_type.value.replace("_", " ").title(),
44
+ error_type=error_type,
45
+ detail=detail,
46
+ status_code=status_code,
47
+ )
48
+ log.error(detail)
49
+ if traceback:
50
+ log.error(traceback)
51
+ return JSONResponse(
52
+ status_code=status_code,
53
+ content=jsonable_encoder(error_response),
54
+ )