gateway/modules/workflows/methods/methodAi/actions/generateDocument.py

161 lines
7.3 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
import logging
import time
from typing import Dict, Any, Optional, List
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
from modules.datamodels.datamodelExtraction import ContentPart
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionInactiveException
from modules.serviceCenter.services.serviceBilling.mainServiceBilling import BillingContextError
logger = logging.getLogger(__name__)
async def generateDocument(self, parameters: Dict[str, Any]) -> ActionResult:
from modules.workflows.methods.methodAi._common import serialize_context
base_prompt = (parameters.get("prompt") or "").strip()
context_val = serialize_context(parameters.get("context"))
prompt = f"Kontext:\n{context_val}\n\n{base_prompt}" if context_val else base_prompt
if not prompt.strip():
return ActionResult.isFailure(error="prompt is required")
documentList = parameters.get("documentList", [])
documentType = parameters.get("documentType")
# Prefer explicit outputFormat (flow UI); resultType remains for legacy / API callers.
resultType = parameters.get("outputFormat") or parameters.get("resultType")
if isinstance(resultType, str):
resultType = resultType.strip().lstrip(".").lower() or None
if not resultType:
logger.debug("resultType not provided - formats will be determined from prompt by AI")
# Create operation ID for progress tracking
workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
operationId = f"doc_gen_{workflowId}_{int(time.time())}"
parentOperationId = parameters.get('parentOperationId')
try:
# Convert documentList to DocumentReferenceList if needed
docRefList = None
if documentList:
from modules.datamodels.datamodelDocref import DocumentReferenceList
if isinstance(documentList, DocumentReferenceList):
docRefList = documentList
elif isinstance(documentList, str):
docRefList = DocumentReferenceList.from_string_list([documentList])
elif isinstance(documentList, list):
docRefList = DocumentReferenceList.from_string_list(documentList)
else:
docRefList = DocumentReferenceList(references=[])
title_raw = parameters.get("title")
title = (title_raw.strip() if isinstance(title_raw, str) else "") or None
if not title and isinstance(documentType, str) and documentType.strip():
title = documentType.strip()
if not title:
title = "Generated Document"
# Call AI service for document generation
# callAiContent handles documentList internally via Phases 5A-5E
options = AiCallOptions(
operationType=OperationTypeEnum.DATA_GENERATE,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.DETAILED,
compressPrompt=False,
compressContext=False
)
# Apply node-level AI params
allowedModels = parameters.get("allowedModels")
if allowedModels and isinstance(allowedModels, list):
options.allowedModels = allowedModels
requireNeutralization = parameters.get("requireNeutralization")
if requireNeutralization is not None:
_ctx = getattr(self.services, '_context', None)
if _ctx:
_ctx.requireNeutralization = bool(requireNeutralization)
# outputFormat: Optional - if None, formats determined from prompt by AI
aiResponse: AiResponse = await self.services.ai.callAiContent(
prompt=prompt,
options=options,
documentList=docRefList, # Übergebe documentList direkt - callAiContent macht Phasen 5A-5E
outputFormat=resultType, # Can be None - AI determines from prompt
title=title,
parentOperationId=parentOperationId,
generationIntent="document" # NEW: Explicit intent, skips detection
)
# Convert AiResponse to ActionResult
documents = []
# Convert DocumentData to ActionDocument
if aiResponse.documents:
for docData in aiResponse.documents:
documents.append(ActionDocument(
documentName=docData.documentName,
documentData=docData.documentData,
mimeType=docData.mimeType,
sourceJson=docData.sourceJson if hasattr(docData, 'sourceJson') else None,
validationMetadata={
"actionType": "ai.generateDocument",
"documentType": documentType,
"resultType": resultType,
"outputFormat": resultType,
"title": title,
}
))
# If no documents but content exists, create a document from content
if not documents and aiResponse.content:
# Determine document name from metadata
resultTypeFallback = resultType or "txt" # Fallback for file naming
docName = f"document.{resultTypeFallback}"
if aiResponse.metadata and aiResponse.metadata.filename:
docName = aiResponse.metadata.filename
elif aiResponse.metadata and aiResponse.metadata.title:
import re
sanitized = re.sub(r"[^a-zA-Z0-9._-]", "_", aiResponse.metadata.title)
sanitized = re.sub(r"_+", "_", sanitized).strip("_")
if sanitized:
if not sanitized.lower().endswith(f".{resultTypeFallback}"):
docName = f"{sanitized}.{resultTypeFallback}"
else:
docName = sanitized
# Determine mime type
rt = resultTypeFallback
mimeType = "text/plain"
if rt == "html":
mimeType = "text/html"
elif rt == "json":
mimeType = "application/json"
elif rt == "pdf":
mimeType = "application/pdf"
elif rt == "md":
mimeType = "text/markdown"
documents.append(ActionDocument(
documentName=docName,
documentData=aiResponse.content.encode('utf-8') if isinstance(aiResponse.content, str) else aiResponse.content,
mimeType=mimeType,
validationMetadata={
"actionType": "ai.generateDocument",
"documentType": documentType,
"resultType": resultType,
"outputFormat": resultType,
"title": title,
}
))
return ActionResult.isSuccess(documents=documents)
except (SubscriptionInactiveException, BillingContextError):
raise
except Exception as e:
logger.error(f"Error in document generation: {str(e)}")
return ActionResult.isFailure(error=str(e))