Spaces:
Running
Running
Update src/aibom_generator/api.py
Browse files- src/aibom_generator/api.py +502 -572
src/aibom_generator/api.py
CHANGED
@@ -1,187 +1,195 @@
|
|
1 |
-
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks, Query, File, UploadFile, Form, Request
|
2 |
-
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse, RedirectResponse
|
3 |
-
from fastapi.middleware.cors import CORSMiddleware
|
4 |
-
from fastapi.staticfiles import StaticFiles
|
5 |
-
from fastapi.templating import Jinja2Templates
|
6 |
-
from pydantic import BaseModel, Field
|
7 |
-
from typing import Optional, Dict, Any, List
|
8 |
-
import uvicorn
|
9 |
-
import json
|
10 |
import os
|
11 |
-
import
|
|
|
12 |
import uuid
|
13 |
import shutil
|
14 |
-
import
|
|
|
15 |
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
-
#
|
18 |
logging.basicConfig(level=logging.INFO)
|
19 |
logger = logging.getLogger(__name__)
|
20 |
|
21 |
-
# Add parent directory to path to import generator module
|
22 |
-
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
23 |
-
|
24 |
-
# Import the AIBOM generator
|
25 |
-
try:
|
26 |
-
from aibom_fix.final_generator import AIBOMGenerator
|
27 |
-
except ImportError:
|
28 |
-
# If not found, try the mapping directory
|
29 |
-
try:
|
30 |
-
from aibom_mapping.final_generator import AIBOMGenerator
|
31 |
-
except ImportError:
|
32 |
-
# If still not found, use the original generator
|
33 |
-
try:
|
34 |
-
from aibom_fix.generator import AIBOMGenerator
|
35 |
-
except ImportError:
|
36 |
-
try:
|
37 |
-
from generator import AIBOMGenerator
|
38 |
-
except ImportError:
|
39 |
-
# Last resort: try to import from the aibom_generator module
|
40 |
-
try:
|
41 |
-
from aibom_generator.generator import AIBOMGenerator
|
42 |
-
except ImportError:
|
43 |
-
raise ImportError("Could not import AIBOMGenerator from any known location")
|
44 |
-
|
45 |
-
# Create FastAPI app
|
46 |
-
app = FastAPI(
|
47 |
-
title="Aetheris AI SBOM Generator API",
|
48 |
-
description="API for generating CycloneDX JSON AI SBOMs for AI models",
|
49 |
-
version="1.0.0",
|
50 |
-
)
|
51 |
-
|
52 |
-
# Add CORS middleware
|
53 |
-
app.add_middleware(
|
54 |
-
CORSMiddleware,
|
55 |
-
allow_origins=["*"], # Allow all origins in development
|
56 |
-
allow_credentials=True,
|
57 |
-
allow_methods=["*"],
|
58 |
-
allow_headers=["*"],
|
59 |
-
)
|
60 |
-
|
61 |
-
# Define output directory - use a directory that will have proper permissions
|
62 |
-
# Instead of creating it at the module level, we'll create it in the startup event
|
63 |
-
output_dir = "/tmp/aibom_output" # Using /tmp which should be writable
|
64 |
-
|
65 |
# Define templates directory
|
66 |
templates_dir = "templates"
|
67 |
|
68 |
# Initialize templates with a simple path
|
69 |
templates = Jinja2Templates(directory=templates_dir)
|
70 |
|
71 |
-
# Create
|
72 |
-
|
73 |
|
74 |
-
# Define
|
75 |
-
|
76 |
-
model_id: str = Field(..., description="The Hugging Face model ID (e.g., 'meta-llama/Llama-4-Scout-17B-16E-Instruct')")
|
77 |
-
hf_token: Optional[str] = Field(None, description="Optional Hugging Face API token for accessing private models")
|
78 |
-
include_inference: Optional[bool] = Field(True, description="Whether to use AI inference to enhance the AI SBOM")
|
79 |
-
use_best_practices: Optional[bool] = Field(True, description="Whether to use industry best practices for scoring")
|
80 |
|
81 |
-
|
82 |
-
|
83 |
-
model_id: str = Field(..., description="The model ID for which the AI SBOM was generated")
|
84 |
-
generated_at: str = Field(..., description="Timestamp when the AI SBOM was generated")
|
85 |
-
request_id: str = Field(..., description="Unique ID for this request")
|
86 |
-
download_url: Optional[str] = Field(None, description="URL to download the AIBOM JSON file")
|
87 |
|
88 |
-
|
89 |
-
|
90 |
-
ai_model: Optional[str] = Field(None, description="The AI model used for enhancement, if any")
|
91 |
-
original_score: Dict[str, Any] = Field(..., description="Original completeness score before enhancement")
|
92 |
-
final_score: Dict[str, Any] = Field(..., description="Final completeness score after enhancement")
|
93 |
-
improvement: float = Field(..., description="Score improvement from enhancement")
|
94 |
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
97 |
|
98 |
-
class
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
|
103 |
class BatchGenerateRequest(BaseModel):
|
104 |
-
model_ids: List[str]
|
105 |
-
|
106 |
-
|
107 |
-
use_best_practices: Optional[bool] = Field(True, description="Whether to use industry best practices for scoring")
|
108 |
|
109 |
-
class
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
114 |
|
115 |
-
|
116 |
-
|
|
|
|
|
|
|
117 |
|
118 |
-
# Startup event to
|
119 |
@app.on_event("startup")
|
120 |
async def startup_event():
|
121 |
"""Create necessary directories on startup."""
|
122 |
-
os.makedirs(
|
123 |
-
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
-
#
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
|
136 |
-
#
|
137 |
-
# Root route now serves the UI
|
138 |
@app.get("/", response_class=HTMLResponse)
|
139 |
async def root(request: Request):
|
140 |
"""Serve the web UI interface as the default view."""
|
141 |
try:
|
142 |
-
|
143 |
-
logger.info(f"Current working directory: {os.getcwd()}")
|
144 |
-
|
145 |
if os.path.exists(templates_dir):
|
146 |
-
logger.info(f"
|
|
|
|
|
|
|
147 |
|
148 |
-
# Try
|
149 |
try:
|
150 |
return templates.TemplateResponse("index.html", {"request": request})
|
151 |
except Exception as e:
|
152 |
-
logger.error(f"Error
|
153 |
|
154 |
-
#
|
155 |
try:
|
156 |
-
|
157 |
-
|
|
|
|
|
|
|
158 |
|
159 |
-
|
160 |
-
|
|
|
161 |
html_content = f.read()
|
162 |
return HTMLResponse(content=html_content)
|
163 |
-
|
164 |
-
logger.error(f"
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
if os.path.exists(abs_path):
|
171 |
-
with open(abs_path, "r") as f:
|
172 |
-
html_content = f.read()
|
173 |
-
return HTMLResponse(content=html_content)
|
174 |
-
else:
|
175 |
-
logger.error(f"Template file not found at absolute path: {abs_path}")
|
176 |
-
raise HTTPException(status_code=404, detail="UI template not found")
|
177 |
-
except Exception as file_error:
|
178 |
-
logger.error(f"Error reading file directly: {str(file_error)}")
|
179 |
-
raise HTTPException(status_code=500, detail=f"Error serving UI: {str(file_error)}")
|
180 |
-
except Exception as outer_error:
|
181 |
-
logger.error(f"Outer error in UI endpoint: {str(outer_error)}")
|
182 |
-
raise HTTPException(status_code=500, detail=f"Error in UI endpoint: {str(outer_error)}")
|
183 |
|
184 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
@app.get("/api/status", response_model=StatusResponse)
|
186 |
async def get_status():
|
187 |
"""Get the API status and version information."""
|
@@ -191,525 +199,447 @@ async def get_status():
|
|
191 |
"generator_version": "1.0.0",
|
192 |
}
|
193 |
|
194 |
-
#
|
195 |
-
@app.get("/ui", response_class=HTMLResponse)
|
196 |
-
async def ui(request: Request):
|
197 |
-
"""Serve the web UI interface (kept for backward compatibility)."""
|
198 |
-
return await root(request)
|
199 |
-
|
200 |
-
# Form-based generate endpoint for HTML form submissions
|
201 |
@app.post("/generate", response_class=HTMLResponse)
|
202 |
-
async def
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
|
|
208 |
try:
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
|
|
219 |
|
220 |
-
# Create
|
221 |
-
|
222 |
-
output_file = os.path.join(output_dir, f"{safe_model_id}_{request_id}.json")
|
223 |
|
224 |
-
# Generate
|
225 |
-
aibom =
|
226 |
model_id=model_id,
|
227 |
-
include_inference=
|
228 |
-
use_best_practices=
|
229 |
-
output_file=output_file
|
230 |
)
|
231 |
|
232 |
-
#
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
template_enhancement_report = {
|
238 |
-
"ai_enhanced": True,
|
239 |
-
"ai_model": "Default AI Model",
|
240 |
-
"original_score": {
|
241 |
-
"total_score": 0,
|
242 |
-
"completeness_score": 0 # Explicitly add completeness_score
|
243 |
-
},
|
244 |
-
"final_score": {
|
245 |
-
"total_score": 0,
|
246 |
-
"completeness_score": 0 # Explicitly add completeness_score
|
247 |
-
},
|
248 |
-
"improvement": 0
|
249 |
-
}
|
250 |
-
|
251 |
-
# If we have a real enhancement report, copy the values we have
|
252 |
-
if enhancement_report:
|
253 |
-
template_enhancement_report["ai_enhanced"] = enhancement_report.get("ai_enhanced", True)
|
254 |
-
template_enhancement_report["ai_model"] = enhancement_report.get("ai_model", "Default AI Model")
|
255 |
-
|
256 |
-
if "original_score" in enhancement_report:
|
257 |
-
template_enhancement_report["original_score"]["total_score"] = enhancement_report["original_score"].get("total_score", 0)
|
258 |
-
# Always set completeness_score equal to total_score if not present
|
259 |
-
template_enhancement_report["original_score"]["completeness_score"] = enhancement_report["original_score"].get("completeness_score",
|
260 |
-
enhancement_report["original_score"].get("total_score", 0))
|
261 |
-
|
262 |
-
if "final_score" in enhancement_report:
|
263 |
-
template_enhancement_report["final_score"]["total_score"] = enhancement_report["final_score"].get("total_score", 0)
|
264 |
-
# Always set completeness_score equal to total_score if not present
|
265 |
-
template_enhancement_report["final_score"]["completeness_score"] = enhancement_report["final_score"].get("completeness_score",
|
266 |
-
enhancement_report["final_score"].get("total_score", 0))
|
267 |
-
|
268 |
-
template_enhancement_report["improvement"] = enhancement_report.get("improvement", 0)
|
269 |
|
270 |
# Create download URL
|
271 |
-
download_url = f"/output/{
|
272 |
|
273 |
-
#
|
274 |
-
download_script =
|
275 |
<script>
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
// Add tab switching functionality
|
286 |
-
function switchTab(tabId) {{
|
287 |
-
// Hide all tab contents
|
288 |
-
const tabContents = document.getElementsByClassName('tab-content');
|
289 |
-
for (let i = 0; i < tabContents.length; i++) {{
|
290 |
-
tabContents[i].classList.remove('active');
|
291 |
-
}}
|
292 |
-
|
293 |
-
// Deactivate all tabs
|
294 |
-
const tabs = document.getElementsByClassName('aibom-tab');
|
295 |
-
for (let i = 0; i < tabs.length; i++) {{
|
296 |
-
tabs[i].classList.remove('active');
|
297 |
-
}}
|
298 |
-
|
299 |
-
// Activate the selected tab and content
|
300 |
-
document.getElementById(tabId).classList.add('active');
|
301 |
-
document.querySelector('.aibom-tab[onclick="switchTab(\\'' + tabId + '\\')"]').classList.add('active');
|
302 |
-
}}
|
303 |
-
|
304 |
-
// Add collapsible functionality
|
305 |
-
function toggleCollapsible(element) {{
|
306 |
-
element.classList.toggle('active');
|
307 |
-
const content = element.nextElementSibling;
|
308 |
-
if (content.classList.contains('active')) {{
|
309 |
-
content.classList.remove('active');
|
310 |
-
}} else {{
|
311 |
-
content.classList.add('active');
|
312 |
-
}}
|
313 |
-
}}
|
314 |
</script>
|
315 |
-
"""
|
316 |
|
317 |
-
#
|
318 |
-
|
|
|
319 |
|
320 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
return templates.TemplateResponse(
|
322 |
-
"result.html",
|
323 |
{
|
324 |
"request": request,
|
325 |
"model_id": model_id,
|
326 |
"aibom": aibom,
|
327 |
-
"enhancement_report":
|
|
|
328 |
"download_url": download_url,
|
329 |
"download_script": download_script
|
330 |
}
|
331 |
)
|
332 |
except Exception as e:
|
333 |
logger.error(f"Error generating AI SBOM: {str(e)}")
|
334 |
-
# Render the error template
|
335 |
return templates.TemplateResponse(
|
336 |
-
"error.html",
|
337 |
{
|
338 |
"request": request,
|
339 |
-
"
|
340 |
}
|
341 |
)
|
342 |
|
343 |
-
# JSON
|
344 |
-
@app.post("/api/generate"
|
345 |
-
async def
|
346 |
-
"""
|
347 |
-
Generate a CycloneDX JSON AI SBOM for a Hugging Face model.
|
348 |
-
|
349 |
-
This endpoint takes a JSON request and returns a JSON response for API clients.
|
350 |
-
"""
|
351 |
try:
|
352 |
-
#
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
|
|
|
|
361 |
|
362 |
-
# Create
|
363 |
-
|
364 |
-
output_file = os.path.join(output_dir, f"{safe_model_id}_{request_id}.json")
|
365 |
|
366 |
-
# Generate
|
367 |
-
aibom =
|
368 |
model_id=request.model_id,
|
369 |
include_inference=request.include_inference,
|
370 |
-
use_best_practices=request.use_best_practices
|
371 |
-
output_file=output_file
|
372 |
)
|
373 |
|
374 |
-
|
375 |
-
download_url = f"/output/{os.path.basename(output_file)}"
|
376 |
-
|
377 |
-
# Create response
|
378 |
-
response = {
|
379 |
-
"aibom": aibom,
|
380 |
-
"model_id": request.model_id,
|
381 |
-
"generated_at": datetime.utcnow().isoformat() + "Z",
|
382 |
-
"request_id": request_id,
|
383 |
-
"download_url": download_url
|
384 |
-
}
|
385 |
-
|
386 |
-
return response
|
387 |
except Exception as e:
|
388 |
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
389 |
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
"""
|
394 |
-
Generate a CycloneDX JSON AI SBOM with an enhancement report.
|
395 |
-
|
396 |
-
This endpoint is similar to /api/generate but also includes a report
|
397 |
-
on the AI enhancement results, including before/after scores.
|
398 |
-
"""
|
399 |
try:
|
400 |
-
#
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
|
|
|
|
409 |
|
410 |
-
# Create
|
411 |
-
|
412 |
-
output_file = os.path.join(output_dir, f"{safe_model_id}_{request_id}.json")
|
413 |
|
414 |
-
# Generate
|
415 |
-
aibom =
|
416 |
model_id=request.model_id,
|
417 |
include_inference=request.include_inference,
|
418 |
-
use_best_practices=request.use_best_practices
|
419 |
-
output_file=output_file
|
420 |
)
|
421 |
|
422 |
-
#
|
423 |
-
enhancement_report =
|
424 |
-
|
425 |
-
# Create a new enhancement report structure for the API response
|
426 |
-
api_enhancement_report = {
|
427 |
-
"ai_enhanced": True,
|
428 |
-
"ai_model": "Default AI Model",
|
429 |
-
"original_score": {
|
430 |
-
"total_score": 0,
|
431 |
-
"completeness_score": 0
|
432 |
-
},
|
433 |
-
"final_score": {
|
434 |
-
"total_score": 0,
|
435 |
-
"completeness_score": 0
|
436 |
-
},
|
437 |
-
"improvement": 0
|
438 |
-
}
|
439 |
|
440 |
-
|
441 |
-
if enhancement_report:
|
442 |
-
api_enhancement_report["ai_enhanced"] = enhancement_report.get("ai_enhanced", True)
|
443 |
-
api_enhancement_report["ai_model"] = enhancement_report.get("ai_model", "Default AI Model")
|
444 |
-
|
445 |
-
if "original_score" in enhancement_report:
|
446 |
-
api_enhancement_report["original_score"]["total_score"] = enhancement_report["original_score"].get("total_score", 0)
|
447 |
-
api_enhancement_report["original_score"]["completeness_score"] = enhancement_report["original_score"].get("completeness_score",
|
448 |
-
enhancement_report["original_score"].get("total_score", 0))
|
449 |
-
|
450 |
-
if "final_score" in enhancement_report:
|
451 |
-
api_enhancement_report["final_score"]["total_score"] = enhancement_report["final_score"].get("total_score", 0)
|
452 |
-
api_enhancement_report["final_score"]["completeness_score"] = enhancement_report["final_score"].get("completeness_score",
|
453 |
-
enhancement_report["final_score"].get("total_score", 0))
|
454 |
-
|
455 |
-
api_enhancement_report["improvement"] = enhancement_report.get("improvement", 0)
|
456 |
-
|
457 |
-
# Create download URL
|
458 |
-
download_url = f"/output/{os.path.basename(output_file)}"
|
459 |
-
|
460 |
-
# Create response
|
461 |
-
response = {
|
462 |
"aibom": aibom,
|
463 |
-
"
|
464 |
-
"generated_at": datetime.utcnow().isoformat() + "Z",
|
465 |
-
"request_id": request_id,
|
466 |
-
"download_url": download_url,
|
467 |
-
"enhancement_report": api_enhancement_report
|
468 |
}
|
469 |
-
|
470 |
-
return response
|
471 |
except Exception as e:
|
472 |
-
raise HTTPException(status_code=500, detail=f"Error generating
|
473 |
|
474 |
-
@app.
|
475 |
-
async def
|
476 |
-
|
477 |
-
|
478 |
-
use_best_practices: bool = Query(True, description="Whether to use industry best practices for scoring")
|
479 |
):
|
480 |
-
"""
|
481 |
-
Get the completeness score for a model without generating a full AI SBOM.
|
482 |
-
|
483 |
-
This is a lightweight endpoint that only returns the scoring information.
|
484 |
-
"""
|
485 |
try:
|
486 |
-
#
|
487 |
-
gen = AIBOMGenerator(
|
488 |
-
hf_token=hf_token,
|
489 |
-
use_inference=False, # Don't use inference for scoring only
|
490 |
-
use_best_practices=use_best_practices
|
491 |
-
)
|
492 |
-
|
493 |
-
# Generate the AIBOM (needed to calculate score)
|
494 |
-
aibom = gen.generate_aibom(
|
495 |
-
model_id=model_id,
|
496 |
-
include_inference=False, # Don't use inference for scoring only
|
497 |
-
use_best_practices=use_best_practices
|
498 |
-
)
|
499 |
-
|
500 |
-
# Get the enhancement report for the score
|
501 |
-
enhancement_report = gen.get_enhancement_report()
|
502 |
-
|
503 |
-
# Create a score response with the expected structure
|
504 |
-
score_response = {
|
505 |
-
"total_score": 0,
|
506 |
-
"completeness_score": 0
|
507 |
-
}
|
508 |
-
|
509 |
-
if enhancement_report and "final_score" in enhancement_report:
|
510 |
-
score_response["total_score"] = enhancement_report["final_score"].get("total_score", 0)
|
511 |
-
score_response["completeness_score"] = enhancement_report["final_score"].get("completeness_score",
|
512 |
-
enhancement_report["final_score"].get("total_score", 0))
|
513 |
-
|
514 |
-
return score_response
|
515 |
-
except Exception as e:
|
516 |
-
raise HTTPException(status_code=500, detail=f"Error calculating score: {str(e)}")
|
517 |
-
|
518 |
-
@app.post("/batch", response_model=BatchJobResponse)
|
519 |
-
async def batch_generate(request: BatchGenerateRequest, background_tasks: BackgroundTasks):
|
520 |
-
"""
|
521 |
-
Start a batch job to generate AI SBOMs for multiple models.
|
522 |
-
|
523 |
-
This endpoint queues a background task to generate AI SBOMs for all the
|
524 |
-
specified model IDs and returns a job ID that can be used to check status.
|
525 |
-
"""
|
526 |
-
try:
|
527 |
-
# Generate a job ID
|
528 |
job_id = str(uuid.uuid4())
|
529 |
|
530 |
-
# Create job
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
|
|
|
|
545 |
background_tasks.add_task(
|
546 |
process_batch_job,
|
547 |
job_id=job_id,
|
548 |
model_ids=request.model_ids,
|
549 |
-
hf_token=request.hf_token,
|
550 |
include_inference=request.include_inference,
|
551 |
use_best_practices=request.use_best_practices
|
552 |
)
|
553 |
|
554 |
-
# Return job info
|
555 |
return {
|
556 |
"job_id": job_id,
|
557 |
-
"status": "
|
558 |
"model_ids": request.model_ids,
|
559 |
-
"
|
560 |
}
|
561 |
except Exception as e:
|
562 |
raise HTTPException(status_code=500, detail=f"Error starting batch job: {str(e)}")
|
563 |
|
564 |
-
@app.get("/batch/{job_id}"
|
565 |
-
async def
|
566 |
-
"""
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
576 |
|
577 |
-
@app.post("/upload-model-card")
|
578 |
-
async def
|
579 |
model_id: str = Form(...),
|
580 |
-
model_card: UploadFile = File(...)
|
581 |
-
include_inference: bool = Form(True),
|
582 |
-
use_best_practices: bool = Form(True)
|
583 |
):
|
584 |
-
"""
|
585 |
-
Generate an AI SBOM from an uploaded model card file.
|
586 |
-
|
587 |
-
This endpoint allows users to upload a model card file directly
|
588 |
-
instead of requiring the model to be on Hugging Face.
|
589 |
-
"""
|
590 |
try:
|
591 |
-
# Create
|
592 |
-
|
593 |
-
|
|
|
594 |
|
595 |
-
#
|
596 |
-
|
597 |
-
|
598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
599 |
|
600 |
-
#
|
601 |
-
|
602 |
-
# instead of a model ID, which is beyond the scope of this example
|
603 |
|
604 |
-
#
|
605 |
-
|
606 |
-
|
607 |
-
|
608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
609 |
except Exception as e:
|
610 |
-
raise HTTPException(status_code=500, detail=f"Error processing
|
611 |
|
612 |
@app.get("/download/{filename}")
|
613 |
-
async def
|
614 |
-
"""
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
|
|
|
|
|
|
|
|
|
|
|
624 |
|
625 |
-
# Background task
|
626 |
-
async def process_batch_job(
|
|
|
|
|
|
|
|
|
|
|
627 |
"""Process a batch job in the background."""
|
628 |
-
|
629 |
-
|
630 |
-
|
631 |
-
# Create output directory
|
632 |
-
batch_output_dir = os.path.join(output_dir, job_id)
|
633 |
-
os.makedirs(batch_output_dir, exist_ok=True)
|
634 |
-
|
635 |
-
# Process each model
|
636 |
-
for model_id in model_ids:
|
637 |
try:
|
638 |
-
|
639 |
-
|
640 |
-
|
641 |
-
|
642 |
-
|
643 |
-
|
644 |
-
|
645 |
-
|
646 |
-
|
647 |
-
|
648 |
-
|
649 |
-
|
650 |
-
|
651 |
-
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
657 |
-
|
658 |
-
|
659 |
-
|
660 |
-
|
661 |
-
|
662 |
-
|
663 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
-
|
676 |
-
if enhancement_report:
|
677 |
-
batch_enhancement_report["ai_enhanced"] = enhancement_report.get("ai_enhanced", True)
|
678 |
-
batch_enhancement_report["ai_model"] = enhancement_report.get("ai_model", "Default AI Model")
|
679 |
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
|
|
|
|
|
|
|
|
684 |
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
|
|
|
|
|
|
|
|
689 |
|
690 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
691 |
|
692 |
-
|
693 |
-
|
694 |
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
"enhancement_report": batch_enhancement_report
|
700 |
-
}
|
701 |
-
except Exception as e:
|
702 |
-
# Store error
|
703 |
-
batch_jobs[job_id]["results"][model_id] = {
|
704 |
-
"status": "error",
|
705 |
-
"error": str(e)
|
706 |
-
}
|
707 |
-
|
708 |
-
# Update progress
|
709 |
-
batch_jobs[job_id]["completed"] += 1
|
710 |
-
|
711 |
-
# Update job status
|
712 |
-
batch_jobs[job_id]["status"] = "completed"
|
713 |
-
|
714 |
-
if __name__ == "__main__":
|
715 |
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
import uuid
|
5 |
import shutil
|
6 |
+
import tempfile
|
7 |
+
from typing import Dict, List, Optional, Any, Union
|
8 |
from datetime import datetime
|
9 |
+
from pathlib import Path
|
10 |
+
|
11 |
+
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks, Query, File, UploadFile, Form, Request
|
12 |
+
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
|
13 |
+
from fastapi.staticfiles import StaticFiles
|
14 |
+
from fastapi.templating import Jinja2Templates
|
15 |
+
from pydantic import BaseModel, Field
|
16 |
|
17 |
+
# Configure logging
|
18 |
logging.basicConfig(level=logging.INFO)
|
19 |
logger = logging.getLogger(__name__)
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
# Define templates directory
|
22 |
templates_dir = "templates"
|
23 |
|
24 |
# Initialize templates with a simple path
|
25 |
templates = Jinja2Templates(directory=templates_dir)
|
26 |
|
27 |
+
# Create app
|
28 |
+
app = FastAPI(title="AI SBOM Generator API")
|
29 |
|
30 |
+
# Define output directory for generated AIBOMs
|
31 |
+
OUTPUT_DIR = "/tmp/aibom_output"
|
|
|
|
|
|
|
|
|
32 |
|
33 |
+
# Create output directory if it doesn't exist
|
34 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
|
|
|
|
|
35 |
|
36 |
+
# Mount output directory as static files
|
37 |
+
app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output")
|
|
|
|
|
|
|
|
|
38 |
|
39 |
+
# Define models
|
40 |
+
class GenerateRequest(BaseModel):
|
41 |
+
model_id: str
|
42 |
+
include_inference: bool = False
|
43 |
+
use_best_practices: bool = True
|
44 |
|
45 |
+
class GenerateWithReportRequest(BaseModel):
|
46 |
+
model_id: str
|
47 |
+
include_inference: bool = False
|
48 |
+
use_best_practices: bool = True
|
49 |
|
50 |
class BatchGenerateRequest(BaseModel):
|
51 |
+
model_ids: List[str]
|
52 |
+
include_inference: bool = False
|
53 |
+
use_best_practices: bool = True
|
|
|
54 |
|
55 |
+
class ModelScoreRequest(BaseModel):
|
56 |
+
model_id: str
|
57 |
+
|
58 |
+
class StatusResponse(BaseModel):
|
59 |
+
status: str
|
60 |
+
version: str
|
61 |
+
generator_version: str
|
62 |
|
63 |
+
class BatchJobResponse(BaseModel):
|
64 |
+
job_id: str
|
65 |
+
status: str
|
66 |
+
model_ids: List[str]
|
67 |
+
message: str
|
68 |
|
69 |
+
# Startup event to ensure directories exist
|
70 |
@app.on_event("startup")
|
71 |
async def startup_event():
|
72 |
"""Create necessary directories on startup."""
|
73 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
74 |
+
logger.info(f"Created output directory at {OUTPUT_DIR}")
|
75 |
+
|
76 |
+
# Helper function to ensure enhancement report has the expected structure
|
77 |
+
def ensure_enhancement_report_structure(enhancement_report):
|
78 |
+
"""Ensure the enhancement report has all the expected fields for templates."""
|
79 |
+
if enhancement_report is None:
|
80 |
+
enhancement_report = {}
|
81 |
+
|
82 |
+
# Ensure ai_enhanced exists
|
83 |
+
if "ai_enhanced" not in enhancement_report:
|
84 |
+
enhancement_report["ai_enhanced"] = False
|
85 |
+
|
86 |
+
# Ensure ai_model exists
|
87 |
+
if "ai_model" not in enhancement_report:
|
88 |
+
enhancement_report["ai_model"] = "Default AI Model"
|
89 |
+
|
90 |
+
# Ensure original_score exists and has completeness_score
|
91 |
+
if "original_score" not in enhancement_report:
|
92 |
+
enhancement_report["original_score"] = {"total_score": 0, "completeness_score": 0}
|
93 |
+
elif "completeness_score" not in enhancement_report["original_score"]:
|
94 |
+
enhancement_report["original_score"]["completeness_score"] = enhancement_report["original_score"].get("total_score", 0)
|
95 |
+
|
96 |
+
# Ensure final_score exists and has completeness_score
|
97 |
+
if "final_score" not in enhancement_report:
|
98 |
+
enhancement_report["final_score"] = {"total_score": 0, "completeness_score": 0}
|
99 |
+
elif "completeness_score" not in enhancement_report["final_score"]:
|
100 |
+
enhancement_report["final_score"]["completeness_score"] = enhancement_report["final_score"].get("total_score", 0)
|
101 |
+
|
102 |
+
# Ensure improvement exists
|
103 |
+
if "improvement" not in enhancement_report:
|
104 |
+
enhancement_report["improvement"] = 0
|
105 |
+
|
106 |
+
return enhancement_report
|
107 |
+
|
108 |
+
# Helper function to ensure completeness_score has the expected structure
|
109 |
+
def ensure_completeness_score_structure(completeness_score):
|
110 |
+
"""Ensure the completeness_score has all the expected fields for templates."""
|
111 |
+
if completeness_score is None:
|
112 |
+
completeness_score = {}
|
113 |
+
|
114 |
+
# Ensure total_score exists
|
115 |
+
if "total_score" not in completeness_score:
|
116 |
+
completeness_score["total_score"] = 0
|
117 |
|
118 |
+
# Ensure completeness_score exists
|
119 |
+
if "completeness_score" not in completeness_score:
|
120 |
+
completeness_score["completeness_score"] = completeness_score.get("total_score", 0)
|
121 |
+
|
122 |
+
# Ensure section_scores exists
|
123 |
+
if "section_scores" not in completeness_score:
|
124 |
+
completeness_score["section_scores"] = {}
|
125 |
+
|
126 |
+
# Ensure max_scores exists
|
127 |
+
if "max_scores" not in completeness_score:
|
128 |
+
completeness_score["max_scores"] = {}
|
129 |
+
|
130 |
+
# Ensure field_checklist exists
|
131 |
+
if "field_checklist" not in completeness_score:
|
132 |
+
completeness_score["field_checklist"] = {}
|
133 |
+
|
134 |
+
# Ensure field_tiers exists
|
135 |
+
if "field_tiers" not in completeness_score:
|
136 |
+
completeness_score["field_tiers"] = {}
|
137 |
+
|
138 |
+
# Ensure completeness_profile exists
|
139 |
+
if "completeness_profile" not in completeness_score:
|
140 |
+
completeness_score["completeness_profile"] = {
|
141 |
+
"name": "Incomplete",
|
142 |
+
"description": "This AI SBOM needs improvement to meet transparency requirements."
|
143 |
+
}
|
144 |
+
|
145 |
+
return completeness_score
|
146 |
|
147 |
+
# Root route serves the UI
|
|
|
148 |
@app.get("/", response_class=HTMLResponse)
|
149 |
async def root(request: Request):
|
150 |
"""Serve the web UI interface as the default view."""
|
151 |
try:
|
152 |
+
# Check if templates directory exists
|
|
|
|
|
153 |
if os.path.exists(templates_dir):
|
154 |
+
logger.info(f"Templates directory exists")
|
155 |
+
logger.info(f"Templates directory contents: {os.listdir(templates_dir)}")
|
156 |
+
else:
|
157 |
+
logger.warning(f"Templates directory does not exist: {templates_dir}")
|
158 |
|
159 |
+
# Try to render the template
|
160 |
try:
|
161 |
return templates.TemplateResponse("index.html", {"request": request})
|
162 |
except Exception as e:
|
163 |
+
logger.error(f"Error rendering template: {str(e)}")
|
164 |
|
165 |
+
# Try direct file reading as fallback
|
166 |
try:
|
167 |
+
with open(os.path.join(templates_dir, "index.html"), "r") as f:
|
168 |
+
html_content = f.read()
|
169 |
+
return HTMLResponse(content=html_content)
|
170 |
+
except Exception as file_e:
|
171 |
+
logger.error(f"Error reading file directly: {str(file_e)}")
|
172 |
|
173 |
+
# Last resort: try absolute path
|
174 |
+
try:
|
175 |
+
with open("/app/templates/index.html", "r") as f:
|
176 |
html_content = f.read()
|
177 |
return HTMLResponse(content=html_content)
|
178 |
+
except Exception as abs_e:
|
179 |
+
logger.error(f"Error reading file from absolute path: {str(abs_e)}")
|
180 |
+
raise HTTPException(status_code=500, detail=f"Error in UI endpoint: {str(e)}")
|
181 |
+
except Exception as outer_e:
|
182 |
+
logger.error(f"Outer error in UI endpoint: {str(outer_e)}")
|
183 |
+
raise HTTPException(status_code=500, detail=f"Error in UI endpoint: {str(outer_e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
|
185 |
+
# UI route for backward compatibility
|
186 |
+
@app.get("/ui", response_class=HTMLResponse)
|
187 |
+
async def ui(request: Request):
|
188 |
+
"""Serve the web UI interface (kept for backward compatibility)."""
|
189 |
+
return await root(request)
|
190 |
+
|
191 |
+
# Status endpoint - both at root and /api path for compatibility
|
192 |
+
@app.get("/status", response_model=StatusResponse)
|
193 |
@app.get("/api/status", response_model=StatusResponse)
|
194 |
async def get_status():
|
195 |
"""Get the API status and version information."""
|
|
|
199 |
"generator_version": "1.0.0",
|
200 |
}
|
201 |
|
202 |
+
# Form-based generate endpoint for web UI
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
@app.post("/generate", response_class=HTMLResponse)
|
204 |
+
async def generate_form(
|
205 |
+
request: Request,
|
206 |
+
model_id: str = Form(...),
|
207 |
+
include_inference: bool = Form(False),
|
208 |
+
use_best_practices: bool = Form(True)
|
209 |
+
):
|
210 |
+
"""Generate an AI SBOM from form data and render the result template."""
|
211 |
try:
|
212 |
+
# Import the generator here to avoid circular imports
|
213 |
+
try:
|
214 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
215 |
+
except ImportError:
|
216 |
+
try:
|
217 |
+
from aibom_generator.generator import AIBOMGenerator
|
218 |
+
except ImportError:
|
219 |
+
try:
|
220 |
+
from generator import AIBOMGenerator
|
221 |
+
except ImportError:
|
222 |
+
raise ImportError("Could not import AIBOMGenerator. Please check your installation.")
|
223 |
|
224 |
+
# Create generator instance
|
225 |
+
generator = AIBOMGenerator()
|
|
|
226 |
|
227 |
+
# Generate AIBOM
|
228 |
+
aibom, enhancement_report = generator.generate(
|
229 |
model_id=model_id,
|
230 |
+
include_inference=include_inference,
|
231 |
+
use_best_practices=use_best_practices
|
|
|
232 |
)
|
233 |
|
234 |
+
# Save AIBOM to file
|
235 |
+
filename = f"{model_id.replace('/', '_')}_aibom.json"
|
236 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
237 |
+
with open(filepath, "w") as f:
|
238 |
+
json.dump(aibom, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
|
240 |
# Create download URL
|
241 |
+
download_url = f"/output/{filename}"
|
242 |
|
243 |
+
# Create download script
|
244 |
+
download_script = """
|
245 |
<script>
|
246 |
+
function downloadJSON() {
|
247 |
+
const a = document.createElement('a');
|
248 |
+
a.href = '%s';
|
249 |
+
a.download = '%s';
|
250 |
+
document.body.appendChild(a);
|
251 |
+
a.click();
|
252 |
+
document.body.removeChild(a);
|
253 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
254 |
</script>
|
255 |
+
""" % (download_url, filename)
|
256 |
|
257 |
+
# Process enhancement report for template
|
258 |
+
enhancement_report = ensure_enhancement_report_structure(enhancement_report)
|
259 |
+
logger.info(f"Enhancement report structure: {enhancement_report}")
|
260 |
|
261 |
+
# Get completeness score
|
262 |
+
completeness_score = None
|
263 |
+
if hasattr(generator, 'get_completeness_score'):
|
264 |
+
try:
|
265 |
+
completeness_score = generator.get_completeness_score(model_id)
|
266 |
+
completeness_score = ensure_completeness_score_structure(completeness_score)
|
267 |
+
logger.info(f"Completeness score structure: {completeness_score}")
|
268 |
+
except Exception as e:
|
269 |
+
logger.error(f"Error getting completeness score: {str(e)}")
|
270 |
+
completeness_score = ensure_completeness_score_structure(None)
|
271 |
+
else:
|
272 |
+
# Create a default completeness score if the method doesn't exist
|
273 |
+
completeness_score = ensure_completeness_score_structure(None)
|
274 |
+
|
275 |
+
# Render result template
|
276 |
return templates.TemplateResponse(
|
277 |
+
"result.html",
|
278 |
{
|
279 |
"request": request,
|
280 |
"model_id": model_id,
|
281 |
"aibom": aibom,
|
282 |
+
"enhancement_report": enhancement_report,
|
283 |
+
"completeness_score": completeness_score,
|
284 |
"download_url": download_url,
|
285 |
"download_script": download_script
|
286 |
}
|
287 |
)
|
288 |
except Exception as e:
|
289 |
logger.error(f"Error generating AI SBOM: {str(e)}")
|
|
|
290 |
return templates.TemplateResponse(
|
291 |
+
"error.html",
|
292 |
{
|
293 |
"request": request,
|
294 |
+
"error_message": f"Error generating AI SBOM: {str(e)}"
|
295 |
}
|
296 |
)
|
297 |
|
298 |
+
# JSON API endpoints
|
299 |
+
@app.post("/api/generate")
|
300 |
+
async def generate_api(request: GenerateRequest):
|
301 |
+
"""Generate an AI SBOM and return it as JSON."""
|
|
|
|
|
|
|
|
|
302 |
try:
|
303 |
+
# Import the generator here to avoid circular imports
|
304 |
+
try:
|
305 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
306 |
+
except ImportError:
|
307 |
+
try:
|
308 |
+
from aibom_generator.generator import AIBOMGenerator
|
309 |
+
except ImportError:
|
310 |
+
try:
|
311 |
+
from generator import AIBOMGenerator
|
312 |
+
except ImportError:
|
313 |
+
raise ImportError("Could not import AIBOMGenerator. Please check your installation.")
|
314 |
|
315 |
+
# Create generator instance
|
316 |
+
generator = AIBOMGenerator()
|
|
|
317 |
|
318 |
+
# Generate AIBOM
|
319 |
+
aibom, _ = generator.generate(
|
320 |
model_id=request.model_id,
|
321 |
include_inference=request.include_inference,
|
322 |
+
use_best_practices=request.use_best_practices
|
|
|
323 |
)
|
324 |
|
325 |
+
return aibom
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
326 |
except Exception as e:
|
327 |
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
328 |
|
329 |
+
@app.post("/api/generate-with-report")
|
330 |
+
async def generate_with_report_api(request: GenerateWithReportRequest):
|
331 |
+
"""Generate an AI SBOM with enhancement report and return both as JSON."""
|
|
|
|
|
|
|
|
|
|
|
|
|
332 |
try:
|
333 |
+
# Import the generator here to avoid circular imports
|
334 |
+
try:
|
335 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
336 |
+
except ImportError:
|
337 |
+
try:
|
338 |
+
from aibom_generator.generator import AIBOMGenerator
|
339 |
+
except ImportError:
|
340 |
+
try:
|
341 |
+
from generator import AIBOMGenerator
|
342 |
+
except ImportError:
|
343 |
+
raise ImportError("Could not import AIBOMGenerator. Please check your installation.")
|
344 |
|
345 |
+
# Create generator instance
|
346 |
+
generator = AIBOMGenerator()
|
|
|
347 |
|
348 |
+
# Generate AIBOM
|
349 |
+
aibom, enhancement_report = generator.generate(
|
350 |
model_id=request.model_id,
|
351 |
include_inference=request.include_inference,
|
352 |
+
use_best_practices=request.use_best_practices
|
|
|
353 |
)
|
354 |
|
355 |
+
# Process enhancement report for API response
|
356 |
+
enhancement_report = ensure_enhancement_report_structure(enhancement_report)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
|
358 |
+
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
359 |
"aibom": aibom,
|
360 |
+
"enhancement_report": enhancement_report
|
|
|
|
|
|
|
|
|
361 |
}
|
|
|
|
|
362 |
except Exception as e:
|
363 |
+
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
364 |
|
365 |
+
@app.post("/api/batch-generate")
|
366 |
+
async def batch_generate_api(
|
367 |
+
request: BatchGenerateRequest,
|
368 |
+
background_tasks: BackgroundTasks
|
|
|
369 |
):
|
370 |
+
"""Start a batch job to generate multiple AI SBOMs."""
|
|
|
|
|
|
|
|
|
371 |
try:
|
372 |
+
# Generate job ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
job_id = str(uuid.uuid4())
|
374 |
|
375 |
+
# Create job directory
|
376 |
+
job_dir = os.path.join(OUTPUT_DIR, job_id)
|
377 |
+
os.makedirs(job_dir, exist_ok=True)
|
378 |
+
|
379 |
+
# Create job status file
|
380 |
+
status_file = os.path.join(job_dir, "status.json")
|
381 |
+
with open(status_file, "w") as f:
|
382 |
+
json.dump({
|
383 |
+
"job_id": job_id,
|
384 |
+
"status": "pending",
|
385 |
+
"model_ids": request.model_ids,
|
386 |
+
"completed": 0,
|
387 |
+
"total": len(request.model_ids),
|
388 |
+
"results": {}
|
389 |
+
}, f, indent=2)
|
390 |
+
|
391 |
+
# Start background task
|
392 |
background_tasks.add_task(
|
393 |
process_batch_job,
|
394 |
job_id=job_id,
|
395 |
model_ids=request.model_ids,
|
|
|
396 |
include_inference=request.include_inference,
|
397 |
use_best_practices=request.use_best_practices
|
398 |
)
|
399 |
|
|
|
400 |
return {
|
401 |
"job_id": job_id,
|
402 |
+
"status": "pending",
|
403 |
"model_ids": request.model_ids,
|
404 |
+
"message": f"Batch job started. {len(request.model_ids)} models will be processed."
|
405 |
}
|
406 |
except Exception as e:
|
407 |
raise HTTPException(status_code=500, detail=f"Error starting batch job: {str(e)}")
|
408 |
|
409 |
+
@app.get("/api/batch-status/{job_id}")
|
410 |
+
async def batch_status_api(job_id: str):
|
411 |
+
"""Get the status of a batch job."""
|
412 |
+
try:
|
413 |
+
# Check if job exists
|
414 |
+
job_dir = os.path.join(OUTPUT_DIR, job_id)
|
415 |
+
if not os.path.exists(job_dir):
|
416 |
+
raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
|
417 |
+
|
418 |
+
# Read job status
|
419 |
+
status_file = os.path.join(job_dir, "status.json")
|
420 |
+
with open(status_file, "r") as f:
|
421 |
+
status = json.load(f)
|
422 |
+
|
423 |
+
return status
|
424 |
+
except HTTPException:
|
425 |
+
raise
|
426 |
+
except Exception as e:
|
427 |
+
raise HTTPException(status_code=500, detail=f"Error getting job status: {str(e)}")
|
428 |
+
|
429 |
+
@app.get("/api/batch-results/{job_id}")
|
430 |
+
async def batch_results_api(job_id: str):
|
431 |
+
"""Get the results of a completed batch job."""
|
432 |
+
try:
|
433 |
+
# Check if job exists
|
434 |
+
job_dir = os.path.join(OUTPUT_DIR, job_id)
|
435 |
+
if not os.path.exists(job_dir):
|
436 |
+
raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
|
437 |
+
|
438 |
+
# Read job status
|
439 |
+
status_file = os.path.join(job_dir, "status.json")
|
440 |
+
with open(status_file, "r") as f:
|
441 |
+
status = json.load(f)
|
442 |
+
|
443 |
+
# Check if job is completed
|
444 |
+
if status["status"] != "completed":
|
445 |
+
raise HTTPException(status_code=400, detail=f"Job {job_id} is not completed yet")
|
446 |
+
|
447 |
+
# Read results
|
448 |
+
results = {}
|
449 |
+
for model_id, result_file in status["results"].items():
|
450 |
+
with open(os.path.join(job_dir, result_file), "r") as f:
|
451 |
+
results[model_id] = json.load(f)
|
452 |
+
|
453 |
+
return results
|
454 |
+
except HTTPException:
|
455 |
+
raise
|
456 |
+
except Exception as e:
|
457 |
+
raise HTTPException(status_code=500, detail=f"Error getting job results: {str(e)}")
|
458 |
+
|
459 |
+
@app.get("/api/model-score/{model_id}")
|
460 |
+
async def model_score_api(model_id: str):
|
461 |
+
"""Get the completeness score for a model."""
|
462 |
+
try:
|
463 |
+
# Import the generator here to avoid circular imports
|
464 |
+
try:
|
465 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
466 |
+
except ImportError:
|
467 |
+
try:
|
468 |
+
from aibom_generator.generator import AIBOMGenerator
|
469 |
+
except ImportError:
|
470 |
+
try:
|
471 |
+
from generator import AIBOMGenerator
|
472 |
+
except ImportError:
|
473 |
+
raise ImportError("Could not import AIBOMGenerator. Please check your installation.")
|
474 |
+
|
475 |
+
# Create generator instance
|
476 |
+
generator = AIBOMGenerator()
|
477 |
+
|
478 |
+
# Get completeness score
|
479 |
+
if hasattr(generator, 'get_completeness_score'):
|
480 |
+
completeness_score = generator.get_completeness_score(model_id)
|
481 |
+
completeness_score = ensure_completeness_score_structure(completeness_score)
|
482 |
+
return completeness_score
|
483 |
+
else:
|
484 |
+
# Create a default completeness score if the method doesn't exist
|
485 |
+
completeness_score = ensure_completeness_score_structure(None)
|
486 |
+
return completeness_score
|
487 |
+
except Exception as e:
|
488 |
+
raise HTTPException(status_code=500, detail=f"Error getting model score: {str(e)}")
|
489 |
|
490 |
+
@app.post("/api/upload-model-card")
|
491 |
+
async def upload_model_card_api(
|
492 |
model_id: str = Form(...),
|
493 |
+
model_card: UploadFile = File(...)
|
|
|
|
|
494 |
):
|
495 |
+
"""Upload a model card file for a model."""
|
|
|
|
|
|
|
|
|
|
|
496 |
try:
|
497 |
+
# Create temporary file
|
498 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".md") as temp_file:
|
499 |
+
# Write uploaded file to temporary file
|
500 |
+
shutil.copyfileobj(model_card.file, temp_file)
|
501 |
|
502 |
+
# Import the generator here to avoid circular imports
|
503 |
+
try:
|
504 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
505 |
+
except ImportError:
|
506 |
+
try:
|
507 |
+
from aibom_generator.generator import AIBOMGenerator
|
508 |
+
except ImportError:
|
509 |
+
try:
|
510 |
+
from generator import AIBOMGenerator
|
511 |
+
except ImportError:
|
512 |
+
raise ImportError("Could not import AIBOMGenerator. Please check your installation.")
|
513 |
|
514 |
+
# Create generator instance
|
515 |
+
generator = AIBOMGenerator()
|
|
|
516 |
|
517 |
+
# Process model card
|
518 |
+
if hasattr(generator, 'process_model_card'):
|
519 |
+
result = generator.process_model_card(model_id, temp_file.name)
|
520 |
+
|
521 |
+
# Clean up temporary file
|
522 |
+
os.unlink(temp_file.name)
|
523 |
+
|
524 |
+
return result
|
525 |
+
else:
|
526 |
+
# Clean up temporary file
|
527 |
+
os.unlink(temp_file.name)
|
528 |
+
|
529 |
+
raise HTTPException(status_code=501, detail="Model card processing not implemented")
|
530 |
except Exception as e:
|
531 |
+
raise HTTPException(status_code=500, detail=f"Error processing model card: {str(e)}")
|
532 |
|
533 |
@app.get("/download/{filename}")
|
534 |
+
async def download_file(filename: str):
|
535 |
+
"""Download a generated AI SBOM file."""
|
536 |
+
try:
|
537 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
538 |
+
if not os.path.exists(filepath):
|
539 |
+
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
540 |
+
|
541 |
+
return FileResponse(
|
542 |
+
filepath,
|
543 |
+
media_type="application/json",
|
544 |
+
filename=filename
|
545 |
+
)
|
546 |
+
except HTTPException:
|
547 |
+
raise
|
548 |
+
except Exception as e:
|
549 |
+
raise HTTPException(status_code=500, detail=f"Error downloading file: {str(e)}")
|
550 |
|
551 |
+
# Background task for batch processing
|
552 |
+
async def process_batch_job(
|
553 |
+
job_id: str,
|
554 |
+
model_ids: List[str],
|
555 |
+
include_inference: bool,
|
556 |
+
use_best_practices: bool
|
557 |
+
):
|
558 |
"""Process a batch job in the background."""
|
559 |
+
try:
|
560 |
+
# Import the generator here to avoid circular imports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
561 |
try:
|
562 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
563 |
+
except ImportError:
|
564 |
+
try:
|
565 |
+
from aibom_generator.generator import AIBOMGenerator
|
566 |
+
except ImportError:
|
567 |
+
try:
|
568 |
+
from generator import AIBOMGenerator
|
569 |
+
except ImportError:
|
570 |
+
raise ImportError("Could not import AIBOMGenerator. Please check your installation.")
|
571 |
+
|
572 |
+
# Create generator instance
|
573 |
+
generator = AIBOMGenerator()
|
574 |
+
|
575 |
+
# Create job directory
|
576 |
+
job_dir = os.path.join(OUTPUT_DIR, job_id)
|
577 |
+
|
578 |
+
# Read job status
|
579 |
+
status_file = os.path.join(job_dir, "status.json")
|
580 |
+
with open(status_file, "r") as f:
|
581 |
+
status = json.load(f)
|
582 |
+
|
583 |
+
# Update status to running
|
584 |
+
status["status"] = "running"
|
585 |
+
with open(status_file, "w") as f:
|
586 |
+
json.dump(status, f, indent=2)
|
587 |
+
|
588 |
+
# Process each model
|
589 |
+
for i, model_id in enumerate(model_ids):
|
590 |
+
try:
|
591 |
+
# Generate AIBOM
|
592 |
+
aibom, enhancement_report = generator.generate(
|
593 |
+
model_id=model_id,
|
594 |
+
include_inference=include_inference,
|
595 |
+
use_best_practices=use_best_practices
|
596 |
+
)
|
597 |
+
|
598 |
+
# Process enhancement report for API response
|
599 |
+
enhancement_report = ensure_enhancement_report_structure(enhancement_report)
|
|
|
|
|
|
|
600 |
|
601 |
+
# Save result to file
|
602 |
+
result_file = f"{model_id.replace('/', '_')}_aibom.json"
|
603 |
+
result_path = os.path.join(job_dir, result_file)
|
604 |
+
with open(result_path, "w") as f:
|
605 |
+
json.dump({
|
606 |
+
"aibom": aibom,
|
607 |
+
"enhancement_report": enhancement_report
|
608 |
+
}, f, indent=2)
|
609 |
|
610 |
+
# Update status
|
611 |
+
status["completed"] += 1
|
612 |
+
status["results"][model_id] = result_file
|
613 |
+
with open(status_file, "w") as f:
|
614 |
+
json.dump(status, f, indent=2)
|
615 |
+
except Exception as e:
|
616 |
+
# Log error
|
617 |
+
logger.error(f"Error processing model {model_id}: {str(e)}")
|
618 |
|
619 |
+
# Update status
|
620 |
+
status["completed"] += 1
|
621 |
+
status["results"][model_id] = f"Error: {str(e)}"
|
622 |
+
with open(status_file, "w") as f:
|
623 |
+
json.dump(status, f, indent=2)
|
624 |
+
|
625 |
+
# Update status to completed
|
626 |
+
status["status"] = "completed"
|
627 |
+
with open(status_file, "w") as f:
|
628 |
+
json.dump(status, f, indent=2)
|
629 |
+
except Exception as e:
|
630 |
+
# Log error
|
631 |
+
logger.error(f"Error processing batch job {job_id}: {str(e)}")
|
632 |
+
|
633 |
+
try:
|
634 |
+
# Update status to failed
|
635 |
+
status_file = os.path.join(OUTPUT_DIR, job_id, "status.json")
|
636 |
+
with open(status_file, "r") as f:
|
637 |
+
status = json.load(f)
|
638 |
|
639 |
+
status["status"] = "failed"
|
640 |
+
status["error"] = str(e)
|
641 |
|
642 |
+
with open(status_file, "w") as f:
|
643 |
+
json.dump(status, f, indent=2)
|
644 |
+
except Exception as status_e:
|
645 |
+
logger.error(f"Error updating job status: {str(status_e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|