db items serialized, uuid overall, auth enhanced

This commit is contained in:
ValueOn AG 2025-05-18 22:25:26 +02:00
parent 16338a6f94
commit 40f82a3848
26 changed files with 1563 additions and 897 deletions

9
app.py
View file

@ -57,17 +57,14 @@ initLogging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL") 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 # Define lifespan context manager for application startup/shutdown events
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Startup logic (if any) # Startup logic
logger.info("Application is starting up") logger.info("Application is starting up")
yield yield
# Shutdown logic # Shutdown logic
logger.info("Application has been shut down") logger.info("Application has been shut down")

View file

@ -5,11 +5,11 @@ APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8000 APP_API_URL = http://localhost:8000
# Database Configuration System # Database Configuration Gateway
DB_SYSTEM_HOST=D:/Temp/_powerondb DB_GATEWAY_HOST=D:/Temp/_powerondb
DB_SYSTEM_DATABASE=system DB_GATEWAY_DATABASE=gateway
DB_SYSTEM_USER=dev_user DB_GATEWAY_USER=dev_user
DB_SYSTEM_PASSWORD_SECRET=dev_password DB_GATEWAY_PASSWORD_SECRET=dev_password
# Database Configuration LucyDOM # Database Configuration LucyDOM
DB_LUCYDOM_HOST=D:/Temp/_powerondb DB_LUCYDOM_HOST=D:/Temp/_powerondb

View file

@ -5,11 +5,11 @@ APP_ENV_TYPE = prod
APP_ENV_LABEL = Production Instance APP_ENV_LABEL = Production Instance
APP_API_URL = https://gateway.poweron-center.net APP_API_URL = https://gateway.poweron-center.net
# Database Configuration System # Database Configuration Gateway
DB_SYSTEM_HOST=/home/_powerondb DB_GATEWAY_HOST=/home/_powerondb
DB_SYSTEM_DATABASE=system DB_GATEWAY_DATABASE=gateway
DB_SYSTEM_USER=dev_user DB_GATEWAY_USER=dev_user
DB_SYSTEM_PASSWORD_SECRET=prod_password DB_GATEWAY_PASSWORD_SECRET=prod_password
# Database Configuration LucyDOM # Database Configuration LucyDOM
DB_LUCYDOM_HOST=/home/_powerondb DB_LUCYDOM_HOST=/home/_powerondb

View file

