diff --git a/modules/services/serviceAi/subDocumentGeneration.py b/modules/services/serviceAi/subDocumentGeneration.py index 8315a592..3d3e1c91 100644 --- a/modules/services/serviceAi/subDocumentGeneration.py +++ b/modules/services/serviceAi/subDocumentGeneration.py @@ -119,9 +119,9 @@ class SubDocumentGeneration: # Update progress - starting AI processing progressLogger.updateProgress(operationId, 0.3, "AI processing") - # Process documents with format-specific prompt using JSON mode - # This ensures structured JSON output instead of text - aiResponseJson = await self._callAiJson(extractionPrompt, documents, options) + # Process documents with format-specific prompt using JSON mode with chunking + # This ensures structured JSON output instead of text and handles large documents + aiResponseJson = await self._callAiJsonWithChunking(extractionPrompt, documents, options, progressLogger, operationId) # Update progress - AI processing completed progressLogger.updateProgress(operationId, 0.6, "Processing done") @@ -648,6 +648,90 @@ class SubDocumentGeneration: # Process documents with JSON merging return await self.documentProcessor.processDocumentsPerChunkJson(documents, prompt, options) + async def _callAiJsonWithChunking( + self, + prompt: str, + documents: Optional[List[ChatDocument]], + options: AiCallOptions, + progressLogger, + operationId: str + ) -> Dict[str, Any]: + """ + Handle AI calls with document processing for JSON output using chunking for large documents. + Supports continuation when documents are too large for single AI call. + """ + # Initialize the document structure + completeDocument = { + "metadata": {"title": "Generated Document"}, + "sections": [], + "continue": True + } + + continuationContext = None + chunkCount = 0 + maxChunks = 10 # Prevent infinite loops + + while completeDocument.get("continue", False) and chunkCount < maxChunks: + chunkCount += 1 + logger.info(f"Processing generation chunk {chunkCount}") + + # Update progress + progressLogger.updateProgress(operationId, 0.3 + (chunkCount * 0.3 / maxChunks), f"Generating chunk {chunkCount}") + + # Prepare the chunk prompt + if continuationContext: + chunkPrompt = f""" +{prompt} + +CONTINUATION CONTEXT: +- Last completed section: {continuationContext.get('last_section_id', 'none')} +- Last completed element index: {continuationContext.get('last_element_index', 0)} +- Remaining requirements: {continuationContext.get('remaining_requirements', 'complete the document')} + +Continue generating the document from where you left off. Include all previously generated content and add the remaining sections. +""" + else: + chunkPrompt = prompt + + # Call AI for this chunk using the existing document processor + aiResponseJson = await self.documentProcessor.processDocumentsPerChunkJson(documents, chunkPrompt, options) + + # Validate JSON response + if not isinstance(aiResponseJson, dict): + raise Exception("AI response is not valid JSON document structure") + + # Merge the chunk with the complete document + if chunkCount == 1: + # First chunk - use as base + completeDocument = aiResponseJson + else: + # Subsequent chunks - merge sections + if "sections" in aiResponseJson: + # Find the last section ID from continuation context + lastSectionId = continuationContext.get('last_section_id', '') if continuationContext else '' + + # Add new sections after the last completed one + newSections = [] + for section in aiResponseJson["sections"]: + if section.get("id") != lastSectionId: + newSections.append(section) + + completeDocument["sections"].extend(newSections) + + # Check if we need to continue + if aiResponseJson.get("continue", False): + continuationContext = aiResponseJson.get("continuation_context", {}) + logger.info(f"Document generation needs continuation: {continuationContext}") + else: + completeDocument["continue"] = False + logger.info("Document generation completed") + + if chunkCount >= maxChunks: + logger.warning(f"Document generation stopped after {maxChunks} chunks (max limit reached)") + completeDocument["continue"] = False + + return completeDocument + async def _analyzePromptIntent(self, prompt: str, ai_service=None) -> Dict[str, Any]: """Use AI to analyze user prompt and determine processing requirements.""" if not ai_service: diff --git a/modules/services/serviceGeneration/subPromptBuilder.py b/modules/services/serviceGeneration/subPromptBuilder.py index cbcce375..4a5a8e97 100644 --- a/modules/services/serviceGeneration/subPromptBuilder.py +++ b/modules/services/serviceGeneration/subPromptBuilder.py @@ -581,9 +581,19 @@ CRITICAL: The AI MUST generate content using the CANONICAL JSON FORMAT with this ], "order": 3 }} - ] + ], + "continue": false }} +IMPORTANT CHUNKING LOGIC: +- If the document is too large to generate completely in one response, set "continue": true +- When "continue": true, include a "continuation_context" field with: + - "last_section_id": "id of the last completed section" + - "last_element_index": "index of the last completed element in that section" + - "remaining_requirements": "brief description of what still needs to be generated" +- The AI will be called again with this context to continue generation +- Only set "continue": false when the document is completely generated + The AI should NOT create format-specific structures like "sheets" or "columns" - only use the canonical format with "sections" and "elements". Write the instructions as plain text, not JSON. Start with "Generate JSON content that..." and provide clear, actionable instructions for creating structured JSON data in the canonical format. diff --git a/modules/workflows/processing/shared/promptGenerationTaskplan.py b/modules/workflows/processing/shared/promptGenerationTaskplan.py index 6da51e1c..eb34df66 100644 --- a/modules/workflows/processing/shared/promptGenerationTaskplan.py +++ b/modules/workflows/processing/shared/promptGenerationTaskplan.py @@ -18,10 +18,14 @@ logger = logging.getLogger(__name__) def generateTaskPlanningPrompt(services, context: Any) -> PromptBundle: """Define placeholders first, then the template; return PromptBundle.""" + # Extract user language from services + userLanguage = getattr(services, 'currentUserLanguage', None) or 'en' + placeholders: List[PromptPlaceholder] = [ PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False), PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True), PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True), + PromptPlaceholder(label="USER_LANGUAGE", content=userLanguage, summaryAllowed=False), ] template = """# Task Planning @@ -71,7 +75,7 @@ Break down user requests into logical, executable task steps. ```json { "overview": "Brief description of the overall plan", - "userMessage": "User-friendly message explaining the task plan (use the user's detected language from intent)", + "userMessage": "User-friendly message explaining the task plan (use {{KEY:USER_LANGUAGE}} language)", "tasks": [ { "id": "task_1",