""" FastAPI backend for human assessment of creative ideas. """ import json from datetime import datetime from pathlib import Path from typing import Any from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware try: from . import database as db from .models import ( DIMENSION_DEFINITIONS, ExportData, ExportRating, IdeaForRating, Progress, QueryInfo, QueryWithIdeas, Rater, RaterCreate, RaterProgress, Rating, RatingSubmit, Statistics, ) except ImportError: import database as db from models import ( DIMENSION_DEFINITIONS, ExportData, ExportRating, IdeaForRating, Progress, QueryInfo, QueryWithIdeas, Rater, RaterCreate, RaterProgress, Rating, RatingSubmit, Statistics, ) # Load assessment data DATA_PATH = Path(__file__).parent.parent / 'data' / 'assessment_items.json' def load_assessment_data() -> dict[str, Any]: """Load the assessment items data.""" if not DATA_PATH.exists(): raise RuntimeError(f"Assessment data not found at {DATA_PATH}. Run prepare_data.py first.") with open(DATA_PATH, 'r', encoding='utf-8') as f: return json.load(f) # Initialize FastAPI app app = FastAPI( title="Creative Idea Assessment API", description="API for human assessment of creative ideas using Torrance-inspired metrics", version="1.0.0" ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Cache for assessment data _assessment_data: dict[str, Any] | None = None def get_assessment_data() -> dict[str, Any]: """Get cached assessment data.""" global _assessment_data if _assessment_data is None: _assessment_data = load_assessment_data() return _assessment_data # Rater endpoints @app.get("/api/raters", response_model=list[Rater]) def list_raters() -> list[dict[str, Any]]: """List all registered raters.""" return db.list_raters() @app.post("/api/raters", response_model=Rater) def create_or_get_rater(rater_data: RaterCreate) -> dict[str, Any]: """Register a new rater or get existing one.""" return db.create_rater(rater_data.rater_id, rater_data.name) @app.get("/api/raters/{rater_id}", response_model=Rater) def get_rater(rater_id: str) -> dict[str, Any]: """Get a specific rater.""" rater = db.get_rater(rater_id) if not rater: raise HTTPException(status_code=404, detail="Rater not found") return rater # Query endpoints @app.get("/api/queries", response_model=list[QueryInfo]) def list_queries() -> list[dict[str, Any]]: """List all queries available for assessment.""" data = get_assessment_data() return [ { 'query_id': q['query_id'], 'query_text': q['query_text'], 'category': q.get('category', ''), 'idea_count': q['idea_count'] } for q in data['queries'] ] @app.get("/api/queries/{query_id}", response_model=QueryWithIdeas) def get_query_with_ideas(query_id: str) -> dict[str, Any]: """Get a query with all its ideas for rating (without hidden metadata).""" data = get_assessment_data() for query in data['queries']: if query['query_id'] == query_id: ideas = [ IdeaForRating( idea_id=idea['idea_id'], text=idea['text'], index=idx ) for idx, idea in enumerate(query['ideas']) ] return QueryWithIdeas( query_id=query['query_id'], query_text=query['query_text'], category=query.get('category', ''), ideas=ideas, total_count=len(ideas) ) raise HTTPException(status_code=404, detail="Query not found") @app.get("/api/queries/{query_id}/unrated", response_model=QueryWithIdeas) def get_unrated_ideas(query_id: str, rater_id: str) -> dict[str, Any]: """Get unrated ideas for a query by a specific rater.""" data = get_assessment_data() for query in data['queries']: if query['query_id'] == query_id: # Get already rated idea IDs rated_ids = db.get_rated_idea_ids(rater_id, query_id) # Filter to unrated ideas unrated_ideas = [ IdeaForRating( idea_id=idea['idea_id'], text=idea['text'], index=idx ) for idx, idea in enumerate(query['ideas']) if idea['idea_id'] not in rated_ids ] return QueryWithIdeas( query_id=query['query_id'], query_text=query['query_text'], category=query.get('category', ''), ideas=unrated_ideas, total_count=query['idea_count'] ) raise HTTPException(status_code=404, detail="Query not found") # Rating endpoints @app.post("/api/ratings", response_model=dict[str, Any]) def submit_rating(rating: RatingSubmit) -> dict[str, Any]: """Submit a rating for an idea.""" # Validate that rater exists rater = db.get_rater(rating.rater_id) if not rater: raise HTTPException(status_code=404, detail="Rater not found. Please register first.") # Validate idea exists data = get_assessment_data() idea_found = False for query in data['queries']: for idea in query['ideas']: if idea['idea_id'] == rating.idea_id: idea_found = True break if idea_found: break if not idea_found: raise HTTPException(status_code=404, detail="Idea not found") # If not skipped, require all ratings if not rating.skipped: if rating.originality is None or rating.elaboration is None or rating.coherence is None or rating.usefulness is None: raise HTTPException( status_code=400, detail="All dimensions must be rated unless skipping" ) # Save rating return db.save_rating( rater_id=rating.rater_id, idea_id=rating.idea_id, query_id=rating.query_id, originality=rating.originality, elaboration=rating.elaboration, coherence=rating.coherence, usefulness=rating.usefulness, skipped=rating.skipped ) @app.get("/api/ratings/{rater_id}/{idea_id}", response_model=Rating | None) def get_rating(rater_id: str, idea_id: str) -> dict[str, Any] | None: """Get a specific rating.""" return db.get_rating(rater_id, idea_id) @app.get("/api/ratings/rater/{rater_id}", response_model=list[Rating]) def get_ratings_by_rater(rater_id: str) -> list[dict[str, Any]]: """Get all ratings by a rater.""" return db.get_ratings_by_rater(rater_id) # Progress endpoints @app.get("/api/progress/{rater_id}", response_model=RaterProgress) def get_rater_progress(rater_id: str) -> RaterProgress: """Get complete progress for a rater.""" rater = db.get_rater(rater_id) if not rater: raise HTTPException(status_code=404, detail="Rater not found") data = get_assessment_data() # Get rated idea counts per query ratings = db.get_ratings_by_rater(rater_id) ratings_per_query: dict[str, int] = {} for r in ratings: qid = r['query_id'] ratings_per_query[qid] = ratings_per_query.get(qid, 0) + 1 # Build progress list query_progress = [] total_completed = 0 total_ideas = 0 for query in data['queries']: qid = query['query_id'] completed = ratings_per_query.get(qid, 0) total = query['idea_count'] query_progress.append(Progress( rater_id=rater_id, query_id=qid, completed_count=completed, total_count=total )) total_completed += completed total_ideas += total percentage = (total_completed / total_ideas * 100) if total_ideas > 0 else 0 return RaterProgress( rater_id=rater_id, queries=query_progress, total_completed=total_completed, total_ideas=total_ideas, percentage=round(percentage, 1) ) # Statistics endpoint @app.get("/api/statistics", response_model=Statistics) def get_statistics() -> Statistics: """Get overall assessment statistics.""" stats = db.get_statistics() return Statistics(**stats) # Dimension definitions endpoint @app.get("/api/dimensions") def get_dimensions() -> dict[str, Any]: """Get dimension definitions for the UI.""" return DIMENSION_DEFINITIONS # Export endpoint @app.get("/api/export", response_model=ExportData) def export_ratings() -> ExportData: """Export all ratings with hidden metadata for analysis.""" data = get_assessment_data() all_ratings = db.get_all_ratings() # Build idea lookup with hidden metadata idea_lookup: dict[str, dict[str, Any]] = {} query_lookup: dict[str, str] = {} for query in data['queries']: query_lookup[query['query_id']] = query['query_text'] for idea in query['ideas']: idea_lookup[idea['idea_id']] = { 'text': idea['text'], 'condition': idea['_hidden']['condition'], 'expert_name': idea['_hidden']['expert_name'], 'keyword': idea['_hidden']['keyword'] } # Build export ratings export_ratings = [] for r in all_ratings: idea_data = idea_lookup.get(r['idea_id'], {}) export_ratings.append(ExportRating( rater_id=r['rater_id'], idea_id=r['idea_id'], query_id=r['query_id'], query_text=query_lookup.get(r['query_id'], ''), idea_text=idea_data.get('text', ''), originality=r['originality'], elaboration=r['elaboration'], coherence=r['coherence'], usefulness=r['usefulness'], skipped=bool(r['skipped']), condition=idea_data.get('condition', ''), expert_name=idea_data.get('expert_name', ''), keyword=idea_data.get('keyword', ''), timestamp=r['timestamp'] )) return ExportData( experiment_id=data['experiment_id'], export_timestamp=datetime.utcnow(), rater_count=len(db.list_raters()), rating_count=len(export_ratings), ratings=export_ratings ) # Health check @app.get("/api/health") def health_check() -> dict[str, str]: """Health check endpoint.""" return {"status": "healthy"} # Info endpoint @app.get("/api/info") def get_info() -> dict[str, Any]: """Get assessment session info.""" data = get_assessment_data() return { 'experiment_id': data['experiment_id'], 'total_ideas': data['total_ideas'], 'query_count': data['query_count'], 'conditions': data['conditions'], 'randomization_seed': data['randomization_seed'] }