@ -2,6 +2,8 @@ import json
import os import os
from typing import List, Dict, Any, Optional, Union from typing import List, Dict, Any, Optional, Union
import logging import logging
from datetime import datetime
import uuid
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -9,19 +11,28 @@ class DatabaseConnector:
""" """
A connector for JSON-based data storage. A connector for JSON-based data storage.
Provides generic database operations without user/mandate filtering. 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, 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 # Store the input parameters
self.dbHost = dbHost self.dbHost = dbHost
self.dbDatabase = dbDatabase self.dbDatabase = dbDatabase
self.dbUser = dbUser self.dbUser = dbUser
self.dbPassword = dbPassword self.dbPassword = dbPassword
self.skipInitialIdLookup = skipInitialIdLookup
# Check if context parameters are set # Check if context parameters are set
if mandateId is None or userId is None: if _mandateId is None and _userId is None:
raise ValueError("mandateId and userId must be set") # 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 # Ensure the database directory exists
self.dbFolder = os.path.join(self.dbHost, self.dbDatabase) self.dbFolder = os.path.join(self.dbHost, self.dbDatabase)
@ -29,34 +40,33 @@ class DatabaseConnector:
# Cache for loaded data # Cache for loaded data
self._tablesCache = {} self._tablesCache = {}
self._tableMetadataCache = {} # Cache for table metadata (record IDs, etc.)
# Initialize system table # Initialize system table
self._systemTableName = "_system" self._systemTableName = "_system"
self._initializeSystemTable() self._initializeSystemTable()
# Temporarily store mandateId and userId # If IDs are empty and we're not skipping lookup, try to use initial IDs
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 not skipInitialIdLookup: if not skipInitialIdLookup:
if mandateId == 0: self._resolveInitialIds()
initialMandateId = self.getInitialId("mandates")
if initialMandateId is not None:
self._mandateId = initialMandateId
logger.info(f"Using initial mandateId: {initialMandateId} instead of 0")
if userId == 0: logger.debug(f"Context: _mandateId={self._mandateId}, _userId={self._userId}")
initialUserId = self.getInitialId("users")
if initialUserId is not None: def _resolveInitialIds(self):
self._userId = initialUserId """
logger.info(f"Using initial userId: {initialUserId} instead of 0") 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 if not self._userId:
self.mandateId = self._mandateId initialUserId = self.getInitialId("users")
self.userId = self._userId if initialUserId is not None:
self._userId = initialUserId
logger.debug(f"Context: mandateId={self.mandateId}, userId={self.userId}") logger.info(f"Using initial _userId: {initialUserId}")
def _initializeSystemTable(self): def _initializeSystemTable(self):
"""Initializes the system table if it doesn't exist yet.""" """Initializes the system table if it doesn't exist yet."""
@ -70,7 +80,7 @@ class DatabaseConnector:
self._loadSystemTable() self._loadSystemTable()
logger.debug(f"Existing system table loaded from {systemTablePath}") 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.""" """Loads the system table with the initial IDs."""
# Check if system table is in cache # Check if system table is in cache
if f"_{self._systemTableName}" in self._tablesCache: if f"_{self._systemTableName}" in self._tablesCache:
@ -92,7 +102,7 @@ class DatabaseConnector:
self._tablesCache[f"_{self._systemTableName}"] = {} self._tablesCache[f"_{self._systemTableName}"] = {}
return {} 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.""" """Saves the system table with the initial IDs."""
systemTablePath = self._getTablePath(self._systemTableName) systemTablePath = self._getTablePath(self._systemTableName)
try: try:
@ -106,76 +116,111 @@ class DatabaseConnector:
return False return False
def _getTablePath(self, table: str) -> str: def _getTablePath(self, table: str) -> str:
"""Returns the full path to a table file""" """Returns the full path to a table folder"""
return os.path.join(self.dbFolder, f"{table}.json") return os.path.join(self.dbFolder, table)
def _loadTable(self, table: str) -> List[Dict[str, Any]]: def _getRecordPath(self, table: str, recordId: Union[str, int]) -> str:
"""Loads a table from the corresponding JSON file""" """Returns the full path to a record file"""
path = self._getTablePath(table) 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 the table is the system table, load it directly
if table == self._systemTableName: 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 the table is already in the cache, use the cache
if table in self._tablesCache: if table in self._tablesCache:
return self._tablesCache[table] return self._tablesCache[table]
# Otherwise load the file # Load metadata first
try: metadata = self._loadTableMetadata(table)
if os.path.exists(path): records = []
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f) # Load each record
self._tablesCache[table] = data for recordId in metadata["recordIds"]:
record = self._loadRecord(table, recordId)
# If data was loaded and no initial ID is registered yet, if record:
# register the ID of the first record (if available) records.append(record)
if data and not self.hasInitialId(table):
if "id" in data[0]: self._tablesCache[table] = records
self._registerInitialId(table, data[0]["id"]) return records
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 []
def _saveTable(self, table: str, data: List[Dict[str, Any]]) -> bool: 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 # The system table is handled specially
if table == self._systemTableName: if table == self._systemTableName:
return False return self._saveSystemTable(data)
path = self._getTablePath(table) tablePath = self._getTablePath(table)
try: try:
# Check if directory exists and is writable # Ensure table directory exists
dir_path = os.path.dirname(path) os.makedirs(tablePath, exist_ok=True)
if not os.path.exists(dir_path):
logger.error(f"Directory does not exist: {dir_path}") # Save each record as a separate file
return False for record in data:
if not os.access(dir_path, os.W_OK): if "id" not in record:
logger.error(f"Directory is not writable: {dir_path}") logger.error(f"Record missing ID in table {table}")
return False continue
# Check if file exists and is writable recordPath = self._getRecordPath(table, record["id"])
if os.path.exists(path) and not os.access(path, os.W_OK): with open(recordPath, 'w', encoding='utf-8') as f:
logger.error(f"File exists but is not writable: {path}") json.dump(record, f, indent=2, ensure_ascii=False)
return False
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, 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 # Update the cache
self._tablesCache[table] = data self._tablesCache[table] = data
logger.debug(f"Successfully saved table {table}") 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 type: {type(e).__name__}")
logger.error(f"Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No details available'}") logger.error(f"Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No details available'}")
return False return False
def _applyRecordFilter(self, records: List[Dict[str, Any]], recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]: def _applyRecordFilter(self, records: List[Dict[str, Any]], recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Applies a record filter to the records""" """Applies a record filter to the records"""
if not recordFilter: if not recordFilter:
@ -202,19 +247,12 @@ class DatabaseConnector:
match = False match = False
break break
# Handle type conversion for integer comparisons both ways # Convert both values to strings for comparison
if isinstance(value, int) and isinstance(record[field], str) and record[field].isdigit(): recordValue = str(record[field])
# Filter value is int, record value is string filterValue = str(value)
if value != int(record[field]):
match = False # Direct string comparison
break if recordValue != filterValue:
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:
match = False match = False
break break
@ -223,7 +261,7 @@ class DatabaseConnector:
return filteredRecords 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.""" """Registers the initial ID for a table."""
try: try:
systemData = self._loadSystemTable() systemData = self._loadSystemTable()
@ -255,6 +293,41 @@ class DatabaseConnector:
logger.error(f"Error removing initial ID for table {table}: {e}") logger.error(f"Error removing initial ID for table {table}: {e}")
return False 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 # Public API
def getTables(self) -> List[str]: def getTables(self) -> List[str]:
@ -262,10 +335,10 @@ class DatabaseConnector:
tables = [] tables = []
try: try:
for filename in os.listdir(self.dbFolder): for item in os.listdir(self.dbFolder):
if filename.endswith('.json') and not filename.startswith('_'): itemPath = os.path.join(self.dbFolder, item)
tableName = filename[:-5] # Remove the .json extension if os.path.isdir(itemPath) and not item.startswith('_'):
tables.append(tableName) tables.append(item)
except Exception as e: except Exception as e:
logger.error(f"Error reading the database directory: {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]]: 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.""" """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 # Apply recordFilter if available
if recordFilter: if recordFilter:
data = self._applyRecordFilter(data, recordFilter) records = self._applyRecordFilter(records, recordFilter)
# If fieldFilter is available, reduce the fields # If fieldFilter is available, reduce the fields
if fieldFilter and isinstance(fieldFilter, list): if fieldFilter and isinstance(fieldFilter, list):
result = [] result = []
for record in data: for record in records:
filteredRecord = {} filteredRecord = {}
for field in fieldFilter: for field in fieldFilter:
if field in record: if field in record:
@ -323,92 +406,159 @@ class DatabaseConnector:
result.append(filteredRecord) result.append(filteredRecord)
return result return result
return data return records
def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]: def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a new record in the table.""" """Creates a new record in the specified table."""
data = self._loadTable(table) try:
# Ensure table directory exists
# Add mandateId and userId if not present if not self._ensureTableDirectory(table):
if "mandateId" not in recordData or recordData["mandateId"] == 0: raise ValueError(f"Error creating table directory for {table}")
recordData["mandateId"] = self.mandateId
# Load table metadata
if "userId" not in recordData or recordData["userId"] == 0: metadata = self._loadTableMetadata(table)
recordData["userId"] = self.userId
# Generate new ID if not provided
# Determine the next ID if not present if "id" not in recordData:
if "id" not in recordData: recordData["id"] = str(uuid.uuid4())
nextId = 1 else:
if data: # Ensure ID is a string
nextId = max(record["id"] for record in data if "id" in record) + 1 recordData["id"] = str(recordData["id"])
recordData["id"] = nextId
# Add context fields
# If the table is empty and a system ID should be registered recordData["_mandateId"] = self._mandateId
if not data: recordData["_userId"] = self._userId
self._registerInitialId(table, recordData["id"])
logger.info(f"Initial ID {recordData['id']} for table {table} has been registered") # Update metadata
if "recordIds" not in metadata:
# Add the new record metadata["recordIds"] = []
data.append(recordData) metadata["recordIds"].append(recordData["id"])
metadata["recordIds"].sort()
# Save the updated table
if self._saveTable(table, data): # 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 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}") 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.""" """Deletes a record from the table."""
data = self._loadTable(table) # Load metadata
metadata = self._loadTableMetadata(table)
# Search for the record if recordId not in metadata["recordIds"]:
for i, record in enumerate(data): return False
if "id" in record and record["id"] == recordId:
# Check if it's an initial record # Check if it's an initial record
initialId = self.getInitialId(table) initialId = self.getInitialId(table)
if initialId is not None and initialId == recordId: if initialId is not None and initialId == recordId:
self._removeInitialId(table) self._removeInitialId(table)
logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table") logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
# Delete the record # Delete the record file
del data[i] recordPath = self._getRecordPath(table, recordId)
try:
# Save the updated table if os.path.exists(recordPath):
return self._saveTable(table, data) 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 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.""" """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 # Ensure recordId is a string
for i, record in enumerate(data): recordId = str(recordId)
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}")
# Record not found if recordId not in metadata["recordIds"]:
raise ValueError(f"Record with ID {recordId} not found in table {table}") 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: def hasInitialId(self, table: str) -> bool:
"""Checks if an initial ID is registered for a table.""" """Checks if an initial ID is registered for a table."""
systemData = self._loadSystemTable() systemData = self._loadSystemTable()
return table in systemData 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.""" """Returns the initial ID for a table."""
systemData = self._loadSystemTable() systemData = self._loadSystemTable()
initialId = systemData.get(table) initialId = systemData.get(table)
@ -417,7 +567,7 @@ class DatabaseConnector:
logger.debug(f"No initial ID found for table {table}") logger.debug(f"No initial ID found for table {table}")
return initialId return initialId
def getAllInitialIds(self) -> Dict[str, int]: def getAllInitialIds(self) -> Dict[str, str]:
"""Returns all registered initial IDs.""" """Returns all registered initial IDs."""
systemData = self._loadSystemTable() systemData = self._loadSystemTable()
return systemData.copy() # Return a copy to protect the original return systemData.copy() # Return a copy to protect the original

View file

@ -5,7 +5,7 @@ Manages user access and permissions.
from typing import Dict, Any, List, Optional 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 Unified user access management function that filters data based on user privileges
and adds access control attributes. 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 currentUser: Current user information dictionary
table: Name of the table table: Name of the table
recordset: Recordset to filter based on access rules recordset: Recordset to filter based on access rules
mandateId: Current mandate ID _mandateId: Current mandate ID
userId: Current user ID _userId: Current user ID
db: Database connector instance db: Database connector instance
Returns: 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 filtered_records = recordset # System admins see all records
elif userPrivilege == "admin": elif userPrivilege == "admin":
# Admins see records in their mandate # 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 else: # Regular users
# Users only see records they own within their mandate # Users only see records they own within their mandate
filtered_records = [r for r in recordset 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 # Add access control attributes to each record
for record in filtered_records: 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 # Set access control flags based on user permissions
if table == "mandates": if table == "mandates":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = 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) record["_hideDelete"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db)
elif table == "users": elif table == "users":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = 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) record["_hideDelete"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db)
else: else:
# Default access control for other tables # Default access control for other tables
record["_hideView"] = False record["_hideView"] = False
record["_hideEdit"] = 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) record["_hideDelete"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db)
return filtered_records 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. 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 currentUser: Current user information dictionary
table: Name of the table table: Name of the table
recordId: Optional record ID for specific record check recordId: Optional record ID for specific record check
mandateId: Current mandate ID _mandateId: Current mandate ID
userId: Current user ID _userId: Current user ID
db: Database connector instance db: Database connector instance
Returns: Returns:
@ -87,15 +87,15 @@ def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int]
record = records[0] record = records[0]
# Admins can modify anything in their mandate # 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 # Exception: Can't modify Root mandate unless you are a sysadmin
if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin": if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin":
return False return False
return True return True
# Users can only modify their own records # Users can only modify their own records
if (record.get("mandateId") == mandateId and if (record.get("_mandateId") == _mandateId and
record.get("userId") == userId): record.get("_userId") == _userId):
return True return True
return False return False

View file

@ -25,11 +25,11 @@ class GatewayInterface:
Manages users and mandates. 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.""" """Initializes the Gateway Interface with optional mandate and user context."""
# Context can be empty during initialization # Context can be empty during initialization
self.mandateId = mandateId self._mandateId = _mandateId
self.userId = userId self._userId = _userId
# Initialize database # Initialize database
self._initializeDatabase() self._initializeDatabase()
@ -40,41 +40,47 @@ class GatewayInterface:
# Initialize standard records if needed # Initialize standard records if needed
self._initRecords() 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): def _initializeDatabase(self):
"""Initializes the database connection.""" """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( self.db = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"), dbHost=dbHost,
dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"), dbDatabase=dbDatabase,
dbUser=APP_CONFIG.get("DB_SYSTEM_USER"), dbUser=dbUser,
dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"), dbPassword=dbPassword,
mandateId=self.mandateId if self.mandateId else 0, _mandateId=self._mandateId,
userId=self.userId if self.userId else 0 _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): def _initRecords(self):
"""Initializes standard records in the database if they don't exist.""" """Initializes standard records in the database if they don't exist."""
self._initRootMandate() self._initRootMandate()
self._initAdminUser() 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): def _initRootMandate(self):
"""Creates the Root mandate if it doesn't exist.""" """Creates the Root mandate if it doesn't exist."""
@ -89,8 +95,11 @@ class GatewayInterface:
createdMandate = self.db.recordCreate("mandates", rootMandate) createdMandate = self.db.recordCreate("mandates", rootMandate)
logger.info(f"Root mandate created with ID {createdMandate['id']}") logger.info(f"Root mandate created with ID {createdMandate['id']}")
# Register the initial ID
self.db._registerInitialId("mandates", createdMandate['id'])
# Update mandate context # Update mandate context
self.mandateId = createdMandate['id'] self._mandateId = createdMandate['id']
def _initAdminUser(self): def _initAdminUser(self):
"""Creates the Admin user if it doesn't exist.""" """Creates the Admin user if it doesn't exist."""
@ -99,7 +108,7 @@ class GatewayInterface:
if existingUserId is None or not users: if existingUserId is None or not users:
logger.info("Creating Admin user") logger.info("Creating Admin user")
adminUser = { adminUser = {
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"username": "admin", "username": "admin",
"email": "admin@example.com", "email": "admin@example.com",
"fullName": "Administrator", "fullName": "Administrator",
@ -111,8 +120,11 @@ class GatewayInterface:
createdUser = self.db.recordCreate("users", adminUser) createdUser = self.db.recordCreate("users", adminUser)
logger.info(f"Admin user created with ID {createdUser['id']}") logger.info(f"Admin user created with ID {createdUser['id']}")
# Register the initial ID
self.db._registerInitialId("users", createdUser['id'])
# Update user context # 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]]: def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
""" """
@ -126,9 +138,9 @@ class GatewayInterface:
Returns: Returns:
Filtered recordset with access control attributes 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. Checks if the current user can modify (create/update/delete) records in a table.
@ -139,9 +151,9 @@ class GatewayInterface:
Returns: Returns:
Boolean indicating permission 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.""" """Returns the initial ID for a table."""
return self.db.getInitialId(table) return self.db.getInitialId(table)
@ -165,7 +177,7 @@ class GatewayInterface:
allMandates = self.db.getRecordset("mandates") allMandates = self.db.getRecordset("mandates")
return self._uam("mandates", allMandates) 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.""" """Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId}) mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
if not mandates: if not mandates:
@ -186,7 +198,7 @@ class GatewayInterface:
return self.db.recordCreate("mandates", mandateData) 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.""" """Updates a mandate if user has access."""
# Check if the mandate exists and user has access # Check if the mandate exists and user has access
mandate = self.getMandate(mandateId) mandate = self.getMandate(mandateId)
@ -199,7 +211,7 @@ class GatewayInterface:
# Update the mandate # Update the mandate
return self.db.recordModify("mandates", mandateId, mandateData) 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. Deletes a mandate and all associated users and data if user has permission.
""" """
@ -248,15 +260,15 @@ class GatewayInterface:
return filteredUsers 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.""" """Returns users for a specific mandate if user has access."""
# First check if user has access to the mandate # First check if user has access to the mandate
mandate = self.getMandate(mandateId) mandate = self.getMandate(_mandateId)
if not mandate: if not mandate:
return [] return []
# Get users for this mandate # 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) filteredUsers = self._uam("users", users)
# Remove password hashes # Remove password hashes
@ -268,6 +280,7 @@ class GatewayInterface:
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]: def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
"""Returns a user by username.""" """Returns a user by username."""
# Get all users without mandate filter
users = self.db.getRecordset("users") users = self.db.getRecordset("users")
for user in users: for user in users:
if user.get("username") == username: if user.get("username") == username:
@ -278,9 +291,9 @@ class GatewayInterface:
logger.debug(f"No user found with username {username}") logger.debug(f"No user found with username {username}")
return None 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.""" """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: if not users:
return None return None
@ -299,31 +312,58 @@ class GatewayInterface:
return user return user
def createUser(self, username: str, password: str, email: str = None, 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]: disabled: bool = False, privilege: str = "user") -> Dict[str, Any]:
"""Creates a new user if current user has permission.""" """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 # Check if the username already exists
existingUser = self.getUserByUsername(username) existingUser = self.getUserByUsername(username)
if existingUser: if existingUser:
raise ValueError(f"User '{username}' already exists") raise ValueError(f"Benutzer '{username}' existiert bereits")
# Use the provided mandateId or the current context # Use the provided _mandateId or the current context
userMandateId = mandateId if mandateId is not None else self.mandateId userMandateId = _mandateId if _mandateId is not None else self._mandateId
# Check if user has access to the mandate # Check if user has access to the mandate
if userMandateId != self.mandateId and self.currentUser.get("privilege") != "sysadmin": if userMandateId != self._mandateId and self.currentUser.get("privilege") != "sysadmin":
raise PermissionError(f"No permission to create users in mandate {userMandateId}") raise PermissionError(f"Keine Berechtigung, Benutzer in Mandat {userMandateId} zu erstellen")
if not self._canModify("users"): if not self._canModify("users"):
raise PermissionError("No permission to create users") raise PermissionError("Keine Berechtigung, Benutzer zu erstellen")
# Check privilege escalation # Check privilege escalation
if (privilege == "sysadmin" or if (privilege == "sysadmin" or
(privilege == "admin" and self.currentUser.get("privilege") == "user")): (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 = { userData = {
"mandateId": userMandateId, "_mandateId": userMandateId,
"username": username, "username": username,
"email": email, "email": email,
"fullName": fullName, "fullName": fullName,
@ -335,6 +375,10 @@ class GatewayInterface:
createdUser = self.db.recordCreate("users", userData) 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 the complete user record
return createdUser return createdUser
@ -344,9 +388,8 @@ class GatewayInterface:
if "users" in self.db._tablesCache: if "users" in self.db._tablesCache:
del self.db._tablesCache["users"] del self.db._tablesCache["users"]
# Get fresh user data # Get user by username
users = self.db.getRecordset("users") user = self.getUserByUsername(username)
user = next((u for u in users if u.get("username") == username), None)
if not user: if not user:
raise ValueError("Benutzer nicht gefunden") raise ValueError("Benutzer nicht gefunden")
@ -366,19 +409,19 @@ class GatewayInterface:
return authenticatedUser 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.""" """Updates a user if current user has permission."""
# Check if the user exists and current user has access # Check if the user exists and current user has access
user = self.getUser(userId) user = self.getUser(_userId)
if not user: if not user:
# Try to get the raw user record for admin access check # 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: 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 # Check if current user is admin/sysadmin
if not self._canModify("users", userId): if not self._canModify("users", _userId):
raise PermissionError(f"No permission to update user {userId}") raise PermissionError(f"No permission to update user {_userId}")
user = users[0] user = users[0]
@ -397,7 +440,7 @@ class GatewayInterface:
del userData["password"] del userData["password"]
# Update the user # Update the user
updatedUser = self.db.recordModify("users", userId, userData) updatedUser = self.db.recordModify("users", _userId, userData)
# Remove password hash from the response # Remove password hash from the response
if "hashedPassword" in updatedUser: if "hashedPassword" in updatedUser:
@ -405,53 +448,53 @@ class GatewayInterface:
return updatedUser 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.""" """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.""" """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.""" """Deletes all data associated with a user."""
# Delete user attributes # Delete user attributes
try: try:
attributes = self.db.getRecordset("attributes", recordFilter={"userId": userId}) attributes = self.db.getRecordset("attributes", recordFilter={"_userId": _userId})
for attribute in attributes: for attribute in attributes:
self.db.recordDelete("attributes", attribute["id"]) self.db.recordDelete("attributes", attribute["id"])
except Exception as e: 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.""" """Deletes a user and all associated data if current user has permission."""
# Check if the user exists # Check if the user exists
users = self.db.getRecordset("users", recordFilter={"id": userId}) users = self.db.getRecordset("users", recordFilter={"_userId": _userId})
if not users: if not users:
return False return False
# Check if current user has permission # Check if current user has permission
if not self._canModify("users", userId): if not self._canModify("users", _userId):
raise PermissionError(f"No permission to delete user {userId}") raise PermissionError(f"No permission to delete user {_userId}")
# Check if it's the initial user # Check if it's the initial user
initialUserId = self.getInitialId("users") 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") logger.warning("Attempt to delete the Root Admin was prevented")
return False return False
# Delete all data associated with the user # Delete all data associated with the user
self._deleteUserReferencedData(userId) self._deleteUserReferencedData(_userId)
# Delete the user # Delete the user
success = self.db.recordDelete("users", userId) success = self.db.recordDelete("users", _userId)
if success: if success:
logger.info(f"User with ID {userId} was successfully deleted") logger.info(f"User with ID {_userId} was successfully deleted")
else: else:
logger.error(f"Error deleting user with ID {userId}") logger.error(f"Error deleting user with ID {_userId}")
return success return success
@ -459,15 +502,16 @@ class GatewayInterface:
# Singleton factory for GatewayInterface instances per context # Singleton factory for GatewayInterface instances per context
_gatewayInterfaces = {} _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. Returns a GatewayInterface instance for the specified context.
Reuses existing instances. 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: if contextKey not in _gatewayInterfaces:
_gatewayInterfaces[contextKey] = GatewayInterface(mandateId, userId) _gatewayInterfaces[contextKey] = GatewayInterface(_mandateId or '', _userId or '')
return _gatewayInterfaces[contextKey] return _gatewayInterfaces[contextKey]
# Initialize an instance # Initialize an instance with empty strings
getGatewayInterface() getGatewayInterface('', '')

