wiki/z-archive/appdoc/implementation_concept_ai_workflow_rev4.md

75 KiB

Überarbeitetes Implementierungskonzept: AI Workflow System

Übersicht

Dieses Dokument definiert ein überarbeitetes, klares Implementierungskonzept für das AI Workflow System, das alle identifizierten Probleme adressiert und einen korrekten Metadatenfluss durch die gesamte Pipeline sicherstellt.

Kernprinzipien

  1. Single Source of Truth: Alle Dokument-Verarbeitungslogik ist im AI Service Center zentralisiert
  2. Klarer Metadatenfluss: Jeder ContentPart trägt vollständige Metadaten über Herkunft, Format und Verwendungszweck
  3. Format-Klarheit: ContentParts zeigen explizit ihr Format an (reference, object oder extracted text)
  4. Intent-getriebene Verarbeitung: Dokument-Verarbeitung wird durch explizite Intent-Analyse gesteuert
  5. Einheitlicher AI-Call: Alle AI-Actions verwenden denselben zugrundeliegenden AI-Call-Mechanismus, unterscheiden sich nur in Parametern
  6. Harmonisierte Debug-Logs: Alle Debug-Datei-Aufrufe sind vereinheitlicht und konsistent
  7. Korrekte ChatLog-Hierarchie: Alle ChatLog-Nachrichten haben korrekte Parent/Child-Referenzen

Workflow-Phasen

Phase 1+2: User Input, Intent-Analyse & Komplexitäts-Erkennung (Kombiniert)

Location: workflowManager._sendFirstMessage() + workflowProcessor.detectComplexity()

Was passiert:

  1. User sendet Prompt mit Dokumenten
  2. Kombinierte AI-Analyse in einem AI-Call:
    • Intent-Analyse: Identifiziert User-Intentionen und extrahiert Context vom Prompt in weitere Dokumente
    • Komplexitäts-Erkennung: Bestimmt Fast Track vs Regular Track
  3. Resultat: Klare User-Intention + Dokumente + Komplexitäts-Bewertung

Kombinierter AI-Call:

async def _analyzeUserInputAndComplexity(
    self,
    userPrompt: str,
    documents: List[ChatDocument]
) -> Dict[str, Any]:
    """
    Kombinierte Analyse: Intent + Komplexität in einem AI-Call.
    """
    analysisPrompt = f"""
Analysiere die User-Anfrage und bestimme in einem Durchgang:

1. detectedLanguage: ISO 639-1 Sprachcode (z.B. de, en)
2. normalizedRequest: Vollständige, explizite Umformulierung der User-Anfrage
3. intent: Kurze Kern-Anfrage für High-Level-Routing
4. contextItems: Große Datenblöcke, die als separate Dokumente extrahiert werden sollen
5. complexity: "simple" | "moderate" | "complex"
6. needsWorkflowHistory: bool (ob Workflow-History benötigt wird)
7. fastTrack: bool (ob Fast Track möglich ist)

User-Anfrage: "{userPrompt}"
Dokumente: {len(documents)} Dokumente vorhanden

Antworte mit JSON:
{{
    "detectedLanguage": "de",
    "normalizedRequest": "...",
    "intent": "...",
    "contextItems": [...],
    "complexity": "complex",
    "needsWorkflowHistory": false,
    "fastTrack": false
}}
"""
    
    # Debug-Log
    self.services.utils.writeDebugFile(analysisPrompt, "user_input_analysis_prompt")
    
    # AI-Call
    aiResponse = await self.services.ai.callAiContent(
        prompt=analysisPrompt,
        options=AiCallOptions(operationType=OperationTypeEnum.DATA_GENERATE),
        outputFormat="json"
    )
    
    # Debug-Log
    self.services.utils.writeDebugFile(aiResponse.content, "user_input_analysis_response")
    
    # Parse Result
    result = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
    
    return result

Vorteile der Kombination:

  • Weniger AI-Calls: Ein Call statt zwei
  • Konsistente Analyse: Beide Analysen verwenden denselben Kontext
  • Bessere Performance: Schneller, weniger Kosten
  • Klarere Struktur: Ein Analyse-Schritt statt zwei

Output:

  • normalizedRequest: User-Prompt ohne Context
  • detectedLanguage: Sprachcode
  • contextItems: Große Content-Blöcke als separate Dokumente
  • intent: High-Level-Kern-Anfrage
  • complexity: "simple" | "moderate" | "complex"
  • needsWorkflowHistory: bool
  • fastTrack: bool

Dokumente nach Phase 1+2:

  • Original User-uploaded Dokumente
  • Context-extrahierten Dokumente (aus Prompt-Analyse)
  • Alle Dokumente haben: fileId, fileName, mimeType, fileSize

Code-Referenz:

# Analyze the user's input to detect language, normalize request, extract intent, and offload bulky context into documents

Phase 3: Task Planning

Location: workflowProcessor.generateTaskPlan()

Was passiert:

  • Trennt verschiedene User-Intentionen in einzelne Tasks
  • Jeder Task repräsentiert ein distinctes Ziel
  • Tasks sind unabhängig, können aber auf Ergebnisse anderer Tasks referenzieren

Task-Struktur:

Task {
    id: str
    objective: str  # Was soll erreicht werden
    requiredDocuments: List[DocumentReference]  # Welche Dokumente werden benötigt
    expectedOutput: str  # Erwartetes Output-Format/Typ
}

Code-Referenz:

async def generateTaskPlan(self, userInput: str, workflow: ChatWorkflow) -> TaskPlan:

Phase 4: Task Execution (Iterative Action Execution)

Location: modeDynamic.executeTask()

Was passiert:

  • Identifiziert und führt iterativ Actions aus
  • Jede Action erhält:
    • Task-Objective
    • Verfügbare Dokumente (von vorherigen Actions)
    • Context von vergangenen Rounds und Tasks:
      • context.executedActions: Liste aller bereits ausgeführten Actions
      • context.workflowHistory: History aus vorherigen Rounds
      • context.taskHistory: History aus vorherigen Tasks
      • AVAILABLE_DOCUMENTS_INDEX: Alle verfügbaren Dokumente aus vorherigen Actions
      • context.nextActionGuidance: Vorgabe für nächste Action (von Refinement-Entscheidung)

Action Types:

  • ai.* Actions: Alle verwenden unified AI Service Center
  • context.* Actions: Dokument-Extraktion/Management
  • jira.*, sharepoint.*, etc.: Externe Service-Actions

Action Planner & Refinement Planner:

  • Action Planner (_planSelect): Wählt nächste Action basierend auf:

    • Task-Objective
    • Verfügbare Dokumente (AVAILABLE_DOCUMENTS_INDEX)
    • Action-History (context.executedActions)
    • Workflow-History (context.workflowHistory)
    • Task-History (context.taskHistory)
    • context.nextActionGuidance (falls vorhanden)
  • Refinement Planner: Entscheidet über nächste Action nach Content-Validation:

    • Nutzt Validation-Ergebnisse
    • Kann context.nextActionGuidance setzen
    • Berücksichtigt Action-History für adaptive Entscheidungen

Diese Phase bleibt unverändert - sie funktioniert korrekt.

Code-Referenz:

async def _actExecute(self, context, selection, taskStep, workflow, step):

Context-Struktur:

TaskContext {
    workflowId: str
    executedActions: List[Dict]  # [{action: "...", parameters: {...}, step: 1}, ...]
    workflowHistory: List[Dict]  # History aus vorherigen Rounds
    taskHistory: List[Dict]  # History aus vorherigen Tasks
    nextActionGuidance: Optional[Dict]  # Vorgabe für nächste Action
    # ... weitere Context-Felder
}

Phase 5: AI Action Execution (Zentrale Verarbeitung)

Location: serviceAi.mainServiceAi.callAiContent()

Dies ist der Kern des überarbeiteten Konzepts. Alle AI-Actions (ai.process, ai.generateDocument, ai.summarizeDocument, etc.) routen durch diese einzige Funktion.

Kernprinzip: AI-Actions unterscheiden sich nur in Parametern, nicht in Logik. Die Dokument-Erstellungslogik ist hier zentralisiert.


Phase 5 Detailliert: AI Service Center Verarbeitung

5A: Dokument-Intent-Klärung

Was passiert:

  • Für jedes Dokument wird dessen Verwendungszweck bestimmt:
    • Extract: Content-Extraktion benötigt (Text, Struktur, etc.)
    • Reference: Dokument-Referenz/Attachment (keine Extraktion)
    • Render: Image/Binary soll als-is gerendert werden

Input:

  • documentList: Liste der zu verarbeitenden Dokumente
  • userPrompt: User-Anfrage
  • actionParameters: Action-spezifische Parameter (z.B. resultType, outputFormat)

Process:

