From 24f152d0b9fece67c97f6139b341e66a57e48bf4 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 29 Oct 2025 00:38:57 +0100
Subject: [PATCH] ai loop: generic continuation logic
---
modules/interfaces/interfaceAiObjects.py | 25 +-
modules/services/serviceAi/mainServiceAi.py | 3 +-
modules/services/serviceAi/subCoreAi.py | 287 ++++++++----------
.../serviceAi/subDocumentProcessing.py | 1 -
.../subPromptBuilderGeneration.py | 71 ++++-
modules/shared/jsonUtils.py | 219 +++++++++++++
modules/workflows/methods/methodOutlook.py | 6 +-
.../promptGenerationActionsActionplan.py | 19 +-
.../shared/promptGenerationTaskplan.py | 11 +-
9 files changed, 433 insertions(+), 209 deletions(-)
diff --git a/modules/interfaces/interfaceAiObjects.py b/modules/interfaces/interfaceAiObjects.py
index 5b458925..6c12d267 100644
--- a/modules/interfaces/interfaceAiObjects.py
+++ b/modules/interfaces/interfaceAiObjects.py
@@ -461,18 +461,16 @@ class AiObjects:
# Calculate input bytes from prompt and context
inputBytes = len((prompt + context).encode('utf-8'))
- # Replace placeholder in prompt for this specific model
- # Use maxTokens for output limit, not contextLength
- if model.maxTokens > 0:
- tokenLimit = str(model.maxTokens)
+ # Replace placeholder with model's maxTokens value
+ if "" in prompt:
+ if model.maxTokens > 0:
+ tokenLimit = str(model.maxTokens)
+ modelPrompt = prompt.replace("", tokenLimit)
+ logger.debug(f"Replaced with {tokenLimit} for model {model.name}")
+ else:
+ raise ValueError(f"Model {model.name} has invalid maxTokens ({model.maxTokens}). Cannot set token limit.")
else:
- tokenLimit = "16000" # Default for text generation
-
- # Create a copy of the prompt for this model call
- modelPrompt = prompt
- if "" in modelPrompt:
- modelPrompt = modelPrompt.replace("", tokenLimit)
- logger.debug(f"Replaced with {tokenLimit} for model {model.name}")
+ modelPrompt = prompt
# Update messages array with replaced content
messages = []
@@ -483,11 +481,6 @@ class AiObjects:
# Start timing
startTime = time.time()
- # Get the connector for this model
- connector = modelRegistry.getConnectorForModel(model.name)
- if not connector:
- raise ValueError(f"No connector found for model {model.name}")
-
# Call the model's function directly - completely generic
if model.functionCall:
# Create standardized call object
diff --git a/modules/services/serviceAi/mainServiceAi.py b/modules/services/serviceAi/mainServiceAi.py
index 08286ca0..b1326967 100644
--- a/modules/services/serviceAi/mainServiceAi.py
+++ b/modules/services/serviceAi/mainServiceAi.py
@@ -139,8 +139,7 @@ class AiService:
) -> Union[str, Dict[str, Any]]:
"""Document generation AI call for all non-planning calls."""
await self._ensureAiObjectsInitialized()
- # Use "json" for document generation calls since they return JSON
- return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title, "json")
+ return await self.coreAi.callAiDocuments(prompt, documents, options, outputFormat, title)
def sanitizePromptContent(self, content: str, contentType: str = "text") -> str:
"""Sanitize prompt content to prevent injection attacks and ensure safe presentation."""
diff --git a/modules/services/serviceAi/subCoreAi.py b/modules/services/serviceAi/subCoreAi.py
index 977c0e9a..cd177cc9 100644
--- a/modules/services/serviceAi/subCoreAi.py
+++ b/modules/services/serviceAi/subCoreAi.py
@@ -9,24 +9,17 @@ from modules.services.serviceAi.subSharedAiUtils import (
reduceText,
determineCallType
)
+from modules.shared.jsonUtils import (
+ extractJsonString,
+ repairBrokenJson,
+ extractSectionsFromDocument,
+ buildContinuationContext
+)
logger = logging.getLogger(__name__)
-# Generic continuation instruction for all prompts with JSON responses
-# Used by _callAiWithLooping() to replace LOOP_INSTRUCTION placeholder
-LOOP_INSTRUCTION_TEXT = """
-MANDATORY RULE:
-Return ONLY raw JSON (no ```json blocks, no text before/after)
-
-CONTINUATION REQUIREMENT:
-Your response must be a valid JSON object with a "continuation" field.
-
-- If you can complete the FULL request: Set {"continuation": null}
-- If you MUST stop early (due to token limits): Set {"continuation": {"last_data_items": "brief summary of what was delivered for context", "next_instruction": "what to deliver next to complete the request"}}
-
-The "continuation" field controls whether this AI call continues in a loop or stops.
-Refer to the json template below to see where to set the "continuation" information.
-"""
+# Repair-based looping system - no longer needs LOOP_INSTRUCTION_TEXT
+# Sections are accumulated and repair mechanism handles broken JSON automatically
# Rebuild the model to resolve forward references
AiCallRequest.model_rebuild()
@@ -126,7 +119,7 @@ Respond with ONLY a JSON object in this exact format:
- # Shared Core Function for AI Calls with Looping
+ # Shared Core Function for AI Calls with Looping and Repair
async def _callAiWithLooping(
self,
prompt: str,
@@ -134,9 +127,8 @@ Respond with ONLY a JSON object in this exact format:
debugPrefix: str = "ai_call"
) -> str:
"""
- Shared core function for AI calls with looping system.
- Handles continuation logic when response needs multiple rounds.
- Delivers prompt and response to debug file log.
+ Shared core function for AI calls with repair-based looping system.
+ Automatically repairs broken JSON and continues generation seamlessly.
Args:
prompt: The prompt to send to AI
@@ -146,42 +138,28 @@ Respond with ONLY a JSON object in this exact format:
Returns:
Complete AI response after all iterations
"""
- max_iterations = 100 # Prevent infinite loops
+ max_iterations = 50 # Prevent infinite loops
iteration = 0
- accumulatedContent = []
- lastContinuationData = None
-
- logger.debug(f"Starting AI call with looping (debug prefix: {debugPrefix})")
-
- # Use generic LOOP_INSTRUCTION_TEXT
- loopInstruction = LOOP_INSTRUCTION_TEXT if ("LOOP_INSTRUCTION" in prompt) else ""
+ allSections = [] # Accumulate all sections across iterations
+ logger.debug(f"Starting AI call with repair-based looping (debug prefix: {debugPrefix})")
while iteration < max_iterations:
iteration += 1
logger.debug(f"AI call iteration {iteration}/{max_iterations}")
# Build iteration prompt
- if iteration == 1:
- # First iteration - replace LOOP_INSTRUCTION with standardized instruction
- if "LOOP_INSTRUCTION" in prompt:
- iterationPrompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction)
- else:
- iterationPrompt = prompt
+ if len(allSections) > 0:
+ # This is a continuation - build continuation context
+ continuationContext = buildContinuationContext(allSections)
+ logger.info(f"Continuation context: {continuationContext.get('section_count')} sections, next order: {continuationContext.get('next_order')}")
+
+ # If prompt contains a placeholder for continuation, inject the context
+ # For now, we'll handle this at the calling code level
+ iterationPrompt = prompt
else:
- # Subsequent iterations - include continuation data if available
- if lastContinuationData and isinstance(lastContinuationData, dict):
- continuationPrompt = self._buildContinuationPrompt(lastContinuationData, iteration)
- if "LOOP_INSTRUCTION" in prompt:
- iterationPrompt = prompt.replace("LOOP_INSTRUCTION", f"{continuationPrompt}\n\n{loopInstruction}")
- else:
- iterationPrompt = prompt
- else:
- # No continuation data - re-send original prompt
- if "LOOP_INSTRUCTION" in prompt:
- iterationPrompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction)
- else:
- iterationPrompt = prompt
+ # First iteration - use original prompt
+ iterationPrompt = prompt
# Make AI call
try:
@@ -192,12 +170,10 @@ Respond with ONLY a JSON object in this exact format:
options=options
)
- # Write the ACTUAL prompt sent to AI (including continuation context)
+ # Write the ACTUAL prompt sent to AI
if iteration == 1:
- # First iteration - use the historic naming pattern
self.services.utils.writeDebugFile(iterationPrompt, f"{debugPrefix}_prompt")
else:
- # Subsequent iterations - include iteration number
self.services.utils.writeDebugFile(iterationPrompt, f"{debugPrefix}_prompt_iteration_{iteration}")
response = await self.aiObjects.call(request)
@@ -205,10 +181,8 @@ Respond with ONLY a JSON object in this exact format:
# Write raw AI response to debug file
if iteration == 1:
- # First iteration - use the historic naming pattern
self.services.utils.writeDebugFile(result, f"{debugPrefix}_response")
else:
- # Subsequent iterations - include iteration number
self.services.utils.writeDebugFile(result, f"{debugPrefix}_response_iteration_{iteration}")
# Emit stats for this iteration
@@ -222,35 +196,24 @@ Respond with ONLY a JSON object in this exact format:
logger.warning(f"Iteration {iteration}: Empty response, stopping")
break
- accumulatedContent.append(result)
+ # Extract sections from response (handles both valid and broken JSON)
+ extractedSections, wasJsonComplete = self._extractSectionsFromResponse(result, iteration, debugPrefix)
- # Check if this is a continuation response (only when LOOP_INSTRUCTION was used)
- if loopInstruction:
- try:
- # Extract JSON substring if wrapped (e.g., ```json ... ```)
- extracted = self.services.utils.jsonExtractString(result)
- parsed_result = json.loads(extracted)
-
- if isinstance(parsed_result, dict):
- continuation = parsed_result.get("continuation")
-
- if continuation is None:
- # Final response - break loop
- logger.debug(f"Iteration {iteration}: Final response received (continuation: null)")
- break
- else:
- # Continuation detected - extract data for next iteration
- lastContinuationData = continuation if isinstance(continuation, dict) else None
- logger.debug(f"Iteration {iteration}: Continuation detected, continuing...")
- continue
- except json.JSONDecodeError:
- # Not JSON, treat as final response
- logger.warning(f"Iteration {iteration}: Non-JSON response - treating as final")
- self.services.utils.writeDebugFile(result, f"{debugPrefix}_error_non_json_response_iteration_{iteration}")
- break
+ if not extractedSections:
+ logger.warning(f"Iteration {iteration}: No sections extracted, stopping")
+ break
+
+ # Add new sections to accumulator
+ allSections.extend(extractedSections)
+ logger.info(f"Iteration {iteration}: Extracted {len(extractedSections)} sections (total: {len(allSections)})")
+
+ # Check if we should continue (completion detection)
+ if self._shouldContinueGeneration(allSections, iteration, wasJsonComplete):
+ logger.debug(f"Iteration {iteration}: Continuing generation")
+ continue
else:
- # No loop instruction format - treat as final response
- logger.debug(f"Iteration {iteration}: Final response received (no loop format)")
+ # Done - build final result
+ logger.info(f"Iteration {iteration}: Generation complete")
break
except Exception as e:
@@ -260,95 +223,112 @@ Respond with ONLY a JSON object in this exact format:
if iteration >= max_iterations:
logger.warning(f"AI call stopped after maximum iterations ({max_iterations})")
- # Intelligently merge JSON content from all iterations
- final_result = self._mergeJsonContent(accumulatedContent) if accumulatedContent else ""
+ # Build final result from accumulated sections
+ final_result = self._buildFinalResultFromSections(allSections)
# Write final result to debug file
self.services.utils.writeDebugFile(final_result, f"{debugPrefix}_final_result")
- logger.info(f"AI call completed: {len(accumulatedContent)} parts from {iteration} iterations")
+ logger.info(f"AI call completed: {len(allSections)} total sections from {iteration} iterations")
return final_result
-
- def _buildContinuationPrompt(
+
+ def _extractSectionsFromResponse(
self,
- continuationData: dict,
- iteration: int
- ) -> str:
+ result: str,
+ iteration: int,
+ debugPrefix: str
+ ) -> Tuple[List[Dict[str, Any]], bool]:
"""
- Build standardized continuation prompt from continuation data dict.
- This replaces the complex _buildContinuationContent method with a simpler approach.
-
- Args:
- continuationData: Dictionary containing last_data_items and next_instruction
- iteration: Current iteration number
+ Extract sections from AI response, handling both valid and broken JSON.
+ Uses repair mechanism for broken JSON.
+ Returns (sections, wasJsonComplete)
+ """
+ # First, try to parse as valid JSON
+ try:
+ extracted = extractJsonString(result)
+ parsed_result = json.loads(extracted)
- Returns:
- Formatted continuation prompt string
- """
- last_data_items = continuationData.get("last_data_items", "")
- next_instruction = continuationData.get("next_instruction", "")
+ # Extract sections from parsed JSON
+ sections = extractSectionsFromDocument(parsed_result)
+ logger.debug(f"Iteration {iteration}: Valid JSON - extracted {len(sections)} sections")
+ return sections, True # JSON was complete
+
+ except json.JSONDecodeError as e:
+ # Broken JSON - try repair mechanism
+ logger.warning(f"Iteration {iteration}: Invalid JSON, attempting repair: {str(e)}")
+ self.services.utils.writeDebugFile(result, f"{debugPrefix}_broken_json_iteration_{iteration}")
+
+ # Try to repair
+ repaired_json = repairBrokenJson(result)
+
+ if repaired_json:
+ # Extract sections from repaired JSON
+ sections = extractSectionsFromDocument(repaired_json)
+ logger.info(f"Iteration {iteration}: Repaired JSON - extracted {len(sections)} sections")
+ return sections, False # JSON was broken but repaired
+ else:
+ # Repair failed - log error
+ logger.error(f"Iteration {iteration}: All repair strategies failed")
+ return [], False
- continuation_prompt = f"""CONTINUATION REQUEST (Iteration {iteration}):
-You are continuing a previous response. DO NOT repeat any previous content.
-
-{f"Already delivered data: {last_data_items}" if last_data_items else "No previous data specified"}
-
-{f"Your task to deliver: {next_instruction}" if next_instruction else "No specific task provided"}
-
-CRITICAL REQUIREMENTS:
-- Start from the exact point specified above
-- DO NOT repeat any previous content"""
+ except Exception as e:
+ logger.error(f"Iteration {iteration}: Unexpected error during parsing: {str(e)}")
+ return [], False
+
+ def _shouldContinueGeneration(
+ self,
+ allSections: List[Dict[str, Any]],
+ iteration: int,
+ wasJsonComplete: bool
+ ) -> bool:
+ """
+ Determine if generation should continue based on JSON completeness.
+ Returns True if we should continue, False if done.
+ """
+ if len(allSections) == 0:
+ return True # No sections yet, continue
- return continuation_prompt
-
- def _mergeJsonContent(self, accumulatedContent: List[str]) -> str:
+ # Simple rule: if JSON was complete, we're done
+ # If JSON was broken and repaired, continue to get more content
+ if wasJsonComplete:
+ logger.info("JSON was complete - stopping generation")
+ return False
+ else:
+ logger.info("JSON was broken/repaired - continuing generation")
+ return True
+
+ def _buildFinalResultFromSections(
+ self,
+ allSections: List[Dict[str, Any]]
+ ) -> str:
"""
- Generic JSON merger that combines all lists from multiple iterations.
- Structure: root attributes + 1..n lists that get merged together.
+ Build final JSON result from accumulated sections.
"""
- if not accumulatedContent:
+ if not allSections:
return ""
- if len(accumulatedContent) == 1:
- return accumulatedContent[0]
+ # Build documents structure
+ # Assuming single document for now
+ documents = [{
+ "id": "doc_1",
+ "title": "Generated Document", # This should come from prompt
+ "filename": "document.json",
+ "sections": allSections
+ }]
- try:
-
- # Parse all JSON responses
- parsed_responses = []
- for content in accumulatedContent:
- try:
- extracted = self.services.utils.jsonExtractString(content)
- parsed = json.loads(extracted)
- 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
+ result = {
+ "metadata": {
+ "split_strategy": "single_document",
+ "source_documents": [],
+ "extraction_method": "ai_generation"
+ },
+ "documents": documents
+ }
+
+ return json.dumps(result, indent=2)
+
+ # Old _buildContinuationPrompt and _mergeJsonContent methods removed
+ # Now handled by repair mechanism in jsonUtils.py and section accumulation
# Planning AI Call
@@ -429,7 +409,8 @@ CRITICAL REQUIREMENTS:
extracted_content = None
logger.debug(f"[DEBUG] title value: {title}, type: {type(title)}")
from modules.services.serviceGeneration.subPromptBuilderGeneration import buildGenerationPrompt
- generation_prompt = await buildGenerationPrompt(outputFormat, prompt, title, extracted_content)
+ # First call without continuation context
+ generation_prompt = await buildGenerationPrompt(outputFormat, prompt, title, extracted_content, None)
generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation")
# Parse the generated JSON (extract fenced/embedded JSON first)
diff --git a/modules/services/serviceAi/subDocumentProcessing.py b/modules/services/serviceAi/subDocumentProcessing.py
index 75eb5841..d6f390ac 100644
--- a/modules/services/serviceAi/subDocumentProcessing.py
+++ b/modules/services/serviceAi/subDocumentProcessing.py
@@ -291,7 +291,6 @@ class SubDocumentProcessing:
Build a prompt that includes partial results continuation instructions.
NOTE: This uses a different continuation pattern than SubCoreAi:
- - SubCoreAi uses "continuation": null/dict for generic JSON responses
- This uses "continue": true/false + "continuation_context" for document sections
- Kept separate because it's tightly coupled to document processing needs
"""
diff --git a/modules/services/serviceGeneration/subPromptBuilderGeneration.py b/modules/services/serviceGeneration/subPromptBuilderGeneration.py
index ca0ea575..0cf32bf7 100644
--- a/modules/services/serviceGeneration/subPromptBuilderGeneration.py
+++ b/modules/services/serviceGeneration/subPromptBuilderGeneration.py
@@ -4,6 +4,7 @@ This module builds prompts for generating documents from extracted content.
"""
import logging
+from typing import Dict, Any
logger = logging.getLogger(__name__)
@@ -34,8 +35,7 @@ TEMPLATE_JSON_DOCUMENT_GENERATION = """{
}
]
}
- ],
- "continuation": null
+ ]
}"""
@@ -43,38 +43,83 @@ async def buildGenerationPrompt(
outputFormat: str,
userPrompt: str,
title: str,
- extracted_content: str = None
+ extracted_content: str = None,
+ continuationContext: Dict[str, Any] = None
) -> str:
"""
Build the unified generation prompt using a single JSON template.
+ Simplified version without continuation logic in prompt.
Args:
outputFormat: Target output format (html, pdf, docx, etc.)
userPrompt: User's original prompt for document generation
title: Title for the document
extracted_content: Optional extracted content from documents to prepend to prompt
+ continuationContext: Optional context from previous generation for continuation
Returns:
Complete generation prompt string
"""
# Create a template - let AI generate title if not provided
- prompt_instruction = f"Use the following title: \"{title}\""
- json_template = TEMPLATE_JSON_DOCUMENT_GENERATION.replace("{{DOCUMENT_TITLE}}", title)
+ title_value = title if title else "Generated Document"
+ json_template = TEMPLATE_JSON_DOCUMENT_GENERATION.replace("{{DOCUMENT_TITLE}}", title_value)
- # Always use the proper generation prompt template with LOOP_INSTRUCTION
- generation_prompt = f"""Generate structured JSON content for document creation.
+ # Check if this is a continuation request
+ if continuationContext and continuationContext.get("section_count", 0) > 0:
+ # Continuation prompt - simple and focused
+ section_count = continuationContext.get("section_count", 0)
+ next_order = continuationContext.get("next_order", 1)
+ last_content_sample = continuationContext.get("last_content_sample", "")
+
+ generation_prompt = f"""Continue generating structured JSON content.
-USER CONTEXT: "{userPrompt}"
+ORIGINAL REQUEST: "{userPrompt}"
TARGET FORMAT: {outputFormat}
-TITLE INSTRUCTION: {prompt_instruction}
+TITLE: "{title_value}"
-LOOP_INSTRUCTION
+CONTEXT - Already generated:
+- Total sections generated: {section_count}
+- Next section order: {next_order}
+- Last content: {last_content_sample}
+
+YOUR TASK:
+Continue where previous generation stopped.
+Generate the NEXT section(s) starting with section_{next_order}.
+Generate as much content as possible.
RULES:
-- Follow the template structure below exactly; emit only one JSON object in the response
-- Fill sections with content based on the user request
-- Use appropriate content_type
+- Follow the JSON template structure below exactly
+- Fill sections with ACTUAL data based on the user request
+- Use appropriate content_type for the data
+- Generate REAL content, not summaries or placeholders
+- Generate multiple sections if possible
+Return raw JSON (no ```json blocks, no text before/after)
+
+JSON Template
+{json_template}
+"""
+ else:
+ # First call - simple prompt without continuation complexity
+ generation_prompt = f"""Generate structured JSON content for document creation.
+
+USER REQUEST: "{userPrompt}"
+TARGET FORMAT: {outputFormat}
+TITLE: "{title_value}"
+
+INSTRUCTIONS:
+- Follow the JSON template structure below exactly
+- Emit only one JSON object in the response
+- Fill sections with ACTUAL data based on the user request
+- Use appropriate content_type for each section
+- Generate REAL content, not summaries or instructions
+- Structure content in sections with order 1, 2, 3...
+- Each section should be complete before next
+- Generate as much content as possible
+
+Return raw JSON (no ```json blocks, no text before/after)
+
+JSON Template
{json_template}
"""
diff --git a/modules/shared/jsonUtils.py b/modules/shared/jsonUtils.py
index 8a236182..92c6dd84 100644
--- a/modules/shared/jsonUtils.py
+++ b/modules/shared/jsonUtils.py
@@ -135,3 +135,222 @@ def mergeRootLists(json_parts: List[Union[str, Dict, List]]) -> Dict[str, Any]:
return base
+def repairBrokenJson(text: str) -> Optional[Dict[str, Any]]:
+ """
+ Attempt to repair broken JSON using multiple strategies.
+ Returns the best repair attempt or None if all fail.
+ """
+ if not text:
+ return None
+
+ # Strategy 1: Progressive parsing - try to find longest valid prefix
+ best_result = None
+ best_valid_length = 0
+
+ for i in range(len(text), 0, -1):
+ test_str = text[:i]
+ closed_str = _closeJsonStructures(test_str)
+ obj, err, _ = tryParseJson(closed_str)
+ if err is None and isinstance(obj, dict):
+ best_result = obj
+ best_valid_length = i
+ logger.debug(f"Progressive parsing success at length {i}")
+ break
+
+ if best_result:
+ logger.info(f"Repaired JSON using progressive parsing (valid length: {best_valid_length})")
+ return best_result
+
+ # Strategy 2: Structure closing - close incomplete structures
+ closed_str = _closeJsonStructures(text)
+ obj, err, _ = tryParseJson(closed_str)
+ if err is None and isinstance(obj, dict):
+ logger.info("Repaired JSON using structure closing")
+ return obj
+
+ # Strategy 3: Regex extraction (fallback for completely broken JSON)
+ extracted = _extractSectionsRegex(text)
+ if extracted:
+ logger.info("Repaired JSON using regex extraction")
+ return {"documents": [{"sections": extracted}]}
+
+ logger.warning("All repair strategies failed")
+ return None
+
+
+def _closeJsonStructures(text: str) -> str:
+ """
+ Close incomplete JSON structures by adding missing closing brackets.
+ """
+ if not text:
+ return text
+
+ # Count open/close brackets and braces
+ open_braces = text.count('{')
+ close_braces = text.count('}')
+ open_brackets = text.count('[')
+ close_brackets = text.count(']')
+
+ # Close incomplete structures
+ result = text
+ for _ in range(open_braces - close_braces):
+ result += '}'
+ for _ in range(open_brackets - close_brackets):
+ result += ']'
+
+ return result
+
+
+def _extractSectionsRegex(text: str) -> List[Dict[str, Any]]:
+ """
+ Extract sections from broken JSON using regex patterns.
+ Fallback strategy when JSON is completely corrupted.
+ """
+ import re
+
+ sections = []
+
+ # Pattern to find section objects
+ section_pattern = r'"id"\s*:\s*"(section_\d+)"\s*,?\s*"content_type"\s*:\s*"(\w+)"\s*,?\s*"order"\s*:\s*(\d+)'
+
+ for match in re.finditer(section_pattern, text, re.IGNORECASE):
+ section_id = match.group(1)
+ content_type = match.group(2)
+ order = int(match.group(3))
+
+ # Try to extract elements array
+ elements_match = re.search(
+ r'"elements"\s*:\s*\[(.*?)\]',
+ text[match.end():match.end()+500] # Look ahead for elements
+ )
+
+ elements = []
+ if elements_match:
+ try:
+ elements_str = '[' + elements_match.group(1) + ']'
+ elements = json.loads(elements_str)
+ except:
+ pass
+
+ sections.append({
+ "id": section_id,
+ "content_type": content_type,
+ "elements": elements,
+ "order": order
+ })
+
+ return sections
+
+
+def extractSectionsFromDocument(documentData: Dict[str, Any]) -> List[Dict[str, Any]]:
+ """
+ Extract all sections from document data structure.
+ Handles both flat and nested document structures.
+ """
+ if not isinstance(documentData, dict):
+ return []
+
+ # Try to extract sections from documents array
+ if "documents" in documentData:
+ all_sections = []
+ for doc in documentData.get("documents", []):
+ if isinstance(doc, dict) and "sections" in doc:
+ sections = doc.get("sections", [])
+ if isinstance(sections, list):
+ all_sections.extend(sections)
+ return all_sections
+
+ # Try to extract sections directly from root
+ if "sections" in documentData:
+ sections = documentData.get("sections", [])
+ if isinstance(sections, list):
+ return sections
+
+ return []
+
+
+def extractContentSample(section: Dict[str, Any]) -> str:
+ """
+ Extract a sample of content from a section for continuation context.
+ Returns a string describing the last content for context.
+ """
+ if not isinstance(section, dict):
+ return ""
+
+ content_type = section.get("content_type", "").lower()
+ elements = section.get("elements", [])
+
+ if not elements or not isinstance(elements, list):
+ return "Content exists"
+
+ # Get last elements for sampling
+ sample_elements = elements[-5:] if len(elements) > 5 else elements
+
+ if content_type == "list":
+ # Extract last few list items
+ items_text = []
+ for elem in sample_elements:
+ if isinstance(elem, dict) and "text" in elem:
+ items_text.append(elem.get("text", ""))
+ if items_text:
+ return f"Last {len(items_text)} items: {', '.join(items_text[:3])}"
+
+ elif content_type == "paragraph":
+ # Extract text and take last 150 chars
+ for elem in sample_elements:
+ if isinstance(elem, dict) and "text" in elem:
+ text = elem.get("text", "")
+ if len(text) > 150:
+ text = "..." + text[-150:]
+ return f"Last content: {text}"
+
+ elif content_type == "code":
+ # Extract last few lines
+ for elem in sample_elements:
+ if isinstance(elem, dict) and "code" in elem:
+ code = elem.get("code", "")
+ lines = code.split('\n')
+ if len(lines) > 5:
+ return f"Last lines ({len(lines)} total): {', '.join(lines[-3:])}"
+ return f"Code ({len(lines)} lines)"
+
+ elif content_type == "table":
+ # Extract last rows
+ for elem in sample_elements:
+ if isinstance(elem, dict) and "rows" in elem:
+ rows = elem.get("rows", [])
+ return f"Table with {len(rows)} rows"
+
+ return "Content exists"
+
+
+def buildContinuationContext(allSections: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """
+ Build context information from accumulated sections for continuation prompt.
+ Returns dict with metadata about what was already generated.
+ """
+ if not allSections:
+ return {
+ "section_count": 0,
+ "next_order": 1,
+ "last_content_sample": "No content yet"
+ }
+
+ # Sort sections by order
+ sorted_sections = sorted(allSections, key=lambda s: s.get("order", 0))
+
+ last_section = sorted_sections[-1]
+ last_order = last_section.get("order", 0)
+
+ # Get content sample from last section
+ last_content_sample = extractContentSample(last_section)
+
+ return {
+ "section_count": len(allSections),
+ "last_section_id": last_section.get("id", ""),
+ "last_order": last_order,
+ "next_order": last_order + 1,
+ "last_content_type": last_section.get("content_type", ""),
+ "last_content_sample": last_content_sample
+ }
+
diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py
index 961bc562..a5dccd9b 100644
--- a/modules/workflows/methods/methodOutlook.py
+++ b/modules/workflows/methods/methodOutlook.py
@@ -1178,11 +1178,9 @@ Return JSON:
{{
"subject": "subject line",
"body": "email body (HTML allowed)",
- "attachments": ["doc_ref1", "doc_ref2"],
- "continuation": null
+ "attachments": ["doc_ref1", "doc_ref2"]
}}
-
-LOOP_INSTRUCTION"""
+"""
# Call AI service to generate email content
try:
diff --git a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
index 76413d24..ac5732ef 100644
--- a/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
+++ b/modules/workflows/processing/shared/promptGenerationActionsActionplan.py
@@ -78,8 +78,7 @@ 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,8 +94,7 @@ Generate the next action to advance toward completing the task objective.
"description": "Extract data from documents",
"userMessage": "Extracting data from documents"
}
- ],
- "continuation": null
+ ]
}
```
@@ -125,7 +123,6 @@ Generate the next action to advance toward completing the task objective.
## 🚀 Response Format
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
"""
return PromptBundle(prompt=template, placeholders=placeholders)
@@ -137,7 +134,7 @@ def generateResultReviewPrompt(context: Any) -> PromptBundle:
PromptPlaceholder(label="REVIEW_CONTENT", content=extractReviewContent(context), summaryAllowed=True),
]
- template = """# Result Review & Validation
+ template = f"""# Result Review & Validation
Review task execution outcomes and determine success, retry needs, or failure.
@@ -166,7 +163,7 @@ def generateResultReviewPrompt(context: Any) -> PromptBundle:
## 📊 Required JSON Structure
```json
- {
+ {{
"status": "success|retry|failed",
"reason": "Detailed explanation of the validation decision",
"improvements": ["specific improvement 1", "specific improvement 2"],
@@ -174,9 +171,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}}'",
- "continuation": null
- }
+ "userMessage": "User-friendly message explaining the validation result in language '{{KEY:USER_LANGUAGE}}'"
+ }}
```
## 🎯 Validation Principles
@@ -232,9 +228,6 @@ def generateResultReviewPrompt(context: Any) -> PromptBundle:
- "Include user language parameter for better localization"
- "Break down complex objective into smaller, focused actions"
- "Verify document references before processing"
-
-
-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 702a7868..31b48a02 100644
--- a/modules/workflows/processing/shared/promptGenerationTaskplan.py
+++ b/modules/workflows/processing/shared/promptGenerationTaskplan.py
@@ -72,21 +72,20 @@ Break down user requests into logical, executable task steps.
## 📊 Required JSON Structure
```json
-{
+{{
"overview": "Brief description of the overall plan",
"userMessage": "User-friendly message explaining the task plan in language '{{KEY:USER_LANGUAGE}}'",
"tasks": [
- {
+ {{
"id": "task_1",
"objective": "Clear business objective focusing on what to deliver",
"dependencies": ["task_0"],
"success_criteria": ["measurable criteria 1", "measurable criteria 2"],
"estimated_complexity": "low|medium|high",
"userMessage": "What this task will accomplish in language '{{KEY:USER_LANGUAGE}}'"
- }
+ }}
],
- "continuation": null
-}
+}}
```
## 🎯 Task Structure Guidelines
@@ -127,7 +126,5 @@ Break down user requests into logical, executable task steps.
- **Low**: Simple, single-action tasks (1-2 actions)
- **Medium**: Multi-action tasks for one topic (3-5 actions)
- **High**: Complex strategic tasks (6+ actions)
-
-LOOP_INSTRUCTION
"""
return PromptBundle(prompt=template, placeholders=placeholders)