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:
166
app/stiftung/agent/models.py
Normal file
166
app/stiftung/agent/models.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
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]}"
|
||||
Reference in New Issue
Block a user