async def _clarifyDocumentIntents(
    self,
    documents: List[ChatDocument],
    userPrompt: str,
    actionParameters: Dict[str, Any],
    parentOperationId: str  # Für ChatLog-Hierarchie
) -> List[DocumentIntent]:
    """
    Analysiert, welche Dokumente Extraktion vs Referenz benötigen.
    Gibt DocumentIntent für jedes Dokument zurück.
    """
    # Erstelle Operation-ID für Intent-Analyse
    intentOperationId = f"{parentOperationId}_intent_analysis"
    
    # Starte ChatLog mit Parent-Referenz
    self.services.chat.progressLogStart(
        intentOperationId,
        "Document Intent Analysis",
        "Intent Analysis",
        f"Analyzing {len(documents)} documents",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    try:
        # AI-basierte Analyse des User-Prompts + Dokumente
        intentPrompt = self._buildIntentAnalysisPrompt(userPrompt, documents, actionParameters)
        
        # Debug-Log (harmonisiert)
        self.services.utils.writeDebugFile(intentPrompt, "document_intent_analysis_prompt")
        
        # AI-Call
        aiResponse = await self.services.ai.callAiContent(
            prompt=intentPrompt,
            options=AiCallOptions(operationType=OperationTypeEnum.DATA_GENERATE),
            outputFormat="json",
            parentOperationId=intentOperationId  # Parent-Referenz für AI-Call-Logs
        )
        
        # Debug-Log (harmonisiert)
        self.services.utils.writeDebugFile(aiResponse.content, "document_intent_analysis_response")
        
        # Parse Result
        intentsData = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
        documentIntents = [DocumentIntent(**intent) for intent in intentsData.get("intents", [])]
        
        # ChatLog abschließen
        self.services.chat.progressLogFinish(intentOperationId, True)
        
        return documentIntents
    
    except Exception as e:
        self.services.chat.progressLogFinish(intentOperationId, False)
        raise

Output:

[
    DocumentIntent(
        documentId="doc_1",
        intents=["extract"],
        extractionPrompt="Extract all text content, preserving structure",
        reasoning="User needs text content for document generation"
    ),
    DocumentIntent(
        documentId="doc_2",
        intents=["render"],
        extractionPrompt=None,
        reasoning="Image should be rendered as visual element"
    ),
    DocumentIntent(
        documentId="doc_3",
        intents=["reference"],
        extractionPrompt=None,
        reasoning="Document is only used as reference, no extraction needed"
    )
]

⚠️ WICHTIG: Unterschied zum alten Purpose-System

Das alte System in subDocumentPurposeAnalyzer.py verwendet viele spezifische "purpose"-Tags (extract_text_content, include_image, analyze_image_vision, etc.) und muss durch dieses einfache DocumentIntent-System ersetzt werden.

Alt (subDocumentPurposeAnalyzer) Neu (DocumentIntent)
purpose: "extract_text_content" intents: ["extract"]
purpose: "include_image" intents: ["render"]
purpose: "analyze_image_vision" intents: ["extract"] mit extractionMethod="vision"
purpose: "use_as_reference" intents: ["reference"]
Ein Purpose pro Dokument Mehrere Intents pro Dokument möglich!
Viele spezifische Tags Nur 3 Basis-Intents: extract, render, reference

Intent-Analyse-Prompt-Format:

USER REQUEST:
{userPrompt}

DOCUMENTS TO ANALYZE:
{documentList}

TASK: For each document, determine its intents (can be multiple):
- "extract": Content extraction needed (text, structure, OCR, etc.)
- "render": Image/binary should be rendered as-is (visual element)
- "reference": Document reference/attachment (no extraction, just reference)

RETURN JSON:
{
  "intents": [
    {
      "documentId": "doc_1",
      "intents": ["extract"],  # Array - can contain multiple!
      "extractionPrompt": "Extract all text content, preserving structure",
      "reasoning": "User needs text content for document generation"
    },
    {
      "documentId": "doc_2",
      "intents": ["extract", "render"],  # Both! Image needs text extraction AND visual rendering
      "extractionPrompt": "Extract text content from image using vision AI",
      "reasoning": "Image contains text that needs extraction, but also should be rendered visually"
    },
    {
      "documentId": "doc_3",
      "intents": ["reference"],
      "extractionPrompt": null,
      "reasoning": "Document is only used as reference, no extraction needed"
    }
  ]
}

Debug-Logging (harmonisiert):

# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(
    json.dumps([intent.dict() for intent in documentIntents], indent=2),
    "document_intent_analysis_result"
)

5B: Content-Extraktion & Vorbereitung

Was passiert:

  • Extrahiert Content von Dokumenten, die Extraktion benötigen
  • Bereitet ContentParts mit vollständigen Metadaten vor
  • Behandelt drei Content-Formate explizit

Content Part Formate:

Jeder ContentPart kann in einem von drei Formaten sein:

  1. Document Reference (contentFormat="reference"):

    ContentPart(
        id="part_ref_1",
        typeGroup="reference",
        mimeType="application/pdf",
        data="",  # Leer - nur Referenz
        metadata={
            "contentFormat": "reference",
            "documentId": "doc_1",
            "documentReference": "docItem:doc_1:document.pdf",
            "intent": "reference",  # Wo dieser Content benötigt wird
            "usageHint": "Include as attachment in section X"
        }
    )
    
  2. Object/Binary (contentFormat="object"):

    ContentPart(
        id="part_obj_1",
        typeGroup="image",
        mimeType="image/png",
        data="base64encodeddata...",  # Base64-kodiertes Binary
        metadata={
            "contentFormat": "object",
            "documentId": "doc_2",
            "intent": "render",  # Wo dieser Content benötigt wird
            "usageHint": "Render as image in section Y",
            "originalFileName": "logo.png"
        }
    )
    
  3. Extracted Text (contentFormat="extracted"):

    ContentPart(
        id="part_ext_1",
        typeGroup="text",
        mimeType="text/plain",
        data="Extracted text content...",  # Extrahierter Text
        metadata={
            "contentFormat": "extracted",
            "documentId": "doc_1",
            "extractionPrompt": "Extract all text content",
            "intent": "extract",  # Wo dieser Content benötigt wird
            "usageHint": "Use in paragraph sections",
            "extractionMethod": "vision"  # oder "text", "ocr", etc.
        }
    )
    

Process:

