# 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))