From 0eaaeb35501ccb4b48b6d8a734c2efd61a45b93e Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Fri, 26 Dec 2025 00:16:14 +0100 Subject: [PATCH] refactored ai service container (3000 lines) with submodules, and enhanced generation part with dynamic chapters --- ...ation_concept_generation_structure_rev4.md | 881 ++++++++++++++++++ 1 file changed, 881 insertions(+) create mode 100644 appdoc/implementation_concept_generation_structure_rev4.md diff --git a/appdoc/implementation_concept_generation_structure_rev4.md b/appdoc/implementation_concept_generation_structure_rev4.md new file mode 100644 index 0000000..c0d3e81 --- /dev/null +++ b/appdoc/implementation_concept_generation_structure_rev4.md @@ -0,0 +1,881 @@ +# 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)