supervaizer 0.9.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supervaizer/__init__.py +88 -0
- supervaizer/__version__.py +10 -0
- supervaizer/account.py +304 -0
- supervaizer/account_service.py +87 -0
- supervaizer/admin/routes.py +1254 -0
- supervaizer/admin/templates/agent_detail.html +145 -0
- supervaizer/admin/templates/agents.html +175 -0
- supervaizer/admin/templates/agents_grid.html +80 -0
- supervaizer/admin/templates/base.html +233 -0
- supervaizer/admin/templates/case_detail.html +230 -0
- supervaizer/admin/templates/cases_list.html +182 -0
- supervaizer/admin/templates/cases_table.html +134 -0
- supervaizer/admin/templates/console.html +389 -0
- supervaizer/admin/templates/dashboard.html +153 -0
- supervaizer/admin/templates/job_detail.html +192 -0
- supervaizer/admin/templates/jobs_list.html +180 -0
- supervaizer/admin/templates/jobs_table.html +122 -0
- supervaizer/admin/templates/navigation.html +153 -0
- supervaizer/admin/templates/recent_activity.html +81 -0
- supervaizer/admin/templates/server.html +105 -0
- supervaizer/admin/templates/server_status_cards.html +121 -0
- supervaizer/agent.py +816 -0
- supervaizer/case.py +400 -0
- supervaizer/cli.py +135 -0
- supervaizer/common.py +283 -0
- supervaizer/event.py +181 -0
- supervaizer/examples/controller-template.py +195 -0
- supervaizer/instructions.py +145 -0
- supervaizer/job.py +379 -0
- supervaizer/job_service.py +155 -0
- supervaizer/lifecycle.py +417 -0
- supervaizer/parameter.py +173 -0
- supervaizer/protocol/__init__.py +11 -0
- supervaizer/protocol/a2a/__init__.py +21 -0
- supervaizer/protocol/a2a/model.py +227 -0
- supervaizer/protocol/a2a/routes.py +99 -0
- supervaizer/protocol/acp/__init__.py +21 -0
- supervaizer/protocol/acp/model.py +198 -0
- supervaizer/protocol/acp/routes.py +74 -0
- supervaizer/py.typed +1 -0
- supervaizer/routes.py +667 -0
- supervaizer/server.py +554 -0
- supervaizer/server_utils.py +54 -0
- supervaizer/storage.py +436 -0
- supervaizer/telemetry.py +81 -0
- supervaizer-0.9.6.dist-info/METADATA +245 -0
- supervaizer-0.9.6.dist-info/RECORD +50 -0
- supervaizer-0.9.6.dist-info/WHEEL +4 -0
- supervaizer-0.9.6.dist-info/entry_points.txt +2 -0
- supervaizer-0.9.6.dist-info/licenses/LICENSE.md +346 -0
supervaizer/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
|
+
)
|