refactor: Archive experimental LLM features for MVP stability (Phase 1.1)
Moved experimental LLM integration code to optimization_engine/future/: - llm_optimization_runner.py - Runtime LLM API runner - llm_workflow_analyzer.py - Workflow analysis - inline_code_generator.py - Auto-generate calculations - hook_generator.py - Auto-generate hooks - report_generator.py - LLM report generation - extractor_orchestrator.py - Extractor orchestration Added comprehensive optimization_engine/future/README.md explaining: - MVP LLM strategy (Claude Code skills, not runtime LLM) - Why files were archived - When to revisit post-MVP - Production architecture reference Production runner confirmed: optimization_engine/runner.py is sole active runner. This establishes clear separation between: - Production code (stable, no runtime LLM dependencies) - Experimental code (archived for post-MVP exploration) Part of Phase 1: Core Stabilization & Organization for MVP Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,15 @@ Optimization API endpoints
|
||||
Handles study status, history retrieval, and control operations
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
import json
|
||||
import sys
|
||||
import sqlite3
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
|
||||
@@ -307,12 +310,40 @@ async def get_optimization_history(study_id: str, limit: Optional[int] = None):
|
||||
except (ValueError, TypeError):
|
||||
params[param_name] = param_value
|
||||
|
||||
# Get user attributes (extracted results: mass, frequency, stress, displacement, etc.)
|
||||
cursor.execute("""
|
||||
SELECT key, value_json
|
||||
FROM trial_user_attributes
|
||||
WHERE trial_id = ?
|
||||
""", (trial_id,))
|
||||
user_attrs = {}
|
||||
for key, value_json in cursor.fetchall():
|
||||
try:
|
||||
user_attrs[key] = json.loads(value_json)
|
||||
except (ValueError, TypeError):
|
||||
user_attrs[key] = value_json
|
||||
|
||||
# Extract relevant metrics for results (mass, frequency, stress, displacement, etc.)
|
||||
results = {}
|
||||
if "mass" in user_attrs:
|
||||
results["mass"] = user_attrs["mass"]
|
||||
if "frequency" in user_attrs:
|
||||
results["frequency"] = user_attrs["frequency"]
|
||||
if "max_stress" in user_attrs:
|
||||
results["max_stress"] = user_attrs["max_stress"]
|
||||
if "max_displacement" in user_attrs:
|
||||
results["max_displacement"] = user_attrs["max_displacement"]
|
||||
# Fallback to first frequency from objectives if available
|
||||
if not results and len(values) > 0:
|
||||
results["first_frequency"] = values[0]
|
||||
|
||||
trials.append({
|
||||
"trial_number": trial_num,
|
||||
"objective": values[0] if len(values) > 0 else None, # Primary objective
|
||||
"objectives": values if len(values) > 1 else None, # All objectives for multi-objective
|
||||
"design_variables": params,
|
||||
"results": {"first_frequency": values[0]} if len(values) > 0 else {},
|
||||
"results": results,
|
||||
"user_attrs": user_attrs, # Include all user attributes
|
||||
"start_time": start_time,
|
||||
"end_time": end_time
|
||||
})
|
||||
@@ -488,3 +519,268 @@ async def get_pareto_front(study_id: str):
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get Pareto front: {str(e)}")
|
||||
|
||||
@router.post("/studies")
|
||||
async def create_study(
|
||||
config: str = Form(...),
|
||||
prt_file: Optional[UploadFile] = File(None),
|
||||
sim_file: Optional[UploadFile] = File(None),
|
||||
fem_file: Optional[UploadFile] = File(None)
|
||||
):
|
||||
"""
|
||||
Create a new optimization study
|
||||
Accepts:
|
||||
- config: JSON string with study configuration
|
||||
- prt_file: NX part file (optional if using existing study)
|
||||
- sim_file: NX simulation file (optional)
|
||||
- fem_file: NX FEM file (optional)
|
||||
"""
|
||||
try:
|
||||
# Parse config
|
||||
config_data = json.loads(config)
|
||||
study_name = config_data.get("name") # Changed from study_name to name to match frontend
|
||||
|
||||
if not study_name:
|
||||
raise HTTPException(status_code=400, detail="name is required in config")
|
||||
|
||||
# Create study directory structure
|
||||
study_dir = STUDIES_DIR / study_name
|
||||
if study_dir.exists():
|
||||
raise HTTPException(status_code=400, detail=f"Study {study_name} already exists")
|
||||
|
||||
setup_dir = study_dir / "1_setup"
|
||||
model_dir = setup_dir / "model"
|
||||
results_dir = study_dir / "2_results"
|
||||
|
||||
setup_dir.mkdir(parents=True, exist_ok=True)
|
||||
model_dir.mkdir(parents=True, exist_ok=True)
|
||||
results_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save config file
|
||||
config_file = setup_dir / "optimization_config.json"
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(config_data, f, indent=2)
|
||||
|
||||
# Save uploaded files
|
||||
files_saved = {}
|
||||
if prt_file:
|
||||
prt_path = model_dir / prt_file.filename
|
||||
with open(prt_path, 'wb') as f:
|
||||
content = await prt_file.read()
|
||||
f.write(content)
|
||||
files_saved['prt_file'] = str(prt_path)
|
||||
|
||||
if sim_file:
|
||||
sim_path = model_dir / sim_file.filename
|
||||
with open(sim_path, 'wb') as f:
|
||||
content = await sim_file.read()
|
||||
f.write(content)
|
||||
files_saved['sim_file'] = str(sim_path)
|
||||
|
||||
if fem_file:
|
||||
fem_path = model_dir / fem_file.filename
|
||||
with open(fem_path, 'wb') as f:
|
||||
content = await fem_file.read()
|
||||
f.write(content)
|
||||
files_saved['fem_file'] = str(fem_path)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=201,
|
||||
content={
|
||||
"status": "created",
|
||||
"study_id": study_name,
|
||||
"study_path": str(study_dir),
|
||||
"config_path": str(config_file),
|
||||
"files_saved": files_saved,
|
||||
"message": f"Study {study_name} created successfully. Ready to run optimization."
|
||||
}
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid JSON in config: {str(e)}")
|
||||
except Exception as e:
|
||||
# Clean up on error
|
||||
if 'study_dir' in locals() and study_dir.exists():
|
||||
shutil.rmtree(study_dir)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create study: {str(e)}")
|
||||
|
||||
@router.post("/studies/{study_id}/convert-mesh")
|
||||
async def convert_study_mesh(study_id: str):
|
||||
"""
|
||||
Convert study mesh to GLTF for 3D visualization
|
||||
Creates a web-viewable 3D model with FEA results as vertex colors
|
||||
"""
|
||||
try:
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
if not study_dir.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
|
||||
# Import mesh converter
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
|
||||
from optimization_engine.mesh_converter import convert_study_mesh
|
||||
|
||||
# Convert mesh
|
||||
output_path = convert_study_mesh(study_dir)
|
||||
|
||||
if output_path and output_path.exists():
|
||||
return {
|
||||
"status": "success",
|
||||
"gltf_path": str(output_path),
|
||||
"gltf_url": f"/api/optimization/studies/{study_id}/mesh/model.gltf",
|
||||
"metadata_url": f"/api/optimization/studies/{study_id}/mesh/model.json",
|
||||
"message": "Mesh converted successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Mesh conversion failed")
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to convert mesh: {str(e)}")
|
||||
|
||||
@router.get("/studies/{study_id}/mesh/{filename}")
|
||||
async def get_mesh_file(study_id: str, filename: str):
|
||||
"""
|
||||
Serve GLTF mesh files and metadata
|
||||
Supports .gltf, .bin, and .json files
|
||||
"""
|
||||
try:
|
||||
# Validate filename to prevent directory traversal
|
||||
if '..' in filename or '/' in filename or '\\' in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
visualization_dir = study_dir / "3_visualization"
|
||||
|
||||
file_path = visualization_dir / filename
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
||||
|
||||
# Determine content type
|
||||
suffix = file_path.suffix.lower()
|
||||
content_types = {
|
||||
'.gltf': 'model/gltf+json',
|
||||
'.bin': 'application/octet-stream',
|
||||
'.json': 'application/json',
|
||||
'.glb': 'model/gltf-binary'
|
||||
}
|
||||
|
||||
content_type = content_types.get(suffix, 'application/octet-stream')
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=content_type,
|
||||
filename=filename
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"File not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to serve mesh file: {str(e)}")
|
||||
|
||||
@router.post("/studies/{study_id}/generate-report")
|
||||
async def generate_report(
|
||||
study_id: str,
|
||||
format: str = "markdown",
|
||||
include_llm_summary: bool = False
|
||||
):
|
||||
"""
|
||||
Generate an optimization report in the specified format
|
||||
|
||||
Args:
|
||||
study_id: Study identifier
|
||||
format: Report format ('markdown', 'html', or 'pdf')
|
||||
include_llm_summary: Whether to include LLM-generated executive summary
|
||||
|
||||
Returns:
|
||||
Information about the generated report including download URL
|
||||
"""
|
||||
try:
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
if not study_dir.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
|
||||
# Validate format
|
||||
valid_formats = ['markdown', 'md', 'html', 'pdf']
|
||||
if format.lower() not in valid_formats:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid format. Must be one of: {', '.join(valid_formats)}")
|
||||
|
||||
# Import report generator
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
|
||||
from optimization_engine.report_generator import generate_study_report
|
||||
|
||||
# Generate report
|
||||
output_path = generate_study_report(
|
||||
study_dir=study_dir,
|
||||
output_format=format.lower(),
|
||||
include_llm_summary=include_llm_summary
|
||||
)
|
||||
|
||||
if output_path and output_path.exists():
|
||||
# Get relative path for URL
|
||||
rel_path = output_path.relative_to(study_dir)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format,
|
||||
"file_path": str(output_path),
|
||||
"download_url": f"/api/optimization/studies/{study_id}/reports/{output_path.name}",
|
||||
"file_size": output_path.stat().st_size,
|
||||
"message": f"Report generated successfully in {format} format"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Report generation failed")
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate report: {str(e)}")
|
||||
|
||||
@router.get("/studies/{study_id}/reports/{filename}")
|
||||
async def download_report(study_id: str, filename: str):
|
||||
"""
|
||||
Download a generated report file
|
||||
|
||||
Args:
|
||||
study_id: Study identifier
|
||||
filename: Report filename
|
||||
|
||||
Returns:
|
||||
Report file for download
|
||||
"""
|
||||
try:
|
||||
# Validate filename to prevent directory traversal
|
||||
if '..' in filename or '/' in filename or '\\' in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
results_dir = study_dir / "2_results"
|
||||
|
||||
file_path = results_dir / filename
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Report file {filename} not found")
|
||||
|
||||
# Determine content type
|
||||
suffix = file_path.suffix.lower()
|
||||
content_types = {
|
||||
'.md': 'text/markdown',
|
||||
'.html': 'text/html',
|
||||
'.pdf': 'application/pdf',
|
||||
'.json': 'application/json'
|
||||
}
|
||||
|
||||
content_type = content_types.get(suffix, 'application/octet-stream')
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=content_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Report file not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to download report: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user