diff --git a/modules/interfaces/interfaceDbChatObjects.py b/modules/interfaces/interfaceDbChatObjects.py index ce39a1e5..d29c0f14 100644 --- a/modules/interfaces/interfaceDbChatObjects.py +++ b/modules/interfaces/interfaceDbChatObjects.py @@ -978,7 +978,7 @@ class ChatObjects: def _storeDebugMessageAndDocuments(self, message: ChatMessage) -> None: """ Store message and documents (metadata and file bytes) for debugging purposes. - Structure: gateway/test-chat/messages/m_round_task_action_timestamp/documentlist_label/ + Structure: {log_dir}/debug/messages/m_round_task_action_timestamp/documentlist_label/ - message.json, message_text.txt - document_###_metadata.json - document_###_ (actual file bytes) @@ -992,7 +992,13 @@ class ChatObjects: from datetime import datetime, UTC # Create base debug directory - debug_root = "./test-chat/messages" + # 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', 'messages') os.makedirs(debug_root, exist_ok=True) # Generate timestamp diff --git a/modules/services/serviceAi/mainServiceAi.py b/modules/services/serviceAi/mainServiceAi.py index d2086c57..0c6293c0 100644 --- a/modules/services/serviceAi/mainServiceAi.py +++ b/modules/services/serviceAi/mainServiceAi.py @@ -153,43 +153,6 @@ class AiService: await self._ensureAiObjectsInitialized() return await self.webResearchService.webResearch(request) - # Master AI Call (process user prompt with optional unlimited count of input documents delivering one or many output documents, no size limitations) - async def callAi( - self, - prompt: str, - documents: Optional[List[ChatDocument]] = None, - placeholders: Optional[List[PromptPlaceholder]] = None, - options: Optional[AiCallOptions] = None, - outputFormat: Optional[str] = None, - title: Optional[str] = None - ) -> Union[str, Dict[str, Any]]: - """ - Unified AI call interface that automatically routes to appropriate handler. - - Args: - prompt: The main prompt for the AI call - documents: Optional list of documents to process - placeholders: Optional list of placeholder replacements for planning calls - options: AI call configuration options - outputFormat: Optional output format (html, pdf, docx, txt, md, json, csv, xlsx) for document generation - title: Optional title for generated documents - - Returns: - AI response as string, or dict with documents if outputFormat is specified - - Raises: - Exception: If all available models fail - """ - await self._ensureAiObjectsInitialized() - - # Get document processor and generator - documentProcessor = self.documentProcessor - documentGenerator = self.documentGenerator - - return await self.coreAi.callAi( - prompt, documents, placeholders, options, outputFormat, title, - documentProcessor, documentGenerator - ) def sanitizePromptContent(self, content: str, contentType: str = "text") -> str: """ diff --git a/modules/services/serviceAi/subCoreAi.py b/modules/services/serviceAi/subCoreAi.py index 84ef012f..506f66a1 100644 --- a/modules/services/serviceAi/subCoreAi.py +++ b/modules/services/serviceAi/subCoreAi.py @@ -20,12 +20,211 @@ class SubCoreAi: self.services = services self.aiObjects = aiObjects - # AI Processing Call - async def callAi( + # Shared Core Function for AI Calls with Looping + async def _callAiWithLooping( + self, + prompt: str, + options: AiCallOptions, + debug_prefix: str = "ai_call" + ) -> str: + """ + Shared core function for AI calls with looping system. + Handles continuation logic when response needs multiple rounds. + Delivers prompt and response to debug file log. + + Args: + prompt: The prompt to send to AI + options: AI call configuration options + debug_prefix: Prefix for debug file names + + Returns: + Complete AI response after all iterations + """ + max_iterations = 10 # Prevent infinite loops + iteration = 0 + accumulated_content = [] + + logger.info(f"Starting AI call with looping (debug prefix: {debug_prefix})") + + # Write initial prompt to debug file + from modules.shared.debugLogger import writeDebugFile + writeDebugFile(prompt, f"{debug_prefix}_prompt", None) + + while iteration < max_iterations: + iteration += 1 + logger.info(f"AI call iteration {iteration}/{max_iterations}") + + # Build iteration prompt + if iteration == 1: + iteration_prompt = prompt + else: + iteration_prompt = self._buildContinuationPrompt(prompt, accumulated_content, iteration) + + # Make AI call + try: + from modules.datamodels.datamodelAi import AiCallRequest + request = AiCallRequest( + prompt=iteration_prompt, + context="", + options=options + ) + response = await self.aiObjects.call(request) + result = response.content + + # Write raw AI response to debug file + writeDebugFile(result, f"{debug_prefix}_response_iteration_{iteration}", None) + + # Emit stats for this iteration + self.services.workflow.storeWorkflowStat( + self.services.currentWorkflow, + response, + f"ai.call.{debug_prefix}.iteration_{iteration}" + ) + + if not result or not result.strip(): + logger.warning(f"Iteration {iteration}: Empty response, stopping") + break + + # Check if this is a continuation response + if "[CONTINUE:" in result: + # Extract the content before the continuation marker + content_part = result.split("[CONTINUE:")[0].strip() + if content_part: + accumulated_content.append(content_part) + logger.info(f"Iteration {iteration}: Continuation detected, continuing...") + continue + else: + # This is the final response + accumulated_content.append(result) + logger.info(f"Iteration {iteration}: Final response received") + break + + except Exception as e: + logger.error(f"Error in AI call iteration {iteration}: {str(e)}") + break + + if iteration >= max_iterations: + logger.warning(f"AI call stopped after maximum iterations ({max_iterations})") + + # Combine all accumulated content + final_result = "\n\n".join(accumulated_content) if accumulated_content else "" + + # Write final result to debug file + writeDebugFile(final_result, f"{debug_prefix}_final_result", None) + + logger.info(f"AI call completed: {len(accumulated_content)} parts from {iteration} iterations") + return final_result + + def _buildContinuationPrompt( + self, + base_prompt: str, + accumulated_content: List[str], + iteration: int + ) -> str: + """ + Build a prompt for continuation iterations. + """ + continuation_instructions = f""" + +CONTINUATION REQUEST (Iteration {iteration}): +You are continuing from a previous response. Please continue generating content from where you left off. + +IMPORTANT: +- Continue from the exact point where you stopped +- Maintain the same format and structure +- If you cannot complete the full response, end with: [CONTINUE: brief description of what still needs to be generated] +- Only stop when the response is completely generated + +Previous content generated: +{chr(10).join(accumulated_content[-1:]) if accumulated_content else "None"} + +Continue generating content now: +""" + + return f"{base_prompt}{continuation_instructions}" + + def _buildPromptWithPlaceholders(self, prompt: str, placeholders: Optional[Dict[str, str]]) -> str: + """ + Build full prompt by replacing placeholders with their content. + Uses the new {{KEY:placeholder}} format. + """ + if not placeholders: + return prompt + + 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) + + return full_prompt + + 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, + aiService=self, + services=self.services + ) + + # 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, + options: Optional[AiCallOptions] = None + ) -> str: + """ + Planning AI call for task planning, action planning, action selection, etc. + + Args: + prompt: The planning prompt + placeholders: Optional list of placeholder replacements + options: AI call configuration options + + Returns: + Planning JSON response + """ + if options is None: + options = AiCallOptions() + + # Build full prompt with placeholders + if placeholders: + placeholders_dict = {p.key: p.value for p in placeholders} + full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders_dict) + else: + full_prompt = prompt + + # Use shared core function with planning-specific debug prefix + return await self._callAiWithLooping(full_prompt, options, "planning") + + # Document Generation AI Call + async def callAiDocuments( self, prompt: str, documents: Optional[List[ChatDocument]] = None, - placeholders: Optional[List[PromptPlaceholder]] = None, options: Optional[AiCallOptions] = None, outputFormat: Optional[str] = None, title: Optional[str] = None, @@ -33,94 +232,43 @@ class SubCoreAi: documentGenerator=None ) -> Union[str, Dict[str, Any]]: """ - Unified AI call interface that automatically routes to appropriate handler. + Document generation AI call for all non-planning calls. + Uses the current unified path with extraction and generation. Args: prompt: The main prompt for the AI call documents: Optional list of documents to process - placeholders: Optional list of placeholder replacements for planning calls options: AI call configuration options - outputFormat: Optional output format (html, pdf, docx, txt, md, json, csv, xlsx) for document generation + outputFormat: Optional output format for document generation title: Optional title for generated documents documentProcessor: Document processing service instance documentGenerator: Document generation service instance Returns: AI response as string, or dict with documents if outputFormat is specified - - Raises: - Exception: If all available models fail """ if options is None: options = AiCallOptions() - # Normalize placeholders from List[PromptPlaceholder] - placeholders_dict: Dict[str, str] = {} - placeholders_meta: Dict[str, bool] = {} - if placeholders: - placeholders_dict = {p.label: p.content for p in placeholders} - placeholders_meta = {p.label: bool(getattr(p, 'summaryAllowed', False)) for p in placeholders} - - # Auto-determine call type based on documents and operation type - call_type = self._determineCallType(documents, options.operationType) - options.callType = call_type - - try: - # Build the full prompt that will be sent to AI - if placeholders: - full_prompt = prompt - for p in placeholders: - placeholder = f"{{{{KEY:{p.label}}}}}" - full_prompt = full_prompt.replace(placeholder, p.content) - else: - full_prompt = prompt - - # Check for unresolved placeholders and clean them up - try: - import re - # Find only {{KEY:...}} patterns that need to be removed - unresolved_placeholders = re.findall(r'\{\{KEY:[^}]+\}\}', full_prompt) - if unresolved_placeholders: - logger.warning(f"Found unresolved KEY placeholders in prompt: {unresolved_placeholders}") - # Remove only {{KEY:...}} patterns, leave other {{...}} content intact - full_prompt = re.sub(r'\{\{KEY:[^}]+\}\}', '', full_prompt) - # Clean up extra whitespace - full_prompt = re.sub(r'\n\s*\n\s*\n', '\n\n', full_prompt) - full_prompt = full_prompt.strip() - logger.info("Cleaned up unresolved KEY placeholders from prompt") - except Exception as e: - logger.warning(f"Error cleaning up prompt placeholders: {str(e)}") - - # Log the final integrated prompt that AI will receive - try: - from modules.shared.debugLogger import writeDebugFile - # Determine the prompt type based on operation type - if options.operationType == OperationType.GENERATE_PLAN: - prompt_type = "taskplanPrompt" - elif options.operationType == OperationType.ANALYSE_CONTENT: - prompt_type = "analysisPrompt" - else: - prompt_type = "aiPrompt" - - writeDebugFile(full_prompt, prompt_type, documents) - except Exception: - pass # Don't fail on debug logging - except Exception: - pass - # Handle document generation with specific output format using unified approach if outputFormat and documentGenerator: # Use unified generation method for all document generation if documents and len(documents) > 0: # Extract content from documents first logger.info(f"Extracting content from {len(documents)} documents") - extracted_content = await documentProcessor.callAiText(full_prompt, documents, options) - # Generate with extracted content - generated_json = await self._callAiUnifiedGeneration(full_prompt, extracted_content, options, outputFormat, title) + extracted_content = await documentProcessor.callAiText(prompt, documents, options) + # Generate with extracted content using shared core function + generation_prompt = await self._buildGenerationPrompt(prompt, extracted_content, outputFormat, title) + generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation") else: # Direct generation without documents logger.info("No documents provided - using direct generation") - generated_json = await self._callAiUnifiedGeneration(full_prompt, None, options, outputFormat, title) + generation_prompt = await self._buildGenerationPrompt(prompt, None, outputFormat, title) + generated_json = await self._callAiWithLooping(generation_prompt, options, "document_generation") + + # Write the generated JSON to debug file + from modules.shared.debugLogger import writeDebugFile + writeDebugFile(generated_json, "unified_generation_response", documents) # Parse the generated JSON try: @@ -128,6 +276,13 @@ class SubCoreAi: generated_data = json.loads(generated_json) except json.JSONDecodeError as e: logger.error(f"Failed to parse generated JSON: {str(e)}") + logger.error(f"JSON content length: {len(generated_json)}") + logger.error(f"JSON content preview (last 200 chars): ...{generated_json[-200:]}") + logger.error(f"JSON content around error position: {generated_json[max(0, e.pos-50):e.pos+50]}") + + # Write the problematic JSON to debug file + writeDebugFile(generated_json, "failed_json_parsing", None) + return {"success": False, "error": f"Generated content is not valid JSON: {str(e)}"} # Render to final format using the existing renderer @@ -135,7 +290,7 @@ class SubCoreAi: from modules.services.serviceGeneration.mainServiceGeneration import GenerationService generationService = GenerationService(self.services) rendered_content, mime_type = await generationService.renderReport( - generated_data, outputFormat, title or "Generated Document", full_prompt, self + generated_data, outputFormat, title or "Generated Document", prompt, self ) # Build result in the expected format @@ -162,47 +317,24 @@ class SubCoreAi: writeDebugFile(str(result), "documentGenerationResponse", documents) except Exception: pass - return result + return result + except Exception as e: logger.error(f"Error rendering document: {str(e)}") return {"success": False, "error": f"Rendering failed: {str(e)}"} - if call_type == "planning": - result = await self._callAiPlanning(prompt, placeholders_dict, placeholders_meta, options) - # Log AI response for debugging - try: - from modules.shared.debugLogger import writeDebugFile - writeDebugFile(str(result or ""), "taskplanResponse", documents) - except Exception: - pass - return result + # Handle text calls (no output format specified) + if documents and documentProcessor: + # Use document processing for text calls with documents + result = await documentProcessor.callAiText(prompt, documents, options) else: - # Set processDocumentsIndividually from the legacy parameter if not set in options - if options.processDocumentsIndividually is None and documents: - options.processDocumentsIndividually = False # Default to batch processing - - # For text calls, we need to build the full prompt with placeholders here - # since _callAiText doesn't handle placeholders directly - if placeholders_dict: - full_prompt = self._buildPromptWithPlaceholders(prompt, placeholders_dict) - else: - full_prompt = prompt - - if documentProcessor and documents: - result = await documentProcessor.callAiText(full_prompt, documents, options) - else: - # Enhanced direct AI call with partial results support - result = await self._callAiWithPartialResults(full_prompt, options) - - # Log AI response for debugging (additional logging for text calls) - try: - from modules.shared.debugLogger import writeDebugFile - writeDebugFile(str(result or ""), "aiTextResponse", documents) - except Exception: - pass + # Use shared core function for direct text calls + result = await self._callAiWithLooping(prompt, options, "text") + return result + # AI Image Analysis async def readImage( self, @@ -312,382 +444,14 @@ class SubCoreAi: else: return "text" - async def _callAiPlanning( - self, - prompt: str, - placeholders: Optional[Dict[str, str]], - placeholdersMeta: Optional[Dict[str, bool]], - options: AiCallOptions - ) -> str: - """ - Handle planning calls with placeholder system and selective summarization. - """ - # Build full prompt with placeholders; if too large, summarize summaryAllowed placeholders proportionally - effective_placeholders = placeholders or {} - full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders) - if options.compressPrompt and placeholdersMeta: - # Determine model capacity - try: - caps = self._getModelCapabilitiesForContent(full_prompt, None, options) - max_bytes = caps.get("maxContextBytes", len(full_prompt.encode("utf-8"))) - except Exception: - max_bytes = len(full_prompt.encode("utf-8")) - current_bytes = len(full_prompt.encode("utf-8")) - if current_bytes > max_bytes: - # Compute total bytes contributed by allowed placeholders (approximate by content length) - allowed_labels = [l for l, allow in placeholdersMeta.items() if allow] - allowed_sizes = {l: len((effective_placeholders.get(l) or "").encode("utf-8")) for l in allowed_labels} - total_allowed = sum(allowed_sizes.values()) - overage = current_bytes - max_bytes - if total_allowed > 0 and overage > 0: - # Target total for allowed after reduction - target_allowed = max(total_allowed - overage, 0) - # Global ratio to apply across allowed placeholders - ratio = target_allowed / total_allowed if total_allowed > 0 else 1.0 - ratio = max(0.0, min(1.0, ratio)) - reduced: Dict[str, str] = {} - for label, content in effective_placeholders.items(): - if label in allowed_labels and isinstance(content, str) and len(content) > 0: - old_len = len(content) - # Reduce by proportional ratio on characters (fallback if empty) - reduction_factor = ratio if old_len > 0 else 1.0 - reduced[label] = self._reduceText(content, reduction_factor) - else: - reduced[label] = content - effective_placeholders = reduced - full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders) - # If still slightly over, perform a second-pass fine adjustment with updated ratio - current_bytes = len(full_prompt.encode("utf-8")) - if current_bytes > max_bytes and total_allowed > 0: - overage2 = current_bytes - max_bytes - # Recompute allowed sizes after first reduction - allowed_sizes2 = {l: len((effective_placeholders.get(l) or "").encode("utf-8")) for l in allowed_labels} - total_allowed2 = sum(allowed_sizes2.values()) - if total_allowed2 > 0 and overage2 > 0: - target_allowed2 = max(total_allowed2 - overage2, 0) - ratio2 = target_allowed2 / total_allowed2 - ratio2 = max(0.0, min(1.0, ratio2)) - reduced2: Dict[str, str] = {} - for label, content in effective_placeholders.items(): - if label in allowed_labels and isinstance(content, str) and len(content) > 0: - old_len = len(content) - reduction_factor = ratio2 if old_len > 0 else 1.0 - reduced2[label] = self._reduceText(content, reduction_factor) - else: - reduced2[label] = content - effective_placeholders = reduced2 - full_prompt = self._buildPromptWithPlaceholders(prompt, effective_placeholders) - - - # Make AI call using AiObjects (let it handle model selection) - request = AiCallRequest( - prompt=full_prompt, - context="", # Context is already included in the prompt - options=options - ) - response = await self.aiObjects.call(request) - try: - logger.debug(f"AI model selected (planning): {getattr(response, 'modelName', 'unknown')}") - except Exception: - pass - return response.content - async def _callAiWithPartialResults( - self, - prompt: str, - options: AiCallOptions - ) -> str: - """ - Call AI with partial results continuation logic for direct calls. - Handles cases where AI needs to generate large responses in chunks. - """ - logger.info("Starting direct AI call with partial results support") - - # Build enhanced prompt with continuation instructions - enhanced_prompt = self._buildDirectContinuationPrompt(prompt) - - # Process with continuation logic - return await self._processDirectWithContinuationLoop(enhanced_prompt, options) - def _buildDirectContinuationPrompt(self, base_prompt: str) -> str: - """ - Build a prompt for direct AI calls that includes partial results instructions. - """ - continuation_instructions = """ - -IMPORTANT: If your response is too large to generate completely in one response, you can deliver partial results and continue. - -CONTINUATION LOGIC: -- If you cannot complete the full response, end your response with: - [CONTINUE: brief description of what still needs to be generated] -- The system will call you again to continue from where you left off -- Continue generating from the exact point where you stopped -- Maintain consistency with your previous partial response -- Only stop when you have generated the complete response - -Examples: - -Example - Code Generation: -If generating a large code file and you can only generate part of it: -- Generate the first part (imports, classes, functions) -- End with: [CONTINUE: Generate the remaining methods and main execution code] -- In the next call, continue from where you left off - -Example - Documentation: -If writing comprehensive documentation and you can only generate sections 1-3: -- Generate sections 1-3 with full content -- End with: [CONTINUE: Generate sections 4-8 covering advanced topics and examples] -- In the next call, continue with sections 4-8 - -This allows you to handle very large responses that exceed normal limits. -""" - - return f"{base_prompt}{continuation_instructions}" - - async def _processDirectWithContinuationLoop( - self, - enhanced_prompt: str, - options: AiCallOptions - ) -> str: - """ - Process direct AI call with continuation loop until complete. - """ - max_iterations = 10 # Prevent infinite loops - iteration = 0 - accumulated_content = [] - continuation_hint = None - - while iteration < max_iterations: - iteration += 1 - logger.info(f"Direct AI continuation iteration {iteration}/{max_iterations}") - - # Build prompt for this iteration - if continuation_hint: - iteration_prompt = self._buildDirectContinuationIterationPrompt( - enhanced_prompt, continuation_hint, accumulated_content - ) - else: - iteration_prompt = enhanced_prompt - - # Make AI call for this iteration - try: - request = AiCallRequest( - prompt=iteration_prompt, - context="", - options=options - ) - response = await self.aiObjects.call(request) - result = response.content - - # Emit stats for this iteration - self.services.workflow.storeWorkflowStat( - self.services.currentWorkflow, - response, - f"ai.call.{options.operationType}.iteration_{iteration}" - ) - - if not result or not result.strip(): - logger.warning(f"Iteration {iteration}: Empty response, stopping") - break - - # Check for continuation marker - if "[CONTINUE:" in result: - # Extract the continuation hint - import re - continue_match = re.search(r'\[CONTINUE:\s*([^\]]+)\]', result) - if continue_match: - continuation_hint = continue_match.group(1).strip() - # Remove the continuation marker from the result - result = re.sub(r'\s*\[CONTINUE:[^\]]+\]', '', result).strip() - else: - continuation_hint = "Continue from where you left off" - - # Add this partial result to accumulated content - if result.strip(): - accumulated_content.append(result.strip()) - - logger.info(f"Iteration {iteration}: Partial result added, continue hint: {continuation_hint}") - else: - # No continuation marker - this is the final result - if result.strip(): - accumulated_content.append(result.strip()) - - logger.info(f"Direct AI continuation complete after {iteration} iterations") - break - - except Exception as e: - logger.error(f"Direct AI iteration {iteration} failed: {str(e)}") - break - - if iteration >= max_iterations: - logger.warning(f"Direct AI continuation stopped after maximum iterations ({max_iterations})") - - # For JSON responses, we need to merge them properly instead of concatenating - if accumulated_content: - import json - # Parse each part as JSON and merge them - merged_documents = [] - merged_metadata = None - - for content in accumulated_content: - parsed = json.loads(content) - if isinstance(parsed, dict): - # Extract metadata from first valid JSON - if merged_metadata is None and "metadata" in parsed: - merged_metadata = parsed["metadata"] - - # Extract documents from this part - if "documents" in parsed and isinstance(parsed["documents"], list): - merged_documents.extend(parsed["documents"]) - - # Create final merged JSON - NO FALLBACK - final_result = json.dumps({ - "metadata": merged_metadata or { - "title": "Generated Document", - "splitStrategy": "single_document", - "source_documents": [], - "extraction_method": "ai_generation" - }, - "documents": merged_documents - }, indent=2) - else: - # Return empty JSON structure if no content - final_result = json.dumps({ - "metadata": { - "title": "Generated Document", - "splitStrategy": "single_document", - "source_documents": [], - "extraction_method": "ai_generation" - }, - "documents": [] - }, indent=2) - - logger.info(f"Final direct AI result: {len(accumulated_content)} parts from {iteration} iterations") - return final_result - - def _buildDirectContinuationIterationPrompt( - self, - base_prompt: str, - continuation_hint: str, - accumulated_content: List[str] - ) -> str: - """ - Build a prompt for continuation iteration with context. - """ - # Build context of what's already been generated - context_summary = "PREVIOUSLY GENERATED CONTENT:\n" - for i, content in enumerate(accumulated_content[-2:]): # Show last 2 parts for context - preview = content[:200] + "..." if len(content) > 200 else content - context_summary += f"Part {i+1}: {preview}\n" - - continuation_prompt = f""" -{base_prompt} - -{context_summary} - -CONTINUATION INSTRUCTIONS: -- Continue from where you left off -- Continuation hint: {continuation_hint} -- Generate the next part of the content -- Maintain consistency with previously generated content -- End with [CONTINUE: description] if more content is needed -- End without [CONTINUE] if the response is complete -""" - - return continuation_prompt - - async def _callAiUnifiedGeneration( - self, - prompt: str, - extracted_content: Optional[str] = None, - options: Optional[AiCallOptions] = None, - outputFormat: str = "json", - title: str = "Generated Document" - ) -> str: - """ - Unified generation method that handles both scenarios: - - With extracted content (from documents) - - Without extracted content (direct generation) - - Always uses continuation logic for long responses. - Always returns standardized JSON format using the multi-document schema. - """ - if options is None: - options = AiCallOptions() - - logger.info("Starting unified AI generation with continuation logic") - - # Use the existing buildGenerationPrompt to get the proper canonical format instructions - 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, - aiService=self, - services=self.services - ) - - # If we have extracted content, prepend it to the prompt - if extracted_content: - generation_prompt = f"""EXTRACTED CONTENT FROM DOCUMENTS: -{extracted_content} - -{generation_prompt}""" - - # Use continuation logic for long responses - return await self._processDirectWithContinuationLoop(generation_prompt, options) - - async def _callAiDirect( - self, - prompt: str, - documents: Optional[List[ChatDocument]], - options: AiCallOptions, - documentProcessor=None - ) -> Dict[str, Any]: - """ - Call AI directly with prompt and documents for JSON output. - Used for multi-file generation - uses the existing generation pipeline. - """ - # Use the existing generation pipeline that already works - # This ensures proper document processing and content extraction - logger.info(f"Using existing generation pipeline for {len(documents) if documents else 0} documents") - - if documentProcessor: - # Process documents with JSON merging using the existing pipeline - result = await documentProcessor.processDocumentsPerChunkJson(documents, prompt, options) - else: - # Fallback to simple AI call - request = AiCallRequest( - prompt=prompt, - context="", - options=options - ) - response = await self.aiObjects.call(request) - result = {"metadata": {"title": "AI Response"}, "sections": [{"id": "section_1", "content_type": "paragraph", "elements": [{"text": response.content}]}]} - - # Convert single-file result to multi-file format if needed - if "sections" in result and "documents" not in result: - logger.info("Converting single-file result to multi-file format") - # This is a single-file result, convert it to multi-file format - return { - "metadata": result.get("metadata", {"title": "Converted Document"}), - "documents": [{ - "id": "doc_1", - "title": result.get("metadata", {}).get("title", "Document"), - "filename": "document.txt", - "sections": result.get("sections", []) - }] - } - - return result def _getModelCapabilitiesForContent(self, prompt: str, documents: Optional[List[ChatDocument]], options: AiCallOptions) -> Dict[str, int]: """ diff --git a/modules/services/serviceAi/subDocumentGeneration.py b/modules/services/serviceAi/subDocumentGeneration.py index a4eded1d..d11a1122 100644 --- a/modules/services/serviceAi/subDocumentGeneration.py +++ b/modules/services/serviceAi/subDocumentGeneration.py @@ -100,6 +100,10 @@ class SubDocumentGeneration: # Update progress - generating extraction prompt progressLogger.updateProgress(operationId, 0.1, "Generating prompt") + + # Write prompt to debug file + from modules.shared.debugLogger import writeDebugFile + writeDebugFile(extractionPrompt, "extraction_prompt", documents) # Process with unified JSON pipeline using continuation logic aiResponse = await self.documentProcessor.processDocumentsWithContinuation( @@ -109,11 +113,13 @@ class SubDocumentGeneration: # Update progress - AI processing completed progressLogger.updateProgress(operationId, 0.6, "Processing done") - # Log the AI response for debugging - logger.info(f"AI response received for validation:") - logger.info(f" - Type: {type(aiResponse)}") - logger.info(f" - Keys: {list(aiResponse.keys()) if isinstance(aiResponse, dict) else 'Not a dict'}") - logger.info(f" - Content: {aiResponse}") + + + # Write AI response to debug file + from modules.shared.debugLogger import writeDebugFile + import json + response_json = json.dumps(aiResponse, indent=2, ensure_ascii=False) if isinstance(aiResponse, dict) else str(aiResponse) + writeDebugFile(response_json, "ai_response", documents) # Validate response structure if not self._validateUnifiedResponseStructure(aiResponse): diff --git a/modules/services/serviceAi/subDocumentProcessing.py b/modules/services/serviceAi/subDocumentProcessing.py index 9757350a..8fe3714a 100644 --- a/modules/services/serviceAi/subDocumentProcessing.py +++ b/modules/services/serviceAi/subDocumentProcessing.py @@ -605,7 +605,13 @@ CONTINUATION INSTRUCTIONS: import os from datetime import datetime, UTC ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") - debug_root = "./test-chat/ai" + # 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_image_chunk_{chunk_index}.txt"), "w", encoding="utf-8") as f: f.write(f"EXTRACTION IMAGE RESPONSE:\n{ai_result if ai_result else 'No response'}\n") diff --git a/modules/services/serviceAi/subUtilities.py b/modules/services/serviceAi/subUtilities.py index 0f5bcc4d..64508d71 100644 --- a/modules/services/serviceAi/subUtilities.py +++ b/modules/services/serviceAi/subUtilities.py @@ -61,7 +61,7 @@ class SubUtilities: pass def _writeAiResponseDebug(self, label: str, content: str, partIndex: int = 1, modelName: str = None, continuation: bool = None) -> None: - """Persist raw AI response parts for debugging under test-chat/ai - only if debug enabled.""" + """Persist raw AI response parts for debugging under configured log directory - only if debug enabled.""" try: # Check if debug logging is enabled debug_enabled = self.services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False) @@ -70,10 +70,13 @@ class SubUtilities: import os from datetime import datetime, UTC - # Base dir: gateway/test-chat/ai (go up 4 levels from this file) - # .../gateway/modules/services/serviceAi/subUtilities.py -> up to gateway root - gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) - outDir = os.path.join(gatewayDir, 'test-chat', 'ai') + # 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.dirname(os.path.abspath(__file__))))) + logDir = os.path.join(gatewayDir, logDir) + outDir = os.path.join(logDir, 'debug') os.makedirs(outDir, exist_ok=True) ts = datetime.now(UTC).strftime('%Y%m%d-%H%M%S-%f')[:-3] suffix = [] diff --git a/modules/services/serviceGeneration/subPromptBuilder.py b/modules/services/serviceGeneration/subPromptBuilder.py index bfb4052f..c2ba3c3e 100644 --- a/modules/services/serviceGeneration/subPromptBuilder.py +++ b/modules/services/serviceGeneration/subPromptBuilder.py @@ -403,7 +403,13 @@ DO NOT return a schema description - return actual extracted content in the JSON import os from datetime import datetime, UTC ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") - debug_root = "./test-chat/ai" + # 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) @@ -435,118 +441,70 @@ async def buildGenerationPrompt( # Debug output services.utils.debugLogToFile(f"GENERATION PROMPT REQUEST: buildGenerationPrompt called with outputFormat='{outputFormat}', title='{title}'", "PROMPT_BUILDER") - # AI call to generate the appropriate generation prompt - generationPromptRequest = f""" -You are creating instructions for an AI to generate JSON content in the CANONICAL FORMAT that will be converted to a {outputFormat} document. + # Return static generation prompt template instead of calling AI + services.utils.debugLogToFile("GENERATION PROMPT REQUEST: Using static template instead of AI call", "PROMPT_BUILDER") + + # Return static generation prompt template + result = f"""You are an AI assistant that generates structured JSON content for document creation. -User request: "{safeUserPrompt}" -Document title: "{title}" -Target format: {outputFormat} +USER REQUEST: "{safeUserPrompt}" +DOCUMENT TITLE: "{title}" +TARGET FORMAT: {outputFormat} -Write clear, detailed instructions that tell the AI how to generate JSON content using the CANONICAL JSON FORMAT. Focus on: +TASK: Generate JSON content that fulfills the user's request. -1. What content is most important for the user -2. How to structure and organize the content using the canonical JSON format with 'sections' -3. Specific formatting requirements for the target format -4. Language requirements to preserve -5. How to ensure the JSON content meets the user's needs - -CRITICAL: The AI MUST generate content using the CANONICAL JSON FORMAT with this exact structure: +CRITICAL: You MUST return ONLY valid JSON in this exact structure: {{ "metadata": {{ - "title": "Document Title" + "title": "{title}", + "splitStrategy": "single_document", + "source_documents": [], + "extraction_method": "ai_generation" }}, - "sections": [ + "documents": [ {{ - "id": "section_1", - "content_type": "heading", - "elements": [ + "id": "doc_1", + "title": "{title}", + "filename": "document.{outputFormat}", + "sections": [ {{ - "level": 1, - "text": "1. SECTION TITLE" - }} - ], - "order": 1 - }}, - {{ - "id": "section_2", - "content_type": "paragraph", - "elements": [ + "id": "section_1", + "content_type": "heading", + "elements": [ + {{ + "level": 1, + "text": "1. SECTION TITLE" + }} + ], + "order": 1 + }}, {{ - "text": "This is the actual content that should be extracted from the document." + "id": "section_2", + "content_type": "paragraph", + "elements": [ + {{ + "text": "This is the actual content that should be generated." + }} + ], + "order": 2 }} - ], - "order": 2 - }}, - {{ - "id": "section_3", - "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 + ] }} - ], - "continue": false + ] }} -IMPORTANT CHUNKING LOGIC: -- If the document is too large to generate completely in one response, set "continue": true -- When "continue": true, include a "continuation_context" field with: - - "last_section_id": "id of the last completed section" - - "last_element_index": "index of the last completed element in that section" - - "remaining_requirements": "brief description of what still needs to be generated" -- The AI will be called again with this context to continue generation -- Only set "continue": false when the document is completely generated - -The AI should NOT create format-specific structures like "sheets" or "columns" - only use the canonical format with "sections" and "elements". - -Write the instructions as plain text, not JSON. Start with "Generate JSON content that..." and provide clear, actionable instructions for creating structured JSON data in the canonical format. +IMPORTANT: +- Return ONLY the JSON structure above +- Do NOT include any text before or after the JSON +- Fill in the actual content based on the user request: {safeUserPrompt} +- If the content is too large, you can split it into multiple sections +- Each section should have a unique id and appropriate content_type """ - # Call AI service to generate the prompt - services.utils.debugLogToFile("GENERATION PROMPT REQUEST: Calling AI for generation prompt...", "PROMPT_BUILDER") - - # Import and set proper options for AI call - from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType - request_options = AiCallOptions() - request_options.operationType = OperationType.GENERAL - - request = AiCallRequest(prompt=generationPromptRequest, context="", options=request_options) - response = await aiService.aiObjects.call(request) - result = response.content if response else "" - - # Replace the placeholder that the AI created with actual format rules - if result: - formatRules = _getFormatRules(outputFormat) - result = result.replace("PLACEHOLDER_FOR_FORMAT_RULES", formatRules) - # Debug output services.utils.debugLogToFile(f"GENERATION PROMPT: Generated successfully", "PROMPT_BUILDER") - # Save full generation prompt and AI response 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") - debug_root = "./test-chat/ai" - os.makedirs(debug_root, exist_ok=True) - with open(os.path.join(debug_root, f"{ts}_generation_prompt.txt"), "w", encoding="utf-8") as f: - f.write(f"GENERATION PROMPT REQUEST:\n{generationPromptRequest}\n\n") - f.write(f"GENERATION PROMPT AI RESPONSE:\n{response.content if response else 'No response'}\n\n") - f.write(f"GENERATION PROMPT FINAL:\n{result if result else 'None'}\n") - except Exception: - pass - - return result if result else f"Generate a comprehensive {outputFormat} document titled '{title}' based on the extracted content." + return result.strip() except Exception as e: # Fallback on any error - preserve user prompt for language instructions diff --git a/modules/services/serviceNormalization/mainServiceNormalization.py b/modules/services/serviceNormalization/mainServiceNormalization.py index 34805ef2..4dfbf9cb 100644 --- a/modules/services/serviceNormalization/mainServiceNormalization.py +++ b/modules/services/serviceNormalization/mainServiceNormalization.py @@ -90,7 +90,7 @@ class NormalizationService: " \"Date\": {\"formats\": [\"DD.MM.YYYY\",\"YYYY-MM-DD\"]}\n }\n}\n" ) - response = await self.services.ai.callAi(prompt=prompt) + response = await self.services.ai.coreAi.callAiPlanning(prompt=prompt, placeholders=None, options=None) if not response: return {"mapping": {}, "normalizationPolicy": {}} @@ -244,7 +244,13 @@ class NormalizationService: debugEnabled = self.services.utils.configGet("APP_DEBUG_CHAT_WORKFLOW_ENABLED", False) if not debugEnabled: return - root = "./test-chat/ai" + # 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) + root = os.path.join(logDir, 'debug') os.makedirs(root, exist_ok=True) # Prefix timestamp for files that are frequently overwritten ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S") diff --git a/modules/services/serviceUtils/mainServiceUtils.py b/modules/services/serviceUtils/mainServiceUtils.py index 8379382a..caa07528 100644 --- a/modules/services/serviceUtils/mainServiceUtils.py +++ b/modules/services/serviceUtils/mainServiceUtils.py @@ -157,7 +157,12 @@ class UtilsService: return # Get debug directory - debug_dir = self.configGet("APP_DEBUG_CHAT_WORKFLOW_DIR", "./test-chat") + # Use configured log directory instead of hardcoded test-chat + logDir = self.configGet("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_dir = os.path.join(logDir, 'debug') if not os.path.isabs(debug_dir): # If relative path, make it relative to the gateway directory gateway_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) diff --git a/modules/services/serviceWorkflow/mainServiceWorkflow.py b/modules/services/serviceWorkflow/mainServiceWorkflow.py index ff814ac2..1edafafa 100644 --- a/modules/services/serviceWorkflow/mainServiceWorkflow.py +++ b/modules/services/serviceWorkflow/mainServiceWorkflow.py @@ -59,7 +59,7 @@ class WorkflowService: # Get summary using AI service directly (avoiding circular dependency) ai_service = AiService(self) - return await ai_service.callAi( + return await ai_service.coreAi.callAiDocuments( prompt=prompt, documents=None, options={ @@ -69,7 +69,9 @@ class WorkflowService: "compress_prompt": True, "compress_documents": False, "max_cost": 0.01 - } + }, + documentProcessor=ai_service.documentProcessor, + documentGenerator=ai_service.documentGenerator ) except Exception as e: diff --git a/modules/shared/debugLogger.py b/modules/shared/debugLogger.py index 82dcb1c9..947bf816 100644 --- a/modules/shared/debugLogger.py +++ b/modules/shared/debugLogger.py @@ -1,16 +1,25 @@ """ Simple debug logger for AI prompts and responses. -Writes files chronologically to gateway/test-chat/ai/ with sequential numbering. +Writes files chronologically to the configured log directory with sequential numbering. """ import os from datetime import datetime, UTC from typing import List, Optional +from modules.shared.configuration import APP_CONFIG def _getDebugDir() -> str: - """Get the debug directory path.""" - gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - return os.path.join(gatewayDir, 'test-chat', 'ai') + """Get the debug directory path from configuration.""" + # Get log directory from config (same as used by main logging system) + logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./") + if not os.path.isabs(logDir): + # If relative path, make it relative to the gateway directory + gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + logDir = os.path.join(gatewayDir, logDir) + + # Create debug subdirectory within the log directory + debugDir = os.path.join(logDir, 'debug') + return debugDir def _getNextSequenceNumber() -> int: diff --git a/modules/workflows/methods/methodAi.py b/modules/workflows/methods/methodAi.py index c820114e..e10f7fe9 100644 --- a/modules/workflows/methods/methodAi.py +++ b/modules/workflows/methods/methodAi.py @@ -106,23 +106,8 @@ class MethodAi(MethodBase): if chatDocuments: logger.info(f"Prepared {len(chatDocuments)} documents for AI processing") - # Update progress - building prompt - progressLogger.updateProgress(operationId, 0.4, "Building prompt") - - # Build enhanced prompt - enhanced_prompt = aiPrompt - - # Add processing mode instructions if specified (generic, not analysis-specific) - if processingMode == "detailed": - enhanced_prompt += "\n\nPlease provide a detailed response with comprehensive information." - elif processingMode == "advanced": - enhanced_prompt += "\n\nPlease provide an advanced response with deep insights." - - # Note: customInstructions parameter was removed as it's not defined in the method signature - - # Add format guidance to prompt - if normalized_result_type != "txt": - enhanced_prompt += f"\n\nPlease deliver the result in {normalized_result_type.upper()} format. Ensure the output follows the proper {normalized_result_type.upper()} syntax and structure." + # Update progress - preparing AI call + progressLogger.updateProgress(operationId, 0.4, "Preparing AI call") # Build options and delegate document handling to AI/Extraction/Generation services output_format = output_extension.replace('.', '') or 'txt' @@ -139,17 +124,16 @@ class MethodAi(MethodBase): requiredTags=requiredTags ) - supported_generation_formats = {"html", "pdf", "docx", "txt", "md", "json", "csv", "xlsx"} - output_format_arg = output_format if output_format in supported_generation_formats else None - # Update progress - calling AI progressLogger.updateProgress(operationId, 0.6, "Calling AI") - result = await self.services.ai.callAi( - prompt=enhanced_prompt, + result = await self.services.ai.coreAi.callAiDocuments( + prompt=aiPrompt, # Use original prompt, let unified generation handle prompt building documents=chatDocuments if chatDocuments else None, options=options, - outputFormat=output_format_arg + outputFormat=output_format, + documentProcessor=self.services.ai.documentProcessor, + documentGenerator=self.services.ai.documentGenerator ) # Update progress - processing result diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py index 9909bd9f..a4949a0d 100644 --- a/modules/workflows/methods/methodOutlook.py +++ b/modules/workflows/methods/methodOutlook.py @@ -1186,7 +1186,7 @@ Return JSON: # Call AI service to generate email content try: - ai_response = await self.services.ai.callAi( + ai_response = await self.services.ai.coreAi.callAiDocuments( prompt=ai_prompt, documents=chatDocuments, options=AiCallOptions( @@ -1199,7 +1199,9 @@ Return JSON: resultFormat="json", maxCost=0.50, maxProcessingTime=30 - ) + ), + documentProcessor=self.services.ai.documentProcessor, + documentGenerator=self.services.ai.documentGenerator ) # Parse AI response diff --git a/modules/workflows/processing/adaptive/contentValidator.py b/modules/workflows/processing/adaptive/contentValidator.py index 91896373..156dd2c9 100644 --- a/modules/workflows/processing/adaptive/contentValidator.py +++ b/modules/workflows/processing/adaptive/contentValidator.py @@ -120,9 +120,9 @@ DELIVERED CONTENT TO CHECK: request_options = AiCallOptions() request_options.operationType = OperationType.GENERAL - response = await self.services.ai.callAi( + response = await self.services.ai.coreAi.callAiPlanning( prompt=validationPrompt, - documents=None, + placeholders=None, options=request_options ) diff --git a/modules/workflows/processing/adaptive/intentAnalyzer.py b/modules/workflows/processing/adaptive/intentAnalyzer.py index 7d21b8d1..74283629 100644 --- a/modules/workflows/processing/adaptive/intentAnalyzer.py +++ b/modules/workflows/processing/adaptive/intentAnalyzer.py @@ -63,9 +63,9 @@ CRITICAL: Respond with ONLY the JSON object below. Do not include any explanator request_options = AiCallOptions() request_options.operationType = OperationType.GENERAL - response = await self.services.ai.callAi( + response = await self.services.ai.coreAi.callAiPlanning( prompt=analysisPrompt, - documents=None, + placeholders=None, options=request_options ) diff --git a/modules/workflows/processing/core/taskPlanner.py b/modules/workflows/processing/core/taskPlanner.py index 3bea9c76..361c86b2 100644 --- a/modules/workflows/processing/core/taskPlanner.py +++ b/modules/workflows/processing/core/taskPlanner.py @@ -105,7 +105,7 @@ class TaskPlanner: maxProcessingTime=30 ) - prompt = await self.services.ai.callAi( + prompt = await self.services.ai.coreAi.callAiPlanning( prompt=taskPlanningPromptTemplate, placeholders=placeholders, options=options diff --git a/modules/workflows/processing/modes/modeActionplan.py b/modules/workflows/processing/modes/modeActionplan.py index c1521a71..0e11ac88 100644 --- a/modules/workflows/processing/modes/modeActionplan.py +++ b/modules/workflows/processing/modes/modeActionplan.py @@ -137,7 +137,7 @@ class ActionplanMode(BaseMode): maxProcessingTime=30 ) - prompt = await self.services.ai.callAi(prompt=actionPromptTemplate, placeholders=placeholders, options=options) + prompt = await self.services.ai.coreAi.callAiPlanning(prompt=actionPromptTemplate, placeholders=placeholders, options=options) # Check if AI response is valid if not prompt: @@ -476,7 +476,7 @@ class ActionplanMode(BaseMode): maxProcessingTime=30 ) - response = await self.services.ai.callAi(prompt=promptTemplate, placeholders=placeholders, options=options) + response = await self.services.ai.coreAi.callAiPlanning(prompt=promptTemplate, placeholders=placeholders, options=options) # Log result review response received logger.info("=== RESULT REVIEW AI RESPONSE RECEIVED ===") diff --git a/modules/workflows/processing/modes/modeReact.py b/modules/workflows/processing/modes/modeReact.py index cbe29eee..405f530e 100644 --- a/modules/workflows/processing/modes/modeReact.py +++ b/modules/workflows/processing/modes/modeReact.py @@ -201,7 +201,7 @@ class ReactMode(BaseMode): maxProcessingTime=30 ) - response = await self.services.ai.callAi( + response = await self.services.ai.coreAi.callAiPlanning( prompt=promptTemplate, placeholders=placeholders, options=options @@ -313,7 +313,7 @@ class ReactMode(BaseMode): resultFormat="json" # Explicitly request JSON format ) - paramsResp = await self.services.ai.callAi( + paramsResp = await self.services.ai.coreAi.callAiPlanning( prompt=promptTemplate, placeholders=placeholders, options=options @@ -625,7 +625,7 @@ class ReactMode(BaseMode): maxProcessingTime=30 ) - resp = await self.services.ai.callAi( + resp = await self.services.ai.coreAi.callAiPlanning( prompt=promptTemplate, placeholders=placeholders, options=options @@ -719,8 +719,9 @@ User language: {userLanguage} Return only the user-friendly message, no technical details.""" # Call AI to generate user-friendly message - response = await self.services.ai.callAi( + response = await self.services.ai.coreAi.callAiPlanning( prompt=prompt, + placeholders=None, options=AiCallOptions( operationType=OperationType.GENERATE_CONTENT, priority=Priority.SPEED, @@ -759,8 +760,9 @@ Result context: {resultContext} Return only the user-friendly message, no technical details.""" # Call AI to generate user-friendly result message - response = await self.services.ai.callAi( + response = await self.services.ai.coreAi.callAiPlanning( prompt=prompt, + placeholders=None, options=AiCallOptions( operationType=OperationType.GENERATE_CONTENT, priority=Priority.SPEED, diff --git a/modules/workflows/workflowManager.py b/modules/workflows/workflowManager.py index cb3a09b5..1dfebd77 100644 --- a/modules/workflows/workflowManager.py +++ b/modules/workflows/workflowManager.py @@ -220,7 +220,7 @@ class WorkflowManager: ) # Call AI analyzer - aiResponse = await self.services.ai.callAi(prompt=analyzerPrompt) + aiResponse = await self.services.ai.coreAi.callAiPlanning(prompt=analyzerPrompt, placeholders=None, options=None) detectedLanguage = None normalizedRequest = None diff --git a/test_unified_architecture.py b/test_unified_architecture.py deleted file mode 100644 index bf0e0750..00000000 --- a/test_unified_architecture.py +++ /dev/null @@ -1,258 +0,0 @@ -import asyncio -import sys -import os -from unittest.mock import AsyncMock, MagicMock - -# Add the project root to the sys.path -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) - -from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType -from modules.datamodels.datamodelChat import ChatDocument -from modules.services.serviceAi.subCoreAi import SubCoreAi - -class MockAiObjects: - def __init__(self, responses): - self.responses = responses - self.call_count = 0 - - async def call(self, request: AiCallRequest): - if self.call_count < len(self.responses): - response_content = self.responses[self.call_count] - self.call_count += 1 - mock_response = MagicMock() - mock_response.content = response_content - mock_response.modelName = "mock-model" - mock_response.priceUsd = 0.001 - mock_response.processingTime = 0.1 - print(f" Mock AI Call {self.call_count}: Responding with partial result (length: {len(response_content)})") - return mock_response - else: - print(" Mock AI Call: No more mock responses, returning empty.") - mock_response = MagicMock() - mock_response.content = "" - return mock_response - -class MockServices: - def __init__(self): - self.currentWorkflow = MagicMock() - self.currentWorkflow.id = "test_workflow_123" - self.workflow = MagicMock() - self.workflow.createProgressLogger.return_value = MagicMock() - self.workflow.storeWorkflowStat = AsyncMock() - self.ai = MagicMock() - self.ai.sanitizePromptContent.side_effect = lambda content, type: content - self.utils = MagicMock() - self.utils.debugLogToFile.side_effect = lambda msg, tag: print(f" DEBUG ({tag}): {msg}") - self.utils.configGet.return_value = False # Disable debug files for tests - -class MockDocumentProcessor: - async def callAiText(self, prompt, documents, options): - return "Extracted content from documents: Sample text content" - -async def test_unified_architecture(): - print("\n=== Testing Unified Architecture ===") - - # Mock responses: 1 for generation prompt building + 2 for actual generation - mock_responses = [ - # Response 1: Generation prompt building - "Generate JSON content that creates a structured document with prime numbers in a table format. Use the canonical JSON format with sections and elements.", - - # Response 2: First part of generation - """{ - "metadata": { - "title": "Prime Numbers List", - "splitStrategy": "single_document", - "source_documents": [], - "extraction_method": "ai_generation" - }, - "documents": [ - { - "id": "doc_primes_1_500", - "title": "Prime Numbers 1-500", - "filename": "primes_1_500.docx", - "sections": [ - { - "id": "section_1", - "content_type": "table", - "elements": [ - { - "headers": ["Number", "Prime"], - "rows": [ - ["1", "2"], ["2", "3"], ["3", "5"], ["4", "7"], ["5", "11"] - ] - } - ], - "order": 1 - } - ] - } - ] -} [CONTINUE: Generate remaining prime numbers from 501 to 1000]""", - - # Response 3: Second part of generation - """{ - "metadata": { - "title": "Prime Numbers List", - "splitStrategy": "single_document", - "source_documents": [], - "extraction_method": "ai_generation" - }, - "documents": [ - { - "id": "doc_primes_501_1000", - "title": "Prime Numbers 501-1000", - "filename": "primes_501_1000.docx", - "sections": [ - { - "id": "section_2", - "content_type": "table", - "elements": [ - { - "headers": ["Number", "Prime"], - "rows": [ - ["501", "3571"], ["502", "3572"], ["503", "3581"] - ] - } - ], - "order": 2 - } - ] - } - ] -}""" - ] - - mock_ai_objects = MockAiObjects(mock_responses) - mock_services = MockServices() - mock_document_processor = MockDocumentProcessor() - - core_ai_service = SubCoreAi(mock_services, mock_ai_objects) - - prompt = "Generate the first 1000 prime numbers and arrange them in a structured table format." - options = AiCallOptions(operationType=OperationType.GENERATE_CONTENT) - output_format = "docx" - title = "Prime Numbers List" - - print(f"User Prompt: '{prompt}'") - print("Testing unified architecture with direct generation (no documents)...") - - # Test the unified generation method directly - result = await core_ai_service._callAiUnifiedGeneration(prompt, None, options, output_format, title) - - print("\n--- Generated JSON Result ---") - print(f"Result length: {len(result)} characters") - print(f"Result preview: {result[:300]}...") - - # Verify it's valid JSON - import json - try: - parsed_result = json.loads(result) - print(f"✅ Valid JSON with {len(parsed_result.get('documents', []))} documents") - - # Verify it's using the multi-document format - if "documents" in parsed_result and "metadata" in parsed_result: - print("✅ Using unified multi-document format") - print("✅ Architecture is properly unified!") - return True - else: - print("❌ Not using multi-document format") - return False - except json.JSONDecodeError as e: - print(f"❌ Invalid JSON: {str(e)}") - return False - -async def test_with_documents(): - print("\n=== Testing Unified Architecture WITH Documents ===") - - # Mock responses: 1 for generation prompt building + 1 for actual generation - mock_responses = [ - # Response 1: Generation prompt building - "Generate JSON content that creates a comprehensive fruit analysis report based on the extracted content. Use the canonical JSON format with sections and elements.", - - # Response 2: Generation with extracted content - """{ - "metadata": { - "title": "Fruit Analysis Report", - "splitStrategy": "single_document", - "source_documents": ["doc1"], - "extraction_method": "ai_generation" - }, - "documents": [ - { - "id": "doc_fruit_analysis", - "title": "Fruit Analysis Report", - "filename": "fruit_analysis.docx", - "sections": [ - { - "id": "section_1", - "content_type": "paragraph", - "elements": [ - { - "text": "Based on the extracted content, here is a comprehensive fruit analysis..." - } - ], - "order": 1 - } - ] - } - ] -}""" - ] - - mock_ai_objects = MockAiObjects(mock_responses) - mock_services = MockServices() - mock_document_processor = MockDocumentProcessor() - - core_ai_service = SubCoreAi(mock_services, mock_ai_objects) - - prompt = "Extract all fruit information and create a comprehensive analysis report." - options = AiCallOptions(operationType=OperationType.GENERATE_CONTENT) - output_format = "docx" - title = "Fruit Analysis Report" - - print(f"User Prompt: '{prompt}'") - print("Testing unified architecture with document extraction...") - - # Test the unified generation method with extracted content - result = await core_ai_service._callAiUnifiedGeneration(prompt, "Sample fruit data: apples, oranges, bananas", options, output_format, title) - - print("\n--- Generated JSON Result ---") - print(f"Result length: {len(result)} characters") - print(f"Result preview: {result[:300]}...") - - # Verify it's valid JSON - import json - try: - parsed_result = json.loads(result) - print(f"✅ Valid JSON with {len(parsed_result.get('documents', []))} documents") - - # Verify it's using the multi-document format - if "documents" in parsed_result and "metadata" in parsed_result: - print("✅ Using unified multi-document format") - print("✅ Architecture is properly unified!") - return True - else: - print("❌ Not using multi-document format") - return False - except json.JSONDecodeError as e: - print(f"❌ Invalid JSON: {str(e)}") - return False - -async def main(): - print("🚀 Testing Unified Architecture Implementation") - print("=" * 60) - - success1 = await test_unified_architecture() - success2 = await test_with_documents() - - if success1 and success2: - print("\n🎉 ALL TESTS PASSED! Unified architecture is properly implemented.") - print("✅ Single document = multi-document with n=1") - print("✅ Always uses multi-document JSON format") - print("✅ Continuation logic works for long responses") - print("✅ Both scenarios (with/without documents) work") - else: - print("\n❌ Some tests failed. Please check the implementation.") - -if __name__ == "__main__": - asyncio.run(main())