diff --git a/function_call_diagram.md b/function_call_diagram.md new file mode 100644 index 00000000..7ef22517 --- /dev/null +++ b/function_call_diagram.md @@ -0,0 +1,254 @@ +# Complete Function Call Diagram + +```mermaid +graph TB + subgraph AI_Service["AI Service Modules"] + MA[mainServiceAi
AiService] + SC[subCoreAi
SubCoreAi] + SDG[subDocumentGeneration
SubDocumentGeneration] + SDP[subDocumentProcessing
SubDocumentProcessing] + SU[subSharedAiUtils
Utilities] + end + + subgraph EXT_Service["Extraction Service Modules"] + MSE[mainServiceExtraction
ExtractionService] + SPE[subPromptBuilderExtraction
buildExtractionPrompt] + SP[subPipeline
runExtraction] + end + + subgraph GEN_Service["Generation Service Modules"] + MSG[mainServiceGeneration
GenerationService] + SPG[subPromptBuilderGeneration
buildGenerationPrompt] + SJ[subJsonSchema
Schemas] + end + + %% subCoreAi calls + SC -->|_buildGenerationPrompt| SPG + SC -->|callAiDocuments| SDP + SC -->|sanitizePromptContent| SU + + %% subDocumentGeneration calls + SDG -->|processDocumentsWithContinuation| SDP + SDG -->|buildGenerationPrompt| SPG + SDG -->|renderReport| MSG + SDG -->|sanitizePromptContent| SU + + %% subDocumentProcessing calls + SDP -->|extractContent 3x| MSE + SDP -->|_applyMerging 3x| SP + SDP -->|readImage| SC + + %% mainServiceExtraction calls + MSE -->|runExtraction| SP + + %% subPromptBuilderExtraction calls + SPE -->|get_document_subJsonSchema| SJ + SPE -->|sanitizePromptContent| SU + + %% mainServiceGeneration calls utilities + MSG -->|utility functions| SU + + %% subCoreAi detailed calls + SC -.->|aiObjects.call| AI_Interface["AiObjects Interface"] + SDP -.->|aiObjects.call| AI_Interface + + %% Style + classDef aiClass fill:#e1f5ff,stroke:#0066cc,stroke-width:2px + classDef extClass fill:#fff5e1,stroke:#cc6600,stroke-width:2px + classDef genClass fill:#e1ffe1,stroke:#006600,stroke-width:2px + classDef utilClass fill:#f0f0f0,stroke:#666,stroke-width:2px + classDef interfaceClass fill:#ffe1f5,stroke:#cc0066,stroke-width:2px + + class MA,SC,SDG,SDP,SU aiClass + class MSE,SPE,SP extClass + class MSG,SPG,SJ genClass + class AI_Interface interfaceClass +``` + +## Detailed Call Map with Function Names + +```mermaid +graph LR + %% Nodes + SC[subCoreAi] + SDG[subDocumentGeneration] + SDP[subDocumentProcessing] + SU[subSharedAiUtils] + SPE[subPromptBuilderExtraction] + SPG[subPromptBuilderGeneration] + MSE[mainServiceExtraction] + MSG[mainServiceGeneration] + SP[subPipeline] + SJ[subJsonSchema] + + %% subCoreAi function calls + SC -->|"_buildGenerationPrompt()
calls"| SPG + SC -->|"callAiDocuments()
calls callAiText()"| SDP + SC -->|"sanitizePromptContent()"| SU + + %% subDocumentGeneration function calls + SDG -->|"_processDocumentsUnified()
calls"| SDP + SDG -->|"_processDocument()
calls"| SPG + SDG -->|"_processDocument()
calls"| MSG + SDG -->|"sanitizePromptContent()"| SU + + %% subDocumentProcessing function calls + SDP -->|"extractContent()"| MSE + SDP -->|"_mergePartResults()
_convertPartResultsToJson()
_mergeChunkResultsJson()
all call"| SP + SDP -->|"_processChunksWithMapping()
calls readImage()"| SC + + %% Extraction service calls + MSE -->|"extractContent()
calls"| SP + + %% Prompt builder calls + SPE -->|"get_document_subJsonSchema()"| SJ + SPE -->|"sanitizePromptContent()"| SU + + %% Generation service calls + MSG -->|"uses utility functions"| SU + + classDef aiModule fill:#e1f5ff,stroke:#0066cc + classDef extModule fill:#fff5e1,stroke:#cc6600 + classDef genModule fill:#e1ffe1,stroke:#006600 + + class SC,SDG,SDP,SU aiModule + class MSE,SPE,SP extModule + class MSG,SPG,SJ genModule +``` + +## Call Flow by Module + +### 1. subCoreAi (SubCoreAi Class) +**Calls Out:** +- `buildGenerationPrompt()` → subPromptBuilderGeneration (line 363-366) +- `callAiText()` → subDocumentProcessing (line 453) +- `renderReport()` → mainServiceGeneration (line 478-482) +- `sanitizePromptContent()` → subSharedAiUtils (line 61, via services.ai) + +**Called By:** +- mainServiceAi (creates instance) +- subDocumentProcessing._processChunksWithMapping (calls readImage at line 672-675) + +--- + +### 2. subDocumentGeneration (SubDocumentGeneration Class) +**Calls Out:** +- `processDocumentsWithContinuation()` → subDocumentProcessing (line 110) +- `buildGenerationPrompt()` → subPromptBuilderGeneration (line 330) +- `renderReport()` → mainServiceGeneration (line 392) +- `sanitizePromptContent()` → subSharedAiUtils (line 466) + +**Called By:** +- mainServiceAi (creates instance) + +--- + +### 3. subDocumentProcessing (SubDocumentProcessing Class) +**Calls Out:** +- `extractContent()` → mainServiceExtraction (lines 78, 131, 220) +- `_applyMerging()` → subPipeline (lines 1044, 1095, 1232, 1293, 1345) +- `readImage()` → subCoreAi (line 672-675) +- `sanitizePromptContent()` → subSharedAiUtils (via self.services.ai) + +**Called By:** +- mainServiceAi (creates instance) +- subCoreAi.callAiDocuments (calls callAiText at line 453) +- subDocumentGeneration._processDocumentsUnified (calls processDocumentsWithContinuation) + +--- + +### 4. mainServiceExtraction (ExtractionService Class) +**Calls Out:** +- `runExtraction()` → subPipeline (line 61) +- Uses ExtractorRegistry from subRegistry + +**Called By:** +- subDocumentProcessing.extractContent (3 times) + +--- + +### 5. subPromptBuilderExtraction +**Calls Out:** +- `get_document_subJsonSchema()` → subJsonSchema (line 172) +- `sanitizePromptContent()` → subSharedAiUtils (via services.ai) + +**Called By:** +- mainServiceGeneration (indirectly via getAdaptiveExtractionPrompt) + +--- + +### 6. mainServiceGeneration (GenerationService Class) +**Calls Out:** +- `get_renderer()` → renderers.registry (line 501) +- Utility functions from subDocumentUtility +- Uses modelRegistry (external) + +**Called By:** +- subCoreAi.callAiDocuments (calls renderReport) +- subDocumentGeneration._processDocument (calls renderReport) + +--- + +### 7. subPromptBuilderGeneration +**Calls Out:** +- Returns prompt template string + +**Called By:** +- subCoreAi._buildGenerationPrompt (line 363-366) +- subDocumentGeneration._processDocument (line 330) + +--- + +### 8. subPipeline +**Calls Out:** +- Creates IntelligentTokenAwareMerger from subMerger (line 96) +- Uses mergers from merging submodules + +**Called By:** +- mainServiceExtraction.extractContent (calls runExtraction) +- subDocumentProcessing (calls _applyMerging 5 times) + +--- + +### 9. subSharedAiUtils +**Functions Provided:** +- `buildPromptWithPlaceholders()` +- `sanitizePromptContent()` +- `extractTextFromContentParts()` +- `reduceText()` +- `determineCallType()` + +**Called By:** +- subCoreAi (imports and calls functions) +- subDocumentGeneration (via services.ai.sanitizePromptContent) +- subPromptBuilderExtraction (via services.ai.sanitizePromptContent) + +--- + +### 10. subJsonSchema +**Functions Provided:** +- `get_document_subJsonSchema()` +- `get_multi_document_subJsonSchema()` + +**Called By:** +- subPromptBuilderExtraction.buildExtractionPrompt (line 172) + +--- + +## Circular Dependencies + +**AI Service Loop:** +1. subDocumentProcessing → subCoreAi.readImage() (for image processing) +2. subDocumentProcessing → mainServiceExtraction (for extraction) +3. mainServiceExtraction → subPipeline (for processing) +4. subPipeline creates IntelligentTokenAwareMerger + +**Flow:** +``` +subDocumentProcessing.extractContent() + → mainServiceExtraction.extractContent() + → subPipeline.runExtraction() + → returns ContentExtracted + → processed by subDocumentProcessing + → calls subPipeline._applyMerging() +``` diff --git a/modules/services/serviceAi/mainServiceAi.py b/modules/services/serviceAi/mainServiceAi.py index 27434915..08286ca0 100644 --- a/modules/services/serviceAi/mainServiceAi.py +++ b/modules/services/serviceAi/mainServiceAi.py @@ -127,7 +127,7 @@ class AiService: """Planning AI call for task planning, action planning, action selection, etc.""" await self._ensureAiObjectsInitialized() # Always use "json" for planning calls since they return JSON - return await self.coreAi.callAiPlanning(prompt, placeholders, "json") + return await self.coreAi.callAiPlanning(prompt, placeholders) async def callAiDocuments( self, diff --git a/modules/services/serviceAi/subCoreAi.py b/modules/services/serviceAi/subCoreAi.py index 0952e750..977c0e9a 100644 --- a/modules/services/serviceAi/subCoreAi.py +++ b/modules/services/serviceAi/subCoreAi.py @@ -12,37 +12,26 @@ from modules.services.serviceAi.subSharedAiUtils import ( logger = logging.getLogger(__name__) +# Generic continuation instruction for all prompts with JSON responses +# Used by _callAiWithLooping() to replace LOOP_INSTRUCTION placeholder +LOOP_INSTRUCTION_TEXT = """ +MANDATORY RULE: +Return ONLY raw JSON (no ```json blocks, no text before/after) + +CONTINUATION REQUIREMENT: +Your response must be a valid JSON object with a "continuation" field. + +- If you can complete the FULL request: Set {"continuation": null} +- If you MUST stop early (due to token limits): Set {"continuation": {"last_data_items": "brief summary of what was delivered for context", "next_instruction": "what to deliver next to complete the request"}} + +The "continuation" field controls whether this AI call continues in a loop or stops. +Refer to the json template below to see where to set the "continuation" information. +""" + # Rebuild the model to resolve forward references AiCallRequest.model_rebuild() -# Loop instruction texts for different formats -LoopInstructionTexts = { - "json": """ -CRITICAL LIMITS: tokens total (reserve 20% for JSON structure) - -MANDATORY RULES: -1. STOP at approximately 80% of limit to ensure valid JSON completion -2. Return ONLY raw JSON (no ```json blocks, no text before/after) - -CONTINUATION REQUIREMENTS: -Refer to the json object below where to set the "continuation" information: -- If you can complete the full request: {"continuation": null} -- If you must stop early: { - "continuation": { - "last_data_items": "delivered last data for context (copy them)", - "next_instruction": "instruction for next data to deliver" - } -} - -BE CONSERVATIVE: Stop generating content when you reach approximately 3200-3500 characters to ensure JSON completion. -""", - # Add more formats here as needed - # "xml": "...", - # "text": "...", -} - - class SubCoreAi: """Core AI operations including image analysis, text generation, and planning calls.""" @@ -142,8 +131,7 @@ Respond with ONLY a JSON object in this exact format: self, prompt: str, options: AiCallOptions, - debugPrefix: str = "ai_call", - loopInstructionFormat: str = None + debugPrefix: str = "ai_call" ) -> str: """ Shared core function for AI calls with looping system. @@ -154,7 +142,6 @@ Respond with ONLY a JSON object in this exact format: prompt: The prompt to send to AI options: AI call configuration options debugPrefix: Prefix for debug file names - loopInstructionFormat: If provided, replaces LOOP_INSTRUCTION placeholder and includes in continuation prompts Returns: Complete AI response after all iterations @@ -162,18 +149,12 @@ Respond with ONLY a JSON object in this exact format: max_iterations = 100 # Prevent infinite loops iteration = 0 accumulatedContent = [] + lastContinuationData = None - logger.debug(f"Starting AI call with looping (debug prefix: {debugPrefix}, loopInstructionFormat: {loopInstructionFormat is not None})") + logger.debug(f"Starting AI call with looping (debug prefix: {debugPrefix})") - - # Determine loopInstruction based on loopInstructionFormat (before iterations) - if not loopInstructionFormat: - loopInstruction = "" - elif loopInstructionFormat in LoopInstructionTexts: - loopInstruction = LoopInstructionTexts[loopInstructionFormat] - else: - logger.error(f"Unsupported loopInstructionFormat for prompt: {loopInstructionFormat}") - loopInstruction = "" + # Use generic LOOP_INSTRUCTION_TEXT + loopInstruction = LOOP_INSTRUCTION_TEXT if ("LOOP_INSTRUCTION" in prompt) else "" while iteration < max_iterations: @@ -182,18 +163,25 @@ Respond with ONLY a JSON object in this exact format: # Build iteration prompt if iteration == 1: + # First iteration - replace LOOP_INSTRUCTION with standardized instruction if "LOOP_INSTRUCTION" in prompt: iterationPrompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction) else: iterationPrompt = prompt - elif loopInstruction and iteration > 1: - continuationContent = self._buildContinuationContent(accumulatedContent, iteration) - if "LOOP_INSTRUCTION" in prompt: - iterationPrompt = prompt.replace("LOOP_INSTRUCTION", f"{continuationContent}\n\n{loopInstruction}") - else: - iterationPrompt = prompt else: - iterationPrompt = prompt + # Subsequent iterations - include continuation data if available + if lastContinuationData and isinstance(lastContinuationData, dict): + continuationPrompt = self._buildContinuationPrompt(lastContinuationData, iteration) + if "LOOP_INSTRUCTION" in prompt: + iterationPrompt = prompt.replace("LOOP_INSTRUCTION", f"{continuationPrompt}\n\n{loopInstruction}") + else: + iterationPrompt = prompt + else: + # No continuation data - re-send original prompt + if "LOOP_INSTRUCTION" in prompt: + iterationPrompt = prompt.replace("LOOP_INSTRUCTION", loopInstruction) + else: + iterationPrompt = prompt # Make AI call try: @@ -234,33 +222,35 @@ Respond with ONLY a JSON object in this exact format: logger.warning(f"Iteration {iteration}: Empty response, stopping") break - # Check if this is a continuation response (only for supported formats) - if loopInstructionFormat in LoopInstructionTexts: + accumulatedContent.append(result) + + # Check if this is a continuation response (only when LOOP_INSTRUCTION was used) + if loopInstruction: try: # Extract JSON substring if wrapped (e.g., ```json ... ```) extracted = self.services.utils.jsonExtractString(result) - # Try to parse as JSON to check for continuation attribute parsed_result = json.loads(extracted) - if isinstance(parsed_result, dict) and parsed_result.get("continuation") is not None: - # This is a continuation response - accumulatedContent.append(result) - logger.debug(f"Iteration {iteration}: Continuation detected in JSON, continuing...") - continue - else: - # This is the final response (continuation is null or missing) - accumulatedContent.append(result) - logger.debug(f"Iteration {iteration}: Final response received") - break + + if isinstance(parsed_result, dict): + continuation = parsed_result.get("continuation") + + if continuation is None: + # Final response - break loop + logger.debug(f"Iteration {iteration}: Final response received (continuation: null)") + break + else: + # Continuation detected - extract data for next iteration + lastContinuationData = continuation if isinstance(continuation, dict) else None + logger.debug(f"Iteration {iteration}: Continuation detected, continuing...") + continue except json.JSONDecodeError: # Not JSON, treat as final response - accumulatedContent.append(result) - logger.warning(f"Iteration {iteration}: Non-JSON response received") + logger.warning(f"Iteration {iteration}: Non-JSON response - treating as final") self.services.utils.writeDebugFile(result, f"{debugPrefix}_error_non_json_response_iteration_{iteration}") break else: - # This is the final response - accumulatedContent.append(result) - logger.debug(f"Iteration {iteration}: Final response received") + # No loop instruction format - treat as final response + logger.debug(f"Iteration {iteration}: Final response received (no loop format)") break except Exception as e: @@ -279,51 +269,26 @@ Respond with ONLY a JSON object in this exact format: logger.info(f"AI call completed: {len(accumulatedContent)} parts from {iteration} iterations") return final_result - def _buildContinuationContent( + def _buildContinuationPrompt( self, - accumulatedContent: List[str], + continuationData: dict, iteration: int ) -> str: """ - Build continuation content for follow-up iterations. + Build standardized continuation prompt from continuation data dict. + This replaces the complex _buildContinuationContent method with a simpler approach. + + Args: + continuationData: Dictionary containing last_data_items and next_instruction + iteration: Current iteration number + + Returns: + Formatted continuation prompt string """ - # Extract continuation description from the last response - continuation_description = "" - if accumulatedContent: - try: - last_response = accumulatedContent[-1] - # Use the same JSON extraction logic as the main loop - extracted = self.services.utils.jsonExtractString(last_response) - parsed_response = json.loads(extracted) - if isinstance(parsed_response, dict): - # Check for continuation at root level or in metadata - continuation = parsed_response.get("continuation") - if continuation is None and "metadata" in parsed_response: - continuation = parsed_response["metadata"].get("continuation") - - if continuation: - continuation_description = continuation - except (json.JSONDecodeError, KeyError, ValueError): - pass + last_data_items = continuationData.get("last_data_items", "") + next_instruction = continuationData.get("next_instruction", "") - # Extract specific attributes from continuation object - last_data_items = "" - next_instruction = "" - - if continuation_description: - try: - if isinstance(continuation_description, str): - continuation_obj = json.loads(continuation_description) - else: - continuation_obj = continuation_description - - if isinstance(continuation_obj, dict): - last_data_items = continuation_obj.get("last_data_items", "") - next_instruction = continuation_obj.get("next_instruction", "") - except (json.JSONDecodeError, TypeError): - pass - - continuation_content = f"""CONTINUATION REQUEST (Iteration {iteration}): + continuation_prompt = f"""CONTINUATION REQUEST (Iteration {iteration}): You are continuing a previous response. DO NOT repeat any previous content. {f"Already delivered data: {last_data_items}" if last_data_items else "No previous data specified"} @@ -331,12 +296,10 @@ You are continuing a previous response. DO NOT repeat any previous content. {f"Your task to deliver: {next_instruction}" if next_instruction else "No specific task provided"} CRITICAL REQUIREMENTS: -- Start from the exact point specified in continuation instructions -- DO NOT repeat any previous content -- BE CONSERVATIVE: Stop at approximately 3200-3500 characters to ensure JSON completion -- ALWAYS include continuation field - set to null if complete, or provide next instruction if incomplete -""" - return continuation_content +- Start from the exact point specified above +- DO NOT repeat any previous content""" + + return continuation_prompt def _mergeJsonContent(self, accumulatedContent: List[str]) -> str: """ @@ -387,40 +350,12 @@ CRITICAL REQUIREMENTS: logger.error(f"Error merging JSON content: {str(e)}") return accumulatedContent[0] # Return first response on error - async def _buildGenerationPrompt( - self, - prompt: str, - extracted_content: Optional[str], - outputFormat: str, - title: str - ) -> str: - """ - Build generation prompt for document generation. - """ - from modules.services.serviceGeneration.subPromptBuilder import buildGenerationPrompt - - # Build the generation prompt using the existing system - generation_prompt = await buildGenerationPrompt( - outputFormat=outputFormat, - userPrompt=prompt, - title=title - ) - - # If we have extracted content, prepend it to the prompt - if extracted_content: - generation_prompt = f"""EXTRACTED CONTENT FROM DOCUMENTS: -{extracted_content} - -{generation_prompt}""" - - return generation_prompt # Planning AI Call async def callAiPlanning( self, prompt: str, - placeholders: Optional[List[PromptPlaceholder]] = None, - loopInstructionFormat: Optional[str] = None + placeholders: Optional[List[PromptPlaceholder]] = None ) -> str: """ Planning AI call for task planning, action planning, action selection, etc. @@ -429,7 +364,6 @@ CRITICAL REQUIREMENTS: Args: prompt: The planning prompt placeholders: Optional list of placeholder replacements - loopInstructionFormat: Optional loop instruction format Returns: Planning JSON response @@ -452,7 +386,7 @@ CRITICAL REQUIREMENTS: full_prompt = prompt # Use shared core function with planning-specific debug prefix - return await self._callAiWithLooping(full_prompt, options, "plan", loopInstructionFormat=loopInstructionFormat) + return await self._callAiWithLooping(full_prompt, options, "plan") # Document Generation AI Call async def callAiDocuments( @@ -461,8 +395,7 @@ CRITICAL REQUIREMENTS: documents: Optional[List[ChatDocument]] = None, options: Optional[AiCallOptions] = None, outputFormat: Optional[str] = None, - title: Optional[str] = None, - loopInstructionFormat: Optional[str] = None + title: Optional[str] = None ) -> Union[str, Dict[str, Any]]: """ Document generation AI call for all non-planning calls. @@ -494,8 +427,10 @@ CRITICAL REQUIREMENTS: else: logger.debug("No documents provided - using direct generation") extracted_content = None - generation_prompt = await self._buildGenerationPrompt(prompt, extracted_content, outputFormat, title) - generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation", loopInstructionFormat=loopInstructionFormat) + logger.debug(f"[DEBUG] title value: {title}, type: {type(title)}") + from modules.services.serviceGeneration.subPromptBuilderGeneration import buildGenerationPrompt + generation_prompt = await buildGenerationPrompt(outputFormat, prompt, title, extracted_content) + generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation") # Parse the generated JSON (extract fenced/embedded JSON first) try: @@ -552,7 +487,7 @@ CRITICAL REQUIREMENTS: result = await self.services.ai.documentProcessor.callAiText(prompt, documents, options) else: # Use shared core function for direct text calls - result = await self._callAiWithLooping(prompt, options, "text", loopInstructionFormat=None) + result = await self._callAiWithLooping(prompt, options, "text") return result diff --git a/modules/services/serviceAi/subDocumentGeneration.py b/modules/services/serviceAi/subDocumentGeneration.py index 82d1df67..351f68cc 100644 --- a/modules/services/serviceAi/subDocumentGeneration.py +++ b/modules/services/serviceAi/subDocumentGeneration.py @@ -48,11 +48,7 @@ class SubDocumentGeneration: Dict with generated documents and metadata in unified structure """ try: - # 1. Analyze prompt intent - promptAnalysis = await self._analyzePromptIntent(prompt, self) - logger.info(f"Prompt analysis result: {promptAnalysis}") - - # 2. Get unified extraction prompt + # 1. Get unified extraction prompt from modules.services.serviceGeneration.mainServiceGeneration import GenerationService generationService = GenerationService(self.services) @@ -60,17 +56,16 @@ class SubDocumentGeneration: outputFormat=outputFormat, userPrompt=prompt, title=title, - promptAnalysis=promptAnalysis, aiService=self ) - # 3. Process with unified pipeline (always multi-file approach) + # 2. Process with unified pipeline (always multi-file approach) aiResponse = await self._processDocumentsUnified( documents, extractionPrompt, options ) - # 4. Return unified result structure - return await self._buildUnifiedResult(aiResponse, outputFormat, title, promptAnalysis) + # 3. Return unified result structure + return await self._buildUnifiedResult(aiResponse, outputFormat, title) except Exception as e: logger.error(f"Error in unified document generation: {str(e)}") @@ -263,9 +258,8 @@ class SubDocumentGeneration: self, aiResponse: Dict[str, Any], outputFormat: str, - title: str, - promptAnalysis: Dict[str, Any] - ) -> Dict[str, Any]: + title: str + ) -> Dict[str, Any]: """ Build unified result structure that always returns array-based format. Content is always a multi-document structure. @@ -296,7 +290,6 @@ class SubDocumentGeneration: "is_multi_file": len(generatedDocuments) > 1, "format": outputFormat, "title": title, - "split_strategy": promptAnalysis.get("strategy", "single"), "total_documents": len(generatedDocuments), "processed_documents": len(generatedDocuments) } @@ -313,7 +306,7 @@ class SubDocumentGeneration: outputFormat: str, title: str, documentIndex: int - ) -> Dict[str, Any]: + ) -> Dict[str, Any]: """ Process individual document with content enhancement and rendering. """ @@ -326,12 +319,12 @@ class SubDocumentGeneration: enhancedContent = docData # Default to original if docData.get("sections"): try: - # Get generation prompt - generationPrompt = await generationService.getGenerationPrompt( + # Get generation prompt directly + from modules.services.serviceGeneration.subPromptBuilderGeneration import buildGenerationPrompt + generationPrompt = await buildGenerationPrompt( outputFormat=outputFormat, userPrompt=title, - title=docData.get("title", title), - aiService=self + title=docData.get("title", title) ) # Prepare the AI call @@ -454,57 +447,6 @@ class SubDocumentGeneration: # Process documents with JSON merging return await self.documentProcessor.processDocumentsPerChunkJson(documents, prompt, options) - async def _analyzePromptIntent(self, prompt: str, ai_service=None) -> Dict[str, Any]: - """Use AI to analyze user prompt and determine processing requirements.""" - if not ai_service: - return {"is_multi_file": False, "strategy": "single", "criteria": None} - - try: - analysis_prompt = f""" -Analyze this user request and determine if it requires multiple file output or single file output. - -User request: "{self.services.ai.sanitizePromptContent(prompt, 'userinput')}" - -Respond with JSON only in this exact format: -{{ - "is_multi_file": true/false, - "strategy": "single|per_entity|by_section|by_criteria|custom", - "criteria": "description of how to split content", - "file_naming_pattern": "suggested pattern for filenames", - "reasoning": "brief explanation of the analysis" -}} - -Consider: -- Does the user want separate files for different entities (customers, products, etc.)? -- Does the user want to split content into multiple documents? -- What would be the most logical way to organize the content? -- What language is the request in? (analyze in the original language) - -Return only the JSON response. -""" - - from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum - request_options = AiCallOptions() - request_options.operationType = OperationTypeEnum.DATA_GENERATE - - request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options) - response = await ai_service.aiObjects.call(request) - - if response and response.content: - # Extract JSON from response - result = response.content.strip() - json_match = re.search(r'\{.*\}', result, re.DOTALL) - if json_match: - result = json_match.group(0) - - analysis = json.loads(result) - return analysis - else: - return {"is_multi_file": False, "strategy": "single", "criteria": None} - - except Exception as e: - logger.warning(f"AI prompt analysis failed: {str(e)}, defaulting to single file") - return {"is_multi_file": False, "strategy": "single", "criteria": None} async def _postRawDataChatMessage(self, payload: Any, label: str = "raw_extraction") -> None: """ diff --git a/modules/services/serviceAi/subDocumentProcessing.py b/modules/services/serviceAi/subDocumentProcessing.py index 5664f218..75eb5841 100644 --- a/modules/services/serviceAi/subDocumentProcessing.py +++ b/modules/services/serviceAi/subDocumentProcessing.py @@ -289,6 +289,11 @@ class SubDocumentProcessing: def _buildContinuationPrompt(self, base_prompt: str) -> str: """ Build a prompt that includes partial results continuation instructions. + + NOTE: This uses a different continuation pattern than SubCoreAi: + - SubCoreAi uses "continuation": null/dict for generic JSON responses + - This uses "continue": true/false + "continuation_context" for document sections + - Kept separate because it's tightly coupled to document processing needs """ continuation_instructions = """ diff --git a/modules/services/serviceAi/subSharedAiUtils.py b/modules/services/serviceAi/subSharedAiUtils.py index a4b825e9..1dcf6c41 100644 --- a/modules/services/serviceAi/subSharedAiUtils.py +++ b/modules/services/serviceAi/subSharedAiUtils.py @@ -29,9 +29,11 @@ def buildPromptWithPlaceholders(prompt: str, placeholders: Optional[Dict[str, st full_prompt = prompt for placeholder, content in placeholders.items(): - # Replace both old format {{placeholder}} and new format {{KEY:placeholder}} - full_prompt = full_prompt.replace(f"{{{{{placeholder}}}}}", content) - full_prompt = full_prompt.replace(f"{{{{KEY:{placeholder}}}}}", content) + # Skip if content is None or empty + if content is None: + continue + # Replace {{KEY:placeholder}} + full_prompt = full_prompt.replace(f"{{{{KEY:{placeholder}}}}}", str(content)) return full_prompt diff --git a/modules/services/serviceExtraction/subPromptBuilderExtraction.py b/modules/services/serviceExtraction/subPromptBuilderExtraction.py new file mode 100644 index 00000000..5b887482 --- /dev/null +++ b/modules/services/serviceExtraction/subPromptBuilderExtraction.py @@ -0,0 +1,219 @@ +""" +Prompt builder for document extraction. +This module builds prompts for extracting content from documents. +""" + +import json +import logging +from typing import Dict, Any, Optional +from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum + +# Type hint for renderer parameter +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from modules.services.serviceGeneration.renderers.rendererBaseTemplate import BaseRenderer + _RendererLike = BaseRenderer +else: + _RendererLike = Any + +logger = logging.getLogger(__name__) + + +async def buildExtractionPrompt( + outputFormat: str, + userPrompt: str, + title: str, + aiService=None, + services=None, + renderer: _RendererLike = None +) -> str: + """ + Build unified extraction prompt for extracting content from documents. + Always uses multi-file format (single doc = multi with n=1). + + Args: + outputFormat: Target output format + userPrompt: User's prompt describing what to extract + title: Document title + aiService: Optional AI service for intent parsing + services: Services instance + renderer: Optional renderer for format-specific guidelines + + Returns: + Complete extraction prompt string + """ + + # Unified multi-file example (single doc = multi with n=1) + json_example = { + "metadata": { + "title": "Multi-Document Example", + "split_strategy": "by_section", + "source_documents": ["doc_001"], + "extraction_method": "ai_extraction" + }, + "documents": [ + { + "id": "doc_section_1", + "title": "Section 1 Title", + "filename": "section_1.xlsx", + "sections": [ + { + "id": "section_1", + "content_type": "heading", + "elements": [ + { + "level": 1, + "text": "1. SECTION TITLE" + } + ], + "order": 1 + }, + { + "id": "section_2", + "content_type": "paragraph", + "elements": [ + { + "text": "This is the actual content that should be extracted from the document." + } + ], + "order": 2 + }, + { + "id": "section_3", + "content_type": "table", + "elements": [ + { + "headers": ["Column 1", "Column 2"], + "rows": [["Value 1", "Value 2"]] + } + ], + "order": 3 + } + ] + } + ] + } + + structure_instruction = "CRITICAL: You MUST return a JSON structure with a \"documents\" array. For single documents, create one document entry with all sections." + + # Parse extraction intent if AI service is available + extraction_intent = await _parseExtractionIntent(userPrompt, outputFormat, aiService, services) if aiService else userPrompt + + # Build base prompt + adaptive_prompt = f""" +{services.ai.sanitizePromptContent(userPrompt, 'userinput') if services else userPrompt} + +You are a document processing assistant that extracts and structures content from documents. Your task is to analyze the provided document content and create a structured JSON output. + +TASK: Extract the actual content from the document and organize it into documents. For single documents, create one document entry. For multi-document requests, create multiple document entries. + +{extraction_intent} + +REQUIREMENTS: +1. Analyze the document content provided in the context below +2. Identify distinct sections in the document (by headings, topics, or logical breaks) +3. Create one or more JSON document entries based on the content structure +4. Extract the real content from each section (headings, paragraphs, lists, etc.) +5. Generate appropriate filenames for each document + +{structure_instruction} + +OUTPUT FORMAT: Return only valid JSON in this exact structure: +{json.dumps(json_example, indent=2)} + +Requirements: +- Preserve all original data - do not summarize or interpret +- Use the exact JSON format shown above +- Maintain data integrity and structure + +Content Types to Extract: +1. Tables: Extract all rows and columns with proper headers +2. Lists: Extract all items with proper nesting +3. Headings: Extract with appropriate levels +4. Paragraphs: Extract as structured text +5. Code: Extract code blocks with language identification +6. Images: Analyze images and describe all visible content including text, tables, logos, graphics, layout, and visual elements + +Image Analysis Requirements: +- If you cannot analyze an image for any reason, explain why in the JSON response +- Describe everything you see in the image +- Include all text content, tables, logos, graphics, layout, and visual elements +- If the image is too small, corrupted, or unclear, explain this +- Always provide feedback - never return empty responses + +Return only the JSON structure with actual data from the documents. Do not include any text before or after the JSON. + +Extract the ACTUAL CONTENT from the source documents. Do not use placeholder text like "Section 1", "Section 2", etc. Extract the real headings, paragraphs, and content from the documents. +""".strip() + + # Add renderer-specific guidelines if provided + if renderer: + try: + if hasattr(renderer, 'getExtractionGuidelines'): + formatGuidelines = renderer.getExtractionGuidelines() + adaptive_prompt = f"{adaptive_prompt}\n\n{formatGuidelines}".strip() + except Exception: + pass + + # Save extraction prompt to debug file - only if debug enabled + if services: + try: + debug_enabled = services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False) + if debug_enabled: + import os + from datetime import datetime, UTC + ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") + from modules.shared.configuration import APP_CONFIG + logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./") + if not os.path.isabs(logDir): + gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + logDir = os.path.join(gatewayDir, logDir) + debug_root = os.path.join(logDir, 'debug') + os.makedirs(debug_root, exist_ok=True) + with open(os.path.join(debug_root, f"{ts}_extraction_prompt.txt"), "w", encoding="utf-8") as f: + f.write(adaptive_prompt) + except Exception: + pass + + return adaptive_prompt + + + + + +async def _parseExtractionIntent(userPrompt: str, outputFormat: str, aiService=None, services=None) -> str: + """ + Parse user prompt to extract the core extraction intent. + """ + if not aiService: + return f"Extract content from the provided documents and create a {outputFormat} report." + + try: + analysis_prompt = f""" +Analyze this user request and extract the core extraction intent: + +User request: "{userPrompt}" +Target format: {outputFormat} + +Extract the main intent and requirements for document processing. Focus on: +1. What content needs to be extracted +2. How it should be organized +3. Any specific requirements or preferences + +Respond with a clear, concise statement of the extraction intent. +""" + request_options = AiCallOptions() + request_options.operationType = OperationTypeEnum.DATA_GENERATE + + request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options) + response = await aiService.aiObjects.call(request) + + if response and response.content: + return response.content.strip() + else: + return f"Extract content from the provided documents and create a {outputFormat} report." + + except Exception as e: + services.utils.debugLogToFile(f"Extraction intent analysis failed: {str(e)}", "PROMPT_BUILDER") + return f"Extract content from the provided documents and create a {outputFormat} report." + diff --git a/modules/services/serviceGeneration/mainServiceGeneration.py b/modules/services/serviceGeneration/mainServiceGeneration.py index c84db686..321856fb 100644 --- a/modules/services/serviceGeneration/mainServiceGeneration.py +++ b/modules/services/serviceGeneration/mainServiceGeneration.py @@ -299,6 +299,7 @@ class GenerationService: async def renderReport(self, extractedContent: Dict[str, Any], outputFormat: str, title: str, userPrompt: str = None, aiService=None) -> tuple[str, str]: """ Render extracted JSON content to the specified output format. + Always uses unified "documents" array format. Args: extractedContent: Structured JSON document from AI extraction @@ -315,31 +316,25 @@ class GenerationService: if not isinstance(extractedContent, dict): raise ValueError("extractedContent must be a JSON dictionary") - # Check if this is a multi-document structure - if "documents" in extractedContent and len(extractedContent["documents"]) > 1: - # Multiple documents - use multi-file renderer - generated_documents = await self._renderMultiFileReport(extractedContent, outputFormat, title, userPrompt, aiService) - # For multi-document, return the first document's content and mime type - if generated_documents: - return generated_documents[0]["content"], generated_documents[0]["mime_type"] - else: - raise ValueError("No documents could be rendered") - elif "documents" in extractedContent and len(extractedContent["documents"]) == 1: - # Single document in documents array - extract sections - single_doc = extractedContent["documents"][0] - if "sections" not in single_doc: - raise ValueError("Document must contain 'sections' field") - # Create content for single document renderer - contentToRender = { - "sections": single_doc["sections"], - "metadata": extractedContent.get("metadata", {}), - "continuation": extractedContent.get("continuation", None) - } - elif "sections" in extractedContent: - # Direct sections format - contentToRender = extractedContent - else: - raise ValueError("extractedContent must contain 'sections' field or 'documents' array") + # Unified approach: Always expect "documents" array (single doc = n=1) + if "documents" not in extractedContent: + raise ValueError("extractedContent must contain 'documents' array") + + documents = extractedContent["documents"] + if len(documents) == 0: + raise ValueError("No documents found in 'documents' array") + + # Use first document for rendering + single_doc = documents[0] + if "sections" not in single_doc: + raise ValueError("Document must contain 'sections' field") + + # Create content for single document renderer + contentToRender = { + "sections": single_doc["sections"], + "metadata": extractedContent.get("metadata", {}), + "continuation": extractedContent.get("continuation", None) + } # Get the appropriate renderer for the format renderer = self._getFormatRenderer(outputFormat) @@ -362,171 +357,18 @@ class GenerationService: outputFormat: str, userPrompt: str, title: str, - promptAnalysis: Dict[str, Any], aiService=None ) -> str: - """Get adaptive extraction prompt based on AI analysis.""" - from .subPromptBuilder import buildAdaptiveExtractionPrompt - return await buildAdaptiveExtractionPrompt( + """Get adaptive extraction prompt.""" + from modules.services.serviceExtraction.subPromptBuilderExtraction import buildExtractionPrompt + return await buildExtractionPrompt( outputFormat=outputFormat, userPrompt=userPrompt, title=title, - promptAnalysis=promptAnalysis, aiService=aiService, services=self.services ) - - async def getGenerationPrompt( - self, - outputFormat: str, - userPrompt: str, - title: str - ) -> str: - """Get generation prompt for enhancing extracted JSON content.""" - from .subPromptBuilder import buildGenerationPrompt - return await buildGenerationPrompt( - outputFormat=outputFormat, - userPrompt=userPrompt, - title=title - ) - - async def renderAdaptiveReport( - self, - extractedContent: Dict[str, Any], - outputFormat: str, - title: str, - userPrompt: str = None, - aiService=None, - isMultiFile: bool = False - ) -> Union[Tuple[str, str], List[Dict[str, Any]]]: - """Render report adaptively based on content structure.""" - - # Start timing for generation - startTime = time.time() - - try: - if isMultiFile and "documents" in extractedContent: - result = await self._renderMultiFileReport( - extractedContent, outputFormat, title, userPrompt, aiService - ) - else: - result = await self._renderSingleFileReport( - extractedContent, outputFormat, title, userPrompt, aiService - ) - - # Calculate timing and emit stats - endTime = time.time() - processingTime = endTime - startTime - - # Calculate bytes (rough estimation) - if isinstance(result, tuple): - content, mime_type = result - bytesReceived = len(content.encode('utf-8')) if isinstance(content, str) else len(content) - elif isinstance(result, list): - bytesReceived = sum(len(str(doc).encode('utf-8')) for doc in result) - else: - bytesReceived = len(str(result).encode('utf-8')) - - # Use internal generation model for pricing - modelName = "internal_generation" - model = modelRegistry.getModel(modelName) - priceUsd = model.calculatePriceUsd(processingTime, 0, bytesReceived) - - aiResponse = AiCallResponse( - content="", # No content for generation stats needed - modelName=modelName, - priceUsd=priceUsd, - processingTime=processingTime, - bytesSent=0, # Input is already processed - bytesReceived=bytesReceived, - errorCount=0 - ) - - self.services.workflow.storeWorkflowStat( - self.services.currentWorkflow, - aiResponse, - f"generation.render.{outputFormat}" - ) - - return result - - except Exception as e: - # Calculate timing for error case - endTime = time.time() - processingTime = endTime - startTime - - # Use internal generation model for pricing - modelName = "internal_generation" - model = modelRegistry.getModel(modelName) - priceUsd = model.calculatePriceUsd(processingTime, 0, 0) - - aiResponse = AiCallResponse( - content="", # No content for generation stats needed - modelName=modelName, - priceUsd=priceUsd, - processingTime=processingTime, - bytesSent=0, - bytesReceived=0, - errorCount=1 - ) - - self.services.workflow.storeWorkflowStat( - self.services.currentWorkflow, - aiResponse, - f"generation.render.{outputFormat}" - ) - - raise - - async def _renderMultiFileReport( - self, - extractedContent: Dict[str, Any], - outputFormat: str, - title: str, - userPrompt: str = None, - aiService=None - ) -> List[Dict[str, Any]]: - """Render multiple documents from extracted content.""" - - generated_documents = [] - - for doc_data in extractedContent.get("documents", []): - # Use existing single-file renderer for each document - renderer = self._getFormatRenderer(outputFormat) - if not renderer: - continue - - # Render individual document - rendered_content, mime_type = await renderer.render( - extractedContent={"sections": doc_data["sections"]}, - title=doc_data["title"], - userPrompt=userPrompt, - aiService=aiService - ) - - generated_documents.append({ - "filename": doc_data["filename"], - "content": rendered_content, - "mime_type": mime_type, - "title": doc_data["title"] - }) - - return generated_documents - - async def _renderSingleFileReport( - self, - extractedContent: Dict[str, Any], - outputFormat: str, - title: str, - userPrompt: str = None, - aiService=None - ) -> Tuple[str, str]: - """Render single file report (existing functionality).""" - # Use existing renderReport method - return await self.renderReport( - extractedContent, outputFormat, title, userPrompt, aiService - ) def _getFormatRenderer(self, output_format: str): """Get the appropriate renderer for the specified format using auto-discovery.""" diff --git a/modules/services/serviceGeneration/subJsonSchema.py b/modules/services/serviceGeneration/subJsonSchema.py index 868a6ca4..72c722b1 100644 --- a/modules/services/serviceGeneration/subJsonSchema.py +++ b/modules/services/serviceGeneration/subJsonSchema.py @@ -14,10 +14,10 @@ def get_multi_document_subJsonSchema() -> Dict[str, Any]: "properties": { "metadata": { "type": "object", - "required": ["title", "splitStrategy"], + "required": ["title", "split_strategy"], "properties": { "title": {"type": "string", "description": "Document title"}, - "splitStrategy": { + "split_strategy": { "type": "string", "enum": ["per_entity", "by_section", "by_criteria", "by_data_type", "custom"], "description": "Strategy for splitting content into multiple files" @@ -437,7 +437,7 @@ def validate_json_document(json_data: Dict[str, Any]) -> bool: return False metadata = json_data["metadata"] - if not isinstance(metadata, dict) or "title" not in metadata or "splitStrategy" not in metadata: + if not isinstance(metadata, dict) or "title" not in metadata or "split_strategy" not in metadata: return False documents = json_data["documents"] diff --git a/modules/services/serviceGeneration/subPromptBuilder.py b/modules/services/serviceGeneration/subPromptBuilder.py deleted file mode 100644 index 32c8ca73..00000000 --- a/modules/services/serviceGeneration/subPromptBuilder.py +++ /dev/null @@ -1,397 +0,0 @@ -""" -Prompt builder for AI document generation and extraction. -This module builds prompts for AI services to extract and generate documents. -""" - -import json -import logging -from typing import Dict, Any, Optional, List, TYPE_CHECKING -from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum - -# Type hint for renderer parameter -if TYPE_CHECKING: - from .renderers.rendererBaseTemplate import BaseRenderer - _RendererLike = BaseRenderer -else: - _RendererLike = Any - -logger = logging.getLogger(__name__) - -# Centralized JSON structure template for document generation -JSON_STRUCTURE_TEMPLATE = """{ - "metadata": { - "title": "{{DOCUMENT_TITLE}}", - "splitStrategy": "single_document", - "source_documents": [], - "extraction_method": "ai_generation" - }, - "documents": [{ - "id": "doc_1", - "title": "{{DOCUMENT_TITLE}}", - "filename": "document.json", - "sections": [ - { - "id": "section_1", - "content_type": "heading|paragraph|table|list|code", - "elements": [ - // heading: {"level": 1, "text": "..."} - // paragraph: {"text": "..."} - // table: {"headers": [...], "rows": [[...]], "caption": "..."} - // list: {"items": [{"text": "...", "subitems": [...]}], "list_type": "bullet|numbered"} - // code: {"code": "...", "language": "..."} - ], - "order": 1 - } - ] - }], - "continuation": null, -}""" - -async def buildAdaptiveExtractionPrompt( - outputFormat: str, - userPrompt: str, - title: str, - promptAnalysis: Dict[str, Any], - aiService=None, - services=None -) -> str: - """ - Build adaptive extraction prompt based on AI analysis. - Uses multi-file or single-file approach based on analysis. - """ - - # Multi-file example data instead of schema - multi_file_example = { - "metadata": { - "title": "Multi-Document Example", - "splitStrategy": "by_section", - "source_documents": ["doc_001"], - "extraction_method": "ai_extraction" - }, - "documents": [ - { - "id": "doc_section_1", - "title": "Section 1 Title", - "filename": "section_1.xlsx", - "sections": [ - { - "id": "section_1", - "content_type": "heading", - "elements": [ - { - "level": 1, - "text": "1. SECTION TITLE" - } - ], - "order": 1 - }, - { - "id": "section_2", - "content_type": "paragraph", - "elements": [ - { - "text": "This is the actual content that should be extracted from the document." - } - ], - "order": 2 - }, - { - "id": "section_3", - "content_type": "table", - "elements": [ - { - "headers": ["Column 1", "Column 2"], - "rows": [["Value 1", "Value 2"]] - } - ], - "order": 3 - } - ] - } - ] - } - - # UNIFIED APPROACH: Always use multi-document format (single doc = multi with n=1) - adaptive_prompt = f""" -{services.ai.sanitizePromptContent(userPrompt, 'userinput')} - -You are a document processing assistant that extracts and structures content from documents. Your task is to analyze the provided document content and create a structured JSON output. - -TASK: Extract the actual content from the document and organize it into documents. For single documents, create one document entry. For multi-document requests, create multiple document entries. - -REQUIREMENTS: -1. Analyze the document content provided in the context below -2. Identify distinct sections in the document (by headings, topics, or logical breaks) -3. Create one or more JSON document entries based on the content structure -4. Extract the real content from each section (headings, paragraphs, lists, etc.) -5. Generate appropriate filenames for each document - -CRITICAL: You MUST return a JSON structure with a "documents" array, NOT a "sections" array. - -OUTPUT FORMAT: Return only valid JSON in this exact structure: -{json.dumps(multi_file_example, indent=2)} - -IMPORTANT: The JSON must have a "documents" key containing an array of document objects. Each document object must have: -- "id": unique identifier -- "title": document title -- "filename": appropriate filename for the document -- "sections": array of content sections - -DO NOT return a JSON with "sections" at the root level. Return a JSON with "documents" at the root level. - -INSTRUCTIONS: -- For single document requests: Create one document with all content in its sections -- For multi-document requests: Create multiple documents, each with relevant sections -- Use actual section titles, headings, and text from the document -- Create meaningful filenames based on content -- Ensure each section contains the complete content for that part -- Do not use generic placeholder text like "Section 1", "Section 2" -- Extract real headings, paragraphs, lists, and other content elements -- CRITICAL: Return JSON with "documents" array, not "sections" array - -CONTEXT (Document Content): - -Content Types to Extract: -1. Tables: Extract all rows and columns with proper headers -2. Lists: Extract all items with proper nesting -3. Headings: Extract with appropriate levels -4. Paragraphs: Extract as structured text -5. Code: Extract code blocks with language identification -6. Images: Analyze images and describe all visible content including text, tables, logos, graphics, layout, and visual elements - -Image Analysis Requirements: -- If you cannot analyze an image for any reason, explain why in the JSON response -- Describe everything you see in the image -- Include all text content, tables, logos, graphics, layout, and visual elements -- If the image is too small, corrupted, or unclear, explain this -- Always provide feedback - never return empty responses - -Return only the JSON structure with actual data from the documents. Do not include any text before or after the JSON. - -Extract the ACTUAL CONTENT from the source documents. Do not use placeholder text like "Section 1", "Section 2", etc. Extract the real headings, paragraphs, and content from the documents. -""".strip() - - return adaptive_prompt - -async def buildGenerationPrompt( - outputFormat: str, - userPrompt: str, - title: str -) -> str: - """Build the unified generation prompt using a single JSON template.""" - # Create a template with the actual title - json_template = JSON_STRUCTURE_TEMPLATE.replace("{{DOCUMENT_TITLE}}", title) - - # Always use the proper generation prompt template with LOOP_INSTRUCTION - result = f"""Generate structured JSON content for document creation. - -USER CONTEXT: "{userPrompt}" -DOCUMENT TITLE: "{title}" -TARGET FORMAT: {outputFormat} - -LOOP_INSTRUCTION - -RULES: -- Follow the template structure below exactly; emit only one JSON object in the response -- Fill sections with content based on the user request -- Use appropriate content_type - -Return ONLY valid JSON matching this structure (template below). Do not include any prose before/after. Use this as the single template reference for your output: -{json_template} -""" - - return result.strip() - -async def buildExtractionPrompt( - outputFormat: str, - renderer: _RendererLike, - userPrompt: str, - title: str, - aiService=None, - services=None -) -> str: - """ - Build the final extraction prompt by combining: - - Parsed extraction intent from user prompt (using AI) - - Generic cross-format instructions (filename header + real-data policy) - - Format-specific guidelines snippet provided by the renderer - - The AI must place a single filename header at the very top: - FILENAME: - followed by a blank line and then ONLY the document content according to the target format. - """ - - # Parse user prompt to separate extraction intent from generation format using AI - extractionIntent = await _parseExtractionIntent(userPrompt, outputFormat, aiService, services) - - # Import JSON schema for structured output - from .subJsonSchema import get_document_subJsonSchema - jsonSchema = get_document_subJsonSchema() - - # Generic block for JSON extraction - use mixed example data showing different content types - example_data = { - "metadata": { - "title": "Example Document", - "author": "AI Assistant", - "source_documents": ["document_001"], - "extraction_method": "ai_extraction" - }, - "sections": [ - { - "id": "section_001", - "content_type": "heading", - "elements": [ - { - "level": 1, - "text": "1. INTRODUCTION" - } - ], - "order": 1, - "metadata": {} - }, - { - "id": "section_002", - "content_type": "paragraph", - "elements": [ - { - "text": "This is a sample paragraph with actual content that should be extracted from the document." - } - ], - "order": 2, - "metadata": {} - }, - { - "id": "section_003", - "content_type": "table", - "elements": [ - { - "headers": ["Column 1", "Column 2", "Column 3"], - "rows": [ - ["Value 1", "Value 2", "Value 3"], - ["Value 4", "Value 5", "Value 6"] - ] - } - ], - "order": 3, - "metadata": {} - } - ], - "summary": "", - "tags": [] - } - - genericIntro = f""" -{extractionIntent} - -You are a document processing assistant that extracts and structures content from documents. Your task is to analyze the provided document content and create a structured JSON output. - -TASK: Extract the actual content from the document and organize it into structured sections. - -REQUIREMENTS: -1. Analyze the document content provided in the context below -2. Extract all content and organize it into logical sections -3. Create structured JSON with sections containing the extracted content -4. Preserve the original structure and data - -OUTPUT FORMAT: Return only valid JSON in this exact structure: -{json.dumps(example_data, indent=2)} - -Requirements: -- Preserve all original data - do not summarize or interpret -- Use the exact JSON format shown above -- Maintain data integrity and structure - -Content Types to Extract: -1. Tables: Extract all rows and columns with proper headers -2. Lists: Extract all items with proper nesting -3. Headings: Extract with appropriate levels -4. Paragraphs: Extract as structured text -5. Code: Extract code blocks with language identification -6. Images: Analyze images and describe all visible content including text, tables, logos, graphics, layout, and visual elements - -Image Analysis Requirements: -- If you cannot analyze an image for any reason, explain why in the JSON response -- Describe everything you see in the image -- Include all text content, tables, logos, graphics, layout, and visual elements -- If the image is too small, corrupted, or unclear, explain this -- Always provide feedback - never return empty responses - -Return only the JSON structure with actual data from the documents. Do not include any text before or after the JSON. - -Extract the ACTUAL CONTENT from the source documents. Do not use placeholder text like "Section 1", "Section 2", etc. Extract the real headings, paragraphs, and content from the documents. - -DO NOT return a schema description - return actual extracted content in the JSON format shown above. -""" - - # Get format-specific guidelines from renderer - formatGuidelines = "" - try: - if hasattr(renderer, 'getExtractionGuidelines'): - formatGuidelines = renderer.getExtractionGuidelines() - except Exception: - pass - - # Combine all parts - finalPrompt = f"{genericIntro}\n\n{formatGuidelines}".strip() - - # Save extraction prompt to debug file - only if debug enabled - try: - debug_enabled = services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False) - if debug_enabled: - import os - from datetime import datetime, UTC - ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") - # Use configured log directory instead of hardcoded test-chat - from modules.shared.configuration import APP_CONFIG - logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./") - if not os.path.isabs(logDir): - gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - logDir = os.path.join(gatewayDir, logDir) - debug_root = os.path.join(logDir, 'debug') - os.makedirs(debug_root, exist_ok=True) - with open(os.path.join(debug_root, f"{ts}_extraction_prompt.txt"), "w", encoding="utf-8") as f: - f.write(finalPrompt) - except Exception: - pass - - return finalPrompt - - - - -async def _parseExtractionIntent(userPrompt: str, outputFormat: str, aiService=None, services=None) -> str: - """ - Parse user prompt to extract the core extraction intent. - """ - if not aiService: - return f"Extract content from the provided documents and create a {outputFormat} report." - - try: - analysis_prompt = f""" -Analyze this user request and extract the core extraction intent: - -User request: "{userPrompt}" -Target format: {outputFormat} - -Extract the main intent and requirements for document processing. Focus on: -1. What content needs to be extracted -2. How it should be organized -3. Any specific requirements or preferences - -Respond with a clear, concise statement of the extraction intent. -""" - request_options = AiCallOptions() - request_options.operationType = OperationTypeEnum.DATA_GENERATE - - request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options) - response = await aiService.aiObjects.call(request) - - if response and response.content: - return response.content.strip() - else: - return f"Extract content from the provided documents and create a {outputFormat} report." - - except Exception as e: - services.utils.debugLogToFile(f"Extraction intent analysis failed: {str(e)}", "PROMPT_BUILDER") - return f"Extract content from the provided documents and create a {outputFormat} report." - diff --git a/modules/services/serviceGeneration/subPromptBuilderGeneration.py b/modules/services/serviceGeneration/subPromptBuilderGeneration.py new file mode 100644 index 00000000..ca0ea575 --- /dev/null +++ b/modules/services/serviceGeneration/subPromptBuilderGeneration.py @@ -0,0 +1,89 @@ +""" +Prompt builder for document generation. +This module builds prompts for generating documents from extracted content. +""" + +import logging + +logger = logging.getLogger(__name__) + +# Centralized JSON structure template for document generation +TEMPLATE_JSON_DOCUMENT_GENERATION = """{ + "metadata": { + "split_strategy": "single_document", + "source_documents": [], + "extraction_method": "ai_generation" + }, + "documents": [ + { + "id": "doc_1", + "title": "{{DOCUMENT_TITLE}}", + "filename": "document.json", + "sections": [ + { + "id": "section_1", + "content_type": "heading|paragraph|table|list|code", + "elements": [ + // heading: {"level": 1, "text": "..."} + // paragraph: {"text": "..."} + // table: {"headers": [...], "rows": [[...]], "caption": "..."} + // list: {"items": [{"text": "...", "subitems": [...]}], "list_type": "bullet|numbered"} + // code: {"code": "...", "language": "..."} + ], + "order": 1 + } + ] + } + ], + "continuation": null +}""" + + +async def buildGenerationPrompt( + outputFormat: str, + userPrompt: str, + title: str, + extracted_content: str = None +) -> str: + """ + Build the unified generation prompt using a single JSON template. + + Args: + outputFormat: Target output format (html, pdf, docx, etc.) + userPrompt: User's original prompt for document generation + title: Title for the document + extracted_content: Optional extracted content from documents to prepend to prompt + + Returns: + Complete generation prompt string + """ + # Create a template - let AI generate title if not provided + prompt_instruction = f"Use the following title: \"{title}\"" + json_template = TEMPLATE_JSON_DOCUMENT_GENERATION.replace("{{DOCUMENT_TITLE}}", title) + + # Always use the proper generation prompt template with LOOP_INSTRUCTION + generation_prompt = f"""Generate structured JSON content for document creation. + +USER CONTEXT: "{userPrompt}" +TARGET FORMAT: {outputFormat} +TITLE INSTRUCTION: {prompt_instruction} + +LOOP_INSTRUCTION + +RULES: +- Follow the template structure below exactly; emit only one JSON object in the response +- Fill sections with content based on the user request +- Use appropriate content_type + +{json_template} +""" + + # If we have extracted content, prepend it to the prompt + if extracted_content: + generation_prompt = f"""EXTRACTED CONTENT FROM DOCUMENTS: +{extracted_content} + +{generation_prompt}""" + + return generation_prompt.strip() + diff --git a/modules/services/serviceWorkflow/mainServiceWorkflow.py b/modules/services/serviceWorkflow/mainServiceWorkflow.py index 50d14611..4db3ce0f 100644 --- a/modules/services/serviceWorkflow/mainServiceWorkflow.py +++ b/modules/services/serviceWorkflow/mainServiceWorkflow.py @@ -20,61 +20,6 @@ class WorkflowService: self.interfaceDbApp = serviceCenter.interfaceDbApp self._progressLogger = None - async def summarizeChat(self, messages: List[ChatMessage]) -> str: - """ - Summarize chat messages from last to first message with status="first" - - Args: - messages: List of chat messages to summarize - - Returns: - str: Summary of the chat in user's language - """ - try: - # Get messages from last to first, stopping at first message with status="first" - relevantMessages = [] - for msg in reversed(messages): - relevantMessages.append(msg) - if msg.status == "first": - break - - # Create prompt for AI - prompt = f""" -You are an AI assistant providing a summary of a chat conversation. -Please respond in '{self.user.language}' language. - -Chat History: -{chr(10).join(f"- {msg.message}" for msg in reversed(relevantMessages))} - -Instructions: -1. Summarize the conversation's key points and outcomes -2. Be concise but informative -3. Use a professional but friendly tone -4. Focus on important decisions and next steps if any - -LOOP_INSTRUCTION - -Please provide a comprehensive summary of this conversation.""" - - # Get summary using AI service through proper main service interface - - return await self.services.ai.callAiDocuments( - prompt=prompt, - documents=None, - options=AiCallOptions( - operationType=OperationTypeEnum.DATA_GENERATE, - priority=PriorityEnum.SPEED, - processingMode=ProcessingModeEnum.BASIC, - compressPrompt=True, - compressContext=False, - maxCost=0.01 - ) - ) - - except Exception as e: - logger.error(f"Error summarizing chat: {str(e)}") - return f"Error summarizing chat: {str(e)}" - def getChatDocumentsFromDocumentList(self, documentList: List[str]) -> List[ChatDocument]: """Get ChatDocuments from a list of document references using all three formats.""" try: @@ -928,7 +873,9 @@ Please provide a comprehensive summary of this conversation.""" def _getProgressLogger(self): """Get or create the progress logger instance""" if self._progressLogger is None: - self._progressLogger = ProgressLogger(self, self.workflow) + # Use currentWorkflow from self.services instead of self.workflow (which is self) + workflow = getattr(self.services, 'currentWorkflow', None) + self._progressLogger = ProgressLogger(self, workflow) return self._progressLogger def createProgressLogger(self, workflow) -> ProgressLogger: diff --git a/modules/workflows/methods/methodAi.py b/modules/workflows/methods/methodAi.py index 178b6264..b656916b 100644 --- a/modules/workflows/methods/methodAi.py +++ b/modules/workflows/methods/methodAi.py @@ -42,15 +42,22 @@ class MethodAi(MethodBase): """ try: # Init progress logger - operationId = f"ai_process_{self.services.currentWorkflow.id}_{int(time.time())}" + workflowId = self.services.currentWorkflow.id if self.services.currentWorkflow else f"no-workflow-{int(time.time())}" + operationId = f"ai_process_{workflowId}_{int(time.time())}" # Start progress tracking - self.services.workflow.progressLogStart( - operationId, - "Generate", - "AI Processing", - f"Format: {parameters.get('resultType', 'txt')}" - ) + if hasattr(self.services, 'workflow') and self.services.workflow: # TODO: Entfernen für PROD! (block) + try: + self.services.workflow.progressLogStart( + operationId, + "Generate", + "AI Processing", + f"Format: {parameters.get('resultType', 'txt')}" + ) + except Exception as e: + # Silently skip progress tracking errors (e.g., in test environments) + logger.debug(f"Skipping progress logging: {str(e)}") + # Debug logging to see what parameters are received logger.info(f"MethodAi.process received parameters: {parameters}") diff --git a/test_ai_models.py b/test1_ai_models.py similarity index 100% rename from test_ai_models.py rename to test1_ai_models.py diff --git a/test_ai_model_selection.py b/test2_ai_model_selection.py similarity index 100% rename from test_ai_model_selection.py rename to test2_ai_model_selection.py diff --git a/test_ai_behavior.py b/test3_ai_behavior.py similarity index 100% rename from test_ai_behavior.py rename to test3_ai_behavior.py diff --git a/test4_method_ai_operations.py b/test4_method_ai_operations.py new file mode 100644 index 00000000..e0bd5861 --- /dev/null +++ b/test4_method_ai_operations.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +""" +Test script for methodAi operations. +Tests all OperationType's with various prompts through the workflow action interface. +""" + +import asyncio +import sys +import os +from datetime import datetime +from typing import Dict, Any, List + +# Add the gateway to path +sys.path.append(os.path.dirname(__file__)) + +from modules.datamodels.datamodelAi import OperationTypeEnum +from modules.datamodels.datamodelChat import ChatWorkflow, ChatDocument +from modules.datamodels.datamodelUam import User + + +class MethodAiOperationsTester: + """Test all operation types through methodAi.process() action.""" + + def __init__(self): + # Use root user for testing (has full access to everything) + from modules.interfaces.interfaceDbAppObjects import getRootInterface + rootInterface = getRootInterface() + self.testUser = rootInterface.currentUser + + self.services = None + self.methodAi = None + self.testResults = [] + + # Create logs directory if it doesn't exist + self.logsDir = os.path.join(os.path.dirname(__file__), "..", "local", "logs") + os.makedirs(self.logsDir, exist_ok=True) + + # Create modeltest subdirectory + self.modelTestDir = os.path.join(self.logsDir, "modeltest") + os.makedirs(self.modelTestDir, exist_ok=True) + + # Test prompts for each operation type + self.testPrompts = { + OperationTypeEnum.PLAN: { + "aiPrompt": "Create a 5-step plan to organize a project meeting and include the manual for the project management office.", + "resultType": "json" + }, + OperationTypeEnum.DATA_ANALYSE: { + "aiPrompt": "Analyze the following text and extract the main topics and key points: 'Machine learning is transforming healthcare by enabling early disease detection through pattern recognition in medical images.'", + "resultType": "json" + }, + OperationTypeEnum.DATA_GENERATE: { + "aiPrompt": "Generate the first 9000 prime numbers.", + "resultType": "txt" + }, + OperationTypeEnum.DATA_EXTRACT: { + "aiPrompt": "Extract all email addresses and phone numbers from the following text: 'Contact us at support@example.com or call 123-456-7890. For sales, email sales@example.com or call 987-654-3210.'", + "resultType": "json" + }, + OperationTypeEnum.IMAGE_ANALYSE: { + "aiPrompt": "Analyze this image and describe what you see, including any text or numbers visible.", + "resultType": "json", + "documentList": ["_testdata_photo_2025-06-03_13-05-52.jpg"] if os.path.exists(os.path.join(self.logsDir, "_testdata_photo_2025-06-03_13-05-52.jpg")) else [] + }, + OperationTypeEnum.IMAGE_GENERATE: { + "aiPrompt": "A beautiful sunset over the ocean with purple and orange hues", + "resultType": "png" + }, + OperationTypeEnum.WEB_SEARCH: { + "aiPrompt": "Find recent articles about ValueOn AG in Switzeerland in 2025", + "resultType": "json" + }, + OperationTypeEnum.WEB_CRAWL: { + "aiPrompt": "Extract who works in this company", + "resultType": "json", + "documentList": ["https://www.valueon.com"] + } + } + + async def initialize(self): + """Initialize services and methodAi.""" + print("🔧 Initializing services...") + + # Set logging level to DEBUG to see debug messages + import logging + logging.getLogger().setLevel(logging.DEBUG) + + # Import and initialize services - use the same approach as routeChatPlayground + import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects + interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser) + + # Import and initialize services + from modules.features.chatPlayground.mainChatPlayground import getServices + + # Get services first + self.services = getServices(self.testUser, None) + + # Now create AND SAVE workflow in database using the interface + import uuid + import time + currentTimestamp = time.time() + + testWorkflow = ChatWorkflow( + id=str(uuid.uuid4()), + name="Test Workflow", + status="running", + startedAt=currentTimestamp, + lastActivity=currentTimestamp, + currentRound=1, + currentTask=0, + currentAction=0, + totalTasks=0, + totalActions=0, + mandateId=self.testUser.mandateId, + messageIds=[], + workflowMode="React", + maxSteps=5 + ) + + # SAVE workflow to database so it exists for access control + # Convert ChatWorkflow to dict for createWorkflow + workflowDict = testWorkflow.model_dump() + interfaceDbChat.createWorkflow(workflowDict) + + # Set the workflow in services + self.services.currentWorkflow = testWorkflow + + # Debug: Print workflow status + print(f"Debug: services.currentWorkflow is set: {hasattr(self.services, 'currentWorkflow') and self.services.currentWorkflow is not None}") + if self.services.currentWorkflow: + print(f"Debug: Workflow ID: {self.services.currentWorkflow.id}") + + # Import and initialize methodAi AFTER setting workflow + from modules.workflows.methods.methodAi import MethodAi + self.methodAi = MethodAi(self.services) + + # Verify methodAi has access to the workflow + if hasattr(self.methodAi, 'services'): + print(f"Debug: methodAi.services.currentWorkflow is set: {hasattr(self.methodAi.services, 'currentWorkflow') and self.methodAi.services.currentWorkflow is not None}") + + print("✅ Services initialized") + print(f"📁 Results will be saved to: {self.modelTestDir}") + + async def testOperation(self, operationType: OperationTypeEnum) -> Dict[str, Any]: + """Test a specific operation type.""" + print(f"\n{'='*80}") + print(f"TESTING OPERATION: {operationType.value}") + print(f"{'='*80}") + + startTime = asyncio.get_event_loop().time() + + # Get test prompt for this operation + testConfig = self.testPrompts.get(operationType, {}) + + if not testConfig: + result = { + "operationType": operationType.value, + "status": "ERROR", + "error": "No test configuration found for this operation type", + "processingTime": 0.0 + } + self.testResults.append(result) + return result + + print(f"Prompt: {testConfig.get('aiPrompt', 'N/A')}") + print(f"Result Type: {testConfig.get('resultType', 'txt')}") + + try: + # Prepare parameters + parameters = { + "aiPrompt": testConfig.get("aiPrompt"), + "resultType": testConfig.get("resultType", "txt") + } + + # Add document list if provided + if "documentList" in testConfig and testConfig["documentList"]: + parameters["documentList"] = testConfig["documentList"] + + # Ensure workflow is still set in both self.services AND methodAi.services + if not self.services.currentWorkflow or (hasattr(self, 'methodAi') and hasattr(self.methodAi, 'services') and not self.methodAi.services.currentWorkflow): + print(f"⚠️ Warning: Workflow is None, trying to re-set it...") + import time + import uuid + currentTimestamp = time.time() + testWorkflow = ChatWorkflow( + id=str(uuid.uuid4()), + name="Test Workflow", + status="running", + startedAt=currentTimestamp, + lastActivity=currentTimestamp, + currentRound=1, + currentTask=0, + currentAction=0, + totalTasks=0, + totalActions=0, + mandateId="test_mandate", + messageIds=[], + workflowMode="React", + maxSteps=5 + ) + self.services.currentWorkflow = testWorkflow + # Also set in methodAi.services if it exists + if hasattr(self, 'methodAi') and hasattr(self.methodAi, 'services'): + self.methodAi.services.currentWorkflow = testWorkflow + + # Call methodAi.process() + print(f"Calling methodAi.process()...") + print(f"Debug: Current workflow ID before call: {self.services.currentWorkflow.id if self.services.currentWorkflow else 'None'}") + print(f"Debug: methodAi.services.currentWorkflow: {self.methodAi.services.currentWorkflow.id if hasattr(self.methodAi, 'services') and self.methodAi.services.currentWorkflow else 'None/NotSet'}") + print(f"Debug: Is same services object? {self.services is self.methodAi.services}") + print(f"Debug: services id: {id(self.services)}") + print(f"Debug: methodAi.services id: {id(self.methodAi.services)}") + + # Final safety check: ensure methodAi.services has the workflow + if hasattr(self.methodAi, 'services') and not self.methodAi.services.currentWorkflow: + print(f"⚠️ Fixing: Setting workflow in methodAi.services...") + self.methodAi.services.currentWorkflow = self.services.currentWorkflow + + actionResult = await self.methodAi.process(parameters) + + endTime = asyncio.get_event_loop().time() + processingTime = endTime - startTime + + # Analyze result + result = { + "operationType": operationType.value, + "status": "SUCCESS" if actionResult.success else "ERROR", + "processingTime": round(processingTime, 2), + "hasDocuments": len(actionResult.documents) > 0 if actionResult.documents else False, + "documentCount": len(actionResult.documents) if actionResult.documents else 0, + "error": actionResult.error if not actionResult.success else None + } + + # Extract document information + if actionResult.documents: + doc = actionResult.documents[0] + result["documentName"] = doc.documentName + result["mimeType"] = doc.mimeType + result["dataSize"] = len(doc.documentData) if doc.documentData else 0 + result["dataPreview"] = str(doc.documentData)[:200] + "..." if len(str(doc.documentData)) > 200 else str(doc.documentData) + + print(f"✅ Status: {result['status']}") + print(f"⏱️ Processing time: {result['processingTime']}s") + print(f"📄 Documents: {result.get('documentCount', 0)}") + + if actionResult.success: + if result.get('documentName'): + print(f"📄 Saved: {result['documentName']}") + print(f"📄 MIME type: {result.get('mimeType')}") + print(f"📄 Size: {result.get('dataSize')} bytes") + + # Try to decode if it's JSON + if result.get('mimeType') == 'application/json': + try: + import json + jsonData = json.loads(actionResult.documents[0].documentData) + result["isValidJson"] = True + result["jsonKeys"] = list(jsonData.keys()) if isinstance(jsonData, dict) else "Not a dict" + print(f"✅ Valid JSON with keys: {result['jsonKeys']}") + except: + result["isValidJson"] = False + print(f"⚠️ Not valid JSON") + else: + print(f"❌ Error: {result.get('error')}") + + self.testResults.append(result) + return result + + except Exception as e: + endTime = asyncio.get_event_loop().time() + processingTime = endTime - startTime + + result = { + "operationType": operationType.value, + "status": "EXCEPTION", + "processingTime": round(processingTime, 2), + "error": str(e), + "hasDocuments": False + } + + print(f"💥 EXCEPTION: {str(e)}") + self.testResults.append(result) + return result + + async def testAllOperations(self): + """Test all operation types.""" + print(f"\n{'='*80}") + print("STARTING METHODAI OPERATIONS TESTS - DATA_GENERATE ONLY") + print(f"{'='*80}") + print("Testing DATA_GENERATE operation type...") + + # Test only DATA_GENERATE + await self.testOperation(OperationTypeEnum.DATA_GENERATE) + print(f"\n{'─'*80}") + + # Print summary + self.printSummary() + + def printSummary(self): + """Print test summary.""" + print(f"\n{'='*80}") + print("TEST SUMMARY") + print(f"{'='*80}") + + successfulTests = [r for r in self.testResults if r["status"] == "SUCCESS"] + failedTests = [r for r in self.testResults if r["status"] == "ERROR"] + exceptionTests = [r for r in self.testResults if r["status"] == "EXCEPTION"] + + print(f"\nTotal tests: {len(self.testResults)}") + print(f"✅ Successful: {len(successfulTests)}") + print(f"❌ Failed: {len(failedTests)}") + print(f"💥 Exceptions: {len(exceptionTests)}") + + if successfulTests: + print(f"\n{'─'*80}") + print("SUCCESSFUL TESTS") + print(f"{'─'*80}") + for result in successfulTests: + print(f"✅ {result['operationType']}: {result['processingTime']}s") + + if failedTests: + print(f"\n{'─'*80}") + print("FAILED TESTS") + print(f"{'─'*80}") + for result in failedTests: + print(f"❌ {result['operationType']}: {result.get('error', 'Unknown error')}") + + if exceptionTests: + print(f"\n{'─'*80}") + print("EXCEPTIONS") + print(f"{'─'*80}") + for result in exceptionTests: + print(f"💥 {result['operationType']}: {result.get('error', 'Unknown error')}") + + # Save results + import json + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + resultsFile = os.path.join(self.modelTestDir, f"method_ai_operations_test_{timestamp}.json") + + with open(resultsFile, 'w', encoding='utf-8') as f: + json.dump({ + "timestamp": timestamp, + "summary": { + "total": len(self.testResults), + "successful": len(successfulTests), + "failed": len(failedTests), + "exceptions": len(exceptionTests) + }, + "results": self.testResults + }, f, indent=2, ensure_ascii=False) + + print(f"\n📄 Results saved to: {resultsFile}") + + +async def main(): + """Run methodAI operations tests.""" + tester = MethodAiOperationsTester() + + await tester.initialize() + await tester.testAllOperations() + + print(f"\n{'='*80}") + print("TESTING COMPLETED") + print(f"{'='*80}") + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/test_operation_type_ratings.py b/test_operation_type_ratings.py deleted file mode 100644 index e39f4486..00000000 --- a/test_operation_type_ratings.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to demonstrate the new operation type rating system. -This shows how models are now sorted by their capability ratings for specific operation types. -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from modules.datamodels.datamodelAi import OperationTypeEnum, createOperationTypeRatings, AiCallOptions, PriorityEnum, ProcessingModeEnum -from modules.aicore.aicorePluginPerplexity import AiPerplexity -from modules.aicore.aicorePluginTavily import ConnectorWeb -from modules.aicore.aicorePluginAnthropic import AiAnthropic -from modules.aicore.aicorePluginOpenai import AiOpenai -from modules.aicore.aicorePluginInternal import AiInternal -from modules.aicore.aicoreModelSelector import ModelSelector - -def testOperationTypeRatings(): - """Test the new operation type rating system.""" - print("🧪 Testing Operation Type Rating System") - print("=" * 50) - - # Initialize connectors - perplexity = AiPerplexity() - tavily = ConnectorWeb() - anthropic = AiAnthropic() - openai = AiOpenai() - internal = AiInternal() - modelSelector = ModelSelector() - - # Get all models - allModels = (perplexity.getModels() + tavily.getModels() + - anthropic.getModels() + openai.getModels() + internal.getModels()) - - print(f"📊 Total models available: {len(allModels)}") - print() - - # Test different operation types - testCases = [ - (OperationTypeEnum.WEB_RESEARCH, "Web Research"), - (OperationTypeEnum.WEB_NEWS, "Web News"), - (OperationTypeEnum.WEB_QUESTIONS, "Web Questions"), - (OperationTypeEnum.WEB_SEARCH, "Web Search"), - (OperationTypeEnum.DATA_ANALYSE, "Data Analysis tasks"), - (OperationTypeEnum.DATA_GENERATE, "Data Generation tasks"), - (OperationTypeEnum.DATA_EXTRACT, "Data Extraction tasks"), - (OperationTypeEnum.PLAN, "Planning tasks") - ] - - for operationType, description in testCases: - print(f"🎯 Testing: {description} ({operationType.value})") - print("-" * 40) - - # Create AI call options - options = AiCallOptions( - operationType=operationType, - priority=PriorityEnum.BALANCED, - processingMode=ProcessingModeEnum.BASIC - ) - - # Get failover model list (sorted by rating) - failoverModels = modelSelector.getFailoverModelList( - prompt="Test prompt", - context="Test context", - options=options, - availableModels=allModels - ) - - if failoverModels: - print(f"✅ Found {len(failoverModels)} suitable models:") - for i, model in enumerate(failoverModels[:5]): # Show top 5 - # Get the rating for this operation type - rating = 0 - for ot_rating in model.operationTypes: - if ot_rating.operationType == operationType: - rating = ot_rating.rating - break - - print(f" {i+1}. {model.displayName}") - print(f" Rating: {rating}/10 | Speed: {model.speedRating}/10 | Quality: {model.qualityRating}/10") - print(f" Cost: ${model.costPer1kTokensInput:.4f}/1k tokens") - else: - print("❌ No suitable models found") - - print() - - # Test the helper function - print("🔧 Testing Helper Function") - print("-" * 30) - - # Create operation type ratings using the helper - ratings = createOperationTypeRatings( - (OperationTypeEnum.WEB_RESEARCH, 10), - (OperationTypeEnum.WEB_NEWS, 8), - (OperationTypeEnum.DATA_ANALYSE, 6) - ) - - print("Created ratings:") - for rating in ratings: - print(f" {rating.operationType.value}: {rating.rating}/10") - - print() - print("✅ All tests completed successfully!") - -if __name__ == "__main__": - testOperationTypeRatings()