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__)
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
# Import models - import generically for INITIALIZATION
from modules.interfaces.gatewayInterface import getGatewayInterface
gateway = getGatewayInterface()
# Define lifespan context manager for application startup/shutdown events
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup logic (if any)
# Startup logic
logger.info("Application is starting up")
yield
# Shutdown logic
logger.info("Application has been shut down")

View file

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

View file

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

View file

@ -2,6 +2,8 @@ import json
import os
from typing import List, Dict, Any, Optional, Union
import logging
from datetime import datetime
import uuid
logger = logging.getLogger(__name__)
@ -9,19 +11,28 @@ class DatabaseConnector:
"""
A connector for JSON-based data storage.
Provides generic database operations without user/mandate filtering.
Stores tables as folders and records as individual files.
Implements lazy loading for better performance.
"""
def __init__(self, dbHost: str, dbDatabase: str, dbUser: str = None, dbPassword: str = None,
mandateId: int = None, userId: int = None, skipInitialIdLookup: bool = False):
_mandateId: str = None, _userId: str = None, skipInitialIdLookup: bool = False):
# Store the input parameters
self.dbHost = dbHost
self.dbDatabase = dbDatabase
self.dbUser = dbUser
self.dbPassword = dbPassword
self.skipInitialIdLookup = skipInitialIdLookup
# Check if context parameters are set
if mandateId is None or userId is None:
raise ValueError("mandateId and userId must be set")
if _mandateId is None and _userId is None:
# Allow initialization with empty strings
self._mandateId = ''
self._userId = ''
else:
# Ensure both parameters are provided
if _mandateId is None or _userId is None:
raise ValueError("_mandateId and _userId must both be provided or both be None")
self._mandateId = _mandateId
self._userId = _userId
# Ensure the database directory exists
self.dbFolder = os.path.join(self.dbHost, self.dbDatabase)
@ -29,34 +40,33 @@ class DatabaseConnector:
# Cache for loaded data
self._tablesCache = {}
self._tableMetadataCache = {} # Cache for table metadata (record IDs, etc.)
# Initialize system table
self._systemTableName = "_system"
self._initializeSystemTable()
# Temporarily store mandateId and userId
self._mandateId = mandateId
self._userId = userId
# If mandateId or userId are 0 and we're not skipping ID lookup, try to use the initial IDs
# If IDs are empty and we're not skipping lookup, try to use initial IDs
if not skipInitialIdLookup:
if mandateId == 0:
initialMandateId = self.getInitialId("mandates")
if initialMandateId is not None:
self._mandateId = initialMandateId
logger.info(f"Using initial mandateId: {initialMandateId} instead of 0")
self._resolveInitialIds()
if userId == 0:
initialUserId = self.getInitialId("users")
if initialUserId is not None:
self._userId = initialUserId
logger.info(f"Using initial userId: {initialUserId} instead of 0")
logger.debug(f"Context: _mandateId={self._mandateId}, _userId={self._userId}")
def _resolveInitialIds(self):
"""
Resolve initial IDs for mandate and user if they're empty.
"""
if not self._mandateId:
initialMandateId = self.getInitialId("mandates")
if initialMandateId is not None:
self._mandateId = initialMandateId
logger.info(f"Using initial _mandateId: {initialMandateId}")
# Set the effective IDs as properties
self.mandateId = self._mandateId
self.userId = self._userId
logger.debug(f"Context: mandateId={self.mandateId}, userId={self.userId}")
if not self._userId:
initialUserId = self.getInitialId("users")
if initialUserId is not None:
self._userId = initialUserId
logger.info(f"Using initial _userId: {initialUserId}")
def _initializeSystemTable(self):
"""Initializes the system table if it doesn't exist yet."""
@ -70,7 +80,7 @@ class DatabaseConnector:
self._loadSystemTable()
logger.debug(f"Existing system table loaded from {systemTablePath}")
def _loadSystemTable(self) -> Dict[str, int]:
def _loadSystemTable(self) -> Dict[str, str]:
"""Loads the system table with the initial IDs."""
# Check if system table is in cache
if f"_{self._systemTableName}" in self._tablesCache:
@ -92,7 +102,7 @@ class DatabaseConnector:
self._tablesCache[f"_{self._systemTableName}"] = {}
return {}
def _saveSystemTable(self, data: Dict[str, int]) -> bool:
def _saveSystemTable(self, data: Dict[str, str]) -> bool:
"""Saves the system table with the initial IDs."""
systemTablePath = self._getTablePath(self._systemTableName)
try:
@ -106,76 +116,111 @@ class DatabaseConnector:
return False
def _getTablePath(self, table: str) -> str:
"""Returns the full path to a table file"""
return os.path.join(self.dbFolder, f"{table}.json")
def _loadTable(self, table: str) -> List[Dict[str, Any]]:
"""Loads a table from the corresponding JSON file"""
path = self._getTablePath(table)
"""Returns the full path to a table folder"""
return os.path.join(self.dbFolder, table)
def _getRecordPath(self, table: str, recordId: Union[str, int]) -> str:
"""Returns the full path to a record file"""
return os.path.join(self._getTablePath(table), f"{recordId}.json")
def _ensureTableDirectory(self, table: str) -> bool:
"""Ensures the table directory exists."""
if table == self._systemTableName:
return True
tablePath = self._getTablePath(table)
try:
os.makedirs(tablePath, exist_ok=True)
return True
except Exception as e:
logger.error(f"Error creating table directory {tablePath}: {e}")
return False
def _loadTableMetadata(self, table: str) -> Dict[str, Any]:
"""Loads table metadata (list of record IDs) without loading actual records."""
if table in self._tableMetadataCache:
return self._tableMetadataCache[table]
# Ensure table directory exists
if not self._ensureTableDirectory(table):
return {"recordIds": []}
tablePath = self._getTablePath(table)
metadata = {"recordIds": []}
try:
if os.path.exists(tablePath):
for filename in os.listdir(tablePath):
if filename.endswith('.json'):
recordId = filename[:-5] # Remove .json extension
metadata["recordIds"].append(recordId)
metadata["recordIds"].sort()
self._tableMetadataCache[table] = metadata
except Exception as e:
logger.error(f"Error loading table metadata for {table}: {e}")
return metadata
def _loadRecord(self, table: str, recordId: Union[str, int]) -> Optional[Dict[str, Any]]:
"""Loads a single record from the table."""
recordPath = self._getRecordPath(table, recordId)
try:
if os.path.exists(recordPath):
with open(recordPath, 'r', encoding='utf-8') as f:
record = json.load(f)
# Ensure ID is a string
if "id" in record:
record["id"] = str(record["id"])
return record
except Exception as e:
logger.error(f"Error loading record {recordId} from table {table}: {e}")
return None
def _loadTable(self, table: str) -> List[Dict[str, Any]]:
"""Loads all records from a table folder."""
# If the table is the system table, load it directly
if table == self._systemTableName:
return [] # The system table is not treated like normal tables
return self._loadSystemTable()
# If the table is already in the cache, use the cache
if table in self._tablesCache:
return self._tablesCache[table]
# Otherwise load the file
try:
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
self._tablesCache[table] = data
# If data was loaded and no initial ID is registered yet,
# register the ID of the first record (if available)
if data and not self.hasInitialId(table):
if "id" in data[0]:
self._registerInitialId(table, data[0]["id"])
logger.info(f"Initial ID {data[0]['id']} for table {table} retroactively registered")
return data
else:
# If the file doesn't exist, create an empty table
logger.info(f"New table {table}")
self._tablesCache[table] = []
self._saveTable(table, [])
return []
except Exception as e:
logger.error(f"Error loading table {table}: {e}")
return []
# Load metadata first
metadata = self._loadTableMetadata(table)
records = []
# Load each record
for recordId in metadata["recordIds"]:
record = self._loadRecord(table, recordId)
if record:
records.append(record)
self._tablesCache[table] = records
return records
def _saveTable(self, table: str, data: List[Dict[str, Any]]) -> bool:
"""Saves a table to the corresponding JSON file"""
"""Saves all records to a table folder"""
# The system table is handled specially
if table == self._systemTableName:
return False
return self._saveSystemTable(data)
path = self._getTablePath(table)
tablePath = self._getTablePath(table)
try:
# Check if directory exists and is writable
dir_path = os.path.dirname(path)
if not os.path.exists(dir_path):
logger.error(f"Directory does not exist: {dir_path}")
return False
if not os.access(dir_path, os.W_OK):
logger.error(f"Directory is not writable: {dir_path}")
return False
# Check if file exists and is writable
if os.path.exists(path) and not os.access(path, os.W_OK):
logger.error(f"File exists but is not writable: {path}")
return False
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# Ensure table directory exists
os.makedirs(tablePath, exist_ok=True)
# Save each record as a separate file
for record in data:
if "id" not in record:
logger.error(f"Record missing ID in table {table}")
continue
recordPath = self._getRecordPath(table, record["id"])
with open(recordPath, 'w', encoding='utf-8') as f:
json.dump(record, f, indent=2, ensure_ascii=False)
# Verify the file was written correctly
if not os.path.exists(path):
logger.error(f"File was not created after write: {path}")
return False
# Update the cache
self._tablesCache[table] = data
logger.debug(f"Successfully saved table {table}")
@ -185,7 +230,7 @@ class DatabaseConnector:
logger.error(f"Error type: {type(e).__name__}")
logger.error(f"Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No details available'}")
return False
def _applyRecordFilter(self, records: List[Dict[str, Any]], recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Applies a record filter to the records"""
if not recordFilter:
@ -202,19 +247,12 @@ class DatabaseConnector:
match = False
break
# Handle type conversion for integer comparisons both ways
if isinstance(value, int) and isinstance(record[field], str) and record[field].isdigit():
# Filter value is int, record value is string
if value != int(record[field]):
match = False
break
elif isinstance(value, str) and value.isdigit() and isinstance(record[field], int):
# Filter value is string, record value is int
if record[field] != int(value):
match = False
break
# Otherwise direct comparison
elif record[field] != value:
# Convert both values to strings for comparison
recordValue = str(record[field])
filterValue = str(value)
# Direct string comparison
if recordValue != filterValue:
match = False
break
@ -223,7 +261,7 @@ class DatabaseConnector:
return filteredRecords
def _registerInitialId(self, table: str, initialId: int) -> bool:
def _registerInitialId(self, table: str, initialId: str) -> bool:
"""Registers the initial ID for a table."""
try:
systemData = self._loadSystemTable()
@ -255,6 +293,41 @@ class DatabaseConnector:
logger.error(f"Error removing initial ID for table {table}: {e}")
return False
def _getCurrentTimestamp(self) -> str:
"""Returns the current timestamp in ISO format."""
return datetime.now().isoformat()
def _saveTableMetadata(self, table: str, metadata: Dict[str, Any]) -> bool:
"""Saves table metadata to a metadata file."""
try:
# Create metadata file path
metadataPath = os.path.join(self._getTablePath(table), "_metadata.json")
# Save metadata
with open(metadataPath, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
# Update cache
self._tableMetadataCache[table] = metadata
return True
except Exception as e:
logger.error(f"Error saving metadata for table {table}: {e}")
return False
def updateContext(self, _mandateId: str, _userId: str) -> None:
"""Updates the context of the database connector."""
if _mandateId is None or _userId is None:
raise ValueError("_mandateId and _userId must both be provided")
self._mandateId = _mandateId
self._userId = _userId
logger.info(f"Updated database context: _mandateId={self._mandateId}, _userId={self._userId}")
# Clear cache to ensure fresh data with new context
self._tablesCache = {}
self._tableMetadataCache = {}
# Public API
def getTables(self) -> List[str]:
@ -262,10 +335,10 @@ class DatabaseConnector:
tables = []
try:
for filename in os.listdir(self.dbFolder):
if filename.endswith('.json') and not filename.startswith('_'):
tableName = filename[:-5] # Remove the .json extension
tables.append(tableName)
for item in os.listdir(self.dbFolder):
itemPath = os.path.join(self.dbFolder, item)
if os.path.isdir(itemPath) and not item.startswith('_'):
tables.append(item)
except Exception as e:
logger.error(f"Error reading the database directory: {e}")
@ -306,16 +379,26 @@ class DatabaseConnector:
def getRecordset(self, table: str, fieldFilter: List[str] = None, recordFilter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""Returns a list of records from a table, filtered by criteria."""
data = self._loadTable(table)
# If we have specific record IDs in the filter, only load those records
if recordFilter and "id" in recordFilter:
recordId = recordFilter["id"]
record = self._loadRecord(table, recordId)
if record:
records = [record]
else:
return []
else:
# Load all records if no specific ID filter
records = self._loadTable(table)
# Apply recordFilter if available
if recordFilter:
data = self._applyRecordFilter(data, recordFilter)
records = self._applyRecordFilter(records, recordFilter)
# If fieldFilter is available, reduce the fields
if fieldFilter and isinstance(fieldFilter, list):
result = []
for record in data:
for record in records:
filteredRecord = {}
for field in fieldFilter:
if field in record:
@ -323,92 +406,159 @@ class DatabaseConnector:
result.append(filteredRecord)
return result
return data
return records
def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a new record in the table."""
data = self._loadTable(table)
# Add mandateId and userId if not present
if "mandateId" not in recordData or recordData["mandateId"] == 0:
recordData["mandateId"] = self.mandateId
if "userId" not in recordData or recordData["userId"] == 0:
recordData["userId"] = self.userId
# Determine the next ID if not present
if "id" not in recordData:
nextId = 1
if data:
nextId = max(record["id"] for record in data if "id" in record) + 1
recordData["id"] = nextId
# If the table is empty and a system ID should be registered
if not data:
self._registerInitialId(table, recordData["id"])
logger.info(f"Initial ID {recordData['id']} for table {table} has been registered")
# Add the new record
data.append(recordData)
# Save the updated table
if self._saveTable(table, data):
"""Creates a new record in the specified table."""
try:
# Ensure table directory exists
if not self._ensureTableDirectory(table):
raise ValueError(f"Error creating table directory for {table}")
# Load table metadata
metadata = self._loadTableMetadata(table)
# Generate new ID if not provided
if "id" not in recordData:
recordData["id"] = str(uuid.uuid4())
else:
# Ensure ID is a string
recordData["id"] = str(recordData["id"])
# Add context fields
recordData["_mandateId"] = self._mandateId
recordData["_userId"] = self._userId
# Update metadata
if "recordIds" not in metadata:
metadata["recordIds"] = []
metadata["recordIds"].append(recordData["id"])
metadata["recordIds"].sort()
# Add creation timestamp
currentTime = self._getCurrentTimestamp()
recordData["_createdAt"] = currentTime
recordData["_modifiedAt"] = currentTime
# Save the record
recordPath = self._getRecordPath(table, recordData["id"])
os.makedirs(os.path.dirname(recordPath), exist_ok=True)
with open(recordPath, 'w', encoding='utf-8') as f:
json.dump(recordData, f, indent=2, ensure_ascii=False)
# Save metadata
if not self._saveTableMetadata(table, metadata):
raise ValueError(f"Error saving metadata for table {table}")
# Update cache safely
if table in self._tablesCache:
if isinstance(self._tablesCache[table], list):
self._tablesCache[table].append(recordData)
else:
self._tablesCache[table] = [recordData]
else:
self._tablesCache[table] = [recordData]
# Verify the record was created
if not os.path.exists(recordPath):
raise ValueError(f"Record file was not created at {recordPath}")
return recordData
else:
except Exception as e:
logger.error(f"Error creating record in table {table}: {str(e)}")
raise ValueError(f"Error creating the record in table {table}")
def recordDelete(self, table: str, recordId: Union[str, int]) -> bool:
def recordDelete(self, table: str, recordId: str) -> bool:
"""Deletes a record from the table."""
data = self._loadTable(table)
# Load metadata
metadata = self._loadTableMetadata(table)
# Search for the record
for i, record in enumerate(data):
if "id" in record and record["id"] == recordId:
# Check if it's an initial record
initialId = self.getInitialId(table)
if initialId is not None and initialId == recordId:
self._removeInitialId(table)
logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
# Delete the record
del data[i]
# Save the updated table
return self._saveTable(table, data)
if recordId not in metadata["recordIds"]:
return False
# Check if it's an initial record
initialId = self.getInitialId(table)
if initialId is not None and initialId == recordId:
self._removeInitialId(table)
logger.info(f"Initial ID {recordId} for table {table} has been removed from the system table")
# Delete the record file
recordPath = self._getRecordPath(table, recordId)
try:
if os.path.exists(recordPath):
os.remove(recordPath)
# Update metadata cache
metadata["recordIds"].remove(recordId)
self._tableMetadataCache[table] = metadata
# Update table cache if it exists
if table in self._tablesCache:
self._tablesCache[table] = [r for r in self._tablesCache[table] if r.get("id") != recordId]
return True
except Exception as e:
logger.error(f"Error deleting record file {recordPath}: {e}")
return False
# Record not found
return False
def recordModify(self, table: str, recordId: Union[str, int], recordData: Dict[str, Any]) -> Dict[str, Any]:
def recordModify(self, table: str, recordId: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
"""Modifies a record in the table."""
data = self._loadTable(table)
# Ensure table directory exists
if not self._ensureTableDirectory(table):
raise ValueError(f"Error creating table directory for {table}")
# Load metadata to check if record exists
metadata = self._loadTableMetadata(table)
# Search for the record
for i, record in enumerate(data):
if "id" in record and record["id"] == recordId:
# Prevent changing the ID
if "id" in recordData and recordData["id"] != recordId:
raise ValueError(f"The ID of a record in table {table} cannot be changed")
# Update the record
for key, value in recordData.items():
data[i][key] = value
# Save the updated table
if self._saveTable(table, data):
return data[i]
else:
raise ValueError(f"Error updating record in table {table}")
# Ensure recordId is a string
recordId = str(recordId)
# Record not found
raise ValueError(f"Record with ID {recordId} not found in table {table}")
if recordId not in metadata["recordIds"]:
raise ValueError(f"Record with ID {recordId} not found in table {table}")
# Prevent changing the ID
if "id" in recordData and str(recordData["id"]) != recordId:
raise ValueError(f"The ID of a record in table {table} cannot be changed")
# Load existing record
existingRecord = self._loadRecord(table, recordId)
if not existingRecord:
raise ValueError(f"Record with ID {recordId} not found in table {table}")
# Update the record
for key, value in recordData.items():
existingRecord[key] = value
# Update modified timestamp
existingRecord["_modifiedAt"] = self._getCurrentTimestamp()
# Save the updated record
recordPath = self._getRecordPath(table, recordId)
try:
with open(recordPath, 'w', encoding='utf-8') as f:
json.dump(existingRecord, f, indent=2, ensure_ascii=False)
# Update table cache if it exists
if table in self._tablesCache:
for i, record in enumerate(self._tablesCache[table]):
if str(record.get("id")) == recordId:
self._tablesCache[table][i] = existingRecord
break
return existingRecord
except Exception as e:
logger.error(f"Error updating record file {recordPath}: {e}")
raise ValueError(f"Error updating record in table {table}")
def hasInitialId(self, table: str) -> bool:
"""Checks if an initial ID is registered for a table."""
systemData = self._loadSystemTable()
return table in systemData
def getInitialId(self, table: str) -> Optional[int]:
def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
systemData = self._loadSystemTable()
initialId = systemData.get(table)
@ -417,7 +567,7 @@ class DatabaseConnector:
logger.debug(f"No initial ID found for table {table}")
return initialId
def getAllInitialIds(self) -> Dict[str, int]:
def getAllInitialIds(self) -> Dict[str, str]:
"""Returns all registered initial IDs."""
systemData = self._loadSystemTable()
return systemData.copy() # Return a copy to protect the original

View file

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

View file

@ -25,11 +25,11 @@ class GatewayInterface:
Manages users and mandates.
"""
def __init__(self, mandateId: int = None, userId: int = None):
def __init__(self, _mandateId: str = None, _userId: str = None):
"""Initializes the Gateway Interface with optional mandate and user context."""
# Context can be empty during initialization
self.mandateId = mandateId
self.userId = userId
self._mandateId = _mandateId
self._userId = _userId
# Initialize database
self._initializeDatabase()
@ -40,41 +40,47 @@ class GatewayInterface:
# Initialize standard records if needed
self._initRecords()
def _getCurrentUserInfo(self) -> Dict[str, Any]:
"""Gets information about the current user including privileges."""
# For initialization, set default values
userInfo = {
"id": self.userId,
"mandateId": self.mandateId,
"privilege": "user", # Default privilege level
"language": "en"
}
# Try to load actual user info if IDs are provided
if self.userId:
userRecords = self.db.getRecordset("users", recordFilter={"id": self.userId})
if userRecords:
user = userRecords[0]
userInfo["privilege"] = user.get("privilege", "user")
userInfo["language"] = user.get("language", "en")
return userInfo
def _initializeDatabase(self):
"""Initializes the database connection."""
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway")
dbUser = APP_CONFIG.get("DB_GATEWAY_USER")
dbPassword = APP_CONFIG.get("DB_GATEWAY_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
dbUser=APP_CONFIG.get("DB_SYSTEM_USER"),
dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"),
mandateId=self.mandateId if self.mandateId else 0,
userId=self.userId if self.userId else 0
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
_mandateId=self._mandateId,
_userId=self._userId
)
def _getCurrentUserInfo(self) -> Optional[Dict[str, Any]]:
"""Returns information about the current user."""
if not self._userId:
return None
users = self.db.getRecordset("users", recordFilter={"id": self._userId})
if users:
return users[0]
return None
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
self._initRootMandate()
self._initAdminUser()
# Update database context with new IDs
if self._mandateId and self._userId:
self.db.updateContext(self._mandateId, self._userId)
# Reload user information with new context
self.currentUser = self._getCurrentUserInfo()
def _initRootMandate(self):
"""Creates the Root mandate if it doesn't exist."""
@ -89,8 +95,11 @@ class GatewayInterface:
createdMandate = self.db.recordCreate("mandates", rootMandate)
logger.info(f"Root mandate created with ID {createdMandate['id']}")
# Register the initial ID
self.db._registerInitialId("mandates", createdMandate['id'])
# Update mandate context
self.mandateId = createdMandate['id']
self._mandateId = createdMandate['id']
def _initAdminUser(self):
"""Creates the Admin user if it doesn't exist."""
@ -99,7 +108,7 @@ class GatewayInterface:
if existingUserId is None or not users:
logger.info("Creating Admin user")
adminUser = {
"mandateId": self.mandateId,
"_mandateId": self._mandateId,
"username": "admin",
"email": "admin@example.com",
"fullName": "Administrator",
@ -111,8 +120,11 @@ class GatewayInterface:
createdUser = self.db.recordCreate("users", adminUser)
logger.info(f"Admin user created with ID {createdUser['id']}")
# Register the initial ID
self.db._registerInitialId("users", createdUser['id'])
# Update user context
self.userId = createdUser['id']
self._userId = createdUser['id']
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
@ -126,9 +138,9 @@ class GatewayInterface:
Returns:
Filtered recordset with access control attributes
"""
return _uam(self.currentUser, table, recordset, self.mandateId, self.userId, self.db)
return _uam(self.currentUser, table, recordset, self._mandateId, self._userId, self.db)
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
@ -139,9 +151,9 @@ class GatewayInterface:
Returns:
Boolean indicating permission
"""
return _canModify(self.currentUser, table, recordId, self.mandateId, self.userId, self.db)
return _canModify(self.currentUser, table, recordId, self._mandateId, self._userId, self.db)
def getInitialId(self, table: str) -> Optional[int]:
def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
@ -165,7 +177,7 @@ class GatewayInterface:
allMandates = self.db.getRecordset("mandates")
return self._uam("mandates", allMandates)
def getMandate(self, mandateId: int) -> Optional[Dict[str, Any]]:
def getMandate(self, mandateId: str) -> Optional[Dict[str, Any]]:
"""Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
if not mandates:
@ -186,7 +198,7 @@ class GatewayInterface:
return self.db.recordCreate("mandates", mandateData)
def updateMandate(self, mandateId: int, mandateData: Dict[str, Any]) -> Dict[str, Any]:
def updateMandate(self, mandateId: str, mandateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a mandate if user has access."""
# Check if the mandate exists and user has access
mandate = self.getMandate(mandateId)
@ -199,7 +211,7 @@ class GatewayInterface:
# Update the mandate
return self.db.recordModify("mandates", mandateId, mandateData)
def deleteMandate(self, mandateId: int) -> bool:
def deleteMandate(self, mandateId: str) -> bool:
"""
Deletes a mandate and all associated users and data if user has permission.
"""
@ -248,15 +260,15 @@ class GatewayInterface:
return filteredUsers
def getUsersByMandate(self, mandateId: int) -> List[Dict[str, Any]]:
def getUsersByMandate(self, _mandateId: str) -> List[Dict[str, Any]]:
"""Returns users for a specific mandate if user has access."""
# First check if user has access to the mandate
mandate = self.getMandate(mandateId)
mandate = self.getMandate(_mandateId)
if not mandate:
return []
# Get users for this mandate
users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId})
users = self.db.getRecordset("users", recordFilter={"_mandateId": _mandateId})
filteredUsers = self._uam("users", users)
# Remove password hashes
@ -268,6 +280,7 @@ class GatewayInterface:
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
"""Returns a user by username."""
# Get all users without mandate filter
users = self.db.getRecordset("users")
for user in users:
if user.get("username") == username:
@ -278,9 +291,9 @@ class GatewayInterface:
logger.debug(f"No user found with username {username}")
return None
def getUser(self, userId: int) -> Optional[Dict[str, Any]]:
def getUser(self, _userId: str) -> Optional[Dict[str, Any]]:
"""Returns a user by ID if user has access."""
users = self.db.getRecordset("users", recordFilter={"id": userId})
users = self.db.getRecordset("users", recordFilter={"_userId": _userId})
if not users:
return None
@ -299,31 +312,58 @@ class GatewayInterface:
return user
def createUser(self, username: str, password: str, email: str = None,
fullName: str = None, language: str = "de", mandateId: int = None,
fullName: str = None, language: str = "de", _mandateId: str = None,
disabled: bool = False, privilege: str = "user") -> Dict[str, Any]:
"""Creates a new user if current user has permission."""
# Validate username
if not username or len(username) < 3:
raise ValueError("Benutzername muss mindestens 3 Zeichen lang sein")
# Validate password
if not password:
raise ValueError("Passwort ist erforderlich")
# Password requirements
if len(password) < 8:
raise ValueError("Passwort muss mindestens 8 Zeichen lang sein")
if not any(c.isupper() for c in password):
raise ValueError("Passwort muss mindestens einen Grossbuchstaben enthalten")
if not any(c.islower() for c in password):
raise ValueError("Passwort muss mindestens einen Kleinbuchstaben enthalten")
if not any(c.isdigit() for c in password):
raise ValueError("Passwort muss mindestens eine Zahl enthalten")
if not any(c in "!@#$%^&*(),.?\":{}|<>" for c in password):
raise ValueError("Passwort muss mindestens ein Sonderzeichen enthalten")
# Validate email if provided
if email:
import re
email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
if not re.match(email_pattern, email):
raise ValueError("Ungültiges E-Mail-Format")
# Check if the username already exists
existingUser = self.getUserByUsername(username)
if existingUser:
raise ValueError(f"User '{username}' already exists")
raise ValueError(f"Benutzer '{username}' existiert bereits")
# Use the provided mandateId or the current context
userMandateId = mandateId if mandateId is not None else self.mandateId
# Use the provided _mandateId or the current context
userMandateId = _mandateId if _mandateId is not None else self._mandateId
# Check if user has access to the mandate
if userMandateId != self.mandateId and self.currentUser.get("privilege") != "sysadmin":
raise PermissionError(f"No permission to create users in mandate {userMandateId}")
if userMandateId != self._mandateId and self.currentUser.get("privilege") != "sysadmin":
raise PermissionError(f"Keine Berechtigung, Benutzer in Mandat {userMandateId} zu erstellen")
if not self._canModify("users"):
raise PermissionError("No permission to create users")
raise PermissionError("Keine Berechtigung, Benutzer zu erstellen")
# Check privilege escalation
if (privilege == "sysadmin" or
(privilege == "admin" and self.currentUser.get("privilege") == "user")):
raise PermissionError(f"Cannot create user with higher privilege: {privilege}")
raise PermissionError(f"Keine Berechtigung, Benutzer mit höherem Privileg zu erstellen: {privilege}")
userData = {
"mandateId": userMandateId,
"_mandateId": userMandateId,
"username": username,
"email": email,
"fullName": fullName,
@ -335,6 +375,10 @@ class GatewayInterface:
createdUser = self.db.recordCreate("users", userData)
# Clear the users table from cache to ensure fresh data
if "users" in self.db._tablesCache:
del self.db._tablesCache["users"]
# Return the complete user record
return createdUser
@ -344,9 +388,8 @@ class GatewayInterface:
if "users" in self.db._tablesCache:
del self.db._tablesCache["users"]
# Get fresh user data
users = self.db.getRecordset("users")
user = next((u for u in users if u.get("username") == username), None)
# Get user by username
user = self.getUserByUsername(username)
if not user:
raise ValueError("Benutzer nicht gefunden")
@ -366,19 +409,19 @@ class GatewayInterface:
return authenticatedUser
def updateUser(self, userId: int, userData: Dict[str, Any]) -> Dict[str, Any]:
def updateUser(self, _userId: str, userData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a user if current user has permission."""
# Check if the user exists and current user has access
user = self.getUser(userId)
user = self.getUser(_userId)
if not user:
# Try to get the raw user record for admin access check
users = self.db.getRecordset("users", recordFilter={"id": userId})
users = self.db.getRecordset("users", recordFilter={"_userId": _userId})
if not users:
raise ValueError(f"User with ID {userId} not found")
raise ValueError(f"User with ID {_userId} not found")
# Check if current user is admin/sysadmin
if not self._canModify("users", userId):
raise PermissionError(f"No permission to update user {userId}")
if not self._canModify("users", _userId):
raise PermissionError(f"No permission to update user {_userId}")
user = users[0]
@ -397,7 +440,7 @@ class GatewayInterface:
del userData["password"]
# Update the user
updatedUser = self.db.recordModify("users", userId, userData)
updatedUser = self.db.recordModify("users", _userId, userData)
# Remove password hash from the response
if "hashedPassword" in updatedUser:
@ -405,53 +448,53 @@ class GatewayInterface:
return updatedUser
def disableUser(self, userId: int) -> Dict[str, Any]:
def disableUser(self, _userId: str) -> Dict[str, Any]:
"""Disables a user if current user has permission."""
return self.updateUser(userId, {"disabled": True})
return self.updateUser(_userId, {"disabled": True})
def enableUser(self, userId: int) -> Dict[str, Any]:
def enableUser(self, _userId: str) -> Dict[str, Any]:
"""Enables a user if current user has permission."""
return self.updateUser(userId, {"disabled": False})
return self.updateUser(_userId, {"disabled": False})
def _deleteUserReferencedData(self, userId: int) -> None:
def _deleteUserReferencedData(self, _userId: str) -> None:
"""Deletes all data associated with a user."""
# Delete user attributes
try:
attributes = self.db.getRecordset("attributes", recordFilter={"userId": userId})
attributes = self.db.getRecordset("attributes", recordFilter={"_userId": _userId})
for attribute in attributes:
self.db.recordDelete("attributes", attribute["id"])
except Exception as e:
logger.error(f"Error deleting attributes for user {userId}: {e}")
logger.error(f"Error deleting attributes for user {_userId}: {e}")
logger.info(f"All referenced data for user {userId} has been deleted")
logger.info(f"All referenced data for user {_userId} has been deleted")
def deleteUser(self, userId: int) -> bool:
def deleteUser(self, _userId: str) -> bool:
"""Deletes a user and all associated data if current user has permission."""
# Check if the user exists
users = self.db.getRecordset("users", recordFilter={"id": userId})
users = self.db.getRecordset("users", recordFilter={"_userId": _userId})
if not users:
return False
# Check if current user has permission
if not self._canModify("users", userId):
raise PermissionError(f"No permission to delete user {userId}")
if not self._canModify("users", _userId):
raise PermissionError(f"No permission to delete user {_userId}")
# Check if it's the initial user
initialUserId = self.getInitialId("users")
if initialUserId is not None and userId == initialUserId:
if initialUserId is not None and _userId == initialUserId:
logger.warning("Attempt to delete the Root Admin was prevented")
return False
# Delete all data associated with the user
self._deleteUserReferencedData(userId)
self._deleteUserReferencedData(_userId)
# Delete the user
success = self.db.recordDelete("users", userId)
success = self.db.recordDelete("users", _userId)
if success:
logger.info(f"User with ID {userId} was successfully deleted")
logger.info(f"User with ID {_userId} was successfully deleted")
else:
logger.error(f"Error deleting user with ID {userId}")
logger.error(f"Error deleting user with ID {_userId}")
return success
@ -459,15 +502,16 @@ class GatewayInterface:
# Singleton factory for GatewayInterface instances per context
_gatewayInterfaces = {}
def getGatewayInterface(mandateId: int = None, userId: int = None) -> GatewayInterface:
def getGatewayInterface(_mandateId: str = None, _userId: str = None) -> GatewayInterface:
"""
Returns a GatewayInterface instance for the specified context.
Reuses existing instances.
"""
contextKey = f"{mandateId}_{userId}"
# For initialization, use empty strings instead of None
contextKey = f"{_mandateId or ''}_{_userId or ''}"
if contextKey not in _gatewayInterfaces:
_gatewayInterfaces[contextKey] = GatewayInterface(mandateId, userId)
_gatewayInterfaces[contextKey] = GatewayInterface(_mandateId or '', _userId or '')
return _gatewayInterfaces[contextKey]
# Initialize an instance
getGatewayInterface()
# Initialize an instance with empty strings
getGatewayInterface('', '')

View file

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

View file

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

View file

@ -24,6 +24,24 @@ from modules.connectors.connectorAiOpenai import ChatService
from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__)
# Initialize AI service at module level
_aiService = None
def initializeAIService():
"""Initialize the AI service for the LucyDOM interface."""
global _aiService
if _aiService is None:
try:
_aiService = ChatService()
logger.info("AI service initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize AI service: {str(e)}")
_aiService = None
return _aiService
# Initialize AI service when module is imported
initializeAIService()
# Custom exceptions for file handling
class FileError(Exception):
"""Base class for file handling exceptions."""
@ -45,6 +63,7 @@ class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file."""
pass
from modules.security.auth import getInitialContext
class LucyDOMInterface:
"""
@ -52,14 +71,19 @@ class LucyDOMInterface:
Uses the JSON connector for data access.
"""
def __init__(self, mandateId: int, userId: int):
def __init__(self, _mandateId: str, _userId: str):
"""Initializes the LucyDOM Interface with mandate and user context."""
self.mandateId = mandateId
self.userId = userId
logger.debug(f"Initializing LucyDOMInterface with mandateId={_mandateId}, userId={_userId}")
self._mandateId = _mandateId
self._userId = _userId
# Add language settings
self.userLanguage = "en" # Default user language
self.aiService = None # Will be set externally
# Set AI service from module-level instance
self.aiService = _aiService
if not self.aiService:
logger.warning("AI service not available during LucyDOMInterface initialization")
# Initialize database connector
self._initializeDatabase()
@ -68,10 +92,22 @@ class LucyDOMInterface:
self.currentUser = self._getCurrentUserInfo()
# Initialize access control
self.access = LucyDOMAccess(self.currentUser, self.mandateId, self.userId)
self.access = LucyDOMAccess(self.currentUser, self._mandateId, self._userId)
self.access.db = self.db # Share database connection
# Initialize standard database records if needed
# Get initial IDs if not provided
if not self._mandateId or not self._userId:
logger.debug("No context provided, getting initial context from auth")
self._mandateId, self._userId = getInitialContext()
logger.debug(f"Retrieved initial context: mandate={self._mandateId}, user={self._userId}")
if self._mandateId and self._userId:
self.db.updateContext(self._mandateId, self._userId)
logger.debug(f"Updated database context with initial IDs")
else:
logger.warning("No initial context available from auth")
# Initialize standard records if needed
self._initRecords()
def _getCurrentUserInfo(self) -> Dict[str, Any]:
@ -79,74 +115,64 @@ class LucyDOMInterface:
# For production, you would get this from authentication
# For now return basic user info with default privilege
return {
"id": self.userId,
"mandateId": self.mandateId,
"id": self._userId,
"_mandateId": self._mandateId,
"privilege": "user", # Default privilege level
"language": self.userLanguage
}
def _initializeDatabase(self):
"""Initializes the database connection."""
effectiveMandateId = self.mandateId
effectiveUserId = self.userId
if effectiveMandateId is None or effectiveUserId is None:
return
self.db = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_LUCYDOM_HOST"),
dbDatabase=APP_CONFIG.get("DB_LUCYDOM_DATABASE"),
dbUser=APP_CONFIG.get("DB_LUCYDOM_USER"),
dbPassword=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"),
mandateId=self.mandateId,
userId=self.userId,
skipInitialIdLookup=True
_mandateId=self._mandateId,
_userId=self._userId,
skipInitialIdLookup=True
)
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
self._initializeStandardPrompts()
# Only initialize prompts if we have valid context
if self._mandateId and self._userId:
logger.debug(f"Initializing prompts with context: mandate={self._mandateId}, user={self._userId}")
self._initializeStandardPrompts()
else:
logger.warning("Skipping prompt initialization - no valid context available")
def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist."""
prompts = self.db.getRecordset("prompts")
logger.debug(f"Found {len(prompts)} existing prompts")
if not prompts:
logger.info("Creating standard prompts")
logger.debug("Creating standard prompts")
# Define standard prompts
standardPrompts = [
{
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Research the current market trends and developments in [TOPIC]. Collect information about leading companies, innovative products or services, and current challenges. Present the results in a structured overview with relevant data and sources.",
"name": "Web Research: Market Research"
},
{
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Analyze the attached dataset on [TOPIC] and identify the most important trends, patterns, and anomalies. Perform statistical calculations to support your findings. Present the results in a clearly structured analysis and draw relevant conclusions.",
"name": "Analysis: Data Analysis"
},
{
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Create a detailed protocol of our meeting on [TOPIC]. Capture all discussed points, decisions made, and agreed measures. Structure the protocol clearly with agenda items, participant list, and clear responsibilities for follow-up actions.",
"name": "Protocol: Meeting Minutes"
},
{
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Develop a UI/UX design concept for [APPLICATION/WEBSITE]. Consider the target audience, main functions, and brand identity. Describe the visual design, navigation, interaction patterns, and information architecture. Explain how the design optimizes user-friendliness and user experience.",
"name": "Design: UI/UX Design"
},
{
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Gib mir die ersten 1000 Primzahlen",
"name": "Code: Primzahlen"
},
{
"mandateId": self.mandateId,
"userId": self.userId,
"content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
"name": "Mail: Vorbereitung"
},
@ -155,13 +181,15 @@ class LucyDOMInterface:
# Create prompts
for promptData in standardPrompts:
createdPrompt = self.db.recordCreate("prompts", promptData)
logger.info(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']}")
logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('_mandateId')}, user={createdPrompt.get('_userId')}")
else:
logger.debug("Prompts already exist, skipping creation")
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module."""
return self.access._uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool:
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module."""
return self.access._canModify(table, recordId)
@ -170,7 +198,7 @@ class LucyDOMInterface:
def setUserLanguage(self, languageCode: str):
"""Set the user's preferred language"""
self.userLanguage = languageCode
logger.info(f"User language set to: {languageCode}")
logger.debug(f"User language set to: {languageCode}")
# AI Call Root Function
@ -208,7 +236,7 @@ class LucyDOMInterface:
# Utilities
def getInitialId(self, table: str) -> Optional[int]:
def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
@ -223,7 +251,7 @@ class LucyDOMInterface:
allPrompts = self.db.getRecordset("prompts")
return self._uam("prompts", allPrompts)
def getPrompt(self, promptId: int) -> Optional[Dict[str, Any]]:
def getPrompt(self, promptId: str) -> Optional[Dict[str, Any]]:
"""Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
if not prompts:
@ -238,8 +266,6 @@ class LucyDOMInterface:
raise PermissionError("No permission to create prompts")
promptData = {
"mandateId": self.mandateId,
"userId": self.userId,
"content": content,
"name": name,
"createdAt": self._getCurrentTimestamp()
@ -247,7 +273,7 @@ class LucyDOMInterface:
return self.db.recordCreate("prompts", promptData)
def updatePrompt(self, promptId: int, content: str = None, name: str = None) -> Dict[str, Any]:
def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Dict[str, Any]:
"""Updates a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
@ -268,7 +294,7 @@ class LucyDOMInterface:
# Update prompt
return self.db.recordModify("prompts", promptId, promptData)
def deletePrompt(self, promptId: int) -> bool:
def deletePrompt(self, promptId: str) -> bool:
"""Deletes a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
@ -290,8 +316,8 @@ class LucyDOMInterface:
"""Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash,
"mandateId": self.mandateId,
"userId": self.userId
"_mandateId": self._mandateId,
"_userId": self._userId
})
if files:
return files[0]
@ -334,7 +360,7 @@ class LucyDOMInterface:
allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles)
def getFile(self, fileId: int) -> Optional[Dict[str, Any]]:
def getFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files:
@ -349,8 +375,8 @@ class LucyDOMInterface:
raise PermissionError("No permission to create files")
fileData = {
"mandateId": self.mandateId,
"userId": self.userId,
"_mandateId": self._mandateId,
"_userId": self._userId,
"name": name,
"mimeType": mimeType,
"size": size,
@ -359,7 +385,7 @@ class LucyDOMInterface:
}
return self.db.recordCreate("files", fileData)
def updateFile(self, fileId: int, updateData: Dict[str, Any]) -> Dict[str, Any]:
def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates file metadata if user has access."""
# Check if the file exists and user has access
file = self.getFile(fileId)
@ -372,7 +398,7 @@ class LucyDOMInterface:
# Update file
return self.db.recordModify("files", fileId, updateData)
def deleteFile(self, fileId: int) -> bool:
def deleteFile(self, fileId: str) -> bool:
"""Deletes a file if user has access."""
try:
# Check if the file exists and user has access
@ -396,7 +422,7 @@ class LucyDOMInterface:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries:
self.db.recordDelete("fileData", fileId)
logger.info(f"FileData for file {fileId} deleted")
logger.debug(f"FileData for file {fileId} deleted")
except Exception as e:
logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}")
@ -413,7 +439,7 @@ class LucyDOMInterface:
# FileData methods - data operations
def createFileData(self, fileId: int, data: bytes) -> bool:
def createFileData(self, fileId: str, data: bytes) -> bool:
"""Stores the binary data of a file in the database."""
try:
import base64
@ -459,13 +485,13 @@ class LucyDOMInterface:
}
self.db.recordCreate("fileData", fileDataObj)
logger.info(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
logger.debug(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error storing data for file {fileId}: {str(e)}")
return False
def getFileData(self, fileId: int) -> Optional[bytes]:
def getFileData(self, fileId: str) -> Optional[bytes]:
"""Returns the binary data of a file if user has access."""
# Check file access
file = self.getFile(fileId)
@ -499,7 +525,7 @@ class LucyDOMInterface:
logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None
def updateFileData(self, fileId: int, data: Union[bytes, str]) -> bool:
def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool:
"""Updates file data if user has access."""
# Check file access
file = self.getFile(fileId)
@ -573,12 +599,12 @@ class LucyDOMInterface:
if fileDataEntries:
# Update the existing record
self.db.recordModify("fileData", fileId, dataUpdate)
logger.info(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
logger.debug(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
else:
# Create a new record
dataUpdate["id"] = fileId
self.db.recordCreate("fileData", dataUpdate)
logger.info(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
logger.debug(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
@ -592,7 +618,7 @@ class LucyDOMInterface:
if not self._canModify("files"):
raise PermissionError("No permission to upload files")
logger.info(f"Starting upload process for file: {fileName}")
logger.debug(f"Starting upload process for file: {fileName}")
if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}")
@ -605,7 +631,7 @@ class LucyDOMInterface:
# Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
logger.info(f"Duplicate found for {fileName}: {existingFile['id']}")
logger.debug(f"Duplicate found for {fileName}: {existingFile['id']}")
return existingFile
# Determine MIME type and size
@ -613,7 +639,7 @@ class LucyDOMInterface:
fileSize = len(fileContent)
# Save metadata
logger.info(f"Saving file metadata to database for file: {fileName}")
logger.debug(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile(
name=fileName,
mimeType=mimeType,
@ -622,17 +648,17 @@ class LucyDOMInterface:
)
# Save binary data
logger.info(f"Saving file content to database for file: {fileName}")
logger.debug(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
logger.info(f"File upload process completed for: {fileName}")
logger.debug(f"File upload process completed for: {fileName}")
return dbFile
except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
raise FileStorageError(f"Error saving file: {str(e)}")
def downloadFile(self, fileId: int) -> Optional[Dict[str, Any]]:
def downloadFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file for download if user has access."""
try:
# Check file access
@ -667,10 +693,10 @@ class LucyDOMInterface:
allWorkflows = self.db.getRecordset("workflows")
return self._uam("workflows", allWorkflows)
def getWorkflowsByUser(self, userId: int) -> List[Dict[str, Any]]:
def getWorkflowsByUser(self, _userId: str) -> List[Dict[str, Any]]:
"""Returns workflows for a specific user if current user has access."""
# Get workflows by userId
workflows = self.db.getRecordset("workflows", recordFilter={"userId": userId})
# Get workflows by _userId
workflows = self.db.getRecordset("workflows", recordFilter={"_userId": _userId})
# Apply access control
return self._uam("workflows", workflows)
@ -690,11 +716,11 @@ class LucyDOMInterface:
raise PermissionError("No permission to create workflows")
# Make sure mandateId and userId are set
if "mandateId" not in workflowData:
workflowData["mandateId"] = self.mandateId
if "_mandateId" not in workflowData:
workflowData["_mandateId"] = self._mandateId
if "userId" not in workflowData:
workflowData["userId"] = self.userId
if "_userId" not in workflowData:
workflowData["_userId"] = self._userId
# Set timestamp if not present
currentTime = self._getCurrentTimestamp()
@ -877,7 +903,7 @@ class LucyDOMInterface:
# Update the message
updatedMessage = self.db.recordModify("workflowMessages", messageId, messageData)
if updatedMessage:
logger.info(f"Message {messageId} updated successfully")
logger.debug(f"Message {messageId} updated successfully")
else:
logger.warning(f"Failed to update message {messageId}")
@ -912,7 +938,7 @@ class LucyDOMInterface:
logger.error(f"Error deleting message {messageId}: {str(e)}")
return False
def deleteFileFromMessage(self, workflowId: str, messageId: str, fileId: int) -> bool:
def deleteFileFromMessage(self, workflowId: str, messageId: str, fileId: str) -> bool:
"""Removes a file reference from a message if user has access."""
try:
# Check workflow access
@ -924,7 +950,7 @@ class LucyDOMInterface:
if not self._canModify("workflows", workflowId):
raise PermissionError(f"No permission to modify workflow {workflowId}")
logger.info(f"Removing file {fileId} from message {messageId} in workflow {workflowId}")
logger.debug(f"Removing file {fileId} from message {messageId} in workflow {workflowId}")
# Get all workflow messages
allMessages = self.getWorkflowMessages(workflowId)
@ -951,7 +977,7 @@ class LucyDOMInterface:
return False
# Log the found message
logger.info(f"Found message: {message.get('id')}")
logger.debug(f"Found message: {message.get('id')}")
# Check if message has documents
if "documents" not in message or not message["documents"]:
@ -980,7 +1006,7 @@ class LucyDOMInterface:
if shouldRemove:
removed = True
logger.info(f"Found file to remove: docId={docId}, fileId={fileIdValue}")
logger.debug(f"Found file to remove: docId={docId}, fileId={fileIdValue}")
else:
updatedDocuments.append(doc)
@ -997,7 +1023,7 @@ class LucyDOMInterface:
updated = self.db.recordModify("workflowMessages", message["id"], messageUpdate)
if updated:
logger.info(f"Successfully removed file {fileId} from message {messageId}")
logger.debug(f"Successfully removed file {fileId} from message {messageId}")
return True
else:
logger.warning(f"Failed to update message {messageId} in database")
@ -1081,8 +1107,8 @@ class LucyDOMInterface:
# Extract only the database-relevant workflow fields
workflowDbData = {
"id": workflowId,
"mandateId": workflow.get("mandateId", self.mandateId),
"userId": workflow.get("userId", self.userId),
"_mandateId": workflow.get("_mandateId", self._mandateId),
"_userId": workflow.get("_userId", self._userId),
"name": workflow.get("name", f"Workflow {workflowId}"),
"status": workflow.get("status", "completed"),
"startedAt": workflow.get("startedAt", self._getCurrentTimestamp()),
@ -1187,13 +1213,13 @@ class LucyDOMInterface:
messageIds = [msg.get("id") for msg in messages]
# Update in database
self.updateWorkflow(workflowId, {"messageIds": messageIds})
logger.info(f"Rebuilt messageIds for workflow {workflowId}")
logger.debug(f"Rebuilt messageIds for workflow {workflowId}")
# Log document counts for each message
for msg in messages:
docCount = len(msg.get("documents", []))
if docCount > 0:
logger.info(f"Message {msg.get('id')} has {docCount} documents loaded from database")
logger.debug(f"Message {msg.get('id')} has {docCount} documents loaded from database")
# Load logs
logs = self.getWorkflowLogs(workflowId)
@ -1218,16 +1244,16 @@ class LucyDOMInterface:
try:
# Get token from database using current user's mandateId and userId
tokens = self.db.getRecordset("msftTokens", recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
"_mandateId": self._mandateId,
"_userId": self._userId
})
if tokens and len(tokens) > 0:
token_data = json.loads(tokens[0]["token_data"])
logger.info(f"Retrieved Microsoft token for user {self.userId}")
logger.debug(f"Retrieved Microsoft token for user {self._userId}")
return token_data
else:
logger.info(f"No Microsoft token found for user {self.userId}")
logger.debug(f"No Microsoft token found for user {self._userId}")
return None
except Exception as e:
@ -1239,8 +1265,8 @@ class LucyDOMInterface:
try:
# Check if token already exists
tokens = self.db.getRecordset("msftTokens", recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
"_mandateId": self._mandateId,
"_userId": self._userId
})
if tokens and len(tokens) > 0:
@ -1251,18 +1277,18 @@ class LucyDOMInterface:
"updated_at": datetime.now().isoformat()
}
self.db.recordModify("msftTokens", token_id, updated_data)
logger.info(f"Updated Microsoft token for user {self.userId}")
logger.debug(f"Updated Microsoft token for user {self._userId}")
else:
# Create new token
# Create new token with UUID
new_token = {
"mandateId": self.mandateId,
"userId": self.userId,
"_mandateId": self._mandateId,
"_userId": self._userId,
"token_data": json.dumps(token_data),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.db.recordCreate("msftTokens", new_token)
logger.info(f"Saved new Microsoft token for user {self.userId}")
logger.debug(f"Saved new Microsoft token for user {self._userId}")
return True
@ -1273,20 +1299,23 @@ class LucyDOMInterface:
# Singleton factory for LucyDOMInterface instances per context
_lucydomInterfaces = {}
def getLucydomInterface(mandateId: int = 0, userId: int = 0) -> LucyDOMInterface:
def getLucydomInterface(_mandateId: str = None, _userId: str = None) -> LucyDOMInterface:
"""
Returns a LucyDOMInterface instance for the specified context.
Reuses existing instances.
Ensures AI service is initialized and preserves it across instances.
"""
contextKey = f"{mandateId}_{userId}"
# For initialization, use empty strings instead of None
contextKey = f"{_mandateId or ''}_{_userId or ''}"
# Ensure AI service is initialized
if _aiService is None:
initializeAIService()
# Create new instance if needed
if contextKey not in _lucydomInterfaces:
# Create new interface instance
interface = LucyDOMInterface(mandateId, userId)
# Initialize AI service
aiService = ChatService()
interface.aiService = aiService
_lucydomInterfaces[contextKey] = interface
_lucydomInterfaces[contextKey] = LucyDOMInterface(_mandateId or '', _userId or '')
return _lucydomInterfaces[contextKey]
# Initialize an instance
getLucydomInterface()
# Initialize default instance with empty strings
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 typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
# CORE MODELS
class Label(BaseModel):
"""Label for an attribute or a class with support for multiple languages"""
default: str
@ -20,9 +24,7 @@ class Label(BaseModel):
class Prompt(BaseModel):
"""Data model for a prompt"""
id: int = Field(description="Unique ID of the prompt")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the creator")
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the prompt")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Display name of the prompt")
@ -34,23 +36,18 @@ class Prompt(BaseModel):
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
"name": Label(default="Name", translations={"en": "Label", "fr": "Nom"}),
"name": Label(default="Name", translations={"en": "Label", "fr": "Nom"})
}
class FileItem(BaseModel):
"""Data model for a file"""
id: int = Field(description="Unique ID of the data object")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the creator")
name: str = Field(description="Name of the data object")
mimeType: str = Field(description="Type of the data object MIME type")
size: Optional[int] = Field(None, description="Size of the data object in bytes")
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the data object")
mimeType: str = Field(description="Type of the file MIME type")
fileName: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file in bytes")
fileHash: str = Field(description="Hash code for deduplication")
creationDate: Optional[str] = Field(None, description="Upload date")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
label: Label = Field(
@ -61,99 +58,88 @@ class FileItem(BaseModel):
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"mimeType": Label(default="Type", translations={"en": "Type", "fr": "Type"}),
"size": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
"fileName": Label(default="Filename", translations={"en": "fileName", "fr": "Nom de fichier"}),
"fileSize": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
"fileHash": Label(default="File Hash", translations={"en": "Hash", "fr": "Hash"}),
"creationDate": Label(default="Upload date", translations={"en": "Upload date", "fr": "Date de téléchargement"}),
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"})
}
class FileData(BaseModel):
"""Data model for file content"""
id: int = Field(description="Unique ID of the data object")
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the data object")
data: str = Field(description="content of the file, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
class UserInputRequest(BaseModel):
"""Request for user input to a running workflow"""
prompt: str = Field(description="Message from the user")
listFileId: List[str] = Field(default=[], description="List of FileItem IDs")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for the request")
class MsftToken(BaseModel):
"""Data model for Microsoft authentication tokens"""
id: int = Field(description="Unique ID of the token")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the user")
token_data: str = Field(description="JSON string containing the token data")
created_at: str = Field(description="Timestamp when the token was created")
updated_at: str = Field(description="Timestamp when the token was last updated")
label: Label = Field(
default=Label(default="Microsoft Token", translations={"en": "Microsoft Token", "fr": "Jeton Microsoft"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"token_data": Label(default="Token Data", translations={"en": "Token Data", "fr": "Données du jeton"}),
"created_at": Label(default="Created At", translations={"en": "Created At", "fr": "Créé le"}),
"updated_at": Label(default="Updated At", translations={"en": "Updated At", "fr": "Mis à jour le"})
}
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the token")
tokenData: str = Field(description="JSON string containing the token data")
expiresAt: datetime = Field(description="Expiration date and time")
refreshToken: Optional[str] = Field(None, description="Refresh token if available")
scope: str = Field(description="Token scope")
# Workflow model classes
# WORKFLOW MODELS
class DocumentContent(BaseModel):
"""Content of a document in the workflow"""
class ChatContent(BaseModel):
"""Content of a document in the chat"""
sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
name: str = Field(description="Designation")
ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
mimeType: str = Field(description="MIME type")
summary: str = Field(description="Summary of the file content")
data: str = Field(description="Actual content, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content, such as isText flag, format information, encoding, etc.")
data: str = Field(description="Actual content")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content")
class Document(BaseModel):
"""Document in the workflow - References a file directly in the database"""
id: str = Field(description="Unique ID of the document")
name: str = Field(description="Name of the data object")
ext: str = Field(description="Extension of the data object")
fileId: int = Field(description="ID of the referenced file in the database")
class ChatDocument(BaseModel):
"""Document in the chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the document")
fileId: str = Field(description="ID of the referenced file in the database")
fileName: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file in bytes")
mimeType: str = Field(description="MIME type")
data: str = Field(description="Content of the data as text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
contents: List[DocumentContent] = Field(description="Document contents")
contents: List[ChatContent] = Field(default=[], description="Document contents")
class DataStats(BaseModel):
class ChatStat(BaseModel):
"""Statistics for performance and data usage"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the stats")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Token count (for AI models)")
bytesSent: Optional[int] = Field(None, description="Bytes sent")
bytesReceived: Optional[int] = Field(None, description="Bytes received")
class WorkflowMessage(BaseModel):
"""Message object in the workflow"""
id: str = Field(description="Unique ID of the message")
class ChatMessage(BaseModel):
"""Message object in the chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the message")
workflowId: str = Field(description="Reference to the parent workflow")
parentMessageId: Optional[str] = Field(None, description="Reference to the replied message")
startedAt: str = Field(description="Timestamp for message creation")
finishedAt: Optional[str] = Field(None, description="Timestamp for message completion")
sequenceNo: int = Field(description="Sequence number for sorting")
status: str = Field(description="Status of the message ('first', 'step', 'last')")
role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
dataStats: Optional[DataStats] = Field(None, description="Statistics")
documents: Optional[List[Document]] = Field(None, description="Documents in this message (references to files in the database)")
content: Optional[str] = Field(None, description="Text content of the message")
agentName: Optional[str] = Field(None, description="Name of the agent used")
documents: Optional[List[ChatDocument]] = Field(None, description="Documents in this message")
message: Optional[str] = Field(None, description="Text content of the message")
role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
status: str = Field(description="Status of the message ('first', 'step', 'last')")
class WorkflowLog(BaseModel):
"""Log entry for a workflow"""
id: str = Field(description="Unique ID of the log entry")
sequenceNr: int = Field(description="Sequence number for sorting")
startedAt: datetime = Field(description="Timestamp for message creation")
finishedAt: Optional[datetime] = Field(None, description="Timestamp for message completion")
stats: Optional[ChatStat] = Field(None, description="Statistics")
class ChatLog(BaseModel):
"""Log entry for a chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the log entry")
workflowId: str = Field(description="ID of the associated workflow")
message: str = Field(description="Log message content")
type: str = Field(description="Type of log ('info', 'warning', 'error')")
@ -161,105 +147,50 @@ class WorkflowLog(BaseModel):
agentName: str = Field(description="Name of the agent that created the log")
status: str = Field(description="Status of the workflow at log time")
progress: Optional[int] = Field(None, description="Progress value (0-100)")
mandateId: Optional[int] = Field(None, description="ID of the mandate")
userId: Optional[int] = Field(None, description="ID of the user")
class Workflow(BaseModel):
"""Workflow object for multi-agent system"""
id: str = Field(description="Unique ID of the workflow")
name: Optional[str] = Field(None, description="Name of the workflow")
mandateId: int = Field(description="ID of the mandate")
userId: int = Field(description="ID of the user")
status: str = Field(description="Status of the workflow ('running', 'completed', 'failed', 'stopped')")
startedAt: str = Field(description="Start timestamp")
class ChatWorkflow(BaseModel):
"""Chat workflow object for multi-agent system"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the chat workflow")
status: str = Field(description="Status of the chat workflow")
name: Optional[str] = Field(None, description="Name of the chat workflow")
currentRound: int = Field(default=1, description="Current round/iteration")
lastActivity: str = Field(description="Timestamp of the last activity")
dataStats: Optional[Dict[str, Any]] = Field(None, description="Total statistics")
currentRound: int = Field(default=1, description="Current round/iteration of the workflow")
messageIds: List[str] = Field(default=[], description="List of message IDs in this workflow")
messages: List[WorkflowMessage] = Field(default=[], description="Message history (in-memory representation)")
logs: List[WorkflowLog] = Field(default=[], description="Log entries (in-memory representation)")
startedAt: str = Field(description="Start timestamp")
logs: List[ChatLog] = Field(default=[], description="Log entries")
messages: List[ChatMessage] = Field(default=[], description="Message history")
stats: Optional[ChatStat] = Field(None, description="Statistics")
# Agent and Workflow Task Models
# AGENT AND TASK MODELS
class AgentResult(BaseModel):
"""Result structure returned by agent processing"""
feedback: str = Field(description="Text response explaining what the agent did")
documents: List[Document] = Field(default=[], description="List of document objects created by the agent")
label: Label = Field(
default=Label(default="Agent Result", translations={"en": "Agent Result", "fr": "Résultat d'agent"}),
description="Label for the class"
)
class AgentInfo(BaseModel):
"""Information about an agent's capabilities"""
class Agent(BaseModel):
"""Data model for an agent"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the agent")
name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent's functionality")
capabilities: List[str] = Field(default=[], description="List of agent capabilities")
label: Label = Field(
default=Label(default="Agent Information", translations={"en": "Agent Information", "fr": "Information d'agent"}),
description="Label for the class"
)
class AgentResponse(BaseModel):
"""Response structure returned by agent processing"""
response: str = Field(description="Text response from the agent")
documents: List[ChatDocument] = Field(default=[], description="List of document objects created by the agent")
class InputDocument(BaseModel):
"""Input document specification for a task"""
label: str = Field(description="Document label in the format 'filename.ext'")
fileId: Optional[int] = Field(None, description="ID of the existing document if referring to one")
contentPart: str = Field(default="", description="Content part to focus on, empty string for all contents")
prompt: str = Field(description="AI prompt to describe what data to extract from the file")
class OutputDocument(BaseModel):
"""Output document specification for a task"""
label: str = Field(description="Document label in the format 'filename.ext'")
prompt: str = Field(description="AI prompt to describe the content of the file")
class TaskItem(BaseModel):
"""Individual task in the workplan"""
agent: str = Field(description="Name of an available agent")
prompt: str = Field(description="Specific instructions to the agent, that he knows what to do with which documents and which output to provide")
outputDocuments: List[OutputDocument] = Field(default=[], description="List of required output documents")
inputDocuments: List[InputDocument] = Field(default=[], description="List of input documents to process")
sequenceNr: int = Field(description="Sequence number of the task")
agentName: str = Field(description="Name of an available agent")
prompt: str = Field(description="Specific instructions to the agent")
userLanguage: str = Field(description="Language code of the user's request")
filesInput: List[str] = Field(default=[], description="List of input files in format 'fileName[;documentId]'")
filesOutput: List[str] = Field(default=[], description="List of output files in format 'fileName'")
label: Label = Field(
default=Label(default="Task Item", translations={"en": "Task Item", "fr": "Élément de tâche"}),
description="Label for the class"
)
class TaskPlan(BaseModel):
"""Work plan created by project manager"""
objFinalDocuments: List[str] = Field(default=[], description="List of required result documents")
objWorkplan: List[TaskItem] = Field(default=[], description="Plan for executing agents")
objUserResponse: str = Field(description="Response to the user explaining the plan")
fileList: List[str] = Field(default=[], description="List of required result documents in format 'fileName'")
taskItems: List[TaskItem] = Field(default=[], description="Plan for executing agents")
userResponse: str = Field(description="Response to the user explaining the plan")
userLanguage: str = Field(default="en", description="Language code of the user's request")
label: Label = Field(
default=Label(default="Task Plan", translations={"en": "Task Plan", "fr": "Plan de tâches"}),
description="Label for the class"
)
class WorkflowStatus(BaseModel):
"""Workflow status messages"""
init: str = Field(default="Workflow initialized")
running: str = Field(default="Running workflow")
waiting: str = Field(default="Waiting for input")
completed: str = Field(default="Workflow completed successfully")
stopped: str = Field(default="Workflow stopped by user")
failed: str = Field(default="Error in workflow")
label: Label = Field(
default=Label(default="Workflow Status", translations={"en": "Workflow Status", "fr": "État du workflow"}),
description="Label for the class"
)
# Request models for the API
class UserInputRequest(BaseModel):
"""Request for user input to a running workflow"""
prompt: str = Field(description="Message from the user")
listFileId: List[int] = Field(default=[], description="List of FileItem IDs")

View file

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

View file

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

View file

@ -6,6 +6,7 @@ from typing import Dict, Any
from datetime import timedelta
import pathlib
import os
import logging
from modules.shared.configuration import APP_CONFIG
from modules.security.auth import (
@ -27,6 +28,8 @@ os.makedirs(staticFolder, exist_ok=True)
# Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
logger = logging.getLogger(__name__)
@router.get("/favicon.ico")
async def favicon():
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
@ -56,31 +59,140 @@ async def get_environment():
@router.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
# Initialize Gateway interface without context
gateway = getGatewayInterface()
# Authenticate user
user = gateway.authenticateUser(formData.username, formData.password)
# Get root mandate and admin user IDs
adminGateway = getGatewayInterface()
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not user:
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Bearer"},
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="System is not properly initialized with root mandate and admin user"
)
# Create token with tenant ID
accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
accessToken = createAccessToken(
data={
"sub": user["username"],
"mandateId": user["mandateId"]
},
expiresDelta=accessTokenExpires
)
return {"accessToken": accessToken, "tokenType": "bearer"}
# Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
try:
# Authenticate user
user = adminGateway.authenticateUser(formData.username, formData.password)
# Create token with mandate ID and user ID
accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
accessToken = createAccessToken(
data={
"sub": user["username"],
"_mandateId": str(user["_mandateId"]), # Ensure string
"_userId": str(user["id"]) # Ensure string
},
expiresDelta=accessTokenExpires
)
logger.info(f"User {user['username']} successfully logged in with context: _mandateId={user['_mandateId']}, _userId={user['id']}")
return {"accessToken": accessToken, "tokenType": "bearer"}
except ValueError as e:
# Handle authentication errors
error_msg = str(e)
logger.warning(f"Authentication failed for user {formData.username}: {error_msg}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_msg,
headers={"WWW-Authenticate": "Bearer"},
)
except Exception as e:
# Handle other errors
error_msg = f"Login failed: {str(e)}"
logger.error(f"Unexpected error during login for user {formData.username}: {error_msg}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_msg
)
@router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
return currentUser
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
mandateAttributes = getModelAttributes(Mandate)
@dataclass
class AppContext:
"""Context object for all required connections and user information"""
mandateId: int
userId: int
interfaceData: Any # Gateway Interface
def __init__(self, mandateId: int, userId: int):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getGatewayInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections"""
mandateId, userId = await getUserContext(currentUser)
interfaceData = getGatewayInterface(mandateId, userId)
return AppContext(
mandateId=mandateId,
userId=userId,
interfaceData=interfaceData
)
return AppContext(mandateId, userId)
# Create router for mandate endpoints
router = APIRouter(
@ -89,19 +81,19 @@ async def createMandate(
return newMandate
@router.get("/{mandateId}", response_model=Dict[str, Any])
@router.get("/{_mandateId}", response_model=Dict[str, Any])
async def getMandate(
mandateId: int,
_mandateId: str = Path(..., description="ID of the mandate"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a specific mandate"""
"""Get a mandate by ID."""
context = await getContext(currentUser)
# Permission check
# Admin can only see their own mandate, SysAdmin can see all
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context.mandateId == mandateId
isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
@ -110,36 +102,36 @@ async def getMandate(
)
# Get mandate
mandate = context.interfaceData.getMandate(mandateId)
mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate with ID {mandateId} not found"
detail=f"Mandate with ID {_mandateId} not found"
)
return mandate
@router.put("/{mandateId}", response_model=Dict[str, Any])
@router.put("/{_mandateId}", response_model=Dict[str, Any])
async def updateMandate(
mandateId: int = Path(..., description="ID of the mandate to update"),
mandateData: Dict[str, Any] = Body(..., description="Updated mandate data"),
_mandateId: str = Path(..., description="ID of the mandate to update"),
mandateData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Update an existing mandate"""
"""Update a mandate."""
context = await getContext(currentUser)
# Mandate exists?
mandate = context.interfaceData.getMandate(mandateId)
# Get mandate
mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate with ID {mandateId} not found"
detail=f"Mandate with ID {_mandateId} not found"
)
# Permission check
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context.mandateId == mandateId
isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
@ -154,33 +146,29 @@ async def updateMandate(
updateData[attr] = mandateData[attr]
# Update mandate
updatedMandate = context.interfaceData.updateMandate(
mandateId=mandateId,
mandateData=updateData
)
updatedMandate = context.interfaceData.updateMandate(_mandateId, mandateData)
return updatedMandate
@router.delete("/{mandateId}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{_mandateId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteMandate(
mandateId: int = Path(..., description="ID of the mandate to delete"),
_mandateId: str = Path(..., description="ID of the mandate to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a mandate, including all associated users and referenced objects"""
"""Delete a mandate."""
context = await getContext(currentUser)
# Mandate exists?
mandate = context.interfaceData.getMandate(mandateId)
# Get mandate
mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate with ID {mandateId} not found"
detail=f"Mandate with ID {_mandateId} not found"
)
# Permission check
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context.mandateId == mandateId
isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
@ -189,11 +177,11 @@ async def deleteMandate(
)
# Delete mandate
success = context.interfaceData.deleteMandate(mandateId)
success = context.interfaceData.deleteMandate(_mandateId)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting mandate with ID {mandateId}"
detail=f"Error deleting mandate with ID {_mandateId}"
)
return None

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

View file

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

View file

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

View file

@ -29,34 +29,15 @@ router = APIRouter(
responses={404: {"description": "Not found"}}
)
@dataclass
class AppContext:
"""Context object for all required connections and user information"""
mandateId: int
userId: int
interfaceData: Any # LucyDOM Interface
interfaceChat: Any # Workflow Manager
def __init__(self, mandateId: int, userId: int):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""
Creates a central context object with all required interfaces
Args:
currentUser: Current user from authentication
Returns:
AppContext object with all required connections
"""
mandateId, userId = await getUserContext(currentUser)
interfaceData = getLucydomInterface(mandateId, userId)
interfaceChat = getWorkflowManager(mandateId, userId)
return AppContext(
mandateId=mandateId,
userId=userId,
interfaceData=interfaceData,
interfaceChat=interfaceChat
)
return AppContext(mandateId, userId)
# State 1: Workflow Initialization endpoint
@router.post("/start", response_model=Dict[str, Any])
@ -87,7 +68,7 @@ async def startWorkflow(
}
# Start or continue workflow using the workflow manager
workflow = await context.interfaceChat.workflowStart(userInputDict, workflowId)
workflow = await getWorkflowManager(context._mandateId, context._userId).workflowStart(userInputDict, workflowId)
logger.info("User Input received. Answer:",workflow)
return {
@ -130,14 +111,14 @@ async def stopWorkflow(
detail=f"Workflow with ID {workflowId} not found"
)
if workflow.get("userId") != context.userId:
if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to stop this workflow"
)
# Stop the workflow
stoppedWorkflow = await context.interfaceChat.workflowStop(workflowId)
stoppedWorkflow = getWorkflowManager(context._mandateId, context._userId).workflowStop(workflowId)
return {
"id": workflowId,
@ -183,7 +164,7 @@ async def deleteWorkflow(
)
# Check if user has permission to delete
if workflow.get("userId") != context.userId:
if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to delete this workflow"
@ -230,7 +211,7 @@ async def listWorkflows(
try:
# Retrieve workflows for the user
workflows = context.interfaceData.getWorkflowsByUser(context.userId)
workflows = context.interfaceData.getWorkflowsByUser(context._userId)
return workflows
except Exception as e:
logger.error(f"Error listing workflows: {str(e)}", exc_info=True)
@ -431,7 +412,7 @@ async def deleteWorkflowMessage(
detail=f"Workflow with ID {workflowId} not found"
)
if workflow.get("userId") != context.userId:
if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow"
@ -471,7 +452,7 @@ async def deleteWorkflowMessage(
async def deleteFileFromMessage(
workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"),
fileId: int = Path(..., description="ID of the file to delete"),
fileId: str = Path(..., description="ID of the file to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
@ -498,7 +479,7 @@ async def deleteFileFromMessage(
detail=f"Workflow with ID {workflowId} not found"
)
if workflow.get("userId") != context.userId:
if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow"
@ -533,7 +514,7 @@ async def deleteFileFromMessage(
@router.get("/files/{fileId}/preview", response_model=Dict[str, Any])
async def previewFile(
fileId: int = Path(..., description="ID of the file to preview"),
fileId: str = Path(..., description="ID of the file to preview"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
@ -558,7 +539,7 @@ async def previewFile(
)
# Check if file belongs to user or their mandate
if file.get("mandateId") != context.mandateId and file.get("userId") != context.userId:
if file.get("_mandateId") != context._mandateId and file.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this file"
@ -636,7 +617,7 @@ async def previewFile(
@router.get("/files/{fileId}/download")
async def downloadFile(
fileId: int = Path(..., description="ID of the file to download"),
fileId: str = Path(..., description="ID of the file to download"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
@ -676,4 +657,122 @@ async def downloadFile(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading file: {str(e)}"
)
)
@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:
raise credentialsException
# Extract mandate ID from token (if present)
mandateId: int = payload.get("mandateId", 1) # Default: Root mandate
# Extract mandate ID and user ID from token
_mandateId: str = payload.get("_mandateId")
_userId: str = payload.get("_userId")
if not _mandateId or not _userId:
logger.error(f"Missing context in token: _mandateId={_mandateId}, _userId={_userId}")
raise credentialsException
except JWTError:
logger.warning("Invalid JWT Token")
raise credentialsException
# Initialize Gateway Interface without context
gateway = getGatewayInterface()
# Initialize Gateway Interface with context
gateway = getGatewayInterface(_mandateId, _userId)
# Retrieve user from database
user = gateway.getUserByUsername(username)
@ -96,6 +101,11 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
logger.warning(f"User {username} is disabled")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
# Ensure the user has the correct context
if str(user.get("_mandateId")) != str(_mandateId) or str(user.get("id")) != str(_userId):
logger.error(f"User context mismatch: token(_mandateId={_mandateId}, _userId={_userId}) vs user(_mandateId={user.get('_mandateId')}, id={user.get('id')})")
raise credentialsException
return user
async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, Any]:
@ -116,43 +126,48 @@ async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentU
return currentUser
async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[int, int]:
async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[str, str]:
"""
Extracts the mandate ID and user ID from the current user.
Enhanced with better logging.
Args:
currentUser: The current user
Returns:
Tuple of (mandateId, userId)
Tuple of (_mandateId, _userId) as strings
Raises:
HTTPException: If mandate or user ID is missing
"""
# Default values
defaultMandateId = 0
defaultUserId = 0
# Extract _mandateId
_mandateId = currentUser.get("_mandateId")
if not _mandateId:
logger.error("No _mandateId found in currentUser")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing mandate context"
)
# Extract mandateId
mandateId = currentUser.get("mandateId", None)
if mandateId is None:
logger.warning(f"No mandateId found in currentUser, using default: {defaultMandateId}")
mandateId = defaultMandateId
else:
try:
mandateId = int(mandateId)
except (ValueError, TypeError):
logger.error(f"Invalid mandateId value: {mandateId}, using default: {defaultMandateId}")
mandateId = defaultMandateId
# Extract _userId
_userId = currentUser.get("id") # Note: using 'id' instead of '_userId'
if not _userId:
logger.error("No _userId found in currentUser")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing user context"
)
# Extract userId
userId = currentUser.get("id", None)
if userId is None:
logger.warning(f"No userId found in currentUser, using default: {defaultUserId}")
userId = defaultUserId
else:
try:
userId = int(userId)
except (ValueError, TypeError):
logger.error(f"Invalid userId value: {userId}, using default: {defaultUserId}")
userId = defaultUserId
return str(_mandateId), str(_userId)
def getInitialContext() -> tuple[str, str]:
"""
Returns the initial mandate and user IDs from the gateway.
This is used by other interfaces to get their context.
Returns:
tuple[str, str]: (_mandateId, _userId) or (None, None) if not available
"""
gateway = getGatewayInterface()
mandateId = gateway.getInitialId("mandates")
userId = gateway.getInitialId("users")
return mandateId, userId

View file

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

View file

@ -25,11 +25,18 @@ class AgentBase:
self.label = "Base Agent"
self.description = "Base agent functionality"
self.capabilities = []
self.workflowManager = None
self.mydom = None
self.workflowManager = None # Will be set by workflow manager
def setDependencies(self, mydom=None):
"""Set external dependencies for the agent."""
def setWorkflowManager(self, workflowManager):
"""Set the workflow manager reference."""
self.workflowManager = workflowManager
# Also set mydom reference from workflow manager
if workflowManager and hasattr(workflowManager, 'mydom'):
self.mydom = workflowManager.mydom
def setMydom(self, mydom):
"""Set the LucyDOM interface reference."""
self.mydom = mydom
def getAgentInfo(self) -> Dict[str, Any]:

View file

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

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
class WorkflowManager:
"""
Manages the processing of chat requests, agent execution, and
the integration of results into the workflow, following a state machine approach.
"""
def __init__(self, mandateId: int, userId: int):
"""
Initializes the WorkflowManager with mandate and user context.
Args:
mandateId: ID of the current mandate
userId: ID of the current user
"""
self.mandateId = mandateId
self.userId = userId
self.mydom = domInterface(mandateId, userId)
"""Manages the execution of workflows and their associated agents."""
def __init__(self, _mandateId: str, _userId: str):
"""Initialize the workflow manager with mandate and user context."""
self._mandateId = _mandateId
self._userId = _userId
self.mydom = domInterface(_mandateId, _userId)
self.agentRegistry = getAgentRegistry()
self.agentRegistry.setMydom(self.mydom)
self.agentRegistry.setWorkflowManager(self) # Set self as workflow manager for all agents
self.agentRegistry.initialize(mydom=self.mydom, workflowManager=self)
def workflowStart(self, workflowId: str, workflowData: dict) -> dict:
"""Start a new workflow with the given ID and data."""
try:
# Update the LucyDOM interface with current user context
self.mydom._mandateId = self._mandateId
self.mydom._userId = self._userId
# Initialize workflow state
workflowState = {
'workflowId': workflowId,
'status': 'running',
'startTime': datetime.now().isoformat(),
'currentStep': 0,
'steps': [],
'data': workflowData
}
# Get workflow definition
workflowDef = self._getWorkflowDefinition(workflowId)
if not workflowDef:
raise ValueError(f"Workflow definition not found for ID: {workflowId}")
# Initialize steps
workflowState['steps'] = self._initializeSteps(workflowDef)
# Start workflow execution
self._executeWorkflow(workflowState)
return workflowState
except Exception as e:
logger.error(f"Error starting workflow {workflowId}: {str(e)}")
raise
### Workflow State Machine Implementation
@ -280,8 +303,8 @@ class WorkflowManager:
newWorkflowId = str(uuid.uuid4()) if workflowId is None else workflowId
workflow = {
"id": newWorkflowId,
"mandateId": self.mandateId,
"userId": self.userId,
"_mandateId": self._mandateId,
"_userId": self._userId,
"name": f"Workflow {newWorkflowId[:8]}",
"startedAt": currentTime,
"messages": [], # Empty list - will be filled with references
@ -301,8 +324,8 @@ class WorkflowManager:
# Save to database - only the workflow metadata
workflowDb = {
"id": workflow["id"],
"mandateId": workflow["mandateId"],
"userId": workflow["userId"],
"_mandateId": workflow["_mandateId"],
"_userId": workflow["_userId"],
"name": workflow["name"],
"startedAt": workflow["startedAt"],
"status": workflow["status"],
@ -593,7 +616,12 @@ JSON_OUTPUT = {{
try:
# Process the task using the agent's standardized interface
logger.debug("TASK: "+self.parseJson2text(agentTask))
logger.debug(f"Agent '{agentName}' AI service available: {agent.mydom is not None}")
# Ensure AI service is available
if not self.mydom.aiService:
logger.error("AI service not available in LucyDOM interface")
self.logAdd(workflow, "Error: AI service not available", level="error")
return []
# Calculate bytes sent before processing
bytesSent = len(json.dumps(agentTask).encode('utf-8'))
@ -885,8 +913,8 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
continue
# Check if file belongs to the current mandate
if file.get("mandateId") != self.mandateId:
logger.warning(f"File {fileId} does not belong to mandate {self.mandateId}")
if file.get("_mandateId") != self._mandateId:
logger.warning(f"File {fileId} does not belong to mandate {self._mandateId}")
continue
# Load file content
@ -1511,49 +1539,48 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"userLanguage": "en"
}
def _createWorkflowData(self, workflow: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a workflow data structure."""
return {
"_mandateId": self._mandateId,
"_userId": self._userId,
"name": workflow.get("name", "New Workflow"),
"status": workflow.get("status", "running"),
"startedAt": workflow.get("startedAt", self._getCurrentTimestamp()),
"lastActivity": workflow.get("lastActivity", self._getCurrentTimestamp()),
"dataStats": workflow.get("dataStats", {})
}
def _checkFileAccess(self, fileId: int) -> bool:
"""Checks if the current user has access to a file."""
file = self.mydom.getFile(fileId)
if not file:
return False
if file.get("_mandateId") != self._mandateId:
logger.warning(f"File {fileId} does not belong to mandate {self._mandateId}")
return False
return True
# Singleton factory for the WorkflowManager
_workflowManagers = {}
_workflowManagerLastAccess = {} # Track last access time for cleanup
def getWorkflowManager(mandateId: int = 0, userId: int = 0) -> WorkflowManager:
"""
Returns a WorkflowManager for the specified context.
Reuses existing instances but implements cleanup for inactive instances.
Args:
mandateId: ID of the mandate
userId: ID of the user
Returns:
WorkflowManager instance
"""
contextKey = f"{mandateId}_{userId}"
current_time = datetime.now()
# Update last access time
_workflowManagerLastAccess[contextKey] = current_time
# Cleanup old instances (older than 1 hour)
cleanup_threshold = current_time - timedelta(hours=1)
for key in list(_workflowManagers.keys()):
if _workflowManagerLastAccess.get(key, current_time) < cleanup_threshold:
del _workflowManagers[key]
del _workflowManagerLastAccess[key]
if contextKey not in _workflowManagers:
_workflowManagers[contextKey] = WorkflowManager(mandateId, userId)
return _workflowManagers[contextKey]
def getWorkflowManager(_mandateId: str = '', _userId: str = '') -> WorkflowManager:
"""Get a workflow manager instance with the specified context."""
return WorkflowManager(_mandateId=_mandateId, _userId=_userId)
def cleanupWorkflowManager(mandateId: int, userId: int) -> None:
def cleanupWorkflowManager(_mandateId: int, _userId: int) -> None:
"""
Explicitly cleanup a WorkflowManager instance.
Args:
mandateId: ID of the mandate
userId: ID of the user
_mandateId: ID of the mandate
_userId: ID of the user
"""
contextKey = f"{mandateId}_{userId}"
contextKey = f"{_mandateId}_{_userId}"
if contextKey in _workflowManagers:
del _workflowManagers[contextKey]
if contextKey in _workflowManagerLastAccess:

View file

@ -1,5 +1,12 @@
....................... TASKS
for all created records
- to add _createdAt (datetime) and _modifiedAt (datetime), initially _createdAt=_modifiedAt
for all updated records
- to update attribute _modifiedAt
! function callAI() to ask with userPrompt,systemPrompt optional), not with json
! in the taskplan to refer files always in context of user/mandate
! userinput to handle with object AgentQuery --> when received in frontend to enhance for full object