- 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>
167 lines
4.8 KiB
Python
167 lines
4.8 KiB
Python
"""
|
||
AI Agent Models: AgentConfig (Singleton), ChatSession, ChatMessage.
|
||
"""
|
||
import uuid
|
||
|
||
from django.contrib.auth.models import User
|
||
from django.db import models
|
||
|
||
DEFAULT_SYSTEM_PROMPT = """Du bist RentmeisterAI, der KI-Assistent der van Hees-Theyssen-Vogel'schen Stiftung.
|
||
|
||
Du hast Zugriff auf die Stiftungsdatenbank und kannst Informationen zu Destinatären, Ländereien, \
|
||
Finanzen, Förderungen und weiteren Stiftungsdaten abrufen.
|
||
|
||
Regeln:
|
||
- Antworte stets auf Deutsch, präzise und sachlich.
|
||
- Schütze personenbezogene Daten – gib keine unnötigen Details heraus.
|
||
- Du kannst keine Daten ändern, nur lesen.
|
||
- Bei rechtlichen oder steuerlichen Fragen weise auf Fachberatung hin.
|
||
- Wenn du dir unsicher bist, sage das klar.
|
||
"""
|
||
|
||
|
||
class AgentConfig(models.Model):
|
||
"""Singleton-Konfiguration für den AI Agent."""
|
||
|
||
PROVIDER_CHOICES = [
|
||
("ollama", "Ollama (lokal)"),
|
||
("openai", "OpenAI"),
|
||
("anthropic", "Anthropic"),
|
||
]
|
||
|
||
provider = models.CharField(
|
||
max_length=20,
|
||
choices=PROVIDER_CHOICES,
|
||
default="ollama",
|
||
verbose_name="LLM-Provider",
|
||
)
|
||
model_name = models.CharField(
|
||
max_length=100,
|
||
default="qwen2.5:3b",
|
||
verbose_name="Modell-Name",
|
||
)
|
||
ollama_url = models.CharField(
|
||
max_length=255,
|
||
default="http://ollama:11434",
|
||
verbose_name="Ollama-URL",
|
||
)
|
||
openai_api_key = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
verbose_name="OpenAI API-Key",
|
||
help_text="Nur erforderlich wenn Provider = OpenAI",
|
||
)
|
||
anthropic_api_key = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
verbose_name="Anthropic API-Key",
|
||
help_text="Nur erforderlich wenn Provider = Anthropic",
|
||
)
|
||
system_prompt = models.TextField(
|
||
default=DEFAULT_SYSTEM_PROMPT,
|
||
verbose_name="System-Prompt",
|
||
)
|
||
allow_write = models.BooleanField(
|
||
default=False,
|
||
verbose_name="Schreib-Tools erlaubt",
|
||
help_text="Achtung: Schreib-Zugriff auf Stiftungsdaten aktivieren",
|
||
)
|
||
chat_retention_days = models.IntegerField(
|
||
default=30,
|
||
verbose_name="Chat-Verlauf Aufbewahrung (Tage)",
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = "Agent-Konfiguration"
|
||
verbose_name_plural = "Agent-Konfiguration"
|
||
|
||
def __str__(self):
|
||
return f"Agent Config ({self.get_provider_display()} / {self.model_name})"
|
||
|
||
def save(self, *args, **kwargs):
|
||
# Singleton: always use pk=1
|
||
self.pk = 1
|
||
super().save(*args, **kwargs)
|
||
|
||
def delete(self, *args, **kwargs):
|
||
pass # Singleton cannot be deleted
|
||
|
||
@classmethod
|
||
def get_config(cls):
|
||
config, _ = cls.objects.get_or_create(pk=1)
|
||
return config
|
||
|
||
|
||
class ChatSession(models.Model):
|
||
"""Chat-Sitzung eines Benutzers mit dem AI Agent."""
|
||
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
user = models.ForeignKey(
|
||
User,
|
||
on_delete=models.CASCADE,
|
||
related_name="agent_sessions",
|
||
verbose_name="Benutzer",
|
||
)
|
||
title = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name="Titel",
|
||
help_text="Automatisch aus erster Nachricht generiert",
|
||
)
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt")
|
||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Zuletzt aktiv")
|
||
|
||
class Meta:
|
||
verbose_name = "Chat-Sitzung"
|
||
verbose_name_plural = "Chat-Sitzungen"
|
||
ordering = ["-updated_at"]
|
||
|
||
def __str__(self):
|
||
return f"{self.user.username} – {self.title or str(self.id)[:8]} ({self.created_at.strftime('%d.%m.%Y')})"
|
||
|
||
def message_count(self):
|
||
return self.messages.count()
|
||
|
||
|
||
class ChatMessage(models.Model):
|
||
"""Einzelne Nachricht in einer Chat-Sitzung."""
|
||
|
||
ROLE_CHOICES = [
|
||
("user", "Benutzer"),
|
||
("assistant", "Assistent"),
|
||
("tool", "Tool-Ergebnis"),
|
||
]
|
||
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
session = models.ForeignKey(
|
||
ChatSession,
|
||
on_delete=models.CASCADE,
|
||
related_name="messages",
|
||
verbose_name="Sitzung",
|
||
)
|
||
role = models.CharField(
|
||
max_length=20,
|
||
choices=ROLE_CHOICES,
|
||
verbose_name="Rolle",
|
||
)
|
||
content = models.TextField(verbose_name="Inhalt")
|
||
tool_name = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
verbose_name="Tool-Name",
|
||
)
|
||
tool_call_id = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
verbose_name="Tool-Call-ID",
|
||
)
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt")
|
||
|
||
class Meta:
|
||
verbose_name = "Chat-Nachricht"
|
||
verbose_name_plural = "Chat-Nachrichten"
|
||
ordering = ["created_at"]
|
||
|
||
def __str__(self):
|
||
return f"[{self.role}] {self.content[:60]}"
|