""" Views für den AI Agent. Endpunkte: POST /agent/chat/ – Neue Nachricht senden (startet neue oder bestehende Session) GET /agent/chat/stream/ – SSE-Stream für laufende Anfrage GET /agent/sessions/ – Liste der Chat-Sitzungen (JSON) DELETE /agent/sessions// – Sitzung löschen """ from __future__ import annotations import json import logging from django.contrib.auth.decorators import login_required from django.core.cache import cache from django.http import ( HttpResponse, JsonResponse, StreamingHttpResponse, ) from django.shortcuts import get_object_or_404 from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from .models import AgentConfig, ChatSession, ChatMessage from .orchestrator import run_agent_stream logger = logging.getLogger(__name__) RATE_LIMIT_PER_MINUTE = 20 def _check_rate_limit(user_id: int) -> bool: """Einfaches Rate-Limiting via Django-Cache (Redis). True = erlaubt.""" key = f"agent_rl_{user_id}" count = cache.get(key, 0) if count >= RATE_LIMIT_PER_MINUTE: return False cache.set(key, count + 1, timeout=60) return True def _require_agent_permission(user) -> bool: """Prüft ob der Benutzer den Agent nutzen darf.""" return ( user.is_superuser or user.has_perm("stiftung.can_use_agent") ) @login_required @require_http_methods(["GET"]) def agent_index(request): """Einstiegsseite für den Chat (wird als Modal geöffnet, nicht direkt navigiert).""" config = AgentConfig.get_config() return JsonResponse({ "provider": config.provider, "model": config.model_name, "allow_write": config.allow_write, }) @login_required @require_http_methods(["POST"]) def agent_chat(request): """ Startet oder setzt einen Chat fort. Body (JSON): { "message": "Wie viele aktive Destinatäre gibt es?", "session_id": "optional-uuid", "page_context": "optional – aktueller Seiteninhalt als Text" } Antwort: { "session_id": "...", "stream_url": "/agent/chat/stream/?session_id=..." } """ if not _require_agent_permission(request.user): return JsonResponse({"error": "Keine Berechtigung für den AI-Assistenten."}, status=403) if not _check_rate_limit(request.user.id): return JsonResponse( {"error": "Rate-Limit erreicht. Bitte warten Sie eine Minute."}, status=429, ) try: body = json.loads(request.body) except (json.JSONDecodeError, ValueError): return JsonResponse({"error": "Ungültiger JSON-Body."}, status=400) message = (body.get("message") or "").strip() if not message: return JsonResponse({"error": "Nachricht darf nicht leer sein."}, status=400) page_context = (body.get("page_context") or "")[:2000] session_id = body.get("session_id") # Session ermitteln oder neu erstellen session = None if session_id: try: session = ChatSession.objects.get(id=session_id, user=request.user) except ChatSession.DoesNotExist: pass if session is None: session = ChatSession.objects.create(user=request.user) # Nachricht + Page-Context in Cache für Stream-Endpunkt speichern cache_key = f"agent_pending_{session.id}" cache.set( cache_key, {"message": message, "page_context": page_context}, timeout=300, # 5 Minuten ) return JsonResponse({ "session_id": str(session.id), "stream_url": f"/agent/chat/stream/?session_id={session.id}", }) @login_required @require_http_methods(["GET"]) def agent_chat_stream(request): """ SSE-Endpunkt: streamt die Antwort des Agenten. Query-Params: session_id: UUID der Chat-Sitzung """ if not _require_agent_permission(request.user): return HttpResponse("Keine Berechtigung.", status=403) session_id = request.GET.get("session_id") if not session_id: return HttpResponse("session_id fehlt.", status=400) session = get_object_or_404(ChatSession, id=session_id, user=request.user) cache_key = f"agent_pending_{session.id}" pending = cache.get(cache_key) if not pending: return HttpResponse("Keine ausstehende Nachricht gefunden.", status=400) cache.delete(cache_key) message = pending["message"] page_context = pending.get("page_context", "") def event_stream(): try: yield from run_agent_stream( session=session, user_message=message, page_context=page_context, user=request.user, ) except Exception as e: logger.error("Agent-Stream-Fehler: %s", e, exc_info=True) import json yield f"data: {json.dumps({'type': 'error', 'message': 'Interner Fehler.'})}\n\n" response = StreamingHttpResponse( event_stream(), content_type="text/event-stream", ) response["Cache-Control"] = "no-cache" response["X-Accel-Buffering"] = "no" return response @login_required @require_http_methods(["GET"]) def agent_sessions(request): """Gibt die Chat-Sitzungen des Benutzers zurück (letzte 20).""" if not _require_agent_permission(request.user): return JsonResponse({"error": "Keine Berechtigung."}, status=403) sessions = ChatSession.objects.filter(user=request.user).order_by("-updated_at")[:20] data = [] for s in sessions: data.append({ "id": str(s.id), "title": s.title or "Neue Unterhaltung", "created_at": s.created_at.isoformat(), "updated_at": s.updated_at.isoformat(), "message_count": s.messages.count(), }) return JsonResponse({"sessions": data}) @login_required @require_http_methods(["GET"]) def agent_session_messages(request, session_id): """Gibt alle Nachrichten einer Sitzung zurück.""" if not _require_agent_permission(request.user): return JsonResponse({"error": "Keine Berechtigung."}, status=403) session = get_object_or_404(ChatSession, id=session_id, user=request.user) messages = session.messages.exclude(role="tool").order_by("created_at") data = [] for m in messages: data.append({ "id": str(m.id), "role": m.role, "content": m.content, "created_at": m.created_at.isoformat(), }) return JsonResponse({ "session_id": str(session.id), "title": session.title, "messages": data, }) @login_required @require_http_methods(["POST"]) def agent_session_delete(request, session_id): """Löscht eine Chat-Sitzung.""" if not _require_agent_permission(request.user): return JsonResponse({"error": "Keine Berechtigung."}, status=403) session = get_object_or_404(ChatSession, id=session_id, user=request.user) session.delete() return JsonResponse({"ok": True})