aligned docList and docItem context

This commit is contained in:
ValueOn AG 2025-07-08 13:55:17 +02:00
parent 55fed49a28
commit 171e18b0d7
15 changed files with 432 additions and 591 deletions

View file

@ -86,6 +86,8 @@ class ContentMetadata(BaseModel, ModelMixin):
colorMode: Optional[str] = Field(None, description="Color mode (e.g., RGB, CMYK, grayscale)") colorMode: Optional[str] = Field(None, description="Color mode (e.g., RGB, CMYK, grayscale)")
fps: Optional[float] = Field(None, description="Frames per second for videos") fps: Optional[float] = Field(None, description="Frames per second for videos")
durationSec: Optional[float] = Field(None, description="Duration in seconds for videos/audio") durationSec: Optional[float] = Field(None, description="Duration in seconds for videos/audio")
mimeType: str = Field(description="MIME type of the content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
# Register labels for ContentMetadata # Register labels for ContentMetadata
register_model_labels( register_model_labels(
@ -99,7 +101,9 @@ register_model_labels(
"height": {"en": "Height", "fr": "Hauteur"}, "height": {"en": "Height", "fr": "Hauteur"},
"colorMode": {"en": "Color Mode", "fr": "Mode de couleur"}, "colorMode": {"en": "Color Mode", "fr": "Mode de couleur"},
"fps": {"en": "FPS", "fr": "IPS"}, "fps": {"en": "FPS", "fr": "IPS"},
"durationSec": {"en": "Duration", "fr": "Durée"} "durationSec": {"en": "Duration", "fr": "Durée"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"}
} }
) )
@ -107,8 +111,6 @@ class ContentItem(BaseModel, ModelMixin):
"""Individual content item from a document""" """Individual content item from a document"""
label: str = Field(description="Content label (e.g., tab name, tag name)") label: str = Field(description="Content label (e.g., tab name, tag name)")
data: str = Field(description="Extracted text content") data: str = Field(description="Extracted text content")
mimeType: str = Field(description="MIME type of the content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
metadata: ContentMetadata = Field(description="Content metadata") metadata: ContentMetadata = Field(description="Content metadata")
# Register labels for ContentItem # Register labels for ContentItem
@ -143,6 +145,21 @@ register_model_labels(
} }
) )
class DocumentExchange(BaseModel, ModelMixin):
"""Data model for document exchange between AI actions"""
documentsLabel: str = Field(description="Label for the set of documents")
documents: List[str] = Field(default_factory=list, description="List of document references")
# Register labels for DocumentExchange
register_model_labels(
"DocumentExchange",
{"en": "Document Exchange", "fr": "Échange de documents"},
{
"documentsLabel": {"en": "Documents Label", "fr": "Label des documents"},
"documents": {"en": "Documents", "fr": "Documents"}
}
)
class ExtractedContent(BaseModel, ModelMixin): class ExtractedContent(BaseModel, ModelMixin):
"""Data model for extracted content""" """Data model for extracted content"""
id: str = Field(description="Reference to source ChatDocument") id: str = Field(description="Reference to source ChatDocument")

View file

@ -504,20 +504,25 @@ class ComponentObjects:
return newFilename return newFilename
counter += 1 counter += 1
def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> FileItem: def createFile(self, name: str, mimeType: str, content: bytes) -> FileItem:
"""Creates a new file entry if user has permission.""" """Creates a new file entry if user has permission. Computes fileHash and fileSize from content."""
import hashlib
if not self._canModify("files"): if not self._canModify("files"):
raise PermissionError("No permission to create files") raise PermissionError("No permission to create files")
# Ensure filename is unique # Ensure filename is unique
uniqueName = self._generateUniqueFilename(name) uniqueName = self._generateUniqueFilename(name)
# Compute file size and hash
fileSize = len(content)
fileHash = hashlib.sha256(content).hexdigest()
# Create FileItem instance # Create FileItem instance
fileItem = FileItem( fileItem = FileItem(
mandateId=self.currentUser.mandateId, mandateId=self.currentUser.mandateId,
filename=uniqueName, filename=uniqueName,
mimeType=mimeType, mimeType=mimeType,
fileSize=size, fileSize=fileSize,
fileHash=fileHash fileHash=fileHash
) )
@ -818,27 +823,15 @@ class ComponentObjects:
logger.error(f"Invalid fileContent type: {type(fileContent)}") logger.error(f"Invalid fileContent type: {type(fileContent)}")
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}") raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
# Calculate file hash for deduplication # Determine MIME type
fileHash = self.calculateFileHash(fileContent)
logger.debug(f"Calculated file hash: {fileHash}")
# Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
logger.debug(f"Duplicate found for {fileName}: {existingFile.id}")
return existingFile
# Determine MIME type and size
mimeType = self.getMimeType(fileName) mimeType = self.getMimeType(fileName)
fileSize = len(fileContent)
# Save metadata # Save metadata and file (hash/size computed inside createFile)
logger.debug(f"Saving file metadata to database for file: {fileName}") logger.debug(f"Saving file metadata to database for file: {fileName}")
fileItem = self.createFile( fileItem = self.createFile(
name=fileName, name=fileName,
mimeType=mimeType, mimeType=mimeType,
size=fileSize, content=fileContent
fileHash=fileHash
) )
# Save binary data # Save binary data

View file