View file

@ -4,6 +4,7 @@ Data models for the gateway system.
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
import uuid
class Label(BaseModel): class Label(BaseModel):
@ -20,7 +21,7 @@ class Label(BaseModel):
class Mandate(BaseModel): class Mandate(BaseModel):
"""Data model for a mandate""" """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") name: str = Field(description="Name of the mandate")
language: str = Field(description="Default language of the mandate") language: str = Field(description="Default language of the mandate")
@ -38,8 +39,7 @@ class Mandate(BaseModel):
class User(BaseModel): class User(BaseModel):
"""Data model for a user""" """Data model for a user"""
id: int = Field(description="Unique ID of the user") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
mandateId: int = Field(description="ID of the associated mandate")
username: str = Field(description="Username for login") username: str = Field(description="Username for login")
email: Optional[str] = Field(None, description="Email address of the user") email: Optional[str] = Field(None, description="Email address of the user")
fullName: Optional[str] = Field(None, description="Full name of the user") fullName: Optional[str] = Field(None, description="Full name of the user")
@ -99,5 +99,5 @@ class Token(BaseModel):
class TokenData(BaseModel): class TokenData(BaseModel):
"""Data for token decoding and validation""" """Data for token decoding and validation"""
username: Optional[str] = None username: Optional[str] = None
mandateId: Optional[int] = None mandateId: Optional[str] = None
exp: Optional[datetime] = None exp: Optional[datetime] = None

View file

@ -11,11 +11,11 @@ class LucyDOMAccess:
Handles user access management and permission checks. 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.""" """Initialize with user context."""
self.currentUser = currentUser self.currentUser = currentUser
self.mandateId = mandateId self._mandateId = _mandateId
self.userId = userId self._userId = _userId
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 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 filtered_records = recordset # System admins see all records
elif userPrivilege == "admin": elif userPrivilege == "admin":
# Admins see records in their mandate # 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 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": if table == "prompts":
filtered_records = [r for r in recordset if filtered_records = [r for r in recordset if r.get("_mandateId") == self._mandateId]
(r.get("mandateId") == self.mandateId and r.get("userId") == self.userId)
or
(r.get("mandateId") == 0)
]
else: else:
# Users see only their records # Users see only their records for other tables
filtered_records = [r for r in recordset 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 # Add access control attributes to each record
for record in filtered_records: for record in filtered_records:
@ -58,8 +54,14 @@ class LucyDOMAccess:
# Set access control flags based on user permissions # Set access control flags based on user permissions
if table == "prompts": if table == "prompts":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("prompts", record_id) # Only allow modification of own prompts or if admin/sysadmin
record["_hideDelete"] = not self._canModify("prompts", record_id) 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": elif table == "files":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("files", record_id) record["_hideEdit"] = not self._canModify("files", record_id)
@ -112,12 +114,12 @@ class LucyDOMAccess:
record = records[0] record = records[0]
# Admins can modify anything in their mandate # 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 return True
# Regular users can only modify their own records # Regular users can only modify their own records
if (record.get("mandateId") == self.mandateId and if (record.get("_mandateId") == self._mandateId and
record.get("userId") == self.userId): record.get("_userId") == self._userId):
return True return True
return False return False

View file

@ -24,6 +24,24 @@ from modules.connectors.connectorAiOpenai import ChatService
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__) 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 # Custom exceptions for file handling
class FileError(Exception): class FileError(Exception):
"""Base class for file handling exceptions.""" """Base class for file handling exceptions."""
@ -45,6 +63,7 @@ class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file.""" """Exception raised when there's an error deleting a file."""
pass pass
from modules.security.auth import getInitialContext
class LucyDOMInterface: class LucyDOMInterface:
""" """
@ -52,14 +71,19 @@ class LucyDOMInterface:
Uses the JSON connector for data access. 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.""" """Initializes the LucyDOM Interface with mandate and user context."""
self.mandateId = mandateId logger.debug(f"Initializing LucyDOMInterface with mandateId={_mandateId}, userId={_userId}")
self.userId = userId self._mandateId = _mandateId
self._userId = _userId
# Add language settings # Add language settings
self.userLanguage = "en" # Default user language 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 # Initialize database connector
self._initializeDatabase() self._initializeDatabase()
@ -68,10 +92,22 @@ class LucyDOMInterface:
self.currentUser = self._getCurrentUserInfo() self.currentUser = self._getCurrentUserInfo()
# Initialize access control # 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 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() self._initRecords()
def _getCurrentUserInfo(self) -> Dict[str, Any]: def _getCurrentUserInfo(self) -> Dict[str, Any]:
@ -79,74 +115,64 @@ class LucyDOMInterface:
# For production, you would get this from authentication # For production, you would get this from authentication
# For now return basic user info with default privilege # For now return basic user info with default privilege
return { return {
"id": self.userId, "id": self._userId,
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"privilege": "user", # Default privilege level "privilege": "user", # Default privilege level
"language": self.userLanguage "language": self.userLanguage
} }
def _initializeDatabase(self): def _initializeDatabase(self):
"""Initializes the database connection.""" """Initializes the database connection."""
effectiveMandateId = self.mandateId
effectiveUserId = self.userId
if effectiveMandateId is None or effectiveUserId is None:
return
self.db = DatabaseConnector( self.db = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_LUCYDOM_HOST"), dbHost=APP_CONFIG.get("DB_LUCYDOM_HOST"),
dbDatabase=APP_CONFIG.get("DB_LUCYDOM_DATABASE"), dbDatabase=APP_CONFIG.get("DB_LUCYDOM_DATABASE"),
dbUser=APP_CONFIG.get("DB_LUCYDOM_USER"), dbUser=APP_CONFIG.get("DB_LUCYDOM_USER"),
dbPassword=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"), dbPassword=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"),
mandateId=self.mandateId, _mandateId=self._mandateId,
userId=self.userId, _userId=self._userId,
skipInitialIdLookup=True skipInitialIdLookup=True
) )
def _initRecords(self): def _initRecords(self):
"""Initializes standard records in the database if they don't exist.""" """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): def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist.""" """Creates standard prompts if they don't exist."""
prompts = self.db.getRecordset("prompts") prompts = self.db.getRecordset("prompts")
logger.debug(f"Found {len(prompts)} existing prompts")
if not prompts: if not prompts:
logger.info("Creating standard prompts") logger.debug("Creating standard prompts")
# Define standard prompts # Define standard prompts
standardPrompts = [ 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.", "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" "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.", "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" "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.", "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" "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.", "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" "name": "Design: UI/UX Design"
}, },
{ {
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Gib mir die ersten 1000 Primzahlen", "content": "Gib mir die ersten 1000 Primzahlen",
"name": "Code: 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.", "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" "name": "Mail: Vorbereitung"
}, },
@ -155,13 +181,15 @@ class LucyDOMInterface:
# Create prompts # Create prompts
for promptData in standardPrompts: for promptData in standardPrompts:
createdPrompt = self.db.recordCreate("prompts", promptData) 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]]: def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module.""" """Delegate to access control module."""
return self.access._uam(table, recordset) 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.""" """Delegate to access control module."""
return self.access._canModify(table, recordId) return self.access._canModify(table, recordId)
@ -170,7 +198,7 @@ class LucyDOMInterface:
def setUserLanguage(self, languageCode: str): def setUserLanguage(self, languageCode: str):
"""Set the user's preferred language""" """Set the user's preferred language"""
self.userLanguage = languageCode self.userLanguage = languageCode
logger.info(f"User language set to: {languageCode}") logger.debug(f"User language set to: {languageCode}")
# AI Call Root Function # AI Call Root Function
@ -208,7 +236,7 @@ class LucyDOMInterface:
# Utilities # Utilities
def getInitialId(self, table: str) -> Optional[int]: def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table.""" """Returns the initial ID for a table."""
return self.db.getInitialId(table) return self.db.getInitialId(table)
@ -223,7 +251,7 @@ class LucyDOMInterface:
allPrompts = self.db.getRecordset("prompts") allPrompts = self.db.getRecordset("prompts")
return self._uam("prompts", allPrompts) 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.""" """Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId}) prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
if not prompts: if not prompts:
@ -238,8 +266,6 @@ class LucyDOMInterface:
raise PermissionError("No permission to create prompts") raise PermissionError("No permission to create prompts")
promptData = { promptData = {
"mandateId": self.mandateId,
"userId": self.userId,
"content": content, "content": content,
"name": name, "name": name,
"createdAt": self._getCurrentTimestamp() "createdAt": self._getCurrentTimestamp()
@ -247,7 +273,7 @@ class LucyDOMInterface:
return self.db.recordCreate("prompts", promptData) 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.""" """Updates a prompt if user has access."""
# Check if the prompt exists and user has access # Check if the prompt exists and user has access
prompt = self.getPrompt(promptId) prompt = self.getPrompt(promptId)
@ -268,7 +294,7 @@ class LucyDOMInterface:
# Update prompt # Update prompt
return self.db.recordModify("prompts", promptId, promptData) 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.""" """Deletes a prompt if user has access."""
# Check if the prompt exists and user has access # Check if the prompt exists and user has access
prompt = self.getPrompt(promptId) 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.""" """Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={ files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash, "fileHash": fileHash,
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"userId": self.userId "_userId": self._userId
}) })
if files: if files:
return files[0] return files[0]
@ -334,7 +360,7 @@ class LucyDOMInterface:
allFiles = self.db.getRecordset("files") allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles) 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.""" """Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId}) files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files: if not files:
@ -349,8 +375,8 @@ class LucyDOMInterface:
raise PermissionError("No permission to create files") raise PermissionError("No permission to create files")
fileData = { fileData = {
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"userId": self.userId, "_userId": self._userId,
"name": name, "name": name,
"mimeType": mimeType, "mimeType": mimeType,
"size": size, "size": size,
@ -359,7 +385,7 @@ class LucyDOMInterface:
} }
return self.db.recordCreate("files", fileData) 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.""" """Updates file metadata if user has access."""
# Check if the file exists and user has access # Check if the file exists and user has access
file = self.getFile(fileId) file = self.getFile(fileId)
@ -372,7 +398,7 @@ class LucyDOMInterface:
# Update file # Update file
return self.db.recordModify("files", fileId, updateData) 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.""" """Deletes a file if user has access."""
try: try:
# Check if the file exists and user has access # Check if the file exists and user has access
@ -396,7 +422,7 @@ class LucyDOMInterface:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId}) fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries: if fileDataEntries:
self.db.recordDelete("fileData", fileId) 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: except Exception as e:
logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}") logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}")
@ -413,7 +439,7 @@ class LucyDOMInterface:
# FileData methods - data operations # 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.""" """Stores the binary data of a file in the database."""
try: try:
import base64 import base64
@ -459,13 +485,13 @@ class LucyDOMInterface:
} }
self.db.recordCreate("fileData", fileDataObj) 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 return True
except Exception as e: except Exception as e:
logger.error(f"Error storing data for file {fileId}: {str(e)}") logger.error(f"Error storing data for file {fileId}: {str(e)}")
return False 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.""" """Returns the binary data of a file if user has access."""
# Check file access # Check file access
file = self.getFile(fileId) file = self.getFile(fileId)
@ -499,7 +525,7 @@ class LucyDOMInterface:
logger.error(f"Error processing file data for {fileId}: {str(e)}") logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None 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.""" """Updates file data if user has access."""
# Check file access # Check file access
file = self.getFile(fileId) file = self.getFile(fileId)
@ -573,12 +599,12 @@ class LucyDOMInterface:
if fileDataEntries: if fileDataEntries:
# Update the existing record # Update the existing record
self.db.recordModify("fileData", fileId, dataUpdate) 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: else:
# Create a new record # Create a new record
dataUpdate["id"] = fileId dataUpdate["id"] = fileId
self.db.recordCreate("fileData", dataUpdate) 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 return True
except Exception as e: except Exception as e:
@ -592,7 +618,7 @@ class LucyDOMInterface:
if not self._canModify("files"): if not self._canModify("files"):
raise PermissionError("No permission to upload 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): if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}") logger.error(f"Invalid fileContent type: {type(fileContent)}")
@ -605,7 +631,7 @@ class LucyDOMInterface:
# Check for duplicate within same user/mandate # Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash) existingFile = self.checkForDuplicateFile(fileHash)
if existingFile: if existingFile:
logger.info(f"Duplicate found for {fileName}: {existingFile['id']}") logger.debug(f"Duplicate found for {fileName}: {existingFile['id']}")
return existingFile return existingFile
# Determine MIME type and size # Determine MIME type and size
@ -613,7 +639,7 @@ class LucyDOMInterface:
fileSize = len(fileContent) fileSize = len(fileContent)
# Save metadata # 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( dbFile = self.createFile(
name=fileName, name=fileName,
mimeType=mimeType, mimeType=mimeType,
@ -622,17 +648,17 @@ class LucyDOMInterface:
) )
# Save binary data # 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) 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 return dbFile
except Exception as e: except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True) logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
raise FileStorageError(f"Error saving file: {str(e)}") 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.""" """Returns a file for download if user has access."""
try: try:
# Check file access # Check file access
@ -667,10 +693,10 @@ class LucyDOMInterface:
allWorkflows = self.db.getRecordset("workflows") allWorkflows = self.db.getRecordset("workflows")
return self._uam("workflows", allWorkflows) 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.""" """Returns workflows for a specific user if current user has access."""
# Get workflows by userId # Get workflows by _userId
workflows = self.db.getRecordset("workflows", recordFilter={"userId": userId}) workflows = self.db.getRecordset("workflows", recordFilter={"_userId": _userId})
# Apply access control # Apply access control
return self._uam("workflows", workflows) return self._uam("workflows", workflows)
@ -690,11 +716,11 @@ class LucyDOMInterface:
raise PermissionError("No permission to create workflows") raise PermissionError("No permission to create workflows")
# Make sure mandateId and userId are set # Make sure mandateId and userId are set
if "mandateId" not in workflowData: if "_mandateId" not in workflowData:
workflowData["mandateId"] = self.mandateId workflowData["_mandateId"] = self._mandateId
if "userId" not in workflowData: if "_userId" not in workflowData:
workflowData["userId"] = self.userId workflowData["_userId"] = self._userId
# Set timestamp if not present # Set timestamp if not present
currentTime = self._getCurrentTimestamp() currentTime = self._getCurrentTimestamp()
@ -877,7 +903,7 @@ class LucyDOMInterface:
# Update the message # Update the message
updatedMessage = self.db.recordModify("workflowMessages", messageId, messageData) updatedMessage = self.db.recordModify("workflowMessages", messageId, messageData)
if updatedMessage: if updatedMessage:
logger.info(f"Message {messageId} updated successfully") logger.debug(f"Message {messageId} updated successfully")
else: else:
logger.warning(f"Failed to update message {messageId}") logger.warning(f"Failed to update message {messageId}")
@ -912,7 +938,7 @@ class LucyDOMInterface:
logger.error(f"Error deleting message {messageId}: {str(e)}") logger.error(f"Error deleting message {messageId}: {str(e)}")
return False 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.""" """Removes a file reference from a message if user has access."""
try: try:
# Check workflow access # Check workflow access
@ -924,7 +950,7 @@ class LucyDOMInterface:
if not self._canModify("workflows", workflowId): if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {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 # Get all workflow messages
allMessages = self.getWorkflowMessages(workflowId) allMessages = self.getWorkflowMessages(workflowId)
@ -951,7 +977,7 @@ class LucyDOMInterface:
return False return False
# Log the found message # 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 # Check if message has documents
if "documents" not in message or not message["documents"]: if "documents" not in message or not message["documents"]:
@ -980,7 +1006,7 @@ class LucyDOMInterface:
if shouldRemove: if shouldRemove:
removed = True 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: else:
updatedDocuments.append(doc) updatedDocuments.append(doc)
@ -997,7 +1023,7 @@ class LucyDOMInterface:
updated = self.db.recordModify("workflowMessages", message["id"], messageUpdate) updated = self.db.recordModify("workflowMessages", message["id"], messageUpdate)
if updated: if updated:
logger.info(f"Successfully removed file {fileId} from message {messageId}") logger.debug(f"Successfully removed file {fileId} from message {messageId}")
return True return True
else: else:
logger.warning(f"Failed to update message {messageId} in database") logger.warning(f"Failed to update message {messageId} in database")
@ -1081,8 +1107,8 @@ class LucyDOMInterface:
# Extract only the database-relevant workflow fields # Extract only the database-relevant workflow fields
workflowDbData = { workflowDbData = {
"id": workflowId, "id": workflowId,
"mandateId": workflow.get("mandateId", self.mandateId), "_mandateId": workflow.get("_mandateId", self._mandateId),
"userId": workflow.get("userId", self.userId), "_userId": workflow.get("_userId", self._userId),
"name": workflow.get("name", f"Workflow {workflowId}"), "name": workflow.get("name", f"Workflow {workflowId}"),
"status": workflow.get("status", "completed"), "status": workflow.get("status", "completed"),
"startedAt": workflow.get("startedAt", self._getCurrentTimestamp()), "startedAt": workflow.get("startedAt", self._getCurrentTimestamp()),
@ -1187,13 +1213,13 @@ class LucyDOMInterface:
messageIds = [msg.get("id") for msg in messages] messageIds = [msg.get("id") for msg in messages]
# Update in database # Update in database
self.updateWorkflow(workflowId, {"messageIds": messageIds}) 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 # Log document counts for each message
for msg in messages: for msg in messages:
docCount = len(msg.get("documents", [])) docCount = len(msg.get("documents", []))
if docCount > 0: 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 # Load logs
logs = self.getWorkflowLogs(workflowId) logs = self.getWorkflowLogs(workflowId)
@ -1218,16 +1244,16 @@ class LucyDOMInterface:
try: try:
# Get token from database using current user's mandateId and userId # Get token from database using current user's mandateId and userId
tokens = self.db.getRecordset("msftTokens", recordFilter={ tokens = self.db.getRecordset("msftTokens", recordFilter={
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"userId": self.userId "_userId": self._userId
}) })
if tokens and len(tokens) > 0: if tokens and len(tokens) > 0:
token_data = json.loads(tokens[0]["token_data"]) 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 return token_data
else: 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 return None
except Exception as e: except Exception as e:
@ -1239,8 +1265,8 @@ class LucyDOMInterface:
try: try:
# Check if token already exists # Check if token already exists
tokens = self.db.getRecordset("msftTokens", recordFilter={ tokens = self.db.getRecordset("msftTokens", recordFilter={
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"userId": self.userId "_userId": self._userId
}) })
if tokens and len(tokens) > 0: if tokens and len(tokens) > 0:
@ -1251,18 +1277,18 @@ class LucyDOMInterface:
"updated_at": datetime.now().isoformat() "updated_at": datetime.now().isoformat()
} }
self.db.recordModify("msftTokens", token_id, updated_data) 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: else:
# Create new token # Create new token with UUID
new_token = { new_token = {
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"userId": self.userId, "_userId": self._userId,
"token_data": json.dumps(token_data), "token_data": json.dumps(token_data),
"created_at": datetime.now().isoformat(), "created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat() "updated_at": datetime.now().isoformat()
} }
self.db.recordCreate("msftTokens", new_token) 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 return True
@ -1273,20 +1299,23 @@ class LucyDOMInterface:
# Singleton factory for LucyDOMInterface instances per context # Singleton factory for LucyDOMInterface instances per context
_lucydomInterfaces = {} _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. 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: if contextKey not in _lucydomInterfaces:
# Create new interface instance _lucydomInterfaces[contextKey] = LucyDOMInterface(_mandateId or '', _userId or '')
interface = LucyDOMInterface(mandateId, userId)
# Initialize AI service
aiService = ChatService()
interface.aiService = aiService
_lucydomInterfaces[contextKey] = interface
return _lucydomInterfaces[contextKey] return _lucydomInterfaces[contextKey]
# Initialize an instance # Initialize default instance with empty strings
getLucydomInterface() getLucydomInterface('', '')

View file

@ -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")

View file

@ -4,8 +4,12 @@ LucyDOM model classes for the workflow and document system.
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
# CORE MODELS
class Label(BaseModel): class Label(BaseModel):
"""Label for an attribute or a class with support for multiple languages""" """Label for an attribute or a class with support for multiple languages"""
default: str default: str
@ -20,9 +24,7 @@ class Label(BaseModel):
class Prompt(BaseModel): class Prompt(BaseModel):
"""Data model for a prompt""" """Data model for a prompt"""
id: int = Field(description="Unique ID of the prompt") id: str = Field(default_factory=lambda: str(uuid.uuid4()), 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") content: str = Field(description="Content of the prompt")
name: str = Field(description="Display name of the prompt") name: str = Field(description="Display name of the prompt")
@ -34,23 +36,18 @@ class Prompt(BaseModel):
# Labels for attributes # Labels for attributes
fieldLabels: Dict[str, Label] = { fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}), "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"}), "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): class FileItem(BaseModel):
"""Data model for a file""" """Data model for a file"""
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")
mandateId: int = Field(description="ID of the associated mandate") mimeType: str = Field(description="Type of the file MIME type")
userId: int = Field(description="ID of the creator") fileName: str = Field(description="Name of the file")
name: str = Field(description="Name of the data object") fileSize: int = Field(description="Size of the file in bytes")
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") 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") workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
label: Label = Field( label: Label = Field(
@ -61,99 +58,88 @@ class FileItem(BaseModel):
# Labels for attributes # Labels for attributes
fieldLabels: Dict[str, Label] = { fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}), "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"}), "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"}), "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"}) "workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"})
} }
class FileData(BaseModel): class FileData(BaseModel):
"""Data model for file content""" """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") 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") 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): class MsftToken(BaseModel):
"""Data model for Microsoft authentication tokens""" """Data model for Microsoft authentication tokens"""
id: int = Field(description="Unique ID of the token") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the token")
mandateId: int = Field(description="ID of the associated mandate") tokenData: str = Field(description="JSON string containing the token data")
userId: int = Field(description="ID of the user") expiresAt: datetime = Field(description="Expiration date and time")
token_data: str = Field(description="JSON string containing the token data") refreshToken: Optional[str] = Field(None, description="Refresh token if available")
created_at: str = Field(description="Timestamp when the token was created") scope: str = Field(description="Token scope")
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 # WORKFLOW MODELS
class DocumentContent(BaseModel): class ChatContent(BaseModel):
"""Content of a document in the workflow""" """Content of a document in the chat"""
sequenceNr: int = Field(1, description="Sequence number of the content in the source document") sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
name: str = Field(description="Designation") name: str = Field(description="Designation")
ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
mimeType: str = Field(description="MIME type") mimeType: str = Field(description="MIME type")
summary: str = Field(description="Summary of the file content") data: str = Field(description="Actual content")
data: str = Field(description="Actual content, text or base64 encoded based on base64Encoded flag") metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content")
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""" class ChatDocument(BaseModel):
id: str = Field(description="Unique ID of the document") """Document in the chat workflow"""
name: str = Field(description="Name of the data object") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the document")
ext: str = Field(description="Extension of the data object") fileId: str = Field(description="ID of the referenced file in the database")
fileId: int = 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") mimeType: str = Field(description="MIME type")
data: str = Field(description="Content of the data as text or base64 encoded based on base64Encoded flag") contents: List[ChatContent] = Field(default=[], description="Document contents")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
contents: List[DocumentContent] = Field(description="Document contents")
class DataStats(BaseModel):
class ChatStat(BaseModel):
"""Statistics for performance and data usage""" """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") processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Token count (for AI models)") tokenCount: Optional[int] = Field(None, description="Token count (for AI models)")
bytesSent: Optional[int] = Field(None, description="Bytes sent") bytesSent: Optional[int] = Field(None, description="Bytes sent")
bytesReceived: Optional[int] = Field(None, description="Bytes received") bytesReceived: Optional[int] = Field(None, description="Bytes received")
class WorkflowMessage(BaseModel):
"""Message object in the workflow""" class ChatMessage(BaseModel):
id: str = Field(description="Unique ID of the message") """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") workflowId: str = Field(description="Reference to the parent workflow")
parentMessageId: Optional[str] = Field(None, description="Reference to the replied message") 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") 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): sequenceNr: int = Field(description="Sequence number for sorting")
"""Log entry for a workflow""" startedAt: datetime = Field(description="Timestamp for message creation")
id: str = Field(description="Unique ID of the log entry") 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") workflowId: str = Field(description="ID of the associated workflow")
message: str = Field(description="Log message content") message: str = Field(description="Log message content")
type: str = Field(description="Type of log ('info', 'warning', 'error')") 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") agentName: str = Field(description="Name of the agent that created the log")
status: str = Field(description="Status of the workflow at log time") status: str = Field(description="Status of the workflow at log time")
progress: Optional[int] = Field(None, description="Progress value (0-100)") 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""" class ChatWorkflow(BaseModel):
id: str = Field(description="Unique ID of the workflow") """Chat workflow object for multi-agent system"""
name: Optional[str] = Field(None, description="Name of the workflow") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the chat workflow")
mandateId: int = Field(description="ID of the mandate") status: str = Field(description="Status of the chat workflow")
userId: int = Field(description="ID of the user") name: Optional[str] = Field(None, description="Name of the chat workflow")
status: str = Field(description="Status of the workflow ('running', 'completed', 'failed', 'stopped')") currentRound: int = Field(default=1, description="Current round/iteration")
startedAt: str = Field(description="Start timestamp")
lastActivity: str = Field(description="Timestamp of the last activity") lastActivity: str = Field(description="Timestamp of the last activity")
dataStats: Optional[Dict[str, Any]] = Field(None, description="Total statistics") startedAt: str = Field(description="Start timestamp")
currentRound: int = Field(default=1, description="Current round/iteration of the workflow") logs: List[ChatLog] = Field(default=[], description="Log entries")
messageIds: List[str] = Field(default=[], description="List of message IDs in this workflow") messages: List[ChatMessage] = Field(default=[], description="Message history")
stats: Optional[ChatStat] = Field(None, description="Statistics")
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 # AGENT AND TASK MODELS
class AgentResult(BaseModel): class Agent(BaseModel):
"""Result structure returned by agent processing""" """Data model for an agent"""
feedback: str = Field(description="Text response explaining what the agent did") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the agent")
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") name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent's functionality") description: str = Field(description="Description of the agent's functionality")
capabilities: List[str] = Field(default=[], description="List of agent capabilities") 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): class TaskItem(BaseModel):
"""Individual task in the workplan""" """Individual task in the workplan"""
agent: str = Field(description="Name of an available agent") sequenceNr: int = Field(description="Sequence number of the task")
prompt: str = Field(description="Specific instructions to the agent, that he knows what to do with which documents and which output to provide") agentName: str = Field(description="Name of an available agent")
outputDocuments: List[OutputDocument] = Field(default=[], description="List of required output documents") prompt: str = Field(description="Specific instructions to the agent")
inputDocuments: List[InputDocument] = Field(default=[], description="List of input documents to process") 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): class TaskPlan(BaseModel):
"""Work plan created by project manager""" """Work plan created by project manager"""
objFinalDocuments: List[str] = Field(default=[], description="List of required result documents") fileList: List[str] = Field(default=[], description="List of required result documents in format 'fileName'")
objWorkplan: List[TaskItem] = Field(default=[], description="Plan for executing agents") taskItems: List[TaskItem] = Field(default=[], description="Plan for executing agents")
objUserResponse: str = Field(description="Response to the user explaining the plan") userResponse: str = Field(description="Response to the user explaining the plan")
userLanguage: str = Field(default="en", description="Language code of the user's request") 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")

