summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/server.py295
1 files changed, 193 insertions, 102 deletions
diff --git a/api/server.py b/api/server.py
index c6c788c..e0f9472 100644
--- a/api/server.py
+++ b/api/server.py
@@ -1,9 +1,15 @@
"""
FastAPI server for the AI Evaluation Visualization Dashboard.
Serves read-only results, JSON logs, and static visuals.
+
+Discovers data from multiple directories:
+- CSV results: logs/full_experiment/, output/, and config.output_dir
+- Viz images: viz/ (project root) and output_dir/viz
+- Judge logs: judge_logs/ and logs/judge_full_experiment/
"""
import os
import sys
+import argparse
from pathlib import Path
from typing import Dict, Any, List
import pandas as pd
@@ -13,6 +19,7 @@ import math
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
+from fastapi.middleware.cors import CORSMiddleware
# Add project root to path to import core modules
PROJECT_ROOT = Path(__file__).parent.parent
@@ -22,7 +29,15 @@ from src.config_loader import ConfigLoader
app = FastAPI(
title="AI Agent Evaluation Dashboard",
description="Web GUI for analyzing experiment results",
- version="0.2.0"
+ version="0.3.0"
+)
+
+# CORS for local development
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
)
# Mount static files
@@ -30,6 +45,7 @@ static_dir = Path(__file__).parent / "static"
if static_dir.exists():
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
+
def get_config():
"""Helper to load config safely."""
try:
@@ -40,6 +56,56 @@ def get_config():
print(f"Warning: Could not load config: {e}")
return None
+
+def _discover_csv_dirs() -> List[Path]:
+ """Discover all directories containing CSV result files."""
+ candidates = [
+ PROJECT_ROOT / "logs" / "full_experiment",
+ PROJECT_ROOT / "logs" / "v2_experiment",
+ PROJECT_ROOT / "output",
+ ]
+ # Also add whatever the config says
+ config = get_config()
+ if config:
+ candidates.append(PROJECT_ROOT / config.output_dir)
+
+ return [d for d in candidates if d.exists()]
+
+
+def _discover_viz_dirs() -> List[Path]:
+ """Discover all directories containing visualization images."""
+ candidates = [
+ PROJECT_ROOT / "viz",
+ PROJECT_ROOT / "output" / "charts",
+ ]
+ config = get_config()
+ if config:
+ candidates.append(PROJECT_ROOT / config.output_dir / "viz")
+
+ return [d for d in candidates if d.exists()]
+
+
+def _discover_judge_dirs() -> List[Path]:
+ """Discover all directories containing judge logs."""
+ candidates = [
+ PROJECT_ROOT / "judge_logs",
+ PROJECT_ROOT / "logs" / "judge_full_experiment",
+ ]
+ config = get_config()
+ if config:
+ judge_log_dir = config._config.get('judge', {}).get('log_dir', 'judge_logs')
+ candidates.append(PROJECT_ROOT / judge_log_dir)
+
+ return [d for d in candidates if d.exists()]
+
+
+def _clean_value(val):
+ """Convert NaN/Inf to None for JSON serialization."""
+ if isinstance(val, float) and (math.isnan(val) or math.isinf(val)):
+ return None
+ return val
+
+
# ============================================================================
# Root Endpoint - Serve HTML
# ============================================================================
@@ -52,108 +118,93 @@ async def root():
return FileResponse(index_path)
return HTMLResponse(content="<h1>index.html not found</h1>", status_code=404)
+
# ============================================================================
# Results Endpoints
# ============================================================================
@app.get("/api/results/files")
async def get_results_files():
- """List all CSV files in the output directory."""
- config = get_config()
- if not config:
- raise HTTPException(status_code=500, detail="Config not loaded")
-
- output_dir = PROJECT_ROOT / config.output_dir
- if not output_dir.exists():
- return []
-
- files = []
- for p in output_dir.rglob("*.csv"):
- files.append(str(p.relative_to(output_dir)))
- return sorted(files)
+ """List all CSV files across all discovered result directories."""
+ all_files = {}
+ for csv_dir in _discover_csv_dirs():
+ dir_label = str(csv_dir.relative_to(PROJECT_ROOT))
+ for p in sorted(csv_dir.rglob("*.csv")):
+ rel = str(p.relative_to(csv_dir))
+ # Prefix with directory label to distinguish sources
+ key = f"{dir_label}/{rel}"
+ all_files[key] = str(p)
+
+ return sorted(all_files.keys())
+
@app.get("/api/results")
async def get_results(file: str = None):
"""Get experiment results as JSON, optionally for a specific file."""
- config = get_config()
- if not config:
- raise HTTPException(status_code=500, detail="Config not loaded")
-
- output_dir = PROJECT_ROOT / config.output_dir
-
- csv_files = []
if file:
- file_path = output_dir / file
+ # Resolve back to absolute path from the prefixed key
+ file_path = PROJECT_ROOT / file
if not file_path.exists() or ".." in file:
- raise HTTPException(status_code=404, detail="File not found")
- csv_files.append(file_path)
+ raise HTTPException(status_code=404, detail=f"File not found: {file}")
+ csv_files = [file_path]
else:
- csv_files = list(output_dir.glob("*.csv")) if output_dir.exists() else []
-
+ # Load everything from first discovered dir
+ csv_files = []
+ for csv_dir in _discover_csv_dirs():
+ csv_files.extend(csv_dir.glob("*.csv"))
+
results = {}
for csv_file in csv_files:
try:
df = pd.read_csv(csv_file)
-
- # Convert NaN values to None for JSON serialization
- def clean_value(val):
- if isinstance(val, float) and (math.isnan(val) or math.isinf(val)):
- return None
- return val
-
- # Clean each row
+
cleaned_data = []
for record in df.to_dict(orient="records"):
- cleaned_record = {k: clean_value(v) for k, v in record.items()}
+ cleaned_record = {k: _clean_value(v) for k, v in record.items()}
cleaned_data.append(cleaned_record)
-
+
results[csv_file.stem] = {
"columns": df.columns.tolist(),
"data": cleaned_data
}
except Exception as e:
results[csv_file.stem] = {"error": str(e)}
-
+
return results
+
@app.get("/api/results/images")
async def list_result_images():
- """List generated visualization images."""
- config = get_config()
- if not config:
- return []
-
- output_dir = PROJECT_ROOT / config.output_dir
- viz_dir = output_dir / "viz"
-
- if not viz_dir.exists():
- return []
-
+ """List generated visualization images from all viz directories."""
images = []
- for img in viz_dir.glob("*"):
- if img.suffix.lower() in ['.png', '.jpg', '.jpeg', '.gif', '.svg']:
- images.append({
- "name": img.name,
- "path": str(img.relative_to(PROJECT_ROOT))
- })
-
+ seen = set()
+ for viz_dir in _discover_viz_dirs():
+ for img in sorted(viz_dir.glob("*")):
+ if img.suffix.lower() in ['.png', '.jpg', '.jpeg', '.gif', '.svg']:
+ if img.name not in seen:
+ seen.add(img.name)
+ images.append({
+ "name": img.name,
+ "path": str(img.relative_to(PROJECT_ROOT)),
+ "source": str(viz_dir.relative_to(PROJECT_ROOT))
+ })
+
return images
+
@app.get("/api/results/images/{image_name}")
async def get_result_image(image_name: str):
- """Serve a specific image."""
- config = get_config()
- if not config:
- raise HTTPException(status_code=500, detail="Config not loaded")
-
- output_dir = PROJECT_ROOT / config.output_dir
- viz_dir = output_dir / "viz"
- image_path = viz_dir / image_name
-
- if not image_path.exists():
- raise HTTPException(status_code=404, detail="Image not found")
-
- return FileResponse(image_path)
+ """Serve a specific image from any viz directory."""
+ if ".." in image_name:
+ raise HTTPException(status_code=400, detail="Invalid path")
+
+ for viz_dir in _discover_viz_dirs():
+ image_path = viz_dir / image_name
+ if image_path.exists():
+ return FileResponse(image_path)
+
+ raise HTTPException(status_code=404, detail="Image not found")
+
# ============================================================================
# Judge Results Endpoints
@@ -161,54 +212,45 @@ async def get_result_image(image_name: str):
@app.get("/api/judge/files")
async def get_judge_files():
- """Get list of judge result files."""
- config = get_config()
- if not config:
- raise HTTPException(status_code=500, detail="Config not loaded")
-
- judge_log_dir = config._config.get('judge', {}).get('log_dir', 'judge_logs')
- judge_dir = PROJECT_ROOT / judge_log_dir
-
- if not judge_dir.exists():
- return []
-
- files = []
- for p in judge_dir.rglob("*"):
- if p.suffix in ['.csv', '.json']:
- files.append(str(p.relative_to(judge_dir)))
- return sorted(files)
+ """Get list of judge result files from all judge directories."""
+ all_files = {}
+ for judge_dir in _discover_judge_dirs():
+ dir_label = str(judge_dir.relative_to(PROJECT_ROOT))
+ for p in sorted(judge_dir.rglob("*")):
+ if p.suffix in ['.csv', '.json'] and p.is_file():
+ key = f"{dir_label}/{p.relative_to(judge_dir)}"
+ all_files[key] = str(p)
+
+ return sorted(all_files.keys())
+
@app.get("/api/judge/results")
async def get_judge_results(file: str = None):
"""Get judge results if available."""
- config = get_config()
- if not config:
- raise HTTPException(status_code=500, detail="Config not loaded")
-
- judge_log_dir = config._config.get('judge', {}).get('log_dir', 'judge_logs')
- judge_dir = PROJECT_ROOT / judge_log_dir
-
- if not judge_dir.exists():
- return {"message": "No judge results found"}
-
- result_files = []
if file:
- file_path = judge_dir / file
+ file_path = PROJECT_ROOT / file
if not file_path.exists() or ".." in file:
raise HTTPException(status_code=404, detail="File not found")
- result_files.append(file_path)
+ result_files = [file_path]
else:
- result_files = list(judge_dir.glob("*.csv")) + list(judge_dir.glob("*.json"))
-
+ result_files = []
+ for judge_dir in _discover_judge_dirs():
+ result_files.extend(judge_dir.glob("*.csv"))
+ result_files.extend(judge_dir.glob("*.json"))
+
results = {}
for rf in result_files:
if rf.suffix == '.csv':
try:
df = pd.read_csv(rf)
+ cleaned_data = []
+ for record in df.to_dict(orient="records"):
+ cleaned_record = {k: _clean_value(v) for k, v in record.items()}
+ cleaned_data.append(cleaned_record)
results[rf.stem] = {
"type": "csv",
"columns": df.columns.tolist(),
- "data": df.to_dict(orient="records")
+ "data": cleaned_data
}
except Exception as e:
results[rf.stem] = {"error": str(e)}
@@ -218,9 +260,58 @@ async def get_judge_results(file: str = None):
results[rf.stem] = {"type": "json", "data": json.load(f)}
except Exception as e:
results[rf.stem] = {"error": str(e)}
-
+
return results
+
+# ============================================================================
+# Experiment Logs Endpoints (NEW - browse raw JSON logs)
+# ============================================================================
+
+@app.get("/api/logs/dirs")
+async def get_log_dirs():
+ """List available log directories."""
+ logs_root = PROJECT_ROOT / "logs"
+ if not logs_root.exists():
+ return []
+
+ dirs = []
+ for d in sorted(logs_root.iterdir()):
+ if d.is_dir() and d.name != ".git":
+ json_count = len(list(d.rglob("*.json")))
+ if json_count > 0:
+ dirs.append({
+ "name": d.name,
+ "path": str(d.relative_to(PROJECT_ROOT)),
+ "files": json_count
+ })
+ return dirs
+
+
+@app.get("/api/logs/browse")
+async def browse_log(path: str):
+ """Browse a specific log JSON file."""
+ if ".." in path:
+ raise HTTPException(status_code=400, detail="Invalid path")
+
+ file_path = PROJECT_ROOT / path
+ if not file_path.exists() or file_path.suffix != '.json':
+ raise HTTPException(status_code=404, detail="Log file not found")
+
+ try:
+ with open(file_path) as f:
+ data = json.load(f)
+ return data
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
if __name__ == "__main__":
import uvicorn
- uvicorn.run(app, host="0.0.0.0", port=8000)
+
+ parser = argparse.ArgumentParser(description="AI Evaluation Dashboard Server")
+ parser.add_argument("--port", type=int, default=8000, help="Port to run on")
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to")
+ args = parser.parse_args()
+
+ uvicorn.run(app, host=args.host, port=args.port)