db items serialized, uuid overall, auth enhanced
This commit is contained in:
parent
16338a6f94
commit
40f82a3848
26 changed files with 1563 additions and 897 deletions
9
app.py
9
app.py
|
|
@ -57,17 +57,14 @@ initLogging()
|
|||
logger = logging.getLogger(__name__)
|
||||
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
|
||||
|
||||
# Import models - import generically for INITIALIZATION
|
||||
from modules.interfaces.gatewayInterface import getGatewayInterface
|
||||
gateway = getGatewayInterface()
|
||||
|
||||
|
||||
# Define lifespan context manager for application startup/shutdown events
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup logic (if any)
|
||||
# Startup logic
|
||||
logger.info("Application is starting up")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown logic
|
||||
logger.info("Application has been shut down")
|
||||
|
||||
|
|
|
|||
10
env_dev.env
10
env_dev.env
|
|
@ -5,11 +5,11 @@ APP_ENV_TYPE = dev
|
|||
APP_ENV_LABEL = Development Instance Patrick
|
||||
APP_API_URL = http://localhost:8000
|
||||
|
||||
# Database Configuration System
|
||||
DB_SYSTEM_HOST=D:/Temp/_powerondb
|
||||
DB_SYSTEM_DATABASE=system
|
||||
DB_SYSTEM_USER=dev_user
|
||||
DB_SYSTEM_PASSWORD_SECRET=dev_password
|
||||
# Database Configuration Gateway
|
||||
DB_GATEWAY_HOST=D:/Temp/_powerondb
|
||||
DB_GATEWAY_DATABASE=gateway
|
||||
DB_GATEWAY_USER=dev_user
|
||||
DB_GATEWAY_PASSWORD_SECRET=dev_password
|
||||
|
||||
# Database Configuration LucyDOM
|
||||
DB_LUCYDOM_HOST=D:/Temp/_powerondb
|
||||
|
|
|
|||
10
env_prod.env
10
env_prod.env
|
|
@ -5,11 +5,11 @@ APP_ENV_TYPE = prod
|
|||
APP_ENV_LABEL = Production Instance
|
||||
APP_API_URL = https://gateway.poweron-center.net
|
||||
|
||||
# Database Configuration System
|
||||
DB_SYSTEM_HOST=/home/_powerondb
|
||||
DB_SYSTEM_DATABASE=system
|
||||
DB_SYSTEM_USER=dev_user
|
||||
DB_SYSTEM_PASSWORD_SECRET=prod_password
|
||||
# Database Configuration Gateway
|
||||
DB_GATEWAY_HOST=/home/_powerondb
|
||||
DB_GATEWAY_DATABASE=gateway
|
||||
DB_GATEWAY_USER=dev_user
|
||||
DB_GATEWAY_PASSWORD_SECRET=prod_password
|
||||
|
||||
# Database Configuration LucyDOM
|
||||
DB_LUCYDOM_HOST=/home/_powerondb
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('', '')
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('', '')
|
||||
265
modules/interfaces/lucydomModel BACKUP.py
Normal file
265
modules/interfaces/lucydomModel BACKUP.py
Normal 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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue