implemented enhanced standardized ai looping system with usecases

This commit is contained in:
ValueOn AG 2026-01-04 21:38:26 +01:00
parent 64590aa61e
commit 1c0604106b
7 changed files with 610 additions and 584 deletions

View file

@ -6,8 +6,6 @@ from enum import Enum
# Import ContentPart for runtime use (needed for Pydantic model rebuilding)
from modules.datamodels.datamodelExtraction import ContentPart
# Import JSON utilities for safe conversion
from modules.shared.jsonUtils import extractJsonString, tryParseJson, repairBrokenJson
# Operation Types
class OperationTypeEnum(str, Enum):
@ -258,3 +256,53 @@ class JsonAccumulationState(BaseModel):
description="KPI definitions with current values: [{id, description, jsonPath, targetValue, currentValue}, ...]"
)
class ContinuationContext(BaseModel):
"""Pydantic model for continuation context information."""
section_count: int
delivered_summary: str
cut_off_element: Optional[str] = None
element_before_cutoff: Optional[str] = None
template_structure: Optional[str] = None
last_complete_part: Optional[str] = None
incomplete_part: Optional[str] = None
structure_context: Optional[str] = None
last_raw_json: Optional[str] = None
class SectionPromptArgs(BaseModel):
"""Type-safe arguments for section content prompt builder."""
section: Dict[str, Any]
contentParts: List[ContentPart]
userPrompt: str
generationHint: str
allSections: List[Dict[str, Any]]
sectionIndex: int
isAggregation: bool
language: str
class ChapterStructurePromptArgs(BaseModel):
"""Type-safe arguments for chapter structure prompt builder."""
userPrompt: str
contentParts: List[ContentPart] = Field(default_factory=list)
outputFormat: str
class CodeContentPromptArgs(BaseModel):
"""Type-safe arguments for code content prompt builder."""
filename: str
fileType: str
functions: List[Dict] = Field(default_factory=list)
classes: List[Dict] = Field(default_factory=list)
dependencies: List[str] = Field(default_factory=list)
metadata: Dict[str, Any] = Field(default_factory=dict)
userPrompt: str
contentParts: List[ContentPart] = Field(default_factory=list)
contextInfo: str = ""
class CodeStructurePromptArgs(BaseModel):
"""Type-safe arguments for code structure prompt builder."""
userPrompt: str
contentParts: List[ContentPart] = Field(default_factory=list)

View file

@ -12,7 +12,9 @@ import json
import logging
from typing import Dict, Any, List, Optional, Callable
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum, JsonAccumulationState
from modules.datamodels.datamodelAi import (
AiCallRequest, AiCallOptions
)
from modules.datamodels.datamodelExtraction import ContentPart
from modules.shared.jsonUtils import buildContinuationContext, extractJsonString, tryParseJson
from modules.services.serviceAi.subJsonResponseHandling import JsonResponseHandler
@ -110,18 +112,38 @@ class AiCallLooper:
# CRITICAL: Build continuation prompt if we have sections OR if we have a previous response (even if broken)
# This ensures continuation prompts are built even when JSON is so broken that no sections can be extracted
if (len(allSections) > 0 or lastRawResponse) and promptBuilder and promptArgs:
# Extract templateStructure and basePrompt from promptArgs (REQUIRED)
templateStructure = promptArgs.get("templateStructure")
if not templateStructure:
raise ValueError(
f"templateStructure is REQUIRED in promptArgs for use case '{useCaseId}'. "
"Prompt creation functions must return (prompt, templateStructure) tuple."
)
basePrompt = promptArgs.get("basePrompt")
if not basePrompt:
# Fallback: use prompt parameter (should be the same)
basePrompt = prompt
logger.warning(
f"basePrompt not found in promptArgs for use case '{useCaseId}', "
"using prompt parameter instead. This may indicate a bug."
)
# This is a continuation - build continuation context with raw JSON and rebuild prompt
continuationContext = buildContinuationContext(allSections, lastRawResponse, useCaseId)
continuationContext = buildContinuationContext(
allSections, lastRawResponse, useCaseId, templateStructure
)
if not lastRawResponse:
logger.warning(f"Iteration {iteration}: No previous response available for continuation!")
# Unified prompt builder call: All prompt builders accept continuationContext and **kwargs
# Each builder extracts only the parameters it needs from kwargs
# This ensures consistent architecture across all use cases
if not promptArgs.get('services') and hasattr(self, 'services'):
promptArgs['services'] = self.services
iterationPrompt = await promptBuilder(continuationContext=continuationContext, **promptArgs)
# Unified prompt builder call: Continuation builders only need continuationContext, templateStructure, and basePrompt
# All initial context (section, userPrompt, etc.) is already in basePrompt, so promptArgs is not needed
# Extract templateStructure and basePrompt from promptArgs (they're explicit parameters)
iterationPrompt = await promptBuilder(
continuationContext=continuationContext,
templateStructure=templateStructure,
basePrompt=basePrompt
)
else:
# First iteration - use original prompt
iterationPrompt = prompt
@ -238,11 +260,10 @@ class AiCallLooper:
pass
# Handle use cases that return JSON directly (no section extraction needed)
directReturnUseCases = ["section_content", "chapter_structure", "code_structure", "code_content"]
if useCaseId in directReturnUseCases:
# For chapter_structure, code_structure, section_content, and code_content, check completeness and support looping
loopingUseCases = ["chapter_structure", "code_structure", "section_content", "code_content"]
if useCaseId in loopingUseCases:
# Check if use case supports direct return (all registered use cases do)
if useCase and not useCase.requiresExtraction:
# For all direct return use cases, check completeness and support looping
if True: # All registered use cases support looping
# CRITICAL: Check if JSON string is incomplete BEFORE parsing
# If JSON is truncated, it will be closed for parsing, making it appear complete
# So we need to check the original string, not the parsed JSON
@ -310,7 +331,8 @@ class AiCallLooper:
extracted = extractJsonString(mergedJsonString)
parsed, parseErr, _ = tryParseJson(extracted)
if parseErr is None and parsed:
normalized = self._normalizeJsonStructure(parsed, useCaseId)
# Use callback to normalize JSON structure
normalized = self._normalizeJsonStructure(parsed, useCase)
parsedJsonForUseCase = normalized
result = json.dumps(normalized, indent=2, ensure_ascii=False)
except Exception:
@ -322,8 +344,8 @@ class AiCallLooper:
parsed, parseErr, _ = tryParseJson(extracted)
if parseErr is None and parsed:
# Parsing succeeded - normalize and use
normalized = self._normalizeJsonStructure(parsed, useCaseId)
# Parsing succeeded - normalize and use (via callback)
normalized = self._normalizeJsonStructure(parsed, useCase)
parsedJsonForUseCase = normalized
result = json.dumps(normalized, indent=2, ensure_ascii=False)
else:
@ -334,7 +356,8 @@ class AiCallLooper:
extracted = extractJsonString(jsonStr)
parsed, parseErr, _ = tryParseJson(extracted)
if parseErr is None and parsed:
normalized = self._normalizeJsonStructure(parsed, useCaseId)
# Use callback to normalize JSON structure
normalized = self._normalizeJsonStructure(parsed, useCase)
allParsed.append(normalized)
if allParsed:
@ -399,18 +422,16 @@ class AiCallLooper:
if iterationOperationId:
self.services.chat.progressLogFinish(iterationOperationId, True)
# For section_content, return raw result to allow merging of multiple JSON blocks
# The merging logic in subStructureFilling.py will handle extraction and merging
if useCaseId == "section_content":
final_json = result # Return raw response to preserve all JSON blocks
# Write final merged result for section_content (overwrites iteration 1 response with complete merged result)
self.services.utils.writeDebugFile(final_json, f"{debugPrefix}_response")
else:
final_json = json.dumps(parsedJsonForUseCase, indent=2, ensure_ascii=False) if parsedJsonForUseCase else (extractedJsonForUseCase or result)
# Write final result for chapter structure and code structure
if useCaseId in ["chapter_structure", "code_structure"]:
self.services.utils.writeDebugFile(final_json, f"{debugPrefix}_final_result")
# Use callback to handle final result formatting and debug file writing (REQUIRED - no fallback)
if not useCase.finalResultHandler:
raise ValueError(
f"Use case '{useCaseId}' is missing required 'finalResultHandler' callback. "
"All use cases must provide a finalResultHandler function."
)
final_json = useCase.finalResultHandler(
result, parsedJsonForUseCase, extractedJsonForUseCase,
debugPrefix, self.services
)
return final_json
@ -423,8 +444,8 @@ class AiCallLooper:
if iteration >= maxIterations:
logger.warning(f"AI call stopped after maximum iterations ({maxIterations})")
# This code path is never reached because all use cases are in directReturnUseCases
# and return early at line 417. This code would only execute for use cases that
# This code path should never be reached because all registered use cases
# return early when JSON is complete. This would only execute for use cases that
# require section extraction, but no such use cases are currently registered.
logger.error(f"Unexpected code path: reached end of loop without return for use case '{useCaseId}'")
return result if result else ""
@ -539,51 +560,23 @@ class AiCallLooper:
# Doesn't parse even after closing - might be malformed, but assume incomplete to be safe
return True
def _normalizeJsonStructure(self, parsed: Any, useCaseId: str) -> Any:
def _normalizeJsonStructure(self, parsed: Any, useCase) -> Any:
"""
Normalize JSON structure to ensure consistent format before merging.
Handles different response formats and converts them to expected structure.
Args:
parsed: Parsed JSON object (can be dict, list, or primitive)
useCaseId: Use case ID to determine expected structure
useCase: LoopingUseCase instance with jsonNormalizer callback
Returns:
Normalized JSON structure
"""
# For section_content, expect {"elements": [...]} structure
if useCaseId == "section_content":
if isinstance(parsed, list):
# Check if list contains strings (invalid format) or element objects
if parsed and isinstance(parsed[0], str):
# Invalid format - list of strings instead of elements
# Try to convert strings to paragraph elements as fallback
# This can happen if AI returns raw text instead of structured JSON
logger.debug(f"Received list of strings instead of elements array, converting to paragraph elements")
elements = []
for text in parsed:
if isinstance(text, str) and text.strip():
elements.append({
"type": "paragraph",
"content": {
"text": text.strip()
}
})
return {"elements": elements} if elements else {"elements": []}
else:
# Convert plain list of elements to elements structure
return {"elements": parsed}
elif isinstance(parsed, dict):
# If it already has "elements", return as-is
if "elements" in parsed:
return parsed
# If it has "type" and looks like an element, wrap in elements array
elif parsed.get("type"):
return {"elements": [parsed]}
# Otherwise, assume it's already in correct format
else:
return parsed
# For other use cases, return as-is (they have their own structures)
return parsed
# Use callback to normalize JSON structure (REQUIRED - no fallback)
if not useCase or not useCase.jsonNormalizer:
raise ValueError(
f"Use case '{useCase.useCaseId if useCase else 'unknown'}' is missing required 'jsonNormalizer' callback. "
"All use cases must provide a jsonNormalizer function."
)
return useCase.jsonNormalizer(parsed, useCase.useCaseId)

View file

