feat: add langgraph first tool; pydantic v2

This commit is contained in:
Christopher Gondek 2025-10-03 09:48:32 +02:00
parent 68d6ab9890
commit 98b258ae53
7 changed files with 718 additions and 432 deletions

View file

@ -1,7 +1,7 @@
"""Security models: Token and AuthEvent.""" """Security models: Token and AuthEvent."""
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, ConfigDict
from modules.shared.attributeUtils import register_model_labels, ModelMixin from modules.shared.attributeUtils import register_model_labels, ModelMixin
from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.timezoneUtils import get_utc_timestamp
from .datamodelUam import AuthAuthority from .datamodelUam import AuthAuthority
@ -18,21 +18,36 @@ class Token(BaseModel, ModelMixin):
id: Optional[str] = None id: Optional[str] = None
userId: str userId: str
authority: AuthAuthority authority: AuthAuthority
connectionId: Optional[str] = Field(None, description="ID of the connection this token belongs to") connectionId: Optional[str] = Field(
None, description="ID of the connection this token belongs to"
)
tokenAccess: str tokenAccess: str
tokenType: str = "bearer" tokenType: str = "bearer"
expiresAt: float = Field(description="When the token expires (UTC timestamp in seconds)") expiresAt: float = Field(
description="When the token expires (UTC timestamp in seconds)"
)
tokenRefresh: Optional[str] = None tokenRefresh: Optional[str] = None
createdAt: Optional[float] = Field(None, description="When the token was created (UTC timestamp in seconds)") createdAt: Optional[float] = Field(
status: TokenStatus = Field(default=TokenStatus.ACTIVE, description="Token status: active/revoked") None, description="When the token was created (UTC timestamp in seconds)"
revokedAt: Optional[float] = Field(None, description="When the token was revoked (UTC timestamp in seconds)") )
revokedBy: Optional[str] = Field(None, description="User ID who revoked the token (admin/self)") status: TokenStatus = Field(
default=TokenStatus.ACTIVE, description="Token status: active/revoked"
)
revokedAt: Optional[float] = Field(
None, description="When the token was revoked (UTC timestamp in seconds)"
)
revokedBy: Optional[str] = Field(
None, description="User ID who revoked the token (admin/self)"
)
reason: Optional[str] = Field(None, description="Optional revocation reason") reason: Optional[str] = Field(None, description="Optional revocation reason")
sessionId: Optional[str] = Field(None, description="Logical session grouping for logout revocation") sessionId: Optional[str] = Field(
mandateId: Optional[str] = Field(None, description="Mandate ID for tenant scoping of the token") None, description="Logical session grouping for logout revocation"
)
mandateId: Optional[str] = Field(
None, description="Mandate ID for tenant scoping of the token"
)
class Config: model_config = ConfigDict(use_enum_values=True)
use_enum_values = True
register_model_labels( register_model_labels(
@ -59,14 +74,60 @@ register_model_labels(
class AuthEvent(BaseModel, ModelMixin): class AuthEvent(BaseModel, ModelMixin):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the auth event", frontend_type="text", frontend_readonly=True, frontend_required=False) id: str = Field(
userId: str = Field(description="ID of the user this event belongs to", frontend_type="text", frontend_readonly=True, frontend_required=True) default_factory=lambda: str(uuid.uuid4()),
eventType: str = Field(description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')", frontend_type="text", frontend_readonly=True, frontend_required=True) description="Unique ID of the auth event",
timestamp: float = Field(default_factory=get_utc_timestamp, description="Unix timestamp when the event occurred", frontend_type="datetime", frontend_readonly=True, frontend_required=True) frontend_type="text",
ipAddress: Optional[str] = Field(default=None, description="IP address from which the event originated", frontend_type="text", frontend_readonly=True, frontend_required=False) frontend_readonly=True,
userAgent: Optional[str] = Field(default=None, description="User agent string from the request", frontend_type="text", frontend_readonly=True, frontend_required=False) frontend_required=False,
success: bool = Field(default=True, description="Whether the authentication event was successful", frontend_type="boolean", frontend_readonly=True, frontend_required=True) )
details: Optional[str] = Field(default=None, description="Additional details about the event", frontend_type="text", frontend_readonly=True, frontend_required=False) userId: str = Field(
description="ID of the user this event belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=True,
)
eventType: str = Field(
description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')",
frontend_type="text",
frontend_readonly=True,
frontend_required=True,
)
timestamp: float = Field(
default_factory=get_utc_timestamp,
description="Unix timestamp when the event occurred",
frontend_type="datetime",
frontend_readonly=True,
frontend_required=True,
)
ipAddress: Optional[str] = Field(
default=None,
description="IP address from which the event originated",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
userAgent: Optional[str] = Field(
default=None,
description="User agent string from the request",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
success: bool = Field(
default=True,
description="Whether the authentication event was successful",
frontend_type="boolean",
frontend_readonly=True,
frontend_required=True,
)
details: Optional[str] = Field(
default=None,
description="Additional details about the event",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
)
register_model_labels( register_model_labels(
@ -83,5 +144,3 @@ register_model_labels(
"details": {"en": "Details", "fr": "Détails"}, "details": {"en": "Details", "fr": "Détails"},
}, },
) )

View file

@ -0,0 +1 @@
"""Contains all tools available for the chatbot to use."""

View file

@ -1 +1,7 @@
"""Tools that are custom to a specific customer go here.""" """Shared tools available across all chatbot implementations."""
from modules.features.chatBot.chatbotTools.sharedTools.toolTavilySearch import (
tavily_search,
)
__all__ = ["tavily_search"]

View file

@ -0,0 +1,55 @@
"""Tavily Search Tool for LangGraph.
This tool provides web search capabilities using the Tavily API.
"""
import logging
from typing import Annotated
from langchain_core.tools import tool
from modules.connectors.connectorAiTavily import ConnectorWeb
logger = logging.getLogger(__name__)
@tool
async def tavily_search(
query: Annotated[str, "The search query to look up on the web"],
) -> str:
"""Search the web using Tavily API.
Use this tool to search for current information, news, or any web content.
The tool returns relevant search results including titles and URLs.
Args:
query: The search query string
Returns:
A formatted string containing search results with titles and URLs
"""
try:
# Create connector instance
connector = await ConnectorWeb.create()
# Perform search with default parameters
results = await connector._search(
query=query,
max_results=5,
search_depth="basic",
include_answer=True,
include_raw_content=False,
)
# Format results
if not results:
return f"No results found for query: {query}"
formatted_results = [f"Search results for '{query}':\n"]
for i, result in enumerate(results, 1):
formatted_results.append(f"{i}. {result.title}")
formatted_results.append(f" URL: {result.url}\n")
return "\n".join(formatted_results)
except Exception as e:
logger.error(f"Error in tavily_search tool: {str(e)}")
return f"Error performing search: {str(e)}"

View file

@ -18,11 +18,19 @@ from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp
from modules.interfaces.interfaceDbAppAccess import AppAccess from modules.interfaces.interfaceDbAppAccess import AppAccess
from modules.datamodels.datamodelUam import ( from modules.datamodels.datamodelUam import (
User, Mandate, UserInDB, UserConnection, User,
AuthAuthority, UserPrivilege, ConnectionStatus, Mandate,
UserInDB,
UserConnection,
AuthAuthority,
UserPrivilege,
ConnectionStatus,
) )
from modules.datamodels.datamodelSecurity import Token, AuthEvent, TokenStatus from modules.datamodels.datamodelSecurity import Token, AuthEvent, TokenStatus
from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes from modules.datamodels.datamodelNeutralizer import (
DataNeutraliserConfig,
DataNeutralizerAttributes,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -35,6 +43,7 @@ _rootAppObjects = None
# Password-Hashing # Password-Hashing
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto") pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
class AppObjects: class AppObjects:
""" """
Interface to the Gateway system. Interface to the Gateway system.
@ -76,14 +85,16 @@ class AppObjects:
self.userLanguage = currentUser.language # Default user language self.userLanguage = currentUser.language # Default user language
# Initialize access control with user context # Initialize access control with user context
self.access = AppAccess(self.currentUser, self.db) # Convert to dict only when needed self.access = AppAccess(
self.currentUser, self.db
) # Convert to dict only when needed
# Update database context # Update database context
self.db.updateContext(self.userId) self.db.updateContext(self.userId)
def __del__(self): def __del__(self):
"""Cleanup method to close database connection.""" """Cleanup method to close database connection."""
if hasattr(self, 'db') and self.db is not None: if hasattr(self, "db") and self.db is not None:
try: try:
self.db.close() self.db.close()
except Exception as e: except Exception as e:
@ -106,7 +117,7 @@ class AppObjects:
dbUser=dbUser, dbUser=dbUser,
dbPassword=dbPassword, dbPassword=dbPassword,
dbPort=dbPort, dbPort=dbPort,
userId=self.userId userId=self.userId,
) )
# Initialize database system # Initialize database system
@ -129,16 +140,12 @@ class AppObjects:
mandates = self.db.getRecordset(Mandate) mandates = self.db.getRecordset(Mandate)
if existingMandateId is None or not mandates: if existingMandateId is None or not mandates:
logger.info("Creating Root mandate") logger.info("Creating Root mandate")
rootMandate = Mandate( rootMandate = Mandate(name="Root", language="en", enabled=True)
name="Root",
language="en",
enabled=True
)
createdMandate = self.db.recordCreate(Mandate, rootMandate) createdMandate = self.db.recordCreate(Mandate, rootMandate)
logger.info(f"Root mandate created with ID {createdMandate['id']}") logger.info(f"Root mandate created with ID {createdMandate['id']}")
# Update mandate context # Update mandate context
self.mandateId = createdMandate['id'] self.mandateId = createdMandate["id"]
def _initAdminUser(self): def _initAdminUser(self):
"""Creates the Admin user if it doesn't exist.""" """Creates the Admin user if it doesn't exist."""
@ -155,8 +162,10 @@ class AppObjects:
language="en", language="en",
privilege=UserPrivilege.SYSADMIN, privilege=UserPrivilege.SYSADMIN,
authenticationAuthority="local", # Using lowercase value directly authenticationAuthority="local", # Using lowercase value directly
hashedPassword=self._getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")), hashedPassword=self._getPasswordHash(
connections=[] APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")
),
connections=[],
) )
createdUser = self.db.recordCreate(UserInDB, adminUser) createdUser = self.db.recordCreate(UserInDB, adminUser)
logger.info(f"Admin user created with ID {createdUser['id']}") logger.info(f"Admin user created with ID {createdUser['id']}")
@ -168,7 +177,9 @@ class AppObjects:
def _initEventUser(self): def _initEventUser(self):
"""Creates the Event user if it doesn't exist.""" """Creates the Event user if it doesn't exist."""
# Check if event user already exists # Check if event user already exists
existingUsers = self.db.getRecordset(UserInDB, recordFilter={"username": "event"}) existingUsers = self.db.getRecordset(
UserInDB, recordFilter={"username": "event"}
)
if not existingUsers: if not existingUsers:
logger.info("Creating Event user") logger.info("Creating Event user")
eventUser = UserInDB( eventUser = UserInDB(
@ -180,13 +191,17 @@ class AppObjects:
language="en", language="en",
privilege=UserPrivilege.SYSADMIN, privilege=UserPrivilege.SYSADMIN,
authenticationAuthority="local", # Using lowercase value directly authenticationAuthority="local", # Using lowercase value directly
hashedPassword=self._getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")), hashedPassword=self._getPasswordHash(
connections=[] APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")
),
connections=[],
) )
createdUser = self.db.recordCreate(UserInDB, eventUser) createdUser = self.db.recordCreate(UserInDB, eventUser)
logger.info(f"Event user created with ID {createdUser['id']}") logger.info(f"Event user created with ID {createdUser['id']}")
def _uam(self, model_class: type, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _uam(
self, model_class: type, recordset: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
""" """
Unified user access management function that filters data based on user privileges Unified user access management function that filters data based on user privileges
and adds access control attributes. and adds access control attributes.
@ -205,7 +220,7 @@ class AppObjects:
cleanedRecords = [] cleanedRecords = []
for record in filteredRecords: for record in filteredRecords:
# Create a new dict with only non-database fields # Create a new dict with only non-database fields
cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')} cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
cleanedRecords.append(cleanedRecord) cleanedRecords.append(cleanedRecord)
return cleanedRecords return cleanedRecords
@ -317,12 +332,20 @@ class AppObjects:
return user return user
def createUser(self, username: str, password: str = None, email: str = None, def createUser(
fullName: str = None, language: str = "en", enabled: bool = True, self,
username: str,
password: str = None,
email: str = None,
fullName: str = None,
language: str = "en",
enabled: bool = True,
privilege: UserPrivilege = UserPrivilege.USER, privilege: UserPrivilege = UserPrivilege.USER,
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL, authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
externalId: str = None, externalUsername: str = None, externalId: str = None,
externalEmail: str = None) -> User: externalUsername: str = None,
externalEmail: str = None,
) -> User:
"""Create a new user with optional external connection""" """Create a new user with optional external connection"""
try: try:
# Ensure username is a string # Ensure username is a string
@ -348,7 +371,7 @@ class AppObjects:
privilege=privilege, privilege=privilege,
authenticationAuthority=authenticationAuthority, authenticationAuthority=authenticationAuthority,
hashedPassword=self._getPasswordHash(password) if password else None, hashedPassword=self._getPasswordHash(password) if password else None,
connections=[] connections=[],
) )
# Create user record # Create user record
@ -356,7 +379,6 @@ class AppObjects:
if not createdRecord or not createdRecord.get("id"): if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create user record") raise ValueError("Failed to create user record")
# Add external connection if provided # Add external connection if provided
if externalId and externalUsername: if externalId and externalUsername:
self.addUserConnection( self.addUserConnection(
@ -364,11 +386,13 @@ class AppObjects:
authenticationAuthority, authenticationAuthority,
externalId, externalId,
externalUsername, externalUsername,
externalEmail externalEmail,
) )
# Get created user using the returned ID # Get created user using the returned ID
createdUser = self.db.getRecordset(UserInDB, recordFilter={"id": createdRecord["id"]}) createdUser = self.db.getRecordset(
UserInDB, recordFilter={"id": createdRecord["id"]}
)
if not createdUser or len(createdUser) == 0: if not createdUser or len(createdUser) == 0:
raise ValueError("Failed to retrieve created user") raise ValueError("Failed to retrieve created user")
@ -399,7 +423,6 @@ class AppObjects:
# Update user record # Update user record
self.db.recordModify(UserInDB, userId, updatedUser) self.db.recordModify(UserInDB, userId, updatedUser)
# Get updated user # Get updated user
updatedUser = self.getUser(userId) updatedUser = self.getUser(userId)
if not updatedUser: if not updatedUser:
@ -422,8 +445,6 @@ class AppObjects:
def _deleteUserReferencedData(self, userId: str) -> None: def _deleteUserReferencedData(self, userId: str) -> None:
"""Deletes all data associated with a user.""" """Deletes all data associated with a user."""
try: try:
# Delete user auth events # Delete user auth events
events = self.db.getRecordset(AuthEvent, recordFilter={"userId": userId}) events = self.db.getRecordset(AuthEvent, recordFilter={"userId": userId})
for event in events: for event in events:
@ -434,9 +455,10 @@ class AppObjects:
for token in tokens: for token in tokens:
self.db.recordDelete(Token, token["id"]) self.db.recordDelete(Token, token["id"])
# Delete user connections # Delete user connections
connections = self.db.getRecordset(UserConnection, recordFilter={"userId": userId}) connections = self.db.getRecordset(
UserConnection, recordFilter={"userId": userId}
)
for conn in connections: for conn in connections:
self.db.recordDelete(UserConnection, conn["id"]) self.db.recordDelete(UserConnection, conn["id"])
@ -465,7 +487,6 @@ class AppObjects:
if not success: if not success:
raise ValueError(f"Failed to delete user {userId}") raise ValueError(f"Failed to delete user {userId}")
logger.info(f"User {userId} successfully deleted") logger.info(f"User {userId} successfully deleted")
return True return True
@ -493,31 +514,22 @@ class AppObjects:
authenticationAuthority = checkData.get("authenticationAuthority", "local") authenticationAuthority = checkData.get("authenticationAuthority", "local")
if not username: if not username:
return { return {"available": False, "message": "Username is required"}
"available": False,
"message": "Username is required"
}
# Get user by username # Get user by username
user = self.getUserByUsername(username) user = self.getUserByUsername(username)
# Check if user exists (User model instance) # Check if user exists (User model instance)
if user is not None: if user is not None:
return { return {"available": False, "message": "Username is already taken"}
"available": False,
"message": "Username is already taken"
}
return { return {"available": True, "message": "Username is available"}
"available": True,
"message": "Username is available"
}
except Exception as e: except Exception as e:
logger.error(f"Error checking username availability: {str(e)}") logger.error(f"Error checking username availability: {str(e)}")
return { return {
"available": False, "available": False,
"message": f"Error checking username availability: {str(e)}" "message": f"Error checking username availability: {str(e)}",
} }
# Connection methods # Connection methods
@ -526,7 +538,9 @@ class AppObjects:
"""Returns all connections for a user.""" """Returns all connections for a user."""
try: try:
# Get connections for this user # Get connections for this user
connections = self.db.getRecordset(UserConnection, recordFilter={"userId": userId}) connections = self.db.getRecordset(
UserConnection, recordFilter={"userId": userId}
)
# Convert to UserConnection objects # Convert to UserConnection objects
result = [] result = []
@ -543,11 +557,13 @@ class AppObjects:
status=conn_dict.get("status", "pending"), status=conn_dict.get("status", "pending"),
connectedAt=conn_dict.get("connectedAt"), connectedAt=conn_dict.get("connectedAt"),
lastChecked=conn_dict.get("lastChecked"), lastChecked=conn_dict.get("lastChecked"),
expiresAt=conn_dict.get("expiresAt") expiresAt=conn_dict.get("expiresAt"),
) )
result.append(connection) result.append(connection)
except Exception as e: except Exception as e:
logger.error(f"Error converting connection dict to object: {str(e)}") logger.error(
f"Error converting connection dict to object: {str(e)}"
)
continue continue
return result return result
@ -555,9 +571,15 @@ class AppObjects:
logger.error(f"Error getting user connections: {str(e)}") logger.error(f"Error getting user connections: {str(e)}")
return [] return []
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str, def addUserConnection(
externalUsername: str, externalEmail: Optional[str] = None, self,
status: ConnectionStatus = ConnectionStatus.PENDING) -> UserConnection: userId: str,
authority: AuthAuthority,
externalId: str,
externalUsername: str,
externalEmail: Optional[str] = None,
status: ConnectionStatus = ConnectionStatus.PENDING,
) -> UserConnection:
""" """
Adds a new connection for a user. Adds a new connection for a user.
@ -589,13 +611,12 @@ class AppObjects:
status=status, status=status,
connectedAt=get_utc_timestamp(), connectedAt=get_utc_timestamp(),
lastChecked=get_utc_timestamp(), lastChecked=get_utc_timestamp(),
expiresAt=None # Optional field, set to None by default expiresAt=None, # Optional field, set to None by default
) )
# Save to connections table # Save to connections table
self.db.recordCreate(UserConnection, connection) self.db.recordCreate(UserConnection, connection)
return connection return connection
except Exception as e: except Exception as e:
@ -606,9 +627,9 @@ class AppObjects:
"""Remove a connection to an external service""" """Remove a connection to an external service"""
try: try:
# Get connection # Get connection
connections = self.db.getRecordset(UserConnection, recordFilter={ connections = self.db.getRecordset(
"id": connectionId UserConnection, recordFilter={"id": connectionId}
}) )
if not connections: if not connections:
raise ValueError(f"Connection {connectionId} not found") raise ValueError(f"Connection {connectionId} not found")
@ -616,7 +637,6 @@ class AppObjects:
# Delete connection # Delete connection
self.db.recordDelete(UserConnection, connectionId) self.db.recordDelete(UserConnection, connectionId)
except Exception as e: except Exception as e:
logger.error(f"Error removing user connection: {str(e)}") logger.error(f"Error removing user connection: {str(e)}")
raise ValueError(f"Failed to remove user connection: {str(e)}") raise ValueError(f"Failed to remove user connection: {str(e)}")
@ -647,17 +667,13 @@ class AppObjects:
raise PermissionError("No permission to create mandates") raise PermissionError("No permission to create mandates")
# Create mandate data using model # Create mandate data using model
mandateData = Mandate( mandateData = Mandate(name=name, language=language)
name=name,
language=language
)
# Create mandate record # Create mandate record
createdRecord = self.db.recordCreate(Mandate, mandateData) createdRecord = self.db.recordCreate(Mandate, mandateData)
if not createdRecord or not createdRecord.get("id"): if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create mandate record") raise ValueError("Failed to create mandate record")
return Mandate.from_dict(createdRecord) return Mandate.from_dict(createdRecord)
def updateMandate(self, mandateId: str, updateData: Dict[str, Any]) -> Mandate: def updateMandate(self, mandateId: str, updateData: Dict[str, Any]) -> Mandate:
@ -707,7 +723,9 @@ class AppObjects:
# Check if mandate has users # Check if mandate has users
users = self.getUsersByMandate(mandateId) users = self.getUsersByMandate(mandateId)
if users: if users:
raise ValueError(f"Cannot delete mandate {mandateId} with existing users") raise ValueError(
f"Cannot delete mandate {mandateId} with existing users"
)
# Delete mandate # Delete mandate
success = self.db.recordDelete(Mandate, mandateId) success = self.db.recordDelete(Mandate, mandateId)
@ -727,7 +745,9 @@ class AppObjects:
try: try:
# Validate that this is NOT a connection token # Validate that this is NOT a connection token
if token.connectionId: if token.connectionId:
raise ValueError("Access tokens cannot have connectionId - use saveConnectionToken instead") raise ValueError(
"Access tokens cannot have connectionId - use saveConnectionToken instead"
)
# Validate user context # Validate user context
if not self.currentUser or not self.currentUser.id: if not self.currentUser or not self.currentUser.id:
@ -745,33 +765,44 @@ class AppObjects:
# If replace_existing is True, delete old access tokens for this user and authority first # If replace_existing is True, delete old access tokens for this user and authority first
if replace_existing: if replace_existing:
try: try:
old_tokens = self.db.getRecordset(Token, recordFilter={ old_tokens = self.db.getRecordset(
Token,
recordFilter={
"userId": self.currentUser.id, "userId": self.currentUser.id,
"authority": token.authority, "authority": token.authority,
"connectionId": None # Ensure we only delete access tokens "connectionId": None, # Ensure we only delete access tokens
}) },
)
deleted_count = 0 deleted_count = 0
for old_token in old_tokens: for old_token in old_tokens:
if old_token["id"] != token.id: # Don't delete the new token if it already exists if (
old_token["id"] != token.id
): # Don't delete the new token if it already exists
self.db.recordDelete(Token, old_token["id"]) self.db.recordDelete(Token, old_token["id"])
deleted_count += 1 deleted_count += 1
if deleted_count > 0: if deleted_count > 0:
logger.info(f"Replaced {deleted_count} old access tokens for user {self.currentUser.id} and authority {token.authority}") logger.info(
f"Replaced {deleted_count} old access tokens for user {self.currentUser.id} and authority {token.authority}"
)
except Exception as e: except Exception as e:
logger.warning(f"Failed to delete old access tokens for user {self.currentUser.id} and authority {token.authority}: {str(e)}") logger.warning(
f"Failed to delete old access tokens for user {self.currentUser.id} and authority {token.authority}: {str(e)}"
)
# Continue with saving the new token even if deletion fails # Continue with saving the new token even if deletion fails
# Convert to dict and ensure all fields are properly set # Convert to dict and ensure all fields are properly set
token_dict = token.dict() token_dict = token.model_dump()
# Ensure userId is set to current user
# Convert to dict and ensure all fields are properly set
token_dict = token.model_dump()
# Ensure userId is set to current user # Ensure userId is set to current user
token_dict["userId"] = self.currentUser.id token_dict["userId"] = self.currentUser.id
# Save to database # Save to database
self.db.recordCreate(Token, token_dict) self.db.recordCreate(Token, token_dict)
except Exception as e: except Exception as e:
logger.error(f"Error saving access token: {str(e)}") logger.error(f"Error saving access token: {str(e)}")
raise raise
@ -781,7 +812,9 @@ class AppObjects:
try: try:
# Validate that this IS a connection token # Validate that this IS a connection token
if not token.connectionId: if not token.connectionId:
raise ValueError("Connection tokens must have connectionId - use saveAccessToken instead") raise ValueError(
"Connection tokens must have connectionId - use saveAccessToken instead"
)
# Validate user context # Validate user context
if not self.currentUser or not self.currentUser.id: if not self.currentUser or not self.currentUser.id:
@ -799,31 +832,36 @@ class AppObjects:
# If replace_existing is True, delete old tokens for this connectionId first # If replace_existing is True, delete old tokens for this connectionId first
if replace_existing: if replace_existing:
try: try:
old_tokens = self.db.getRecordset(Token, recordFilter={ old_tokens = self.db.getRecordset(
"connectionId": token.connectionId Token, recordFilter={"connectionId": token.connectionId}
}) )
deleted_count = 0 deleted_count = 0
for old_token in old_tokens: for old_token in old_tokens:
if old_token["id"] != token.id: # Don't delete the new token if it already exists if (
old_token["id"] != token.id
): # Don't delete the new token if it already exists
self.db.recordDelete(Token, old_token["id"]) self.db.recordDelete(Token, old_token["id"])
deleted_count += 1 deleted_count += 1
if deleted_count > 0: if deleted_count > 0:
logger.info(f"Replaced {deleted_count} old tokens for connectionId {token.connectionId}") logger.info(
f"Replaced {deleted_count} old tokens for connectionId {token.connectionId}"
)
except Exception as e: except Exception as e:
logger.warning(f"Failed to delete old tokens for connectionId {token.connectionId}: {str(e)}") logger.warning(
f"Failed to delete old tokens for connectionId {token.connectionId}: {str(e)}"
)
# Continue with saving the new token even if deletion fails # Continue with saving the new token even if deletion fails
# Convert to dict and ensure all fields are properly set # Convert to dict and ensure all fields are properly set
token_dict = token.dict() token_dict = token.model_dump()
# Ensure userId is set to current user # Ensure userId is set to current user
token_dict["userId"] = self.currentUser.id token_dict["userId"] = self.currentUser.id
# Save to database # Save to database
self.db.recordCreate(Token, token_dict) self.db.recordCreate(Token, token_dict)
except Exception as e: except Exception as e:
logger.error(f"Error saving connection token: {str(e)}") logger.error(f"Error saving connection token: {str(e)}")
raise raise
@ -837,13 +875,14 @@ class AppObjects:
# Get token for this specific connection # Get token for this specific connection
# Query for specific connection # Query for specific connection
tokens = self.db.getRecordset(Token, recordFilter={ tokens = self.db.getRecordset(
"connectionId": connectionId Token, recordFilter={"connectionId": connectionId}
}) )
if not tokens: if not tokens:
logger.warning(f"No connection token found for connectionId: {connectionId}") logger.warning(
f"No connection token found for connectionId: {connectionId}"
)
return None return None
# Sort by expiration date and get the latest (most recent expiration) # Sort by expiration date and get the latest (most recent expiration)
@ -855,16 +894,27 @@ class AppObjects:
return latest_token return latest_token
except Exception as e: except Exception as e:
logger.error(f"Error getting connection token for connectionId {connectionId}: {str(e)}") logger.error(
f"Error getting connection token for connectionId {connectionId}: {str(e)}"
)
return None return None
def findActiveTokenById(self, tokenId: str, userId: str, authority: AuthAuthority, sessionId: str = None, mandateId: str = None) -> Optional[Token]: def findActiveTokenById(
self,
tokenId: str,
userId: str,
authority: AuthAuthority,
sessionId: str = None,
mandateId: str = None,
) -> Optional[Token]:
"""Find an active access token by its id (jti) with optional session/tenant scoping.""" """Find an active access token by its id (jti) with optional session/tenant scoping."""
try: try:
recordFilter = { recordFilter = {
"id": tokenId, "id": tokenId,
"userId": userId, "userId": userId,
"authority": authority.value if hasattr(authority, 'value') else str(authority), "authority": authority.value
if hasattr(authority, "value")
else str(authority),
"status": TokenStatus.ACTIVE, "status": TokenStatus.ACTIVE,
} }
if sessionId is not None: if sessionId is not None:
@ -892,7 +942,7 @@ class AppObjects:
"status": TokenStatus.REVOKED, "status": TokenStatus.REVOKED,
"revokedAt": get_utc_timestamp(), "revokedAt": get_utc_timestamp(),
"revokedBy": revokedBy, "revokedBy": revokedBy,
"reason": reason or "revoked" "reason": reason or "revoked",
} }
self.db.recordModify(Token, tokenId, tokenUpdate) self.db.recordModify(Token, tokenId, tokenUpdate)
return True return True
@ -900,30 +950,53 @@ class AppObjects:
logger.error(f"Error revoking token {tokenId}: {str(e)}") logger.error(f"Error revoking token {tokenId}: {str(e)}")
return False return False
def revokeTokensBySessionId(self, sessionId: str, userId: str, authority: AuthAuthority, revokedBy: str, reason: str = None) -> int: def revokeTokensBySessionId(
self,
sessionId: str,
userId: str,
authority: AuthAuthority,
revokedBy: str,
reason: str = None,
) -> int:
"""Revoke all tokens of a session for a user/authority.""" """Revoke all tokens of a session for a user/authority."""
try: try:
tokens = self.db.getRecordset(Token, recordFilter={ tokens = self.db.getRecordset(
Token,
recordFilter={
"userId": userId, "userId": userId,
"authority": authority.value if hasattr(authority, 'value') else str(authority), "authority": authority.value
if hasattr(authority, "value")
else str(authority),
"sessionId": sessionId, "sessionId": sessionId,
"status": TokenStatus.ACTIVE "status": TokenStatus.ACTIVE,
}) },
)
count = 0 count = 0
for t in tokens: for t in tokens:
self.db.recordModify(Token, t["id"], { self.db.recordModify(
Token,
t["id"],
{
"status": TokenStatus.REVOKED, "status": TokenStatus.REVOKED,
"revokedAt": get_utc_timestamp(), "revokedAt": get_utc_timestamp(),
"revokedBy": revokedBy, "revokedBy": revokedBy,
"reason": reason or "session logout" "reason": reason or "session logout",
}) },
)
count += 1 count += 1
return count return count
except Exception as e: except Exception as e:
logger.error(f"Error revoking tokens for session {sessionId}: {str(e)}") logger.error(f"Error revoking tokens for session {sessionId}: {str(e)}")
return 0 return 0
def revokeTokensByUser(self, userId: str, authority: AuthAuthority = None, mandateId: str = None, revokedBy: str = None, reason: str = None) -> int: def revokeTokensByUser(
self,
userId: str,
authority: AuthAuthority = None,
mandateId: str = None,
revokedBy: str = None,
reason: str = None,
) -> int:
"""Revoke all active tokens for a user, optionally filtered by authority/mandate.""" """Revoke all active tokens for a user, optionally filtered by authority/mandate."""
try: try:
# Fetch all active tokens for user (optionally filtered by authority) # Fetch all active tokens for user (optionally filtered by authority)
@ -932,16 +1005,22 @@ class AppObjects:
"status": TokenStatus.ACTIVE, "status": TokenStatus.ACTIVE,
} }
if authority is not None: if authority is not None:
recordFilter["authority"] = authority.value if hasattr(authority, 'value') else str(authority) recordFilter["authority"] = (
authority.value if hasattr(authority, "value") else str(authority)
)
tokens = self.db.getRecordset(Token, recordFilter=recordFilter) tokens = self.db.getRecordset(Token, recordFilter=recordFilter)
count = 0 count = 0
for t in tokens: for t in tokens:
self.db.recordModify(Token, t["id"], { self.db.recordModify(
Token,
t["id"],
{
"status": TokenStatus.REVOKED, "status": TokenStatus.REVOKED,
"revokedAt": get_utc_timestamp(), "revokedAt": get_utc_timestamp(),
"revokedBy": revokedBy, "revokedBy": revokedBy,
"reason": reason or "admin revoke" "reason": reason or "admin revoke",
}) },
)
count += 1 count += 1
return count return count
except Exception as e: except Exception as e:
@ -958,7 +1037,10 @@ class AppObjects:
all_tokens = self.db.getRecordset(Token, recordFilter={}) all_tokens = self.db.getRecordset(Token, recordFilter={})
for token_data in all_tokens: for token_data in all_tokens:
if token_data.get("expiresAt") and token_data.get("expiresAt") < current_time: if (
token_data.get("expiresAt")
and token_data.get("expiresAt") < current_time
):
# Token is expired, delete it # Token is expired, delete it
self.db.recordDelete(Token, token_data["id"]) self.db.recordDelete(Token, token_data["id"])
cleaned_count += 1 cleaned_count += 1
@ -983,7 +1065,7 @@ class AppObjects:
self.access = None self.access = None
# Clear database context # Clear database context
if hasattr(self, 'db'): if hasattr(self, "db"):
self.db.updateContext("") self.db.updateContext("")
logger.info("User logged out successfully") logger.info("User logged out successfully")
@ -997,7 +1079,9 @@ class AppObjects:
def getNeutralizationConfig(self) -> Optional[DataNeutraliserConfig]: def getNeutralizationConfig(self) -> Optional[DataNeutraliserConfig]:
"""Get the data neutralization configuration for the current user's mandate""" """Get the data neutralization configuration for the current user's mandate"""
try: try:
configs = self.db.getRecordset(DataNeutraliserConfig, recordFilter={"mandateId": self.mandateId}) configs = self.db.getRecordset(
DataNeutraliserConfig, recordFilter={"mandateId": self.mandateId}
)
if not configs: if not configs:
return None return None
@ -1012,7 +1096,9 @@ class AppObjects:
logger.error(f"Error getting neutralization config: {str(e)}") logger.error(f"Error getting neutralization config: {str(e)}")
return None return None
def createOrUpdateNeutralizationConfig(self, config_data: Dict[str, Any]) -> DataNeutraliserConfig: def createOrUpdateNeutralizationConfig(
self, config_data: Dict[str, Any]
) -> DataNeutraliserConfig:
"""Create or update the data neutralization configuration""" """Create or update the data neutralization configuration"""
try: try:
# Check if config already exists # Check if config already exists
@ -1025,7 +1111,9 @@ class AppObjects:
update_data["updatedAt"] = get_utc_timestamp() update_data["updatedAt"] = get_utc_timestamp()
updated_config = DataNeutraliserConfig.from_dict(update_data) updated_config = DataNeutraliserConfig.from_dict(update_data)
self.db.recordModify(DataNeutraliserConfig, existing_config.id, updated_config) self.db.recordModify(
DataNeutraliserConfig, existing_config.id, updated_config
)
return updated_config return updated_config
else: else:
@ -1042,17 +1130,24 @@ class AppObjects:
logger.error(f"Error creating/updating neutralization config: {str(e)}") logger.error(f"Error creating/updating neutralization config: {str(e)}")
raise ValueError(f"Failed to create/update neutralization config: {str(e)}") raise ValueError(f"Failed to create/update neutralization config: {str(e)}")
def getNeutralizationAttributes(self, file_id: Optional[str] = None) -> List[DataNeutralizerAttributes]: def getNeutralizationAttributes(
self, file_id: Optional[str] = None
) -> List[DataNeutralizerAttributes]:
"""Get neutralization attributes, optionally filtered by file ID""" """Get neutralization attributes, optionally filtered by file ID"""
try: try:
filter_dict = {"mandateId": self.mandateId} filter_dict = {"mandateId": self.mandateId}
if file_id: if file_id:
filter_dict["fileId"] = file_id filter_dict["fileId"] = file_id
attributes = self.db.getRecordset(DataNeutralizerAttributes, recordFilter=filter_dict) attributes = self.db.getRecordset(
DataNeutralizerAttributes, recordFilter=filter_dict
)
filtered_attributes = self._uam(DataNeutralizerAttributes, attributes) filtered_attributes = self._uam(DataNeutralizerAttributes, attributes)
return [DataNeutralizerAttributes.from_dict(attr) for attr in filtered_attributes] return [
DataNeutralizerAttributes.from_dict(attr)
for attr in filtered_attributes
]
except Exception as e: except Exception as e:
logger.error(f"Error getting neutralization attributes: {str(e)}") logger.error(f"Error getting neutralization attributes: {str(e)}")
@ -1061,23 +1156,27 @@ class AppObjects:
def deleteNeutralizationAttributes(self, file_id: str) -> bool: def deleteNeutralizationAttributes(self, file_id: str) -> bool:
"""Delete all neutralization attributes for a specific file""" """Delete all neutralization attributes for a specific file"""
try: try:
attributes = self.db.getRecordset(DataNeutralizerAttributes, recordFilter={ attributes = self.db.getRecordset(
"mandateId": self.mandateId, DataNeutralizerAttributes,
"fileId": file_id recordFilter={"mandateId": self.mandateId, "fileId": file_id},
}) )
for attribute in attributes: for attribute in attributes:
self.db.recordDelete(DataNeutralizerAttributes, attribute["id"]) self.db.recordDelete(DataNeutralizerAttributes, attribute["id"])
logger.info(f"Deleted {len(attributes)} neutralization attributes for file {file_id}") logger.info(
f"Deleted {len(attributes)} neutralization attributes for file {file_id}"
)
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error deleting neutralization attributes: {str(e)}") logger.error(f"Error deleting neutralization attributes: {str(e)}")
return False return False
# Public Methods # Public Methods
def getInterface(currentUser: User) -> AppObjects: def getInterface(currentUser: User) -> AppObjects:
""" """
Returns a AppObjects instance for the current user. Returns a AppObjects instance for the current user.
@ -1095,6 +1194,7 @@ def getInterface(currentUser: User) -> AppObjects:
return _gatewayInterfaces[contextKey] return _gatewayInterfaces[contextKey]
def getRootInterface() -> AppObjects: def getRootInterface() -> AppObjects:
""" """
Returns a AppObjects instance with root privileges. Returns a AppObjects instance with root privileges.
@ -1112,13 +1212,15 @@ def getRootInterface() -> AppObjects:
if not initialUserId: if not initialUserId:
raise ValueError("No initial user ID found in database") raise ValueError("No initial user ID found in database")
users = tempInterface.db.getRecordset(UserInDB, recordFilter={"id": initialUserId}) users = tempInterface.db.getRecordset(
UserInDB, recordFilter={"id": initialUserId}
)
if not users: if not users:
raise ValueError("Initial user not found in database") raise ValueError("Initial user not found in database")
# Convert to User model # Convert to User model
user_data = users[0] user_data = users[0]
rootUser = User.parse_obj(user_data) rootUser = User.model_validate(user_data)
# Create root interface with the root user # Create root interface with the root user
_rootAppObjects = AppObjects(rootUser) _rootAppObjects = AppObjects(rootUser)

