refactored chat manager

This commit is contained in:
ValueOn AG 2025-07-18 14:20:11 +02:00
parent a602d13e16
commit 02d34b914e
17 changed files with 4711 additions and 3843 deletions

7
app.py
View file

@ -37,6 +37,11 @@ def initLogging():
('.well-known/appspecific/com.chrome.devtools.json' in record.msg or
'Request: /index.html' in record.msg))
# Add filter to exclude all httpcore loggers (including sub-loggers)
class HttpcoreStarFilter(logging.Filter):
def filter(self, record):
return not (record.name == 'httpcore' or record.name.startswith('httpcore.'))
# Add filter to exclude HTTP debug messages
class HTTPDebugFilter(logging.Filter):
def filter(self, record):
@ -61,6 +66,7 @@ def initLogging():
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(consoleFormatter)
consoleHandler.addFilter(ChromeDevToolsFilter())
consoleHandler.addFilter(HttpcoreStarFilter())
consoleHandler.addFilter(HTTPDebugFilter())
handlers.append(consoleHandler)
@ -88,6 +94,7 @@ def initLogging():
)
fileHandler.setFormatter(fileFormatter)
fileHandler.addFilter(ChromeDevToolsFilter())
fileHandler.addFilter(HttpcoreStarFilter())
fileHandler.addFilter(HTTPDebugFilter())
handlers.append(fileHandler)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,124 @@
# Contains all document creation functions extracted from managerChat.py
import logging
import json
from typing import Dict, Any, Optional, List, Union
from datetime import datetime, UTC
class DocumentCreator:
def __init__(self, service):
self.service = service
def getFileExtension(self, filename: str) -> str:
"""Extract file extension from filename"""
return self.service.getFileExtension(filename)
def getMimeType(self, extension: str) -> str:
"""Get MIME type based on file extension"""
return self.service.getMimeTypeFromExtension(extension)
def detectMimeTypeFromContent(self, content: Any, filename: str) -> str:
"""
Detect MIME type from content and filename using service center.
Only returns a detected MIME type if it's better than application/octet-stream.
"""
try:
if isinstance(content, str):
file_bytes = content.encode('utf-8')
elif isinstance(content, dict):
file_bytes = json.dumps(content, ensure_ascii=False).encode('utf-8')
else:
file_bytes = str(content).encode('utf-8')
detected_mime_type = self.service.detectContentTypeFromData(file_bytes, filename)
if detected_mime_type != "application/octet-stream":
return detected_mime_type
return "application/octet-stream"
except Exception as e:
logging.warning(f"Error in MIME type detection for {filename}: {str(e)}")
return 'application/octet-stream'
def detectMimeTypeFromDocument(self, document: Any, filename: str) -> str:
"""
Detect MIME type from document object using service center.
Only returns a detected MIME type if it's better than application/octet-stream.
"""
try:
content = getattr(document, 'content', '')
if isinstance(content, str):
file_bytes = content.encode('utf-8')
else:
file_bytes = str(content).encode('utf-8')
detected_mime_type = self.service.detectContentTypeFromData(file_bytes, filename)
if detected_mime_type != "application/octet-stream":
return detected_mime_type
return "application/octet-stream"
except Exception as e:
logging.warning(f"Error in MIME type detection for document {filename}: {str(e)}")
return 'application/octet-stream'
def convertDocumentDataToString(self, document_data: Dict[str, Any], file_extension: str) -> str:
"""Convert document data to string content based on file type with enhanced processing"""
try:
if document_data is None:
return ""
if isinstance(document_data, str):
return document_data
if isinstance(document_data, dict):
if file_extension == 'json':
return json.dumps(document_data, indent=2, ensure_ascii=False)
elif file_extension in ['txt', 'md', 'html', 'css', 'js', 'py']:
text_fields = ['content', 'text', 'data', 'result', 'summary', 'extracted_content', 'table_data']
for field in text_fields:
if field in document_data:
content = document_data[field]
if isinstance(content, str):
return content
elif isinstance(content, (dict, list)):
return json.dumps(content, indent=2, ensure_ascii=False)
return json.dumps(document_data, indent=2, ensure_ascii=False)
elif file_extension == 'csv':
csv_fields = ['table_data', 'csv_data', 'rows', 'data', 'content', 'text']
for field in csv_fields:
if field in document_data:
content = document_data[field]
if isinstance(content, str):
return content
elif isinstance(content, list):
if content and isinstance(content[0], (list, dict)):
import csv
import io
output = io.StringIO()
if isinstance(content[0], dict):
if content:
fieldnames = content[0].keys()
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(content)
else:
writer = csv.writer(output)
writer.writerows(content)
return output.getvalue()
return json.dumps(document_data, indent=2, ensure_ascii=False)
else:
return json.dumps(document_data, indent=2, ensure_ascii=False)
elif isinstance(document_data, list):
if file_extension == 'csv':
import csv
import io
output = io.StringIO()
if document_data and isinstance(document_data[0], dict):
fieldnames = document_data[0].keys()
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(document_data)
else:
writer = csv.writer(output)
writer.writerows(document_data)
return output.getvalue()
else:
return json.dumps(document_data, indent=2, ensure_ascii=False)
else:
return str(document_data)
except Exception as e:
logging.error(f"Error converting document data to string: {str(e)}")
return str(document_data)

View file

@ -0,0 +1,53 @@
# executionState.py
# Contains all execution state management logic extracted from managerChat.py
from typing import List
from modules.interfaces.interfaceChatModel import TaskStep, ActionExecutionResult
class TaskExecutionState:
"""Manages state during task execution with retry logic"""
def __init__(self, task_step: TaskStep):
self.task_step = task_step
self.successful_actions: List[ActionExecutionResult] = [] # Preserved across retries
self.failed_actions: List[ActionExecutionResult] = [] # For analysis
self.current_action_index = 0
self.retry_count = 0
self.improvements = []
self.partial_results = {} # Store intermediate results
self.max_retries = 3
def addSuccessfulAction(self, action_result: ActionExecutionResult):
self.successful_actions.append(action_result)
if action_result.data.get('resultLabel'):
self.partial_results[action_result.data['resultLabel']] = action_result
def addFailedAction(self, action_result: ActionExecutionResult):
self.failed_actions.append(action_result)
def getAvailableResults(self) -> list:
return [result.data.get('resultLabel', '') for result in self.successful_actions if result.data.get('resultLabel')]
def shouldRetryTask(self) -> bool:
return len(self.successful_actions) > 0 and len(self.failed_actions) > 0
def canRetry(self) -> bool:
return self.retry_count < self.max_retries
def incrementRetryCount(self):
self.retry_count += 1
def getFailurePatterns(self) -> list:
patterns = []
for action in self.failed_actions:
error = action.error.lower() if action.error else ''
if "timeout" in error:
patterns.append("timeout_issues")
elif "document_not_found" in error or "file not found" in error:
patterns.append("document_reference_issues")
elif "empty_result" in error or "no content" in error:
patterns.append("content_extraction_issues")
elif "invalid_format" in error or "wrong format" in error:
patterns.append("format_issues")
elif "permission" in error or "access denied" in error:
patterns.append("permission_issues")
return list(set(patterns))

View file

