2012 lines
75 KiB
Markdown
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
|