View file

@ -2,13 +2,14 @@
Shared utilities for model attributes and labels. Shared utilities for model attributes and labels.
""" """
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, ConfigDict
from typing import Dict, Any, List, Type, Optional, Union from typing import Dict, Any, List, Type, Optional, Union
import inspect import inspect
import importlib import importlib
import os import os
from datetime import datetime from datetime import datetime
class ModelMixin: class ModelMixin:
"""Mixin class that provides serialization methods for Pydantic models.""" """Mixin class that provides serialization methods for Pydantic models."""
@ -22,7 +23,7 @@ class ModelMixin:
Dict[str, Any]: Dictionary representation of the model Dict[str, Any]: Dictionary representation of the model
""" """
# Get the raw dictionary # Get the raw dictionary
if hasattr(self, 'model_dump'): if hasattr(self, "model_dump"):
data: Dict[str, Any] = self.model_dump() # Pydantic v2 data: Dict[str, Any] = self.model_dump() # Pydantic v2
else: else:
data: Dict[str, Any] = self.dict() # Pydantic v1 data: Dict[str, Any] = self.dict() # Pydantic v1
@ -33,7 +34,7 @@ class ModelMixin:
return data return data
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin': def from_dict(cls, data: Dict[str, Any]) -> "ModelMixin":
""" """
Create a Pydantic model instance from a dictionary. Create a Pydantic model instance from a dictionary.
@ -45,9 +46,11 @@ class ModelMixin:
""" """
return cls(**data) return cls(**data)
# Define the AttributeDefinition class here instead of importing it # Define the AttributeDefinition class here instead of importing it
class AttributeDefinition(BaseModel, ModelMixin): class AttributeDefinition(BaseModel, ModelMixin):
"""Definition of a model attribute with its metadata.""" """Definition of a model attribute with its metadata."""
name: str name: str
type: str type: str
label: str label: str
@ -64,9 +67,11 @@ class AttributeDefinition(BaseModel, ModelMixin):
order: int = 0 order: int = 0
placeholder: Optional[str] = None placeholder: Optional[str] = None
# Global registry for model labels # Global registry for model labels
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {} MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
def to_dict(model: BaseModel) -> Dict[str, Any]: def to_dict(model: BaseModel) -> Dict[str, Any]:
""" """
Convert a Pydantic model to a dictionary. Convert a Pydantic model to a dictionary.
@ -78,10 +83,11 @@ def to_dict(model: BaseModel) -> Dict[str, Any]:
Returns: Returns:
Dict[str, Any]: Dictionary representation of the model Dict[str, Any]: Dictionary representation of the model
""" """
if hasattr(model, 'model_dump'): if hasattr(model, "model_dump"):
return model.model_dump() # Pydantic v2 return model.model_dump() # Pydantic v2
return model.dict() # Pydantic v1 return model.dict() # Pydantic v1
def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel: def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
""" """
Create a Pydantic model instance from a dictionary. Create a Pydantic model instance from a dictionary.
@ -95,7 +101,10 @@ def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
""" """
return model_class(**data) return model_class(**data)
def register_model_labels(model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]]):
def register_model_labels(
model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]]
):
""" """
Register labels for a model's attributes and the model itself. Register labels for a model's attributes and the model itself.
@ -106,10 +115,8 @@ def register_model_labels(model_name: str, model_label: Dict[str, str], labels:
labels: Dictionary mapping attribute names to their translations labels: Dictionary mapping attribute names to their translations
e.g. {"name": {"en": "Name", "fr": "Nom"}} e.g. {"name": {"en": "Name", "fr": "Nom"}}
""" """
MODEL_LABELS[model_name] = { MODEL_LABELS[model_name] = {"model": model_label, "attributes": labels}
"model": model_label,
"attributes": labels
}
def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]: def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
""" """
@ -130,6 +137,7 @@ def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
for attr, translations in attribute_labels.items() for attr, translations in attribute_labels.items()
} }
def get_model_label(model_name: str, language: str = "en") -> str: def get_model_label(model_name: str, language: str = "en") -> str:
""" """
Get the label for a model in the specified language. Get the label for a model in the specified language.
@ -145,7 +153,10 @@ def get_model_label(model_name: str, language: str = "en") -> str:
model_label = model_data.get("model", {}) model_label = model_data.get("model", {})
return model_label.get(language, model_label.get("en", model_name)) return model_label.get(language, model_label.get("en", model_name))
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
def getModelAttributeDefinitions(
modelClass: Type[BaseModel] = None, userLanguage: str = "en"
) -> Dict[str, Any]:
""" """
Get attribute definitions for a model class. Get attribute definitions for a model class.
@ -165,11 +176,11 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
model_label = get_model_label(model_name, userLanguage) model_label = get_model_label(model_name, userLanguage)
# Handle both Pydantic v1 and v2 # Handle both Pydantic v1 and v2
if hasattr(modelClass, 'model_fields'): # Pydantic v2 if hasattr(modelClass, "model_fields"): # Pydantic v2
fields = modelClass.model_fields fields = modelClass.model_fields
for name, field in fields.items(): for name, field in fields.items():
# Extract frontend metadata from field info # Extract frontend metadata from field info
field_info = field.field_info if hasattr(field, 'field_info') else None field_info = field.field_info if hasattr(field, "field_info") else None
# Check both direct attributes and extra field for frontend metadata # Check both direct attributes and extra field for frontend metadata
frontend_type = None frontend_type = None
frontend_readonly = False frontend_readonly = False
@ -178,43 +189,63 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
if field_info: if field_info:
# Try direct attributes first # Try direct attributes first
frontend_type = getattr(field_info, 'frontend_type', None) frontend_type = getattr(field_info, "frontend_type", None)
frontend_readonly = getattr(field_info, 'frontend_readonly', False) frontend_readonly = getattr(field_info, "frontend_readonly", False)
frontend_required = getattr(field_info, 'frontend_required', frontend_required) frontend_required = getattr(
frontend_options = getattr(field_info, 'frontend_options', None) field_info, "frontend_required", frontend_required
)
frontend_options = getattr(field_info, "frontend_options", None)
# If not found, check extra field # If not found, check extra field
if hasattr(field_info, 'extra') and field_info.extra: if hasattr(field_info, "extra") and field_info.extra:
if frontend_type is None: if frontend_type is None:
frontend_type = field_info.extra.get('frontend_type') frontend_type = field_info.extra.get("frontend_type")
if not frontend_readonly: if not frontend_readonly:
frontend_readonly = field_info.extra.get('frontend_readonly', False) frontend_readonly = field_info.extra.get(
if frontend_required == field.is_required(): # Only override if we didn't get it from direct attribute "frontend_readonly", False
frontend_required = field_info.extra.get('frontend_required', frontend_required) )
if (
frontend_required == field.is_required()
): # Only override if we didn't get it from direct attribute
frontend_required = field_info.extra.get(
"frontend_required", frontend_required
)
if frontend_options is None: if frontend_options is None:
frontend_options = field_info.extra.get('frontend_options') frontend_options = field_info.extra.get("frontend_options")
# Use frontend type if available, otherwise fall back to Python type # Use frontend type if available, otherwise fall back to Python type
field_type = frontend_type if frontend_type else (field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation)) field_type = (
frontend_type
if frontend_type
else (
field.annotation.__name__
if hasattr(field.annotation, "__name__")
else str(field.annotation)
)
)
attributes.append({ attributes.append(
{
"name": name, "name": name,
"type": field_type, "type": field_type,
"required": frontend_required, "required": frontend_required,
"description": field.description if hasattr(field, "description") else "", "description": field.description
if hasattr(field, "description")
else "",
"label": labels.get(name, name), "label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}", "placeholder": f"Please enter {labels.get(name, name)}",
"editable": not frontend_readonly, "editable": not frontend_readonly,
"visible": True, "visible": True,
"order": len(attributes), "order": len(attributes),
"readonly": frontend_readonly, "readonly": frontend_readonly,
"options": frontend_options "options": frontend_options,
}) }
)
else: # Pydantic v1 else: # Pydantic v1
fields = modelClass.__fields__ fields = modelClass.__fields__
for name, field in fields.items(): for name, field in fields.items():
# Extract frontend metadata from field info # Extract frontend metadata from field info
field_info = field.field_info if hasattr(field, 'field_info') else None field_info = field.field_info if hasattr(field, "field_info") else None
# Check both direct attributes and extra field for frontend metadata # Check both direct attributes and extra field for frontend metadata
frontend_type = None frontend_type = None
frontend_readonly = False frontend_readonly = False
@ -223,43 +254,61 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
if field_info: if field_info:
# Try direct attributes first # Try direct attributes first
frontend_type = getattr(field_info, 'frontend_type', None) frontend_type = getattr(field_info, "frontend_type", None)
frontend_readonly = getattr(field_info, 'frontend_readonly', False) frontend_readonly = getattr(field_info, "frontend_readonly", False)
frontend_required = getattr(field_info, 'frontend_required', frontend_required) frontend_required = getattr(
frontend_options = getattr(field_info, 'frontend_options', None) field_info, "frontend_required", frontend_required
)
frontend_options = getattr(field_info, "frontend_options", None)
# If not found, check extra field # If not found, check extra field
if hasattr(field_info, 'extra') and field_info.extra: if hasattr(field_info, "extra") and field_info.extra:
if frontend_type is None: if frontend_type is None:
frontend_type = field_info.extra.get('frontend_type') frontend_type = field_info.extra.get("frontend_type")
if not frontend_readonly: if not frontend_readonly:
frontend_readonly = field_info.extra.get('frontend_readonly', False) frontend_readonly = field_info.extra.get(
if frontend_required == field.required: # Only override if we didn't get it from direct attribute "frontend_readonly", False
frontend_required = field_info.extra.get('frontend_required', frontend_required) )
if (
frontend_required == field.required
): # Only override if we didn't get it from direct attribute
frontend_required = field_info.extra.get(
"frontend_required", frontend_required
)
if frontend_options is None: if frontend_options is None:
frontend_options = field_info.extra.get('frontend_options') frontend_options = field_info.extra.get("frontend_options")
# Use frontend type if available, otherwise fall back to Python type # Use frontend type if available, otherwise fall back to Python type
field_type = frontend_type if frontend_type else (field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_)) field_type = (
frontend_type
if frontend_type
else (
field.type_.__name__
if hasattr(field.type_, "__name__")
else str(field.type_)
)
)
attributes.append({ attributes.append(
{
"name": name, "name": name,
"type": field_type, "type": field_type,
"required": frontend_required, "required": frontend_required,
"description": field.field_info.description if hasattr(field.field_info, "description") else "", "description": field.field_info.description
if hasattr(field.field_info, "description")
else "",
"label": labels.get(name, name), "label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}", "placeholder": f"Please enter {labels.get(name, name)}",
"editable": not frontend_readonly, "editable": not frontend_readonly,
"visible": True, "visible": True,
"order": len(attributes), "order": len(attributes),
"readonly": frontend_readonly, "readonly": frontend_readonly,
"options": frontend_options "options": frontend_options,
})
return {
"model": model_label,
"attributes": attributes
} }
)
return {"model": model_label, "attributes": attributes}
def getModelClasses() -> Dict[str, Type[BaseModel]]: def getModelClasses() -> Dict[str, Type[BaseModel]]:
""" """
@ -271,30 +320,38 @@ def getModelClasses() -> Dict[str, Type[BaseModel]]:
modelClasses = {} modelClasses = {}
# Get the interfaces directory path # Get the interfaces directory path
interfaces_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'interfaces') interfaces_dir = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "interfaces"
)
# Find all model files # Find all model files
for fileName in os.listdir(interfaces_dir): for fileName in os.listdir(interfaces_dir):
if fileName.endswith('Model.py'): if fileName.endswith("Model.py"):
# Convert fileName to module name (e.g., gatewayModel.py -> gatewayModel) # Convert fileName to module name (e.g., gatewayModel.py -> gatewayModel)
module_name = fileName[:-3] module_name = fileName[:-3]
# Import the module dynamically # Import the module dynamically
module = importlib.import_module(f'modules.interfaces.{module_name}') module = importlib.import_module(f"modules.interfaces.{module_name}")
# Get all classes from the module # Get all classes from the module
for name, obj in inspect.getmembers(module): for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj != BaseModel: if (
inspect.isclass(obj)
and issubclass(obj, BaseModel)
and obj != BaseModel
):
modelClasses[name] = obj modelClasses[name] = obj
return modelClasses return modelClasses
class AttributeResponse(BaseModel): class AttributeResponse(BaseModel):
"""Response model for entity attributes""" """Response model for entity attributes"""
attributes: List[AttributeDefinition] attributes: List[AttributeDefinition]
class Config: model_config = ConfigDict(
schema_extra = { json_schema_extra={
"example": { "example": {
"attributes": [ "attributes": [
{ {
@ -305,8 +362,9 @@ class AttributeResponse(BaseModel):
"placeholder": "Please enter username", "placeholder": "Please enter username",
"editable": True, "editable": True,
"visible": True, "visible": True,
"order": 0 "order": 0,
} }
] ]
} }
} }
)

View file

@ -4,7 +4,7 @@ websockets==12.0
uvicorn==0.23.2 uvicorn==0.23.2
python-multipart==0.0.6 python-multipart==0.0.6
httpx==0.25.0 httpx==0.25.0
pydantic==1.10.13 # Ältere Version ohne Rust-Abhängigkeit pydantic>=2.0.0 # Upgraded to v2 for LangChain compatibility
email-validator==2.0.0 # Required by Pydantic for email validation email-validator==2.0.0 # Required by Pydantic for email validation
slowapi==0.1.8 # For rate limiting slowapi==0.1.8 # For rate limiting
@ -108,3 +108,8 @@ xyzservices>=2021.09.1
# PostgreSQL connector dependencies # PostgreSQL connector dependencies
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
## LangChain & LangGraph
langchain==0.3.27
langgraph==0.6.8
langchain-core==0.3.77