From ff85bc03987dc7698da18437b86fbbe56c659caf Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 4 May 2025 00:55:18 +0200
Subject: [PATCH] prod azure 1.0.6
---
connectors/connectorDbJson.py | 202 +---
modules/BACKUP-lucydomInterface.py | 1343 ++++++++++++++++++++++
modules/gatewayInterface.py | 400 ++++---
modules/lucydomInterface.py | 735 ++++++------
result.txt | 1 -
static/1_generated_code.py | 38 +
static/2_execution_history.json | 19 +
primes.txt => static/3_prime_numbers.txt | 350 ++++++
8 files changed, 2336 insertions(+), 752 deletions(-)
create mode 100644 modules/BACKUP-lucydomInterface.py
delete mode 100644 result.txt
create mode 100644 static/1_generated_code.py
create mode 100644 static/2_execution_history.json
rename primes.txt => static/3_prime_numbers.txt (63%)
diff --git a/connectors/connectorDbJson.py b/connectors/connectorDbJson.py
index f4bdea80..449d3bb3 100644
--- a/connectors/connectorDbJson.py
+++ b/connectors/connectorDbJson.py
@@ -8,22 +8,10 @@ logger = logging.getLogger(__name__)
class DatabaseConnector:
"""
A connector for JSON-based data storage.
- Provides generic database operations with tenant and user context support.
+ Provides generic database operations without user/mandate filtering.
"""
def __init__(self, dbHost: str, dbDatabase: str, dbUser: str = None, dbPassword: str = None,
mandateId: int = None, userId: int = None, skipInitialIdLookup: bool = False):
- """
- Initializes the JSON database connector.
-
- Args:
- dbHost: Directory for the JSON files
- dbDatabase: Database name
- dbUser: Username for authentication (optional)
- dbPassword: API key for authentication (optional)
- mandateId: Context parameter for the tenant
- userId: Context parameter for the user
- skipInitialIdLookup: When True, skips looking up initial IDs for mandateId and userId
- """
# Store the input parameters
self.dbHost = dbHost
self.dbDatabase = dbDatabase
@@ -177,35 +165,6 @@ class DatabaseConnector:
logger.error(f"Error saving table {table}: {e}")
return False
- def _filterByContext(self, records: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- """
- Filters records by tenant and user context,
- if these fields exist in the record.
- """
- filteredRecords = []
-
- for record in records:
- # Check if mandateId exists in the record and is not null
- hasMandate = "mandateId" in record and record["mandateId"] is not None and record["mandateId"] != ""
-
- # Check if userId exists in the record and is not null
- hasUser = "userId" in record and record["userId"] is not None and record["userId"] != ""
-
- # If both exist, filter accordingly
- if hasMandate and hasUser:
- if record["mandateId"] == self.mandateId:
- filteredRecords.append(record)
- # If only mandateId exists
- elif hasMandate and not hasUser:
- if record["mandateId"] == self.mandateId:
- filteredRecords.append(record)
- # If neither mandateId nor userId exist, add the record
- elif not hasMandate and not hasUser:
- filteredRecords.append(record)
-
- return filteredRecords
-
-
def _applyRecordFilter(self, records: List[Dict[str, Any]], recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Applies a record filter to the records"""
if not recordFilter:
@@ -244,21 +203,10 @@ class DatabaseConnector:
return filteredRecords
def _registerInitialId(self, table: str, initialId: int) -> bool:
- """
- Registers the initial ID for a table.
-
- Args:
- table: Name of the table
- initialId: The initial ID
-
- Returns:
- True on success, False on error
- """
+ """Registers the initial ID for a table."""
try:
- # Load the current system table
systemData = self._loadSystemTable()
- # Only register if not already present
if table not in systemData:
systemData[table] = initialId
success = self._saveSystemTable(systemData)
@@ -271,20 +219,10 @@ class DatabaseConnector:
return False
def _removeInitialId(self, table: str) -> bool:
- """
- Removes the initial ID for a table from the system table.
-
- Args:
- table: Name of the table
-
- Returns:
- True on success, False on error
- """
+ """Removes the initial ID for a table from the system table."""
try:
- # Load the current system table
systemData = self._loadSystemTable()
- # Remove the entry if it exists
if table in systemData:
del systemData[table]
success = self._saveSystemTable(systemData)
@@ -299,12 +237,7 @@ class DatabaseConnector:
# Public API
def getTables(self) -> List[str]:
- """
- Returns a list of all available tables.
-
- Returns:
- List of table names
- """
+ """Returns a list of all available tables."""
tables = []
try:
@@ -318,38 +251,18 @@ class DatabaseConnector:
return tables
def getFields(self, table: str) -> List[str]:
- """
- Returns a list of all fields in a table.
-
- Args:
- table: Name of the table
-
- Returns:
- List of field names
- """
- # Load the table data
+ """Returns a list of all fields in a table."""
data = self._loadTable(table)
if not data:
return []
- # Take the first record as a reference for the fields
fields = list(data[0].keys()) if data else []
return fields
def getSchema(self, table: str, language: str = None) -> Dict[str, Dict[str, Any]]:
- """
- Returns a schema object for a table with data types and labels.
-
- Args:
- table: Name of the table
- language: Language for the labels (optional)
-
- Returns:
- Schema object with fields, data types and labels
- """
- # Load the table data
+ """Returns a schema object for a table with data types and labels."""
data = self._loadTable(table)
schema = {}
@@ -357,14 +270,10 @@ class DatabaseConnector:
if not data:
return schema
- # Take the first record as a reference for the fields and data types
firstRecord = data[0]
for field, value in firstRecord.items():
- # Determine the data type
dataType = type(value).__name__
-
- # Create label (default is the field name)
label = field
schema[field] = {
@@ -375,32 +284,18 @@ class DatabaseConnector:
return schema
def getRecordset(self, table: str, fieldFilter: List[str] = None, recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
- """
- Returns a list of records from a table, filtered by criteria.
-
- Args:
- table: Name of the table
- fieldFilter: Filter for fields (which fields should be returned)
- recordFilter: Filter for records (which records should be returned)
-
- Returns:
- List of filtered records
- """
- # Load the table data
+ """Returns a list of records from a table, filtered by criteria."""
data = self._loadTable(table)
- logger.debug(f"getRecordset: data volume of {len(data)} bytes")
-
- # Filter by tenant and user context
- filteredData = self._filterByContext(data)
+ logger.debug(f"getRecordset: data volume of {len(data)} records")
# Apply recordFilter if available
if recordFilter:
- filteredData = self._applyRecordFilter(filteredData, recordFilter)
+ data = self._applyRecordFilter(data, recordFilter)
# If fieldFilter is available, reduce the fields
if fieldFilter and isinstance(fieldFilter, list):
result = []
- for record in filteredData:
+ for record in data:
filteredRecord = {}
for field in fieldFilter:
if field in record:
@@ -408,23 +303,13 @@ class DatabaseConnector:
result.append(filteredRecord)
return result
- return filteredData
+ return data
def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Creates a new record in the table.
-
- Args:
- table: Name of the table
- recordData: Data for the new record
-
- Returns:
- The created record
- """
- # Load the table data
+ """Creates a new record in the table."""
data = self._loadTable(table)
- # Add mandateId and userId if not present or 0
+ # Add mandateId and userId if not present
if "mandateId" not in recordData or recordData["mandateId"] == 0:
recordData["mandateId"] = self.mandateId
@@ -453,30 +338,15 @@ class DatabaseConnector:
raise ValueError(f"Error creating the record in table {table}")
def recordDelete(self, table: str, recordId: Union[str, int]) -> bool:
- """
- Deletes a record from the table.
-
- Args:
- table: Name of the table
- recordId: ID of the record to delete
-
- Returns:
- True on success, False on error
- """
- # Load table data
+ """Deletes a record from the table."""
data = self._loadTable(table)
# Search for the record
for i, record in enumerate(data):
if "id" in record and record["id"] == recordId:
- # Check if the record belongs to the current mandate
- if "mandateId" in record and record["mandateId"] != self.mandateId:
- raise ValueError("Not your mandate")
-
# Check if it's an initial record
initialId = self.getInitialId(table)
if initialId is not None and initialId == recordId:
- # Remove this entry from the system table
self._removeInitialId(table)
logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
@@ -490,27 +360,12 @@ class DatabaseConnector:
return False
def recordModify(self, table: str, recordId: Union[str, int], recordData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Modifies a record in the table.
-
- Args:
- table: Name of the table
- recordId: ID of the record to modify
- recordData: New data for the record
-
- Returns:
- The updated record
- """
- # Load table data
+ """Modifies a record in the table."""
data = self._loadTable(table)
# Search for the record
for i, record in enumerate(data):
if "id" in record and record["id"] == recordId:
- # Check if the record belongs to the current mandate
- if "mandateId" in record and record["mandateId"] != self.mandateId:
- raise ValueError("Not your mandate")
-
# Prevent changing the ID
if "id" in recordData and recordData["id"] != recordId:
raise ValueError(f"The ID of a record in table {table} cannot be changed")
@@ -529,28 +384,12 @@ class DatabaseConnector:
raise ValueError(f"Record with ID {recordId} not found in table {table}")
def hasInitialId(self, table: str) -> bool:
- """
- Checks if an initial ID is registered for a table.
-
- Args:
- table: Name of the table
-
- Returns:
- True if an initial ID is registered, otherwise False
- """
+ """Checks if an initial ID is registered for a table."""
systemData = self._loadSystemTable()
return table in systemData
def getInitialId(self, table: str) -> Optional[int]:
- """
- Returns the initial ID for a table.
-
- Args:
- table: Name of the table
-
- Returns:
- The initial ID or None if not present
- """
+ """Returns the initial ID for a table."""
systemData = self._loadSystemTable()
initialId = systemData.get(table)
logger.debug(f"Database '{self.dbDatabase}': Initial ID for table '{table}' is {initialId}")
@@ -559,11 +398,6 @@ class DatabaseConnector:
return initialId
def getAllInitialIds(self) -> Dict[str, int]:
- """
- Returns all registered initial IDs.
-
- Returns:
- Dictionary with table names as keys and initial IDs as values
- """
+ """Returns all registered initial IDs."""
systemData = self._loadSystemTable()
return systemData.copy() # Return a copy to protect the original
\ No newline at end of file
diff --git a/modules/BACKUP-lucydomInterface.py b/modules/BACKUP-lucydomInterface.py
new file mode 100644
index 00000000..50ae9fc1
--- /dev/null
+++ b/modules/BACKUP-lucydomInterface.py
@@ -0,0 +1,1343 @@
+"""
+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 importlib
+import hashlib
+
+from modules.mimeUtils import isTextMimeType, determineContentEncoding
+
+# DYNAMIC PART: Connectors to the Interface
+from connectors.connectorDbJson import DatabaseConnector
+from connectors.connectorAiOpenai import ChatService
+
+# Basic Configurations
+from modules.configuration import APP_CONFIG
+logger = logging.getLogger(__name__)
+
+# 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, mandateId: int, userId: int):
+ """
+ Initializes the LucyDOM Interface with mandate and user context.
+
+ Args:
+ mandateId: ID of the current mandate
+ userId: ID of the current user
+ """
+ self.mandateId = mandateId
+ self.userId = userId
+
+ # Add language settings
+ self.userLanguage = "en" # Default user language
+ self.aiService = None # Will be set externally
+
+ # Import data model module
+ try:
+ self.modelModule = importlib.import_module("modules.lucydomModel")
+ logger.info("lucydomModel successfully imported")
+ except ImportError as e:
+ logger.error(f"Error importing lucydomModel: {e}")
+ raise
+
+ # Initialize database if needed
+ self._initializeDatabase()
+
+ def _initializeDatabase(self):
+ """
+ Initializes the database with minimal objects for the logged-in user in the mandate, if it doesn't exist yet.
+ No initialization without a valid user.
+ Creates an initial dataset for each table defined in the data model.
+ """
+ effectiveMandateId = self.mandateId
+ effectiveUserId = self.userId
+ if effectiveMandateId is None or effectiveUserId is None:
+ #data available
+ return
+
+ self.db = DatabaseConnector(
+ dbHost=APP_CONFIG.get("DB_LUCYDOM_HOST"),
+ dbDatabase=APP_CONFIG.get("DB_LUCYDOM_DATABASE"),
+ dbUser=APP_CONFIG.get("DB_LUCYDOM_USER"),
+ dbPassword=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"),
+ mandateId=self.mandateId,
+ userId=self.userId,
+ skipInitialIdLookup=True
+ )
+
+ # Initialize standard prompts for different areas
+ prompts = self.db.getRecordset("prompts")
+ if not prompts:
+ logger.info("Creating standard prompts")
+
+ # Define standard prompts
+ standardPrompts = [
+ {
+ "mandateId": effectiveMandateId,
+ "userId": effectiveUserId,
+ "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"
+ },
+ {
+ "mandateId": effectiveMandateId,
+ "userId": effectiveUserId,
+ "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"
+ },
+ {
+ "mandateId": effectiveMandateId,
+ "userId": effectiveUserId,
+ "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"
+ },
+ {
+ "mandateId": effectiveMandateId,
+ "userId": effectiveUserId,
+ "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"
+ },
+ {
+ "mandateId": effectiveMandateId,
+ "userId": effectiveUserId,
+ "content": "Gib mir die ersten 1000 Primzahlen",
+ "name": "Code: Primzahlen"
+ }
+ ]
+
+ # Create prompts
+ for promptData in standardPrompts:
+ createdPrompt = self.db.recordCreate("prompts", promptData)
+ logger.info(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']}")
+
+ # Language support methods
+
+ def setUserLanguage(self, languageCode: str):
+ """Set the user's preferred language"""
+ self.userLanguage = languageCode
+ logger.info(f"User language set to: {languageCode}")
+
+ async def callAi(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str:
+ """
+ Enhanced AI service call with language support
+
+ Args:
+ messages: List of message dictionaries
+ produceUserAnswer: Whether this response is for the end-user
+ temperature: Optional temperature setting
+
+ Returns:
+ AI response text
+ """
+ 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)
+
+ # Utilities
+
+ def getInitialId(self, table: str) -> Optional[int]:
+ """
+ Returns the initial ID for a table.
+
+ Args:
+ table: Name of the table
+
+ Returns:
+ The initial ID or None if not present
+ """
+ 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 all prompts for the current mandate"""
+ return self.db.getRecordset("prompts")
+
+ def getPrompt(self, promptId: int) -> Optional[Dict[str, Any]]:
+ """Returns a prompt by its ID"""
+ prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
+ if prompts:
+ return prompts[0]
+ return None
+
+ def createPrompt(self, content: str, name: str) -> Dict[str, Any]:
+ """Creates a new prompt"""
+ promptData = {
+ "mandateId": self.mandateId,
+ "userId": self.userId,
+ "content": content,
+ "name": name,
+ "createdAt": self._getCurrentTimestamp()
+ }
+
+ return self.db.recordCreate("prompts", promptData)
+
+ def updatePrompt(self, promptId: int, content: str = None, name: str = None) -> Dict[str, Any]:
+ """
+ Updates an existing prompt
+
+ Args:
+ promptId: ID of the prompt to update
+ content: New content for the prompt
+
+ Returns:
+ The updated prompt object
+ """
+ # Check if the prompt exists
+ prompt = self.getPrompt(promptId)
+ if not prompt:
+ return None
+
+ # 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: int) -> bool:
+ """
+ Deletes a prompt from the database
+
+ Args:
+ promptId: ID of the prompt to delete
+
+ Returns:
+ True if the prompt was successfully deleted, otherwise False
+ """
+ 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"""
+ files = self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
+ 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 all files for the current mandate without binary data.
+
+ Returns:
+ List of FileItem objects without binary data
+ """
+ files = self.db.getRecordset("files")
+ return files
+
+ def getFile(self, fileId: int) -> Optional[Dict[str, Any]]:
+ """
+ Returns a file by its ID, without binary data.
+
+ Args:
+ fileId: ID of the file
+
+ Returns:
+ FileItem without binary data or None if not found
+ """
+ files = self.db.getRecordset("files", recordFilter={"id": fileId})
+ if files:
+ return files[0]
+ return None
+
+ def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> Dict[str, Any]:
+ """
+ Creates a new file entry in the database without content.
+ The actual file content is stored separately in the FileData table.
+
+ Args:
+ name: Name of the file
+ mimeType: MIME type of the file
+ size: Size of the file in bytes
+ fileHash: Hash value of the file for deduplication
+
+ Returns:
+ The created FileItem object
+ """
+ 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: int, updateData: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Updates the metadata of an existing file without affecting the binary data.
+
+ Args:
+ fileId: ID of the file to update
+ updateData: Dictionary with fields to update
+
+ Returns:
+ The updated FileItem object
+ """
+ # Check if the file exists
+ file = self.getFile(fileId)
+ if not file:
+ raise FileNotFoundError(f"File with ID {fileId} not found")
+
+ # Update file
+ return self.db.recordModify("files", fileId, updateData)
+
+ def deleteFile(self, fileId: int) -> bool:
+ """
+ Deletes a file from the database (metadata and content).
+
+ Args:
+ fileId: ID of the file
+
+ Returns:
+ True on success, False on error
+ """
+ try:
+ # Find the file in the database
+ file = self.getFile(fileId)
+
+ if not file:
+ raise FileNotFoundError(f"File with ID {fileId} not found")
+
+ # Check if the file belongs to the current mandate
+ if file.get("mandateId") != self.mandateId:
+ raise FilePermissionError(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]
+
+ # If other files reference this content, only delete the database entry for FileItem
+ if otherReferences:
+ logger.info(f"Other references to the file content found, only FileItem will be deleted: {fileId}")
+ else:
+ # Also delete the file content in the FileData table
+ try:
+ fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
+ if fileDataEntries:
+ self.db.recordDelete("fileData", fileId)
+ logger.info(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:
+ # Pass through FileNotFoundError
+ raise
+ except FilePermissionError as e:
+ # Pass through FilePermissionError
+ 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
+
+ """
+ This contains the modified file handling methods for the LucyDOMInterface class
+ to implement consistent handling of base64 encoding flags.
+ """
+
+ def createFileData(self, fileId: int, data: bytes) -> bool:
+ """
+ Stores the binary data of a file in the database, using base64 encoding for binary files.
+ Always sets the base64Encoded flag appropriately.
+
+ Args:
+ fileId: ID of the associated file
+ data: Binary data
+
+ Returns:
+ True on success, False on error
+ """
+ try:
+ import base64
+
+ # Check the file metadata to determine if this should be stored as text or base64
+ 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 that should be stored as text
+ mimeType = file.get("mimeType", "application/octet-stream")
+ isTextFormat = isTextMimeType(mimeType)
+
+ base64Encoded = False
+ fileData = None
+
+ if isTextFormat:
+ # Try to decode as text
+ try:
+ # Convert bytes to text
+ 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.info(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: int) -> Optional[bytes]:
+ """
+ Returns the binary data of a file.
+ Uses the base64Encoded flag to determine if decoding is necessary.
+
+ Args:
+ fileId: ID of the file
+
+ Returns:
+ Binary data or None if not found
+ """
+ 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: int, data: Union[bytes, str]) -> bool:
+ """
+ Updates the binary data of a file in the database.
+ Handles base64 encoding based on the file type.
+
+ Args:
+ fileId: ID of the file
+ data: New binary data or text content
+
+ Returns:
+ True on success, False on error
+ """
+ try:
+ import base64
+
+ # Check file metadata to determine if this should be stored as text or base64
+ file = self.getFile(fileId)
+ if not file:
+ logger.error(f"File with ID {fileId} not found when updating data")
+ return False
+
+ # Determine if this is a text-based format that should be stored as text
+ mimeType = file.get("mimeType", "application/octet-stream")
+ isTextFormat = (
+ mimeType.startswith("text/") or
+ mimeType in [
+ "application/json",
+ "application/xml",
+ "application/javascript",
+ "application/x-python",
+ "image/svg+xml"
+ ]
+ )
+
+ base64Encoded = False
+ fileData = None
+
+ # Convert input data to the right format based on its type and the file's 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.info(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
+ else:
+ # Create a new record
+ dataUpdate["id"] = fileId
+ self.db.recordCreate("fileData", dataUpdate)
+ logger.info(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 in the database.
+ Metadata is stored in the 'files' table,
+ Binary data in the 'fileData' table with the appropriate base64Encoded flag.
+
+ Args:
+ fileContent: Binary data of the file
+ fileName: Name of the file
+
+ Returns:
+ Dictionary with metadata of the saved file
+ """
+ try:
+ # Debug: Log the start of the file upload process
+ logger.info(f"Starting upload process for file: {fileName}")
+
+ # Debug: Check if fileContent is valid bytes
+ 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
+ existingFile = self.checkForDuplicateFile(fileHash)
+ if existingFile:
+ # Simply return the existing file metadata
+ logger.info(f"Duplicate found for {fileName}: {existingFile['id']}")
+ return existingFile
+
+ # Determine MIME type
+ mimeType = self.getMimeType(fileName)
+
+ # Determine file size
+ fileSize = len(fileContent)
+
+ # 1. Save metadata in the 'files' table
+ logger.info(f"Saving file metadata to database for file: {fileName}")
+ dbFile = self.createFile(
+ name=fileName,
+ mimeType=mimeType,
+ size=fileSize,
+ fileHash=fileHash
+ )
+
+ # 2. Save binary data with appropriate base64 encoding based on file type
+ logger.info(f"Saving file content to database for file: {fileName}")
+ self.createFileData(dbFile["id"], fileContent)
+
+ # Debug: Export file to static folder
+ self._exportFileToStatic(fileContent, dbFile["id"], fileName) # DEBUG TODO
+
+ # Debug: Verify database record was created
+ if not dbFile:
+ logger.warning(f"Database record for file {fileName} was not created properly")
+ else:
+ logger.debug(f"Database record created for file {fileName}")
+
+ logger.info(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: int) -> Optional[Dict[str, Any]]:
+ """
+ Returns a file for download, including binary data.
+ Uses the base64Encoded flag to determine how to process the file data.
+
+ Args:
+ fileId: ID of the file
+
+ Returns:
+ Dictionary with file data and metadata or None if not found
+ """
+ try:
+ # 1. Get metadata from the 'files' table
+ file = self.getFile(fileId)
+
+ if not file:
+ raise FileNotFoundError(f"File with ID {fileId} not found")
+
+ # 2. Get binary data from the 'fileData' table using the new flag-aware method
+ 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:
+ # Re-raise FileNotFoundError as is
+ raise
+ except Exception as e:
+ logger.error(f"Error downloading file {fileId}: {str(e)}")
+ raise FileError(f"Error downloading file: {str(e)}")
+
+ def _exportFileToStatic(self, fileContent: bytes, fileId: int, fileName: str):
+ debugFilename = f"{fileId}_{fileName}"
+ with open(f"./static/{debugFilename}", 'wb') as f:
+ f.write(fileContent)
+
+ # Workflow methods
+
+ def getAllWorkflows(self) -> List[Dict[str, Any]]:
+ """Returns all workflows for the current mandate"""
+ return self.db.getRecordset("workflows")
+
+ def getWorkflowsByUser(self, userId: int) -> List[Dict[str, Any]]:
+ """Returns all workflows for a user"""
+ return self.db.getRecordset("workflows", recordFilter={"userId": userId})
+
+ def getWorkflow(self, workflowId: str) -> Optional[Dict[str, Any]]:
+ """Returns a workflow by its ID"""
+ workflows = self.db.getRecordset("workflows", recordFilter={"id": workflowId})
+ if workflows:
+ return workflows[0]
+ return None
+
+ def createWorkflow(self, workflowData: Dict[str, Any]) -> Dict[str, Any]:
+ """Creates a new workflow in the database"""
+ # 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 an existing workflow.
+
+ Args:
+ workflowId: ID of the workflow to update
+ workflowData: New data for the workflow
+
+ Returns:
+ The updated workflow object
+ """
+ # Check if the workflow exists
+ workflow = self.getWorkflow(workflowId)
+ if not workflow:
+ return None
+
+ # 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 from the database.
+
+ Args:
+ workflowId: ID of the workflow to delete
+
+ Returns:
+ True on success, False if the workflow doesn't exist
+ """
+ # Check if the workflow exists
+ workflow = self.getWorkflow(workflowId)
+ if not workflow:
+ return False
+
+ # Check if the user is the owner or has admin rights
+ if workflow.get("userId") != self.userId:
+ # Here could be a check for admin rights
+ return False
+
+ # Delete workflow
+ return self.db.recordDelete("workflows", workflowId)
+
+
+ # Workflow Messages
+
+ def getWorkflowMessages(self, workflowId: str) -> List[Dict[str, Any]]:
+ """Returns all messages of a workflow"""
+ return self.db.getRecordset("workflowMessages", recordFilter={"workflowId": workflowId})
+
+ def createWorkflowMessage(self, messageData: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Creates a new message for a workflow.
+
+ Args:
+ messageData: The message data
+
+ Returns:
+ The created message or None on error
+ """
+ try:
+ # Check if required fields are present
+ 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")
+
+ # 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(messageData["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"] = ""
+
+ # Debug log for data to create
+ logger.debug(f"Creating workflow message with data: {messageData}")
+
+ # Create message in database
+ createdMessage = self.db.recordCreate("workflowMessages", messageData)
+
+ # Update workflow's messageIds if this is a new message
+ if createdMessage:
+ workflowId = messageData["workflowId"]
+ workflow = self.getWorkflow(workflowId)
+
+ if workflow:
+ # 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 instead of raising to avoid cascading failures
+ return None
+
+ def updateWorkflowMessage(self, messageId: str, messageData: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Updates an existing workflow message in the database.
+
+ Args:
+ messageId: ID of the message
+ messageData: Data to update
+
+ Returns:
+ The updated message object or None on error
+ """
+ try:
+ # Debug info
+ 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:
+ logger.info(f"Creating new message with ID {messageId} for workflow {messageData.get('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]
+
+ # 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.info(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)
+ # Re-raise with full information
+ raise ValueError(f"Error updating message {messageId}: {str(e)}")
+
+ def deleteWorkflowMessage(self, workflowId: str, messageId: str) -> bool:
+ """
+ Deletes a message from a workflow in the database.
+
+ Args:
+ workflowId: ID of the associated workflow
+ messageId: ID of the message to delete
+
+ Returns:
+ True on success, False on error
+ """
+ try:
+ # 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: int) -> bool:
+ """
+ Removes a file reference from a message.
+ The file itself is not deleted, only the reference in the message.
+ Enhanced version with improved file matching.
+
+ Args:
+ workflowId: ID of the associated workflow
+ messageId: ID of the message
+ fileId: ID of the file to remove
+
+ Returns:
+ True on success, False on error
+ """
+ try:
+ # Log operation
+ logger.info(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.info(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")
+ for i, doc in enumerate(documents):
+ docId = doc.get("id", "unknown")
+ fileIdValue = doc.get("fileId", "unknown")
+ logger.debug(f"Document {i}: docId={docId}, fileId={fileIdValue}")
+
+ # 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.info(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.info(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 all log entries for a workflow"""
+ return self.db.getRecordset("workflowLogs", recordFilter={"workflowId": workflowId})
+
+ def createWorkflowLog(self, logData: Dict[str, Any]) -> Dict[str, Any]:
+ """Creates a new log entry for a workflow"""
+ # 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 the state of a workflow to the database.
+ Workflow data is updated, but messages are stored separately.
+
+ Args:
+ workflow: The workflow object
+ saveMessages: Flag to determine if messages should be saved
+ saveLogs: Flag to determine if logs should be saved
+
+ Returns:
+ True on success, False on failure
+ """
+ try:
+ workflowId = workflow.get("id")
+ if not workflowId:
+ return False
+
+ # Extract only the database-relevant workflow fields
+ # IMPORTANT: Don't store messages in the workflow table!
+ 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
+ existingWorkflow = self.getWorkflow(workflowId)
+ 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
+
+ # Since each message is already saved with createWorkflowMessage,
+ # we only need to check if updates are necessary
+ # First, 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
+ # It should have been saved via createWorkflowMessage
+ # If not, log a warning
+ 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 the complete state of a workflow from the database.
+ This includes the workflow itself, messages, and logs.
+
+ Args:
+ workflowId: ID of the workflow to load
+
+ Returns:
+ The complete workflow object or None on error
+ """
+ try:
+ # Load base workflow
+ workflow = self.getWorkflow(workflowId)
+ if not workflow:
+ return None
+
+ # Log the workflow base retrieval
+ 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))
+
+ # Debug log for messages and document counts
+ 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.info(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.info(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 # Ensure messageIds is included
+ completeWorkflow["logs"] = logs
+
+ return completeWorkflow
+ except Exception as e:
+ logger.error(f"Error loading workflow state: {str(e)}")
+ return None
+
+
+# Singleton factory for LucyDOMInterface instances per context
+_lucydomInterfaces = {}
+
+def getLucydomInterface(mandateId: int = 0, userId: int = 0) -> LucyDOMInterface:
+ """
+ Returns a LucyDOMInterface instance for the specified context.
+ Reuses existing instances.
+
+ Args:
+ mandateId: ID of the mandate
+ userId: ID of the user
+
+ Returns:
+ LucyDOMInterface instance
+ """
+ contextKey = f"{mandateId}_{userId}"
+ if contextKey not in _lucydomInterfaces:
+ # Create new interface instance
+ interface = LucyDOMInterface(mandateId, userId)
+ # Initialize AI service
+ aiService = ChatService()
+ interface.aiService = aiService # Directly set the attribute
+ _lucydomInterfaces[contextKey] = interface
+ return _lucydomInterfaces[contextKey]
+
+# Init
+getLucydomInterface()
\ No newline at end of file
diff --git a/modules/gatewayInterface.py b/modules/gatewayInterface.py
index 3e1120c7..7c7f0aea 100644
--- a/modules/gatewayInterface.py
+++ b/modules/gatewayInterface.py
@@ -25,13 +25,7 @@ class GatewayInterface:
"""
def __init__(self, mandateId: int = None, userId: int = None):
- """
- Initializes the Gateway Interface with optional mandate and user context.
-
- Args:
- mandateId: ID of the current mandate (optional)
- userId: ID of the current user (optional)
- """
+ """Initializes the Gateway Interface with optional mandate and user context."""
# Context can be empty during initialization
self.mandateId = mandateId
self.userId = userId
@@ -46,12 +40,35 @@ class GatewayInterface:
# Initialize database
self._initializeDatabase()
+
+ # Load user information
+ self.currentUser = self._getCurrentUserInfo()
+
+ # Initialize standard records if needed
+ self._initRecords()
+
+ def _getCurrentUserInfo(self) -> Dict[str, Any]:
+ """Gets information about the current user including privileges."""
+ # For initialization, set default values
+ userInfo = {
+ "id": self.userId,
+ "mandateId": self.mandateId,
+ "privilege": "user", # Default privilege level
+ "language": "en"
+ }
+
+ # Try to load actual user info if IDs are provided
+ if self.userId:
+ userRecords = self.db.getRecordset("users", recordFilter={"id": self.userId})
+ if userRecords:
+ user = userRecords[0]
+ userInfo["privilege"] = user.get("privilege", "user")
+ userInfo["language"] = user.get("language", "en")
+
+ return userInfo
def _initializeDatabase(self):
- """
- Initializes the database with minimal objects
- """
-
+ """Initializes the database connection."""
self.db = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
@@ -61,7 +78,13 @@ class GatewayInterface:
userId=self.userId if self.userId else 0
)
- # Create Root mandate if needed
+ def _initRecords(self):
+ """Initializes standard records in the database if they don't exist."""
+ self._initRootMandate()
+ self._initAdminUser()
+
+ def _initRootMandate(self):
+ """Creates the Root mandate if it doesn't exist."""
existingMandateId = self.getInitialId("mandates")
mandates = self.db.getRecordset("mandates")
if existingMandateId is None or not mandates:
@@ -75,19 +98,9 @@ class GatewayInterface:
# Update mandate context
self.mandateId = createdMandate['id']
- self.userId = createdMandate['userId']
-
- # Recreate connector with correct context
- self.db = DatabaseConnector(
- dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
- dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
- dbUser=APP_CONFIG.get("DB_SYSTEM_USER"),
- dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"),
- mandateId=self.mandateId,
- userId=self.userId
- )
-
- # Create Admin user if needed
+
+ def _initAdminUser(self):
+ """Creates the Admin user if it doesn't exist."""
existingUserId = self.getInitialId("users")
users = self.db.getRecordset("users")
if existingUserId is None or not users:
@@ -99,57 +112,127 @@ class GatewayInterface:
"fullName": "Administrator",
"disabled": False,
"language": "de",
- "privilege": "sysadmin", # SysAdmin privilege
- "hashedPassword": self._getPasswordHash("admin") # Use a secure password in production!
+ "privilege": "sysadmin",
+ "hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production!
}
createdUser = self.db.recordCreate("users", adminUser)
logger.info(f"Admin user created with ID {createdUser['id']}")
# Update user context
self.userId = createdUser['id']
+
+ def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Unified user access management function that filters data based on user privileges.
+
+ Args:
+ table: Name of the table
+ recordset: Recordset to filter based on access rules
- # Recreate connector with correct context
- self.db = DatabaseConnector(
- dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
- dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
- dbUser=APP_CONFIG.get("DB_SYSTEM_USER"),
- dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"),
- mandateId=self.mandateId,
- userId=self.userId
- )
+ Returns:
+ Filtered recordset based on user privilege level
+ """
+ userPrivilege = self.currentUser.get("privilege", "user")
+
+ # Apply filtering based on privilege
+ if userPrivilege == "sysadmin":
+ return recordset # System admins see all records
+ elif userPrivilege == "admin":
+ # Admins see records in their mandate
+ return [r for r in recordset if r.get("mandateId") == self.mandateId]
+ else: # Regular users
+ # Users only see records they own within their mandate
+ return [r for r in recordset
+ if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId]
+
+ def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
+ """
+ Checks if the current user can modify (create/update/delete) records in a table.
+
+ Args:
+ table: Name of the table
+ recordId: Optional record ID for specific record check
+
+ Returns:
+ Boolean indicating permission
+ """
+ userPrivilege = self.currentUser.get("privilege", "user")
+
+ # System admins can modify anything
+ if userPrivilege == "sysadmin":
+ return True
+
+ # Check specific record permissions
+ if recordId is not None:
+ # Get the record to check ownership
+ records = self.db.getRecordset(table, recordFilter={"id": recordId})
+ if not records:
+ return False
+
+ record = records[0]
+
+ # Admins can modify anything in their mandate
+ if userPrivilege == "admin" and record.get("mandateId") == self.mandateId:
+ # Exception: Can't modify Root mandate unless you are a sysadmin
+ if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin":
+ return False
+ return True
+
+ # Users can only modify their own records
+ if (record.get("mandateId") == self.mandateId and
+ record.get("userId") == self.userId):
+ return True
+
+ return False
+ else:
+ # For general table modify permission (e.g., create)
+ # Admins can create anything in their mandate
+ if userPrivilege == "admin":
+ return True
+
+ # Regular users can create most entities
+ if table == "mandates":
+ return False # Regular users can't create mandates
+ return True
def getInitialId(self, table: str) -> Optional[int]:
- """Returns the initial ID for a table"""
+ """Returns the initial ID for a table."""
return self.db.getInitialId(table)
def _getPasswordHash(self, password: str) -> str:
- """Creates a hash for a password"""
+ """Creates a hash for a password."""
return pwdContext.hash(password)
def _verifyPassword(self, plainPassword: str, hashedPassword: str) -> bool:
- """Checks if the password matches the hash"""
+ """Checks if the password matches the hash."""
return pwdContext.verify(plainPassword, hashedPassword)
def _getCurrentTimestamp(self) -> str:
- """Returns the current timestamp in ISO format"""
+ """Returns the current timestamp in ISO format."""
from datetime import datetime
return datetime.now().isoformat()
# Mandate methods
def getAllMandates(self) -> List[Dict[str, Any]]:
- """Returns all mandates"""
- return self.db.getRecordset("mandates")
+ """Returns mandates based on user access level."""
+ allMandates = self.db.getRecordset("mandates")
+ return self._uam("mandates", allMandates)
def getMandate(self, mandateId: int) -> Optional[Dict[str, Any]]:
- """Returns a mandate by its ID"""
+ """Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
- if mandates:
- return mandates[0]
- return None
+ if not mandates:
+ return None
+
+ filteredMandates = self._uam("mandates", mandates)
+ return filteredMandates[0] if filteredMandates else None
def createMandate(self, name: str, language: str = "de") -> Dict[str, Any]:
- """Creates a new mandate"""
+ """Creates a new mandate if user has permission."""
+ if not self._canModify("mandates"):
+ raise PermissionError("No permission to create mandates")
+
mandateData = {
"name": name,
"language": language
@@ -158,43 +241,29 @@ class GatewayInterface:
return self.db.recordCreate("mandates", mandateData)
def updateMandate(self, mandateId: int, mandateData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Updates an existing mandate
-
- Args:
- mandateId: The ID of the mandate to update
- mandateData: The mandate data to update
-
- Returns:
- Dict[str, Any]: The updated mandate data
-
- Raises:
- ValueError: If the mandate is not found
- """
- # Check if the mandate exists
+ """Updates a mandate if user has access."""
+ # Check if the mandate exists and user has access
mandate = self.getMandate(mandateId)
if not mandate:
raise ValueError(f"Mandate with ID {mandateId} not found")
+
+ if not self._canModify("mandates", mandateId):
+ raise PermissionError(f"No permission to update mandate {mandateId}")
# Update the mandate
- updatedMandate = self.db.recordModify("mandates", mandateId, mandateData)
-
- return updatedMandate
+ return self.db.recordModify("mandates", mandateId, mandateData)
def deleteMandate(self, mandateId: int) -> bool:
"""
- Deletes a mandate and all associated users and data
-
- Args:
- mandateId: The ID of the mandate to delete
-
- Returns:
- bool: True if the mandate was successfully deleted, otherwise False
+ Deletes a mandate and all associated users and data if user has permission.
"""
- # Check if the mandate exists
+ # Check if the mandate exists and user has access
mandate = self.getMandate(mandateId)
if not mandate:
return False
+
+ if not self._canModify("mandates", mandateId):
+ raise PermissionError(f"No permission to delete mandate {mandateId}")
# Check if it's the initial mandate
initialMandateId = self.getInitialId("mandates")
@@ -222,33 +291,37 @@ class GatewayInterface:
# User methods
def getAllUsers(self) -> List[Dict[str, Any]]:
- """Returns all users"""
- users = self.db.getRecordset("users")
- # Remove password hashes from the response
- for user in users:
+ """Returns users based on user access level."""
+ allUsers = self.db.getRecordset("users")
+ filteredUsers = self._uam("users", allUsers)
+
+ # Remove password hashes
+ for user in filteredUsers:
if "hashedPassword" in user:
del user["hashedPassword"]
- return users
+
+ return filteredUsers
def getUsersByMandate(self, mandateId: int) -> List[Dict[str, Any]]:
- """
- Returns all users of a specific mandate
-
- Args:
- mandateId: The ID of the mandate
+ """Returns users for a specific mandate if user has access."""
+ # First check if user has access to the mandate
+ mandate = self.getMandate(mandateId)
+ if not mandate:
+ return []
- Returns:
- List[Dict[str, Any]]: List of users in the mandate
- """
+ # Get users for this mandate
users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId})
- # Remove password hashes from the response
- for user in users:
+ filteredUsers = self._uam("users", users)
+
+ # Remove password hashes
+ for user in filteredUsers:
if "hashedPassword" in user:
del user["hashedPassword"]
- return users
+
+ return filteredUsers
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
- """Returns a user by username"""
+ """Returns a user by username."""
users = self.db.getRecordset("users")
for user in users:
if user.get("username") == username:
@@ -256,48 +329,49 @@ class GatewayInterface:
return None
def getUser(self, userId: int) -> Optional[Dict[str, Any]]:
- """Returns a user by ID"""
+ """Returns a user by ID if user has access."""
users = self.db.getRecordset("users", recordFilter={"id": userId})
- if users:
- user = users[0]
- # Remove password hash from the API response
- if "hashedPassword" in user:
- userCopy = user.copy()
- del userCopy["hashedPassword"]
- return userCopy
- return user
- return None
+ if not users:
+ return None
+
+ filteredUsers = self._uam("users", users)
+ if not filteredUsers:
+ return None
+
+ user = filteredUsers[0]
+
+ # Remove password hash
+ if "hashedPassword" in user:
+ userCopy = user.copy()
+ del userCopy["hashedPassword"]
+ return userCopy
+
+ return user
def createUser(self, username: str, password: str, email: str = None,
fullName: str = None, language: str = "de", mandateId: int = None,
disabled: bool = False, privilege: str = "user") -> Dict[str, Any]:
- """
- Creates a new user
-
- Args:
- username: The username
- password: The password
- email: The email address (optional)
- fullName: The full name (optional)
- language: The preferred language (default: "de")
- mandateId: The ID of the mandate (optional)
- disabled: Whether the user is disabled (default: False)
- privilege: The privilege level (default: "user")
-
- Returns:
- Dict[str, Any]: The created user data
-
- Raises:
- ValueError: If the username already exists
- """
+ """Creates a new user if current user has permission."""
# Check if the username already exists
existingUser = self.getUserByUsername(username)
if existingUser:
raise ValueError(f"User '{username}' already exists")
-
+
# Use the provided mandateId or the current context
userMandateId = mandateId if mandateId is not None else self.mandateId
+ # Check if user has access to the mandate
+ if userMandateId != self.mandateId and self.currentUser.get("privilege") != "sysadmin":
+ raise PermissionError(f"No permission to create users in mandate {userMandateId}")
+
+ if not self._canModify("users"):
+ raise PermissionError("No permission to create users")
+
+ # Check privilege escalation
+ if (privilege == "sysadmin" or
+ (privilege == "admin" and self.currentUser.get("privilege") == "user")):
+ raise PermissionError(f"Cannot create user with higher privilege: {privilege}")
+
userData = {
"mandateId": userMandateId,
"username": username,
@@ -318,16 +392,7 @@ class GatewayInterface:
return createdUser
def authenticateUser(self, username: str, password: str) -> Optional[Dict[str, Any]]:
- """
- Authenticates a user by username and password
-
- Args:
- username: The username
- password: The password
-
- Returns:
- Optional[Dict[str, Any]]: The user data or None if authentication fails
- """
+ """Authenticates a user by username and password."""
user = self.getUserByUsername(username)
if not user:
@@ -348,25 +413,29 @@ class GatewayInterface:
return authenticatedUser
def updateUser(self, userId: int, userData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Updates a user
+ """Updates a user if current user has permission."""
+ # Check if the user exists and current user has access
+ user = self.getUser(userId)
+ if not user:
+ # Try to get the raw user record for admin access check
+ users = self.db.getRecordset("users", recordFilter={"id": userId})
+ if not users:
+ raise ValueError(f"User with ID {userId} not found")
+
+ # Check if current user is admin/sysadmin
+ if not self._canModify("users", userId):
+ raise PermissionError(f"No permission to update user {userId}")
+
+ user = users[0]
- Args:
- userId: The ID of the user to update
- userData: The user data to update
+ # Check privilege escalation
+ if "privilege" in userData:
+ currentPrivilege = self.currentUser.get("privilege")
+ targetPrivilege = userData["privilege"]
- Returns:
- Dict[str, Any]: The updated user data
-
- Raises:
- ValueError: If the user is not found
- """
- # Get the current user with password hash (directly from DB)
- users = self.db.getRecordset("users", recordFilter={"id": userId})
- if not users:
- raise ValueError(f"User with ID {userId} not found")
-
- user = users[0]
+ if (targetPrivilege == "sysadmin" and currentPrivilege != "sysadmin") or (
+ targetPrivilege == "admin" and currentPrivilege == "user"):
+ raise PermissionError(f"Cannot escalate privilege to {targetPrivilege}")
# If the password is being changed, hash it
if "password" in userData:
@@ -383,22 +452,15 @@ class GatewayInterface:
return updatedUser
def disableUser(self, userId: int) -> Dict[str, Any]:
- """Disables a user"""
+ """Disables a user if current user has permission."""
return self.updateUser(userId, {"disabled": True})
def enableUser(self, userId: int) -> Dict[str, Any]:
- """Enables a user"""
+ """Enables a user if current user has permission."""
return self.updateUser(userId, {"disabled": False})
def _deleteUserReferencedData(self, userId: int) -> None:
- """
- Deletes all data associated with a user
-
- Args:
- userId: The ID of the user
- """
- # Here all tables are searched and all entries referencing this user are deleted
-
+ """Deletes all data associated with a user."""
# Delete user attributes
try:
attributes = self.db.getRecordset("attributes", recordFilter={"userId": userId})
@@ -407,25 +469,18 @@ class GatewayInterface:
except Exception as e:
logger.error(f"Error deleting attributes for user {userId}: {e}")
- # Other tables that might reference the user
- # (Depending on the application's database structure)
-
logger.info(f"All referenced data for user {userId} has been deleted")
def deleteUser(self, userId: int) -> bool:
- """
- Deletes a user and all associated data
-
- Args:
- userId: The ID of the user to delete
-
- Returns:
- bool: True if the user was successfully deleted, otherwise False
- """
+ """Deletes a user and all associated data if current user has permission."""
# Check if the user exists
users = self.db.getRecordset("users", recordFilter={"id": userId})
if not users:
return False
+
+ # Check if current user has permission
+ if not self._canModify("users", userId):
+ raise PermissionError(f"No permission to delete user {userId}")
# Check if it's the initial user
initialUserId = self.getInitialId("users")
@@ -454,18 +509,11 @@ def getGatewayInterface(mandateId: int = None, userId: int = None) -> GatewayInt
"""
Returns a GatewayInterface instance for the specified context.
Reuses existing instances.
-
- Args:
- mandateId: ID of the mandate
- userId: ID of the user
-
- Returns:
- GatewayInterface instance
"""
contextKey = f"{mandateId}_{userId}"
if contextKey not in _gatewayInterfaces:
_gatewayInterfaces[contextKey] = GatewayInterface(mandateId, userId)
return _gatewayInterfaces[contextKey]
-# Initialize the interface
+# Initialize an instance
getGatewayInterface()
\ No newline at end of file
diff --git a/modules/lucydomInterface.py b/modules/lucydomInterface.py
index 50ae9fc1..54679543 100644
--- a/modules/lucydomInterface.py
+++ b/modules/lucydomInterface.py
@@ -51,13 +51,7 @@ class LucyDOMInterface:
"""
def __init__(self, mandateId: int, userId: int):
- """
- Initializes the LucyDOM Interface with mandate and user context.
-
- Args:
- mandateId: ID of the current mandate
- userId: ID of the current user
- """
+ """Initializes the LucyDOM Interface with mandate and user context."""
self.mandateId = mandateId
self.userId = userId
@@ -73,19 +67,31 @@ class LucyDOMInterface:
logger.error(f"Error importing lucydomModel: {e}")
raise
- # Initialize database if needed
+ # Initialize database connector
self._initializeDatabase()
+ # Load user information
+ self.currentUser = self._getCurrentUserInfo()
+
+ # Initialize standard database records if needed
+ self._initRecords()
+
+ def _getCurrentUserInfo(self) -> Dict[str, Any]:
+ """Gets information about the current user including privileges."""
+ # For production, you would get this from authentication
+ # For now return basic user info with default privilege
+ return {
+ "id": self.userId,
+ "mandateId": self.mandateId,
+ "privilege": "user", # Default privilege level
+ "language": self.userLanguage
+ }
+
def _initializeDatabase(self):
- """
- Initializes the database with minimal objects for the logged-in user in the mandate, if it doesn't exist yet.
- No initialization without a valid user.
- Creates an initial dataset for each table defined in the data model.
- """
+ """Initializes the database connection."""
effectiveMandateId = self.mandateId
effectiveUserId = self.userId
if effectiveMandateId is None or effectiveUserId is None:
- #data available
return
self.db = DatabaseConnector(
@@ -98,7 +104,12 @@ class LucyDOMInterface:
skipInitialIdLookup=True
)
- # Initialize standard prompts for different areas
+ def _initRecords(self):
+ """Initializes standard records in the database if they don't exist."""
+ self._initializeStandardPrompts()
+
+ def _initializeStandardPrompts(self):
+ """Creates standard prompts if they don't exist."""
prompts = self.db.getRecordset("prompts")
if not prompts:
logger.info("Creating standard prompts")
@@ -106,32 +117,32 @@ class LucyDOMInterface:
# Define standard prompts
standardPrompts = [
{
- "mandateId": effectiveMandateId,
- "userId": effectiveUserId,
+ "mandateId": self.mandateId,
+ "userId": self.userId,
"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"
},
{
- "mandateId": effectiveMandateId,
- "userId": effectiveUserId,
+ "mandateId": self.mandateId,
+ "userId": self.userId,
"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"
},
{
- "mandateId": effectiveMandateId,
- "userId": effectiveUserId,
+ "mandateId": self.mandateId,
+ "userId": self.userId,
"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"
},
{
- "mandateId": effectiveMandateId,
- "userId": effectiveUserId,
+ "mandateId": self.mandateId,
+ "userId": self.userId,
"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"
},
{
- "mandateId": effectiveMandateId,
- "userId": effectiveUserId,
+ "mandateId": self.mandateId,
+ "userId": self.userId,
"content": "Gib mir die ersten 1000 Primzahlen",
"name": "Code: Primzahlen"
}
@@ -142,25 +153,92 @@ class LucyDOMInterface:
createdPrompt = self.db.recordCreate("prompts", promptData)
logger.info(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']}")
+ def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Unified user access management function that filters data based on user privileges.
+
+ Args:
+ table: Name of the table
+ recordset: Recordset to filter based on access rules
+
+ Returns:
+ Filtered recordset based on user privilege level
+ """
+ userPrivilege = self.currentUser.get("privilege", "user")
+
+ # Apply filtering based on privilege
+ if userPrivilege == "sysadmin":
+ return recordset # System admins see all records
+ elif userPrivilege == "admin":
+ # Admins see records in their mandate
+ return [r for r in recordset if r.get("mandateId") == self.mandateId]
+ else: # Regular users
+ # To see all prompts from mandate 0 and own
+ if table == "prompts":
+ return [r for r in recordset if
+ (r.get("mandateId") == self.mandateId and r.get("userId") == self.userId)
+ or
+ (r.get("mandateId") == 0)
+ ]
+ # Users see only their records
+ return [r for r in recordset
+ if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId]
+
+ def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
+ """
+ Checks if the current user can modify (create/update/delete) records in a table.
+
+ Args:
+ table: Name of the table
+ recordId: Optional record ID for specific record check
+
+ Returns:
+ Boolean indicating permission
+ """
+ userPrivilege = self.currentUser.get("privilege", "user")
+
+ # System admins can modify anything
+ if userPrivilege == "sysadmin":
+ return True
+
+ # For regular users and admins, check specific cases
+ if recordId is not None:
+ # Get the record to check ownership
+ records = self.db.getRecordset(table, recordFilter={"id": recordId})
+ if not records:
+ return False
+
+ record = records[0]
+
+ # Admins can modify anything in their mandate
+ if userPrivilege == "admin" and record.get("mandateId") == self.mandateId:
+ return True
+
+ # Regular users can only modify their own records
+ if (record.get("mandateId") == self.mandateId and
+ record.get("userId") == self.userId):
+ return True
+
+ return False
+ else:
+ # For general modification permission (e.g., create)
+ # Admins can create anything in their mandate
+ if userPrivilege == "admin":
+ return True
+
+ # Regular users can create in most tables
+ return True
+
# Language support methods
def setUserLanguage(self, languageCode: str):
"""Set the user's preferred language"""
self.userLanguage = languageCode
+ self.currentUser["language"] = languageCode
logger.info(f"User language set to: {languageCode}")
async def callAi(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str:
- """
- Enhanced AI service call with language support
-
- Args:
- messages: List of message dictionaries
- produceUserAnswer: Whether this response is for the end-user
- temperature: Optional temperature setting
-
- Returns:
- AI response text
- """
+ """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"
@@ -187,37 +265,34 @@ class LucyDOMInterface:
# Utilities
def getInitialId(self, table: str) -> Optional[int]:
- """
- Returns the initial ID for a table.
-
- Args:
- table: Name of the table
-
- Returns:
- The initial ID or None if not present
- """
+ """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 all prompts for the current mandate"""
- return self.db.getRecordset("prompts")
+ """Returns prompts based on user access level."""
+ allPrompts = self.db.getRecordset("prompts")
+ return self._uam("prompts", allPrompts)
def getPrompt(self, promptId: int) -> Optional[Dict[str, Any]]:
- """Returns a prompt by its ID"""
+ """Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
- if prompts:
- return prompts[0]
- return None
+ 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"""
+ """Creates a new prompt if user has permission."""
+ if not self._canModify("prompts"):
+ raise PermissionError("No permission to create prompts")
+
promptData = {
"mandateId": self.mandateId,
"userId": self.userId,
@@ -229,20 +304,14 @@ class LucyDOMInterface:
return self.db.recordCreate("prompts", promptData)
def updatePrompt(self, promptId: int, content: str = None, name: str = None) -> Dict[str, Any]:
- """
- Updates an existing prompt
-
- Args:
- promptId: ID of the prompt to update
- content: New content for the prompt
-
- Returns:
- The updated prompt object
- """
- # Check if the prompt exists
+ """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 = {}
@@ -256,17 +325,16 @@ class LucyDOMInterface:
return self.db.recordModify("prompts", promptId, promptData)
def deletePrompt(self, promptId: int) -> bool:
- """
- Deletes a prompt from the database
-
- Args:
- promptId: ID of the prompt to delete
+ """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}")
- Returns:
- True if the prompt was successfully deleted, otherwise False
- """
return self.db.recordDelete("prompts", promptId)
-
# File Utilities
@@ -275,14 +343,15 @@ class LucyDOMInterface:
return hashlib.sha256(fileContent).hexdigest()
def checkForDuplicateFile(self, fileHash: str) -> Optional[Dict[str, Any]]:
- """Checks if a file with the same hash already exists"""
+ """Checks if a file with the same hash already exists."""
files = self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
- if files:
- return files[0]
+ filteredFiles = self._uam("files", files)
+ if filteredFiles:
+ return filteredFiles[0]
return None
def getMimeType(self, filename: str) -> str:
- """Determines the MIME type based on the file extension"""
+ """Determines the MIME type based on the file extension."""
import os
ext = os.path.splitext(filename)[1].lower()[1:]
extensionToMime = {
@@ -311,48 +380,27 @@ class LucyDOMInterface:
}
return extensionToMime.get(ext.lower(), "application/octet-stream")
-
# File methods - metadata-based operations
def getAllFiles(self) -> List[Dict[str, Any]]:
- """
- Returns all files for the current mandate without binary data.
-
- Returns:
- List of FileItem objects without binary data
- """
- files = self.db.getRecordset("files")
- return files
+ """Returns files based on user access level."""
+ allFiles = self.db.getRecordset("files")
+ return self._uam("files", allFiles)
def getFile(self, fileId: int) -> Optional[Dict[str, Any]]:
- """
- Returns a file by its ID, without binary data.
-
- Args:
- fileId: ID of the file
-
- Returns:
- FileItem without binary data or None if not found
- """
+ """Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId})
- if files:
- return files[0]
- return None
+ 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 in the database without content.
- The actual file content is stored separately in the FileData table.
-
- Args:
- name: Name of the file
- mimeType: MIME type of the file
- size: Size of the file in bytes
- fileHash: Hash value of the file for deduplication
+ """Creates a new file entry if user has permission."""
+ if not self._canModify("files"):
+ raise PermissionError("No permission to create files")
- Returns:
- The created FileItem object
- """
fileData = {
"mandateId": self.mandateId,
"userId": self.userId,
@@ -365,44 +413,29 @@ class LucyDOMInterface:
return self.db.recordCreate("files", fileData)
def updateFile(self, fileId: int, updateData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Updates the metadata of an existing file without affecting the binary data.
-
- Args:
- fileId: ID of the file to update
- updateData: Dictionary with fields to update
-
- Returns:
- The updated FileItem object
- """
- # Check if the file exists
+ """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: int) -> bool:
- """
- Deletes a file from the database (metadata and content).
-
- Args:
- fileId: ID of the file
-
- Returns:
- True on success, False on error
- """
+ """Deletes a file if user has access."""
try:
- # Find the file in the database
+ # 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")
- # Check if the file belongs to the current mandate
- if file.get("mandateId") != self.mandateId:
- raise FilePermissionError(f"No permission to delete file {fileId}")
+ 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")
@@ -410,11 +443,8 @@ class LucyDOMInterface:
otherReferences = [f for f in self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
if f.get("id") != fileId]
- # If other files reference this content, only delete the database entry for FileItem
- if otherReferences:
- logger.info(f"Other references to the file content found, only FileItem will be deleted: {fileId}")
- else:
- # Also delete the file content in the FileData table
+ # Only delete associated fileData if no other references exist
+ if not otherReferences:
try:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries:
@@ -427,45 +457,27 @@ class LucyDOMInterface:
return self.db.recordDelete("files", fileId)
except FileNotFoundError as e:
- # Pass through FileNotFoundError
raise
except FilePermissionError as e:
- # Pass through FilePermissionError
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
-
- """
- This contains the modified file handling methods for the LucyDOMInterface class
- to implement consistent handling of base64 encoding flags.
- """
-
+
def createFileData(self, fileId: int, data: bytes) -> bool:
- """
- Stores the binary data of a file in the database, using base64 encoding for binary files.
- Always sets the base64Encoded flag appropriately.
-
- Args:
- fileId: ID of the associated file
- data: Binary data
-
- Returns:
- True on success, False on error
- """
+ """Stores the binary data of a file in the database."""
try:
import base64
- # Check the file metadata to determine if this should be stored as text or 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 that should be stored as text
+ # Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
@@ -475,7 +487,6 @@ class LucyDOMInterface:
if isTextFormat:
# Try to decode as text
try:
- # Convert bytes to text
textContent = data.decode('utf-8')
fileData = textContent
base64Encoded = False
@@ -508,16 +519,13 @@ class LucyDOMInterface:
return False
def getFileData(self, fileId: int) -> Optional[bytes]:
- """
- Returns the binary data of a file.
- Uses the base64Encoded flag to determine if decoding is necessary.
-
- Args:
- fileId: ID of the file
+ """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
- Returns:
- Binary data or None if not found
- """
import base64
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
@@ -545,43 +553,28 @@ class LucyDOMInterface:
return None
def updateFileData(self, fileId: int, data: Union[bytes, str]) -> bool:
- """
- Updates the binary data of a file in the database.
- Handles base64 encoding based on the file type.
-
- Args:
- fileId: ID of the file
- data: New binary data or text content
+ """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
- Returns:
- True on success, False on error
- """
try:
import base64
- # Check file metadata to determine if this should be stored as text or base64
- file = self.getFile(fileId)
- if not file:
- logger.error(f"File with ID {fileId} not found when updating data")
- return False
-
- # Determine if this is a text-based format that should be stored as text
+ # Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
- isTextFormat = (
- mimeType.startswith("text/") or
- mimeType in [
- "application/json",
- "application/xml",
- "application/javascript",
- "application/x-python",
- "image/svg+xml"
- ]
- )
+ isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
- # Convert input data to the right format based on its type and the file's format
+ # Convert input data to the right format
if isinstance(data, bytes):
if isTextFormat:
try:
@@ -646,23 +639,14 @@ class LucyDOMInterface:
return False
def saveUploadedFile(self, fileContent: bytes, fileName: str) -> Dict[str, Any]:
- """
- Saves an uploaded file in the database.
- Metadata is stored in the 'files' table,
- Binary data in the 'fileData' table with the appropriate base64Encoded flag.
-
- Args:
- fileContent: Binary data of the file
- fileName: Name of the file
-
- Returns:
- Dictionary with metadata of the saved file
- """
+ """Saves an uploaded file if user has permission."""
try:
- # Debug: Log the start of the file upload process
+ # Check file creation permission
+ if not self._canModify("files"):
+ raise PermissionError("No permission to upload files")
+
logger.info(f"Starting upload process for file: {fileName}")
- # Debug: Check if fileContent is valid bytes
if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}")
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
@@ -674,17 +658,14 @@ class LucyDOMInterface:
# Check for duplicate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
- # Simply return the existing file metadata
logger.info(f"Duplicate found for {fileName}: {existingFile['id']}")
return existingFile
- # Determine MIME type
+ # Determine MIME type and size
mimeType = self.getMimeType(fileName)
-
- # Determine file size
fileSize = len(fileContent)
- # 1. Save metadata in the 'files' table
+ # Save metadata
logger.info(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile(
name=fileName,
@@ -693,18 +674,12 @@ class LucyDOMInterface:
fileHash=fileHash
)
- # 2. Save binary data with appropriate base64 encoding based on file type
+ # Save binary data
logger.info(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
# Debug: Export file to static folder
- self._exportFileToStatic(fileContent, dbFile["id"], fileName) # DEBUG TODO
-
- # Debug: Verify database record was created
- if not dbFile:
- logger.warning(f"Database record for file {fileName} was not created properly")
- else:
- logger.debug(f"Database record created for file {fileName}")
+ self._exportFileToStatic(fileContent, dbFile["id"], fileName)
logger.info(f"File upload process completed for: {fileName}")
return dbFile
@@ -714,24 +689,15 @@ class LucyDOMInterface:
raise FileStorageError(f"Error saving file: {str(e)}")
def downloadFile(self, fileId: int) -> Optional[Dict[str, Any]]:
- """
- Returns a file for download, including binary data.
- Uses the base64Encoded flag to determine how to process the file data.
-
- Args:
- fileId: ID of the file
-
- Returns:
- Dictionary with file data and metadata or None if not found
- """
+ """Returns a file for download if user has access."""
try:
- # 1. Get metadata from the 'files' table
+ # Check file access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
- # 2. Get binary data from the 'fileData' table using the new flag-aware method
+ # Get binary data
fileContent = self.getFileData(fileId)
if fileContent is None:
@@ -745,13 +711,13 @@ class LucyDOMInterface:
"content": fileContent
}
except FileNotFoundError as e:
- # Re-raise FileNotFoundError as is
raise
except Exception as e:
logger.error(f"Error downloading file {fileId}: {str(e)}")
raise FileError(f"Error downloading file: {str(e)}")
def _exportFileToStatic(self, fileContent: bytes, fileId: int, fileName: str):
+ """Debug helper to export files to static folder."""
debugFilename = f"{fileId}_{fileName}"
with open(f"./static/{debugFilename}", 'wb') as f:
f.write(fileContent)
@@ -759,22 +725,32 @@ class LucyDOMInterface:
# Workflow methods
def getAllWorkflows(self) -> List[Dict[str, Any]]:
- """Returns all workflows for the current mandate"""
- return self.db.getRecordset("workflows")
+ """Returns workflows based on user access level."""
+ allWorkflows = self.db.getRecordset("workflows")
+ return self._uam("workflows", allWorkflows)
def getWorkflowsByUser(self, userId: int) -> List[Dict[str, Any]]:
- """Returns all workflows for a user"""
- return self.db.getRecordset("workflows", recordFilter={"userId": userId})
+ """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 its ID"""
+ """Returns a workflow by ID if user has access."""
workflows = self.db.getRecordset("workflows", recordFilter={"id": workflowId})
- if workflows:
- return workflows[0]
- return None
+ 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 in the database"""
+ """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
@@ -793,20 +769,14 @@ class LucyDOMInterface:
return self.db.recordCreate("workflows", workflowData)
def updateWorkflow(self, workflowId: str, workflowData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Updates an existing workflow.
-
- Args:
- workflowId: ID of the workflow to update
- workflowData: New data for the workflow
-
- Returns:
- The updated workflow object
- """
- # Check if the workflow exists
+ """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()
@@ -815,53 +785,50 @@ class LucyDOMInterface:
return self.db.recordModify("workflows", workflowId, workflowData)
def deleteWorkflow(self, workflowId: str) -> bool:
- """
- Deletes a workflow from the database.
-
- Args:
- workflowId: ID of the workflow to delete
-
- Returns:
- True on success, False if the workflow doesn't exist
- """
- # Check if the workflow exists
+ """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
-
- # Check if the user is the owner or has admin rights
- if workflow.get("userId") != self.userId:
- # Here could be a check for admin rights
- 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 all messages of a workflow"""
- return self.db.getRecordset("workflowMessages", recordFilter={"workflowId": workflowId})
+ """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 new message for a workflow.
-
- Args:
- messageData: The message data
-
- Returns:
- The created message or None on error
- """
+ """Creates a message for a workflow if user has access."""
try:
- # Check if required fields are present
+ # 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()}"
@@ -882,7 +849,7 @@ class LucyDOMInterface:
# Set sequence number if not present
if "sequenceNo" not in messageData:
# Get current messages to determine next sequence number
- existingMessages = self.getWorkflowMessages(messageData["workflowId"])
+ existingMessages = self.getWorkflowMessages(workflowId)
messageData["sequenceNo"] = len(existingMessages) + 1
# Ensure role and agentName are present
@@ -892,45 +859,27 @@ class LucyDOMInterface:
if "agentName" not in messageData:
messageData["agentName"] = ""
- # Debug log for data to create
- logger.debug(f"Creating workflow message with data: {messageData}")
-
# Create message in database
createdMessage = self.db.recordCreate("workflowMessages", messageData)
# Update workflow's messageIds if this is a new message
if createdMessage:
- workflowId = messageData["workflowId"]
- workflow = self.getWorkflow(workflowId)
+ # Get current messageIds or initialize empty list
+ messageIds = workflow.get("messageIds", [])
- if workflow:
- # 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})
+ # 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 instead of raising to avoid cascading failures
return None
def updateWorkflowMessage(self, messageId: str, messageData: Dict[str, Any]) -> Dict[str, Any]:
- """
- Updates an existing workflow message in the database.
-
- Args:
- messageId: ID of the message
- messageData: Data to update
-
- Returns:
- The updated message object or None on error
- """
+ """Updates a workflow message if user has access to the workflow."""
try:
- # Debug info
logger.debug(f"Updating message {messageId} in database")
# Ensure messageId is provided
@@ -945,7 +894,17 @@ class LucyDOMInterface:
# If message doesn't exist but we have workflowId, create it
if "workflowId" in messageData:
- logger.info(f"Creating new message with ID {messageId} for workflow {messageData.get('workflowId')}")
+ 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}")
@@ -954,6 +913,15 @@ class LucyDOMInterface:
# 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:
@@ -978,21 +946,20 @@ class LucyDOMInterface:
return updatedMessage
except Exception as e:
logger.error(f"Error updating message {messageId}: {str(e)}", exc_info=True)
- # Re-raise with full information
raise ValueError(f"Error updating message {messageId}: {str(e)}")
def deleteWorkflowMessage(self, workflowId: str, messageId: str) -> bool:
- """
- Deletes a message from a workflow in the database.
-
- Args:
- workflowId: ID of the associated workflow
- messageId: ID of the message to delete
-
- Returns:
- True on success, False on error
- """
+ """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)
@@ -1008,21 +975,17 @@ class LucyDOMInterface:
return False
def deleteFileFromMessage(self, workflowId: str, messageId: str, fileId: int) -> bool:
- """
- Removes a file reference from a message.
- The file itself is not deleted, only the reference in the message.
- Enhanced version with improved file matching.
-
- Args:
- workflowId: ID of the associated workflow
- messageId: ID of the message
- fileId: ID of the file to remove
-
- Returns:
- True on success, False on error
- """
+ """Removes a file reference from a message if user has access."""
try:
- # Log operation
+ # 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.info(f"Removing file {fileId} from message {messageId} in workflow {workflowId}")
# Get all workflow messages
@@ -1060,10 +1023,6 @@ class LucyDOMInterface:
# Log existing documents
documents = message.get("documents", [])
logger.debug(f"Message has {len(documents)} documents")
- for i, doc in enumerate(documents):
- docId = doc.get("id", "unknown")
- fileIdValue = doc.get("fileId", "unknown")
- logger.debug(f"Document {i}: docId={docId}, fileId={fileIdValue}")
# Create a new list of documents without the one to delete
updatedDocuments = []
@@ -1110,15 +1069,35 @@ class LucyDOMInterface:
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 all log entries for a workflow"""
- return self.db.getRecordset("workflowLogs", recordFilter={"workflowId": workflowId})
+ """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 new log entry for a workflow"""
+ """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()
@@ -1141,30 +1120,27 @@ class LucyDOMInterface:
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 the state of a workflow to the database.
- Workflow data is updated, but messages are stored separately.
-
- Args:
- workflow: The workflow object
- saveMessages: Flag to determine if messages should be saved
- saveLogs: Flag to determine if logs should be saved
-
- Returns:
- True on success, False on failure
- """
+ """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
- # IMPORTANT: Don't store messages in the workflow table!
workflowDbData = {
"id": workflowId,
"mandateId": workflow.get("mandateId", self.mandateId),
@@ -1177,7 +1153,6 @@ class LucyDOMInterface:
}
# Check if workflow already exists
- existingWorkflow = self.getWorkflow(workflowId)
if existingWorkflow:
self.updateWorkflow(workflowId, workflowDbData)
else:
@@ -1190,9 +1165,7 @@ class LucyDOMInterface:
if not messageId:
continue
- # Since each message is already saved with createWorkflowMessage,
- # we only need to check if updates are necessary
- # First, get existing message from database
+ # Get existing message from database
existingMessages = self.getWorkflowMessages(workflowId)
existingMessage = next((m for m in existingMessages if m.get("id") == messageId), None)
@@ -1216,8 +1189,6 @@ class LucyDOMInterface:
self.updateWorkflowMessage(messageId, messageData)
else:
# Message doesn't exist in database yet
- # It should have been saved via createWorkflowMessage
- # If not, log a warning
logger.warning(f"Message {messageId} in workflow {workflowId} not found in database")
# Save logs
@@ -1254,23 +1225,13 @@ class LucyDOMInterface:
return False
def loadWorkflowState(self, workflowId: str) -> Optional[Dict[str, Any]]:
- """
- Loads the complete state of a workflow from the database.
- This includes the workflow itself, messages, and logs.
-
- Args:
- workflowId: ID of the workflow to load
-
- Returns:
- The complete workflow object or None on error
- """
+ """Loads workflow state if user has access."""
try:
- # Load base workflow
+ # Check workflow access
workflow = self.getWorkflow(workflowId)
if not workflow:
return None
- # Log the workflow base retrieval
logger.debug(f"Loaded base workflow {workflowId} from database")
# Load messages
@@ -1278,7 +1239,6 @@ class LucyDOMInterface:
# Sort by sequence number
messages.sort(key=lambda x: x.get("sequenceNo", 0))
- # Debug log for messages and document counts
messageCount = len(messages)
logger.debug(f"Loaded {messageCount} messages for workflow {workflowId}")
@@ -1305,7 +1265,7 @@ class LucyDOMInterface:
# Assemble complete workflow object
completeWorkflow = workflow.copy()
completeWorkflow["messages"] = messages
- completeWorkflow["messageIds"] = messageIds # Ensure messageIds is included
+ completeWorkflow["messageIds"] = messageIds
completeWorkflow["logs"] = logs
return completeWorkflow
@@ -1321,13 +1281,6 @@ def getLucydomInterface(mandateId: int = 0, userId: int = 0) -> LucyDOMInterface
"""
Returns a LucyDOMInterface instance for the specified context.
Reuses existing instances.
-
- Args:
- mandateId: ID of the mandate
- userId: ID of the user
-
- Returns:
- LucyDOMInterface instance
"""
contextKey = f"{mandateId}_{userId}"
if contextKey not in _lucydomInterfaces:
@@ -1335,9 +1288,9 @@ def getLucydomInterface(mandateId: int = 0, userId: int = 0) -> LucyDOMInterface
interface = LucyDOMInterface(mandateId, userId)
# Initialize AI service
aiService = ChatService()
- interface.aiService = aiService # Directly set the attribute
+ interface.aiService = aiService
_lucydomInterfaces[contextKey] = interface
return _lucydomInterfaces[contextKey]
-# Init
-getLucydomInterface()
\ No newline at end of file
+# Initialize an instance
+getLucydomInterface()
\ No newline at end of file
diff --git a/result.txt b/result.txt
deleted file mode 100644
index 4a03458a..00000000
--- a/result.txt
+++ /dev/null
@@ -1 +0,0 @@
-{'total_pixels': None, 'total_characters': None}
\ No newline at end of file
diff --git a/static/1_generated_code.py b/static/1_generated_code.py
new file mode 100644
index 00000000..2caf7d4f
--- /dev/null
+++ b/static/1_generated_code.py
@@ -0,0 +1,38 @@
+inputFiles = [] # DO NOT CHANGE THIS LINE
+
+def is_prime(n):
+ if n <= 1:
+ return False
+ if n <= 3:
+ return True
+ if n % 2 == 0 or n % 3 == 0:
+ return False
+ i = 5
+ while i * i <= n:
+ if n % i == 0 or n % (i + 2) == 0:
+ return False
+ i += 6
+ return True
+
+def generate_primes(count):
+ primes = []
+ num = 2
+ while len(primes) < count:
+ if is_prime(num):
+ primes.append(num)
+ num += 1
+ return primes
+
+primes = generate_primes(1000)
+prime_numbers_content = "\n".join(map(str, primes))
+
+result = {
+ "prime_numbers.txt": {
+ "content": prime_numbers_content,
+ "base64Encoded": False,
+ "contentType": "text/plain"
+ }
+}
+
+import json
+print(json.dumps(result))
\ No newline at end of file
diff --git a/static/2_execution_history.json b/static/2_execution_history.json
new file mode 100644
index 00000000..8a89a8b8
--- /dev/null
+++ b/static/2_execution_history.json
@@ -0,0 +1,19 @@
+[
+ {
+ "attempt": 1,
+ "code": "inputFiles = [] # DO NOT CHANGE THIS LINE\n\ndef is_prime(n):\n if n <= 1:\n return False\n if n <= 3:\n return True\n if n % 2 == 0 or n % 3 == 0:\n return False\n i = 5\n while i * i <= n:\n if n % i == 0 or n % (i + 2) == 0:\n return False\n i += 6\n return True\n\ndef generate_primes(count):\n primes = []\n num = 2\n while len(primes) < count:\n if is_prime(num):\n primes.append(num)\n num += 1\n return primes\n\nprimes = generate_primes(1000)\nprime_numbers_content = \"\\n\".join(map(str, primes))\n\nresult = {\n \"prime_numbers.txt\": {\n \"content\": prime_numbers_content,\n \"base64Encoded\": False,\n \"contentType\": \"text/plain\"\n }\n}\n\nimport json\nprint(json.dumps(result))",
+ "result": {
+ "success": true,
+ "output": "{\"prime_numbers.txt\": {\"content\": \"2\\n3\\n5\\n7\\n11\\n13\\n17\\n19\\n23\\n29\\n31\\n37\\n41\\n43\\n47\\n53\\n59\\n61\\n67\\n71\\n73\\n79\\n83\\n89\\n97\\n101\\n103\\n107\\n109\\n113\\n127\\n131\\n137\\n139\\n149\\n151\\n157\\n163\\n167\\n173\\n179\\n181\\n191\\n193\\n197\\n199\\n211\\n223\\n227\\n229\\n233\\n239\\n241\\n251\\n257\\n263\\n269\\n271\\n277\\n281\\n283\\n293\\n307\\n311\\n313\\n317\\n331\\n337\\n347\\n349\\n353\\n359\\n367\\n373\\n379\\n383\\n389\\n397\\n401\\n409\\n419\\n421\\n431\\n433\\n439\\n443\\n449\\n457\\n461\\n463\\n467\\n479\\n487\\n491\\n499\\n503\\n509\\n521\\n523\\n541\\n547\\n557\\n563\\n569\\n571\\n577\\n587\\n593\\n599\\n601\\n607\\n613\\n617\\n619\\n631\\n641\\n643\\n647\\n653\\n659\\n661\\n673\\n677\\n683\\n691\\n701\\n709\\n719\\n727\\n733\\n739\\n743\\n751\\n757\\n761\\n769\\n773\\n787\\n797\\n809\\n811\\n821\\n823\\n827\\n829\\n839\\n853\\n857\\n859\\n863\\n877\\n881\\n883\\n887\\n907\\n911\\n919\\n929\\n937\\n941\\n947\\n953\\n967\\n971\\n977\\n983\\n991\\n997\\n1009\\n1013\\n1019\\n1021\\n1031\\n1033\\n1039\\n1049\\n1051\\n1061\\n1063\\n1069\\n1087\\n1091\\n1093\\n1097\\n1103\\n1109\\n1117\\n1123\\n1129\\n1151\\n1153\\n1163\\n1171\\n1181\\n1187\\n1193\\n1201\\n1213\\n1217\\n1223\\n1229\\n1231\\n1237\\n1249\\n1259\\n1277\\n1279\\n1283\\n1289\\n1291\\n1297\\n1301\\n1303\\n1307\\n1319\\n1321\\n1327\\n1361\\n1367\\n1373\\n1381\\n1399\\n1409\\n1423\\n1427\\n1429\\n1433\\n1439\\n1447\\n1451\\n1453\\n1459\\n1471\\n1481\\n1483\\n1487\\n1489\\n1493\\n1499\\n1511\\n1523\\n1531\\n1543\\n1549\\n1553\\n1559\\n1567\\n1571\\n1579\\n1583\\n1597\\n1601\\n1607\\n1609\\n1613\\n1619\\n1621\\n1627\\n1637\\n1657\\n1663\\n1667\\n1669\\n1693\\n1697\\n1699\\n1709\\n1721\\n1723\\n1733\\n1741\\n1747\\n1753\\n1759\\n1777\\n1783\\n1787\\n1789\\n1801\\n1811\\n1823\\n1831\\n1847\\n1861\\n1867\\n1871\\n1873\\n1877\\n1879\\n1889\\n1901\\n1907\\n1913\\n1931\\n1933\\n1949\\n1951\\n1973\\n1979\\n1987\\n1993\\n1997\\n1999\\n2003\\n2011\\n2017\\n2027\\n2029\\n2039\\n2053\\n2063\\n2069\\n2081\\n2083\\n2087\\n2089\\n2099\\n2111\\n2113\\n2129\\n2131\\n2137\\n2141\\n2143\\n2153\\n2161\\n2179\\n2203\\n2207\\n2213\\n2221\\n2237\\n2239\\n2243\\n2251\\n2267\\n2269\\n2273\\n2281\\n2287\\n2293\\n2297\\n2309\\n2311\\n2333\\n2339\\n2341\\n2347\\n2351\\n2357\\n2371\\n2377\\n2381\\n2383\\n2389\\n2393\\n2399\\n2411\\n2417\\n2423\\n2437\\n2441\\n2447\\n2459\\n2467\\n2473\\n2477\\n2503\\n2521\\n2531\\n2539\\n2543\\n2549\\n2551\\n2557\\n2579\\n2591\\n2593\\n2609\\n2617\\n2621\\n2633\\n2647\\n2657\\n2659\\n2663\\n2671\\n2677\\n2683\\n2687\\n2689\\n2693\\n2699\\n2707\\n2711\\n2713\\n2719\\n2729\\n2731\\n2741\\n2749\\n2753\\n2767\\n2777\\n2789\\n2791\\n2797\\n2801\\n2803\\n2819\\n2833\\n2837\\n2843\\n2851\\n2857\\n2861\\n2879\\n2887\\n2897\\n2903\\n2909\\n2917\\n2927\\n2939\\n2953\\n2957\\n2963\\n2969\\n2971\\n2999\\n3001\\n3011\\n3019\\n3023\\n3037\\n3041\\n3049\\n3061\\n3067\\n3079\\n3083\\n3089\\n3109\\n3119\\n3121\\n3137\\n3163\\n3167\\n3169\\n3181\\n3187\\n3191\\n3203\\n3209\\n3217\\n3221\\n3229\\n3251\\n3253\\n3257\\n3259\\n3271\\n3299\\n3301\\n3307\\n3313\\n3319\\n3323\\n3329\\n3331\\n3343\\n3347\\n3359\\n3361\\n3371\\n3373\\n3389\\n3391\\n3407\\n3413\\n3433\\n3449\\n3457\\n3461\\n3463\\n3467\\n3469\\n3491\\n3499\\n3511\\n3517\\n3527\\n3529\\n3533\\n3539\\n3541\\n3547\\n3557\\n3559\\n3571\\n3581\\n3583\\n3593\\n3607\\n3613\\n3617\\n3623\\n3631\\n3637\\n3643\\n3659\\n3671\\n3673\\n3677\\n3691\\n3697\\n3701\\n3709\\n3719\\n3727\\n3733\\n3739\\n3761\\n3767\\n3769\\n3779\\n3793\\n3797\\n3803\\n3821\\n3823\\n3833\\n3847\\n3851\\n3853\\n3863\\n3877\\n3881\\n3889\\n3907\\n3911\\n3917\\n3919\\n3923\\n3929\\n3931\\n3943\\n3947\\n3967\\n3989\\n4001\\n4003\\n4007\\n4013\\n4019\\n4021\\n4027\\n4049\\n4051\\n4057\\n4073\\n4079\\n4091\\n4093\\n4099\\n4111\\n4127\\n4129\\n4133\\n4139\\n4153\\n4157\\n4159\\n4177\\n4201\\n4211\\n4217\\n4219\\n4229\\n4231\\n4241\\n4243\\n4253\\n4259\\n4261\\n4271\\n4273\\n4283\\n4289\\n4297\\n4327\\n4337\\n4339\\n4349\\n4357\\n4363\\n4373\\n4391\\n4397\\n4409\\n4421\\n4423\\n4441\\n4447\\n4451\\n4457\\n4463\\n4481\\n4483\\n4493\\n4507\\n4513\\n4517\\n4519\\n4523\\n4547\\n4549\\n4561\\n4567\\n4583\\n4591\\n4597\\n4603\\n4621\\n4637\\n4639\\n4643\\n4649\\n4651\\n4657\\n4663\\n4673\\n4679\\n4691\\n4703\\n4721\\n4723\\n4729\\n4733\\n4751\\n4759\\n4783\\n4787\\n4789\\n4793\\n4799\\n4801\\n4813\\n4817\\n4831\\n4861\\n4871\\n4877\\n4889\\n4903\\n4909\\n4919\\n4931\\n4933\\n4937\\n4943\\n4951\\n4957\\n4967\\n4969\\n4973\\n4987\\n4993\\n4999\\n5003\\n5009\\n5011\\n5021\\n5023\\n5039\\n5051\\n5059\\n5077\\n5081\\n5087\\n5099\\n5101\\n5107\\n5113\\n5119\\n5147\\n5153\\n5167\\n5171\\n5179\\n5189\\n5197\\n5209\\n5227\\n5231\\n5233\\n5237\\n5261\\n5273\\n5279\\n5281\\n5297\\n5303\\n5309\\n5323\\n5333\\n5347\\n5351\\n5381\\n5387\\n5393\\n5399\\n5407\\n5413\\n5417\\n5419\\n5431\\n5437\\n5441\\n5443\\n5449\\n5471\\n5477\\n5479\\n5483\\n5501\\n5503\\n5507\\n5519\\n5521\\n5527\\n5531\\n5557\\n5563\\n5569\\n5573\\n5581\\n5591\\n5623\\n5639\\n5641\\n5647\\n5651\\n5653\\n5657\\n5659\\n5669\\n5683\\n5689\\n5693\\n5701\\n5711\\n5717\\n5737\\n5741\\n5743\\n5749\\n5779\\n5783\\n5791\\n5801\\n5807\\n5813\\n5821\\n5827\\n5839\\n5843\\n5849\\n5851\\n5857\\n5861\\n5867\\n5869\\n5879\\n5881\\n5897\\n5903\\n5923\\n5927\\n5939\\n5953\\n5981\\n5987\\n6007\\n6011\\n6029\\n6037\\n6043\\n6047\\n6053\\n6067\\n6073\\n6079\\n6089\\n6091\\n6101\\n6113\\n6121\\n6131\\n6133\\n6143\\n6151\\n6163\\n6173\\n6197\\n6199\\n6203\\n6211\\n6217\\n6221\\n6229\\n6247\\n6257\\n6263\\n6269\\n6271\\n6277\\n6287\\n6299\\n6301\\n6311\\n6317\\n6323\\n6329\\n6337\\n6343\\n6353\\n6359\\n6361\\n6367\\n6373\\n6379\\n6389\\n6397\\n6421\\n6427\\n6449\\n6451\\n6469\\n6473\\n6481\\n6491\\n6521\\n6529\\n6547\\n6551\\n6553\\n6563\\n6569\\n6571\\n6577\\n6581\\n6599\\n6607\\n6619\\n6637\\n6653\\n6659\\n6661\\n6673\\n6679\\n6689\\n6691\\n6701\\n6703\\n6709\\n6719\\n6733\\n6737\\n6761\\n6763\\n6779\\n6781\\n6791\\n6793\\n6803\\n6823\\n6827\\n6829\\n6833\\n6841\\n6857\\n6863\\n6869\\n6871\\n6883\\n6899\\n6907\\n6911\\n6917\\n6947\\n6949\\n6959\\n6961\\n6967\\n6971\\n6977\\n6983\\n6991\\n6997\\n7001\\n7013\\n7019\\n7027\\n7039\\n7043\\n7057\\n7069\\n7079\\n7103\\n7109\\n7121\\n7127\\n7129\\n7151\\n7159\\n7177\\n7187\\n7193\\n7207\\n7211\\n7213\\n7219\\n7229\\n7237\\n7243\\n7247\\n7253\\n7283\\n7297\\n7307\\n7309\\n7321\\n7331\\n7333\\n7349\\n7351\\n7369\\n7393\\n7411\\n7417\\n7433\\n7451\\n7457\\n7459\\n7477\\n7481\\n7487\\n7489\\n7499\\n7507\\n7517\\n7523\\n7529\\n7537\\n7541\\n7547\\n7549\\n7559\\n7561\\n7573\\n7577\\n7583\\n7589\\n7591\\n7603\\n7607\\n7621\\n7639\\n7643\\n7649\\n7669\\n7673\\n7681\\n7687\\n7691\\n7699\\n7703\\n7717\\n7723\\n7727\\n7741\\n7753\\n7757\\n7759\\n7789\\n7793\\n7817\\n7823\\n7829\\n7841\\n7853\\n7867\\n7873\\n7877\\n7879\\n7883\\n7901\\n7907\\n7919\", \"base64Encoded\": false, \"contentType\": \"text/plain\"}}\n",
+ "error": "",
+ "result": {
+ "prime_numbers.txt": {
+ "content": "2\n3\n5\n7\n11\n13\n17\n19\n23\n29\n31\n37\n41\n43\n47\n53\n59\n61\n67\n71\n73\n79\n83\n89\n97\n101\n103\n107\n109\n113\n127\n131\n137\n139\n149\n151\n157\n163\n167\n173\n179\n181\n191\n193\n197\n199\n211\n223\n227\n229\n233\n239\n241\n251\n257\n263\n269\n271\n277\n281\n283\n293\n307\n311\n313\n317\n331\n337\n347\n349\n353\n359\n367\n373\n379\n383\n389\n397\n401\n409\n419\n421\n431\n433\n439\n443\n449\n457\n461\n463\n467\n479\n487\n491\n499\n503\n509\n521\n523\n541\n547\n557\n563\n569\n571\n577\n587\n593\n599\n601\n607\n613\n617\n619\n631\n641\n643\n647\n653\n659\n661\n673\n677\n683\n691\n701\n709\n719\n727\n733\n739\n743\n751\n757\n761\n769\n773\n787\n797\n809\n811\n821\n823\n827\n829\n839\n853\n857\n859\n863\n877\n881\n883\n887\n907\n911\n919\n929\n937\n941\n947\n953\n967\n971\n977\n983\n991\n997\n1009\n1013\n1019\n1021\n1031\n1033\n1039\n1049\n1051\n1061\n1063\n1069\n1087\n1091\n1093\n1097\n1103\n1109\n1117\n1123\n1129\n1151\n1153\n1163\n1171\n1181\n1187\n1193\n1201\n1213\n1217\n1223\n1229\n1231\n1237\n1249\n1259\n1277\n1279\n1283\n1289\n1291\n1297\n1301\n1303\n1307\n1319\n1321\n1327\n1361\n1367\n1373\n1381\n1399\n1409\n1423\n1427\n1429\n1433\n1439\n1447\n1451\n1453\n1459\n1471\n1481\n1483\n1487\n1489\n1493\n1499\n1511\n1523\n1531\n1543\n1549\n1553\n1559\n1567\n1571\n1579\n1583\n1597\n1601\n1607\n1609\n1613\n1619\n1621\n1627\n1637\n1657\n1663\n1667\n1669\n1693\n1697\n1699\n1709\n1721\n1723\n1733\n1741\n1747\n1753\n1759\n1777\n1783\n1787\n1789\n1801\n1811\n1823\n1831\n1847\n1861\n1867\n1871\n1873\n1877\n1879\n1889\n1901\n1907\n1913\n1931\n1933\n1949\n1951\n1973\n1979\n1987\n1993\n1997\n1999\n2003\n2011\n2017\n2027\n2029\n2039\n2053\n2063\n2069\n2081\n2083\n2087\n2089\n2099\n2111\n2113\n2129\n2131\n2137\n2141\n2143\n2153\n2161\n2179\n2203\n2207\n2213\n2221\n2237\n2239\n2243\n2251\n2267\n2269\n2273\n2281\n2287\n2293\n2297\n2309\n2311\n2333\n2339\n2341\n2347\n2351\n2357\n2371\n2377\n2381\n2383\n2389\n2393\n2399\n2411\n2417\n2423\n2437\n2441\n2447\n2459\n2467\n2473\n2477\n2503\n2521\n2531\n2539\n2543\n2549\n2551\n2557\n2579\n2591\n2593\n2609\n2617\n2621\n2633\n2647\n2657\n2659\n2663\n2671\n2677\n2683\n2687\n2689\n2693\n2699\n2707\n2711\n2713\n2719\n2729\n2731\n2741\n2749\n2753\n2767\n2777\n2789\n2791\n2797\n2801\n2803\n2819\n2833\n2837\n2843\n2851\n2857\n2861\n2879\n2887\n2897\n2903\n2909\n2917\n2927\n2939\n2953\n2957\n2963\n2969\n2971\n2999\n3001\n3011\n3019\n3023\n3037\n3041\n3049\n3061\n3067\n3079\n3083\n3089\n3109\n3119\n3121\n3137\n3163\n3167\n3169\n3181\n3187\n3191\n3203\n3209\n3217\n3221\n3229\n3251\n3253\n3257\n3259\n3271\n3299\n3301\n3307\n3313\n3319\n3323\n3329\n3331\n3343\n3347\n3359\n3361\n3371\n3373\n3389\n3391\n3407\n3413\n3433\n3449\n3457\n3461\n3463\n3467\n3469\n3491\n3499\n3511\n3517\n3527\n3529\n3533\n3539\n3541\n3547\n3557\n3559\n3571\n3581\n3583\n3593\n3607\n3613\n3617\n3623\n3631\n3637\n3643\n3659\n3671\n3673\n3677\n3691\n3697\n3701\n3709\n3719\n3727\n3733\n3739\n3761\n3767\n3769\n3779\n3793\n3797\n3803\n3821\n3823\n3833\n3847\n3851\n3853\n3863\n3877\n3881\n3889\n3907\n3911\n3917\n3919\n3923\n3929\n3931\n3943\n3947\n3967\n3989\n4001\n4003\n4007\n4013\n4019\n4021\n4027\n4049\n4051\n4057\n4073\n4079\n4091\n4093\n4099\n4111\n4127\n4129\n4133\n4139\n4153\n4157\n4159\n4177\n4201\n4211\n4217\n4219\n4229\n4231\n4241\n4243\n4253\n4259\n4261\n4271\n4273\n4283\n4289\n4297\n4327\n4337\n4339\n4349\n4357\n4363\n4373\n4391\n4397\n4409\n4421\n4423\n4441\n4447\n4451\n4457\n4463\n4481\n4483\n4493\n4507\n4513\n4517\n4519\n4523\n4547\n4549\n4561\n4567\n4583\n4591\n4597\n4603\n4621\n4637\n4639\n4643\n4649\n4651\n4657\n4663\n4673\n4679\n4691\n4703\n4721\n4723\n4729\n4733\n4751\n4759\n4783\n4787\n4789\n4793\n4799\n4801\n4813\n4817\n4831\n4861\n4871\n4877\n4889\n4903\n4909\n4919\n4931\n4933\n4937\n4943\n4951\n4957\n4967\n4969\n4973\n4987\n4993\n4999\n5003\n5009\n5011\n5021\n5023\n5039\n5051\n5059\n5077\n5081\n5087\n5099\n5101\n5107\n5113\n5119\n5147\n5153\n5167\n5171\n5179\n5189\n5197\n5209\n5227\n5231\n5233\n5237\n5261\n5273\n5279\n5281\n5297\n5303\n5309\n5323\n5333\n5347\n5351\n5381\n5387\n5393\n5399\n5407\n5413\n5417\n5419\n5431\n5437\n5441\n5443\n5449\n5471\n5477\n5479\n5483\n5501\n5503\n5507\n5519\n5521\n5527\n5531\n5557\n5563\n5569\n5573\n5581\n5591\n5623\n5639\n5641\n5647\n5651\n5653\n5657\n5659\n5669\n5683\n5689\n5693\n5701\n5711\n5717\n5737\n5741\n5743\n5749\n5779\n5783\n5791\n5801\n5807\n5813\n5821\n5827\n5839\n5843\n5849\n5851\n5857\n5861\n5867\n5869\n5879\n5881\n5897\n5903\n5923\n5927\n5939\n5953\n5981\n5987\n6007\n6011\n6029\n6037\n6043\n6047\n6053\n6067\n6073\n6079\n6089\n6091\n6101\n6113\n6121\n6131\n6133\n6143\n6151\n6163\n6173\n6197\n6199\n6203\n6211\n6217\n6221\n6229\n6247\n6257\n6263\n6269\n6271\n6277\n6287\n6299\n6301\n6311\n6317\n6323\n6329\n6337\n6343\n6353\n6359\n6361\n6367\n6373\n6379\n6389\n6397\n6421\n6427\n6449\n6451\n6469\n6473\n6481\n6491\n6521\n6529\n6547\n6551\n6553\n6563\n6569\n6571\n6577\n6581\n6599\n6607\n6619\n6637\n6653\n6659\n6661\n6673\n6679\n6689\n6691\n6701\n6703\n6709\n6719\n6733\n6737\n6761\n6763\n6779\n6781\n6791\n6793\n6803\n6823\n6827\n6829\n6833\n6841\n6857\n6863\n6869\n6871\n6883\n6899\n6907\n6911\n6917\n6947\n6949\n6959\n6961\n6967\n6971\n6977\n6983\n6991\n6997\n7001\n7013\n7019\n7027\n7039\n7043\n7057\n7069\n7079\n7103\n7109\n7121\n7127\n7129\n7151\n7159\n7177\n7187\n7193\n7207\n7211\n7213\n7219\n7229\n7237\n7243\n7247\n7253\n7283\n7297\n7307\n7309\n7321\n7331\n7333\n7349\n7351\n7369\n7393\n7411\n7417\n7433\n7451\n7457\n7459\n7477\n7481\n7487\n7489\n7499\n7507\n7517\n7523\n7529\n7537\n7541\n7547\n7549\n7559\n7561\n7573\n7577\n7583\n7589\n7591\n7603\n7607\n7621\n7639\n7643\n7649\n7669\n7673\n7681\n7687\n7691\n7699\n7703\n7717\n7723\n7727\n7741\n7753\n7757\n7759\n7789\n7793\n7817\n7823\n7829\n7841\n7853\n7867\n7873\n7877\n7879\n7883\n7901\n7907\n7919",
+ "base64Encoded": false,
+ "contentType": "text/plain"
+ }
+ },
+ "exitCode": 0
+ }
+ }
+]
\ No newline at end of file
diff --git a/primes.txt b/static/3_prime_numbers.txt
similarity index 63%
rename from primes.txt
rename to static/3_prime_numbers.txt
index 7c591232..4dbadc38 100644
--- a/primes.txt
+++ b/static/3_prime_numbers.txt
@@ -648,3 +648,353 @@
4813
4817
4831
+4861
+4871
+4877
+4889
+4903
+4909
+4919
+4931
+4933
+4937
+4943
+4951
+4957
+4967
+4969
+4973
+4987
+4993
+4999
+5003
+5009
+5011
+5021
+5023
+5039
+5051
+5059
+5077
+5081
+5087
+5099
+5101
+5107
+5113
+5119
+5147
+5153
+5167
+5171
+5179
+5189
+5197
+5209
+5227
+5231
+5233
+5237
+5261
+5273
+5279
+5281
+5297
+5303
+5309
+5323
+5333
+5347
+5351
+5381
+5387
+5393
+5399
+5407
+5413
+5417
+5419
+5431
+5437
+5441
+5443
+5449
+5471
+5477
+5479
+5483
+5501
+5503
+5507
+5519
+5521
+5527
+5531
+5557
+5563
+5569
+5573
+5581
+5591
+5623
+5639
+5641
+5647
+5651
+5653
+5657
+5659
+5669
+5683
+5689
+5693
+5701
+5711
+5717
+5737
+5741
+5743
+5749
+5779
+5783
+5791
+5801
+5807
+5813
+5821
+5827
+5839
+5843
+5849
+5851
+5857
+5861
+5867
+5869
+5879
+5881
+5897
+5903
+5923
+5927
+5939
+5953
+5981
+5987
+6007
+6011
+6029
+6037
+6043
+6047
+6053
+6067
+6073
+6079
+6089
+6091
+6101
+6113
+6121
+6131
+6133
+6143
+6151
+6163
+6173
+6197
+6199
+6203
+6211
+6217
+6221
+6229
+6247
+6257
+6263
+6269
+6271
+6277
+6287
+6299
+6301
+6311
+6317
+6323
+6329
+6337
+6343
+6353
+6359
+6361
+6367
+6373
+6379
+6389
+6397
+6421
+6427
+6449
+6451
+6469
+6473
+6481
+6491
+6521
+6529
+6547
+6551
+6553
+6563
+6569
+6571
+6577
+6581
+6599
+6607
+6619
+6637
+6653
+6659
+6661
+6673
+6679
+6689
+6691
+6701
+6703
+6709
+6719
+6733
+6737
+6761
+6763
+6779
+6781
+6791
+6793
+6803
+6823
+6827
+6829
+6833
+6841
+6857
+6863
+6869
+6871
+6883
+6899
+6907
+6911
+6917
+6947
+6949
+6959
+6961
+6967
+6971
+6977
+6983
+6991
+6997
+7001
+7013
+7019
+7027
+7039
+7043
+7057
+7069
+7079
+7103
+7109
+7121
+7127
+7129
+7151
+7159
+7177
+7187
+7193
+7207
+7211
+7213
+7219
+7229
+7237
+7243
+7247
+7253
+7283
+7297
+7307
+7309
+7321
+7331
+7333
+7349
+7351
+7369
+7393
+7411
+7417
+7433
+7451
+7457
+7459
+7477
+7481
+7487
+7489
+7499
+7507
+7517
+7523
+7529
+7537
+7541
+7547
+7549
+7559
+7561
+7573
+7577
+7583
+7589
+7591
+7603
+7607
+7621
+7639
+7643
+7649
+7669
+7673
+7681
+7687
+7691
+7699
+7703
+7717
+7723
+7727
+7741
+7753
+7757
+7759
+7789
+7793
+7817
+7823
+7829
+7841
+7853
+7867
+7873
+7877
+7879
+7883
+7901
+7907
+7919
\ No newline at end of file