feat: Add dashboard chat integration and MCP server
Major changes: - Dashboard: WebSocket-based chat with session management - Dashboard: New chat components (ChatPane, ChatInput, ModeToggle) - Dashboard: Enhanced UI with parallel coordinates chart - MCP Server: New atomizer-tools server for Claude integration - Extractors: Enhanced Zernike OPD extractor - Reports: Improved report generator New studies (configs and scripts only): - M1 Mirror: Cost reduction campaign studies - Simple Beam, Simple Bracket, UAV Arm studies Note: Large iteration data (2_iterations/, best_design_archive/) excluded via .gitignore - kept on local Gitea only. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -550,3 +550,158 @@ async def get_insights_summary(study_id: str):
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/studies/{study_id}/new-best")
|
||||
async def check_new_best(study_id: str):
|
||||
"""Check if there's a new best solution that needs insight generation.
|
||||
|
||||
The optimization script writes new_best.json when a new best is found.
|
||||
Dashboard can poll this endpoint to auto-generate insights.
|
||||
"""
|
||||
try:
|
||||
study_path = resolve_study_path(study_id)
|
||||
new_best_file = study_path / "3_results" / "new_best.json"
|
||||
|
||||
if not new_best_file.exists():
|
||||
return {
|
||||
"study_id": study_id,
|
||||
"has_new_best": False,
|
||||
"new_best": None
|
||||
}
|
||||
|
||||
with open(new_best_file) as f:
|
||||
new_best = json.load(f)
|
||||
|
||||
return {
|
||||
"study_id": study_id,
|
||||
"has_new_best": new_best.get("needs_insights", False),
|
||||
"new_best": new_best
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/studies/{study_id}/auto-insights")
|
||||
async def generate_auto_insights(study_id: str):
|
||||
"""Generate insights for the current best solution based on config.
|
||||
|
||||
Reads insights config from optimization_config.json and generates
|
||||
all enabled insights for the best (or latest new best) iteration.
|
||||
Clears the needs_insights flag after generation.
|
||||
"""
|
||||
try:
|
||||
study_path = resolve_study_path(study_id)
|
||||
|
||||
# Find the iteration to generate insights for
|
||||
new_best_file = study_path / "3_results" / "new_best.json"
|
||||
iteration_id = None
|
||||
|
||||
if new_best_file.exists():
|
||||
with open(new_best_file) as f:
|
||||
new_best = json.load(f)
|
||||
iteration_id = new_best.get("iteration_folder")
|
||||
|
||||
if not iteration_id:
|
||||
# Fall back to finding best from database or latest iteration
|
||||
iter_dir = study_path / "2_iterations"
|
||||
if iter_dir.exists():
|
||||
iterations = sorted(
|
||||
[d for d in iter_dir.iterdir() if d.is_dir() and any(d.glob("*.op2"))],
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True
|
||||
)
|
||||
if iterations:
|
||||
iteration_id = iterations[0].name
|
||||
|
||||
if not iteration_id:
|
||||
raise HTTPException(status_code=400, detail="No iteration found for insight generation")
|
||||
|
||||
# Load insights config
|
||||
config_path = study_path / "1_setup" / "optimization_config.json"
|
||||
if not config_path.exists():
|
||||
config_path = study_path / "optimization_config.json"
|
||||
|
||||
if not config_path.exists():
|
||||
raise HTTPException(status_code=400, detail="No optimization_config.json found")
|
||||
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
|
||||
insights_config = config.get("insights", [])
|
||||
if not insights_config:
|
||||
return {
|
||||
"study_id": study_id,
|
||||
"message": "No insights configured in optimization_config.json",
|
||||
"results": []
|
||||
}
|
||||
|
||||
# Generate insights
|
||||
from optimization_engine.insights import get_insight, InsightConfig
|
||||
|
||||
iter_path = study_path / "2_iterations" / iteration_id
|
||||
op2_files = list(iter_path.glob("*.op2"))
|
||||
|
||||
if not op2_files:
|
||||
raise HTTPException(status_code=400, detail=f"No OP2 file in {iteration_id}")
|
||||
|
||||
op2_path = op2_files[0]
|
||||
results = []
|
||||
|
||||
for insight_spec in insights_config:
|
||||
if not insight_spec.get("enabled", True):
|
||||
continue
|
||||
|
||||
insight_type = insight_spec.get("type")
|
||||
insight_name = insight_spec.get("name", insight_type)
|
||||
|
||||
try:
|
||||
insight = get_insight(insight_type, study_path)
|
||||
if insight:
|
||||
insight.op2_path = op2_path
|
||||
config_obj = InsightConfig(extra=insight_spec.get("config", {}))
|
||||
result = insight.generate(config_obj)
|
||||
|
||||
results.append({
|
||||
"type": insight_type,
|
||||
"name": insight_name,
|
||||
"success": result.success,
|
||||
"html_path": str(result.html_path) if result.html_path else None,
|
||||
"error": result.error
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"type": insight_type,
|
||||
"name": insight_name,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
# Clear the needs_insights flag
|
||||
if new_best_file.exists():
|
||||
with open(new_best_file) as f:
|
||||
new_best = json.load(f)
|
||||
new_best["needs_insights"] = False
|
||||
with open(new_best_file, 'w') as f:
|
||||
json.dump(new_best, f, indent=2)
|
||||
|
||||
return {
|
||||
"study_id": study_id,
|
||||
"iteration": iteration_id,
|
||||
"results": results,
|
||||
"summary": {
|
||||
"total": len(results),
|
||||
"successful": sum(1 for r in results if r["success"]),
|
||||
"failed": sum(1 for r in results if not r["success"])
|
||||
}
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ImportError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Insights module not available: {str(e)}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
Reference in New Issue
Block a user