@ -0,0 +1,432 @@
# handlingActions.py
# Contains all action handling functions extracted from managerChat.py
import logging
import json
import time
from typing import Dict, Any, Optional, List, Union
from datetime import datetime, UTC
from modules.interfaces.interfaceChatModel import ReviewResult, ActionResult
from modules.chat.documents.documentCreation import DocumentCreator
from .promptFactory import createResultReviewPrompt
logger = logging.getLogger(__name__)
class HandlingActions:
def __init__(self, service, chatInterface):
self.service = service
self.chatInterface = chatInterface
self.documentCreator = DocumentCreator(self.service)
async def validateActionResult(self, action_result, action, context) -> dict:
try:
prompt = self._createGenericValidationPrompt(action_result, action, context)
response = await self._callAIWithCircuitBreaker(prompt, "action_validation")
validation = self._parseValidationResponse(response)
validation['action_id'] = action.id
validation['action_method'] = action.execMethod
validation['action_name'] = action.execAction
validation['result_label'] = action.execResultLabel
return validation
except Exception as e:
logger.error(f"Error validating action result: {str(e)}")
return {
'status': 'success',
'reason': f'Validation failed: {str(e)}',
'confidence': 0.5,
'improvements': [],
'action_id': action.id,
'action_method': action.execMethod,
'action_name': action.execAction,
'result_label': action.execResultLabel
}
def _createGenericValidationPrompt(self, action_result, action, context) -> str:
success = action_result.success
result_data = action_result.data
error = action_result.error
validation_messages = action_result.validation
result_text = result_data.get("result", "") if isinstance(result_data, dict) else str(result_data)
documents = result_data.get("documents", []) if isinstance(result_data, dict) else []
doc_count = len(documents)
expected_result_label = action.execResultLabel
expected_format = action.execParameters.get('outputFormat', 'unknown')
expected_document_formats = action.expectedDocumentFormats or []
actual_result_label = result_data.get("resultLabel", "") if isinstance(result_data, dict) else ""
result_label_match = actual_result_label == expected_result_label
delivered_files = []
delivered_formats = []
content_items = []
for doc in documents:
if hasattr(doc, 'filename'):
delivered_files.append(doc.filename)
file_extension = self._getFileExtension(doc.filename)
mime_type = getattr(doc, 'mimeType', 'application/octet-stream')
delivered_formats.append({
'filename': doc.filename,
'extension': file_extension,
'mimeType': mime_type
})
elif isinstance(doc, dict) and 'filename' in doc:
delivered_files.append(doc['filename'])
file_extension = self._getFileExtension(doc['filename'])
mime_type = doc.get('mimeType', 'application/octet-stream')
delivered_formats.append({
'filename': doc['filename'],
'extension': file_extension,
'mimeType': mime_type
})
else:
delivered_files.append(f"document_{len(delivered_files)}")
delivered_formats.append({
'filename': f"document_{len(delivered_files)}",
'extension': 'unknown',
'mimeType': 'application/octet-stream'
})
if isinstance(result_data, dict):
if 'extractedContent' in result_data:
extracted_content = result_data['extractedContent']
if hasattr(extracted_content, 'contents'):
content_items = extracted_content.contents
elif 'contents' in result_data:
content_items = result_data['contents']
if delivered_files and not content_items:
content_items = [f"File content available in: {', '.join(delivered_files)}"]
content_summary = []
for item in content_items:
if hasattr(item, 'label') and hasattr(item, 'metadata'):
content_summary.append(f"{item.label}: {item.metadata.mimeType if hasattr(item.metadata, 'mimeType') else 'unknown'}")
elif isinstance(item, str):
content_summary.append(item)
else:
content_summary.append(str(item))
return f"""You are an action result validator. Your primary focus is to validate that the action delivered the promised result files in the promised format.\n\nACTION DETAILS:\n- Method: {action.execMethod}\n- Action: {action.execAction}\n- Expected Result Label: {expected_result_label}\n- Actual Result Label: {actual_result_label}\n- Result Label Match: {result_label_match}\n- Expected Format: {expected_format}\n- Expected Document Formats: {json.dumps(expected_document_formats, indent=2) if expected_document_formats else 'None specified'}\n- Parameters: {json.dumps(action.execParameters, indent=2)}\n\nRESULT TO VALIDATE:\n- Success: {success}\n- Result Data: {result_text[:500]}{'...' if len(result_text) > 500 else ''}\n- Error: {error}\n- Validation Messages: {', '.join(validation_messages) if validation_messages else 'None'}\n- Documents Produced: {doc_count}\n- Delivered Files: {', '.join(delivered_files) if delivered_files else 'None'}\n- Delivered Formats: {json.dumps(delivered_formats, indent=2) if delivered_formats else 'None'}\n- Content Items: {', '.join(content_summary) if content_summary else 'None'}\n\nCRITICAL VALIDATION CRITERIA:\n1. **Result Label Match**: Does the action result contain the expected result label?\n2. **File Delivery**: Did the action deliver the promised result file(s)?\n3. **Format Compliance**: If expected document formats were specified, do the delivered files match the expected formats?\n4. **Content Quality**: Is the content of the delivered files usable and complete?\n5. **Content Processing**: If content extraction was expected, was it performed correctly?\n\nCONTEXT:\n- Task Description: {context.task_step.description if context.task_step else 'Unknown'}\n- Previous Results: {', '.join(context.previous_results) if context.previous_results else 'None'}\n\nVALIDATION INSTRUCTIONS:\n1. **Result Label Check**: Verify that the expected result label \"{expected_result_label}\" is present in the action result data. This is the primary success criterion.\n2. **File Delivery**: Check if files were delivered when expected. The individual filenames don't need to match the result label - focus on whether content was actually produced.\n3. **Format Compliance**: If expected document formats were specified, check if delivered files match the expected extensions and MIME types. If no formats were specified, this criterion is satisfied.\n4. **Content Quality**: If files were delivered, consider the action successful. The presence of delivered files indicates content was processed and stored.\n5. **Content Processing**: If files were delivered, assume content extraction was performed correctly. The file delivery is evidence of successful processing.\n6. **Success Criteria**: The action is successful if the result label matches AND files were delivered. If expected formats were specified, they should also match.\n\nIMPORTANT NOTES:\n- The result label must be present in the action result data for success\n- Individual filenames can be different from the result label\n- If files were delivered, consider the action successful even if content details are not provided\n- Focus on whether the action accomplished its intended purpose (file delivery)\n- Empty files should be considered failures, but delivered files indicate success\n\nREQUIRED JSON RESPONSE:\n{{\n \"status\": \"success|retry|fail\",\n \"reason\": \"Detailed explanation focusing on result label match and content quality\",\n \"confidence\": 0.0-1.0,\n \"improvements\": [\"specific improvements if needed\"],\n \"quality_score\": 1-10,\n \"missing_elements\": [\"missing result label\", \"missing files\", \"content issues\"],\n \"suggested_retry_approach\": \"Specific approach for retry if status is retry\"\n}}\n\nNOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
def _parseValidationResponse(self, response: str) -> dict:
try:
json_start = response.find('{')
json_end = response.rfind('}') + 1
if json_start == -1 or json_end == 0:
raise ValueError("No JSON found in validation response")
json_str = response[json_start:json_end]
validation = json.loads(json_str)
if 'status' not in validation:
raise ValueError("Validation response missing 'status' field")
validation.setdefault('confidence', 0.5)
validation.setdefault('improvements', [])
validation.setdefault('quality_score', 5)
validation.setdefault('missing_elements', [])
validation.setdefault('suggested_retry_approach', '')
return validation
except Exception as e:
logger.error(f"Error parsing validation response: {str(e)}")
return {
'status': 'success',
'reason': f'Parse error: {str(e)}',
'confidence': 0.5,
'improvements': [],
'quality_score': 5,
'missing_elements': [],
'suggested_retry_approach': ''
}
async def executeSingleAction(self, action, workflow):
"""Execute a single action and return ActionResult with enhanced document processing"""
try:
# Use DocumentCreator methods
# Enhance parameters with expected document formats if specified
enhanced_parameters = action.execParameters.copy()
if action.expectedDocumentFormats:
enhanced_parameters['expectedDocumentFormats'] = action.expectedDocumentFormats
logger.info(f"Action {action.execMethod}.{action.execAction} expects formats: {action.expectedDocumentFormats}")
result = await self.service.executeAction(
methodName=action.execMethod,
actionName=action.execAction,
parameters=enhanced_parameters
)
result_label = action.execResultLabel
if result.success:
action.setSuccess()
action.result = result.data.get("result", "")
action.execResultLabel = result_label
await self.createActionMessage(action, result, workflow, result_label)
else:
action.setError(result.error or "Action execution failed")
documents = result.data.get("documents", [])
processed_documents = []
for doc in documents:
if hasattr(doc, 'filename') and doc.filename:
mime_type = getattr(doc, 'mimeType', 'application/octet-stream')
if mime_type == "application/octet-stream":
mime_type = self.documentCreator.detectMimeTypeFromDocument(doc, doc.filename)
processed_documents.append({
'filename': doc.filename,
'fileSize': getattr(doc, 'fileSize', 0),
'mimeType': mime_type,
'content': getattr(doc, 'content', ''),
'document': doc
})
elif isinstance(doc, dict):
filename = doc.get('documentName', doc.get('filename', f"{action.execMethod}_{action.execAction}_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}"))
fileSize = doc.get('fileSize', len(str(doc.get('documentData', ''))))
mimeType = doc.get('mimeType', 'application/octet-stream')
if mimeType == "application/octet-stream":
document_data = doc.get('documentData', '')
mimeType = self.documentCreator.detectMimeTypeFromContent(document_data, filename)
processed_documents.append({
'filename': filename,
'fileSize': fileSize,
'mimeType': mimeType,
'content': doc.get('documentData', ''),
'document': doc
})
else:
logger.warning(f"Unknown document type for action {action.execMethod}.{action.execAction}: {type(doc)}")
filename = f"{action.execMethod}_{action.execAction}_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}"
mimeType = 'application/octet-stream'
mimeType = self.documentCreator.detectMimeTypeFromContent(doc, filename)
processed_documents.append({
'filename': filename,
'fileSize': 0,
'mimeType': mimeType,
'content': str(doc),
'document': doc
})
return ActionResult(
success=result.success,
data={
"result": result.data.get("result", ""),
"documents": processed_documents,
"actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction,
"resultLabel": result_label
},
metadata={
"actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction,
"resultLabel": result_label
},
validation=[],
error=result.error or ""
)
except Exception as e:
logger.error(f"Error executing single action: {str(e)}")
action.setError(str(e))
return ActionResult(
success=False,
data={
"actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction,
"documents": []
},
metadata={
"actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction
},
validation=[],
error=str(e)
)
async def createActionMessage(self, action, result, workflow, result_label=None):
"""Create and store a message for the action result in the workflow with enhanced document processing"""
try:
# Use DocumentCreator methods
result_data = result.data if hasattr(result, 'data') else {}
documents_data = result_data.get("documents", [])
if result_label is None:
result_label = action.execResultLabel
message_data = {
"workflowId": workflow.id,
"role": "assistant",
"message": f"Executed action {action.execMethod}.{action.execAction}",
"status": "step",
"sequenceNr": len(workflow.messages) + 1,
"publishedAt": datetime.now(UTC).isoformat(),
"actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction,
"documentsLabel": result_label, # Use intent label from action definition
"documents": []
}
if documents_data:
processed_documents = []
for doc_data in documents_data:
try:
if isinstance(doc_data, dict):
document_name = doc_data.get("documentName", doc_data.get("filename", f"{action.execMethod}_{action.execAction}_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}"))
document_data = doc_data.get("documentData", {})
file_size = doc_data.get("fileSize", 0)
mime_type = doc_data.get("mimeType", "application/octet-stream")
elif hasattr(doc_data, 'filename'):
document_name = doc_data.filename
document_data = getattr(doc_data, 'content', {})
file_size = getattr(doc_data, 'fileSize', 0)
mime_type = getattr(doc_data, 'mimeType', "application/octet-stream")
else:
document_name = f"{action.execMethod}_{action.execAction}_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}"
document_data = doc_data
file_size = len(str(doc_data))
mime_type = "application/octet-stream"
if mime_type == "application/octet-stream":
mime_type = self.documentCreator.detectMimeTypeFromContent(document_data, document_name)
content = self.documentCreator.convertDocumentDataToString(document_data, self.documentCreator.getFileExtension(document_name))
minimal_content_patterns = ['{}', '[]', 'null', '""', "''"]
if not content or content.strip() == "" or content.strip() in minimal_content_patterns:
logger.warning(f"Empty or minimal content for document {document_name}, skipping")
continue
file_id = self.service.createFile(
fileName=document_name,
mimeType=mime_type,
content=content,
base64encoded=False
)
if not file_id:
logger.error(f"Failed to create file for document {document_name}")
continue
document = self.service.createDocument(
fileName=document_name,
mimeType=mime_type,
content=content,
base64encoded=False
)
if document:
processed_documents.append(document)
logger.info(f"Created document: {document_name} with file ID: {file_id} and MIME type: {mime_type}")
else:
logger.error(f"Failed to create ChatDocument object for {document_name}")
except Exception as e:
logger.error(f"Error processing document {getattr(doc_data, 'documentName', 'unknown') if isinstance(doc_data, dict) else 'unknown'}: {str(e)}")
continue
message_data["documents"] = processed_documents
message = self.chatInterface.createWorkflowMessage(message_data)
if message:
workflow.messages.append(message)
logger.info(f"Created action message for {action.execMethod}.{action.execAction} with {len(message_data.get('documents', []))} documents")
logger.debug(f"WORKFLOW STATE after createActionMessage: id={id(workflow)}, message_count={len(workflow.messages)}")
for idx, msg in enumerate(workflow.messages):
label = getattr(msg, 'documentsLabel', None)
docs = getattr(msg, 'documents', None)
logger.debug(f" Message {idx}: label='{label}', documents_count={len(docs) if docs else 0}")
else:
logger.error(f"Failed to create workflow message for action {action.execMethod}.{action.execAction}")
except Exception as e:
logger.error(f"Error creating action message: {str(e)}")
async def performTaskReview(self, review_context) -> 'ReviewResult':
"""Perform AI-based task review with enhanced retry logic"""
try:
# Prepare prompt for result review
prompt = await createResultReviewPrompt(self, review_context)
# Call AI with circuit breaker
response = await self._callAIWithCircuitBreaker(prompt, "result_review")
# Parse review result
review_dict = self._parseReviewResponse(response)
# Add default values for missing fields
review_dict.setdefault('status', 'unknown')
review_dict.setdefault('reason', 'No reason provided')
review_dict.setdefault('quality_score', 5)
# Enhanced retry logic based on result quality
if review_dict.get('status') == 'retry':
# Analyze the specific issues for better retry guidance
action_results = review_context.action_results or []
if action_results:
# Check for common issues that warrant retry
# Only consider empty results a problem if there are no documents produced
has_empty_results = any(
not result.data.get('result', '').strip() and
not result.data.get('documents') and
not result.data.get('documents')
for result in action_results
if result.success
)
has_incomplete_metadata = any(
any(doc.get('filename') == 'unknown' for doc in result.data.get('documents', []) or [])
for result in action_results
if result.success
)
if has_empty_results:
review_dict['improvements'] = (review_dict.get('improvements', '') +
" Ensure the document extraction returns actual content, not empty results. " +
"Check if the AI prompt is specific enough to extract meaningful data.")
if has_incomplete_metadata:
review_dict['improvements'] = (review_dict.get('improvements', '') +
" Ensure proper document metadata is extracted including filename, size, and mime type. " +
"The document processing should provide complete file information.")
# If we have specific issues, adjust quality score
if has_empty_results or has_incomplete_metadata:
review_dict['quality_score'] = max(1, review_dict.get('quality_score', 5) - 2)
# Create ReviewResult model
return ReviewResult(
status=review_dict.get('status', 'unknown'),
reason=review_dict.get('reason', 'No reason provided'),
improvements=review_dict.get('improvements', []),
quality_score=review_dict.get('quality_score', 5),
missing_outputs=review_dict.get('missing_outputs', []),
met_criteria=review_dict.get('met_criteria', []),
unmet_criteria=review_dict.get('unmet_criteria', []),
confidence=review_dict.get('confidence', 0.5)
)
except Exception as e:
logger.error(f"Error performing task review: {str(e)}")
return ReviewResult(
status='success', # Default to success to avoid blocking workflow
reason=f'Review failed: {str(e)}',
quality_score=5,
confidence=0.5
)
def parseActionResponse(self, response: str) -> list:
"""Parse AI response into action list"""
try:
json_start = response.find('{')
json_end = response.rfind('}') + 1
if json_start == -1 or json_end == 0:
raise ValueError("No JSON found in response")
json_str = response[json_start:json_end]
action_data = json.loads(json_str)
if 'actions' not in action_data:
raise ValueError("Action response missing 'actions' field")
return action_data['actions']
except Exception as e:
logger.error(f"Error parsing action response: {str(e)}")
return []
def parseReviewResponse(self, response: str) -> dict:
"""Parse AI response into review result"""
try:
json_start = response.find('{')
json_end = response.rfind('}') + 1
if json_start == -1 or json_end == 0:
raise ValueError("No JSON found in response")
json_str = response[json_start:json_end]
review = json.loads(json_str)
if 'status' not in review:
raise ValueError("Review response missing 'status' field")
return review
except Exception as e:
logger.error(f"Error parsing review response: {str(e)}")
return {'status': 'failed', 'reason': f'Parse error: {str(e)}'}
# Utility method for file extension
def _getFileExtension(self, filename):
if '.' in filename:
return filename.rsplit('.', 1)[-1].lower()
return ''
# Placeholder methods for AI and prompt logic (to be implemented or injected)
async def _callAIWithCircuitBreaker(self, prompt, purpose):
raise NotImplementedError("_callAIWithCircuitBreaker must be implemented in the subclass or injected.")

View file

@ -0,0 +1,291 @@
# handlingTasks.py
# Refactored for clarity and consolidation
import asyncio
import logging
import json
import time
from typing import Dict, Any, Optional, List, Union
from datetime import datetime, UTC
from modules.interfaces.interfaceChatModel import (
TaskStatus, TaskStep, TaskContext, TaskAction, ActionExecutionResult, ReviewResult, TaskPlan, WorkflowResult, TaskResult, ReviewContext
)
from .executionState import TaskExecutionState
from .handlingActions import HandlingActions
from .promptFactory import createTaskPlanningPrompt, createActionDefinitionPrompt, createResultReviewPrompt
logger = logging.getLogger(__name__)
class HandlingTasks:
def __init__(self, chatInterface, service, workflow=None):
self.chatInterface = chatInterface
self.service = service
self.workflow = workflow
self.handlingActions = HandlingActions(service, chatInterface)
async def generateTaskPlan(self, userInput: str, workflow) -> TaskPlan:
"""Generate a high-level task plan for the workflow."""
try:
logger.info(f"Generating task plan for workflow {workflow.id}")
prompt = await self.service.callAiTextAdvanced(
createTaskPlanningPrompt(self, {
'user_request': userInput,
'available_documents': self._getAvailableDocuments(workflow),
'workflow_id': workflow.id
})
)
task_plan_dict = self._parseTaskPlanResponse(prompt)
if not self._validateTaskPlan(task_plan_dict):
logger.error("Generated task plan failed validation")
raise Exception("AI-generated task plan failed validation - AI is required for task planning")
tasks = [TaskStep(**task_dict) for task_dict in task_plan_dict.get('tasks', [])]
return TaskPlan(
overview=task_plan_dict.get('overview', ''),
tasks=tasks
)
except Exception as e:
logger.error(f"Error in generateTaskPlan: {str(e)}")
raise
async def generateTaskActions(self, task_step, workflow, previous_results=None, enhanced_context=None) -> List[TaskAction]:
"""Generate actions for a given task step."""
try:
logger.info(f"Generating actions for task: {task_step.description}")
context = enhanced_context or TaskContext(
task_step=task_step,
workflow=workflow,
workflow_id=workflow.id,
available_documents=self._getAvailableDocuments(workflow),
previous_results=previous_results or [],
improvements=[],
retry_count=0,
previous_action_results=[],
previous_review_result=None,
is_regeneration=False,
failure_patterns=[],
failed_actions=[],
successful_actions=[]
)
prompt = await self.service.callAiTextAdvanced(
createActionDefinitionPrompt(self, context)
)
actions = self.handlingActions.parseActionResponse(prompt)
if not self._validateActions(actions, context):
logger.error("Generated actions failed validation")
raise Exception("AI-generated actions failed validation - AI is required for action generation")
# Convert to TaskAction objects
task_actions = [self.chatInterface.createTaskAction({
"execMethod": a.get('method', 'unknown'),
"execAction": a.get('action', 'unknown'),
"execParameters": a.get('parameters', {}),
"execResultLabel": a.get('resultLabel', ''),
"expectedDocumentFormats": a.get('expectedDocumentFormats', None),
"status": TaskStatus.PENDING
}) for a in actions]
return [ta for ta in task_actions if ta]
except Exception as e:
logger.error(f"Error in generateTaskActions: {str(e)}")
return []
async def executeTask(self, task_step, workflow, context) -> TaskResult:
"""Execute all actions for a task step, with state management and retries."""
logger.info(f"Executing task: {task_step.description}")
state = TaskExecutionState(task_step)
retry_context = context
max_retries = state.max_retries
for attempt in range(max_retries):
logger.info(f"Task execution attempt {attempt+1}/{max_retries}")
actions = await self.generateTaskActions(task_step, workflow, previous_results=retry_context.previous_results, enhanced_context=retry_context)
if not actions:
logger.error("No actions defined for task step, aborting task execution")
break
action_results = []
for action in actions:
result = await self.handlingActions.executeSingleAction(action, workflow)
action_results.append(result)
if result.success:
state.addSuccessfulAction(result)
else:
state.addFailedAction(result)
review_result = await self.reviewTaskCompletion(task_step, actions, action_results, workflow)
success = review_result.status == 'success'
feedback = review_result.reason
error = None if success else review_result.reason
if success:
logger.info(f"Task step '{task_step.description}' completed successfully")
return TaskResult(
taskId=task_step.id,
status=TaskStatus.COMPLETED,
success=True,
feedback=feedback,
error=None
)
elif review_result.status == 'retry' and state.canRetry():
logger.warning(f"Task step '{task_step.description}' requires retry: {review_result.improvements}")
state.incrementRetryCount()
retry_context.retry_count = state.retry_count
retry_context.improvements = review_result.improvements
retry_context.previous_action_results = action_results
retry_context.previous_review_result = review_result
retry_context.is_regeneration = True
retry_context.failure_patterns = state.getFailurePatterns()
retry_context.failed_actions = state.failed_actions
retry_context.successful_actions = state.successful_actions
continue
else:
logger.error(f"Task step '{task_step.description}' failed after {attempt+1} attempts")
return TaskResult(
taskId=task_step.id,
status=TaskStatus.FAILED,
success=False,
feedback=feedback,
error=error
)
logger.error(f"Task step '{task_step.description}' failed after all retries")
return TaskResult(
taskId=task_step.id,
status=TaskStatus.FAILED,
success=False,
feedback="Task failed after all retries.",
error="Task failed after all retries."
)
async def reviewTaskCompletion(self, task_step, task_actions, action_results, workflow):
try:
review_context = ReviewContext(
task_step=task_step,
action_results=action_results,
workflow=workflow,
step_result={
'successful_actions': sum(1 for result in action_results if result.success),
'total_actions': len(action_results),
'results': [result.data.get('result', '') for result in action_results if result.success],
'errors': [result.error for result in action_results if not result.success]
}
)
# Use promptFactory for review prompt
prompt = await createResultReviewPrompt(self, review_context)
response = await self.service.callAiTextAdvanced(prompt)
review_dict = self.handlingActions.parseReviewResponse(response)
review_dict.setdefault('status', 'unknown')
review_dict.setdefault('reason', 'No reason provided')
review_dict.setdefault('quality_score', 5)
return ReviewResult(
status=review_dict.get('status', 'unknown'),
reason=review_dict.get('reason', 'No reason provided'),
improvements=review_dict.get('improvements', []),
quality_score=review_dict.get('quality_score', 5),
missing_outputs=review_dict.get('missing_outputs', []),
met_criteria=review_dict.get('met_criteria', []),
unmet_criteria=review_dict.get('unmet_criteria', []),
confidence=review_dict.get('confidence', 0.5)
)
except Exception as e:
logger.error(f"Error in reviewTaskCompletion: {str(e)}")
return ReviewResult(
status='failed',
reason=str(e),
quality_score=0
)
async def prepareTaskHandover(self, task_step, task_actions, review_result, workflow):
try:
handover_data = {
'task_id': task_step.id,
'task_description': task_step.description,
'actions': [action.to_dict() for action in task_actions],
'review_result': review_result.to_dict() if hasattr(review_result, 'to_dict') else review_result,
'workflow_id': workflow.id,
'handover_time': datetime.now(UTC).isoformat()
}
logger.info(f"Prepared handover for task {task_step.id} in workflow {workflow.id}")
return handover_data
except Exception as e:
logger.error(f"Error in prepareTaskHandover: {str(e)}")
return {'error': str(e)}
# --- Helper and validation methods (unchanged, but can be inlined or made private) ---
def _getAvailableDocuments(self, workflow):
documents = []
for message in workflow.messages:
for doc in message.documents:
documents.append(doc.filename)
return documents
def _parseTaskPlanResponse(self, response: str) -> dict:
try:
json_start = response.find('{')
json_end = response.rfind('}') + 1
if json_start == -1 or json_end == 0:
raise ValueError("No JSON found in response")
json_str = response[json_start:json_end]
task_plan = json.loads(json_str)
if 'tasks' not in task_plan:
raise ValueError("Task plan missing 'tasks' field")
return task_plan
except Exception as e:
logger.error(f"Error parsing task plan response: {str(e)}")
return {'tasks': []}
def _validateTaskPlan(self, task_plan: Dict[str, Any]) -> bool:
try:
if not isinstance(task_plan, dict):
return False
if 'tasks' not in task_plan or not isinstance(task_plan['tasks'], list):
return False
task_ids = set()
for task in task_plan['tasks']:
if not isinstance(task, dict):
return False
required_fields = ['id', 'description', 'expected_outputs', 'success_criteria']
if not all(field in task for field in required_fields):
return False
if task['id'] in task_ids:
return False
task_ids.add(task['id'])
dependencies = task.get('dependencies', [])
if not isinstance(dependencies, list):
return False
for dep in dependencies:
if dep not in task_ids and dep != 'task_0':
return False
if 'ai_prompt' in task and not isinstance(task['ai_prompt'], str):
return False
return True
except Exception as e:
logger.error(f"Error validating task plan: {str(e)}")
return False
def _validateActions(self, actions: List[Dict[str, Any]], context) -> bool:
try:
if not isinstance(actions, list):
logger.error("Actions must be a list")
return False
if len(actions) == 0:
logger.warning("No actions generated")
return False
for i, action in enumerate(actions):
if not isinstance(action, dict):
logger.error(f"Action {i} must be a dictionary")
return False
required_fields = ['method', 'action', 'parameters', 'resultLabel']
missing_fields = []
for field in required_fields:
if field not in action or not action[field]:
missing_fields.append(field)
if missing_fields:
logger.error(f"Action {i} missing required fields: {missing_fields}")
return False
result_label = action.get('resultLabel', '')
if not result_label.startswith('task'):
logger.error(f"Action {i} result label must start with 'task': {result_label}")
return False
parameters = action.get('parameters', {})
if not isinstance(parameters, dict):
logger.error(f"Action {i} parameters must be a dictionary")
return False
logger.info(f"Successfully validated {len(actions)} actions")
return True
except Exception as e:
logger.error(f"Error validating actions: {str(e)}")
return False

View file

@ -0,0 +1,405 @@
# promptFactory.py
# Contains all prompt creation functions extracted from managerChat.py
import json
import logging
from typing import Any, Dict
# Prompt creation helpers extracted from managerChat.py
def createTaskPlanningPrompt(self, context: Dict[str, Any]) -> str:
"""Create prompt for task planning"""
return f"""You are a task planning AI that analyzes user requests and creates structured task plans.
USER REQUEST: {context['user_request']}
AVAILABLE DOCUMENTS: {', '.join(context['available_documents'])}
INSTRUCTIONS:
1. Analyze the user request and available documents
2. Break down the request into 2-4 meaningful high-level task steps
3. Focus on business outcomes, not technical operations
4. For document processing, create ONE task with a comprehensive AI prompt rather than multiple granular tasks
5. Each task should produce meaningful, usable outputs
6. Ensure proper handover between tasks using result labels
7. Return a JSON object with the exact structure shown below
TASK PLANNING PRINCIPLES:
- Combine related operations into single tasks (e.g., \"Extract and analyze all candidate profiles\" instead of separate \"read file\" and \"analyze content\" tasks)
- Use comprehensive AI prompts for document processing rather than multiple small tasks
- Focus on business value and outcomes
- Keep tasks at a meaningful level of abstraction
- Each task should produce results that can be used by subsequent tasks
REQUIRED JSON STRUCTURE:
{{
\"overview\": \"Brief description of the overall plan\",
\"tasks\": [
{{
\"id\": \"task_1\",
\"description\": \"Clear description of what this task accomplishes (business outcome)\",
\"dependencies\": [\"task_0\"], // IDs of tasks that must complete first
\"expected_outputs\": [\"output1\", \"output2\"],
\"success_criteria\": [\"criteria1\", \"criteria2\"],
\"required_documents\": [\"doc1\", \"doc2\"],
\"estimated_complexity\": \"low|medium|high\",
\"ai_prompt\": \"Comprehensive AI prompt for document processing tasks (if applicable)\"
}}
]
}}
EXAMPLES OF GOOD TASK DESCRIPTIONS:
- \"Extract and analyze all candidate profiles to identify key qualifications and experience\"
- \"Create evaluation matrix and rate candidates against product designer criteria\"
- \"Generate comprehensive PowerPoint presentation for management decision\"
- \"Store final presentation in SharePoint for specified account\"
EXAMPLES OF BAD TASK DESCRIPTIONS:
- \"Open and read the PDF file\" (too granular)
- \"Identify table structure\" (technical detail)
- \"Convert data to CSV format\" (implementation detail)
NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
async def createActionDefinitionPrompt(self, context) -> str:
"""Create prompt for action generation with enhanced document extraction guidance and retry context"""
task_step = context.task_step
workflow = context.workflow
available_docs = context.available_documents or []
previous_results = context.previous_results or []
improvements = context.improvements or []
retry_count = context.retry_count or 0
previous_action_results = context.previous_action_results or []
previous_review_result = context.previous_review_result
methodList = self.service.getMethodsList()
method_actions = {}
for sig in methodList:
if '.' in sig:
method, rest = sig.split('.', 1)
action = rest.split('(')[0]
method_actions.setdefault(method, []).append((action, sig))
messageSummary = await self.service.summarizeChat(workflow.messages)
docRefs = self.service.getDocumentReferenceList()
connRefs = self.service.getConnectionReferenceList()
all_doc_refs = docRefs.get('chat', []) + docRefs.get('history', [])
available_methods_str = ''
for method, actions in method_actions.items():
available_methods_str += f"- {method}:\n"
for action, sig in actions:
available_methods_str += f" - {action}: {sig}\n"
task_ai_prompt = task_step.ai_prompt or ''
retry_context = ""
if retry_count > 0:
retry_context = f"""
RETRY CONTEXT (Attempt {retry_count}):
Previous action results that failed or were incomplete:
"""
for i, result in enumerate(previous_action_results):
retry_context += f"- Action {i+1}: {result.actionMethod or 'unknown'}.{result.actionName or 'unknown'}\n"
retry_context += f" Status: {result.success and 'success' or 'failed'}\n"
retry_context += f" Error: {result.error or 'None'}\n"
retry_context += f" Result: {(result.data.get('result', '') if result.data else '')[:100]}...\n"
if previous_review_result:
retry_context += f"""
Previous review feedback:
- Status: {previous_review_result.status or 'unknown'}
- Reason: {previous_review_result.reason or 'No reason provided'}
- Quality Score: {previous_review_result.quality_score or 0}/10
- Missing Outputs: {', '.join(previous_review_result.missing_outputs or [])}
- Unmet Criteria: {', '.join(previous_review_result.unmet_criteria or [])}
"""
expected_outputs_str = ', '.join(task_step.expected_outputs or [])
success_criteria_str = ', '.join(task_step.success_criteria or [])
previous_results_str = ', '.join(previous_results) if previous_results else 'None'
improvements_str = str(improvements) if improvements else 'None'
available_connections_str = '\n'.join(f"- {conn}" for conn in connRefs)
available_documents_str = '\n'.join(
f"- {doc.documentsLabel} contains {', '.join(doc.documents)}" for doc in all_doc_refs
)
prompt = f"""
You are an action generation AI that creates specific actions to accomplish a task step.
DOCUMENT REFERENCE TYPES:
- docItem: Reference to a single document. Format: "docItem:<id>:<filename>"
- docList: Reference to a group of documents under a label. Format: <label> (e.g., "task1_action2_results").
- Each docList label maps to a list of docItem references (see AVAILABLE DOCUMENTS).
- A label like "task1_action2_results" refers to the output of action 2 in task 1.
CRITICAL DOCUMENT REFERENCE RULES:
- ONLY use the exact labels listed in AVAILABLE DOCUMENTS below
- NEVER invent new labels or use message IDs
- NEVER use formats like "msg_xxx:documents" or "task_X_results" (these will fail)
- ONLY use the exact labels shown in AVAILABLE DOCUMENTS
- **When generating multiple actions, you may only use as input documents those that are already present in AVAILABLE DOCUMENTS or produced by actions that come earlier in the list. Do NOT use as input any document label that will be produced by a later action.**
TASK STEP: {task_step.description} (ID: {task_step.id})
EXPECTED OUTPUTS: {expected_outputs_str}
SUCCESS CRITERIA: {success_criteria_str}
TASK AI PROMPT: {task_ai_prompt if task_ai_prompt else 'None provided'}
CONTEXT - Chat History:
{messageSummary}
AVAILABLE METHODS AND ACTIONS (with signatures):
{available_methods_str}
AVAILABLE CONNECTIONS:
{available_connections_str}
AVAILABLE DOCUMENTS:
{available_documents_str}
DOCUMENT REFERENCE EXAMPLES:
CORRECT: Use exact labels from AVAILABLE DOCUMENTS above
- "task2_action1_personnel_search"
- "task2_action3_personnel_analysis"
- "docItem:doc_abc:file1.txt"
- "docList:msg123:user_uploads" (supported format, but use actual labels instead)
INCORRECT: These will cause errors
- "msg_xxx:documents" (invalid format - missing docList/docItem prefix)
- "task_2_results" (not a valid label - use exact labels from AVAILABLE DOCUMENTS)
- Inventing message IDs instead of using actual document labels
PREVIOUS RESULTS: {previous_results_str}
IMPROVEMENTS NEEDED: {improvements_str}{retry_context}
ACTION GENERATION PRINCIPLES:
- Create meaningful actions per task step
- Use comprehensive AI prompts for document processing
- Focus on business outcomes, not technical operations
- Combine related operations into single actions when possible
- Use the task's AI prompt if provided, or create a comprehensive one
- Each action should produce meaningful, usable outputs
- For document extraction, ensure prompts are specific and detailed
- Include validation steps in extraction prompts
- If this is a retry, learn from previous failures and improve the approach
- Address specific issues mentioned in previous review feedback
- When specifying expectedDocumentFormats, ensure AI prompts explicitly request pure data without markdown formatting
INSTRUCTIONS:
- Generate actions to accomplish this task step using available documents, connections, and previous results
- Use docItem for single documents and docList labels for groups of documents as shown in AVAILABLE DOCUMENTS
- Always pass documentList as a LIST of references (docItem and/or docList)
- For resultLabel, use the format: "task{{task_id}}_action{{action_number}}_{{short_label}}" where:
- {{task_id}} = the current task's id (e.g., 1)
- {{action_number}} = the sequence number of the action within the task (e.g., 2)
- {{short_label}} = a short, descriptive label for the output (e.g., "analysis_results")
Example: "task1_action2_analysis_results"
- If this is a retry, ensure the new actions address the specific issues from previous attempts
- Follow the JSON structure below. All fields are required.
REQUIRED JSON STRUCTURE:
{{
"actions": [
{{
"method": "method_name", // Use only the method name (e.g., "document")
"action": "action_name", // Use only the action name (e.g., "extract")
"parameters": {{
"documentList": ["docItem:doc_abc:file1.txt", "task1_action2_results"],
"aiPrompt": "Comprehensive AI prompt describing what to accomplish"
}},
"resultLabel": "task1_action3_analysis_results",
"expectedDocumentFormats": [ // OPTIONAL: Specify expected document formats when needed
{{
"extension": ".csv",
"mimeType": "text/csv",
"description": "Structured data output"
}}
],
"description": "What this action accomplishes (business outcome)"
}}
]
}}
FIELD REQUIREMENTS:
- "method": Must be from AVAILABLE METHODS
- "action": Must be valid for the method
- "parameters": Method-specific, must include documentList as a list if required by the signature
- "resultLabel": Must follow the format above (e.g., "task1_action3_analysis_results")
- "expectedDocumentFormats": OPTIONAL - Only specify when you need to control output format
- Use when you need specific file types (e.g., CSV for data, JSON for structured output)
- Omit when format is flexible (e.g., folder queries with mixed file types)
- Each format should specify: extension, mimeType, description
- When using expectedDocumentFormats, ensure the aiPrompt explicitly requests pure data without markdown formatting
- "description": Clear summary of the business outcome
EXAMPLES OF GOOD ACTIONS:
1. Document analysis with specific output format (use expectedDocumentFormats):
{{
"method": "document",
"action": "extract",
"parameters": {{
"documentList": ["docItem:doc_57520394-6b6d-41c2-b641-bab3fc6d7f4b:candidate_1_profile.txt"],
"aiPrompt": "Extract and analyze the candidate's qualifications, experience, skills, and suitability for the product designer position. Identify key strengths, relevant experience, technical skills, and any areas of concern. Provide a comprehensive assessment that can be used for evaluation."
}},
"resultLabel": "task1_action1_candidate_analysis",
"expectedDocumentFormats": [
{{
"extension": ".json",
"mimeType": "application/json",
"description": "Structured candidate analysis data"
}}
],
"description": "Comprehensive analysis of candidate profile for evaluation"
}}
2. Multi-document processing with flexible output (omit expectedDocumentFormats):
{{
"method": "document",
"action": "extract",
"parameters": {{
"documentList": ["task1_action1_candidate_analysis", "task1_action2_candidate_analysis", "task1_action3_candidate_analysis"],
"aiPrompt": "Compare all three candidate profiles and create an evaluation matrix. Rate each candidate on technical skills, experience level, cultural fit, portfolio quality, and communication skills. Provide clear rankings and recommendations for the product designer position."
}},
"resultLabel": "task1_action4_evaluation_matrix",
"description": "Create comprehensive evaluation matrix comparing all candidates"
}}
3. Data extraction with specific CSV format:
{{
"method": "document",
"action": "extract",
"parameters": {{
"documentList": ["docItem:doc_abc:table_data.pdf"],
"aiPrompt": "Extract all table data and convert to structured CSV format with proper headers and data types. IMPORTANT: Deliver pure CSV data without any markdown formatting, code blocks, or additional text. Output only the CSV content with proper headers and data rows."
}},
"resultLabel": "task1_action2_structured_data",
"expectedDocumentFormats": [
{{
"extension": ".csv",
"mimeType": "text/csv",
"description": "Structured table data in CSV format"
}}
],
"description": "Extract and structure table data for analysis"
}}
4. Comprehensive summary report from multiple documents (use generateReport):
{{
"method": "document",
"action": "generateReport",
"parameters": {{
"documentList": ["task1_action1_candidate_analysis", "task1_action2_candidate_analysis", "task1_action3_candidate_analysis"],
"title": "Comprehensive Candidate Evaluation Report"
}},
"resultLabel": "task1_action5_summary_report",
"description": "Generate a comprehensive, professional HTML report consolidating all candidate analyses and findings"
}}
5. Correct chaining of actions within a task:
{{
"actions": [
{{
"method": "document",
"action": "extract",
"parameters": {{
"documentList": ["docItem:doc_abc:file1.txt"],
"aiPrompt": "Extract data from file1."
}},
"resultLabel": "task1_action1_extracted_data",
"description": "Extract data from file1."
}},
{{
"method": "document",
"action": "generateReport",
"parameters": {{
"documentList": ["task1_action1_extracted_data"],
"title": "Report"
}},
"resultLabel": "task1_action2_report",
"description": "Generate report from extracted data."
}}
]
}}
NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
logging.debug(f"[ACTION PLAN PROMPT] Available Documents Section:\n{available_documents_str}\nUser Connections Section:\n{available_connections_str}\nAvailable Methods (summarized):\n{', '.join(method_actions.keys())}")
return prompt
async def createResultReviewPrompt(self, review_context) -> str:
"""Create prompt for result review"""
task_step = review_context.task_step
step_result = review_context.step_result or {}
step_result_serializable = {
'task_step': {
'id': task_step.id,
'description': task_step.description,
'expected_outputs': task_step.expected_outputs or [],
'success_criteria': task_step.success_criteria or []
},
'action_results': [],
'successful_actions': step_result.get('successful_actions', 0),
'total_actions': step_result.get('total_actions', 0),
'results_count': len(step_result.get('results', [])),
'errors_count': len(step_result.get('errors', []))
}
for action_result in (review_context.action_results or []):
documents_metadata = []
for doc in (action_result.documents or []):
if hasattr(doc, 'filename'):
documents_metadata.append({
'filename': doc.filename,
'fileSize': getattr(doc, 'fileSize', 0),
'mimeType': getattr(doc, 'mimeType', 'unknown')
})
elif isinstance(doc, dict):
documents_metadata.append({
'filename': doc.get('filename', 'unknown'),
'fileSize': doc.get('fileSize', 0),
'mimeType': doc.get('mimeType', 'unknown')
})
serializable_action_result = {
'status': 'completed' if action_result.success else 'failed',
'result_summary': action_result.data.get('result', '')[:200] + '...' if len(action_result.data.get('result', '')) > 200 else action_result.data.get('result', ''),
'error': action_result.error,
'resultLabel': action_result.data.get('resultLabel', ''),
'documents_count': len(documents_metadata),
'documents_metadata': documents_metadata,
'actionId': action_result.actionId,
'actionMethod': action_result.actionMethod,
'actionName': action_result.actionName,
'success_indicator': (
'documents' if len(documents_metadata) > 0 else
'text_result' if action_result.data.get('result', '').strip() else 'none'
)
}
step_result_serializable['action_results'].append(serializable_action_result)
step_result_json = json.dumps(step_result_serializable, indent=2, ensure_ascii=False)
expected_outputs_str = ', '.join(task_step.expected_outputs or [])
success_criteria_str = ', '.join(task_step.success_criteria or [])
return f"""You are a result review AI that evaluates task step completion and decides on next actions.
TASK STEP: {task_step.description}
EXPECTED OUTPUTS: {expected_outputs_str}
SUCCESS CRITERIA: {success_criteria_str}
STEP RESULT: {step_result_json}
INSTRUCTIONS:
1. Evaluate if the task step was completed successfully
2. Check if all expected outputs were produced
3. Verify if success criteria were met
4. Decide on next action: continue, retry, or fail
5. If retry, provide specific improvements needed
IMPORTANT NOTES:
- Actions can produce either text results OR documents (or both)
- Empty result_summary is acceptable if documents were produced (documents_count > 0)
- Focus on whether the action achieved its intended purpose, not just text output
- Document-based actions (like file extractions) often have empty text results but successful document outputs
- Check the 'success_indicator' field: 'documents' means success via document output, 'text_result' means success via text, 'none' means no output
REQUIRED JSON STRUCTURE:
{{
"status": "success|retry|failed",
"reason": "Explanation of the decision",
"improvements": "Specific improvements for retry (if status is retry)",
"quality_score": 1-10,
"missing_outputs": ["output1", "output2"],
"met_criteria": ["criteria1", "criteria2"],
"unmet_criteria": ["criteria3", "criteria4"]
}}
NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""

