""" Chat API Router Handles character chat interactions with RAG. """ from fastapi import APIRouter, Request, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from typing import Optional from slowapi import Limiter from slowapi.util import get_remote_address from deps import get_qdrant_client from config import get_settings from services.provider_factory import ProviderFactory from services.config_loader import ConfigLoader from utils.rag_chat import RAGChatService from utils.chat_logger import ChatLogger from utils.security import PromptInjectionFilter router = APIRouter() # Rate limiter (5 requests per minute per IP - strict for production) limiter = Limiter(key_func=get_remote_address) # Security filter security_filter = PromptInjectionFilter() # Character configurations CHARACTERS = { "eule": { "name": "Krümeleule", "prompt": ( "Du bist die Krümeleule, eine weise und geduldige Begleiterin im Crumbforest. " "Du hörst zuerst zu, stellst kluge Fragen und antwortest mit Ruhe und Respekt. " "Du schützt das Recht der Kinder zu fragen wie einen wertvollen Schatz. " "Deine Antworten sind kindgerecht, aber niemals herablassend. " "Du kennst das Terminal, Python, und die digitale Welt – aber du erklärst alles " "so, dass auch Anfänger es verstehen können. " "Antworte auf Deutsch, wenn nicht anders gewünscht." ) }, "fox": { "name": "FunkFox", "prompt": ( "Du bist FunkFox – der rappende Bit im Crumbforest! 🦊🎤\n" "Du antwortest IMMER in Rap-Form mit Reimen und Flow. " "Du bist neugierig, energetisch und tech-begeistert. " "Du liebst das Terminal, Code, Kommandos und alles Digitale. " "Deine Antworten sind cool, direkt und immer im Rhythmus. " "Du erklärst technische Dinge auf eine Art, die Spaß macht und hängenbleibt. " "Jede Antwort ist ein kleiner Rap – mit Reimen, Beats und Groove. " "Du bist hilfsbereit, wissbegierig und immer bereit für den nächsten Drop. " "Antworte auf Deutsch, wenn nicht anders gewünscht. " "Denk daran: Keep it real, keep it rhythmisch! 🌲🎧" ) }, "bugsy": { "name": "Bugsy", "prompt": ( "Du bist Bugsy – der freundliche Debugging-Begleiter im Crumbforest! 🐞\n" "Du machst Fehler sichtbar, aber NIEMALS mit Schuld oder Scham. " "Fehler sind für dich Einladungen zum Verstehen, keine Katastrophen. " "Du bist geduldig, ermunternd und technisch präzise. " "Wenn jemand einen Fehler hat, sagst du: 'Hey, lass uns das gemeinsam anschauen!' " "Du erklärst, WAS passiert ist, WARUM es passiert ist, und WIE man es löst. " "Du betonst immer: Fehler sind normal, jeder macht sie, sie sind Teil des Lernens. " "Deine Antworten sind strukturiert: Problem → Ursache → Lösung → Lernen. " "Du gibst konkrete Debug-Tipps, aber immer in einer freundlichen, ermutigenden Art. " "Du feierst kleine Erfolge ('Super, du hast den Fehler gefunden!') " "Antworte auf Deutsch, wenn nicht anders gewünscht. " "Denk daran: Kein Bug ist zu klein oder zu groß – wir schaffen das zusammen! 🐞💚" ) } # More characters can be added here in the future } def get_characters(): """ Get all available characters, merging hardcoded ones with config. """ # Start with hardcoded defaults chars = CHARACTERS.copy() # Load additional roles from config try: config = ConfigLoader.load_config() roles_config = config.get('roles', {}) for role_id, role_data in roles_config.items(): # Skip if already exists (hardcoded takes precedence? or config?) # Let's say config adds to or overrides if needed. # Map config format to chat format chars[role_id] = { "name": role_data.get("name", role_id), "prompt": role_data.get("system_prompt", "") } except Exception as e: print(f"Error loading roles from config: {e}") return chars class ChatRequest(BaseModel): """Chat request model.""" character_id: str = Field(..., max_length=50) question: str = Field(..., min_length=1, max_length=2000) lang: Optional[str] = Field(default="de", pattern="^(de|en)$") class ChatResponse(BaseModel): """Chat response model.""" answer: str sources: list context_found: bool provider: str model: str @router.post("/api/chat", response_model=ChatResponse) @limiter.limit("5/minute") async def chat_with_character(chat_request: ChatRequest, request: Request): """ Chat with a character using RAG. Args: chat_request: Chat request with character_id and question request: FastAPI request object for session access Returns: ChatResponse with answer and metadata """ # Validate character available_chars = get_characters() if chat_request.character_id not in available_chars: raise HTTPException( status_code=400, detail=f"Unknown character: {chat_request.character_id}" ) # Security: Check for prompt injection is_valid, error_msg = security_filter.validate(chat_request.question, max_length=2000) if not is_valid: raise HTTPException( status_code=400, detail=f"Invalid input: {error_msg}" ) character_config = available_chars[chat_request.character_id] # Get settings settings = get_settings() # Check if OpenRouter is available if not settings.openrouter_api_key: raise HTTPException( status_code=503, detail="AI service not configured. Please contact administrator." ) try: # Create providers embedding_provider = ProviderFactory.create_provider( provider_name="openrouter", settings=settings ) completion_provider = ProviderFactory.create_provider( provider_name="openrouter", settings=settings ) # Get Qdrant client qdrant_client = get_qdrant_client() # Create RAG chat service rag_service = RAGChatService( qdrant_client=qdrant_client, embedding_provider=embedding_provider, completion_provider=completion_provider ) # Generate answer with RAG result = rag_service.chat_with_context( question=chat_request.question, character_name=character_config["name"], character_prompt=character_config["prompt"], context_limit=3, lang=chat_request.lang ) # Log interaction user = request.session.get("user") session_id = request.session.get("session_id", "anonymous") logger = ChatLogger() logger.log_interaction( character_id=chat_request.character_id, character_name=character_config["name"], user_id=user.get("id") if user else None, user_role=user.get("role") if user else "anonymous", question=chat_request.question, answer=result["answer"], model=result["model"], provider=result["provider"], context_found=result["context_found"], sources_count=len(result["sources"]), lang=chat_request.lang, session_id=session_id ) # Return response return ChatResponse( answer=result["answer"], sources=result["sources"], context_found=result["context_found"], provider=result["provider"], model=result["model"] ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: raise HTTPException(status_code=503, detail=f"AI service error: {str(e)}") except Exception as e: print(f"❌ Chat error: {e}") import traceback traceback.print_exc() raise HTTPException(status_code=500, detail="Internal server error") @router.get("/api/chat/stats") async def get_chat_stats(): """ Get chat statistics. Returns: Statistics about logged interactions """ logger = ChatLogger() stats = logger.get_stats() return JSONResponse(content=stats)