75 KiB
Ü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
- Single Source of Truth: Alle Dokument-Verarbeitungslogik ist im AI Service Center zentralisiert
- Klarer Metadatenfluss: Jeder ContentPart trägt vollständige Metadaten über Herkunft, Format und Verwendungszweck
- Format-Klarheit: ContentParts zeigen explizit ihr Format an (reference, object oder extracted text)
- Intent-getriebene Verarbeitung: Dokument-Verarbeitung wird durch explizite Intent-Analyse gesteuert
- Einheitlicher AI-Call: Alle AI-Actions verwenden denselben zugrundeliegenden AI-Call-Mechanismus, unterscheiden sich nur in Parametern
- Harmonisierte Debug-Logs: Alle Debug-Datei-Aufrufe sind vereinheitlicht und konsistent
- 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:
- User sendet Prompt mit Dokumenten
- 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
- Resultat: Klare User-Intention + Dokumente + Komplexitäts-Bewertung
Kombinierter AI-Call:
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 ContextdetectedLanguage: SprachcodecontextItems: Große Content-Blöcke als separate Dokumenteintent: High-Level-Kern-Anfragecomplexity: "simple" | "moderate" | "complex"needsWorkflowHistory: boolfastTrack: 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:
# 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:
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:
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 Actionscontext.workflowHistory: History aus vorherigen Roundscontext.taskHistory: History aus vorherigen TasksAVAILABLE_DOCUMENTS_INDEX: Alle verfügbaren Dokumente aus vorherigen Actionscontext.nextActionGuidance: Vorgabe für nächste Action (von Refinement-Entscheidung)
Action Types:
ai.*Actions: Alle verwenden unified AI Service Centercontext.*Actions: Dokument-Extraktion/Managementjira.*,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.nextActionGuidancesetzen - Berücksichtigt Action-History für adaptive Entscheidungen
Diese Phase bleibt unverändert - sie funktioniert korrekt.
Code-Referenz:
async def _actExecute(self, context, selection, taskStep, workflow, step):
Context-Struktur:
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 DokumenteuserPrompt: User-AnfrageactionParameters: Action-spezifische Parameter (z.B.resultType,outputFormat)
Process:
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:
[
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):
# 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:
-
Document Reference (
contentFormat="reference"):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" } ) -
Object/Binary (
contentFormat="object"):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" } ) -
Extracted Text (
contentFormat="extracted"):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:
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"]
# 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 PartrelatedObjectPartId: 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):
# 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):
# 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-AnfragecontentParts: Alle vorbereiteten ContentParts mit MetadatenoutputFormat: Ziel-Format (html, docx, pdf, etc.)
Process:
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):
{
"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:
{
"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
contentFormatMetadaten - Generiert AI-Content wo nötig
Process:
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
documentsArray werden gerendert
Process:
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:
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
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
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:
# Immer ohne Checks - Funktion ist IMMER verfügbar
self.services.utils.writeDebugFile(content, filename)
Harmonisierte Dateinamen:
user_input_analysis_prompt/user_input_analysis_responsedocument_intent_analysis_prompt/document_intent_analysis_response/document_intent_analysis_resultcontent_extraction_prompt_{documentId}/content_extraction_response/content_extraction_resultdocument_generation_structure_prompt/document_generation_structure_responsesection_generation_prompt_{sectionId}/section_generation_response_{sectionId}document_generation_response(finales JSON)
Cleanup-Aufgaben:
- Entferne alle Checks:
if hasattr(...)→ Direkter Aufruf - Standardisiere alle Dateinamen nach obigem Pattern
- Stelle sicher, dass alle AI-Prompts und Responses geloggt werden
- Dokumentiere Dateinamen-Pattern im Code
Beispiel-Cleanup:
# 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
-
Jede Funktion, die Logs erstellt, muss
parentOperationIdParameter haben:async def _clarifyDocumentIntents( self, ..., parentOperationId: str # ERFORDERLICH! ) -> List[DocumentIntent]: -
Jede Operation-ID muss eindeutig sein:
intentOperationId = f"{parentOperationId}_intent_analysis" -
Jeder
progressLogStartmussparentOperationIdverwenden:self.services.chat.progressLogStart( intentOperationId, "Document Intent Analysis", "Intent Analysis", "...", parentOperationId=parentOperationId # ERFORDERLICH! ) -
Jeder AI-Call muss
parentOperationIdweiterreichen:aiResponse = await self.services.ai.callAiContent( ..., parentOperationId=intentOperationId # Parent-Referenz! ) -
Jeder Service-Call muss
parentOperationIdweiterreichen:extractedResults = await self.services.extraction.extractContent( ..., operationId=extractionOperationId, parentOperationId=extractionOperationId # Parent-Referenz! )
Deep Analysis Aufgaben
-
Analysiere alle Funktionen, die Logs erstellen:
- Identifiziere alle
progressLogStartAufrufe - Prüfe, ob
parentOperationIdgesetzt ist - Prüfe, ob
parentOperationIddurchgereicht wird
- Identifiziere alle
-
Analysiere alle Sub-Funktionen:
- Identifiziere alle Funktionen, die andere Funktionen aufrufen, die Logs erstellen
- Stelle sicher, dass
parentOperationIddurchgereicht wird
-
Analysiere alle Service-Calls:
- Identifiziere alle Calls zu anderen Services (extraction, generation, etc.)
- Stelle sicher, dass
parentOperationIdweitergegeben wird
-
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
# 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
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)
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
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",dataenthält base64 - Image als Reference:
contentFormat="reference",metadata.documentReferenceenthält Referenz - Image als Extracted Text:
contentFormat="extracted",typeGroup="text",dataenthält Text,metadata.originalImageIdenthält Referenz zum Original
Migrations-Pfad
Schritt 1: ContentPart-Modell erweitern
- Füge
contentFormatzum 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 - ❌
DocumentPurposeAnalyzerKlasse mitanalyzeDocumentPurposes()Methode - ❌ Alle Purpose-Listen und Purpose-Tags im Code
Zu implementieren:
- ✅
DocumentIntentModell verwenden (bereits definiert indatamodelExtraction.py) - ✅
_clarifyDocumentIntents()Funktion inmainServiceAi.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:
# 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
intentsaus 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
parentOperationIdParameter hinzu wo nötig - Stelle sicher, dass Parent-Referenzen korrekt durchgereicht werden
- Validiere Hierarchie
Vorteile des überarbeiteten Konzepts
- Klarere Trennung: Jede Phase hat distincte Verantwortung
- Metadatenfluss: Vollständige Metadaten in jedem Schritt
- Format-Klarheit: Explizite Format-Indikation
- Zentralisierte Logik: Single Source of Truth
- Debuggability: Alle Prompts/Responses geloggt (harmonisiert)
- Flexibilität: Unterstützt alle Use Cases
- Wartbarkeit: Klare Struktur, einfach zu erweitern
- Korrekte Hierarchie: Alle ChatLogs haben korrekte Parent/Child-Referenzen
- Bessere Performance: Phase 1+2 kombiniert = weniger AI-Calls
- 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"mitcontentFormat="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"
- PDFs:
- Content-Extraktion (5B):
- PDF-Text wird extrahiert:
contentFormat="extracted" - Bilder werden analysiert (OCR/Vision):
contentFormat="extracted"mitextractionMethod="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)
- PDF-Text wird extrahiert:
- 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"
- Bilder:
- 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 RenderingContentPart(id="img_1_ext", contentFormat="extracted", ...)- für Storyline-Analyse
- Für Rendering: Bilder als
- 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"
- Dokumente:
- Content-Extraktion (5B):
- Dokumente werden extrahiert:
contentFormat="extracted"
- Dokumente werden extrahiert:
- 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:
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:
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:
# 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
- ✅ Multi-Dokument-Rendering: In einem Call - Resultat kann mehrere Dokumente sein
- ✅ Section-Generierung Parallelisierung: Parallel, wenn möglich - Sections können parallel generiert werden, um Performance zu verbessern
- ✅ 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
- ✅ Caching: Nein, kein Caching - Extrahierter Content wird nicht gecacht, um Konsistenz und Klarheit zu gewährleisten
- ✅ 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.
- ✅ Mehrfache ContentParts: Ja, natürlich! - Das ist das grundlegende Design! Ein Dokument kann mehrere ContentParts erzeugen (z.B.
object+extractedfür Bilder, die sowohl gerendert als auch analysiert werden sollen) - ✅ 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
- Review und Approve dieses Konzepts
- Implementiere ContentPart-Metadaten-Erweiterungen
- Refactore callAiContent nach 5A-5E Struktur
- Aktualisiere Action-Methoden für zentralisierte Logik
- Harmonisiere Debug-Logging (entferne Checks, standardisiere Namen)
- Korrigiere ChatLog-Hierarchie (füge Parent-Referenzen hinzu)
- Implementiere Multi-Dokument-Rendering
- Teste mit verschiedenen Szenarien
- Dokumentiere API-Änderungen