File diff suppressed because it is too large Load diff

View file

@ -6,20 +6,18 @@ import os
from typing import Dict, Any, List, Optional
from modules.interfaces.interfaceAppModel import User, UserConnection
from modules.interfaces.interfaceChatModel import (
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult,
ChatStat, ChatLog, ChatMessage, ChatWorkflow, DocumentExchange, ExtractedContent
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult, ChatStat, ChatLog, ChatMessage, ChatWorkflow, DocumentExchange, ExtractedContent
)
from modules.interfaces.interfaceAiCalls import AiCalls
from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects
from modules.interfaces.interfaceChatModel import ActionResult
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentObjects
from modules.interfaces.interfaceAppObjects import getInterface as getAppObjects
from modules.chat.processorDocument import DocumentProcessor
from gateway.modules.chat.documents.documentProcessing import DocumentProcessor
from modules.chat.methodBase import MethodBase
import uuid
import base64
import hashlib
import asyncio
logger = logging.getLogger(__name__)
@ -580,41 +578,63 @@ Please provide a clear summary of this message."""
logger.error(f"Error summarizing message: {str(e)}")
return f"Error summarizing message: {str(e)}"
async def callAiTextBasic(self, prompt: str, context: str = None) -> str:
"""Basic text processing using OpenAI"""
# Calculate prompt size for stats
prompt_size = self.calculateObjectSize(prompt)
if context:
prompt_size += self.calculateObjectSize(context)
# Call AI
response = await self.interfaceAiCalls.callAiTextBasic(prompt, context)
# Calculate response size for stats
response_size = self.calculateObjectSize(response)
# Update stats
self.updateWorkflowStats(eventLabel="aicall.openai.text", bytesSent=prompt_size, bytesReceived=response_size)
return response
async def callAiTextAdvanced(self, prompt: str, context: str = None) -> str:
"""Advanced text processing using Anthropic"""
# Calculate prompt size for stats
prompt_size = self.calculateObjectSize(prompt)
if context:
prompt_size += self.calculateObjectSize(context)
# Call AI
response = await self.interfaceAiCalls.callAiTextAdvanced(prompt, context)
# Calculate response size for stats
response_size = self.calculateObjectSize(response)
# Update stats
self.updateWorkflowStats(eventLabel="aicall.anthropic.text", bytesSent=prompt_size, bytesReceived=response_size)
return response
"""Advanced text processing using Anthropic, with fallback to OpenAI basic if advanced fails."""
max_retries = 3
base_delay = 2
last_error = None
# Try advanced AI first, with retries
for attempt in range(max_retries):
try:
prompt_size = self.calculateObjectSize(prompt)
if context:
prompt_size += self.calculateObjectSize(context)
response = await self.interfaceAiCalls.callAiTextAdvanced(prompt, context)
response_size = self.calculateObjectSize(response)
self.updateWorkflowStats(eventLabel="aicall.anthropic.text", bytesSent=prompt_size, bytesReceived=response_size)
return response
except Exception as e:
last_error = e
logger.warning(f"Advanced AI call failed (attempt {attempt+1}/{max_retries}): {str(e)}")
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
await asyncio.sleep(delay)
# Fallback to basic AI if advanced fails
logger.info("Falling back to basic AI after advanced AI failed.")
for attempt in range(max_retries):
try:
return await self.callAiTextBasic(prompt, context)
except Exception as e:
last_error = e
logger.warning(f"Basic AI fallback failed (attempt {attempt+1}/{max_retries}): {str(e)}")
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
await asyncio.sleep(delay)
logger.error(f"All AI calls failed: {str(last_error)}")
raise Exception(f"All AI calls failed: {str(last_error)}")
async def callAiTextBasic(self, prompt: str, context: str = None) -> str:
"""Basic text processing using OpenAI, with retry logic."""
max_retries = 3
base_delay = 2
last_error = None
for attempt in range(max_retries):
try:
prompt_size = self.calculateObjectSize(prompt)
if context:
prompt_size += self.calculateObjectSize(context)
response = await self.interfaceAiCalls.callAiTextBasic(prompt, context)
response_size = self.calculateObjectSize(response)
self.updateWorkflowStats(eventLabel="aicall.openai.text", bytesSent=prompt_size, bytesReceived=response_size)
return response
except Exception as e:
last_error = e
logger.warning(f"Basic AI call failed (attempt {attempt+1}/{max_retries}): {str(e)}")
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
await asyncio.sleep(delay)
logger.error(f"Basic AI call failed after {max_retries} attempts: {str(last_error)}")
raise Exception(f"Basic AI call failed after {max_retries} attempts: {str(last_error)}")
async def callAiImageBasic(self, prompt: str, imageData: str, mimeType: str) -> str:
"""Basic image processing using OpenAI"""
@ -858,6 +878,35 @@ Please provide a clear summary of this message."""
logger.error(f"Error executing method {methodName}.{actionName}: {str(e)}")
raise
async def processFileIds(self, fileIds: List[str]) -> List[ChatDocument]:
"""Process file IDs and return ChatDocument objects"""
documents = []
for fileId in fileIds:
try:
# Get file info from service
fileInfo = self.getFileInfo(fileId)
if fileInfo:
# Create document using interface
documentData = {
"fileId": fileId,
"filename": fileInfo.get("filename", "unknown"),
"fileSize": fileInfo.get("size", 0),
"mimeType": fileInfo.get("mimeType", "application/octet-stream")
}
document = self.interfaceChat.createChatDocument(documentData)
if document:
documents.append(document)
logger.info(f"Processed file ID {fileId} -> {document.filename}")
else:
logger.warning(f"No file info found for file ID {fileId}")
except Exception as e:
logger.error(f"Error processing file ID {fileId}: {str(e)}")
return documents
def setUserLanguage(self, language: str) -> None:
"""Set user language for the service center"""
self.user.language = language
# Create singleton instance
serviceObject = None

