From 40f82a3848876b655edfbfaea1a8b5cc2e18f84d Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 18 May 2025 22:25:26 +0200
Subject: [PATCH] db items serialized, uuid overall, auth enhanced
---
app.py | 9 +-
env_dev.env | 10 +-
env_prod.env | 10 +-
modules/connectors/connectorDbJson.py | 494 ++++++++++++++-------
modules/interfaces/gatewayAccess.py | 34 +-
modules/interfaces/gatewayInterface.py | 216 +++++----
modules/interfaces/gatewayModel.py | 8 +-
modules/interfaces/lucydomAccess.py | 36 +-
modules/interfaces/lucydomInterface.py | 225 ++++++----
modules/interfaces/lucydomModel BACKUP.py | 265 +++++++++++
modules/interfaces/lucydomModel.py | 245 ++++------
modules/routes/routeAttributes.py | 50 ++-
modules/routes/routeFiles.py | 28 +-
modules/routes/routeGeneral.py | 154 ++++++-
modules/routes/routeMandates.py | 70 ++-
modules/routes/routeMsft.py | 50 +--
modules/routes/routePrompts.py | 24 +-
modules/routes/routeUsers.py | 71 +--
modules/routes/routeWorkflows.py | 171 +++++--
modules/security/auth.py | 79 ++--
modules/shared/defAttributes.py | 4 +-
modules/workflow/agentBase.py | 13 +-
modules/workflow/agentRegistry.py | 34 +-
modules/workflow/workflowAgentsRegistry.py | 10 -
modules/workflow/workflowManager.py | 143 +++---
notes/changelog.txt | 7 +
26 files changed, 1563 insertions(+), 897 deletions(-)
create mode 100644 modules/interfaces/lucydomModel BACKUP.py
delete mode 100644 modules/workflow/workflowAgentsRegistry.py
diff --git a/app.py b/app.py
index dba65587..89282ca1 100644
--- a/app.py
+++ b/app.py
@@ -57,17 +57,14 @@ initLogging()
logger = logging.getLogger(__name__)
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
-# Import models - import generically for INITIALIZATION
-from modules.interfaces.gatewayInterface import getGatewayInterface
-gateway = getGatewayInterface()
-
-
# Define lifespan context manager for application startup/shutdown events
@asynccontextmanager
async def lifespan(app: FastAPI):
- # Startup logic (if any)
+ # Startup logic
logger.info("Application is starting up")
+
yield
+
# Shutdown logic
logger.info("Application has been shut down")
diff --git a/env_dev.env b/env_dev.env
index ad9fcbaa..188ce330 100644
--- a/env_dev.env
+++ b/env_dev.env
@@ -5,11 +5,11 @@ APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8000
-# Database Configuration System
-DB_SYSTEM_HOST=D:/Temp/_powerondb
-DB_SYSTEM_DATABASE=system
-DB_SYSTEM_USER=dev_user
-DB_SYSTEM_PASSWORD_SECRET=dev_password
+# Database Configuration Gateway
+DB_GATEWAY_HOST=D:/Temp/_powerondb
+DB_GATEWAY_DATABASE=gateway
+DB_GATEWAY_USER=dev_user
+DB_GATEWAY_PASSWORD_SECRET=dev_password
# Database Configuration LucyDOM
DB_LUCYDOM_HOST=D:/Temp/_powerondb
diff --git a/env_prod.env b/env_prod.env
index 42a47166..f18db4af 100644
--- a/env_prod.env
+++ b/env_prod.env
@@ -5,11 +5,11 @@ APP_ENV_TYPE = prod
APP_ENV_LABEL = Production Instance
APP_API_URL = https://gateway.poweron-center.net
-# Database Configuration System
-DB_SYSTEM_HOST=/home/_powerondb
-DB_SYSTEM_DATABASE=system
-DB_SYSTEM_USER=dev_user
-DB_SYSTEM_PASSWORD_SECRET=prod_password
+# Database Configuration Gateway
+DB_GATEWAY_HOST=/home/_powerondb
+DB_GATEWAY_DATABASE=gateway
+DB_GATEWAY_USER=dev_user
+DB_GATEWAY_PASSWORD_SECRET=prod_password
# Database Configuration LucyDOM
DB_LUCYDOM_HOST=/home/_powerondb
diff --git a/modules/connectors/connectorDbJson.py b/modules/connectors/connectorDbJson.py
index 024a4984..fa23fb24 100644
--- a/modules/connectors/connectorDbJson.py
+++ b/modules/connectors/connectorDbJson.py
@@ -2,6 +2,8 @@ import json
import os
from typing import List, Dict, Any, Optional, Union
import logging
+from datetime import datetime
+import uuid
logger = logging.getLogger(__name__)
@@ -9,19 +11,28 @@ class DatabaseConnector:
"""
A connector for JSON-based data storage.
Provides generic database operations without user/mandate filtering.
+ Stores tables as folders and records as individual files.
+ Implements lazy loading for better performance.
"""
def __init__(self, dbHost: str, dbDatabase: str, dbUser: str = None, dbPassword: str = None,
- mandateId: int = None, userId: int = None, skipInitialIdLookup: bool = False):
+ _mandateId: str = None, _userId: str = None, skipInitialIdLookup: bool = False):
# Store the input parameters
self.dbHost = dbHost
self.dbDatabase = dbDatabase
self.dbUser = dbUser
self.dbPassword = dbPassword
- self.skipInitialIdLookup = skipInitialIdLookup
# Check if context parameters are set
- if mandateId is None or userId is None:
- raise ValueError("mandateId and userId must be set")
+ if _mandateId is None and _userId is None:
+ # Allow initialization with empty strings
+ self._mandateId = ''
+ self._userId = ''
+ else:
+ # Ensure both parameters are provided
+ if _mandateId is None or _userId is None:
+ raise ValueError("_mandateId and _userId must both be provided or both be None")
+ self._mandateId = _mandateId
+ self._userId = _userId
# Ensure the database directory exists
self.dbFolder = os.path.join(self.dbHost, self.dbDatabase)
@@ -29,34 +40,33 @@ class DatabaseConnector:
# Cache for loaded data
self._tablesCache = {}
+ self._tableMetadataCache = {} # Cache for table metadata (record IDs, etc.)
# Initialize system table
self._systemTableName = "_system"
self._initializeSystemTable()
- # Temporarily store mandateId and userId
- self._mandateId = mandateId
- self._userId = userId
-
- # If mandateId or userId are 0 and we're not skipping ID lookup, try to use the initial IDs
+ # If IDs are empty and we're not skipping lookup, try to use initial IDs
if not skipInitialIdLookup:
- if mandateId == 0:
- initialMandateId = self.getInitialId("mandates")
- if initialMandateId is not None:
- self._mandateId = initialMandateId
- logger.info(f"Using initial mandateId: {initialMandateId} instead of 0")
+ self._resolveInitialIds()
- if userId == 0:
- initialUserId = self.getInitialId("users")
- if initialUserId is not None:
- self._userId = initialUserId
- logger.info(f"Using initial userId: {initialUserId} instead of 0")
+ logger.debug(f"Context: _mandateId={self._mandateId}, _userId={self._userId}")
+
+ def _resolveInitialIds(self):
+ """
+ Resolve initial IDs for mandate and user if they're empty.
+ """
+ if not self._mandateId:
+ initialMandateId = self.getInitialId("mandates")
+ if initialMandateId is not None:
+ self._mandateId = initialMandateId
+ logger.info(f"Using initial _mandateId: {initialMandateId}")
- # Set the effective IDs as properties
- self.mandateId = self._mandateId
- self.userId = self._userId
-
- logger.debug(f"Context: mandateId={self.mandateId}, userId={self.userId}")
+ if not self._userId:
+ initialUserId = self.getInitialId("users")
+ if initialUserId is not None:
+ self._userId = initialUserId
+ logger.info(f"Using initial _userId: {initialUserId}")
def _initializeSystemTable(self):
"""Initializes the system table if it doesn't exist yet."""
@@ -70,7 +80,7 @@ class DatabaseConnector:
self._loadSystemTable()
logger.debug(f"Existing system table loaded from {systemTablePath}")
- def _loadSystemTable(self) -> Dict[str, int]:
+ def _loadSystemTable(self) -> Dict[str, str]:
"""Loads the system table with the initial IDs."""
# Check if system table is in cache
if f"_{self._systemTableName}" in self._tablesCache:
@@ -92,7 +102,7 @@ class DatabaseConnector:
self._tablesCache[f"_{self._systemTableName}"] = {}
return {}
- def _saveSystemTable(self, data: Dict[str, int]) -> bool:
+ def _saveSystemTable(self, data: Dict[str, str]) -> bool:
"""Saves the system table with the initial IDs."""
systemTablePath = self._getTablePath(self._systemTableName)
try:
@@ -106,76 +116,111 @@ class DatabaseConnector:
return False
def _getTablePath(self, table: str) -> str:
- """Returns the full path to a table file"""
- return os.path.join(self.dbFolder, f"{table}.json")
-
- def _loadTable(self, table: str) -> List[Dict[str, Any]]:
- """Loads a table from the corresponding JSON file"""
- path = self._getTablePath(table)
+ """Returns the full path to a table folder"""
+ return os.path.join(self.dbFolder, table)
+
+ def _getRecordPath(self, table: str, recordId: Union[str, int]) -> str:
+ """Returns the full path to a record file"""
+ return os.path.join(self._getTablePath(table), f"{recordId}.json")
+
+ def _ensureTableDirectory(self, table: str) -> bool:
+ """Ensures the table directory exists."""
+ if table == self._systemTableName:
+ return True
+
+ tablePath = self._getTablePath(table)
+ try:
+ os.makedirs(tablePath, exist_ok=True)
+ return True
+ except Exception as e:
+ logger.error(f"Error creating table directory {tablePath}: {e}")
+ return False
+
+ def _loadTableMetadata(self, table: str) -> Dict[str, Any]:
+ """Loads table metadata (list of record IDs) without loading actual records."""
+ if table in self._tableMetadataCache:
+ return self._tableMetadataCache[table]
+
+ # Ensure table directory exists
+ if not self._ensureTableDirectory(table):
+ return {"recordIds": []}
+
+ tablePath = self._getTablePath(table)
+ metadata = {"recordIds": []}
+ try:
+ if os.path.exists(tablePath):
+ for filename in os.listdir(tablePath):
+ if filename.endswith('.json'):
+ recordId = filename[:-5] # Remove .json extension
+ metadata["recordIds"].append(recordId)
+
+ metadata["recordIds"].sort()
+ self._tableMetadataCache[table] = metadata
+ except Exception as e:
+ logger.error(f"Error loading table metadata for {table}: {e}")
+
+ return metadata
+
+ def _loadRecord(self, table: str, recordId: Union[str, int]) -> Optional[Dict[str, Any]]:
+ """Loads a single record from the table."""
+ recordPath = self._getRecordPath(table, recordId)
+ try:
+ if os.path.exists(recordPath):
+ with open(recordPath, 'r', encoding='utf-8') as f:
+ record = json.load(f)
+ # Ensure ID is a string
+ if "id" in record:
+ record["id"] = str(record["id"])
+ return record
+ except Exception as e:
+ logger.error(f"Error loading record {recordId} from table {table}: {e}")
+ return None
+
+ def _loadTable(self, table: str) -> List[Dict[str, Any]]:
+ """Loads all records from a table folder."""
# If the table is the system table, load it directly
if table == self._systemTableName:
- return [] # The system table is not treated like normal tables
+ return self._loadSystemTable()
# If the table is already in the cache, use the cache
if table in self._tablesCache:
return self._tablesCache[table]
- # Otherwise load the file
- try:
- if os.path.exists(path):
- with open(path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- self._tablesCache[table] = data
-
- # If data was loaded and no initial ID is registered yet,
- # register the ID of the first record (if available)
- if data and not self.hasInitialId(table):
- if "id" in data[0]:
- self._registerInitialId(table, data[0]["id"])
- logger.info(f"Initial ID {data[0]['id']} for table {table} retroactively registered")
-
- return data
- else:
- # If the file doesn't exist, create an empty table
- logger.info(f"New table {table}")
- self._tablesCache[table] = []
- self._saveTable(table, [])
- return []
- except Exception as e:
- logger.error(f"Error loading table {table}: {e}")
- return []
-
+ # Load metadata first
+ metadata = self._loadTableMetadata(table)
+ records = []
+
+ # Load each record
+ for recordId in metadata["recordIds"]:
+ record = self._loadRecord(table, recordId)
+ if record:
+ records.append(record)
+
+ self._tablesCache[table] = records
+ return records
+
def _saveTable(self, table: str, data: List[Dict[str, Any]]) -> bool:
- """Saves a table to the corresponding JSON file"""
+ """Saves all records to a table folder"""
# The system table is handled specially
if table == self._systemTableName:
- return False
+ return self._saveSystemTable(data)
- path = self._getTablePath(table)
+ tablePath = self._getTablePath(table)
try:
- # Check if directory exists and is writable
- dir_path = os.path.dirname(path)
- if not os.path.exists(dir_path):
- logger.error(f"Directory does not exist: {dir_path}")
- return False
- if not os.access(dir_path, os.W_OK):
- logger.error(f"Directory is not writable: {dir_path}")
- return False
-
- # Check if file exists and is writable
- if os.path.exists(path) and not os.access(path, os.W_OK):
- logger.error(f"File exists but is not writable: {path}")
- return False
-
- with open(path, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
+ # Ensure table directory exists
+ os.makedirs(tablePath, exist_ok=True)
+
+ # Save each record as a separate file
+ for record in data:
+ if "id" not in record:
+ logger.error(f"Record missing ID in table {table}")
+ continue
+
+ recordPath = self._getRecordPath(table, record["id"])
+ with open(recordPath, 'w', encoding='utf-8') as f:
+ json.dump(record, f, indent=2, ensure_ascii=False)
- # Verify the file was written correctly
- if not os.path.exists(path):
- logger.error(f"File was not created after write: {path}")
- return False
-
# Update the cache
self._tablesCache[table] = data
logger.debug(f"Successfully saved table {table}")
@@ -185,7 +230,7 @@ class DatabaseConnector:
logger.error(f"Error type: {type(e).__name__}")
logger.error(f"Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No details available'}")
return False
-
+
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:
@@ -202,19 +247,12 @@ class DatabaseConnector:
match = False
break
- # Handle type conversion for integer comparisons both ways
- if isinstance(value, int) and isinstance(record[field], str) and record[field].isdigit():
- # Filter value is int, record value is string
- if value != int(record[field]):
- match = False
- break
- elif isinstance(value, str) and value.isdigit() and isinstance(record[field], int):
- # Filter value is string, record value is int
- if record[field] != int(value):
- match = False
- break
- # Otherwise direct comparison
- elif record[field] != value:
+ # Convert both values to strings for comparison
+ recordValue = str(record[field])
+ filterValue = str(value)
+
+ # Direct string comparison
+ if recordValue != filterValue:
match = False
break
@@ -223,7 +261,7 @@ class DatabaseConnector:
return filteredRecords
- def _registerInitialId(self, table: str, initialId: int) -> bool:
+ def _registerInitialId(self, table: str, initialId: str) -> bool:
"""Registers the initial ID for a table."""
try:
systemData = self._loadSystemTable()
@@ -255,6 +293,41 @@ class DatabaseConnector:
logger.error(f"Error removing initial ID for table {table}: {e}")
return False
+ def _getCurrentTimestamp(self) -> str:
+ """Returns the current timestamp in ISO format."""
+ return datetime.now().isoformat()
+
+ def _saveTableMetadata(self, table: str, metadata: Dict[str, Any]) -> bool:
+ """Saves table metadata to a metadata file."""
+ try:
+ # Create metadata file path
+ metadataPath = os.path.join(self._getTablePath(table), "_metadata.json")
+
+ # Save metadata
+ with open(metadataPath, 'w', encoding='utf-8') as f:
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
+
+ # Update cache
+ self._tableMetadataCache[table] = metadata
+
+ return True
+ except Exception as e:
+ logger.error(f"Error saving metadata for table {table}: {e}")
+ return False
+
+ def updateContext(self, _mandateId: str, _userId: str) -> None:
+ """Updates the context of the database connector."""
+ if _mandateId is None or _userId is None:
+ raise ValueError("_mandateId and _userId must both be provided")
+
+ self._mandateId = _mandateId
+ self._userId = _userId
+ logger.info(f"Updated database context: _mandateId={self._mandateId}, _userId={self._userId}")
+
+ # Clear cache to ensure fresh data with new context
+ self._tablesCache = {}
+ self._tableMetadataCache = {}
+
# Public API
def getTables(self) -> List[str]:
@@ -262,10 +335,10 @@ class DatabaseConnector:
tables = []
try:
- for filename in os.listdir(self.dbFolder):
- if filename.endswith('.json') and not filename.startswith('_'):
- tableName = filename[:-5] # Remove the .json extension
- tables.append(tableName)
+ for item in os.listdir(self.dbFolder):
+ itemPath = os.path.join(self.dbFolder, item)
+ if os.path.isdir(itemPath) and not item.startswith('_'):
+ tables.append(item)
except Exception as e:
logger.error(f"Error reading the database directory: {e}")
@@ -306,16 +379,26 @@ class DatabaseConnector:
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."""
- data = self._loadTable(table)
+ # If we have specific record IDs in the filter, only load those records
+ if recordFilter and "id" in recordFilter:
+ recordId = recordFilter["id"]
+ record = self._loadRecord(table, recordId)
+ if record:
+ records = [record]
+ else:
+ return []
+ else:
+ # Load all records if no specific ID filter
+ records = self._loadTable(table)
# Apply recordFilter if available
if recordFilter:
- data = self._applyRecordFilter(data, recordFilter)
+ records = self._applyRecordFilter(records, recordFilter)
# If fieldFilter is available, reduce the fields
if fieldFilter and isinstance(fieldFilter, list):
result = []
- for record in data:
+ for record in records:
filteredRecord = {}
for field in fieldFilter:
if field in record:
@@ -323,92 +406,159 @@ class DatabaseConnector:
result.append(filteredRecord)
return result
- return data
+ return records
def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
- """Creates a new record in the table."""
- data = self._loadTable(table)
-
- # Add mandateId and userId if not present
- if "mandateId" not in recordData or recordData["mandateId"] == 0:
- recordData["mandateId"] = self.mandateId
-
- if "userId" not in recordData or recordData["userId"] == 0:
- recordData["userId"] = self.userId
-
- # Determine the next ID if not present
- if "id" not in recordData:
- nextId = 1
- if data:
- nextId = max(record["id"] for record in data if "id" in record) + 1
- recordData["id"] = nextId
-
- # If the table is empty and a system ID should be registered
- if not data:
- self._registerInitialId(table, recordData["id"])
- logger.info(f"Initial ID {recordData['id']} for table {table} has been registered")
-
- # Add the new record
- data.append(recordData)
-
- # Save the updated table
- if self._saveTable(table, data):
+ """Creates a new record in the specified table."""
+ try:
+ # Ensure table directory exists
+ if not self._ensureTableDirectory(table):
+ raise ValueError(f"Error creating table directory for {table}")
+
+ # Load table metadata
+ metadata = self._loadTableMetadata(table)
+
+ # Generate new ID if not provided
+ if "id" not in recordData:
+ recordData["id"] = str(uuid.uuid4())
+ else:
+ # Ensure ID is a string
+ recordData["id"] = str(recordData["id"])
+
+ # Add context fields
+ recordData["_mandateId"] = self._mandateId
+ recordData["_userId"] = self._userId
+
+ # Update metadata
+ if "recordIds" not in metadata:
+ metadata["recordIds"] = []
+ metadata["recordIds"].append(recordData["id"])
+ metadata["recordIds"].sort()
+
+ # Add creation timestamp
+ currentTime = self._getCurrentTimestamp()
+ recordData["_createdAt"] = currentTime
+ recordData["_modifiedAt"] = currentTime
+
+ # Save the record
+ recordPath = self._getRecordPath(table, recordData["id"])
+ os.makedirs(os.path.dirname(recordPath), exist_ok=True)
+ with open(recordPath, 'w', encoding='utf-8') as f:
+ json.dump(recordData, f, indent=2, ensure_ascii=False)
+
+ # Save metadata
+ if not self._saveTableMetadata(table, metadata):
+ raise ValueError(f"Error saving metadata for table {table}")
+
+ # Update cache safely
+ if table in self._tablesCache:
+ if isinstance(self._tablesCache[table], list):
+ self._tablesCache[table].append(recordData)
+ else:
+ self._tablesCache[table] = [recordData]
+ else:
+ self._tablesCache[table] = [recordData]
+
+ # Verify the record was created
+ if not os.path.exists(recordPath):
+ raise ValueError(f"Record file was not created at {recordPath}")
+
return recordData
- else:
+
+ except Exception as e:
+ logger.error(f"Error creating record in table {table}: {str(e)}")
raise ValueError(f"Error creating the record in table {table}")
- def recordDelete(self, table: str, recordId: Union[str, int]) -> bool:
+ def recordDelete(self, table: str, recordId: str) -> bool:
"""Deletes a record from the table."""
- data = self._loadTable(table)
+ # Load metadata
+ metadata = self._loadTableMetadata(table)
- # Search for the record
- for i, record in enumerate(data):
- if "id" in record and record["id"] == recordId:
- # Check if it's an initial record
- initialId = self.getInitialId(table)
- if initialId is not None and initialId == recordId:
- self._removeInitialId(table)
- logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
-
- # Delete the record
- del data[i]
-
- # Save the updated table
- return self._saveTable(table, data)
+ if recordId not in metadata["recordIds"]:
+ return False
+
+ # Check if it's an initial record
+ initialId = self.getInitialId(table)
+ if initialId is not None and initialId == recordId:
+ self._removeInitialId(table)
+ logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
+
+ # Delete the record file
+ recordPath = self._getRecordPath(table, recordId)
+ try:
+ if os.path.exists(recordPath):
+ os.remove(recordPath)
+
+ # Update metadata cache
+ metadata["recordIds"].remove(recordId)
+ self._tableMetadataCache[table] = metadata
+
+ # Update table cache if it exists
+ if table in self._tablesCache:
+ self._tablesCache[table] = [r for r in self._tablesCache[table] if r.get("id") != recordId]
+
+ return True
+ except Exception as e:
+ logger.error(f"Error deleting record file {recordPath}: {e}")
+ return False
- # Record not found
return False
- def recordModify(self, table: str, recordId: Union[str, int], recordData: Dict[str, Any]) -> Dict[str, Any]:
+ def recordModify(self, table: str, recordId: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
"""Modifies a record in the table."""
- data = self._loadTable(table)
+ # Ensure table directory exists
+ if not self._ensureTableDirectory(table):
+ raise ValueError(f"Error creating table directory for {table}")
+
+ # Load metadata to check if record exists
+ metadata = self._loadTableMetadata(table)
- # Search for the record
- for i, record in enumerate(data):
- if "id" in record and record["id"] == recordId:
- # 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")
-
- # Update the record
- for key, value in recordData.items():
- data[i][key] = value
-
- # Save the updated table
- if self._saveTable(table, data):
- return data[i]
- else:
- raise ValueError(f"Error updating record in table {table}")
+ # Ensure recordId is a string
+ recordId = str(recordId)
- # Record not found
- raise ValueError(f"Record with ID {recordId} not found in table {table}")
+ if recordId not in metadata["recordIds"]:
+ raise ValueError(f"Record with ID {recordId} not found in table {table}")
+
+ # Prevent changing the ID
+ if "id" in recordData and str(recordData["id"]) != recordId:
+ raise ValueError(f"The ID of a record in table {table} cannot be changed")
+
+ # Load existing record
+ existingRecord = self._loadRecord(table, recordId)
+ if not existingRecord:
+ raise ValueError(f"Record with ID {recordId} not found in table {table}")
+
+ # Update the record
+ for key, value in recordData.items():
+ existingRecord[key] = value
+
+ # Update modified timestamp
+ existingRecord["_modifiedAt"] = self._getCurrentTimestamp()
+
+ # Save the updated record
+ recordPath = self._getRecordPath(table, recordId)
+ try:
+ with open(recordPath, 'w', encoding='utf-8') as f:
+ json.dump(existingRecord, f, indent=2, ensure_ascii=False)
+
+ # Update table cache if it exists
+ if table in self._tablesCache:
+ for i, record in enumerate(self._tablesCache[table]):
+ if str(record.get("id")) == recordId:
+ self._tablesCache[table][i] = existingRecord
+ break
+
+ return existingRecord
+ except Exception as e:
+ logger.error(f"Error updating record file {recordPath}: {e}")
+ raise ValueError(f"Error updating record in table {table}")
def hasInitialId(self, table: str) -> bool:
"""Checks if an initial ID is registered for a table."""
systemData = self._loadSystemTable()
return table in systemData
- def getInitialId(self, table: str) -> Optional[int]:
+ def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
systemData = self._loadSystemTable()
initialId = systemData.get(table)
@@ -417,7 +567,7 @@ class DatabaseConnector:
logger.debug(f"No initial ID found for table {table}")
return initialId
- def getAllInitialIds(self) -> Dict[str, int]:
+ def getAllInitialIds(self) -> Dict[str, str]:
"""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/interfaces/gatewayAccess.py b/modules/interfaces/gatewayAccess.py
index bf93d58f..95e27a2e 100644
--- a/modules/interfaces/gatewayAccess.py
+++ b/modules/interfaces/gatewayAccess.py
@@ -5,7 +5,7 @@ Manages user access and permissions.
from typing import Dict, Any, List, Optional
-def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]], mandateId: int, userId: int, db) -> List[Dict[str, Any]]:
+def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]], _mandateId: int, _userId: int, db) -> List[Dict[str, Any]]:
"""
Unified user access management function that filters data based on user privileges
and adds access control attributes.
@@ -14,8 +14,8 @@ def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]
currentUser: Current user information dictionary
table: Name of the table
recordset: Recordset to filter based on access rules
- mandateId: Current mandate ID
- userId: Current user ID
+ _mandateId: Current mandate ID
+ _userId: Current user ID
db: Database connector instance
Returns:
@@ -29,11 +29,11 @@ def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]
filtered_records = recordset # System admins see all records
elif userPrivilege == "admin":
# Admins see records in their mandate
- filtered_records = [r for r in recordset if r.get("mandateId") == mandateId]
+ filtered_records = [r for r in recordset if r.get("_mandateId") == _mandateId]
else: # Regular users
# Users only see records they own within their mandate
filtered_records = [r for r in recordset
- if r.get("mandateId") == mandateId and r.get("userId") == userId]
+ if r.get("_mandateId") == _mandateId and r.get("_userId") == _userId]
# Add access control attributes to each record
for record in filtered_records:
@@ -42,21 +42,21 @@ def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]
# Set access control flags based on user permissions
if table == "mandates":
record["_hideView"] = False # Everyone can view
- record["_hideEdit"] = not _canModify(currentUser, "mandates", record_id, mandateId, userId, db)
- record["_hideDelete"] = not _canModify(currentUser, "mandates", record_id, mandateId, userId, db)
+ record["_hideEdit"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db)
+ record["_hideDelete"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db)
elif table == "users":
record["_hideView"] = False # Everyone can view
- record["_hideEdit"] = not _canModify(currentUser, "users", record_id, mandateId, userId, db)
- record["_hideDelete"] = not _canModify(currentUser, "users", record_id, mandateId, userId, db)
+ record["_hideEdit"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db)
+ record["_hideDelete"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db)
else:
# Default access control for other tables
record["_hideView"] = False
- record["_hideEdit"] = not _canModify(currentUser, table, record_id, mandateId, userId, db)
- record["_hideDelete"] = not _canModify(currentUser, table, record_id, mandateId, userId, db)
+ record["_hideEdit"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db)
+ record["_hideDelete"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db)
return filtered_records
-def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int] = None, mandateId: int = None, userId: int = None, db = None) -> bool:
+def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int] = None, _mandateId: int = None, _userId: int = None, db = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
@@ -64,8 +64,8 @@ def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int]
currentUser: Current user information dictionary
table: Name of the table
recordId: Optional record ID for specific record check
- mandateId: Current mandate ID
- userId: Current user ID
+ _mandateId: Current mandate ID
+ _userId: Current user ID
db: Database connector instance
Returns:
@@ -87,15 +87,15 @@ def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int]
record = records[0]
# Admins can modify anything in their mandate
- if userPrivilege == "admin" and record.get("mandateId") == mandateId:
+ if userPrivilege == "admin" and record.get("_mandateId") == _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") == mandateId and
- record.get("userId") == userId):
+ if (record.get("_mandateId") == _mandateId and
+ record.get("_userId") == _userId):
return True
return False
diff --git a/modules/interfaces/gatewayInterface.py b/modules/interfaces/gatewayInterface.py
index 6ab523d4..7ed117b3 100644
--- a/modules/interfaces/gatewayInterface.py
+++ b/modules/interfaces/gatewayInterface.py
@@ -25,11 +25,11 @@ class GatewayInterface:
Manages users and mandates.
"""
- def __init__(self, mandateId: int = None, userId: int = None):
+ def __init__(self, _mandateId: str = None, _userId: str = None):
"""Initializes the Gateway Interface with optional mandate and user context."""
# Context can be empty during initialization
- self.mandateId = mandateId
- self.userId = userId
+ self._mandateId = _mandateId
+ self._userId = _userId
# Initialize database
self._initializeDatabase()
@@ -40,41 +40,47 @@ class GatewayInterface:
# 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 connection."""
+ # Get configuration values with defaults
+ dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data")
+ dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway")
+ dbUser = APP_CONFIG.get("DB_GATEWAY_USER")
+ dbPassword = APP_CONFIG.get("DB_GATEWAY_PASSWORD_SECRET")
+
+ # Ensure the database directory exists
+ os.makedirs(dbHost, exist_ok=True)
+
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 if self.mandateId else 0,
- userId=self.userId if self.userId else 0
+ dbHost=dbHost,
+ dbDatabase=dbDatabase,
+ dbUser=dbUser,
+ dbPassword=dbPassword,
+ _mandateId=self._mandateId,
+ _userId=self._userId
)
-
+
+ def _getCurrentUserInfo(self) -> Optional[Dict[str, Any]]:
+ """Returns information about the current user."""
+ if not self._userId:
+ return None
+
+ users = self.db.getRecordset("users", recordFilter={"id": self._userId})
+ if users:
+ return users[0]
+ return None
+
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
self._initRootMandate()
self._initAdminUser()
+
+ # Update database context with new IDs
+ if self._mandateId and self._userId:
+ self.db.updateContext(self._mandateId, self._userId)
+
+ # Reload user information with new context
+ self.currentUser = self._getCurrentUserInfo()
def _initRootMandate(self):
"""Creates the Root mandate if it doesn't exist."""
@@ -89,8 +95,11 @@ class GatewayInterface:
createdMandate = self.db.recordCreate("mandates", rootMandate)
logger.info(f"Root mandate created with ID {createdMandate['id']}")
+ # Register the initial ID
+ self.db._registerInitialId("mandates", createdMandate['id'])
+
# Update mandate context
- self.mandateId = createdMandate['id']
+ self._mandateId = createdMandate['id']
def _initAdminUser(self):
"""Creates the Admin user if it doesn't exist."""
@@ -99,7 +108,7 @@ class GatewayInterface:
if existingUserId is None or not users:
logger.info("Creating Admin user")
adminUser = {
- "mandateId": self.mandateId,
+ "_mandateId": self._mandateId,
"username": "admin",
"email": "admin@example.com",
"fullName": "Administrator",
@@ -111,8 +120,11 @@ class GatewayInterface:
createdUser = self.db.recordCreate("users", adminUser)
logger.info(f"Admin user created with ID {createdUser['id']}")
+ # Register the initial ID
+ self.db._registerInitialId("users", createdUser['id'])
+
# Update user context
- self.userId = createdUser['id']
+ self._userId = createdUser['id']
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
@@ -126,9 +138,9 @@ class GatewayInterface:
Returns:
Filtered recordset with access control attributes
"""
- return _uam(self.currentUser, table, recordset, self.mandateId, self.userId, self.db)
+ return _uam(self.currentUser, table, recordset, self._mandateId, self._userId, self.db)
- def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
+ def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
@@ -139,9 +151,9 @@ class GatewayInterface:
Returns:
Boolean indicating permission
"""
- return _canModify(self.currentUser, table, recordId, self.mandateId, self.userId, self.db)
+ return _canModify(self.currentUser, table, recordId, self._mandateId, self._userId, self.db)
- def getInitialId(self, table: str) -> Optional[int]:
+ def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
@@ -165,7 +177,7 @@ class GatewayInterface:
allMandates = self.db.getRecordset("mandates")
return self._uam("mandates", allMandates)
- def getMandate(self, mandateId: int) -> Optional[Dict[str, Any]]:
+ def getMandate(self, mandateId: str) -> Optional[Dict[str, Any]]:
"""Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
if not mandates:
@@ -186,7 +198,7 @@ class GatewayInterface:
return self.db.recordCreate("mandates", mandateData)
- def updateMandate(self, mandateId: int, mandateData: Dict[str, Any]) -> Dict[str, Any]:
+ def updateMandate(self, mandateId: str, mandateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a mandate if user has access."""
# Check if the mandate exists and user has access
mandate = self.getMandate(mandateId)
@@ -199,7 +211,7 @@ class GatewayInterface:
# Update the mandate
return self.db.recordModify("mandates", mandateId, mandateData)
- def deleteMandate(self, mandateId: int) -> bool:
+ def deleteMandate(self, mandateId: str) -> bool:
"""
Deletes a mandate and all associated users and data if user has permission.
"""
@@ -248,15 +260,15 @@ class GatewayInterface:
return filteredUsers
- def getUsersByMandate(self, mandateId: int) -> List[Dict[str, Any]]:
+ def getUsersByMandate(self, _mandateId: str) -> List[Dict[str, Any]]:
"""Returns users for a specific mandate if user has access."""
# First check if user has access to the mandate
- mandate = self.getMandate(mandateId)
+ mandate = self.getMandate(_mandateId)
if not mandate:
return []
# Get users for this mandate
- users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId})
+ users = self.db.getRecordset("users", recordFilter={"_mandateId": _mandateId})
filteredUsers = self._uam("users", users)
# Remove password hashes
@@ -268,6 +280,7 @@ class GatewayInterface:
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
"""Returns a user by username."""
+ # Get all users without mandate filter
users = self.db.getRecordset("users")
for user in users:
if user.get("username") == username:
@@ -278,9 +291,9 @@ class GatewayInterface:
logger.debug(f"No user found with username {username}")
return None
- def getUser(self, userId: int) -> Optional[Dict[str, Any]]:
+ def getUser(self, _userId: str) -> Optional[Dict[str, Any]]:
"""Returns a user by ID if user has access."""
- users = self.db.getRecordset("users", recordFilter={"id": userId})
+ users = self.db.getRecordset("users", recordFilter={"_userId": _userId})
if not users:
return None
@@ -299,31 +312,58 @@ class GatewayInterface:
return user
def createUser(self, username: str, password: str, email: str = None,
- fullName: str = None, language: str = "de", mandateId: int = None,
+ fullName: str = None, language: str = "de", _mandateId: str = None,
disabled: bool = False, privilege: str = "user") -> Dict[str, Any]:
"""Creates a new user if current user has permission."""
+ # Validate username
+ if not username or len(username) < 3:
+ raise ValueError("Benutzername muss mindestens 3 Zeichen lang sein")
+
+ # Validate password
+ if not password:
+ raise ValueError("Passwort ist erforderlich")
+
+ # Password requirements
+ if len(password) < 8:
+ raise ValueError("Passwort muss mindestens 8 Zeichen lang sein")
+ if not any(c.isupper() for c in password):
+ raise ValueError("Passwort muss mindestens einen Grossbuchstaben enthalten")
+ if not any(c.islower() for c in password):
+ raise ValueError("Passwort muss mindestens einen Kleinbuchstaben enthalten")
+ if not any(c.isdigit() for c in password):
+ raise ValueError("Passwort muss mindestens eine Zahl enthalten")
+ if not any(c in "!@#$%^&*(),.?\":{}|<>" for c in password):
+ raise ValueError("Passwort muss mindestens ein Sonderzeichen enthalten")
+
+ # Validate email if provided
+ if email:
+ import re
+ email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
+ if not re.match(email_pattern, email):
+ raise ValueError("Ungültiges E-Mail-Format")
+
# Check if the username already exists
existingUser = self.getUserByUsername(username)
if existingUser:
- raise ValueError(f"User '{username}' already exists")
+ raise ValueError(f"Benutzer '{username}' existiert bereits")
- # Use the provided mandateId or the current context
- userMandateId = mandateId if mandateId is not None else self.mandateId
+ # 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 userMandateId != self._mandateId and self.currentUser.get("privilege") != "sysadmin":
+ raise PermissionError(f"Keine Berechtigung, Benutzer in Mandat {userMandateId} zu erstellen")
if not self._canModify("users"):
- raise PermissionError("No permission to create users")
+ raise PermissionError("Keine Berechtigung, Benutzer zu erstellen")
# 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}")
+ raise PermissionError(f"Keine Berechtigung, Benutzer mit höherem Privileg zu erstellen: {privilege}")
userData = {
- "mandateId": userMandateId,
+ "_mandateId": userMandateId,
"username": username,
"email": email,
"fullName": fullName,
@@ -335,6 +375,10 @@ class GatewayInterface:
createdUser = self.db.recordCreate("users", userData)
+ # Clear the users table from cache to ensure fresh data
+ if "users" in self.db._tablesCache:
+ del self.db._tablesCache["users"]
+
# Return the complete user record
return createdUser
@@ -344,9 +388,8 @@ class GatewayInterface:
if "users" in self.db._tablesCache:
del self.db._tablesCache["users"]
- # Get fresh user data
- users = self.db.getRecordset("users")
- user = next((u for u in users if u.get("username") == username), None)
+ # Get user by username
+ user = self.getUserByUsername(username)
if not user:
raise ValueError("Benutzer nicht gefunden")
@@ -366,19 +409,19 @@ class GatewayInterface:
return authenticatedUser
- def updateUser(self, userId: int, userData: Dict[str, Any]) -> Dict[str, Any]:
+ def updateUser(self, _userId: str, userData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a user if current user has permission."""
# Check if the user exists and current user has access
- user = self.getUser(userId)
+ 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})
+ users = self.db.getRecordset("users", recordFilter={"_userId": _userId})
if not users:
- raise ValueError(f"User with ID {userId} not found")
+ 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}")
+ if not self._canModify("users", _userId):
+ raise PermissionError(f"No permission to update user {_userId}")
user = users[0]
@@ -397,7 +440,7 @@ class GatewayInterface:
del userData["password"]
# Update the user
- updatedUser = self.db.recordModify("users", userId, userData)
+ updatedUser = self.db.recordModify("users", _userId, userData)
# Remove password hash from the response
if "hashedPassword" in updatedUser:
@@ -405,53 +448,53 @@ class GatewayInterface:
return updatedUser
- def disableUser(self, userId: int) -> Dict[str, Any]:
+ def disableUser(self, _userId: str) -> Dict[str, Any]:
"""Disables a user if current user has permission."""
- return self.updateUser(userId, {"disabled": True})
+ return self.updateUser(_userId, {"disabled": True})
- def enableUser(self, userId: int) -> Dict[str, Any]:
+ def enableUser(self, _userId: str) -> Dict[str, Any]:
"""Enables a user if current user has permission."""
- return self.updateUser(userId, {"disabled": False})
+ return self.updateUser(_userId, {"disabled": False})
- def _deleteUserReferencedData(self, userId: int) -> None:
+ def _deleteUserReferencedData(self, _userId: str) -> None:
"""Deletes all data associated with a user."""
# Delete user attributes
try:
- attributes = self.db.getRecordset("attributes", recordFilter={"userId": userId})
+ attributes = self.db.getRecordset("attributes", recordFilter={"_userId": _userId})
for attribute in attributes:
self.db.recordDelete("attributes", attribute["id"])
except Exception as e:
- logger.error(f"Error deleting attributes for user {userId}: {e}")
+ logger.error(f"Error deleting attributes for user {_userId}: {e}")
- logger.info(f"All referenced data for user {userId} has been deleted")
+ logger.info(f"All referenced data for user {_userId} has been deleted")
- def deleteUser(self, userId: int) -> bool:
+ def deleteUser(self, _userId: str) -> bool:
"""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})
+ users = self.db.getRecordset("users", recordFilter={"_userId": _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}")
+ 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")
- if initialUserId is not None and userId == initialUserId:
+ if initialUserId is not None and _userId == initialUserId:
logger.warning("Attempt to delete the Root Admin was prevented")
return False
# Delete all data associated with the user
- self._deleteUserReferencedData(userId)
+ self._deleteUserReferencedData(_userId)
# Delete the user
- success = self.db.recordDelete("users", userId)
+ success = self.db.recordDelete("users", _userId)
if success:
- logger.info(f"User with ID {userId} was successfully deleted")
+ logger.info(f"User with ID {_userId} was successfully deleted")
else:
- logger.error(f"Error deleting user with ID {userId}")
+ logger.error(f"Error deleting user with ID {_userId}")
return success
@@ -459,15 +502,16 @@ class GatewayInterface:
# Singleton factory for GatewayInterface instances per context
_gatewayInterfaces = {}
-def getGatewayInterface(mandateId: int = None, userId: int = None) -> GatewayInterface:
+def getGatewayInterface(_mandateId: str = None, _userId: str = None) -> GatewayInterface:
"""
Returns a GatewayInterface instance for the specified context.
Reuses existing instances.
"""
- contextKey = f"{mandateId}_{userId}"
+ # For initialization, use empty strings instead of None
+ contextKey = f"{_mandateId or ''}_{_userId or ''}"
if contextKey not in _gatewayInterfaces:
- _gatewayInterfaces[contextKey] = GatewayInterface(mandateId, userId)
+ _gatewayInterfaces[contextKey] = GatewayInterface(_mandateId or '', _userId or '')
return _gatewayInterfaces[contextKey]
-# Initialize an instance
-getGatewayInterface()
\ No newline at end of file
+# Initialize an instance with empty strings
+getGatewayInterface('', '')
\ No newline at end of file
diff --git a/modules/interfaces/gatewayModel.py b/modules/interfaces/gatewayModel.py
index 83d759c7..86f371bc 100644
--- a/modules/interfaces/gatewayModel.py
+++ b/modules/interfaces/gatewayModel.py
@@ -4,6 +4,7 @@ Data models for the gateway system.
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
+import uuid
class Label(BaseModel):
@@ -20,7 +21,7 @@ class Label(BaseModel):
class Mandate(BaseModel):
"""Data model for a mandate"""
- id: int = Field(description="Unique ID of the mandate")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
name: str = Field(description="Name of the mandate")
language: str = Field(description="Default language of the mandate")
@@ -38,8 +39,7 @@ class Mandate(BaseModel):
class User(BaseModel):
"""Data model for a user"""
- id: int = Field(description="Unique ID of the user")
- mandateId: int = Field(description="ID of the associated mandate")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
username: str = Field(description="Username for login")
email: Optional[str] = Field(None, description="Email address of the user")
fullName: Optional[str] = Field(None, description="Full name of the user")
@@ -99,5 +99,5 @@ class Token(BaseModel):
class TokenData(BaseModel):
"""Data for token decoding and validation"""
username: Optional[str] = None
- mandateId: Optional[int] = None
+ mandateId: Optional[str] = None
exp: Optional[datetime] = None
\ No newline at end of file
diff --git a/modules/interfaces/lucydomAccess.py b/modules/interfaces/lucydomAccess.py
index 2c018c52..0db95a88 100644
--- a/modules/interfaces/lucydomAccess.py
+++ b/modules/interfaces/lucydomAccess.py
@@ -11,11 +11,11 @@ class LucyDOMAccess:
Handles user access management and permission checks.
"""
- def __init__(self, currentUser: Dict[str, Any], mandateId: int, userId: int):
+ def __init__(self, currentUser: Dict[str, Any], _mandateId: int, _userId: int):
"""Initialize with user context."""
self.currentUser = currentUser
- self.mandateId = mandateId
- self.userId = userId
+ self._mandateId = _mandateId
+ self._userId = _userId
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
@@ -37,19 +37,15 @@ class LucyDOMAccess:
filtered_records = recordset # System admins see all records
elif userPrivilege == "admin":
# Admins see records in their mandate
- filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
+ filtered_records = [r for r in recordset if r.get("_mandateId") == self._mandateId]
else: # Regular users
- # To see all prompts from mandate 0 and own
+ # For prompts, users can see all prompts from their mandate
if table == "prompts":
- filtered_records = [r for r in recordset if
- (r.get("mandateId") == self.mandateId and r.get("userId") == self.userId)
- or
- (r.get("mandateId") == 0)
- ]
+ filtered_records = [r for r in recordset if r.get("_mandateId") == self._mandateId]
else:
- # Users see only their records
+ # Users see only their records for other tables
filtered_records = [r for r in recordset
- if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId]
+ if r.get("_mandateId") == self._mandateId and r.get("_userId") == self._userId]
# Add access control attributes to each record
for record in filtered_records:
@@ -58,8 +54,14 @@ class LucyDOMAccess:
# Set access control flags based on user permissions
if table == "prompts":
record["_hideView"] = False # Everyone can view
- record["_hideEdit"] = not self._canModify("prompts", record_id)
- record["_hideDelete"] = not self._canModify("prompts", record_id)
+ # Only allow modification of own prompts or if admin/sysadmin
+ can_modify = (
+ userPrivilege == "sysadmin" or
+ (userPrivilege == "admin" and record.get("_mandateId") == self._mandateId) or
+ (record.get("_mandateId") == self._mandateId and record.get("_userId") == self._userId)
+ )
+ record["_hideEdit"] = not can_modify
+ record["_hideDelete"] = not can_modify
elif table == "files":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("files", record_id)
@@ -112,12 +114,12 @@ class LucyDOMAccess:
record = records[0]
# Admins can modify anything in their mandate
- if userPrivilege == "admin" and record.get("mandateId") == self.mandateId:
+ 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):
+ if (record.get("_mandateId") == self._mandateId and
+ record.get("_userId") == self._userId):
return True
return False
diff --git a/modules/interfaces/lucydomInterface.py b/modules/interfaces/lucydomInterface.py
index 21214117..0b745ab3 100644
--- a/modules/interfaces/lucydomInterface.py
+++ b/modules/interfaces/lucydomInterface.py
@@ -24,6 +24,24 @@ from modules.connectors.connectorAiOpenai import ChatService
from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__)
+# Initialize AI service at module level
+_aiService = None
+
+def initializeAIService():
+ """Initialize the AI service for the LucyDOM interface."""
+ global _aiService
+ if _aiService is None:
+ try:
+ _aiService = ChatService()
+ logger.info("AI service initialized successfully")
+ except Exception as e:
+ logger.error(f"Failed to initialize AI service: {str(e)}")
+ _aiService = None
+ return _aiService
+
+# Initialize AI service when module is imported
+initializeAIService()
+
# Custom exceptions for file handling
class FileError(Exception):
"""Base class for file handling exceptions."""
@@ -45,6 +63,7 @@ class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file."""
pass
+from modules.security.auth import getInitialContext
class LucyDOMInterface:
"""
@@ -52,14 +71,19 @@ class LucyDOMInterface:
Uses the JSON connector for data access.
"""
- def __init__(self, mandateId: int, userId: int):
+ def __init__(self, _mandateId: str, _userId: str):
"""Initializes the LucyDOM Interface with mandate and user context."""
- self.mandateId = mandateId
- self.userId = userId
+ logger.debug(f"Initializing LucyDOMInterface with mandateId={_mandateId}, userId={_userId}")
+ self._mandateId = _mandateId
+ self._userId = _userId
# Add language settings
self.userLanguage = "en" # Default user language
- self.aiService = None # Will be set externally
+
+ # Set AI service from module-level instance
+ self.aiService = _aiService
+ if not self.aiService:
+ logger.warning("AI service not available during LucyDOMInterface initialization")
# Initialize database connector
self._initializeDatabase()
@@ -68,10 +92,22 @@ class LucyDOMInterface:
self.currentUser = self._getCurrentUserInfo()
# Initialize access control
- self.access = LucyDOMAccess(self.currentUser, self.mandateId, self.userId)
+ self.access = LucyDOMAccess(self.currentUser, self._mandateId, self._userId)
self.access.db = self.db # Share database connection
- # Initialize standard database records if needed
+ # Get initial IDs if not provided
+ if not self._mandateId or not self._userId:
+ logger.debug("No context provided, getting initial context from auth")
+ self._mandateId, self._userId = getInitialContext()
+ logger.debug(f"Retrieved initial context: mandate={self._mandateId}, user={self._userId}")
+
+ if self._mandateId and self._userId:
+ self.db.updateContext(self._mandateId, self._userId)
+ logger.debug(f"Updated database context with initial IDs")
+ else:
+ logger.warning("No initial context available from auth")
+
+ # Initialize standard records if needed
self._initRecords()
def _getCurrentUserInfo(self) -> Dict[str, Any]:
@@ -79,74 +115,64 @@ class LucyDOMInterface:
# For production, you would get this from authentication
# For now return basic user info with default privilege
return {
- "id": self.userId,
- "mandateId": self.mandateId,
+ "id": self._userId,
+ "_mandateId": self._mandateId,
"privilege": "user", # Default privilege level
"language": self.userLanguage
}
def _initializeDatabase(self):
"""Initializes the database connection."""
- effectiveMandateId = self.mandateId
- effectiveUserId = self.userId
- if effectiveMandateId is None or effectiveUserId is None:
- 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
+ _mandateId=self._mandateId,
+ _userId=self._userId,
+ skipInitialIdLookup=True
)
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
- self._initializeStandardPrompts()
+ # Only initialize prompts if we have valid context
+ if self._mandateId and self._userId:
+ logger.debug(f"Initializing prompts with context: mandate={self._mandateId}, user={self._userId}")
+ self._initializeStandardPrompts()
+ else:
+ logger.warning("Skipping prompt initialization - no valid context available")
def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist."""
prompts = self.db.getRecordset("prompts")
+ logger.debug(f"Found {len(prompts)} existing prompts")
+
if not prompts:
- logger.info("Creating standard prompts")
+ logger.debug("Creating standard prompts")
# Define standard prompts
standardPrompts = [
{
- "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": 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": 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": 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": self.mandateId,
- "userId": self.userId,
"content": "Gib mir die ersten 1000 Primzahlen",
"name": "Code: Primzahlen"
},
{
- "mandateId": self.mandateId,
- "userId": self.userId,
"content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
"name": "Mail: Vorbereitung"
},
@@ -155,13 +181,15 @@ class LucyDOMInterface:
# 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']}")
+ logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('_mandateId')}, user={createdPrompt.get('_userId')}")
+ else:
+ logger.debug("Prompts already exist, skipping creation")
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module."""
return self.access._uam(table, recordset)
- def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
+ def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module."""
return self.access._canModify(table, recordId)
@@ -170,7 +198,7 @@ class LucyDOMInterface:
def setUserLanguage(self, languageCode: str):
"""Set the user's preferred language"""
self.userLanguage = languageCode
- logger.info(f"User language set to: {languageCode}")
+ logger.debug(f"User language set to: {languageCode}")
# AI Call Root Function
@@ -208,7 +236,7 @@ class LucyDOMInterface:
# Utilities
- def getInitialId(self, table: str) -> Optional[int]:
+ def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
@@ -223,7 +251,7 @@ class LucyDOMInterface:
allPrompts = self.db.getRecordset("prompts")
return self._uam("prompts", allPrompts)
- def getPrompt(self, promptId: int) -> Optional[Dict[str, Any]]:
+ def getPrompt(self, promptId: str) -> Optional[Dict[str, Any]]:
"""Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
if not prompts:
@@ -238,8 +266,6 @@ class LucyDOMInterface:
raise PermissionError("No permission to create prompts")
promptData = {
- "mandateId": self.mandateId,
- "userId": self.userId,
"content": content,
"name": name,
"createdAt": self._getCurrentTimestamp()
@@ -247,7 +273,7 @@ class LucyDOMInterface:
return self.db.recordCreate("prompts", promptData)
- def updatePrompt(self, promptId: int, content: str = None, name: str = None) -> Dict[str, Any]:
+ def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Dict[str, Any]:
"""Updates a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
@@ -268,7 +294,7 @@ class LucyDOMInterface:
# Update prompt
return self.db.recordModify("prompts", promptId, promptData)
- def deletePrompt(self, promptId: int) -> bool:
+ def deletePrompt(self, promptId: str) -> bool:
"""Deletes a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
@@ -290,8 +316,8 @@ class LucyDOMInterface:
"""Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash,
- "mandateId": self.mandateId,
- "userId": self.userId
+ "_mandateId": self._mandateId,
+ "_userId": self._userId
})
if files:
return files[0]
@@ -334,7 +360,7 @@ class LucyDOMInterface:
allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles)
- def getFile(self, fileId: int) -> Optional[Dict[str, Any]]:
+ def getFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files:
@@ -349,8 +375,8 @@ class LucyDOMInterface:
raise PermissionError("No permission to create files")
fileData = {
- "mandateId": self.mandateId,
- "userId": self.userId,
+ "_mandateId": self._mandateId,
+ "_userId": self._userId,
"name": name,
"mimeType": mimeType,
"size": size,
@@ -359,7 +385,7 @@ class LucyDOMInterface:
}
return self.db.recordCreate("files", fileData)
- def updateFile(self, fileId: int, updateData: Dict[str, Any]) -> Dict[str, Any]:
+ def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates file metadata if user has access."""
# Check if the file exists and user has access
file = self.getFile(fileId)
@@ -372,7 +398,7 @@ class LucyDOMInterface:
# Update file
return self.db.recordModify("files", fileId, updateData)
- def deleteFile(self, fileId: int) -> bool:
+ def deleteFile(self, fileId: str) -> bool:
"""Deletes a file if user has access."""
try:
# Check if the file exists and user has access
@@ -396,7 +422,7 @@ class LucyDOMInterface:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries:
self.db.recordDelete("fileData", fileId)
- logger.info(f"FileData for file {fileId} deleted")
+ logger.debug(f"FileData for file {fileId} deleted")
except Exception as e:
logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}")
@@ -413,7 +439,7 @@ class LucyDOMInterface:
# FileData methods - data operations
- def createFileData(self, fileId: int, data: bytes) -> bool:
+ def createFileData(self, fileId: str, data: bytes) -> bool:
"""Stores the binary data of a file in the database."""
try:
import base64
@@ -459,13 +485,13 @@ class LucyDOMInterface:
}
self.db.recordCreate("fileData", fileDataObj)
- logger.info(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
+ logger.debug(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error storing data for file {fileId}: {str(e)}")
return False
- def getFileData(self, fileId: int) -> Optional[bytes]:
+ def getFileData(self, fileId: str) -> Optional[bytes]:
"""Returns the binary data of a file if user has access."""
# Check file access
file = self.getFile(fileId)
@@ -499,7 +525,7 @@ class LucyDOMInterface:
logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None
- def updateFileData(self, fileId: int, data: Union[bytes, str]) -> bool:
+ def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool:
"""Updates file data if user has access."""
# Check file access
file = self.getFile(fileId)
@@ -573,12 +599,12 @@ class LucyDOMInterface:
if fileDataEntries:
# Update the existing record
self.db.recordModify("fileData", fileId, dataUpdate)
- logger.info(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
+ logger.debug(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
else:
# Create a new record
dataUpdate["id"] = fileId
self.db.recordCreate("fileData", dataUpdate)
- logger.info(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
+ logger.debug(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
@@ -592,7 +618,7 @@ class LucyDOMInterface:
if not self._canModify("files"):
raise PermissionError("No permission to upload files")
- logger.info(f"Starting upload process for file: {fileName}")
+ logger.debug(f"Starting upload process for file: {fileName}")
if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}")
@@ -605,7 +631,7 @@ class LucyDOMInterface:
# Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
- logger.info(f"Duplicate found for {fileName}: {existingFile['id']}")
+ logger.debug(f"Duplicate found for {fileName}: {existingFile['id']}")
return existingFile
# Determine MIME type and size
@@ -613,7 +639,7 @@ class LucyDOMInterface:
fileSize = len(fileContent)
# Save metadata
- logger.info(f"Saving file metadata to database for file: {fileName}")
+ logger.debug(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile(
name=fileName,
mimeType=mimeType,
@@ -622,17 +648,17 @@ class LucyDOMInterface:
)
# Save binary data
- logger.info(f"Saving file content to database for file: {fileName}")
+ logger.debug(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
- logger.info(f"File upload process completed for: {fileName}")
+ logger.debug(f"File upload process completed for: {fileName}")
return dbFile
except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
raise FileStorageError(f"Error saving file: {str(e)}")
- def downloadFile(self, fileId: int) -> Optional[Dict[str, Any]]:
+ def downloadFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file for download if user has access."""
try:
# Check file access
@@ -667,10 +693,10 @@ class LucyDOMInterface:
allWorkflows = self.db.getRecordset("workflows")
return self._uam("workflows", allWorkflows)
- def getWorkflowsByUser(self, userId: int) -> List[Dict[str, Any]]:
+ def getWorkflowsByUser(self, _userId: str) -> List[Dict[str, Any]]:
"""Returns workflows for a specific user if current user has access."""
- # Get workflows by userId
- workflows = self.db.getRecordset("workflows", recordFilter={"userId": userId})
+ # Get workflows by _userId
+ workflows = self.db.getRecordset("workflows", recordFilter={"_userId": _userId})
# Apply access control
return self._uam("workflows", workflows)
@@ -690,11 +716,11 @@ class LucyDOMInterface:
raise PermissionError("No permission to create workflows")
# Make sure mandateId and userId are set
- if "mandateId" not in workflowData:
- workflowData["mandateId"] = self.mandateId
+ if "_mandateId" not in workflowData:
+ workflowData["_mandateId"] = self._mandateId
- if "userId" not in workflowData:
- workflowData["userId"] = self.userId
+ if "_userId" not in workflowData:
+ workflowData["_userId"] = self._userId
# Set timestamp if not present
currentTime = self._getCurrentTimestamp()
@@ -877,7 +903,7 @@ class LucyDOMInterface:
# Update the message
updatedMessage = self.db.recordModify("workflowMessages", messageId, messageData)
if updatedMessage:
- logger.info(f"Message {messageId} updated successfully")
+ logger.debug(f"Message {messageId} updated successfully")
else:
logger.warning(f"Failed to update message {messageId}")
@@ -912,7 +938,7 @@ class LucyDOMInterface:
logger.error(f"Error deleting message {messageId}: {str(e)}")
return False
- def deleteFileFromMessage(self, workflowId: str, messageId: str, fileId: int) -> bool:
+ def deleteFileFromMessage(self, workflowId: str, messageId: str, fileId: str) -> bool:
"""Removes a file reference from a message if user has access."""
try:
# Check workflow access
@@ -924,7 +950,7 @@ class LucyDOMInterface:
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}")
+ logger.debug(f"Removing file {fileId} from message {messageId} in workflow {workflowId}")
# Get all workflow messages
allMessages = self.getWorkflowMessages(workflowId)
@@ -951,7 +977,7 @@ class LucyDOMInterface:
return False
# Log the found message
- logger.info(f"Found message: {message.get('id')}")
+ logger.debug(f"Found message: {message.get('id')}")
# Check if message has documents
if "documents" not in message or not message["documents"]:
@@ -980,7 +1006,7 @@ class LucyDOMInterface:
if shouldRemove:
removed = True
- logger.info(f"Found file to remove: docId={docId}, fileId={fileIdValue}")
+ logger.debug(f"Found file to remove: docId={docId}, fileId={fileIdValue}")
else:
updatedDocuments.append(doc)
@@ -997,7 +1023,7 @@ class LucyDOMInterface:
updated = self.db.recordModify("workflowMessages", message["id"], messageUpdate)
if updated:
- logger.info(f"Successfully removed file {fileId} from message {messageId}")
+ logger.debug(f"Successfully removed file {fileId} from message {messageId}")
return True
else:
logger.warning(f"Failed to update message {messageId} in database")
@@ -1081,8 +1107,8 @@ class LucyDOMInterface:
# Extract only the database-relevant workflow fields
workflowDbData = {
"id": workflowId,
- "mandateId": workflow.get("mandateId", self.mandateId),
- "userId": workflow.get("userId", self.userId),
+ "_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()),
@@ -1187,13 +1213,13 @@ class LucyDOMInterface:
messageIds = [msg.get("id") for msg in messages]
# Update in database
self.updateWorkflow(workflowId, {"messageIds": messageIds})
- logger.info(f"Rebuilt messageIds for workflow {workflowId}")
+ logger.debug(f"Rebuilt messageIds for workflow {workflowId}")
# Log document counts for each message
for msg in messages:
docCount = len(msg.get("documents", []))
if docCount > 0:
- logger.info(f"Message {msg.get('id')} has {docCount} documents loaded from database")
+ logger.debug(f"Message {msg.get('id')} has {docCount} documents loaded from database")
# Load logs
logs = self.getWorkflowLogs(workflowId)
@@ -1218,16 +1244,16 @@ class LucyDOMInterface:
try:
# Get token from database using current user's mandateId and userId
tokens = self.db.getRecordset("msftTokens", recordFilter={
- "mandateId": self.mandateId,
- "userId": self.userId
+ "_mandateId": self._mandateId,
+ "_userId": self._userId
})
if tokens and len(tokens) > 0:
token_data = json.loads(tokens[0]["token_data"])
- logger.info(f"Retrieved Microsoft token for user {self.userId}")
+ logger.debug(f"Retrieved Microsoft token for user {self._userId}")
return token_data
else:
- logger.info(f"No Microsoft token found for user {self.userId}")
+ logger.debug(f"No Microsoft token found for user {self._userId}")
return None
except Exception as e:
@@ -1239,8 +1265,8 @@ class LucyDOMInterface:
try:
# Check if token already exists
tokens = self.db.getRecordset("msftTokens", recordFilter={
- "mandateId": self.mandateId,
- "userId": self.userId
+ "_mandateId": self._mandateId,
+ "_userId": self._userId
})
if tokens and len(tokens) > 0:
@@ -1251,18 +1277,18 @@ class LucyDOMInterface:
"updated_at": datetime.now().isoformat()
}
self.db.recordModify("msftTokens", token_id, updated_data)
- logger.info(f"Updated Microsoft token for user {self.userId}")
+ logger.debug(f"Updated Microsoft token for user {self._userId}")
else:
- # Create new token
+ # Create new token with UUID
new_token = {
- "mandateId": self.mandateId,
- "userId": self.userId,
+ "_mandateId": self._mandateId,
+ "_userId": self._userId,
"token_data": json.dumps(token_data),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.db.recordCreate("msftTokens", new_token)
- logger.info(f"Saved new Microsoft token for user {self.userId}")
+ logger.debug(f"Saved new Microsoft token for user {self._userId}")
return True
@@ -1273,20 +1299,23 @@ class LucyDOMInterface:
# Singleton factory for LucyDOMInterface instances per context
_lucydomInterfaces = {}
-def getLucydomInterface(mandateId: int = 0, userId: int = 0) -> LucyDOMInterface:
+def getLucydomInterface(_mandateId: str = None, _userId: str = None) -> LucyDOMInterface:
"""
Returns a LucyDOMInterface instance for the specified context.
- Reuses existing instances.
+ Ensures AI service is initialized and preserves it across instances.
"""
- contextKey = f"{mandateId}_{userId}"
+ # For initialization, use empty strings instead of None
+ contextKey = f"{_mandateId or ''}_{_userId or ''}"
+
+ # Ensure AI service is initialized
+ if _aiService is None:
+ initializeAIService()
+
+ # Create new instance if needed
if contextKey not in _lucydomInterfaces:
- # Create new interface instance
- interface = LucyDOMInterface(mandateId, userId)
- # Initialize AI service
- aiService = ChatService()
- interface.aiService = aiService
- _lucydomInterfaces[contextKey] = interface
+ _lucydomInterfaces[contextKey] = LucyDOMInterface(_mandateId or '', _userId or '')
+
return _lucydomInterfaces[contextKey]
-# Initialize an instance
-getLucydomInterface()
\ No newline at end of file
+# Initialize default instance with empty strings
+getLucydomInterface('', '')
\ No newline at end of file
diff --git a/modules/interfaces/lucydomModel BACKUP.py b/modules/interfaces/lucydomModel BACKUP.py
new file mode 100644
index 00000000..2b731c13
--- /dev/null
+++ b/modules/interfaces/lucydomModel BACKUP.py
@@ -0,0 +1,265 @@
+"""
+LucyDOM model classes for the workflow and document system.
+"""
+
+from pydantic import BaseModel, Field
+from typing import List, Dict, Any, Optional
+from datetime import datetime
+
+
+class Label(BaseModel):
+ """Label for an attribute or a class with support for multiple languages"""
+ default: str
+ translations: Dict[str, str] = {}
+
+ def getLabel(self, language: str = None):
+ """Returns the label in the specified language, or the default value if not available"""
+ if language and language in self.translations:
+ return self.translations[language]
+ return self.default
+
+
+class Prompt(BaseModel):
+ """Data model for a prompt"""
+ id: int = Field(description="Unique ID of the prompt")
+ mandateId: int = Field(description="ID of the associated mandate")
+ userId: int = Field(description="ID of the creator")
+ content: str = Field(description="Content of the prompt")
+ name: str = Field(description="Display name of the prompt")
+
+ label: Label = Field(
+ default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
+ description="Label for the class"
+ )
+
+ # Labels for attributes
+ fieldLabels: Dict[str, Label] = {
+ "id": Label(default="ID", translations={}),
+ "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
+ "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
+ "content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
+ "name": Label(default="Name", translations={"en": "Label", "fr": "Nom"})
+ }
+
+
+class FileItem(BaseModel):
+ """Data model for a file"""
+ id: int = Field(description="Unique ID of the data object")
+ mandateId: int = Field(description="ID of the associated mandate")
+ userId: int = Field(description="ID of the creator")
+ name: str = Field(description="Name of the data object")
+ mimeType: str = Field(description="Type of the data object MIME type")
+ size: Optional[int] = Field(None, description="Size of the data object in bytes")
+ fileHash: str = Field(description="Hash code for deduplication")
+ creationDate: Optional[str] = Field(None, description="Upload date")
+ workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
+
+ label: Label = Field(
+ default=Label(default="Data Object", translations={"en": "Data Object", "fr": "Objet de données"}),
+ description="Label for the class"
+ )
+
+ # Labels for attributes
+ fieldLabels: Dict[str, Label] = {
+ "id": Label(default="ID", translations={}),
+ "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
+ "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
+ "name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
+ "mimeType": Label(default="Type", translations={"en": "Type", "fr": "Type"}),
+ "size": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
+ "fileHash": Label(default="File Hash", translations={"en": "Hash", "fr": "Hash"}),
+ "creationDate": Label(default="Upload date", translations={"en": "Upload date", "fr": "Date de téléchargement"}),
+ "workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"})
+ }
+
+class FileData(BaseModel):
+ """Data model for file content"""
+ id: int = Field(description="Unique ID of the data object")
+ data: str = Field(description="content of the file, text or base64 encoded based on base64Encoded flag")
+ base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
+
+
+class MsftToken(BaseModel):
+ """Data model for Microsoft authentication tokens"""
+ id: int = Field(description="Unique ID of the token")
+ mandateId: int = Field(description="ID of the associated mandate")
+ userId: int = Field(description="ID of the user")
+ token_data: str = Field(description="JSON string containing the token data")
+ created_at: str = Field(description="Timestamp when the token was created")
+ updated_at: str = Field(description="Timestamp when the token was last updated")
+
+ label: Label = Field(
+ default=Label(default="Microsoft Token", translations={"en": "Microsoft Token", "fr": "Jeton Microsoft"}),
+ description="Label for the class"
+ )
+
+ # Labels for attributes
+ fieldLabels: Dict[str, Label] = {
+ "id": Label(default="ID", translations={}),
+ "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
+ "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
+ "token_data": Label(default="Token Data", translations={"en": "Token Data", "fr": "Données du jeton"}),
+ "created_at": Label(default="Created At", translations={"en": "Created At", "fr": "Créé le"}),
+ "updated_at": Label(default="Updated At", translations={"en": "Updated At", "fr": "Mis à jour le"})
+ }
+
+
+# Workflow model classes
+
+class DocumentContent(BaseModel):
+ """Content of a document in the workflow"""
+ sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
+ name: str = Field(description="Designation")
+ ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
+ mimeType: str = Field(description="MIME type")
+ summary: str = Field(description="Summary of the file content")
+ data: str = Field(description="Actual content, text or base64 encoded based on base64Encoded flag")
+ base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content, such as isText flag, format information, encoding, etc.")
+
+class Document(BaseModel):
+ """Document in the workflow - References a file directly in the database"""
+ id: str = Field(description="Unique ID of the document")
+ name: str = Field(description="Name of the data object")
+ ext: str = Field(description="Extension of the data object")
+ fileId: int = Field(description="ID of the referenced file in the database")
+ mimeType: str = Field(description="MIME type")
+ data: str = Field(description="Content of the data as text or base64 encoded based on base64Encoded flag")
+ base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
+ contents: List[DocumentContent] = Field(description="Document contents")
+
+class DataStats(BaseModel):
+ """Statistics for performance and data usage"""
+ processingTime: Optional[float] = Field(None, description="Processing time in seconds")
+ tokenCount: Optional[int] = Field(None, description="Token count (for AI models)")
+ bytesSent: Optional[int] = Field(None, description="Bytes sent")
+ bytesReceived: Optional[int] = Field(None, description="Bytes received")
+
+class WorkflowMessage(BaseModel):
+ """Message object in the workflow"""
+ id: str = Field(description="Unique ID of the message")
+ workflowId: str = Field(description="Reference to the parent workflow")
+ parentMessageId: Optional[str] = Field(None, description="Reference to the replied message")
+ startedAt: str = Field(description="Timestamp for message creation")
+ finishedAt: Optional[str] = Field(None, description="Timestamp for message completion")
+ sequenceNo: int = Field(description="Sequence number for sorting")
+
+ status: str = Field(description="Status of the message ('first', 'step', 'last')")
+ role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
+
+ dataStats: Optional[DataStats] = Field(None, description="Statistics")
+ documents: Optional[List[Document]] = Field(None, description="Documents in this message (references to files in the database)")
+ content: Optional[str] = Field(None, description="Text content of the message")
+ agentName: Optional[str] = Field(None, description="Name of the agent used")
+
+class WorkflowLog(BaseModel):
+ """Log entry for a workflow"""
+ id: str = Field(description="Unique ID of the log entry")
+ workflowId: str = Field(description="ID of the associated workflow")
+ message: str = Field(description="Log message content")
+ type: str = Field(description="Type of log ('info', 'warning', 'error')")
+ timestamp: str = Field(description="Timestamp of the log entry")
+ agentName: str = Field(description="Name of the agent that created the log")
+ status: str = Field(description="Status of the workflow at log time")
+ progress: Optional[int] = Field(None, description="Progress value (0-100)")
+ mandateId: Optional[int] = Field(None, description="ID of the mandate")
+ userId: Optional[int] = Field(None, description="ID of the user")
+
+class Workflow(BaseModel):
+ """Workflow object for multi-agent system"""
+ id: str = Field(description="Unique ID of the workflow")
+ name: Optional[str] = Field(None, description="Name of the workflow")
+ mandateId: int = Field(description="ID of the mandate")
+ userId: int = Field(description="ID of the user")
+ status: str = Field(description="Status of the workflow ('running', 'completed', 'failed', 'stopped')")
+ startedAt: str = Field(description="Start timestamp")
+ lastActivity: str = Field(description="Timestamp of the last activity")
+ dataStats: Optional[Dict[str, Any]] = Field(None, description="Total statistics")
+ currentRound: int = Field(default=1, description="Current round/iteration of the workflow")
+ messageIds: List[str] = Field(default=[], description="List of message IDs in this workflow")
+
+ messages: List[WorkflowMessage] = Field(default=[], description="Message history (in-memory representation)")
+ logs: List[WorkflowLog] = Field(default=[], description="Log entries (in-memory representation)")
+
+
+# Agent and Workflow Task Models
+
+class AgentResult(BaseModel):
+ """Result structure returned by agent processing"""
+ feedback: str = Field(description="Text response explaining what the agent did")
+ documents: List[Document] = Field(default=[], description="List of document objects created by the agent")
+
+ label: Label = Field(
+ default=Label(default="Agent Result", translations={"en": "Agent Result", "fr": "Résultat d'agent"}),
+ description="Label for the class"
+ )
+
+class AgentInfo(BaseModel):
+ """Information about an agent's capabilities"""
+ name: str = Field(description="Name of the agent")
+ description: str = Field(description="Description of the agent's functionality")
+ capabilities: List[str] = Field(default=[], description="List of agent capabilities")
+
+ label: Label = Field(
+ default=Label(default="Agent Information", translations={"en": "Agent Information", "fr": "Information d'agent"}),
+ description="Label for the class"
+ )
+
+
+class InputDocument(BaseModel):
+ """Input document specification for a task"""
+ label: str = Field(description="Document label in the format 'filename.ext'")
+ fileId: Optional[int] = Field(None, description="ID of the existing document if referring to one")
+ contentPart: str = Field(default="", description="Content part to focus on, empty string for all contents")
+ prompt: str = Field(description="AI prompt to describe what data to extract from the file")
+
+class OutputDocument(BaseModel):
+ """Output document specification for a task"""
+ label: str = Field(description="Document label in the format 'filename.ext'")
+ prompt: str = Field(description="AI prompt to describe the content of the file")
+
+class TaskItem(BaseModel):
+ """Individual task in the workplan"""
+ agent: str = Field(description="Name of an available agent")
+ prompt: str = Field(description="Specific instructions to the agent, that he knows what to do with which documents and which output to provide")
+ outputDocuments: List[OutputDocument] = Field(default=[], description="List of required output documents")
+ inputDocuments: List[InputDocument] = Field(default=[], description="List of input documents to process")
+
+ label: Label = Field(
+ default=Label(default="Task Item", translations={"en": "Task Item", "fr": "Élément de tâche"}),
+ description="Label for the class"
+ )
+
+class TaskPlan(BaseModel):
+ """Work plan created by project manager"""
+ objFinalDocuments: List[str] = Field(default=[], description="List of required result documents")
+ objWorkplan: List[TaskItem] = Field(default=[], description="Plan for executing agents")
+ objUserResponse: str = Field(description="Response to the user explaining the plan")
+ userLanguage: str = Field(default="en", description="Language code of the user's request")
+
+ label: Label = Field(
+ default=Label(default="Task Plan", translations={"en": "Task Plan", "fr": "Plan de tâches"}),
+ description="Label for the class"
+ )
+
+class WorkflowStatus(BaseModel):
+ """Workflow status messages"""
+ init: str = Field(default="Workflow initialized")
+ running: str = Field(default="Running workflow")
+ waiting: str = Field(default="Waiting for input")
+ completed: str = Field(default="Workflow completed successfully")
+ stopped: str = Field(default="Workflow stopped by user")
+ failed: str = Field(default="Error in workflow")
+
+ label: Label = Field(
+ default=Label(default="Workflow Status", translations={"en": "Workflow Status", "fr": "État du workflow"}),
+ description="Label for the class"
+ )
+
+
+# Request models for the API
+
+class UserInputRequest(BaseModel):
+ """Request for user input to a running workflow"""
+ prompt: str = Field(description="Message from the user")
+ listFileId: List[int] = Field(default=[], description="List of FileItem IDs")
\ No newline at end of file
diff --git a/modules/interfaces/lucydomModel.py b/modules/interfaces/lucydomModel.py
index c7bb6d37..8c82d7bd 100644
--- a/modules/interfaces/lucydomModel.py
+++ b/modules/interfaces/lucydomModel.py
@@ -4,8 +4,12 @@ LucyDOM model classes for the workflow and document system.
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
+from datetime import datetime
+import uuid
+# CORE MODELS
+
class Label(BaseModel):
"""Label for an attribute or a class with support for multiple languages"""
default: str
@@ -20,9 +24,7 @@ class Label(BaseModel):
class Prompt(BaseModel):
"""Data model for a prompt"""
- id: int = Field(description="Unique ID of the prompt")
- mandateId: int = Field(description="ID of the associated mandate")
- userId: int = Field(description="ID of the creator")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the prompt")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Display name of the prompt")
@@ -34,23 +36,18 @@ class Prompt(BaseModel):
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
- "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
- "name": Label(default="Name", translations={"en": "Label", "fr": "Nom"}),
+ "name": Label(default="Name", translations={"en": "Label", "fr": "Nom"})
}
class FileItem(BaseModel):
"""Data model for a file"""
- id: int = Field(description="Unique ID of the data object")
- mandateId: int = Field(description="ID of the associated mandate")
- userId: int = Field(description="ID of the creator")
- name: str = Field(description="Name of the data object")
- mimeType: str = Field(description="Type of the data object MIME type")
- size: Optional[int] = Field(None, description="Size of the data object in bytes")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the data object")
+ mimeType: str = Field(description="Type of the file MIME type")
+ fileName: str = Field(description="Name of the file")
+ fileSize: int = Field(description="Size of the file in bytes")
fileHash: str = Field(description="Hash code for deduplication")
- creationDate: Optional[str] = Field(None, description="Upload date")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
label: Label = Field(
@@ -61,99 +58,88 @@ class FileItem(BaseModel):
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
- "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
- "name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"mimeType": Label(default="Type", translations={"en": "Type", "fr": "Type"}),
- "size": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
+ "fileName": Label(default="Filename", translations={"en": "fileName", "fr": "Nom de fichier"}),
+ "fileSize": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
"fileHash": Label(default="File Hash", translations={"en": "Hash", "fr": "Hash"}),
- "creationDate": Label(default="Upload date", translations={"en": "Upload date", "fr": "Date de téléchargement"}),
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"})
}
+
class FileData(BaseModel):
"""Data model for file content"""
- id: int = Field(description="Unique ID of the data object")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the data object")
data: str = Field(description="content of the file, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
+ workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
+
+
+class UserInputRequest(BaseModel):
+ """Request for user input to a running workflow"""
+ prompt: str = Field(description="Message from the user")
+ listFileId: List[str] = Field(default=[], description="List of FileItem IDs")
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for the request")
class MsftToken(BaseModel):
"""Data model for Microsoft authentication tokens"""
- id: int = Field(description="Unique ID of the token")
- mandateId: int = Field(description="ID of the associated mandate")
- userId: int = Field(description="ID of the user")
- token_data: str = Field(description="JSON string containing the token data")
- created_at: str = Field(description="Timestamp when the token was created")
- updated_at: str = Field(description="Timestamp when the token was last updated")
-
- label: Label = Field(
- default=Label(default="Microsoft Token", translations={"en": "Microsoft Token", "fr": "Jeton Microsoft"}),
- description="Label for the class"
- )
-
- # Labels for attributes
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
- "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
- "token_data": Label(default="Token Data", translations={"en": "Token Data", "fr": "Données du jeton"}),
- "created_at": Label(default="Created At", translations={"en": "Created At", "fr": "Créé le"}),
- "updated_at": Label(default="Updated At", translations={"en": "Updated At", "fr": "Mis à jour le"})
- }
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the token")
+ tokenData: str = Field(description="JSON string containing the token data")
+ expiresAt: datetime = Field(description="Expiration date and time")
+ refreshToken: Optional[str] = Field(None, description="Refresh token if available")
+ scope: str = Field(description="Token scope")
-# Workflow model classes
+# WORKFLOW MODELS
-class DocumentContent(BaseModel):
- """Content of a document in the workflow"""
+class ChatContent(BaseModel):
+ """Content of a document in the chat"""
sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
name: str = Field(description="Designation")
- ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
mimeType: str = Field(description="MIME type")
- summary: str = Field(description="Summary of the file content")
- data: str = Field(description="Actual content, text or base64 encoded based on base64Encoded flag")
- base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
- metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content, such as isText flag, format information, encoding, etc.")
+ data: str = Field(description="Actual content")
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content")
-class Document(BaseModel):
- """Document in the workflow - References a file directly in the database"""
- id: str = Field(description="Unique ID of the document")
- name: str = Field(description="Name of the data object")
- ext: str = Field(description="Extension of the data object")
- fileId: int = Field(description="ID of the referenced file in the database")
+
+class ChatDocument(BaseModel):
+ """Document in the chat workflow"""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the document")
+ fileId: str = Field(description="ID of the referenced file in the database")
+ fileName: str = Field(description="Name of the file")
+ fileSize: int = Field(description="Size of the file in bytes")
mimeType: str = Field(description="MIME type")
- data: str = Field(description="Content of the data as text or base64 encoded based on base64Encoded flag")
- base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
- contents: List[DocumentContent] = Field(description="Document contents")
+ contents: List[ChatContent] = Field(default=[], description="Document contents")
-class DataStats(BaseModel):
+
+class ChatStat(BaseModel):
"""Statistics for performance and data usage"""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the stats")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Token count (for AI models)")
bytesSent: Optional[int] = Field(None, description="Bytes sent")
bytesReceived: Optional[int] = Field(None, description="Bytes received")
-class WorkflowMessage(BaseModel):
- """Message object in the workflow"""
- id: str = Field(description="Unique ID of the message")
+
+class ChatMessage(BaseModel):
+ """Message object in the chat workflow"""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the message")
workflowId: str = Field(description="Reference to the parent workflow")
parentMessageId: Optional[str] = Field(None, description="Reference to the replied message")
- startedAt: str = Field(description="Timestamp for message creation")
- finishedAt: Optional[str] = Field(None, description="Timestamp for message completion")
- sequenceNo: int = Field(description="Sequence number for sorting")
-
- status: str = Field(description="Status of the message ('first', 'step', 'last')")
- role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
-
- dataStats: Optional[DataStats] = Field(None, description="Statistics")
- documents: Optional[List[Document]] = Field(None, description="Documents in this message (references to files in the database)")
- content: Optional[str] = Field(None, description="Text content of the message")
agentName: Optional[str] = Field(None, description="Name of the agent used")
+ documents: Optional[List[ChatDocument]] = Field(None, description="Documents in this message")
+ message: Optional[str] = Field(None, description="Text content of the message")
+ role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
+ status: str = Field(description="Status of the message ('first', 'step', 'last')")
-class WorkflowLog(BaseModel):
- """Log entry for a workflow"""
- id: str = Field(description="Unique ID of the log entry")
+ sequenceNr: int = Field(description="Sequence number for sorting")
+ startedAt: datetime = Field(description="Timestamp for message creation")
+ finishedAt: Optional[datetime] = Field(None, description="Timestamp for message completion")
+ stats: Optional[ChatStat] = Field(None, description="Statistics")
+
+
+class ChatLog(BaseModel):
+ """Log entry for a chat workflow"""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the log entry")
workflowId: str = Field(description="ID of the associated workflow")
message: str = Field(description="Log message content")
type: str = Field(description="Type of log ('info', 'warning', 'error')")
@@ -161,105 +147,50 @@ class WorkflowLog(BaseModel):
agentName: str = Field(description="Name of the agent that created the log")
status: str = Field(description="Status of the workflow at log time")
progress: Optional[int] = Field(None, description="Progress value (0-100)")
- mandateId: Optional[int] = Field(None, description="ID of the mandate")
- userId: Optional[int] = Field(None, description="ID of the user")
-class Workflow(BaseModel):
- """Workflow object for multi-agent system"""
- id: str = Field(description="Unique ID of the workflow")
- name: Optional[str] = Field(None, description="Name of the workflow")
- mandateId: int = Field(description="ID of the mandate")
- userId: int = Field(description="ID of the user")
- status: str = Field(description="Status of the workflow ('running', 'completed', 'failed', 'stopped')")
- startedAt: str = Field(description="Start timestamp")
+
+class ChatWorkflow(BaseModel):
+ """Chat workflow object for multi-agent system"""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the chat workflow")
+ status: str = Field(description="Status of the chat workflow")
+ name: Optional[str] = Field(None, description="Name of the chat workflow")
+ currentRound: int = Field(default=1, description="Current round/iteration")
lastActivity: str = Field(description="Timestamp of the last activity")
- dataStats: Optional[Dict[str, Any]] = Field(None, description="Total statistics")
- currentRound: int = Field(default=1, description="Current round/iteration of the workflow")
- messageIds: List[str] = Field(default=[], description="List of message IDs in this workflow")
-
- messages: List[WorkflowMessage] = Field(default=[], description="Message history (in-memory representation)")
- logs: List[WorkflowLog] = Field(default=[], description="Log entries (in-memory representation)")
+ startedAt: str = Field(description="Start timestamp")
+ logs: List[ChatLog] = Field(default=[], description="Log entries")
+ messages: List[ChatMessage] = Field(default=[], description="Message history")
+ stats: Optional[ChatStat] = Field(None, description="Statistics")
-# Agent and Workflow Task Models
+# AGENT AND TASK MODELS
-class AgentResult(BaseModel):
- """Result structure returned by agent processing"""
- feedback: str = Field(description="Text response explaining what the agent did")
- documents: List[Document] = Field(default=[], description="List of document objects created by the agent")
-
- label: Label = Field(
- default=Label(default="Agent Result", translations={"en": "Agent Result", "fr": "Résultat d'agent"}),
- description="Label for the class"
- )
-
-class AgentInfo(BaseModel):
- """Information about an agent's capabilities"""
+class Agent(BaseModel):
+ """Data model for an agent"""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the agent")
name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent's functionality")
capabilities: List[str] = Field(default=[], description="List of agent capabilities")
- label: Label = Field(
- default=Label(default="Agent Information", translations={"en": "Agent Information", "fr": "Information d'agent"}),
- description="Label for the class"
- )
+class AgentResponse(BaseModel):
+ """Response structure returned by agent processing"""
+ response: str = Field(description="Text response from the agent")
+ documents: List[ChatDocument] = Field(default=[], description="List of document objects created by the agent")
-class InputDocument(BaseModel):
- """Input document specification for a task"""
- label: str = Field(description="Document label in the format 'filename.ext'")
- fileId: Optional[int] = Field(None, description="ID of the existing document if referring to one")
- contentPart: str = Field(default="", description="Content part to focus on, empty string for all contents")
- prompt: str = Field(description="AI prompt to describe what data to extract from the file")
-
-class OutputDocument(BaseModel):
- """Output document specification for a task"""
- label: str = Field(description="Document label in the format 'filename.ext'")
- prompt: str = Field(description="AI prompt to describe the content of the file")
-
class TaskItem(BaseModel):
"""Individual task in the workplan"""
- agent: str = Field(description="Name of an available agent")
- prompt: str = Field(description="Specific instructions to the agent, that he knows what to do with which documents and which output to provide")
- outputDocuments: List[OutputDocument] = Field(default=[], description="List of required output documents")
- inputDocuments: List[InputDocument] = Field(default=[], description="List of input documents to process")
+ sequenceNr: int = Field(description="Sequence number of the task")
+ agentName: str = Field(description="Name of an available agent")
+ prompt: str = Field(description="Specific instructions to the agent")
+ userLanguage: str = Field(description="Language code of the user's request")
+ filesInput: List[str] = Field(default=[], description="List of input files in format 'fileName[;documentId]'")
+ filesOutput: List[str] = Field(default=[], description="List of output files in format 'fileName'")
- label: Label = Field(
- default=Label(default="Task Item", translations={"en": "Task Item", "fr": "Élément de tâche"}),
- description="Label for the class"
- )
class TaskPlan(BaseModel):
"""Work plan created by project manager"""
- objFinalDocuments: List[str] = Field(default=[], description="List of required result documents")
- objWorkplan: List[TaskItem] = Field(default=[], description="Plan for executing agents")
- objUserResponse: str = Field(description="Response to the user explaining the plan")
+ fileList: List[str] = Field(default=[], description="List of required result documents in format 'fileName'")
+ taskItems: List[TaskItem] = Field(default=[], description="Plan for executing agents")
+ userResponse: str = Field(description="Response to the user explaining the plan")
userLanguage: str = Field(default="en", description="Language code of the user's request")
-
- label: Label = Field(
- default=Label(default="Task Plan", translations={"en": "Task Plan", "fr": "Plan de tâches"}),
- description="Label for the class"
- )
-
-class WorkflowStatus(BaseModel):
- """Workflow status messages"""
- init: str = Field(default="Workflow initialized")
- running: str = Field(default="Running workflow")
- waiting: str = Field(default="Waiting for input")
- completed: str = Field(default="Workflow completed successfully")
- stopped: str = Field(default="Workflow stopped by user")
- failed: str = Field(default="Error in workflow")
-
- label: Label = Field(
- default=Label(default="Workflow Status", translations={"en": "Workflow Status", "fr": "État du workflow"}),
- description="Label for the class"
- )
-
-
-# Request models for the API
-
-class UserInputRequest(BaseModel):
- """Request for user input to a running workflow"""
- prompt: str = Field(description="Message from the user")
- listFileId: List[int] = Field(default=[], description="List of FileItem IDs")
\ No newline at end of file
diff --git a/modules/routes/routeAttributes.py b/modules/routes/routeAttributes.py
index b9cd752f..45f2092d 100644
--- a/modules/routes/routeAttributes.py
+++ b/modules/routes/routeAttributes.py
@@ -1,34 +1,39 @@
from fastapi import APIRouter, HTTPException, Depends, Path, Response
from typing import List, Dict, Any
from fastapi import status
+import inspect
+import importlib
+import os
+from pydantic import BaseModel
from modules.security.auth import getCurrentActiveUser, getUserContext
# Import the attribute definition and helper functions
from modules.shared.defAttributes import AttributeDefinition, getModelAttributes
-# Import the model modules (without specific classes)
-import modules.interfaces.gatewayModel as gatewayModel
-import modules.interfaces.lucydomModel as lucydomModel
-
-modelClasses = {
- # Gateway model classes
- "mandate": gatewayModel.Mandate,
- "user": gatewayModel.User,
+def getModelClasses() -> Dict[str, Any]:
+ """Dynamically get all model classes from all model modules"""
+ modelClasses = {}
- # LucyDOM model classes - admin
- "file": lucydomModel.FileItem,
- "prompt": lucydomModel.Prompt,
-
- # LucyDOM model classes - chat
- "documentContent": lucydomModel.DocumentContent,
- "document": lucydomModel.Document,
- "dataStats": lucydomModel.DataStats,
- "userInputRequest": lucydomModel.UserInputRequest,
- "workflow": lucydomModel.Workflow,
- "workflowMessage": lucydomModel.WorkflowMessage,
- "workflowLog": lucydomModel.WorkflowLog,
-}
+ # Get the interfaces directory path
+ # Since we're in modules/routes/, we need to go up one level to modules/ then into interfaces/
+ interfaces_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'interfaces')
+
+ # Find all model files
+ for filename in os.listdir(interfaces_dir):
+ if filename.endswith('Model.py'):
+ # Convert filename to module name (e.g., gatewayModel.py -> gatewayModel)
+ module_name = filename[:-3]
+
+ # Import the module dynamically
+ module = importlib.import_module(f'modules.interfaces.{module_name}')
+
+ # Get all classes from the module
+ for name, obj in inspect.getmembers(module):
+ if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj != BaseModel:
+ modelClasses[name.lower()] = obj
+
+ return modelClasses
# Create a router for the attribute endpoints
router = APIRouter(
@@ -52,6 +57,9 @@ async def getEntityAttributes(
# Determine preferred language of the user
userLanguage = currentUser.get("language", "de")
+ # Get model classes dynamically
+ modelClasses = getModelClasses()
+
# Check if entity type is known
if entityType not in modelClasses:
raise HTTPException(
diff --git a/modules/routes/routeFiles.py b/modules/routes/routeFiles.py
index 524016ba..4e64a038 100644
--- a/modules/routes/routeFiles.py
+++ b/modules/routes/routeFiles.py
@@ -26,23 +26,25 @@ def getModelAttributes(modelClass):
# Model attributes for FileItem
fileAttributes = getModelAttributes(FileItem)
-@dataclass
class AppContext:
- """Context object for all required connections and user information"""
- mandateId: int
- userId: int
- interfaceData: Any # LucyDOM Interface
+ def __init__(self, mandateId: int, userId: int):
+ self._mandateId = mandateId
+ self._userId = userId
+ self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
- """Creates a central context object with all required connections"""
- mandateId, userId = await getUserContext(currentUser)
- interfaceData = getLucydomInterface(mandateId, userId)
+ """
+ Creates a central context object with all required interfaces
- return AppContext(
- mandateId=mandateId,
- userId=userId,
- interfaceData=interfaceData
- )
+ Args:
+ currentUser: Current user from authentication
+
+ Returns:
+ AppContext object with all required connections
+ """
+ _mandateId, _userId = await getUserContext(currentUser)
+
+ return AppContext(_mandateId, _userId)
# Create router for file endpoints
router = APIRouter(
diff --git a/modules/routes/routeGeneral.py b/modules/routes/routeGeneral.py
index 0aa60055..ad1562ae 100644
--- a/modules/routes/routeGeneral.py
+++ b/modules/routes/routeGeneral.py
@@ -6,6 +6,7 @@ from typing import Dict, Any
from datetime import timedelta
import pathlib
import os
+import logging
from modules.shared.configuration import APP_CONFIG
from modules.security.auth import (
@@ -27,6 +28,8 @@ os.makedirs(staticFolder, exist_ok=True)
# Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
+logger = logging.getLogger(__name__)
+
@router.get("/favicon.ico")
async def favicon():
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
@@ -56,31 +59,140 @@ async def get_environment():
@router.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
- # Initialize Gateway interface without context
- gateway = getGatewayInterface()
-
- # Authenticate user
- user = gateway.authenticateUser(formData.username, formData.password)
+ # Get root mandate and admin user IDs
+ adminGateway = getGatewayInterface()
+ rootMandateId = adminGateway.getInitialId("mandates")
+ adminUserId = adminGateway.getInitialId("users")
- if not user:
+ if not rootMandateId or not adminUserId:
raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid username or password",
- headers={"WWW-Authenticate": "Bearer"},
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="System is not properly initialized with root mandate and admin user"
)
- # Create token with tenant ID
- accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
- accessToken = createAccessToken(
- data={
- "sub": user["username"],
- "mandateId": user["mandateId"]
- },
- expiresDelta=accessTokenExpires
- )
-
- return {"accessToken": accessToken, "tokenType": "bearer"}
+ # Create a new gateway interface instance with admin context
+ adminGateway = getGatewayInterface(rootMandateId, adminUserId)
+
+ try:
+ # Authenticate user
+ user = adminGateway.authenticateUser(formData.username, formData.password)
+
+ # Create token with mandate ID and user ID
+ accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ accessToken = createAccessToken(
+ data={
+ "sub": user["username"],
+ "_mandateId": str(user["_mandateId"]), # Ensure string
+ "_userId": str(user["id"]) # Ensure string
+ },
+ expiresDelta=accessTokenExpires
+ )
+
+ logger.info(f"User {user['username']} successfully logged in with context: _mandateId={user['_mandateId']}, _userId={user['id']}")
+ return {"accessToken": accessToken, "tokenType": "bearer"}
+ except ValueError as e:
+ # Handle authentication errors
+ error_msg = str(e)
+ logger.warning(f"Authentication failed for user {formData.username}: {error_msg}")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=error_msg,
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ except Exception as e:
+ # Handle other errors
+ error_msg = f"Login failed: {str(e)}"
+ logger.error(f"Unexpected error during login for user {formData.username}: {error_msg}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=error_msg
+ )
@router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
- return currentUser
\ No newline at end of file
+ return currentUser
+
+@router.post("/api/users/register", response_model=Dict[str, Any], tags=["General"])
+async def registerUser(userData: Dict[str, Any]):
+ """Register a new user."""
+ try:
+ logger.info("Received registration request")
+ logger.info(f"Raw userData type: {type(userData)}")
+ logger.info(f"Raw userData content: {userData}")
+
+ # Get root mandate and admin user IDs
+ adminGateway = getGatewayInterface()
+ rootMandateId = adminGateway.getInitialId("mandates")
+ adminUserId = adminGateway.getInitialId("users")
+
+ if not rootMandateId or not adminUserId:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="System is not properly initialized with root mandate and admin user"
+ )
+
+ # Create a new gateway interface instance with admin context
+ adminGateway = getGatewayInterface(rootMandateId, adminUserId)
+
+ # Check required fields
+ if not userData or not isinstance(userData, dict):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid user data format"
+ )
+
+ if not userData.get("username") or not userData.get("password"):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Username and password are required"
+ )
+
+ # Create user data with mandate ID
+ userData = {
+ "username": userData["username"],
+ "password": userData["password"],
+ "email": userData.get("email"),
+ "fullName": userData.get("fullName"),
+ "language": userData.get("language", "de"),
+ "_mandateId": rootMandateId,
+ "disabled": False,
+ "privilege": "user"
+ }
+
+ # Create the user
+ createdUser = adminGateway.createUser(**userData)
+
+ if not createdUser:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Failed to create user"
+ )
+
+ # Clear the users table from cache to ensure fresh data
+ if hasattr(adminGateway.db, '_tablesCache') and "users" in adminGateway.db._tablesCache:
+ del adminGateway.db._tablesCache["users"]
+
+ # Return the created user (without password)
+ if "hashedPassword" in createdUser:
+ del createdUser["hashedPassword"]
+ return createdUser
+ except ValueError as e:
+ logger.error(f"ValueError during registration: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e)
+ )
+ except PermissionError as e:
+ logger.error(f"PermissionError during registration: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail=str(e)
+ )
+ except Exception as e:
+ logger.error(f"Error during user registration: {str(e)}")
+ logger.error(f"Error type: {type(e)}")
+ logger.error(f"Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No details available'}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Failed to register user"
+ )
\ No newline at end of file
diff --git a/modules/routes/routeMandates.py b/modules/routes/routeMandates.py
index 981508f9..ce7f784c 100644
--- a/modules/routes/routeMandates.py
+++ b/modules/routes/routeMandates.py
@@ -19,23 +19,15 @@ def getModelAttributes(modelClass):
# Model attributes for Mandate
mandateAttributes = getModelAttributes(Mandate)
-@dataclass
class AppContext:
- """Context object for all required connections and user information"""
- mandateId: int
- userId: int
- interfaceData: Any # Gateway Interface
+ def __init__(self, mandateId: int, userId: int):
+ self._mandateId = mandateId
+ self._userId = userId
+ self.interfaceData = getGatewayInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
- """Creates a central context object with all required connections"""
mandateId, userId = await getUserContext(currentUser)
- interfaceData = getGatewayInterface(mandateId, userId)
-
- return AppContext(
- mandateId=mandateId,
- userId=userId,
- interfaceData=interfaceData
- )
+ return AppContext(mandateId, userId)
# Create router for mandate endpoints
router = APIRouter(
@@ -89,19 +81,19 @@ async def createMandate(
return newMandate
-@router.get("/{mandateId}", response_model=Dict[str, Any])
+@router.get("/{_mandateId}", response_model=Dict[str, Any])
async def getMandate(
- mandateId: int,
+ _mandateId: str = Path(..., description="ID of the mandate"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
- """Get a specific mandate"""
+ """Get a mandate by ID."""
context = await getContext(currentUser)
# Permission check
# Admin can only see their own mandate, SysAdmin can see all
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
- isOwnMandate = context.mandateId == mandateId
+ isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
@@ -110,36 +102,36 @@ async def getMandate(
)
# Get mandate
- mandate = context.interfaceData.getMandate(mandateId)
+ mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Mandate with ID {mandateId} not found"
+ detail=f"Mandate with ID {_mandateId} not found"
)
return mandate
-@router.put("/{mandateId}", response_model=Dict[str, Any])
+@router.put("/{_mandateId}", response_model=Dict[str, Any])
async def updateMandate(
- mandateId: int = Path(..., description="ID of the mandate to update"),
- mandateData: Dict[str, Any] = Body(..., description="Updated mandate data"),
+ _mandateId: str = Path(..., description="ID of the mandate to update"),
+ mandateData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
- """Update an existing mandate"""
+ """Update a mandate."""
context = await getContext(currentUser)
- # Mandate exists?
- mandate = context.interfaceData.getMandate(mandateId)
+ # Get mandate
+ mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Mandate with ID {mandateId} not found"
+ detail=f"Mandate with ID {_mandateId} not found"
)
# Permission check
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
- isOwnMandate = context.mandateId == mandateId
+ isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
@@ -154,33 +146,29 @@ async def updateMandate(
updateData[attr] = mandateData[attr]
# Update mandate
- updatedMandate = context.interfaceData.updateMandate(
- mandateId=mandateId,
- mandateData=updateData
- )
-
+ updatedMandate = context.interfaceData.updateMandate(_mandateId, mandateData)
return updatedMandate
-@router.delete("/{mandateId}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete("/{_mandateId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteMandate(
- mandateId: int = Path(..., description="ID of the mandate to delete"),
+ _mandateId: str = Path(..., description="ID of the mandate to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
- """Delete a mandate, including all associated users and referenced objects"""
+ """Delete a mandate."""
context = await getContext(currentUser)
- # Mandate exists?
- mandate = context.interfaceData.getMandate(mandateId)
+ # Get mandate
+ mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Mandate with ID {mandateId} not found"
+ detail=f"Mandate with ID {_mandateId} not found"
)
# Permission check
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
- isOwnMandate = context.mandateId == mandateId
+ isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
@@ -189,11 +177,11 @@ async def deleteMandate(
)
# Delete mandate
- success = context.interfaceData.deleteMandate(mandateId)
+ success = context.interfaceData.deleteMandate(_mandateId)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Error deleting mandate with ID {mandateId}"
+ detail=f"Error deleting mandate with ID {_mandateId}"
)
return None
\ No newline at end of file
diff --git a/modules/routes/routeMsft.py b/modules/routes/routeMsft.py
index 77fb9f9a..8401dee8 100644
--- a/modules/routes/routeMsft.py
+++ b/modules/routes/routeMsft.py
@@ -48,15 +48,15 @@ async def save_token_to_file(token_data, currentUser: Dict[str, Any]):
"""Save token data to database using LucyDOMInterface"""
try:
# Get current user context
- mandateId, userId = await getUserContext(currentUser)
- if not mandateId or not userId:
+ _mandateId, _userId = await getUserContext(currentUser)
+ if not _mandateId or not _userId:
logger.error("No user context available for token storage")
return False
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
- mandateId=mandateId,
- userId=userId
+ _mandateId=_mandateId,
+ _userId=_userId
)
if not mydom:
logger.error("No LucyDOM interface available for token storage")
@@ -79,15 +79,15 @@ async def load_token_from_file(currentUser: Dict[str, Any]):
"""Load token data from database using LucyDOMInterface"""
try:
# Get current user context
- mandateId, userId = await getUserContext(currentUser)
- if not mandateId or not userId:
+ _mandateId, _userId = await getUserContext(currentUser)
+ if not _mandateId or not _userId:
logger.error("No user context available for token retrieval")
return None
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
- mandateId=mandateId,
- userId=userId
+ _mandateId=_mandateId,
+ _userId=_userId
)
if not mydom:
logger.error("No LucyDOM interface available for token retrieval")
@@ -350,8 +350,8 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
"""Check Microsoft authentication status"""
try:
# Get current user context
- mandateId, userId = await getUserContext(currentUser)
- if not mandateId or not userId:
+ _mandateId, _userId = await getUserContext(currentUser)
+ if not _mandateId or not _userId:
logger.info("No user context found")
return JSONResponse({
"authenticated": False,
@@ -362,7 +362,7 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
token_data = await load_token_from_file(currentUser)
if not token_data:
- logger.info(f"No token data found for user {userId}")
+ logger.info(f"No token data found for user {_userId}")
return JSONResponse({
"authenticated": False,
"message": "Not authenticated with Microsoft"
@@ -372,7 +372,7 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
if not verify_token(token_data["access_token"]):
logger.info("Token invalid, attempting refresh")
# Try to refresh the token
- if not await refresh_token(userId, currentUser):
+ if not await refresh_token(_userId, currentUser):
logger.info("Token refresh failed")
return JSONResponse({
"authenticated": False,
@@ -408,16 +408,16 @@ async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Logout from Microsoft"""
try:
# Get current user context
- mandateId, userId = await getUserContext(currentUser)
- if not mandateId or not userId:
+ _mandateId, _userId = await getUserContext(currentUser)
+ if not _mandateId or not _userId:
return JSONResponse({
"message": "Not authenticated with Microsoft"
})
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
- mandateId=mandateId,
- userId=userId
+ _mandateId=_mandateId,
+ _userId=_userId
)
if not mydom:
return JSONResponse({
@@ -426,13 +426,13 @@ async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
# Remove token from database
tokens = mydom.db.getRecordset("msftTokens", recordFilter={
- "mandateId": mandateId,
- "userId": userId
+ "_mandateId": _mandateId,
+ "_userId": _userId
})
if tokens and len(tokens) > 0:
mydom.db.recordDelete("msftTokens", tokens[0]["id"])
- logger.info(f"Removed Microsoft token for user {userId}")
+ logger.info(f"Removed Microsoft token for user {_userId}")
return JSONResponse({
"message": "Successfully logged out from Microsoft"
@@ -491,7 +491,7 @@ async def get_backend_token(request: Request):
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid authorization header"
)
-
+
# Extract the MSAL token
msal_token = auth_header.split(' ')[1]
@@ -502,7 +502,7 @@ async def get_backend_token(request: Request):
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid MSAL token"
)
-
+
# Get the user from the database using the email
gateway = getGatewayInterface()
user = gateway.getUserByUsername(user_info["email"])
@@ -512,17 +512,17 @@ async def get_backend_token(request: Request):
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not registered in the system"
)
-
+
# Create backend token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = createAccessToken(
data={
"sub": user["username"],
- "mandateId": user["mandateId"]
+ "_mandateId": user["_mandateId"]
},
expiresDelta=access_token_expires
)
-
+
return {
"accessToken": access_token,
"tokenType": "bearer",
@@ -530,7 +530,7 @@ async def get_backend_token(request: Request):
"username": user["username"],
"email": user["email"],
"fullName": user.get("fullName", ""),
- "mandateId": user["mandateId"]
+ "_mandateId": user["_mandateId"]
}
}
diff --git a/modules/routes/routePrompts.py b/modules/routes/routePrompts.py
index a42ca865..90f9c41b 100644
--- a/modules/routes/routePrompts.py
+++ b/modules/routes/routePrompts.py
@@ -21,23 +21,15 @@ def getModelAttributes(modelClass):
# Model attributes for Prompt
promptAttributes = getModelAttributes(Prompt)
-@dataclass
class AppContext:
- """Context object for all required connections and user information"""
- mandateId: int
- userId: int
- interfaceData: Any # LucyDOM Interface
+ def __init__(self, mandateId: str, userId: str):
+ self._mandateId = mandateId
+ self._userId = userId
+ self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
- """Creates a central context object with all required connections"""
mandateId, userId = await getUserContext(currentUser)
- interfaceData = getLucydomInterface(mandateId, userId)
-
- return AppContext(
- mandateId=mandateId,
- userId=userId,
- interfaceData=interfaceData
- )
+ return AppContext(mandateId, userId)
# Create router for prompt endpoints
router = APIRouter(
@@ -82,7 +74,7 @@ async def createPrompt(
@router.get("/{promptId}", response_model=Dict[str, Any])
async def getPrompt(
- promptId: int,
+ promptId: str = Path(..., description="ID of the prompt"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a specific prompt"""
@@ -100,7 +92,7 @@ async def getPrompt(
@router.put("/{promptId}", response_model=Dict[str, Any])
async def updatePrompt(
- promptId: int,
+ promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
@@ -136,7 +128,7 @@ async def updatePrompt(
@router.delete("/{promptId}", response_model=Dict[str, Any])
async def deletePrompt(
- promptId: int,
+ promptId: str = Path(..., description="ID of the prompt to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a prompt"""
diff --git a/modules/routes/routeUsers.py b/modules/routes/routeUsers.py
index af53b75c..468fe867 100644
--- a/modules/routes/routeUsers.py
+++ b/modules/routes/routeUsers.py
@@ -30,18 +30,18 @@ userAttributes = getModelAttributes(User)
@dataclass
class AppContext:
"""Context object for all required connections and user information"""
- mandateId: int
- userId: int
+ _mandateId: int
+ _userId: int
interfaceData: Any # Gateway Interface
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections"""
- mandateId, userId = await getUserContext(currentUser)
- interfaceData = getGatewayInterface(mandateId, userId)
+ _mandateId, _userId = await getUserContext(currentUser)
+ interfaceData = getGatewayInterface(_mandateId, _userId)
return AppContext(
- mandateId=mandateId,
- userId=userId,
+ _mandateId=_mandateId,
+ _userId=_userId,
interfaceData=interfaceData
)
@@ -66,7 +66,7 @@ async def getUsers(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
# Admin sees only users of own mandate, SysAdmin sees all
if currentUser.get("privilege") == "admin":
- return context.interfaceData.getUsersByMandate(context.mandateId)
+ return context.interfaceData.getUsersByMandate(context._mandateId)
else: # sysadmin
return context.interfaceData.getAllUsers()
@@ -80,8 +80,15 @@ async def registerUser(request: Request):
logger.info(f"Registration request data: {data}")
# Get root mandate and admin user IDs
- rootMandateId = 1 # Root mandate is always ID 1
- adminUserId = 1 # Admin user is always ID 1
+ adminGateway = getGatewayInterface()
+ rootMandateId = adminGateway.getInitialId("mandates")
+ adminUserId = adminGateway.getInitialId("users")
+
+ if not rootMandateId or not adminUserId:
+ raise HTTPException(
+ status_code=500,
+ detail="System is not properly initialized with root mandate and admin user"
+ )
# Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
@@ -98,7 +105,7 @@ async def registerUser(request: Request):
"email": data.get("email"),
"fullName": data.get("fullName"),
"language": data.get("language", "de"),
- "mandateId": rootMandateId,
+ "_mandateId": rootMandateId,
"disabled": False,
"privilege": "user"
}
@@ -125,16 +132,26 @@ async def registerUser(request: Request):
logger.info("User verification successful")
# Test authentication
- authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
- if not authResult:
- logger.error("Authentication test failed after user creation")
+ try:
+ authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
+ if not authResult:
+ logger.error("Authentication test failed after user creation")
+ # Try to delete the user
+ try:
+ adminGateway.deleteUser(createdUser["id"])
+ logger.info("Successfully deleted user after authentication test failure")
+ except Exception as e:
+ logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
+ raise HTTPException(status_code=500, detail="Authentication test failed")
+ except ValueError as e:
+ logger.error(f"Authentication test failed: {str(e)}")
# Try to delete the user
try:
- # adminGateway.deleteUser(createdUser["id"])
- logger.info("Successfully NOT deleted user after authentication test failure")
+ adminGateway.deleteUser(createdUser["id"])
+ logger.info("Successfully deleted user after authentication test failure")
except Exception as e:
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
- raise HTTPException(status_code=500, detail="Authentication test failed")
+ raise HTTPException(status_code=500, detail=f"Authentication test failed: {str(e)}")
logger.info("Authentication test successful")
@@ -194,7 +211,7 @@ async def registerUserWithMsal(userData: dict = Body(...)):
email=userData.get("email"),
fullName=userData.get("fullName"),
language=userData.get("language", "de"),
- mandateId=rootMandateId,
+ _mandateId=rootMandateId,
disabled=False,
privilege="user"
)
@@ -214,7 +231,7 @@ async def registerUserWithMsal(userData: dict = Body(...)):
@router.get("/{userId}", response_model=Dict[str, Any])
async def getUser(
- userId: int,
+ userId: str = Path(..., description="ID of the user"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a specific user"""
@@ -230,10 +247,10 @@ async def getUser(
# Permission check
# User can only view themselves, Admin only users of their own mandate, SysAdmin all
- if userId == context.userId:
+ if userId == str(context._userId):
# User can view themselves
pass
- elif currentUser.get("privilege") == "admin" and userToGet.get("mandateId") == context.mandateId:
+ elif currentUser.get("privilege") == "admin" and userToGet.get("_mandateId") == context._mandateId:
# Admin can view users of their own mandate
pass
elif currentUser.get("privilege") == "sysadmin":
@@ -249,7 +266,7 @@ async def getUser(
@router.put("/{userId}", response_model=Dict[str, Any])
async def updateUser(
- userId: int = Path(..., description="ID of the user to update"),
+ userId: str = Path(..., description="ID of the user to update"),
userData: Dict[str, Any] = Body(..., description="Updated user data"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
@@ -265,14 +282,14 @@ async def updateUser(
)
# Permission check
- isSelfUpdate = userId == context.userId
+ isSelfUpdate = userId == str(context._userId)
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
- sameMandate = userToUpdate.get("mandateId") == context.mandateId
+ sameMandate = userToUpdate.get("_mandateId") == context._mandateId
# Filter allowed fields based on permission level
allowedFields = {"username", "email", "fullName", "language"}
- sensitiveFields = {"mandateId", "disabled", "privilege"}
+ sensitiveFields = {"_mandateId", "disabled", "privilege"}
# Check if sensitive fields should be changed
sensitiveUpdate = any(field in userData for field in sensitiveFields)
@@ -312,7 +329,7 @@ async def updateUser(
@router.delete("/{userId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteUser(
- userId: int = Path(..., description="ID of the user to delete"),
+ userId: str = Path(..., description="ID of the user to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a user"""
@@ -327,10 +344,10 @@ async def deleteUser(
)
# Permission check
- isSelfDelete = userId == context.userId
+ isSelfDelete = userId == str(context._userId)
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
- sameMandate = userToDelete.get("mandateId") == context.mandateId
+ sameMandate = userToDelete.get("_mandateId") == context._mandateId
if isSelfDelete:
# User can delete themselves
diff --git a/modules/routes/routeWorkflows.py b/modules/routes/routeWorkflows.py
index 3165b156..04902b10 100644
--- a/modules/routes/routeWorkflows.py
+++ b/modules/routes/routeWorkflows.py
@@ -29,34 +29,15 @@ router = APIRouter(
responses={404: {"description": "Not found"}}
)
-@dataclass
class AppContext:
- """Context object for all required connections and user information"""
- mandateId: int
- userId: int
- interfaceData: Any # LucyDOM Interface
- interfaceChat: Any # Workflow Manager
+ def __init__(self, mandateId: int, userId: int):
+ self._mandateId = mandateId
+ self._userId = userId
+ self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
- """
- Creates a central context object with all required interfaces
-
- Args:
- currentUser: Current user from authentication
-
- Returns:
- AppContext object with all required connections
- """
mandateId, userId = await getUserContext(currentUser)
- interfaceData = getLucydomInterface(mandateId, userId)
- interfaceChat = getWorkflowManager(mandateId, userId)
-
- return AppContext(
- mandateId=mandateId,
- userId=userId,
- interfaceData=interfaceData,
- interfaceChat=interfaceChat
- )
+ return AppContext(mandateId, userId)
# State 1: Workflow Initialization endpoint
@router.post("/start", response_model=Dict[str, Any])
@@ -87,7 +68,7 @@ async def startWorkflow(
}
# Start or continue workflow using the workflow manager
- workflow = await context.interfaceChat.workflowStart(userInputDict, workflowId)
+ workflow = await getWorkflowManager(context._mandateId, context._userId).workflowStart(userInputDict, workflowId)
logger.info("User Input received. Answer:",workflow)
return {
@@ -130,14 +111,14 @@ async def stopWorkflow(
detail=f"Workflow with ID {workflowId} not found"
)
- if workflow.get("userId") != context.userId:
+ if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to stop this workflow"
)
# Stop the workflow
- stoppedWorkflow = await context.interfaceChat.workflowStop(workflowId)
+ stoppedWorkflow = getWorkflowManager(context._mandateId, context._userId).workflowStop(workflowId)
return {
"id": workflowId,
@@ -183,7 +164,7 @@ async def deleteWorkflow(
)
# Check if user has permission to delete
- if workflow.get("userId") != context.userId:
+ if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to delete this workflow"
@@ -230,7 +211,7 @@ async def listWorkflows(
try:
# Retrieve workflows for the user
- workflows = context.interfaceData.getWorkflowsByUser(context.userId)
+ workflows = context.interfaceData.getWorkflowsByUser(context._userId)
return workflows
except Exception as e:
logger.error(f"Error listing workflows: {str(e)}", exc_info=True)
@@ -431,7 +412,7 @@ async def deleteWorkflowMessage(
detail=f"Workflow with ID {workflowId} not found"
)
- if workflow.get("userId") != context.userId:
+ if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow"
@@ -471,7 +452,7 @@ async def deleteWorkflowMessage(
async def deleteFileFromMessage(
workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"),
- fileId: int = Path(..., description="ID of the file to delete"),
+ fileId: str = Path(..., description="ID of the file to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
@@ -498,7 +479,7 @@ async def deleteFileFromMessage(
detail=f"Workflow with ID {workflowId} not found"
)
- if workflow.get("userId") != context.userId:
+ if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow"
@@ -533,7 +514,7 @@ async def deleteFileFromMessage(
@router.get("/files/{fileId}/preview", response_model=Dict[str, Any])
async def previewFile(
- fileId: int = Path(..., description="ID of the file to preview"),
+ fileId: str = Path(..., description="ID of the file to preview"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
@@ -558,7 +539,7 @@ async def previewFile(
)
# Check if file belongs to user or their mandate
- if file.get("mandateId") != context.mandateId and file.get("userId") != context.userId:
+ if file.get("_mandateId") != context._mandateId and file.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this file"
@@ -636,7 +617,7 @@ async def previewFile(
@router.get("/files/{fileId}/download")
async def downloadFile(
- fileId: int = Path(..., description="ID of the file to download"),
+ fileId: str = Path(..., description="ID of the file to download"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
@@ -676,4 +657,122 @@ async def downloadFile(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading file: {str(e)}"
- )
\ No newline at end of file
+ )
+
+@router.get("/workflows", response_model=List[Dict[str, Any]])
+async def getWorkflows(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
+ context = await getContext(currentUser)
+
+ # Get all workflows for the mandate
+ workflows = context.interfaceData.getWorkflowsByMandate(context._mandateId)
+
+ return workflows
+
+@router.post("/workflows", response_model=Dict[str, Any])
+async def createWorkflow(
+ workflow: Dict[str, Any],
+ currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
+):
+ context = await getContext(currentUser)
+
+ # Create workflow
+ newWorkflow = context.interfaceData.createWorkflow(workflow)
+
+ return newWorkflow
+
+@router.get("/workflows/{workflowId}", response_model=Dict[str, Any])
+async def getWorkflow(
+ workflowId: str = Path(..., description="ID of the workflow"),
+ currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
+):
+ context = await getContext(currentUser)
+
+ # Get workflow
+ workflow = context.interfaceData.getWorkflow(workflowId)
+ if not workflow:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ # Check if user has access to this workflow
+ if workflow.get("_userId") != context._userId:
+ raise HTTPException(status_code=403, detail="Not authorized to access this workflow")
+
+ return workflow
+
+@router.put("/workflows/{workflowId}", response_model=Dict[str, Any])
+async def updateWorkflow(
+ workflow: Dict[str, Any],
+ workflowId: str = Path(..., description="ID of the workflow to update"),
+ currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
+):
+ context = await getContext(currentUser)
+
+ # Get workflow
+ existingWorkflow = context.interfaceData.getWorkflow(workflowId)
+ if not existingWorkflow:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ # Check if user has access to this workflow
+ if existingWorkflow.get("_userId") != context._userId:
+ raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
+
+ # Update workflow
+ updatedWorkflow = context.interfaceData.updateWorkflow(workflowId, workflow)
+
+ return updatedWorkflow
+
+@router.delete("/workflows/{workflowId}")
+async def deleteWorkflow(
+ workflowId: str = Path(..., description="ID of the workflow to delete"),
+ currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
+):
+ context = await getContext(currentUser)
+
+ # Get workflow
+ workflow = context.interfaceData.getWorkflow(workflowId)
+ if not workflow:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ # Check if user has access to this workflow
+ if workflow.get("_userId") != context._userId:
+ raise HTTPException(status_code=403, detail="Not authorized to delete this workflow")
+
+ # Delete workflow
+ success = context.interfaceData.deleteWorkflow(workflowId)
+ if not success:
+ raise HTTPException(status_code=500, detail="Failed to delete workflow")
+
+ return {"status": "success"}
+
+@router.post("/workflows/{workflowId}/files/{fileId}")
+async def addFileToWorkflow(
+ workflowId: str = Path(..., description="ID of the workflow"),
+ fileId: str = Path(..., description="ID of the file to add"),
+ currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
+):
+ """Add a file to a workflow."""
+ context = await getContext(currentUser)
+
+ # Get workflow
+ workflow = context.interfaceData.getWorkflow(workflowId)
+ if not workflow:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ # Check access
+ if workflow.get("_userId") != context._userId:
+ raise HTTPException(status_code=403, detail="No access to this workflow")
+
+ # Get file
+ file = context.interfaceData.getFile(fileId)
+ if not file:
+ raise HTTPException(status_code=404, detail="File not found")
+
+ # Check file access
+ if file.get("_mandateId") != context._mandateId and file.get("_userId") != context._userId:
+ raise HTTPException(status_code=403, detail="No access to this file")
+
+ # Add file to workflow
+ success = context.interfaceData.addFileToWorkflow(workflowId, fileId)
+ if not success:
+ raise HTTPException(status_code=500, detail="Failed to add file to workflow")
+
+ return {"status": "success"}
\ No newline at end of file
diff --git a/modules/security/auth.py b/modules/security/auth.py
index 7506ea12..9d15e4c2 100644
--- a/modules/security/auth.py
+++ b/modules/security/auth.py
@@ -75,15 +75,20 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
if username is None:
raise credentialsException
- # Extract mandate ID from token (if present)
- mandateId: int = payload.get("mandateId", 1) # Default: Root mandate
+ # Extract mandate ID and user ID from token
+ _mandateId: str = payload.get("_mandateId")
+ _userId: str = payload.get("_userId")
+
+ if not _mandateId or not _userId:
+ logger.error(f"Missing context in token: _mandateId={_mandateId}, _userId={_userId}")
+ raise credentialsException
except JWTError:
logger.warning("Invalid JWT Token")
raise credentialsException
- # Initialize Gateway Interface without context
- gateway = getGatewayInterface()
+ # Initialize Gateway Interface with context
+ gateway = getGatewayInterface(_mandateId, _userId)
# Retrieve user from database
user = gateway.getUserByUsername(username)
@@ -96,6 +101,11 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
logger.warning(f"User {username} is disabled")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
+ # Ensure the user has the correct context
+ if str(user.get("_mandateId")) != str(_mandateId) or str(user.get("id")) != str(_userId):
+ logger.error(f"User context mismatch: token(_mandateId={_mandateId}, _userId={_userId}) vs user(_mandateId={user.get('_mandateId')}, id={user.get('id')})")
+ raise credentialsException
+
return user
async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, Any]:
@@ -116,43 +126,48 @@ async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentU
return currentUser
-async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[int, int]:
+async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[str, str]:
"""
Extracts the mandate ID and user ID from the current user.
- Enhanced with better logging.
Args:
currentUser: The current user
Returns:
- Tuple of (mandateId, userId)
+ Tuple of (_mandateId, _userId) as strings
+
+ Raises:
+ HTTPException: If mandate or user ID is missing
"""
- # Default values
- defaultMandateId = 0
- defaultUserId = 0
+ # Extract _mandateId
+ _mandateId = currentUser.get("_mandateId")
+ if not _mandateId:
+ logger.error("No _mandateId found in currentUser")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Missing mandate context"
+ )
- # Extract mandateId
- mandateId = currentUser.get("mandateId", None)
- if mandateId is None:
- logger.warning(f"No mandateId found in currentUser, using default: {defaultMandateId}")
- mandateId = defaultMandateId
- else:
- try:
- mandateId = int(mandateId)
- except (ValueError, TypeError):
- logger.error(f"Invalid mandateId value: {mandateId}, using default: {defaultMandateId}")
- mandateId = defaultMandateId
+ # Extract _userId
+ _userId = currentUser.get("id") # Note: using 'id' instead of '_userId'
+ if not _userId:
+ logger.error("No _userId found in currentUser")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Missing user context"
+ )
- # Extract userId
- userId = currentUser.get("id", None)
- if userId is None:
- logger.warning(f"No userId found in currentUser, using default: {defaultUserId}")
- userId = defaultUserId
- else:
- try:
- userId = int(userId)
- except (ValueError, TypeError):
- logger.error(f"Invalid userId value: {userId}, using default: {defaultUserId}")
- userId = defaultUserId
+ return str(_mandateId), str(_userId)
+
+def getInitialContext() -> tuple[str, str]:
+ """
+ Returns the initial mandate and user IDs from the gateway.
+ This is used by other interfaces to get their context.
+ Returns:
+ tuple[str, str]: (_mandateId, _userId) or (None, None) if not available
+ """
+ gateway = getGatewayInterface()
+ mandateId = gateway.getInitialId("mandates")
+ userId = gateway.getInitialId("users")
return mandateId, userId
\ No newline at end of file
diff --git a/modules/shared/defAttributes.py b/modules/shared/defAttributes.py
index 731ecfd9..39fc21a1 100644
--- a/modules/shared/defAttributes.py
+++ b/modules/shared/defAttributes.py
@@ -111,8 +111,8 @@ def getModelAttributes(modelClass, userLanguage="de"):
placeholder=placeholder,
defaultValue=defaultValue,
options=options,
- editable=fieldName not in ["id", "mandateId", "userId", "createdAt", "uploadDate"],
- visible=fieldName not in ["hashedPassword", "mandateId", "userId"],
+ editable=fieldName not in ["id", "_mandateId", "_userId", "uploadDate", "_createdAt", "_modifiedAt"],
+ visible=fieldName not in ["hashedPassword", "_mandateId", "_userId"],
order=i,
validation=validation,
helpText=description or "" # Set empty string as default value if no description found
diff --git a/modules/workflow/agentBase.py b/modules/workflow/agentBase.py
index 05223388..bcfac6e0 100644
--- a/modules/workflow/agentBase.py
+++ b/modules/workflow/agentBase.py
@@ -25,11 +25,18 @@ class AgentBase:
self.label = "Base Agent"
self.description = "Base agent functionality"
self.capabilities = []
+ self.workflowManager = None
self.mydom = None
- self.workflowManager = None # Will be set by workflow manager
- def setDependencies(self, mydom=None):
- """Set external dependencies for the agent."""
+ def setWorkflowManager(self, workflowManager):
+ """Set the workflow manager reference."""
+ self.workflowManager = workflowManager
+ # Also set mydom reference from workflow manager
+ if workflowManager and hasattr(workflowManager, 'mydom'):
+ self.mydom = workflowManager.mydom
+
+ def setMydom(self, mydom):
+ """Set the LucyDOM interface reference."""
self.mydom = mydom
def getAgentInfo(self) -> Dict[str, Any]:
diff --git a/modules/workflow/agentRegistry.py b/modules/workflow/agentRegistry.py
index 2665df25..4558c7b2 100644
--- a/modules/workflow/agentRegistry.py
+++ b/modules/workflow/agentRegistry.py
@@ -30,9 +30,16 @@ class AgentRegistry:
raise RuntimeError("Singleton instance already exists - use getInstance()")
self.agents = {}
- self.mydom = None
self._loadAgents()
+ def initialize(self, mydom=None, workflowManager=None):
+ """Initialize or update the registry with workflow manager and LucyDOM references."""
+ for agent in self.agents.values():
+ if workflowManager and hasattr(agent, 'setWorkflowManager'):
+ agent.setWorkflowManager(workflowManager)
+ elif mydom and hasattr(agent, 'setMydom'):
+ agent.setMydom(mydom)
+
def _loadAgents(self):
"""Load all available agents from modules."""
logger.info("Loading agent modules...")
@@ -88,22 +95,6 @@ class AgentRegistry:
except Exception as e:
logger.error(f"Error loading agent from module {moduleName}: {e}")
- def setMydom(self, mydom):
- """Set the AI service for all agents."""
- self.mydom = mydom
- self.updateAgentDependencies()
-
- def setWorkflowManager(self, workflowManager):
- """Set the workflow manager reference for all agents."""
- for agent in self.agents.values():
- agent.workflowManager = workflowManager
-
- def updateAgentDependencies(self):
- """Update dependencies for all registered agents."""
- for agentId, agent in self.agents.items():
- if hasattr(agent, 'setDependencies'):
- agent.setDependencies(mydom=self.mydom)
-
def registerAgent(self, agent):
"""
Register an agent in the registry.
@@ -112,9 +103,6 @@ class AgentRegistry:
agent: The agent to register
"""
agentId = getattr(agent, 'name', "unknown_agent")
- # Initialize agent with dependencies
- if hasattr(agent, 'setDependencies'):
- agent.setDependencies(mydom=self.mydom)
self.agents[agentId] = agent
logger.debug(f"Agent '{agent.name}' registered")
@@ -127,11 +115,7 @@ class AgentRegistry:
Agent instance or None if not found
"""
if agentIdentifier in self.agents:
- agent = self.agents[agentIdentifier]
- # Ensure the agent has the AI service
- if self.mydom:
- agent.mydom = self.mydom
- return agent
+ return self.agents[agentIdentifier]
logger.error(f"Agent with identifier '{agentIdentifier}' not found")
return None
diff --git a/modules/workflow/workflowAgentsRegistry.py b/modules/workflow/workflowAgentsRegistry.py
deleted file mode 100644
index f742a1ac..00000000
--- a/modules/workflow/workflowAgentsRegistry.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-Agent Registry Module.
-Provides a central registry system for all available agents.
-Optimized for the standardized task processing pattern.
-"""
-
-from .agentBase import AgentBase
-from .agentRegistry import AgentRegistry, getAgentRegistry
-
-__all__ = ['AgentBase', 'AgentRegistry', 'getAgentRegistry']
\ No newline at end of file
diff --git a/modules/workflow/workflowManager.py b/modules/workflow/workflowManager.py
index 5082b563..a0c2b6b0 100644
--- a/modules/workflow/workflowManager.py
+++ b/modules/workflow/workflowManager.py
@@ -41,26 +41,49 @@ class WorkflowStoppedException(Exception):
pass
class WorkflowManager:
- """
- Manages the processing of chat requests, agent execution, and
- the integration of results into the workflow, following a state machine approach.
- """
-
- def __init__(self, mandateId: int, userId: int):
- """
- Initializes the WorkflowManager with mandate and user context.
-
- Args:
- mandateId: ID of the current mandate
- userId: ID of the current user
- """
- self.mandateId = mandateId
- self.userId = userId
- self.mydom = domInterface(mandateId, userId)
+ """Manages the execution of workflows and their associated agents."""
+
+ def __init__(self, _mandateId: str, _userId: str):
+ """Initialize the workflow manager with mandate and user context."""
+ self._mandateId = _mandateId
+ self._userId = _userId
+ self.mydom = domInterface(_mandateId, _userId)
self.agentRegistry = getAgentRegistry()
- self.agentRegistry.setMydom(self.mydom)
- self.agentRegistry.setWorkflowManager(self) # Set self as workflow manager for all agents
-
+ self.agentRegistry.initialize(mydom=self.mydom, workflowManager=self)
+
+ def workflowStart(self, workflowId: str, workflowData: dict) -> dict:
+ """Start a new workflow with the given ID and data."""
+ try:
+ # Update the LucyDOM interface with current user context
+ self.mydom._mandateId = self._mandateId
+ self.mydom._userId = self._userId
+
+ # Initialize workflow state
+ workflowState = {
+ 'workflowId': workflowId,
+ 'status': 'running',
+ 'startTime': datetime.now().isoformat(),
+ 'currentStep': 0,
+ 'steps': [],
+ 'data': workflowData
+ }
+
+ # Get workflow definition
+ workflowDef = self._getWorkflowDefinition(workflowId)
+ if not workflowDef:
+ raise ValueError(f"Workflow definition not found for ID: {workflowId}")
+
+ # Initialize steps
+ workflowState['steps'] = self._initializeSteps(workflowDef)
+
+ # Start workflow execution
+ self._executeWorkflow(workflowState)
+
+ return workflowState
+
+ except Exception as e:
+ logger.error(f"Error starting workflow {workflowId}: {str(e)}")
+ raise
### Workflow State Machine Implementation
@@ -280,8 +303,8 @@ class WorkflowManager:
newWorkflowId = str(uuid.uuid4()) if workflowId is None else workflowId
workflow = {
"id": newWorkflowId,
- "mandateId": self.mandateId,
- "userId": self.userId,
+ "_mandateId": self._mandateId,
+ "_userId": self._userId,
"name": f"Workflow {newWorkflowId[:8]}",
"startedAt": currentTime,
"messages": [], # Empty list - will be filled with references
@@ -301,8 +324,8 @@ class WorkflowManager:
# Save to database - only the workflow metadata
workflowDb = {
"id": workflow["id"],
- "mandateId": workflow["mandateId"],
- "userId": workflow["userId"],
+ "_mandateId": workflow["_mandateId"],
+ "_userId": workflow["_userId"],
"name": workflow["name"],
"startedAt": workflow["startedAt"],
"status": workflow["status"],
@@ -593,7 +616,12 @@ JSON_OUTPUT = {{
try:
# Process the task using the agent's standardized interface
logger.debug("TASK: "+self.parseJson2text(agentTask))
- logger.debug(f"Agent '{agentName}' AI service available: {agent.mydom is not None}")
+
+ # Ensure AI service is available
+ if not self.mydom.aiService:
+ logger.error("AI service not available in LucyDOM interface")
+ self.logAdd(workflow, "Error: AI service not available", level="error")
+ return []
# Calculate bytes sent before processing
bytesSent = len(json.dumps(agentTask).encode('utf-8'))
@@ -885,8 +913,8 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
continue
# Check if file belongs to the current mandate
- if file.get("mandateId") != self.mandateId:
- logger.warning(f"File {fileId} does not belong to mandate {self.mandateId}")
+ if file.get("_mandateId") != self._mandateId:
+ logger.warning(f"File {fileId} does not belong to mandate {self._mandateId}")
continue
# Load file content
@@ -1511,49 +1539,48 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"userLanguage": "en"
}
+ def _createWorkflowData(self, workflow: Dict[str, Any]) -> Dict[str, Any]:
+ """Creates a workflow data structure."""
+ return {
+ "_mandateId": self._mandateId,
+ "_userId": self._userId,
+ "name": workflow.get("name", "New Workflow"),
+ "status": workflow.get("status", "running"),
+ "startedAt": workflow.get("startedAt", self._getCurrentTimestamp()),
+ "lastActivity": workflow.get("lastActivity", self._getCurrentTimestamp()),
+ "dataStats": workflow.get("dataStats", {})
+ }
+
+ def _checkFileAccess(self, fileId: int) -> bool:
+ """Checks if the current user has access to a file."""
+ file = self.mydom.getFile(fileId)
+ if not file:
+ return False
+
+ if file.get("_mandateId") != self._mandateId:
+ logger.warning(f"File {fileId} does not belong to mandate {self._mandateId}")
+ return False
+
+ return True
+
# Singleton factory for the WorkflowManager
_workflowManagers = {}
_workflowManagerLastAccess = {} # Track last access time for cleanup
-def getWorkflowManager(mandateId: int = 0, userId: int = 0) -> WorkflowManager:
- """
- Returns a WorkflowManager for the specified context.
- Reuses existing instances but implements cleanup for inactive instances.
-
- Args:
- mandateId: ID of the mandate
- userId: ID of the user
-
- Returns:
- WorkflowManager instance
- """
- contextKey = f"{mandateId}_{userId}"
- current_time = datetime.now()
-
- # Update last access time
- _workflowManagerLastAccess[contextKey] = current_time
-
- # Cleanup old instances (older than 1 hour)
- cleanup_threshold = current_time - timedelta(hours=1)
- for key in list(_workflowManagers.keys()):
- if _workflowManagerLastAccess.get(key, current_time) < cleanup_threshold:
- del _workflowManagers[key]
- del _workflowManagerLastAccess[key]
-
- if contextKey not in _workflowManagers:
- _workflowManagers[contextKey] = WorkflowManager(mandateId, userId)
- return _workflowManagers[contextKey]
+def getWorkflowManager(_mandateId: str = '', _userId: str = '') -> WorkflowManager:
+ """Get a workflow manager instance with the specified context."""
+ return WorkflowManager(_mandateId=_mandateId, _userId=_userId)
-def cleanupWorkflowManager(mandateId: int, userId: int) -> None:
+def cleanupWorkflowManager(_mandateId: int, _userId: int) -> None:
"""
Explicitly cleanup a WorkflowManager instance.
Args:
- mandateId: ID of the mandate
- userId: ID of the user
+ _mandateId: ID of the mandate
+ _userId: ID of the user
"""
- contextKey = f"{mandateId}_{userId}"
+ contextKey = f"{_mandateId}_{_userId}"
if contextKey in _workflowManagers:
del _workflowManagers[contextKey]
if contextKey in _workflowManagerLastAccess:
diff --git a/notes/changelog.txt b/notes/changelog.txt
index 056a465e..f225346e 100644
--- a/notes/changelog.txt
+++ b/notes/changelog.txt
@@ -1,5 +1,12 @@
....................... TASKS
+for all created records
+- to add _createdAt (datetime) and _modifiedAt (datetime), initially _createdAt=_modifiedAt
+
+for all updated records
+- to update attribute _modifiedAt
+
+
! function callAI() to ask with userPrompt,systemPrompt optional), not with json
! in the taskplan to refer files always in context of user/mandate
! userinput to handle with object AgentQuery --> when received in frontend to enhance for full object