View file

@ -1,34 +1,39 @@
from fastapi import APIRouter, HTTPException, Depends, Path, Response from fastapi import APIRouter, HTTPException, Depends, Path, Response
from typing import List, Dict, Any from typing import List, Dict, Any
from fastapi import status from fastapi import status
import inspect
import importlib
import os
from pydantic import BaseModel
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.security.auth import getCurrentActiveUser, getUserContext
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.shared.defAttributes import AttributeDefinition, getModelAttributes from modules.shared.defAttributes import AttributeDefinition, getModelAttributes
# Import the model modules (without specific classes) def getModelClasses() -> Dict[str, Any]:
import modules.interfaces.gatewayModel as gatewayModel """Dynamically get all model classes from all model modules"""
import modules.interfaces.lucydomModel as lucydomModel modelClasses = {}
modelClasses = {
# Gateway model classes
"mandate": gatewayModel.Mandate,
"user": gatewayModel.User,
# LucyDOM model classes - admin # Get the interfaces directory path
"file": lucydomModel.FileItem, # Since we're in modules/routes/, we need to go up one level to modules/ then into interfaces/
"prompt": lucydomModel.Prompt, interfaces_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'interfaces')
# LucyDOM model classes - chat # Find all model files
"documentContent": lucydomModel.DocumentContent, for filename in os.listdir(interfaces_dir):
"document": lucydomModel.Document, if filename.endswith('Model.py'):
"dataStats": lucydomModel.DataStats, # Convert filename to module name (e.g., gatewayModel.py -> gatewayModel)
"userInputRequest": lucydomModel.UserInputRequest, module_name = filename[:-3]
"workflow": lucydomModel.Workflow,
"workflowMessage": lucydomModel.WorkflowMessage, # Import the module dynamically
"workflowLog": lucydomModel.WorkflowLog, 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 # Create a router for the attribute endpoints
router = APIRouter( router = APIRouter(
@ -52,6 +57,9 @@ async def getEntityAttributes(
# Determine preferred language of the user # Determine preferred language of the user
userLanguage = currentUser.get("language", "de") userLanguage = currentUser.get("language", "de")
# Get model classes dynamically
modelClasses = getModelClasses()
# Check if entity type is known # Check if entity type is known
if entityType not in modelClasses: if entityType not in modelClasses:
raise HTTPException( raise HTTPException(

View file

@ -26,23 +26,25 @@ def getModelAttributes(modelClass):
# Model attributes for FileItem # Model attributes for FileItem
fileAttributes = getModelAttributes(FileItem) fileAttributes = getModelAttributes(FileItem)
@dataclass
class AppContext: class AppContext:
"""Context object for all required connections and user information""" def __init__(self, mandateId: int, userId: int):
mandateId: int self._mandateId = mandateId
userId: int self._userId = userId
interfaceData: Any # LucyDOM Interface self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext: async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections""" """
mandateId, userId = await getUserContext(currentUser) Creates a central context object with all required interfaces
interfaceData = getLucydomInterface(mandateId, userId)
return AppContext( Args:
mandateId=mandateId, currentUser: Current user from authentication
userId=userId,
interfaceData=interfaceData Returns:
) AppContext object with all required connections
"""
_mandateId, _userId = await getUserContext(currentUser)
return AppContext(_mandateId, _userId)
# Create router for file endpoints # Create router for file endpoints
router = APIRouter( router = APIRouter(

View file

@ -6,6 +6,7 @@ from typing import Dict, Any
from datetime import timedelta from datetime import timedelta
import pathlib import pathlib
import os import os
import logging
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.security.auth import ( from modules.security.auth import (
@ -27,6 +28,8 @@ os.makedirs(staticFolder, exist_ok=True)
# Mount static files # Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static") router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
logger = logging.getLogger(__name__)
@router.get("/favicon.ico") @router.get("/favicon.ico")
async def favicon(): async def favicon():
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon") 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"]) @router.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()): async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
# Initialize Gateway interface without context # Get root mandate and admin user IDs
gateway = getGatewayInterface() adminGateway = getGatewayInterface()
rootMandateId = adminGateway.getInitialId("mandates")
# Authenticate user adminUserId = adminGateway.getInitialId("users")
user = gateway.authenticateUser(formData.username, formData.password)
if not user: if not rootMandateId or not adminUserId:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Invalid username or password", detail="System is not properly initialized with root mandate and admin user"
headers={"WWW-Authenticate": "Bearer"},
) )
# Create token with tenant ID # Create a new gateway interface instance with admin context
accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) adminGateway = getGatewayInterface(rootMandateId, adminUserId)
accessToken = createAccessToken(
data={ try:
"sub": user["username"], # Authenticate user
"mandateId": user["mandateId"] user = adminGateway.authenticateUser(formData.username, formData.password)
},
expiresDelta=accessTokenExpires # Create token with mandate ID and user ID
) accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
accessToken = createAccessToken(
return {"accessToken": accessToken, "tokenType": "bearer"} 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"]) @router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
return currentUser 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"
)

View file

@ -19,23 +19,15 @@ def getModelAttributes(modelClass):
# Model attributes for Mandate # Model attributes for Mandate
mandateAttributes = getModelAttributes(Mandate) mandateAttributes = getModelAttributes(Mandate)
@dataclass
class AppContext: class AppContext:
"""Context object for all required connections and user information""" def __init__(self, mandateId: int, userId: int):
mandateId: int self._mandateId = mandateId
userId: int self._userId = userId
interfaceData: Any # Gateway Interface self.interfaceData = getGatewayInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext: async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections"""
mandateId, userId = await getUserContext(currentUser) mandateId, userId = await getUserContext(currentUser)
interfaceData = getGatewayInterface(mandateId, userId) return AppContext(mandateId, userId)
return AppContext(
mandateId=mandateId,
userId=userId,
interfaceData=interfaceData
)
# Create router for mandate endpoints # Create router for mandate endpoints
router = APIRouter( router = APIRouter(
@ -89,19 +81,19 @@ async def createMandate(
return newMandate return newMandate
@router.get("/{mandateId}", response_model=Dict[str, Any]) @router.get("/{_mandateId}", response_model=Dict[str, Any])
async def getMandate( async def getMandate(
mandateId: int, _mandateId: str = Path(..., description="ID of the mandate"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Get a specific mandate""" """Get a mandate by ID."""
context = await getContext(currentUser) context = await getContext(currentUser)
# Permission check # Permission check
# Admin can only see their own mandate, SysAdmin can see all # Admin can only see their own mandate, SysAdmin can see all
isAdmin = currentUser.get("privilege") == "admin" isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin" isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context.mandateId == mandateId isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin: if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException( raise HTTPException(
@ -110,36 +102,36 @@ async def getMandate(
) )
# Get mandate # Get mandate
mandate = context.interfaceData.getMandate(mandateId) mandate = context.interfaceData.getMandate(_mandateId)
if not mandate: if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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 return mandate
@router.put("/{mandateId}", response_model=Dict[str, Any]) @router.put("/{_mandateId}", response_model=Dict[str, Any])
async def updateMandate( async def updateMandate(
mandateId: int = Path(..., description="ID of the mandate to update"), _mandateId: str = Path(..., description="ID of the mandate to update"),
mandateData: Dict[str, Any] = Body(..., description="Updated mandate data"), mandateData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Update an existing mandate""" """Update a mandate."""
context = await getContext(currentUser) context = await getContext(currentUser)
# Mandate exists? # Get mandate
mandate = context.interfaceData.getMandate(mandateId) mandate = context.interfaceData.getMandate(_mandateId)
if not mandate: if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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 # Permission check
isAdmin = currentUser.get("privilege") == "admin" isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin" isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context.mandateId == mandateId isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin: if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException( raise HTTPException(
@ -154,33 +146,29 @@ async def updateMandate(
updateData[attr] = mandateData[attr] updateData[attr] = mandateData[attr]
# Update mandate # Update mandate
updatedMandate = context.interfaceData.updateMandate( updatedMandate = context.interfaceData.updateMandate(_mandateId, mandateData)
mandateId=mandateId,
mandateData=updateData
)
return updatedMandate 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( 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) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Delete a mandate, including all associated users and referenced objects""" """Delete a mandate."""
context = await getContext(currentUser) context = await getContext(currentUser)
# Mandate exists? # Get mandate
mandate = context.interfaceData.getMandate(mandateId) mandate = context.interfaceData.getMandate(_mandateId)
if not mandate: if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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 # Permission check
isAdmin = currentUser.get("privilege") == "admin" isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin" isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context.mandateId == mandateId isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin: if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException( raise HTTPException(
@ -189,11 +177,11 @@ async def deleteMandate(
) )
# Delete mandate # Delete mandate
success = context.interfaceData.deleteMandate(mandateId) success = context.interfaceData.deleteMandate(_mandateId)
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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 return None

View file

@ -48,15 +48,15 @@ async def save_token_to_file(token_data, currentUser: Dict[str, Any]):
"""Save token data to database using LucyDOMInterface""" """Save token data to database using LucyDOMInterface"""
try: try:
# Get current user context # Get current user context
mandateId, userId = await getUserContext(currentUser) _mandateId, _userId = await getUserContext(currentUser)
if not mandateId or not userId: if not _mandateId or not _userId:
logger.error("No user context available for token storage") logger.error("No user context available for token storage")
return False return False
# Get LucyDOM interface for current user # Get LucyDOM interface for current user
mydom = getLucydomInterface( mydom = getLucydomInterface(
mandateId=mandateId, _mandateId=_mandateId,
userId=userId _userId=_userId
) )
if not mydom: if not mydom:
logger.error("No LucyDOM interface available for token storage") 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""" """Load token data from database using LucyDOMInterface"""
try: try:
# Get current user context # Get current user context
mandateId, userId = await getUserContext(currentUser) _mandateId, _userId = await getUserContext(currentUser)
if not mandateId or not userId: if not _mandateId or not _userId:
logger.error("No user context available for token retrieval") logger.error("No user context available for token retrieval")
return None return None
# Get LucyDOM interface for current user # Get LucyDOM interface for current user
mydom = getLucydomInterface( mydom = getLucydomInterface(
mandateId=mandateId, _mandateId=_mandateId,
userId=userId _userId=_userId
) )
if not mydom: if not mydom:
logger.error("No LucyDOM interface available for token retrieval") 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""" """Check Microsoft authentication status"""
try: try:
# Get current user context # Get current user context
mandateId, userId = await getUserContext(currentUser) _mandateId, _userId = await getUserContext(currentUser)
if not mandateId or not userId: if not _mandateId or not _userId:
logger.info("No user context found") logger.info("No user context found")
return JSONResponse({ return JSONResponse({
"authenticated": False, "authenticated": False,
@ -362,7 +362,7 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
token_data = await load_token_from_file(currentUser) token_data = await load_token_from_file(currentUser)
if not token_data: 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({ return JSONResponse({
"authenticated": False, "authenticated": False,
"message": "Not authenticated with Microsoft" "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"]): if not verify_token(token_data["access_token"]):
logger.info("Token invalid, attempting refresh") logger.info("Token invalid, attempting refresh")
# Try to refresh the token # Try to refresh the token
if not await refresh_token(userId, currentUser): if not await refresh_token(_userId, currentUser):
logger.info("Token refresh failed") logger.info("Token refresh failed")
return JSONResponse({ return JSONResponse({
"authenticated": False, "authenticated": False,
@ -408,16 +408,16 @@ async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Logout from Microsoft""" """Logout from Microsoft"""
try: try:
# Get current user context # Get current user context
mandateId, userId = await getUserContext(currentUser) _mandateId, _userId = await getUserContext(currentUser)
if not mandateId or not userId: if not _mandateId or not _userId:
return JSONResponse({ return JSONResponse({
"message": "Not authenticated with Microsoft" "message": "Not authenticated with Microsoft"
}) })
# Get LucyDOM interface for current user # Get LucyDOM interface for current user
mydom = getLucydomInterface( mydom = getLucydomInterface(
mandateId=mandateId, _mandateId=_mandateId,
userId=userId _userId=_userId
) )
if not mydom: if not mydom:
return JSONResponse({ return JSONResponse({
@ -426,13 +426,13 @@ async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
# Remove token from database # Remove token from database
tokens = mydom.db.getRecordset("msftTokens", recordFilter={ tokens = mydom.db.getRecordset("msftTokens", recordFilter={
"mandateId": mandateId, "_mandateId": _mandateId,
"userId": userId "_userId": _userId
}) })
if tokens and len(tokens) > 0: if tokens and len(tokens) > 0:
mydom.db.recordDelete("msftTokens", tokens[0]["id"]) 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({ return JSONResponse({
"message": "Successfully logged out from Microsoft" "message": "Successfully logged out from Microsoft"
@ -491,7 +491,7 @@ async def get_backend_token(request: Request):
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid authorization header" detail="Missing or invalid authorization header"
) )
# Extract the MSAL token # Extract the MSAL token
msal_token = auth_header.split(' ')[1] msal_token = auth_header.split(' ')[1]
@ -502,7 +502,7 @@ async def get_backend_token(request: Request):
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid MSAL token" detail="Invalid MSAL token"
) )
# Get the user from the database using the email # Get the user from the database using the email
gateway = getGatewayInterface() gateway = getGatewayInterface()
user = gateway.getUserByUsername(user_info["email"]) user = gateway.getUserByUsername(user_info["email"])
@ -512,17 +512,17 @@ async def get_backend_token(request: Request):
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not registered in the system" detail="User not registered in the system"
) )
# Create backend token # Create backend token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = createAccessToken( access_token = createAccessToken(
data={ data={
"sub": user["username"], "sub": user["username"],
"mandateId": user["mandateId"] "_mandateId": user["_mandateId"]
}, },
expiresDelta=access_token_expires expiresDelta=access_token_expires
) )
return { return {
"accessToken": access_token, "accessToken": access_token,
"tokenType": "bearer", "tokenType": "bearer",
@ -530,7 +530,7 @@ async def get_backend_token(request: Request):
"username": user["username"], "username": user["username"],
"email": user["email"], "email": user["email"],
"fullName": user.get("fullName", ""), "fullName": user.get("fullName", ""),
"mandateId": user["mandateId"] "_mandateId": user["_mandateId"]
} }
} }

View file

@ -21,23 +21,15 @@ def getModelAttributes(modelClass):
# Model attributes for Prompt # Model attributes for Prompt
promptAttributes = getModelAttributes(Prompt) promptAttributes = getModelAttributes(Prompt)
@dataclass
class AppContext: class AppContext:
"""Context object for all required connections and user information""" def __init__(self, mandateId: str, userId: str):
mandateId: int self._mandateId = mandateId
userId: int self._userId = userId
interfaceData: Any # LucyDOM Interface self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext: async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections"""
mandateId, userId = await getUserContext(currentUser) mandateId, userId = await getUserContext(currentUser)
interfaceData = getLucydomInterface(mandateId, userId) return AppContext(mandateId, userId)
return AppContext(
mandateId=mandateId,
userId=userId,
interfaceData=interfaceData
)
# Create router for prompt endpoints # Create router for prompt endpoints
router = APIRouter( router = APIRouter(
@ -82,7 +74,7 @@ async def createPrompt(
@router.get("/{promptId}", response_model=Dict[str, Any]) @router.get("/{promptId}", response_model=Dict[str, Any])
async def getPrompt( async def getPrompt(
promptId: int, promptId: str = Path(..., description="ID of the prompt"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Get a specific prompt""" """Get a specific prompt"""
@ -100,7 +92,7 @@ async def getPrompt(
@router.put("/{promptId}", response_model=Dict[str, Any]) @router.put("/{promptId}", response_model=Dict[str, Any])
async def updatePrompt( async def updatePrompt(
promptId: int, promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Dict[str, Any] = Body(...), promptData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
@ -136,7 +128,7 @@ async def updatePrompt(
@router.delete("/{promptId}", response_model=Dict[str, Any]) @router.delete("/{promptId}", response_model=Dict[str, Any])
async def deletePrompt( async def deletePrompt(
promptId: int, promptId: str = Path(..., description="ID of the prompt to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Delete a prompt""" """Delete a prompt"""

View file

@ -30,18 +30,18 @@ userAttributes = getModelAttributes(User)
@dataclass @dataclass
class AppContext: class AppContext:
"""Context object for all required connections and user information""" """Context object for all required connections and user information"""
mandateId: int _mandateId: int
userId: int _userId: int
interfaceData: Any # Gateway Interface interfaceData: Any # Gateway Interface
async def getContext(currentUser: Dict[str, Any]) -> AppContext: async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections""" """Creates a central context object with all required connections"""
mandateId, userId = await getUserContext(currentUser) _mandateId, _userId = await getUserContext(currentUser)
interfaceData = getGatewayInterface(mandateId, userId) interfaceData = getGatewayInterface(_mandateId, _userId)
return AppContext( return AppContext(
mandateId=mandateId, _mandateId=_mandateId,
userId=userId, _userId=_userId,
interfaceData=interfaceData 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 # Admin sees only users of own mandate, SysAdmin sees all
if currentUser.get("privilege") == "admin": if currentUser.get("privilege") == "admin":
return context.interfaceData.getUsersByMandate(context.mandateId) return context.interfaceData.getUsersByMandate(context._mandateId)
else: # sysadmin else: # sysadmin
return context.interfaceData.getAllUsers() return context.interfaceData.getAllUsers()
@ -80,8 +80,15 @@ async def registerUser(request: Request):
logger.info(f"Registration request data: {data}") logger.info(f"Registration request data: {data}")
# Get root mandate and admin user IDs # Get root mandate and admin user IDs
rootMandateId = 1 # Root mandate is always ID 1 adminGateway = getGatewayInterface()
adminUserId = 1 # Admin user is always ID 1 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 # Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId) adminGateway = getGatewayInterface(rootMandateId, adminUserId)
@ -98,7 +105,7 @@ async def registerUser(request: Request):
"email": data.get("email"), "email": data.get("email"),
"fullName": data.get("fullName"), "fullName": data.get("fullName"),
"language": data.get("language", "de"), "language": data.get("language", "de"),
"mandateId": rootMandateId, "_mandateId": rootMandateId,
"disabled": False, "disabled": False,
"privilege": "user" "privilege": "user"
} }
@ -125,16 +132,26 @@ async def registerUser(request: Request):
logger.info("User verification successful") logger.info("User verification successful")
# Test authentication # Test authentication
authResult = adminGateway.authenticateUser(userData["username"], userData["password"]) try:
if not authResult: authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
logger.error("Authentication test failed after user creation") 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 to delete the user
try: try:
# adminGateway.deleteUser(createdUser["id"]) adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully NOT deleted user after authentication test failure") logger.info("Successfully deleted user after authentication test failure")
except Exception as e: except Exception as e:
logger.error(f"Failed to delete user after authentication test failure: {str(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") logger.info("Authentication test successful")
@ -194,7 +211,7 @@ async def registerUserWithMsal(userData: dict = Body(...)):
email=userData.get("email"), email=userData.get("email"),
fullName=userData.get("fullName"), fullName=userData.get("fullName"),
language=userData.get("language", "de"), language=userData.get("language", "de"),
mandateId=rootMandateId, _mandateId=rootMandateId,
disabled=False, disabled=False,
privilege="user" privilege="user"
) )
@ -214,7 +231,7 @@ async def registerUserWithMsal(userData: dict = Body(...)):
@router.get("/{userId}", response_model=Dict[str, Any]) @router.get("/{userId}", response_model=Dict[str, Any])
async def getUser( async def getUser(
userId: int, userId: str = Path(..., description="ID of the user"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Get a specific user""" """Get a specific user"""
@ -230,10 +247,10 @@ async def getUser(
# Permission check # Permission check
# User can only view themselves, Admin only users of their own mandate, SysAdmin all # 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 # User can view themselves
pass 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 # Admin can view users of their own mandate
pass pass
elif currentUser.get("privilege") == "sysadmin": elif currentUser.get("privilege") == "sysadmin":
@ -249,7 +266,7 @@ async def getUser(
@router.put("/{userId}", response_model=Dict[str, Any]) @router.put("/{userId}", response_model=Dict[str, Any])
async def updateUser( 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"), userData: Dict[str, Any] = Body(..., description="Updated user data"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
@ -265,14 +282,14 @@ async def updateUser(
) )
# Permission check # Permission check
isSelfUpdate = userId == context.userId isSelfUpdate = userId == str(context._userId)
isAdmin = currentUser.get("privilege") == "admin" isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin" isSysadmin = currentUser.get("privilege") == "sysadmin"
sameMandate = userToUpdate.get("mandateId") == context.mandateId sameMandate = userToUpdate.get("_mandateId") == context._mandateId
# Filter allowed fields based on permission level # Filter allowed fields based on permission level
allowedFields = {"username", "email", "fullName", "language"} allowedFields = {"username", "email", "fullName", "language"}
sensitiveFields = {"mandateId", "disabled", "privilege"} sensitiveFields = {"_mandateId", "disabled", "privilege"}
# Check if sensitive fields should be changed # Check if sensitive fields should be changed
sensitiveUpdate = any(field in userData for field in sensitiveFields) 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) @router.delete("/{userId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteUser( 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) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Delete a user""" """Delete a user"""
@ -327,10 +344,10 @@ async def deleteUser(
) )
# Permission check # Permission check
isSelfDelete = userId == context.userId isSelfDelete = userId == str(context._userId)
isAdmin = currentUser.get("privilege") == "admin" isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin" isSysadmin = currentUser.get("privilege") == "sysadmin"
sameMandate = userToDelete.get("mandateId") == context.mandateId sameMandate = userToDelete.get("_mandateId") == context._mandateId
if isSelfDelete: if isSelfDelete:
# User can delete themselves # User can delete themselves

View file

@ -29,34 +29,15 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@dataclass
class AppContext: class AppContext:
"""Context object for all required connections and user information""" def __init__(self, mandateId: int, userId: int):
mandateId: int self._mandateId = mandateId
userId: int self._userId = userId
interfaceData: Any # LucyDOM Interface self.interfaceData = getLucydomInterface(mandateId, userId)
interfaceChat: Any # Workflow Manager
async def getContext(currentUser: Dict[str, Any]) -> AppContext: 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) mandateId, userId = await getUserContext(currentUser)
interfaceData = getLucydomInterface(mandateId, userId) return AppContext(mandateId, userId)
interfaceChat = getWorkflowManager(mandateId, userId)
return AppContext(
mandateId=mandateId,
userId=userId,
interfaceData=interfaceData,
interfaceChat=interfaceChat
)
# State 1: Workflow Initialization endpoint # State 1: Workflow Initialization endpoint
@router.post("/start", response_model=Dict[str, Any]) @router.post("/start", response_model=Dict[str, Any])
@ -87,7 +68,7 @@ async def startWorkflow(
} }
# Start or continue workflow using the workflow manager # 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) logger.info("User Input received. Answer:",workflow)
return { return {
@ -130,14 +111,14 @@ async def stopWorkflow(
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
if workflow.get("userId") != context.userId: if workflow.get("_userId") != context._userId:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to stop this workflow" detail="You don't have permission to stop this workflow"
) )
# Stop the workflow # Stop the workflow
stoppedWorkflow = await context.interfaceChat.workflowStop(workflowId) stoppedWorkflow = getWorkflowManager(context._mandateId, context._userId).workflowStop(workflowId)
return { return {
"id": workflowId, "id": workflowId,
@ -183,7 +164,7 @@ async def deleteWorkflow(
) )
# Check if user has permission to delete # Check if user has permission to delete
if workflow.get("userId") != context.userId: if workflow.get("_userId") != context._userId:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to delete this workflow" detail="You don't have permission to delete this workflow"
@ -230,7 +211,7 @@ async def listWorkflows(
try: try:
# Retrieve workflows for the user # Retrieve workflows for the user
workflows = context.interfaceData.getWorkflowsByUser(context.userId) workflows = context.interfaceData.getWorkflowsByUser(context._userId)
return workflows return workflows
except Exception as e: except Exception as e:
logger.error(f"Error listing workflows: {str(e)}", exc_info=True) 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" detail=f"Workflow with ID {workflowId} not found"
) )
if workflow.get("userId") != context.userId: if workflow.get("_userId") != context._userId:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow" detail="You don't have permission to modify this workflow"
@ -471,7 +452,7 @@ async def deleteWorkflowMessage(
async def deleteFileFromMessage( async def deleteFileFromMessage(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"), 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) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """
@ -498,7 +479,7 @@ async def deleteFileFromMessage(
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
if workflow.get("userId") != context.userId: if workflow.get("_userId") != context._userId:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow" 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]) @router.get("/files/{fileId}/preview", response_model=Dict[str, Any])
async def previewFile( 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) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """
@ -558,7 +539,7 @@ async def previewFile(
) )
# Check if file belongs to user or their mandate # 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( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this file" detail="You don't have permission to access this file"
@ -636,7 +617,7 @@ async def previewFile(
@router.get("/files/{fileId}/download") @router.get("/files/{fileId}/download")
async def downloadFile( 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) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """
@ -676,4 +657,122 @@ async def downloadFile(
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading file: {str(e)}" detail=f"Error downloading file: {str(e)}"
) )
@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"}

View file

@ -75,15 +75,20 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
if username is None: if username is None:
raise credentialsException raise credentialsException
# Extract mandate ID from token (if present) # Extract mandate ID and user ID from token
mandateId: int = payload.get("mandateId", 1) # Default: Root mandate _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: except JWTError:
logger.warning("Invalid JWT Token") logger.warning("Invalid JWT Token")
raise credentialsException raise credentialsException
# Initialize Gateway Interface without context # Initialize Gateway Interface with context
gateway = getGatewayInterface() gateway = getGatewayInterface(_mandateId, _userId)
# Retrieve user from database # Retrieve user from database
user = gateway.getUserByUsername(username) 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") logger.warning(f"User {username} is disabled")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User 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 return user
async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, Any]: 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 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. Extracts the mandate ID and user ID from the current user.
Enhanced with better logging.
Args: Args:
currentUser: The current user currentUser: The current user
Returns: Returns:
Tuple of (mandateId, userId) Tuple of (_mandateId, _userId) as strings
Raises:
HTTPException: If mandate or user ID is missing
""" """
# Default values # Extract _mandateId
defaultMandateId = 0 _mandateId = currentUser.get("_mandateId")
defaultUserId = 0 if not _mandateId:
logger.error("No _mandateId found in currentUser")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing mandate context"
)
# Extract mandateId # Extract _userId
mandateId = currentUser.get("mandateId", None) _userId = currentUser.get("id") # Note: using 'id' instead of '_userId'
if mandateId is None: if not _userId:
logger.warning(f"No mandateId found in currentUser, using default: {defaultMandateId}") logger.error("No _userId found in currentUser")
mandateId = defaultMandateId raise HTTPException(
else: status_code=status.HTTP_401_UNAUTHORIZED,
try: detail="Missing user context"
mandateId = int(mandateId) )
except (ValueError, TypeError):
logger.error(f"Invalid mandateId value: {mandateId}, using default: {defaultMandateId}")
mandateId = defaultMandateId
# Extract userId return str(_mandateId), str(_userId)
userId = currentUser.get("id", None)
if userId is None: def getInitialContext() -> tuple[str, str]:
logger.warning(f"No userId found in currentUser, using default: {defaultUserId}") """
userId = defaultUserId Returns the initial mandate and user IDs from the gateway.
else: This is used by other interfaces to get their context.
try:
userId = int(userId)
except (ValueError, TypeError):
logger.error(f"Invalid userId value: {userId}, using default: {defaultUserId}")
userId = defaultUserId
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 return mandateId, userId

View file

@ -111,8 +111,8 @@ def getModelAttributes(modelClass, userLanguage="de"):
placeholder=placeholder, placeholder=placeholder,
defaultValue=defaultValue, defaultValue=defaultValue,
options=options, options=options,
editable=fieldName not in ["id", "mandateId", "userId", "createdAt", "uploadDate"], editable=fieldName not in ["id", "_mandateId", "_userId", "uploadDate", "_createdAt", "_modifiedAt"],
visible=fieldName not in ["hashedPassword", "mandateId", "userId"], visible=fieldName not in ["hashedPassword", "_mandateId", "_userId"],
order=i, order=i,
validation=validation, validation=validation,
helpText=description or "" # Set empty string as default value if no description found helpText=description or "" # Set empty string as default value if no description found

View file

@ -25,11 +25,18 @@ class AgentBase:
self.label = "Base Agent" self.label = "Base Agent"
self.description = "Base agent functionality" self.description = "Base agent functionality"
self.capabilities = [] self.capabilities = []
self.workflowManager = None
self.mydom = None self.mydom = None
self.workflowManager = None # Will be set by workflow manager
def setDependencies(self, mydom=None): def setWorkflowManager(self, workflowManager):
"""Set external dependencies for the agent.""" """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 self.mydom = mydom
def getAgentInfo(self) -> Dict[str, Any]: def getAgentInfo(self) -> Dict[str, Any]:

View file

@ -30,9 +30,16 @@ class AgentRegistry:
raise RuntimeError("Singleton instance already exists - use getInstance()") raise RuntimeError("Singleton instance already exists - use getInstance()")
self.agents = {} self.agents = {}
self.mydom = None
self._loadAgents() 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): def _loadAgents(self):
"""Load all available agents from modules.""" """Load all available agents from modules."""
logger.info("Loading agent modules...") logger.info("Loading agent modules...")
@ -88,22 +95,6 @@ class AgentRegistry:
except Exception as e: except Exception as e:
logger.error(f"Error loading agent from module {moduleName}: {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): def registerAgent(self, agent):
""" """
Register an agent in the registry. Register an agent in the registry.
@ -112,9 +103,6 @@ class AgentRegistry:
agent: The agent to register agent: The agent to register
""" """
agentId = getattr(agent, 'name', "unknown_agent") agentId = getattr(agent, 'name', "unknown_agent")
# Initialize agent with dependencies
if hasattr(agent, 'setDependencies'):
agent.setDependencies(mydom=self.mydom)
self.agents[agentId] = agent self.agents[agentId] = agent
logger.debug(f"Agent '{agent.name}' registered") logger.debug(f"Agent '{agent.name}' registered")
@ -127,11 +115,7 @@ class AgentRegistry:
Agent instance or None if not found Agent instance or None if not found
""" """
if agentIdentifier in self.agents: if agentIdentifier in self.agents:
agent = self.agents[agentIdentifier] return self.agents[agentIdentifier]
# Ensure the agent has the AI service
if self.mydom:
agent.mydom = self.mydom
return agent
logger.error(f"Agent with identifier '{agentIdentifier}' not found") logger.error(f"Agent with identifier '{agentIdentifier}' not found")
return None return None

View file

@ -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']

View file

@ -41,26 +41,49 @@ class WorkflowStoppedException(Exception):
pass pass
class WorkflowManager: class WorkflowManager:
""" """Manages the execution of workflows and their associated agents."""
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: str, _userId: str):
""" """Initialize the workflow manager with mandate and user context."""
self._mandateId = _mandateId
def __init__(self, mandateId: int, userId: int): self._userId = _userId
""" self.mydom = domInterface(_mandateId, _userId)
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)
self.agentRegistry = getAgentRegistry() self.agentRegistry = getAgentRegistry()
self.agentRegistry.setMydom(self.mydom) self.agentRegistry.initialize(mydom=self.mydom, workflowManager=self)
self.agentRegistry.setWorkflowManager(self) # Set self as workflow manager for all agents
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 ### Workflow State Machine Implementation
@ -280,8 +303,8 @@ class WorkflowManager:
newWorkflowId = str(uuid.uuid4()) if workflowId is None else workflowId newWorkflowId = str(uuid.uuid4()) if workflowId is None else workflowId
workflow = { workflow = {
"id": newWorkflowId, "id": newWorkflowId,
"mandateId": self.mandateId, "_mandateId": self._mandateId,
"userId": self.userId, "_userId": self._userId,
"name": f"Workflow {newWorkflowId[:8]}", "name": f"Workflow {newWorkflowId[:8]}",
"startedAt": currentTime, "startedAt": currentTime,
"messages": [], # Empty list - will be filled with references "messages": [], # Empty list - will be filled with references
@ -301,8 +324,8 @@ class WorkflowManager:
# Save to database - only the workflow metadata # Save to database - only the workflow metadata
workflowDb = { workflowDb = {
"id": workflow["id"], "id": workflow["id"],
"mandateId": workflow["mandateId"], "_mandateId": workflow["_mandateId"],
"userId": workflow["userId"], "_userId": workflow["_userId"],
"name": workflow["name"], "name": workflow["name"],
"startedAt": workflow["startedAt"], "startedAt": workflow["startedAt"],
"status": workflow["status"], "status": workflow["status"],
@ -593,7 +616,12 @@ JSON_OUTPUT = {{
try: try:
# Process the task using the agent's standardized interface # Process the task using the agent's standardized interface
logger.debug("TASK: "+self.parseJson2text(agentTask)) 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 # Calculate bytes sent before processing
bytesSent = len(json.dumps(agentTask).encode('utf-8')) bytesSent = len(json.dumps(agentTask).encode('utf-8'))
@ -885,8 +913,8 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
continue continue
# Check if file belongs to the current mandate # Check if file belongs to the current mandate
if file.get("mandateId") != self.mandateId: if file.get("_mandateId") != self._mandateId:
logger.warning(f"File {fileId} does not belong to mandate {self.mandateId}") logger.warning(f"File {fileId} does not belong to mandate {self._mandateId}")
continue continue
# Load file content # Load file content
@ -1511,49 +1539,48 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"userLanguage": "en" "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 # Singleton factory for the WorkflowManager
_workflowManagers = {} _workflowManagers = {}
_workflowManagerLastAccess = {} # Track last access time for cleanup _workflowManagerLastAccess = {} # Track last access time for cleanup
def getWorkflowManager(mandateId: int = 0, userId: int = 0) -> WorkflowManager: def getWorkflowManager(_mandateId: str = '', _userId: str = '') -> WorkflowManager:
""" """Get a workflow manager instance with the specified context."""
Returns a WorkflowManager for the specified context. return WorkflowManager(_mandateId=_mandateId, _userId=_userId)
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 cleanupWorkflowManager(mandateId: int, userId: int) -> None: def cleanupWorkflowManager(_mandateId: int, _userId: int) -> None:
""" """
Explicitly cleanup a WorkflowManager instance. Explicitly cleanup a WorkflowManager instance.
Args: Args:
mandateId: ID of the mandate _mandateId: ID of the mandate
userId: ID of the user _userId: ID of the user
""" """
contextKey = f"{mandateId}_{userId}" contextKey = f"{_mandateId}_{_userId}"
if contextKey in _workflowManagers: if contextKey in _workflowManagers:
del _workflowManagers[contextKey] del _workflowManagers[contextKey]
if contextKey in _workflowManagerLastAccess: if contextKey in _workflowManagerLastAccess:

View file

@ -1,5 +1,12 @@
....................... TASKS ....................... 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 ! function callAI() to ask with userPrompt,systemPrompt optional), not with json
! in the taskplan to refer files always in context of user/mandate ! 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 ! userinput to handle with object AgentQuery --> when received in frontend to enhance for full object