wiki/appdoc/implementation_concept_ai_workflow_rev4.md
2025-12-25 00:09:38 +01:00

2012 lines
75 KiB
Markdown

# Überarbeitetes Implementierungskonzept: AI Workflow System
## Übersicht
Dieses Dokument definiert ein überarbeitetes, klares Implementierungskonzept für das AI Workflow System, das alle identifizierten Probleme adressiert und einen korrekten Metadatenfluss durch die gesamte Pipeline sicherstellt.
## Kernprinzipien
1. **Single Source of Truth**: Alle Dokument-Verarbeitungslogik ist im AI Service Center zentralisiert
2. **Klarer Metadatenfluss**: Jeder ContentPart trägt vollständige Metadaten über Herkunft, Format und Verwendungszweck
3. **Format-Klarheit**: ContentParts zeigen explizit ihr Format an (reference, object oder extracted text)
4. **Intent-getriebene Verarbeitung**: Dokument-Verarbeitung wird durch explizite Intent-Analyse gesteuert
5. **Einheitlicher AI-Call**: Alle AI-Actions verwenden denselben zugrundeliegenden AI-Call-Mechanismus, unterscheiden sich nur in Parametern
6. **Harmonisierte Debug-Logs**: Alle Debug-Datei-Aufrufe sind vereinheitlicht und konsistent
7. **Korrekte ChatLog-Hierarchie**: Alle ChatLog-Nachrichten haben korrekte Parent/Child-Referenzen
---
## Workflow-Phasen
### Phase 1+2: User Input, Intent-Analyse & Komplexitäts-Erkennung (Kombiniert)
**Location**: `workflowManager._sendFirstMessage()` + `workflowProcessor.detectComplexity()`
**Was passiert:**
1. User sendet Prompt mit Dokumenten
2. **Kombinierte AI-Analyse** in einem AI-Call:
- **Intent-Analyse**: Identifiziert User-Intentionen und extrahiert Context vom Prompt in weitere Dokumente
- **Komplexitäts-Erkennung**: Bestimmt Fast Track vs Regular Track
3. Resultat: Klare User-Intention + Dokumente + Komplexitäts-Bewertung
**Kombinierter AI-Call:**
```python
async def _analyzeUserInputAndComplexity(
self,
userPrompt: str,
documents: List[ChatDocument]
) -> Dict[str, Any]:
"""
Kombinierte Analyse: Intent + Komplexität in einem AI-Call.
"""
analysisPrompt = f"""
Analysiere die User-Anfrage und bestimme in einem Durchgang:
1. detectedLanguage: ISO 639-1 Sprachcode (z.B. de, en)
2. normalizedRequest: Vollständige, explizite Umformulierung der User-Anfrage
3. intent: Kurze Kern-Anfrage für High-Level-Routing
4. contextItems: Große Datenblöcke, die als separate Dokumente extrahiert werden sollen
5. complexity: "simple" | "moderate" | "complex"
6. needsWorkflowHistory: bool (ob Workflow-History benötigt wird)
7. fastTrack: bool (ob Fast Track möglich ist)
User-Anfrage: "{userPrompt}"
Dokumente: {len(documents)} Dokumente vorhanden
Antworte mit JSON:
{{
"detectedLanguage": "de",
"normalizedRequest": "...",
"intent": "...",
"contextItems": [...],
"complexity": "complex",
"needsWorkflowHistory": false,
"fastTrack": false
}}
"""
# Debug-Log
self.services.utils.writeDebugFile(analysisPrompt, "user_input_analysis_prompt")
# AI-Call
aiResponse = await self.services.ai.callAiContent(
prompt=analysisPrompt,
options=AiCallOptions(operationType=OperationTypeEnum.DATA_GENERATE),
outputFormat="json"
)
# Debug-Log
self.services.utils.writeDebugFile(aiResponse.content, "user_input_analysis_response")
# Parse Result
result = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
return result
```
**Vorteile der Kombination:**
- **Weniger AI-Calls**: Ein Call statt zwei
- **Konsistente Analyse**: Beide Analysen verwenden denselben Kontext
- **Bessere Performance**: Schneller, weniger Kosten
- **Klarere Struktur**: Ein Analyse-Schritt statt zwei
**Output:**
- `normalizedRequest`: User-Prompt ohne Context
- `detectedLanguage`: Sprachcode
- `contextItems`: Große Content-Blöcke als separate Dokumente
- `intent`: High-Level-Kern-Anfrage
- `complexity`: "simple" | "moderate" | "complex"
- `needsWorkflowHistory`: bool
- `fastTrack`: bool
**Dokumente nach Phase 1+2:**
- Original User-uploaded Dokumente
- Context-extrahierten Dokumente (aus Prompt-Analyse)
- Alle Dokumente haben: `fileId`, `fileName`, `mimeType`, `fileSize`
**Code-Referenz:**
```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
```
---
### Phase 3: Task Planning
**Location**: `workflowProcessor.generateTaskPlan()`
**Was passiert:**
- Trennt verschiedene User-Intentionen in einzelne Tasks
- Jeder Task repräsentiert ein distinctes Ziel
- Tasks sind unabhängig, können aber auf Ergebnisse anderer Tasks referenzieren
**Task-Struktur:**
```python
Task {
id: str
objective: str # Was soll erreicht werden
requiredDocuments: List[DocumentReference] # Welche Dokumente werden benötigt
expectedOutput: str # Erwartetes Output-Format/Typ
}
```
**Code-Referenz:**
```42:103:poweron/gateway/modules/workflows/processing/workflowProcessor.py
async def generateTaskPlan(self, userInput: str, workflow: ChatWorkflow) -> TaskPlan:
```
---
### Phase 4: Task Execution (Iterative Action Execution)
**Location**: `modeDynamic.executeTask()`
**Was passiert:**
- Identifiziert und führt iterativ Actions aus
- Jede Action erhält:
- Task-Objective
- Verfügbare Dokumente (von vorherigen Actions)
- **Context von vergangenen Rounds und Tasks**:
- `context.executedActions`: Liste aller bereits ausgeführten Actions
- `context.workflowHistory`: History aus vorherigen Rounds
- `context.taskHistory`: History aus vorherigen Tasks
- `AVAILABLE_DOCUMENTS_INDEX`: Alle verfügbaren Dokumente aus vorherigen Actions
- `context.nextActionGuidance`: Vorgabe für nächste Action (von Refinement-Entscheidung)
**Action Types:**
- `ai.*` Actions: Alle verwenden unified AI Service Center
- `context.*` Actions: Dokument-Extraktion/Management
- `jira.*`, `sharepoint.*`, etc.: Externe Service-Actions
**Action Planner & Refinement Planner:**
- **Action Planner** (`_planSelect`): Wählt nächste Action basierend auf:
- Task-Objective
- Verfügbare Dokumente (`AVAILABLE_DOCUMENTS_INDEX`)
- Action-History (`context.executedActions`)
- Workflow-History (`context.workflowHistory`)
- Task-History (`context.taskHistory`)
- `context.nextActionGuidance` (falls vorhanden)
- **Refinement Planner**: Entscheidet über nächste Action nach Content-Validation:
- Nutzt Validation-Ergebnisse
- Kann `context.nextActionGuidance` setzen
- Berücksichtigt Action-History für adaptive Entscheidungen
**Diese Phase bleibt unverändert** - sie funktioniert korrekt.
**Code-Referenz:**
```435:640:poweron/gateway/modules/workflows/processing/modes/modeDynamic.py
async def _actExecute(self, context, selection, taskStep, workflow, step):
```
**Context-Struktur:**
```python
TaskContext {
workflowId: str
executedActions: List[Dict] # [{action: "...", parameters: {...}, step: 1}, ...]
workflowHistory: List[Dict] # History aus vorherigen Rounds
taskHistory: List[Dict] # History aus vorherigen Tasks
nextActionGuidance: Optional[Dict] # Vorgabe für nächste Action
# ... weitere Context-Felder
}
```
---
### Phase 5: AI Action Execution (Zentrale Verarbeitung)
**Location**: `serviceAi.mainServiceAi.callAiContent()`
**Dies ist der Kern des überarbeiteten Konzepts.** Alle AI-Actions (`ai.process`, `ai.generateDocument`, `ai.summarizeDocument`, etc.) routen durch diese einzige Funktion.
**Kernprinzip**: AI-Actions unterscheiden sich nur in **Parametern**, nicht in **Logik**. Die Dokument-Erstellungslogik ist hier zentralisiert.
---
## Phase 5 Detailliert: AI Service Center Verarbeitung
### 5A: Dokument-Intent-Klärung
**Was passiert:**
- Für jedes Dokument wird dessen Verwendungszweck bestimmt:
- **Extract**: Content-Extraktion benötigt (Text, Struktur, etc.)
- **Reference**: Dokument-Referenz/Attachment (keine Extraktion)
- **Render**: Image/Binary soll als-is gerendert werden
**Input:**
- `documentList`: Liste der zu verarbeitenden Dokumente
- `userPrompt`: User-Anfrage
- `actionParameters`: Action-spezifische Parameter (z.B. `resultType`, `outputFormat`)
**Process:**
```python
async def _clarifyDocumentIntents(
self,
documents: List[ChatDocument],
userPrompt: str,
actionParameters: Dict[str, Any],
parentOperationId: str # Für ChatLog-Hierarchie
) -> List[DocumentIntent]:
"""
Analysiert, welche Dokumente Extraktion vs Referenz benötigen.
Gibt DocumentIntent für jedes Dokument zurück.
"""
# Erstelle Operation-ID für Intent-Analyse
intentOperationId = f"{parentOperationId}_intent_analysis"
# Starte ChatLog mit Parent-Referenz
self.services.chat.progressLogStart(
intentOperationId,
"Document Intent Analysis",
"Intent Analysis",
f"Analyzing {len(documents)} documents",
parentOperationId=parentOperationId # Parent-Referenz!
)
try:
# AI-basierte Analyse des User-Prompts + Dokumente
intentPrompt = self._buildIntentAnalysisPrompt(userPrompt, documents, actionParameters)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(intentPrompt, "document_intent_analysis_prompt")
# AI-Call
aiResponse = await self.services.ai.callAiContent(
prompt=intentPrompt,
options=AiCallOptions(operationType=OperationTypeEnum.DATA_GENERATE),
outputFormat="json",
parentOperationId=intentOperationId # Parent-Referenz für AI-Call-Logs
)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(aiResponse.content, "document_intent_analysis_response")
# Parse Result
intentsData = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
documentIntents = [DocumentIntent(**intent) for intent in intentsData.get("intents", [])]
# ChatLog abschließen
self.services.chat.progressLogFinish(intentOperationId, True)
return documentIntents
except Exception as e:
self.services.chat.progressLogFinish(intentOperationId, False)
raise
```
**Output:**
```python
[
DocumentIntent(
documentId="doc_1",
intents=["extract"],
extractionPrompt="Extract all text content, preserving structure",
reasoning="User needs text content for document generation"
),
DocumentIntent(
documentId="doc_2",
intents=["render"],
extractionPrompt=None,
reasoning="Image should be rendered as visual element"
),
DocumentIntent(
documentId="doc_3",
intents=["reference"],
extractionPrompt=None,
reasoning="Document is only used as reference, no extraction needed"
)
]
```
**⚠️ WICHTIG: Unterschied zum alten Purpose-System**
Das alte System in `subDocumentPurposeAnalyzer.py` verwendet viele spezifische "purpose"-Tags (`extract_text_content`, `include_image`, `analyze_image_vision`, etc.) und muss durch dieses einfache DocumentIntent-System ersetzt werden.
| Alt (subDocumentPurposeAnalyzer) | Neu (DocumentIntent) |
|-----------------------------------|----------------------|
| `purpose: "extract_text_content"` | `intents: ["extract"]` |
| `purpose: "include_image"` | `intents: ["render"]` |
| `purpose: "analyze_image_vision"` | `intents: ["extract"]` mit `extractionMethod="vision"` |
| `purpose: "use_as_reference"` | `intents: ["reference"]` |
| **Ein Purpose pro Dokument** | **Mehrere Intents pro Dokument möglich!** |
| Viele spezifische Tags | Nur 3 Basis-Intents: extract, render, reference |
**Intent-Analyse-Prompt-Format:**
```
USER REQUEST:
{userPrompt}
DOCUMENTS TO ANALYZE:
{documentList}
TASK: For each document, determine its intents (can be multiple):
- "extract": Content extraction needed (text, structure, OCR, etc.)
- "render": Image/binary should be rendered as-is (visual element)
- "reference": Document reference/attachment (no extraction, just reference)
RETURN JSON:
{
"intents": [
{
"documentId": "doc_1",
"intents": ["extract"], # Array - can contain multiple!
"extractionPrompt": "Extract all text content, preserving structure",
"reasoning": "User needs text content for document generation"
},
{
"documentId": "doc_2",
"intents": ["extract", "render"], # Both! Image needs text extraction AND visual rendering
"extractionPrompt": "Extract text content from image using vision AI",
"reasoning": "Image contains text that needs extraction, but also should be rendered visually"
},
{
"documentId": "doc_3",
"intents": ["reference"],
"extractionPrompt": null,
"reasoning": "Document is only used as reference, no extraction needed"
}
]
}
```
**Debug-Logging (harmonisiert):**
```python
# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(
json.dumps([intent.dict() for intent in documentIntents], indent=2),
"document_intent_analysis_result"
)
```
---
### 5B: Content-Extraktion & Vorbereitung
**Was passiert:**
- Extrahiert Content von Dokumenten, die Extraktion benötigen
- Bereitet ContentParts mit vollständigen Metadaten vor
- Behandelt drei Content-Formate explizit
**Content Part Formate:**
Jeder `ContentPart` kann in einem von drei Formaten sein:
1. **Document Reference** (`contentFormat="reference"`):
```python
ContentPart(
id="part_ref_1",
typeGroup="reference",
mimeType="application/pdf",
data="", # Leer - nur Referenz
metadata={
"contentFormat": "reference",
"documentId": "doc_1",
"documentReference": "docItem:doc_1:document.pdf",
"intent": "reference", # Wo dieser Content benötigt wird
"usageHint": "Include as attachment in section X"
}
)
```
2. **Object/Binary** (`contentFormat="object"`):
```python
ContentPart(
id="part_obj_1",
typeGroup="image",
mimeType="image/png",
data="base64encodeddata...", # Base64-kodiertes Binary
metadata={
"contentFormat": "object",
"documentId": "doc_2",
"intent": "render", # Wo dieser Content benötigt wird
"usageHint": "Render as image in section Y",
"originalFileName": "logo.png"
}
)
```
3. **Extracted Text** (`contentFormat="extracted"`):
```python
ContentPart(
id="part_ext_1",
typeGroup="text",
mimeType="text/plain",
data="Extracted text content...", # Extrahierter Text
metadata={
"contentFormat": "extracted",
"documentId": "doc_1",
"extractionPrompt": "Extract all text content",
"intent": "extract", # Wo dieser Content benötigt wird
"usageHint": "Use in paragraph sections",
"extractionMethod": "vision" # oder "text", "ocr", etc.
}
)
```
**Process:**
```python
async def _extractAndPrepareContent(
self,
documents: List[ChatDocument],
documentIntents: List[DocumentIntent],
parentOperationId: str # Für ChatLog-Hierarchie
) -> List[ContentPart]:
"""
Extrahiert Content basierend auf Intents und bereitet ContentParts mit Metadaten vor.
Gibt Liste von ContentParts im passenden Format zurück.
WICHTIG: Ein Dokument kann mehrere ContentParts erzeugen, wenn mehrere Intents vorhanden sind.
Beispiel: Bild mit intents=["extract", "render"] erzeugt:
- ContentPart(contentFormat="object", ...) für Rendering
- ContentPart(contentFormat="extracted", ...) für Text-Analyse
"""
# Erstelle Operation-ID für Extraktion
extractionOperationId = f"{parentOperationId}_content_extraction"
# Starte ChatLog mit Parent-Referenz
self.services.chat.progressLogStart(
extractionOperationId,
"Content Extraction",
"Extraction",
f"Extracting from {len(documents)} documents",
parentOperationId=parentOperationId # Parent-Referenz!
)
try:
allContentParts = []
for document in documents:
intent = getIntentForDocument(document.id, documentIntents)
# WICHTIG: Prüfe alle Intents - ein Dokument kann mehrere ContentParts erzeugen
if "reference" in intent.intents:
# Erstelle Reference ContentPart
contentPart = ContentPart(
id=f"ref_{document.id}",
typeGroup="reference",
mimeType=document.mimeType,
data="",
metadata={
"contentFormat": "reference",
"documentId": document.id,
"documentReference": f"docItem:{document.id}:{document.fileName}",
"intent": "reference",
"usageHint": f"Reference document: {document.fileName}"
}
)
allContentParts.append(contentPart)
# WICHTIG: "render" und "extract" können beide vorhanden sein!
# In diesem Fall erzeugen wir BEIDE ContentParts
if "render" in intent.intents:
# Für Images/Binary: extrahiere als Object
if document.mimeType.startswith("image/") or isBinary(document.mimeType):
# Lade Binary-Daten
binaryData = await self.services.interfaceDbComponent.getFileData(document.fileId)
base64Data = base64.b64encode(binaryData).decode('utf-8')
contentPart = ContentPart(
id=f"obj_{document.id}",
typeGroup="image" if document.mimeType.startswith("image/") else "binary",
mimeType=document.mimeType,
data=base64Data,
metadata={
"contentFormat": "object",
"documentId": document.id,
"intent": "render",
"usageHint": f"Render as visual element: {document.fileName}",
"originalFileName": document.fileName,
# Verknüpfung zu extracted Part (falls vorhanden)
"relatedExtractedPartId": f"ext_{document.id}" if "extract" in intent.intents else None
}
)
allContentParts.append(contentPart)
if "extract" in intent.intents:
# Extrahiere Content mit Extraction Service
extractionPrompt = intent.extractionPrompt or "Extract all content from the document"
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(
extractionPrompt,
f"content_extraction_prompt_{document.id}"
)
# Führe Extraktion aus
extractedResults = await self.services.extraction.extractContent(
[document],
ExtractionOptions(
prompt=extractionPrompt,
mergeStrategy=MergeStrategy(...)
),
operationId=extractionOperationId, # Für ChatLog-Hierarchie
parentOperationId=extractionOperationId # Parent-Referenz!
)
# Konvertiere extrahierte Ergebnisse zu ContentParts mit Metadaten
for extracted in extractedResults:
for part in extracted.parts:
# Markiere als extracted Format
part.metadata.update({
"contentFormat": "extracted",
"documentId": document.id,
"extractionPrompt": extractionPrompt,
"intent": "extract",
"usageHint": f"Use extracted content from {document.fileName}",
# Verknüpfung zu object Part (falls vorhanden)
"relatedObjectPartId": f"obj_{document.id}" if "render" in intent.intents else None
})
# Stelle sicher, dass ID eindeutig ist (falls object Part existiert)
if "render" in intent.intents:
part.id = f"ext_{document.id}_{part.id}"
allContentParts.append(part)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(
json.dumps([part.dict() for part in allContentParts], indent=2),
"content_extraction_result"
)
# ChatLog abschließen
self.services.chat.progressLogFinish(extractionOperationId, True)
return allContentParts
except Exception as e:
self.services.chat.progressLogFinish(extractionOperationId, False)
raise
```
**Wichtige Erweiterung: Mehrfache ContentParts pro Dokument****GRUNDLEGENDES DESIGN**
**Dies ist das grundlegende Design!** Wenn ein Dokument mehrere Intents hat (z.B. `["extract", "render"]`), werden **mehrere ContentParts** erzeugt:
**Beispiel: Bild mit `intents=["extract", "render"]`**
```python
# ContentPart 1: Object für Rendering
ContentPart(
id="obj_img_1",
contentFormat="object",
data="base64...",
metadata={
"documentId": "img_1",
"intent": "render",
"relatedExtractedPartId": "ext_img_1" # Verknüpfung
}
)
# ContentPart 2: Extracted für Text-Analyse
ContentPart(
id="ext_img_1",
contentFormat="extracted",
data="This image shows...",
metadata={
"documentId": "img_1",
"intent": "extract",
"extractionMethod": "vision",
"relatedObjectPartId": "obj_img_1" # Verknüpfung
}
)
```
**Verknüpfung zwischen ContentParts:**
- `relatedExtractedPartId`: Object-Part verweist auf extracted Part
- `relatedObjectPartId`: Extracted Part verweist auf object Part
- Ermöglicht Generation-Prompt, beide Formate zu verwenden
**Spezielle Behandlung: Bereits extrahierter Content**
Wenn ein Dokument bereits extrahierten Content enthält (von vorheriger Action):
```python
# Prüfe Metadaten auf skipExtraction Flag
if part.metadata.get("skipExtraction", False):
# Content ist bereits extrahiert - verwende as-is
# Stelle nur sicher, dass Metadaten vollständig sind
part.metadata.update({
"contentFormat": "extracted",
"sourceAction": part.metadata.get("sourceAction", "unknown"),
"isPreExtracted": True
})
# Verwende direkt ohne Re-Extraktion
```
**Debug-Logging (harmonisiert):**
```python
# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(extractionPrompt, "content_extraction_prompt")
self.services.utils.writeDebugFile(
json.dumps([part.dict() for part in allContentParts], indent=2),
"content_extraction_result"
)
```
---
### 5C: Generierungs-Struktur-Definition
**Was passiert:**
- Definiert die Struktur des Result-JSON
- Spezifiziert, welcher Content in welche Sections geht
- Definiert Format für jede Section
- **Unterstützt strukturierte Datenextraktion** (z.B. CSV mit spezifischen Spalten)
**Input:**
- `userPrompt`: User-Anfrage
- `contentParts`: Alle vorbereiteten ContentParts mit Metadaten
- `outputFormat`: Ziel-Format (html, docx, pdf, etc.)
**Process:**
```python
async def _generateStructure(
self,
userPrompt: str,
contentParts: List[ContentPart],
outputFormat: str,
parentOperationId: str # Für ChatLog-Hierarchie
) -> Dict[str, Any]:
"""
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
"""
# 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 # Parent-Referenz!
)
try:
# Baue Struktur-Prompt mit Content-Index
structurePrompt = self._buildStructurePrompt(
userPrompt=userPrompt,
contentParts=contentParts, # Inkludiere Metadaten für jeden Part
outputFormat=outputFormat
)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(structurePrompt, "document_generation_structure_prompt")
# AI-Call für Struktur-Generierung
aiResponse = await self.services.ai.callAiContent(
prompt=structurePrompt,
options=AiCallOptions(
operationType=OperationTypeEnum.DATA_GENERATE,
resultFormat="json"
),
outputFormat="json",
parentOperationId=structureOperationId # Parent-Referenz für AI-Call-Logs
)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(aiResponse.content, "document_generation_structure_response")
# Parse Struktur (keine Validierung - klarer Code ohne zusätzliche Komplexität)
structure = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
# ChatLog abschließen
self.services.chat.progressLogFinish(structureOperationId, True)
return structure
except Exception as e:
self.services.chat.progressLogFinish(structureOperationId, False)
raise
```
**Struktur-Prompt-Format:**
**Standard-Format (für Dokument-Generierung):**
```
USER REQUEST:
{userPrompt}
AVAILABLE CONTENT PARTS:
{contentPartsIndex}
Für jeden ContentPart:
- id: {part.id}
- format: {part.metadata.contentFormat} # reference, object, oder extracted
- type: {part.typeGroup}
- source: {part.metadata.documentId}
- usage hint: {part.metadata.usageHint}
- data preview: {preview von part.data wenn extracted, oder "reference" oder "base64 object"}
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]
- contentFormat: Wie jeder ContentPart zu verwenden ist (reference, object, extracted)
- generation_hint: Was AI für diese Section generieren soll
- elements: [] (leer, wird in nächster Phase gefüllt)
```
**Strukturierte Datenextraktion (z.B. CSV):**
```
USER REQUEST:
{userPrompt}
AVAILABLE CONTENT PARTS:
{contentPartsIndex}
ERFORDERLICHE FELDER/SPALTEN:
{requiredFields} # z.B. ["date", "shop", "CHF", "VAT", "Description"]
TASK: Generiere Struktur für strukturierte Datenextraktion.
Die Struktur muss folgende Felder enthalten:
{requiredFields}
Für jedes Feld, spezifiziere:
- field_name: Name des Feldes
- contentPartIds: [Liste von ContentPart-IDs, die Daten für dieses Feld enthalten]
- extraction_hint: Wie Daten aus ContentParts extrahiert werden sollen
- data_type: Erwarteter Datentyp (string, number, date, etc.)
Die Struktur sollte ein Array von Datensätzen sein, wobei jeder Datensatz alle Felder enthält.
**WICHTIG**: Phase 5C unterstützt explizit strukturierte Datenextraktion für Formate wie CSV, JSON, XLSX.
```
**Beispiel für strukturierte Datenextraktion (Expenses CSV):**
```json
{
"metadata": {
"title": "Expenses Overview",
"outputFormat": "csv",
"fields": ["date", "shop", "CHF", "VAT", "Description"]
},
"documents": [{
"id": "expenses_csv",
"title": "Expenses CSV",
"filename": "expenses.csv",
"sections": [
{
"id": "data_extraction",
"content_type": "table",
"generation_hint": "Extract structured expense data from all content parts",
"contentPartIds": ["ext_pdf_1", "ext_pdf_2", ..., "ext_pdf_10"],
"contentFormats": {
"ext_pdf_1": "extracted",
"ext_pdf_2": "extracted",
...
},
"extractionFields": {
"date": {
"contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
"extractionHint": "Extract date from receipt/invoice",
"dataType": "date"
},
"shop": {
"contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
"extractionHint": "Extract shop/vendor name",
"dataType": "string"
},
"CHF": {
"contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
"extractionHint": "Extract amount in CHF",
"dataType": "number"
},
"VAT": {
"contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
"extractionHint": "Extract VAT amount",
"dataType": "number"
},
"Description": {
"contentPartIds": ["ext_pdf_1", "ext_pdf_2", ...],
"extractionHint": "Extract item description",
"dataType": "string"
}
},
"elements": []
}
]
}]
}
```
**Output-Struktur:**
```json
{
"metadata": {
"title": "Document Title",
"language": "de"
},
"documents": [{
"id": "doc_1",
"title": "Document Title",
"filename": "document.html",
"sections": [
{
"id": "section_1",
"content_type": "heading",
"generation_hint": "Main title",
"contentPartIds": [],
"elements": []
},
{
"id": "section_2",
"content_type": "paragraph",
"generation_hint": "Introduction paragraph",
"contentPartIds": ["part_ext_1"],
"contentFormats": {
"part_ext_1": "extracted" # Verwende extrahierten Text
},
"elements": []
},
{
"id": "section_3",
"content_type": "image",
"generation_hint": "Logo image",
"contentPartIds": ["part_obj_1"],
"contentFormats": {
"part_obj_1": "object" # Verwende base64 Object
},
"elements": []
},
{
"id": "section_4",
"content_type": "paragraph",
"generation_hint": "Reference to attachment",
"contentPartIds": ["part_ref_1"],
"contentFormats": {
"part_ref_1": "reference" # Verwende Dokument-Referenz
},
"elements": []
}
]
}]
}
```
---
### 5D: Struktur-Abfüllen
**Was passiert:**
- Füllt jede Section mit tatsächlichem Content
- Verwendet passendes Format basierend auf `contentFormat` Metadaten
- Generiert AI-Content wo nötig
**Process:**
```python
async def _fillStructure(
self,
structure: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
parentOperationId: str # Für ChatLog-Hierarchie
) -> Dict[str, Any]:
"""
Füllt Struktur mit tatsächlichem Content.
Für jede Section:
- Wenn contentPartIds spezifiziert: Verwende ContentParts im spezifizierten Format
- Wenn generation_hint spezifiziert: Generiere AI-Content
**Implementierungsdetails:**
- Sections werden **parallel generiert**, wenn möglich (Performance-Optimierung)
- Fehlerhafte Sections werden mit Fehlermeldung gerendert (kein Abbruch des gesamten Prozesses)
"""
# Erstelle Operation-ID für Struktur-Abfüllen
fillOperationId = f"{parentOperationId}_structure_filling"
# Starte ChatLog mit Parent-Referenz
self.services.chat.progressLogStart(
fillOperationId,
"Structure Filling",
"Filling",
f"Filling {len(structure.get('documents', [{}])[0].get('sections', []))} sections",
parentOperationId=parentOperationId # Parent-Referenz!
)
try:
filledStructure = copy.deepcopy(structure)
# Sammle alle Sections für parallele Verarbeitung
sections_to_process = []
for doc in filledStructure.get("documents", []):
for section in doc.get("sections", []):
sections_to_process.append((doc, section))
# Parallele Section-Generierung
import asyncio
section_tasks = []
for doc, section in sections_to_process:
section_tasks.append(self._fillSection(doc, section, contentParts, userPrompt, fillOperationId))
# Führe alle Sections parallel aus
section_results = await asyncio.gather(*section_tasks, return_exceptions=True)
# Verarbeite Ergebnisse (inkl. Fehlerbehandlung)
for (doc, section), result in zip(sections_to_process, section_results):
if isinstance(result, Exception):
# Fehlerhafte Section mit Fehlermeldung rendern
section["elements"] = [{
"type": "error",
"message": f"Error generating section {section.get('id')}: {str(result)}",
"sectionId": section.get("id")
}]
logger.error(f"Error filling section {section.get('id')}: {str(result)}")
else:
section["elements"] = result
# Alte sequenzielle Implementierung (ersetzt durch parallele Version oben):
# for doc in filledStructure.get("documents", []):
# for section in doc.get("sections", []):
sectionId = section.get("id")
contentPartIds = section.get("contentPartIds", [])
contentFormats = section.get("contentFormats", {})
generationHint = section.get("generation_hint")
elements = []
# Verarbeite ContentParts
for partId in contentPartIds:
part = findContentPartById(partId, contentParts)
if not part:
continue
contentFormat = contentFormats.get(partId, part.metadata.get("contentFormat"))
if contentFormat == "reference":
# Füge Dokument-Referenz hinzu
elements.append({
"type": "reference",
"documentReference": part.metadata.get("documentReference"),
"label": part.metadata.get("usageHint", part.label)
})
elif contentFormat == "object":
# Füge base64 Object hinzu
elements.append({
"type": part.typeGroup, # "image", "binary", etc.
"base64Data": part.data,
"mimeType": part.mimeType,
"altText": part.metadata.get("usageHint", part.label)
})
elif contentFormat == "extracted":
# Füge extrahierten Text hinzu (kann in AI-Generierungs-Prompt verwendet werden)
elements.append({
"type": "extracted_text",
"content": part.data,
"source": part.metadata.get("documentId"),
"extractionPrompt": part.metadata.get("extractionPrompt")
})
# Generiere AI-Content wenn nötig
if generationHint:
generationPrompt = self._buildSectionGenerationPrompt(
section=section,
contentParts=[findContentPartById(pid, contentParts) for pid in contentPartIds],
userPrompt=userPrompt,
generationHint=generationHint
)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(
generationPrompt,
f"section_generation_prompt_{sectionId}"
)
# Erstelle Operation-ID für Section-Generierung
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
# Starte ChatLog mit Parent-Referenz
self.services.chat.progressLogStart(
sectionOperationId,
"Section Generation",
"Section",
f"Generating section {sectionId}",
parentOperationId=fillOperationId # Parent-Referenz!
)
try:
# Generiere Content
aiResponse = await self.services.ai.callAiContent(
prompt=generationPrompt,
options=AiCallOptions(
operationType=OperationTypeEnum.DATA_GENERATE,
resultFormat="json"
),
outputFormat="json",
parentOperationId=sectionOperationId # Parent-Referenz für AI-Call-Logs
)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(
aiResponse.content,
f"section_generation_response_{sectionId}"
)
# Parse und füge zu elements hinzu
generatedElements = json.loads(
self.services.utils.jsonExtractString(aiResponse.content)
)
if isinstance(generatedElements, list):
elements.extend(generatedElements)
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
elements.extend(generatedElements["elements"])
# ChatLog abschließen
self.services.chat.progressLogFinish(sectionOperationId, True)
except Exception as e:
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
self.services.chat.progressLogFinish(sectionOperationId, False)
elements.append({
"type": "error",
"message": f"Error generating section {sectionId}: {str(e)}",
"sectionId": sectionId
})
logger.error(f"Error generating section {sectionId}: {str(e)}")
# NICHT raise - Section wird mit Fehlermeldung gerendert
section["elements"] = elements
# ChatLog abschließen
self.services.chat.progressLogFinish(fillOperationId, True)
return filledStructure
except Exception as e:
self.services.chat.progressLogFinish(fillOperationId, False)
raise
```
**Section-Generierungs-Prompt-Format:**
```
USER REQUEST:
{userPrompt}
SECTION TO GENERATE:
{generationHint}
AVAILABLE CONTENT FOR THIS SECTION:
{contentPartsForSection}
Für jeden ContentPart:
- Format: {contentFormat}
- Content: {part.data wenn extracted, oder Beschreibung wenn reference/object}
CRITICAL: Return ONLY a JSON object with an "elements" array.
Jedes Element sollte dem content_type der Section entsprechen.
```
---
### 5E: Rendering
**Was passiert:**
- Rendert die gefüllte JSON-Struktur zum Ziel-Format
- Verwendet Rendering-Engines (PDF, DOCX, HTML, etc.)
- **Multi-Dokument-Support**: Alle Dokumente im `documents` Array werden gerendert
**Process:**
```python
async def _renderResult(
self,
filledStructure: Dict[str, Any],
outputFormat: str,
title: str,
userPrompt: str,
parentOperationId: str # Für ChatLog-Hierarchie
) -> Tuple[bytes, str]:
"""
Rendert gefüllte Struktur zum Ziel-Format.
Unterstützt Multi-Dokument-Rendering: Alle Dokumente werden gerendert.
"""
# Erstelle Operation-ID für Rendering
renderOperationId = f"{parentOperationId}_rendering"
# Starte ChatLog mit Parent-Referenz
self.services.chat.progressLogStart(
renderOperationId,
"Content Rendering",
"Rendering",
f"Rendering to {outputFormat} format",
parentOperationId=parentOperationId # Parent-Referenz!
)
try:
from modules.services.serviceGeneration.mainServiceGeneration import GenerationService
generationService = GenerationService(self.services)
# Multi-Dokument-Rendering
documents = filledStructure.get("documents", [])
if len(documents) == 1:
# Einzelnes Dokument - wie bisher
renderedContent, mimeType, images = await generationService.renderReport(
filledStructure,
outputFormat,
title,
userPrompt,
self
)
else:
# Mehrere Dokumente - rendere alle
# Option: Alle Sections zusammenführen und als ein Dokument rendern
all_sections = []
for doc in documents:
if "sections" in doc:
all_sections.extend(doc.get("sections", []))
# Erstelle temporäres Dokument mit allen Sections
merged_document = {
"metadata": filledStructure["metadata"],
"documents": [{
"id": "merged",
"title": title,
"filename": f"{title}.{outputFormat}",
"sections": all_sections
}]
}
renderedContent, mimeType, images = await generationService.renderReport(
merged_document,
outputFormat,
title,
userPrompt,
self
)
# ChatLog abschließen
self.services.chat.progressLogFinish(renderOperationId, True)
return renderedContent, mimeType
except Exception as e:
self.services.chat.progressLogFinish(renderOperationId, False)
raise
```
**Code-Referenz:**
```322:352:poweron/gateway/modules/services/serviceGeneration/mainServiceGeneration.py
async def renderReport(self, extractedContent: Dict[str, Any], outputFormat: str, title: str, userPrompt: str = None, aiService=None) -> tuple[str, str, List[Dict[str, Any]]]:
```
---
## Überarbeitete callAiContent Funktion-Struktur
### Vereinfachte Hauptfunktion
```python
async def callAiContent(
self,
prompt: str,
options: AiCallOptions,
contentParts: Optional[List[ContentPart]] = None,
documentList: Optional[DocumentReferenceList] = None,
documentIntents: Optional[List[DocumentIntent]] = None,
outputFormat: Optional[str] = None,
title: Optional[str] = None,
parentOperationId: Optional[str] = None
) -> AiResponse:
"""
Einheitliche AI-Content-Verarbeitung - Single Entry Point für alle AI-Actions.
Alle AI-Actions (ai.process, ai.generateDocument, etc.) routen hier durch.
Sie unterscheiden sich nur in Parametern, nicht in Logik.
"""
await self.ensureAiObjectsInitialized()
# Erstelle Operation-ID
workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
aiOperationId = f"ai_content_{workflowId}_{int(time.time())}"
# Starte Progress-Tracking mit Parent-Referenz
self.services.chat.progressLogStart(
aiOperationId,
"AI content processing",
"Content Processing",
f"Format: {outputFormat or 'text'}",
parentOperationId=parentOperationId # Parent-Referenz!
)
try:
# Initialisiere Defaults
if not outputFormat:
outputFormat = "txt"
opType = getattr(options, "operationType", None)
if not opType:
options.operationType = OperationTypeEnum.DATA_GENERATE
opType = OperationTypeEnum.DATA_GENERATE
# Route zu Operation-spezifischen Handlern
if opType == OperationTypeEnum.IMAGE_GENERATE:
return await self._handleImageGeneration(prompt, options, title, aiOperationId)
if opType == OperationTypeEnum.WEB_SEARCH or opType == OperationTypeEnum.WEB_CRAWL:
return await self._handleWebOperation(prompt, options, opType, aiOperationId)
# Dokument-Generierungs-Pfad
options.compressPrompt = False
options.compressContext = False
# Schritt 5A: Kläre Dokument-Intents
documents = []
if documentList:
documents = self.services.chat.getChatDocumentsFromDocumentList(documentList)
if not documentIntents and documents:
documentIntents = await self._clarifyDocumentIntents(
documents,
prompt,
{"outputFormat": outputFormat},
aiOperationId # Parent-Referenz!
)
# Schritt 5B: Extrahiere und bereite Content vor
if documents:
preparedContentParts = await self._extractAndPrepareContent(
documents,
documentIntents or [],
aiOperationId # Parent-Referenz!
)
# WICHTIG: Kein Caching - Content wird immer neu verarbeitet für Konsistenz
# Merge mit bereitgestellten contentParts (falls vorhanden)
if contentParts:
# Prüfe auf pre-extracted Content
for part in contentParts:
if part.metadata.get("skipExtraction", False):
# Bereits extrahiert - verwende as-is, stelle sicher dass Metadaten vollständig
part.metadata.setdefault("contentFormat", "extracted")
part.metadata.setdefault("isPreExtracted", True)
preparedContentParts.extend(contentParts)
contentParts = preparedContentParts
# WICHTIG: Kein Caching - Content wird immer neu verarbeitet für Konsistenz
# Schritt 5C: Generiere Struktur
structure = await self._generateStructure(
prompt,
contentParts or [],
outputFormat,
aiOperationId # Parent-Referenz!
)
# Schritt 5D: Fülle Struktur
filledStructure = await self._fillStructure(
structure,
contentParts or [],
prompt,
aiOperationId # Parent-Referenz!
)
# Schritt 5E: Rendere Resultat
renderedContent, mimeType = await self._renderResult(
filledStructure,
outputFormat,
title or "Generated Document",
prompt,
aiOperationId # Parent-Referenz!
)
# Baue Response
documentName = self._determineDocumentName(filledStructure, outputFormat, title)
docData = DocumentData(
documentName=documentName,
documentData=renderedContent,
mimeType=mimeType,
sourceJson=filledStructure
)
metadata = AiResponseMetadata(
title=title or filledStructure.get("metadata", {}).get("title", "Generated Document"),
operationType=opType.value
)
# Debug-Log (harmonisiert)
self.services.utils.writeDebugFile(
json.dumps(filledStructure, indent=2, ensure_ascii=False),
"document_generation_response"
)
self.services.chat.progressLogFinish(aiOperationId, True)
return AiResponse(
content=json.dumps(filledStructure),
metadata=metadata,
documents=[docData]
)
except Exception as e:
logger.error(f"Error in callAiContent: {str(e)}")
self.services.chat.progressLogFinish(aiOperationId, False)
raise
```
---
## ContentPart Metadaten-Schema
### Erforderliche Metadaten-Felder
```python
ContentPart.metadata = {
# Format-Identifikation (ERFORDERLICH)
"contentFormat": Literal["reference", "object", "extracted"], # ERFORDERLICH
# Dokument-Referenz (ERFORDERLICH für alle)
"documentId": str, # Source-Dokument-ID
# Für reference Format
"documentReference": str, # z.B. "docItem:doc_1:file.pdf"
# Für object Format
"originalFileName": Optional[str], # Original-Dateiname
# Für extracted Format
"extractionPrompt": Optional[str], # Prompt verwendet für Extraktion
"extractionMethod": Optional[str], # "vision", "text", "ocr", etc.
# Verwendungs-Information
"intent": str, # "extract", "render", "reference"
"usageHint": str, # Wo/wie dieser Content verwendet werden soll
# Pre-Extraction Flag
"isPreExtracted": Optional[bool], # True wenn bereits von vorheriger Action extrahiert
"skipExtraction": Optional[bool], # True wenn Extraktion übersprungen werden soll
# Source-Tracking
"sourceAction": Optional[str], # Welche Action diesen Part erstellt hat
}
```
---
## Harmonisierung & Cleanup: Debug-Datei-Log-Aufrufe
### Aktuelle Situation
Debug-Datei-Log-Aufrufe sind über den Code verteilt mit unterschiedlichen Patterns:
- Manche mit Checks: `if hasattr(self.services, 'utils') and hasattr(self.services.utils, 'writeDebugFile')`
- Manche ohne Checks
- Unterschiedliche Dateinamen-Patterns
- Inkonsistente Verwendung
### Harmonisiertes Pattern
**Regel**: Die Funktion `self.services.utils.writeDebugFile()` ist **IMMER verfügbar** - keine Checks nötig!
**Standardisiertes Pattern:**
```python
# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(content, filename)
```
**Harmonisierte Dateinamen:**
- `user_input_analysis_prompt` / `user_input_analysis_response`
- `document_intent_analysis_prompt` / `document_intent_analysis_response` / `document_intent_analysis_result`
- `content_extraction_prompt_{documentId}` / `content_extraction_response` / `content_extraction_result`
- `document_generation_structure_prompt` / `document_generation_structure_response`
- `section_generation_prompt_{sectionId}` / `section_generation_response_{sectionId}`
- `document_generation_response` (finales JSON)
**Cleanup-Aufgaben:**
1. Entferne alle Checks: `if hasattr(...)` → Direkter Aufruf
2. Standardisiere alle Dateinamen nach obigem Pattern
3. Stelle sicher, dass alle AI-Prompts und Responses geloggt werden
4. Dokumentiere Dateinamen-Pattern im Code
**Beispiel-Cleanup:**
```python
# VORHER (inkonsistent):
if self.services and hasattr(self.services, 'utils') and hasattr(self.services.utils, 'writeDebugFile'):
try:
self.services.utils.writeDebugFile(structurePrompt, "document_generation_structure_prompt")
except Exception as e:
logger.debug(f"Could not write debug file: {e}")
# NACHHER (harmonisiert):
# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(structurePrompt, "document_generation_structure_prompt")
```
---
## Deep Analysis & Anpassung: ChatLog Parent/Child-Referenzen
### Aktuelle Situation
ChatLog-Nachrichten haben teilweise fehlende oder inkorrekte Parent/Child-Referenzen:
- Sub-Funktionen erstellen Logs ohne Parent-Referenz
- Parent-Operation-ID wird nicht durchgereicht
- Hierarchie geht verloren
### Korrekte Hierarchie-Struktur
**Prinzip**: Jeder Log in einer Sub-Funktion ist ein **Child** des Logs in der aufrufenden Parent-Funktion.
**Hierarchie-Beispiel:**
```
callAiContent (aiOperationId)
├── _clarifyDocumentIntents (intentOperationId, parent: aiOperationId)
│ └── AI-Call für Intent-Analyse (parent: intentOperationId)
├── _extractAndPrepareContent (extractionOperationId, parent: aiOperationId)
│ └── extractContent (parent: extractionOperationId)
│ └── Per-Dokument-Logs (parent: extractionOperationId)
├── _generateStructure (structureOperationId, parent: aiOperationId)
│ └── AI-Call für Struktur (parent: structureOperationId)
├── _fillStructure (fillOperationId, parent: aiOperationId)
│ └── Per-Section-Logs (sectionOperationId, parent: fillOperationId)
│ └── AI-Call für Section (parent: sectionOperationId)
└── _renderResult (renderOperationId, parent: aiOperationId)
└── renderReport (parent: renderOperationId)
```
### Implementierungs-Regeln
1. **Jede Funktion, die Logs erstellt, muss `parentOperationId` Parameter haben:**
```python
async def _clarifyDocumentIntents(
self,
...,
parentOperationId: str # ERFORDERLICH!
) -> List[DocumentIntent]:
```
2. **Jede Operation-ID muss eindeutig sein:**
```python
intentOperationId = f"{parentOperationId}_intent_analysis"
```
3. **Jeder `progressLogStart` muss `parentOperationId` verwenden:**
```python
self.services.chat.progressLogStart(
intentOperationId,
"Document Intent Analysis",
"Intent Analysis",
"...",
parentOperationId=parentOperationId # ERFORDERLICH!
)
```
4. **Jeder AI-Call muss `parentOperationId` weiterreichen:**
```python
aiResponse = await self.services.ai.callAiContent(
...,
parentOperationId=intentOperationId # Parent-Referenz!
)
```
5. **Jeder Service-Call muss `parentOperationId` weiterreichen:**
```python
extractedResults = await self.services.extraction.extractContent(
...,
operationId=extractionOperationId,
parentOperationId=extractionOperationId # Parent-Referenz!
)
```
### Deep Analysis Aufgaben
1. **Analysiere alle Funktionen, die Logs erstellen:**
- Identifiziere alle `progressLogStart` Aufrufe
- Prüfe, ob `parentOperationId` gesetzt ist
- Prüfe, ob `parentOperationId` durchgereicht wird
2. **Analysiere alle Sub-Funktionen:**
- Identifiziere alle Funktionen, die andere Funktionen aufrufen, die Logs erstellen
- Stelle sicher, dass `parentOperationId` durchgereicht wird
3. **Analysiere alle Service-Calls:**
- Identifiziere alle Calls zu anderen Services (extraction, generation, etc.)
- Stelle sicher, dass `parentOperationId` weitergegeben wird
4. **Validiere Hierarchie:**
- Stelle sicher, dass jede Log-Hierarchie korrekt ist
- Prüfe, dass keine Logs ohne Parent existieren (außer Root-Logs)
### Code-Beispiel: Korrekte Hierarchie
```python
# Root-Funktion
async def callAiContent(..., parentOperationId: Optional[str] = None):
aiOperationId = f"ai_content_{workflowId}_{int(time.time())}"
# Root-Log (kein Parent)
self.services.chat.progressLogStart(
aiOperationId,
"AI content processing",
"Content Processing",
"...",
parentOperationId=parentOperationId # Kann None sein für Root
)
# Sub-Funktion mit Parent-Referenz
documentIntents = await self._clarifyDocumentIntents(
...,
parentOperationId=aiOperationId # Parent-Referenz!
)
# Sub-Funktion
async def _clarifyDocumentIntents(..., parentOperationId: str):
intentOperationId = f"{parentOperationId}_intent_analysis"
# Child-Log mit Parent-Referenz
self.services.chat.progressLogStart(
intentOperationId,
"Document Intent Analysis",
"Intent Analysis",
"...",
parentOperationId=parentOperationId # Parent-Referenz!
)
# AI-Call mit Parent-Referenz
aiResponse = await self.services.ai.callAiContent(
...,
parentOperationId=intentOperationId # Parent-Referenz!
)
# Log abschließen
self.services.chat.progressLogFinish(intentOperationId, True)
```
---
## Image-Handling: Drei Formate
### Format 1: Document Reference
```python
ContentPart(
typeGroup="image",
mimeType="image/png",
data="", # Leer
metadata={
"contentFormat": "reference",
"documentReference": "docItem:img_1:logo.png",
"usageHint": "Include as attachment"
}
)
```
**Verwendung**: Referenz auf Image-Dokument, kein Datentransfer.
### Format 2: Object (Base64)
```python
ContentPart(
typeGroup="image",
mimeType="image/png",
data="iVBORw0KGgoAAAANS...", # Base64-kodiert
metadata={
"contentFormat": "object",
"usageHint": "Render as visual element"
}
)
```
**Verwendung**: Image-Daten eingebettet, bereit für Rendering.
### Format 3: Extracted Text
```python
ContentPart(
typeGroup="text",
mimeType="text/plain",
data="This image shows a red car...", # Extrahierte Beschreibung
metadata={
"contentFormat": "extracted",
"extractionPrompt": "Describe the image content",
"extractionMethod": "vision",
"originalImageId": "img_1" # Referenz auf Original
}
)
```
**Verwendung**: Text extrahiert aus Image, für Verwendung in Text-Sections.
**Wichtig**: In einem extrahierten ContentPart sollte klar sichtbar sein:
- **Image als Object (base64)**: `contentFormat="object"`, `typeGroup="image"`, `data` enthält base64
- **Image als Reference**: `contentFormat="reference"`, `metadata.documentReference` enthält Referenz
- **Image als Extracted Text**: `contentFormat="extracted"`, `typeGroup="text"`, `data` enthält Text, `metadata.originalImageId` enthält Referenz zum Original
---
## Migrations-Pfad
### Schritt 1: ContentPart-Modell erweitern
- Füge `contentFormat` zum Metadaten-Schema hinzu
- Stelle sicher, dass alle ContentParts Format spezifiziert haben
### Schritt 2: Document Intent Analysis überarbeiten ⚠️ **KRITISCH**
**Problem**: `subDocumentPurposeAnalyzer.py` verwendet ein veraltetes "purpose"-System mit vielen Tags (`extract_text_content`, `include_image`, `analyze_image_vision`, etc.), das nicht mit dem DocumentIntent-System aus diesem Konzept übereinstimmt.
**Lösung**: Ersetze das alte Purpose-System durch das DocumentIntent-System.
**Zu entfernen**:
- ❌ Altes Purpose-System: `extract_text_content`, `include_image`, `analyze_image_vision`, `use_as_template`, `use_as_reference`, `extract_data`, `attach`, `convert_format`, `translate`, `summarize`, `compare`, `merge`, `extract_tables_charts`, `use_for_styling`, `extract_metadata`
-`DocumentPurposeAnalyzer` Klasse mit `analyzeDocumentPurposes()` Methode
- ❌ Alle Purpose-Listen und Purpose-Tags im Code
**Zu implementieren**:
-`DocumentIntent` Modell verwenden (bereits definiert in `datamodelExtraction.py`)
-`_clarifyDocumentIntents()` Funktion in `mainServiceAi.py` (wie in Phase 5A beschrieben)
- ✅ Einfaches Intent-System: `intents=["extract", "render", "reference"]`
- ✅ Jedes Dokument kann mehrere Intents haben: `intents=["extract", "render"]` für Bilder, die sowohl analysiert als auch gerendert werden sollen
**Code-Änderungen**:
```python
# ALT (subDocumentPurposeAnalyzer.py):
purpose = "extract_text_content" # oder "include_image", "analyze_image_vision", etc.
# NEU (DocumentIntent):
documentIntent = DocumentIntent(
documentId="doc_1",
intents=["extract", "render"], # Einfach und klar!
extractionPrompt="Extract text content from image",
reasoning="Image needs both text extraction and visual rendering"
)
```
**Dateien zu überarbeiten**:
- `subDocumentPurposeAnalyzer.py`: Entfernen oder komplett umschreiben auf DocumentIntent-System
- Alle Stellen, die `analyzeDocumentPurposes()` aufrufen: Umstellen auf `_clarifyDocumentIntents()`
- Alle Stellen, die "purpose" verwenden: Umstellen auf `intents` aus DocumentIntent
### Schritt 3: callAiContent refactoren
- Implementiere 5A-5E Phasen
- Entferne duplizierte Logik aus Action-Methoden
- Zentralisiere alle Dokument-Verarbeitung
- Implementiere korrekte Parent/Child-Referenzen
- **Wichtig**: Verwende DocumentIntent-System aus Schritt 2, nicht das alte Purpose-System
### Schritt 4: Action-Methoden aktualisieren
- Entferne Dokument-Verarbeitungslogik
- Übergebe Parameter an callAiContent
- Behalte nur Parameter-Vorbereitung
### Schritt 5: Extraction-Service aktualisieren
- Stelle sicher, dass ContentParts vollständige Metadaten haben
- Markiere pre-extracted Content korrekt
- Implementiere korrekte Parent/Child-Referenzen
### Schritt 6: Generation-Service aktualisieren
- Behandle drei Content-Formate
- Verwende Metadaten für Format-Auswahl
- Implementiere Multi-Dokument-Rendering
- **Wichtig**: Entferne alle Referenzen zum alten Purpose-System
### Schritt 7: Debug-Logging harmonisieren
- Entferne alle Checks: `if hasattr(...)`
- Standardisiere alle Dateinamen
- Stelle sicher, dass alle AI-Prompts/Responses geloggt werden
### Schritt 8: ChatLog-Hierarchie korrigieren
- Analysiere alle Log-Erstellungen
- Füge `parentOperationId` Parameter hinzu wo nötig
- Stelle sicher, dass Parent-Referenzen korrekt durchgereicht werden
- Validiere Hierarchie
---
## Vorteile des überarbeiteten Konzepts
1. **Klarere Trennung**: Jede Phase hat distincte Verantwortung
2. **Metadatenfluss**: Vollständige Metadaten in jedem Schritt
3. **Format-Klarheit**: Explizite Format-Indikation
4. **Zentralisierte Logik**: Single Source of Truth
5. **Debuggability**: Alle Prompts/Responses geloggt (harmonisiert)
6. **Flexibilität**: Unterstützt alle Use Cases
7. **Wartbarkeit**: Klare Struktur, einfach zu erweitern
8. **Korrekte Hierarchie**: Alle ChatLogs haben korrekte Parent/Child-Referenzen
9. **Bessere Performance**: Phase 1+2 kombiniert = weniger AI-Calls
10. **Context-Aware**: Action Planner nutzt Context von vergangenen Rounds/Tasks
---
## Use-Case-Analyse: Drei konkrete Szenarien
### Use Case 1: Expenses CSV - Strukturierte Datenextraktion aus PDFs mit Bildern
**User Prompt:**
> "combine all expenses documents (each with image in pdf) into a csv file with the columns date;shop;CHF;VAT;Description"
**Input:** 10 PDF-Dateien (jede enthält Bilder mit Rechnungsdaten)
**Erwartetes Verhalten:**
**Phase 1+2: Intent-Analyse & Komplexität**
- **Intent**: Strukturierte Datenextraktion aus mehreren PDFs, Kombination in CSV
- **Komplexität**: "complex" (Multi-Dokument, strukturierte Extraktion, Aggregation)
- **Fast Track**: false (Dokument-Verarbeitung benötigt)
**Phase 3: Task Planning**
- **Task 1**: Extrahiere strukturierte Daten aus allen PDFs
- Objective: "Extract expense data (date, shop, CHF, VAT, description) from all PDF documents"
- RequiredDocuments: Alle 10 PDFs
- ExpectedOutput: "structured_data"
**Phase 4: Task Execution**
**Action 1: `context.extractContent`**
- Extrahiert alle PDFs zu ContentParts
- **Wichtig**: Bilder werden als `typeGroup="image"` mit `contentFormat="object"` (base64) erstellt
- **Aber**: Für Datenextraktion benötigen wir auch Text-Extraktion aus Bildern
**Action 2: `ai.process`** (mit spezifischem Prompt für strukturierte Datenextraktion)
- **Input**: Alle extrahierten ContentParts
- **Intent-Analyse (5A)**:
- PDFs: `intents=["extract"]` (Text-Extraktion)
- Bilder in PDFs: `intents=["extract"]` (OCR/Text-Extraktion für strukturierte Daten)
- **ExtractionPrompt für Bilder**: "Extract structured expense data: date, shop name, amount in CHF, VAT amount, description"
- **Content-Extraktion (5B)**:
- PDF-Text wird extrahiert: `contentFormat="extracted"`
- Bilder werden analysiert (OCR/Vision): `contentFormat="extracted"` mit `extractionMethod="vision"`
- **Wichtig**: Bilder haben beide Formate möglich:
- `contentFormat="object"` für visuelle Darstellung (falls benötigt)
- `contentFormat="extracted"` für Text-Extraktion (für CSV)
- **Struktur-Generierung (5C)**:
- Struktur definiert CSV-Format mit Spalten: date, shop, CHF, VAT, Description
- **Struktur-Abfüllen (5D)**:
- Extrahiert strukturierte Daten aus allen ContentParts
- Kombiniert Daten aus allen 10 PDFs
- **Rendering (5E)**:
- Rendert zu CSV-Format
**Kritische Punkte:**
-**Bilder müssen analysiert werden** (OCR/Vision) für strukturierte Datenextraktion
-**Multi-Dokument-Verarbeitung**: Alle 10 PDFs müssen verarbeitet werden
-**Strukturierte Datenextraktion**: Spezifische Felder müssen extrahiert werden
-**Kombination**: Daten aus allen PDFs müssen in einem CSV kombiniert werden
**Konzept-Abdeckung:**
- ✅ Phase 5A: Intent-Analyse erkennt "extract" für Bilder (OCR benötigt)
- ✅ Phase 5B: Bilder werden sowohl als "object" (base64) als auch als "extracted" (Text) behandelt
- ✅ Phase 5C: Struktur definiert CSV-Format
- ✅ Phase 5D: Extrahiert und kombiniert strukturierte Daten
- ✅ Phase 5E: Rendert zu CSV
**Mögliche Verbesserung:**
- Konzept sollte explizit erwähnen: **Bilder können beide Formate gleichzeitig haben** (object für Rendering, extracted für Text-Analyse)
---
### Use Case 2: PowerPoint mit Bildern - Bilder rendern UND analysieren
**User Prompt:**
> "make a powerpoint slideshow for the customer about the product with the images integrated in the slides. the images also give information about the storyline"
**Input:** 10 Bilder
**Erwartetes Verhalten:**
**Phase 1+2: Intent-Analyse & Komplexität**
- **Intent**: PowerPoint-Generierung mit Bildern, Storyline-Analyse
- **Komplexität**: "complex" (Multi-Bild, Generierung, Storyline-Analyse)
- **Fast Track**: false
**Phase 3: Task Planning**
- **Task 1**: Generiere PowerPoint mit integrierten Bildern und Storyline
- Objective: "Create PowerPoint presentation with images integrated in slides, analyze images for storyline information"
- RequiredDocuments: Alle 10 Bilder
- ExpectedOutput: "pptx"
**Phase 4: Task Execution**
**Action 1: `ai.generateDocument`** (oder `ai.process` mit `outputFormat="pptx"`)
- **Input**: Alle 10 Bilder
- **Intent-Analyse (5A)**:
- Bilder: `intents=["extract", "render"]` (beide!)
- **"render"**: Bilder müssen in Slides integriert werden
- **"extract"**: Bilder müssen analysiert werden für Storyline-Information
- **ExtractionPrompt**: "Analyze image content to extract storyline information, product features, and narrative elements"
- **Content-Extraktion (5B)**:
- **Für Rendering**: Bilder als `contentFormat="object"` (base64) für PowerPoint-Integration
- **Für Storyline**: Bilder analysiert als `contentFormat="extracted"` (Text-Beschreibung) für Storyline-Generierung
- **Wichtig**: Jedes Bild erzeugt **zwei ContentParts**:
- `ContentPart(id="img_1_obj", contentFormat="object", ...)` - für Rendering
- `ContentPart(id="img_1_ext", contentFormat="extracted", ...)` - für Storyline-Analyse
- **Struktur-Generierung (5C)**:
- Struktur definiert PowerPoint-Slides
- Jede Slide kann Bilder enthalten (object-Format)
- Storyline-Information aus extracted-Format wird für Slide-Content verwendet
- **Struktur-Abfüllen (5D)**:
- Bilder werden in Slides integriert (object-Format)
- Storyline-Text wird aus extracted-Format generiert
- AI generiert Slide-Content basierend auf Storyline
- **Rendering (5E)**:
- Rendert zu PPTX-Format mit integrierten Bildern
**Kritische Punkte:**
-**Bilder müssen beide Formate haben**: object (für Rendering) UND extracted (für Storyline)
-**Multi-Bild-Verarbeitung**: Alle 10 Bilder müssen verarbeitet werden
-**Storyline-Analyse**: Bilder müssen analysiert werden für narrative Elemente
-**PowerPoint-Generierung**: Strukturierte Slide-Generierung mit Bildern
**Konzept-Abdeckung:**
- ✅ Phase 5A: Intent-Analyse erkennt beide Intents: `["extract", "render"]`
- ✅ Phase 5B: Bilder werden in beiden Formaten erstellt (object + extracted)
- ✅ Phase 5C: Struktur definiert PowerPoint-Format mit Bild-Platzhaltern
- ✅ Phase 5D: Integriert Bilder (object) und verwendet Storyline (extracted)
- ✅ Phase 5E: Rendert zu PPTX
**Mögliche Verbesserung:**
- Konzept sollte explizit erwähnen: **Ein Dokument kann mehrere ContentParts erzeugen** (z.B. Bild als object UND als extracted)
---
### Use Case 3: SharePoint-Analyse - Keine angehängten Dokumente
**User Prompt:**
> "make me an overview about all sharepoint folders and the summary of documents stored in each folder for sharepoint https://xxx.yy.zz"
**Input:** Keine Dokumente angehängt
**Erwartetes Verhalten:**
**Phase 1+2: Intent-Analyse & Komplexität**
- **Intent**: SharePoint-Integration, Folder-Analyse, Dokument-Zusammenfassungen
- **Komplexität**: "complex" (Externe Integration, Multi-Step-Workflow)
- **Fast Track**: false
**Phase 3: Task Planning**
- **Task 1**: Analysiere SharePoint-Struktur
- Objective: "List all folders in SharePoint site and analyze document structure"
- RequiredDocuments: Keine (SharePoint-URL im Prompt)
- ExpectedOutput: "structured_overview"
- **Task 2**: Generiere Zusammenfassungen
- Objective: "Generate summaries for documents in each folder"
- RequiredDocuments: Von Task 1 (SharePoint-Dokumente)
- ExpectedOutput: "summary_document"
**Phase 4: Task Execution**
**Action 1: `sharepoint.listDocuments`**
- Listet alle Ordner im SharePoint
- Gibt Dokument-Referenzen zurück
- **Wichtig**: Keine ContentParts erstellt - nur Referenzen
**Action 2: `sharepoint.readDocuments`** (für jeden Ordner)
- Liest Dokumente aus SharePoint
- Erstellt ContentParts mit `contentFormat="reference"` (SharePoint-Referenzen)
- Oder lädt Dokumente herunter und erstellt ContentParts
**Action 3: `ai.process`** (für Zusammenfassungen)
- **Input**: SharePoint-Dokument-Referenzen oder heruntergeladene Dokumente
- **Intent-Analyse (5A)**:
- Dokumente: `intents=["extract"]` (Zusammenfassung benötigt Extraktion)
- **ExtractionPrompt**: "Extract key information and create summary"
- **Content-Extraktion (5B)**:
- Dokumente werden extrahiert: `contentFormat="extracted"`
- **Struktur-Generierung (5C)**:
- Struktur definiert Overview-Format mit Foldern und Zusammenfassungen
- **Struktur-Abfüllen (5D)**:
- Generiert Zusammenfassungen für jedes Dokument
- Gruppiert nach Ordnern
- **Rendering (5E)**:
- Rendert zu Overview-Dokument (HTML, DOCX, etc.)
**Kritische Punkte:**
-**Keine angehängten Dokumente**: Workflow startet ohne Dokumente
-**SharePoint-Integration**: Externe Service-Actions werden verwendet
-**Multi-Step-Workflow**: Mehrere Actions sequenziell
-**Dokument-Referenzen**: SharePoint-Dokumente werden als Referenzen behandelt
**Konzept-Abdeckung:**
- ✅ Phase 4: Action Planner kann externe Actions (sharepoint.*) verwenden
- ✅ Phase 5A: Intent-Analyse funktioniert auch für SharePoint-Dokumente
- ✅ Phase 5B: SharePoint-Dokumente können als "reference" oder "extracted" behandelt werden
- ✅ Phase 5C-5E: Standard-Generierungsprozess funktioniert
**Mögliche Verbesserung:**
- Konzept sollte explizit erwähnen: **Workflows können ohne angehängte Dokumente starten** (Dokumente kommen von externen Services)
---
## Konzept-Verbesserungen basierend auf Use-Case-Analyse
### Verbesserung 1: Mehrfache ContentParts pro Dokument
**Problem**: Ein Dokument (z.B. Bild) kann sowohl als "object" (für Rendering) als auch als "extracted" (für Text-Analyse) benötigt werden.
**Lösung**: Phase 5B sollte explizit unterstützen, dass ein Dokument **mehrere ContentParts** erzeugen kann:
```python
async def _extractAndPrepareContent(...):
"""
Ein Dokument kann mehrere ContentParts erzeugen, wenn mehrere Intents vorhanden sind.
Beispiel: Bild mit intents=["extract", "render"] erzeugt:
- ContentPart(contentFormat="object", ...) für Rendering
- ContentPart(contentFormat="extracted", ...) für Text-Analyse
"""
for document in documents:
intent = getIntentForDocument(document.id, documentIntents)
# Wenn mehrere Intents: Erzeuge mehrere ContentParts
if "render" in intent.intents and "extract" in intent.intents:
# Erzeuge object-ContentPart für Rendering
objectPart = ContentPart(
id=f"obj_{document.id}",
contentFormat="object",
...
)
allContentParts.append(objectPart)
# Erzeuge extracted-ContentPart für Text-Analyse
extractedPart = await self._extractTextFromDocument(document, intent.extractionPrompt)
extractedPart.id = f"ext_{document.id}"
extractedPart.metadata["contentFormat"] = "extracted"
extractedPart.metadata["originalDocumentId"] = document.id
extractedPart.metadata["relatedContentPartId"] = objectPart.id # Verknüpfung
allContentParts.append(extractedPart)
```
### Verbesserung 2: Strukturierte Datenextraktion
**Problem**: Use Case 1 benötigt strukturierte Datenextraktion (CSV mit spezifischen Spalten).
**Lösung**: Phase 5C sollte explizit strukturierte Datenextraktion unterstützen:
```python
async def _generateStructure(...):
"""
Für strukturierte Datenextraktion (z.B. CSV):
- Struktur definiert explizit die erwarteten Felder/Spalten
- ContentParts werden diesen Feldern zugeordnet
"""
# Prüfe ob strukturierte Extraktion benötigt wird
if "extract structured data" in userPrompt.lower() or outputFormat == "csv":
# Struktur definiert explizit Felder
structurePrompt = f"""
Extract structured data with fields: {fields}
Map content parts to these fields.
...
"""
```
### Verbesserung 3: Externe Service-Integration
**Problem**: Use Case 3 startet ohne Dokumente, Dokumente kommen von SharePoint.
**Lösung**: Phase 4 sollte explizit dokumentieren, dass externe Actions Dokumente bereitstellen können:
```python
# Action 1: sharepoint.listDocuments
# Erstellt Dokument-Referenzen, die in AVAILABLE_DOCUMENTS_INDEX verfügbar werden
# Action 2: sharepoint.readDocuments
# Lädt Dokumente herunter und erstellt ContentParts
# Diese werden dann in nachfolgenden Actions verwendet
```
---
## Offene Fragen & Entscheidungen
1.**Multi-Dokument-Rendering**: In einem Call - Resultat kann mehrere Dokumente sein
2.**Section-Generierung Parallelisierung**: **Parallel, wenn möglich** - Sections können parallel generiert werden, um Performance zu verbessern
3.**Error-Handling**: **Fehlerhafte Sections mit Fehlermeldung rendern** - Wenn eine Section fehlschlägt, wird eine Fehlermeldung in der gerenderten Ausgabe angezeigt, statt den gesamten Prozess zu stoppen
4.**Caching**: **Nein, kein Caching** - Extrahierter Content wird nicht gecacht, um Konsistenz und Klarheit zu gewährleisten
5.**Validierung**: **Keine Validierung** - Es ist kein Validierungsprozess definiert, und es gibt keinen definierten Prozess für den Fall, dass eine Validierung fehlschlagen würde. Klarer Code ohne zusätzliche Komplexität.
6.**Mehrfache ContentParts**: **Ja, natürlich!** - Das ist das grundlegende Design! Ein Dokument kann mehrere ContentParts erzeugen (z.B. `object` + `extracted` für Bilder, die sowohl gerendert als auch analysiert werden sollen)
7.**Strukturierte Datenextraktion**: **Ja, Phase 5C soll dies unterstützen** - Phase 5C (Struktur-Generierung) soll explizit strukturierte Datenextraktion unterstützen (z.B. CSV mit spezifischen Spalten)
---
## Nächste Schritte
1. Review und Approve dieses Konzepts
2. Implementiere ContentPart-Metadaten-Erweiterungen
3. Refactore callAiContent nach 5A-5E Struktur
4. Aktualisiere Action-Methoden für zentralisierte Logik
5. Harmonisiere Debug-Logging (entferne Checks, standardisiere Namen)
6. Korrigiere ChatLog-Hierarchie (füge Parent-Referenzen hinzu)
7. Implementiere Multi-Dokument-Rendering
8. Teste mit verschiedenen Szenarien
9. Dokumentiere API-Änderungen