@ -46,7 +46,8 @@ class MethodCoder(MethodBase):
error="AI prompt is required" error="AI prompt is required"
) )
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) # Handle new document list format (list of strings)
chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -59,15 +60,15 @@ class MethodCoder(MethodBase):
for chatDocument in chatDocuments: for chatDocument in chatDocuments:
fileId = chatDocument.fileId fileId = chatDocument.fileId
code = self.serviceContainer.getFileData(fileId) code = self.service.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId) file_info = self.service.getFileInfo(fileId)
if not code: if not code:
logger.warning(f"Code file is empty for fileId: {fileId}") logger.warning(f"Code file is empty for fileId: {fileId}")
continue continue
# Use AI prompt to extract relevant code content # Use AI prompt to extract relevant code content
extracted_content = await self.serviceContainer.extractContentFromFileData( extracted_content = await self.service.extractContentFromFileData(
prompt=aiPrompt, prompt=aiPrompt,
fileData=code, fileData=code,
filename=file_info.get('name', 'code'), filename=file_info.get('name', 'code'),
@ -107,7 +108,7 @@ class MethodCoder(MethodBase):
""" """
# Use AI service for analysis # Use AI service for analysis
analysis_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(analysis_prompt) analysis_result = await self.service.interfaceAiCalls.callAiTextAdvanced(analysis_prompt)
# Create result data # Create result data
result_data = { result_data = {
@ -121,8 +122,12 @@ class MethodCoder(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"code_analysis_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"code_analysis_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
@ -173,7 +178,7 @@ class MethodCoder(MethodBase):
""" """
# Use AI service for code generation # Use AI service for code generation
generated_code = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(generation_prompt) generated_code = await self.service.interfaceAiCalls.callAiTextAdvanced(generation_prompt)
# Create result data # Create result data
result_data = { result_data = {
@ -228,7 +233,8 @@ class MethodCoder(MethodBase):
error="AI improvement prompt is required" error="AI improvement prompt is required"
) )
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) # Handle new document list format (list of strings)
chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -241,8 +247,8 @@ class MethodCoder(MethodBase):
for chatDocument in chatDocuments: for chatDocument in chatDocuments:
fileId = chatDocument.fileId fileId = chatDocument.fileId
code = self.serviceContainer.getFileData(fileId) code = self.service.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId) file_info = self.service.getFileInfo(fileId)
if not code: if not code:
logger.warning(f"Code file is empty for fileId: {fileId}") logger.warning(f"Code file is empty for fileId: {fileId}")
@ -266,7 +272,7 @@ class MethodCoder(MethodBase):
""" """
# Use AI service for refactoring # Use AI service for refactoring
refactored_code = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(refactor_prompt) refactored_code = await self.service.interfaceAiCalls.callAiTextAdvanced(refactor_prompt)
refactored_results.append({ refactored_results.append({
"original_file": file_info.get('name', 'unknown'), "original_file": file_info.get('name', 'unknown'),

View file

@ -54,7 +54,7 @@ class MethodDocument(MethodBase):
error="AI prompt is required" error="AI prompt is required"
) )
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -68,19 +68,20 @@ class MethodDocument(MethodBase):
for chatDocument in chatDocuments: for chatDocument in chatDocuments:
fileId = chatDocument.fileId fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId) file_data = self.service.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId) file_info = self.service.getFileInfo(fileId)
if not file_data: if not file_data:
logger.warning(f"File not found or empty for fileId: {fileId}") logger.warning(f"File not found or empty for fileId: {fileId}")
continue continue
extracted_content = await self.serviceContainer.extractContentFromFileData( extracted_content = await self.service.extractContentFromFileData(
prompt=aiPrompt, prompt=aiPrompt,
fileData=file_data, fileData=file_data,
filename=file_info.get('name', 'document'), filename=file_info.get('name', 'document'),
mimeType=file_info.get('mimeType', 'application/octet-stream'), mimeType=file_info.get('mimeType', 'application/octet-stream'),
base64Encoded=False base64Encoded=False,
documentId=chatDocument.id
) )
all_extracted_content.append(extracted_content) all_extracted_content.append(extracted_content)
@ -108,8 +109,12 @@ class MethodDocument(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"extracted_content_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.txt", "documents": [
"documentData": result_data {
"documentName": f"extracted_content_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.txt",
"documentData": result_data
}
]
} }
) )
except Exception as e: except Exception as e:
@ -149,7 +154,7 @@ class MethodDocument(MethodBase):
error="AI prompt is required" error="AI prompt is required"
) )
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -162,19 +167,20 @@ class MethodDocument(MethodBase):
for chatDocument in chatDocuments: for chatDocument in chatDocuments:
fileId = chatDocument.fileId fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId) file_data = self.service.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId) file_info = self.service.getFileInfo(fileId)
if not file_data: if not file_data:
logger.warning(f"File not found or empty for fileId: {fileId}") logger.warning(f"File not found or empty for fileId: {fileId}")
continue continue
extracted_content = await self.serviceContainer.extractContentFromFileData( extracted_content = await self.service.extractContentFromFileData(
prompt=aiPrompt, prompt=aiPrompt,
fileData=file_data, fileData=file_data,
filename=file_info.get('name', 'document'), filename=file_info.get('name', 'document'),
mimeType=file_info.get('mimeType', 'application/octet-stream'), mimeType=file_info.get('mimeType', 'application/octet-stream'),
base64Encoded=False base64Encoded=False,
documentId=chatDocument.id
) )
all_extracted_content.append(extracted_content) all_extracted_content.append(extracted_content)
@ -205,7 +211,7 @@ class MethodDocument(MethodBase):
6. Document structure and organization 6. Document structure and organization
""" """
analysis_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(analysis_prompt) analysis_result = await self.service.interfaceAiCalls.callAiTextAdvanced(analysis_prompt)
result_data = { result_data = {
"documentCount": len(chatDocuments), "documentCount": len(chatDocuments),
@ -218,8 +224,12 @@ class MethodDocument(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"document_analysis_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"document_analysis_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
except Exception as e: except Exception as e:
@ -261,7 +271,7 @@ class MethodDocument(MethodBase):
error="AI prompt is required" error="AI prompt is required"
) )
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -274,19 +284,20 @@ class MethodDocument(MethodBase):
for chatDocument in chatDocuments: for chatDocument in chatDocuments:
fileId = chatDocument.fileId fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId) file_data = self.service.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId) file_info = self.service.getFileInfo(fileId)
if not file_data: if not file_data:
logger.warning(f"File not found or empty for fileId: {fileId}") logger.warning(f"File not found or empty for fileId: {fileId}")
continue continue
extracted_content = await self.serviceContainer.extractContentFromFileData( extracted_content = await self.service.extractContentFromFileData(
prompt=aiPrompt, prompt=aiPrompt,
fileData=file_data, fileData=file_data,
filename=file_info.get('name', 'document'), filename=file_info.get('name', 'document'),
mimeType=file_info.get('mimeType', 'application/octet-stream'), mimeType=file_info.get('mimeType', 'application/octet-stream'),
base64Encoded=False base64Encoded=False,
documentId=chatDocument.id
) )
all_extracted_content.append(extracted_content) all_extracted_content.append(extracted_content)
@ -316,7 +327,7 @@ class MethodDocument(MethodBase):
- Highlight important insights and conclusions - Highlight important insights and conclusions
""" """
summary = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(summary_prompt) summary = await self.service.interfaceAiCalls.callAiTextAdvanced(summary_prompt)
result_data = { result_data = {
"documentCount": len(chatDocuments), "documentCount": len(chatDocuments),
@ -331,8 +342,12 @@ class MethodDocument(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"document_summary_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.txt", "documents": [
"documentData": result_data {
"documentName": f"document_summary_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.txt",
"documentData": result_data
}
]
} }
) )
except Exception as e: except Exception as e:

View file

