finalized integration testing of enhanced ai service
This commit is contained in:
parent
1362470f00
commit
52f2f40774
23 changed files with 386 additions and 73 deletions
|
|
@ -117,18 +117,24 @@ class AiCallLooper:
|
||||||
if not lastRawResponse:
|
if not lastRawResponse:
|
||||||
logger.warning(f"Iteration {iteration}: No previous response available for continuation!")
|
logger.warning(f"Iteration {iteration}: No previous response available for continuation!")
|
||||||
|
|
||||||
# Filter promptArgs to only include parameters that buildGenerationPrompt accepts
|
# For section_content, pass all promptArgs (it uses buildSectionPromptWithContinuation which needs all args)
|
||||||
# buildGenerationPrompt accepts: outputFormat, userPrompt, title, extracted_content, continuationContext, services
|
# For other use cases (chapter_structure, code_structure), filter to only accepted parameters
|
||||||
filteredPromptArgs = {
|
if useCaseId == "section_content":
|
||||||
k: v for k, v in promptArgs.items()
|
# Pass all promptArgs plus continuationContext for section_content
|
||||||
if k in ['outputFormat', 'userPrompt', 'title', 'extracted_content', 'services']
|
iterationPrompt = await promptBuilder(**promptArgs, continuationContext=continuationContext)
|
||||||
}
|
else:
|
||||||
# Always include services if available
|
# Filter promptArgs to only include parameters that buildGenerationPrompt accepts
|
||||||
if not filteredPromptArgs.get('services') and hasattr(self, 'services'):
|
# buildGenerationPrompt accepts: outputFormat, userPrompt, title, extracted_content, continuationContext, services
|
||||||
filteredPromptArgs['services'] = self.services
|
filteredPromptArgs = {
|
||||||
|
k: v for k, v in promptArgs.items()
|
||||||
# Rebuild prompt with continuation context using the provided prompt builder
|
if k in ['outputFormat', 'userPrompt', 'title', 'extracted_content', 'services']
|
||||||
iterationPrompt = await promptBuilder(**filteredPromptArgs, continuationContext=continuationContext)
|
}
|
||||||
|
# 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)
|
||||||
else:
|
else:
|
||||||
# First iteration - use original prompt
|
# First iteration - use original prompt
|
||||||
iterationPrompt = prompt
|
iterationPrompt = prompt
|
||||||
|
|
@ -241,8 +247,22 @@ class AiCallLooper:
|
||||||
# Handle use cases that return JSON directly (no section extraction needed)
|
# 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", "image_batch"]
|
||||||
if useCaseId in directReturnUseCases:
|
if useCaseId in directReturnUseCases:
|
||||||
# For chapter_structure and code_structure, check completeness and support looping
|
# For chapter_structure, code_structure, and section_content, check completeness and support looping
|
||||||
if useCaseId in ["chapter_structure", "code_structure"] and parsedJsonForUseCase:
|
loopingUseCases = ["chapter_structure", "code_structure", "section_content"]
|
||||||
|
if useCaseId in loopingUseCases:
|
||||||
|
# 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")
|
||||||
|
# Accumulate response for merging in next iteration
|
||||||
|
accumulatedDirectJson.append(result)
|
||||||
|
|
||||||
|
# Continue to next iteration - continuation prompt builder will handle the rest
|
||||||
|
if iterationOperationId:
|
||||||
|
self.services.chat.progressLogUpdate(iterationOperationId, 0.7, "JSON incomplete, requesting continuation")
|
||||||
|
self.services.chat.progressLogFinish(iterationOperationId, True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check completeness if we have parsed JSON
|
||||||
isComplete = JsonResponseHandler.isJsonComplete(parsedJsonForUseCase)
|
isComplete = JsonResponseHandler.isJsonComplete(parsedJsonForUseCase)
|
||||||
|
|
||||||
if not isComplete:
|
if not isComplete:
|
||||||
|
|
|
||||||
|
|
@ -2192,7 +2192,8 @@ Return a JSON object with this structure:
|
||||||
|
|
||||||
Output requirements:
|
Output requirements:
|
||||||
- "content" must be an object (never a string)
|
- "content" must be an object (never a string)
|
||||||
- Return only valid JSON, no explanatory text
|
- Return only valid JSON - no text before, no text after, no comments, no explanations, no markdown code fences
|
||||||
|
- Start with {{ and end with }} - return ONLY the JSON object itself
|
||||||
- No invented data: Return empty structures if ContentParts have no data
|
- No invented data: Return empty structures if ContentParts have no data
|
||||||
|
|
||||||
## USER REQUEST
|
## USER REQUEST
|
||||||
|
|
@ -2235,7 +2236,8 @@ Return a JSON object with this structure:
|
||||||
|
|
||||||
Output requirements:
|
Output requirements:
|
||||||
- "content" must be an object (never a string)
|
- "content" must be an object (never a string)
|
||||||
- Return only valid JSON, no explanatory text
|
- Return only valid JSON - no text before, no text after, no comments, no explanations, no markdown code fences
|
||||||
|
- Start with {{ and end with }} - return ONLY the JSON object itself
|
||||||
- Generate meaningful content based on the Generation Hint
|
- Generate meaningful content based on the Generation Hint
|
||||||
|
|
||||||
## USER REQUEST
|
## USER REQUEST
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,9 @@ Then chapters that generate those generic content types MUST assign the relevant
|
||||||
- Generate chapters based on USER REQUEST - analyze what structure the user wants
|
- Generate chapters based on USER REQUEST - analyze what structure the user wants
|
||||||
- Each chapter needs: id, level (1, 2, 3, etc.), title
|
- Each chapter needs: id, level (1, 2, 3, etc.), title
|
||||||
- contentParts: {{"partId": {{"instruction": "..."}} or {{"caption": "..."}} or both}} - Assign ContentParts as required by CONTENT ASSIGNMENT RULE above
|
- contentParts: {{"partId": {{"instruction": "..."}} or {{"caption": "..."}} or both}} - Assign ContentParts as required by CONTENT ASSIGNMENT RULE above
|
||||||
|
- The "instruction" field for each ContentPart MUST contain ALL relevant details from the USER REQUEST that apply to content extraction for this specific chapter. Include all formatting rules, data requirements, constraints, and specifications mentioned in the user request that are relevant for processing this ContentPart in this chapter.
|
||||||
- generationHint: Description of what content to generate for this chapter
|
- generationHint: Description of what content to generate for this chapter
|
||||||
|
The generationHint MUST contain ALL relevant details from the USER REQUEST that apply to this specific chapter. Include all formatting rules, data requirements, constraints, column specifications, validation rules, and any other specifications mentioned in the user request that are relevant for generating content for this chapter. Do NOT use generic descriptions - include specific details from the user request.
|
||||||
- The number of chapters depends on the user request - create only what is requested
|
- The number of chapters depends on the user request - create only what is requested
|
||||||
|
|
||||||
## DOCUMENT OUTPUT FORMAT
|
## DOCUMENT OUTPUT FORMAT
|
||||||
|
|
@ -420,10 +422,10 @@ EXAMPLE STRUCTURE (for reference only - adapt to user request):
|
||||||
"title": "Chapter Title",
|
"title": "Chapter Title",
|
||||||
"contentParts": {{
|
"contentParts": {{
|
||||||
"extracted_part_id": {{
|
"extracted_part_id": {{
|
||||||
"instruction": "Use extracted content..."
|
"instruction": "Use extracted content with ALL relevant details from user request"
|
||||||
}}
|
}}
|
||||||
}},
|
}},
|
||||||
"generationHint": "Description of chapter content",
|
"generationHint": "Detailed description including ALL relevant details from user request for this chapter",
|
||||||
"sections": []
|
"sections": []
|
||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,16 @@ class GenerationService:
|
||||||
logger.warning(f"Unsupported format '{docFormat}' for document {doc.get('id', docIndex)}, skipping")
|
logger.warning(f"Unsupported format '{docFormat}' for document {doc.get('id', docIndex)}, skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check output style classification (code/document/image/etc.) from renderer
|
||||||
|
from modules.services.serviceGeneration.renderers.registry import getOutputStyle
|
||||||
|
outputStyle = getOutputStyle(docFormat)
|
||||||
|
if outputStyle:
|
||||||
|
logger.debug(f"Document {doc.get('id', docIndex)} format '{docFormat}' classified as '{outputStyle}' style")
|
||||||
|
# Store style in document metadata for potential use in processing paths
|
||||||
|
if "metadata" not in doc:
|
||||||
|
doc["metadata"] = {}
|
||||||
|
doc["metadata"]["outputStyle"] = outputStyle
|
||||||
|
|
||||||
# Create JSON structure with single document (preserving metadata)
|
# Create JSON structure with single document (preserving metadata)
|
||||||
singleDocContent = {
|
singleDocContent = {
|
||||||
"metadata": {**metadata, "language": docLanguage}, # Add per-document language to metadata
|
"metadata": {**metadata, "language": docLanguage}, # Add per-document language to metadata
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,32 @@ class RendererRegistry:
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
def getOutputStyle(self, outputFormat: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the output style classification for a given format.
|
||||||
|
Returns: 'code', 'document', 'image', or other (e.g., 'video' for future use)
|
||||||
|
"""
|
||||||
|
if not self._discovered:
|
||||||
|
self.discoverRenderers()
|
||||||
|
|
||||||
|
# Normalize format name
|
||||||
|
formatName = outputFormat.lower().strip()
|
||||||
|
|
||||||
|
# Check for aliases first
|
||||||
|
if formatName in self._format_mappings:
|
||||||
|
formatName = self._format_mappings[formatName]
|
||||||
|
|
||||||
|
# Get renderer class and call getOutputStyle (all renderers have same signature)
|
||||||
|
rendererClass = self._renderers.get(formatName)
|
||||||
|
try:
|
||||||
|
return rendererClass.getOutputStyle(formatName)
|
||||||
|
except (AttributeError, TypeError) as e:
|
||||||
|
logger.warning(f"No renderer found for format: {outputFormat}, cannot determine output style")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error getting output style for {outputFormat}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
# Global registry instance
|
# Global registry instance
|
||||||
_registry = RendererRegistry()
|
_registry = RendererRegistry()
|
||||||
|
|
@ -154,3 +180,7 @@ def getSupportedFormats() -> List[str]:
|
||||||
def getRendererInfo() -> Dict[str, Dict[str, str]]:
|
def getRendererInfo() -> Dict[str, Dict[str, str]]:
|
||||||
"""Get information about all registered renderers."""
|
"""Get information about all registered renderers."""
|
||||||
return _registry.getRendererInfo()
|
return _registry.getRendererInfo()
|
||||||
|
|
||||||
|
def getOutputStyle(outputFormat: str) -> Optional[str]:
|
||||||
|
"""Get the output style classification for a given format."""
|
||||||
|
return _registry.getOutputStyle(outputFormat)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Base renderer class for all format renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Dict, Any, List, Tuple
|
from typing import Dict, Any, List, Tuple, Optional
|
||||||
from modules.datamodels.datamodelJson import supportedSectionTypes
|
from modules.datamodels.datamodelJson import supportedSectionTypes
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
import json
|
import json
|
||||||
|
|
@ -50,6 +50,19 @@ class BaseRenderer(ABC):
|
||||||
"""
|
"""
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Return the output style classification for this renderer.
|
||||||
|
Returns: 'code', 'document', 'image', or other (e.g., 'video' for future use)
|
||||||
|
Override this method in subclasses to specify the output style.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
formatName: Optional format name (e.g., 'txt', 'js', 'csv') - useful for renderers
|
||||||
|
that handle multiple formats with different styles (e.g., RendererText)
|
||||||
|
"""
|
||||||
|
return 'document' # Default to document style
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ CSV renderer for report generation.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
class RendererCsv(BaseRenderer):
|
class RendererCsv(BaseRenderer):
|
||||||
"""Renders content to CSV format with format-specific extraction."""
|
"""Renders content to CSV format with format-specific extraction."""
|
||||||
|
|
@ -26,6 +26,11 @@ class RendererCsv(BaseRenderer):
|
||||||
"""Return priority for CSV renderer."""
|
"""Return priority for CSV renderer."""
|
||||||
return 70
|
return 70
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: CSV requires specific structure (header, then data rows)."""
|
||||||
|
return 'code'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to CSV format."""
|
"""Render extracted JSON content to CSV format."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ DOCX renderer for report generation using python-docx.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
|
|
@ -39,6 +39,11 @@ class RendererDocx(BaseRenderer):
|
||||||
"""Return priority for DOCX renderer."""
|
"""Return priority for DOCX renderer."""
|
||||||
return 115
|
return 115
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: Word documents are formatted documents."""
|
||||||
|
return 'document'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to DOCX format using AI-analyzed styling."""
|
"""Render extracted JSON content to DOCX format using AI-analyzed styling."""
|
||||||
self.services.utils.debugLogToFile(f"DOCX RENDER CALLED: title={title}, user_prompt={userPrompt[:50] if userPrompt else 'None'}...", "DOCX_RENDERER")
|
self.services.utils.debugLogToFile(f"DOCX RENDER CALLED: title={title}, user_prompt={userPrompt[:50] if userPrompt else 'None'}...", "DOCX_RENDERER")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ HTML renderer for report generation.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
class RendererHtml(BaseRenderer):
|
class RendererHtml(BaseRenderer):
|
||||||
"""Renders content to HTML format with format-specific extraction."""
|
"""Renders content to HTML format with format-specific extraction."""
|
||||||
|
|
@ -26,6 +26,11 @@ class RendererHtml(BaseRenderer):
|
||||||
"""Return priority for HTML renderer."""
|
"""Return priority for HTML renderer."""
|
||||||
return 100
|
return 100
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: HTML web pages are rendered documents."""
|
||||||
|
return 'document'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""
|
"""
|
||||||
Render HTML document with images as separate files.
|
Render HTML document with images as separate files.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Image renderer for report generation using AI image generation.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
import logging
|
import logging
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
@ -30,6 +30,11 @@ class RendererImage(BaseRenderer):
|
||||||
"""Return priority for image renderer."""
|
"""Return priority for image renderer."""
|
||||||
return 90
|
return 90
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: Images are visual media."""
|
||||||
|
return 'image'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to image format using AI image generation."""
|
"""Render extracted JSON content to image format using AI image generation."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ JSON renderer for report generation.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
import json
|
import json
|
||||||
|
|
||||||
class RendererJson(BaseRenderer):
|
class RendererJson(BaseRenderer):
|
||||||
|
|
@ -27,6 +27,11 @@ class RendererJson(BaseRenderer):
|
||||||
"""Return priority for JSON renderer."""
|
"""Return priority for JSON renderer."""
|
||||||
return 80
|
return 80
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: JSON is structured data format."""
|
||||||
|
return 'code'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to JSON format."""
|
"""Render extracted JSON content to JSON format."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Markdown renderer for report generation.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
class RendererMarkdown(BaseRenderer):
|
class RendererMarkdown(BaseRenderer):
|
||||||
"""Renders content to Markdown format with format-specific extraction."""
|
"""Renders content to Markdown format with format-specific extraction."""
|
||||||
|
|
@ -26,6 +26,11 @@ class RendererMarkdown(BaseRenderer):
|
||||||
"""Return priority for markdown renderer."""
|
"""Return priority for markdown renderer."""
|
||||||
return 95
|
return 95
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: Markdown documents are formatted documents."""
|
||||||
|
return 'document'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to Markdown format."""
|
"""Render extracted JSON content to Markdown format."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ PDF renderer for report generation using reportlab.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
@ -39,6 +39,11 @@ class RendererPdf(BaseRenderer):
|
||||||
"""Return priority for PDF renderer."""
|
"""Return priority for PDF renderer."""
|
||||||
return 120
|
return 120
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: PDF documents are formatted documents."""
|
||||||
|
return 'document'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to PDF format using AI-analyzed styling."""
|
"""Render extracted JSON content to PDF format using AI-analyzed styling."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,21 @@ class RendererPptx(BaseRenderer):
|
||||||
"""Get list of supported output formats."""
|
"""Get list of supported output formats."""
|
||||||
return ["pptx", "ppt"]
|
return ["pptx", "ppt"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getFormatAliases(cls) -> List[str]:
|
||||||
|
"""Return format aliases."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getPriority(cls) -> int:
|
||||||
|
"""Return priority for PowerPoint renderer."""
|
||||||
|
return 105
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: PowerPoint presentations are formatted documents."""
|
||||||
|
return 'document'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""
|
"""
|
||||||
Render content as PowerPoint presentation from JSON data.
|
Render content as PowerPoint presentation from JSON data.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Text renderer for report generation.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
class RendererText(BaseRenderer):
|
class RendererText(BaseRenderer):
|
||||||
"""Renders content to plain text format with format-specific extraction."""
|
"""Renders content to plain text format with format-specific extraction."""
|
||||||
|
|
@ -48,6 +48,21 @@ class RendererText(BaseRenderer):
|
||||||
"""Return priority for text renderer."""
|
"""Return priority for text renderer."""
|
||||||
return 90
|
return 90
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: str = None) -> str:
|
||||||
|
"""
|
||||||
|
Return output style classification based on format.
|
||||||
|
For txt/text/plain: 'document' (unstructured text)
|
||||||
|
For all other formats: 'code' (structured formats with rules/syntax)
|
||||||
|
|
||||||
|
Note: formatName parameter is provided by registry when calling this method.
|
||||||
|
"""
|
||||||
|
# Plain text formats are document style
|
||||||
|
if formatName and formatName.lower() in ['txt', 'text', 'plain']:
|
||||||
|
return 'document'
|
||||||
|
# All other formats handled by RendererText are code style
|
||||||
|
return 'code'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to plain text format."""
|
"""Render extracted JSON content to plain text format."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Excel renderer for report generation using openpyxl.
|
||||||
|
|
||||||
from .rendererBaseTemplate import BaseRenderer
|
from .rendererBaseTemplate import BaseRenderer
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime, UTC, date
|
from datetime import datetime, UTC, date
|
||||||
|
|
@ -43,6 +43,11 @@ class RendererXlsx(BaseRenderer):
|
||||||
"""Return priority for Excel renderer."""
|
"""Return priority for Excel renderer."""
|
||||||
return 110
|
return 110
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getOutputStyle(cls, formatName: Optional[str] = None) -> str:
|
||||||
|
"""Return output style classification: Excel spreadsheets are formatted documents."""
|
||||||
|
return 'document'
|
||||||
|
|
||||||
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
async def render(self, extractedContent: Dict[str, Any], title: str, userPrompt: str = None, aiService=None) -> List[RenderedDocument]:
|
||||||
"""Render extracted JSON content to Excel format using AI-analyzed styling."""
|
"""Render extracted JSON content to Excel format using AI-analyzed styling."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -798,6 +803,7 @@ class RendererXlsx(BaseRenderer):
|
||||||
|
|
||||||
# Add rows - handle both array format and cells object format
|
# Add rows - handle both array format and cells object format
|
||||||
cell_style = styles.get("table_cell", {})
|
cell_style = styles.get("table_cell", {})
|
||||||
|
header_count = len(headers)
|
||||||
for row_idx, row_data in enumerate(rows, 4):
|
for row_idx, row_data in enumerate(rows, 4):
|
||||||
# Handle different row formats
|
# Handle different row formats
|
||||||
if isinstance(row_data, list):
|
if isinstance(row_data, list):
|
||||||
|
|
@ -810,6 +816,14 @@ class RendererXlsx(BaseRenderer):
|
||||||
# Unknown format, skip
|
# Unknown format, skip
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Validate row column count matches headers - pad or truncate if needed
|
||||||
|
if len(cell_values) < header_count:
|
||||||
|
# Pad with empty strings if row has fewer columns
|
||||||
|
cell_values.extend([""] * (header_count - len(cell_values)))
|
||||||
|
elif len(cell_values) > header_count:
|
||||||
|
# Truncate if row has more columns than headers
|
||||||
|
cell_values = cell_values[:header_count]
|
||||||
|
|
||||||
for col_idx, cell_value in enumerate(cell_values, 1):
|
for col_idx, cell_value in enumerate(cell_values, 1):
|
||||||
# Extract value if it's a dict with "value" key
|
# Extract value if it's a dict with "value" key
|
||||||
if isinstance(cell_value, dict):
|
if isinstance(cell_value, dict):
|
||||||
|
|
@ -1178,6 +1192,7 @@ class RendererXlsx(BaseRenderer):
|
||||||
|
|
||||||
# Add rows with formatting
|
# Add rows with formatting
|
||||||
cell_style = styles.get("table_cell", {})
|
cell_style = styles.get("table_cell", {})
|
||||||
|
header_count = len(headers)
|
||||||
for row_data in rows:
|
for row_data in rows:
|
||||||
# Handle different row formats
|
# Handle different row formats
|
||||||
if isinstance(row_data, list):
|
if isinstance(row_data, list):
|
||||||
|
|
@ -1187,6 +1202,14 @@ class RendererXlsx(BaseRenderer):
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Validate row column count matches headers - pad or truncate if needed
|
||||||
|
if len(cell_values) < header_count:
|
||||||
|
# Pad with empty strings if row has fewer columns
|
||||||
|
cell_values.extend([""] * (header_count - len(cell_values)))
|
||||||
|
elif len(cell_values) > header_count:
|
||||||
|
# Truncate if row has more columns than headers
|
||||||
|
cell_values = cell_values[:header_count]
|
||||||
|
|
||||||
for col, cell_value in enumerate(cell_values, 1):
|
for col, cell_value in enumerate(cell_values, 1):
|
||||||
sanitized_value = self._sanitizeCellValue(cell_value)
|
sanitized_value = self._sanitizeCellValue(cell_value)
|
||||||
cell = sheet.cell(row=startRow, column=col, value=sanitized_value)
|
cell = sheet.cell(row=startRow, column=col, value=sanitized_value)
|
||||||
|
|
|
||||||
|
|
@ -540,7 +540,7 @@ class ContentValidator:
|
||||||
if not hasattr(self, 'services') or not self.services or not hasattr(self.services, 'ai'):
|
if not hasattr(self, 'services') or not self.services or not hasattr(self.services, 'ai'):
|
||||||
return self._createFailedValidationResult("AI service not available")
|
return self._createFailedValidationResult("AI service not available")
|
||||||
|
|
||||||
# Use taskStep.objective if available, otherwise fall back to intent.primaryGoal
|
# Use taskStep.objective if available, otherwise fall back to workflow intent
|
||||||
taskObjective = None
|
taskObjective = None
|
||||||
if taskStep and hasattr(taskStep, 'objective'):
|
if taskStep and hasattr(taskStep, 'objective'):
|
||||||
taskObjective = taskStep.objective
|
taskObjective = taskStep.objective
|
||||||
|
|
@ -567,7 +567,9 @@ class ContentValidator:
|
||||||
expectedFormats = intent.get('expectedFormats', [])
|
expectedFormats = intent.get('expectedFormats', [])
|
||||||
|
|
||||||
# Determine objective text and label
|
# Determine objective text and label
|
||||||
objectiveText = taskObjective if taskObjective else intent.get('primaryGoal', 'Unknown')
|
workflowIntent = getattr(self.services.workflow, '_workflowIntent', {}) if hasattr(self.services, 'workflow') and self.services.workflow else {}
|
||||||
|
intentText = workflowIntent.get('intent', 'Unknown')
|
||||||
|
objectiveText = taskObjective if taskObjective else intentText
|
||||||
objectiveLabel = "TASK OBJECTIVE" if taskObjective else "USER REQUEST"
|
objectiveLabel = "TASK OBJECTIVE" if taskObjective else "USER REQUEST"
|
||||||
|
|
||||||
# Build prompt base WITHOUT document summaries first
|
# Build prompt base WITHOUT document summaries first
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ class ProgressTracker:
|
||||||
improvementSuggestions = validation.get('improvementSuggestions', [])
|
improvementSuggestions = validation.get('improvementSuggestions', [])
|
||||||
|
|
||||||
# Get task objective from taskIntent (task-level, not workflow-level)
|
# Get task objective from taskIntent (task-level, not workflow-level)
|
||||||
taskObjective = taskIntent.get('taskObjective', taskIntent.get('primaryGoal', 'Unknown'))
|
# Fallback to 'Unknown' if task objective not available
|
||||||
|
taskObjective = taskIntent.get('taskObjective', 'Unknown')
|
||||||
|
|
||||||
# If validation is not schema compliant, treat as indeterminate (do not count as failure)
|
# If validation is not schema compliant, treat as indeterminate (do not count as failure)
|
||||||
if not schemaCompliant or overallSuccess is None or qualityScore is None:
|
if not schemaCompliant or overallSuccess is None or qualityScore is None:
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class TaskPlanner:
|
||||||
# Use workflowIntent from workflow object (set in workflowManager from userintention analysis)
|
# Use workflowIntent from workflow object (set in workflowManager from userintention analysis)
|
||||||
workflowIntent = getattr(workflow, '_workflowIntent', None)
|
workflowIntent = getattr(workflow, '_workflowIntent', None)
|
||||||
if workflowIntent and isinstance(workflowIntent, dict):
|
if workflowIntent and isinstance(workflowIntent, dict):
|
||||||
cleanedObjective = workflowIntent.get('primaryGoal', actualUserPrompt)
|
cleanedObjective = workflowIntent.get('intent', actualUserPrompt)
|
||||||
else:
|
else:
|
||||||
# Fallback: use user prompt directly if workflowIntent not available
|
# Fallback: use user prompt directly if workflowIntent not available
|
||||||
cleanedObjective = actualUserPrompt
|
cleanedObjective = actualUserPrompt
|
||||||
|
|
|
||||||
|
|
@ -149,21 +149,63 @@ class DynamicMode(BaseMode):
|
||||||
})
|
})
|
||||||
|
|
||||||
# Content validation (against original cleaned user prompt / workflow intent)
|
# Content validation (against original cleaned user prompt / workflow intent)
|
||||||
if getattr(self, 'workflowIntent', None) and result.documents:
|
if getattr(self, 'workflowIntent', None):
|
||||||
# Pass ALL documents to validator - validator decides what to validate (generic approach)
|
# Collect ALL documents from current round, not just from last action
|
||||||
# Pass taskStep so validator can use task.objective and format fields
|
# Start with documents from current action (ActionDocument objects with metadata)
|
||||||
# Pass action name so validator knows which action created the documents
|
allRoundDocuments = list(result.documents) if result and result.documents else []
|
||||||
# Pass action parameters so validator can verify parameter-specific requirements
|
|
||||||
# Pass action history so validator can validate process-oriented criteria in multi-step workflows
|
# Also collect ChatDocument references from all messages in current round
|
||||||
actionName = selection.get('action', 'unknown')
|
# These provide document existence info even if we don't have full metadata
|
||||||
actionParameters = selection.get('parameters', {})
|
if workflow and hasattr(workflow, 'messages') and workflow.messages:
|
||||||
actionHistory = getattr(context, 'executedActions', None) if hasattr(context, 'executedActions') else None
|
currentRound = getattr(workflow, 'currentRound', 0)
|
||||||
validationResult = await self.contentValidator.validateContent(result.documents, self.workflowIntent, taskStep, actionName, actionParameters, actionHistory, context)
|
currentTask = getattr(workflow, 'currentTask', 0)
|
||||||
observation.contentValidation = validationResult
|
# Collect documents from all messages in current round
|
||||||
quality_score = validationResult.get('qualityScore', 0.0)
|
for message in workflow.messages:
|
||||||
if quality_score is None:
|
if hasattr(message, 'documents') and message.documents:
|
||||||
quality_score = 0.0
|
for chatDoc in message.documents:
|
||||||
logger.info(f"Content validation: {validationResult['overallSuccess']} (quality: {quality_score:.2f})")
|
# Include documents from current round and current task
|
||||||
|
docRound = getattr(chatDoc, 'roundNumber', None)
|
||||||
|
docTask = getattr(chatDoc, 'taskNumber', None)
|
||||||
|
if docRound == currentRound and (docTask is None or docTask == currentTask):
|
||||||
|
# Avoid duplicates - check if document already in list by fileId
|
||||||
|
chatDocFileId = getattr(chatDoc, 'fileId', None)
|
||||||
|
if chatDocFileId:
|
||||||
|
# Check if we already have this document (by fileId for ChatDocument, by documentName for ActionDocument)
|
||||||
|
isDuplicate = False
|
||||||
|
for existingDoc in allRoundDocuments:
|
||||||
|
existingFileId = getattr(existingDoc, 'fileId', None)
|
||||||
|
existingDocName = getattr(existingDoc, 'documentName', None)
|
||||||
|
# Match by fileId or by documentName matching fileName
|
||||||
|
if (existingFileId == chatDocFileId) or \
|
||||||
|
(existingDocName and hasattr(chatDoc, 'fileName') and existingDocName == chatDoc.fileName):
|
||||||
|
isDuplicate = True
|
||||||
|
break
|
||||||
|
if not isDuplicate:
|
||||||
|
allRoundDocuments.append(chatDoc)
|
||||||
|
|
||||||
|
# Only validate if we have documents to validate
|
||||||
|
if allRoundDocuments:
|
||||||
|
# Pass ALL documents from current round to validator
|
||||||
|
# Pass taskStep so validator can use task.objective and format fields
|
||||||
|
# Pass action name so validator knows which action created the documents
|
||||||
|
# Pass action parameters so validator can verify parameter-specific requirements
|
||||||
|
# Pass action history so validator can validate process-oriented criteria in multi-step workflows
|
||||||
|
actionName = selection.get('action', 'unknown')
|
||||||
|
actionParameters = selection.get('parameters', {})
|
||||||
|
actionHistory = getattr(context, 'executedActions', None) if hasattr(context, 'executedActions') else None
|
||||||
|
validationResult = await self.contentValidator.validateContent(allRoundDocuments, self.workflowIntent, taskStep, actionName, actionParameters, actionHistory, context)
|
||||||
|
else:
|
||||||
|
# No documents to validate
|
||||||
|
validationResult = None
|
||||||
|
|
||||||
|
if validationResult:
|
||||||
|
observation.contentValidation = validationResult
|
||||||
|
quality_score = validationResult.get('qualityScore', 0.0)
|
||||||
|
if quality_score is None:
|
||||||
|
quality_score = 0.0
|
||||||
|
logger.info(f"Content validation: {validationResult.get('overallSuccess', False)} (quality: {quality_score:.2f})")
|
||||||
|
else:
|
||||||
|
logger.info("Content validation skipped: no documents to validate")
|
||||||
|
|
||||||
# NEW: Record validation result for adaptive learning
|
# NEW: Record validation result for adaptive learning
|
||||||
actionValue = selection.get('action', 'unknown')
|
actionValue = selection.get('action', 'unknown')
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,52 @@ def extractUserPrompt(context: Any) -> str:
|
||||||
return context.taskStep.objective
|
return context.taskStep.objective
|
||||||
return 'No request specified'
|
return 'No request specified'
|
||||||
|
|
||||||
|
def extractNormalizedRequest(services: Any) -> str:
|
||||||
|
"""Extract normalized user request from services. Maps to {{KEY:NORMALIZED_REQUEST}}.
|
||||||
|
Returns the full normalized request from user input analysis (preserves all constraints and details).
|
||||||
|
CRITICAL: Must return the actual normalizedRequest from analysis, NOT intent.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get normalized request from currentUserPromptNormalized (stores the normalizedRequest from analysis)
|
||||||
|
if services and getattr(services, 'currentUserPromptNormalized', None):
|
||||||
|
normalized = services.currentUserPromptNormalized
|
||||||
|
# Validate that it's not the intent (which is shorter and less detailed)
|
||||||
|
# Intent is typically a concise objective, normalized request should be longer and more detailed
|
||||||
|
workflowIntent = getattr(services.workflow, '_workflowIntent', {}) if hasattr(services, 'workflow') and services.workflow else {}
|
||||||
|
intent = workflowIntent.get('intent', '')
|
||||||
|
|
||||||
|
# If normalized matches intent exactly, it's wrong - log warning
|
||||||
|
if intent and normalized == intent:
|
||||||
|
logger.warning(f"extractNormalizedRequest: normalized request matches intent - this is incorrect! normalized={normalized[:100]}...")
|
||||||
|
# Try to get from workflow intent or return error message
|
||||||
|
return f"ERROR: Normalized request not properly stored. Expected detailed request, got intent: {intent}"
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
return 'No normalized request specified'
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error extracting normalized request: {str(e)}")
|
||||||
|
return 'No normalized request specified'
|
||||||
|
|
||||||
|
def extractUserIntent(services: Any) -> str:
|
||||||
|
"""Extract user intent from services. Maps to {{KEY:USER_INTENT}}.
|
||||||
|
Returns the concise intent from user input analysis, or falls back to normalized request.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get intent from currentUserPrompt (stores the intent from analysis)
|
||||||
|
if services and getattr(services, 'currentUserPrompt', None):
|
||||||
|
intent = services.currentUserPrompt
|
||||||
|
# If intent is same as normalized, it's fine - use it
|
||||||
|
return intent
|
||||||
|
|
||||||
|
# Fallback to normalized request if intent not available
|
||||||
|
if services and getattr(services, 'currentUserPromptNormalized', None):
|
||||||
|
return services.currentUserPromptNormalized
|
||||||
|
|
||||||
|
return 'No intent specified'
|
||||||
|
except Exception:
|
||||||
|
return 'No intent specified'
|
||||||
|
|
||||||
def extractWorkflowHistory(service: Any) -> str:
|
def extractWorkflowHistory(service: Any) -> str:
|
||||||
"""Extract workflow history. Maps to {{KEY:WORKFLOW_HISTORY}}
|
"""Extract workflow history. Maps to {{KEY:WORKFLOW_HISTORY}}
|
||||||
Reverse-chronological, enriched with message summaries and document labels.
|
Reverse-chronological, enriched with message summaries and document labels.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ from modules.workflows.processing.shared.placeholderFactory import (
|
||||||
extractUserPrompt,
|
extractUserPrompt,
|
||||||
extractAvailableDocumentsSummary,
|
extractAvailableDocumentsSummary,
|
||||||
extractWorkflowHistory,
|
extractWorkflowHistory,
|
||||||
|
extractUserIntent,
|
||||||
|
extractNormalizedRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -41,13 +43,13 @@ def generateTaskPlanningPrompt(services, context: Any) -> PromptBundle:
|
||||||
- Data Type: {workflowIntent.get('dataType', 'unknown')}
|
- Data Type: {workflowIntent.get('dataType', 'unknown')}
|
||||||
- Expected Formats: {workflowIntent.get('expectedFormats', [])}
|
- Expected Formats: {workflowIntent.get('expectedFormats', [])}
|
||||||
- Quality Requirements: {workflowIntent.get('qualityRequirements', {})}
|
- Quality Requirements: {workflowIntent.get('qualityRequirements', {})}
|
||||||
- Primary Goal: {workflowIntent.get('primaryGoal', '')}
|
|
||||||
|
|
||||||
Note: Tasks can override these if task-specific needs differ (e.g., workflow wants PDF, but task needs CSV for intermediate step).
|
Note: Tasks can override these if task-specific needs differ (e.g., workflow wants PDF, but task needs CSV for intermediate step).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
placeholders: List[PromptPlaceholder] = [
|
placeholders: List[PromptPlaceholder] = [
|
||||||
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
|
PromptPlaceholder(label="NORMALIZED_REQUEST", content=extractNormalizedRequest(services), summaryAllowed=False),
|
||||||
|
PromptPlaceholder(label="USER_INTENT", content=extractUserIntent(services), summaryAllowed=False),
|
||||||
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
|
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
|
||||||
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services), summaryAllowed=True),
|
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services), summaryAllowed=True),
|
||||||
PromptPlaceholder(label="USER_LANGUAGE", content=userLanguage, summaryAllowed=False),
|
PromptPlaceholder(label="USER_LANGUAGE", content=userLanguage, summaryAllowed=False),
|
||||||
|
|
@ -62,9 +64,17 @@ Break down user requests into logical, executable task steps.
|
||||||
|
|
||||||
## 📋 Context
|
## 📋 Context
|
||||||
|
|
||||||
### User Request
|
### Normalized User Request
|
||||||
The following is the user's normalized request:
|
The following is the user's full normalized request (preserves all constraints and details):
|
||||||
{{KEY:USER_PROMPT}}
|
```
|
||||||
|
{{KEY:NORMALIZED_REQUEST}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Intent
|
||||||
|
The following is the user's intent (concise objective):
|
||||||
|
```
|
||||||
|
{{KEY:USER_INTENT}}
|
||||||
|
```
|
||||||
|
|
||||||
### Workflow Intent
|
### Workflow Intent
|
||||||
{{KEY:WORKFLOW_INTENT}}
|
{{KEY:WORKFLOW_INTENT}}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ class WorkflowManager:
|
||||||
|
|
||||||
# Extract intent analysis fields and store as workflowIntent
|
# Extract intent analysis fields and store as workflowIntent
|
||||||
workflowIntent = {
|
workflowIntent = {
|
||||||
'primaryGoal': analysisResult.get('primaryGoal'),
|
'intent': intentText, # Use intent instead of primaryGoal
|
||||||
'dataType': analysisResult.get('dataType', 'unknown'),
|
'dataType': analysisResult.get('dataType', 'unknown'),
|
||||||
'expectedFormats': analysisResult.get('expectedFormats', []),
|
'expectedFormats': analysisResult.get('expectedFormats', []),
|
||||||
'qualityRequirements': analysisResult.get('qualityRequirements', {}),
|
'qualityRequirements': analysisResult.get('qualityRequirements', {}),
|
||||||
|
|
@ -229,8 +229,16 @@ class WorkflowManager:
|
||||||
self.services.workflow._workflowIntent = workflowIntent
|
self.services.workflow._workflowIntent = workflowIntent
|
||||||
|
|
||||||
# Store normalized request and intent
|
# Store normalized request and intent
|
||||||
|
# CRITICAL: normalizedRequest MUST be used if available, do NOT fall back to intent
|
||||||
self.services.currentUserPrompt = intentText or userInput.prompt
|
self.services.currentUserPrompt = intentText or userInput.prompt
|
||||||
self.services.currentUserPromptNormalized = normalizedRequest or intentText or userInput.prompt
|
if normalizedRequest and normalizedRequest.strip():
|
||||||
|
# Use normalizedRequest if available and not empty
|
||||||
|
self.services.currentUserPromptNormalized = normalizedRequest
|
||||||
|
logger.info(f"Stored normalized request (length: {len(normalizedRequest)}, preview: {normalizedRequest[:100]}...)")
|
||||||
|
else:
|
||||||
|
# Fallback only if normalizedRequest is None or empty
|
||||||
|
logger.warning(f"normalizedRequest is None or empty, falling back to intentText. normalizedRequest={normalizedRequest}, intentText={intentText[:100] if intentText else None}...")
|
||||||
|
self.services.currentUserPromptNormalized = intentText or userInput.prompt
|
||||||
if contextItems is not None:
|
if contextItems is not None:
|
||||||
self.services.currentUserContextItems = contextItems
|
self.services.currentUserContextItems = contextItems
|
||||||
|
|
||||||
|
|
@ -289,7 +297,6 @@ class WorkflowManager:
|
||||||
- complexity: "simple" | "moderate" | "complex"
|
- complexity: "simple" | "moderate" | "complex"
|
||||||
- needsWorkflowHistory: bool
|
- needsWorkflowHistory: bool
|
||||||
- fastTrack: bool
|
- fastTrack: bool
|
||||||
- primaryGoal: Hauptziel
|
|
||||||
- dataType: Datentyp
|
- dataType: Datentyp
|
||||||
- expectedFormats: Erwartete Formate
|
- expectedFormats: Erwartete Formate
|
||||||
- qualityRequirements: Qualitätsanforderungen
|
- qualityRequirements: Qualitätsanforderungen
|
||||||
|
|
@ -313,11 +320,10 @@ class WorkflowManager:
|
||||||
- "complex": Multi-task workflow, many documents, research needed, content generation required, multi-step planning (60-120s)
|
- "complex": Multi-task workflow, many documents, research needed, content generation required, multi-step planning (60-120s)
|
||||||
6. needsWorkflowHistory: Boolean indicating if this request needs previous workflow rounds/history (e.g., 'continue', 'retry', 'fix', 'improve', 'update', 'modify', 'based on previous', 'build on', references to earlier work)
|
6. needsWorkflowHistory: Boolean indicating if this request needs previous workflow rounds/history (e.g., 'continue', 'retry', 'fix', 'improve', 'update', 'modify', 'based on previous', 'build on', references to earlier work)
|
||||||
7. fastTrack: Boolean indicating if Fast Track is possible (simple requests without documents and without workflow history)
|
7. fastTrack: Boolean indicating if Fast Track is possible (simple requests without documents and without workflow history)
|
||||||
8. primaryGoal: The main objective the user wants to achieve
|
8. dataType: What type of data/content they want (numbers|text|documents|analysis|code|unknown)
|
||||||
9. dataType: What type of data/content they want (numbers|text|documents|analysis|code|unknown)
|
9. expectedFormats: What file format(s) they expect - provide matching file format extensions list (e.g., ["xlsx", "pdf"]). If format is unclear or not specified, use empty list []
|
||||||
10. expectedFormats: What file format(s) they expect - provide matching file format extensions list (e.g., ["xlsx", "pdf"]). If format is unclear or not specified, use empty list []
|
10. qualityRequirements: Quality requirements they have (accuracy, completeness) as {{accuracyThreshold: 0.0-1.0, completenessThreshold: 0.0-1.0}}
|
||||||
11. qualityRequirements: Quality requirements they have (accuracy, completeness) as {{accuracyThreshold: 0.0-1.0, completenessThreshold: 0.0-1.0}}
|
11. successCriteria: Specific success criteria that define completion (array of strings)
|
||||||
12. successCriteria: Specific success criteria that define completion (array of strings)
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- If total content (intent + data) is < 10% of model max tokens, do not extract; return empty contextItems and keep intent compact and self-contained
|
- If total content (intent + data) is < 10% of model max tokens, do not extract; return empty contextItems and keep intent compact and self-contained
|
||||||
|
|
@ -345,7 +351,6 @@ Return ONLY JSON (no markdown) with this exact structure:
|
||||||
"complexity": "simple" | "moderate" | "complex",
|
"complexity": "simple" | "moderate" | "complex",
|
||||||
"needsWorkflowHistory": true|false,
|
"needsWorkflowHistory": true|false,
|
||||||
"fastTrack": true|false,
|
"fastTrack": true|false,
|
||||||
"primaryGoal": "The main objective the user wants to achieve",
|
|
||||||
"dataType": "numbers|text|documents|analysis|code|unknown",
|
"dataType": "numbers|text|documents|analysis|code|unknown",
|
||||||
"expectedFormats": ["pdf", "docx", "xlsx", "txt", "json", "csv", "html", "md"],
|
"expectedFormats": ["pdf", "docx", "xlsx", "txt", "json", "csv", "html", "md"],
|
||||||
"qualityRequirements": {{
|
"qualityRequirements": {{
|
||||||
|
|
@ -395,7 +400,6 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
"complexity": "moderate",
|
"complexity": "moderate",
|
||||||
"needsWorkflowHistory": False,
|
"needsWorkflowHistory": False,
|
||||||
"fastTrack": False,
|
"fastTrack": False,
|
||||||
"primaryGoal": None,
|
|
||||||
"dataType": "unknown",
|
"dataType": "unknown",
|
||||||
"expectedFormats": [],
|
"expectedFormats": [],
|
||||||
"qualityRequirements": {
|
"qualityRequirements": {
|
||||||
|
|
@ -523,10 +527,14 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
roundNum = workflow.currentRound
|
roundNum = workflow.currentRound
|
||||||
contextLabel = f"round{roundNum}_usercontext"
|
contextLabel = f"round{roundNum}_usercontext"
|
||||||
|
|
||||||
|
# Use normalized request if available (from combined analysis), otherwise use original prompt
|
||||||
|
# This ensures the first message uses the normalized request for security
|
||||||
|
normalizedRequest = getattr(self.services, 'currentUserPromptNormalized', None) or userInput.prompt
|
||||||
|
|
||||||
messageData = {
|
messageData = {
|
||||||
"workflowId": workflow.id,
|
"workflowId": workflow.id,
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"message": userInput.prompt,
|
"message": normalizedRequest, # Use normalized request instead of original prompt
|
||||||
"status": "first",
|
"status": "first",
|
||||||
"sequenceNr": 1,
|
"sequenceNr": 1,
|
||||||
"publishedAt": self.services.utils.timestampGetUtc(),
|
"publishedAt": self.services.utils.timestampGetUtc(),
|
||||||
|
|
@ -602,12 +610,11 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
"2) normalizedRequest: full, explicit restatement of the user's request in the detected language; do NOT summarize; preserve ALL constraints and details.\n"
|
"2) normalizedRequest: full, explicit restatement of the user's request in the detected language; do NOT summarize; preserve ALL constraints and details.\n"
|
||||||
"3) intent: concise single-paragraph core request in the detected language for high-level routing.\n"
|
"3) intent: concise single-paragraph core request in the detected language for high-level routing.\n"
|
||||||
"4) contextItems: supportive data blocks to attach as separate documents if significantly larger than the intent (large literal content, long lists/tables, code/JSON blocks, transcripts, CSV fragments, detailed specs). Keep URLs in the intent unless they embed large pasted content.\n"
|
"4) contextItems: supportive data blocks to attach as separate documents if significantly larger than the intent (large literal content, long lists/tables, code/JSON blocks, transcripts, CSV fragments, detailed specs). Keep URLs in the intent unless they embed large pasted content.\n"
|
||||||
"5) primaryGoal: The main objective the user wants to achieve.\n"
|
"5) dataType: What type of data/content they want (numbers|text|documents|analysis|code|unknown).\n"
|
||||||
"6) dataType: What type of data/content they want (numbers|text|documents|analysis|code|unknown).\n"
|
"6) expectedFormats: What file format(s) they expect - provide matching file format extensions list (e.g., [\"xlsx\", \"pdf\"]). If format is unclear or not specified, use empty list [].\n"
|
||||||
"7) expectedFormats: What file format(s) they expect - provide matching file format extensions list (e.g., [\"xlsx\", \"pdf\"]). If format is unclear or not specified, use empty list [].\n"
|
"7) qualityRequirements: Quality requirements they have (accuracy, completeness) as {accuracyThreshold: 0.0-1.0, completenessThreshold: 0.0-1.0}.\n"
|
||||||
"8) qualityRequirements: Quality requirements they have (accuracy, completeness) as {accuracyThreshold: 0.0-1.0, completenessThreshold: 0.0-1.0}.\n"
|
"8) successCriteria: Specific success criteria that define completion (array of strings).\n"
|
||||||
"9) successCriteria: Specific success criteria that define completion (array of strings).\n"
|
"9) needsWorkflowHistory: Boolean indicating if this request needs previous workflow rounds/history to be understood or completed (e.g., 'continue', 'retry', 'fix', 'improve', 'update', 'modify', 'based on previous', 'build on', references to earlier work). Return true if the request is a continuation, retry, modification, or builds upon previous work.\n\n"
|
||||||
"10) needsWorkflowHistory: Boolean indicating if this request needs previous workflow rounds/history to be understood or completed (e.g., 'continue', 'retry', 'fix', 'improve', 'update', 'modify', 'based on previous', 'build on', references to earlier work). Return true if the request is a continuation, retry, modification, or builds upon previous work.\n\n"
|
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
"- If total content (intent + data) is < 10% of model max tokens, do not extract; return empty contextItems and keep intent compact and self-contained.\n"
|
"- If total content (intent + data) is < 10% of model max tokens, do not extract; return empty contextItems and keep intent compact and self-contained.\n"
|
||||||
"- If content exceeds that threshold, move bulky parts into contextItems; keep intent short and clear.\n"
|
"- If content exceeds that threshold, move bulky parts into contextItems; keep intent short and clear.\n"
|
||||||
|
|
@ -625,7 +632,6 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
" \"content\": \"Full extracted content block here\"\n"
|
" \"content\": \"Full extracted content block here\"\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
" ],\n"
|
" ],\n"
|
||||||
" \"primaryGoal\": \"The main objective the user wants to achieve\",\n"
|
|
||||||
" \"dataType\": \"numbers|text|documents|analysis|code|unknown\",\n"
|
" \"dataType\": \"numbers|text|documents|analysis|code|unknown\",\n"
|
||||||
" \"expectedFormats\": [\"pdf\", \"docx\", \"xlsx\", \"txt\", \"json\", \"csv\", \"html\", \"md\"],\n"
|
" \"expectedFormats\": [\"pdf\", \"docx\", \"xlsx\", \"txt\", \"json\", \"csv\", \"html\", \"md\"],\n"
|
||||||
" \"qualityRequirements\": {\n"
|
" \"qualityRequirements\": {\n"
|
||||||
|
|
@ -668,8 +674,9 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
contextItems = parsed.get('contextItems') or []
|
contextItems = parsed.get('contextItems') or []
|
||||||
|
|
||||||
# Extract intent analysis fields and store as workflowIntent
|
# Extract intent analysis fields and store as workflowIntent
|
||||||
|
intentText = parsed.get('intent') or userInput.prompt
|
||||||
workflowIntent = {
|
workflowIntent = {
|
||||||
'primaryGoal': parsed.get('primaryGoal'),
|
'intent': intentText, # Use intent instead of primaryGoal
|
||||||
'dataType': parsed.get('dataType', 'unknown'),
|
'dataType': parsed.get('dataType', 'unknown'),
|
||||||
'expectedFormats': parsed.get('expectedFormats', []),
|
'expectedFormats': parsed.get('expectedFormats', []),
|
||||||
'qualityRequirements': parsed.get('qualityRequirements', {}),
|
'qualityRequirements': parsed.get('qualityRequirements', {}),
|
||||||
|
|
@ -727,10 +734,22 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
pass
|
pass
|
||||||
self.services.currentUserPrompt = intentText or userInput.prompt
|
self.services.currentUserPrompt = intentText or userInput.prompt
|
||||||
# Always set currentUserPromptNormalized - use normalizedRequest if available, otherwise fallback to currentUserPrompt
|
# Always set currentUserPromptNormalized - use normalizedRequest if available, otherwise fallback to currentUserPrompt
|
||||||
normalizedValue = normalizedRequest or intentText or userInput.prompt
|
# CRITICAL: normalizedRequest MUST be used if available, do NOT fall back to intent
|
||||||
self.services.currentUserPromptNormalized = normalizedValue
|
if normalizedRequest and normalizedRequest.strip():
|
||||||
|
# Use normalizedRequest if available and not empty
|
||||||
|
self.services.currentUserPromptNormalized = normalizedRequest
|
||||||
|
logger.debug(f"Stored normalized request from analysis (length: {len(normalizedRequest)})")
|
||||||
|
else:
|
||||||
|
# Fallback only if normalizedRequest is None or empty
|
||||||
|
logger.warning(f"normalizedRequest is None or empty in analysis, falling back to intentText. normalizedRequest={normalizedRequest}, intentText={intentText}")
|
||||||
|
self.services.currentUserPromptNormalized = intentText or userInput.prompt
|
||||||
if contextItems is not None:
|
if contextItems is not None:
|
||||||
self.services.currentUserContextItems = contextItems
|
self.services.currentUserContextItems = contextItems
|
||||||
|
|
||||||
|
# Update message with normalized request if analysis produced one
|
||||||
|
if normalizedRequest and normalizedRequest != userInput.prompt:
|
||||||
|
messageData["message"] = normalizedRequest
|
||||||
|
logger.debug(f"Updated first message with normalized request (length: {len(normalizedRequest)})")
|
||||||
|
|
||||||
# Create documents for context items
|
# Create documents for context items
|
||||||
if contextItems and isinstance(contextItems, list):
|
if contextItems and isinstance(contextItems, list):
|
||||||
|
|
@ -784,6 +803,34 @@ The following is the user's original input message. Analyze intent, normalize th
|
||||||
# Finally, persist and bind the first message with combined documents (context + user)
|
# Finally, persist and bind the first message with combined documents (context + user)
|
||||||
self.services.chat.storeMessageWithDocuments(workflow, messageData, createdDocs)
|
self.services.chat.storeMessageWithDocuments(workflow, messageData, createdDocs)
|
||||||
|
|
||||||
|
# Create ChatMessage with success criteria (KPI) AFTER the first user message
|
||||||
|
# This ensures the KPI message appears after the user message in the UI
|
||||||
|
workflowIntent = getattr(workflow, '_workflowIntent', None)
|
||||||
|
if workflowIntent and isinstance(workflowIntent, dict):
|
||||||
|
successCriteria = workflowIntent.get('successCriteria', [])
|
||||||
|
if successCriteria and isinstance(successCriteria, list) and len(successCriteria) > 0:
|
||||||
|
try:
|
||||||
|
# Format success criteria as message with "KPI" title
|
||||||
|
criteriaText = "**KPI**\n\n" + "\n".join([f"• {criterion}" for criterion in successCriteria])
|
||||||
|
|
||||||
|
kpiMessageData = {
|
||||||
|
"workflowId": workflow.id,
|
||||||
|
"role": "system",
|
||||||
|
"message": criteriaText,
|
||||||
|
"summary": f"KPI: {len(successCriteria)} success criteria",
|
||||||
|
"status": "step",
|
||||||
|
"sequenceNr": len(workflow.messages) + 1, # After user message
|
||||||
|
"publishedAt": self.services.utils.timestampGetUtc(),
|
||||||
|
"roundNumber": workflow.currentRound,
|
||||||
|
"taskNumber": 0,
|
||||||
|
"actionNumber": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.services.chat.storeMessageWithDocuments(workflow, kpiMessageData, [])
|
||||||
|
logger.info(f"Created KPI message with {len(successCriteria)} success criteria after first user message")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating KPI message: {str(e)}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending first message: {str(e)}")
|
logger.error(f"Error sending first message: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue