# Implementierungskonzept: Chapter-basierte Generierungs-Struktur ## Übersicht Wechsel von section-basierter zu **chapter-basierter Struktur** zur Lösung folgender Probleme: 1. Section-Generierungs-Prompts kennen den Standard-JSON-Schema nicht 2. Gemischte Element-Typen können nicht korrekt verarbeitet werden 3. Sections sind zu starr - können nicht mehrere Element-Typen enthalten ## Kritische Analyse: Aggregation mehrerer ContentParts **Problem identifiziert:** - Bestimmte `content_type` (z.B. `table`, `bullet_list`) benötigen Aggregation mehrerer ContentParts - Beispiel: 20 Spesenbelege → eine Excel-Tabelle - Aktuell: Jeder ContentPart wird einzeln verarbeitet → keine Aggregation möglich **Lösung implementiert:** - Generische `_needsAggregation()` Funktion erkennt Aggregations-Bedarf - Wenn Aggregation nötig: Alle ContentParts zusammen an `callAi` übergeben - Verwendet `callAi` statt `callAiPlanning` für ContentParts-Unterstützung - Automatisches Chunking funktioniert auch bei aggregierten Parts **Unterstützte Aggregations-Typen:** - `table`: Mehrere Parts → eine Tabelle (z.B. Excel-Liste) - `bullet_list`: Mehrere Parts → eine Liste - Weitere Typen können einfach hinzugefügt werden ## Kernprinzipien 1. **Chapter-basierte Struktur**: Struktur-Generierung definiert Chapters, nicht Sections 2. **Chapters enthalten Sections**: Jedes Chapter kann mehrere Sections unterschiedlicher Typen enthalten 3. **Standard JSON Schema in Prompts**: Chapter-Generierungs-Prompts enthalten vollständiges Standard-JSON-Schema 4. **Flexible Content-Verarbeitung**: Chapters können gemischte ContentParts enthalten 5. **Hierarchische Überschriften**: Chapters sind hierarchische Überschriften (Level 1, 2, 3, etc.) --- ## Architektur ### Chapters als Helper-Struktur Chapters sind eine intermediate Helper-Struktur für die Generierung. Die finale Output-Struktur bleibt unverändert: **Finale Output-Struktur:** ``` Document └── Sections[] └── content_type └── elements[] ``` **Chapter-Struktur (Helper):** ``` ChapterStructure └── Chapters[] └── level, title └── contentPartIds[] └── contentPartInstructions{} └── generationHint └── Sections[] (generiert) └── content_type └── elements[] ``` **Wichtig:** - Chapter = Container zur Generierung eines Dokument-Teils mit Sections - Jedes Chapter hat eine vordefinierte Heading-Section (Chapter-Title + Level) - Finale Output-Struktur hat keine Chapters - nur Sections - Chapters werden zu Sections geflatten für das finale Output --- ## Workflow-Phasen **Wichtig - Debug-File-Logging:** - Alle AI-Calls und Responses werden in Debug-Files geloggt - Prompts: `{operationType}_{identifier}_prompt.txt` - Responses: `{operationType}_{identifier}_response.txt` - Beispiele: - Phase 5C: `chapter_structure_generation_prompt.txt` / `chapter_structure_generation_response.txt` - Phase 5D.1: `chapter_structure_{chapterId}_prompt.txt` / `chapter_structure_{chapterId}_response.txt` - Phase 5D.2: `section_content_{sectionId}_prompt.txt` / `section_content_{sectionId}_response.txt` --- ### Phase 5B: Content Extraction **Was passiert:** - Extrahiert Content basierend auf Intents - Bereitet ContentParts mit Metadaten vor - Alle Extraktionen passieren VOR Struktur-Generierung **Output:** - Liste von ContentParts mit vollständigen Metadaten --- ### Phase 5C: Chapter-Struktur-Generierung **Was passiert:** - Generiert Chapter-Struktur (Table of Contents) - Definiert für jedes Chapter: - Level, Title - contentPartIds - contentPartInstructions - generationHint **Input:** - `userPrompt`: User-Anfrage - `contentParts`: Alle vorbereiteten ContentParts (bereits extrahiert) - `outputFormat`: Ziel-Format **Process:** ```python async def _generateChapterStructure( self, userPrompt: str, contentParts: List[ContentPart], outputFormat: str, parentOperationId: str ) -> Dict[str, Any]: structurePrompt = self._buildChapterStructurePrompt( userPrompt=userPrompt, contentParts=contentParts, outputFormat=outputFormat ) # Debug: Log Prompt self.services.utils.writeDebugFile( structurePrompt, "chapter_structure_generation_prompt" ) aiResponse = await self.services.ai.callAiPlanning( prompt=structurePrompt ) # Debug: Log Response self.services.utils.writeDebugFile( aiResponse, "chapter_structure_generation_response" ) structure = json.loads( self.services.utils.jsonExtractString(aiResponse) ) return structure ``` **Prompt-Format:** ``` USER REQUEST: {userPrompt} AVAILABLE CONTENT PARTS: {contentPartsIndex} TASK: Generiere Chapter-Struktur für die zu generierenden Dokumente. Für jedes Chapter: - chapter id - level (1, 2, 3, etc.) - title - contentPartIds: [Liste von ContentPart-IDs] - contentPartInstructions: { "partId": { "instruction": "Wie Content strukturiert werden soll" } } - generationHint: Beschreibung des Inhalts RETURN JSON: { "metadata": {...}, "documents": [{ "chapters": [ { "id": "chapter_1", "level": 1, "title": "Introduction", "contentPartIds": ["part_ext_1"], "contentPartInstructions": {...}, "generationHint": "...", "sections": [] } ] }] } ``` **Output-Struktur:** ```json { "metadata": {"title": "...", "language": "de"}, "documents": [{ "chapters": [ { "id": "chapter_summary", "level": 1, "title": "Summary", "contentPartIds": ["extracted_doc1_part1"], "contentPartInstructions": { "extracted_doc1_part1": { "instruction": "Erstelle Zusammenfassungsparagraph" } }, "generationHint": "Create summary", "sections": [] } ] }] } ``` --- ### Phase 5D: Chapter-Content-Generierung **Zwei-Phasen-Ansatz:** #### Phase 5D.1: Sections-Struktur generieren **Was passiert:** - Generiert Sections-Struktur für jedes Chapter (ohne Content) - Sections enthalten: content_type, contentPartIds, generationHint, useAiCall - AI setzt `useAiCall` Flag direkt im JSON **useAiCall Flag:** - `useAiCall = true` wenn: - `content_type != "paragraph"` (Transformation nötig) - Oder spezifische Anweisungen in contentPartInstructions (nur Teile verwenden) - `useAiCall = false` sonst (Content direkt einfügen) **Process:** ```python async def _generateChapterStructure( self, chapterStructure: Dict[str, Any], contentParts: List[ContentPart], userPrompt: str, parentOperationId: str ) -> Dict[str, Any]: for doc in chapterStructure.get("documents", []): for chapter in doc.get("chapters", []): chapterId = chapter.get("id", "unknown") chapterPrompt = self._buildChapterStructurePrompt( chapter=chapter, contentPartIds=chapter.get("contentPartIds"), contentPartInstructions=chapter.get("contentPartInstructions"), userPrompt=userPrompt ) # Debug: Log Prompt self.services.utils.writeDebugFile( chapterPrompt, f"chapter_structure_{chapterId}_prompt" ) aiResponse = await self.services.ai.callAiPlanning( prompt=chapterPrompt ) # Debug: Log Response self.services.utils.writeDebugFile( aiResponse, f"chapter_structure_{chapterId}_response" ) sectionsStructure = json.loads( self.services.utils.jsonExtractString(aiResponse) ) chapter["sections"] = sectionsStructure.get("sections", []) # Setze useAiCall Flag (falls nicht von AI gesetzt) for section in chapter["sections"]: if "useAiCall" not in section: contentType = section.get("content_type", "paragraph") useAiCall = contentType != "paragraph" # Prüfe contentPartInstructions if not useAiCall: for partId in section.get("contentPartIds", []): instruction = contentPartInstructions.get(partId, {}).get("instruction", "") if instruction and instruction.lower() not in ["include full text", "include all content"]: useAiCall = True break section["useAiCall"] = useAiCall return chapterStructure ``` **Prompt-Format:** ``` TASK: Generate Chapter Sections Structure CHAPTER METADATA: - Chapter ID: {chapterId} - Chapter Level: {chapterLevel} - Chapter Title: {chapterTitle} - Generation Hint: {generationHint} WICHTIG: Chapter hat bereits vordefinierte Heading-Section. Generiere NICHT eine Heading-Section für Chapter-Title! AVAILABLE CONTENT PARTS: {contentPartIds} # Nur IDs, KEINE Previews! Für jeden ContentPart: - ContentPart ID: {partId} - Format: {contentFormat} - Instruction: {contentPartInstructions[partId].instruction} STANDARD JSON SCHEMA FOR SECTIONS: [... Standard JSON Schema ...] Return JSON: { "sections": [ { "id": "section_1", "content_type": "paragraph", "contentPartIds": ["part_ext_1"], "generationHint": "...", "useAiCall": false, # AI setzt Flag direkt "elements": [] } ] } ``` #### Phase 5D.2: Sections mit ContentParts füllen **Was passiert:** - Füllt Sections separat mit ContentParts - Basierend auf `useAiCall` Flag: - `useAiCall = true`: Separater AI-Call mit ContentPart(s) (Chunking bei großen Parts) - `useAiCall = false`: Content direkt einfügen - Rendering/Reference content: Immer direkt ohne AI-Call **Aggregation mehrerer ContentParts:** - Bestimmte `content_type` benötigen Aggregation mehrerer Parts: - `table`: Mehrere Parts → eine Tabelle (z.B. 20 Belege → Excel-Liste) - `bullet_list`: Mehrere Parts → eine Liste - `paragraph`: Kann auch aggregiert werden (z.B. Vergleich mehrerer Dokumente) - Wenn Aggregation nötig: Alle Parts zusammen an AI übergeben (nicht einzeln) - Verwendet `callAi` statt `callAiPlanning` für ContentParts-Unterstützung **Process:** ```python async def _fillChapterSections( self, chapterStructure: Dict[str, Any], contentParts: List[ContentPart], userPrompt: str, parentOperationId: str ) -> Dict[str, Any]: for doc in chapterStructure.get("documents", []): for chapter in doc.get("chapters", []): for section in chapter.get("sections", []): elements = [] useAiCall = section.get("useAiCall", False) contentType = section.get("content_type", "paragraph") contentPartIds = section.get("contentPartIds", []) # Prüfe ob Aggregation nötig ist needsAggregation = self._needsAggregation( contentType=contentType, contentPartCount=len(contentPartIds) ) if needsAggregation and useAiCall: # Aggregation: Alle Parts zusammen verarbeiten sectionParts = [ self._findContentPartById(pid, contentParts) for pid in contentPartIds ] sectionParts = [p for p in sectionParts if p is not None] if sectionParts: sectionId = section.get("id", "unknown") sectionPrompt = self._buildSectionContentPrompt( section=section, contentParts=sectionParts, # ALLE PARTS! generationHint=section.get("generationHint"), userPrompt=userPrompt ) # Debug: Log Prompt self.services.utils.writeDebugFile( sectionPrompt, f"section_content_{sectionId}_prompt" ) # Verwende callAi für ContentParts-Unterstützung request = AiCallRequest( prompt=sectionPrompt, contentParts=sectionParts, # ALLE PARTS! options=AiCallOptions( operationType=OperationTypeEnum.DATA_ANALYSE, priority=PriorityEnum.BALANCED, processingMode=ProcessingModeEnum.DETAILED ) ) aiResponse = await self.services.ai.callAi(request) # Debug: Log Response self.services.utils.writeDebugFile( aiResponse.content, f"section_content_{sectionId}_response" ) elements.extend(parseElements(aiResponse.content)) else: # Einzelverarbeitung: Jeder Part einzeln for partId in contentPartIds: part = self._findContentPartById(partId, contentParts) if not part: continue contentFormat = part.metadata.get("contentFormat") if contentFormat == "extracted": if useAiCall: # AI-Call mit einzelnen ContentPart sectionId = section.get("id", "unknown") sectionPrompt = self._buildSectionContentPrompt( section=section, contentParts=[part], # EIN PART generationHint=section.get("generationHint"), userPrompt=userPrompt ) # Debug: Log Prompt self.services.utils.writeDebugFile( sectionPrompt, f"section_content_{sectionId}_prompt" ) request = AiCallRequest( prompt=sectionPrompt, contentParts=[part], options=AiCallOptions(...) ) aiResponse = await self.services.ai.callAi(request) # Debug: Log Response self.services.utils.writeDebugFile( aiResponse.content, f"section_content_{sectionId}_response" ) elements.extend(parseElements(aiResponse.content)) else: # Content direkt einfügen elements.append({ "type": "paragraph", "content": part.data or "" }) elif contentFormat == "reference": elements.append({ "type": "reference", "documentReference": part.metadata.get("documentReference") }) elif contentFormat == "object": elements.append({ "type": "image", "base64Data": part.data }) section["elements"] = elements return chapterStructure def _needsAggregation( self, contentType: str, contentPartCount: int ) -> bool: """ Bestimmt ob mehrere ContentParts aggregiert werden müssen. Aggregation nötig wenn: - content_type erfordert Aggregation (table, bullet_list) - UND mehrere ContentParts vorhanden sind (> 1) Args: contentType: Section content_type contentPartCount: Anzahl der ContentParts in dieser Section Returns: True wenn Aggregation nötig, False sonst """ aggregationTypes = ["table", "bullet_list"] if contentType in aggregationTypes and contentPartCount > 1: return True # Optional: Auch für paragraph wenn mehrere Parts vorhanden # (z.B. Vergleich mehrerer Dokumente) if contentType == "paragraph" and contentPartCount > 1: # Prüfe generationHint für Hinweise auf Aggregation # (z.B. "Vergleiche", "Zusammenfassung", "Liste") return False # Standard: Keine Aggregation für paragraph return False ``` **Prompt-Format (für AI-Call):** **Einzelverarbeitung (ein ContentPart):** ``` TASK: Generate Section Content SECTION METADATA: - Section ID: {sectionId} - Content Type: {contentType} - Generation Hint: {generationHint} CONTEXT: - User Request: {userPrompt} - What to generate: {generationHint} CONTENT PART: - ContentPart ID: {partId} - Format: extracted - ContentPart wird als Parameter übergeben (nicht im Prompt!) - Kann sehr groß sein (z.B. 200MB) → Chunking automatisch STANDARD JSON SCHEMA FOR ELEMENTS: [... Standard JSON Schema ...] Return JSON: { "elements": [ {"type": "paragraph", "content": "..."}, {"type": "table", "headers": [...], "rows": [...]} ] } ``` **Aggregation (mehrere ContentParts):** ``` TASK: Generate Section Content SECTION METADATA: - Section ID: {sectionId} - Content Type: {contentType} # z.B. "table" - Generation Hint: {generationHint} # z.B. "Erstelle Excel-Liste aller Spesenbelege" CONTEXT: - User Request: {userPrompt} - What to generate: {generationHint} CONTENT PARTS (Aggregation): - Anzahl: {contentPartCount} ContentParts - Alle ContentParts werden als Parameter übergeben (nicht im Prompt!) - Jeder Part kann sehr groß sein → Chunking automatisch - WICHTIG: Aggregiere ALLE Parts zu einem Element (z.B. eine Tabelle) ContentPart IDs: {contentPartIds} # Liste aller IDs STANDARD JSON SCHEMA FOR ELEMENTS: [... Standard JSON Schema ...] Return JSON: { "elements": [ { "type": "table", "headers": ["Spalte1", "Spalte2", ...], "rows": [ ["Daten aus Part 1", ...], ["Daten aus Part 2", ...], ... ] } ] } ``` **Hauptfunktion:** ```python async def _generateChapterContent( self, chapterStructure: Dict[str, Any], contentParts: List[ContentPart], userPrompt: str, parentOperationId: str ) -> Dict[str, Any]: # Phase 5D.1: Sections-Struktur generieren structureWithSections = await self._generateChapterStructure( chapterStructure, contentParts, userPrompt, parentOperationId ) # Phase 5D.2: Sections mit ContentParts füllen filledStructure = await self._fillChapterSections( structureWithSections, contentParts, userPrompt, parentOperationId ) return filledStructure ``` --- ## Standard JSON Schema ### Supported Section Types ```python supportedSectionTypes = [ "table", "bullet_list", "heading", "paragraph", "code_block", "image" ] ``` ### Section Element Types 1. **Standard Elements:** - `heading`, `paragraph`, `table`, `bullet_list`, `code_block`, `image` 2. **Special Elements:** - `extracted_text`: Extrahierter Text mit Source - `reference`: Dokument-Referenz --- ## Flattening: Chapters zu Sections **Wichtig:** Finale Output-Struktur hat keine Chapters - nur Sections. ```python def flattenChapterStructureToSections( chapterStructure: Dict[str, Any] ) -> Dict[str, Any]: result = { "metadata": chapterStructure.get("metadata", {}), "documents": [] } for doc in chapterStructure.get("documents", []): flattened_doc = { "id": doc.get("id"), "title": doc.get("title"), "filename": doc.get("filename"), "sections": [] } for chapter in doc.get("chapters", []): # 1. Vordefinierte Heading-Section heading_section = { "id": f"{chapter['id']}_heading", "content_type": "heading", "elements": [{ "type": "heading", "content": chapter.get("title"), "level": chapter.get("level", 1) }] } flattened_doc["sections"].append(heading_section) # 2. Generierte Sections flattened_doc["sections"].extend(chapter.get("sections", [])) result["documents"].append(flattened_doc) return result ``` --- ## Pydantic Models ```python class ContentPartInstruction(BaseModel): instruction: str = Field( description="Anweisung, wie der bereits extrahierte Content strukturiert werden soll" ) class Chapter(BaseModel): id: str level: int = Field(ge=1, le=6) title: str contentPartIds: List[str] = Field(default_factory=list) contentPartInstructions: Dict[str, ContentPartInstruction] = Field(default_factory=dict) generationHint: str sections: List[Dict[str, Any]] = Field(default_factory=list) class ChapterStructure(BaseModel): metadata: Dict[str, Any] documents: List[Dict[str, Any]] def flattenToSections(self) -> Dict[str, Any]: # Flattening-Logik ... ``` --- ## Chunking für große ContentParts **Wichtig:** ContentParts können sehr groß sein (z.B. 200MB). Chunking passiert automatisch. **Flow:** 1. `callAi` mit ContentParts → routet zu `processContentPartsWithAi` 2. `processContentPartsWithAi` → `processContentPartWithFallback` für jeden Part 3. Wenn Part zu groß → `chunkContentPartForAi` → Chunking passiert EINMAL 4. Gechunkte Parts werden sequenziell verarbeitet 5. `_callWithModel` macht kein weiteres Chunking **Keine Rekursion:** - Chunking passiert einmal pro ContentPart - Gechunkte Parts werden sequenziell verarbeitet (nicht rekursiv) - `_callWithModel` ruft nur Model auf (kein Chunking) --- ## Implementierungsanforderungen 1. **Phase 5C**: Generiert Chapters statt Sections 2. **Phase 5D.1**: Generiert Sections-Struktur mit useAiCall Flag 3. **Phase 5D.2**: Füllt Sections basierend auf useAiCall Flag 4. **Flattening**: Konvertiert Chapters zu finaler Section-Struktur 5. **Pydantic Models**: ChapterStructure Model definieren 6. **Standard-JSON-Schema**: In Chapter-Prompts enthalten 7. **Renderer**: Bleiben unverändert (verwenden finale Section-Struktur) --- ## Wichtige Punkte 1. **ContentParts Integration:** - ContentParts kommen aus Phase 5B (bereits extrahiert) - Phase 5D.1: Nur IDs im Prompt (keine Previews) - Phase 5D.2: ContentParts als Parameter übergeben (nicht im Prompt) 2. **useAiCall Flag:** - AI setzt Flag direkt im JSON - Fallback: Automatisch gesetzt basierend auf content_type und instructions - Generisch, sprachunabhängig (keine Stichwort-Abfragen) 3. **Aggregation mehrerer ContentParts:** - Bestimmte content_types benötigen Aggregation (table, bullet_list) - Wenn mehrere Parts vorhanden: Alle zusammen an AI übergeben - Verwendet `callAi` statt `callAiPlanning` für ContentParts-Unterstützung - Automatisches Chunking bei großen aggregierten Parts 4. **Chunking:** - Automatisch bei großen ContentParts - Funktioniert auch bei Aggregation mehrerer Parts - Keine Rekursion möglich - Chunks werden sequenziell verarbeitet 5. **Mehrere Dokumente:** - Struktur unterstützt mehrere Dokumente mit eigenen Chapters 6. **ContentPart Instructions:** - ContentParts sind bereits extrahiert - Instructions geben Kontext für Strukturierung - Kein "usage" Feld (Format durch contentFormat klar) 7. **Debug-File-Logging:** - Alle AI-Calls und Responses werden in Debug-Files geloggt - Prompts: `{operationType}_{identifier}_prompt.txt` - Responses: `{operationType}_{identifier}_response.txt` - Beispiele: - Phase 5C: `chapter_structure_generation_prompt.txt` / `chapter_structure_generation_response.txt` - Phase 5D.1: `chapter_structure_{chapterId}_prompt.txt` / `chapter_structure_{chapterId}_response.txt` - Phase 5D.2: `section_content_{sectionId}_prompt.txt` / `section_content_{sectionId}_response.txt` --- ## Beispiel-Szenarien ### Beispiel 1: Excel-Liste der Spesenbelege **User Prompt:** "Erstelle eine Excel-Liste der Spesenbelege" **Input:** 20 PDF-Dokumente, jedes mit einem Foto eines Beleges **Phase 5B:** - 20 PDFs werden extrahiert → 20 ContentParts (contentFormat: "extracted") **Phase 5C:** - Generiert Chapter mit allen 20 contentPartIds **Phase 5D.1:** - Generiert Section mit: - `content_type: "table"` - `contentPartIds: [part_1, ..., part_20]` - `useAiCall: true` **Phase 5D.2:** - `_needsAggregation("table", 20)` → `True` - Alle 20 ContentParts werden zusammen an `callAi` übergeben - AI generiert eine Tabelle mit allen Belegdaten **Ergebnis:** ✅ Funktioniert mit Aggregationslogik --- ### Beispiel 2: Vergleich mehrerer Dokumente **User Prompt:** "Vergleiche die drei Verträge" **Input:** 3 PDF-Dokumente (Verträge) **Phase 5B:** - 3 PDFs werden extrahiert → 3 ContentParts **Phase 5C:** - Generiert Chapter mit allen 3 contentPartIds **Phase 5D.1:** - Generiert Section mit: - `content_type: "table"` (für Vergleichstabelle) - `contentPartIds: [part_1, part_2, part_3]` - `useAiCall: true` **Phase 5D.2:** - `_needsAggregation("table", 3)` → `True` - Alle 3 ContentParts werden zusammen an `callAi` übergeben - AI generiert Vergleichstabelle **Ergebnis:** ✅ Funktioniert mit Aggregationslogik --- ### Beispiel 3: Liste von Produkten **User Prompt:** "Erstelle eine Liste aller Produkte aus den Katalogen" **Input:** 5 PDF-Dokumente (Produktkataloge) **Phase 5B:** - 5 PDFs werden extrahiert → 5 ContentParts **Phase 5C:** - Generiert Chapter mit allen 5 contentPartIds **Phase 5D.1:** - Generiert Section mit: - `content_type: "bullet_list"` - `contentPartIds: [part_1, ..., part_5]` - `useAiCall: true` **Phase 5D.2:** - `_needsAggregation("bullet_list", 5)` → `True` - Alle 5 ContentParts werden zusammen an `callAi` übergeben - AI generiert eine Liste mit allen Produkten **Ergebnis:** ✅ Funktioniert mit Aggregationslogik --- ### Beispiel 4: Einzelnes Dokument **User Prompt:** "Zusammenfassung des Dokuments" **Input:** 1 PDF-Dokument **Phase 5B:** - 1 PDF wird extrahiert → 1 ContentPart **Phase 5C:** - Generiert Chapter mit 1 contentPartId **Phase 5D.1:** - Generiert Section mit: - `content_type: "paragraph"` - `contentPartIds: [part_1]` - `useAiCall: true` **Phase 5D.2:** - `_needsAggregation("paragraph", 1)` → `False` - Einzelverarbeitung: 1 ContentPart wird an `callAi` übergeben - AI generiert Zusammenfassung **Ergebnis:** ✅ Funktioniert (keine Aggregation nötig)