@ -1,339 +0,0 @@
"""Operator method implementation for handling collections and AI operations"""
from typing import Dict, List, Any, Optional
from datetime import datetime, UTC
import logging
import uuid
from modules.workflow.methodBase import MethodBase, ActionResult, action
logger = logging.getLogger(__name__)
class MethodOperator(MethodBase):
"""Operator method implementation for data operations"""
def __init__(self, serviceContainer: Any):
super().__init__(serviceContainer)
self.name = "operator"
self.description = "Handle data operations like filtering, sorting, and transformation"
@action
async def filter(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Filter data based on criteria
Parameters:
documentList (str): Reference to the document list to filter
criteria (Dict[str, Any]): Filter criteria
field (str, optional): Field to filter on
"""
try:
documentList = parameters.get("documentList")
criteria = parameters.get("criteria")
field = parameters.get("field")
if not documentList or not criteria:
return self._createResult(
success=False,
data={},
error="Document list reference and criteria are required"
)
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList)
if not chatDocuments:
return self._createResult(
success=False,
data={},
error="No documents found for the provided reference"
)
# Extract content from all documents
all_document_content = []
for chatDocument in chatDocuments:
fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId)
if not file_data:
logger.warning(f"File data not found for fileId: {fileId}")
continue
all_document_content.append({
"fileId": fileId,
"fileName": file_info.get('name', 'unknown'),
"content": file_data
})
if not all_document_content:
return self._createResult(
success=False,
data={},
error="No content could be extracted from any documents"
)
# Combine all document content for filtering
combined_content = "\n\n--- DOCUMENT SEPARATOR ---\n\n".join([
f"File: {doc['fileName']}\nContent: {doc['content']}"
for doc in all_document_content
])
filter_prompt = f"""
Filter the following data based on the specified criteria.
Data to filter:
{combined_content}
Filter criteria:
{criteria}
Field to filter on: {field or 'All fields'}
Please provide:
1. Filtered data that matches the criteria
2. Summary of filtering results
3. Number of items before and after filtering
4. Any data quality insights
"""
filtered_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(filter_prompt)
result_data = {
"documentCount": len(chatDocuments),
"criteria": criteria,
"field": field,
"filteredData": filtered_result,
"originalCount": len(all_document_content),
"timestamp": datetime.now(UTC).isoformat()
}
return self._createResult(
success=True,
data={
"documentName": f"filtered_data_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
)
except Exception as e:
logger.error(f"Error filtering data: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)
@action
async def sort(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Sort data by specified field
Parameters:
documentList (str): Reference to the document list to sort
field (str): Field to sort by
order (str, optional): Sort order (asc/desc, default: "asc")
"""
try:
documentList = parameters.get("documentList")
field = parameters.get("field")
order = parameters.get("order", "asc")
if not documentList or not field:
return self._createResult(
success=False,
data={},
error="Document list reference and field are required"
)
# Get documents from reference
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList)
if not chatDocuments:
return self._createResult(
success=False,
data={},
error="No documents found for the provided reference"
)
# Extract content from all documents
all_document_content = []
for chatDocument in chatDocuments:
fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId)
if not file_data:
logger.warning(f"File data not found for fileId: {fileId}")
continue
all_document_content.append({
"fileId": fileId,
"fileName": file_info.get('name', 'unknown'),
"content": file_data
})
if not all_document_content:
return self._createResult(
success=False,
data={},
error="No content could be extracted from any documents"
)
# Combine all document content for sorting
combined_content = "\n\n--- DOCUMENT SEPARATOR ---\n\n".join([
f"File: {doc['fileName']}\nContent: {doc['content']}"
for doc in all_document_content
])
# Create sorting prompt
sort_prompt = f"""
Sort the following data by the specified field.
Data to sort:
{combined_content}
Sort field: {field}
Sort order: {order}
Please provide:
1. Sorted data in the specified order
2. Summary of sorting results
3. Any data insights from the sorting
4. Validation of sort field existence
"""
# Use AI to perform sorting
sorted_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(sort_prompt)
# Create result data
result_data = {
"documentCount": len(chatDocuments),
"field": field,
"order": order,
"sortedData": sorted_result,
"timestamp": datetime.now(UTC).isoformat()
}
return self._createResult(
success=True,
data={
"documentName": f"sorted_data_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
)
except Exception as e:
logger.error(f"Error sorting data: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)
@action
async def transform(self, parameters: Dict[str, Any]) -> ActionResult:
"""
Transform data structure or format
Parameters:
documentList (str): Reference to the document list to transform
transformation (Dict[str, Any]): Transformation rules
outputFormat (str, optional): Desired output format
"""
try:
documentList = parameters.get("documentList")
transformation = parameters.get("transformation")
outputFormat = parameters.get("outputFormat", "json")
if not documentList or not transformation:
return self._createResult(
success=False,
data={},
error="Document list reference and transformation rules are required"
)
# Get documents from reference
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList)
if not chatDocuments:
return self._createResult(
success=False,
data={},
error="No documents found for the provided reference"
)
# Extract content from all documents
all_document_content = []
for chatDocument in chatDocuments:
fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId)
file_info = self.serviceContainer.getFileInfo(fileId)
if not file_data:
logger.warning(f"File data not found for fileId: {fileId}")
continue
all_document_content.append({
"fileId": fileId,
"fileName": file_info.get('name', 'unknown'),
"content": file_data
})
if not all_document_content:
return self._createResult(
success=False,
data={},
error="No content could be extracted from any documents"
)
# Combine all document content for transformation
combined_content = "\n\n--- DOCUMENT SEPARATOR ---\n\n".join([
f"File: {doc['fileName']}\nContent: {doc['content']}"
for doc in all_document_content
])
# Create transformation prompt
transform_prompt = f"""
Transform the following data according to the specified rules.
Data to transform:
{combined_content}
Transformation rules:
{transformation}
Output format: {outputFormat}
Please provide:
1. Transformed data in the specified format
2. Summary of transformation results
3. Validation of transformation rules
4. Any data quality improvements
"""
# Use AI to perform transformation
transformed_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(transform_prompt)
# Create result data
result_data = {
"documentCount": len(chatDocuments),
"transformation": transformation,
"outputFormat": outputFormat,
"transformedData": transformed_result,
"timestamp": datetime.now(UTC).isoformat()
}
return self._createResult(
success=True,
data={
"documentName": f"transformed_data_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.{outputFormat}",
"documentData": result_data
}
)
except Exception as e:
logger.error(f"Error transforming data: {str(e)}")
return self._createResult(
success=False,
data={},
error=str(e)
)

View file

@ -25,12 +25,12 @@ class MethodOutlook(MethodBase):
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]: def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
"""Get Microsoft connection from connection reference""" """Get Microsoft connection from connection reference"""
try: try:
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference) userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
if not userConnection or userConnection.authority != "msft" or userConnection.status != "active": if not userConnection or userConnection.authority != "msft" or userConnection.status != "active":
return None return None
# Get the corresponding token for this user and authority # Get the corresponding token for this user and authority
token = self.serviceContainer.interfaceApp.getToken(userConnection.authority) token = self.service.interfaceApp.getToken(userConnection.authority)
if not token: if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}") logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}")
return None return None
@ -95,7 +95,7 @@ class MethodOutlook(MethodBase):
""" """
# Use AI to simulate email reading # Use AI to simulate email reading
email_data = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(email_prompt) email_data = await self.service.interfaceAiCalls.callAiTextAdvanced(email_prompt)
# Create result data # Create result data
result_data = { result_data = {
@ -115,8 +115,12 @@ class MethodOutlook(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"outlook_emails_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"outlook_emails_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
@ -184,7 +188,7 @@ class MethodOutlook(MethodBase):
""" """
# Use AI to simulate email sending # Use AI to simulate email sending
send_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(send_prompt) send_result = await self.service.interfaceAiCalls.callAiTextAdvanced(send_prompt)
# Create result data # Create result data
result_data = { result_data = {
@ -269,7 +273,7 @@ class MethodOutlook(MethodBase):
""" """
# Use AI to simulate email search # Use AI to simulate email search
search_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(search_prompt) search_result = await self.service.interfaceAiCalls.callAiTextAdvanced(search_prompt)
# Create result data # Create result data
result_data = { result_data = {

View file

@ -24,12 +24,12 @@ class MethodSharepoint(MethodBase):
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]: def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
"""Get Microsoft connection from connection reference""" """Get Microsoft connection from connection reference"""
try: try:
userConnection = self.serviceContainer.getUserConnectionFromConnectionReference(connectionReference) userConnection = self.service.getUserConnectionFromConnectionReference(connectionReference)
if not userConnection or userConnection.authority != "msft" or userConnection.status != "active": if not userConnection or userConnection.authority != "msft" or userConnection.status != "active":
return None return None
# Get the corresponding token for this user and authority # Get the corresponding token for this user and authority
token = self.serviceContainer.interfaceApp.getToken(userConnection.authority) token = self.service.interfaceApp.getToken(userConnection.authority)
if not token: if not token:
logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}") logger.warning(f"No token found for user {userConnection.userId} and authority {userConnection.authority}")
return None return None
@ -92,7 +92,7 @@ class MethodSharepoint(MethodBase):
5. Search statistics and coverage 5. Search statistics and coverage
""" """
find_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(find_prompt) find_result = await self.service.interfaceAiCalls.callAiTextAdvanced(find_prompt)
result_data = { result_data = {
"connectionReference": connectionReference, "connectionReference": connectionReference,
@ -111,8 +111,12 @@ class MethodSharepoint(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"sharepoint_find_path_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"sharepoint_find_path_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
@ -150,7 +154,8 @@ class MethodSharepoint(MethodBase):
error="Document list reference, connection reference, site URL, and document paths are required" error="Document list reference, connection reference, site URL, and document paths are required"
) )
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) # Get documents from reference
chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -191,7 +196,7 @@ class MethodSharepoint(MethodBase):
5. Version history if available 5. Version history if available
""" """
document_data = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(sharepoint_prompt) document_data = await self.service.interfaceAiCalls.callAiTextAdvanced(sharepoint_prompt)
read_results.append({ read_results.append({
"documentPath": documentPath, "documentPath": documentPath,
@ -216,8 +221,12 @@ class MethodSharepoint(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"sharepoint_documents_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"sharepoint_documents_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
except Exception as e: except Exception as e:
@ -264,7 +273,7 @@ class MethodSharepoint(MethodBase):
) )
# Get documents from reference # Get documents from reference
chatDocuments = self.serviceContainer.getChatDocumentsFromDocumentReference(documentList) chatDocuments = self.service.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return self._createResult( return self._createResult(
success=False, success=False,
@ -279,7 +288,7 @@ class MethodSharepoint(MethodBase):
if i < len(chatDocuments): if i < len(chatDocuments):
chatDocument = chatDocuments[i] chatDocument = chatDocuments[i]
fileId = chatDocument.fileId fileId = chatDocument.fileId
file_data = self.serviceContainer.getFileData(fileId) file_data = self.service.getFileData(fileId)
if not file_data: if not file_data:
logger.warning(f"File data not found for fileId: {fileId}") logger.warning(f"File data not found for fileId: {fileId}")
@ -305,7 +314,7 @@ class MethodSharepoint(MethodBase):
""" """
# Use AI to simulate SharePoint upload # Use AI to simulate SharePoint upload
upload_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(upload_prompt) upload_result = await self.service.interfaceAiCalls.callAiTextAdvanced(upload_prompt)
upload_results.append({ upload_results.append({
"documentPath": documentPath, "documentPath": documentPath,
@ -333,8 +342,12 @@ class MethodSharepoint(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"sharepoint_upload_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"sharepoint_upload_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
@ -401,7 +414,7 @@ class MethodSharepoint(MethodBase):
""" """
# Use AI to simulate SharePoint listing # Use AI to simulate SharePoint listing
list_result = await self.serviceContainer.interfaceAiCalls.callAiTextAdvanced(list_prompt) list_result = await self.service.interfaceAiCalls.callAiTextAdvanced(list_prompt)
list_results.append({ list_results.append({
"folderPath": folderPath, "folderPath": folderPath,
@ -426,8 +439,12 @@ class MethodSharepoint(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"sharepoint_document_list_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"sharepoint_document_list_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )

View file

@ -310,8 +310,12 @@ class MethodWeb(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"web_crawl_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"web_crawl_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
@ -399,8 +403,12 @@ class MethodWeb(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"web_scrape_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.{format}", "documents": [
"documentData": result_data {
"documentName": f"web_scrape_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.{format}",
"documentData": result_data
}
]
} }
) )
@ -446,8 +454,8 @@ class MethodWeb(MethodBase):
else: else:
# Get user language from service container if available # Get user language from service container if available
userLanguage = "en" # Default language userLanguage = "en" # Default language
if hasattr(self.serviceContainer, 'user') and hasattr(self.serviceContainer.user, 'language'): if hasattr(self.service, 'user') and hasattr(self.service.user, 'language'):
userLanguage = self.serviceContainer.user.language userLanguage = self.service.user.language
# Format the search request for SerpAPI # Format the search request for SerpAPI
params = { params = {
@ -528,8 +536,12 @@ class MethodWeb(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"web_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"web_search_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )
@ -600,8 +612,12 @@ class MethodWeb(MethodBase):
return self._createResult( return self._createResult(
success=True, success=True,
data={ data={
"documentName": f"web_validation_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json", "documents": [
"documentData": result_data {
"documentName": f"web_validation_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.json",
"documentData": result_data
}
]
} }
) )

View file

@ -201,10 +201,23 @@ class ChatManager:
if action.status == TaskStatus.PENDING: if action.status == TaskStatus.PENDING:
action.status = TaskStatus.COMPLETED if review_result.get('status') == 'success' else TaskStatus.FAILED action.status = TaskStatus.COMPLETED if review_result.get('status') == 'success' else TaskStatus.FAILED
# Create serializable task actions
task_actions_serializable = []
for action in task_actions:
action_dict = {
'id': action.id,
'execMethod': action.execMethod,
'execAction': action.execAction,
'execParameters': action.execParameters,
'execResultLabel': action.execResultLabel,
'status': action.status.value if hasattr(action.status, 'value') else str(action.status)
}
task_actions_serializable.append(action_dict)
# Create handover data # Create handover data
handover_data = { handover_data = {
'task_step': task_step, 'task_step': task_step,
'task_actions': task_actions, 'task_actions': task_actions_serializable,
'review_result': review_result, 'review_result': review_result,
'next_task_ready': review_result.get('status') == 'success', 'next_task_ready': review_result.get('status') == 'success',
'available_results': self._getPreviousResultsFromActions(task_actions) 'available_results': self._getPreviousResultsFromActions(task_actions)
@ -215,9 +228,22 @@ class ChatManager:
except Exception as e: except Exception as e:
logger.error(f"Error preparing task handover: {str(e)}") logger.error(f"Error preparing task handover: {str(e)}")
# Create serializable task actions for exception case
task_actions_serializable = []
for action in task_actions:
action_dict = {
'id': action.id,
'execMethod': action.execMethod,
'execAction': action.execAction,
'execParameters': action.execParameters,
'execResultLabel': action.execResultLabel,
'status': action.status.value if hasattr(action.status, 'value') else str(action.status)
}
task_actions_serializable.append(action_dict)
return { return {
'task_step': task_step, 'task_step': task_step,
'task_actions': task_actions, 'task_actions': task_actions_serializable,
'review_result': review_result, 'review_result': review_result,
'next_task_ready': False, 'next_task_ready': False,
'available_results': [] 'available_results': []
@ -407,8 +433,8 @@ class ChatManager:
# Validate result label format # Validate result label format
result_label = action.get('resultLabel', '') result_label = action.get('resultLabel', '')
if not result_label.startswith('mdoc:'): if not result_label.startswith('docList:'):
logger.error(f"Action {i} result label must start with 'mdoc:': {result_label}") logger.error(f"Action {i} result label must start with 'docList:': {result_label}")
return False return False
# Validate parameters # Validate parameters
@ -444,7 +470,7 @@ class ChatManager:
"fileId": doc, "fileId": doc,
"analysis": ["entities", "topics", "sentiment"] "analysis": ["entities", "topics", "sentiment"]
}, },
"resultLabel": f"mdoc:fallback:{task_step.get('id', 'unknown')}:{i}:analysis", "resultLabel": f"docList:fallback:{task_step.get('id', 'unknown')}:{i}:analysis",
"description": f"Fallback document analysis for {doc}" "description": f"Fallback document analysis for {doc}"
}) })
@ -525,7 +551,7 @@ AVAILABLE CONNECTIONS
{chr(10).join(f"- {conn}" for conn in connRefs)} {chr(10).join(f"- {conn}" for conn in connRefs)}
AVAILABLE DOCUMENTS AVAILABLE DOCUMENTS
{chr(10).join(f"- {doc['documentReference']} ({doc['actionMethod']}.{doc['actionName']}, {doc['documentCount']} documents, {doc['datetime']})" for doc in docRefs.get('chat', []))} {chr(10).join(f"- {doc.documentsLabel}: {', '.join(doc.documents)}" for doc in docRefs.get('chat', []))}
PREVIOUS RESULTS: PREVIOUS RESULTS:
{', '.join(previous_results) if previous_results else 'None'} {', '.join(previous_results) if previous_results else 'None'}
@ -550,7 +576,7 @@ REQUIRED JSON STRUCTURE:
"param1": "value1", "param1": "value1",
"param2": "value2", "param2": "value2",
}}, }},
"resultLabel": "mdoc:uuid:descriptiveLabel", "resultLabel": "docList:uuid:descriptiveLabel",
"description": "What this action does" "description": "What this action does"
}} }}
] ]
@ -560,7 +586,7 @@ FIELD REQUIREMENTS:
- "method": Must be one of the available methods listed above - "method": Must be one of the available methods listed above
- "action": Must be a valid action for that method - "action": Must be a valid action for that method
- "parameters": Object with method-specific parameters - "parameters": Object with method-specific parameters
- "resultLabel": MUST start with "mdoc:" followed by unique identifier and descriptive label - "resultLabel": MUST start with "docList:" followed by unique identifier and descriptive label
- "description": Clear description of what the action accomplishes - "description": Clear description of what the action accomplishes
MANDATORY PARAMETER AND RETURN VALUE RULES: MANDATORY PARAMETER AND RETURN VALUE RULES:
@ -572,15 +598,16 @@ MANDATORY PARAMETER AND RETURN VALUE RULES:
- Example: "connection:msft:testuser@example.com:1234" - Example: "connection:msft:testuser@example.com:1234"
2. DOCUMENT PARAMETERS: 2. DOCUMENT PARAMETERS:
- Parameter name: "documentReference" (NOT "document", "fileId", "documents", etc.) - Parameter name: "documentList" (NOT "documentReference", "document", "fileId", "documents", etc.)
- Value: Must be a document reference from "Documents" section or previous results - Value: MUST be a LIST of document references from "Documents" section or previous results
- Format: "mdoc:uuid:descriptiveLabel" - Format: Use the exact format shown in "Documents" section (e.g., ["docItem:id:filename"] or ["docList:actionId:label"])
- Document references represent a LIST of documents, not single documents - Document references represent a LIST of documents, not single documents
- All document inputs expect documentList references - All document inputs expect documentList as an ARRAY of strings
- IMPORTANT: Use the exact document reference format as shown in "Documents" section above
3. RETURN VALUES: 3. RETURN VALUES:
- ALL actions must return documentList references in resultLabel - ALL actions must return documentList references in resultLabel
- Result labels must start with "mdoc:" - Result labels must start with "docList:"
- Each action creates a unique documentList for handover - Each action creates a unique documentList for handover
- Document lists can contain 0, 1, or multiple documents - Document lists can contain 0, 1, or multiple documents
- No actions return single documents - always documentLists - No actions return single documents - always documentLists
@ -589,28 +616,24 @@ MANDATORY PARAMETER AND RETURN VALUE RULES:
- Use only document references from "Documents" section above - Use only document references from "Documents" section above
- Use only connection references from "Connections" section above - Use only connection references from "Connections" section above
- Use result labels from previous results in the sequence - Use result labels from previous results in the sequence
- All parameter values must be strings - All parameter values must be strings (except documentList which must be an array)
- Document references show: method.action - document count - timestamp - Document references show: label - list of references
5. RESULT USAGE RULES: 5. RESULT USAGE RULES:
- Previous results can be referenced as: "mdoc:uuid:label" - Previous results can be referenced as: "docList:uuid:label"
- Use result labels from previous actions in the sequence - Use result labels from previous actions in the sequence
- Example: If previous action created "mdoc:abc123:salesData", - Example: If previous action created "docList:abc123:salesData",
reference it as "mdoc:abc123:salesData" in parameters reference it as "docList:abc123:salesData" in parameters
- Results are available in the PREVIOUS RESULTS section above - Results are available in the PREVIOUS RESULTS section above
- Each action should create a unique resultLabel for handover to next actions - Each action should create a unique resultLabel for handover to next actions
- Result labels should be descriptive and indicate the content type - Result labels should be descriptive and indicate the content type
METHOD-SPECIFIC PARAMETER REQUIREMENTS: 6. DOCUMENT HANDLING RULES:
- ALWAYS pass documents as a LIST in documentList parameter
- coder: Uses "code" (string), "language" (string), "requirements" (string) - Single documents: ["docItem:id:filename"]
- document: Uses "documentReference" (documentList), "fileId" (string for single files) - Multiple documents: ["docItem:id1:file1", "docItem:id2:file2"]
- excel: Uses "connectionReference" (connection), "fileId" (string) - Document lists: ["docList:actionId:label"]
- operator: Uses "items" (array), "prompt" (string), "documents" (array of documentReferences) - Mixed references: ["docItem:id:file", "docList:actionId:label"]
- outlook: Uses "connectionReference" (connection)
- powerpoint: Uses "connectionReference" (connection), "fileId" (string)
- sharepoint: Uses "connectionReference" (connection)
- web: Uses "query" (string), "url" (string)
EXAMPLE VALID ACTIONS: EXAMPLE VALID ACTIONS:
@ -622,33 +645,45 @@ EXAMPLE VALID ACTIONS:
"connectionReference": "connection:msft:testuser@example.com:1234", "connectionReference": "connection:msft:testuser@example.com:1234",
"query": "sales quarterly report" "query": "sales quarterly report"
}}, }},
"resultLabel": "mdoc:abc123:salesDocuments", "resultLabel": "docList:abc123:salesDocuments",
"description": "Search SharePoint for sales documents" "description": "Search SharePoint for sales documents"
}} }}
2. Document Analysis using previous results: 2. Document Analysis using single document:
{{ {{
"method": "document", "method": "document",
"action": "analyze", "action": "analyze",
"parameters": {{ "parameters": {{
"documentReference": "mdoc:def456:customerData", "documentList": ["docItem:doc_57520394-6b6d-41c2-b641-bab3fc6d7f4b:candidate_1_profile.txt"],
"analysis": ["entities", "topics", "sentiment"] "aiPrompt": "Analyze the candidate profile for key insights"
}}, }},
"resultLabel": "mdoc:ghi789:customerAnalysis", "resultLabel": "docList:ghi789:candidateAnalysis",
"description": "Analyze customer data for insights" "description": "Analyze candidate profile for insights"
}} }}
3. Excel Read: 3. Document Analysis using multiple documents:
{{ {{
"method": "excel", "method": "document",
"action": "read", "action": "analyze",
"parameters": {{ "parameters": {{
"connectionReference": "connection:msft:testuser@example.com:1234", "documentList": ["docItem:doc_123:profile.txt", "docItem:doc_456:resume.pdf"],
"fileId": "excel_file_123", "aiPrompt": "Compare the profile and resume for consistency"
"sheetName": "Sheet1"
}}, }},
"resultLabel": "mdoc:jkl012:excelData", "resultLabel": "docList:jkl012:comparisonAnalysis",
"description": "Read data from Excel file" "description": "Compare multiple documents for consistency"
}}
4. Document Extraction using document list:
{{
"method": "document",
"action": "extract",
"parameters": {{
"documentList": ["docList:abc123:salesData"],
"aiPrompt": "Extract key information from all sales documents",
"format": "json"
}},
"resultLabel": "docList:mno345:extractedData",
"description": "Extract key information from document list"
}} }}
NOTE: Respond with ONLY the JSON object. Do not include any explanatory text.""" NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
@ -659,13 +694,37 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
task_step = review_context['task_step'] task_step = review_context['task_step']
step_result = review_context['step_result'] step_result = review_context['step_result']
# Create serializable version of step_result
step_result_serializable = {
'task_step': step_result.get('task_step', {}),
'action_results': [],
'successful_actions': step_result.get('successful_actions', 0),
'total_actions': step_result.get('total_actions', 0),
'results': step_result.get('results', []),
'errors': step_result.get('errors', [])
}
# Convert action_results to serializable format
for action_result in step_result.get('action_results', []):
serializable_action_result = {
'status': action_result.get('status', ''),
'result': action_result.get('result', ''),
'error': action_result.get('error', ''),
'resultLabel': action_result.get('resultLabel', ''),
'documents': action_result.get('documents', []),
'actionId': action_result.get('actionId', ''),
'actionMethod': action_result.get('actionMethod', ''),
'actionName': action_result.get('actionName', '')
}
step_result_serializable['action_results'].append(serializable_action_result)
return f"""You are a result review AI that evaluates task step completion and decides on next actions. return f"""You are a result review AI that evaluates task step completion and decides on next actions.
TASK STEP: {task_step.get('description', 'Unknown')} TASK STEP: {task_step.get('description', 'Unknown')}
EXPECTED OUTPUTS: {', '.join(task_step.get('expected_outputs', []))} EXPECTED OUTPUTS: {', '.join(task_step.get('expected_outputs', []))}
SUCCESS CRITERIA: {', '.join(task_step.get('success_criteria', []))} SUCCESS CRITERIA: {', '.join(task_step.get('success_criteria', []))}
STEP RESULT: {json.dumps(step_result, indent=2)} STEP RESULT: {json.dumps(step_result_serializable, indent=2)}
INSTRUCTIONS: INSTRUCTIONS:
1. Evaluate if the task step was completed successfully 1. Evaluate if the task step was completed successfully
@ -723,14 +782,17 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
parameters=action.execParameters parameters=action.execParameters
) )
# Always use the execResultLabel from the action definition
result_label = action.execResultLabel
# Update action based on result # Update action based on result
if result.success: if result.success:
action.setSuccess() action.setSuccess()
action.result = result.data.get("result", "") action.result = result.data.get("result", "")
action.execResultLabel = result.data.get("resultLabel", "") action.execResultLabel = result_label
# Create and store message in workflow for successful action # Create and store message in workflow for successful action
await self._createActionMessage(action, result, workflow) await self._createActionMessage(action, result, workflow, result_label)
else: else:
action.setError(result.error or "Action execution failed") action.setError(result.error or "Action execution failed")
@ -739,9 +801,11 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
"status": "completed" if result.success else "failed", "status": "completed" if result.success else "failed",
"result": result.data.get("result", ""), "result": result.data.get("result", ""),
"error": result.error or "", "error": result.error or "",
"resultLabel": result.data.get("resultLabel", ""), "resultLabel": result_label,
"documents": result.data.get("documents", []), "documents": result.data.get("documents", []),
"action": action "actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction
} }
except Exception as e: except Exception as e:
@ -750,16 +814,19 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
return { return {
"status": "failed", "status": "failed",
"error": str(e), "error": str(e),
"action": action "actionId": action.id,
"actionMethod": action.execMethod,
"actionName": action.execAction
} }
async def _createActionMessage(self, action: TaskAction, result: Any, workflow: ChatWorkflow) -> None: async def _createActionMessage(self, action: TaskAction, result: Any, workflow: ChatWorkflow, result_label: str = None) -> None:
"""Create and store a message for the action result in the workflow""" """Create and store a message for the action result in the workflow"""
try: try:
# Get result data # Get result data
result_data = result.data if hasattr(result, 'data') else {} result_data = result.data if hasattr(result, 'data') else {}
result_label = result_data.get("resultLabel", "")
documents_data = result_data.get("documents", []) documents_data = result_data.get("documents", [])
if result_label is None:
result_label = action.execResultLabel
# Create message data # Create message data
message_data = { message_data = {
@ -772,7 +839,7 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
"actionId": action.id, "actionId": action.id,
"actionMethod": action.execMethod, "actionMethod": action.execMethod,
"actionName": action.execAction, "actionName": action.execAction,
"documentsLabel": result_label, # Use resultLabel as documentsLabel "documentsLabel": result_label, # Always use execResultLabel
"documents": [] "documents": []
} }
@ -1128,8 +1195,18 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
logger.warning(f"No actions defined for task {i+1}, skipping") logger.warning(f"No actions defined for task {i+1}, skipping")
continue continue
# Log task actions # Log task actions (convert to serializable format)
logger.debug(f"TASK {i+1} ACTIONS CREATED: {json.dumps(task_actions, indent=2, ensure_ascii=False)}") task_actions_serializable = []
for action in task_actions:
action_dict = {
'execMethod': action.execMethod,
'execAction': action.execAction,
'execParameters': action.execParameters,
'execResultLabel': action.execResultLabel,
'status': action.status.value if hasattr(action.status, 'value') else str(action.status)
}
task_actions_serializable.append(action_dict)
logger.debug(f"TASK {i+1} ACTIONS CREATED: {json.dumps(task_actions_serializable, indent=2, ensure_ascii=False)}")
# Phase 3: Execute Task Actions # Phase 3: Execute Task Actions
logger.info(f"--- PHASE 3: EXECUTING ACTIONS FOR TASK {i+1} ---") logger.info(f"--- PHASE 3: EXECUTING ACTIONS FOR TASK {i+1} ---")
@ -1173,11 +1250,35 @@ NOTE: Respond with ONLY the JSON object. Do not include any explanatory text."""
successful_tasks = sum(1 for result in workflow_results if result['review_result'].get('status') == 'success') successful_tasks = sum(1 for result in workflow_results if result['review_result'].get('status') == 'success')
total_tasks = len(workflow_results) total_tasks = len(workflow_results)
# Create serializable workflow results
workflow_results_serializable = []
for result in workflow_results:
serializable_result = {
'task_step': result['task_step'],
'action_results': result['action_results'],
'review_result': result['review_result'],
'handover_data': result['handover_data']
}
# Convert task_actions to serializable format
if 'task_actions' in result:
task_actions_serializable = []
for action in result['task_actions']:
action_dict = {
'execMethod': action.execMethod,
'execAction': action.execAction,
'execParameters': action.execParameters,
'execResultLabel': action.execResultLabel,
'status': action.status.value if hasattr(action.status, 'value') else str(action.status)
}
task_actions_serializable.append(action_dict)
serializable_result['task_actions'] = task_actions_serializable
workflow_results_serializable.append(serializable_result)
workflow_summary = { workflow_summary = {
'status': 'completed' if successful_tasks == total_tasks else 'partial', 'status': 'completed' if successful_tasks == total_tasks else 'partial',
'successful_tasks': successful_tasks, 'successful_tasks': successful_tasks,
'total_tasks': total_tasks, 'total_tasks': total_tasks,
'workflow_results': workflow_results, 'workflow_results': workflow_results_serializable,
'final_results': previous_results 'final_results': previous_results
} }

