# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Structure Generation Module Handles document structure generation, including: - Generating document structure with sections - Building structure prompts """ import json import logging from typing import Dict, Any, List from modules.datamodels.datamodelExtraction import ContentPart logger = logging.getLogger(__name__) class StructureGenerator: """Handles document structure generation.""" def __init__(self, services, aiService): """Initialize StructureGenerator with service center and AI service access.""" self.services = services self.aiService = aiService async def generateStructure( self, userPrompt: str, contentParts: List[ContentPart], outputFormat: str, parentOperationId: str ) -> Dict[str, Any]: """ Phase 5C: Generiert Chapter-Struktur (Table of Contents). Definiert für jedes Chapter: - Level, Title - contentPartIds - contentPartInstructions - generationHint Args: userPrompt: User-Anfrage contentParts: Alle vorbereiteten ContentParts mit Metadaten outputFormat: Ziel-Format (html, docx, pdf, etc.) parentOperationId: Parent Operation-ID für ChatLog-Hierarchie Returns: Struktur-Dict mit documents und chapters (nicht sections!) """ # Erstelle Operation-ID für Struktur-Generierung structureOperationId = f"{parentOperationId}_structure_generation" # Starte ChatLog mit Parent-Referenz self.services.chat.progressLogStart( structureOperationId, "Chapter Structure Generation", "Structure", f"Generating chapter structure for {outputFormat}", parentOperationId=parentOperationId ) try: # Baue Chapter-Struktur-Prompt mit Content-Index structurePrompt = self._buildChapterStructurePrompt( userPrompt=userPrompt, contentParts=contentParts, outputFormat=outputFormat ) # Debug: Log Prompt self.services.utils.writeDebugFile( structurePrompt, "chapter_structure_generation_prompt" ) # AI-Call für Chapter-Struktur-Generierung aiResponse = await self.aiService.callAiPlanning( prompt=structurePrompt, debugType="chapter_structure_generation" ) # Debug: Log Response self.services.utils.writeDebugFile( aiResponse, "chapter_structure_generation_response" ) # Parse Struktur structure = json.loads(self.services.utils.jsonExtractString(aiResponse)) # ChatLog abschließen self.services.chat.progressLogFinish(structureOperationId, True) return structure except Exception as e: self.services.chat.progressLogFinish(structureOperationId, False) logger.error(f"Error in generateStructure: {str(e)}") raise def _buildChapterStructurePrompt( self, userPrompt: str, contentParts: List[ContentPart], outputFormat: str ) -> str: """Baue Prompt für Chapter-Struktur-Generierung.""" # Baue ContentParts-Index - filtere leere Parts heraus contentPartsIndex = "" validParts = [] filteredParts = [] for part in contentParts: contentFormat = part.metadata.get("contentFormat", "unknown") # WICHTIG: Reference Parts haben absichtlich leere Daten - immer einschließen if contentFormat == "reference": validParts.append(part) logger.debug(f"Including reference ContentPart {part.id} (intentionally empty data)") continue # Überspringe leere Parts (keine Daten oder nur Container ohne Inhalt) # ABER: Reference Parts wurden bereits oben behandelt if not part.data or (isinstance(part.data, str) and len(part.data.strip()) == 0): # Überspringe Container-Parts ohne Daten if part.typeGroup == "container" and not part.data: filteredParts.append((part.id, "container without data")) continue # Überspringe andere leere Parts (aber nicht Reference, die wurden bereits behandelt) if not part.data: filteredParts.append((part.id, f"no data (format: {contentFormat})")) continue validParts.append(part) logger.debug(f"Including ContentPart {part.id}: format={contentFormat}, type={part.typeGroup}, dataLength={len(str(part.data)) if part.data else 0}") if filteredParts: logger.debug(f"Filtered out {len(filteredParts)} empty ContentParts: {filteredParts}") logger.info(f"Building structure prompt with {len(validParts)} valid ContentParts (from {len(contentParts)} total)") # Baue Index nur für gültige Parts for i, part in enumerate(validParts, 1): contentFormat = part.metadata.get("contentFormat", "unknown") dataPreview = "" if contentFormat == "extracted": # Für Image-Parts: Zeige dass es ein Image ist if part.typeGroup == "image": dataLength = len(part.data) if part.data else 0 mimeType = part.mimeType or "image" dataPreview = f"Image data ({mimeType}, {dataLength} chars) - base64 encoded image content" elif part.typeGroup == "container": # Container ohne Daten überspringen wir bereits oben dataPreview = "Container structure (no text content)" else: # Zeige Preview von extrahiertem Text if part.data: preview = part.data[:200] + "..." if len(part.data) > 200 else part.data dataPreview = preview else: dataPreview = "(empty)" elif contentFormat == "object": dataLength = len(part.data) if part.data else 0 mimeType = part.mimeType or "binary" if part.typeGroup == "image": dataPreview = f"Base64 encoded image ({mimeType}, {dataLength} chars)" else: dataPreview = f"Base64 encoded binary ({mimeType}, {dataLength} chars)" elif contentFormat == "reference": dataPreview = part.metadata.get("documentReference", "reference") originalFileName = part.metadata.get('originalFileName', 'N/A') contentPartsIndex += f"\n{i}. ContentPart ID: {part.id}\n" contentPartsIndex += f" Format: {contentFormat}\n" contentPartsIndex += f" Type: {part.typeGroup}\n" contentPartsIndex += f" MIME Type: {part.mimeType or 'N/A'}\n" contentPartsIndex += f" Source: {part.metadata.get('documentId', 'unknown')}\n" contentPartsIndex += f" Original file name: {originalFileName}\n" contentPartsIndex += f" Usage hint: {part.metadata.get('usageHint', 'N/A')}\n" contentPartsIndex += f" Data preview: {dataPreview}\n" if not contentPartsIndex: contentPartsIndex = "\n(No content parts available)" prompt = f"""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 OUTPUT FORMAT: {outputFormat} RETURN JSON: {{ "metadata": {{ "title": "Document Title", "language": "de" }}, "documents": [{{ "id": "doc_1", "title": "Document Title", "filename": "document.{outputFormat}", "chapters": [ {{ "id": "chapter_1", "level": 1, "title": "Introduction", "contentPartIds": ["part_ext_1"], "contentPartInstructions": {{ "part_ext_1": {{ "instruction": "Use full extracted text" }} }}, "generationHint": "Create introduction section", "sections": [] }} ] }}] }} Return ONLY valid JSON following the structure above. """ return prompt