diff --git a/modules/services/serviceAi/mainServiceAi.py b/modules/services/serviceAi/mainServiceAi.py index 7e4af22d..c036db32 100644 --- a/modules/services/serviceAi/mainServiceAi.py +++ b/modules/services/serviceAi/mainServiceAi.py @@ -161,11 +161,12 @@ class AiService: prompt: str, placeholders: Optional[List[PromptPlaceholder]] = None, options: Optional[AiCallOptions] = None, - loopInstruction: Optional[str] = None + loopInstructionFormat: Optional[str] = None ) -> str: """Planning AI call for task planning, action planning, action selection, etc.""" await self._ensureAiObjectsInitialized() - return await self.coreAi.callAiPlanning(prompt, placeholders, options, loopInstruction) + # Always use "json" for planning calls since they return JSON + return await self.coreAi.callAiPlanning(prompt, placeholders, options, "json") async def callAiDocuments( self, @@ -177,7 +178,8 @@ class AiService: ) -> Union[str, Dict[str, Any]]: """Document generation AI call for all non-planning calls.""" await self._ensureAiObjectsInitialized() - return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title) + # Use "json" for document generation calls since they return JSON + return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title, "json") def sanitizePromptContent(self, content: str, contentType: str = "text") -> str: diff --git a/modules/services/serviceAi/subCoreAi.py b/modules/services/serviceAi/subCoreAi.py index d602a15f..082527d2 100644 --- a/modules/services/serviceAi/subCoreAi.py +++ b/modules/services/serviceAi/subCoreAi.py @@ -6,6 +6,14 @@ from modules.shared.debugLogger import writeDebugFile logger = logging.getLogger(__name__) +# Loop instruction texts for different formats +LoopInstructionTexts = { + "json": """CRITICAL: You MUST set the "continuation" field in your JSON response. If you cannot complete the full response, deliver the possible part and set "continuation" to a brief description of what still needs to be generated. If you can complete the full response, set "continuation" to null.""", + # Add more formats here as needed + # "xml": "...", + # "text": "...", +} + class SubCoreAi: """Core AI operations including image analysis, text generation, and planning calls.""" @@ -26,7 +34,7 @@ class SubCoreAi: prompt: str, options: AiCallOptions, debugPrefix: str = "ai_call", - loopInstruction: str = None + loopInstructionFormat: str = None ) -> str: """ Shared core function for AI calls with looping system. @@ -37,7 +45,7 @@ class SubCoreAi: prompt: The prompt to send to AI options: AI call configuration options debugPrefix: Prefix for debug file names - loopInstruction: If provided, replaces LOOP_INSTRUCTION placeholder and includes in continuation prompts + loopInstructionFormat: If provided, replaces LOOP_INSTRUCTION placeholder and includes in continuation prompts Returns: Complete AI response after all iterations @@ -46,19 +54,17 @@ class SubCoreAi: iteration = 0 accumulatedContent = [] - logger.debug(f"Starting AI call with looping (debug prefix: {debugPrefix}, loopInstruction: {loopInstruction is not None})") + logger.debug(f"Starting AI call with looping (debug prefix: {debugPrefix}, loopInstructionFormat: {loopInstructionFormat is not None})") - # Import debug logger for use in iterations - - # Store original prompt to preserve LOOP_INSTRUCTION placeholder - originalPrompt = prompt - - # Handle LOOP_INSTRUCTION placeholder replacement for first iteration - if loopInstruction and iteration == 0: - if "LOOP_INSTRUCTION" not in prompt: - raise ValueError("LOOP_INSTRUCTION placeholder not found in prompt when loopInstruction provided") - prompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction) - logger.debug("Replaced LOOP_INSTRUCTION placeholder with provided instruction") + + # Determine loopInstruction based on loopInstructionFormat (before iterations) + if not loopInstructionFormat: + loopInstruction = "" + elif loopInstructionFormat in LoopInstructionTexts: + loopInstruction = LoopInstructionTexts[loopInstructionFormat] + else: + logger.error(f"Unsupported loopInstructionFormat for prompt: {loopInstructionFormat}") + loopInstruction = "" while iteration < max_iterations: iteration += 1 @@ -66,12 +72,17 @@ class SubCoreAi: # Build iteration prompt if iteration == 1: - iterationPrompt = prompt + if "LOOP_INSTRUCTION" in prompt: + iterationPrompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction) + else: + iterationPrompt = prompt elif loopInstruction and iteration > 1: - # Only use continuation logic if loopInstruction is provided - iterationPrompt = self._buildContinuationPrompt(originalPrompt, accumulatedContent, iteration, loopInstruction) + continuationContent = self._buildContinuationContent(accumulatedContent, iteration) + if "LOOP_INSTRUCTION" in prompt: + iterationPrompt = prompt.replace("LOOP_INSTRUCTION", f"{loopInstruction}\n\n{continuationContent}") + else: + iterationPrompt = prompt else: - # No looping - use original prompt iterationPrompt = prompt # Make AI call @@ -103,14 +114,27 @@ class SubCoreAi: logger.warning(f"Iteration {iteration}: Empty response, stopping") break - # Check if this is a continuation response (only if loopInstruction is provided) - if loopInstruction and "[CONTINUE:" in result: - # Extract the content before the continuation marker - contentPart = result.split("[CONTINUE:")[0].strip() - if contentPart: - accumulatedContent.append(contentPart) - logger.debug(f"Iteration {iteration}: Continuation detected, continuing...") - continue + # Check if this is a continuation response (only for supported formats) + if loopInstructionFormat in LoopInstructionTexts: + try: + import json + # Try to parse as JSON to check for continuation attribute + parsed_result = json.loads(result) + if isinstance(parsed_result, dict) and parsed_result.get("continuation") is not None: + # This is a continuation response + accumulatedContent.append(result) + logger.debug(f"Iteration {iteration}: Continuation detected in JSON, continuing...") + continue + else: + # This is the final response (continuation is null or missing) + accumulatedContent.append(result) + logger.debug(f"Iteration {iteration}: Final response received") + break + except json.JSONDecodeError: + # Not JSON, treat as final response + accumulatedContent.append(result) + logger.warning(f"Iteration {iteration}: Non-JSON response received") + break else: # This is the final response accumulatedContent.append(result) @@ -124,8 +148,8 @@ class SubCoreAi: if iteration >= max_iterations: logger.warning(f"AI call stopped after maximum iterations ({max_iterations})") - # Combine all accumulated content - final_result = "\n\n".join(accumulatedContent) if accumulatedContent else "" + # Intelligently merge JSON content from all iterations + final_result = self._mergeJsonContent(accumulatedContent) if accumulatedContent else "" # Write final result to debug file writeDebugFile(final_result, f"{debugPrefix}_final_result", None) @@ -133,34 +157,92 @@ class SubCoreAi: logger.info(f"AI call completed: {len(accumulatedContent)} parts from {iteration} iterations") return final_result - def _buildContinuationPrompt( + def _buildContinuationContent( self, - base_prompt: str, accumulatedContent: List[str], - iteration: int, - loopInstruction: str = None + iteration: int ) -> str: """ - Build a prompt for continuation iterations. + Build continuation content for follow-up iterations. """ - continuation_instructions = f""" - -CONTINUATION REQUEST (Iteration {iteration}): -You are continuing from a previous response. Please continue generating content from where you left off. + # Extract continuation description from the last response if it exists + continuation_description = "" + if accumulatedContent: + try: + import json + last_response = accumulatedContent[-1] + parsed_response = json.loads(last_response) + if isinstance(parsed_response, dict) and parsed_response.get("continuation"): + continuation_description = parsed_response["continuation"] + except (json.JSONDecodeError, KeyError): + pass + + continuation_content = f"""CONTINUATION REQUEST (Iteration {iteration}): +Continue generating content from where you left off. IMPORTANT: +- Maintain the same JSON structure - Continue from the exact point where you stopped -- Maintain the same format and structure -- {loopInstruction if loopInstruction else "If you cannot complete the full response, end with: [CONTINUE: brief description of what still needs to be generated]"} -- Only stop when the response is completely generated +- If you can complete the remaining content: set "continuation": null +- If content is still too long, deliver next part and set "continuation": "description of remaining content" -Previous content generated: +Previous content: {chr(10).join(accumulatedContent[-1:]) if accumulatedContent else "None"} -Continue generating content now: -""" +{f"Continuation needed: {continuation_description}" if continuation_description else ""} + +Continue generating content now.""" - return f"{base_prompt}{continuation_instructions}" + return continuation_content + + def _mergeJsonContent(self, accumulatedContent: List[str]) -> str: + """ + Generic JSON merger that combines all lists from multiple iterations. + Structure: root attributes + 1..n lists that get merged together. + """ + if not accumulatedContent: + return "" + + if len(accumulatedContent) == 1: + return accumulatedContent[0] + + try: + import json + + # Parse all JSON responses + parsed_responses = [] + for content in accumulatedContent: + try: + parsed = json.loads(content) + parsed_responses.append(parsed) + except json.JSONDecodeError as e: + logger.warning(f"Failed to parse JSON content: {str(e)}") + continue + + if not parsed_responses: + return accumulatedContent[0] # Return first response if all parsing failed + + # Start with first response as base + merged = parsed_responses[0].copy() + + # Merge all lists from all responses + for response in parsed_responses[1:]: + for key, value in response.items(): + if isinstance(value, list) and key in merged and isinstance(merged[key], list): + # Merge lists by extending + merged[key].extend(value) + elif key not in merged: + # Add new fields + merged[key] = value + + # Mark as complete + merged["continuation"] = None + + return json.dumps(merged, indent=2) + + except Exception as e: + logger.error(f"Error merging JSON content: {str(e)}") + return accumulatedContent[0] # Return first response on error def _buildPromptWithPlaceholders(self, prompt: str, placeholders: Optional[Dict[str, str]]) -> str: """ @@ -214,7 +296,7 @@ Continue generating content now: prompt: str, placeholders: Optional[List[PromptPlaceholder]] = None, options: Optional[AiCallOptions] = None, - loopInstruction: Optional[str] = None + loopInstructionFormat: Optional[str] = None ) -> str: """ Planning AI call for task planning, action planning, action selection, etc. @@ -238,7 +320,7 @@ Continue generating content now: full_prompt = prompt # Use shared core function with planning-specific debug prefix - return await self._callAiWithLooping(full_prompt, options, "planning", loopInstruction=loopInstruction) + return await self._callAiWithLooping(full_prompt, options, "planning", loopInstructionFormat=loopInstructionFormat) # Document Generation AI Call async def callAiDocuments( @@ -247,7 +329,8 @@ Continue generating content now: documents: Optional[List[ChatDocument]] = None, options: Optional[AiCallOptions] = None, outputFormat: Optional[str] = None, - title: Optional[str] = None + title: Optional[str] = None, + loopInstructionFormat: Optional[str] = None ) -> Union[str, Dict[str, Any]]: """ Document generation AI call for all non-planning calls. @@ -276,7 +359,7 @@ Continue generating content now: logger.info("No documents provided - using direct generation") extracted_content = None generation_prompt = await self._buildGenerationPrompt(prompt, extracted_content, outputFormat, title) - generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation", loopInstruction="If you cannot complete the full response, end with: [CONTINUE: brief description of what still needs to be generated]") + generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation", loopInstructionFormat=loopInstructionFormat) # Parse the generated JSON try: @@ -337,7 +420,7 @@ Continue generating content now: result = await self.services.ai.documentProcessor.callAiText(prompt, documents, options) else: # Use shared core function for direct text calls - result = await self._callAiWithLooping(prompt, options, "text", loopInstruction=None) + result = await self._callAiWithLooping(prompt, options, "text", loopInstructionFormat=None) return result diff --git a/modules/services/serviceGeneration/subPromptBuilder.py b/modules/services/serviceGeneration/subPromptBuilder.py index 380fa00b..716ebac0 100644 --- a/modules/services/serviceGeneration/subPromptBuilder.py +++ b/modules/services/serviceGeneration/subPromptBuilder.py @@ -196,15 +196,13 @@ Consider the user's intent and the most logical way to organize the extracted co services.utils.debugLogToFile(f"Generic prompt analysis failed: {str(e)}", "PROMPT_BUILDER") # Always use the proper generation prompt template with LOOP_INSTRUCTION - result = f"""You are an AI assistant that generates structured JSON content for document creation. + result = f"""Generate structured JSON content for document creation. USER REQUEST: "{userPrompt}" DOCUMENT TITLE: "{title}" TARGET FORMAT: {outputFormat} -TASK: Generate JSON content that fulfills the user's request. - -CRITICAL: You MUST return ONLY valid JSON in this exact structure: +Return ONLY this JSON structure: {{ "metadata": {{ "title": "{title}", @@ -241,16 +239,18 @@ CRITICAL: You MUST return ONLY valid JSON in this exact structure: }} ] }} - ] + ], + "continuation": null }} -IMPORTANT: -- Return ONLY the JSON structure above -- Do NOT include any text before or after the JSON -- Fill in the actual content based on the user request: {userPrompt} -- If the content is too large, you can split it into multiple sections -- Each section should have a unique id and appropriate content_type -- LOOP_INSTRUCTION +RULES: +- Fill sections with content based on the user request +- Use appropriate content_type: "heading", "paragraph", "table", "list" +- If content is too long: deliver partial result and set "continuation": "description of remaining content" +- If content fits: deliver complete result and set "continuation": null +- Split large content into multiple sections if needed + +LOOP_INSTRUCTION """ # Debug output @@ -440,15 +440,13 @@ async def buildGenerationPrompt( services.utils.debugLogToFile("GENERATION PROMPT REQUEST: Using static template instead of AI call", "PROMPT_BUILDER") # Return static generation prompt template - result = f"""You are an AI assistant that generates structured JSON content for document creation. + result = f"""Generate structured JSON content for document creation. USER REQUEST: "{safeUserPrompt}" DOCUMENT TITLE: "{title}" TARGET FORMAT: {outputFormat} -TASK: Generate JSON content that fulfills the user's request. - -CRITICAL: You MUST return ONLY valid JSON in this exact structure: +Return ONLY this JSON structure: {{ "metadata": {{ "title": "{title}", @@ -485,15 +483,14 @@ CRITICAL: You MUST return ONLY valid JSON in this exact structure: }} ] }} - ] + ], + "continuation": null }} -IMPORTANT: -- Return ONLY the JSON structure above -- Do NOT include any text before or after the JSON -- Fill in the actual content based on the user request: {safeUserPrompt} -- If the content is too large, you can split it into multiple sections -- Each section should have a unique id and appropriate content_type +RULES: +- Fill sections with content based on the user request +- Use appropriate content_type: "heading", "paragraph", "table", "list" +- Split large content into multiple sections if needed LOOP_INSTRUCTION """ @@ -509,71 +506,6 @@ LOOP_INSTRUCTION return f"Generate a comprehensive {outputFormat} document titled '{title}' based on the extracted content. User requirements: {userPrompt}" -def _getFormatRules(outputFormat: str) -> str: - """ - Get format-specific rules for the generation prompt. - """ - format_rules = { - "xlsx": """ -XLSX Format Rules: -- Create tables with clear headers and organized data -- Use appropriate column widths and formatting -- Include summary information if relevant -- Ensure data is properly structured for spreadsheet analysis -""", - "pdf": """ -PDF Format Rules: -- Create professional document layout -- Use appropriate headings and sections -- Include proper spacing and formatting -- Ensure content is well-organized and readable -""", - "docx": """ -DOCX Format Rules: -- Create professional document layout -- Use appropriate headings and sections -- Include proper spacing and formatting -- Ensure content is well-organized and readable -""", - "html": """ -HTML Format Rules: -- Create clean, semantic HTML structure -- Use appropriate tags for content organization -- Include proper styling classes -- Ensure content is accessible and well-formatted -""", - "json": """ -JSON Format Rules: -- Create well-structured JSON data -- Use appropriate nesting and organization -- Include metadata and context information -- Ensure data is properly formatted and valid -""", - "csv": """ -CSV Format Rules: -- Create clear, organized tabular data -- Use appropriate headers and data types -- Ensure proper CSV formatting -- Include all relevant data in structured format -""", - "txt": """ -TXT Format Rules: -- Create clean, readable text format -- Use appropriate spacing and organization -- Include clear headings and sections -- Ensure content is well-structured and easy to read -""" - } - - return format_rules.get(outputFormat.lower(), f""" -{outputFormat.upper()} Format Rules: -- Create well-structured content appropriate for {outputFormat} -- Use appropriate formatting and organization -- Ensure content is clear and professional -- Include all relevant information in proper format -""") - - async def _parseExtractionIntent(userPrompt: str, outputFormat: str, aiService=None, services=None) -> str: """ Parse user prompt to extract the core extraction intent. diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py index ef9ee6f0..367b04e8 100644 --- a/modules/workflows/methods/methodOutlook.py +++ b/modules/workflows/methods/methodOutlook.py @@ -1181,7 +1181,8 @@ Return JSON: {{ "subject": "subject line", "body": "email body (HTML allowed)", - "attachments": ["doc_ref1", "doc_ref2"] + "attachments": ["doc_ref1", "doc_ref2"], + "continuation": null }} LOOP_INSTRUCTION""" diff --git a/modules/workflows/processing/core/taskPlanner.py b/modules/workflows/processing/core/taskPlanner.py index 2b724c65..2e62daa1 100644 --- a/modules/workflows/processing/core/taskPlanner.py +++ b/modules/workflows/processing/core/taskPlanner.py @@ -108,7 +108,8 @@ class TaskPlanner: prompt = await self.services.ai.callAiPlanning( prompt=taskPlanningPromptTemplate, placeholders=placeholders, - options=options + options=options, + loopInstructionFormat="json" ) # Check if AI response is valid diff --git a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py index 9cbef765..ad8b0f8d 100644 --- a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py +++ b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py @@ -79,7 +79,8 @@ Generate the next action to advance toward completing the task objective. "description": "What this action accomplishes", "userMessage": "User-friendly message in language '{{KEY:USER_LANGUAGE}}'" } - ] + ], + "continuation": null } ``` @@ -95,7 +96,8 @@ Generate the next action to advance toward completing the task objective. "description": "Extract data from documents", "userMessage": "Extracting data from documents" } - ] + ], + "continuation": null } ``` @@ -123,7 +125,7 @@ Generate the next action to advance toward completing the task objective. - **Keep messages concise** but informative ## 🚀 Response Format -Return ONLY the JSON object with complete action objects. If you cannot complete the full response, ensure each action object is complete and valid. +Return ONLY the JSON object with complete action objects. If you cannot complete the full response, set "continuation" to a brief description of what still needs to be generated. If you can complete the response, keep "continuation" as null. LOOP_INSTRUCTION """ @@ -173,7 +175,8 @@ def generateResultReviewPrompt(context: Any) -> PromptBundle: "met_criteria": ["criteria1", "criteria2"], "unmet_criteria": ["criteria3", "criteria4"], "confidence": 0.85, - "userMessage": "User-friendly message explaining the validation result in language '{{KEY:USER_LANGUAGE}}'" + "userMessage": "User-friendly message explaining the validation result in language '{{KEY:USER_LANGUAGE}}'", + "continuation": null } ``` @@ -231,8 +234,9 @@ def generateResultReviewPrompt(context: Any) -> PromptBundle: - "Break down complex objective into smaller, focused actions" - "Verify document references before processing" - ## 🚀 Response Format - Return ONLY the JSON object. Do not include any explanatory text.""" + +LOOP_INSTRUCTION +""" return PromptBundle(prompt=template, placeholders=placeholders) diff --git a/modules/workflows/processing/shared/promptGenerationTaskplan.py b/modules/workflows/processing/shared/promptGenerationTaskplan.py index f5f09960..54e80b50 100644 --- a/modules/workflows/processing/shared/promptGenerationTaskplan.py +++ b/modules/workflows/processing/shared/promptGenerationTaskplan.py @@ -85,7 +85,8 @@ Break down user requests into logical, executable task steps. "estimated_complexity": "low|medium|high", "userMessage": "What this task will accomplish in language '{{KEY:USER_LANGUAGE}}'" } - ] + ], + "continuation": null } ``` @@ -128,8 +129,6 @@ Break down user requests into logical, executable task steps. - **Medium**: Multi-action tasks for one topic (3-5 actions) - **High**: Complex strategic tasks (6+ actions) -## 🚀 Response Format -Return ONLY the JSON object with complete task objects. If you cannot complete the full response, ensure each task object is complete and valid. LOOP_INSTRUCTION """ return PromptBundle(prompt=template, placeholders=placeholders)