async def _extractAndPrepareContent(
    self,
    documents: List[ChatDocument],
    documentIntents: List[DocumentIntent],
    parentOperationId: str  # Für ChatLog-Hierarchie
) -> List[ContentPart]:
    """
    Extrahiert Content basierend auf Intents und bereitet ContentParts mit Metadaten vor.
    Gibt Liste von ContentParts im passenden Format zurück.
    
    WICHTIG: Ein Dokument kann mehrere ContentParts erzeugen, wenn mehrere Intents vorhanden sind.
    Beispiel: Bild mit intents=["extract", "render"] erzeugt:
    - ContentPart(contentFormat="object", ...) für Rendering
    - ContentPart(contentFormat="extracted", ...) für Text-Analyse
    """
    # Erstelle Operation-ID für Extraktion
    extractionOperationId = f"{parentOperationId}_content_extraction"
    
    # Starte ChatLog mit Parent-Referenz
    self.services.chat.progressLogStart(
        extractionOperationId,
        "Content Extraction",
        "Extraction",
        f"Extracting from {len(documents)} documents",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    try:
        allContentParts = []
        
        for document in documents:
            intent = getIntentForDocument(document.id, documentIntents)
            
            # WICHTIG: Prüfe alle Intents - ein Dokument kann mehrere ContentParts erzeugen
            
            if "reference" in intent.intents:
                # Erstelle Reference ContentPart
                contentPart = ContentPart(
                    id=f"ref_{document.id}",
                    typeGroup="reference",
                    mimeType=document.mimeType,
                    data="",
                    metadata={
                        "contentFormat": "reference",
                        "documentId": document.id,
                        "documentReference": f"docItem:{document.id}:{document.fileName}",
                        "intent": "reference",
                        "usageHint": f"Reference document: {document.fileName}"
                    }
                )
                allContentParts.append(contentPart)
            
            # WICHTIG: "render" und "extract" können beide vorhanden sein!
            # In diesem Fall erzeugen wir BEIDE ContentParts
            
            if "render" in intent.intents:
                # Für Images/Binary: extrahiere als Object
                if document.mimeType.startswith("image/") or isBinary(document.mimeType):
                    # Lade Binary-Daten
                    binaryData = await self.services.interfaceDbComponent.getFileData(document.fileId)
                    base64Data = base64.b64encode(binaryData).decode('utf-8')
                    
                    contentPart = ContentPart(
                        id=f"obj_{document.id}",
                        typeGroup="image" if document.mimeType.startswith("image/") else "binary",
                        mimeType=document.mimeType,
                        data=base64Data,
                        metadata={
                            "contentFormat": "object",
                            "documentId": document.id,
                            "intent": "render",
                            "usageHint": f"Render as visual element: {document.fileName}",
                            "originalFileName": document.fileName,
                            # Verknüpfung zu extracted Part (falls vorhanden)
                            "relatedExtractedPartId": f"ext_{document.id}" if "extract" in intent.intents else None
                        }
                    )
                    allContentParts.append(contentPart)
            
            if "extract" in intent.intents:
                # Extrahiere Content mit Extraction Service
                extractionPrompt = intent.extractionPrompt or "Extract all content from the document"
                
                # Debug-Log (harmonisiert)
                self.services.utils.writeDebugFile(
                    extractionPrompt,
                    f"content_extraction_prompt_{document.id}"
                )
                
                # Führe Extraktion aus
                extractedResults = await self.services.extraction.extractContent(
                    [document],
                    ExtractionOptions(
                        prompt=extractionPrompt,
                        mergeStrategy=MergeStrategy(...)
                    ),
                    operationId=extractionOperationId,  # Für ChatLog-Hierarchie
                    parentOperationId=extractionOperationId  # Parent-Referenz!
                )
                
                # Konvertiere extrahierte Ergebnisse zu ContentParts mit Metadaten
                for extracted in extractedResults:
                    for part in extracted.parts:
                        # Markiere als extracted Format
                        part.metadata.update({
                            "contentFormat": "extracted",
                            "documentId": document.id,
                            "extractionPrompt": extractionPrompt,
                            "intent": "extract",
                            "usageHint": f"Use extracted content from {document.fileName}",
                            # Verknüpfung zu object Part (falls vorhanden)
                            "relatedObjectPartId": f"obj_{document.id}" if "render" in intent.intents else None
                        })
                        # Stelle sicher, dass ID eindeutig ist (falls object Part existiert)
                        if "render" in intent.intents:
                            part.id = f"ext_{document.id}_{part.id}"
                        allContentParts.append(part)
        
        # Debug-Log (harmonisiert)
        self.services.utils.writeDebugFile(
            json.dumps([part.dict() for part in allContentParts], indent=2),
            "content_extraction_result"
        )
        
        # ChatLog abschließen
        self.services.chat.progressLogFinish(extractionOperationId, True)
        
        return allContentParts
    
    except Exception as e:
        self.services.chat.progressLogFinish(extractionOperationId, False)
        raise

Wichtige Erweiterung: Mehrfache ContentParts pro Dokument GRUNDLEGENDES DESIGN

Dies ist das grundlegende Design! Wenn ein Dokument mehrere Intents hat (z.B. ["extract", "render"]), werden mehrere ContentParts erzeugt:

Beispiel: Bild mit intents=["extract", "render"]

# ContentPart 1: Object für Rendering
ContentPart(
    id="obj_img_1",
    contentFormat="object",
    data="base64...",
    metadata={
        "documentId": "img_1",
        "intent": "render",
        "relatedExtractedPartId": "ext_img_1"  # Verknüpfung
    }
)

# ContentPart 2: Extracted für Text-Analyse
ContentPart(
    id="ext_img_1",
    contentFormat="extracted",
    data="This image shows...",
    metadata={
        "documentId": "img_1",
        "intent": "extract",
        "extractionMethod": "vision",
        "relatedObjectPartId": "obj_img_1"  # Verknüpfung
    }
)

Verknüpfung zwischen ContentParts:

  • relatedExtractedPartId: Object-Part verweist auf extracted Part
  • relatedObjectPartId: Extracted Part verweist auf object Part
  • Ermöglicht Generation-Prompt, beide Formate zu verwenden

Spezielle Behandlung: Bereits extrahierter Content

Wenn ein Dokument bereits extrahierten Content enthält (von vorheriger Action):

# Prüfe Metadaten auf skipExtraction Flag
if part.metadata.get("skipExtraction", False):
    # Content ist bereits extrahiert - verwende as-is
    # Stelle nur sicher, dass Metadaten vollständig sind
    part.metadata.update({
        "contentFormat": "extracted",
        "sourceAction": part.metadata.get("sourceAction", "unknown"),
        "isPreExtracted": True
    })
    # Verwende direkt ohne Re-Extraktion

Debug-Logging (harmonisiert):

# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(extractionPrompt, "content_extraction_prompt")
self.services.utils.writeDebugFile(
    json.dumps([part.dict() for part in allContentParts], indent=2),
    "content_extraction_result"
)

5C: Generierungs-Struktur-Definition

Was passiert:

  • Definiert die Struktur des Result-JSON
  • Spezifiziert, welcher Content in welche Sections geht
  • Definiert Format für jede Section
  • Unterstützt strukturierte Datenextraktion (z.B. CSV mit spezifischen Spalten)

Input:

  • userPrompt: User-Anfrage
  • contentParts: Alle vorbereiteten ContentParts mit Metadaten
  • outputFormat: Ziel-Format (html, docx, pdf, etc.)

Process:

async def _generateStructure(
    self,
    userPrompt: str,
    contentParts: List[ContentPart],
    outputFormat: str,
    parentOperationId: str  # Für ChatLog-Hierarchie
) -> Dict[str, Any]:
    """
    Generiert Dokument-Struktur mit Sections.
    Jede Section spezifiziert:
    - Welcher Content sollte in dieser Section sein
    - Welche ContentParts zu verwenden sind
    - Format für jeden ContentPart
    """
    # Erstelle Operation-ID für Struktur-Generierung
    structureOperationId = f"{parentOperationId}_structure_generation"
    
    # Starte ChatLog mit Parent-Referenz
    self.services.chat.progressLogStart(
        structureOperationId,
        "Structure Generation",
        "Structure",
        f"Generating structure for {outputFormat}",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    try:
        # Baue Struktur-Prompt mit Content-Index
        structurePrompt = self._buildStructurePrompt(
            userPrompt=userPrompt,
            contentParts=contentParts,  # Inkludiere Metadaten für jeden Part
            outputFormat=outputFormat
        )
        
        # Debug-Log (harmonisiert)
        self.services.utils.writeDebugFile(structurePrompt, "document_generation_structure_prompt")
        
        # AI-Call für Struktur-Generierung
        aiResponse = await self.services.ai.callAiContent(
            prompt=structurePrompt,
            options=AiCallOptions(
                operationType=OperationTypeEnum.DATA_GENERATE,
                resultFormat="json"
            ),
            outputFormat="json",
            parentOperationId=structureOperationId  # Parent-Referenz für AI-Call-Logs
        )
        
        # Debug-Log (harmonisiert)
        self.services.utils.writeDebugFile(aiResponse.content, "document_generation_structure_response")
        
        # Parse Struktur (keine Validierung - klarer Code ohne zusätzliche Komplexität)
        structure = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
        
        # ChatLog abschließen
        self.services.chat.progressLogFinish(structureOperationId, True)
        
        return structure
    
    except Exception as e:
        self.services.chat.progressLogFinish(structureOperationId, False)
        raise

Struktur-Prompt-Format:

Standard-Format (für Dokument-Generierung):

USER REQUEST:
{userPrompt}

AVAILABLE CONTENT PARTS:
{contentPartsIndex}

Für jeden ContentPart:
- id: {part.id}
- format: {part.metadata.contentFormat}  # reference, object, oder extracted
- type: {part.typeGroup}
- source: {part.metadata.documentId}
- usage hint: {part.metadata.usageHint}
- data preview: {preview von part.data wenn extracted, oder "reference" oder "base64 object"}

TASK: Generiere Dokument-Struktur mit Sections.
Für jede Section, spezifiziere:
- section id
- content_type (heading, paragraph, image, table, etc.)
- contentPartIds: [Liste von ContentPart-IDs zu verwenden]
- contentFormat: Wie jeder ContentPart zu verwenden ist (reference, object, extracted)
- generation_hint: Was AI für diese Section generieren soll
- elements: [] (leer, wird in nächster Phase gefüllt)

Strukturierte Datenextraktion (z.B. CSV):

USER REQUEST:
{userPrompt}

AVAILABLE CONTENT PARTS:
{contentPartsIndex}

ERFORDERLICHE FELDER/SPALTEN:
{requiredFields}  # z.B. ["date", "shop", "CHF", "VAT", "Description"]

TASK: Generiere Struktur für strukturierte Datenextraktion.
Die Struktur muss folgende Felder enthalten:
{requiredFields}

Für jedes Feld, spezifiziere:
- field_name: Name des Feldes
- contentPartIds: [Liste von ContentPart-IDs, die Daten für dieses Feld enthalten]
- extraction_hint: Wie Daten aus ContentParts extrahiert werden sollen
- data_type: Erwarteter Datentyp (string, number, date, etc.)

Die Struktur sollte ein Array von Datensätzen sein, wobei jeder Datensatz alle Felder enthält.

**WICHTIG**: Phase 5C unterstützt explizit strukturierte Datenextraktion für Formate wie CSV, JSON, XLSX.

Beispiel für strukturierte Datenextraktion (Expenses CSV):

{
  "metadata": {
    "title": "Expenses Overview",
    "outputFormat": "csv",
    "fields": ["date", "shop", "CHF", "VAT", "Description"]
  },
  "documents": [{
    "id": "expenses_csv",
    "title": "Expenses CSV",
    "filename": "expenses.csv",
    "sections": [
      {
        "id": "data_extraction",
        "content_type": "table",
        "generation_hint": "Extract structured expense data from all content parts",
        "contentPartIds": ["ext_pdf_1", "ext_pdf_2", ..., "ext_pdf_10"],
        "contentFormats": {
          "ext_pdf_1": "extracted",
          "ext_pdf_2": "extracted",
          ...
        },
        "extractionFields": {
          "date": {
            "contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
            "extractionHint": "Extract date from receipt/invoice",
            "dataType": "date"
          },
          "shop": {
            "contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
            "extractionHint": "Extract shop/vendor name",
            "dataType": "string"
          },
          "CHF": {
            "contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
            "extractionHint": "Extract amount in CHF",
            "dataType": "number"
          },
          "VAT": {
            "contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
            "extractionHint": "Extract VAT amount",
            "dataType": "number"
          },
          "Description": {
            "contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
            "extractionHint": "Extract item description",
            "dataType": "string"
          }
        },
        "elements": []
      }
    ]
  }]
}

Output-Struktur:

{
  "metadata": {
    "title": "Document Title",
    "language": "de"
  },
  "documents": [{
    "id": "doc_1",
    "title": "Document Title",
    "filename": "document.html",
    "sections": [
      {
        "id": "section_1",
        "content_type": "heading",
        "generation_hint": "Main title",
        "contentPartIds": [],
        "elements": []
      },
      {
        "id": "section_2",
        "content_type": "paragraph",
        "generation_hint": "Introduction paragraph",
        "contentPartIds": ["part_ext_1"],
        "contentFormats": {
          "part_ext_1": "extracted"  # Verwende extrahierten Text
        },
        "elements": []
      },
      {
        "id": "section_3",
        "content_type": "image",
        "generation_hint": "Logo image",
        "contentPartIds": ["part_obj_1"],
        "contentFormats": {
          "part_obj_1": "object"  # Verwende base64 Object
        },
        "elements": []
      },
      {
        "id": "section_4",
        "content_type": "paragraph",
        "generation_hint": "Reference to attachment",
        "contentPartIds": ["part_ref_1"],
        "contentFormats": {
          "part_ref_1": "reference"  # Verwende Dokument-Referenz
        },
        "elements": []
      }
    ]
  }]
}

5D: Struktur-Abfüllen

Was passiert:

  • Füllt jede Section mit tatsächlichem Content
  • Verwendet passendes Format basierend auf contentFormat Metadaten
  • Generiert AI-Content wo nötig

Process:

async def _fillStructure(
    self,
    structure: Dict[str, Any],
    contentParts: List[ContentPart],
    userPrompt: str,
    parentOperationId: str  # Für ChatLog-Hierarchie
) -> Dict[str, Any]:
    """
    Füllt Struktur mit tatsächlichem Content.
    Für jede Section:
    - Wenn contentPartIds spezifiziert: Verwende ContentParts im spezifizierten Format
    - Wenn generation_hint spezifiziert: Generiere AI-Content
    
    **Implementierungsdetails:**
    - Sections werden **parallel generiert**, wenn möglich (Performance-Optimierung)
    - Fehlerhafte Sections werden mit Fehlermeldung gerendert (kein Abbruch des gesamten Prozesses)
    """
    # Erstelle Operation-ID für Struktur-Abfüllen
    fillOperationId = f"{parentOperationId}_structure_filling"
    
    # Starte ChatLog mit Parent-Referenz
    self.services.chat.progressLogStart(
        fillOperationId,
        "Structure Filling",
        "Filling",
        f"Filling {len(structure.get('documents', [{}])[0].get('sections', []))} sections",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    try:
        filledStructure = copy.deepcopy(structure)
        
        # Sammle alle Sections für parallele Verarbeitung
        sections_to_process = []
        for doc in filledStructure.get("documents", []):
            for section in doc.get("sections", []):
                sections_to_process.append((doc, section))
        
        # Parallele Section-Generierung
        import asyncio
        section_tasks = []
        for doc, section in sections_to_process:
            section_tasks.append(self._fillSection(doc, section, contentParts, userPrompt, fillOperationId))
        
        # Führe alle Sections parallel aus
        section_results = await asyncio.gather(*section_tasks, return_exceptions=True)
        
        # Verarbeite Ergebnisse (inkl. Fehlerbehandlung)
        for (doc, section), result in zip(sections_to_process, section_results):
            if isinstance(result, Exception):
                # Fehlerhafte Section mit Fehlermeldung rendern
                section["elements"] = [{
                    "type": "error",
                    "message": f"Error generating section {section.get('id')}: {str(result)}",
                    "sectionId": section.get("id")
                }]
                logger.error(f"Error filling section {section.get('id')}: {str(result)}")
            else:
                section["elements"] = result
        
        # Alte sequenzielle Implementierung (ersetzt durch parallele Version oben):
        # for doc in filledStructure.get("documents", []):
        #     for section in doc.get("sections", []):
                sectionId = section.get("id")
                contentPartIds = section.get("contentPartIds", [])
                contentFormats = section.get("contentFormats", {})
                generationHint = section.get("generation_hint")
                
                elements = []
                
                # Verarbeite ContentParts
                for partId in contentPartIds:
                    part = findContentPartById(partId, contentParts)
                    if not part:
                        continue
                    
                    contentFormat = contentFormats.get(partId, part.metadata.get("contentFormat"))
                    
                    if contentFormat == "reference":
                        # Füge Dokument-Referenz hinzu
                        elements.append({
                            "type": "reference",
                            "documentReference": part.metadata.get("documentReference"),
                            "label": part.metadata.get("usageHint", part.label)
                        })
                    
                    elif contentFormat == "object":
                        # Füge base64 Object hinzu
                        elements.append({
                            "type": part.typeGroup,  # "image", "binary", etc.
                            "base64Data": part.data,
                            "mimeType": part.mimeType,
                            "altText": part.metadata.get("usageHint", part.label)
                        })
                    
                    elif contentFormat == "extracted":
                        # Füge extrahierten Text hinzu (kann in AI-Generierungs-Prompt verwendet werden)
                        elements.append({
                            "type": "extracted_text",
                            "content": part.data,
                            "source": part.metadata.get("documentId"),
                            "extractionPrompt": part.metadata.get("extractionPrompt")
                        })
                
                # Generiere AI-Content wenn nötig
                if generationHint:
                    generationPrompt = self._buildSectionGenerationPrompt(
                        section=section,
                        contentParts=[findContentPartById(pid, contentParts) for pid in contentPartIds],
                        userPrompt=userPrompt,
                        generationHint=generationHint
                    )
                    
                    # Debug-Log (harmonisiert)
                    self.services.utils.writeDebugFile(
                        generationPrompt,
                        f"section_generation_prompt_{sectionId}"
                    )
                    
                    # Erstelle Operation-ID für Section-Generierung
                    sectionOperationId = f"{fillOperationId}_section_{sectionId}"
                    
                    # Starte ChatLog mit Parent-Referenz
                    self.services.chat.progressLogStart(
                        sectionOperationId,
                        "Section Generation",
                        "Section",
                        f"Generating section {sectionId}",
                        parentOperationId=fillOperationId  # Parent-Referenz!
                    )
                    
                    try:
                        # Generiere Content
                        aiResponse = await self.services.ai.callAiContent(
                            prompt=generationPrompt,
                            options=AiCallOptions(
                                operationType=OperationTypeEnum.DATA_GENERATE,
                                resultFormat="json"
                            ),
                            outputFormat="json",
                            parentOperationId=sectionOperationId  # Parent-Referenz für AI-Call-Logs
                        )
                        
                        # Debug-Log (harmonisiert)
                        self.services.utils.writeDebugFile(
                            aiResponse.content,
                            f"section_generation_response_{sectionId}"
                        )
                        
                        # Parse und füge zu elements hinzu
                        generatedElements = json.loads(
                            self.services.utils.jsonExtractString(aiResponse.content)
                        )
                        if isinstance(generatedElements, list):
                            elements.extend(generatedElements)
                        elif isinstance(generatedElements, dict) and "elements" in generatedElements:
                            elements.extend(generatedElements["elements"])
                        
                        # ChatLog abschließen
                        self.services.chat.progressLogFinish(sectionOperationId, True)
                    
                    except Exception as e:
                        # Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
                        self.services.chat.progressLogFinish(sectionOperationId, False)
                        elements.append({
                            "type": "error",
                            "message": f"Error generating section {sectionId}: {str(e)}",
                            "sectionId": sectionId
                        })
                        logger.error(f"Error generating section {sectionId}: {str(e)}")
                        # NICHT raise - Section wird mit Fehlermeldung gerendert
                
                section["elements"] = elements
        
        # ChatLog abschließen
        self.services.chat.progressLogFinish(fillOperationId, True)
        
        return filledStructure
    
    except Exception as e:
        self.services.chat.progressLogFinish(fillOperationId, False)
        raise

Section-Generierungs-Prompt-Format:

USER REQUEST:
{userPrompt}

SECTION TO GENERATE:
{generationHint}

AVAILABLE CONTENT FOR THIS SECTION:
{contentPartsForSection}

Für jeden ContentPart:
- Format: {contentFormat}
- Content: {part.data wenn extracted, oder Beschreibung wenn reference/object}

CRITICAL: Return ONLY a JSON object with an "elements" array.
Jedes Element sollte dem content_type der Section entsprechen.

5E: Rendering

Was passiert:

  • Rendert die gefüllte JSON-Struktur zum Ziel-Format
  • Verwendet Rendering-Engines (PDF, DOCX, HTML, etc.)
  • Multi-Dokument-Support: Alle Dokumente im documents Array werden gerendert

Process:

async def _renderResult(
    self,
    filledStructure: Dict[str, Any],
    outputFormat: str,
    title: str,
    userPrompt: str,
    parentOperationId: str  # Für ChatLog-Hierarchie
) -> Tuple[bytes, str]:
    """
    Rendert gefüllte Struktur zum Ziel-Format.
    Unterstützt Multi-Dokument-Rendering: Alle Dokumente werden gerendert.
    """
    # Erstelle Operation-ID für Rendering
    renderOperationId = f"{parentOperationId}_rendering"
    
    # Starte ChatLog mit Parent-Referenz
    self.services.chat.progressLogStart(
        renderOperationId,
        "Content Rendering",
        "Rendering",
        f"Rendering to {outputFormat} format",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    try:
        from modules.services.serviceGeneration.mainServiceGeneration import GenerationService
        
        generationService = GenerationService(self.services)
        
        # Multi-Dokument-Rendering
        documents = filledStructure.get("documents", [])
        
        if len(documents) == 1:
            # Einzelnes Dokument - wie bisher
            renderedContent, mimeType, images = await generationService.renderReport(
                filledStructure,
                outputFormat,
                title,
                userPrompt,
                self
            )
        else:
            # Mehrere Dokumente - rendere alle
            # Option: Alle Sections zusammenführen und als ein Dokument rendern
            all_sections = []
            for doc in documents:
                if "sections" in doc:
                    all_sections.extend(doc.get("sections", []))
            
            # Erstelle temporäres Dokument mit allen Sections
            merged_document = {
                "metadata": filledStructure["metadata"],
                "documents": [{
                    "id": "merged",
                    "title": title,
                    "filename": f"{title}.{outputFormat}",
                    "sections": all_sections
                }]
            }
            
            renderedContent, mimeType, images = await generationService.renderReport(
                merged_document,
                outputFormat,
                title,
                userPrompt,
                self
            )
        
        # ChatLog abschließen
        self.services.chat.progressLogFinish(renderOperationId, True)
        
        return renderedContent, mimeType
    
    except Exception as e:
        self.services.chat.progressLogFinish(renderOperationId, False)
        raise

Code-Referenz:

async def renderReport(self, extractedContent: Dict[str, Any], outputFormat: str, title: str, userPrompt: str = None, aiService=None) -> tuple[str, str, List[Dict[str, Any]]]:

Überarbeitete callAiContent Funktion-Struktur

Vereinfachte Hauptfunktion

async def callAiContent(
    self,
    prompt: str,
    options: AiCallOptions,
    contentParts: Optional[List[ContentPart]] = None,
    documentList: Optional[DocumentReferenceList] = None,
    documentIntents: Optional[List[DocumentIntent]] = None,
    outputFormat: Optional[str] = None,
    title: Optional[str] = None,
    parentOperationId: Optional[str] = None
) -> AiResponse:
    """
    Einheitliche AI-Content-Verarbeitung - Single Entry Point für alle AI-Actions.
    
    Alle AI-Actions (ai.process, ai.generateDocument, etc.) routen hier durch.
    Sie unterscheiden sich nur in Parametern, nicht in Logik.
    """
    await self.ensureAiObjectsInitialized()
    
    # Erstelle Operation-ID
    workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
    aiOperationId = f"ai_content_{workflowId}_{int(time.time())}"
    
    # Starte Progress-Tracking mit Parent-Referenz
    self.services.chat.progressLogStart(
        aiOperationId,
        "AI content processing",
        "Content Processing",
        f"Format: {outputFormat or 'text'}",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    try:
        # Initialisiere Defaults
        if not outputFormat:
            outputFormat = "txt"
        
        opType = getattr(options, "operationType", None)
        if not opType:
            options.operationType = OperationTypeEnum.DATA_GENERATE
            opType = OperationTypeEnum.DATA_GENERATE
        
        # Route zu Operation-spezifischen Handlern
        if opType == OperationTypeEnum.IMAGE_GENERATE:
            return await self._handleImageGeneration(prompt, options, title, aiOperationId)
        
        if opType == OperationTypeEnum.WEB_SEARCH or opType == OperationTypeEnum.WEB_CRAWL:
            return await self._handleWebOperation(prompt, options, opType, aiOperationId)
        
        # Dokument-Generierungs-Pfad
        options.compressPrompt = False
        options.compressContext = False
        
        # Schritt 5A: Kläre Dokument-Intents
        documents = []
        if documentList:
            documents = self.services.chat.getChatDocumentsFromDocumentList(documentList)
        
        if not documentIntents and documents:
            documentIntents = await self._clarifyDocumentIntents(
                documents,
                prompt,
                {"outputFormat": outputFormat},
                aiOperationId  # Parent-Referenz!
            )
        
        # Schritt 5B: Extrahiere und bereite Content vor
        if documents:
            preparedContentParts = await self._extractAndPrepareContent(
                documents,
                documentIntents or [],
                aiOperationId  # Parent-Referenz!
            )
            
            # WICHTIG: Kein Caching - Content wird immer neu verarbeitet für Konsistenz
            
            # Merge mit bereitgestellten contentParts (falls vorhanden)
            if contentParts:
                # Prüfe auf pre-extracted Content
                for part in contentParts:
                    if part.metadata.get("skipExtraction", False):
                        # Bereits extrahiert - verwende as-is, stelle sicher dass Metadaten vollständig
                        part.metadata.setdefault("contentFormat", "extracted")
                        part.metadata.setdefault("isPreExtracted", True)
                preparedContentParts.extend(contentParts)
            
            contentParts = preparedContentParts
        
        # WICHTIG: Kein Caching - Content wird immer neu verarbeitet für Konsistenz
        
        # Schritt 5C: Generiere Struktur
        structure = await self._generateStructure(
            prompt,
            contentParts or [],
            outputFormat,
            aiOperationId  # Parent-Referenz!
        )
        
        # Schritt 5D: Fülle Struktur
        filledStructure = await self._fillStructure(
            structure,
            contentParts or [],
            prompt,
            aiOperationId  # Parent-Referenz!
        )
        
        # Schritt 5E: Rendere Resultat
        renderedContent, mimeType = await self._renderResult(
            filledStructure,
            outputFormat,
            title or "Generated Document",
            prompt,
            aiOperationId  # Parent-Referenz!
        )
        
        # Baue Response
        documentName = self._determineDocumentName(filledStructure, outputFormat, title)
        
        docData = DocumentData(
            documentName=documentName,
            documentData=renderedContent,
            mimeType=mimeType,
            sourceJson=filledStructure
        )
        
        metadata = AiResponseMetadata(
            title=title or filledStructure.get("metadata", {}).get("title", "Generated Document"),
            operationType=opType.value
        )
        
        # Debug-Log (harmonisiert)
        self.services.utils.writeDebugFile(
            json.dumps(filledStructure, indent=2, ensure_ascii=False),
            "document_generation_response"
        )
        
        self.services.chat.progressLogFinish(aiOperationId, True)
        
        return AiResponse(
            content=json.dumps(filledStructure),
            metadata=metadata,
            documents=[docData]
        )
        
    except Exception as e:
        logger.error(f"Error in callAiContent: {str(e)}")
        self.services.chat.progressLogFinish(aiOperationId, False)
        raise

ContentPart Metadaten-Schema

Erforderliche Metadaten-Felder

ContentPart.metadata = {
    # Format-Identifikation (ERFORDERLICH)
    "contentFormat": Literal["reference", "object", "extracted"],  # ERFORDERLICH
    
    # Dokument-Referenz (ERFORDERLICH für alle)
    "documentId": str,  # Source-Dokument-ID
    
    # Für reference Format
    "documentReference": str,  # z.B. "docItem:doc_1:file.pdf"
    
    # Für object Format
    "originalFileName": Optional[str],  # Original-Dateiname
    
    # Für extracted Format
    "extractionPrompt": Optional[str],  # Prompt verwendet für Extraktion
    "extractionMethod": Optional[str],  # "vision", "text", "ocr", etc.
    
    # Verwendungs-Information
    "intent": str,  # "extract", "render", "reference"
    "usageHint": str,  # Wo/wie dieser Content verwendet werden soll
    
    # Pre-Extraction Flag
    "isPreExtracted": Optional[bool],  # True wenn bereits von vorheriger Action extrahiert
    "skipExtraction": Optional[bool],  # True wenn Extraktion übersprungen werden soll
    
    # Source-Tracking
    "sourceAction": Optional[str],  # Welche Action diesen Part erstellt hat
}

Harmonisierung & Cleanup: Debug-Datei-Log-Aufrufe

Aktuelle Situation

Debug-Datei-Log-Aufrufe sind über den Code verteilt mit unterschiedlichen Patterns:

  • Manche mit Checks: if hasattr(self.services, 'utils') and hasattr(self.services.utils, 'writeDebugFile')
  • Manche ohne Checks
  • Unterschiedliche Dateinamen-Patterns
  • Inkonsistente Verwendung

Harmonisiertes Pattern

Regel: Die Funktion self.services.utils.writeDebugFile() ist IMMER verfügbar - keine Checks nötig!

Standardisiertes Pattern:

# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(content, filename)

Harmonisierte Dateinamen:

  • user_input_analysis_prompt / user_input_analysis_response
  • document_intent_analysis_prompt / document_intent_analysis_response / document_intent_analysis_result
  • content_extraction_prompt_{documentId} / content_extraction_response / content_extraction_result
  • document_generation_structure_prompt / document_generation_structure_response
  • section_generation_prompt_{sectionId} / section_generation_response_{sectionId}
  • document_generation_response (finales JSON)

Cleanup-Aufgaben:

  1. Entferne alle Checks: if hasattr(...) → Direkter Aufruf
  2. Standardisiere alle Dateinamen nach obigem Pattern
  3. Stelle sicher, dass alle AI-Prompts und Responses geloggt werden
  4. Dokumentiere Dateinamen-Pattern im Code

Beispiel-Cleanup:

# VORHER (inkonsistent):
if self.services and hasattr(self.services, 'utils') and hasattr(self.services.utils, 'writeDebugFile'):
    try:
        self.services.utils.writeDebugFile(structurePrompt, "document_generation_structure_prompt")
    except Exception as e:
        logger.debug(f"Could not write debug file: {e}")

# NACHHER (harmonisiert):
# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(structurePrompt, "document_generation_structure_prompt")

Deep Analysis & Anpassung: ChatLog Parent/Child-Referenzen

Aktuelle Situation

ChatLog-Nachrichten haben teilweise fehlende oder inkorrekte Parent/Child-Referenzen:

  • Sub-Funktionen erstellen Logs ohne Parent-Referenz
  • Parent-Operation-ID wird nicht durchgereicht
  • Hierarchie geht verloren

Korrekte Hierarchie-Struktur

Prinzip: Jeder Log in einer Sub-Funktion ist ein Child des Logs in der aufrufenden Parent-Funktion.

Hierarchie-Beispiel:

callAiContent (aiOperationId)
├── _clarifyDocumentIntents (intentOperationId, parent: aiOperationId)
│   └── AI-Call für Intent-Analyse (parent: intentOperationId)
├── _extractAndPrepareContent (extractionOperationId, parent: aiOperationId)
│   └── extractContent (parent: extractionOperationId)
│       └── Per-Dokument-Logs (parent: extractionOperationId)
├── _generateStructure (structureOperationId, parent: aiOperationId)
│   └── AI-Call für Struktur (parent: structureOperationId)
├── _fillStructure (fillOperationId, parent: aiOperationId)
│   └── Per-Section-Logs (sectionOperationId, parent: fillOperationId)
│       └── AI-Call für Section (parent: sectionOperationId)
└── _renderResult (renderOperationId, parent: aiOperationId)
    └── renderReport (parent: renderOperationId)

Implementierungs-Regeln

  1. Jede Funktion, die Logs erstellt, muss parentOperationId Parameter haben:

    async def _clarifyDocumentIntents(
        self,
        ...,
        parentOperationId: str  # ERFORDERLICH!
    ) -> List[DocumentIntent]:
    
  2. Jede Operation-ID muss eindeutig sein:

    intentOperationId = f"{parentOperationId}_intent_analysis"
    
  3. Jeder progressLogStart muss parentOperationId verwenden:

    self.services.chat.progressLogStart(
        intentOperationId,
        "Document Intent Analysis",
        "Intent Analysis",
        "...",
        parentOperationId=parentOperationId  # ERFORDERLICH!
    )
    
  4. Jeder AI-Call muss parentOperationId weiterreichen:

    aiResponse = await self.services.ai.callAiContent(
        ...,
        parentOperationId=intentOperationId  # Parent-Referenz!
    )
    
  5. Jeder Service-Call muss parentOperationId weiterreichen:

    extractedResults = await self.services.extraction.extractContent(
        ...,
        operationId=extractionOperationId,
        parentOperationId=extractionOperationId  # Parent-Referenz!
    )
    

Deep Analysis Aufgaben

  1. Analysiere alle Funktionen, die Logs erstellen:

    • Identifiziere alle progressLogStart Aufrufe
    • Prüfe, ob parentOperationId gesetzt ist
    • Prüfe, ob parentOperationId durchgereicht wird
  2. Analysiere alle Sub-Funktionen:

    • Identifiziere alle Funktionen, die andere Funktionen aufrufen, die Logs erstellen
    • Stelle sicher, dass parentOperationId durchgereicht wird
  3. Analysiere alle Service-Calls:

    • Identifiziere alle Calls zu anderen Services (extraction, generation, etc.)
    • Stelle sicher, dass parentOperationId weitergegeben wird
  4. Validiere Hierarchie:

    • Stelle sicher, dass jede Log-Hierarchie korrekt ist
    • Prüfe, dass keine Logs ohne Parent existieren (außer Root-Logs)

Code-Beispiel: Korrekte Hierarchie

# Root-Funktion
async def callAiContent(..., parentOperationId: Optional[str] = None):
    aiOperationId = f"ai_content_{workflowId}_{int(time.time())}"
    
    # Root-Log (kein Parent)
    self.services.chat.progressLogStart(
        aiOperationId,
        "AI content processing",
        "Content Processing",
        "...",
        parentOperationId=parentOperationId  # Kann None sein für Root
    )
    
    # Sub-Funktion mit Parent-Referenz
    documentIntents = await self._clarifyDocumentIntents(
        ...,
        parentOperationId=aiOperationId  # Parent-Referenz!
    )

# Sub-Funktion
async def _clarifyDocumentIntents(..., parentOperationId: str):
    intentOperationId = f"{parentOperationId}_intent_analysis"
    
    # Child-Log mit Parent-Referenz
    self.services.chat.progressLogStart(
        intentOperationId,
        "Document Intent Analysis",
        "Intent Analysis",
        "...",
        parentOperationId=parentOperationId  # Parent-Referenz!
    )
    
    # AI-Call mit Parent-Referenz
    aiResponse = await self.services.ai.callAiContent(
        ...,
        parentOperationId=intentOperationId  # Parent-Referenz!
    )
    
    # Log abschließen
    self.services.chat.progressLogFinish(intentOperationId, True)

Image-Handling: Drei Formate

Format 1: Document Reference

ContentPart(
    typeGroup="image",
    mimeType="image/png",
    data="",  # Leer
    metadata={
        "contentFormat": "reference",
        "documentReference": "docItem:img_1:logo.png",
        "usageHint": "Include as attachment"
    }
)

Verwendung: Referenz auf Image-Dokument, kein Datentransfer.

Format 2: Object (Base64)

ContentPart(
    typeGroup="image",
    mimeType="image/png",
    data="iVBORw0KGgoAAAANS...",  # Base64-kodiert
    metadata={
        "contentFormat": "object",
        "usageHint": "Render as visual element"
    }
)

Verwendung: Image-Daten eingebettet, bereit für Rendering.

Format 3: Extracted Text

ContentPart(
    typeGroup="text",
    mimeType="text/plain",
    data="This image shows a red car...",  # Extrahierte Beschreibung
    metadata={
        "contentFormat": "extracted",
        "extractionPrompt": "Describe the image content",
        "extractionMethod": "vision",
        "originalImageId": "img_1"  # Referenz auf Original
    }
)

Verwendung: Text extrahiert aus Image, für Verwendung in Text-Sections.

Wichtig: In einem extrahierten ContentPart sollte klar sichtbar sein:

  • Image als Object (base64): contentFormat="object", typeGroup="image", data enthält base64
  • Image als Reference: contentFormat="reference", metadata.documentReference enthält Referenz
  • Image als Extracted Text: contentFormat="extracted", typeGroup="text", data enthält Text, metadata.originalImageId enthält Referenz zum Original

Migrations-Pfad

Schritt 1: ContentPart-Modell erweitern

  • Füge contentFormat zum Metadaten-Schema hinzu
  • Stelle sicher, dass alle ContentParts Format spezifiziert haben

Schritt 2: Document Intent Analysis überarbeiten ⚠️ KRITISCH

Problem: subDocumentPurposeAnalyzer.py verwendet ein veraltetes "purpose"-System mit vielen Tags (extract_text_content, include_image, analyze_image_vision, etc.), das nicht mit dem DocumentIntent-System aus diesem Konzept übereinstimmt.

Lösung: Ersetze das alte Purpose-System durch das DocumentIntent-System.

Zu entfernen:

  • Altes Purpose-System: extract_text_content, include_image, analyze_image_vision, use_as_template, use_as_reference, extract_data, attach, convert_format, translate, summarize, compare, merge, extract_tables_charts, use_for_styling, extract_metadata
  • DocumentPurposeAnalyzer Klasse mit analyzeDocumentPurposes() Methode
  • Alle Purpose-Listen und Purpose-Tags im Code

Zu implementieren:

  • DocumentIntent Modell verwenden (bereits definiert in datamodelExtraction.py)
  • _clarifyDocumentIntents() Funktion in mainServiceAi.py (wie in Phase 5A beschrieben)
  • Einfaches Intent-System: intents=["extract", "render", "reference"]
  • Jedes Dokument kann mehrere Intents haben: intents=["extract", "render"] für Bilder, die sowohl analysiert als auch gerendert werden sollen

Code-Änderungen:

# ALT (subDocumentPurposeAnalyzer.py):
purpose = "extract_text_content"  # oder "include_image", "analyze_image_vision", etc.

# NEU (DocumentIntent):
documentIntent = DocumentIntent(
    documentId="doc_1",
    intents=["extract", "render"],  # Einfach und klar!
    extractionPrompt="Extract text content from image",
    reasoning="Image needs both text extraction and visual rendering"
)

Dateien zu überarbeiten:

  • subDocumentPurposeAnalyzer.py: Entfernen oder komplett umschreiben auf DocumentIntent-System
  • Alle Stellen, die analyzeDocumentPurposes() aufrufen: Umstellen auf _clarifyDocumentIntents()
  • Alle Stellen, die "purpose" verwenden: Umstellen auf intents aus DocumentIntent

Schritt 3: callAiContent refactoren

  • Implementiere 5A-5E Phasen
  • Entferne duplizierte Logik aus Action-Methoden
  • Zentralisiere alle Dokument-Verarbeitung
  • Implementiere korrekte Parent/Child-Referenzen
  • Wichtig: Verwende DocumentIntent-System aus Schritt 2, nicht das alte Purpose-System

Schritt 4: Action-Methoden aktualisieren

  • Entferne Dokument-Verarbeitungslogik
  • Übergebe Parameter an callAiContent
  • Behalte nur Parameter-Vorbereitung

Schritt 5: Extraction-Service aktualisieren

  • Stelle sicher, dass ContentParts vollständige Metadaten haben
  • Markiere pre-extracted Content korrekt
  • Implementiere korrekte Parent/Child-Referenzen

Schritt 6: Generation-Service aktualisieren

  • Behandle drei Content-Formate
  • Verwende Metadaten für Format-Auswahl
  • Implementiere Multi-Dokument-Rendering
  • Wichtig: Entferne alle Referenzen zum alten Purpose-System

Schritt 7: Debug-Logging harmonisieren

  • Entferne alle Checks: if hasattr(...)
  • Standardisiere alle Dateinamen
  • Stelle sicher, dass alle AI-Prompts/Responses geloggt werden

Schritt 8: ChatLog-Hierarchie korrigieren

  • Analysiere alle Log-Erstellungen
  • Füge parentOperationId Parameter hinzu wo nötig
  • Stelle sicher, dass Parent-Referenzen korrekt durchgereicht werden
  • Validiere Hierarchie

Vorteile des überarbeiteten Konzepts

  1. Klarere Trennung: Jede Phase hat distincte Verantwortung
  2. Metadatenfluss: Vollständige Metadaten in jedem Schritt
  3. Format-Klarheit: Explizite Format-Indikation
  4. Zentralisierte Logik: Single Source of Truth
  5. Debuggability: Alle Prompts/Responses geloggt (harmonisiert)
  6. Flexibilität: Unterstützt alle Use Cases
  7. Wartbarkeit: Klare Struktur, einfach zu erweitern
  8. Korrekte Hierarchie: Alle ChatLogs haben korrekte Parent/Child-Referenzen
  9. Bessere Performance: Phase 1+2 kombiniert = weniger AI-Calls
  10. Context-Aware: Action Planner nutzt Context von vergangenen Rounds/Tasks

Use-Case-Analyse: Drei konkrete Szenarien

Use Case 1: Expenses CSV - Strukturierte Datenextraktion aus PDFs mit Bildern

User Prompt:

"combine all expenses documents (each with image in pdf) into a csv file with the columns date;shop;CHF;VAT;Description"

Input: 10 PDF-Dateien (jede enthält Bilder mit Rechnungsdaten)

Erwartetes Verhalten:

Phase 1+2: Intent-Analyse & Komplexität

  • Intent: Strukturierte Datenextraktion aus mehreren PDFs, Kombination in CSV
  • Komplexität: "complex" (Multi-Dokument, strukturierte Extraktion, Aggregation)
  • Fast Track: false (Dokument-Verarbeitung benötigt)

Phase 3: Task Planning

  • Task 1: Extrahiere strukturierte Daten aus allen PDFs
    • Objective: "Extract expense data (date, shop, CHF, VAT, description) from all PDF documents"
    • RequiredDocuments: Alle 10 PDFs
    • ExpectedOutput: "structured_data"

Phase 4: Task Execution

Action 1: context.extractContent

  • Extrahiert alle PDFs zu ContentParts
  • Wichtig: Bilder werden als typeGroup="image" mit contentFormat="object" (base64) erstellt
  • Aber: Für Datenextraktion benötigen wir auch Text-Extraktion aus Bildern

Action 2: ai.process (mit spezifischem Prompt für strukturierte Datenextraktion)

  • Input: Alle extrahierten ContentParts
  • Intent-Analyse (5A):
    • PDFs: intents=["extract"] (Text-Extraktion)
    • Bilder in PDFs: intents=["extract"] (OCR/Text-Extraktion für strukturierte Daten)
    • ExtractionPrompt für Bilder: "Extract structured expense data: date, shop name, amount in CHF, VAT amount, description"
  • Content-Extraktion (5B):
    • PDF-Text wird extrahiert: contentFormat="extracted"
    • Bilder werden analysiert (OCR/Vision): contentFormat="extracted" mit extractionMethod="vision"
    • Wichtig: Bilder haben beide Formate möglich:
      • contentFormat="object" für visuelle Darstellung (falls benötigt)
      • contentFormat="extracted" für Text-Extraktion (für CSV)
  • Struktur-Generierung (5C):
    • Struktur definiert CSV-Format mit Spalten: date, shop, CHF, VAT, Description
  • Struktur-Abfüllen (5D):
    • Extrahiert strukturierte Daten aus allen ContentParts
    • Kombiniert Daten aus allen 10 PDFs
  • Rendering (5E):
    • Rendert zu CSV-Format

Kritische Punkte:

  • Bilder müssen analysiert werden (OCR/Vision) für strukturierte Datenextraktion
  • Multi-Dokument-Verarbeitung: Alle 10 PDFs müssen verarbeitet werden
  • Strukturierte Datenextraktion: Spezifische Felder müssen extrahiert werden
  • Kombination: Daten aus allen PDFs müssen in einem CSV kombiniert werden

Konzept-Abdeckung:

  • Phase 5A: Intent-Analyse erkennt "extract" für Bilder (OCR benötigt)
  • Phase 5B: Bilder werden sowohl als "object" (base64) als auch als "extracted" (Text) behandelt
  • Phase 5C: Struktur definiert CSV-Format
  • Phase 5D: Extrahiert und kombiniert strukturierte Daten
  • Phase 5E: Rendert zu CSV

Mögliche Verbesserung:

  • Konzept sollte explizit erwähnen: Bilder können beide Formate gleichzeitig haben (object für Rendering, extracted für Text-Analyse)

Use Case 2: PowerPoint mit Bildern - Bilder rendern UND analysieren

User Prompt:

"make a powerpoint slideshow for the customer about the product with the images integrated in the slides. the images also give information about the storyline"

Input: 10 Bilder

Erwartetes Verhalten:

Phase 1+2: Intent-Analyse & Komplexität

  • Intent: PowerPoint-Generierung mit Bildern, Storyline-Analyse
  • Komplexität: "complex" (Multi-Bild, Generierung, Storyline-Analyse)
  • Fast Track: false

Phase 3: Task Planning

  • Task 1: Generiere PowerPoint mit integrierten Bildern und Storyline
    • Objective: "Create PowerPoint presentation with images integrated in slides, analyze images for storyline information"
    • RequiredDocuments: Alle 10 Bilder
    • ExpectedOutput: "pptx"

Phase 4: Task Execution

Action 1: ai.generateDocument (oder ai.process mit outputFormat="pptx")

  • Input: Alle 10 Bilder
  • Intent-Analyse (5A):
    • Bilder: intents=["extract", "render"] (beide!)
      • "render": Bilder müssen in Slides integriert werden
      • "extract": Bilder müssen analysiert werden für Storyline-Information
    • ExtractionPrompt: "Analyze image content to extract storyline information, product features, and narrative elements"
  • Content-Extraktion (5B):
    • Für Rendering: Bilder als contentFormat="object" (base64) für PowerPoint-Integration
    • Für Storyline: Bilder analysiert als contentFormat="extracted" (Text-Beschreibung) für Storyline-Generierung
    • Wichtig: Jedes Bild erzeugt zwei ContentParts:
      • ContentPart(id="img_1_obj", contentFormat="object", ...) - für Rendering
      • ContentPart(id="img_1_ext", contentFormat="extracted", ...) - für Storyline-Analyse
  • Struktur-Generierung (5C):
    • Struktur definiert PowerPoint-Slides
    • Jede Slide kann Bilder enthalten (object-Format)
    • Storyline-Information aus extracted-Format wird für Slide-Content verwendet
  • Struktur-Abfüllen (5D):
    • Bilder werden in Slides integriert (object-Format)
    • Storyline-Text wird aus extracted-Format generiert
    • AI generiert Slide-Content basierend auf Storyline
  • Rendering (5E):
    • Rendert zu PPTX-Format mit integrierten Bildern

Kritische Punkte:

  • Bilder müssen beide Formate haben: object (für Rendering) UND extracted (für Storyline)
  • Multi-Bild-Verarbeitung: Alle 10 Bilder müssen verarbeitet werden
  • Storyline-Analyse: Bilder müssen analysiert werden für narrative Elemente
  • PowerPoint-Generierung: Strukturierte Slide-Generierung mit Bildern

Konzept-Abdeckung:

  • Phase 5A: Intent-Analyse erkennt beide Intents: ["extract", "render"]
  • Phase 5B: Bilder werden in beiden Formaten erstellt (object + extracted)
  • Phase 5C: Struktur definiert PowerPoint-Format mit Bild-Platzhaltern
  • Phase 5D: Integriert Bilder (object) und verwendet Storyline (extracted)
  • Phase 5E: Rendert zu PPTX

Mögliche Verbesserung:

  • Konzept sollte explizit erwähnen: Ein Dokument kann mehrere ContentParts erzeugen (z.B. Bild als object UND als extracted)

Use Case 3: SharePoint-Analyse - Keine angehängten Dokumente

User Prompt:

"make me an overview about all sharepoint folders and the summary of documents stored in each folder for sharepoint https://xxx.yy.zz"

Input: Keine Dokumente angehängt

Erwartetes Verhalten:

Phase 1+2: Intent-Analyse & Komplexität

  • Intent: SharePoint-Integration, Folder-Analyse, Dokument-Zusammenfassungen
  • Komplexität: "complex" (Externe Integration, Multi-Step-Workflow)
  • Fast Track: false

Phase 3: Task Planning

  • Task 1: Analysiere SharePoint-Struktur
    • Objective: "List all folders in SharePoint site and analyze document structure"
    • RequiredDocuments: Keine (SharePoint-URL im Prompt)
    • ExpectedOutput: "structured_overview"
  • Task 2: Generiere Zusammenfassungen
    • Objective: "Generate summaries for documents in each folder"
    • RequiredDocuments: Von Task 1 (SharePoint-Dokumente)
    • ExpectedOutput: "summary_document"

Phase 4: Task Execution

Action 1: sharepoint.listDocuments

  • Listet alle Ordner im SharePoint
  • Gibt Dokument-Referenzen zurück
  • Wichtig: Keine ContentParts erstellt - nur Referenzen

Action 2: sharepoint.readDocuments (für jeden Ordner)

  • Liest Dokumente aus SharePoint
  • Erstellt ContentParts mit contentFormat="reference" (SharePoint-Referenzen)
  • Oder lädt Dokumente herunter und erstellt ContentParts

Action 3: ai.process (für Zusammenfassungen)

  • Input: SharePoint-Dokument-Referenzen oder heruntergeladene Dokumente
  • Intent-Analyse (5A):
    • Dokumente: intents=["extract"] (Zusammenfassung benötigt Extraktion)
    • ExtractionPrompt: "Extract key information and create summary"
  • Content-Extraktion (5B):
    • Dokumente werden extrahiert: contentFormat="extracted"
  • Struktur-Generierung (5C):
    • Struktur definiert Overview-Format mit Foldern und Zusammenfassungen
  • Struktur-Abfüllen (5D):
    • Generiert Zusammenfassungen für jedes Dokument
    • Gruppiert nach Ordnern
  • Rendering (5E):
    • Rendert zu Overview-Dokument (HTML, DOCX, etc.)

Kritische Punkte:

  • Keine angehängten Dokumente: Workflow startet ohne Dokumente
  • SharePoint-Integration: Externe Service-Actions werden verwendet
  • Multi-Step-Workflow: Mehrere Actions sequenziell
  • Dokument-Referenzen: SharePoint-Dokumente werden als Referenzen behandelt

Konzept-Abdeckung:

  • Phase 4: Action Planner kann externe Actions (sharepoint.*) verwenden
  • Phase 5A: Intent-Analyse funktioniert auch für SharePoint-Dokumente
  • Phase 5B: SharePoint-Dokumente können als "reference" oder "extracted" behandelt werden
  • Phase 5C-5E: Standard-Generierungsprozess funktioniert

Mögliche Verbesserung:

  • Konzept sollte explizit erwähnen: Workflows können ohne angehängte Dokumente starten (Dokumente kommen von externen Services)

Konzept-Verbesserungen basierend auf Use-Case-Analyse

Verbesserung 1: Mehrfache ContentParts pro Dokument

Problem: Ein Dokument (z.B. Bild) kann sowohl als "object" (für Rendering) als auch als "extracted" (für Text-Analyse) benötigt werden.

Lösung: Phase 5B sollte explizit unterstützen, dass ein Dokument mehrere ContentParts erzeugen kann:

async def _extractAndPrepareContent(...):
    """
    Ein Dokument kann mehrere ContentParts erzeugen, wenn mehrere Intents vorhanden sind.
    Beispiel: Bild mit intents=["extract", "render"] erzeugt:
    - ContentPart(contentFormat="object", ...) für Rendering
    - ContentPart(contentFormat="extracted", ...) für Text-Analyse
    """
    for document in documents:
        intent = getIntentForDocument(document.id, documentIntents)
        
        # Wenn mehrere Intents: Erzeuge mehrere ContentParts
        if "render" in intent.intents and "extract" in intent.intents:
            # Erzeuge object-ContentPart für Rendering
            objectPart = ContentPart(
                id=f"obj_{document.id}",
                contentFormat="object",
                ...
            )
            allContentParts.append(objectPart)
            
            # Erzeuge extracted-ContentPart für Text-Analyse
            extractedPart = await self._extractTextFromDocument(document, intent.extractionPrompt)
            extractedPart.id = f"ext_{document.id}"
            extractedPart.metadata["contentFormat"] = "extracted"
            extractedPart.metadata["originalDocumentId"] = document.id
            extractedPart.metadata["relatedContentPartId"] = objectPart.id  # Verknüpfung
            allContentParts.append(extractedPart)

Verbesserung 2: Strukturierte Datenextraktion

Problem: Use Case 1 benötigt strukturierte Datenextraktion (CSV mit spezifischen Spalten).

Lösung: Phase 5C sollte explizit strukturierte Datenextraktion unterstützen:

async def _generateStructure(...):
    """
    Für strukturierte Datenextraktion (z.B. CSV):
    - Struktur definiert explizit die erwarteten Felder/Spalten
    - ContentParts werden diesen Feldern zugeordnet
    """
    # Prüfe ob strukturierte Extraktion benötigt wird
    if "extract structured data" in userPrompt.lower() or outputFormat == "csv":
        # Struktur definiert explizit Felder
        structurePrompt = f"""
        Extract structured data with fields: {fields}
        Map content parts to these fields.
        ...
        """

Verbesserung 3: Externe Service-Integration

Problem: Use Case 3 startet ohne Dokumente, Dokumente kommen von SharePoint.

Lösung: Phase 4 sollte explizit dokumentieren, dass externe Actions Dokumente bereitstellen können:

# Action 1: sharepoint.listDocuments
# Erstellt Dokument-Referenzen, die in AVAILABLE_DOCUMENTS_INDEX verfügbar werden

# Action 2: sharepoint.readDocuments
# Lädt Dokumente herunter und erstellt ContentParts
# Diese werden dann in nachfolgenden Actions verwendet

Offene Fragen & Entscheidungen

  1. Multi-Dokument-Rendering: In einem Call - Resultat kann mehrere Dokumente sein
  2. Section-Generierung Parallelisierung: Parallel, wenn möglich - Sections können parallel generiert werden, um Performance zu verbessern
  3. Error-Handling: Fehlerhafte Sections mit Fehlermeldung rendern - Wenn eine Section fehlschlägt, wird eine Fehlermeldung in der gerenderten Ausgabe angezeigt, statt den gesamten Prozess zu stoppen
  4. Caching: Nein, kein Caching - Extrahierter Content wird nicht gecacht, um Konsistenz und Klarheit zu gewährleisten
  5. Validierung: Keine Validierung - Es ist kein Validierungsprozess definiert, und es gibt keinen definierten Prozess für den Fall, dass eine Validierung fehlschlagen würde. Klarer Code ohne zusätzliche Komplexität.
  6. Mehrfache ContentParts: Ja, natürlich! - Das ist das grundlegende Design! Ein Dokument kann mehrere ContentParts erzeugen (z.B. object + extracted für Bilder, die sowohl gerendert als auch analysiert werden sollen)
  7. Strukturierte Datenextraktion: Ja, Phase 5C soll dies unterstützen - Phase 5C (Struktur-Generierung) soll explizit strukturierte Datenextraktion unterstützen (z.B. CSV mit spezifischen Spalten)

Nächste Schritte

  1. Review und Approve dieses Konzepts
  2. Implementiere ContentPart-Metadaten-Erweiterungen
  3. Refactore callAiContent nach 5A-5E Struktur
  4. Aktualisiere Action-Methoden für zentralisierte Logik
  5. Harmonisiere Debug-Logging (entferne Checks, standardisiere Namen)
  6. Korrigiere ChatLog-Hierarchie (füge Parent-Referenzen hinzu)
  7. Implementiere Multi-Dokument-Rendering
  8. Teste mit verschiedenen Szenarien
  9. Dokumentiere API-Änderungen