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