diff --git a/modules/workflows/methods/methodOutlook.py b/modules/workflows/methods/methodOutlook.py index a92014dc..f613cbe1 100644 --- a/modules/workflows/methods/methodOutlook.py +++ b/modules/workflows/methods/methodOutlook.py @@ -251,6 +251,10 @@ class MethodOutlook(MethodBase): if '@' in filter_text and '.' in filter_text and ' ' not in filter_text and not filter_text.startswith('from:'): return {"$filter": f"from/fromAddress/address eq '{filter_text}'"} + # Handle OData filter conditions (contains 'eq', 'ne', 'gt', 'lt', etc.) + if any(op in filter_text.lower() for op in [' eq ', ' ne ', ' gt ', ' lt ', ' ge ', ' le ', ' and ', ' or ']): + return {"$filter": filter_text} + # Handle text content - search in subject return {"$filter": f"contains(subject,'{filter_text}')"} diff --git a/modules/workflows/methods/methodSharepoint.py b/modules/workflows/methods/methodSharepoint.py index 88ee47f7..6dd75bac 100644 --- a/modules/workflows/methods/methodSharepoint.py +++ b/modules/workflows/methods/methodSharepoint.py @@ -931,7 +931,8 @@ class MethodSharepoint(MethodBase): return ActionResult.isFailure(error="pathQuery must start with '/' and include site name with syntax /site:/... e.g. /site:KM LayerFinance/Documents/Work") # Check if pathQuery contains search terms (words without proper path structure) - if not pathQuery.startswith('/site:') and not pathQuery.startswith('/Documents') and not pathQuery.startswith('/Shared Documents'): + valid_path_prefixes = ['/site:', '/Documents', '/documents', '/Shared Documents', '/shared documents'] + if not any(pathQuery.startswith(prefix) for prefix in valid_path_prefixes): return ActionResult.isFailure(error=f"Invalid pathQuery '{pathQuery}'. This appears to be search terms, not a valid SharePoint path. Use findDocumentPath action first to search for folders, then use the returned folder path as pathQuery.") # For pathQuery, we need to discover sites to find the specific one @@ -1627,7 +1628,8 @@ class MethodSharepoint(MethodBase): return ActionResult.isFailure(error="pathQuery must start with '/' and include site name with syntax /site:/... e.g. /site:KM LayerFinance/Documents/Work") # Check if pathQuery contains search terms (words without proper path structure) - if not pathQuery.startswith('/site:') and not pathQuery.startswith('/Documents') and not pathQuery.startswith('/Shared Documents'): + valid_path_prefixes = ['/site:', '/Documents', '/documents', '/Shared Documents', '/shared documents'] + if not any(pathQuery.startswith(prefix) for prefix in valid_path_prefixes): return ActionResult.isFailure(error=f"Invalid pathQuery '{pathQuery}'. This appears to be search terms, not a valid SharePoint path. Use findDocumentPath action first to search for folders, then use the returned folder path as pathQuery.") # For pathQuery, we need to discover sites to find the specific one diff --git a/modules/workflows/processing/adaptive/contentValidator.py b/modules/workflows/processing/adaptive/contentValidator.py index d211d1c3..ad9f6e7f 100644 --- a/modules/workflows/processing/adaptive/contentValidator.py +++ b/modules/workflows/processing/adaptive/contentValidator.py @@ -46,6 +46,53 @@ class ContentValidator: "improvementSuggestions": [f"NEXT STEP: Fix validation error - {error}. Check system logs for more details and retry the operation."] } + def _isValidJsonResponse(self, response: str) -> bool: + """Checks if response contains valid JSON structure""" + try: + import re + # Look for JSON with expected structure + json_match = re.search(r'\{[^{}]*"overallSuccess"[^{}]*\}', response, re.DOTALL) + if json_match: + json.loads(json_match.group(0)) + return True + return False + except: + return False + + def _extractFallbackValidationResult(self, response: str) -> Dict[str, Any]: + """Extracts validation result from malformed AI response""" + try: + import re + + # Extract key values using regex patterns + overall_success = re.search(r'"overallSuccess"\s*:\s*(true|false)', response, re.IGNORECASE) + quality_score = re.search(r'"qualityScore"\s*:\s*([0-9.]+)', response) + gap_analysis = re.search(r'"gapAnalysis"\s*:\s*"([^"]*)"', response) + + # Determine overall success from context if not found + if not overall_success: + # Look for positive/negative indicators in the text + if any(word in response.lower() for word in ['success', 'complete', 'fulfilled', 'satisfied']): + overall_success = True + elif any(word in response.lower() for word in ['failed', 'incomplete', 'missing', 'error']): + overall_success = False + else: + overall_success = False + + return { + "overallSuccess": overall_success.group(1).lower() == 'true' if overall_success else False, + "qualityScore": float(quality_score.group(1)) if quality_score else 0.5, + "validationDetails": [{ + "documentName": "AI Validation (Fallback)", + "gapAnalysis": gap_analysis.group(1) if gap_analysis else "Unable to parse detailed analysis", + "successCriteriaMet": [False] # Conservative fallback + }], + "improvementSuggestions": ["NEXT STEP: AI response was malformed - retry the operation for better results"] + } + except Exception as e: + logger.error(f"Fallback extraction failed: {str(e)}") + return None + async def _validateWithAI(self, documents: List[Any], intent: Dict[str, Any]) -> Dict[str, Any]: """AI-based comprehensive validation - single main function""" try: @@ -81,7 +128,10 @@ Perform comprehensive validation: 5. Identify specific gaps and issues 6. Provide actionable next steps -Respond with JSON only: +CRITICAL: Respond with ONLY the JSON object below. Do not include any explanatory text, analysis, or other content before or after the JSON. + +IMPORTANT: Even if the content is binary files (like .docx, .pdf, etc.), you must still respond with JSON only. Do not explain that files are binary - just validate based on file names and types. + {{ "overallSuccess": true/false, "qualityScore": 0.0-1.0, @@ -110,14 +160,63 @@ Respond with JSON only: documents=None, options=request_options ) - if response: - import re - result = response.strip() - json_match = re.search(r'\{.*\}', result, re.DOTALL) - if json_match: - result = json_match.group(0) + + # If first attempt fails, try with more explicit prompt + if response and not self._isValidJsonResponse(response): + logger.warning("First AI validation attempt failed, retrying with explicit JSON-only prompt") + explicitPrompt = f""" +{validationPrompt} + +IMPORTANT: You must respond with ONLY valid JSON. No explanations, no analysis, no text before or after. Just the JSON object. +""" + response = await self.services.ai.callAi( + prompt=explicitPrompt, + documents=None, + options=request_options + ) + + if not response or not response.strip(): + logger.warning("AI validation returned empty response") + return self._createFailedValidationResult("AI validation failed - empty response") + + # Clean and extract JSON from response + result = response.strip() + logger.debug(f"AI validation response length: {len(result)}") + + # Try to find JSON in the response with multiple strategies + import re + + # Strategy 1: Look for JSON in markdown code blocks + json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', result, re.DOTALL) + if json_match: + result = json_match.group(1) + logger.debug(f"Extracted JSON from markdown code block: {result[:200]}...") + else: + # Strategy 2: Look for JSON object with proper structure + json_match = re.search(r'\{[^{}]*"overallSuccess"[^{}]*\}', result, re.DOTALL) + if not json_match: + # Strategy 3: Look for any JSON object + json_match = re.search(r'\{.*\}', result, re.DOTALL) + if not json_match: + logger.debug(f"No JSON found in AI response, trying fallback extraction: {result[:200]}...") + logger.debug(f"Full AI response: {result}") + + # Try fallback extraction for text responses + fallback_result = self._extractFallbackValidationResult(result) + if fallback_result: + logger.info("Using fallback text extraction for validation") + return fallback_result + + logger.warning("All AI validation attempts failed - no JSON found and fallback extraction failed") + return self._createFailedValidationResult("AI validation failed - no JSON in response") + else: + result = json_match.group(0) + logger.debug(f"Extracted JSON directly: {result[:200]}...") + + try: aiResult = json.loads(result) + logger.info("AI validation JSON parsed successfully") return { "overallSuccess": aiResult.get("overallSuccess", False), @@ -129,6 +228,18 @@ Respond with JSON only: }]), "improvementSuggestions": aiResult.get("improvementSuggestions", []) } + + except json.JSONDecodeError as json_error: + logger.warning(f"All AI validation attempts failed - invalid JSON: {str(json_error)}") + logger.debug(f"JSON content: {result}") + + # Try to extract key information from malformed response + fallbackResult = self._extractFallbackValidationResult(result) + if fallbackResult: + logger.info("Using fallback validation result from malformed JSON") + return fallbackResult + + return self._createFailedValidationResult(f"AI validation failed - invalid JSON: {str(json_error)}") return self._createFailedValidationResult("AI validation failed - no response") diff --git a/modules/workflows/processing/adaptive/intentAnalyzer.py b/modules/workflows/processing/adaptive/intentAnalyzer.py index 3e64e111..e7f10cab 100644 --- a/modules/workflows/processing/adaptive/intentAnalyzer.py +++ b/modules/workflows/processing/adaptive/intentAnalyzer.py @@ -48,7 +48,8 @@ Analyze the user's intent and determine: 3. What quality requirements they have (accuracy, completeness, format) 4. What specific success criteria define completion -Respond with JSON only: +CRITICAL: Respond with ONLY the JSON object below. Do not include any explanatory text, analysis, or other content before or after the JSON. + {{ "primaryGoal": "The main objective the user wants to achieve", "dataType": "numbers|text|documents|analysis|code|unknown", @@ -73,15 +74,61 @@ Respond with JSON only: documents=None, options=request_options ) - if response: - import re - result = response.strip() - json_match = re.search(r'\{.*\}', result, re.DOTALL) - if json_match: - result = json_match.group(0) + + # If first attempt fails, try with more explicit prompt + if response and not self._isValidJsonResponse(response): + logger.debug("First AI intent analysis attempt failed, retrying with explicit JSON-only prompt") + explicitPrompt = f""" +{analysisPrompt} + +IMPORTANT: You must respond with ONLY valid JSON. No explanations, no analysis, no text before or after. Just the JSON object. +""" + response = await self.services.ai.callAi( + prompt=explicitPrompt, + documents=None, + options=request_options + ) + + if not response or not response.strip(): + logger.warning("AI intent analysis returned empty response") + return None + + # Clean and extract JSON from response + result = response.strip() + logger.debug(f"AI intent analysis response length: {len(result)}") + + # Try to find JSON in the response with multiple strategies + import re + + # Strategy 1: Look for JSON in markdown code blocks + json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', result, re.DOTALL) + if json_match: + result = json_match.group(1) + logger.debug(f"Extracted JSON from markdown code block: {result[:200]}...") + else: + # Strategy 2: Look for JSON object with proper structure + json_match = re.search(r'\{[^{}]*"primaryGoal"[^{}]*\}', result, re.DOTALL) + if not json_match: + # Strategy 3: Look for any JSON object + json_match = re.search(r'\{.*\}', result, re.DOTALL) + if not json_match: + logger.warning(f"All AI intent analysis attempts failed - no JSON found in response: {result[:200]}...") + logger.debug(f"Full AI response: {result}") + return None + + result = json_match.group(0) + logger.debug(f"Extracted JSON directly: {result[:200]}...") + + try: aiResult = json.loads(result) + logger.info("AI intent analysis JSON parsed successfully") return aiResult + + except json.JSONDecodeError as json_error: + logger.warning(f"All AI intent analysis attempts failed - invalid JSON: {str(json_error)}") + logger.debug(f"JSON content: {result}") + return None return None @@ -118,3 +165,16 @@ Respond with JSON only: "successCriteria": ["Delivers what the user requested"], "confidenceScore": 0.1 } + + def _isValidJsonResponse(self, response: str) -> bool: + """Checks if response contains valid JSON structure""" + try: + import re + # Look for JSON with expected structure + json_match = re.search(r'\{[^{}]*"primaryGoal"[^{}]*\}', response, re.DOTALL) + if json_match: + json.loads(json_match.group(0)) + return True + return False + except: + return False