Files
Crumb-Core-v.1/app/routers/chat.py

243 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)