View file

@ -60,31 +60,18 @@ class AiCalls:
async def callAiTextAdvanced(self, prompt: str, context: Optional[str] = None) -> str:
"""
Advanced text processing using Anthropic.
Args:
prompt: The user prompt to process
context: Optional system context/prompt
Returns:
The AI response as text
Fallback to OpenAI if Anthropic is overloaded or rate-limited.
"""
# Prepare messages in OpenAI format
messages = []
# Add system message if context provided
if context:
messages.append({
"role": "system",
"content": context
})
# Add user message
messages.append({
"role": "user",
"content": prompt
})
# Add language instruction for user-facing responses
if hasattr(self, 'userLanguage') and self.userLanguage:
ltext = f"Please respond in '{self.userLanguage}' language."
if messages and messages[0]["role"] == "system":
@ -95,13 +82,14 @@ class AiCalls:
"role": "system",
"content": ltext
})
try:
response = await self.anthropicService.callAiBasic(messages)
return response["choices"][0]["message"]["content"]
except Exception as e:
logger.error(f"Error in Anthropic call: {str(e)}")
return f"Error: {str(e)}"
err_str = str(e)
logger.warning(f"[UI NOTICE] Advanced AI failed, falling back to Basic AI (OpenAI). Reason: {err_str}")
# Optionally, you could surface this message to the UI via a return value or error object
return await self.callAiTextBasic(prompt, context)
async def callAiImageBasic(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str:
"""

View file

@ -149,23 +149,6 @@ class MethodDocument(MethodBase):
# Fallback: convert to string representation
text_content = str(extracted_content)
# Skip empty or whitespace-only content
if not text_content or text_content.strip() == "":
logger.info(f"Skipping document {chatDocument.filename} - extraction result is empty or whitespace only")
continue
# Skip minimal content that is essentially empty (like "{}", "[]", etc.)
stripped_content = text_content.strip()
minimal_content_patterns = ['{}', '[]', '""', "''", 'null', 'undefined']
if stripped_content in minimal_content_patterns:
logger.info(f"Skipping document {chatDocument.filename} - extraction result is minimal content: '{stripped_content}'")
continue
# Skip content that's just whitespace or very short meaningless content
if len(stripped_content) <= 2:
logger.info(f"Skipping document {chatDocument.filename} - extraction result is too short: '{stripped_content}' ({len(stripped_content)} chars)")
continue
# Create output filename based on original filename
original_filename = chatDocument.filename
base_name = original_filename.rsplit('.', 1)[0] if '.' in original_filename else original_filename
@ -189,14 +172,6 @@ class MethodDocument(MethodBase):
"mimeType": output_mime_type
})
# Check if we have any valid output documents
if not output_documents:
return self._createResult(
success=False,
data={},
error="No valid content could be extracted from any documents (all results were empty or whitespace only)"
)
return self._createResult(
success=True,
data={
@ -214,10 +189,10 @@ class MethodDocument(MethodBase):
@action
async def generateReport(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Generate a basic HTML report from extracted content
Generate a comprehensive, professional HTML report from multiple documents, consolidating and summarizing all findings using AI.
Parameters:
documentList (str): Reference to the document list to create report from
documentList (str): Reference to the document list to create the report from
title (str, optional): Title for the report (default: "Summary Report")
includeMetadata (bool, optional): Whether to include metadata (default: True)
"""

View file

@ -10,7 +10,14 @@ import requests
from bs4 import BeautifulSoup
import time
import uuid
import json # Added for JSON parsing
import json
import copy
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from modules.chat.methodBase import MethodBase, ActionResult, action
from modules.shared.configuration import APP_CONFIG
@ -18,26 +25,23 @@ from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__)
class MethodWeb(MethodBase):
"""Web method implementation for web operations"""
"""
Web method implementation for web operations.
- web.search: Uses Google SerpAPI to find relevant URLs for a query. Returns only search result metadata (title, URL, snippet). Does NOT fetch or extract page content.
- web.crawl: Fetches and extracts main content from a list of URLs, either provided directly or via referenced documents. Uses a headless browser for JavaScript-heavy pages.
"""
def __init__(self, serviceCenter: Any):
"""Initialize the web method"""
super().__init__(serviceCenter)
self.name = "web"
self.description = "Handle web operations like crawling and scraping"
# Web search configuration from agentWebcrawler
self.description = "Handle web operations like search and crawling"
self.srcApikey = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_APIKEY", "")
self.srcEngine = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_ENGINE", "google")
self.srcCountry = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_COUNTRY", "auto")
self.maxResults = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_RESULTS", "5"))
if not self.srcApikey:
logger.warning("SerpAPI key not configured for web search")
self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
self.timeout = 30
def _readUrl(self, url: str) -> BeautifulSoup:
"""Read a URL and return a BeautifulSoup parser for the content with enhanced error handling"""
if not url or not url.startswith(('http://', 'https://')):
@ -192,8 +196,13 @@ class MethodWeb(MethodBase):
main_content = soup.find('body') or soup
logger.debug("Using body as main content")
# Create a copy to avoid modifying the original
content_copy = main_content.copy()
# Safely copy the main_content element
if main_content is None:
return ""
try:
content_copy = copy.copy(main_content)
except Exception:
content_copy = main_content
# Remove elements that don't contribute to main content (less aggressive)
elements_to_remove = [
@ -464,511 +473,137 @@ class MethodWeb(MethodBase):
return approaches
async def _tryAdvancedAIWebResearch(self, action_type: str, parameters: Dict[str, Any]) -> Optional[Dict[str, Any]]:
@action
def search(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Try to get web research results using advanced AI first
Args:
action_type: Type of action ('crawl', 'scrape', or 'search')
parameters: Action parameters
Returns:
Dict with AI results if successful, None if AI call fails
Perform a web search and output a .txt file with a plain list of URLs (one per line).
"""
try:
# Create appropriate prompt based on action type
if action_type == "crawl":
prompt = self._createCrawlAIPrompt(parameters)
elif action_type == "scrape":
prompt = self._createScrapeAIPrompt(parameters)
elif action_type == "search":
prompt = self._createSearchAIPrompt(parameters)
else:
logger.warning(f"Unknown action type for AI research: {action_type}")
return None
# Try advanced AI call
if hasattr(self.service, 'callAiTextAdvanced'):
logger.info(f"Attempting advanced AI web research for {action_type}")
response = await self.service.callAiTextAdvanced(prompt)
# Parse the AI response
parsed_result = self._parseAIWebResponse(response, action_type)
if parsed_result:
logger.info(f"Advanced AI web research successful for {action_type}")
return parsed_result
else:
logger.warning(f"Failed to parse AI response for {action_type}")
return None
else:
logger.warning("Service does not have callAiTextAdvanced method")
return None
except Exception as e:
logger.warning(f"Advanced AI web research failed for {action_type}: {str(e)}")
return None
def _createCrawlAIPrompt(self, parameters: Dict[str, Any]) -> str:
"""Create AI prompt for web crawling"""
urls = parameters.get("urls", [])
maxDepth = parameters.get("maxDepth", 2)
includeImages = parameters.get("includeImages", False)
followLinks = parameters.get("followLinks", True)
prompt = f"""
You are an advanced AI research assistant with comprehensive knowledge about websites, companies, and online content. Please provide detailed information about the following URLs based on your extensive training data and knowledge.
URLs to research: {urls}
Max depth: {maxDepth}
Include images: {includeImages}
Follow links: {followLinks}
For each URL, please provide comprehensive information including:
1. Company/organization information and background
2. Main business activities and services
3. Key personnel and leadership
4. Contact information and locations
5. Recent news and developments
6. Industry analysis and market position
7. Related companies and partnerships
8. Website structure and key pages
9. Business model and revenue streams
10. Regulatory compliance and certifications
For each URL, provide:
- url: The original URL
- title: Company/organization name
- content: Comprehensive description and analysis
- content_length: Number of characters in content
- meta_info: Business information object
- links: Related companies and important connections
- images: Company logos or key visuals if known
- requires_javascript: Boolean (usually false for static info)
- alternative_approaches: Additional research suggestions
- timestamp: Current timestamp
Return the results in this exact JSON format:
{{
"urls": {urls},
"maxDepth": {maxDepth},
"includeImages": {includeImages},
"followLinks": {followLinks},
"crawlResults": [
{{
"url": "url_here",
"depth": {maxDepth},
"followLinks": {followLinks},
"extractContent": true,
"title": "company_name",
"content": "comprehensive_company_analysis",
"content_length": 1234,
"meta_info": {{
"url": "url_here",
"title": "company_name",
"description": "business_description",
"keywords": "industry_keywords",
"author": "company_info",
"language": "language_code",
"robots": "robots_info",
"viewport": "viewport_info",
"charset": "charset_info",
"canonical": "canonical_url"
}},
"links": [
{{
"url": "related_company_url",
"text": "company_name"
}}
],
"images": [
{{
"src": "logo_url",
"alt": "company_logo",
"title": "company_name",
"width": "width_value",
"height": "height_value"
}}
],
"requires_javascript": false,
"alternative_approaches": ["approach1", "approach2"],
"timestamp": "2024-01-01T00:00:00Z"
}}
],
"summary": {{
"total_urls": {len(urls)},
"successful_crawls": 0,
"failed_crawls": 0,
"total_content_chars": 0
}},
"timestamp": "2024-01-01T00:00:00Z"
}}
Please provide accurate, comprehensive information about each company/organization based on your knowledge. If you don't have specific information about a URL, provide general industry analysis and suggest alternative research approaches.
"""
return prompt
def _createScrapeAIPrompt(self, parameters: Dict[str, Any]) -> str:
"""Create AI prompt for web scraping"""
url = parameters.get("url")
selectors = parameters.get("selectors", {})
format = parameters.get("format", "json")
prompt = f"""
You are an advanced AI research assistant with comprehensive knowledge about websites, companies, and online content. Please provide detailed information about the following URL and the specific data requested based on your extensive training data and knowledge.
URL to research: {url}
Data selectors: {selectors}
Output format: {format}
Please provide comprehensive information including:
1. Company/organization background and history
2. Business activities and services offered
3. Key personnel and leadership information
4. Financial information and performance data
5. Market position and competitive analysis
6. Recent news and developments
7. Contact information and locations
8. Industry trends and insights
9. Related companies and partnerships
10. Regulatory and compliance information
For each data selector requested, provide relevant information in the specified format (text, html, or json).
Return the results in this exact JSON format:
{{
"url": "{url}",
"selectors": {selectors},
"format": "{format}",
"scrapedData": {{
"url": "{url}",
"selectors": {selectors},
"format": "{format}",
"content": {{
"company_info": ["comprehensive_company_analysis"],
"business_activities": ["detailed_business_description"],
"leadership": ["key_personnel_information"],
"financial_data": ["financial_performance_analysis"],
"market_position": ["competitive_analysis"],
"recent_news": ["latest_developments"],
"contact_info": ["contact_details"],
"industry_insights": ["market_trends"],
"partnerships": ["related_companies"],
"compliance": ["regulatory_information"]
}},
"timestamp": "2024-01-01T00:00:00Z"
}},
"timestamp": "2024-01-01T00:00:00Z"
}}
Please provide accurate, comprehensive information about the company/organization based on your knowledge. If you don't have specific information about the URL, provide general industry analysis and suggest alternative research approaches.
"""
return prompt
def _createSearchAIPrompt(self, parameters: Dict[str, Any]) -> str:
"""Create AI prompt for web search"""
query = parameters.get("query")
engine = parameters.get("engine", "google")
maxResults = parameters.get("maxResults", 10)
filter = parameters.get("filter")
prompt = f"""
You are an advanced AI research assistant with comprehensive knowledge about companies, industries, and business information. Please provide detailed information about the following search query based on your extensive training data and knowledge.
Search query: {query}
Search engine: {engine}
Max results: {maxResults}
Filter: {filter}
Please provide comprehensive research results including:
1. Relevant company/organization information
2. Industry analysis and market insights
3. Key personnel and leadership details
4. Business activities and services
5. Financial performance and metrics
6. Recent news and developments
7. Competitive landscape analysis
8. Market trends and opportunities
9. Regulatory and compliance information
10. Related companies and partnerships
For each search result, provide:
- title: Company/organization name
- url: Official website or primary source
- snippet: Brief description and key highlights
- content: Comprehensive analysis and insights
Return the results in this exact JSON format:
{{
"query": "{query}",
"engine": "{engine}",
"maxResults": {maxResults},
"filter": "{filter}",
"searchResults": {{
"query": "{query}",
"maxResults": {maxResults},
"results": [
{{
"title": "company_name",
"url": "official_website",
"snippet": "brief_description",
"content": "comprehensive_analysis"
}}
],
"totalFound": 0,
"timestamp": "2024-01-01T00:00:00Z"
}},
"timestamp": "2024-01-01T00:00:00Z"
}}
Please provide accurate, comprehensive information about the search query based on your knowledge. If you don't have specific information about the query, provide general industry analysis and suggest alternative research approaches.
"""
return prompt
def _parseAIWebResponse(self, response: str, action_type: str) -> Optional[Dict[str, Any]]:
"""Parse AI response into structured data"""
max_results = parameters.get("maxResults", 10)
filter_param = parameters.get("filter")
if not query:
return ActionResult.failure("Search query is required")
if not self.srcApikey:
return ActionResult.failure("SerpAPI key not configured")
userLanguage = "en"
if hasattr(self.service, 'user') and hasattr(self.service.user, 'language'):
userLanguage = self.service.user.language
params = {
"engine": self.srcEngine,
"q": query,
"api_key": self.srcApikey,
"num": min(max_results, self.maxResults),
"hl": userLanguage
}
if filter_param:
params["filter"] = filter_param
try:
# Extract JSON from response
json_start = response.find('{')
json_end = response.rfind('}') + 1
if json_start == -1 or json_end == 0:
logger.warning(f"No JSON found in AI response: {response}")
return None
json_str = response[json_start:json_end]
parsed_data = json.loads(json_str)
# Validate basic structure based on action type
if action_type == "crawl":
if "crawlResults" not in parsed_data:
logger.warning("Invalid crawl response structure")
return None
elif action_type == "scrape":
if "scrapedData" not in parsed_data:
logger.warning("Invalid scrape response structure")
return None
elif action_type == "search":
if "searchResults" not in parsed_data:
logger.warning("Invalid search response structure")
return None
return parsed_data
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse AI response JSON: {str(e)}")
response = requests.get("https://serpapi.com/search", params=params, timeout=self.timeout)
response.raise_for_status()
search_results = response.json()
results = []
if "organic_results" in search_results:
results = search_results["organic_results"][:max_results]
# Assume 'results' is a list of dicts with 'url' keys
urls = [item['url'] for item in results if 'url' in item and isinstance(item['url'], str)]
url_list_str = "\n".join(urls)
filename = f"web_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.txt"
with open(filename, "w", encoding="utf-8") as f:
f.write(url_list_str)
return ActionResult.success(documents=[filename], resultLabel=parameters.get("resultLabel"))
except Exception as e:
logger.error(f"Error searching web: {str(e)}")
return ActionResult.failure(error=str(e))
def _selenium_extract_content(self, url: str) -> Optional[str]:
"""Use Selenium to fetch and extract main content from a JS-heavy page."""
options = Options()
options.headless = True
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument(f'user-agent={self.user_agent}')
try:
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(self.timeout)
driver.get(url)
# Wait for body to load
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
html = driver.page_source
driver.quit()
soup = BeautifulSoup(html, 'html.parser')
return self._extractMainContent(soup)
except WebDriverException as e:
logger.warning(f"Selenium failed for {url}: {str(e)}")
return None
except Exception as e:
logger.warning(f"Error parsing AI response: {str(e)}")
logger.warning(f"Selenium error for {url}: {str(e)}")
return None
@action
async def crawl(self, parameters: Dict[str, Any]) -> ActionResult:
def crawl(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Crawl web pages and extract content with enhanced error handling and content detection
Parameters:
urls (List[str]): List of URLs to crawl
maxDepth (int, optional): Maximum crawl depth (default: 2)
includeImages (bool, optional): Whether to include images (default: False)
followLinks (bool, optional): Whether to follow links (default: True)
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
Crawl a list of URLs provided in a document (.txt) with URLs separated by newline, comma, or semicolon.
"""
try:
urls = parameters.get("urls")
maxDepth = parameters.get("maxDepth", 2)
includeImages = parameters.get("includeImages", False)
followLinks = parameters.get("followLinks", True)
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
if not urls:
return self._createResult(
success=False,
data={},
error="URLs are required"
)
# Try advanced AI research first
ai_result = await self._tryAdvancedAIWebResearch("crawl", parameters)
if ai_result:
logger.info("Using advanced AI web research for crawl")
# Reconstruct the result data from the AI response
result_data = {
"urls": ai_result.get("urls", []),
"maxDepth": ai_result.get("maxDepth", 2),
"includeImages": ai_result.get("includeImages", False),
"followLinks": ai_result.get("followLinks", True),
"crawlResults": ai_result.get("crawlResults", []),
"summary": ai_result.get("summary", {}),
"timestamp": ai_result.get("timestamp", datetime.now(UTC).isoformat())
}
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"web_crawl_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data,
"mimeType": "application/json"
}
]
}
)
else:
logger.info("Advanced AI web research failed, falling back to regular web crawling")
# Crawl each URL
crawl_results = []
for url in urls:
try:
logger.info(f"Crawling URL: {url}")
# Read the URL with enhanced error handling
document = parameters.get("document")
if not document:
return ActionResult.failure("No document with URL list provided.")
# Read the document content
with open(document, "r", encoding="utf-8") as f:
content = f.read()
# Split URLs by newline, comma, or semicolon
import re
urls = re.split(r'[\n,;]+', content)
urls = [u.strip() for u in urls if u.strip()]
if not urls:
return ActionResult.failure("No valid URLs provided in the document.")
crawl_results = []
for url in urls:
try:
logger.info(f"Crawling URL: {url}")
# Try Selenium first
content = self._selenium_extract_content(url)
if not content:
# Fallback to requests/BeautifulSoup
soup = self._readUrl(url)
if not soup:
logger.error(f"Failed to read URL: {url}")
crawl_results.append({
"error": "Failed to read URL - check if the site is accessible and not blocking crawlers",
"url": url,
"suggestions": [
"Try accessing the URL directly in a browser",
"Check if the site requires JavaScript",
"Verify the URL is correct and accessible"
]
})
continue
# Extract comprehensive information
title = self._extractTitle(soup, url)
content = self._extractMainContent(soup)
meta_info = self._extractMetaInformation(soup, url)
# Check if content is meaningful
content_length = len(content)
if content_length < 100:
logger.warning(f"Very little content extracted from {url} ({content_length} chars)")
crawl_results.append({
"url": url,
"title": title,
"content": content,
"content_length": content_length,
"warning": "Very little content extracted - site may require JavaScript or have anti-bot protection",
"meta_info": meta_info,
"timestamp": datetime.now(UTC).isoformat()
})
continue
# Extract links if requested
links = []
if followLinks:
for link in soup.find_all('a', href=True):
href = link.get('href')
if href and href.startswith(('http://', 'https://')):
link_text = link.get_text(strip=True)
if link_text: # Only include links with text
links.append({
'url': href,
'text': link_text[:100]
})
# Extract images if requested
images = []
if includeImages:
for img in soup.find_all('img', src=True):
src = img.get('src')
if src:
images.append({
'src': src,
'alt': img.get('alt', ''),
'title': img.get('title', ''),
'width': img.get('width', ''),
'height': img.get('height', '')
})
# Check for JavaScript rendering requirements
requires_js = self._detectJavaScriptRendering(soup)
# Get alternative approaches if needed
alternative_approaches = self._getAlternativeApproaches(url, requires_js, content_length)
crawl_results.append({
"url": url,
"depth": maxDepth,
"followLinks": followLinks,
"extractContent": True,
"title": title,
"content": content,
"content_length": content_length,
"meta_info": meta_info,
"links": links[:20], # Limit to first 20 links
"images": images[:20], # Limit to first 20 images
"requires_javascript": requires_js,
"alternative_approaches": alternative_approaches,
"timestamp": datetime.now(UTC).isoformat()
})
logger.info(f"Successfully crawled {url} - extracted {content_length} characters")
except Exception as e:
logger.error(f"Error crawling web page {url}: {str(e)}")
crawl_results.append({
"error": str(e),
"url": url,
"suggestions": [
"Check if the URL is accessible",
"Try with a different user agent",
"Verify the site doesn't block automated access"
]
})
# Create result data
result_data = {
"urls": urls,
"maxDepth": maxDepth,
"includeImages": includeImages,
"followLinks": followLinks,
"crawlResults": crawl_results,
"summary": {
"total_urls": len(urls),
"successful_crawls": len([r for r in crawl_results if "error" not in r]),
"failed_crawls": len([r for r in crawl_results if "error" in r]),
"total_content_chars": sum([r.get("content_length", 0) for r in crawl_results if "content_length" in r])
},
"timestamp": datetime.now(UTC).isoformat()
}
# Determine output format based on expected formats
output_extension = ".json" # Default
output_mime_type = "application/json" # Default
if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
# Use the first expected format
expected_format = expectedDocumentFormats[0]
output_extension = expected_format.get("extension", ".json")
output_mime_type = expected_format.get("mimeType", "application/json")
logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
else:
logger.info("No expected format specified, using default .json format")
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"web_crawl_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}",
"documentData": result_data,
"mimeType": output_mime_type
}
title = self._extractTitle(BeautifulSoup(content, 'html.parser'), url) if content else "No title"
meta_info = {"url": url, "title": title}
content_length = len(content) if content else 0
crawl_results.append({
"url": url,
"title": title,
"content": content,
"content_length": content_length,
"meta_info": meta_info,
"timestamp": datetime.now(UTC).isoformat()
})
logger.info(f"Successfully crawled {url} - extracted {content_length} characters")
except Exception as e:
logger.error(f"Error crawling web page {url}: {str(e)}")
crawl_results.append({
"error": str(e),
"url": url,
"suggestions": [
"Check if the URL is accessible",
"Try with a different user agent",
"Verify the site doesn't block automated access"
]
}
)
except Exception as e:
logger.error(f"Error crawling web pages: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)
})
result_data = {
"urls": urls,
"maxDepth": 1, # Simplified crawl
"includeImages": False,
"followLinks": True,
"crawlResults": crawl_results,
"summary": {
"total_urls": len(urls),
"successful_crawls": len([r for r in crawl_results if "error" not in r]),
"failed_crawls": len([r for r in crawl_results if "error" in r]),
"total_content_chars": sum([r.get("content_length", 0) for r in crawl_results if "content_length" in r])
},
"timestamp": datetime.now(UTC).isoformat()
}
return ActionResult.success(result=result_data, resultLabel=parameters.get("resultLabel"))
@action
async def scrape(self, parameters: Dict[str, Any]) -> ActionResult:
@ -994,33 +629,6 @@ Please provide accurate, comprehensive information about the search query based
error="URL and selectors are required"
)
# Try advanced AI research first
ai_result = await self._tryAdvancedAIWebResearch("scrape", parameters)
if ai_result:
logger.info("Using advanced AI web research for scrape")
# Reconstruct the result data from the AI response
result_data = {
"url": ai_result.get("url"),
"selectors": ai_result.get("selectors"),
"format": ai_result.get("format"),
"scrapedData": ai_result.get("scrapedData"),
"timestamp": ai_result.get("timestamp", datetime.now(UTC).isoformat())
}
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"web_scrape_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data,
"mimeType": "application/json"
}
]
}
)
else:
logger.info("Advanced AI web research failed, falling back to regular web scraping")
# Read the URL
soup = self._readUrl(url)
if not soup:
@ -1106,180 +714,3 @@ Please provide accurate, comprehensive information about the search query based
error=str(e)
)
@action
async def search(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Search web content
Parameters:
query (str): Search query
engine (str, optional): Search engine to use (default: "google")
maxResults (int, optional): Maximum number of results (default: 10)
filter (str, optional): Additional search filters
expectedDocumentFormats (list, optional): Expected document formats with extension, mimeType, description
"""
try:
query = parameters.get("query")
engine = parameters.get("engine", "google")
maxResults = parameters.get("maxResults", 10)
filter = parameters.get("filter")
expectedDocumentFormats = parameters.get("expectedDocumentFormats", [])
if not query:
return self._createResult(
success=False,
data={},
error="Search query is required"
)
# Try advanced AI research first
ai_result = await self._tryAdvancedAIWebResearch("search", parameters)
if ai_result:
logger.info("Using advanced AI web research for search")
# Reconstruct the result data from the AI response
result_data = {
"query": ai_result.get("query"),
"engine": ai_result.get("engine"),
"maxResults": ai_result.get("maxResults"),
"filter": ai_result.get("filter"),
"searchResults": ai_result.get("searchResults"),
"timestamp": ai_result.get("timestamp", datetime.now(UTC).isoformat())
}
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"web_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data,
"mimeType": "application/json"
}
]
}
)
else:
logger.info("Advanced AI web research failed, falling back to regular web search")
# Search web content using Google search via SerpAPI
try:
if not self.srcApikey:
search_result = {
"error": "SerpAPI key not configured",
"query": query
}
else:
# Get user language from service center if available
userLanguage = "en" # Default language
if hasattr(self.service, 'user') and hasattr(self.service.user, 'language'):
userLanguage = self.service.user.language
# Format the search request for SerpAPI
params = {
"engine": self.srcEngine,
"q": query,
"api_key": self.srcApikey,
"num": min(maxResults, self.maxResults), # Number of results to return
"hl": userLanguage # User language
}
# Make the API request
response = requests.get("https://serpapi.com/search", params=params, timeout=self.timeout)
response.raise_for_status()
# Parse JSON response
search_results = response.json()
# Extract organic results
results = []
if "organic_results" in search_results:
for result in search_results["organic_results"][:maxResults]:
# Extract title
title = result.get("title", "No title")
# Extract URL
url = result.get("link", "No URL")
# Extract snippet
snippet = result.get("snippet", "No description")
# Get actual page content
try:
targetPageSoup = self._readUrl(url)
content = self._extractMainContent(targetPageSoup)
except Exception as e:
logger.warning(f"Error extracting content from {url}: {str(e)}")
content = f"Error extracting content: {str(e)}"
results.append({
'title': title,
'url': url,
'snippet': snippet,
'content': content
})
# Limit number of results
if len(results) >= maxResults:
break
else:
logger.warning(f"No organic results found in SerpAPI response for: {query}")
search_result = {
"query": query,
"maxResults": maxResults,
"results": results,
"totalFound": len(results),
"timestamp": datetime.now(UTC).isoformat()
}
except Exception as e:
logger.error(f"Error searching web: {str(e)}")
search_result = {
"error": str(e),
"query": query
}
# Create result data
result_data = {
"query": query,
"engine": engine,
"maxResults": maxResults,
"filter": filter,
"searchResults": search_result,
"timestamp": datetime.now(UTC).isoformat()
}
# Determine output format based on expected formats
output_extension = ".json" # Default
output_mime_type = "application/json" # Default
if expectedDocumentFormats and len(expectedDocumentFormats) > 0:
# Use the first expected format
expected_format = expectedDocumentFormats[0]
output_extension = expected_format.get("extension", ".json")
output_mime_type = expected_format.get("mimeType", "application/json")
logger.info(f"Using expected format: {output_extension} ({output_mime_type})")
else:
logger.info("No expected format specified, using default .json format")
return self._createResult(
success=True,
data={
"documents": [
{
"documentName": f"web_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}{output_extension}",
"documentData": result_data,
"mimeType": output_mime_type
}
]
}
)
except Exception as e:
logger.error(f"Error searching web: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)

View file

@ -136,11 +136,36 @@ class WorkflowManager:
logger.error(f"Error sending first message: {str(e)}")
raise
async def _generateWorkflowFeedback(self, workflow: ChatWorkflow) -> str:
"""Generate feedback message for workflow completion"""
try:
# Count messages by role
user_messages = [msg for msg in workflow.messages if msg.role == 'user']
assistant_messages = [msg for msg in workflow.messages if msg.role == 'assistant']
# Generate summary feedback
feedback = f"Workflow completed.\n\n"
feedback += f"Processed {len(user_messages)} user inputs and generated {len(assistant_messages)} responses.\n"
# Add final status
if workflow.status == "completed":
feedback += "All tasks completed successfully."
elif workflow.status == "partial":
feedback += "Some tasks completed with partial success."
else:
feedback += f"Workflow status: {workflow.status}"
return feedback
except Exception as e:
logger.error(f"Error generating workflow feedback: {str(e)}")
return "Workflow processing completed."
async def _sendLastMessage(self, workflow: ChatWorkflow) -> None:
"""Send last message to complete workflow"""
try:
# Generate feedback
feedback = await self.chatManager.generateWorkflowFeedback(workflow)
feedback = await self._generateWorkflowFeedback(workflow)
# Create last message using interface
messageData = {

View file

@ -1,40 +1,12 @@
INIT
conda activate poweron
cd gateway
pip install -r requirements.txt
python app.py
LOGIC
1. HIGH-LEVEL TASK PLANNING
├── Analyze user request
├── Define major task steps
└── Create task plan with dependencies
2. FOR EACH TASK STEP:
├── TASK DEFINITION
│ ├── Define specific actions for this task
│ └── Set success criteria
├── ACTION EXECUTION
│ ├── Execute each action
│ └── Collect results
├── TASK REVIEW
│ ├── Evaluate task completion
│ ├── Check success criteria
│ └── Decide: Continue/Retry/Fail
└── HANDOVER
├── Prepare results for next task
└── Update workflow state
TODO
- neutralizer to put back placeholders to the returned data
- refactory of chat manager
- to put document modules into documents--> creation, extraction -> adapt references over global search
- neutralizer to activate AND put back placeholders to the returned data
- referenceHandling and authentication for connections in the method actions
- check methods
- test for workflow backend with userdata
@ -44,6 +16,17 @@ TODO
INIT
conda activate poweron
cd gateway
pip install -r requirements.txt
python app.py
********************

View file

View file