a1c00l commited on
Commit
edbf608
·
verified ·
1 Parent(s): 5d28f7e

Update src/aibom_generator/api.py

Browse files
Files changed (1) hide show
  1. 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 sys
 
12
  import uuid
13
  import shutil
14
- import logging
 
15
  from datetime import datetime
 
 
 
 
 
 
 
16
 
17
- # Set up logging
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 a global generator instance
72
- generator = AIBOMGenerator(use_best_practices=True)
73
 
74
- # Define request models
75
- class GenerateAIBOMRequest(BaseModel):
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
- class AIBOMResponse(BaseModel):
82
- aibom: Dict[str, Any] = Field(..., description="The generated AIBOM in CycloneDX JSON format")
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
- class EnhancementReport(BaseModel):
89
- ai_enhanced: bool = Field(..., description="Whether AI enhancement was applied")
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
- class AIBOMWithReportResponse(AIBOMResponse):
96
- enhancement_report: Optional[EnhancementReport] = Field(None, description="Report on AI enhancement results")
 
 
 
97
 
98
- class StatusResponse(BaseModel):
99
- status: str = Field(..., description="API status")
100
- version: str = Field(..., description="API version")
101
- generator_version: str = Field(..., description="AIBOM generator version")
102
 
103
  class BatchGenerateRequest(BaseModel):
104
- model_ids: List[str] = Field(..., description="List of Hugging Face model IDs to generate AIBOMs for")
105
- hf_token: Optional[str] = Field(None, description="Optional Hugging Face API token for accessing private models")
106
- include_inference: Optional[bool] = Field(True, description="Whether to use AI inference to enhance the AI SBOM")
107
- use_best_practices: Optional[bool] = Field(True, description="Whether to use industry best practices for scoring")
108
 
109
- class BatchJobResponse(BaseModel):
110
- job_id: str = Field(..., description="Unique ID for the batch job")
111
- status: str = Field(..., description="Job status (e.g., 'queued', 'processing', 'completed')")
112
- model_ids: List[str] = Field(..., description="List of model IDs in the batch")
113
- created_at: str = Field(..., description="Timestamp when the job was created")
 
 
114
 
115
- # In-memory storage for batch jobs
116
- batch_jobs = {}
 
 
 
117
 
118
- # Startup event to create output directory
119
  @app.on_event("startup")
120
  async def startup_event():
121
  """Create necessary directories on startup."""
122
- os.makedirs(output_dir, exist_ok=True)
123
- # Mount the static files directory after it's created
124
- app.mount("/output", StaticFiles(directory=output_dir), name="output")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- # Log environment information for debugging
127
- logger.info(f"Starting up API server")
128
- logger.info(f"Current working directory: {os.getcwd()}")
129
- logger.info(f"Directory contents: {os.listdir('.')}")
130
- if os.path.exists(templates_dir):
131
- logger.info(f"Templates directory exists")
132
- logger.info(f"Templates directory contents: {os.listdir(templates_dir)}")
133
- else:
134
- logger.warning(f"Templates directory does not exist")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- # Define API endpoints
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
- logger.info("Root endpoint accessed - serving UI")
143
- logger.info(f"Current working directory: {os.getcwd()}")
144
-
145
  if os.path.exists(templates_dir):
146
- logger.info(f"Files in templates dir: {os.listdir(templates_dir)}")
 
 
 
147
 
148
- # Try using templates first
149
  try:
150
  return templates.TemplateResponse("index.html", {"request": request})
151
  except Exception as e:
152
- logger.error(f"Error using templates: {str(e)}")
153
 
154
- # Fallback to direct file reading
155
  try:
156
- template_path = os.path.join(templates_dir, "index.html")
157
- logger.info(f"Trying direct file read from: {template_path}")
 
 
 
158
 
159
- if os.path.exists(template_path):
160
- with open(template_path, "r") as f:
 
161
  html_content = f.read()
162
  return HTMLResponse(content=html_content)
163
- else:
164
- logger.error(f"Template file not found at: {template_path}")
165
-
166
- # Try absolute path as last resort
167
- abs_path = os.path.join(os.getcwd(), templates_dir, "index.html")
168
- logger.info(f"Trying absolute path: {abs_path}")
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
- # Status endpoint moved to /api/status
 
 
 
 
 
 
 
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
- # Keep the /ui endpoint for backward compatibility
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 generate_aibom_form(request: Request, model_id: str = Form(...)):
203
- """
204
- Generate a CycloneDX JSON AI SBOM for a Hugging Face model from form submission.
205
-
206
- This endpoint handles traditional HTML form submissions and renders the result template.
207
- """
 
