Content Studio

Erstellt: 2025-12-20 | Aktualisiert: 2025-12-20

KI-gestütztes Content-Management mit Review-Workflow und RAG-Integration.

Tool/content
Datenbankki_system
Tabellencontent_orders, content_versions, content_critiques, content_sources

Unterkapitel

KapitelBeschreibung
WorkflowStatus-Übergänge und Workflow-Steuerung
APIREST-API Endpoints und HTMX-Integration
KritikerKI-gestützte Qualitätsprüfung
RAGRetrieval-Augmented Generation

Web-UI (RESTful)

URLBeschreibung
/contentAuftrags-Liste mit Statistik und Filtern
/content/newNeuen Auftrag erstellen
/content/{id}Auftrags-Details anzeigen
/content/{id}/editAuftrag bearbeiten

Workflow

StatusBeschreibung
draftEntwurf erstellt
generatingContent wird generiert
critiqueKritik-Runde läuft
revisionRevision wird erstellt
validateValidierung ausstehend
approvedContent genehmigt
publishedContent publiziert

Konfiguration

KI-Backends

Schnellstart

Neuen Auftrag erstellen

Workflow

Erstellt: 2025-12-20 | Aktualisiert: 2025-12-20

Status-Übergänge und Workflow-Steuerung im Content Studio.

ControllerContentController.php
Status-Feldcontent_orders.status

Status-Diagramm

draft → generating → critique → revision → validate → approved → published
         ↑              ↓           ↑
         └──────────────┴───────────┘

Status-Beschreibung

StatusBeschreibungNächste Aktion
draftAuftrag erstellt, wartet auf Generierunggenerate()
generatingContent wird von KI generiertautomatisch → critique
critiqueKritik-Runde läuftcritique()
revisionRevision nach Kritik erforderlichrevise()
validateAlle Kritiker bestanden, wartet auf Freigabeapprove() / decline()
approvedContent vom Benutzer genehmigtpublish()
publishedContent 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.

Controller-Methoden

MethodeRouteStatus-Änderung
store()POST /content→ draft
generate()POST /content/{id}/generatedraft → generating → critique
critique()POST /content/{id}/critiquecritique → revision/validate
revise()POST /content/{id}/reviserevision → generating → critique
approve()POST /content/{id}/approvevalidate → approved
decline()POST /content/{id}/declinevalidate → draft

API

Erstellt: 2025-12-20 | Aktualisiert: 2025-12-31

REST-API Endpoints und HTMX-Integration für das Content Studio.

ControllerContentController.php
Backendweb_generate.py
Base-URL/content

Web-UI Endpoints (RESTful)

MethodeRouteControllerBeschreibung
GET/contentindex()Auftrags-Liste
GET/content/newcontentNew()Erstellungs-Formular
POST/contentstore()Auftrag speichern
GET/content/{id}show()Auftrags-Details
GET/content/{id}/editedit()Bearbeitungs-Formular

HTMX-Endpoints

Diese Endpoints liefern HTML-Fragmente für dynamische Updates.

MethodeRouteControllerResponse
POST/content/{id}/generategenerate()Polling-Partial
GET/content/{id}/generation-statusgenerationStatus()Status/Version-Partial
POST/content/{id}/critiquecritique()Polling-Partial
GET/content/{id}/critique-statuscritiqueStatus()Status/Critique-Partial
POST/content/{id}/reviserevise()Version-Partial
POST/content/{id}/approveapprove()Success-Alert + Reload
POST/content/{id}/declinedecline()Warning-Alert + Reload

Asynchroner Workflow (Polling)

Die Generierung und Kritik laufen asynchron. Der Ablauf ist:

  1. POST /content/{id}/generate startet die Generierung und liefert ein Polling-Partial
  2. Das Partial pollt via HTMX automatisch GET /content/{id}/generation-status
  3. Bei Status generating: Polling-Partial mit Log wird zurückgegeben
  4. Bei Status completed: Fertiges Version-Partial wird zurückgegeben
  5. Bei Status failed: Fehler-Alert wird zurückgegeben