View file

@ -57,7 +57,7 @@ class DocumentManager:
logger.error(f"Error extracting from document: {str(e)}") logger.error(f"Error extracting from document: {str(e)}")
raise raise
async def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False) -> ExtractedContent: async def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False, documentId: str = None) -> ExtractedContent:
"""Extract content from file data directly using prompt""" """Extract content from file data directly using prompt"""
try: try:
return await self._processor.processFileData( return await self._processor.processFileData(
@ -65,7 +65,8 @@ class DocumentManager:
filename=filename, filename=filename,
mimeType=mimeType, mimeType=mimeType,
base64Encoded=base64Encoded, base64Encoded=base64Encoded,
prompt=prompt prompt=prompt,
documentId=documentId
) )
except Exception as e: except Exception as e:
logger.error(f"Error extracting from file data: {str(e)}") logger.error(f"Error extracting from file data: {str(e)}")

View file

@ -130,6 +130,18 @@ class MethodBase:
descriptions[lastParam] += " " + line descriptions[lastParam] += " " + line
return descriptions, types return descriptions, types
def _validateDocumentListParameter(self, parameters: Dict[str, Any], paramName: str = "documentList") -> bool:
"""Validate that documentList parameter is a list of strings"""
if paramName not in parameters:
return False
value = parameters[paramName]
if not isinstance(value, list):
return False
# Check that all items in the list are strings
return all(isinstance(item, str) for item in value)
def _extractMainDescription(self, docstring: str) -> str: def _extractMainDescription(self, docstring: str) -> str:
"""Extract main description from docstring""" """Extract main description from docstring"""
if not docstring: if not docstring:
@ -217,7 +229,18 @@ class MethodBase:
actionDef = self.actions[action] actionDef = self.actions[action]
requiredParams = {k for k, v in actionDef['parameters'].items() if v['required']} requiredParams = {k for k, v in actionDef['parameters'].items() if v['required']}
return all(param in parameters for param in requiredParams)
# Check required parameters
if not all(param in parameters for param in requiredParams):
return False
# Validate documentList parameter if present
if "documentList" in parameters:
if not self._validateDocumentListParameter(parameters, "documentList"):
self.logger.error("documentList parameter must be a list of strings")
return False
return True
except Exception as e: except Exception as e:
self.logger.error(f"Error validating parameters: {str(e)}") self.logger.error(f"Error validating parameters: {str(e)}")

View file

@ -109,7 +109,7 @@ class DocumentProcessor:
except ImportError as e: except ImportError as e:
logger.warning(f"Image processing libraries could not be loaded: {e}") logger.warning(f"Image processing libraries could not be loaded: {e}")
async def processFileData(self, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False, prompt: str = None) -> ExtractedContent: async def processFileData(self, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False, prompt: str = None, documentId: str = None) -> ExtractedContent:
""" """
Process file data directly and extract its contents with AI processing. Process file data directly and extract its contents with AI processing.
@ -154,8 +154,7 @@ class DocumentProcessor:
logger.error(f"Error processing content with AI: {str(e)}") logger.error(f"Error processing content with AI: {str(e)}")
return ExtractedContent( return ExtractedContent(
objectId=str(uuid.uuid4()), id=documentId if documentId else str(uuid.uuid4()),
objectType="FileData",
contents=contentItems contents=contentItems
) )
@ -550,11 +549,11 @@ class DocumentProcessor:
# Chunk content based on type # Chunk content based on type
if mimeType.startswith('text/'): if mimeType.startswith('text/'):
chunks = await self._chunkText(item.data, mimeType) chunks = self._chunkText(item.data, mimeType)
elif mimeType.startswith('image/'): elif mimeType.startswith('image/'):
chunks = await self._chunkImage(item.data) chunks = self._chunkImage(item.data)
elif mimeType.startswith('video/'): elif mimeType.startswith('video/'):
chunks = await self._chunkVideo(item.data) chunks = self._chunkVideo(item.data)
else: else:
# Binary data - no chunking # Binary data - no chunking
chunks = [item.data] chunks = [item.data]

View file

@ -6,7 +6,8 @@ from typing import Dict, Any, List, Optional
from modules.interfaces.interfaceAppModel import User, UserConnection from modules.interfaces.interfaceAppModel import User, UserConnection
from modules.interfaces.interfaceChatModel import ( from modules.interfaces.interfaceChatModel import (
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult, TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult,
ChatStat, ChatLog, ChatMessage, ChatWorkflow
ChatStat, ChatLog, ChatMessage, ChatWorkflow, DocumentExchange
) )
from modules.interfaces.interfaceAiCalls import AiCalls from modules.interfaces.interfaceAiCalls import AiCalls
from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects
@ -17,6 +18,7 @@ from modules.workflow.managerDocument import DocumentManager
from modules.workflow.methodBase import MethodBase from modules.workflow.methodBase import MethodBase
import uuid import uuid
import base64 import base64
import hashlib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -115,9 +117,13 @@ class ServiceContainer:
"""Extract content from document using prompt""" """Extract content from document using prompt"""
return self.documentManager.extractContentFromDocument(prompt, document) return self.documentManager.extractContentFromDocument(prompt, document)
def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False) -> str: async def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False, documentId: str = None) -> str:
"""Extract content from file data directly using prompt""" """Extract content from file data directly using prompt"""
return self.documentManager.extractContentFromFileData(prompt, fileData, filename, mimeType, base64Encoded) extracted_content = await self.documentManager.extractContentFromFileData(prompt, fileData, filename, mimeType, base64Encoded, documentId)
# Convert ExtractedContent to string for backward compatibility
if hasattr(extracted_content, 'contents'):
return "\n".join([item.data for item in extracted_content.contents])
return str(extracted_content)
def getMethodsCatalog(self) -> Dict[str, Any]: def getMethodsCatalog(self) -> Dict[str, Any]:
"""Get catalog of available methods and their actions""" """Get catalog of available methods and their actions"""
@ -148,127 +154,120 @@ class ServiceContainer:
return methodList return methodList
def getDocumentReferenceList(self) -> Dict[str, List[Dict[str, str]]]: def getDocumentReferenceList(self) -> Dict[str, List[DocumentExchange]]:
"""Get list of document references sorted by datetime, categorized by chat round""" """Get list of document exchanges sorted by datetime, categorized by chat round"""
chat_refs = [] chat_exchanges = []
history_refs = [] history_exchanges = []
# Process messages in reverse order to find current chat round # Process messages in reverse order to find current chat round
for message in reversed(self.workflow.messages): for message in reversed(self.workflow.messages):
# Get document references from message # Get document references from message
if message.documents: if message.documents:
# For messages with action context, use documentList reference # For messages with action context, create DocumentExchange with docList reference
if message.actionId and message.documentsLabel: if message.actionId and message.documentsLabel:
doc_ref = self.getDocumentReferenceFromMessage(message) doc_ref = self.getDocumentReferenceFromMessage(message)
if doc_ref: if doc_ref:
doc_info = { # Create DocumentExchange with single docList reference
"documentReference": doc_ref, doc_exchange = DocumentExchange(
"datetime": message.publishedAt, documentsLabel=message.documentsLabel,
"actionMethod": message.actionMethod, documents=[doc_ref]
"actionName": message.actionName, )
"documentCount": len(message.documents)
}
# Add to appropriate list based on message status # Add to appropriate list based on message status
if message.status == "first": if message.status == "first":
chat_refs.append(doc_info) chat_exchanges.append(doc_exchange)
break # Stop after finding first message break # Stop after finding first message
elif message.status == "step": elif message.status == "step":
chat_refs.append(doc_info) chat_exchanges.append(doc_exchange)
else: else:
history_refs.append(doc_info) history_exchanges.append(doc_exchange)
# For regular messages, use individual document references # For regular messages, create DocumentExchange with individual docItem references
else: else:
doc_refs = []
for doc in message.documents: for doc in message.documents:
doc_ref = self.getDocumentReferenceFromChatDocument(doc) doc_ref = self.getDocumentReferenceFromChatDocument(doc)
doc_info = { doc_refs.append(doc_ref)
"documentReference": doc_ref,
"datetime": message.publishedAt, if doc_refs:
"actionMethod": None, # Create DocumentExchange with individual document references
"actionName": None, doc_exchange = DocumentExchange(
"documentCount": 1 documentsLabel=f"{message.id}:documents",
} documents=doc_refs
)
# Add to appropriate list based on message status # Add to appropriate list based on message status
if message.status == "first": if message.status == "first":
chat_refs.append(doc_info) chat_exchanges.append(doc_exchange)
break # Stop after finding first message break # Stop after finding first message
elif message.status == "step": elif message.status == "step":
chat_refs.append(doc_info) chat_exchanges.append(doc_exchange)
else: else:
history_refs.append(doc_info) history_exchanges.append(doc_exchange)
# Stop processing if we hit a first message # Stop processing if we hit a first message
if message.status == "first": if message.status == "first":
break break
# Sort both lists by datetime in descending order # Sort both lists by datetime in descending order
chat_refs.sort(key=lambda x: x["datetime"], reverse=True) chat_exchanges.sort(key=lambda x: x.documentsLabel, reverse=True)
history_refs.sort(key=lambda x: x["datetime"], reverse=True) history_exchanges.sort(key=lambda x: x.documentsLabel, reverse=True)
return { return {
"chat": chat_refs, "chat": chat_exchanges,
"history": history_refs "history": history_exchanges
} }
def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str: def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str:
"""Get document reference from ChatDocument""" """Get document reference from ChatDocument"""
return f"cdoc:{document.id}:{document.filename}" return f"docItem:{document.id}:{document.filename}"
def getDocumentReferenceFromMessage(self, message: ChatMessage) -> str: def getDocumentReferenceFromMessage(self, message: ChatMessage) -> str:
"""Get document reference from ChatMessage with action context""" """Get document reference from ChatMessage"""
if not message.actionId or not message.documentsLabel:
return None
# If documentsLabel already contains the full reference format, return it # If documentsLabel already contains the full reference format, return it
if message.documentsLabel.startswith("mdoc:"): if message.documentsLabel.startswith("docList:"):
return message.documentsLabel return message.documentsLabel
# Otherwise construct the reference using the action ID and documents label # Otherwise construct the reference using the message ID and documents label
return f"mdoc:{message.actionId}:{message.documentsLabel}" return f"docList:{message.id}:{message.documentsLabel}"
def getChatDocumentsFromDocumentReference(self, documentReference: str) -> List[ChatDocument]: def getChatDocumentsFromDocumentList(self, documentList: List[str]) -> List[ChatDocument]:
"""Get ChatDocuments from document reference""" """Get ChatDocuments from a list of document references"""
try: try:
# Parse reference format all_documents = []
parts = documentReference.split(':', 2) # Split into max 3 parts for doc_ref in documentList:
if len(parts) < 3: # Parse reference format
return [] parts = doc_ref.split(':', 2) # Split into max 3 parts
if len(parts) < 3:
continue
ref_type = parts[0] ref_type = parts[0]
ref_id = parts[1] ref_id = parts[1]
ref_label = parts[2] # Keep the full label ref_label = parts[2] # Keep the full label
if ref_type == "cdoc": if ref_type == "docItem":
# Handle ChatDocument reference: cdoc:<id>:<filename> # Handle ChatDocument reference: docItem:<id>:<filename>
# Find document in workflow messages # Find document in workflow messages
for message in self.workflow.messages: for message in self.workflow.messages:
if message.documents: if message.documents:
for doc in message.documents: for doc in message.documents:
if doc.id == ref_id: if doc.id == ref_id:
return [doc] all_documents.append(doc)
break
if any(doc.id == ref_id for doc in message.documents):
break
elif ref_type == "mdoc": elif ref_type == "docList":
# Handle document list reference: mdoc:<action.id>:<label> # Handle document list reference: docList:<message.id>:<label>
# Find message with matching action ID and documents label # Find message by ID
for message in self.workflow.messages: for message in self.workflow.messages:
if (message.actionId == ref_id and if str(message.id) == ref_id and message.documents:
message.documentsLabel == documentReference and # Compare full reference all_documents.extend(message.documents)
message.documents): break
return message.documents
# If not found by actionId, try to find by documentsLabel only return all_documents
# This handles cases where the reference format might be different
for message in self.workflow.messages:
if (message.documentsLabel and
message.documentsLabel == documentReference and
message.documents):
return message.documents
return []
except Exception as e: except Exception as e:
logger.error(f"Error getting documents from reference {documentReference}: {str(e)}") logger.error(f"Error getting documents from document list: {str(e)}")
return [] return []
def getConnectionReferenceList(self) -> List[str]: def getConnectionReferenceList(self) -> List[str]:
@ -421,15 +420,16 @@ Please provide a clear summary of this message."""
"""Create new file and return its ID""" """Create new file and return its ID"""
# Convert content to bytes based on base64 flag # Convert content to bytes based on base64 flag
if base64encoded: if base64encoded:
import base64
content_bytes = base64.b64decode(content) content_bytes = base64.b64decode(content)
else: else:
content_bytes = content.encode('utf-8') content_bytes = content.encode('utf-8')
# First create the file metadata # Create the file (hash and size are computed inside interfaceComponent)
file_item = self.interfaceComponent.createFile( file_item = self.interfaceComponent.createFile(
name=fileName, name=fileName,
mimeType=mimeType, mimeType=mimeType,
size=len(content_bytes) content=content_bytes
) )
# Then store the file data # Then store the file data
@ -454,7 +454,7 @@ Please provide a clear summary of this message."""
mimeType=mimeType mimeType=mimeType
) )
async def executeAction(self, methodName: str, actionName: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> ActionResult: async def executeAction(self, methodName: str, actionName: str, parameters: Dict[str, Any]) -> ActionResult:
"""Execute a method action""" """Execute a method action"""
try: try:
if methodName not in self.methods: if methodName not in self.methods:
@ -467,7 +467,7 @@ Please provide a clear summary of this message."""
action = method['actions'][actionName] action = method['actions'][actionName]
# Execute the action # Execute the action
return await action['method'](parameters, authData) return await action['method'](parameters)
except Exception as e: except Exception as e:
logger.error(f"Error executing method {methodName}.{actionName}: {str(e)}") logger.error(f"Error executing method {methodName}.{actionName}: {str(e)}")

View file

@ -32,10 +32,6 @@ LOGIC
└── Update workflow state └── Update workflow state
TODO methods:
- reultLabel not to generate in the function, but to be set according to the action definition
- after action execution to store the documents in db and in a message object
TODO TODO
- neutralizer to put back placeholders to the returned data - neutralizer to put back placeholders to the returned data

View file

@ -94,15 +94,7 @@ def create_test_workflow() -> ChatWorkflow:
def create_test_user_input() -> UserInputRequest: def create_test_user_input() -> UserInputRequest:
"""Create test user input with a candidate evaluation task""" """Create test user input with a candidate evaluation task"""
return UserInputRequest( return UserInputRequest(
prompt="""I have following list of job profiles from candidates (3 job profiles as text files) and want to know, who is best suited for the position of product designer (file with criteria). Create an evaluation matrix and rate all candidates according to the matrix, then produce PowerPoint presentation for the management to decide and store it on the SharePoint for the account p.motsch valueon. prompt="""I have following list of job profiles from candidates (3 job profiles as text files) and want to know, who is best suited for the position of product designer (file with criteria). Create an evaluation matrix and rate all candidates according to the matrix, then produce a presentation for the management to decide and store it on the SharePoint for an available account.
The task involves:
1. Analyze the job profiles of the 3 candidates
2. Review the product designer position criteria
3. Create a comprehensive evaluation matrix with relevant criteria
4. Rate each candidate against the evaluation matrix
5. Generate a professional PowerPoint presentation for management decision
6. Store the final presentation in SharePoint for p.motsch valueon account
Please ensure the evaluation includes: Please ensure the evaluation includes:
- Technical skills assessment - Technical skills assessment