@ -12,6 +12,89 @@ from typing import Dict, Any, List, Optional, Callable
logger = logging.getLogger(__name__)
# Callback functions for use-case-specific logic
def _handleSectionContentFinalResult(result: str, parsedJsonForUseCase: Any, extractedJsonForUseCase: str,
debugPrefix: str, services: Any) -> str:
"""Handle final result for section_content: return raw result to preserve all JSON blocks."""
final_json = result # Return raw response to preserve all JSON blocks
# Write final merged result for section_content (overwrites iteration 1 response with complete merged result)
if services and hasattr(services, 'utils') and hasattr(services.utils, 'writeDebugFile'):
services.utils.writeDebugFile(final_json, f"{debugPrefix}_response")
return final_json
def _handleChapterStructureFinalResult(result: str, parsedJsonForUseCase: Any, extractedJsonForUseCase: str,
debugPrefix: str, services: Any) -> str:
"""Handle final result for chapter_structure: format JSON and write debug file."""
import json
final_json = json.dumps(parsedJsonForUseCase, indent=2, ensure_ascii=False) if parsedJsonForUseCase else (extractedJsonForUseCase or result)
# Write final result for chapter structure
if services and hasattr(services, 'utils') and hasattr(services.utils, 'writeDebugFile'):
services.utils.writeDebugFile(final_json, f"{debugPrefix}_final_result")
return final_json
def _handleCodeStructureFinalResult(result: str, parsedJsonForUseCase: Any, extractedJsonForUseCase: str,
debugPrefix: str, services: Any) -> str:
"""Handle final result for code_structure: format JSON and write debug file."""
import json
final_json = json.dumps(parsedJsonForUseCase, indent=2, ensure_ascii=False) if parsedJsonForUseCase else (extractedJsonForUseCase or result)
# Write final result for code structure
if services and hasattr(services, 'utils') and hasattr(services.utils, 'writeDebugFile'):
services.utils.writeDebugFile(final_json, f"{debugPrefix}_final_result")
return final_json
def _handleCodeContentFinalResult(result: str, parsedJsonForUseCase: Any, extractedJsonForUseCase: str,
debugPrefix: str, services: Any) -> str:
"""Handle final result for code_content: format JSON."""
import json
final_json = json.dumps(parsedJsonForUseCase, indent=2, ensure_ascii=False) if parsedJsonForUseCase else (extractedJsonForUseCase or result)
return final_json
def _normalizeSectionContentJson(parsed: Any, useCaseId: str) -> Any:
"""Normalize JSON structure for section_content use case."""
# For section_content, expect {"elements": [...]} structure
if isinstance(parsed, list):
# Check if list contains strings (invalid format) or element objects
if parsed and isinstance(parsed[0], str):
# Invalid format - list of strings instead of elements
# Try to convert strings to paragraph elements as fallback
logger.debug(f"Received list of strings instead of elements array, converting to paragraph elements")
elements = []
for text in parsed:
if isinstance(text, str) and text.strip():
elements.append({
"type": "paragraph",
"content": {
"text": text.strip()
}
})
return {"elements": elements} if elements else {"elements": []}
else:
# Convert plain list of elements to elements structure
return {"elements": parsed}
elif isinstance(parsed, dict):
# If it already has "elements", return as-is
if "elements" in parsed:
return parsed
# If it has "type" and looks like an element, wrap in elements array
elif parsed.get("type"):
return {"elements": [parsed]}
# Otherwise, assume it's already in correct format
else:
return parsed
# For other use cases, return as-is (they have their own structures)
return parsed
def _normalizeDefaultJson(parsed: Any, useCaseId: str) -> Any:
"""Default normalizer: return as-is."""
return parsed
@dataclass
class LoopingUseCase:
@ -39,6 +122,10 @@ class LoopingUseCase:
# Result Building
resultBuilder: Optional[Callable] = None # Build final result from accumulated data
# Use-case-specific handlers (callbacks to avoid if/elif chains in generic code)
finalResultHandler: Optional[Callable] = None # Handle final result formatting and debug file writing
jsonNormalizer: Optional[Callable] = None # Normalize JSON structure for this use case
# Metadata
supportsAccumulation: bool = True # Whether this use case supports accumulation
requiresExtraction: bool = False # Whether this requires extraction (like sections)
@ -124,6 +211,8 @@ class LoopingUseCaseRegistry:
merger=None,
continuationContextBuilder=None, # Will use default continuation context
resultBuilder=None, # Return JSON directly
finalResultHandler=_handleSectionContentFinalResult,
jsonNormalizer=_normalizeSectionContentJson,
supportsAccumulation=False,
requiresExtraction=False
))
@ -141,6 +230,8 @@ class LoopingUseCaseRegistry:
merger=None,
continuationContextBuilder=None,
resultBuilder=None, # Return JSON directly
finalResultHandler=_handleChapterStructureFinalResult,
jsonNormalizer=_normalizeDefaultJson,
supportsAccumulation=False,
requiresExtraction=False
))
@ -174,6 +265,8 @@ class LoopingUseCaseRegistry:
merger=None,
continuationContextBuilder=None,
resultBuilder=None,
finalResultHandler=_handleCodeStructureFinalResult,
jsonNormalizer=_normalizeDefaultJson,
supportsAccumulation=False,
requiresExtraction=False
))
@ -190,6 +283,8 @@ class LoopingUseCaseRegistry:
merger=None, # Will use default merger
continuationContextBuilder=None,
resultBuilder=None, # Will use default result builder
finalResultHandler=_handleCodeContentFinalResult,
jsonNormalizer=_normalizeDefaultJson,
supportsAccumulation=True,
requiresExtraction=False
))

View file

@ -753,7 +753,7 @@ class StructureFiller:
if processedExtractedParts:
logger.debug(f"Section {sectionId}: Aggregating {len(processedExtractedParts)} extracted parts with AI")
isAggregation = True
generationPrompt = self._buildSectionGenerationPrompt(
generationPrompt, templateStructure = self._buildSectionGenerationPrompt(
section=section,
contentParts=processedExtractedParts,
userPrompt=userPrompt,
@ -811,106 +811,8 @@ class StructureFiller:
f"{chapterId}_section_{sectionId}_response"
)
else:
async def buildSectionPromptWithContinuation(
continuationContext: Dict[str, Any],
**kwargs
) -> str:
"""Build section prompt with continuation context. Extracts section-specific parameters from kwargs."""
# Extract parameters from kwargs (for section_content use case)
section = kwargs.get("section")
contentParts = kwargs.get("contentParts", [])
userPrompt = kwargs.get("userPrompt", "")
generationHint = kwargs.get("generationHint", "")
allSections = kwargs.get("allSections", [])
sectionIndex = kwargs.get("sectionIndex", 0)
isAggregation = kwargs.get("isAggregation", False)
basePrompt = self._buildSectionGenerationPrompt(
section=section,
contentParts=contentParts,
userPrompt=userPrompt,
generationHint=generationHint,
allSections=allSections,
sectionIndex=sectionIndex,
isAggregation=isAggregation,
language=language
)
# Extract JSON structure context for continuation
incompletePart = continuationContext.get("incomplete_part", "")
lastRawJson = continuationContext.get("last_raw_json", "")
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
# Get last 100 characters for overlap
overlapContext = lastRawJson[-100:].strip()
# Build unified context showing structure hierarchy with cut point
# This combines structure template, last complete part, and incomplete part in one view
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
# Try to find where JSON ends
breakPos = len(lastRawJson.rstrip())
else:
# No incomplete part found - assume end of JSON
breakPos = len(lastRawJson.rstrip())
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
# Fallback: use incomplete part directly
unifiedContext = incompletePart
else:
unifiedContext = "Unable to extract context - response was completely broken"
# Use the SAME template structure as in initial prompt
# Get contentType and contentStructureExample exactly like in _buildSectionGenerationPrompt
contentType = section.get("content_type", "paragraph")
contentStructureExample = self._getContentStructureExample(contentType)
# Build the exact same JSON structure template as in initial prompt
structureTemplate = f"""JSON Structure Template:
{{
"elements": [
{{
"type": "{contentType}",
"content": {contentStructureExample}
}}
]
}}
"""
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
{structureTemplate}Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
# Use consolidated class method
buildSectionPromptWithContinuation = self.buildSectionPromptWithContinuation
options = AiCallOptions(
operationType=operationType,
@ -932,7 +834,8 @@ CRITICAL:
"allSections": all_sections_list,
"sectionIndex": sectionIndex,
"isAggregation": isAggregation,
"services": self.services
"templateStructure": templateStructure,
"basePrompt": generationPrompt
},
operationId=sectionOperationId,
userPrompt=userPrompt,
@ -1038,7 +941,7 @@ CRITICAL:
if len(contentPartIds) == 0 and useAiCall and generationHint:
# Generate content from scratch using only generationHint
logger.debug(f"Processing section {sectionId}: No content parts, generating from generationHint only")
generationPrompt = self._buildSectionGenerationPrompt(
generationPrompt, templateStructure = self._buildSectionGenerationPrompt(
section=section,
contentParts=[],
userPrompt=userPrompt,
@ -1097,106 +1000,8 @@ CRITICAL:
else:
isAggregation = False
async def buildSectionPromptWithContinuation(
continuationContext: Dict[str, Any],
**kwargs
) -> str:
"""Build section prompt with continuation context. Extracts section-specific parameters from kwargs."""
# Extract parameters from kwargs (for section_content use case)
section = kwargs.get("section")
contentParts = kwargs.get("contentParts", [])
userPrompt = kwargs.get("userPrompt", "")
generationHint = kwargs.get("generationHint", "")
allSections = kwargs.get("allSections", [])
sectionIndex = kwargs.get("sectionIndex", 0)
isAggregation = kwargs.get("isAggregation", False)
basePrompt = self._buildSectionGenerationPrompt(
section=section,
contentParts=contentParts,
userPrompt=userPrompt,
generationHint=generationHint,
allSections=allSections,
sectionIndex=sectionIndex,
isAggregation=isAggregation,
language=language
)
# Extract JSON structure context for continuation
incompletePart = continuationContext.get("incomplete_part", "")
lastRawJson = continuationContext.get("last_raw_json", "")
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
# Get last 100 characters for overlap
overlapContext = lastRawJson[-100:].strip()
# Build unified context showing structure hierarchy with cut point
# This combines structure template, last complete part, and incomplete part in one view
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
# Try to find where JSON ends
breakPos = len(lastRawJson.rstrip())
else:
# No incomplete part found - assume end of JSON
breakPos = len(lastRawJson.rstrip())
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
# Fallback: use incomplete part directly
unifiedContext = incompletePart
else:
unifiedContext = "Unable to extract context - response was completely broken"
# Use the SAME template structure as in initial prompt
# Get contentType and contentStructureExample exactly like in _buildSectionGenerationPrompt
contentType = section.get("content_type", "paragraph")
contentStructureExample = self._getContentStructureExample(contentType)
# Build the exact same JSON structure template as in initial prompt
structureTemplate = f"""JSON Structure Template:
{{
"elements": [
{{
"type": "{contentType}",
"content": {contentStructureExample}
}}
]
}}
"""
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
{structureTemplate}Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
# Use consolidated class method
buildSectionPromptWithContinuation = self.buildSectionPromptWithContinuation
options = AiCallOptions(
operationType=operationType,
@ -1208,7 +1013,7 @@ CRITICAL:
prompt=generationPrompt,
options=options,
debugPrefix=f"{chapterId}_section_{sectionId}",
promptBuilder=buildSectionPromptWithContinuation,
promptBuilder=self.buildSectionPromptWithContinuation,
promptArgs={
"section": section,
"contentParts": [],
@ -1217,7 +1022,9 @@ CRITICAL:
"allSections": all_sections_list,
"sectionIndex": sectionIndex,
"isAggregation": isAggregation,
"services": self.services
"templateStructure": templateStructure,
"basePrompt": generationPrompt,
"language": language
},
operationId=sectionOperationId,
userPrompt=userPrompt,
@ -1399,7 +1206,7 @@ CRITICAL:
if useAiCall and generationHint:
# AI-Call mit einzelnen ContentPart (now may be text part after Vision extraction)
logger.debug(f"Processing section {sectionId}: Single extracted part with AI call")
generationPrompt = self._buildSectionGenerationPrompt(
generationPrompt, templateStructure = self._buildSectionGenerationPrompt(
section=section,
contentParts=[part],
userPrompt=userPrompt,
@ -1458,109 +1265,8 @@ CRITICAL:
else:
isAggregation = False
async def buildSectionPromptWithContinuation(
continuationContext: Dict[str, Any],
**kwargs
) -> str:
"""Build section prompt with continuation context. Extracts section-specific parameters from kwargs."""
# Extract parameters from kwargs (for section_content use case)
section = kwargs.get("section")
contentParts = kwargs.get("contentParts", [])
userPrompt = kwargs.get("userPrompt", "")
generationHint = kwargs.get("generationHint", "")
allSections = kwargs.get("allSections", [])
sectionIndex = kwargs.get("sectionIndex", 0)
isAggregation = kwargs.get("isAggregation", False)
services = kwargs.get("services")
basePrompt = self._buildSectionGenerationPrompt(
section=section,
contentParts=contentParts,
userPrompt=userPrompt,
generationHint=generationHint,
allSections=allSections,
sectionIndex=sectionIndex,
isAggregation=isAggregation,
language=language
)
# Extract JSON structure context for continuation
templateStructure = continuationContext.get("template_structure", "")
lastCompletePart = continuationContext.get("last_complete_part", "")
incompletePart = continuationContext.get("incomplete_part", "")
structureContext = continuationContext.get("structure_context", "")
lastRawJson = continuationContext.get("last_raw_json", "")
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
# Get last 100 characters for overlap
overlapContext = lastRawJson[-100:].strip()
# Build unified context showing structure hierarchy with cut point
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
# Try to find where JSON ends
breakPos = len(lastRawJson.rstrip())
else:
# No incomplete part found - assume end of JSON
breakPos = len(lastRawJson.rstrip())
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
# Fallback: use incomplete part directly
unifiedContext = incompletePart
else:
unifiedContext = "Unable to extract context - response was completely broken"
# Use the SAME template structure as in initial prompt
# Get contentType and contentStructureExample exactly like in _buildSectionGenerationPrompt
contentType = section.get("content_type", "paragraph")
contentStructureExample = self._getContentStructureExample(contentType)
# Build the exact same JSON structure template as in initial prompt
structureTemplate = f"""JSON Structure Template:
{{
"elements": [
{{
"type": "{contentType}",
"content": {contentStructureExample}
}}
]
}}
"""
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
{structureTemplate}Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
# Use consolidated class method
buildSectionPromptWithContinuation = self.buildSectionPromptWithContinuation
options = AiCallOptions(
operationType=operationType,
@ -1572,7 +1278,7 @@ CRITICAL:
prompt=generationPrompt,
options=options,
debugPrefix=f"{chapterId}_section_{sectionId}",
promptBuilder=buildSectionPromptWithContinuation,
promptBuilder=self.buildSectionPromptWithContinuation,
promptArgs={
"section": section,
"contentParts": [part],
@ -1581,7 +1287,10 @@ CRITICAL:
"allSections": all_sections_list,
"sectionIndex": sectionIndex,
"isAggregation": isAggregation,
"services": self.services
"services": self.services,
"templateStructure": templateStructure,
"basePrompt": generationPrompt,
"language": language
},
operationId=sectionOperationId,
userPrompt=userPrompt,
@ -2203,7 +1912,7 @@ Return only valid JSON. Do not include any explanatory text outside the JSON.
sectionIndex: Optional[int] = None,
isAggregation: bool = False,
language: str = "en"
) -> str:
) -> tuple[str, str]:
"""Baue Prompt für Section-Generierung mit vollständigem Kontext."""
# Filtere None-Werte
validParts = [p for p in contentParts if p is not None]
@ -2312,6 +2021,17 @@ Return only valid JSON. Do not include any explanatory text outside the JSON.
contentStructureExample = self._getContentStructureExample(contentType)
# Create template structure explicitly (not extracted from prompt)
# This ensures exact identity between initial and continuation prompts
templateStructure = f"""{{
"elements": [
{{
"type": "{contentType}",
"content": {contentStructureExample}
}}
]
}}"""
if isAggregation:
prompt = f"""# TASK: Generate Section Content (Aggregation)
@ -2459,7 +2179,78 @@ Output requirements:
## CONTEXT
{contextText if contextText else ""}
"""
return prompt
return prompt, templateStructure
async def buildSectionPromptWithContinuation(
self,
continuationContext: Any,
templateStructure: str,
basePrompt: str
) -> str:
"""Build section prompt with continuation context. Uses unified signature.
Single unified implementation for all section content generation contexts.
Note: All initial context (section, contentParts, userPrompt, etc.) is already
contained in basePrompt. This function only adds continuation-specific instructions.
"""
# Extract continuation context fields (only what's needed for continuation)
incompletePart = continuationContext.incomplete_part
lastRawJson = continuationContext.last_raw_json
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
overlapContext = lastRawJson[-100:].strip()
# Build unified context showing structure hierarchy with cut point
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
breakPos = len(lastRawJson.rstrip())
else:
breakPos = len(lastRawJson.rstrip())
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
unifiedContext = incompletePart
else:
unifiedContext = "Unable to extract context - response was completely broken"
# Build unified continuation prompt format
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
JSON Structure Template:
{templateStructure}
Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
def _extractAndMergeMultipleJsonBlocks(self, responseText: str, contentType: str, sectionId: str) -> List[Dict[str, Any]]:
"""

View file

@ -107,52 +107,80 @@ class StructureGenerator:
resultFormat="json"
)
structurePrompt, templateStructure = self._buildChapterStructurePrompt(
userPrompt=userPrompt,
contentParts=contentParts,
outputFormat=outputFormat
)
# Create prompt builder for continuation support
async def buildChapterStructurePromptWithContinuation(
continuationContext: Optional[Dict[str, Any]] = None,
**kwargs
continuationContext: Any,
templateStructure: str,
basePrompt: str
) -> str:
"""Build chapter structure prompt with optional continuation context. Extracts chapter-specific parameters from kwargs."""
# Extract parameters from kwargs (for chapter_structure use case)
userPrompt = kwargs.get("userPrompt", "")
contentParts = kwargs.get("contentParts", [])
outputFormat = kwargs.get("outputFormat", "txt")
"""Build chapter structure prompt with continuation context. Uses unified signature.
basePrompt = self._buildChapterStructurePrompt(
userPrompt=userPrompt,
contentParts=contentParts,
outputFormat=outputFormat
)
Note: All initial context (userPrompt, contentParts, outputFormat, etc.) is already
contained in basePrompt. This function only adds continuation-specific instructions.
"""
# Extract continuation context fields (only what's needed for continuation)
incompletePart = continuationContext.incomplete_part
lastRawJson = continuationContext.last_raw_json
if continuationContext:
# Add continuation instructions
deliveredSummary = continuationContext.get("delivered_summary", "")
elementBeforeCutoff = continuationContext.get("element_before_cutoff", "")
cutOffElement = continuationContext.get("cut_off_element", "")
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
overlapContext = lastRawJson[-100:].strip()
# Build unified context showing structure hierarchy with cut point
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
breakPos = len(lastRawJson.rstrip())
else:
breakPos = len(lastRawJson.rstrip())
continuationText = f"{deliveredSummary}\n\n"
continuationText += "⚠️ CONTINUATION: Response was cut off. Generate ONLY the remaining content that comes AFTER the reference elements below.\n\n"
if elementBeforeCutoff:
continuationText += "# REFERENCE: Last complete element (already delivered - DO NOT repeat):\n"
continuationText += f"{elementBeforeCutoff}\n\n"
if cutOffElement:
continuationText += "# REFERENCE: Incomplete element (cut off here - DO NOT repeat):\n"
continuationText += f"{cutOffElement}\n\n"
continuationText += "⚠️ CRITICAL: The elements above are REFERENCE ONLY. They are already delivered.\n"
continuationText += "Generate ONLY what comes AFTER these elements. DO NOT regenerate the entire JSON structure.\n"
continuationText += "Start directly with the next chapter that should follow.\n\n"
return f"""{basePrompt}
{continuationText}
Continue generating the remaining chapters now.
"""
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
unifiedContext = incompletePart
else:
return basePrompt
unifiedContext = "Unable to extract context - response was completely broken"
# Build unified continuation prompt format
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
JSON Structure Template:
{templateStructure}
Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
# Call AI with looping support
# NOTE: Do NOT pass contentParts here - we only need metadata for structure generation
@ -167,7 +195,8 @@ Continue generating the remaining chapters now.
promptArgs={
"userPrompt": userPrompt,
"outputFormat": outputFormat,
"services": self.services
"templateStructure": templateStructure,
"basePrompt": structurePrompt
},
useCaseId="chapter_structure", # REQUIRED: Explicit use case ID
operationId=structureOperationId,
@ -280,7 +309,7 @@ Continue generating the remaining chapters now.
userPrompt: str,
contentParts: List[ContentPart],
outputFormat: str
) -> str:
) -> tuple[str, str]:
"""Baue Prompt für Chapter-Struktur-Generierung."""
# Baue ContentParts-Index - filtere leere Parts heraus
contentPartsIndex = ""
@ -336,6 +365,36 @@ Continue generating the remaining chapters now.
language = self._getUserLanguage()
logger.debug(f"Using language from services (user intention analysis) for structure generation: {language}")
# Create template structure explicitly (not extracted from prompt)
# This ensures exact identity between initial and continuation prompts
templateStructure = f"""{{
"metadata": {{
"title": "Document Title",
"language": "{language}"
}},
"documents": [{{
"id": "doc_1",
"title": "Document Title",
"filename": "document.{outputFormat}",
"outputFormat": "{outputFormat}",
"language": "{language}",
"chapters": [
{{
"id": "chapter_1",
"level": 1,
"title": "Chapter Title",
"contentParts": {{
"extracted_part_id": {{
"instruction": "Use extracted content with ALL relevant details from user request"
}}
}},
"generationHint": "Detailed description including ALL relevant details from user request for this chapter",
"sections": []
}}
]
}}]
}}"""
prompt = f"""# TASK: Generate Chapter Structure
This is a PLANNING task. Return EXACTLY ONE complete JSON object. Do not generate multiple JSON objects, alternatives, or variations. Do not use separators like "---" between JSON objects.
@ -463,5 +522,5 @@ For each chapter, verify:
OUTPUT FORMAT: Start with {{ and end with }}. Do NOT use markdown code fences (```json). Do NOT add explanatory text before or after the JSON. Return ONLY the JSON object itself.
"""
return prompt
return prompt, templateStructure

View file

@ -233,6 +233,26 @@ class CodeGenerationPath:
if not contentPartsIndex:
contentPartsIndex = "\n(No content parts available)"
# Create template structure explicitly (not extracted from prompt)
templateStructure = f"""{{
"metadata": {{
"language": "{language}",
"projectType": "single_file|multi_file",
"projectName": ""
}},
"files": [
{{
"id": "",
"filename": "",
"fileType": "",
"dependencies": [],
"imports": [],
"functions": [],
"classes": []
}}
]
}}"""
# Build structure generation prompt
structurePrompt = f"""# TASK: Generate Code Project Structure
@ -302,6 +322,75 @@ For single-file projects, return one file. For multi-file projects, include ALL
Return ONLY valid JSON matching the request above.
"""
# Build continuation prompt builder
async def buildCodeStructurePromptWithContinuation(
continuationContext: Any,
templateStructure: str,
basePrompt: str
) -> str:
"""Build code structure prompt with continuation context. Uses unified signature.
Note: All initial context (userPrompt, contentParts, etc.) is already
contained in basePrompt. This function only adds continuation-specific instructions.
"""
# Extract continuation context fields (only what's needed for continuation)
incompletePart = continuationContext.incomplete_part
lastRawJson = continuationContext.last_raw_json
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
overlapContext = lastRawJson[-100:].strip()
# Build unified context showing structure hierarchy with cut point
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
breakPos = len(lastRawJson.rstrip())
else:
breakPos = len(lastRawJson.rstrip())
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
unifiedContext = incompletePart
else:
unifiedContext = "Unable to extract context - response was completely broken"
# Build unified continuation prompt format
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
JSON Structure Template:
{templateStructure}
Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
# Use generic looping system with code_structure use case
options = AiCallOptions(
operationType=OperationTypeEnum.DATA_GENERATE,
@ -311,6 +400,13 @@ Return ONLY valid JSON matching the request above.
structureJson = await self.services.ai.callAiWithLooping(
prompt=structurePrompt,
options=options,
promptBuilder=buildCodeStructurePromptWithContinuation,
promptArgs={
"userPrompt": userPrompt,
"contentParts": contentParts,
"templateStructure": templateStructure,
"basePrompt": structurePrompt
},
useCaseId="code_structure",
debugPrefix="code_structure_generation",
contentParts=contentParts
@ -640,6 +736,18 @@ Return ONLY valid JSON matching the request above.
```
"""
# Create template structure explicitly (not extracted from prompt)
templateStructure = f"""{{
"files": [
{{
"filename": "{filename}",
"content": "// Complete code here",
"functions": {json.dumps(functions, indent=2) if functions else '[]'},
"classes": {json.dumps(classes, indent=2) if classes else '[]'}
}}
]
}}"""
# Build base prompt
contentPrompt = f"""# TASK: Generate Code File Content
@ -667,141 +775,77 @@ Generate complete, production-ready code with:
5. Type hints where appropriate
Return ONLY valid JSON in this format:
{{
"files": [
{{
"filename": "{filename}",
"content": "// Complete code here",
"functions": {json.dumps(functions, indent=2) if functions else '[]'},
"classes": {json.dumps(classes, indent=2) if classes else '[]'}
}}
]
}}
{templateStructure}
"""
# Build continuation prompt builder
async def buildCodeContentPromptWithContinuation(
continuationContext: Optional[Dict[str, Any]] = None,
**kwargs
continuationContext: Any,
templateStructure: str,
basePrompt: str
) -> str:
"""Build code content prompt with optional continuation context. Extracts code-specific parameters from kwargs."""
# Extract parameters from kwargs (for code_content use case)
filename = kwargs.get("filename", "")
fileType = kwargs.get("fileType", "")
functions = kwargs.get("functions", [])
classes = kwargs.get("classes", [])
dependencies = kwargs.get("dependencies", [])
metadata = kwargs.get("metadata", {})
userPrompt = kwargs.get("userPrompt", "")
contentParts = kwargs.get("contentParts", [])
contextInfo = kwargs.get("contextInfo", "")
"""Build code content prompt with continuation context. Uses unified signature.
# Rebuild base prompt (same as initial prompt)
userRequestSection = ""
if userPrompt:
userRequestSection = f"""
## ORIGINAL USER REQUEST
```
{userPrompt}
```
"""
Note: All initial context (filename, fileType, functions, etc.) is already
contained in basePrompt. This function only adds continuation-specific instructions.
"""
# Extract continuation context fields (only what's needed for continuation)
incompletePart = continuationContext.incomplete_part
lastRawJson = continuationContext.last_raw_json
contentPartsSection = ""
if contentParts:
relevantParts = []
for part in contentParts:
usageHint = part.metadata.get('usageHint', '').lower()
originalFileName = part.metadata.get('originalFileName', '').lower()
filenameLower = filename.lower()
if (filenameLower in usageHint or
filenameLower in originalFileName or
part.metadata.get('contentFormat') == 'reference' or
(part.data and len(str(part.data).strip()) > 0)):
relevantParts.append(part)
if relevantParts:
contentPartsSection = "\n## AVAILABLE CONTENT PARTS\n"
for i, part in enumerate(relevantParts, 1):
contentFormat = part.metadata.get("contentFormat", "unknown")
originalFileName = part.metadata.get('originalFileName', 'N/A')
contentPartsSection += f"\n{i}. ContentPart ID: {part.id}\n"
contentPartsSection += f" Format: {contentFormat}\n"
contentPartsSection += f" Type: {part.typeGroup}\n"
contentPartsSection += f" Original file name: {originalFileName}\n"
contentPartsSection += f" Usage hint: {part.metadata.get('usageHint', 'N/A')}\n"
if part.data and isinstance(part.data, str) and len(part.data) < 2000:
contentPartsSection += f" Content preview: {part.data[:500]}...\n"
# Build overlap context: extract last ~100 characters from the response for overlap
overlapContext = ""
if lastRawJson:
overlapContext = lastRawJson[-100:].strip()
basePrompt = f"""# TASK: Generate Code File Content
Generate complete, executable code for the file: {filename}
{userRequestSection}## FILE SPECIFICATIONS
File Type: {fileType}
Language: {metadata.get('language', 'python') if metadata else 'python'}
{contentPartsSection}
Required functions:
{json.dumps(functions, indent=2) if functions else 'None specified'}
Required classes:
{json.dumps(classes, indent=2) if classes else 'None specified'}
Dependencies on other files: {', '.join(dependencies) if dependencies else 'None'}
{contextInfo}
Generate complete, production-ready code with:
1. Proper imports (including imports from other files in the project if dependencies exist)
2. All required functions and classes
3. Error handling
4. Documentation/docstrings
5. Type hints where appropriate
Return ONLY valid JSON in this format:
{{
"files": [
{{
"filename": "{filename}",
"content": "// Complete code here",
"functions": {json.dumps(functions, indent=2) if functions else '[]'},
"classes": {json.dumps(classes, indent=2) if classes else '[]'}
}}
]
}}
"""
# Build unified context showing structure hierarchy with cut point
unifiedContext = ""
if lastRawJson:
# Find break position in raw JSON
if incompletePart:
breakPos = lastRawJson.find(incompletePart)
if breakPos == -1:
breakPos = len(lastRawJson.rstrip())
else:
breakPos = len(lastRawJson.rstrip())
# Build intelligent context showing hierarchy
from modules.shared.jsonUtils import _buildIncompleteContext
unifiedContext = _buildIncompleteContext(lastRawJson, breakPos)
elif incompletePart:
unifiedContext = incompletePart
else:
unifiedContext = "Unable to extract context - response was completely broken"
if continuationContext:
# Add continuation instructions
deliveredSummary = continuationContext.get("delivered_summary", "")
elementBeforeCutoff = continuationContext.get("element_before_cutoff", "")
cutOffElement = continuationContext.get("cut_off_element", "")
continuationText = f"{deliveredSummary}\n\n"
continuationText += "⚠️ CONTINUATION: Response was cut off. Generate ONLY the remaining content that comes AFTER the reference elements below.\n\n"
if elementBeforeCutoff:
continuationText += "# REFERENCE: Last complete element (already delivered - DO NOT repeat):\n"
continuationText += f"{elementBeforeCutoff}\n\n"
if cutOffElement:
continuationText += "# REFERENCE: Incomplete element (cut off here - DO NOT repeat):\n"
continuationText += f"{cutOffElement}\n\n"
continuationText += "⚠️ CRITICAL: The elements above are REFERENCE ONLY. They are already delivered.\n"
continuationText += "Generate ONLY what comes AFTER these elements. DO NOT regenerate the entire JSON structure.\n"
continuationText += "Continue generating the remaining code content now.\n\n"
return f"""{basePrompt}
# Build unified continuation prompt format
continuationPrompt = f"""{basePrompt}
--- CONTINUATION REQUEST ---
The previous JSON response was incomplete. Continue from where it stopped.
{continuationText}
JSON Structure Template:
{templateStructure}
Continue generating the remaining code content now.
"""
else:
return basePrompt
Context showing structure hierarchy with cut point:
{unifiedContext}
Overlap Requirement:
To ensure proper merging, your response MUST start by repeating approximately the last 100 characters from the previous response, then continue with new content.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
TASK:
1. Start your response by repeating the last ~100 characters shown above (for overlap/merging)
2. Complete the incomplete element shown in the context above (marked with CUT POINT)
3. Continue generating the remaining content following the JSON structure template above
4. Return ONLY valid JSON following the structure template - no overlap/continuation wrapper objects
CRITICAL:
- Your response must be valid JSON matching the structure template above
- Start with overlap (~100 chars) then continue seamlessly
- Complete the incomplete element and continue with remaining elements"""
return continuationPrompt
# Use generic looping system with code_content use case
options = AiCallOptions(
@ -823,7 +867,8 @@ Continue generating the remaining code content now.
"userPrompt": userPrompt,
"contentParts": contentParts,
"contextInfo": contextInfo,
"services": self.services
"templateStructure": templateStructure,
"basePrompt": contentPrompt
},
useCaseId="code_content",
debugPrefix=f"code_content_{fileStructure.get('id', 'file')}",

View file

@ -5,6 +5,7 @@ import logging
import re
from typing import Any, Dict, List, Optional, Tuple, Union, Type, TypeVar
from pydantic import BaseModel, ValidationError
from modules.datamodels.datamodelAi import ContinuationContext
logger = logging.getLogger(__name__)
@ -843,8 +844,9 @@ def _extractOverlapFromElement(elem: Dict[str, Any], elemType: str) -> Optional[
def buildContinuationContext(
allSections: List[Dict[str, Any]],
lastRawResponse: Optional[str] = None,
useCaseId: Optional[str] = None
) -> Dict[str, Any]:
useCaseId: Optional[str] = None,
templateStructure: Optional[str] = None
) -> ContinuationContext:
"""
Build context information from accumulated sections for continuation prompt.
@ -854,14 +856,12 @@ def buildContinuationContext(
allSections: List of ALL sections accumulated across ALL iterations
lastRawResponse: Raw JSON response from last iteration (can be broken/incomplete)
useCaseId: Optional use case ID to determine expected JSON structure
templateStructure: JSON structure template from initial prompt (MUST be identical)
Returns:
Dict with delivered_summary, cut_off_element, element_before_cutoff, template_structure,
last_complete_part, incomplete_part, structure_context
ContinuationContext: Pydantic model with all continuation context information
"""
context = {
"section_count": len(allSections),
}
section_count = len(allSections)
# Build summary of delivered data (per-section counts)
summary_lines = []
@ -978,7 +978,7 @@ def buildContinuationContext(
else:
summary_lines.extend(summary_items)
context["delivered_summary"] = "\n".join(summary_lines)
delivered_summary = "\n".join(summary_lines)
# Extract cut-off point using new algorithm
# 1. Loop over all sections until finding incomplete section
@ -1029,9 +1029,6 @@ def buildContinuationContext(
except Exception as e:
logger.debug(f"Error extracting cut-off point: {e}")
context["element_before_cutoff"] = element_before_cutoff
context["cut_off_element"] = cut_off_element
# Extract overlap information for continuation prompt
# GENERIC overlap extraction: handles elements of any size, including long strings
# Strategy: Extract last N elements, but if an element is very large, extract only a portion
@ -1067,38 +1064,36 @@ def buildContinuationContext(
if overlapStrings:
overlapString = ",\n".join(overlapStrings)
context["overlap_elements"] = overlapElements
context["overlap_string"] = overlapString
# Store raw JSON response and extract structure context
last_raw_json = lastRawResponse or ""
last_complete_part = ""
incomplete_part = ""
structure_context = ""
# Store raw JSON response for prompt builder to check
if lastRawResponse:
context["last_raw_json"] = lastRawResponse
# Extract JSON structure context for continuation prompt
# This provides: template structure, last complete part, incomplete part, structure context
# This provides: last complete part, incomplete part, structure context
# NOTE: template_structure is now passed as parameter, not extracted
try:
structureContext = extractJsonStructureContext(lastRawResponse, useCaseId)
context["template_structure"] = structureContext.get("template_structure", "")
context["last_complete_part"] = structureContext.get("last_complete_part", "")
context["incomplete_part"] = structureContext.get("incomplete_part", "")
context["structure_context"] = structureContext.get("structure_context", "")
# Log if extraction succeeded but returned empty values
if not context["template_structure"] and not context["structure_context"]:
logger.debug(f"JSON structure context extraction returned empty values for useCaseId={useCaseId}")
last_complete_part = structureContext.get("last_complete_part", "")
incomplete_part = structureContext.get("incomplete_part", "")
structure_context = structureContext.get("structure_context", "")
except Exception as e:
logger.warning(f"Error extracting JSON structure context: {e}", exc_info=True)
context["template_structure"] = ""
context["last_complete_part"] = ""
context["incomplete_part"] = ""
context["structure_context"] = ""
else:
context["last_raw_json"] = ""
context["template_structure"] = ""
context["last_complete_part"] = ""
context["incomplete_part"] = ""
context["structure_context"] = ""
return context
# Return ContinuationContext Pydantic model
return ContinuationContext(
section_count=section_count,
delivered_summary=delivered_summary,
cut_off_element=cut_off_element,
element_before_cutoff=element_before_cutoff,
template_structure=templateStructure, # Use passed parameter, not extracted
last_complete_part=last_complete_part,
incomplete_part=incomplete_part,
structure_context=structure_context,
last_raw_json=last_raw_json
)
def extractJsonStructureContext(