This commit is contained in:
ValueOn AG 2026-01-04 20:01:34 +01:00
parent 3cdd212606
commit 64590aa61e
14 changed files with 6478 additions and 3735 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,376 +0,0 @@
# Parallel Processing Refactoring Concept
## Current State (Sequential)
### Chapter Sections Structure Generation (`_generateChapterSectionsStructure`)
- **Current**: Processes chapters sequentially, one after another
- **Flow**:
1. Iterate through documents
2. For each document, iterate through chapters
3. For each chapter, generate sections structure using AI
4. Update progress after each chapter
### Section Content Generation (`_fillChapterSections`)
- **Current**: Processes chapters sequentially, sections within each chapter sequentially
- **Flow**:
1. Iterate through documents
2. For each document, iterate through chapters
3. For each chapter, iterate through sections
4. For each section, generate content using AI
5. Update progress after each section
## Desired State (Parallel)
### Chapter Sections Structure Generation
- **Target**: Process all chapters in parallel
- **Requirements**:
- Maintain chapter order in final result
- Each chapter can be processed independently
- Progress updates should reflect parallel processing
- Errors in one chapter should not stop others
### Section Content Generation
- **Target**: Process sections within each chapter in parallel
- **Requirements**:
- Maintain section order within each chapter
- Sections within a chapter can be processed independently
- Chapters still processed sequentially (to maintain order)
- Progress updates should reflect parallel processing
- Errors in one section should not stop others
## Implementation Strategy
### Phase 1: Chapter Sections Structure Generation Parallelization
#### Step 1.1: Extract Single Chapter Processing
- **Create**: `_generateSingleChapterSectionsStructure()` method
- **Purpose**: Process one chapter independently
- **Parameters**:
- `chapter`: Chapter dict
- `chapterIndex`: Index for ordering
- `chapterId`, `chapterLevel`, `chapterTitle`: Chapter metadata
- `generationHint`: Generation instructions
- `contentPartIds`, `contentPartInstructions`: Content part info
- `contentParts`: Full content parts list
- `userPrompt`: User's original prompt
- `language`: Language for generation
- `parentOperationId`: For progress logging
- **Returns**: None (modifies chapter dict in place)
- **Error Handling**: Logs errors, raises exception to be caught by caller
#### Step 1.2: Refactor Main Method
- **Modify**: `_generateChapterSectionsStructure()`
- **Changes**:
1. Collect all chapters with their indices
2. Create async tasks for each chapter using `_generateSingleChapterSectionsStructure`
3. Use `asyncio.gather()` to execute all tasks in parallel
4. Process results in order (using `zip` with original order)
5. Handle errors per chapter (don't fail entire operation)
6. Update progress after each chapter completes
#### Step 1.3: Progress Reporting
- **Maintain**: Overall progress tracking
- **Update**: Progress after each chapter completes (not sequentially)
- **Format**: "Chapter X/Y completed" or "Chapter X/Y error"
### Phase 2: Section Content Generation Parallelization
#### Step 2.1: Extract Single Section Processing
- **Create**: `_processSingleSection()` method
- **Purpose**: Process one section independently
- **Parameters**:
- `section`: Section dict
- `sectionIndex`: Index for ordering
- `totalSections`: Total sections in chapter
- `chapterIndex`: Chapter index
- `totalChapters`: Total chapters
- `chapterId`: Chapter ID
- `chapterOperationId`: Chapter progress operation ID
- `fillOperationId`: Overall fill operation ID
- `contentParts`: Full content parts list
- `userPrompt`: User's original prompt
- `all_sections_list`: All sections for context
- `language`: Language for generation
- `calculateOverallProgress`: Function to calculate overall progress
- **Returns**: `List[Dict[str, Any]]` (elements for the section)
- **Error Handling**: Returns error element instead of raising
#### Step 2.2: Extract Section Processing Logic
- **Create**: Helper methods for different processing paths:
- `_processSectionAggregation()`: Handle aggregation path (multiple parts)
- `_processSectionGeneration()`: Handle generation without parts (only generationHint)
- `_processSectionParts()`: Handle individual part processing
- **Purpose**: Keep logic organized and reusable
#### Step 2.3: Refactor Main Method
- **Modify**: `_fillChapterSections()`
- **Changes**:
1. Keep sequential chapter processing (maintains order)
2. For each chapter, collect all sections with indices
3. Create async tasks for each section using `_processSingleSection`
4. Use `asyncio.gather()` to execute all section tasks in parallel
5. Process results in order (using `zip` with original order)
6. Assign elements to sections in correct order
7. Update progress after each section completes
8. Handle errors per section (don't fail entire chapter)
#### Step 2.4: Progress Reporting
- **Maintain**: Hierarchical progress tracking
- **Update**:
- Section progress: After each section completes
- Chapter progress: After all sections in chapter complete
- Overall progress: After each section/chapter completes
- **Format**: "Chapter X/Y, Section A/B completed"
## Key Considerations
### Order Preservation
- **Chapters**: Must maintain document order → process chapters sequentially
- **Sections**: Must maintain chapter order → process sections sequentially within chapter
- **Solution**: Use `asyncio.gather()` with ordered task list, then `zip` results with original order
### Error Handling
- **Chapters**: Error in one chapter should not stop others
- **Sections**: Error in one section should not stop others
- **Solution**: Use `return_exceptions=True` in `asyncio.gather()`, check `isinstance(result, Exception)`
### Progress Reporting
- **Challenge**: Progress updates happen out of order
- **Solution**: Update progress when each task completes, not sequentially
- **Format**: Show completed count, not sequential position
### Shared State
- **Chapters**: Modify chapter dicts in place (safe, each chapter is independent)
- **Sections**: Return elements, assign to sections in order (safe, each section is independent)
- **Content Parts**: Read-only, passed to all tasks (safe)
### Dependencies
- **Chapters**: No dependencies between chapters
- **Sections**: No dependencies between sections (each is self-contained)
- **Solution**: All tasks can run truly in parallel
## Implementation Steps
### Step 1: Clean Current Code
1. Ensure current sequential implementation is correct
2. Fix any existing bugs
3. Verify all tests pass
### Step 2: Implement Chapter Parallelization
1. Create `_generateSingleChapterSectionsStructure()` method
2. Extract chapter processing logic
3. Refactor `_generateChapterSectionsStructure()` to use parallel processing
4. Test with single chapter
5. Test with multiple chapters
6. Verify order preservation
7. Verify error handling
### Step 3: Implement Section Parallelization
1. Create `_processSingleSection()` method
2. Extract section processing logic into helper methods
3. Refactor `_fillChapterSections()` to use parallel processing for sections
4. Test with single section
5. Test with multiple sections
6. Test with multiple chapters
7. Verify order preservation
8. Verify error handling
### Step 4: Testing & Validation
1. Test with various document structures
2. Test error scenarios
3. Verify progress reporting accuracy
4. Performance testing (compare sequential vs parallel)
5. Verify final output order matches input order
## Code Structure
### New Methods to Create
```python
async def _generateSingleChapterSectionsStructure(
self,
chapter: Dict[str, Any],
chapterIndex: int,
chapterId: str,
chapterLevel: int,
chapterTitle: str,
generationHint: str,
contentPartIds: List[str],
contentPartInstructions: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
language: str,
parentOperationId: str
) -> None:
"""Generate sections structure for a single chapter (used for parallel processing)."""
# Extract logic from current sequential loop
# Modify chapter dict in place
# Handle errors internally, raise if critical
async def _processSingleSection(
self,
section: Dict[str, Any],
sectionIndex: int,
totalSections: int,
chapterIndex: int,
totalChapters: int,
chapterId: str,
chapterOperationId: str,
fillOperationId: str,
contentParts: List[ContentPart],
userPrompt: str,
all_sections_list: List[Dict[str, Any]],
language: str,
calculateOverallProgress: Callable
) -> List[Dict[str, Any]]:
"""Process a single section and return its elements."""
# Extract logic from current sequential loop
# Return elements list
# Return error element on failure (don't raise)
async def _processSectionAggregation(
self,
section: Dict[str, Any],
sectionId: str,
sectionTitle: str,
sectionIndex: int,
totalSections: int,
chapterId: str,
chapterOperationId: str,
fillOperationId: str,
contentPartIds: List[str],
contentFormats: Dict[str, str],
contentParts: List[ContentPart],
userPrompt: str,
generationHint: str,
all_sections_list: List[Dict[str, Any]],
language: str
) -> List[Dict[str, Any]]:
"""Process section with aggregation (multiple parts together)."""
# Extract aggregation logic
# Return elements list
async def _processSectionGeneration(
self,
section: Dict[str, Any],
sectionId: str,
sectionTitle: str,
sectionIndex: int,
totalSections: int,
chapterId: str,
chapterOperationId: str,
fillOperationId: str,
contentType: str,
userPrompt: str,
generationHint: str,
all_sections_list: List[Dict[str, Any]],
language: str
) -> List[Dict[str, Any]]:
"""Process section generation without content parts (only generationHint)."""
# Extract generation logic
# Return elements list
async def _processSectionParts(
self,
section: Dict[str, Any],
sectionId: str,
sectionTitle: str,
sectionIndex: int,
totalSections: int,
chapterId: str,
chapterOperationId: str,
fillOperationId: str,
contentPartIds: List[str],
contentFormats: Dict[str, str],
contentParts: List[ContentPart],
contentType: str,
useAiCall: bool,
generationHint: str,
userPrompt: str,
all_sections_list: List[Dict[str, Any]],
language: str
) -> List[Dict[str, Any]]:
"""Process individual parts in a section."""
# Extract individual part processing logic
# Return elements list
```
### Modified Methods
```python
async def _generateChapterSectionsStructure(
self,
chapterStructure: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
parentOperationId: str
) -> Dict[str, Any]:
"""Generate sections structure for all chapters in parallel."""
# Collect chapters with indices
# Create tasks
# Execute in parallel
# Process results in order
# Update progress
async def _fillChapterSections(
self,
chapterStructure: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
fillOperationId: str
) -> Dict[str, Any]:
"""Fill sections with content, processing sections in parallel within each chapter."""
# Process chapters sequentially
# For each chapter, process sections in parallel
# Maintain order
# Update progress
```
## Testing Strategy
### Unit Tests
1. Test `_generateSingleChapterSectionsStructure` independently
2. Test `_processSingleSection` independently
3. Test helper methods independently
### Integration Tests
1. Test parallel chapter processing with multiple chapters
2. Test parallel section processing with multiple sections
3. Test error handling (one chapter/section fails)
4. Test order preservation
### Performance Tests
1. Measure sequential vs parallel execution time
2. Verify parallel processing is faster
3. Check resource usage (memory, CPU)
## Risk Mitigation
### Risks
1. **Order not preserved**: Use `zip` with original order
2. **Race conditions**: No shared mutable state between tasks
3. **Progress reporting incorrect**: Update progress when tasks complete
4. **Errors not handled**: Use `return_exceptions=True` and check results
5. **Performance degradation**: Test and measure, fallback to sequential if needed
### Safety Measures
1. Keep sequential implementation as fallback (commented out)
2. Add feature flag to enable/disable parallel processing
3. Extensive logging for debugging
4. Gradual rollout (test with small datasets first)
## Migration Path
1. **Phase 1**: Implement chapter parallelization, test thoroughly
2. **Phase 2**: Implement section parallelization, test thoroughly
3. **Phase 3**: Enable both in production with monitoring
4. **Phase 4**: Remove sequential fallback code (if stable)
## Notes
- All async methods must use `await` correctly
- Progress updates happen asynchronously (may appear out of order in logs)
- Final result order is guaranteed by processing results in order
- Error handling is per-task, not global
- No shared mutable state between parallel tasks (read-only contentParts, independent chapter/section dicts)

View file

@ -1,78 +0,0 @@
# Module Structure - serviceAi
## Übersicht
Das `mainServiceAi.py` Modul wurde in mehrere Submodule aufgeteilt, um die Übersichtlichkeit zu verbessern.
## Modulstruktur
### Hauptmodul
- **mainServiceAi.py** (~800 Zeilen)
- Initialisierung (`__init__`, `create`, `ensureAiObjectsInitialized`)
- Public API (`callAiPlanning`, `callAiContent`)
- Routing zu Submodulen
- Helper-Methoden
### Submodule
1. **subJsonResponseHandling.py** (bereits vorhanden)
- JSON Response Merging
- Section Merging
- Fragment Detection
2. **subResponseParsing.py** (~200 Zeilen)
- `ResponseParser.extractSectionsFromResponse()` - Extrahiert Sections aus AI-Responses
- `ResponseParser.shouldContinueGeneration()` - Entscheidet ob Generation fortgesetzt werden soll
- `ResponseParser._isStuckInLoop()` - Loop-Detection
- `ResponseParser.extractDocumentMetadata()` - Extrahiert Metadaten
- `ResponseParser.buildFinalResultFromSections()` - Baut finales JSON
3. **subDocumentIntents.py** (~300 Zeilen)
- `DocumentIntentAnalyzer.clarifyDocumentIntents()` - Analysiert Dokument-Intents
- `DocumentIntentAnalyzer.resolvePreExtractedDocument()` - Löst pre-extracted Dokumente auf
- `DocumentIntentAnalyzer._buildIntentAnalysisPrompt()` - Baut Intent-Analyse-Prompt
4. **subContentExtraction.py** (~600 Zeilen)
- `ContentExtractor.extractAndPrepareContent()` - Extrahiert und bereitet Content vor
- `ContentExtractor.extractTextFromImage()` - Vision AI für Bilder
- `ContentExtractor.processTextContentWithAi()` - AI-Verarbeitung von Text
- `ContentExtractor._isBinary()` - Helper für Binary-Check
5. **subStructureGeneration.py** (~200 Zeilen)
- `StructureGenerator.generateStructure()` - Generiert Dokument-Struktur
- `StructureGenerator._buildStructurePrompt()` - Baut Struktur-Prompt
6. **subStructureFilling.py** (~400 Zeilen)
- `StructureFiller.fillStructure()` - Füllt Struktur mit Content
- `StructureFiller._buildSectionGenerationPrompt()` - Baut Section-Generation-Prompt
- `StructureFiller._findContentPartById()` - Helper für ContentPart-Suche
- `StructureFiller._needsAggregation()` - Entscheidet ob Aggregation nötig
7. **subAiCallLooping.py** (~400 Zeilen)
- `AiCallLooper.callAiWithLooping()` - Haupt-Looping-Logik
- `AiCallLooper._defineKpisFromPrompt()` - KPI-Definition
## Verwendung
Alle Submodule werden über das Hauptmodul `AiService` verwendet:
```python
# Initialisierung
aiService = await AiService.create(serviceCenter)
# Submodule werden automatisch initialisiert
# aiService.responseParser
# aiService.intentAnalyzer
# aiService.contentExtractor
# etc.
```
## Migration
Die öffentliche API bleibt unverändert. Interne Methoden wurden in Submodule verschoben:
- `_extractSectionsFromResponse``responseParser.extractSectionsFromResponse`
- `_clarifyDocumentIntents``intentAnalyzer.clarifyDocumentIntents`
- `_extractAndPrepareContent``contentExtractor.extractAndPrepareContent`
- etc.

View file

@ -222,18 +222,6 @@ Respond with ONLY a JSON object in this exact format:
prompt, options, debugPrefix, promptBuilder, promptArgs, operationId, userPrompt, contentParts, useCaseId
)
async def _defineKpisFromPrompt(
self,
userPrompt: str,
rawJsonString: Optional[str],
continuationContext: Dict[str, Any],
debugPrefix: str = "kpi"
) -> List[Dict[str, Any]]:
"""Delegate to AiCallLooper."""
return await self.aiCallLooper._defineKpisFromPrompt(
userPrompt, rawJsonString, continuationContext, debugPrefix
)
# JSON merging logic moved to subJsonResponseHandling.py
def _extractSectionsFromResponse(

View file

@ -0,0 +1,529 @@
================================================================================
JSON MERGE OPERATION #1
================================================================================
Timestamp: 2026-01-04T15:18:28.448964
INPUT:
Accumulated length: 36937 chars
New Fragment length: 36843 chars
Accumulated: 223 lines (showing first 5 and last 5)
{
"elements": [
{
"type": "table",
"content": {
... (213 lines omitted) ...
["2111", "18433", "2112", "18439", "2113", "18443", "2114", "18451", "2115", "18457", "2116", "18461", "2117", "18481", "2118", "18493", "2119", "18503", "2120", "18517"],
["2121", "18521", "2122", "18523", "2123", "18539", "2124", "18541", "2125", "18553", "2126", "18583", "2127", "18587", "2128", "18593", "2129", "18617", "2130", "18637"],
["2131", "18661", "2132", "18671", "2133", "18679", "2134", "18691", "2135", "18701", "2136", "18713", "2137", "18719", "2138", "18731", "2139", "18743", "2140", "18749"],
["2141", "18757", "2142", "18773", "2143", "18787", "2144", "18793", "2145", "18797", "2146", "18803", "2147", "18839", "2148", "18859", "2149", "18869", "2150", "18899"],
["2151", "189
New Fragment: 209 lines (showing first 5 and last 5)
```json
{
"elements": [
{
"type": "table",
... (199 lines omitted) ...
["4061", "38569", "4062", "38593", "4063", "38603", "4064", "38609", "4065", "38611", "4066", "38629", "4067", "38639", "4068", "38651", "4069", "38653", "4070", "38669"],
["4071", "38671", "4072", "38677", "4073", "38693", "4074", "38699", "4075", "38707", "4076", "38711", "4077", "38713", "4078", "38723", "4079", "38729", "4080", "38737"],
["4081", "38747", "4082", "38749", "4083", "38767", "4084", "38783", "4085", "38791", "4086", "38803", "4087", "38821", "4088", "38833", "4089", "38839", "4090", "38851"],
["4091", "38861", "4092", "38867", "4093", "38873", "4094", "38891", "4095", "38903", "4096", "38917", "4097", "38921", "4098", "38923", "4099", "38933", "4100", "38953"],
["4101", "38959", "4102", "38971", "4103", "38977", "4104", "38993", "4105", "39019", "4106", "39023", "4107
Normalized Accumulated (36937 chars)
(showing first 5 and last 5 of 223 lines)
{
"elements": [
{
"type": "table",
"content": {
... (213 lines omitted) ...
["2111", "18433", "2112", "18439", "2113", "18443", "2114", "18451", "2115", "18457", "2116", "18461", "2117", "18481", "2118", "18493", "2119", "18503", "2120", "18517"],
["2121", "18521", "2122", "18523", "2123", "18539", "2124", "18541", "2125", "18553", "2126", "18583", "2127", "18587", "2128", "18593", "2129", "18617", "2130", "18637"],
["2131", "18661", "2132", "18671", "2133", "18679", "2134", "18691", "2135", "18701", "2136", "18713", "2137", "18719", "2138", "18731", "2139", "18743", "2140", "18749"],
["2141", "18757", "2142", "18773", "2143", "18787", "2144", "18793", "2145", "18797", "2146", "18803", "2147", "18839", "2148", "18859", "2149", "18869", "2150", "18899"],
["2151", "189
Normalized New Fragment (36835 chars)
(showing first 5 and last 5 of 208 lines)
{
"elements": [
{
"type": "table",
"content": {
... (198 lines omitted) ...
["4061", "38569", "4062", "38593", "4063", "38603", "4064", "38609", "4065", "38611", "4066", "38629", "4067", "38639", "4068", "38651", "4069", "38653", "4070", "38669"],
["4071", "38671", "4072", "38677", "4073", "38693", "4074", "38699", "4075", "38707", "4076", "38711", "4077", "38713", "4078", "38723", "4079", "38729", "4080", "38737"],
["4081", "38747", "4082", "38749", "4083", "38767", "4084", "38783", "4085", "38791", "4086", "38803", "4087", "38821", "4088", "38833", "4089", "38839", "4090", "38851"],
["4091", "38861", "4092", "38867", "4093", "38873", "4094", "38891", "4095", "38903", "4096", "38917", "4097", "38921", "4098", "38923", "4099", "38933", "4100", "38953"],
["4101", "38959", "4102", "38971", "4103", "38977", "4104", "38993", "4105", "39019", "4106", "39023", "4107
STEP: PHASE 1
Description: Finding overlap between JSON strings
⏳ In progress...
Overlap Detection (string):
Overlap length: 0
⚠️ No overlap detected - appending all
⚠️ NO OVERLAP FOUND - This indicates iterations should stop
Closing JSON and returning final result
Closed JSON (36944 chars):
==============================================================================
{
"elements": [
{
"type": "table",
"content": {
"headers": ["Nr.1", "Primzahl1", "Nr.2", "Primzahl2", "Nr.3", "Primzahl3", "Nr.4", "Primzahl4", "Nr.5", "Primzahl5", "Nr.6", "Primzahl6", "Nr.7", "Primzahl7", "Nr.8", "Primzahl8", "Nr.9", "Primzahl9", "Nr.10", "Primzahl10"],
"rows": [
["1", "2", "2", "3", "3", "5", "4", "7", "5", "11", "6", "13", "7", "17", "8", "19", "9", "23", "10", "29"],
["11", "31", "12", "37", "13", "41", "14", "43", "15", "47", "16", "53", "17", "59", "18", "61", "19", "67", "20", "71"],
["21", "73", "22", "79", "23", "83", "24", "89", "25", "97", "26", "101", "27", "103", "28", "107", "29", "109", "30", "113"],
["31", "127", "32", "131", "33", "137", "34", "139", "35", "149", "36", "151", "37", "157", "38", "163", "39", "167", "40", "173"],
["41", "179", "42", "181", "43", "191", "44", "193", "45", "197", "46", "199", "47", "211", "48", "223", "49", "227", "50", "229"],
["51", "233", "52", "239", "53", "241", "54", "251", "55", "257", "56", "263", "57", "269", "58", "271", "59", "277", "60", "281"],
["61", "283", "62", "293", "63", "307", "64", "311", "65", "313", "66", "317", "67", "331", "68", "337", "69", "347", "70", "349"],
["71", "353", "72", "359", "73", "367", "74", "373", "75", "379", "76", "383", "77", "389", "78", "397", "79", "401", "80", "409"],
["81", "419", "82", "421", "83", "431", "84", "433", "85", "439", "86", "443", "87", "449", "88", "457", "89", "461", "90", "463"],
["91", "467", "92", "479", "93", "487", "94", "491", "95", "499", "96", "503", "97", "509", "98", "521", "99", "523", "100", "541"],
["101", "547", "102", "557", "103", "563", "104", "569", "105", "571", "106", "577", "107", "587", "108", "593", "109", "599", "110", "601"],
["111", "607", "112", "613", "113", "617", "114", "619", "115", "631", "116", "641", "117", "643", "118", "647", "119", "653", "120", "659"],
["121", "661", "122", "673", "123", "677", "124", "683", "125", "691", "126", "701", "127", "709", "128", "719", "129", "727", "130", "733"],
["131", "739", "132", "743", "133", "751", "134", "757", "135", "761", "136", "769", "137", "773", "138", "787", "139", "797", "140", "809"],
["141", "811", "142", "821", "143", "823", "144", "827", "145", "829", "146", "839", "147", "853", "148", "857", "149", "859", "150", "863"],
["151", "877", "152", "881", "153", "883", "154", "887", "155", "907", "156", "911", "157", "919", "158", "929", "159", "937", "160", "941"],
["161", "947", "162", "953", "163", "967", "164", "971", "165", "977", "166", "983", "167", "991", "168", "997", "169", "1009", "170", "1013"],
["171", "1019", "172", "1021", "173", "1031", "174", "1033", "175", "1039", "176", "1049", "177", "1051", "178", "1061", "179", "1063", "180", "1069"],
["181", "1087", "182", "1091", "183", "1093", "184", "1097", "185", "1103", "186", "1109", "187", "1117", "188", "1123", "189", "1129", "190", "1151"],
["191", "1153", "192", "1163", "193", "1171", "194", "1181", "195", "1187", "196", "1193", "197", "1201", "198", "1213", "199", "1217", "200", "1223"],
["201", "1229", "202", "1231", "203", "1237", "204", "1249", "205", "1259", "206", "1277", "207", "1279", "208", "1283", "209", "1289", "210", "1291"],
["211", "1297", "212", "1301", "213", "1303", "214", "1307", "215", "1319", "216", "1321", "217", "1327", "218", "1361", "219", "1367", "220", "1373"],
["221", "1381", "222", "1399", "223", "1409", "224", "1423", "225", "1427", "226", "1429", "227", "1433", "228", "1439", "229", "1447", "230", "1451"],
["231", "1453", "232", "1459", "233", "1471", "234", "1481", "235", "1483", "236", "1487", "237", "1489", "238", "1493", "239", "1499", "240", "1511"],
["241", "1523", "242", "1531", "243", "1543", "244", "1549", "245", "1553", "246", "1559", "247", "1567", "248", "1571", "249", "1579", "250", "1583"],
["251", "1597", "252", "1601", "253", "1607", "254", "1609", "255", "1613", "256", "1619", "257", "1621", "258", "1627", "259", "1637", "260", "1657"],
["261", "1663", "262", "1667", "263", "1669", "264", "1693", "265", "1697", "266", "1699", "267", "1709", "268", "1721", "269", "1723", "270", "1733"],
["271", "1741", "272", "1747", "273", "1753", "274", "1759", "275", "1777", "276", "1783", "277", "1787", "278", "1789", "279", "1801", "280", "1811"],
["281", "1823", "282", "1831", "283", "1847", "284", "1861", "285", "1867", "286", "1871", "287", "1873", "288", "1877", "289", "1879", "290", "1889"],
["291", "1901", "292", "1907", "293", "1913", "294", "1931", "295", "1933", "296", "1949", "297", "1951", "298", "1973", "299", "1979", "300", "1987"],
["301", "1993", "302", "1997", "303", "1999", "304", "2003", "305", "2011", "306", "2017", "307", "2027", "308", "2029", "309", "2039", "310", "2053"],
["311", "2063", "312", "2069", "313", "2081", "314", "2083", "315", "2087", "316", "2089", "317", "2099", "318", "2111", "319", "2113", "320", "2129"],
["321", "2131", "322", "2137", "323", "2141", "324", "2143", "325", "2153", "326", "2161", "327", "2179", "328", "2203", "329", "2207", "330", "2213"],
["331", "2221", "332", "2237", "333", "2239", "334", "2243", "335", "2251", "336", "2267", "337", "2269", "338", "2273", "339", "2281", "340", "2287"],
["341", "2293", "342", "2297", "343", "2309", "344", "2311", "345", "2333", "346", "2339", "347", "2341", "348", "2347", "349", "2351", "350", "2357"],
["351", "2371", "352", "2377", "353", "2381", "354", "2383", "355", "2389", "356", "2393", "357", "2399", "358", "2411", "359", "2417", "360", "2423"],
["361", "2437", "362", "2441", "363", "2447", "364", "2459", "365", "2467", "366", "2473", "367", "2477", "368", "2503", "369", "2521", "370", "2531"],
["371", "2539", "372", "2543", "373", "2549", "374", "2551", "375", "2557", "376", "2579", "377", "2591", "378", "2593", "379", "2609", "380", "2617"],
["381", "2621", "382", "2633", "383", "2647", "384", "2657", "385", "2659", "386", "2663", "387", "2671", "388", "2677", "389", "2683", "390", "2687"],
["391", "2689", "392", "2693", "393", "2699", "394", "2707", "395", "2711", "396", "2713", "397", "2719", "398", "2729", "399", "2731", "400", "2741"],
["401", "2749", "402", "2753", "403", "2767", "404", "2777", "405", "2789", "406", "2791", "407", "2797", "408", "2801", "409", "2803", "410", "2819"],
["411", "2833", "412", "2837", "413", "2843", "414", "2851", "415", "2857", "416", "2861", "417", "2879", "418", "2887", "419", "2897", "420", "2903"],
["421", "2909", "422", "2917", "423", "2927", "424", "2939", "425", "2953", "426", "2957", "427", "2963", "428", "2969", "429", "2971", "430", "2999"],
["431", "3001", "432", "3011", "433", "3019", "434", "3023", "435", "3037", "436", "3041", "437", "3049", "438", "3061", "439", "3067", "440", "3079"],
["441", "3083", "442", "3089", "443", "3109", "444", "3119", "445", "3121", "446", "3137", "447", "3163", "448", "3167", "449", "3169", "450", "3181"],
["451", "3187", "452", "3191", "453", "3203", "454", "3209", "455", "3217", "456", "3221", "457", "3229", "458", "3251", "459", "3253", "460", "3257"],
["461", "3259", "462", "3271", "463", "3299", "464", "3301", "465", "3307", "466", "3313", "467", "3319", "468", "3323", "469", "3329", "470", "3331"],
["471", "3343", "472", "3347", "473", "3359", "474", "3361", "475", "3371", "476", "3373", "477", "3389", "478", "3391", "479", "3407", "480", "3413"],
["481", "3433", "482", "3449", "483", "3457", "484", "3461", "485", "3463", "486", "3467", "487", "3469", "488", "3491", "489", "3499", "490", "3511"],
["491", "3517", "492", "3527", "493", "3529", "494", "3533", "495", "3539", "496", "3541", "497", "3547", "498", "3557", "499", "3559", "500", "3571"],
["501", "3581", "502", "3583", "503", "3593", "504", "3607", "505", "3613", "506", "3617", "507", "3623", "508", "3631", "509", "3637", "510", "3643"],
["511", "3659", "512", "3671", "513", "3673", "514", "3677", "515", "3691", "516", "3697", "517", "3701", "518", "3709", "519", "3719", "520", "3727"],
["521", "3733", "522", "3739", "523", "3761", "524", "3767", "525", "3769", "526", "3779", "527", "3793", "528", "3797", "529", "3803", "530", "3821"],
["531", "3823", "532", "3833", "533", "3847", "534", "3851", "535", "3853", "536", "3863", "537", "3877", "538", "3881", "539", "3889", "540", "3907"],
["541", "3911", "542", "3917", "543", "3919", "544", "3923", "545", "3929", "546", "3931", "547", "3943", "548", "3947", "549", "3967", "550", "3989"],
["551", "4001", "552", "4003", "553", "4007", "554", "4013", "555", "4019", "556", "4021", "557", "4027", "558", "4049", "559", "4051", "560", "4057"],
["561", "4073", "562", "4079", "563", "4091", "564", "4093", "565", "4099", "566", "4111", "567", "4127", "568", "4129", "569", "4133", "570", "4139"],
["571", "4153", "572", "4157", "573", "4159", "574", "4177", "575", "4201", "576", "4211", "577", "4217", "578", "4219", "579", "4229", "580", "4231"],
["581", "4241", "582", "4243", "583", "4253", "584", "4259", "585", "4261", "586", "4271", "587", "4273", "588", "4283", "589", "4289", "590", "4297"],
["591", "4327", "592", "4337", "593", "4339", "594", "4349", "595", "4357", "596", "4363", "597", "4373", "598", "4391", "599", "4397", "600", "4409"],
["601", "4421", "602", "4423", "603", "4441", "604", "4447", "605", "4451", "606", "4457", "607", "4463", "608", "4481", "609", "4483", "610", "4493"],
["611", "4507", "612", "4513", "613", "4517", "614", "4519", "615", "4523", "616", "4547", "617", "4549", "618", "4561", "619", "4567", "620", "4583"],
["621", "4591", "622", "4597", "623", "4603", "624", "4621", "625", "4637", "626", "4639", "627", "4643", "628", "4649", "629", "4651", "630", "4657"],
["631", "4663", "632", "4673", "633", "4679", "634", "4691", "635", "4703", "636", "4721", "637", "4723", "638", "4729", "639", "4733", "640", "4751"],
["641", "4759", "642", "4783", "643", "4787", "644", "4789", "645", "4793", "646", "4799", "647", "4801", "648", "4813", "649", "4817", "650", "4831"],
["651", "4861", "652", "4871", "653", "4877", "654", "4889", "655", "4903", "656", "4909", "657", "4919", "658", "4931", "659", "4933", "660", "4937"],
["661", "4943", "662", "4951", "663", "4957", "664", "4967", "665", "4969", "666", "4973", "667", "4987", "668", "4993", "669", "4999", "670", "5003"],
["671", "5009", "672", "5011", "673", "5021", "674", "5023", "675", "5039", "676", "5051", "677", "5059", "678", "5077", "679", "5081", "680", "5087"],
["681", "5099", "682", "5101", "683", "5107", "684", "5113", "685", "5119", "686", "5147", "687", "5153", "688", "5167", "689", "5171", "690", "5179"],
["691", "5189", "692", "5197", "693", "5209", "694", "5227", "695", "5231", "696", "5233", "697", "5237", "698", "5261", "699", "5273", "700", "5279"],
["701", "5281", "702", "5297", "703", "5303", "704", "5309", "705", "5323", "706", "5333", "707", "5347", "708", "5351", "709", "5381", "710", "5387"],
["711", "5393", "712", "5399", "713", "5407", "714", "5413", "715", "5417", "716", "5419", "717", "5431", "718", "5437", "719", "5441", "720", "5443"],
["721", "5449", "722", "5471", "723", "5477", "724", "5479", "725", "5483", "726", "5501", "727", "5503", "728", "5507", "729", "5519", "730", "5521"],
["731", "5527", "732", "5531", "733", "5557", "734", "5563", "735", "5569", "736", "5573", "737", "5581", "738", "5591", "739", "5623", "740", "5639"],
["741", "5641", "742", "5647", "743", "5651", "744", "5653", "745", "5657", "746", "5659", "747", "5669", "748", "5683", "749", "5689", "750", "5693"],
["751", "5701", "752", "5711", "753", "5717", "754", "5737", "755", "5741", "756", "5743", "757", "5749", "758", "5779", "759", "5783", "760", "5791"],
["761", "5801", "762", "5807", "763", "5813", "764", "5821", "765", "5827", "766", "5839", "767", "5843", "768", "5849", "769", "5851", "770", "5857"],
["771", "5861", "772", "5867", "773", "5869", "774", "5879", "775", "5881", "776", "5897", "777", "5903", "778", "5923", "779", "5927", "780", "5939"],
["781", "5953", "782", "5981", "783", "5987", "784", "6007", "785", "6011", "786", "6029", "787", "6037", "788", "6043", "789", "6047", "790", "6053"],
["791", "6067", "792", "6073", "793", "6079", "794", "6089", "795", "6091", "796", "6101", "797", "6113", "798", "6121", "799", "6131", "800", "6133"],
["801", "6143", "802", "6151", "803", "6163", "804", "6173", "805", "6197", "806", "6199", "807", "6203", "808", "6211", "809", "6217", "810", "6221"],
["811", "6229", "812", "6247", "813", "6257", "814", "6263", "815", "6269", "816", "6271", "817", "6277", "818", "6287", "819", "6299", "820", "6301"],
["821", "6311", "822", "6317", "823", "6323", "824", "6329", "825", "6337", "826", "6343", "827", "6353", "828", "6359", "829", "6361", "830", "6367"],
["831", "6373", "832", "6379", "833", "6389", "834", "6397", "835", "6421", "836", "6427", "837", "6449", "838", "6451", "839", "6469", "840", "6473"],
["841", "6481", "842", "6491", "843", "6521", "844", "6529", "845", "6547", "846", "6551", "847", "6553", "848", "6563", "849", "6569", "850", "6571"],
["851", "6577", "852", "6581", "853", "6599", "854", "6607", "855", "6619", "856", "6637", "857", "6653", "858", "6659", "859", "6661", "860", "6673"],
["861", "6679", "862", "6689", "863", "6691", "864", "6701", "865", "6703", "866", "6709", "867", "6719", "868", "6733", "869", "6737", "870", "6761"],
["871", "6763", "872", "6779", "873", "6781", "874", "6791", "875", "6793", "876", "6803", "877", "6823", "878", "6827", "879", "6829", "880", "6833"],
["881", "6841", "882", "6857", "883", "6863", "884", "6869", "885", "6871", "886", "6883", "887", "6899", "888", "6907", "889", "6911", "890", "6917"],
["891", "6947", "892", "6949", "893", "6959", "894", "6961", "895", "6967", "896", "6971", "897", "6977", "898", "6983", "899", "6991", "900", "6997"],
["901", "7001", "902", "7013", "903", "7019", "904", "7027", "905", "7039", "906", "7043", "907", "7057", "908", "7069", "909", "7079", "910", "7103"],
["911", "7109", "912", "7121", "913", "7127", "914", "7129", "915", "7151", "916", "7159", "917", "7177", "918", "7187", "919", "7193", "920", "7207"],
["921", "7211", "922", "7213", "923", "7219", "924", "7229", "925", "7237", "926", "7243", "927", "7247", "928", "7253", "929", "7283", "930", "7297"],
["931", "7307", "932", "7309", "933", "7321", "934", "7331", "935", "7333", "936", "7349", "937", "7351", "938", "7369", "939", "7393", "940", "7411"],
["941", "7417", "942", "7433", "943", "7451", "944", "7457", "945", "7459", "946", "7477", "947", "7481", "948", "7487", "949", "7489", "950", "7499"],
["951", "7507", "952", "7517", "953", "7523", "954", "7529", "955", "7537", "956", "7541", "957", "7547", "958", "7549", "959", "7559", "960", "7561"],
["961", "7573", "962", "7577", "963", "7583", "964", "7589", "965", "7591", "966", "7603", "967", "7607", "968", "7621", "969", "7639", "970", "7643"],
["971", "7649", "972", "7669", "973", "7673", "974", "7681", "975", "7687", "976", "7691", "977", "7699", "978", "7703", "979", "7717", "980", "7723"],
["981", "7727", "982", "7741", "983", "7753", "984", "7757", "985", "7759", "986", "7789", "987", "7793", "988", "7817", "989", "7823", "990", "7829"],
["991", "7841", "992", "7853", "993", "7867", "994", "7873", "995", "7877", "996", "7879", "997", "7883", "998", "7901", "999", "7907", "1000", "7919"],
["1001", "7927", "1002", "7933", "1003", "7937", "1004", "7949", "1005", "7951", "1006", "7963", "1007", "7993", "1008", "8009", "1009", "8011", "1010", "8017"],
["1011", "8039", "1012", "8053", "1013", "8059", "1014", "8069", "1015", "8081", "1016", "8087", "1017", "8089", "1018", "8093", "1019", "8101", "1020", "8111"],
["1021", "8117", "1022", "8123", "1023", "8147", "1024", "8161", "1025", "8167", "1026", "8171", "1027", "8179", "1028", "8191", "1029", "8209", "1030", "8219"],
["1031", "8221", "1032", "8231", "1033", "8233", "1034", "8237", "1035", "8243", "1036", "8263", "1037", "8269", "1038", "8273", "1039", "8287", "1040", "8291"],
["1041", "8293", "1042", "8297", "1043", "8311", "1044", "8317", "1045", "8329", "1046", "8353", "1047", "8363", "1048", "8369", "1049", "8377", "1050", "8387"],
["1051", "8389", "1052", "8419", "1053", "8423", "1054", "8429", "1055", "8431", "1056", "8443", "1057", "8447", "1058", "8461", "1059", "8467", "1060", "8501"],
["1061", "8513", "1062", "8521", "1063", "8527", "1064", "8537", "1065", "8539", "1066", "8543", "1067", "8563", "1068", "8573", "1069", "8581", "1070", "8597"],
["1071", "8599", "1072", "8609", "1073", "8623", "1074", "8627", "1075", "8629", "1076", "8641", "1077", "8647", "1078", "8663", "1079", "8669", "1080", "8677"],
["1081", "8681", "1082", "8689", "1083", "8693", "1084", "8699", "1085", "8707", "1086", "8713", "1087", "8719", "1088", "8731", "1089", "8737", "1090", "8741"],
["1091", "8747", "1092", "8753", "1093", "8761", "1094", "8779", "1095", "8783", "1096", "8803", "1097", "8807", "1098", "8819", "1099", "8821", "1100", "8831"],
["1101", "8837", "1102", "8839", "1103", "8849", "1104", "8861", "1105", "8863", "1106", "8867", "1107", "8887", "1108", "8893", "1109", "8923", "1110", "8929"],
["1111", "8933", "1112", "8941", "1113", "8951", "1114", "8963", "1115", "8969", "1116", "8971", "1117", "8999", "1118", "9001", "1119", "9007", "1120", "9011"],
["1121", "9013", "1122", "9029", "1123", "9041", "1124", "9043", "1125", "9049", "1126", "9059", "1127", "9067", "1128", "9091", "1129", "9103", "1130", "9109"],
["1131", "9127", "1132", "9133", "1133", "9137", "1134", "9151", "1135", "9157", "1136", "9161", "1137", "9173", "1138", "9181", "1139", "9187", "1140", "9199"],
["1141", "9203", "1142", "9209", "1143", "9221", "1144", "9227", "1145", "9239", "1146", "9241", "1147", "9257", "1148", "9277", "1149", "9281", "1150", "9283"],
["1151", "9293", "1152", "9311", "1153", "9319", "1154", "9323", "1155", "9337", "1156", "9341", "1157", "9343", "1158", "9349", "1159", "9371", "1160", "9377"],
["1161", "9391", "1162", "9397", "1163", "9403", "1164", "9413", "1165", "9419", "1166", "9421", "1167", "9431", "1168", "9433", "1169", "9437", "1170", "9439"],
["1171", "9461", "1172", "9463", "1173", "9467", "1174", "9473", "1175", "9479", "1176", "9491", "1177", "9497", "1178", "9511", "1179", "9521", "1180", "9533"],
["1181", "9539", "1182", "9547", "1183", "9551", "1184", "9587", "1185", "9601", "1186", "9613", "1187", "9619", "1188", "9623", "1189", "9629", "1190", "9631"],
["1191", "9643", "1192", "9649", "1193", "9661", "1194", "9677", "1195", "9679", "1196", "9689", "1197", "9697", "1198", "9719", "1199", "9721", "1200", "9733"],
["1201", "9739", "1202", "9743", "1203", "9749", "1204", "9767", "1205", "9769", "1206", "9781", "1207", "9787", "1208", "9791", "1209", "9803", "1210", "9811"],
["1211", "9817", "1212", "9829", "1213", "9833", "1214", "9839", "1215", "9851", "1216", "9857", "1217", "9859", "1218", "9871", "1219", "9883", "1220", "9887"],
["1221", "9901", "1222", "9907", "1223", "9923", "1224", "9929", "1225", "9931", "1226", "9941", "1227", "9949", "1228", "9967", "1229", "9973", "1230", "10007"],
["1231", "10009", "1232", "10037", "1233", "10039", "1234", "10061", "1235", "10067", "1236", "10069", "1237", "10079", "1238", "10091", "1239", "10093", "1240", "10099"],
["1241", "10103", "1242", "10111", "1243", "10133", "1244", "10139", "1245", "10141", "1246", "10151", "1247", "10159", "1248", "10163", "1249", "10169", "1250", "10177"],
["1251", "10181", "1252", "10193", "1253", "10211", "1254", "10223", "1255", "10243", "1256", "10247", "1257", "10253", "1258", "10259", "1259", "10267", "1260", "10271"],
["1261", "10273", "1262", "10289", "1263", "10301", "1264", "10303", "1265", "10313", "1266", "10321", "1267", "10331", "1268", "10333", "1269", "10337", "1270", "10343"],
["1271", "10357", "1272", "10369", "1273", "10391", "1274", "10399", "1275", "10427", "1276", "10429", "1277", "10433", "1278", "10453", "1279", "10457", "1280", "10459"],
["1281", "10463", "1282", "10477", "1283", "10487", "1284", "10499", "1285", "10501", "1286", "10513", "1287", "10529", "1288", "10531", "1289", "10559", "1290", "10567"],
["1291", "10589", "1292", "10597", "1293", "10601", "1294", "10607", "1295", "10613", "1296", "10627", "1297", "10631", "1298", "10639", "1299", "10651", "1300", "10657"],
["1301", "10663", "1302", "10667", "1303", "10687", "1304", "10691", "1305", "10709", "1306", "10711", "1307", "10723", "1308", "10729", "1309", "10733", "1310", "10739"],
["1311", "10753", "1312", "10771", "1313", "10781", "1314", "10789", "1315", "10799", "1316", "10831", "1317", "10837", "1318", "10847", "1319", "10853", "1320", "10859"],
["1321", "10861", "1322", "10867", "1323", "10883", "1324", "10889", "1325", "10891", "1326", "10903", "1327", "10909", "1328", "10937", "1329", "10939", "1330", "10949"],
["1331", "10957", "1332", "10973", "1333", "10979", "1334", "10987", "1335", "10993", "1336", "11003", "1337", "11027", "1338", "11047", "1339", "11057", "1340", "11059"],
["1341", "11069", "1342", "11071", "1343", "11083", "1344", "11087", "1345", "11093", "1346", "11113", "1347", "11117", "1348", "11119", "1349", "11131", "1350", "11149"],
["1351", "11159", "1352", "11161", "1353", "11171", "1354", "11173", "1355", "11177", "1356", "11197", "1357", "11213", "1358", "11239", "1359", "11243", "1360", "11251"],
["1361", "11257", "1362", "11261", "1363", "11273", "1364", "11279", "1365", "11287", "1366", "11299", "1367", "11311", "1368", "11317", "1369", "11321", "1370", "11329"],
["1371", "11351", "1372", "11353", "1373", "11369", "1374", "11383", "1375", "11393", "1376", "11399", "1377", "11411", "1378", "11423", "1379", "11437", "1380", "11443"],
["1381", "11447", "1382", "11467", "1383", "11471", "1384", "11483", "1385", "11489", "1386", "11491", "1387", "11497", "1388", "11503", "1389", "11519", "1390", "11527"],
["1391", "11549", "1392", "11551", "1393", "11579", "1394", "11587", "1395", "11593", "1396", "11597", "1397", "11617", "1398", "11621", "1399", "11633", "1400", "11657"],
["1401", "11677", "1402", "11681", "1403", "11689", "1404", "11699", "1405", "11701", "1406", "11717", "1407", "11719", "1408", "11731", "1409", "11743", "1410", "11777"],
["1411", "11779", "1412", "11783", "1413", "11789", "1414", "11801", "1415", "11807", "1416", "11813", "1417", "11821", "1418", "11827", "1419", "11831", "1420", "11833"],
["1421", "11839", "1422", "11863", "1423", "11867", "1424", "11887", "1425", "11897", "1426", "11903", "1427", "11909", "1428", "11923", "1429", "11927", "1430", "11933"],
["1431", "11939", "1432", "11941", "1433", "11953", "1434", "11959", "1435", "11969", "1436", "11971", "1437", "11981", "1438", "11987", "1439", "12007", "1440", "12011"],
["1441", "12037", "1442", "12041", "1443", "12043", "1444", "12049", "1445", "12071", "1446", "12073", "1447", "12097", "1448", "12101", "1449", "12107", "1450", "12109"],
["1451", "12113", "1452", "12119", "1453", "12143", "1454", "12149", "1455", "12157", "1456", "12161", "1457", "12163", "1458", "12197", "1459", "12203", "1460", "12211"],
["1461", "12227", "1462", "12239", "1463", "12241", "1464", "12251", "1465", "12253", "1466", "12263", "1467", "12269", "1468", "12277", "1469", "12281", "1470", "12289"],
["1471", "12301", "1472", "12323", "1473", "12329", "1474", "12343", "1475", "12347", "1476", "12373", "1477", "12377", "1478", "12379", "1479", "12391", "1480", "12401"],
["1481", "12409", "1482", "12413", "1483", "12421", "1484", "12433", "1485", "12437", "1486", "12451", "1487", "12457", "1488", "12473", "1489", "12479", "1490", "12487"],
["1491", "12491", "1492", "12497", "1493", "12503", "1494", "12511", "1495", "12517", "1496", "12527", "1497", "12539", "1498", "12541", "1499", "12547", "1500", "12553"],
["1501", "12569", "1502", "12577", "1503", "12583", "1504", "12589", "1505", "12601", "1506", "12611", "1507", "12613", "1508", "12619", "1509", "12637", "1510", "12641"],
["1511", "12647", "1512", "12653", "1513", "12659", "1514", "12671", "1515", "12689", "1516", "12697", "1517", "12703", "1518", "12713", "1519", "12721", "1520", "12739"],
["1521", "12743", "1522", "12757", "1523", "12763", "1524", "12781", "1525", "12791", "1526", "12799", "1527", "12809", "1528", "12821", "1529", "12823", "1530", "12829"],
["1531", "12841", "1532", "12853", "1533", "12889", "1534", "12893", "1535", "12899", "1536", "12907", "1537", "12911", "1538", "12917", "1539", "12919", "1540", "12923"],
["1541", "12941", "1542", "12953", "1543", "12959", "1544", "12967", "1545", "12973", "1546", "12979", "1547", "12983", "1548", "13001", "1549", "13003", "1550", "13007"],
["1551", "13009", "1552", "13033", "1553", "13037", "1554", "13043", "1555", "13049", "1556", "13063", "1557", "13093", "1558", "13099", "1559", "13103", "1560", "13109"],
["1561", "13121", "1562", "13127", "1563", "13147", "1564", "13151", "1565", "13159", "1566", "13163", "1567", "13171", "1568", "13177", "1569", "13183", "1570", "13187"],
["1571", "13217", "1572", "13219", "1573", "13229", "1574", "13241", "1575", "13249", "1576", "13259", "1577", "13267", "1578", "13291", "1579", "13297", "1580", "13309"],
["1581", "13313", "1582", "13327", "1583", "13331", "1584", "13337", "1585", "13339", "1586", "13367", "1587", "13381", "1588", "13397", "1589", "13399", "1590", "13411"],
["1591", "13417", "1592", "13421", "1593", "13441", "1594", "13451", "1595", "13457", "1596", "13463", "1597", "13469", "1598", "13477", "1599", "13487", "1600", "13499"],
["1601", "13513", "1602", "13523", "1603", "13537", "1604", "13553", "1605", "13567", "1606", "13577", "1607", "13591", "1608", "13597", "1609", "13613", "1610", "13619"],
["1611", "13627", "1612", "13633", "1613", "13649", "1614", "13669", "1615", "13679", "1616", "13681", "1617", "13687", "1618", "13691", "1619", "13693", "1620", "13697"],
["1621", "13709", "1622", "13711", "1623", "13721", "1624", "13723", "1625", "13729", "1626", "13751", "1627", "13757", "1628", "13759", "1629", "13763", "1630", "13781"],
["1631", "13789", "1632", "13799", "1633", "13807", "1634", "13829", "1635", "13831", "1636", "13841", "1637", "13859", "1638", "13873", "1639", "13877", "1640", "13879"],
["1641", "13883", "1642", "13901", "1643", "13903", "1644", "13907", "1645", "13913", "1646", "13921", "1647", "13931", "1648", "13933", "1649", "13963", "1650", "13967"],
["1651", "13997", "1652", "13999", "1653", "14009", "1654", "14011", "1655", "14029", "1656", "14033", "1657", "14051", "1658", "14057", "1659", "14071", "1660", "14081"],
["1661", "14083", "1662", "14087", "1663", "14107", "1664", "14143", "1665", "14149", "1666", "14153", "1667", "14159", "1668", "14173", "1669", "14177", "1670", "14197"],
["1671", "14207", "1672", "14221", "1673", "14243", "1674", "14249", "1675", "14251", "1676", "14281", "1677", "14293", "1678", "14303", "1679", "14321", "1680", "14323"],
["1681", "14327", "1682", "14341", "1683", "14347", "1684", "14369", "1685", "14387", "1686", "14389", "1687", "14401", "1688", "14407", "1689", "14411", "1690", "14419"],
["1691", "14423", "1692", "14431", "1693", "14437", "1694", "14447", "1695", "14449", "1696", "14461", "1697", "14479", "1698", "14489", "1699", "14503", "1700", "14519"],
["1701", "14533", "1702", "14537", "1703", "14543", "1704", "14549", "1705", "14551", "1706", "14557", "1707", "14561", "1708", "14563", "1709", "14591", "1710", "14593"],
["1711", "14621", "1712", "14627", "1713", "14629", "1714", "14633", "1715", "14639", "1716", "14653", "1717", "14657", "1718", "14669", "1719", "14683", "1720", "14699"],
["1721", "14713", "1722", "14717", "1723", "14723", "1724", "14731", "1725", "14737", "1726", "14741", "1727", "14747", "1728", "14753", "1729", "14759", "1730", "14767"],
["1731", "14771", "1732", "14779", "1733", "14783", "1734", "14797", "1735", "14813", "1736", "14821", "1737", "14827", "1738", "14831", "1739", "14843", "1740", "14851"],
["1741", "14867", "1742", "14869", "1743", "14879", "1744", "14887", "1745", "14891", "1746", "14897", "1747", "14923", "1748", "14929", "1749", "14939", "1750", "14947"],
["1751", "14951", "1752", "14957", "1753", "14969", "1754", "14983", "1755", "15013", "1756", "15017", "1757", "15031", "1758", "15053", "1759", "15061", "1760", "15073"],
["1761", "15077", "1762", "15083", "1763", "15091", "1764", "15101", "1765", "15107", "1766", "15121", "1767", "15131", "1768", "15137", "1769", "15139", "1770", "15149"],
["1771", "15161", "1772", "15173", "1773", "15187", "1774", "15193", "1775", "15199", "1776", "15217", "1777", "15227", "1778", "15233", "1779", "15241", "1780", "15259"],
["1781", "15263", "1782", "15269", "1783", "15271", "1784", "15277", "1785", "15287", "1786", "15289", "1787", "15299", "1788", "15307", "1789", "15313", "1790", "15319"],
["1791", "15329", "1792", "15331", "1793", "15349", "1794", "15359", "1795", "15361", "1796", "15373", "1797", "15377", "1798", "15383", "1799", "15391", "1800", "15401"],
["1801", "15413", "1802", "15427", "1803", "15439", "1804", "15443", "1805", "15451", "1806", "15461", "1807", "15467", "1808", "15473", "1809", "15493", "1810", "15497"],
["1811", "15511", "1812", "15527", "1813", "15541", "1814", "15551", "1815", "15559", "1816", "15569", "1817", "15581", "1818", "15583", "1819", "15601", "1820", "15607"],
["1821", "15619", "1822", "15629", "1823", "15641", "1824", "15643", "1825", "15647", "1826", "15649", "1827", "15661", "1828", "15667", "1829", "15671", "1830", "15679"],
["1831", "15683", "1832", "15727", "1833", "15731", "1834", "15733", "1835", "15737", "1836", "15739", "1837", "15749", "1838", "15761", "1839", "15767", "1840", "15773"],
["1841", "15787", "1842", "15791", "1843", "15797", "1844", "15803", "1845", "15809", "1846", "15817", "1847", "15823", "1848", "15859", "1849", "15877", "1850", "15881"],
["1851", "15887", "1852", "15889", "1853", "15901", "1854", "15907", "1855", "15913", "1856", "15919", "1857", "15923", "1858", "15937", "1859", "15959", "1860", "15971"],
["1861", "15973", "1862", "15991", "1863", "16001", "1864", "16007", "1865", "16033", "1866", "16057", "1867", "16061", "1868", "16063", "1869", "16067", "1870", "16069"],
["1871", "16073", "1872", "16087", "1873", "16091", "1874", "16097", "1875", "16103", "1876", "16111", "1877", "16127", "1878", "16139", "1879", "16141", "1880", "16183"],
["1881", "16187", "1882", "16189", "1883", "16193", "1884", "16217", "1885", "16223", "1886", "16229", "1887", "16231", "1888", "16249", "1889", "16253", "1890", "16267"],
["1891", "16273", "1892", "16301", "1893", "16319", "1894", "16333", "1895", "16339", "1896", "16349", "1897", "16361", "1898", "16363", "1899", "16369", "1900", "16381"],
["1901", "16411", "1902", "16417", "1903", "16421", "1904", "16427", "1905", "16433", "1906", "16447", "1907", "16451", "1908", "16453", "1909", "16477", "1910", "16481"],
["1911", "16487", "1912", "16493", "1913", "16519", "1914", "16529", "1915", "16547", "1916", "16553", "1917", "16561", "1918", "16567", "1919", "16573", "1920", "16603"],
["1921", "16607", "1922", "16619", "1923", "16631", "1924", "16633", "1925", "16649", "1926", "16651", "1927", "16657", "1928", "16661", "1929", "16673", "1930", "16691"],
["1931", "16693", "1932", "16699", "1933", "16703", "1934", "16729", "1935", "16741", "1936", "16747", "1937", "16759", "1938", "16763", "1939", "16787", "1940", "16811"],
["1941", "16823", "1942", "16829", "1943", "16831", "1944", "16843", "1945", "16871", "1946", "16879", "1947", "16883", "1948", "16889", "1949", "16901", "1950", "16903"],
["1951", "16921", "1952", "16927", "1953", "16931", "1954", "16937", "1955", "16943", "1956", "16963", "1957", "16979", "1958", "16981", "1959", "16987", "1960", "16993"],
["1961", "17011", "1962", "17021", "1963", "17027", "1964", "17029", "1965", "17033", "1966", "17041", "1967", "17047", "1968", "17053", "1969", "17077", "1970", "17093"],
["1971", "17099", "1972", "17107", "1973", "17117", "1974", "17123", "1975", "17137", "1976", "17159", "1977", "17167", "1978", "17183", "1979", "17189", "1980", "17191"],
["1981", "17203", "1982", "17207", "1983", "17209", "1984", "17231", "1985", "17239", "1986", "17257", "1987", "17291", "1988", "17293", "1989", "17299", "1990", "17317"],
["1991", "17321", "1992", "17327", "1993", "17333", "1994", "17341", "1995", "17351", "1996", "17359", "1997", "17377", "1998", "17383", "1999", "17387", "2000", "17389"],
["2001", "17393", "2002", "17401", "2003", "17417", "2004", "17419", "2005", "17431", "2006", "17443", "2007", "17449", "2008", "17467", "2009", "17471", "2010", "17477"],
["2011", "17483", "2012", "17489", "2013", "17491", "2014", "17497", "2015", "17509", "2016", "17519", "2017", "17539", "2018", "17551", "2019", "17569", "2020", "17573"],
["2021", "17579", "2022", "17581", "2023", "17597", "2024", "17599", "2025", "17609", "2026", "17623", "2027", "17627", "2028", "17657", "2029", "17659", "2030", "17669"],
["2031", "17681", "2032", "17683", "2033", "17707", "2034", "17713", "2035", "17729", "2036", "17737", "2037", "17747", "2038", "17749", "2039", "17761", "2040", "17783"],
["2041", "17789", "2042", "17791", "2043", "17807", "2044", "17827", "2045", "17837", "2046", "17839", "2047", "17851", "2048", "17863", "2049", "17881", "2050", "17891"],
["2051", "17903", "2052", "17909", "2053", "17911", "2054", "17921", "2055", "17923", "2056", "17929", "2057", "17939", "2058", "17957", "2059", "17959", "2060", "17971"],
["2061", "17977", "2062", "17981", "2063", "17987", "2064", "17989", "2065", "18013", "2066", "18041", "2067", "18043", "2068", "18047", "2069", "18049", "2070", "18059"],
["2071", "18061", "2072", "18077", "2073", "18089", "2074", "18097", "2075", "18119", "2076", "18121", "2077", "18127", "2078", "18131", "2079", "18133", "2080", "18143"],
["2081", "18149", "2082", "18169", "2083", "18181", "2084", "18191", "2085", "18199", "2086", "18211", "2087", "18217", "2088", "18223", "2089", "18229", "2090", "18233"],
["2091", "18251", "2092", "18253", "2093", "18257", "2094", "18269", "2095", "18287", "2096", "18289", "2097", "18301", "2098", "18307", "2099", "18311", "2100", "18313"],
["2101", "18329", "2102", "18341", "2103", "18353", "2104", "18367", "2105", "18371", "2106", "18379", "2107", "18397", "2108", "18401", "2109", "18413", "2110", "18427"],
["2111", "18433", "2112", "18439", "2113", "18443", "2114", "18451", "2115", "18457", "2116", "18461", "2117", "18481", "2118", "18493", "2119", "18503", "2120", "18517"],
["2121", "18521", "2122", "18523", "2123", "18539", "2124", "18541", "2125", "18553", "2126", "18583", "2127", "18587", "2128", "18593", "2129", "18617", "2130", "18637"],
["2131", "18661", "2132", "18671", "2133", "18679", "2134", "18691", "2135", "18701", "2136", "18713", "2137", "18719", "2138", "18731", "2139", "18743", "2140", "18749"],
["2141", "18757", "2142", "18773", "2143", "18787", "2144", "18793", "2145", "18797", "2146", "18803", "2147", "18839", "2148", "18859", "2149", "18869", "2150", "18899"],
["2151", "189"]]}}]}
==============================================================================
================================================================================
MERGE RESULT: ✅ SUCCESS
================================================================================
Final result length: 36944 chars
Final result (COMPLETE):
================================================================================
{
"elements": [
{
"type": "table",
"content": {
"headers": ["Nr.1", "Primzahl1", "Nr.2", "Primzahl2", "Nr.3", "Primzahl3", "Nr.4", "Primzahl4", "Nr.5", "Primzahl5", "Nr.6", "Primzahl6", "Nr.7", "Primzahl7", "Nr.8", "Primzahl8", "Nr.9", "Primzahl9", "Nr.10", "Primzahl10"],
"rows": [
["1", "2", "2", "3", "3", "5", "4", "7", "5", "11", "6", "13", "7", "17", "8", "19", "9", "23", "10", "29"],
["11", "31", "12", "37", "13", "41", "14", "43", "15", "47", "16", "53", "17", "59", "18", "61", "19", "67", "20", "71"],
["21", "73", "22", "79", "23", "83", "24", "89", "25", "97", "26", "101", "27", "103", "28", "107", "29", "109", "30", "113"],
["31", "127", "32", "131", "33", "137", "34", "139", "35", "149", "36", "151", "37", "157", "38", "163", "39", "167", "40", "173"],
["41", "179", "42", "181", "43", "191", "44", "193", "45", "197", "46", "199", "47", "211", "48", "223", "49", "227", "50", "229"],
["51", "233", "52", "239", "53", "241", "54", "251", "55", "257", "56", "263", "57", "269", "58", "271", "59", "277", "60", "281"],
["61", "283", "62", "293", "63", "307", "64", "311", "65", "313", "66", "317", "67", "331", "68", "337", "69", "347", "70", "349"],
["71", "353", "72", "359", "73", "367", "74", "373", "75", "379", "76", "383", "77", "389", "78", "397", "79", "401", "80", "409"],
["81", "419", "82", "421", "83", "431", "84", "433", "85", "439", "86", "443", "87", "449", "88", "457", "89", "461", "90", "463"],
["91", "467", "92", "479", "93", "487", "94", "491", "95", "499", "96", "503", "97", "509", "98", "521", "99", "523", "100", "541"],
["101", "547", "102", "557", "103", "563", "104", "569", "105", "571", "106", "577", "107", "587", "108", "593", "109", "599", "110", "601"],
["111", "607", "112", "613", "113", "617", "114", "619", "115", "631", "116", "641", "117", "643", "118", "647", "119", "653", "120", "659"],
["121", "661", "122", "673", "123", "677", "124", "683", "125", "691", "126", "701", "127", "709", "128", "719", "129", "727", "130", "733"],
["131", "739", "132", "743", "133", "751", "134", "757", "135", "761", "136", "769", "137", "773", "138", "787", "139", "797", "140", "809"],
["141", "811", "142", "821", "143", "823", "144", "827", "145", "829", "146", "839", "147", "853", "148", "857", "149", "859", "150", "863"],
["151", "877", "152", "881", "153", "883", "154", "887", "155", "907", "156", "911", "157", "919", "158", "929", "159", "937", "160", "941"],
["161", "947", "162", "953", "163", "967", "164", "971", "165", "977", "166", "983", "167", "991", "168", "997", "169", "1009", "170", "1013"],
["171", "1019", "172", "1021", "173", "1031", "174", "1033", "175", "1039", "176", "1049", "177", "1051", "178", "1061", "179", "1063", "180", "1069"],
["181", "1087", "182", "1091", "183", "1093", "184", "1097", "185", "1103", "186", "1109", "187", "1117", "188", "1123", "189", "1129", "190", "1151"],
["191", "1153", "192", "1163", "193", "1171", "194", "1181", "195", "1187", "196", "1193", "197", "1201", "198", "1213", "199", "1217", "200", "1223"],
["201", "1229", "202", "1231", "203", "1237", "204", "1249", "205", "1259", "206", "1277", "207", "1279", "208", "1283", "209", "1289", "210", "1291"],
["211", "1297", "212", "1301", "213", "1303", "214", "1307", "215", "1319", "216", "1321", "217", "1327", "218", "1361", "219", "1367", "220", "1373"],
["221", "1381", "222", "1399", "223", "1409", "224", "1423", "225", "1427", "226", "1429", "227", "1433", "228", "1439", "229", "1447", "230", "1451"],
["231", "1453", "232", "1459", "233", "1471", "234", "1481", "235", "1483", "236", "1487", "237", "1489", "238", "1493", "239", "1499", "240", "1511"],
["241", "1523", "242", "1531", "243", "1543", "244", "1549", "245", "1553", "246", "1559", "247", "1567", "248", "1571", "249", "1579", "250", "1583"],
["251", "1597", "252", "1601", "253", "1607", "254", "1609", "255", "1613", "256", "1619", "257", "1621", "258", "1627", "259", "1637", "260", "1657"],
["261", "1663", "262", "1667", "263", "1669", "264", "1693", "265", "1697", "266", "1699", "267", "1709", "268", "1721", "269", "1723", "270", "1733"],
["271", "1741", "272", "1747", "273", "1753", "274", "1759", "275", "1777", "276", "1783", "277", "1787", "278", "1789", "279", "1801", "280", "1811"],
["281", "1823", "282", "1831", "283", "1847", "284", "1861", "285", "1867", "286", "1871", "287", "1873", "288", "1877", "289", "1879", "290", "1889"],
["291", "1901", "292", "1907", "293", "1913", "294", "1931", "295", "1933", "296", "1949", "297", "1951", "298", "1973", "299", "1979", "300", "1987"],
["301", "1993", "302", "1997", "303", "1999", "304", "2003", "305", "2011", "306", "2017", "307", "2027", "308", "2029", "309", "2039", "310", "2053"],
["311", "2063", "312", "2069", "313", "2081", "314", "2083", "315", "2087", "316", "2089", "317", "2099", "318", "2111", "319", "2113", "320", "2129"],
["321", "2131", "322", "2137", "323", "2141", "324", "2143", "325", "2153", "326", "2161", "327", "2179", "328", "2203", "329", "2207", "330", "2213"],
["331", "2221", "332", "2237", "333", "2239", "334", "2243", "335", "2251", "336", "2267", "337", "2269", "338", "2273", "339", "2281", "340", "2287"],
["341", "2293", "342", "2297", "343", "2309", "344", "2311", "345", "2333", "346", "2339", "347", "2341", "348", "2347", "349", "2351", "350", "2357"],
["351", "2371", "352", "2377", "353", "2381", "354", "2383", "355", "2389", "356", "2393", "357", "2399", "358", "2411", "359", "2417", "360", "2423"],
["361", "2437", "362", "2441", "363", "2447", "364", "2459", "365", "2467", "366", "2473", "367", "2477", "368", "2503", "369", "2521", "370", "2531"],
["371", "2539", "372", "2543", "373", "2549", "374", "2551", "375", "2557", "376", "2579", "377", "2591", "378", "2593", "379", "2609", "380", "2617"],
["381", "2621", "382", "2633", "383", "2647", "384", "2657", "385", "2659", "386", "2663", "387", "2671", "388", "2677", "389", "2683", "390", "2687"],
["391", "2689", "392", "2693", "393", "2699", "394", "2707", "395", "2711", "396", "2713", "397", "2719", "398", "2729", "399", "2731", "400", "2741"],
["401", "2749", "402", "2753", "403", "2767", "404", "2777", "405", "2789", "406", "2791", "407", "2797", "408", "2801", "409", "2803", "410", "2819"],
["411", "2833", "412", "2837", "413", "2843", "414", "2851", "415", "2857", "416", "2861", "417", "2879", "418", "2887", "419", "2897", "420", "2903"],
["421", "2909", "422", "2917", "423", "2927", "424", "2939", "425", "2953", "426", "2957", "427", "2963", "428", "2969", "429", "2971", "430", "2999"],
["431", "3001", "432", "3011", "433", "3019", "434", "3023", "435", "3037", "436", "3041", "437", "3049", "438", "3061", "439", "3067", "440", "3079"],
["441", "3083", "442", "3089", "443", "3109", "444", "3119", "445", "3121", "446", "3137", "447", "3163", "448", "3167", "449", "3169", "450", "3181"],
["451", "3187", "452", "3191", "453", "3203", "454", "3209", "455", "3217", "456", "3221", "457", "3229", "458", "3251", "459", "3253", "460", "3257"],
["461", "3259", "462", "3271", "463", "3299", "464", "3301", "465", "3307", "466", "3313", "467", "3319", "468", "3323", "469", "3329", "470", "3331"],
["471", "3343", "472", "3347", "473", "3359", "474", "3361", "475", "3371", "476", "3373", "477", "3389", "478", "3391", "479", "3407", "480", "3413"],
["481", "3433", "482", "3449", "483", "3457", "484", "3461", "485", "3463", "486", "3467", "487", "3469", "488", "3491", "489", "3499", "490", "3511"],
["491", "3517", "492", "3527", "493", "3529", "494", "3533", "495", "3539", "496", "3541", "497", "3547", "498", "3557", "499", "3559", "500", "3571"],
["501", "3581", "502", "3583", "503", "3593", "504", "3607", "505", "3613", "506", "3617", "507", "3623", "508", "3631", "509", "3637", "510", "3643"],
["511", "3659", "512", "3671", "513", "3673", "514", "3677", "515", "3691", "516", "3697", "517", "3701", "518", "3709", "519", "3719", "520", "3727"],
["521", "3733", "522", "3739", "523", "3761", "524", "3767", "525", "3769", "526", "3779", "527", "3793", "528", "3797", "529", "3803", "530", "3821"],
["531", "3823", "532", "3833", "533", "3847", "534", "3851", "535", "3853", "536", "3863", "537", "3877", "538", "3881", "539", "3889", "540", "3907"],
["541", "3911", "542", "3917", "543", "3919", "544", "3923", "545", "3929", "546", "3931", "547", "3943", "548", "3947", "549", "3967", "550", "3989"],
["551", "4001", "552", "4003", "553", "4007", "554", "4013", "555", "4019", "556", "4021", "557", "4027", "558", "4049", "559", "4051", "560", "4057"],
["561", "4073", "562", "4079", "563", "4091", "564", "4093", "565", "4099", "566", "4111", "567", "4127", "568", "4129", "569", "4133", "570", "4139"],
["571", "4153", "572", "4157", "573", "4159", "574", "4177", "575", "4201", "576", "4211", "577", "4217", "578", "4219", "579", "4229", "580", "4231"],
["581", "4241", "582", "4243", "583", "4253", "584", "4259", "585", "4261", "586", "4271", "587", "4273", "588", "4283", "589", "4289", "590", "4297"],
["591", "4327", "592", "4337", "593", "4339", "594", "4349", "595", "4357", "596", "4363", "597", "4373", "598", "4391", "599", "4397", "600", "4409"],
["601", "4421", "602", "4423", "603", "4441", "604", "4447", "605", "4451", "606", "4457", "607", "4463", "608", "4481", "609", "4483", "610", "4493"],
["611", "4507", "612", "4513", "613", "4517", "614", "4519", "615", "4523", "616", "4547", "617", "4549", "618", "4561", "619", "4567", "620", "4583"],
["621", "4591", "622", "4597", "623", "4603", "624", "4621", "625", "4637", "626", "4639", "627", "4643", "628", "4649", "629", "4651", "630", "4657"],
["631", "4663", "632", "4673", "633", "4679", "634", "4691", "635", "4703", "636", "4721", "637", "4723", "638", "4729", "639", "4733", "640", "4751"],
["641", "4759", "642", "4783", "643", "4787", "644", "4789", "645", "4793", "646", "4799", "647", "4801", "648", "4813", "649", "4817", "650", "4831"],
["651", "4861", "652", "4871", "653", "4877", "654", "4889", "655", "4903", "656", "4909", "657", "4919", "658", "4931", "659", "4933", "660", "4937"],
["661", "4943", "662", "4951", "663", "4957", "664", "4967", "665", "4969", "666", "4973", "667", "4987", "668", "4993", "669", "4999", "670", "5003"],
["671", "5009", "672", "5011", "673", "5021", "674", "5023", "675", "5039", "676", "5051", "677", "5059", "678", "5077", "679", "5081", "680", "5087"],
["681", "5099", "682", "5101", "683", "5107", "684", "5113", "685", "5119", "686", "5147", "687", "5153", "688", "5167", "689", "5171", "690", "5179"],
["691", "5189", "692", "5197", "693", "5209", "694", "5227", "695", "5231", "696", "5233", "697", "5237", "698", "5261", "699", "5273", "700", "5279"],
["701", "5281", "702", "5297", "703", "5303", "704", "5309", "705", "5323", "706", "5333", "707", "5347", "708", "5351", "709", "5381", "710", "5387"],
["711", "5393", "712", "5399", "713", "5407", "714", "5413", "715", "5417", "716", "5419", "717", "5431", "718", "5437", "719", "5441", "720", "5443"],
["721", "5449", "722", "5471", "723", "5477", "724", "5479", "725", "5483", "726", "5501", "727", "5503", "728", "5507", "729", "5519", "730", "5521"],
["731", "5527", "732", "5531", "733", "5557", "734", "5563", "735", "5569", "736", "5573", "737", "5581", "738", "5591", "739", "5623", "740", "5639"],
["741", "5641", "742", "5647", "743", "5651", "744", "5653", "745", "5657", "746", "5659", "747", "5669", "748", "5683", "749", "5689", "750", "5693"],
["751", "5701", "752", "5711", "753", "5717", "754", "5737", "755", "5741", "756", "5743", "757", "5749", "758", "5779", "759", "5783", "760", "5791"],
["761", "5801", "762", "5807", "763", "5813", "764", "5821", "765", "5827", "766", "5839", "767", "5843", "768", "5849", "769", "5851", "770", "5857"],
["771", "5861", "772", "5867", "773", "5869", "774", "5879", "775", "5881", "776", "5897", "777", "5903", "778", "5923", "779", "5927", "780", "5939"],
["781", "5953", "782", "5981", "783", "5987", "784", "6007", "785", "6011", "786", "6029", "787", "6037", "788", "6043", "789", "6047", "790", "6053"],
["791", "6067", "792", "6073", "793", "6079", "794", "6089", "795", "6091", "796", "6101", "797", "6113", "798", "6121", "799", "6131", "800", "6133"],
["801", "6143", "802", "6151", "803", "6163", "804", "6173", "805", "6197", "806", "6199", "807", "6203", "808", "6211", "809", "6217", "810", "6221"],
["811", "6229", "812", "6247", "813", "6257", "814", "6263", "815", "6269", "816", "6271", "817", "6277", "818", "6287", "819", "6299", "820", "6301"],
["821", "6311", "822", "6317", "823", "6323", "824", "6329", "825", "6337", "826", "6343", "827", "6353", "828", "6359", "829", "6361", "830", "6367"],
["831", "6373", "832", "6379", "833", "6389", "834", "6397", "835", "6421", "836", "6427", "837", "6449", "838", "6451", "839", "6469", "840", "6473"],
["841", "6481", "842", "6491", "843", "6521", "844", "6529", "845", "6547", "846", "6551", "847", "6553", "848", "6563", "849", "6569", "850", "6571"],
["851", "6577", "852", "6581", "853", "6599", "854", "6607", "855", "6619", "856", "6637", "857", "6653", "858", "6659", "859", "6661", "860", "6673"],
["861", "6679", "862", "6689", "863", "6691", "864", "6701", "865", "6703", "866", "6709", "867", "6719", "868", "6733", "869", "6737", "870", "6761"],
["871", "6763", "872", "6779", "873", "6781", "874", "6791", "875", "6793", "876", "6803", "877", "6823", "878", "6827", "879", "6829", "880", "6833"],
["881", "6841", "882", "6857", "883", "6863", "884", "6869", "885", "6871", "886", "6883", "887", "6899", "888", "6907", "889", "6911", "890", "6917"],
["891", "6947", "892", "6949", "893", "6959", "894", "6961", "895", "6967", "896", "6971", "897", "6977", "898", "6983", "899", "6991", "900", "6997"],
["901", "7001", "902", "7013", "903", "7019", "904", "7027", "905", "7039", "906", "7043", "907", "7057", "908", "7069", "909", "7079", "910", "7103"],
["911", "7109", "912", "7121", "913", "7127", "914", "7129", "915", "7151", "916", "7159", "917", "7177", "918", "7187", "919", "7193", "920", "7207"],
["921", "7211", "922", "7213", "923", "7219", "924", "7229", "925", "7237", "926", "7243", "927", "7247", "928", "7253", "929", "7283", "930", "7297"],
["931", "7307", "932", "7309", "933", "7321", "934", "7331", "935", "7333", "936", "7349", "937", "7351", "938", "7369", "939", "7393", "940", "7411"],
["941", "7417", "942", "7433", "943", "7451", "944", "7457", "945", "7459", "946", "7477", "947", "7481", "948", "7487", "949", "7489", "950", "7499"],
["951", "7507", "952", "7517", "953", "7523", "954", "7529", "955", "7537", "956", "7541", "957", "7547", "958", "7549", "959", "7559", "960", "7561"],
["961", "7573", "962", "7577", "963", "7583", "964", "7589", "965", "7591", "966", "7603", "967", "7607", "968", "7621", "969", "7639", "970", "7643"],
["971", "7649", "972", "7669", "973", "7673", "974", "7681", "975", "7687", "976", "7691", "977", "7699", "978", "7703", "979", "7717", "980", "7723"],
["981", "7727", "982", "7741", "983", "7753", "984", "7757", "985", "7759", "986", "7789", "987", "7793", "988", "7817", "989", "7823", "990", "7829"],
["991", "7841", "992", "7853", "993", "7867", "994", "7873", "995", "7877", "996", "7879", "997", "7883", "998", "7901", "999", "7907", "1000", "7919"],
["1001", "7927", "1002", "7933", "1003", "7937", "1004", "7949", "1005", "7951", "1006", "7963", "1007", "7993", "1008", "8009", "1009", "8011", "1010", "8017"],
["1011", "8039", "1012", "8053", "1013", "8059", "1014", "8069", "1015", "8081", "1016", "8087", "1017", "8089", "1018", "8093", "1019", "8101", "1020", "8111"],
["1021", "8117", "1022", "8123", "1023", "8147", "1024", "8161", "1025", "8167", "1026", "8171", "1027", "8179", "1028", "8191", "1029", "8209", "1030", "8219"],
["1031", "8221", "1032", "8231", "1033", "8233", "1034", "8237", "1035", "8243", "1036", "8263", "1037", "8269", "1038", "8273", "1039", "8287", "1040", "8291"],
["1041", "8293", "1042", "8297", "1043", "8311", "1044", "8317", "1045", "8329", "1046", "8353", "1047", "8363", "1048", "8369", "1049", "8377", "1050", "8387"],
["1051", "8389", "1052", "8419", "1053", "8423", "1054", "8429", "1055", "8431", "1056", "8443", "1057", "8447", "1058", "8461", "1059", "8467", "1060", "8501"],
["1061", "8513", "1062", "8521", "1063", "8527", "1064", "8537", "1065", "8539", "1066", "8543", "1067", "8563", "1068", "8573", "1069", "8581", "1070", "8597"],
["1071", "8599", "1072", "8609", "1073", "8623", "1074", "8627", "1075", "8629", "1076", "8641", "1077", "8647", "1078", "8663", "1079", "8669", "1080", "8677"],
["1081", "8681", "1082", "8689", "1083", "8693", "1084", "8699", "1085", "8707", "1086", "8713", "1087", "8719", "1088", "8731", "1089", "8737", "1090", "8741"],
["1091", "8747", "1092", "8753", "1093", "8761", "1094", "8779", "1095", "8783", "1096", "8803", "1097", "8807", "1098", "8819", "1099", "8821", "1100", "8831"],
["1101", "8837", "1102", "8839", "1103", "8849", "1104", "8861", "1105", "8863", "1106", "8867", "1107", "8887", "1108", "8893", "1109", "8923", "1110", "8929"],
["1111", "8933", "1112", "8941", "1113", "8951", "1114", "8963", "1115", "8969", "1116", "8971", "1117", "8999", "1118", "9001", "1119", "9007", "1120", "9011"],
["1121", "9013", "1122", "9029", "1123", "9041", "1124", "9043", "1125", "9049", "1126", "9059", "1127", "9067", "1128", "9091", "1129", "9103", "1130", "9109"],
["1131", "9127", "1132", "9133", "1133", "9137", "1134", "9151", "1135", "9157", "1136", "9161", "1137", "9173", "1138", "9181", "1139", "9187", "1140", "9199"],
["1141", "9203", "1142", "9209", "1143", "9221", "1144", "9227", "1145", "9239", "1146", "9241", "1147", "9257", "1148", "9277", "1149", "9281", "1150", "9283"],
["1151", "9293", "1152", "9311", "1153", "9319", "1154", "9323", "1155", "9337", "1156", "9341", "1157", "9343", "1158", "9349", "1159", "9371", "1160", "9377"],
["1161", "9391", "1162", "9397", "1163", "9403", "1164", "9413", "1165", "9419", "1166", "9421", "1167", "9431", "1168", "9433", "1169", "9437", "1170", "9439"],
["1171", "9461", "1172", "9463", "1173", "9467", "1174", "9473", "1175", "9479", "1176", "9491", "1177", "9497", "1178", "9511", "1179", "9521", "1180", "9533"],
["1181", "9539", "1182", "9547", "1183", "9551", "1184", "9587", "1185", "9601", "1186", "9613", "1187", "9619", "1188", "9623", "1189", "9629", "1190", "9631"],
["1191", "9643", "1192", "9649", "1193", "9661", "1194", "9677", "1195", "9679", "1196", "9689", "1197", "9697", "1198", "9719", "1199", "9721", "1200", "9733"],
["1201", "9739", "1202", "9743", "1203", "9749", "1204", "9767", "1205", "9769", "1206", "9781", "1207", "9787", "1208", "9791", "1209", "9803", "1210", "9811"],
["1211", "9817", "1212", "9829", "1213", "9833", "1214", "9839", "1215", "9851", "1216", "9857", "1217", "9859", "1218", "9871", "1219", "9883", "1220", "9887"],
["1221", "9901", "1222", "9907", "1223", "9923", "1224", "9929", "1225", "9931", "1226", "9941", "1227", "9949", "1228", "9967", "1229", "9973", "1230", "10007"],
["1231", "10009", "1232", "10037", "1233", "10039", "1234", "10061", "1235", "10067", "1236", "10069", "1237", "10079", "1238", "10091", "1239", "10093", "1240", "10099"],
["1241", "10103", "1242", "10111", "1243", "10133", "1244", "10139", "1245", "10141", "1246", "10151", "1247", "10159", "1248", "10163", "1249", "10169", "1250", "10177"],
["1251", "10181", "1252", "10193", "1253", "10211", "1254", "10223", "1255", "10243", "1256", "10247", "1257", "10253", "1258", "10259", "1259", "10267", "1260", "10271"],
["1261", "10273", "1262", "10289", "1263", "10301", "1264", "10303", "1265", "10313", "1266", "10321", "1267", "10331", "1268", "10333", "1269", "10337", "1270", "10343"],
["1271", "10357", "1272", "10369", "1273", "10391", "1274", "10399", "1275", "10427", "1276", "10429", "1277", "10433", "1278", "10453", "1279", "10457", "1280", "10459"],
["1281", "10463", "1282", "10477", "1283", "10487", "1284", "10499", "1285", "10501", "1286", "10513", "1287", "10529", "1288", "10531", "1289", "10559", "1290", "10567"],
["1291", "10589", "1292", "10597", "1293", "10601", "1294", "10607", "1295", "10613", "1296", "10627", "1297", "10631", "1298", "10639", "1299", "10651", "1300", "10657"],
["1301", "10663", "1302", "10667", "1303", "10687", "1304", "10691", "1305", "10709", "1306", "10711", "1307", "10723", "1308", "10729", "1309", "10733", "1310", "10739"],
["1311", "10753", "1312", "10771", "1313", "10781", "1314", "10789", "1315", "10799", "1316", "10831", "1317", "10837", "1318", "10847", "1319", "10853", "1320", "10859"],
["1321", "10861", "1322", "10867", "1323", "10883", "1324", "10889", "1325", "10891", "1326", "10903", "1327", "10909", "1328", "10937", "1329", "10939", "1330", "10949"],
["1331", "10957", "1332", "10973", "1333", "10979", "1334", "10987", "1335", "10993", "1336", "11003", "1337", "11027", "1338", "11047", "1339", "11057", "1340", "11059"],
["1341", "11069", "1342", "11071", "1343", "11083", "1344", "11087", "1345", "11093", "1346", "11113", "1347", "11117", "1348", "11119", "1349", "11131", "1350", "11149"],
["1351", "11159", "1352", "11161", "1353", "11171", "1354", "11173", "1355", "11177", "1356", "11197", "1357", "11213", "1358", "11239", "1359", "11243", "1360", "11251"],
["1361", "11257", "1362", "11261", "1363", "11273", "1364", "11279", "1365", "11287", "1366", "11299", "1367", "11311", "1368", "11317", "1369", "11321", "1370", "11329"],
["1371", "11351", "1372", "11353", "1373", "11369", "1374", "11383", "1375", "11393", "1376", "11399", "1377", "11411", "1378", "11423", "1379", "11437", "1380", "11443"],
["1381", "11447", "1382", "11467", "1383", "11471", "1384", "11483", "1385", "11489", "1386", "11491", "1387", "11497", "1388", "11503", "1389", "11519", "1390", "11527"],
["1391", "11549", "1392", "11551", "1393", "11579", "1394", "11587", "1395", "11593", "1396", "11597", "1397", "11617", "1398", "11621", "1399", "11633", "1400", "11657"],
["1401", "11677", "1402", "11681", "1403", "11689", "1404", "11699", "1405", "11701", "1406", "11717", "1407", "11719", "1408", "11731", "1409", "11743", "1410", "11777"],
["1411", "11779", "1412", "11783", "1413", "11789", "1414", "11801", "1415", "11807", "1416", "11813", "1417", "11821", "1418", "11827", "1419", "11831", "1420", "11833"],
["1421", "11839", "1422", "11863", "1423", "11867", "1424", "11887", "1425", "11897", "1426", "11903", "1427", "11909", "1428", "11923", "1429", "11927", "1430", "11933"],
["1431", "11939", "1432", "11941", "1433", "11953", "1434", "11959", "1435", "11969", "1436", "11971", "1437", "11981", "1438", "11987", "1439", "12007", "1440", "12011"],
["1441", "12037", "1442", "12041", "1443", "12043", "1444", "12049", "1445", "12071", "1446", "12073", "1447", "12097", "1448", "12101", "1449", "12107", "1450", "12109"],
["1451", "12113", "1452", "12119", "1453", "12143", "1454", "12149", "1455", "12157", "1456", "12161", "1457", "12163", "1458", "12197", "1459", "12203", "1460", "12211"],
["1461", "12227", "1462", "12239", "1463", "12241", "1464", "12251", "1465", "12253", "1466", "12263", "1467", "12269", "1468", "12277", "1469", "12281", "1470", "12289"],
["1471", "12301", "1472", "12323", "1473", "12329", "1474", "12343", "1475", "12347", "1476", "12373", "1477", "12377", "1478", "12379", "1479", "12391", "1480", "12401"],
["1481", "12409", "1482", "12413", "1483", "12421", "1484", "12433", "1485", "12437", "1486", "12451", "1487", "12457", "1488", "12473", "1489", "12479", "1490", "12487"],
["1491", "12491", "1492", "12497", "1493", "12503", "1494", "12511", "1495", "12517", "1496", "12527", "1497", "12539", "1498", "12541", "1499", "12547", "1500", "12553"],
["1501", "12569", "1502", "12577", "1503", "12583", "1504", "12589", "1505", "12601", "1506", "12611", "1507", "12613", "1508", "12619", "1509", "12637", "1510", "12641"],
["1511", "12647", "1512", "12653", "1513", "12659", "1514", "12671", "1515", "12689", "1516", "12697", "1517", "12703", "1518", "12713", "1519", "12721", "1520", "12739"],
["1521", "12743", "1522", "12757", "1523", "12763", "1524", "12781", "1525", "12791", "1526", "12799", "1527", "12809", "1528", "12821", "1529", "12823", "1530", "12829"],
["1531", "12841", "1532", "12853", "1533", "12889", "1534", "12893", "1535", "12899", "1536", "12907", "1537", "12911", "1538", "12917", "1539", "12919", "1540", "12923"],
["1541", "12941", "1542", "12953", "1543", "12959", "1544", "12967", "1545", "12973", "1546", "12979", "1547", "12983", "1548", "13001", "1549", "13003", "1550", "13007"],
["1551", "13009", "1552", "13033", "1553", "13037", "1554", "13043", "1555", "13049", "1556", "13063", "1557", "13093", "1558", "13099", "1559", "13103", "1560", "13109"],
["1561", "13121", "1562", "13127", "1563", "13147", "1564", "13151", "1565", "13159", "1566", "13163", "1567", "13171", "1568", "13177", "1569", "13183", "1570", "13187"],
["1571", "13217", "1572", "13219", "1573", "13229", "1574", "13241", "1575", "13249", "1576", "13259", "1577", "13267", "1578", "13291", "1579", "13297", "1580", "13309"],
["1581", "13313", "1582", "13327", "1583", "13331", "1584", "13337", "1585", "13339", "1586", "13367", "1587", "13381", "1588", "13397", "1589", "13399", "1590", "13411"],
["1591", "13417", "1592", "13421", "1593", "13441", "1594", "13451", "1595", "13457", "1596", "13463", "1597", "13469", "1598", "13477", "1599", "13487", "1600", "13499"],
["1601", "13513", "1602", "13523", "1603", "13537", "1604", "13553", "1605", "13567", "1606", "13577", "1607", "13591", "1608", "13597", "1609", "13613", "1610", "13619"],
["1611", "13627", "1612", "13633", "1613", "13649", "1614", "13669", "1615", "13679", "1616", "13681", "1617", "13687", "1618", "13691", "1619", "13693", "1620", "13697"],
["1621", "13709", "1622", "13711", "1623", "13721", "1624", "13723", "1625", "13729", "1626", "13751", "1627", "13757", "1628", "13759", "1629", "13763", "1630", "13781"],
["1631", "13789", "1632", "13799", "1633", "13807", "1634", "13829", "1635", "13831", "1636", "13841", "1637", "13859", "1638", "13873", "1639", "13877", "1640", "13879"],
["1641", "13883", "1642", "13901", "1643", "13903", "1644", "13907", "1645", "13913", "1646", "13921", "1647", "13931", "1648", "13933", "1649", "13963", "1650", "13967"],
["1651", "13997", "1652", "13999", "1653", "14009", "1654", "14011", "1655", "14029", "1656", "14033", "1657", "14051", "1658", "14057", "1659", "14071", "1660", "14081"],
["1661", "14083", "1662", "14087", "1663", "14107", "1664", "14143", "1665", "14149", "1666", "14153", "1667", "14159", "1668", "14173", "1669", "14177", "1670", "14197"],
["1671", "14207", "1672", "14221", "1673", "14243", "1674", "14249", "1675", "14251", "1676", "14281", "1677", "14293", "1678", "14303", "1679", "14321", "1680", "14323"],
["1681", "14327", "1682", "14341", "1683", "14347", "1684", "14369", "1685", "14387", "1686", "14389", "1687", "14401", "1688", "14407", "1689", "14411", "1690", "14419"],
["1691", "14423", "1692", "14431", "1693", "14437", "1694", "14447", "1695", "14449", "1696", "14461", "1697", "14479", "1698", "14489", "1699", "14503", "1700", "14519"],
["1701", "14533", "1702", "14537", "1703", "14543", "1704", "14549", "1705", "14551", "1706", "14557", "1707", "14561", "1708", "14563", "1709", "14591", "1710", "14593"],
["1711", "14621", "1712", "14627", "1713", "14629", "1714", "14633", "1715", "14639", "1716", "14653", "1717", "14657", "1718", "14669", "1719", "14683", "1720", "14699"],
["1721", "14713", "1722", "14717", "1723", "14723", "1724", "14731", "1725", "14737", "1726", "14741", "1727", "14747", "1728", "14753", "1729", "14759", "1730", "14767"],
["1731", "14771", "1732", "14779", "1733", "14783", "1734", "14797", "1735", "14813", "1736", "14821", "1737", "14827", "1738", "14831", "1739", "14843", "1740", "14851"],
["1741", "14867", "1742", "14869", "1743", "14879", "1744", "14887", "1745", "14891", "1746", "14897", "1747", "14923", "1748", "14929", "1749", "14939", "1750", "14947"],
["1751", "14951", "1752", "14957", "1753", "14969", "1754", "14983", "1755", "15013", "1756", "15017", "1757", "15031", "1758", "15053", "1759", "15061", "1760", "15073"],
["1761", "15077", "1762", "15083", "1763", "15091", "1764", "15101", "1765", "15107", "1766", "15121", "1767", "15131", "1768", "15137", "1769", "15139", "1770", "15149"],
["1771", "15161", "1772", "15173", "1773", "15187", "1774", "15193", "1775", "15199", "1776", "15217", "1777", "15227", "1778", "15233", "1779", "15241", "1780", "15259"],
["1781", "15263", "1782", "15269", "1783", "15271", "1784", "15277", "1785", "15287", "1786", "15289", "1787", "15299", "1788", "15307", "1789", "15313", "1790", "15319"],
["1791", "15329", "1792", "15331", "1793", "15349", "1794", "15359", "1795", "15361", "1796", "15373", "1797", "15377", "1798", "15383", "1799", "15391", "1800", "15401"],
["1801", "15413", "1802", "15427", "1803", "15439", "1804", "15443", "1805", "15451", "1806", "15461", "1807", "15467", "1808", "15473", "1809", "15493", "1810", "15497"],
["1811", "15511", "1812", "15527", "1813", "15541", "1814", "15551", "1815", "15559", "1816", "15569", "1817", "15581", "1818", "15583", "1819", "15601", "1820", "15607"],
["1821", "15619", "1822", "15629", "1823", "15641", "1824", "15643", "1825", "15647", "1826", "15649", "1827", "15661", "1828", "15667", "1829", "15671", "1830", "15679"],
["1831", "15683", "1832", "15727", "1833", "15731", "1834", "15733", "1835", "15737", "1836", "15739", "1837", "15749", "1838", "15761", "1839", "15767", "1840", "15773"],
["1841", "15787", "1842", "15791", "1843", "15797", "1844", "15803", "1845", "15809", "1846", "15817", "1847", "15823", "1848", "15859", "1849", "15877", "1850", "15881"],
["1851", "15887", "1852", "15889", "1853", "15901", "1854", "15907", "1855", "15913", "1856", "15919", "1857", "15923", "1858", "15937", "1859", "15959", "1860", "15971"],
["1861", "15973", "1862", "15991", "1863", "16001", "1864", "16007", "1865", "16033", "1866", "16057", "1867", "16061", "1868", "16063", "1869", "16067", "1870", "16069"],
["1871", "16073", "1872", "16087", "1873", "16091", "1874", "16097", "1875", "16103", "1876", "16111", "1877", "16127", "1878", "16139", "1879", "16141", "1880", "16183"],
["1881", "16187", "1882", "16189", "1883", "16193", "1884", "16217", "1885", "16223", "1886", "16229", "1887", "16231", "1888", "16249", "1889", "16253", "1890", "16267"],
["1891", "16273", "1892", "16301", "1893", "16319", "1894", "16333", "1895", "16339", "1896", "16349", "1897", "16361", "1898", "16363", "1899", "16369", "1900", "16381"],
["1901", "16411", "1902", "16417", "1903", "16421", "1904", "16427", "1905", "16433", "1906", "16447", "1907", "16451", "1908", "16453", "1909", "16477", "1910", "16481"],
["1911", "16487", "1912", "16493", "1913", "16519", "1914", "16529", "1915", "16547", "1916", "16553", "1917", "16561", "1918", "16567", "1919", "16573", "1920", "16603"],
["1921", "16607", "1922", "16619", "1923", "16631", "1924", "16633", "1925", "16649", "1926", "16651", "1927", "16657", "1928", "16661", "1929", "16673", "1930", "16691"],
["1931", "16693", "1932", "16699", "1933", "16703", "1934", "16729", "1935", "16741", "1936", "16747", "1937", "16759", "1938", "16763", "1939", "16787", "1940", "16811"],
["1941", "16823", "1942", "16829", "1943", "16831", "1944", "16843", "1945", "16871", "1946", "16879", "1947", "16883", "1948", "16889", "1949", "16901", "1950", "16903"],
["1951", "16921", "1952", "16927", "1953", "16931", "1954", "16937", "1955", "16943", "1956", "16963", "1957", "16979", "1958", "16981", "1959", "16987", "1960", "16993"],
["1961", "17011", "1962", "17021", "1963", "17027", "1964", "17029", "1965", "17033", "1966", "17041", "1967", "17047", "1968", "17053", "1969", "17077", "1970", "17093"],
["1971", "17099", "1972", "17107", "1973", "17117", "1974", "17123", "1975", "17137", "1976", "17159", "1977", "17167", "1978", "17183", "1979", "17189", "1980", "17191"],
["1981", "17203", "1982", "17207", "1983", "17209", "1984", "17231", "1985", "17239", "1986", "17257", "1987", "17291", "1988", "17293", "1989", "17299", "1990", "17317"],
["1991", "17321", "1992", "17327", "1993", "17333", "1994", "17341", "1995", "17351", "1996", "17359", "1997", "17377", "1998", "17383", "1999", "17387", "2000", "17389"],
["2001", "17393", "2002", "17401", "2003", "17417", "2004", "17419", "2005", "17431", "2006", "17443", "2007", "17449", "2008", "17467", "2009", "17471", "2010", "17477"],
["2011", "17483", "2012", "17489", "2013", "17491", "2014", "17497", "2015", "17509", "2016", "17519", "2017", "17539", "2018", "17551", "2019", "17569", "2020", "17573"],
["2021", "17579", "2022", "17581", "2023", "17597", "2024", "17599", "2025", "17609", "2026", "17623", "2027", "17627", "2028", "17657", "2029", "17659", "2030", "17669"],
["2031", "17681", "2032", "17683", "2033", "17707", "2034", "17713", "2035", "17729", "2036", "17737", "2037", "17747", "2038", "17749", "2039", "17761", "2040", "17783"],
["2041", "17789", "2042", "17791", "2043", "17807", "2044", "17827", "2045", "17837", "2046", "17839", "2047", "17851", "2048", "17863", "2049", "17881", "2050", "17891"],
["2051", "17903", "2052", "17909", "2053", "17911", "2054", "17921", "2055", "17923", "2056", "17929", "2057", "17939", "2058", "17957", "2059", "17959", "2060", "17971"],
["2061", "17977", "2062", "17981", "2063", "17987", "2064", "17989", "2065", "18013", "2066", "18041", "2067", "18043", "2068", "18047", "2069", "18049", "2070", "18059"],
["2071", "18061", "2072", "18077", "2073", "18089", "2074", "18097", "2075", "18119", "2076", "18121", "2077", "18127", "2078", "18131", "2079", "18133", "2080", "18143"],
["2081", "18149", "2082", "18169", "2083", "18181", "2084", "18191", "2085", "18199", "2086", "18211", "2087", "18217", "2088", "18223", "2089", "18229", "2090", "18233"],
["2091", "18251", "2092", "18253", "2093", "18257", "2094", "18269", "2095", "18287", "2096", "18289", "2097", "18301", "2098", "18307", "2099", "18311", "2100", "18313"],
["2101", "18329", "2102", "18341", "2103", "18353", "2104", "18367", "2105", "18371", "2106", "18379", "2107", "18397", "2108", "18401", "2109", "18413", "2110", "18427"],
["2111", "18433", "2112", "18439", "2113", "18443", "2114", "18451", "2115", "18457", "2116", "18461", "2117", "18481", "2118", "18493", "2119", "18503", "2120", "18517"],
["2121", "18521", "2122", "18523", "2123", "18539", "2124", "18541", "2125", "18553", "2126", "18583", "2127", "18587", "2128", "18593", "2129", "18617", "2130", "18637"],
["2131", "18661", "2132", "18671", "2133", "18679", "2134", "18691", "2135", "18701", "2136", "18713", "2137", "18719", "2138", "18731", "2139", "18743", "2140", "18749"],
["2141", "18757", "2142", "18773", "2143", "18787", "2144", "18793", "2145", "18797", "2146", "18803", "2147", "18839", "2148", "18859", "2149", "18869", "2150", "18899"],
["2151", "189"]]}}]}
================================================================================

View file

@ -86,8 +86,6 @@ class AiCallLooper:
iteration = 0
allSections = [] # Accumulate all sections across iterations
lastRawResponse = None # Store last raw JSON response for continuation
documentMetadata = None # Store document metadata (title, filename) from first iteration
accumulationState = None # Track accumulation state for string accumulation
accumulatedDirectJson = [] # Accumulate JSON strings for direct return use cases (chapter_structure, code_structure)
# Get parent operation ID for iteration operations (parentId should be operationId, not log entry ID)
@ -113,28 +111,17 @@ class AiCallLooper:
# 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:
# This is a continuation - build continuation context with raw JSON and rebuild prompt
continuationContext = buildContinuationContext(allSections, lastRawResponse)
continuationContext = buildContinuationContext(allSections, lastRawResponse, useCaseId)
if not lastRawResponse:
logger.warning(f"Iteration {iteration}: No previous response available for continuation!")
# For section_content, pass all promptArgs (it uses buildSectionPromptWithContinuation which needs all args)
# For other use cases (chapter_structure, code_structure), filter to only accepted parameters
if useCaseId == "section_content":
# Pass all promptArgs plus continuationContext for section_content
iterationPrompt = await promptBuilder(**promptArgs, continuationContext=continuationContext)
else:
# Filter promptArgs to only include parameters that buildGenerationPrompt accepts
# buildGenerationPrompt accepts: outputFormat, userPrompt, title, extracted_content, continuationContext, services
filteredPromptArgs = {
k: v for k, v in promptArgs.items()
if k in ['outputFormat', 'userPrompt', 'title', 'extracted_content', 'services']
}
# Always include services if available
if not filteredPromptArgs.get('services') and hasattr(self, 'services'):
filteredPromptArgs['services'] = self.services
# Rebuild prompt with continuation context using the provided prompt builder
iterationPrompt = await promptBuilder(**filteredPromptArgs, continuationContext=continuationContext)
# 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)
else:
# First iteration - use original prompt
iterationPrompt = prompt
@ -251,11 +238,16 @@ class AiCallLooper:
pass
# Handle use cases that return JSON directly (no section extraction needed)
directReturnUseCases = ["section_content", "chapter_structure", "code_structure", "code_content", "image_batch"]
directReturnUseCases = ["section_content", "chapter_structure", "code_structure", "code_content"]
if useCaseId in directReturnUseCases:
# For chapter_structure, code_structure, and section_content, check completeness and support looping
loopingUseCases = ["chapter_structure", "code_structure", "section_content"]
# 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:
# 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
isStringIncomplete = self._isJsonStringIncomplete(extractedJsonForUseCase if extractedJsonForUseCase else result)
# If parsing failed (e.g., invalid JSON with comments or truncated JSON), continue looping to get valid JSON
if not parsedJsonForUseCase:
logger.info(f"Iteration {iteration}: Use case '{useCaseId}' - JSON parsing failed (likely incomplete/truncated), continuing iteration to complete")
@ -268,8 +260,12 @@ class AiCallLooper:
self.services.chat.progressLogFinish(iterationOperationId, True)
continue
# Check completeness if we have parsed JSON
isComplete = JsonResponseHandler.isJsonComplete(parsedJsonForUseCase)
# Check completeness: Use string-based check if available, otherwise fall back to parsed JSON check
if isStringIncomplete:
isComplete = False
else:
# Check completeness if we have parsed JSON
isComplete = JsonResponseHandler.isJsonComplete(parsedJsonForUseCase)
if not isComplete:
logger.warning(f"Iteration {iteration}: Use case '{useCaseId}' - JSON is incomplete, continuing for continuation")
@ -294,22 +290,45 @@ class AiCallLooper:
# Step 1: Merge all JSON strings using existing overlap detection
mergedJsonString = allJsonStrings[0] if allJsonStrings else ""
hasOverlap = True # Track if any overlap was found
for jsonStr in allJsonStrings[1:]:
mergedJsonString = JsonResponseHandler.mergeJsonStringsWithOverlap(mergedJsonString, jsonStr)
mergedJsonString, hasOverlapInMerge = JsonResponseHandler.mergeJsonStringsWithOverlap(mergedJsonString, jsonStr)
# If no overlap found in any merge, stop iterations
if not hasOverlapInMerge:
hasOverlap = False
logger.info(f"Iteration {iteration}: No overlap found during merge - stopping iterations and closing JSON")
break
# Step 2: Try to parse the merged string
extracted = extractJsonString(mergedJsonString)
parsed, parseErr, _ = tryParseJson(extracted)
if parseErr is None and parsed:
# Parsing succeeded - normalize and use
normalized = self._normalizeJsonStructure(parsed, useCaseId)
parsedJsonForUseCase = normalized
result = json.dumps(normalized, indent=2, ensure_ascii=False)
# If no overlap was found, mark as complete and use closed JSON
if not hasOverlap:
isComplete = True
# JSON is already closed by mergeJsonStringsWithOverlap when no overlap
# Use the merged (closed) JSON string directly
result = mergedJsonString
# Try to parse it to get parsedJsonForUseCase
try:
extracted = extractJsonString(mergedJsonString)
parsed, parseErr, _ = tryParseJson(extracted)
if parseErr is None and parsed:
normalized = self._normalizeJsonStructure(parsed, useCaseId)
parsedJsonForUseCase = normalized
result = json.dumps(normalized, indent=2, ensure_ascii=False)
except Exception:
pass # Use string result if parsing fails
else:
# Parsing failed - try to extract partial data for section_content
if useCaseId == "section_content":
# Use existing mergeDeepStructures approach: parse what we can from each part
# Overlap found - continue with normal processing
# Step 2: Try to parse the merged string
extracted = extractJsonString(mergedJsonString)
parsed, parseErr, _ = tryParseJson(extracted)
if parseErr is None and parsed:
# Parsing succeeded - normalize and use
normalized = self._normalizeJsonStructure(parsed, useCaseId)
parsedJsonForUseCase = normalized
result = json.dumps(normalized, indent=2, ensure_ascii=False)
else:
# Parsing failed - try to extract partial data using Deep-Structure-Merging
# This fallback works for all use cases: parse what we can from each part
allParsed = []
for jsonStr in allJsonStrings:
extracted = extractJsonString(jsonStr)
@ -319,12 +338,12 @@ class AiCallLooper:
allParsed.append(normalized)
if allParsed:
# Use existing mergeDeepStructures for intelligent merging
# Use mergeDeepStructures for intelligent merging across all use cases
if len(allParsed) > 1:
mergedJsonObj = allParsed[0]
for nextObj in allParsed[1:]:
mergedJsonObj = JsonResponseHandler.mergeDeepStructures(
mergedJsonObj, nextObj, iteration, f"section_content.merge"
mergedJsonObj, nextObj, iteration, f"{useCaseId}.merge"
)
else:
mergedJsonObj = allParsed[0]
@ -334,18 +353,37 @@ class AiCallLooper:
else:
# All parsing failed - use string merge result
result = mergedJsonString
else:
# Not section_content - use string merge result
result = mergedJsonString
except Exception as e:
logger.warning(f"Failed data-based merge, falling back to string merging: {e}")
# Fallback to string merging
mergedJsonString = accumulatedDirectJson[0] if accumulatedDirectJson else result
hasOverlap = True # Track if any overlap was found
for prevJson in accumulatedDirectJson[1:]:
mergedJsonString = JsonResponseHandler.mergeJsonStringsWithOverlap(mergedJsonString, prevJson)
mergedJsonString = JsonResponseHandler.mergeJsonStringsWithOverlap(mergedJsonString, result)
mergedJsonString, hasOverlapInMerge = JsonResponseHandler.mergeJsonStringsWithOverlap(mergedJsonString, prevJson)
if not hasOverlapInMerge:
hasOverlap = False
logger.info(f"Iteration {iteration}: No overlap found during fallback merge - stopping iterations")
break
if hasOverlap:
mergedJsonString, hasOverlapInMerge = JsonResponseHandler.mergeJsonStringsWithOverlap(mergedJsonString, result)
if not hasOverlapInMerge:
hasOverlap = False
logger.info(f"Iteration {iteration}: No overlap found in final fallback merge - stopping iterations")
result = mergedJsonString
# If no overlap was found, mark as complete and use closed JSON
if not hasOverlap:
isComplete = True
# JSON is already closed by mergeJsonStringsWithOverlap when no overlap
# Try to parse it to get parsedJsonForUseCase
try:
extractedMerged = extractJsonString(result)
parsedMerged, parseError, _ = tryParseJson(extractedMerged)
if parseError is None and parsedMerged:
parsedJsonForUseCase = parsedMerged
except Exception:
pass # Use string result if parsing fails
# Try to parse the string-merged result
try:
extractedMerged = extractJsonString(result)
@ -375,233 +413,6 @@ class AiCallLooper:
self.services.utils.writeDebugFile(final_json, f"{debugPrefix}_final_result")
return final_json
# Extract sections from response (handles both valid and broken JSON)
# Only for document generation (JSON responses)
# CRITICAL: Pass allSections and accumulationState to enable string accumulation
extractedSections, wasJsonComplete, parsedResult, accumulationState = self.responseParser.extractSectionsFromResponse(
result, iteration, debugPrefix, allSections, accumulationState
)
# CRITICAL: Merge sections BEFORE KPI validation
# This ensures sections are preserved even if KPI validation fails
if extractedSections:
allSections = JsonResponseHandler.mergeSectionsIntelligently(allSections, extractedSections, iteration)
# Define KPIs if we just entered accumulation mode (iteration 1, incomplete JSON)
if accumulationState and accumulationState.isAccumulationMode and iteration == 1 and not accumulationState.kpis:
logger.info(f"Iteration {iteration}: Defining KPIs for accumulation tracking")
continuationContext = buildContinuationContext(allSections, result)
# Pass raw response string from first iteration for KPI definition
kpiDefinitions = await self._defineKpisFromPrompt(
userPrompt or prompt,
result, # Pass raw JSON string from first iteration
continuationContext,
debugPrefix
)
# Initialize KPIs with currentValue = 0
accumulationState.kpis = [{**kpi, "currentValue": 0} for kpi in kpiDefinitions]
logger.info(f"Defined {len(accumulationState.kpis)} KPIs: {[kpi.get('id') for kpi in accumulationState.kpis]}")
# Extract and validate KPIs (if in accumulation mode with KPIs defined)
if accumulationState and accumulationState.isAccumulationMode and accumulationState.kpis:
# For KPI extraction, prefer accumulated JSON string over repaired JSON
# because repairBrokenJson may lose data (e.g., empty rows array when JSON is incomplete)
updatedKpis = []
# First try to extract from parsedResult (repaired JSON)
if parsedResult:
try:
updatedKpis = JsonResponseHandler.extractKpiValuesFromJson(
parsedResult,
accumulationState.kpis
)
# Check if we got meaningful values (non-zero)
hasValidValues = any(kpi.get("currentValue", 0) > 0 for kpi in updatedKpis)
if not hasValidValues and accumulationState.accumulatedJsonString:
# Repaired JSON has empty values, try accumulated string
logger.debug("Repaired JSON has empty KPI values, trying accumulated JSON string")
updatedKpis = JsonResponseHandler.extractKpiValuesFromIncompleteJson(
accumulationState.accumulatedJsonString,
accumulationState.kpis
)
except Exception as e:
logger.debug(f"Error extracting KPIs from parsedResult: {e}")
updatedKpis = []
# If no parsedResult or extraction failed, try accumulated string
if not updatedKpis and accumulationState.accumulatedJsonString:
try:
updatedKpis = JsonResponseHandler.extractKpiValuesFromIncompleteJson(
accumulationState.accumulatedJsonString,
accumulationState.kpis
)
except Exception as e:
logger.debug(f"Error extracting KPIs from accumulated JSON string: {e}")
updatedKpis = []
if updatedKpis:
shouldProceed, reason = JsonResponseHandler.validateKpiProgression(
accumulationState,
updatedKpis
)
if not shouldProceed:
logger.warning(f"Iteration {iteration}: KPI validation failed: {reason}")
if iterationOperationId:
self.services.chat.progressLogFinish(iterationOperationId, False)
if operationId:
self.services.chat.progressLogUpdate(operationId, 0.9, f"KPI validation failed: {reason} ({iteration} iterations)")
break
# Update KPIs in accumulation state
accumulationState.kpis = updatedKpis
logger.info(f"Iteration {iteration}: KPIs updated: {[(kpi.get('id'), kpi.get('currentValue')) for kpi in updatedKpis]}")
# Check if all KPIs completed
allCompleted = True
for kpi in updatedKpis:
targetValue = kpi.get("targetValue", 0)
currentValue = kpi.get("currentValue", 0)
if currentValue < targetValue:
allCompleted = False
break
if allCompleted:
logger.info(f"Iteration {iteration}: All KPIs completed, finishing accumulation")
wasJsonComplete = True # Mark as complete to exit loop
# CRITICAL: Handle JSON fragments (continuation content)
# Fragment merging happens inside extractSectionsFromResponse
# If merge fails (returns wasJsonComplete=True), stop iterations and complete JSON
if not extractedSections and allSections:
if wasJsonComplete:
# Merge failed - stop iterations, complete JSON with available data
logger.error(f"Iteration {iteration}: ❌ MERGE FAILED - Stopping iterations, completing JSON with available data")
if iterationOperationId:
self.services.chat.progressLogFinish(iterationOperationId, False)
if operationId:
self.services.chat.progressLogUpdate(operationId, 0.9, f"Merge failed, completing JSON ({iteration} iterations)")
break
# Fragment was detected and merged successfully
logger.info(f"Iteration {iteration}: JSON fragment detected and merged, continuing")
# Don't break - fragment was merged, continue to get more content if needed
# Check if we should continue based on JSON completeness
shouldContinue = self.responseParser.shouldContinueGeneration(
allSections,
iteration,
wasJsonComplete,
result
)
if shouldContinue:
if iterationOperationId:
self.services.chat.progressLogUpdate(iterationOperationId, 0.8, "Fragment merged, continuing")
self.services.chat.progressLogFinish(iterationOperationId, True)
continue
else:
# Done - fragment was merged and JSON is complete
if iterationOperationId:
self.services.chat.progressLogFinish(iterationOperationId, True)
if operationId:
self.services.chat.progressLogUpdate(operationId, 0.95, f"Generation complete ({iteration} iterations, fragment merged)")
logger.info(f"Generation complete after {iteration} iterations: fragment merged")
break
# Extract document metadata from first iteration if available
if iteration == 1 and parsedResult and not documentMetadata:
documentMetadata = self.responseParser.extractDocumentMetadata(parsedResult)
# Update progress after parsing
if iterationOperationId:
if extractedSections:
self.services.chat.progressLogUpdate(iterationOperationId, 0.8, f"Extracted {len(extractedSections)} sections")
if not extractedSections:
# CRITICAL: If JSON was incomplete/broken, continue even if no sections extracted
# This allows the AI to retry and complete the broken JSON
if not wasJsonComplete:
logger.warning(f"Iteration {iteration}: No sections extracted from broken JSON, continuing for another attempt")
continue
# If JSON was complete but no sections extracted - check if it was a fragment
# Fragments are handled above, so if we get here and it's complete, it's an error
logger.warning(f"Iteration {iteration}: No sections extracted from complete JSON, stopping")
break
# NOTE: Section merging now happens BEFORE KPI validation (see above)
# This ensures sections are preserved even if KPI validation fails
# Calculate total bytes in merged content for progress display
merged_json_str = json.dumps(allSections, indent=2, ensure_ascii=False)
totalBytesGenerated = len(merged_json_str.encode('utf-8'))
# Update main operation with byte progress
if operationId:
# Format bytes for display
if totalBytesGenerated < 1024:
bytesDisplay = f"{totalBytesGenerated}B"
elif totalBytesGenerated < 1024 * 1024:
bytesDisplay = f"{totalBytesGenerated / 1024:.1f}kB"
else:
bytesDisplay = f"{totalBytesGenerated / (1024 * 1024):.1f}MB"
# Estimate progress based on iterations (rough estimate)
estimatedProgress = min(0.9, 0.4 + (iteration * 0.1))
self.services.chat.progressLogUpdate(operationId, estimatedProgress, f"Pipeline: {bytesDisplay} (iteration {iteration})")
# Log merged sections for debugging
# For section content generation: skip merged sections debug files (only one prompt/response needed)
isSectionContent = "_section_" in debugPrefix
if not isSectionContent:
self.services.utils.writeDebugFile(merged_json_str, f"{debugPrefix}_merged_sections_iteration_{iteration}")
# Check if we should continue (completion detection)
# Simple logic: JSON completeness determines continuation
shouldContinue = self.responseParser.shouldContinueGeneration(
allSections,
iteration,
wasJsonComplete,
result
)
if shouldContinue:
# Finish iteration operation (will continue with next iteration)
if iterationOperationId:
# Show byte progress in iteration completion
iterBytes = len(result.encode('utf-8')) if result else 0
if iterBytes < 1024:
iterBytesDisplay = f"{iterBytes}B"
elif iterBytes < 1024 * 1024:
iterBytesDisplay = f"{iterBytes / 1024:.1f}kB"
else:
iterBytesDisplay = f"{iterBytes / (1024 * 1024):.1f}MB"
self.services.chat.progressLogUpdate(iterationOperationId, 0.95, f"Completed ({iterBytesDisplay})")
self.services.chat.progressLogFinish(iterationOperationId, True)
continue
else:
# Done - finish iteration and update main operation
if iterationOperationId:
# Show final byte count
finalBytes = len(merged_json_str.encode('utf-8'))
if finalBytes < 1024:
finalBytesDisplay = f"{finalBytes}B"
elif finalBytes < 1024 * 1024:
finalBytesDisplay = f"{finalBytes / 1024:.1f}kB"
else:
finalBytesDisplay = f"{finalBytes / (1024 * 1024):.1f}MB"
self.services.chat.progressLogUpdate(iterationOperationId, 0.95, f"Complete ({finalBytesDisplay})")
self.services.chat.progressLogFinish(iterationOperationId, True)
if operationId:
# Show final size in main operation
finalBytes = len(merged_json_str.encode('utf-8'))
if finalBytes < 1024:
finalBytesDisplay = f"{finalBytes}B"
elif finalBytes < 1024 * 1024:
finalBytesDisplay = f"{finalBytes / 1024:.1f}kB"
else:
finalBytesDisplay = f"{finalBytes / (1024 * 1024):.1f}MB"
self.services.chat.progressLogUpdate(operationId, 0.95, f"Generation complete: {finalBytesDisplay} ({iteration} iterations, {len(allSections)} sections)")
logger.info(f"Generation complete after {iteration} iterations: {len(allSections)} sections")
break
except Exception as e:
logger.error(f"Error in AI call iteration {iteration}: {str(e)}")
@ -612,20 +423,121 @@ class AiCallLooper:
if iteration >= maxIterations:
logger.warning(f"AI call stopped after maximum iterations ({maxIterations})")
# CRITICAL: Complete any incomplete structures in sections before building final result
# This ensures JSON is properly closed even if merge failed or iterations stopped early
allSections = JsonResponseHandler.completeIncompleteStructures(allSections)
# 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
# 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 ""
def _isJsonStringIncomplete(self, jsonString: str) -> bool:
"""
Check if JSON string is incomplete (truncated) BEFORE closing/parsing.
# Build final result from accumulated sections
final_result = self.responseParser.buildFinalResultFromSections(allSections, documentMetadata)
This is critical because if JSON is truncated, closing it makes it appear complete,
but we need to detect the truncation to continue iteration.
# Write final result to debug file
# For section content generation: skip final_result debug file (response already written)
isSectionContent = "_section_" in debugPrefix
if not isSectionContent:
self.services.utils.writeDebugFile(final_result, f"{debugPrefix}_final_result")
Args:
jsonString: JSON string to check
Returns:
True if JSON string appears incomplete/truncated, False otherwise
"""
if not jsonString or not jsonString.strip():
return False
return final_result
from modules.shared.jsonUtils import stripCodeFences, normalizeJsonText
# Normalize JSON string
normalized = stripCodeFences(normalizeJsonText(jsonString)).strip()
if not normalized:
return False
# Find first '{' or '[' to start
startIdx = -1
for i, char in enumerate(normalized):
if char in '{[':
startIdx = i
break
if startIdx == -1:
return False
jsonContent = normalized[startIdx:]
# Check if structures are balanced (all opened structures are closed)
braceCount = 0
bracketCount = 0
inString = False
escapeNext = False
for char in jsonContent:
if escapeNext:
escapeNext = False
continue
if char == '\\':
escapeNext = True
continue
if char == '"':
inString = not inString
continue
if not inString:
if char == '{':
braceCount += 1
elif char == '}':
braceCount -= 1
elif char == '[':
bracketCount += 1
elif char == ']':
bracketCount -= 1
# If structures are unbalanced, JSON is incomplete
if braceCount > 0 or bracketCount > 0:
return True
# Check if JSON ends with incomplete value (e.g., unclosed string, incomplete number, trailing comma)
trimmed = jsonContent.rstrip()
if not trimmed:
return False
# Check for trailing comma (might indicate incomplete)
if trimmed.endswith(','):
# Trailing comma might indicate incomplete, but could also be valid
# Check if there's a closing bracket/brace after the comma
return False # Trailing comma alone doesn't mean incomplete
# Check if ends with incomplete string (odd number of quotes)
quoteCount = jsonContent.count('"')
if quoteCount % 2 == 1:
# Odd number of quotes - string is not closed
return True
# Check if ends mid-value (e.g., ends with "417 instead of "4170. 41719"])
# Look for patterns that suggest truncation:
# - Ends with incomplete number (e.g., "417)
# - Ends with incomplete array element (e.g., ["417)
# - Ends with incomplete object property (e.g., {"key": "val)
# If JSON parses successfully without closing, it's complete
from modules.shared.jsonUtils import tryParseJson
parsed, parseErr, _ = tryParseJson(jsonContent)
if parseErr is None:
# Parses successfully - it's complete
return False
# If it doesn't parse, try closing it and see if that helps
from modules.shared.jsonUtils import closeJsonStructures
closed = closeJsonStructures(jsonContent)
parsedClosed, parseErrClosed, _ = tryParseJson(closed)
if parseErrClosed is None:
# Only parses after closing - it was incomplete
return True
# 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:
"""
@ -645,9 +557,19 @@ class AiCallLooper:
# 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
# This shouldn't happen, but we'll log a warning and return empty structure
logger.warning(f"Invalid response format: received list of strings instead of elements array. Expected {{'elements': [...]}} structure.")
return {"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}
@ -664,99 +586,4 @@ class AiCallLooper:
# For other use cases, return as-is (they have their own structures)
return parsed
async def _defineKpisFromPrompt(
self,
userPrompt: str,
rawJsonString: Optional[str],
continuationContext: Dict[str, Any],
debugPrefix: str = "kpi"
) -> List[Dict[str, Any]]:
"""
Make separate AI call to define KPIs based on user prompt and incomplete JSON.
Args:
userPrompt: Original user prompt
rawJsonString: Raw JSON string from first iteration response
continuationContext: Continuation context (not used for JSON, kept for compatibility)
debugPrefix: Prefix for debug file names
Returns:
List of KPI definitions: [{"id": str, "description": str, "jsonPath": str, "targetValue": int}, ...]
"""
# Use raw JSON string from first iteration response
if rawJsonString:
# Remove markdown code fences if present
from modules.shared.jsonUtils import stripCodeFences
incompleteJson = stripCodeFences(rawJsonString.strip())
else:
incompleteJson = "Not available"
kpiDefinitionPrompt = f"""Analyze the user request and incomplete JSON to define KPIs (Key Performance Indicators) for tracking progress.
User Request:
{userPrompt}
Delivered JSON part:
{incompleteJson}
Task: Define which JSON items should be tracked to measure completion progress.
IMPORTANT: Analyze the Delivered JSON part structure to understand what is being tracked:
1. Identify the structure type (table with rows, list with items, etc.)
2. Determine what the jsonPath actually counts (number of rows, number of items, etc.)
3. Calculate targetValue based on what is being tracked, NOT the total quantity requested
For each trackable item, provide:
- id: Unique identifier (use descriptive name)
- description: What this KPI measures (be specific about what is counted)
- jsonPath: Path to extract value from JSON (use dot notation with array indices, e.g., "documents[0].sections[1].elements[0].rows")
- targetValue: Target value to reach (integer) - MUST match what jsonPath actually tracks (rows count, items count, etc.)
Return ONLY valid JSON in this format:
{{
"kpis": [
{{
"id": "unique_id",
"description": "Description of what is measured",
"jsonPath": "path.to.value",
"targetValue": 0
}}
]
}}
If no trackable items can be identified, return: {{"kpis": []}}
"""
try:
request = AiCallRequest(
prompt=kpiDefinitionPrompt,
options=AiCallOptions(
operationType=OperationTypeEnum.DATA_ANALYSE,
priority=PriorityEnum.SPEED,
processingMode=ProcessingModeEnum.BASIC
)
)
# Write KPI definition prompt to debug file
self.services.utils.writeDebugFile(kpiDefinitionPrompt, f"{debugPrefix}_kpi_definition_prompt")
checkWorkflowStopped(self.services)
response = await self.aiService.callAi(request)
# Write KPI definition response to debug file
self.services.utils.writeDebugFile(response.content, f"{debugPrefix}_kpi_definition_response")
# Parse response
extracted = extractJsonString(response.content)
kpiResponse = json.loads(extracted)
kpiDefinitions = kpiResponse.get("kpis", [])
logger.info(f"Defined {len(kpiDefinitions)} KPIs for tracking")
return kpiDefinitions
except Exception as e:
logger.warning(f"Failed to define KPIs: {e}, continuing without KPI tracking")
return []

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ class LoopingUseCase:
"""Configuration for a specific looping use case."""
# Identification
useCaseId: str # "section_content", "chapter_structure", "document_structure", "code_structure", "code_content", "image_batch"
useCaseId: str # "section_content", "chapter_structure", "code_structure", "code_content"
# JSON Format Detection
jsonTemplate: Dict[str, Any] # Expected JSON structure template
@ -145,24 +145,7 @@ class LoopingUseCaseRegistry:
requiresExtraction=False
))
# Use Case 3: Document Structure Generation
# Returns JSON with "documents[0].sections" structure, requires extraction and accumulation
self.register(LoopingUseCase(
useCaseId="document_structure",
jsonTemplate={"documents": [{"sections": []}]},
detectionKeys=["sections"],
detectionPath="documents[0].sections",
initialPromptBuilder=None,
continuationPromptBuilder=None,
accumulator=None, # Will use default accumulator
merger=None, # Will use default merger
continuationContextBuilder=None,
resultBuilder=None, # Will use default result builder
supportsAccumulation=True,
requiresExtraction=True
))
# Use Case 4: Code Structure Generation (NEW)
# Use Case 3: Code Structure Generation
self.register(LoopingUseCase(
useCaseId="code_structure",
jsonTemplate={
@ -211,21 +194,5 @@ class LoopingUseCaseRegistry:
requiresExtraction=False
))
# Use Case 6: Image Batch Generation (NEW)
self.register(LoopingUseCase(
useCaseId="image_batch",
jsonTemplate={"images": []},
detectionKeys=["images"],
detectionPath="images",
initialPromptBuilder=None,
continuationPromptBuilder=None,
accumulator=None, # Direct return
merger=None,
continuationContextBuilder=None,
resultBuilder=None,
supportsAccumulation=False,
requiresExtraction=False
))
logger.info(f"Registered {len(self.useCases)} default looping use cases")

View file

@ -812,16 +812,18 @@ class StructureFiller:
)
else:
async def buildSectionPromptWithContinuation(
section: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
generationHint: str,
allSections: List[Dict[str, Any]],
sectionIndex: int,
isAggregation: bool,
continuationContext: Dict[str, Any],
services: 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,
@ -833,25 +835,81 @@ class StructureFiller:
language=language
)
continuationInfo = continuationContext.get("delivered_summary", "")
cutOffElement = continuationContext.get("cut_off_element", "")
# 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. Please continue from where it stopped.
The previous JSON response was incomplete. Continue from where it stopped.
PREVIOUSLY DELIVERED SUMMARY:
{continuationInfo}
{structureTemplate}Context showing structure hierarchy with cut point:
{unifiedContext}
LAST INCOMPLETE ELEMENT:
{cutOffElement}
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.
TASK: Continue generating the JSON elements array from where it was cut off.
Complete the incomplete element and continue with remaining elements.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
Return ONLY the continuation JSON (starting from the incomplete element).
The JSON should be a fragment that can be merged with the previous response."""
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
options = AiCallOptions(
@ -1040,16 +1098,18 @@ The JSON should be a fragment that can be merged with the previous response."""
isAggregation = False
async def buildSectionPromptWithContinuation(
section: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
generationHint: str,
allSections: List[Dict[str, Any]],
sectionIndex: int,
isAggregation: bool,
continuationContext: Dict[str, Any],
services: 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,
@ -1061,25 +1121,81 @@ The JSON should be a fragment that can be merged with the previous response."""
language=language
)
continuationInfo = continuationContext.get("delivered_summary", "")
cutOffElement = continuationContext.get("cut_off_element", "")
# 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. Please continue from where it stopped.
The previous JSON response was incomplete. Continue from where it stopped.
PREVIOUSLY DELIVERED SUMMARY:
{continuationInfo}
{structureTemplate}Context showing structure hierarchy with cut point:
{unifiedContext}
LAST INCOMPLETE ELEMENT:
{cutOffElement}
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.
TASK: Continue generating the JSON elements array from where it was cut off.
Complete the incomplete element and continue with remaining elements.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
Return ONLY the continuation JSON (starting from the incomplete element).
The JSON should be a fragment that can be merged with the previous response."""
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
options = AiCallOptions(
@ -1343,16 +1459,19 @@ The JSON should be a fragment that can be merged with the previous response."""
isAggregation = False
async def buildSectionPromptWithContinuation(
section: Dict[str, Any],
contentParts: List[ContentPart],
userPrompt: str,
generationHint: str,
allSections: List[Dict[str, Any]],
sectionIndex: int,
isAggregation: bool,
continuationContext: Dict[str, Any],
services: 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,
@ -1364,25 +1483,83 @@ The JSON should be a fragment that can be merged with the previous response."""
language=language
)
continuationInfo = continuationContext.get("delivered_summary", "")
cutOffElement = continuationContext.get("cut_off_element", "")
# 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. Please continue from where it stopped.
The previous JSON response was incomplete. Continue from where it stopped.
PREVIOUSLY DELIVERED SUMMARY:
{continuationInfo}
{structureTemplate}Context showing structure hierarchy with cut point:
{unifiedContext}
LAST INCOMPLETE ELEMENT:
{cutOffElement}
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.
TASK: Continue generating the JSON elements array from where it was cut off.
Complete the incomplete element and continue with remaining elements.
Last ~100 characters from previous response (repeat these at the start):
{overlapContext if overlapContext else "No overlap context available"}
Return ONLY the continuation JSON (starting from the incomplete element).
The JSON should be a fragment that can be merged with the previous response."""
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
options = AiCallOptions(

View file

@ -112,7 +112,12 @@ class StructureGenerator:
continuationContext: Optional[Dict[str, Any]] = None,
**kwargs
) -> str:
"""Build chapter structure prompt with optional continuation context."""
"""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")
basePrompt = self._buildChapterStructurePrompt(
userPrompt=userPrompt,
contentParts=contentParts,

View file

@ -0,0 +1,594 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Test cases for JSON merger with different use cases and random cuts.
Tests the robustness of the JSON merger by:
1. Creating test JSON for different use cases
2. Cutting it randomly at various points
3. Running the merger for each piece
4. Checking completeness against original
"""
import json
import random
import logging
import sys
import os
from typing import Dict, Any, List, Tuple
# Add project root to Python path
# Find project root by looking for gateway/modules structure
currentFile = os.path.abspath(__file__)
currentDir = os.path.dirname(currentFile)
# Navigate up from: gateway/modules/services/serviceAi/test_json_merger.py
# To project root: D:\Athi\Local\Web\poweron
# Try different levels up
candidates = [
os.path.abspath(os.path.join(currentDir, '../../../../')), # From gateway/modules/services/serviceAi
os.path.abspath(os.path.join(currentDir, '../../..')), # Alternative
os.path.abspath(os.path.join(currentDir, '../..')), # Another alternative
]
projectRoot = None
for candidate in candidates:
gatewayModulesPath = os.path.join(candidate, 'gateway', 'modules')
if os.path.exists(gatewayModulesPath):
projectRoot = candidate
break
# If still not found, try to find by looking for gateway directory
if projectRoot is None:
searchDir = currentDir
for _ in range(10): # Max 10 levels up
gatewayPath = os.path.join(searchDir, 'gateway')
if os.path.exists(gatewayPath) and os.path.exists(os.path.join(gatewayPath, 'modules')):
projectRoot = searchDir
break
parent = os.path.dirname(searchDir)
if parent == searchDir: # Reached root
break
searchDir = parent
if projectRoot is None:
raise RuntimeError(f"Could not find project root. Current file: {currentFile}")
# Add gateway directory to Python path (not project root)
gatewayPath = os.path.join(projectRoot, 'gateway')
if gatewayPath not in sys.path:
sys.path.insert(0, gatewayPath)
# Verify the path is correct
modulesPath = os.path.join(projectRoot, 'gateway', 'modules')
if not os.path.exists(modulesPath):
raise RuntimeError(f"Project root verification failed. Expected gateway/modules at: {modulesPath}")
try:
from modules.services.serviceAi.subJsonResponseHandling import JsonResponseHandler
from modules.services.serviceAi.subJsonMerger import JsonMergeLogger
from modules.shared.jsonUtils import (
normalizeJsonText, stripCodeFences, closeJsonStructures, tryParseJson,
extractJsonStructureContext
)
except ImportError as e:
# Try to help debug
print(f"Import error: {e}")
print(f"Project root: {projectRoot}")
print(f"Gateway path: {gatewayPath}")
print(f"Python path (first 3): {sys.path[:3]}")
print(f"Looking for modules at: {modulesPath}")
print(f"Exists: {os.path.exists(modulesPath)}")
if os.path.exists(modulesPath):
print(f"Contents: {os.listdir(modulesPath)[:5]}")
raise
logger = logging.getLogger(__name__)
def createTestJsonForUseCase(useCaseId: str, size: int = 100) -> Dict[str, Any]:
"""
Create test JSON for a specific use case.
Args:
useCaseId: Use case ID (section_content, chapter_structure, etc.)
size: Size of test data (number of elements/rows/items)
Returns:
Test JSON dictionary
"""
if useCaseId == "section_content":
# Create table with rows
elements = [{
"type": "table",
"content": {
"headers": ["Year", "Value"],
"rows": [[str(1947 + i), str(10000 + i * 100)] for i in range(size)]
}
}]
return {"elements": elements}
elif useCaseId == "chapter_structure":
chapters = [{
"id": f"chapter_{i}",
"title": f"Chapter {i}",
"level": 1
} for i in range(size)]
return {"documents": [{"chapters": chapters}]}
elif useCaseId == "code_structure":
files = [{
"id": f"file_{i}",
"filename": f"file_{i}.py",
"fileType": "python",
"functions": [f"function_{i}_{j}" for j in range(5)]
} for i in range(size)]
return {"files": files}
elif useCaseId == "code_content":
files = [{
"id": f"file_{i}",
"content": f"# File {i}\ndef function_{i}():\n pass\n" * 10,
"functions": [{"name": f"function_{i}_{j}", "line": j * 3} for j in range(5)]
} for i in range(size)]
return {"files": files}
else:
raise ValueError(f"Unknown use case: {useCaseId}")
def cutJsonRandomly(jsonString: str, numCuts: int = 5, overlapSize: int = 100) -> List[str]:
"""
Cut JSON string RANDOMLY at different points WITH OVERLAP between fragments.
Each fragment overlaps with the previous one to help merging.
Args:
jsonString: JSON string to cut
numCuts: Number of cuts to make
overlapSize: Size of overlap between fragments (in characters)
Returns:
List of JSON fragments with overlap
"""
fragments = []
currentPos = 0
totalLength = len(jsonString)
if totalLength == 0:
return []
# First fragment: from start to first cut point
if numCuts > 0:
# First cut point (between 20% and 40% of total)
firstCutPoint = random.randint(int(totalLength * 0.2), int(totalLength * 0.4))
fragment = jsonString[:firstCutPoint]
fragments.append(fragment)
currentPos = firstCutPoint
else:
# No cuts - return whole string
return [jsonString]
# Subsequent fragments: each starts with overlap from previous, then continues
for i in range(numCuts - 1):
if currentPos >= totalLength:
break
# Calculate overlap start (go back overlapSize from current position)
overlapStart = max(0, currentPos - overlapSize)
# Calculate next cut point (between 20% and 40% of remaining)
remaining = totalLength - currentPos
if remaining < overlapSize * 2:
# Not enough remaining - add rest as last fragment
fragment = jsonString[overlapStart:]
fragments.append(fragment)
break
# Next cut point from current position
nextCutPoint = currentPos + random.randint(int(remaining * 0.2), int(remaining * 0.4))
nextCutPoint = min(nextCutPoint, totalLength)
# Fragment: from overlap start to next cut point
fragment = jsonString[overlapStart:nextCutPoint]
fragments.append(fragment)
currentPos = nextCutPoint
# Add remaining as last fragment (with overlap)
if currentPos < totalLength:
overlapStart = max(0, currentPos - overlapSize)
fragment = jsonString[overlapStart:]
fragments.append(fragment)
return fragments
def testMergerWithFragments(
originalJson: Dict[str, Any],
fragments: List[str],
useCaseId: str
) -> Tuple[bool, Dict[str, Any], str]:
"""
Test merger by merging fragments sequentially.
Args:
originalJson: Original complete JSON
fragments: List of JSON fragments to merge
useCaseId: Use case ID
Returns:
Tuple of (success, merged_json, error_message)
"""
if not fragments:
return False, {}, "No fragments provided"
# Log structure context for each fragment (especially incomplete ones)
print(f"\n{'='*60}")
print(f"FRAGMENT ANALYSIS (use case: {useCaseId})")
print(f"{'='*60}")
for fragIdx, fragment in enumerate(fragments):
print(f"\nFragment {fragIdx + 1}/{len(fragments)}:")
print(f" Length: {len(fragment)} chars")
# Extract structure context for this fragment
try:
structureContext = extractJsonStructureContext(fragment, useCaseId)
templateStructure = structureContext.get("template_structure", "")
lastCompletePart = structureContext.get("last_complete_part", "")
incompletePart = structureContext.get("incomplete_part", "")
structureContextJson = structureContext.get("structure_context", "")
# Check if fragment is incomplete
normalized = stripCodeFences(normalizeJsonText(fragment)).strip()
parsed, parseErr, _ = tryParseJson(normalized)
isIncomplete = parseErr is not None or (parsed is None)
if isIncomplete:
print(f" Status: INCOMPLETE (cut off)")
print(f" Template Structure:")
if templateStructure:
# Show first few lines of template
templateLines = templateStructure.split('\n')
templateLinesToShow = templateLines[:5]
for line in templateLinesToShow:
print(f" {line}")
if len(templateLines) > 5:
remainingLines = len(templateLines) - 5
print(f" ... ({remainingLines} more lines)")
else:
print(f" (not available)")
print(f" Structure Context:")
if structureContextJson:
# Show structure context
contextLines = structureContextJson.split('\n')
contextLinesToShow = contextLines[:5]
for line in contextLinesToShow:
print(f" {line}")
if len(contextLines) > 5:
remainingContextLines = len(contextLines) - 5
print(f" ... ({remainingContextLines} more lines)")
else:
print(f" (not available)")
print(f" Last Complete Part:")
if lastCompletePart:
# Show last complete part (truncated if too long)
if len(lastCompletePart) > 200:
print(f" {lastCompletePart[:200]}... ({len(lastCompletePart)} chars total)")
else:
print(f" {lastCompletePart}")
else:
print(f" (not available)")
print(f" Incomplete Part:")
if incompletePart:
# Show incomplete part (truncated if too long)
if len(incompletePart) > 200:
print(f" {incompletePart[:200]}... ({len(incompletePart)} chars total)")
else:
print(f" {incompletePart}")
else:
print(f" (not available)")
else:
print(f" Status: COMPLETE")
if structureContextJson:
print(f" Structure Context:")
contextLines = structureContextJson.split('\n')
contextLinesToShow = contextLines[:3]
for line in contextLinesToShow:
print(f" {line}")
if len(contextLines) > 3:
remainingContextLines = len(contextLines) - 3
print(f" ... ({remainingContextLines} more lines)")
except Exception as e:
print(f" Error extracting structure context: {e}")
print(f"\n{'='*60}\n")
# Start with first fragment
accumulated = fragments[0]
# Merge each subsequent fragment
for i, fragment in enumerate(fragments[1:], 1):
try:
accumulated, hasOverlap = JsonResponseHandler.mergeJsonStringsWithOverlap(
accumulated, fragment
)
# Log if no overlap was found (iterations would stop in real scenario)
if not hasOverlap:
print(f" ⚠️ Fragment {i}: No overlap found - iterations would stop here")
# Check if result is empty (should never happen)
if not accumulated or accumulated.strip() in ['{"elements": []}', '{}', '']:
return False, {}, f"Merge {i} returned empty JSON"
except Exception as e:
return False, {}, f"Merge {i} failed with error: {str(e)}"
# Parse merged result
try:
# Normalize and try to parse
normalized = stripCodeFences(normalizeJsonText(accumulated)).strip()
# Try to parse directly
parsed, parseErr, _ = tryParseJson(normalized)
if parseErr is not None:
# Try closing structures if incomplete
try:
closed = closeJsonStructures(normalized)
parsed, parseErr2, _ = tryParseJson(closed)
if parseErr2 is not None:
# Try to extract valid JSON prefix
# JsonResponseHandler is already imported at module level
validPrefix = JsonResponseHandler._extractValidJsonPrefix(normalized)
if validPrefix:
parsed, parseErr3, _ = tryParseJson(validPrefix)
if parseErr3 is not None:
return False, {}, f"Final parse error: {str(parseErr3)}"
else:
return False, {}, f"Final parse error: {str(parseErr2)}"
except Exception as parseErr:
return False, {}, f"Final parse error: {str(parseErr)}"
if not parsed:
return False, {}, "Final parse returned None"
# CRITICAL: Ensure parsed is a dict, not a list
# If it's a list, wrap it in the expected structure based on use case
if isinstance(parsed, list):
# Try to normalize list to expected structure
if useCaseId == "section_content":
# List of elements - wrap in elements structure
parsed = {"elements": parsed}
elif useCaseId == "chapter_structure":
# List of chapters - wrap in documents structure
parsed = {"documents": [{"chapters": parsed}]}
elif useCaseId == "code_structure":
# List of files - wrap in files structure
parsed = {"files": parsed}
elif useCaseId == "code_content":
# List of files - wrap in files structure
parsed = {"files": parsed}
else:
# Unknown use case - try to wrap as elements
parsed = {"elements": parsed}
# Ensure it's a dict now
if not isinstance(parsed, dict):
return False, {}, f"Final parse returned unexpected type: {type(parsed).__name__}"
return True, parsed, ""
except Exception as e:
return False, {}, f"Final parse failed: {str(e)}"
def compareJsonCompleteness(
original: Dict[str, Any],
merged: Dict[str, Any],
useCaseId: str
) -> Tuple[bool, str]:
"""
Compare merged JSON with original to check completeness.
Args:
original: Original JSON
merged: Merged JSON (must be a dict)
useCaseId: Use case ID
Returns:
Tuple of (is_complete, message)
"""
# CRITICAL: Ensure merged is a dict
if not isinstance(merged, dict):
return False, f"Merged JSON is not a dict, got {type(merged).__name__}"
if useCaseId == "section_content":
origElements = original.get("elements", [])
mergedElements = merged.get("elements", [])
if not isinstance(origElements, list):
return False, f"Original elements is not a list: {type(origElements).__name__}"
if not isinstance(mergedElements, list):
return False, f"Merged elements is not a list: {type(mergedElements).__name__}"
if len(mergedElements) < len(origElements):
return False, f"Missing elements: {len(origElements)} expected, {len(mergedElements)} found"
# Check table rows
if origElements and mergedElements:
origTable = origElements[0] if isinstance(origElements[0], dict) else {}
mergedTable = mergedElements[0] if isinstance(mergedElements[0], dict) else {}
if not origTable or not mergedTable:
return False, f"Table structure missing: origTable={bool(origTable)}, mergedTable={bool(mergedTable)}"
origRows = origTable.get("content", {}).get("rows", []) if isinstance(origTable.get("content"), dict) else origTable.get("rows", [])
mergedRows = mergedTable.get("content", {}).get("rows", []) if isinstance(mergedTable.get("content"), dict) else mergedTable.get("rows", [])
if not isinstance(origRows, list):
return False, f"Original rows is not a list: {type(origRows).__name__}"
if not isinstance(mergedRows, list):
return False, f"Merged rows is not a list: {type(mergedRows).__name__}"
if len(mergedRows) < len(origRows):
return False, f"Missing rows: {len(origRows)} expected, {len(mergedRows)} found"
return True, "Complete"
elif useCaseId == "chapter_structure":
origChapters = original.get("documents", [{}])[0].get("chapters", [])
mergedChapters = merged.get("documents", [{}])[0].get("chapters", [])
if len(mergedChapters) < len(origChapters):
return False, f"Missing chapters: {len(origChapters)} expected, {len(mergedChapters)} found"
return True, "Complete"
elif useCaseId == "code_structure":
origFiles = original.get("files", [])
mergedFiles = merged.get("files", [])
if len(mergedFiles) < len(origFiles):
return False, f"Missing files: {len(origFiles)} expected, {len(mergedFiles)} found"
return True, "Complete"
elif useCaseId == "code_content":
origFiles = original.get("files", [])
mergedFiles = merged.get("files", [])
if len(mergedFiles) < len(origFiles):
return False, f"Missing files: {len(origFiles)} expected, {len(mergedFiles)} found"
return True, "Complete"
else:
return False, f"Unknown use case: {useCaseId}"
def runTestForUseCase(useCaseId: str, size: int = 50, numTests: int = 10) -> Dict[str, Any]:
"""
Run multiple tests for a use case with random cuts.
Args:
useCaseId: Use case ID
size: Size of test data
numTests: Number of test runs
Returns:
Test results dictionary
"""
results = {
"useCaseId": useCaseId,
"size": size,
"numTests": numTests,
"passed": 0,
"failed": 0,
"errors": []
}
for testNum in range(numTests):
try:
# Create test JSON
originalJson = createTestJsonForUseCase(useCaseId, size)
originalString = json.dumps(originalJson, indent=2, ensure_ascii=False)
# Cut randomly
fragments = cutJsonRandomly(originalString, numCuts=random.randint(3, 7))
# Test merger
success, mergedJson, errorMsg = testMergerWithFragments(
originalJson, fragments, useCaseId
)
if not success:
results["failed"] += 1
results["errors"].append(f"Test {testNum + 1}: {errorMsg}")
continue
# Check completeness
isComplete, completenessMsg = compareJsonCompleteness(
originalJson, mergedJson, useCaseId
)
if isComplete:
results["passed"] += 1
else:
results["failed"] += 1
results["errors"].append(f"Test {testNum + 1}: {completenessMsg}")
except Exception as e:
results["failed"] += 1
results["errors"].append(f"Test {testNum + 1}: Exception - {str(e)}")
return results
def runAllTests():
"""Run tests for all use cases."""
useCases = [
"section_content",
"chapter_structure",
"code_structure",
"code_content"
]
allResults = []
for useCaseId in useCases:
print(f"\n{'='*60}")
print(f"Testing use case: {useCaseId}")
print(f"{'='*60}")
# Initialize log file for this use case
# Initialize log file (overwrite on each test run)
logFileName = f"json_merger_{useCaseId}.txt"
JsonMergeLogger.initializeLogFile(logFileName)
print(f"Log file: {logFileName}")
results = runTestForUseCase(useCaseId, size=50, numTests=10)
allResults.append(results)
print(f"Passed: {results['passed']}/{results['numTests']}")
print(f"Failed: {results['failed']}/{results['numTests']}")
if results["errors"]:
print("\nErrors:")
for error in results["errors"][:5]: # Show first 5 errors
print(f" - {error}")
# Summary
print(f"\n{'='*60}")
print("SUMMARY")
print(f"{'='*60}")
totalPassed = sum(r["passed"] for r in allResults)
totalFailed = sum(r["failed"] for r in allResults)
totalTests = sum(r["numTests"] for r in allResults)
print(f"Total tests: {totalTests}")
print(f"Passed: {totalPassed}")
print(f"Failed: {totalFailed}")
print(f"Success rate: {totalPassed / totalTests * 100:.1f}%")
return allResults
if __name__ == "__main__":
# Set up logging - use WARNING level to reduce noise from jsonUtils
logging.basicConfig(level=logging.WARNING)
# Run tests
results = runAllTests()
# Save results to file (in project root)
resultsFile = os.path.join(projectRoot, "test_json_merger_results.json")
with open(resultsFile, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\nResults saved to {resultsFile}")

View file

@ -640,6 +640,7 @@ Return ONLY valid JSON matching the request above.
```
"""
# Build base prompt
contentPrompt = f"""# TASK: Generate Code File Content
Generate complete, executable code for the file: {filename}
@ -678,6 +679,130 @@ Return ONLY valid JSON in this format:
}}
"""
# Build continuation prompt builder
async def buildCodeContentPromptWithContinuation(
continuationContext: Optional[Dict[str, Any]] = None,
**kwargs
) -> 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", "")
# Rebuild base prompt (same as initial prompt)
userRequestSection = ""
if userPrompt:
userRequestSection = f"""
## ORIGINAL USER REQUEST
```
{userPrompt}
```
"""
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"
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 '[]'}
}}
]
}}
"""
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}
--- CONTINUATION REQUEST ---
{continuationText}
Continue generating the remaining code content now.
"""
else:
return basePrompt
# Use generic looping system with code_content use case
options = AiCallOptions(
operationType=OperationTypeEnum.DATA_GENERATE,
@ -687,6 +812,19 @@ Return ONLY valid JSON in this format:
contentJson = await self.services.ai.callAiWithLooping(
prompt=contentPrompt,
options=options,
promptBuilder=buildCodeContentPromptWithContinuation,
promptArgs={
"filename": filename,
"fileType": fileType,
"functions": functions,
"classes": classes,
"dependencies": dependencies,
"metadata": metadata,
"userPrompt": userPrompt,
"contentParts": contentParts,
"contextInfo": contextInfo,
"services": self.services
},
useCaseId="code_content",
debugPrefix=f"code_content_{fileStructure.get('id', 'file')}",
)

File diff suppressed because it is too large Load diff