243 lines
8.5 KiB
Python
243 lines
8.5 KiB
Python
"""
|
||
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)
|