208
  try:
209
- logger.info(f"Form-based generate endpoint called with model_id: {model_id}")
210
-
211
- # Create a new generator instance
212
- gen = AIBOMGenerator(
213
- use_inference=True, # Default to true for form submissions
214
- use_best_practices=True
215
- )
216
-
217
- # Generate a request ID
218
- request_id = str(uuid.uuid4())
 
219
 
220
- # Create output file path
221
- safe_model_id = model_id.replace("/", "_")
222
- output_file = os.path.join(output_dir, f"{safe_model_id}_{request_id}.json")
223
 
224
- # Generate the AIBOM
225
- aibom = gen.generate_aibom(
226
  model_id=model_id,
227
- include_inference=True,
228
- use_best_practices=True,
229
- output_file=output_file
230
  )
231
 
232
- # Get the enhancement report
233
- enhancement_report = gen.get_enhancement_report()
234
-
235
- # Create a completely new enhancement report structure that matches what the template expects
236
- # This is a more direct approach than trying to modify the existing structure
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/{os.path.basename(output_file)}"
272
 
273
- # Add a downloadJSON function for the template
274
- download_script = f"""
275
  <script>
276
- function downloadJSON() {{
277
- const a = document.createElement('a');
278
- a.href = '{download_url}';
279
- a.download = '{safe_model_id}_aibom.json';
280
- document.body.appendChild(a);
281
- a.click();
282
- document.body.removeChild(a);
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
- # Log the enhancement report structure for debugging
318
- logger.info(f"Template enhancement report structure: {template_enhancement_report}")
 
319
 
320
- # Render the result template with our carefully constructed enhancement report
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  return templates.TemplateResponse(
322
- "result.html",
323
  {
324
  "request": request,
325
  "model_id": model_id,
326
  "aibom": aibom,
327
- "enhancement_report": template_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
- "error": str(e)
340
  }
341
  )
342
 
343
- # JSON-based generate endpoint for API clients
344
- @app.post("/api/generate", response_model=AIBOMResponse)
345
- async def generate_aibom_json(request: GenerateAIBOMRequest):
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
- # Create a new generator instance with the provided token if available
353
- gen = AIBOMGenerator(
354
- hf_token=request.hf_token,
355
- use_inference=request.include_inference,
356
- use_best_practices=request.use_best_practices
357
- )
358
-
359
- # Generate a request ID
360
- request_id = str(uuid.uuid4())
 
 
361
 
362
- # Create output file path
363
- safe_model_id = request.model_id.replace("/", "_")
364
- output_file = os.path.join(output_dir, f"{safe_model_id}_{request_id}.json")
365
 
366
- # Generate the AIBOM
367
- aibom = gen.generate_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
- # Create download URL
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
- # JSON-based generate with report endpoint for API clients
391
- @app.post("/api/generate-with-report", response_model=AIBOMWithReportResponse)
392
- async def generate_aibom_with_report_json(request: GenerateAIBOMRequest):
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
- # Create a new generator instance with the provided token if available
401
- gen = AIBOMGenerator(
402
- hf_token=request.hf_token,
403
- use_inference=request.include_inference,
404
- use_best_practices=request.use_best_practices
405
- )
406
-
407
- # Generate a request ID
408
- request_id = str(uuid.uuid4())
 
 
409
 
410
- # Create output file path
411
- safe_model_id = request.model_id.replace("/", "_")
412
- output_file = os.path.join(output_dir, f"{safe_model_id}_{request_id}.json")
413
 
414
- # Generate the AIBOM
415
- aibom = gen.generate_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
- # Get the enhancement report
423
- enhancement_report = gen.get_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
- # If we have a real enhancement report, copy the values we have
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
- "model_id": request.model_id,
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 AIBOM: {str(e)}")
473
 
474
- @app.get("/models/{model_id}/score", response_model=Dict[str, Any])
475
- async def get_model_score(
476
- model_id: str,
477
- hf_token: Optional[str] = Query(None, description="Optional Hugging Face API token for accessing private models"),
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
- # Create a new generator instance with the provided token if available
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 record
531
- job = {
532
- "job_id": job_id,
533
- "status": "queued",
534
- "model_ids": request.model_ids,
535
- "created_at": datetime.utcnow().isoformat() + "Z",
536
- "completed": 0,
537
- "total": len(request.model_ids),
538
- "results": {}
539
- }
540
-
541
- # Store job in memory
542
- batch_jobs[job_id] = job
543
-
544
- # Add background task to process the batch
 
 
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": "queued",
558
  "model_ids": request.model_ids,
559
- "created_at": job["created_at"]
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}", response_model=Dict[str, Any])
565
- async def get_batch_status(job_id: str):
566
- """
567
- Get the status of a batch job.
568
-
569
- This endpoint returns the current status of a batch job, including
570
- progress information and results for completed models.
571
- """
572
- if job_id not in batch_jobs:
573
- raise HTTPException(status_code=404, detail="Batch job not found")
574
-
575
- return batch_jobs[job_id]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
- @app.post("/upload-model-card")
578
- async def upload_model_card(
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 a temporary directory to store the uploaded file
592
- temp_dir = os.path.join("/tmp", "aibom_temp")
593
- os.makedirs(temp_dir, exist_ok=True)
 
594
 
595
- # Save the uploaded file
596
- file_path = os.path.join(temp_dir, model_card.filename)
597
- with open(file_path, "wb") as f:
598
- shutil.copyfileobj(model_card.file, f)
 
 
 
 
 
 
 
599
 
600
- # TODO: Implement custom model card processing
601
- # This would require modifying the AIBOMGenerator to accept a file path
602
- # instead of a model ID, which is beyond the scope of this example
603
 
604
- # For now, return a placeholder response
605
- return {
606
- "status": "not_implemented",
607
- "message": "Custom model card processing is not yet implemented"
608
- }
 
 
 
 
 
 
 
 
609
  except Exception as e:
610
- raise HTTPException(status_code=500, detail=f"Error processing uploaded model card: {str(e)}")
611
 
612
  @app.get("/download/{filename}")
613
- async def download_aibom(filename: str):
614
- """
615
- Download a previously generated AI SBOM file.
616
-
617
- This endpoint allows downloading AI SBOM files by filename.
618
- """
619
- file_path = os.path.join(output_dir, filename)
620
- if not os.path.exists(file_path):
621
- raise HTTPException(status_code=404, detail="File not found")
622
-
623
- return FileResponse(file_path, media_type="application/json", filename=filename)
 
 
 
 
 
624
 
625
- # Background task function for batch processing
626
- async def process_batch_job(job_id: str, model_ids: List[str], hf_token: Optional[str], include_inference: bool, use_best_practices: bool):
 
 
 
 
 
627
  """Process a batch job in the background."""
628
- # Update job status
629
- batch_jobs[job_id]["status"] = "processing"
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
- # Create a new generator instance
639
- gen = AIBOMGenerator(
640
- hf_token=hf_token,
641
- use_inference=include_inference,
642
- use_best_practices=use_best_practices
643
- )
644
-
645
- # Create output file path
646
- safe_model_id = model_id.replace("/", "_")
647
- output_file = os.path.join(batch_output_dir, f"{safe_model_id}.json")
648
-
649
- # Generate the AIBOM
650
- aibom = gen.generate_aibom(
651
- model_id=model_id,
652
- include_inference=include_inference,
653
- use_best_practices=use_best_practices,
654
- output_file=output_file
655
- )
656
-
657
- # Get the enhancement report
658
- enhancement_report = gen.get_enhancement_report()
659
-
660
- # Create a new enhancement report structure for the batch results
661
- batch_enhancement_report = {
662
- "ai_enhanced": True,
663
- "ai_model": "Default AI Model",
664
- "original_score": {
665
- "total_score": 0,
666
- "completeness_score": 0
667
- },
668
- "final_score": {
669
- "total_score": 0,
670
- "completeness_score": 0
671
- },
672
- "improvement": 0
673
- }
674
-
675
- # If we have a real enhancement report, copy the values we have
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
- if "original_score" in enhancement_report:
681
- batch_enhancement_report["original_score"]["total_score"] = enhancement_report["original_score"].get("total_score", 0)
682
- batch_enhancement_report["original_score"]["completeness_score"] = enhancement_report["original_score"].get("completeness_score",
683
- enhancement_report["original_score"].get("total_score", 0))
 
 
 
 
684
 
685
- if "final_score" in enhancement_report:
686
- batch_enhancement_report["final_score"]["total_score"] = enhancement_report["final_score"].get("total_score", 0)
687
- batch_enhancement_report["final_score"]["completeness_score"] = enhancement_report["final_score"].get("completeness_score",
688
- enhancement_report["final_score"].get("total_score", 0))
 
 
 
 
689
 
690
- batch_enhancement_report["improvement"] = enhancement_report.get("improvement", 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
 
692
- # Create download URL
693
- download_url = f"/output/{job_id}/{safe_model_id}.json"
694
 
695
- # Store result
696
- batch_jobs[job_id]["results"][model_id] = {
697
- "status": "completed",
698
- "download_url": download_url,
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)}")