- 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>
233 lines
7.0 KiB
Python
233 lines
7.0 KiB
Python
"""
|
||
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})
|