Initial commit: Crumbforest Architecture Refinement v1 (Clean)
This commit is contained in:
215
app/routers/chat.py
Normal file
215
app/routers/chat.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
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 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
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
if chat_request.character_id not in CHARACTERS:
|
||||
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 = CHARACTERS[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)
|
||||
Reference in New Issue
Block a user