Files
stiftung-management-system/app/stiftung/agent/views.py
SysAdmin Agent e0b377014c
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- Save cover email body as DMS document with new 'email' context type
- Show email body separately from attachments in email detail view
- Add per-category DMS document assignment in quarterly confirmation
  (Studiennachweis, Einkommenssituation, Vermögenssituation)
- Add VERSION file and context processor for automatic version display
- Add MCP server, agent system, import/export, and new migrations
- Update compose files and production environment template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:48:52 +00:00

233 lines
7.0 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.

"""
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/<id>/ 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})