Gleiches Prinzip für Kritik:

  1. POST /content/{id}/critique startet die Kritik
  2. Polling via GET /content/{id}/critique-status

Status-Werte

StatusBedeutungResponse
idleKeine Operation aktivLeerer Response
generating / critiquingOperation läuftPolling-Partial mit Log
completedErfolgreich abgeschlossenErgebnis-Partial
failedFehlgeschlagenFehler-Alert

REST-API Endpoint

MethodeRouteBeschreibung
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

ParameterTypDefaultBeschreibung
modelstringanthropicLLM-Backend (anthropic, ollama)
collectionstringdocumentsQdrant-Collection
context_limitint5Anzahl 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

Erstellt: 2025-12-20 | Aktualisiert: 2025-12-31

Qualitätsprüfungs-System mit LLM-basierten und deterministischen Kritikern.

Modulgenerators/critic.py, generators/format_checker.py
Tabellecontent_config (type='critic')
Ergebnissecontent_critiques

Kritiker-Typen

TypKritikerMethodeZuverlässigkeit
InhaltFaktenprüfer, Stilist, StrukturanalystLLMGut für semantische Prüfung
FormatFormatierungsprüferDeterministisch100% zuverlässig
Wichtig: LLMs halluzinieren bei Zeichenprüfungen (Gedankenstriche, Emojis, Anführungszeichen). Der Formatierungsprüfer verwendet daher deterministischen Code statt LLM.

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

RegelQuellePrüfung
emojis_verbotenstructure.formatierung.emojisUnicode-Ranges + Bullet-Emojis
markdown_verbotenstructure.ausgabe.format = "reiner Text"**bold**, *italic*, # headers
fettschrift_verbotenstructure.formatierung.fettschrift**text**, __text__
gedankenstriche_verbotenprofile.grammatik_und_satzbau.gedankenstriche– (U+2013), — (U+2014)
hashtags_verbotenstructure.formatierung.hashtags = "keine"#Hashtag Pattern
ausrufezeichen_sparsamprofile.formatierung.ausrufezeichenMax. 2 Ausrufezeichen

Post-Processing

Datei: generators/persistence.py - strip_markdown()

Für output_format == "reiner Text" werden automatisch ersetzt:

Aktuelle Kritiker

IDNameTypFokus
30FaktenprüferLLMQuellen, Aktualität, Genauigkeit
31StilistLLMStil, Tonalität, Fluss
32StrukturanalystLLMGliederung, Absätze, Aufbau
33FormatierungsprüferCodeEmojis, 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()

  1. Version-Content laden
  2. Alle aktiven Kritiker aus content_config laden
  3. Kritik-Runde inkrementieren
  4. Jeden Kritiker ausführen (LLM oder deterministisch)
  5. Feedback in content_critiques speichern
  6. 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

Erstellt: 2025-12-20 | Aktualisiert: 2025-12-20

Retrieval-Augmented Generation für kontextbasierte Content-Erstellung.

Modulgenerate.py, embed.py
Vektor-DBQdrant
Quellen-Tabellecontent_sources

Architektur

Briefing
    ↓
┌─────────────┐
│  Embedding  │  → Vektor aus Briefing
└─────────────┘
    ↓
┌─────────────┐
│   Qdrant    │  → Ähnlichkeitssuche
└─────────────┘
    ↓
┌─────────────┐
│  Kontext    │  → Top-N relevante Chunks
└─────────────┘
    ↓
┌─────────────┐
│    LLM      │  → Content mit Kontext
└─────────────┘

Collections

CollectionInhaltVerwendung
documentsDokumente, PDFsAllgemeine Wissensbasis
entitiesEntitäten, KonzepteFachbegriffe, Personen
mailE-Mail-KorrespondenzKommunikationshistorie

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

LimitTokens (~)Empfehlung
3~1500Schnelle Generierung, wenig Kontext
5~2500Standard, ausgewogen
10~5000Umfangreicher 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):

ScoreInterpretation
> 0.85Sehr relevant
0.70 - 0.85Relevant
0.50 - 0.70Möglicherweise relevant
< 0.50Geringe 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)

Verwandte Dokumentation