From b2b5761917b3a5c1df868dd1c74f2b2cba12bb56 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Thu, 25 Dec 2025 00:09:38 +0100 Subject: [PATCH] document intent handling --- appdoc/analysis_ai_workflow.md | 2812 +++++++++++++++++ appdoc/doc_ai_system_validator.md | 1237 ++++++++ ...implementation_concept_ai_workflow_rev4.md | 2012 ++++++++++++ deployment/poweron_sec.kdbx | Bin 4222 -> 4222 bytes 4 files changed, 6061 insertions(+) create mode 100644 appdoc/analysis_ai_workflow.md create mode 100644 appdoc/doc_ai_system_validator.md create mode 100644 appdoc/implementation_concept_ai_workflow_rev4.md diff --git a/appdoc/analysis_ai_workflow.md b/appdoc/analysis_ai_workflow.md new file mode 100644 index 0000000..1e03f77 --- /dev/null +++ b/appdoc/analysis_ai_workflow.md @@ -0,0 +1,2812 @@ +# AI Workflow Analysis: Document Management im Kontext von Workflows + +## Übersicht + +Diese Analyse beschreibt den vollständigen Ablauf eines AI-Calls im Kontext eines Workflows, speziell im Hinblick auf das Dokumenten-Management. Der Fokus liegt auf der Klärung, was mit Dokumenten passiert, die der User mitsendet, und wo Entscheidungen über Extraktion vs. direkte Verwendung getroffen werden. + +## Problemstellung + +**Aktuelle Probleme:** +1. Unklarheit darüber, was mit Dokumenten passiert, die der User mitsendet +2. Dokumente werden immer extrahiert, auch wenn Extraktion nicht nötig ist +3. Fehlende Analyse, welche Dokumente extrahiert werden müssen und was extrahiert werden soll +4. Bei Bildern ist unklar, ob das Bild gerendert werden soll oder der Text aus dem Bild extrahiert werden soll +5. Der Prozess scheint chaotisch - es fehlt ein klarer Prozess innerhalb eines AI-Calls + +## Workflow-Ablauf: Von User Input bis Action Execution + +### Phase 0: User Input Analyse und Context-Extraktion + +**Datei:** `workflowManager.py` + +#### 0.1 Prompt-Analyse und Context-Extraktion (`_sendFirstMessage`) + +**Was passiert:** +- **BEVOR** der eigentliche Workflow startet, wird der User-Prompt analysiert +- AI analysiert den Prompt und unterscheidet zwischen: + - **Prompt** (Anweisungen, was gemacht werden soll) + - **Context** (Inhalte, die als separate Dokumente behandelt werden sollen) + +**Code-Stelle:** +```362:530:poweron/gateway/modules/workflows/workflowManager.py +# Analyze the user's input to detect language, normalize request, extract intent, and offload bulky context into documents +``` + +**Analyzer Prompt:** +- AI analysiert den User-Prompt in einem Schritt: + 1. `detectedLanguage`: Sprache erkennen + 2. `normalizedRequest`: Normalisierte Anweisung (ohne Context) + 3. `intent`: Kurze Kern-Anfrage + 4. **`contextItems`**: Große Datenblöcke, die als separate Dokumente extrahiert werden sollen + +**Was wird als Context extrahiert:** +- Große Literal-Inhalte +- Lange Listen/Tabellen +- Code/JSON-Blöcke +- Transkripte +- CSV-Fragmente +- Detaillierte Specs + +**Regeln:** +- Wenn Gesamtinhalt < 10% der Model-Max-Tokens: **KEINE Extraktion**, alles bleibt im Intent +- Wenn Gesamtinhalt > 10%: **Extraktion** von großen Teilen in `contextItems` +- Kritische Referenzen (URLs, Dateinamen) bleiben im Intent + +**Dokument-Erstellung:** +- Jedes `contextItem` wird als separates `ChatDocument` erstellt +- Wird in Component Storage gespeichert +- Wird mit User-uploaded Dokumenten kombiniert + +**Ergebnis:** +- `self.services.currentUserContextItems = contextItems` - Context-Items gespeichert +- `self.services.currentUserPromptNormalized = normalizedRequest` - Normalisierter Prompt (ohne Context) +- `createdDocs[]` - Erstellte Context-Dokumente werden mit User-Dokumenten kombiniert + +**Wichtig:** +- Diese Phase passiert **BEVOR** Complexity Detection +- Context-Dokumente sind dann verfügbar für alle nachfolgenden Phasen +- Der normalisierte Prompt (ohne Context) wird für alle weiteren Schritte verwendet + +### Phase 1: User Input Verarbeitung + +**Datei:** `workflowProcessor.py` + +#### 1.1 Complexity Detection (`detectComplexity`) + +**Was passiert:** +- User Input wird analysiert, um Komplexität zu bestimmen +- Sprache wird erkannt +- Es wird geprüft, ob Workflow-History benötigt wird + +**Dokumenten-Behandlung:** +- Dokumente werden nur gezählt (`len(documents)`) +- Dokument-Typen werden erfasst (`mimeType`) +- **KEINE Extraktion** - nur Metadaten-Analyse + +**Code-Stelle:** +```334:432:poweron/gateway/modules/workflows/processing/workflowProcessor.py +async def detectComplexity(self, prompt: str, documents: Optional[List[ChatDocument]] = None) -> tuple[str, bool, Optional[str]]: +``` + +**Ergebnis:** +- `complexity`: "simple" | "moderate" | "complex" +- `needsWorkflowHistory`: bool +- `detectedLanguage`: str (ISO 639-1) + +#### 1.2 Fast Path (optional) + +**Was passiert:** +- Bei "simple" Requests wird Fast Path verwendet +- Direkter AI-Call ohne Dokumenten-Extraktion +- Dokumente werden **NICHT** verarbeitet + +**Code-Stelle:** +```434:520:poweron/gateway/modules/workflows/processing/workflowProcessor.py +async def fastPathExecute(self, prompt: str, documents: Optional[List[ChatDocument]] = None, userLanguage: Optional[str] = None) -> ActionResult: +``` + +**Dokumenten-Behandlung:** +- Dokumente werden ignoriert (`contentParts=None`) +- Nur Text-Response wird generiert + +### Phase 2: Workflow Planning + +#### 2.1 Task Plan Generation (`generateTaskPlan`) + +**Was passiert:** +- High-level Task Plan wird generiert +- Tasks werden identifiziert +- Actions werden geplant + +**Dokumenten-Behandlung:** +- Dokumente werden **NICHT** extrahiert +- Nur Referenzen werden verwendet + +**Code-Stelle:** +```42:103:poweron/gateway/modules/workflows/processing/workflowProcessor.py +async def generateTaskPlan(self, userInput: str, workflow: ChatWorkflow) -> TaskPlan: +``` + +#### 2.2 Action Generation (`generateActionItems`) + +**Was passiert:** +- Actions für Tasks werden generiert +- `ai.process` Action kann generiert werden + +**Dokumenten-Behandlung:** +- Dokument-Referenzen werden in Action-Parameter eingefügt +- **KEINE Extraktion** - nur Referenzen + +### Phase 3: Action Execution - ai.process + +**Datei:** `methodAi/actions/process.py` + +#### 3.1 Action Start (`process`) - Entry Point + +**Was passiert:** +- Action `ai.process` wird ausgeführt +- Parameter werden extrahiert: `aiPrompt`, `documentList`, `resultType` +- Progress Tracking wird initialisiert + +**Code-Stelle:** +```20:218:poweron/gateway/modules/workflows/methods/methodAi/actions/process.py +@action +async def process(self, parameters: Dict[str, Any]) -> ActionResult: +``` + +**Funktions-Aufruf-Hierarchie:** + +``` +process() [Entry Point] +├─→ getChatDocumentsFromDocumentList() [Chat Service] +│ └─→ Konvertiert DocumentReferenceList zu ChatDocument[] +│ +├─→ extractContent() [Extraction Service] +│ ├─→ runExtraction() [Extraction Pipeline] +│ │ ├─→ ExtractorRegistry.resolve() [Registry] +│ │ │ └─→ Findet passenden Extractor (PDF, DOCX, etc.) +│ │ ├─→ extractor.extract() [Extractor-spezifisch] +│ │ │ └─→ Erstellt ContentPart[] (text, image, table, etc.) +│ │ └─→ applyMerging() [Merging Strategy] +│ │ └─→ Merged ContentParts nach Strategie +│ └─→ Gibt List[ContentExtracted] zurück +│ +├─→ callAiContent() [AI Service] +│ ├─→ buildExtractionPrompt() [Prompt Builder] +│ │ ├─→ _parseExtractionIntent() [Intent Analysis] +│ │ │ └─→ AI-Call zur Intent-Analyse +│ │ └─→ Erstellt Extraction Prompt +│ │ +│ ├─→ callAi() [AI Service Router] +│ │ └─→ processContentPartsWithAi() [Extraction Service] +│ │ ├─→ processContentPartWithFallback() [Per Part] +│ │ │ ├─→ chunkContentPartForAi() [Wenn zu groß] +│ │ │ │ └─→ ChunkerRegistry.resolve() [Registry] +│ │ │ ├─→ aiObjects._callWithModel() [AI Call] +│ │ │ │ └─→ Vision Models für Bilder +│ │ │ └─→ mergeChunkResults() [Wenn gechunkt] +│ │ └─→ mergePartResults() [Merge All Parts] +│ │ └─→ applyMerging() [Merging Strategy] +│ │ +│ ├─→ buildGenerationPrompt() [Prompt Builder] +│ │ └─→ Erstellt Generation Prompt mit extracted_content +│ │ +│ ├─→ _callAiWithLooping() [AI Service] +│ │ ├─→ callAi() [AI Call] +│ │ ├─→ _extractSectionsFromResponse() [JSON Parsing] +│ │ │ ├─→ extractJsonString() [JSON Utils] +│ │ │ ├─→ repairBrokenJson() [JSON Utils] +│ │ │ ├─→ extractSectionsFromDocument() [JSON Utils] +│ │ │ └─→ JsonResponseHandler.mergeSectionsIntelligently() [Merging] +│ │ ├─→ _defineKpisFromPrompt() [KPI Tracking] +│ │ ├─→ JsonResponseHandler.extractKpiValuesFromJson() [KPI Extraction] +│ │ ├─→ JsonResponseHandler.validateKpiProgression() [KPI Validation] +│ │ ├─→ buildContinuationContext() [Continuation] +│ │ └─→ _buildFinalResultFromSections() [Final Assembly] +│ │ +│ └─→ renderReport() [Generation Service] +│ ├─→ getRendererForFormat() [Renderer Selection] +│ └─→ renderer.render() [Format-spezifisch] +│ +└─→ ActionResult.isSuccess() [Return] + └─→ ActionDocument[] mit gerenderten Dokumenten +``` + +#### 3.1.1 Sub-Funktionen im Detail + +**A. Document Resolution** +- **Funktion:** `getChatDocumentsFromDocumentList()` +- **Service:** Chat Service +- **Was passiert:** Konvertiert `DocumentReferenceList` (String-Referenzen) zu `ChatDocument[]` (vollständige Objekte mit fileId, fileName, mimeType) +- **Input:** `DocumentReferenceList` mit String-Referenzen +- **Output:** `List[ChatDocument]` mit vollständigen Metadaten + +**B. Content Extraction Pipeline** +- **Funktion:** `extractContent()` +- **Service:** Extraction Service (`serviceExtraction/mainServiceExtraction.py`) +- **Was passiert:** + - Lädt Dokument-Bytes aus Component Storage + - Findet passenden Extractor über Registry + - Extrahiert ContentParts (text, image, table, structure, etc.) + - Wendet Merging-Strategie an +- **Sub-Funktionen:** + - `runExtraction()`: Orchestriert Extraction Pipeline + - `ExtractorRegistry.resolve()`: Findet passenden Extractor + - `extractor.extract()`: Extrahiert ContentParts (Extractor-spezifisch) + - `applyMerging()`: Merged ContentParts nach Strategie + +**C. Extraction Prompt Building** +- **Funktion:** `buildExtractionPrompt()` +- **Service:** Extraction Service (`serviceExtraction/subPromptBuilderExtraction.py`) +- **Was passiert:** + - Analysiert User Prompt für Extraction Intent + - Erstellt strukturierten Extraction Prompt + - Integriert Format-spezifische Guidelines (via Renderer) +- **Sub-Funktionen:** + - `_parseExtractionIntent()`: AI-basierte Intent-Analyse + - Renderer-Integration für Format-Guidelines + +**D. Content Parts Processing** +- **Funktion:** `processContentPartsWithAi()` +- **Service:** Extraction Service (`serviceExtraction/mainServiceExtraction.py`) +- **Was passiert:** + - Verarbeitet jeden ContentPart einzeln + - Bilder werden mit Vision-Modellen analysiert + - Text wird extrahiert + - Ergebnisse werden gemerged +- **Sub-Funktionen:** + - `processContentPartWithFallback()`: Verarbeitet einzelnen Part mit Fallback + - `chunkContentPartForAi()`: Chunked große Parts für Model-Limits + - `aiObjects._callWithModel()`: Führt AI-Call aus + - `mergeChunkResults()`: Merged Chunk-Ergebnisse + - `mergePartResults()`: Merged alle Part-Ergebnisse + +**E. Generation Prompt Building** +- **Funktion:** `buildGenerationPrompt()` +- **Service:** Generation Service (`serviceGeneration/subPromptBuilderGeneration.py`) +- **Was passiert:** + - Erstellt Generation Prompt mit extracted_content + - Integriert JSON Template + - Handhabt Continuation Context für Looping +- **Input:** `outputFormat`, `userPrompt`, `extracted_content`, `continuationContext` +- **Output:** Kompletter Generation Prompt String + +**F. AI Generation mit Looping** +- **Funktion:** `_callAiWithLooping()` +- **Service:** AI Service (`serviceAi/mainServiceAi.py`) +- **Was passiert:** + - Führt AI-Call aus + - Repariert broken JSON automatisch + - Merged Sections über mehrere Iterationen + - Trackt KPIs für große Dokumente +- **Sub-Funktionen:** + - `callAi()`: Führt AI-Call aus + - `_extractSectionsFromResponse()`: Extrahiert Sections aus JSON + - `JsonResponseHandler.mergeSectionsIntelligently()`: Merged Sections + - `_defineKpisFromPrompt()`: Definiert KPIs für Tracking + - `JsonResponseHandler.extractKpiValuesFromJson()`: Extrahiert KPI-Werte + - `JsonResponseHandler.validateKpiProgression()`: Validiert KPI-Fortschritt + - `buildContinuationContext()`: Erstellt Continuation Context + - `_buildFinalResultFromSections()`: Baut finales JSON zusammen + +**G. Document Rendering** +- **Funktion:** `renderReport()` +- **Service:** Generation Service (`serviceGeneration/mainServiceGeneration.py`) +- **Was passiert:** + - Wählt passenden Renderer für Format + - Rendert JSON-Struktur zu finalem Dokument + - Handhabt Bilder, Tabellen, Strukturen format-spezifisch +- **Sub-Funktionen:** + - `getRendererForFormat()`: Findet Renderer (PDF, DOCX, XLSX, etc.) + - `renderer.render()`: Format-spezifisches Rendering + +#### 3.1.2 Baustein-Separation: Übersicht + +**Die Sub-Funktionen sind sauber separiert in folgende Bausteine:** + +1. **Document Resolution Layer** + - `getChatDocumentsFromDocumentList()`: Konvertiert Referenzen zu Objekten + - **Zuständigkeit:** Metadaten-Auflösung + +2. **Extraction Layer** + - `extractContent()`: Orchestriert Extraction + - `runExtraction()`: Pipeline-Orchestrierung + - `ExtractorRegistry`: Extractor-Verwaltung + - `extractor.extract()`: Format-spezifische Extraktion + - **Zuständigkeit:** Dokument → ContentParts + +3. **Prompt Building Layer** + - `buildExtractionPrompt()`: Extraction Prompt + - `buildGenerationPrompt()`: Generation Prompt + - `_parseExtractionIntent()`: Intent-Analyse + - **Zuständigkeit:** Prompt-Erstellung + +4. **AI Processing Layer** + - `processContentPartsWithAi()`: ContentParts-Verarbeitung + - `processContentPartWithFallback()`: Einzelne Part-Verarbeitung + - `chunkContentPartForAi()`: Chunking für große Parts + - `aiObjects._callWithModel()`: AI-Call-Ausführung + - **Zuständigkeit:** ContentParts → AI-Response + +5. **Generation Layer** + - `_callAiWithLooping()`: Generation mit Looping + - `_extractSectionsFromResponse()`: JSON-Parsing + - `JsonResponseHandler`: Section-Merging, KPI-Tracking + - `_buildFinalResultFromSections()`: Final Assembly + - **Zuständigkeit:** AI-Response → JSON-Struktur + +6. **Rendering Layer** + - `renderReport()`: Rendering-Orchestrierung + - `getRendererForFormat()`: Renderer-Auswahl + - `renderer.render()`: Format-spezifisches Rendering + - **Zuständigkeit:** JSON-Struktur → Finales Dokument + +**Vorteile der Separation:** +- **Klare Zuständigkeiten:** Jeder Baustein hat eine spezifische Aufgabe +- **Wiederverwendbarkeit:** Bausteine können einzeln verwendet werden +- **Testbarkeit:** Jeder Baustein kann isoliert getestet werden +- **Wartbarkeit:** Änderungen sind lokalisiert + +**Aktuelle Probleme:** +- **Fehlende Intent-Analyse:** Keine Pre-Extraction Analysis +- **Automatische Extraktion:** Immer, ohne Analyse +- **Bild-Behandlung:** Immer Vision-Analyse, keine Asset-Bereitstellung + +#### 3.2 Dokument-Extraktion (PROBLEM-BEREICH) + +**Was passiert:** +- **KRITISCH:** Dokumente werden **IMMER** extrahiert, wenn `documentList` vorhanden ist +- Extraktion erfolgt **OHNE** Analyse, ob Extraktion nötig ist +- Standard-ExtractionOptions werden verwendet + +**Code-Stelle:** +```98:132:poweron/gateway/modules/workflows/methods/methodAi/actions/process.py +# If contentParts not provided but documentList is, extract content first +if not contentParts and documentList.references: + self.services.chat.progressLogUpdate(operationId, 0.3, "Extracting content from documents") + + # Get ChatDocuments + chatDocuments = self.services.chat.getChatDocumentsFromDocumentList(documentList) + if not chatDocuments: + logger.warning("No documents found in documentList") + else: + logger.info(f"Extracting content from {len(chatDocuments)} documents") + + # Prepare extraction options (use defaults if not provided) + extractionOptions = parameters.get("extractionOptions") + if not extractionOptions: + extractionOptions = ExtractionOptions( + prompt="Extract all content from the document", + mergeStrategy=MergeStrategy( + mergeType="concatenate", + groupBy="typeGroup", + orderBy="id" + ), + processDocumentsIndividually=True + ) + + # Extract content using extraction service with hierarchical progress logging + # Pass operationId for per-document progress tracking + extractedResults = self.services.extraction.extractContent(chatDocuments, extractionOptions, operationId=operationId) + + # Combine all ContentParts from all extracted results + contentParts = [] + for extracted in extractedResults: + if extracted.parts: + contentParts.extend(extracted.parts) + + logger.info(f"Extracted {len(contentParts)} content parts from {len(extractedResults)} documents") +``` + +**Problem:** +- Extraktion erfolgt **automatisch** ohne Analyse der User-Intention +- Standard-Prompt: "Extract all content from the document" +- Keine Unterscheidung zwischen: + - Dokumenten, die extrahiert werden müssen (z.B. PDF mit Text) + - Dokumenten, die direkt verwendet werden sollen (z.B. Bilder für Rendering) + - Dokumenten, die nur als Referenz dienen + +**Extraction Service (`extractContent`):** + +**Datei:** `serviceExtraction/mainServiceExtraction.py` + +**Was passiert:** +- Dokumente werden durch Extraction Pipeline verarbeitet +- ContentParts werden erstellt (text, table, image, structure, etc.) +- Bilder werden als `typeGroup="image"` erkannt +- Text wird als `typeGroup="text"` erkannt + +**Code-Stelle:** +```39:232:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +def extractContent( + self, + documents: List[ChatDocument], + options: ExtractionOptions, + operationId: Optional[str] = None, + parentOperationId: Optional[str] = None +) -> List[ContentExtracted]: +``` + +**Ergebnis:** +- `List[ContentExtracted]` mit `ContentPart[]` +- Jeder ContentPart hat: + - `typeGroup`: "text" | "table" | "image" | "structure" | "container" | "binary" + - `mimeType`: z.B. "image/png", "text/plain" + - `data`: Content-Daten (Text als String, Bilder als base64) + +#### 3.3 AI Call Preparation + +**Was passiert:** +- `AiCallOptions` wird erstellt +- `callAiContent` wird aufgerufen mit `contentParts` + +**Code-Stelle:** +```147:154:poweron/gateway/modules/workflows/methods/methodAi/actions/process.py +# Use unified callAiContent method with contentParts (extraction is now separate) +aiResponse = await self.services.ai.callAiContent( + prompt=aiPrompt, + options=options, + contentParts=contentParts, # Already extracted (or None if no documents) + outputFormat=output_format, + parentOperationId=operationId +) +``` + +### Phase 4: AI Service - callAiContent + +**Datei:** `serviceAi/mainServiceAi.py` + +#### 4.1 Entry Point (`callAiContent`) + +**Was passiert:** +- Unified AI content processing +- Entscheidet zwischen verschiedenen Operation Types + +**Code-Stelle:** +```974:1387:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +async def callAiContent( + self, + prompt: str, + options: AiCallOptions, + contentParts: Optional[List[ContentPart]] = None, + outputFormat: Optional[str] = None, + title: Optional[str] = None, + parentOperationId: Optional[str] = None # Parent operation ID for hierarchical logging +) -> AiResponse: +``` + +#### 4.2 Content Parts Processing (PROBLEM-BEREICH) + +**Was passiert:** +- Wenn `contentParts` vorhanden sind, werden sie verarbeitet +- **PROBLEM:** Es wird **IMMER** ein Extraction Prompt erstellt, auch wenn nicht klar ist, was extrahiert werden soll + +**Code-Stelle:** +```1117:1207:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +# Process contentParts for generation prompt (if provided) +# Use generic callWithContentParts() which handles all content types (images, text, etc.) +# This automatically processes images with vision models and merges all results +if contentParts: + # Filter out binary/other parts that shouldn't be processed + processableParts = [] + skippedParts = [] + for p in contentParts: + if p.typeGroup in ["image", "text", "table", "structure"] or (p.mimeType and (p.mimeType.startswith("image/") or p.mimeType.startswith("text/"))): + processableParts.append(p) + else: + skippedParts.append(p) + + if skippedParts: + logger.debug(f"Skipping {len(skippedParts)} binary/other parts from document generation") + + if processableParts: + # Count images for progress update + imageCount = len([p for p in processableParts if p.typeGroup == "image" or (p.mimeType and p.mimeType.startswith("image/"))]) + if imageCount > 0: + self.services.chat.progressLogUpdate(aiOperationId, 0.25, f"Extracting data from {imageCount} images using vision models") + + # Build proper extraction prompt using buildExtractionPrompt + # This creates a focused extraction prompt, not the user's generation prompt + from modules.services.serviceExtraction.subPromptBuilderExtraction import buildExtractionPrompt + + # Determine renderer for format-specific guidelines + renderer = None + if outputFormat: + try: + from modules.services.serviceGeneration.mainServiceGeneration import GenerationService + generationService = GenerationService(self.services) + renderer = generationService.getRendererForFormat(outputFormat) + except Exception as e: + logger.debug(f"Could not get renderer for format {outputFormat}: {e}") + + extractionPrompt = await buildExtractionPrompt( + outputFormat=outputFormat or "txt", + userPrompt=prompt, # User's prompt as context for what to extract + title=title or "Document", + aiService=self if hasattr(self, 'aiObjects') and self.aiObjects else None, + services=self.services, + renderer=renderer + ) + + logger.info(f"Processing {len(processableParts)} content parts ({imageCount} images) with extraction prompt") + + # Use DATA_EXTRACT operation type for extraction + extractionOptions = AiCallOptions( + operationType=OperationTypeEnum.DATA_EXTRACT, # Use DATA_EXTRACT for extraction + compressPrompt=options.compressPrompt, + compressContext=options.compressContext + ) + + extractionRequest = AiCallRequest( + prompt=extractionPrompt, # Use proper extraction prompt, not user's generation prompt + context="", + options=extractionOptions, + contentParts=processableParts + ) + + # Write debug file for extraction prompt (all parts) + self.services.utils.writeDebugFile(extractionPrompt, "content_extraction_prompt") + + # Call generic content parts processor - handles images, text, chunking, merging + extractionResponse = await self.callAi(extractionRequest) + + # Write debug file for extraction response + if extractionResponse.content: + self.services.utils.writeDebugFile(extractionResponse.content, "content_extraction_response") + else: + self.services.utils.writeDebugFile(f"Error: No content returned (errorCount={extractionResponse.errorCount})", "content_extraction_response") + logger.warning(f"Content extraction returned no content (errorCount={extractionResponse.errorCount})") + + # Use extracted content directly for generation prompt + if extractionResponse.errorCount == 0 and extractionResponse.content: + # The extracted content is already merged and ready to use + content_for_generation = extractionResponse.content + logger.info(f"Successfully extracted content from {len(processableParts)} parts ({len(extractionResponse.content)} chars) for document generation") + else: + # Extraction failed - use placeholders + logger.warning(f"Content extraction failed, using placeholders") + placeholderParts = [] + for p in processableParts: + placeholderParts.append(f"[{p.typeGroup}: {p.label} - Extraction failed]") + content_for_generation = "\n\n".join(placeholderParts) if placeholderParts else None + else: + content_for_generation = None + logger.debug("No processable parts found in contentParts") +else: + content_for_generation = None +``` + +**Problem:** +- **Bilder werden IMMER mit Vision-Modellen analysiert** (Text-Extraktion) +- Keine Unterscheidung zwischen: + - Bildern, die **gerendert** werden sollen (z.B. Logo im Bericht) + - Bildern, deren **Text extrahiert** werden soll (z.B. Screenshot mit Text) + +#### 4.3 Extraction Prompt Building + +**Datei:** `serviceExtraction/subPromptBuilderExtraction.py` + +**Was passiert:** +- Extraction Prompt wird erstellt +- User Prompt wird analysiert, um Intent zu extrahieren +- **PROBLEM:** Intent-Analyse ist sehr generisch + +**Code-Stelle:** +```24:226:poweron/gateway/modules/services/serviceExtraction/subPromptBuilderExtraction.py +async def buildExtractionPrompt( + outputFormat: str, + userPrompt: str, + title: str, + aiService=None, + services=None, + renderer: _RendererLike = None +) -> str: +``` + +**Extraction Intent Parsing:** +```191:226:poweron/gateway/modules/services/serviceExtraction/subPromptBuilderExtraction.py +async def _parseExtractionIntent(userPrompt: str, outputFormat: str, aiService=None, services=None) -> str: + """ + Parse user prompt to extract the core extraction intent. + """ + if not aiService: + return f"Extract content from the provided documents and create a {outputFormat} report." + + try: + analysis_prompt = f""" +Analyze this user request and extract the core extraction intent: + +User request: "{userPrompt}" +Target format: {outputFormat} + +Extract the main intent and requirements for document processing. Focus on: +1. What content needs to be extracted +2. How it should be organized +3. Any specific requirements or preferences + +Respond with a clear, concise statement of the extraction intent. +""" + request_options = AiCallOptions() + request_options.operationType = OperationTypeEnum.DATA_GENERATE + + request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options) + response = await aiService.aiObjects.call(request) + + if response and response.content: + return response.content.strip() + else: + return f"Extract content from the provided documents and create a {outputFormat} report." + + except Exception as e: + services.utils.debugLogToFile(f"Extraction intent analysis failed: {str(e)}", "PROMPT_BUILDER") + return f"Extract content from the provided documents and create a {outputFormat} report." +``` + +**Problem:** +- Intent-Analyse ist sehr generisch +- Keine spezifische Analyse für Bilder (rendern vs. Text extrahieren) +- Keine Analyse, welche Dokumente überhaupt extrahiert werden müssen + +#### 4.4 Content Parts Processing mit AI + +**Datei:** `serviceExtraction/mainServiceExtraction.py` + +**Was passiert:** +- ContentParts werden mit AI verarbeitet +- Bilder werden mit Vision-Modellen analysiert +- Text wird extrahiert +- Ergebnisse werden gemerged + +**Code-Stelle:** +```1081:1121:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +async def processContentPartsWithAi( + self, + request: AiCallRequest, + aiObjects, # Pass interface for AI calls + progressCallback=None +) -> AiCallResponse: + """Process content parts with model-aware chunking and AI calls. + + Moved from interfaceAiObjects.callWithContentParts() - entry point for content parts processing. + """ + prompt = request.prompt + options = request.options + contentParts = request.contentParts + + # Get failover models + availableModels = modelRegistry.getAvailableModels() + failoverModelList = modelSelector.getFailoverModelList(prompt, "", options, availableModels) + + if not failoverModelList: + return self._createErrorResponse("No suitable models found", 0, 0) + + # Process each content part + allResults = [] + for contentPart in contentParts: + partResult = await self.processContentPartWithFallback( + contentPart, prompt, options, failoverModelList, aiObjects, progressCallback + ) + allResults.append(partResult) + + # Merge all results using unified mergePartResults + mergedContent = self.mergePartResults(allResults) + + return AiCallResponse( + content=mergedContent, + modelName="multiple", + priceUsd=sum(r.priceUsd for r in allResults), + processingTime=sum(r.processingTime for r in allResults), + bytesSent=sum(r.bytesSent for r in allResults), + bytesReceived=sum(r.bytesReceived for r in allResults), + errorCount=sum(r.errorCount for r in allResults) + ) +``` + +**Bild-Verarbeitung:** +```888:1080:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +async def processContentPartWithFallback(self, contentPart, prompt: str, options, failoverModelList, aiObjects, progressCallback=None) -> AiCallResponse: + """Process a single content part with model-aware chunking and fallback. + + Moved from interfaceAiObjects.py - orchestrates chunking and merging. + Calls aiObjects._callWithModel() for actual AI calls. + """ + lastError = None + + # Check if this is an image - Vision models need special handling + isImage = (contentPart.typeGroup == "image") or (contentPart.mimeType and contentPart.mimeType.startswith("image/")) + + # Determine the correct operation type based on content type + actualOperationType = options.operationType + if isImage: + actualOperationType = OperationTypeEnum.IMAGE_ANALYSE + # Get vision-capable models for images + availableModels = modelRegistry.getAvailableModels() + visionFailoverList = modelSelector.getFailoverModelList(prompt, "", AiCallOptions(operationType=actualOperationType), availableModels) + if visionFailoverList: + logger.debug(f"Using {len(visionFailoverList)} vision-capable models for image processing") + failoverModelList = visionFailoverList +``` + +**Problem:** +- **Bilder werden IMMER mit IMAGE_ANALYSE behandelt** (Text-Extraktion) +- Keine Möglichkeit, Bilder für Rendering zu markieren + +#### 4.5 Generation Prompt Building + +**Datei:** `serviceGeneration/subPromptBuilderGeneration.py` + +**Was passiert:** +- Generation Prompt wird erstellt +- Extracted Content wird in Prompt eingefügt +- JSON Template wird verwendet + +**Code-Stelle:** +```16:195:poweron/gateway/modules/services/serviceGeneration/subPromptBuilderGeneration.py +async def buildGenerationPrompt( + outputFormat: str, + userPrompt: str, + title: str, + extracted_content: str = None, + continuationContext: Dict[str, Any] = None, + services: Any = None +) -> str: +``` + +**Extracted Content Integration:** +```123:165:poweron/gateway/modules/services/serviceGeneration/subPromptBuilderGeneration.py +if extracted_content: + # If we have extracted content, put it FIRST and make it very clear it's the source data + generationPrompt = f"""{'='*80} +USER REQUEST / USER PROMPT: +{'='*80} +{userPrompt} +{'='*80} +END OF USER REQUEST / USER PROMPT +{'='*80} + +{'='*80} +⚠️ CRITICAL: USE THIS EXTRACTED CONTENT AS YOUR DATA SOURCE ⚠️ +{'='*80} +The content below contains the ACTUAL DATA extracted from the source documents. +You MUST use this data - DO NOT generate fake or example data. +{'='*80} +EXTRACTED CONTENT FROM DOCUMENTS: +{'='*80} +{extracted_content} +{'='*80} +END OF EXTRACTED CONTENT +{'='*80} + +LANGUAGE REQUIREMENT: All generated content must be in the language '{userLanguage}'. Generate all text, headings, paragraphs, and content in this language. If the extracted content is in a different language, translate it to '{userLanguage}' while preserving the structure and meaning. + +Generate a VALID JSON response using the EXTRACTED CONTENT above as your data source. +The JSON structure template below shows ONLY the structure pattern - the example values are NOT real data. +You MUST use the actual data from EXTRACTED CONTENT above, NOT the example values from the template. + +JSON structure template (structure only - use data from EXTRACTED CONTENT above): +{jsonTemplate} + +Instructions: +- Return ONLY valid JSON (strict). No comments. No trailing commas. Use double quotes. +- Do NOT reuse example section IDs; create your own. +- CRITICAL: Use the ACTUAL DATA from EXTRACTED CONTENT above, NOT the example values from the template. +- Generate complete content based on the user request and the extracted content. Do NOT just give an instruction or comments. Deliver the complete response. +- All content must be in the language '{userLanguage}'. +- IMPORTANT: Set a meaningful "filename" in each document with appropriate file extension (e.g., "prime_numbers.txt", "report.docx", "data.json"). The filename should reflect the content and task objective. +- Output JSON only; no markdown fences or extra text. + +Generate your complete response using the extracted content data. +""" +``` + +**Problem:** +- Extracted Content enthält **nur Text** (auch von Bildern) +- Bilder werden nicht als separate Assets für Rendering bereitgestellt +- Keine Möglichkeit, Bilder direkt im generierten Dokument zu rendern + +#### 4.6 Document Generation mit Looping + +**Was passiert:** +- AI generiert JSON-Struktur +- Looping-System repariert broken JSON +- KPI-Tracking für große Dokumente + +**Code-Stelle:** +```178:577:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +async def _callAiWithLooping( + self, + prompt: str, + options: AiCallOptions, + debugPrefix: str = "ai_call", + promptBuilder: Optional[callable] = None, + promptArgs: Optional[Dict[str, Any]] = None, + operationId: Optional[str] = None, + userPrompt: Optional[str] = None +) -> str: +``` + +#### 4.7 Rendering + +**Was passiert:** +- JSON wird zu finalem Dokument gerendert (PDF, DOCX, etc.) +- Bilder werden aus JSON-Struktur gerendert (wenn vorhanden) + +**Code-Stelle:** +```1327:1382:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +try: + from modules.services.serviceGeneration.mainServiceGeneration import GenerationService + generationService = GenerationService(self.services) + self.services.chat.progressLogUpdate(renderOperationId, 0.5, f"Rendering to {outputFormat} format") + rendered_content, mime_type, _images = await generationService.renderReport( + generated_data, outputFormat, extractedTitle or "Generated Document", prompt, self + ) + self.services.chat.progressLogFinish(renderOperationId, True) + + # Determine document name + if extractedFilename: + documentName = extractedFilename + elif extractedTitle and extractedTitle != "Generated Document": + sanitized = re.sub(r"[^a-zA-Z0-9._-]", "_", extractedTitle) + sanitized = re.sub(r"_+", "_", sanitized).strip("_") + if sanitized: + if not sanitized.lower().endswith(f".{outputFormat}"): + documentName = f"{sanitized}.{outputFormat}" + else: + documentName = sanitized + else: + documentName = f"generated.{outputFormat}" + else: + documentName = f"generated.{outputFormat}" + + # Build document data + docData = DocumentData( + documentName=documentName, + documentData=rendered_content, + mimeType=mime_type, + sourceJson=generated_data # Preserve source JSON for structure validation + ) + + metadata = AiResponseMetadata( + title=extractedTitle or title or "Generated Document", + filename=extractedFilename, + operationType=opType.value if opType else None + ) + + # Write JSON with proper formatting (not str() which can truncate) + jsonStr = json.dumps(generated_data, indent=2, ensure_ascii=False) + self.services.utils.writeDebugFile(jsonStr, "document_generation_response") + self.services.chat.progressLogFinish(aiOperationId, True) + + return AiResponse( + content=json.dumps(generated_data), + metadata=metadata, + documents=[docData] + ) +``` + +**Problem:** +- Bilder müssen in JSON-Struktur enthalten sein, um gerendert zu werden +- Original-Bilder aus `contentParts` werden nicht direkt verwendet +- Nur extrahierter Text von Bildern wird verwendet + +### Phase 5: Action Result Processing + +#### 5.1 Result Extraction + +**Was passiert:** +- `AiResponse` wird zu `ActionResult` konvertiert +- Dokumente werden extrahiert + +**Code-Stelle:** +```159:200:poweron/gateway/modules/workflows/methods/methodAi/actions/process.py +# Extract documents from AiResponse +if aiResponse.documents and len(aiResponse.documents) > 0: + action_documents = [] + for doc in aiResponse.documents: + validationMetadata = { + "actionType": "ai.process", + "resultType": normalized_result_type, + "outputFormat": output_format, + "hasDocuments": True, + "documentCount": len(aiResponse.documents) + } + action_documents.append(ActionDocument( + documentName=doc.documentName, + documentData=doc.documentData, + mimeType=doc.mimeType or output_mime_type, + sourceJson=getattr(doc, 'sourceJson', None), # Preserve source JSON for structure validation + validationMetadata=validationMetadata + )) + + final_documents = action_documents +else: + # Text response - create document from content + extension = output_extension.lstrip('.') + meaningful_name = self._generateMeaningfulFileName( + base_name="ai", + extension=extension, + action_name="result" + ) + validationMetadata = { + "actionType": "ai.process", + "resultType": normalized_result_type, + "outputFormat": output_format, + "hasDocuments": False, + "contentType": "text" + } + action_document = ActionDocument( + documentName=meaningful_name, + documentData=aiResponse.content, + mimeType=output_mime_type, + validationMetadata=validationMetadata + ) + final_documents = [action_document] +``` + +#### 5.2 Task Result Persistence + +**Was passiert:** +- Task Result wird als `ChatMessage` persistiert +- Dokumente werden in Component Storage gespeichert +- Referenzen werden für nachfolgende Tasks verfügbar gemacht + +**Code-Stelle:** +```612:707:poweron/gateway/modules/workflows/processing/workflowProcessor.py +async def persistTaskResult(self, taskResult: Any, workflow: ChatWorkflow, context: Optional[TaskContext] = None) -> ChatMessage: # TaskResult -> ChatMessage +``` + +## Erweiterte Analyse: Drei kritische Aspekte + +### 1. Mehrere Extraction Options für dasselbe Dokument + +**Problem:** Aktuell wird jedes Dokument nur **einmal** extrahiert. Es gibt keine Möglichkeit, für dasselbe Dokument mehrere Extraktionen durchzuführen. + +**Beispiel-Szenario:** +- User möchte: "Erstelle einen Bericht mit Bildern. Übernehme die Bilder als Assets UND extrahiere Text aus den Bildern für Legenden." + +**Aktueller Flow:** +1. PDF wird extrahiert → Bilder werden als `ContentPart` mit `typeGroup="image"` erstellt +2. Bilder werden mit Vision-Modellen analysiert → Text wird extrahiert +3. **PROBLEM:** Original-Bilder gehen verloren, nur Text bleibt übrig + +**Was fehlt:** +- Keine Möglichkeit, dasselbe Bild **sowohl** als Asset **als auch** für Text-Extraktion zu verwenden +- Keine Duplizierung von ContentParts für verschiedene Extraction-Options +- Keine parallele Verarbeitung mit unterschiedlichen Intents + +**Code-Analyse:** + +**Extraction (`extractContent`):** +```39:232:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +def extractContent( + self, + documents: List[ChatDocument], + options: ExtractionOptions, + operationId: Optional[str] = None, + parentOperationId: Optional[str] = None +) -> List[ContentExtracted]: +``` + +- Jedes Dokument wird **einmal** durch die Pipeline geschickt +- `ExtractionOptions` enthält nur **eine** Strategie +- Keine Möglichkeit für mehrere Extraction-Passes + +**Content Parts Processing:** +```1117:1207:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +if contentParts: + # Filter out binary/other parts that shouldn't be processed + processableParts = [] + skippedParts = [] + for p in contentParts: + if p.typeGroup in ["image", "text", "table", "structure"] or (p.mimeType and (p.mimeType.startswith("image/") or p.mimeType.startswith("text/"))): + processableParts.append(p) + else: + skippedParts.append(p) +``` + +- Bilder werden **entweder** analysiert **oder** übersprungen +- Keine parallele Verarbeitung mit verschiedenen Intents + +**Empfohlene Lösung:** + +```python +class ExtractionOptions: + multiPassExtraction: Optional[List[ExtractionPass]] = None + +class ExtractionPass: + intent: str # "image_asset" | "text_extraction" | "structure_extraction" + filterTypeGroups: List[str] # Welche typeGroups sollen verarbeitet werden + operationType: OperationTypeEnum + preserveOriginal: bool # Original-Part behalten + +# Beispiel: +extractionOptions = ExtractionOptions( + multiPassExtraction=[ + ExtractionPass( + intent="image_asset", + filterTypeGroups=["image"], + operationType=OperationTypeEnum.DATA_EXTRACT, + preserveOriginal=True # Bild als Asset behalten + ), + ExtractionPass( + intent="text_extraction", + filterTypeGroups=["image"], + operationType=OperationTypeEnum.IMAGE_ANALYSE, + preserveOriginal=False # Text-Extraktion aus Bild + ) + ] +) +``` + +### 2. Metadaten im Generation Prompt + +**Problem:** Metadaten über Dokument-Herkunft gehen im Generation Prompt verloren. + +**Aktueller Flow:** + +**1. Extraction Phase - Metadaten werden gespeichert:** +```128:133:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +# Attach document id and MIME type to parts if missing +for p in ec.parts: + if "documentId" not in p.metadata: + p.metadata["documentId"] = documentData["id"] or str(uuid.uuid4()) + if "documentMimeType" not in p.metadata: + p.metadata["documentMimeType"] = documentData["mimeType"] +``` + +- ContentParts haben `metadata["documentId"]` und `metadata["documentMimeType"]` + +**2. Content Parts Processing - Metadaten bleiben erhalten:** +```721:776:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +def _convertToContentParts( + self, partResults: Union[List[PartResult], List[AiCallResponse]] +) -> List[ContentPart]: + # ... + metadata={ + **part_result.originalPart.metadata, # ← Metadaten werden übernommen + "aiResult": True, + "partIndex": part_result.partIndex, + "documentId": part_result.documentId, # ← documentId bleibt erhalten + ... + } +``` + +- Metadaten bleiben in ContentParts erhalten + +**3. Merging - Metadaten bleiben erhalten:** +```778:818:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +def mergePartResults( + self, + partResults: Union[List[PartResult], List[AiCallResponse]], + options: Optional[AiCallOptions] = None +) -> str: + # ... + merge_strategy = MergeStrategy( + useIntelligentMerging=True, + groupBy="documentId", # ← Gruppierung nach documentId + orderBy="partIndex", + mergeType="concatenate" + ) + # ... + final_content = "\n\n".join([part.data for part in merged_parts]) # ← NUR data wird verwendet! +``` + +- **PROBLEM:** Beim Merging zu String gehen Metadaten verloren! +- Nur `part.data` wird verwendet, Metadaten werden nicht in den String übernommen + +**4. Generation Prompt - Keine Metadaten:** +```123:165:poweron/gateway/modules/services/serviceGeneration/subPromptBuilderGeneration.py +if extracted_content: + generationPrompt = f""" +... +EXTRACTED CONTENT FROM DOCUMENTS: +{'='*80} +{extracted_content} # ← Nur Text, keine Metadaten! +{'='*80} +END OF EXTRACTED CONTENT +... +""" +``` + +- `extracted_content` ist nur Text-String +- Keine Metadaten über Dokument-Herkunft +- Generation kann nicht unterscheiden, welcher Content von welchem Dokument stammt + +**Code-Stelle, wo Metadaten verloren gehen:** +```814:815:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +# Convert back to string +final_content = "\n\n".join([part.data for part in merged_parts]) +``` + +**Empfohlene Lösung:** + +```python +def mergePartResults(...) -> str: + # ... + merged_parts = applyMerging(content_parts, merge_strategy) + + # Erweitertes Format mit Metadaten + content_sections = [] + for part in merged_parts: + doc_id = part.metadata.get("documentId", "unknown") + doc_mime = part.metadata.get("documentMimeType", "unknown") + label = part.label or "content" + + section = f""" +[SOURCE: documentId={doc_id}, mimeType={doc_mime}, label={label}] +{part.data} +[END SOURCE] +""" + content_sections.append(section) + + final_content = "\n\n".join(content_sections) + return final_content.strip() +``` + +### 3. Korrekte Zusammenführung aller Parts + +**Frage:** Wird sichergestellt, dass alle Parts korrekt zusammengefügt werden? + +**Analyse der Merging-Logik:** + +**A. Merging-Strategien:** + +**1. TextMerger (`mergerText.py`):** +```8:138:poweron/gateway/modules/services/serviceExtraction/merging/mergerText.py +class TextMerger: + def merge(self, parts: List[ContentPart], strategy: MergeStrategy) -> List[ContentPart]: + # Group parts + groups = self._groupParts(parts, groupBy) # ← Gruppierung nach documentId/parentId + + merged: List[ContentPart] = [] + for groupKey, groupParts in groups.items(): + # Sort within group + sortedParts = self._sortParts(groupParts, orderBy) # ← Sortierung nach partIndex + + # Merge respecting maxSize + if maxSize > 0: + merged.extend(self._mergeWithSizeLimit(sortedParts, maxSize)) + else: + merged.extend(self._mergeGroup(sortedParts, groupKey)) + + return merged +``` + +- ✅ Gruppierung nach `documentId` oder `parentId` +- ✅ Sortierung nach `partIndex`, `pageIndex`, oder `sheetIndex` +- ✅ Size-Limits werden respektiert + +**2. IntelligentTokenAwareMerger (`subMerger.py`):** +```14:212:poweron/gateway/modules/services/serviceExtraction/subMerger.py +class IntelligentTokenAwareMerger: + def mergeChunksIntelligently(self, chunks: List[ContentPart], prompt: str = "") -> List[ContentPart]: + # Group chunks by document and type for semantic coherence + groupedChunks = self._groupChunksByDocumentAndType(chunks) # ← Gruppierung nach docId + typeGroup + + mergedParts = [] + for groupKey, groupChunks in groupedChunks.items(): + # Merge chunks within this group optimally + groupMerged = self._mergeGroupOptimally(groupChunks, availableTokens) + mergedParts.extend(groupMerged) + + return mergedParts +``` + +- ✅ Gruppierung nach `documentId` + `typeGroup` +- ✅ Token-bewusste Optimierung +- ⚠️ **PROBLEM:** Sortierung nach Original-Reihenfolge wird nicht explizit erhalten + +**3. mergePartResults:** +```778:818:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +def mergePartResults( + self, + partResults: Union[List[PartResult], List[AiCallResponse]], + options: Optional[AiCallOptions] = None +) -> str: + # ... + if isinstance(partResults[0], PartResult): + merge_strategy = MergeStrategy( + useIntelligentMerging=True, + groupBy="documentId", # ← Gruppierung nach Dokument + orderBy="partIndex", # ← Sortierung nach Part-Index + mergeType="concatenate" + ) + else: + merge_strategy = MergeStrategy( + useIntelligentMerging=True, + groupBy="typeGroup", # ← Gruppierung nach Typ + orderBy="id", # ← Sortierung nach ID + mergeType="concatenate" + ) + + merged_parts = applyMerging(content_parts, merge_strategy) + final_content = "\n\n".join([part.data for part in merged_parts]) +``` + +- ✅ Strategie-basierte Gruppierung und Sortierung +- ⚠️ **PROBLEM:** Bei `AiCallResponse` wird nach `id` sortiert, nicht nach Original-Reihenfolge + +**B. Potenzielle Probleme:** + +**1. Reihenfolge-Verlust bei AiCallResponse:** +```754:774:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +elif isinstance(partResults[0], AiCallResponse): + # Logic from interfaceAiObjects (from content parts processing) + for i, result in enumerate(partResults): + if result.content: + content_part = ContentPart( + id=str(uuid.uuid4()), # ← Neue UUID, keine Original-Reihenfolge! + parentId=None, + label=f"ai_result_{i}", # ← Index in Label, aber nicht für Sortierung verwendet + ... + ) +``` + +- `id` ist neue UUID, keine Original-Reihenfolge +- `orderBy="id"` sortiert nach UUID, nicht nach Verarbeitungs-Reihenfolge + +**2. Chunking kann Reihenfolge durcheinander bringen:** +```820:886:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +async def chunkContentPartForAi(self, contentPart, model, options, prompt: str = "") -> List[Dict[str, Any]]: + # ... + chunks = chunker.chunk(contentPart, chunkingOptions) + return chunks +``` + +- Chunks haben `parentId`, aber keine explizite Reihenfolge innerhalb des Chunks + +**3. Merging bei Chunks:** +```1020:1041:poweron/gateway/modules/services/serviceExtraction/mainServiceExtraction.py +chunkResults = [] +for idx, chunk in enumerate(chunks): + chunkNum = idx + 1 + chunkData = chunk.get('data', '') + logger.info(f"Processing chunk {chunkNum}/{len(chunks)} with model {model.name}") + + try: + chunkResponse = await aiObjects._callWithModel(model, prompt, chunkData, options) + chunkResults.append(chunkResponse) # ← Reihenfolge durch Listen-Append erhalten + logger.info(f"✅ Chunk {chunkNum}/{len(chunks)} processed successfully") + except Exception as e: + logger.error(f"❌ Error processing chunk {chunkNum}/{len(chunks)}: {str(e)}") + raise + +# Merge chunk results +mergedContent = self.mergeChunkResults(chunkResults) +``` + +- ✅ Chunk-Responses werden sequenziell in Liste gespeichert (`chunkResults.append(chunkResponse)`) +- ✅ Reihenfolge wird durch Listen-Reihenfolge erhalten +- ✅ **BEHOBEN:** `mergeChunkResults` wurde durch `mergePartResults(chunkResults)` ersetzt +- ✅ **BEGRÜNDUNG:** `mergePartResults` akzeptiert `List[AiCallResponse]` (Zeile 780) und `chunkResults` ist genau das +- ✅ **KONSISTENZ:** Verwendet jetzt die gleiche Merging-Funktion wie `processContentPartsWithAi` (Zeile 1111) + +**C. Was funktioniert:** + +✅ **Gruppierung:** Parts werden nach `documentId` gruppiert +✅ **Sortierung:** Bei `PartResult` wird nach `partIndex` sortiert +✅ **Size-Limits:** Werden respektiert +✅ **Token-Optimierung:** IntelligentTokenAwareMerger optimiert AI-Calls + +**D. Was problematisch ist:** + +⚠️ **AiCallResponse:** Reihenfolge geht verloren (Sortierung nach UUID statt Index) +⚠️ **Chunks:** Reihenfolge innerhalb von Chunks nicht explizit gesichert +⚠️ **Metadaten:** Gehen beim Merging zu String verloren +✅ **mergeChunkResults:** Bug behoben - wurde durch `mergePartResults` ersetzt + +**E. Zusammenfassung Merging-Logik:** + +| Aspekt | Status | Details | +|--------|--------|---------| +| **Gruppierung** | ✅ Funktioniert | Nach `documentId` oder `typeGroup` | +| **Sortierung (PartResult)** | ✅ Funktioniert | Nach `partIndex`, `pageIndex`, `sheetIndex` | +| **Sortierung (AiCallResponse)** | ⚠️ Problematisch | Nach UUID statt Original-Index | +| **Chunk-Reihenfolge** | ⚠️ Unklar | Listen-Reihenfolge erhalten, aber keine explizite Validierung | +| **Metadaten-Erhaltung** | ❌ Geht verloren | Beim Merging zu String gehen Metadaten verloren | +| **Size-Limits** | ✅ Funktioniert | Werden respektiert | +| **Token-Optimierung** | ✅ Funktioniert | IntelligentTokenAwareMerger optimiert AI-Calls | +| **mergeChunkResults** | ✅ **BEHOBEN** | Wurde durch `mergePartResults` ersetzt (Zeile 1041) | + +**Fazit Merging:** +- Grundlegende Merging-Logik ist vorhanden und funktioniert +- **Kritische Probleme:** Metadaten gehen verloren, AiCallResponse-Reihenfolge unklar +- ✅ **BEHOBEN:** `mergeChunkResults` wurde durch `mergePartResults` ersetzt + - `mergePartResults` akzeptiert `List[AiCallResponse]` (Zeile 780) + - `chunkResults` ist `List[AiCallResponse]` (Zeile 1031) + - Konsistente Verwendung der gleichen Merging-Funktion wie `processContentPartsWithAi` (Zeile 1111) + +**Empfohlene Verbesserungen:** + +```python +# 1. Reihenfolge explizit speichern +content_part = ContentPart( + id=str(uuid.uuid4()), + parentId=None, + label=f"ai_result_{i}", + metadata={ + "originalIndex": i, # ← Explizite Reihenfolge + "processingOrder": i, + ... + } +) + +# 2. Sortierung nach originalIndex +merge_strategy = MergeStrategy( + groupBy="documentId", + orderBy="originalIndex", # ← Statt "id" + mergeType="concatenate" +) + +# 3. Chunk-Reihenfolge explizit +for idx, chunk in enumerate(chunks): + chunk["metadata"] = { + "chunkIndex": idx, + "parentId": contentPart.id, + "totalChunks": len(chunks) + } +``` + +## Zusammenfassung: Aktuelle Probleme + +### Problem 1: Automatische Extraktion ohne Analyse + +**Aktueller Zustand:** +- Dokumente werden **IMMER** extrahiert, wenn `documentList` vorhanden ist +- Keine Analyse, ob Extraktion nötig ist +- Standard-ExtractionOptions: "Extract all content from the document" + +**Beispiel-Szenarien, wo Extraktion nicht nötig ist:** +- User möchte Dokumente nur als Referenz verwenden +- User möchte Dokumente direkt rendern (z.B. Bilder in Bericht einfügen) +- User möchte Dokumente nur analysieren (nicht extrahieren) + +### Problem 2: Unklare Bild-Behandlung + +**Aktueller Zustand:** +- Bilder werden **IMMER** mit Vision-Modellen analysiert (Text-Extraktion) +- Keine Unterscheidung zwischen: + - Bildern, die **gerendert** werden sollen (Logo, Diagramm, Screenshot als Bild) + - Bildern, deren **Text extrahiert** werden soll (Screenshot mit Text-Inhalt) + +**Beispiel-Szenarien:** +- **Szenario A:** User möchte Bericht mit Logo-Bildern generieren + - **Aktuell:** Logo wird analysiert, Text wird extrahiert (falsch!) + - **Erwartet:** Logo wird als Bild-Asset für Rendering bereitgestellt + +- **Szenario B:** User möchte Text aus Screenshot extrahieren + - **Aktuell:** Screenshot wird analysiert, Text wird extrahiert (richtig!) + - **Erwartet:** Text wird extrahiert, Screenshot kann optional auch gerendert werden + +### Problem 3: Fehlende Intent-Analyse + +**Aktueller Zustand:** +- Intent-Analyse ist sehr generisch +- Keine spezifische Analyse für: + - Welche Dokumente extrahiert werden müssen + - Was aus Dokumenten extrahiert werden soll + - Wie Bilder behandelt werden sollen + +**Was fehlt:** +- Analyse der User-Intention bezüglich Dokumenten +- Klassifizierung von Dokumenten nach Verwendungszweck +- Entscheidungslogik für Extraktion vs. direkte Verwendung + +### Problem 4: Fehlende Dokument-Metadaten + +**Aktueller Zustand:** +- Dokumente haben nur Basis-Metadaten (`mimeType`, `fileName`) +- Keine Metadaten für: + - Verwendungszweck (extrahieren, rendern, referenzieren) + - Extraktions-Intent (Text, Struktur, Bilder) + - Rendering-Intent (Bild als Asset, Bild als Text) + +## Empfohlene Lösung: Document Intent Analysis + +### Phase 1: Pre-Extraction Analysis + +**Neue Funktion:** `analyzeDocumentIntent` + +**Was passiert:** +- User Prompt wird analysiert +- Dokument-Referenzen werden analysiert +- Intent wird bestimmt (als Liste von Intents): + - **Extract:** Dokument muss extrahiert werden (Text, Struktur) + - **Render:** Dokument/Bilder müssen in generiertes Dokument integriert werden + - **Reference:** Dokument dient nur als Referenz/Kontext + - **Mehrere Intents möglich:** z.B. `["extract", "render"]` für beide Anforderungen + +**Output:** +```python +DocumentIntent = { + "documentId": str, + "intents": List[str], # Liste von Intents: ["extract", "render", "reference"] - mehrere möglich + "extractionPrompt": Optional[str], # Spezifischer Prompt für Extraktion (z.B. "Extract text from images for legends") + "reasoning": str # Erklärung für Debugging/Transparenz: Warum wurde dieser Intent gewählt? +} +``` + +**Intents (möglich):** +- `"extract"`: Dokument muss extrahiert werden (Text, Struktur, etc.) +- `"render"`: Dokument/Bilder müssen in generiertes Dokument integriert werden +- `"reference"`: Dokument dient nur als Referenz/Kontext + +**Beispiele:** + +```python +# Beispiel 1: Bild im Bericht übernehmen UND Text extrahieren +{ + "documentId": "img_001", + "intents": ["extract", "render"], + "extractionPrompt": "Extract all text content from this image, including legends, labels, and descriptions", + "reasoning": "User wants both: image rendered in report (standard process) AND text extracted for legends" +} + +# Beispiel 2: Nur Text extrahieren +{ + "documentId": "pdf_001", + "intents": ["extract"], + "extractionPrompt": "Extract all text content, preserving structure and formatting", + "reasoning": "User only needs text extraction, no rendering required" +} + +# Beispiel 3: Nur als Referenz +{ + "documentId": "ref_001", + "intents": ["reference"], + "extractionPrompt": None, + "reasoning": "Document is only used as context, no extraction or rendering needed" +} +``` + +**Warum diese Struktur?** + +1. **`intents` als Liste (statt einzelner Wert + "hybrid"):** + - ✅ Mehrere Intents gleichzeitig möglich (z.B. `["extract", "render"]`) + - ✅ Kein "hybrid" nötig - einfach beide Intents in Liste + - ✅ Flexibler und klarer + - ✅ Einfacher zu erweitern (neue Intents einfach hinzufügen) + +2. **`extractionPrompt` statt `extractionType` (statt feste Kriterien):** + - ✅ Flexibler: Kann spezifische Anweisungen enthalten + - ✅ Nicht auf feste Kriterien beschränkt ("text", "structure", "image_text") + - ✅ AI kann genau bestimmen, was extrahiert werden soll + - ✅ Beispiel: "Extract text from images for legends" statt nur "image_text" + - ✅ Kann komplexe Anforderungen beschreiben + +3. **Kein `renderingInstructions` nötig:** + - ✅ Bilder werden **standardmäßig** im JSON integriert und anschließend gerendert + - ✅ Der Prozess ist klar: Wenn `"render" in intents`, werden Bilder in JSON-Struktur integriert + - ✅ Keine separaten Anweisungen nötig - Standard-Verhalten + - ✅ Renderer erwartet `base64Data` in JSON-Struktur (automatisch) + +4. **`reasoning`:** + - ✅ Für Debugging und Transparenz + - ✅ Erklärt, warum bestimmte Intents gewählt wurden + - ✅ Hilft bei Fehlersuche und Optimierung + - ✅ Kann für Logging und Monitoring verwendet werden + +### Phase 2: Conditional Extraction + +**Was passiert:** +- Extraktion erfolgt nur, wenn `"extract" in intents` +- ExtractionOptions werden mit `extractionPrompt` erstellt +- Wenn `extractionPrompt` vorhanden, wird dieser als Prompt für Extraktion verwendet +- Wenn `extractionPrompt` None, wird Standard-Extraktion verwendet + +**Code-Beispiel:** +```python +if "extract" in documentIntent.intents: + extractionOptions = ExtractionOptions( + prompt=documentIntent.extractionPrompt or "Extract all content from the document", + ... + ) + extractedResults = self.extractContent(documents, extractionOptions) +``` + +### Phase 3: Image Handling + +**Was passiert:** +- **WICHTIG:** Bilder werden **standardmäßig** aus JSON-Struktur gerendert (mit `base64Data` in `elements`) +- Keine separate Asset-Pipeline vorhanden +- Bilder müssen in der generierten JSON-Struktur enthalten sein + +**Für Bilder mit `"render" in intents`:** +- Bilder werden **standardmäßig** als `base64Data` in JSON-Struktur integriert +- Keine speziellen Anweisungen nötig - Standard-Verhalten +- AI integriert Bilder automatisch in generierte JSON-Struktur + +**Für Bilder mit `"extract" in intents`:** +- `extractionPrompt` wird für Vision-Analyse verwendet +- Text wird extrahiert und in `extracted_content` bereitgestellt +- Beispiel: "Extract all text from this image, including legends" + +**Kombination möglich:** +- `intents: ["extract", "render"]` +- Bild wird analysiert (Text extrahiert) UND standardmäßig in JSON integriert + +### Phase 4: Generation Prompt Enhancement + +**⚠️ KRITISCHES PROBLEM: Aktuell NICHT implementiert!** + +**Was SOLLTE passieren:** +- Generation Prompt sollte enthalten: + - Extracted Content (Text, Struktur) + - DocumentIntent-Informationen (welche Dokumente/Bilder gerendert werden sollen) + - Referenzen (für Kontext) +- **Bilder sollten integriert werden:** Wenn `"render" in intents`, sollten Bilder in JSON-Struktur integriert werden + +**Was AKTUELL passiert:** + +**1. DocumentIntent-Informationen gehen verloren:** + +**Code-Stelle (callAiContent):** +```1117:1207:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +# Process contentParts for generation prompt (if provided) +if contentParts: + # Filter out binary/other parts that shouldn't be processed + processableParts = [] + skippedParts = [] + for p in contentParts: + if p.typeGroup in ["image", "text", "table", "structure"]: + processableParts.append(p) + + # Build extraction prompt + extractionPrompt = await buildExtractionPrompt(...) + + # Call extraction + extractionResponse = await self.callAi(extractionRequest) + + # Use extracted content directly for generation prompt + content_for_generation = extractionResponse.content # ← NUR Text! +``` + +**Problem:** +- `contentParts` werden IMMER extrahiert (Bilder → Text via Vision-Modelle) +- `extractionResponse.content` enthält nur Text, keine Bild-Assets +- **KEINE DocumentIntent-Informationen werden übergeben** +- **KEINE Information, welche Bilder gerendert werden sollen** + +**2. Generation Prompt erhält keine Intent-Informationen:** + +**Code-Stelle (buildGenerationPrompt):** +```1249:1250:poweron/gateway/modules/services/serviceAi/mainServiceAi.py +generation_prompt = await buildGenerationPrompt( + outputFormat, prompt, title, content_for_generation, None, self.services +) +``` + +**buildGenerationPrompt Signatur:** +```16:23:poweron/gateway/modules/services/serviceGeneration/subPromptBuilderGeneration.py +async def buildGenerationPrompt( + outputFormat: str, + userPrompt: str, + title: str, + extracted_content: str = None, # ← NUR Text + continuationContext: Dict[str, Any] = None, + services: Any = None +) -> str: +``` + +**Problem:** +- `buildGenerationPrompt` erhält nur `extracted_content` (Text) +- **KEINE DocumentIntent-Informationen** +- **KEINE Bild-Assets** +- **KEINE Information über Rendering-Anforderungen** + +**3. Generation Prompt enthält keine Bild-Anweisungen:** + +**Code-Stelle (buildGenerationPrompt Inhalt):** +```123:165:poweron/gateway/modules/services/serviceGeneration/subPromptBuilderGeneration.py +if extracted_content: + generationPrompt = f""" +EXTRACTED CONTENT FROM DOCUMENTS: +{extracted_content} # ← NUR Text, keine Bilder, keine Intent-Info +... +""" +``` + +**Problem:** +- Generation Prompt enthält nur Text +- **KEINE Anweisungen, Bilder zu integrieren** +- **KEINE Bild-Assets verfügbar** +- AI kann Bilder nicht in JSON integrieren, weil: + - Original-Bilder nicht verfügbar sind (nur Text-Extraktion) + - Keine Anweisungen vorhanden sind + - Keine Metadaten über Intent vorhanden sind + +**Was FEHLT für korrekte Implementierung:** + +1. **DocumentIntent-Übergabe:** + ```python + # In callAiContent: + documentIntents: Optional[List[DocumentIntent]] = None + + # In buildGenerationPrompt: + documentIntents: Optional[List[DocumentIntent]] = None + ``` + +2. **Bild-Assets bereitstellen:** + ```python + # Für Bilder mit "render" in intents: + imageAssets = [] + for contentPart in contentParts: + if contentPart.typeGroup == "image": + documentIntent = getIntentForDocument(contentPart.documentId) + if "render" in documentIntent.intents: + imageAssets.append({ + "documentId": contentPart.documentId, + "base64Data": contentPart.data, + "altText": contentPart.label + }) + ``` + +**✅ KORREKTE LOGIK: Struktur → Platzhalter → Code-Integration** + +**Aktueller Flow (teilweise implementiert):** + +**1. Struktur-Generierung (Phase 1):** +```22:99:poweron/gateway/modules/services/serviceGeneration/subStructureGenerator.py +async def generateStructure(...) -> Dict[str, Any]: + # AI generiert Struktur mit leeren elements: [] + # Für bestehende Bilder: image_source="existing", image_reference_id="doc_id" +``` + +**Struktur-Beispiel:** +```json +{ + "sections": [ + { + "id": "section_image_existing", + "content_type": "image", + "image_source": "existing", // ← Platzhalter für Code-Integration (bestehendes Bild) + "image_reference_id": "doc_id_here", + "elements": [] // ← Leer, wird vom Code gefüllt + }, + { + "id": "section_image_generate", + "content_type": "image", + // image_source NICHT gesetzt oder "generate" (default) ← Platzhalter für AI-Generierung + "image_prompt": "A detailed description for image generation", // ← Platzhalter: Prompt für Bild-Generierung + "generation_hint": "Illustration for chapter 1", // ← Optional: Fallback für image_prompt + "complexity": "complex", + "elements": [] // ← Leer, wird von AI (IMAGE_GENERATE) gefüllt + }, + { + "id": "section_paragraph_1", + "content_type": "paragraph", + "complexity": "simple", + "elements": [] // ← Leer, wird von AI gefüllt + } + ] +} +``` + +**Platzhalter für Bilder:** + +| Platzhalter-Typ | `image_source` | Zusätzliche Felder | Wer füllt? | +|----------------|----------------|-------------------|------------| +| **Bestehendes Bild integrieren** | `"existing"` | `image_reference_id` | **Code** (automatisch) | +| **Bild generieren** | Nicht gesetzt oder `"generate"` (default) | `image_prompt` (erforderlich) oder `generation_hint` (Fallback) | **AI** (IMAGE_GENERATE Operation) | + +**2. Content-Abfüllen (Phase 2):** +```23:100:poweron/gateway/modules/services/serviceGeneration/subContentGenerator.py +async def generateContent(...): + # Iteriert durch Sections + # Für jedes Element wird Content gefüllt +``` + +**3. Image-Integration durch Code ODER AI-Generierung:** + +```575:688:poweron/gateway/modules/services/serviceGeneration/subContentGenerator.py +async def _generateImageSection(...): + imageSource = section.get("image_source", "generate") # ← Default: "generate" + + if imageSource == "existing": + # ← CODE integriert bestehendes Bild automatisch! + imageRefId = section.get("image_reference_id") + imageDoc = findImageDocument(imageRefId) + + section["elements"] = [{ + "base64Data": imageDoc.get("base64Data"), // ← Direkt vom Code + "altText": imageDoc.get("altText"), + "mimeType": imageDoc.get("mimeType") + }] + return section // ← Kein AI-Call! + + # Generate new image (imageSource == "generate" oder nicht gesetzt) + imagePrompt = section.get("image_prompt") + if not imagePrompt: + // Fallback: generation_hint verwenden + generationHint = section.get("generation_hint", "") + imagePrompt = f"Create a professional illustration: {generationHint}" + + // ← AI generiert Bild mit IMAGE_GENERATE Operation + options = AiCallOptions( + operationType=OperationTypeEnum.IMAGE_GENERATE, + resultFormat="base64" + ) + aiResponse = await self.services.ai.callAiContent( + prompt=imagePrompt, + options=options, + outputFormat="base64" + ) + + // Extrahiere base64Data aus AI-Response + base64Data = extractBase64FromResponse(aiResponse) + + section["elements"] = [{ + "base64Data": base64Data, // ← Von AI generiert + "altText": imagePrompt[:100], + "mimeType": "image/png" + }] + return section +``` + +**Zusammenfassung Image-Platzhalter:** + +1. **Bestehendes Bild (`image_source="existing"`):** + - Platzhalter: `image_reference_id` + - Integration: **Code** (automatisch, kein AI-Call) + - Beispiel: Bild aus `contentParts` mit `"render" in intents` + +2. **Bild generieren (`image_source` nicht gesetzt oder `"generate"`):** + - Platzhalter: `image_prompt` (erforderlich) oder `generation_hint` (Fallback) + - Integration: **AI** (IMAGE_GENERATE Operation) + - Beispiel: "Erzeuge ein Bild zum Thema Kochen ohne Fleisch" + +**4. Text-Content durch AI:** +- Für `content_type: "paragraph"` → AI generiert Text +- Für `content_type: "heading"` → AI generiert Überschrift +- Verwendet `extracted_content` und `userPrompt` + +**✅ Was FUNKTIONIERT:** +- Struktur wird zuerst generiert (mit Platzhaltern) +- Bilder mit `image_source="existing"` werden automatisch vom Code integriert +- Text-Sections werden von AI gefüllt + +**❌ Was FEHLT für DocumentIntent-Integration:** + +**1. Platzhalter für `"render" in intents`:** +```python +# In generateStructure: +# Für Bilder mit "render" in intents sollte Platzhalter erstellt werden: +{ + "id": "section_image_1", + "content_type": "image", + "image_source": "render", # ← NEU: Statt "existing" + "image_reference_id": contentPart.documentId, # ← Referenz zu contentPart + "elements": [] +} +``` + +**2. Code-Integration erweitern:** +```python +# In _generateImageSection: +if imageSource == "render": # ← NEU + # Finde contentPart basierend auf image_reference_id + contentPart = findContentPartByDocumentId(image_reference_id) + if contentPart and contentPart.typeGroup == "image": + section["elements"] = [{ + "base64Data": contentPart.data, # ← Direkt vom Code + "altText": contentPart.label, + "mimeType": contentPart.mimeType + }] + return section # ← Kein AI-Call! +``` + +**3. Conditional Extraction:** +```python +# In callAiContent (vor Extraction): +for contentPart in contentParts: + documentIntent = getIntentForDocument(contentPart.documentId) + + if "extract" in documentIntent.intents: + # Extract with extractionPrompt + processableParts.append(contentPart) + elif "render" in documentIntent.intents: + # Keep as asset, don't extract + # Add to cachedContent.imageDocuments for StructureGenerator + imageDocuments.append({ + "id": contentPart.documentId, + "base64Data": contentPart.data, + "altText": contentPart.label, + "mimeType": contentPart.mimeType + }) +``` + +**Zusammenfassung der korrekten Logik:** + +1. **Struktur-Generierung:** AI erzeugt Struktur mit Platzhaltern (`elements: []`) +2. **Platzhalter für Bilder:** + - **Bestehendes Bild:** `image_source="existing"` + `image_reference_id` → Code integriert automatisch + - **Bild generieren:** `image_source` nicht gesetzt (default: `"generate"`) + `image_prompt` → AI generiert mit IMAGE_GENERATE + - **Bild mit `"render" in intents`:** `image_source="render"` + `image_reference_id` → Code integriert automatisch (noch nicht implementiert) +3. **Content-Abfüllen:** + - **Bilder (existing/render):** Code füllt automatisch ein (kein AI-Call) + - **Bilder (generate):** AI generiert Bild mit IMAGE_GENERATE Operation + - **Text-Sections:** AI füllt mit `extracted_content` und `userPrompt` +4. **Rendering:** Renderer verwendet `base64Data` aus JSON-Struktur + +**Code-Stelle (aktuell):** +```470:486:poweron/gateway/modules/services/serviceGeneration/renderers/rendererDocx.py +def _renderJsonImage(self, doc: Document, image_data: Dict[str, Any], styles: Dict[str, Any]) -> None: + """Render a JSON image to DOCX.""" + try: + base64_data = image_data.get("base64Data", "") # ← Erwartet base64Data in JSON + alt_text = image_data.get("altText", "Image") + + if base64_data: + image_bytes = base64.b64decode(base64_data) + doc.add_picture(io.BytesIO(image_bytes), width=Inches(4)) +``` + +**❌ AKTUELLER STATUS: NICHT IMPLEMENTIERT** + +**Was FEHLT:** +1. **DocumentIntent-Übergabe:** Intent-Informationen werden nicht von `analyzeDocumentIntent` bis zu `buildGenerationPrompt` übergeben +2. **Bild-Assets:** Bilder werden IMMER extrahiert (Text), nicht als Assets bereitgestellt +3. **Conditional Extraction:** Keine Unterscheidung zwischen "extract" und "render" - alles wird extrahiert +4. **Generation Prompt:** Erhält keine Intent-Informationen oder Bild-Assets + +**Konsequenz:** +- Die Aussage "Bilder werden standardmäßig integriert" ist **NICHT sichergestellt** +- AI kann Bilder nicht in JSON integrieren, weil: + - Original-Bilder nicht verfügbar sind (nur Text-Extraktion) + - Keine Anweisungen vorhanden sind + - Keine Metadaten über Intent vorhanden sind + +**Was IMPLEMENTIERT werden muss:** +Siehe Abschnitt "Was FEHLT für korrekte Implementierung" oben. + +## Nächste Schritte + +1. **Implementierung von `analyzeDocumentIntent`** + - AI-basierte Analyse des User Prompts + - Klassifizierung von Dokumenten nach Verwendungszweck + +2. **Erweiterung von `ExtractionOptions`** + - Neues Feld: `documentIntent: Optional[DocumentIntent]` + - Conditional Extraction basierend auf `intents` + - Verwendung von `extractionPrompt` wenn vorhanden + +3. **Erweiterung von `ContentPart`** + - Neues Feld: `documentIntent: Optional[DocumentIntent]` + - Metadaten enthalten Intent-Informationen + - Unterscheidung zwischen Text-Extraktion und Rendering-Anforderungen + +4. **Erweiterung von `buildGenerationPrompt`** + - Integration von Image Assets + - Separate Behandlung von Extracted Content und Assets + +## Flow-Diagramm: Aktueller vs. Empfohlener Flow + +### Aktueller Flow (Problem) + +``` +User Input + Documents + ↓ +Complexity Check (nur Metadaten) + ↓ +Workflow Planning + ↓ +Action: ai.process + ↓ +[AUTOMATISCHE EXTRAKTION] ← PROBLEM: Immer, ohne Analyse + ↓ +ContentParts (alle extrahiert) + ↓ +[EXTRACTION PROMPT] ← PROBLEM: Generisch, keine Intent-Analyse + ↓ +[BILDER MIT VISION ANALYSIERT] ← PROBLEM: Immer, auch wenn Rendering gewünscht + ↓ +Extracted Content (nur Text) + ↓ +Generation Prompt + ↓ +Document Generation + ↓ +Rendering (Bilder nur aus JSON) +``` + +### Empfohlener Flow (Lösung) + +``` +User Input + Documents + ↓ +Complexity Check (nur Metadaten) + ↓ +Workflow Planning + ↓ +Action: ai.process + ↓ +[DOCUMENT INTENT ANALYSIS] ← NEU: Analyse, was mit Dokumenten gemacht werden soll + ↓ + ├─→ Extract Intent → Conditional Extraction + ├─→ Render Intent → Image Asset Preparation + └─→ Reference Intent → Skip Extraction + ↓ +ContentParts (selektiv extrahiert/bereitgestellt) + ↓ +[EXTRACTION PROMPT] ← Verbessert: Basierend auf Intent + ↓ +[BILDER SELEKTIV BEHANDELT] ← Verbessert: Text-Extraktion ODER Asset-Bereitstellung + ↓ +Extracted Content + Image Assets + ↓ +Generation Prompt (mit Assets) + ↓ +Document Generation (mit Assets) + ↓ +Rendering (Bilder aus Assets + JSON) +``` + +## Prompt-Analyse: Konkrete Use Cases + +### Prompt 1: Buch-Generierung mit Bildern + +**User Prompt:** +> "erzeuge ein buch mit den beigelegten bildern zum thema kochen ohne fleisch und integriere generierte bilder, wo bilder fehlen" + +**Input:** 10 Bilder in einem PDF-Dokument + +#### Aktueller Flow-Analyse + +**Was passiert aktuell:** + +1. **PDF wird extrahiert** (`extractContent`) + - PDF-Extractor extrahiert alle Bilder als `ContentPart` mit `typeGroup="image"` + - Bilder werden als base64-encoded Daten bereitgestellt + +2. **Bilder werden mit Vision-Modellen analysiert** (`processContentPartsWithAi`) + - **PROBLEM:** Alle Bilder werden analysiert, um Text zu extrahieren + - Vision-Models beschreiben Bild-Inhalt als Text + - Original-Bilder gehen verloren (nur Text-Beschreibung bleibt) + +3. **Extraction Prompt wird erstellt** (`buildExtractionPrompt`) + - Generischer Prompt: "Extract content from documents" + - Keine spezifische Anweisung für Bild-Rendering + +4. **Generation Prompt enthält nur Text** (`buildGenerationPrompt`) + - Extracted Content enthält nur Text-Beschreibungen der Bilder + - **PROBLEM:** Original-Bilder sind nicht verfügbar für Rendering + +5. **Rendering** (`renderReport`) + - Renderer kann keine Bilder rendern, da nur Text vorhanden ist + - Generierte Bilder können nicht integriert werden + +#### Probleme + +1. **Bilder werden analysiert statt gerendert** + - Aktuell: Bilder → Vision-Analyse → Text-Beschreibung + - Benötigt: Bilder → Asset-Bereitstellung → Rendering + +2. **Keine Möglichkeit, Bilder als Assets zu behalten** + - ContentParts werden nur für Text-Extraktion verwendet + - Keine separate Asset-Pipeline + +3. **Generierte Bilder können nicht integriert werden** + - Keine Möglichkeit, neue Bilder zu generieren und zu integrieren + - Keine Bild-Generierung im Workflow + +#### Was angepasst werden müsste + +**1. Pre-Extraction Analysis (DocumentIntent):** +```python +DocumentIntent = { + "documentId": "pdf_1", + "intents": ["extract", "render"], # ← Liste statt "hybrid" + "extractionPrompt": "Extract text content from images for legends and descriptions", + "reasoning": "User möchte Buch mit Bildern generieren - Bilder müssen gerendert werden UND Text extrahiert" +} +``` + +**2. Conditional Extraction:** +- PDF wird extrahiert +- **Für Bilder mit `"extract" in intents`:** Vision-Analyse für Text-Extraktion +- **Für Bilder mit `"render" in intents`:** Bilder werden NICHT extrahiert, bleiben als Assets +- Original-Bild-Daten bleiben erhalten für Rendering + +**3. Struktur-Generierung mit Platzhaltern:** +- `StructureGenerator` erzeugt Struktur mit Platzhaltern für Bilder +- Für bestehende Bilder: `image_source="render"` + `image_reference_id` +- Für fehlende Bilder: `image_source` nicht gesetzt + `image_prompt` (AI generiert) + +**4. Content-Abfüllen:** +- **Code integriert Bilder automatisch** (bei `image_source="render"` oder `"existing"`) +- **AI generiert fehlende Bilder** (bei `image_source` nicht gesetzt, mit `image_prompt`) +- **AI füllt Text-Sections** mit `extracted_content` + +**5. Rendering:** +- Renderer verwendet `base64Data` aus JSON-Struktur (bereits integriert) +- Keine separate Asset-Pipeline nötig + +#### Erforderliche Code-Änderungen + +1. **`DocumentIntent` implementieren:** + ```python + class DocumentIntent: + documentId: str + intents: List[str] # ["extract", "render", "reference"] + extractionPrompt: Optional[str] + reasoning: str + ``` + +2. **`callAiContent` erweitern:** + ```python + async def callAiContent( + ..., + documentIntents: Optional[List[DocumentIntent]] = None # ← NEU + ): + # Conditional Extraction basierend auf intents + for contentPart in contentParts: + documentIntent = getIntentForDocument(contentPart.documentId) + + if "extract" in documentIntent.intents: + # Extract with extractionPrompt + processableParts.append(contentPart) + elif "render" in documentIntent.intents: + # Keep as asset, add to cachedContent.imageDocuments + imageDocuments.append({ + "id": contentPart.documentId, + "base64Data": contentPart.data, + "altText": contentPart.label, + "mimeType": contentPart.mimeType + }) + ``` + +3. **`StructureGenerator.generateStructure` erweitern:** + ```python + # Für Bilder mit "render" in intents: + # Erstelle Platzhalter mit image_source="render" + { + "id": "section_image_1", + "content_type": "image", + "image_source": "render", # ← NEU + "image_reference_id": contentPart.documentId, + "elements": [] + } + ``` + +4. **`ContentGenerator._generateImageSection` erweitern:** + ```python + if imageSource == "render": # ← NEU + # Finde contentPart basierend auf image_reference_id + contentPart = findContentPartByDocumentId(image_reference_id) + if contentPart and contentPart.typeGroup == "image": + section["elements"] = [{ + "base64Data": contentPart.data, # ← Code integriert automatisch + "altText": contentPart.label, + "mimeType": contentPart.mimeType + }] + return section # ← Kein AI-Call! + ``` + +5. **`cachedContent` erweitern:** + ```python + # In callAiContent: + cachedContent = { + "extracted_content": extracted_content, # Text + "imageDocuments": imageDocuments # ← Bilder für Rendering + } + ``` + +**✅ KEINE Änderungen nötig:** +- ❌ `ExtractionOptions` erweitern (nicht nötig - DocumentIntent reicht) +- ❌ `ContentPart` erweitern (nicht nötig - Platzhalter-System verwendet) +- ❌ `renderReport` erweitern (nicht nötig - Bilder bereits in JSON-Struktur) + +### Prompt 2: PDF-Splitting mit Web-Research + +**User Prompt:** +> "extrahiere aus dem pdf die einzelnen dokumente, welche mit trennseiten (leere seiten) auseinandergehalten werden, und speichere sie im sharepoint als separate dateien ab. ergänze aus dem web die adressen pro firma jedes dokumentes" + +**Input:** PDF mit 600 Seiten + +**✅ WICHTIG:** Dieser Prompt sollte als **Serie von Actions** gelöst werden, **NICHT** als spezifische Logik im Code! + +#### Lösung als Workflow-Actions + +**Workflow sollte folgende Actions ausführen:** + +1. **Action 1: `ai.process` - Content extrahieren** + ```python + ai.process( + aiPrompt="Extract all content from the PDF, preserving page structure", + documentList=[pdf_document], + resultType="json" + ) + ``` + - Extrahiert Content aus PDF + - Erhält ContentParts mit Seiten-Informationen + +2. **Action 2: `ai.process` - Content analysieren und Dokumente trennen** + ```python + ai.process( + aiPrompt="""Analyze the extracted content and identify document boundaries. + Documents are separated by empty pages (page breaks). + For each document: + 1. Identify the document boundaries (start/end pages) + 2. Extract the company name + 3. Group content by document + + Return JSON structure with separate documents.""", + documentList=[extracted_content_from_action1], + resultType="json" + ) + ``` + - Analysiert ContentParts + - Identifiziert Trennseiten (leere Seiten) + - Gruppiert Content nach Dokumenten + - Extrahiert Firmen-Namen + +3. **Action 3: `ai.webResearch` - Adressen recherchieren** + ```python + ai.webResearch( + prompt="Find the address for company: {company_name}", + country="CH", # oder aus Context + language="de" + ) + ``` + - Für jede Firma: Web-Research für Adresse + - Kann in Loop ausgeführt werden + +4. **Action 4: `ai.process` - Dokumente generieren mit Adressen** + ```python + ai.process( + aiPrompt="""Generate separate documents for each company. + Include the company name, address (from web research), and all content. + Create one document per company.""", + documentList=[analyzed_documents_from_action2, addresses_from_action3], + resultType="docx" # oder pdf + ) + ``` + - ✅ **Multi-Dokument-Generierung:** JSON-Struktur unterstützt `documents` Array + - Generiert mehrere Dokumente im `documents` Array + - Integriert Adressen aus Web-Research + +5. **Action 5: `sharepoint.uploadDocument` - Dokumente hochladen** + ```python + sharepoint.uploadDocument( + connectionReference="Microsoft connection", + documentList=[generated_documents_from_action4], + pathQuery="/sites/SiteName/FolderPath" # Optional + ) + ``` + +#### Funktioniert dies im aktuellen Workflow? + +**✅ Was funktioniert:** +- ✅ Actions können sequenziell ausgeführt werden (Workflow-System) +- ✅ `ai.process` kann Content extrahieren +- ✅ `ai.process` kann Content analysieren +- ✅ `ai.webResearch` existiert bereits +- ✅ `sharepoint.uploadDocument` existiert bereits +- ✅ Multi-Dokument-Generierung ist unterstützt (`documents` Array im JSON) +- ✅ **Dynamic Mode:** Task Planning funktioniert (Actions werden iterativ geplant) +- ✅ **Dynamic Mode:** Ergebnisse zwischen Actions werden über `AVAILABLE_DOCUMENTS_INDEX` übergeben + +**❌ Was fehlt:** +- ❌ `renderReport` rendert nur das erste Dokument (muss erweitert werden) +- ⚠️ Task Planning könnte verbessert werden (funktioniert bereits, aber könnte optimiert werden) + +#### JSON-Struktur für Multi-Dokument-Generierung + +**Code-Stelle:** +```24:113:poweron/gateway/modules/datamodels/datamodelJson.py +jsonTemplateDocument: str = """{ + "metadata": {...}, + "documents": [ // ← Array unterstützt mehrere Dokumente! + { + "id": "doc_1", + "title": "...", + "filename": "...", + "sections": [...] + } + // ← Weitere Dokumente können hier hinzugefügt werden + ] +}""" +``` + +**✅ Multi-Dokument-Generierung ist bereits unterstützt!** +- JSON-Template enthält `documents` Array +- AI kann mehrere Dokumente generieren +- Renderer kann mehrere Dokumente rendern + +#### Was angepasst werden müsste + +**1. Task Planning im Dynamic Mode:** +- ✅ **BEREITS IMPLEMENTIERT:** Dynamic Mode generiert Actions iterativ (ein Action pro Step) +- ✅ **BEREITS IMPLEMENTIERT:** `_planSelect` wählt nächste Action basierend auf Context +- ✅ **BEREITS IMPLEMENTIERT:** `context.executedActions` speichert Action-History +- ✅ **BEREITS IMPLEMENTIERT:** `context.nextActionGuidance` kann nächste Action vorgeben +- ✅ **BEREITS IMPLEMENTIERT:** Dokumente werden über `AVAILABLE_DOCUMENTS_INDEX` verfügbar gemacht +- ✅ **BEREITS IMPLEMENTIERT:** `requiredInputDocuments` wird zu `documentList` konvertiert +- ✅ **BEREITS IMPLEMENTIERT:** Ergebnisse zwischen Actions werden über `AVAILABLE_DOCUMENTS_INDEX` übergeben + +**Code-Stellen:** +- Action Planning: `modeDynamic.py` Zeile 260-360 (`_planSelect`) +- Action Execution: `modeDynamic.py` Zeile 435-640 (`_actExecute`) +- Document Index: `placeholderFactory.py` Zeile 425-431 (`extractAvailableDocumentsIndex`) +- Document Availability: `mainServiceChat.py` Zeile 762-829 (`getAvailableDocuments`) +- Action History: `modeDynamic.py` Zeile 138-149 (`context.executedActions`) + +**Wie es funktioniert:** + +**1. Action Planning (`_planSelect`):** +```260:360:poweron/gateway/modules/workflows/processing/modes/modeDynamic.py +async def _planSelect(self, context: TaskContext) -> Dict[str, Any]: + # Prüft context.nextActionGuidance (von vorheriger Refinement-Entscheidung) + # Oder verwendet AI um nächste Action zu wählen + # AVAILABLE_DOCUMENTS_INDEX enthält alle Dokumente aus vorherigen Actions + bundle = generateDynamicPlanSelectionPrompt(self.services, context, ...) + # AI kann requiredInputDocuments mit docItem: oder docList: Referenzen angeben +``` + +**2. Document Availability (`getAvailableDocuments`):** +```762:829:poweron/gateway/modules/services/serviceChat/mainServiceChat.py +def getAvailableDocuments(self, workflow) -> str: + # Sammelt ALLE Dokumente aus ALLEN Messages des Workflows + # Inkludiert Dokumente aus vorherigen Actions + # Formatiert als docItem:: oder docList: