Content Studio
KI-gestütztes Content-Management mit Review-Workflow und RAG-Integration.
| Tool | /content |
|---|---|
| Datenbank | ki_system |
| Tabellen | content_orders, content_versions, content_critiques, content_sources |
Unterkapitel
| Kapitel | Beschreibung |
|---|---|
| Workflow | Status-Übergänge und Workflow-Steuerung |
| API | REST-API Endpoints und HTMX-Integration |
| Kritiker | KI-gestützte Qualitätsprüfung |
| RAG | Retrieval-Augmented Generation |
Web-UI (RESTful)
| URL | Beschreibung |
|---|---|
| /content | Auftrags-Liste mit Statistik und Filtern |
| /content/new | Neuen Auftrag erstellen |
| /content/{id} | Auftrags-Details anzeigen |
| /content/{id}/edit | Auftrag bearbeiten |
Workflow
| Status | Beschreibung |
|---|---|
| draft | Entwurf erstellt |
| generating | Content wird generiert |
| critique | Kritik-Runde läuft |
| revision | Revision wird erstellt |
| validate | Validierung ausstehend |
| approved | Content genehmigt |
| published | Content publiziert |
Konfiguration
- Autorenprofile: Tonalität und Schreibstil
- Contracts: Vertragliche Richtlinien
- Strukturen: Formatvorlagen (Blog, LinkedIn, etc.)
- Critics: KI-gestützte Qualitätsprüfung
KI-Backends
- Claude (Anthropic): Primäres Backend für Generierung
- Ollama (Mistral): Lokales Backend als Alternative
Schnellstart
Workflow
Status-Übergänge und Workflow-Steuerung im Content Studio.
| Controller | ContentController.php |
|---|---|
| Status-Feld | content_orders.status |
Status-Diagramm
draft → generating → critique → revision → validate → approved → published
↑ ↓ ↑
└──────────────┴───────────┘
Status-Beschreibung
| Status | Beschreibung | Nächste Aktion |
|---|---|---|
| draft | Auftrag erstellt, wartet auf Generierung | generate() |
| generating | Content wird von KI generiert | automatisch → critique |
| critique | Kritik-Runde läuft | critique() |
| revision | Revision nach Kritik erforderlich | revise() |
| validate | Alle Kritiker bestanden, wartet auf Freigabe | approve() / decline() |
| approved | Content vom Benutzer genehmigt | publish() |
| published | Content veröffentlicht | - |
Status-Übergänge im Code
generate() → Zeile 150-166
draft → generating → critique
- RAG-Kontext abrufen
- Content generieren
- Version speichern
- Status: critique
critique() → Zeile 169-194
critique → revision ODER validate
- Alle aktiven Kritiker durchlaufen
- Feedback speichern
- all_passed=true → validate
- all_passed=false → revision
revise() → Zeile 200-220
revision → generating → critique
- Kritik-Feedback laden
- Überarbeitete Version generieren
- Status: critique
approve() → Zeile 226-231
validate → approved
- Manuelle Freigabe
- updateOrderStatus(id, 'approve')
decline() → Zeile 237-242
validate → draft
- Zurück zum Entwurf
- updateOrderStatus(id, 'draft')
Kritik-Runden
Jede Kritik-Runde wird in content_orders.current_critique_round gezählt.
- Runde 1: Erste Kritik nach Generierung
- Runde 2+: Nach jeder Revision
- Maximal empfohlen: 3 Runden
Controller-Methoden
| Methode | Route | Status-Änderung |
|---|---|---|
| store() | POST /content | → draft |
| generate() | POST /content/{id}/generate | draft → generating → critique |
| critique() | POST /content/{id}/critique | critique → revision/validate |
| revise() | POST /content/{id}/revise | revision → generating → critique |
| approve() | POST /content/{id}/approve | validate → approved |
| decline() | POST /content/{id}/decline | validate → draft |
API
REST-API Endpoints und HTMX-Integration für das Content Studio.
| Controller | ContentController.php |
|---|---|
| Backend | web_generate.py |
| Base-URL | /content |
Web-UI Endpoints (RESTful)
| Methode | Route | Controller | Beschreibung |
|---|---|---|---|
| GET | /content | index() | Auftrags-Liste |
| GET | /content/new | contentNew() | Erstellungs-Formular |
| POST | /content | store() | Auftrag speichern |
| GET | /content/{id} | show() | Auftrags-Details |
| GET | /content/{id}/edit | edit() | Bearbeitungs-Formular |
HTMX-Endpoints
Diese Endpoints liefern HTML-Fragmente für dynamische Updates.
| Methode | Route | Controller | Response |
|---|---|---|---|
| POST | /content/{id}/generate | generate() | Polling-Partial |
| GET | /content/{id}/generation-status | generationStatus() | Status/Version-Partial |
| POST | /content/{id}/critique | critique() | Polling-Partial |
| GET | /content/{id}/critique-status | critiqueStatus() | Status/Critique-Partial |
| POST | /content/{id}/revise | revise() | Version-Partial |
| POST | /content/{id}/approve | approve() | Success-Alert + Reload |
| POST | /content/{id}/decline | decline() | Warning-Alert + Reload |
Asynchroner Workflow (Polling)
Die Generierung und Kritik laufen asynchron. Der Ablauf ist:
POST /content/{id}/generatestartet die Generierung und liefert ein Polling-Partial- Das Partial pollt via HTMX automatisch
GET /content/{id}/generation-status - Bei Status
generating: Polling-Partial mit Log wird zurückgegeben - Bei Status
completed: Fertiges Version-Partial wird zurückgegeben - Bei Status
failed: Fehler-Alert wird zurückgegeben
Gleiches Prinzip für Kritik:
POST /content/{id}/critiquestartet die Kritik- Polling via
GET /content/{id}/critique-status
Status-Werte
| Status | Bedeutung | Response |
|---|---|---|
| idle | Keine Operation aktiv | Leerer Response |
| generating / critiquing | Operation läuft | Polling-Partial mit Log |
| completed | Erfolgreich abgeschlossen | Ergebnis-Partial |
| failed | Fehlgeschlagen | Fehler-Alert |
REST-API Endpoint
| Methode | Route | Beschreibung |
|---|---|---|
| PUT | /api/v1/content/{id} | Auftrag aktualisieren |
Generate-Endpoint
Request
POST /content/{id}/generate
Content-Type: application/x-www-form-urlencoded
model=anthropic&collection=documents&context_limit=5
Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| model | string | anthropic | LLM-Backend (anthropic, ollama) |
| collection | string | documents | Qdrant-Collection |
| context_limit | int | 5 | Anzahl RAG-Chunks |
Response (Polling-Partial)
<div hx-get="/content/{id}/generation-status"
hx-trigger="every 2s"
hx-swap="outerHTML">
<div class="spinner">Generiere...</div>
<pre class="log">...</pre>
</div>
Generation-Status Endpoint
Request
GET /content/{id}/generation-status
Response bei completed (Version-Partial)
<div class="version-content">
<div class="version-header">
<span class="version-number">Version 1</span>
<span class="badge success">Generiert</span>
</div>
<div class="content-text">...</div>
<div class="sources">...</div>
</div>
Critique-Endpoint
Request
POST /content/{id}/critique
Content-Type: application/x-www-form-urlencoded
model=anthropic
Response (Polling-Partial)
<div hx-get="/content/{id}/critique-status"
hx-trigger="every 2s"
hx-swap="outerHTML">
<div class="spinner">Analysiere...</div>
</div>
Critique-Status Endpoint
Request
GET /content/{id}/critique-status
Response bei completed (Critique-Partial)
<div class="critique-results">
<div class="critique-header">
<span>Kritik-Runde 1</span>
<span class="badge warning">Revision nötig</span>
</div>
<div class="critique-item failed">...</div>
</div>
Python-Backend
Die Controller-Methoden rufen web_generate.py via Shell auf:
python web_generate.py generate <order_id> [model] [collection] [limit]
python web_generate.py critique <version_id> [model] [order_id]
python web_generate.py revise <version_id> [model]
Hinweis: Der optionale [order_id] Parameter bei critique aktiviert Live-Logging in der Datenbank (critique_log, critique_step). Ohne order_id läuft die Kritik im synchronen Modus ohne Live-Updates.
API-Update (curl)
curl -X PUT https://dev.campus.systemische-tools.de/api/v1/content/1 \
-H "Content-Type: application/json" \
-d '{
"title": "Blogpost über Teamcoaching",
"briefing": "Schreibe einen informativen Artikel..."
}'
Fehlerbehandlung
Bei Fehlern wird ein HTML-Alert zurückgegeben:
<div class="alert error">Fehler: [Fehlermeldung]</div>Kritiker
Qualitätsprüfungs-System mit LLM-basierten und deterministischen Kritikern.
| Modul | generators/critic.py, generators/format_checker.py |
|---|---|
| Tabelle | content_config (type='critic') |
| Ergebnisse | content_critiques |
Kritiker-Typen
| Typ | Kritiker | Methode | Zuverlässigkeit |
|---|---|---|---|
| Inhalt | Faktenprüfer, Stilist, Strukturanalyst | LLM | Gut für semantische Prüfung |
| Format | Formatierungsprüfer | Deterministisch | 100% zuverlässig |
Architektur
Content Version
↓
┌─────────────────────────────────────┐
│ Faktenprüfer (LLM) → 8/10 │
│ Stilist (LLM) → 8/10 │
│ Strukturanalyst (LLM) → 8/10 │
│ Formatierungsprüfer (Code) → 10/10 │
└─────────────────────────────────────┘
↓
all_passed? → validate : revision
Deterministischer Format-Checker
Datei: /var/www/scripts/pipeline/generators/format_checker.py
Geprüfte Regeln
| Regel | Quelle | Prüfung |
|---|---|---|
| emojis_verboten | structure.formatierung.emojis | Unicode-Ranges + Bullet-Emojis |
| markdown_verboten | structure.ausgabe.format = "reiner Text" | **bold**, *italic*, # headers |
| fettschrift_verboten | structure.formatierung.fettschrift | **text**, __text__ |
| gedankenstriche_verboten | profile.grammatik_und_satzbau.gedankenstriche | – (U+2013), — (U+2014) |
| hashtags_verboten | structure.formatierung.hashtags = "keine" | #Hashtag Pattern |
| ausrufezeichen_sparsam | profile.formatierung.ausrufezeichen | Max. 2 Ausrufezeichen |
Post-Processing
Datei: generators/persistence.py - strip_markdown()
Für output_format == "reiner Text" werden automatisch ersetzt:
- Gedankenstriche:
–→-,—→- - Anführungszeichen:
„"→"",‚'→'' - Markdown: **bold** → bold, *italic* → italic
Aktuelle Kritiker
| ID | Name | Typ | Fokus |
|---|---|---|---|
| 30 | Faktenprüfer | LLM | Quellen, Aktualität, Genauigkeit |
| 31 | Stilist | LLM | Stil, Tonalität, Fluss |
| 32 | Strukturanalyst | LLM | Gliederung, Absätze, Aufbau |
| 33 | Formatierungsprüfer | Code | Emojis, Markdown, Gedankenstriche |
Feedback-Format
{
"rating": 8,
"score": 8,
"passed": true,
"issues": ["Issue 1", "Issue 2"],
"suggestions": ["Suggestion 1"],
"summary": "Kurze Zusammenfassung",
"deterministic": false // true bei Formatierungsprüfer
}
API-Verwendung
# Einzelnen Kritiker ausführen
from generators.critic import run_critic
result = run_critic(content, critic_id=33, model="ollama:gemma3:27b-it-qat")
# Deterministisch prüfen (ohne DB)
from generators.format_checker import check_formatting
result = check_formatting(text, structure_config, profile_config)
Kritik-Ablauf
run_critic()
def run_critic(content, critic_id, model, structure_config=None, profile_config=None):
critic = get_critic(critic_id)
# Formatierungsprüfer: Deterministisch
if critic["name"] == "Formatierungsprüfer":
return check_formatting(content, structure_config, profile_config)
# Andere Kritiker: LLM
prompt = prompt_template.format(fokus=fokus_str, content=content)
response = call_llm(prompt, model)
return parse_json(response)
run_critique_round()
- Version-Content laden
- Alle aktiven Kritiker aus content_config laden
- Kritik-Runde inkrementieren
- Jeden Kritiker ausführen (LLM oder deterministisch)
- Feedback in content_critiques speichern
- Status basierend auf Ergebnis setzen
Warum deterministisch?
LLMs halluzinieren bei Zeichenprüfung. Beispiel:
# Text ohne Gedankenstriche
text = "Vertrauen ist keine Eigenschaft, sondern eine Beziehung."
# LLM-Formatierungsprüfer: 6/10 - "2 Gedankenstriche gefunden" (FALSCH!)
# Deterministischer Checker: 10/10 - "Keine Fehler" (KORREKT)
Der Code sucht exakt nach Unicode-Zeichen:
def check_gedankenstriche(text: str) -> list[dict]:
for i, char in enumerate(text):
if char == "–": # U+2013
issues.append({"type": "en_dash", "position": i})
if char == "—": # U+2014
issues.append({"type": "em_dash", "position": i})RAG
Retrieval-Augmented Generation für kontextbasierte Content-Erstellung.
| Modul | generate.py, embed.py |
|---|---|
| Vektor-DB | Qdrant |
| Quellen-Tabelle | content_sources |
Architektur
Briefing
↓
┌─────────────┐
│ Embedding │ → Vektor aus Briefing
└─────────────┘
↓
┌─────────────┐
│ Qdrant │ → Ähnlichkeitssuche
└─────────────┘
↓
┌─────────────┐
│ Kontext │ → Top-N relevante Chunks
└─────────────┘
↓
┌─────────────┐
│ LLM │ → Content mit Kontext
└─────────────┘
Collections
| Collection | Inhalt | Verwendung |
|---|---|---|
| documents | Dokumente, PDFs | Allgemeine Wissensbasis |
| entities | Entitäten, Konzepte | Fachbegriffe, Personen |
| E-Mail-Korrespondenz | Kommunikationshistorie |
get_rag_context() - Zeile 18-35
def get_rag_context(briefing, collection="documents", limit=5):
"""
Relevanten Kontext aus Qdrant laden.
Args:
briefing: Suchtext für Ähnlichkeitssuche
collection: Qdrant-Collection
limit: Anzahl Ergebnisse
Returns:
List[{content, source, score}]
"""
results = search_similar(briefing, collection, limit)
return [{
"content": r["payload"]["content"],
"source": r["payload"]["document_title"],
"score": round(r["score"], 4)
} for r in results]
Kontext-Limit
| Limit | Tokens (~) | Empfehlung |
|---|---|---|
| 3 | ~1500 | Schnelle Generierung, wenig Kontext |
| 5 | ~2500 | Standard, ausgewogen |
| 10 | ~5000 | Umfangreicher Kontext, langsamer |
Prompt-Integration
Der RAG-Kontext wird in den Generierungs-Prompt eingebettet:
## Kontext aus der Wissensbasis:
[Quelle 1: Dokument A]
Lorem ipsum dolor sit amet...
[Quelle 2: Dokument B]
Consectetur adipiscing elit...
## Briefing:
Schreibe einen Artikel über...
Quellen-Speicherung
Nach der Generierung werden Quellen in content_sources gespeichert:
def save_sources(order_id, context):
for ctx in context:
# Chunk-ID via Content-Match finden
chunk = db.execute(
"SELECT id FROM chunks WHERE content LIKE %s",
(ctx["content"][:100] + "%",)
)
# Quelle mit Relevanz-Score speichern
db.execute(
"INSERT INTO content_sources
(order_id, chunk_id, relevance_score)
VALUES (%s, %s, %s)",
(order_id, chunk.id, ctx["score"])
)
Relevanz-Score
Der Score zeigt die semantische Ähnlichkeit (0.0 - 1.0):
| Score | Interpretation |
|---|---|
| > 0.85 | Sehr relevant |
| 0.70 - 0.85 | Relevant |
| 0.50 - 0.70 | Möglicherweise relevant |
| < 0.50 | Geringe Relevanz |
UI-Anzeige
Quellen werden in der Version-Ansicht angezeigt:
<div class="sources">
<strong>Quellen:</strong>
<ul>
<li>Dokument A (85%)</li>
<li>Dokument B (72%)</li>
</ul>
</div>
Qdrant-Verbindung
Die Verbindung wird in embed.py konfiguriert:
from qdrant_client import QdrantClient
client = QdrantClient(host="localhost", port=6333)