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>
This commit is contained in:
232
app/stiftung/agent/views.py
Normal file
232
app/stiftung/agent/views.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
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})
|
||||
Reference in New Issue
Block a user