gateway/modules/interfaces/lucydomInterface.py

1242 lines
No EOL
52 KiB
Python

"""
Interface to LucyDOM database and AI Connectors.
Uses the JSON connector for data access with added language support.
"""
import os
import logging
import uuid
from datetime import datetime
from typing import Dict, Any, List, Optional, Union
import hashlib
from modules.shared.mimeUtils import isTextMimeType
from modules.interfaces.lucydomAccess import LucydomAccess
# DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.connectors.connectorAiOpenai import ChatService
# Basic Configurations
from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__)
# Singleton factory for Lucydom instances with AI service per context
_lucydomInterfaces = {}
# Custom exceptions for file handling
class FileError(Exception):
"""Base class for file handling exceptions."""
pass
class FileNotFoundError(FileError):
"""Exception raised when a file is not found."""
pass
class FileStorageError(FileError):
"""Exception raised when there's an error storing a file."""
pass
class FilePermissionError(FileError):
"""Exception raised when there's a permission issue with a file."""
pass
class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file."""
pass
class LucydomInterface:
"""
Interface to the LucyDOM database.
Uses the JSON connector for data access.
"""
def __init__(self, currentUser: Dict[str, Any]):
"""Initializes the LucyDOM Interface with user context."""
logger.debug(f"Initializing LucydomInterface with currentUser={currentUser}")
# Ensure valid user context
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Add language settings
self.userLanguage = currentUser.get("language", "en") # Default user language
# Initialize database
self._initializeDatabase()
# Initialize standard records
self._initRecords()
# Initialize AI service
self.aiService = ChatService()
if not self.aiService:
logger.warning("AI service not available during LucydomInterface initialization")
# Initialize access control
self.access = LucydomAccess(self.currentUser, self.db)
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_LUCYDOM_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_LUCYDOM_DATABASE", "lucydom")
dbUser = APP_CONFIG.get("DB_LUCYDOM_USER")
dbPassword = APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
_mandateId=self._mandateId,
_userId=self._userId
)
# Set context
self.db.updateContext(self._mandateId, self._userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
try:
# Initialize standard prompts
self._initializeStandardPrompts()
# Add other record initializations here
logger.info("Standard records initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize standard records: {str(e)}")
raise
def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist."""
prompts = self.db.getRecordset("prompts")
logger.debug(f"Found {len(prompts)} existing prompts")
if not prompts:
logger.debug("Creating standard prompts")
# Define standard prompts
standardPrompts = [
{
"content": "Research the current market trends and developments in [TOPIC]. Collect information about leading companies, innovative products or services, and current challenges. Present the results in a structured overview with relevant data and sources.",
"name": "Web Research: Market Research"
},
{
"content": "Analyze the attached dataset on [TOPIC] and identify the most important trends, patterns, and anomalies. Perform statistical calculations to support your findings. Present the results in a clearly structured analysis and draw relevant conclusions.",
"name": "Analysis: Data Analysis"
},
{
"content": "Create a detailed protocol of our meeting on [TOPIC]. Capture all discussed points, decisions made, and agreed measures. Structure the protocol clearly with agenda items, participant list, and clear responsibilities for follow-up actions.",
"name": "Protocol: Meeting Minutes"
},
{
"content": "Develop a UI/UX design concept for [APPLICATION/WEBSITE]. Consider the target audience, main functions, and brand identity. Describe the visual design, navigation, interaction patterns, and information architecture. Explain how the design optimizes user-friendliness and user experience.",
"name": "Design: UI/UX Design"
},
{
"content": "Gib mir die ersten 1000 Primzahlen",
"name": "Code: Primzahlen"
},
{
"content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
"name": "Mail: Vorbereitung"
},
]
# Create prompts
for promptData in standardPrompts:
createdPrompt = self.db.recordCreate("prompts", promptData)
logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('_mandateId')}, user={createdPrompt.get('_userId')}")
else:
logger.debug("Prompts already exist, skipping creation")
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module."""
return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module."""
return self.access.canModify(table, recordId)
# Language support method
def setUserLanguage(self, languageCode: str):
"""Set the user's preferred language"""
self.userLanguage = languageCode
logger.debug(f"User language set to: {languageCode}")
# AI Call Root Function
async def callAi(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str:
"""Enhanced AI service call with language support."""
if not self.aiService:
logger.error("AI service not set in LucydomInterface")
return "Error: AI service not available"
# Add language instruction for user-facing responses
if produceUserAnswer and self.userLanguage:
ltext= f"Please respond in '{self.userLanguage}' language."
if messages and messages[0]["role"] == "system":
if "language" not in messages[0]["content"].lower():
messages[0]["content"] = f"{ltext} {messages[0]['content']}"
else:
# Insert a system message with language instruction
messages.insert(0, {
"role": "system",
"content": ltext
})
# Call the AI service
if temperature is not None:
return await self.aiService.callApi(messages, temperature=temperature)
else:
return await self.aiService.callApi(messages)
async def callAi4Image(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str:
"""Enhanced AI service call with language support."""
if not self.aiService:
logger.error("AI service not set in LucydomInterface")
return "Error: AI service not available"
return await self.aiService.analyzeImage(imageData, mimeType, prompt)
# Utilities
def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
def _getCurrentTimestamp(self) -> str:
"""Returns the current timestamp in ISO format"""
return datetime.now().isoformat()
# Prompt methods
def getAllPrompts(self) -> List[Dict[str, Any]]:
"""Returns prompts based on user access level."""
allPrompts = self.db.getRecordset("prompts")
return self._uam("prompts", allPrompts)
def getPrompt(self, promptId: str) -> Optional[Dict[str, Any]]:
"""Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
if not prompts:
return None
filteredPrompts = self._uam("prompts", prompts)
return filteredPrompts[0] if filteredPrompts else None
def createPrompt(self, content: str, name: str) -> Dict[str, Any]:
"""Creates a new prompt if user has permission."""
if not self._canModify("prompts"):
raise PermissionError("No permission to create prompts")
promptData = {
"content": content,
"name": name,
"createdAt": self._getCurrentTimestamp()
}
return self.db.recordCreate("prompts", promptData)
def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Dict[str, Any]:
"""Updates a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
if not prompt:
return None
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to update prompt {promptId}")
# Prepare data for update
promptData = {}
if content is not None:
promptData["content"] = content
if name is not None:
promptData["name"] = name
# Update prompt
return self.db.recordModify("prompts", promptId, promptData)
def deletePrompt(self, promptId: str) -> bool:
"""Deletes a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
if not prompt:
return False
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to delete prompt {promptId}")
return self.db.recordDelete("prompts", promptId)
# File Utilities
def calculateFileHash(self, fileContent: bytes) -> str:
"""Calculates a SHA-256 hash for the file content"""
return hashlib.sha256(fileContent).hexdigest()
def checkForDuplicateFile(self, fileHash: str) -> Optional[Dict[str, Any]]:
"""Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash,
"_mandateId": self._mandateId,
"_userId": self._userId
})
if files:
return files[0]
return None
def getMimeType(self, filename: str) -> str:
"""Determines the MIME type based on the file extension."""
import os
ext = os.path.splitext(filename)[1].lower()[1:]
extensionToMime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml",
"py": "text/x-python",
"js": "application/javascript",
"css": "text/css"
}
return extensionToMime.get(ext.lower(), "application/octet-stream")
# File methods - metadata-based operations
def getAllFiles(self) -> List[Dict[str, Any]]:
"""Returns files based on user access level."""
allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles)
def getFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files:
return None
filteredFiles = self._uam("files", files)
return filteredFiles[0] if filteredFiles else None
def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> Dict[str, Any]:
"""Creates a new file entry if user has permission."""
if not self._canModify("files"):
raise PermissionError("No permission to create files")
fileData = {
"_mandateId": self._mandateId,
"_userId": self._userId,
"name": name,
"mimeType": mimeType,
"size": size,
"fileHash": fileHash,
"creationDate": self._getCurrentTimestamp()
}
return self.db.recordCreate("files", fileData)
def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates file metadata if user has access."""
# Check if the file exists and user has access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
if not self._canModify("files", fileId):
raise PermissionError(f"No permission to update file {fileId}")
# Update file
return self.db.recordModify("files", fileId, updateData)
def deleteFile(self, fileId: str) -> bool:
"""Deletes a file if user has access."""
try:
# Check if the file exists and user has access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
if not self._canModify("files", fileId):
raise PermissionError(f"No permission to delete file {fileId}")
# Check for other references to this file (by hash)
fileHash = file.get("fileHash")
if fileHash:
otherReferences = [f for f in self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
if f.get("id") != fileId]
# Only delete associated fileData if no other references exist
if not otherReferences:
try:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries:
self.db.recordDelete("fileData", fileId)
logger.debug(f"FileData for file {fileId} deleted")
except Exception as e:
logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}")
# Delete the FileItem entry
return self.db.recordDelete("files", fileId)
except FileNotFoundError as e:
raise
except FilePermissionError as e:
raise
except Exception as e:
logger.error(f"Error deleting file {fileId}: {str(e)}")
raise FileDeletionError(f"Error deleting file: {str(e)}")
# FileData methods - data operations
def createFileData(self, fileId: str, data: bytes) -> bool:
"""Stores the binary data of a file in the database."""
try:
import base64
# Check file access
file = self.getFile(fileId)
if not file:
logger.error(f"File with ID {fileId} not found when storing data")
return False
# Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
if isTextFormat:
# Try to decode as text
try:
textContent = data.decode('utf-8')
fileData = textContent
base64Encoded = False
logger.debug(f"Stored file {fileId} as text")
except UnicodeDecodeError:
# Fallback to base64 if text decoding fails
encodedData = base64.b64encode(data).decode('utf-8')
fileData = encodedData
base64Encoded = True
logger.warning(f"Failed to decode text file {fileId}, falling back to base64")
else:
# Binary format - always use base64
encodedData = base64.b64encode(data).decode('utf-8')
fileData = encodedData
base64Encoded = True
logger.debug(f"Stored file {fileId} as base64")
# Create the fileData record with data and encoding flag
fileDataObj = {
"id": fileId,
"data": fileData,
"base64Encoded": base64Encoded
}
self.db.recordCreate("fileData", fileDataObj)
logger.debug(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error storing data for file {fileId}: {str(e)}")
return False
def getFileData(self, fileId: str) -> Optional[bytes]:
"""Returns the binary data of a file if user has access."""
# Check file access
file = self.getFile(fileId)
if not file:
logger.warning(f"No access to file ID {fileId}")
return None
import base64
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if not fileDataEntries:
logger.warning(f"No data found for file ID {fileId}")
return None
fileDataEntry = fileDataEntries[0]
if "data" not in fileDataEntry:
logger.warning(f"No data field in file data for ID {fileId}")
return None
data = fileDataEntry["data"]
base64Encoded = fileDataEntry.get("base64Encoded", False)
try:
if base64Encoded:
# Decode base64 to bytes
return base64.b64decode(data)
else:
# Convert text to bytes
return data.encode('utf-8')
except Exception as e:
logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None
def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool:
"""Updates file data if user has access."""
# Check file access
file = self.getFile(fileId)
if not file:
logger.error(f"File with ID {fileId} not found when updating data")
return False
if not self._canModify("files", fileId):
logger.error(f"No permission to update file data for {fileId}")
return False
try:
import base64
# Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
# Convert input data to the right format
if isinstance(data, bytes):
if isTextFormat:
try:
# Try to convert bytes to text
fileData = data.decode('utf-8')
base64Encoded = False
except UnicodeDecodeError:
# Fallback to base64 if text decoding fails
fileData = base64.b64encode(data).decode('utf-8')
base64Encoded = True
else:
# Binary format - use base64
fileData = base64.b64encode(data).decode('utf-8')
base64Encoded = True
elif isinstance(data, str):
if isTextFormat:
# Text format - store as text
fileData = data
base64Encoded = False
else:
# Check if it's already base64 encoded
try:
# Try to decode as base64 to validate
base64.b64decode(data)
fileData = data
base64Encoded = True
except:
# Not valid base64, encode the string
fileData = base64.b64encode(data.encode('utf-8')).decode('utf-8')
base64Encoded = True
else:
# Convert to string first
stringData = str(data)
if isTextFormat:
fileData = stringData
base64Encoded = False
else:
fileData = base64.b64encode(stringData.encode('utf-8')).decode('utf-8')
base64Encoded = True
# Check if a record already exists
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
dataUpdate = {
"data": fileData,
"base64Encoded": base64Encoded
}
if fileDataEntries:
# Update the existing record
self.db.recordModify("fileData", fileId, dataUpdate)
logger.debug(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
else:
# Create a new record
dataUpdate["id"] = fileId
self.db.recordCreate("fileData", dataUpdate)
logger.debug(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error updating data for file {fileId}: {str(e)}")
return False
def saveUploadedFile(self, fileContent: bytes, fileName: str) -> Dict[str, Any]:
"""Saves an uploaded file if user has permission."""
try:
# Check file creation permission
if not self._canModify("files"):
raise PermissionError("No permission to upload files")
logger.debug(f"Starting upload process for file: {fileName}")
if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}")
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
# Calculate file hash for deduplication
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)
fileSize = len(fileContent)
# Save metadata
logger.debug(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile(
name=fileName,
mimeType=mimeType,
size=fileSize,
fileHash=fileHash
)
# Save binary data
logger.debug(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
logger.debug(f"File upload process completed for: {fileName}")
return dbFile
except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
raise FileStorageError(f"Error saving file: {str(e)}")
def downloadFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file for download if user has access."""
try:
# Check file access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
# Get binary data
fileContent = self.getFileData(fileId)
if fileContent is None:
raise FileNotFoundError(f"Binary data for file with ID {fileId} not found")
return {
"id": fileId,
"name": file.get("name", f"file_{fileId}"),
"contentType": file.get("mimeType", "application/octet-stream"),
"size": file.get("size", len(fileContent)),
"content": fileContent
}
except FileNotFoundError as e:
raise
except Exception as e:
logger.error(f"Error downloading file {fileId}: {str(e)}")
raise FileError(f"Error downloading file: {str(e)}")
# Workflow methods
def getAllWorkflows(self) -> List[Dict[str, Any]]:
"""Returns workflows based on user access level."""
allWorkflows = self.db.getRecordset("workflows")
return self._uam("workflows", allWorkflows)
def getWorkflowsByUser(self, _userId: str) -> List[Dict[str, Any]]:
"""Returns workflows for a specific user if current user has access."""
# Get workflows by _userId
workflows = self.db.getRecordset("workflows", recordFilter={"_userId": _userId})
# Apply access control
return self._uam("workflows", workflows)
def getWorkflow(self, workflowId: str) -> Optional[Dict[str, Any]]:
"""Returns a workflow by ID if user has access."""
workflows = self.db.getRecordset("workflows", recordFilter={"id": workflowId})
if not workflows:
return None
filteredWorkflows = self._uam("workflows", workflows)
return filteredWorkflows[0] if filteredWorkflows else None
def createWorkflow(self, workflowData: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a new workflow if user has permission."""
if not self._canModify("workflows"):
raise PermissionError("No permission to create workflows")
# Make sure mandateId and userId are set
if "_mandateId" not in workflowData:
workflowData["_mandateId"] = self._mandateId
if "_userId" not in workflowData:
workflowData["_userId"] = self._userId
# Set timestamp if not present
currentTime = self._getCurrentTimestamp()
if "startedAt" not in workflowData:
workflowData["startedAt"] = currentTime
if "lastActivity" not in workflowData:
workflowData["lastActivity"] = currentTime
return self.db.recordCreate("workflows", workflowData)
def updateWorkflow(self, workflowId: str, workflowData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a workflow if user has access."""
# Check if the workflow exists and user has access
workflow = self.getWorkflow(workflowId)
if not workflow:
return None
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to update workflow {workflowId}")
# Set update time
workflowData["lastActivity"] = self._getCurrentTimestamp()
# Update workflow
return self.db.recordModify("workflows", workflowId, workflowData)
def deleteWorkflow(self, workflowId: str) -> bool:
"""Deletes a workflow if user has access."""
# Check if the workflow exists and user has access
workflow = self.getWorkflow(workflowId)
if not workflow:
return False
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to delete workflow {workflowId}")
# Delete workflow
return self.db.recordDelete("workflows", workflowId)
# Workflow Messages
def getWorkflowMessages(self, workflowId: str) -> List[Dict[str, Any]]:
"""Returns messages for a workflow if user has access to the workflow."""
# Check workflow access first
workflow = self.getWorkflow(workflowId)
if not workflow:
return []
# Get messages for this workflow
messages = self.db.getRecordset("workflowMessages", recordFilter={"workflowId": workflowId})
return messages # No further filtering needed since workflow access is already checked
def createWorkflowMessage(self, messageData: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a message for a workflow if user has access."""
try:
# Check required fields
requiredFields = ["id", "workflowId"]
for field in requiredFields:
if field not in messageData:
logger.error(f"Required field '{field}' missing in messageData")
raise ValueError(f"Required field '{field}' missing in message data")
# Check workflow access
workflowId = messageData["workflowId"]
workflow = self.getWorkflow(workflowId)
if not workflow:
raise PermissionError(f"No access to workflow {workflowId}")
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {workflowId}")
# Validate that ID is not None
if messageData["id"] is None:
messageData["id"] = f"msg_{uuid.uuid4()}"
logger.warning(f"Automatically generated ID for workflow message: {messageData['id']}")
# Ensure required fields are present
if "startedAt" not in messageData and "createdAt" not in messageData:
messageData["startedAt"] = self._getCurrentTimestamp()
if "createdAt" in messageData and "startedAt" not in messageData:
messageData["startedAt"] = messageData["createdAt"]
del messageData["createdAt"]
# Set status if not present
if "status" not in messageData:
messageData["status"] = "completed"
# Set sequence number if not present
if "sequenceNo" not in messageData:
# Get current messages to determine next sequence number
existingMessages = self.getWorkflowMessages(workflowId)
messageData["sequenceNo"] = len(existingMessages) + 1
# Ensure role and agentName are present
if "role" not in messageData:
messageData["role"] = "assistant" if messageData.get("agentName") else "user"
if "agentName" not in messageData:
messageData["agentName"] = ""
# Create message in database
createdMessage = self.db.recordCreate("workflowMessages", messageData)
# Update workflow's messageIds if this is a new message
if createdMessage:
# Get current messageIds or initialize empty list
messageIds = workflow.get("messageIds", [])
# Add the new message ID if not already in the list
if createdMessage["id"] not in messageIds:
messageIds.append(createdMessage["id"])
self.updateWorkflow(workflowId, {"messageIds": messageIds})
return createdMessage
except Exception as e:
logger.error(f"Error creating workflow message: {str(e)}")
return None
def updateWorkflowMessage(self, messageId: str, messageData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a workflow message if user has access to the workflow."""
try:
logger.debug(f"Updating message {messageId} in database")
# Ensure messageId is provided
if not messageId:
logger.error("No messageId provided for updateWorkflowMessage")
raise ValueError("messageId cannot be empty")
# Check if message exists in database
messages = self.db.getRecordset("workflowMessages", recordFilter={"id": messageId})
if not messages:
logger.warning(f"Message with ID {messageId} does not exist in database")
# If message doesn't exist but we have workflowId, create it
if "workflowId" in messageData:
workflowId = messageData.get("workflowId")
# Check workflow access
workflow = self.getWorkflow(workflowId)
if not workflow:
raise PermissionError(f"No access to workflow {workflowId}")
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {workflowId}")
logger.info(f"Creating new message with ID {messageId} for workflow {workflowId}")
return self.db.recordCreate("workflowMessages", messageData)
else:
logger.error(f"Workflow ID missing for new message {messageId}")
return None
# Update existing message
existingMessage = messages[0]
# Check workflow access
workflowId = existingMessage.get("workflowId")
workflow = self.getWorkflow(workflowId)
if not workflow:
raise PermissionError(f"No access to workflow {workflowId}")
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {workflowId}")
# Ensure required fields present
for key in ["role", "agentName"]:
if key not in messageData and key not in existingMessage:
messageData[key] = "assistant" if key == "role" else ""
# Ensure ID is in the dataset
if 'id' not in messageData:
messageData['id'] = messageId
# Convert createdAt to startedAt if needed
if "createdAt" in messageData and "startedAt" not in messageData:
messageData["startedAt"] = messageData["createdAt"]
del messageData["createdAt"]
# Update the message
updatedMessage = self.db.recordModify("workflowMessages", messageId, messageData)
if updatedMessage:
logger.debug(f"Message {messageId} updated successfully")
else:
logger.warning(f"Failed to update message {messageId}")
return updatedMessage
except Exception as e:
logger.error(f"Error updating message {messageId}: {str(e)}", exc_info=True)
raise ValueError(f"Error updating message {messageId}: {str(e)}")
def deleteWorkflowMessage(self, workflowId: str, messageId: str) -> bool:
"""Deletes a workflow message if user has access to the workflow."""
try:
# Check workflow access
workflow = self.getWorkflow(workflowId)
if not workflow:
logger.warning(f"No access to workflow {workflowId}")
return False
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {workflowId}")
# Check if the message exists
messages = self.getWorkflowMessages(workflowId)
message = next((m for m in messages if m.get("id") == messageId), None)
if not message:
logger.warning(f"Message {messageId} for workflow {workflowId} not found")
return False
# Delete the message from the database
return self.db.recordDelete("workflowMessages", messageId)
except Exception as e:
logger.error(f"Error deleting message {messageId}: {str(e)}")
return False
def deleteFileFromMessage(self, workflowId: str, messageId: str, fileId: str) -> bool:
"""Removes a file reference from a message if user has access."""
try:
# Check workflow access
workflow = self.getWorkflow(workflowId)
if not workflow:
logger.warning(f"No access to workflow {workflowId}")
return False
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {workflowId}")
logger.debug(f"Removing file {fileId} from message {messageId} in workflow {workflowId}")
# Get all workflow messages
allMessages = self.getWorkflowMessages(workflowId)
logger.debug(f"Workflow {workflowId} has {len(allMessages)} messages")
# Try different approaches to find the message
message = None
# Exact match
message = next((m for m in allMessages if m.get("id") == messageId), None)
# Case-insensitive match
if not message and isinstance(messageId, str):
message = next((m for m in allMessages
if isinstance(m.get("id"), str) and m.get("id").lower() == messageId.lower()), None)
# Partial match (starts with)
if not message and isinstance(messageId, str):
message = next((m for m in allMessages
if isinstance(m.get("id"), str) and m.get("id").startswith(messageId)), None)
if not message:
logger.warning(f"Message {messageId} not found in workflow {workflowId}")
return False
# Log the found message
logger.debug(f"Found message: {message.get('id')}")
# Check if message has documents
if "documents" not in message or not message["documents"]:
logger.warning(f"No documents in message {messageId}")
return False
# Log existing documents
documents = message.get("documents", [])
logger.debug(f"Message has {len(documents)} documents")
# Create a new list of documents without the one to delete
updatedDocuments = []
removed = False
for doc in documents:
docId = doc.get("id")
fileIdValue = doc.get("fileId")
# Flexible matching approach
shouldRemove = (
(docId == fileId) or
(fileIdValue == fileId) or
(isinstance(docId, str) and str(fileId) in docId) or
(isinstance(fileIdValue, str) and str(fileId) in fileIdValue)
)
if shouldRemove:
removed = True
logger.debug(f"Found file to remove: docId={docId}, fileId={fileIdValue}")
else:
updatedDocuments.append(doc)
if not removed:
logger.warning(f"No matching file {fileId} found in message {messageId}")
return False
# Update message with modified documents array
messageUpdate = {
"documents": updatedDocuments
}
# Apply the update directly to the database
updated = self.db.recordModify("workflowMessages", message["id"], messageUpdate)
if updated:
logger.debug(f"Successfully removed file {fileId} from message {messageId}")
return True
else:
logger.warning(f"Failed to update message {messageId} in database")
return False
except Exception as e:
logger.error(f"Error removing file {fileId} from message {messageId}: {str(e)}")
return False
# Workflow Logs
def getWorkflowLogs(self, workflowId: str) -> List[Dict[str, Any]]:
"""Returns logs for a workflow if user has access to the workflow."""
# Check workflow access first
workflow = self.getWorkflow(workflowId)
if not workflow:
return []
# Get logs for this workflow
return self.db.getRecordset("workflowLogs", recordFilter={"workflowId": workflowId})
def createWorkflowLog(self, logData: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a log entry for a workflow if user has access."""
# Check workflow access
workflowId = logData.get("workflowId")
if not workflowId:
logger.error("No workflowId provided for createWorkflowLog")
return None
workflow = self.getWorkflow(workflowId)
if not workflow:
logger.warning(f"No access to workflow {workflowId}")
return None
if not self._canModify("workflows", workflowId):
logger.warning(f"No permission to modify workflow {workflowId}")
return None
# Make sure required fields are present
if "timestamp" not in logData:
logData["timestamp"] = self._getCurrentTimestamp()
# Add status information if not present
if "status" not in logData and "type" in logData:
if logData["type"] == "error":
logData["status"] = "error"
else:
logData["status"] = "running"
# Add progress information if not present
if "progress" not in logData:
# Default progress values based on log type
if logData.get("type") == "info":
logData["progress"] = 50 # Default middle progress
elif logData.get("type") == "error":
logData["progress"] = -1 # Error state
elif logData.get("type") == "warning":
logData["progress"] = 50 # Default middle progress
return self.db.recordCreate("workflowLogs", logData)
# Workflow Management
def saveWorkflowState(self, workflow: Dict[str, Any], saveMessages: bool = True, saveLogs: bool = True) -> bool:
"""Saves workflow state if user has access."""
try:
workflowId = workflow.get("id")
if not workflowId:
return False
# Check workflow access
existingWorkflow = self.getWorkflow(workflowId)
if not existingWorkflow and not self._canModify("workflows"):
logger.warning(f"No permission to create workflow {workflowId}")
return False
if existingWorkflow and not self._canModify("workflows", workflowId):
logger.warning(f"No permission to update workflow {workflowId}")
return False
# Extract only the database-relevant workflow fields
workflowDbData = {
"id": workflowId,
"_mandateId": workflow.get("_mandateId", self._mandateId),
"_userId": workflow.get("_userId", self._userId),
"name": workflow.get("name", f"Workflow {workflowId}"),
"status": workflow.get("status", "completed"),
"startedAt": workflow.get("startedAt", self._getCurrentTimestamp()),
"lastActivity": workflow.get("lastActivity", self._getCurrentTimestamp()),
"dataStats": workflow.get("dataStats", {})
}
# Check if workflow already exists
if existingWorkflow:
self.updateWorkflow(workflowId, workflowDbData)
else:
self.createWorkflow(workflowDbData)
# Save messages
if saveMessages and "messages" in workflow:
for message in workflow["messages"]:
messageId = message.get("id")
if not messageId:
continue
# Get existing message from database
existingMessages = self.getWorkflowMessages(workflowId)
existingMessage = next((m for m in existingMessages if m.get("id") == messageId), None)
if existingMessage:
# Check if updates are needed
hasChanges = False
for key in ["role", "agentName", "content", "status", "documents"]:
if key in message and message.get(key) != existingMessage.get(key):
hasChanges = True
break
if hasChanges:
# Extract only relevant data for the database
messageData = {
"role": message.get("role", existingMessage.get("role", "unknown")),
"content": message.get("content", existingMessage.get("content", "")),
"agentName": message.get("agentName", existingMessage.get("agentName", "")),
"status": message.get("status", existingMessage.get("status", "completed")),
"documents": message.get("documents", existingMessage.get("documents", []))
}
self.updateWorkflowMessage(messageId, messageData)
else:
# Message doesn't exist in database yet
logger.warning(f"Message {messageId} in workflow {workflowId} not found in database")
# Save logs
if saveLogs and "logs" in workflow:
# Get existing logs
existingLogs = {log["id"]: log for log in self.getWorkflowLogs(workflowId)}
for log in workflow["logs"]:
logId = log.get("id")
if not logId:
continue
# Extract only relevant data for the database
logData = {
"id": logId,
"workflowId": workflowId,
"message": log.get("message", ""),
"type": log.get("type", "info"),
"timestamp": log.get("timestamp", self._getCurrentTimestamp()),
"agentName": log.get("agentName", "(undefined)"),
"status": log.get("status", "running"),
"progress": log.get("progress", 50)
}
# Create or update log
if logId in existingLogs:
self.db.recordModify("workflowLogs", logId, logData)
else:
self.db.recordCreate("workflowLogs", logData)
return True
except Exception as e:
logger.error(f"Error saving workflow state: {str(e)}")
return False
def loadWorkflowState(self, workflowId: str) -> Optional[Dict[str, Any]]:
"""Loads workflow state if user has access."""
try:
# Check workflow access
workflow = self.getWorkflow(workflowId)
if not workflow:
return None
logger.debug(f"Loaded base workflow {workflowId} from database")
# Load messages
messages = self.getWorkflowMessages(workflowId)
# Sort by sequence number
messages.sort(key=lambda x: x.get("sequenceNo", 0))
messageCount = len(messages)
logger.debug(f"Loaded {messageCount} messages for workflow {workflowId}")
# Check if messageIds exists and is valid
messageIds = workflow.get("messageIds", [])
if not messageIds or len(messageIds) != len(messages):
# Rebuild messageIds from messages
messageIds = [msg.get("id") for msg in messages]
# Update in database
self.updateWorkflow(workflowId, {"messageIds": messageIds})
logger.debug(f"Rebuilt messageIds for workflow {workflowId}")
# Log document counts for each message
for msg in messages:
docCount = len(msg.get("documents", []))
if docCount > 0:
logger.debug(f"Message {msg.get('id')} has {docCount} documents loaded from database")
# Load logs
logs = self.getWorkflowLogs(workflowId)
# Sort by timestamp
logs.sort(key=lambda x: x.get("timestamp", ""))
# Assemble complete workflow object
completeWorkflow = workflow.copy()
completeWorkflow["messages"] = messages
completeWorkflow["messageIds"] = messageIds
completeWorkflow["logs"] = logs
return completeWorkflow
except Exception as e:
logger.error(f"Error loading workflow state: {str(e)}")
return None
def getInterface(currentUser: Dict[str, Any]) -> 'LucydomInterface':
"""
Returns a LucydomInterface instance for the current user.
Handles initialization of database and records.
"""
# Get user context
mandateId = currentUser.get("_mandateId")
userId = currentUser.get("id")
if not mandateId or not userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Create context key
contextKey = f"{mandateId}_{userId}"
# Create new instance if not exists
if contextKey not in _lucydomInterfaces:
_lucydomInterfaces[contextKey] = LucydomInterface(currentUser)
return _lucydomInterfaces[contextKey]