""" LLM Comparison Route - Vergleicht Antworten verschiedener LLM Backends. Dieses Modul ermoeglicht: - Parallele Anfragen an OpenAI, Claude, Self-hosted+Tavily, Self-hosted+EduSearch - Speichern von Vergleichsergebnissen fuer QA - Parameter-Tuning fuer Self-hosted Modelle """ import asyncio import logging import uuid from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, HTTPException, Depends from ..middleware.auth import verify_api_key from .comparison_models import ( ComparisonRequest, LLMResponse, ComparisonResponse, SavedComparison, _comparisons_store, _system_prompts_store, ) from .comparison_providers import ( call_openai, call_claude, search_tavily, search_edusearch, call_selfhosted_with_search, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/comparison", tags=["LLM Comparison"]) @router.post("/run", response_model=ComparisonResponse) async def run_comparison( request: ComparisonRequest, _: str = Depends(verify_api_key), ): """ Fuehrt LLM-Vergleich durch. Sendet den Prompt parallel an alle aktivierten Provider und sammelt die Antworten. """ comparison_id = f"cmp-{uuid.uuid4().hex[:12]}" tasks = [] system_prompt = request.system_prompt if request.enable_openai: tasks.append(("openai", call_openai(request.prompt, system_prompt))) if request.enable_claude: tasks.append(("claude", call_claude(request.prompt, system_prompt))) if request.enable_selfhosted_tavily: tavily_results = await search_tavily(request.prompt, request.search_results_count) tasks.append(( "selfhosted_tavily", call_selfhosted_with_search( request.prompt, system_prompt, "tavily", tavily_results, request.selfhosted_model, request.temperature, request.top_p, request.max_tokens, ) )) if request.enable_selfhosted_edusearch: edu_results = await search_edusearch( request.prompt, request.search_results_count, request.edu_search_filters, ) tasks.append(( "selfhosted_edusearch", call_selfhosted_with_search( request.prompt, system_prompt, "edusearch", edu_results, request.selfhosted_model, request.temperature, request.top_p, request.max_tokens, ) )) responses = [] if tasks: results = await asyncio.gather(*[t[1] for t in tasks], return_exceptions=True) for (name, _), result in zip(tasks, results): if isinstance(result, Exception): responses.append(LLMResponse( provider=name, model="unknown", response="", latency_ms=0, error=str(result), )) else: responses.append(result) return ComparisonResponse( comparison_id=comparison_id, prompt=request.prompt, system_prompt=system_prompt, responses=responses, ) @router.post("/save/{comparison_id}") async def save_comparison( comparison_id: str, comparison: ComparisonResponse, notes: Optional[str] = None, rating: Optional[dict] = None, _: str = Depends(verify_api_key), ): """Speichert einen Vergleich fuer spaetere Analyse.""" saved = SavedComparison( comparison_id=comparison_id, prompt=comparison.prompt, system_prompt=comparison.system_prompt, responses=comparison.responses, notes=notes, rating=rating, created_at=comparison.created_at, ) _comparisons_store[comparison_id] = saved return {"status": "saved", "comparison_id": comparison_id} @router.get("/history") async def get_comparison_history( limit: int = 50, _: str = Depends(verify_api_key), ): """Gibt gespeicherte Vergleiche zurueck.""" comparisons = list(_comparisons_store.values()) comparisons.sort(key=lambda x: x.created_at, reverse=True) return {"comparisons": comparisons[:limit]} @router.get("/history/{comparison_id}") async def get_comparison( comparison_id: str, _: str = Depends(verify_api_key), ): """Gibt einen bestimmten Vergleich zurueck.""" if comparison_id not in _comparisons_store: raise HTTPException(status_code=404, detail="Vergleich nicht gefunden") return _comparisons_store[comparison_id] # System Prompt Management @router.get("/prompts") async def list_system_prompts( _: str = Depends(verify_api_key), ): """Listet alle gespeicherten System Prompts.""" return {"prompts": list(_system_prompts_store.values())} @router.post("/prompts") async def create_system_prompt( name: str, prompt: str, _: str = Depends(verify_api_key), ): """Erstellt einen neuen System Prompt.""" prompt_id = f"sp-{uuid.uuid4().hex[:8]}" _system_prompts_store[prompt_id] = { "id": prompt_id, "name": name, "prompt": prompt, "created_at": datetime.now(timezone.utc).isoformat(), } return {"status": "created", "prompt_id": prompt_id} @router.put("/prompts/{prompt_id}") async def update_system_prompt( prompt_id: str, name: str, prompt: str, _: str = Depends(verify_api_key), ): """Aktualisiert einen System Prompt.""" if prompt_id not in _system_prompts_store: raise HTTPException(status_code=404, detail="System Prompt nicht gefunden") _system_prompts_store[prompt_id].update({ "name": name, "prompt": prompt, "updated_at": datetime.now(timezone.utc).isoformat(), }) return {"status": "updated", "prompt_id": prompt_id} @router.delete("/prompts/{prompt_id}") async def delete_system_prompt( prompt_id: str, _: str = Depends(verify_api_key), ): """Loescht einen System Prompt.""" if prompt_id not in _system_prompts_store: raise HTTPException(status_code=404, detail="System Prompt nicht gefunden") if prompt_id in ["default", "curriculum", "worksheet"]: raise HTTPException(status_code=400, detail="Standard-Prompts koennen nicht geloescht werden") del _system_prompts_store[prompt_id] return {"status": "deleted", "prompt_id": prompt_id} @router.get("/prompts/{prompt_id}") async def get_system_prompt( prompt_id: str, _: str = Depends(verify_api_key), ): """Gibt einen System Prompt zurueck.""" if prompt_id not in _system_prompts_store: raise HTTPException(status_code=404, detail="System Prompt nicht gefunden") return _system_prompts_store[prompt_id]