229 lines
8.7 KiB
Python
229 lines
8.7 KiB
Python
# 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 Dokument-Struktur mit Sections.
|
|
Jede Section spezifiziert:
|
|
- Welcher Content sollte in dieser Section sein
|
|
- Welche ContentParts zu verwenden sind
|
|
- Format für jeden ContentPart
|
|
|
|
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 sections
|
|
"""
|
|
# 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
|
|
)
|
|
|
|
try:
|
|
# Baue Struktur-Prompt mit Content-Index
|
|
structurePrompt = self._buildStructurePrompt(
|
|
userPrompt=userPrompt,
|
|
contentParts=contentParts,
|
|
outputFormat=outputFormat
|
|
)
|
|
|
|
# AI-Call für Struktur-Generierung (verwende callAiPlanning für einfache JSON-Responses)
|
|
# Debug-Logs werden bereits von callAiPlanning geschrieben
|
|
aiResponse = await self.aiService.callAiPlanning(
|
|
prompt=structurePrompt,
|
|
debugType="document_generation_structure"
|
|
)
|
|
|
|
# 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 _buildStructurePrompt(
|
|
self,
|
|
userPrompt: str,
|
|
contentParts: List[ContentPart],
|
|
outputFormat: str
|
|
) -> str:
|
|
"""Baue Prompt für 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 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]
|
|
- contentFormats: {{"partId": "reference|object|extracted"}} - Wie jeder ContentPart zu verwenden ist
|
|
- generation_hint: Was AI für diese Section generieren soll
|
|
- elements: [] (leer, wird in nächster Phase gefüllt)
|
|
|
|
OUTPUT FORMAT: {outputFormat}
|
|
|
|
RETURN JSON:
|
|
{{
|
|
"metadata": {{
|
|
"title": "Document Title",
|
|
"language": "de"
|
|
}},
|
|
"documents": [{{
|
|
"id": "doc_1",
|
|
"title": "Document Title",
|
|
"filename": "document.{outputFormat}",
|
|
"sections": [
|
|
{{
|
|
"id": "section_1",
|
|
"content_type": "heading",
|
|
"generation_hint": "Main title",
|
|
"contentPartIds": [],
|
|
"contentFormats": {{}},
|
|
"elements": []
|
|
}},
|
|
{{
|
|
"id": "section_2",
|
|
"content_type": "paragraph",
|
|
"generation_hint": "Introduction paragraph",
|
|
"contentPartIds": ["part_ext_1"],
|
|
"contentFormats": {{
|
|
"part_ext_1": "extracted"
|
|
}},
|
|
"elements": []
|
|
}}
|
|
]
|
|
}}]
|
|
}}
|
|
|
|
Return ONLY valid JSON following the structure above.
|
|
"""
|
|
return prompt
|
|
|