mvp running
This commit is contained in:
parent
ecf23255d2
commit
8c9492715a
26 changed files with 1467 additions and 862 deletions
7
app.py
7
app.py
|
|
@ -95,6 +95,10 @@ async def lifespan(app: FastAPI):
|
|||
# Startup logic
|
||||
logger.info("Application is starting up")
|
||||
|
||||
# Initialize root interface to ensure database is properly set up
|
||||
from modules.interfaces.serviceAppClass import getRootInterface
|
||||
getRootInterface()
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown logic
|
||||
|
|
@ -146,6 +150,9 @@ app.include_router(fileRouter)
|
|||
from modules.routes.routeDataPrompts import router as promptRouter
|
||||
app.include_router(promptRouter)
|
||||
|
||||
from modules.routes.routeDataConnections import router as connectionsRouter
|
||||
app.include_router(connectionsRouter)
|
||||
|
||||
from modules.routes.routeWorkflows import router as workflowRouter
|
||||
app.include_router(workflowRouter)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import uuid
|
|||
from modules.workflow.agentBase import AgentBase
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
|
||||
from modules.shared.attributeUtils import ModelMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ from typing import List, Dict, Any, Optional, Union
|
|||
import logging
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pydantic import BaseModel
|
||||
|
||||
from modules.shared.attributeUtils import to_dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -138,14 +141,61 @@ class DatabaseConnector:
|
|||
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 _saveRecord(self, table: str, recordId: str, record: Dict[str, Any]) -> bool:
|
||||
"""Saves a single record to the table."""
|
||||
try:
|
||||
# Ensure table directory exists
|
||||
if not self._ensureTableDirectory(table):
|
||||
raise ValueError(f"Error creating table directory for {table}")
|
||||
|
||||
# Ensure recordId is a string
|
||||
recordId = str(recordId)
|
||||
|
||||
# Add metadata
|
||||
currentTime = datetime.now().isoformat()
|
||||
if "_createdAt" not in record:
|
||||
record["_createdAt"] = currentTime
|
||||
record["_createdBy"] = self.userId
|
||||
record["_modifiedAt"] = currentTime
|
||||
record["_modifiedBy"] = self.userId
|
||||
|
||||
# Save the record file
|
||||
recordPath = self._getRecordPath(table, recordId)
|
||||
os.makedirs(os.path.dirname(recordPath), exist_ok=True)
|
||||
|
||||
with open(recordPath, 'w', encoding='utf-8') as f:
|
||||
json.dump(record, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Update metadata
|
||||
metadata = self._loadTableMetadata(table)
|
||||
if recordId not in metadata["recordIds"]:
|
||||
metadata["recordIds"].append(recordId)
|
||||
metadata["recordIds"].sort()
|
||||
self._saveTableMetadata(table, metadata)
|
||||
|
||||
# Update cache if it exists
|
||||
if table in self._tablesCache:
|
||||
# Find and update existing record or append new one
|
||||
found = False
|
||||
for i, existing_record in enumerate(self._tablesCache[table]):
|
||||
if str(existing_record.get("id")) == recordId:
|
||||
self._tablesCache[table][i] = record
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
self._tablesCache[table].append(record)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving record {recordId} to table {table}: {e}")
|
||||
return False
|
||||
|
||||
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
|
||||
|
|
@ -162,6 +212,9 @@ class DatabaseConnector:
|
|||
|
||||
# Load each record
|
||||
for recordId in metadata["recordIds"]:
|
||||
# Skip metadata file
|
||||
if recordId == "_metadata":
|
||||
continue
|
||||
record = self._loadRecord(table, recordId)
|
||||
if record:
|
||||
records.append(record)
|
||||
|
|
@ -376,67 +429,37 @@ class DatabaseConnector:
|
|||
|
||||
return records
|
||||
|
||||
def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""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}")
|
||||
def recordCreate(self, table: str, record: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Creates a new record in a table."""
|
||||
# Ensure record has an ID
|
||||
if "id" not in record:
|
||||
record["id"] = str(uuid.uuid4())
|
||||
|
||||
# Load table metadata
|
||||
metadata = self._loadTableMetadata(table)
|
||||
# If record is a Pydantic model, convert to dict
|
||||
if isinstance(record, BaseModel):
|
||||
record = to_dict(record)
|
||||
|
||||
# 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"])
|
||||
# Save record
|
||||
self._saveRecord(table, record["id"], record)
|
||||
return record
|
||||
|
||||
# Add context fields
|
||||
recordData["userId"] = self.userId
|
||||
def recordModify(self, table: str, recordId: str, record: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Modifies an existing record in a table."""
|
||||
# Load existing record
|
||||
existingRecord = self._loadRecord(table, recordId)
|
||||
if not existingRecord:
|
||||
raise ValueError(f"Record {recordId} not found in table {table}")
|
||||
|
||||
# Add creation and modification tracking
|
||||
currentTime = self._getCurrentTimestamp()
|
||||
recordData["_createdAt"] = currentTime
|
||||
recordData["_modifiedAt"] = currentTime
|
||||
recordData["_createdBy"] = self.userId
|
||||
recordData["_modifiedBy"] = self.userId
|
||||
# If record is a Pydantic model, convert to dict
|
||||
if isinstance(record, BaseModel):
|
||||
record = to_dict(record)
|
||||
|
||||
# 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)
|
||||
# Update existing record with new data
|
||||
existingRecord.update(record)
|
||||
|
||||
# Update metadata with new record ID
|
||||
if recordData["id"] not in metadata["recordIds"]:
|
||||
metadata["recordIds"].append(recordData["id"])
|
||||
metadata["recordIds"].sort()
|
||||
|
||||
# Save updated metadata
|
||||
if not self._saveTableMetadata(table, metadata):
|
||||
raise ValueError(f"Error saving metadata for table {table}")
|
||||
|
||||
# Update both caches
|
||||
self._tableMetadataCache[table] = metadata
|
||||
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
|
||||
|
||||
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}")
|
||||
# Save updated record
|
||||
self._saveRecord(table, recordId, existingRecord)
|
||||
return existingRecord
|
||||
|
||||
def recordDelete(self, table: str, recordId: str) -> bool:
|
||||
"""Deletes a record from the table."""
|
||||
|
|
@ -473,56 +496,6 @@ class DatabaseConnector:
|
|||
|
||||
return False
|
||||
|
||||
def recordModify(self, table: str, recordId: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Modifies a record in the 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)
|
||||
|
||||
# Ensure recordId is a string
|
||||
recordId = str(recordId)
|
||||
|
||||
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 and user
|
||||
existingRecord["_modifiedAt"] = self._getCurrentTimestamp()
|
||||
existingRecord["_modifiedBy"] = self.userId
|
||||
|
||||
# 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()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from typing import Dict, Any, List, Optional, Union
|
|||
import importlib
|
||||
import json
|
||||
from passlib.context import CryptContext
|
||||
import uuid
|
||||
|
||||
from modules.connectors.connectorDbJson import DatabaseConnector
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
|
@ -17,8 +18,9 @@ from modules.interfaces.serviceAppAccess import AppAccess
|
|||
from modules.interfaces.serviceAppModel import (
|
||||
User, Mandate, UserInDB, UserConnection,
|
||||
Session, AuthEvent, AuthAuthority, UserPrivilege,
|
||||
ConnectionStatus
|
||||
ConnectionStatus, Token, LocalToken, GoogleToken, MsftToken
|
||||
)
|
||||
from modules.shared.attributeUtils import ModelMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -74,6 +76,9 @@ class GatewayInterface:
|
|||
# Initialize access control with user context
|
||||
self.access = AppAccess(self.currentUser, self.db) # Convert to dict only when needed
|
||||
|
||||
# Update database context
|
||||
self.db.updateContext(self.userId)
|
||||
|
||||
logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
|
||||
|
||||
def _initializeDatabase(self):
|
||||
|
|
@ -164,7 +169,17 @@ class GatewayInterface:
|
|||
Returns:
|
||||
Filtered recordset with access control attributes
|
||||
"""
|
||||
return self.access.uam(table, recordset)
|
||||
# First apply access control
|
||||
filteredRecords = self.access.uam(table, recordset)
|
||||
|
||||
# Then filter out database-specific fields
|
||||
cleanedRecords = []
|
||||
for record in filteredRecords:
|
||||
# Create a new dict with only non-database fields
|
||||
cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
|
||||
cleanedRecords.append(cleanedRecord)
|
||||
|
||||
return cleanedRecords
|
||||
|
||||
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
|
||||
"""
|
||||
|
|
@ -445,6 +460,12 @@ class GatewayInterface:
|
|||
self.db.recordDelete("auth_events", event["id"])
|
||||
logger.debug(f"Deleted auth event {event['id']} for user {userId}")
|
||||
|
||||
# Delete user tokens
|
||||
tokens = self.db.getRecordset("tokens", recordFilter={"userId": userId})
|
||||
for token in tokens:
|
||||
self.db.recordDelete("tokens", token["id"])
|
||||
logger.debug(f"Deleted token {token['id']} for user {userId}")
|
||||
|
||||
# Delete user connections
|
||||
user = self.getUser(userId)
|
||||
if user and user.connections:
|
||||
|
|
@ -593,6 +614,78 @@ class GatewayInterface:
|
|||
"message": f"Error checking username availability: {str(e)}"
|
||||
}
|
||||
|
||||
def saveToken(self, token: Token) -> None:
|
||||
"""Save a token for the current user"""
|
||||
try:
|
||||
# Validate user context
|
||||
if not self.currentUser or not self.currentUser.id:
|
||||
raise ValueError("No valid user context available for token storage")
|
||||
|
||||
# Set the user ID and mandate ID
|
||||
token.userId = self.currentUser.id
|
||||
|
||||
# Ensure token has required fields
|
||||
if not token.id:
|
||||
token.id = str(uuid.uuid4())
|
||||
if not token.createdAt:
|
||||
token.createdAt = datetime.now()
|
||||
|
||||
# Convert to dict and ensure all fields are properly set
|
||||
token_dict = token.dict()
|
||||
token_dict["userId"] = self.currentUser.id
|
||||
|
||||
# Convert datetime objects to ISO format strings
|
||||
if isinstance(token_dict.get("createdAt"), datetime):
|
||||
token_dict["createdAt"] = token_dict["createdAt"].isoformat()
|
||||
if isinstance(token_dict.get("expiresAt"), datetime):
|
||||
token_dict["expiresAt"] = token_dict["expiresAt"].isoformat()
|
||||
|
||||
# Save to database
|
||||
self.db.recordCreate("tokens", token_dict)
|
||||
|
||||
logger.debug(f"Token saved for user {self.currentUser.id} with authority {token.authority}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving token: {str(e)}")
|
||||
raise
|
||||
|
||||
def getToken(self, authority: AuthAuthority) -> Optional[Token]:
|
||||
"""Get the latest token for the current user and authority"""
|
||||
try:
|
||||
# Get tokens for this user and authority
|
||||
tokens = self.db.getRecordset("tokens", recordFilter={
|
||||
"userId": self.currentUser.id,
|
||||
"authority": authority
|
||||
})
|
||||
|
||||
if not tokens:
|
||||
return None
|
||||
|
||||
# Sort by creation date and get the latest
|
||||
tokens.sort(key=lambda x: x.get("createdAt", ""), reverse=True)
|
||||
return Token(**tokens[0])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting token: {str(e)}")
|
||||
return None
|
||||
|
||||
def deleteToken(self, authority: AuthAuthority) -> None:
|
||||
"""Delete all tokens for the current user and authority"""
|
||||
try:
|
||||
# Get tokens to delete
|
||||
tokens = self.db.getRecordset("tokens", recordFilter={
|
||||
"userId": self.currentUser.id,
|
||||
"authority": authority
|
||||
})
|
||||
|
||||
# Delete each token
|
||||
for token in tokens:
|
||||
self.db.recordDelete("tokens", token["id"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting token: {str(e)}")
|
||||
raise
|
||||
|
||||
# Public Methods
|
||||
|
||||
def getInterface(currentUser: User) -> GatewayInterface:
|
||||
|
|
|
|||
|
|
@ -7,26 +7,13 @@ from pydantic import BaseModel, Field, EmailStr
|
|||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from modules.shared.attributeUtils import Label, BaseModelWithUI
|
||||
|
||||
class AttributeDefinition(BaseModel):
|
||||
"""Definition of an attribute for UI forms"""
|
||||
name: str = Field(..., description="Name of the attribute")
|
||||
label: str = Field(..., description="Display label for the attribute")
|
||||
type: str = Field(..., description="Type of the attribute (string, number, boolean, etc.)")
|
||||
required: bool = Field(default=False, description="Whether the attribute is required")
|
||||
placeholder: Optional[str] = Field(None, description="Placeholder text for the input")
|
||||
editable: bool = Field(default=True, description="Whether the attribute can be edited")
|
||||
visible: bool = Field(default=True, description="Whether the attribute should be visible in forms")
|
||||
order: int = Field(default=0, description="Order in which to display the attribute")
|
||||
from modules.shared.attributeUtils import register_model_labels, AttributeDefinition, ModelMixin
|
||||
|
||||
class AuthAuthority(str, Enum):
|
||||
"""Authentication authorities"""
|
||||
LOCAL = "local"
|
||||
MICROSOFT = "microsoft"
|
||||
GOOGLE = "google"
|
||||
EXTERNAL = "external"
|
||||
"""Authentication authority enum"""
|
||||
LOCAL = "Local"
|
||||
GOOGLE = "Google"
|
||||
MSFT = "Msft"
|
||||
|
||||
class UserPrivilege(str, Enum):
|
||||
"""User privilege levels"""
|
||||
|
|
@ -41,39 +28,24 @@ class ConnectionStatus(str, Enum):
|
|||
REVOKED = "revoked"
|
||||
PENDING = "pending"
|
||||
|
||||
class Mandate(BaseModelWithUI):
|
||||
class Mandate(BaseModel, ModelMixin):
|
||||
"""Data model for a 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(default="en", description="Default language of the mandate")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="Mandate", translations={"en": "Mandate", "fr": "Mandat"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"name": Label(default="Name of the mandate", translations={"en": "Mandate name", "fr": "Nom du mandat"}),
|
||||
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"})
|
||||
# Register labels for Mandate
|
||||
register_model_labels(
|
||||
"Mandate",
|
||||
{"en": "Mandate", "fr": "Mandat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"name": {"en": "Name", "fr": "Nom"},
|
||||
"language": {"en": "Language", "fr": "Langue"}
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_validations(cls) -> Dict[str, Any]:
|
||||
"""Get validation rules for frontend"""
|
||||
return {
|
||||
"name": {
|
||||
"required": True,
|
||||
"minLength": 2,
|
||||
"maxLength": 100
|
||||
},
|
||||
"language": {
|
||||
"required": True,
|
||||
"pattern": "^[a-z]{2}$"
|
||||
}
|
||||
}
|
||||
|
||||
class UserConnection(BaseModelWithUI):
|
||||
class UserConnection(BaseModel, ModelMixin):
|
||||
"""Data model for a user's connection to an external service"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
|
||||
authority: AuthAuthority = Field(description="Authentication authority")
|
||||
|
|
@ -85,24 +57,24 @@ class UserConnection(BaseModelWithUI):
|
|||
lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
|
||||
expiresAt: Optional[datetime] = Field(None, description="When the connection expires")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="User Connection", translations={"en": "User Connection", "fr": "Connexion utilisateur"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"authority": Label(default="Authority", translations={"en": "Authority", "fr": "Autorité"}),
|
||||
"externalId": Label(default="External ID", translations={"en": "External ID", "fr": "ID externe"}),
|
||||
"externalUsername": Label(default="External Username", translations={"en": "External Username", "fr": "Nom d'utilisateur externe"}),
|
||||
"externalEmail": Label(default="External Email", translations={"en": "External Email", "fr": "Email externe"}),
|
||||
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
|
||||
"connectedAt": Label(default="Connected At", translations={"en": "Connected At", "fr": "Connecté le"}),
|
||||
"lastChecked": Label(default="Last Checked", translations={"en": "Last Checked", "fr": "Dernière vérification"}),
|
||||
"expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"})
|
||||
# Register labels for UserConnection
|
||||
register_model_labels(
|
||||
"UserConnection",
|
||||
{"en": "User Connection", "fr": "Connexion utilisateur"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"authority": {"en": "Authority", "fr": "Autorité"},
|
||||
"externalId": {"en": "External ID", "fr": "ID externe"},
|
||||
"externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
|
||||
"externalEmail": {"en": "External Email", "fr": "Email externe"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"connectedAt": {"en": "Connected At", "fr": "Connecté le"},
|
||||
"lastChecked": {"en": "Last Checked", "fr": "Dernière vérification"},
|
||||
"expiresAt": {"en": "Expires At", "fr": "Expire le"}
|
||||
}
|
||||
)
|
||||
|
||||
class Session(BaseModelWithUI):
|
||||
class Session(BaseModel, ModelMixin):
|
||||
"""Data model for user sessions"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique session ID")
|
||||
userId: str = Field(description="ID of the user")
|
||||
|
|
@ -112,22 +84,22 @@ class Session(BaseModelWithUI):
|
|||
ipAddress: Optional[str] = Field(None, description="IP address of the session")
|
||||
userAgent: Optional[str] = Field(None, description="User agent of the session")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="Session", translations={"en": "Session", "fr": "Session"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}),
|
||||
"tokenId": Label(default="Token ID", translations={"en": "Token ID", "fr": "ID du token"}),
|
||||
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
|
||||
"expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"}),
|
||||
"ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}),
|
||||
"userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
|
||||
# Register labels for Session
|
||||
register_model_labels(
|
||||
"Session",
|
||||
{"en": "Session", "fr": "Session"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"tokenId": {"en": "Token ID", "fr": "ID du token"},
|
||||
"lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
|
||||
"expiresAt": {"en": "Expires At", "fr": "Expire le"},
|
||||
"ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
|
||||
"userAgent": {"en": "User Agent", "fr": "User Agent"}
|
||||
}
|
||||
)
|
||||
|
||||
class AuthEvent(BaseModelWithUI):
|
||||
class AuthEvent(BaseModel, ModelMixin):
|
||||
"""Data model for authentication events"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique event ID")
|
||||
userId: str = Field(description="ID of the user")
|
||||
|
|
@ -137,22 +109,22 @@ class AuthEvent(BaseModelWithUI):
|
|||
ipAddress: Optional[str] = Field(None, description="IP address of the event")
|
||||
userAgent: Optional[str] = Field(None, description="User agent of the event")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="Auth Event", translations={"en": "Auth Event", "fr": "Événement d'authentification"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}),
|
||||
"eventType": Label(default="Event Type", translations={"en": "Event Type", "fr": "Type d'événement"}),
|
||||
"details": Label(default="Details", translations={"en": "Details", "fr": "Détails"}),
|
||||
"timestamp": Label(default="Timestamp", translations={"en": "Timestamp", "fr": "Horodatage"}),
|
||||
"ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}),
|
||||
"userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
|
||||
# Register labels for AuthEvent
|
||||
register_model_labels(
|
||||
"AuthEvent",
|
||||
{"en": "Auth Event", "fr": "Événement d'authentification"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"eventType": {"en": "Event Type", "fr": "Type d'événement"},
|
||||
"details": {"en": "Details", "fr": "Détails"},
|
||||
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
|
||||
"ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
|
||||
"userAgent": {"en": "User Agent", "fr": "User Agent"}
|
||||
}
|
||||
)
|
||||
|
||||
class User(BaseModelWithUI):
|
||||
class User(BaseModel, ModelMixin):
|
||||
"""Data model for a user"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
|
||||
username: str = Field(description="Username for login")
|
||||
|
|
@ -165,58 +137,77 @@ class User(BaseModelWithUI):
|
|||
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
|
||||
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"username": Label(default="Username", translations={"en": "Username", "fr": "Nom d'utilisateur"}),
|
||||
"email": Label(default="Email", translations={"en": "Email", "fr": "Email"}),
|
||||
"fullName": Label(default="Full Name", translations={"en": "Full Name", "fr": "Nom complet"}),
|
||||
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}),
|
||||
"disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}),
|
||||
"privilege": Label(default="Privilege", translations={"en": "Privilege", "fr": "Privilège"}),
|
||||
"authenticationAuthority": Label(default="Auth Authority", translations={"en": "Auth Authority", "fr": "Autorité d'authentification"}),
|
||||
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
|
||||
"connections": Label(default="Connections", translations={"en": "Connections", "fr": "Connexions"})
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_validations(cls) -> Dict[str, Any]:
|
||||
"""Get validation rules for frontend"""
|
||||
return {
|
||||
"username": {
|
||||
"required": True,
|
||||
"minLength": 3,
|
||||
"maxLength": 50,
|
||||
"pattern": "^[a-zA-Z0-9_-]+$"
|
||||
},
|
||||
"email": {
|
||||
"required": False,
|
||||
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
|
||||
},
|
||||
"fullName": {
|
||||
"required": False,
|
||||
"maxLength": 100
|
||||
},
|
||||
"language": {
|
||||
"required": True,
|
||||
"pattern": "^[a-z]{2}$"
|
||||
}
|
||||
# Register labels for User
|
||||
register_model_labels(
|
||||
"User",
|
||||
{"en": "User", "fr": "Utilisateur"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"username": {"en": "Username", "fr": "Nom d'utilisateur"},
|
||||
"email": {"en": "Email", "fr": "Email"},
|
||||
"fullName": {"en": "Full Name", "fr": "Nom complet"},
|
||||
"language": {"en": "Language", "fr": "Langue"},
|
||||
"disabled": {"en": "Disabled", "fr": "Désactivé"},
|
||||
"privilege": {"en": "Privilege", "fr": "Privilège"},
|
||||
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
||||
"connections": {"en": "Connections", "fr": "Connexions"}
|
||||
}
|
||||
)
|
||||
|
||||
class UserInDB(User):
|
||||
"""Extended user class with password hash"""
|
||||
hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="User Access", translations={"en": "User Access", "fr": "Accès de l'utilisateur"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"hashedPassword": Label(default="Password hash", translations={"en": "Password hash", "fr": "Hachage de mot de passe"})
|
||||
# Register labels for UserInDB
|
||||
register_model_labels(
|
||||
"UserInDB",
|
||||
{"en": "User Access", "fr": "Accès de l'utilisateur"},
|
||||
{
|
||||
"hashedPassword": {"en": "Password hash", "fr": "Hachage de mot de passe"}
|
||||
}
|
||||
)
|
||||
|
||||
# Token Models
|
||||
class Token(BaseModel, ModelMixin):
|
||||
"""Token model for all authentication types"""
|
||||
id: Optional[str] = None
|
||||
userId: str
|
||||
authority: AuthAuthority
|
||||
tokenAccess: str
|
||||
tokenType: str = "bearer"
|
||||
expiresAt: float
|
||||
tokenRefresh: Optional[str] = None
|
||||
createdAt: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
useEnumValues = True
|
||||
|
||||
# Register labels for Token
|
||||
register_model_labels(
|
||||
"Token",
|
||||
{"en": "Token", "fr": "Jeton"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"authority": {"en": "Authority", "fr": "Autorité"},
|
||||
"tokenAccess": {"en": "Access Token", "fr": "Jeton d'accès"},
|
||||
"tokenType": {"en": "Token Type", "fr": "Type de jeton"},
|
||||
"expiresAt": {"en": "Expires At", "fr": "Expire le"},
|
||||
"tokenRefresh": {"en": "Refresh Token", "fr": "Jeton de rafraîchissement"},
|
||||
"createdAt": {"en": "Created At", "fr": "Créé le"}
|
||||
}
|
||||
)
|
||||
|
||||
class LocalToken(Token):
|
||||
"""Local authentication token model"""
|
||||
pass
|
||||
|
||||
class GoogleToken(Token):
|
||||
"""Google OAuth token model"""
|
||||
pass
|
||||
|
||||
class MsftToken(Token):
|
||||
"""Microsoft OAuth token model"""
|
||||
pass
|
||||
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""
|
||||
Token models and management for external authentication services.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
class GoogleToken(BaseModel):
|
||||
"""Google OAuth token model"""
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_at: float
|
||||
refresh_token: Optional[str] = None
|
||||
|
||||
class MsftToken(BaseModel):
|
||||
"""Microsoft OAuth token model"""
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_at: float
|
||||
refresh_token: Optional[str] = None
|
||||
|
||||
class LocalToken(BaseModel):
|
||||
"""Local authentication token model"""
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_at: float
|
||||
|
|
@ -92,6 +92,9 @@ class ChatInterface:
|
|||
# Initialize access control with user context
|
||||
self.access = ChatAccess(self.currentUser, self.db) # Convert to dict only when needed
|
||||
|
||||
# Update database context
|
||||
self.db.updateContext(self.userId)
|
||||
|
||||
logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
|
||||
|
||||
def _initializeDatabase(self):
|
||||
|
|
@ -176,7 +179,17 @@ class ChatInterface:
|
|||
|
||||
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)
|
||||
# First apply access control
|
||||
filteredRecords = self.access.uam(table, recordset)
|
||||
|
||||
# Then filter out database-specific fields
|
||||
cleanedRecords = []
|
||||
for record in filteredRecords:
|
||||
# Create a new dict with only non-database fields
|
||||
cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
|
||||
cleanedRecords.append(cleanedRecord)
|
||||
|
||||
return cleanedRecords
|
||||
|
||||
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
|
||||
"""Delegate to access control module."""
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ from typing import List, Dict, Any, Optional
|
|||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from modules.shared.attributeUtils import Label, BaseModelWithUI
|
||||
|
||||
from modules.shared.attributeUtils import register_model_labels, ModelMixin
|
||||
|
||||
# WORKFLOW MODELS
|
||||
|
||||
class ChatContent(BaseModelWithUI):
|
||||
class ChatContent(BaseModel, ModelMixin):
|
||||
"""Data model for chat content"""
|
||||
sequenceNr: int = Field(description="Sequence number of the content")
|
||||
name: str = Field(description="Name of the content")
|
||||
|
|
@ -20,18 +19,45 @@ class ChatContent(BaseModelWithUI):
|
|||
mimeType: str = Field(description="MIME type of the content")
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
||||
|
||||
class ChatDocument(BaseModelWithUI):
|
||||
# Register labels for ChatContent
|
||||
register_model_labels(
|
||||
"ChatContent",
|
||||
{"en": "Chat Content", "fr": "Contenu de chat"},
|
||||
{
|
||||
"sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
|
||||
"name": {"en": "Name", "fr": "Nom"},
|
||||
"data": {"en": "Data", "fr": "Données"},
|
||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
||||
"metadata": {"en": "Metadata", "fr": "Métadonnées"}
|
||||
}
|
||||
)
|
||||
|
||||
class ChatDocument(BaseModel, ModelMixin):
|
||||
"""Data model for a chat document"""
|
||||
id: str = Field(description="Primary key")
|
||||
fileId: int = Field(description="Foreign key to file")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
fileId: str = Field(description="Foreign key to file")
|
||||
filename: str = Field(description="Name of the file")
|
||||
fileSize: int = Field(description="Size of the file")
|
||||
mimeType: str = Field(description="MIME type of the file")
|
||||
contents: List[ChatContent] = Field(default_factory=list, description="List of chat contents")
|
||||
|
||||
class ChatStat(BaseModelWithUI):
|
||||
# Register labels for ChatDocument
|
||||
register_model_labels(
|
||||
"ChatDocument",
|
||||
{"en": "Chat Document", "fr": "Document de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"fileId": {"en": "File ID", "fr": "ID du fichier"},
|
||||
"filename": {"en": "Filename", "fr": "Nom de fichier"},
|
||||
"fileSize": {"en": "File Size", "fr": "Taille du fichier"},
|
||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
||||
"contents": {"en": "Contents", "fr": "Contenus"}
|
||||
}
|
||||
)
|
||||
|
||||
class ChatStat(BaseModel, ModelMixin):
|
||||
"""Data model for chat statistics"""
|
||||
id: str = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
|
||||
tokenCount: Optional[int] = Field(None, description="Number of tokens processed")
|
||||
bytesSent: Optional[int] = Field(None, description="Number of bytes sent")
|
||||
|
|
@ -39,9 +65,24 @@ class ChatStat(BaseModelWithUI):
|
|||
successRate: Optional[float] = Field(None, description="Success rate of operations")
|
||||
errorCount: Optional[int] = Field(None, description="Number of errors encountered")
|
||||
|
||||
class ChatLog(BaseModelWithUI):
|
||||
# Register labels for ChatStat
|
||||
register_model_labels(
|
||||
"ChatStat",
|
||||
{"en": "Chat Statistics", "fr": "Statistiques de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
|
||||
"tokenCount": {"en": "Token Count", "fr": "Nombre de tokens"},
|
||||
"bytesSent": {"en": "Bytes Sent", "fr": "Octets envoyés"},
|
||||
"bytesReceived": {"en": "Bytes Received", "fr": "Octets reçus"},
|
||||
"successRate": {"en": "Success Rate", "fr": "Taux de succès"},
|
||||
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"}
|
||||
}
|
||||
)
|
||||
|
||||
class ChatLog(BaseModel, ModelMixin):
|
||||
"""Data model for a chat log"""
|
||||
id: str = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
workflowId: str = Field(description="Foreign key to workflow")
|
||||
message: str = Field(description="Log message")
|
||||
type: str = Field(description="Type of log entry")
|
||||
|
|
@ -51,9 +92,26 @@ class ChatLog(BaseModelWithUI):
|
|||
progress: Optional[int] = Field(None, description="Progress percentage")
|
||||
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
|
||||
|
||||
class ChatMessage(BaseModelWithUI):
|
||||
# Register labels for ChatLog
|
||||
register_model_labels(
|
||||
"ChatLog",
|
||||
{"en": "Chat Log", "fr": "Journal de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||
"message": {"en": "Message", "fr": "Message"},
|
||||
"type": {"en": "Type", "fr": "Type"},
|
||||
"timestamp": {"en": "Timestamp", "fr": "Horodatage"},
|
||||
"agentName": {"en": "Agent Name", "fr": "Nom de l'agent"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"progress": {"en": "Progress", "fr": "Progression"},
|
||||
"performance": {"en": "Performance", "fr": "Performance"}
|
||||
}
|
||||
)
|
||||
|
||||
class ChatMessage(BaseModel, ModelMixin):
|
||||
"""Data model for a chat message"""
|
||||
id: str = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
workflowId: str = Field(description="Foreign key to workflow")
|
||||
parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading")
|
||||
agentName: Optional[str] = Field(None, description="Name of the agent")
|
||||
|
|
@ -67,9 +125,30 @@ class ChatMessage(BaseModelWithUI):
|
|||
stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
|
||||
success: Optional[bool] = Field(None, description="Whether the message processing was successful")
|
||||
|
||||
class Task(BaseModelWithUI):
|
||||
# Register labels for ChatMessage
|
||||
register_model_labels(
|
||||
"ChatMessage",
|
||||
{"en": "Chat Message", "fr": "Message de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||
"parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
|
||||
"agentName": {"en": "Agent Name", "fr": "Nom de l'agent"},
|
||||
"documents": {"en": "Documents", "fr": "Documents"},
|
||||
"message": {"en": "Message", "fr": "Message"},
|
||||
"role": {"en": "Role", "fr": "Rôle"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
|
||||
"startedAt": {"en": "Started At", "fr": "Démarré le"},
|
||||
"finishedAt": {"en": "Finished At", "fr": "Terminé le"},
|
||||
"stats": {"en": "Statistics", "fr": "Statistiques"},
|
||||
"success": {"en": "Success", "fr": "Succès"}
|
||||
}
|
||||
)
|
||||
|
||||
class Task(BaseModel, ModelMixin):
|
||||
"""Data model for a task"""
|
||||
id: str = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
workflowId: str = Field(description="Foreign key to workflow")
|
||||
agentName: str = Field(description="Name of the agent assigned to this task")
|
||||
status: str = Field(description="Current status of the task")
|
||||
|
|
@ -84,9 +163,31 @@ class Task(BaseModelWithUI):
|
|||
finishedAt: Optional[str] = Field(None, description="When the task finished")
|
||||
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
|
||||
|
||||
class ChatWorkflow(BaseModelWithUI):
|
||||
# Register labels for Task
|
||||
register_model_labels(
|
||||
"Task",
|
||||
{"en": "Task", "fr": "Tâche"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||
"agentName": {"en": "Agent Name", "fr": "Nom de l'agent"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"progress": {"en": "Progress", "fr": "Progression"},
|
||||
"prompt": {"en": "Prompt", "fr": "Invite"},
|
||||
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
|
||||
"filesInput": {"en": "Input Files", "fr": "Fichiers d'entrée"},
|
||||
"filesOutput": {"en": "Output Files", "fr": "Fichiers de sortie"},
|
||||
"result": {"en": "Result", "fr": "Résultat"},
|
||||
"error": {"en": "Error", "fr": "Erreur"},
|
||||
"startedAt": {"en": "Started At", "fr": "Démarré le"},
|
||||
"finishedAt": {"en": "Finished At", "fr": "Terminé le"},
|
||||
"performance": {"en": "Performance", "fr": "Performance"}
|
||||
}
|
||||
)
|
||||
|
||||
class ChatWorkflow(BaseModel, ModelMixin):
|
||||
"""Data model for a chat workflow"""
|
||||
id: str = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
mandateId: str = Field(description="ID of the mandate this workflow belongs to")
|
||||
status: str = Field(description="Current status of the workflow")
|
||||
name: Optional[str] = Field(None, description="Name of the workflow")
|
||||
|
|
@ -98,56 +199,104 @@ class ChatWorkflow(BaseModelWithUI):
|
|||
stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
|
||||
tasks: List[Task] = Field(default_factory=list, description="List of tasks in the workflow")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Flux de travail de chat"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
|
||||
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
|
||||
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
|
||||
"currentRound": Label(default="Current Round", translations={"en": "Current Round", "fr": "Tour actuel"}),
|
||||
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
|
||||
"startedAt": Label(default="Started At", translations={"en": "Started At", "fr": "Démarré le"}),
|
||||
"logs": Label(default="Logs", translations={"en": "Logs", "fr": "Journaux"}),
|
||||
"messages": Label(default="Messages", translations={"en": "Messages", "fr": "Messages"}),
|
||||
"stats": Label(default="Statistics", translations={"en": "Statistics", "fr": "Statistiques"}),
|
||||
"tasks": Label(default="Tasks", translations={"en": "Tasks", "fr": "Tâches"})
|
||||
# Register labels for ChatWorkflow
|
||||
register_model_labels(
|
||||
"ChatWorkflow",
|
||||
{"en": "Chat Workflow", "fr": "Flux de travail de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"name": {"en": "Name", "fr": "Nom"},
|
||||
"currentRound": {"en": "Current Round", "fr": "Tour actuel"},
|
||||
"lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
|
||||
"startedAt": {"en": "Started At", "fr": "Démarré le"},
|
||||
"logs": {"en": "Logs", "fr": "Journaux"},
|
||||
"messages": {"en": "Messages", "fr": "Messages"},
|
||||
"stats": {"en": "Statistics", "fr": "Statistiques"},
|
||||
"tasks": {"en": "Tasks", "fr": "Tâches"}
|
||||
}
|
||||
)
|
||||
|
||||
# AGENT AND TASK MODELS
|
||||
|
||||
class Agent(BaseModelWithUI):
|
||||
class Agent(BaseModel, ModelMixin):
|
||||
"""Data model for an agent"""
|
||||
id: str = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
name: str = Field(description="Name of the agent")
|
||||
description: str = Field(description="Description of the agent")
|
||||
capabilities: List[str] = Field(default_factory=list, description="List of agent capabilities")
|
||||
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
|
||||
|
||||
class AgentResponse(BaseModelWithUI):
|
||||
# Register labels for Agent
|
||||
register_model_labels(
|
||||
"Agent",
|
||||
{"en": "Agent", "fr": "Agent"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"name": {"en": "Name", "fr": "Nom"},
|
||||
"description": {"en": "Description", "fr": "Description"},
|
||||
"capabilities": {"en": "Capabilities", "fr": "Capacités"},
|
||||
"performance": {"en": "Performance", "fr": "Performance"}
|
||||
}
|
||||
)
|
||||
|
||||
class AgentResponse(BaseModel, ModelMixin):
|
||||
"""Data model for an agent response"""
|
||||
success: bool = Field(description="Whether the agent execution was successful")
|
||||
message: ChatMessage = Field(description="Response message from the agent")
|
||||
performance: Dict[str, Any] = Field(default_factory=dict, description="Performance metrics")
|
||||
progress: float = Field(description="Task progress (0-100)")
|
||||
|
||||
class TaskPlan(BaseModelWithUI):
|
||||
# Register labels for AgentResponse
|
||||
register_model_labels(
|
||||
"AgentResponse",
|
||||
{"en": "Agent Response", "fr": "Réponse de l'agent"},
|
||||
{
|
||||
"success": {"en": "Success", "fr": "Succès"},
|
||||
"message": {"en": "Message", "fr": "Message"},
|
||||
"performance": {"en": "Performance", "fr": "Performance"},
|
||||
"progress": {"en": "Progress", "fr": "Progression"}
|
||||
}
|
||||
)
|
||||
|
||||
class TaskPlan(BaseModel, ModelMixin):
|
||||
"""Data model for a task plan"""
|
||||
fileList: List[str] = Field(default_factory=list, description="List of files")
|
||||
tasks: List[Task] = Field(default_factory=list, description="List of tasks in the plan")
|
||||
userLanguage: str = Field(description="User's preferred language")
|
||||
userResponse: str = Field(description="User's response or feedback")
|
||||
|
||||
class UserInputRequest(BaseModelWithUI):
|
||||
# Register labels for TaskPlan
|
||||
register_model_labels(
|
||||
"TaskPlan",
|
||||
{"en": "Task Plan", "fr": "Plan de tâches"},
|
||||
{
|
||||
"fileList": {"en": "File List", "fr": "Liste de fichiers"},
|
||||
"tasks": {"en": "Tasks", "fr": "Tâches"},
|
||||
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
|
||||
"userResponse": {"en": "User Response", "fr": "Réponse de l'utilisateur"}
|
||||
}
|
||||
)
|
||||
|
||||
class UserInputRequest(BaseModel, ModelMixin):
|
||||
"""Data model for a user input request"""
|
||||
prompt: str = Field(description="Prompt for the user")
|
||||
listFileId: List[int] = Field(default_factory=list, description="List of file IDs")
|
||||
userLanguage: str = Field(default="en", description="User's preferred language")
|
||||
|
||||
class AgentProfile(BaseModel):
|
||||
# Register labels for UserInputRequest
|
||||
register_model_labels(
|
||||
"UserInputRequest",
|
||||
{"en": "User Input Request", "fr": "Demande de saisie utilisateur"},
|
||||
{
|
||||
"prompt": {"en": "Prompt", "fr": "Invite"},
|
||||
"listFileId": {"en": "File IDs", "fr": "IDs des fichiers"},
|
||||
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"}
|
||||
}
|
||||
)
|
||||
|
||||
class AgentProfile(BaseModel, ModelMixin):
|
||||
"""Model for agent profile information."""
|
||||
id: str
|
||||
name: str
|
||||
|
|
@ -156,3 +305,18 @@ class AgentProfile(BaseModel):
|
|||
isAvailable: bool = True
|
||||
lastActive: Optional[datetime] = None
|
||||
stats: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Register labels for AgentProfile
|
||||
register_model_labels(
|
||||
"AgentProfile",
|
||||
{"en": "Agent Profile", "fr": "Profil de l'agent"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"name": {"en": "Name", "fr": "Nom"},
|
||||
"description": {"en": "Description", "fr": "Description"},
|
||||
"capabilities": {"en": "Capabilities", "fr": "Capacités"},
|
||||
"isAvailable": {"en": "Available", "fr": "Disponible"},
|
||||
"lastActive": {"en": "Last Active", "fr": "Dernière activité"},
|
||||
"stats": {"en": "Statistics", "fr": "Statistiques"}
|
||||
}
|
||||
)
|
||||
|
|
@ -24,6 +24,25 @@ class ManagementAccess:
|
|||
self.privilege = currentUser.privilege
|
||||
self.db = db
|
||||
|
||||
def canModifyAttribute(self, table: str, attribute: str) -> bool:
|
||||
"""
|
||||
Checks if the current user can modify a specific attribute in a table.
|
||||
|
||||
Args:
|
||||
table: Name of the table
|
||||
attribute: Name of the attribute
|
||||
|
||||
Returns:
|
||||
Boolean indicating permission
|
||||
"""
|
||||
userPrivilege = self.privilege
|
||||
|
||||
# Special case for mandateId in prompts table
|
||||
if table == "prompts" and attribute == "mandateId":
|
||||
return userPrivilege == "sysadmin"
|
||||
|
||||
return True
|
||||
|
||||
def uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Unified user access management function that filters data based on user privileges
|
||||
|
|
@ -37,7 +56,7 @@ class ManagementAccess:
|
|||
Filtered recordset with access control attributes
|
||||
"""
|
||||
userPrivilege = self.privilege
|
||||
logger.debug(f"User privilege: {userPrivilege}, username: {self.currentUser.username}, email: {self.currentUser.email}")
|
||||
|
||||
filtered_records = []
|
||||
|
||||
# Apply filtering based on privilege
|
||||
|
|
@ -45,7 +64,7 @@ class ManagementAccess:
|
|||
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
|
||||
# For prompts, users can see all prompts from their mandate
|
||||
if table == "prompts":
|
||||
|
|
@ -53,7 +72,7 @@ class ManagementAccess:
|
|||
else:
|
||||
# Users see only their records for other tables
|
||||
filtered_records = [r for r in recordset
|
||||
if r.get("mandateId","-") == self.mandateId and r.get("_createdBy") == self.userId]
|
||||
if r.get("mandateId") == self.mandateId and r.get("_createdBy") == self.userId]
|
||||
|
||||
# Add access control attributes to each record
|
||||
for record in filtered_records:
|
||||
|
|
@ -64,6 +83,10 @@ class ManagementAccess:
|
|||
record["_hideView"] = False # Everyone can view
|
||||
record["_hideEdit"] = not self.canModify("prompts", record_id)
|
||||
record["_hideDelete"] = not self.canModify("prompts", record_id)
|
||||
|
||||
# Add attribute-level permissions for mandateId
|
||||
if "mandateId" in record:
|
||||
record["_hideEdit_mandateId"] = not self.canModifyAttribute("prompts", "mandateId")
|
||||
elif table == "files":
|
||||
record["_hideView"] = False # Everyone can view
|
||||
record["_hideEdit"] = not self.canModify("files", record_id)
|
||||
|
|
|
|||
|
|
@ -58,18 +58,18 @@ class ServiceManagement:
|
|||
|
||||
def __init__(self):
|
||||
"""Initializes the Management Interface."""
|
||||
# Initialize variables first
|
||||
self.currentUser: Optional[User] = None
|
||||
self.userId: Optional[str] = None
|
||||
self.access: Optional[ManagementAccess] = None # Will be set when user context is provided
|
||||
self.aiService: Optional[ChatService] = None # Will be set when user context is provided
|
||||
|
||||
# Initialize database
|
||||
self._initializeDatabase()
|
||||
|
||||
# Initialize standard records if needed
|
||||
self._initRecords()
|
||||
|
||||
# Initialize variables
|
||||
self.currentUser: Optional[User] = None
|
||||
self.userId: Optional[str] = None
|
||||
self.access: Optional[ManagementAccess] = None # Will be set when user context is provided
|
||||
self.aiService: Optional[ChatService] = None # Will be set when user context is provided
|
||||
|
||||
def setUserContext(self, currentUser: User):
|
||||
"""Sets the user context for the interface."""
|
||||
if not currentUser:
|
||||
|
|
@ -91,6 +91,9 @@ class ServiceManagement:
|
|||
# Initialize AI service
|
||||
self.aiService = ChatService()
|
||||
|
||||
# Update database context
|
||||
self.db.updateContext(self.userId)
|
||||
|
||||
logger.debug(f"User context set: userId={self.userId}")
|
||||
|
||||
def _initializeDatabase(self):
|
||||
|
|
@ -128,54 +131,112 @@ class ServiceManagement:
|
|||
logger.info("Standard records initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize standard records: {str(e)}")
|
||||
raise
|
||||
# Don't raise the error, just log it
|
||||
# This allows the interface to be created even if initialization fails
|
||||
|
||||
def _initializeStandardPrompts(self):
|
||||
"""Creates standard prompts if they don't exist."""
|
||||
prompts = self.db.getRecordset("prompts")
|
||||
logger.debug(f"Found {len(prompts)} existing prompts")
|
||||
"""Initializes standard prompts if they don't exist yet."""
|
||||
try:
|
||||
# Check if any prompts exist
|
||||
existingPrompts = self.db.getRecordset("prompts")
|
||||
if existingPrompts:
|
||||
logger.info("Prompts already exist, skipping initialization")
|
||||
return
|
||||
|
||||
if not prompts:
|
||||
logger.debug("Creating standard prompts")
|
||||
# Get the root interface to access the initial mandate ID
|
||||
from modules.interfaces.serviceAppClass import getRootInterface
|
||||
rootInterface = getRootInterface()
|
||||
|
||||
# Get initial mandate ID through the root interface
|
||||
mandateId = rootInterface.getInitialId("mandates")
|
||||
if not mandateId:
|
||||
logger.error("No initial mandate ID found")
|
||||
return
|
||||
|
||||
# Get root user for initialization
|
||||
rootUser = rootInterface.getUserByUsername("admin")
|
||||
if not rootUser:
|
||||
logger.error("Root user not found for initialization")
|
||||
return
|
||||
|
||||
# Store current user context if it exists
|
||||
currentUser = self.currentUser
|
||||
|
||||
# Set user context to root user for initialization
|
||||
self.setUserContext(rootUser)
|
||||
|
||||
# Define standard prompts
|
||||
standardPrompts = [
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"content": "Gib mir die ersten 1000 Primzahlen",
|
||||
"name": "Code: Primzahlen"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
Prompt(
|
||||
name="Market Research",
|
||||
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.",
|
||||
mandateId=mandateId
|
||||
),
|
||||
Prompt(
|
||||
name="Data Analysis",
|
||||
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.",
|
||||
mandateId=mandateId
|
||||
),
|
||||
Prompt(
|
||||
name="Meeting Protocol",
|
||||
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.",
|
||||
mandateId=mandateId
|
||||
),
|
||||
Prompt(
|
||||
name="UI/UX Design",
|
||||
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.",
|
||||
mandateId=mandateId
|
||||
),
|
||||
Prompt(
|
||||
name="Primzahlen",
|
||||
content="Gib mir die ersten 1000 Primzahlen.",
|
||||
mandateId=mandateId
|
||||
),
|
||||
Prompt(
|
||||
name="E-Mail",
|
||||
content="Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
|
||||
mandateId=mandateId
|
||||
)
|
||||
]
|
||||
|
||||
# Create prompts
|
||||
for promptData in standardPrompts:
|
||||
createdPrompt = self.db.recordCreate("prompts", promptData)
|
||||
logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('mandateId')}, user={createdPrompt.get('_createdBy')}")
|
||||
for prompt in standardPrompts:
|
||||
self.db.recordCreate("prompts", prompt.to_dict())
|
||||
logger.info(f"Created standard prompt: {prompt.name}")
|
||||
|
||||
# Restore original user context if it existed
|
||||
if currentUser:
|
||||
self.setUserContext(currentUser)
|
||||
else:
|
||||
logger.debug("Prompts already exist, skipping creation")
|
||||
self.currentUser = None
|
||||
self.userId = None
|
||||
self.access = None
|
||||
self.db.updateContext("") # Reset database context
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing standard prompts: {str(e)}")
|
||||
# Ensure we restore user context even if there's an error
|
||||
if 'currentUser' in locals() and currentUser:
|
||||
self.setUserContext(currentUser)
|
||||
else:
|
||||
self.currentUser = None
|
||||
self.userId = None
|
||||
self.access = None
|
||||
self.db.updateContext("") # Reset database context
|
||||
|
||||
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)
|
||||
# First apply access control
|
||||
filteredRecords = self.access.uam(table, recordset)
|
||||
|
||||
# Then filter out database-specific fields
|
||||
cleanedRecords = []
|
||||
for record in filteredRecords:
|
||||
# Create a new dict with only non-database fields
|
||||
cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
|
||||
cleanedRecords.append(cleanedRecord)
|
||||
|
||||
return cleanedRecords
|
||||
|
||||
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
|
||||
"""Delegate to access control module."""
|
||||
|
|
@ -236,10 +297,17 @@ class ServiceManagement:
|
|||
|
||||
def getAllPrompts(self) -> List[Prompt]:
|
||||
"""Returns prompts based on user access level."""
|
||||
try:
|
||||
allPrompts = self.db.getRecordset("prompts")
|
||||
filteredPrompts = self._uam("prompts", allPrompts)
|
||||
|
||||
# Convert to Prompt objects
|
||||
return [Prompt.from_dict(prompt) for prompt in filteredPrompts]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting prompts: {str(e)}")
|
||||
return []
|
||||
|
||||
def getPrompt(self, promptId: str) -> Optional[Prompt]:
|
||||
"""Returns a prompt by ID if user has access."""
|
||||
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
|
||||
|
|
@ -269,20 +337,15 @@ class ServiceManagement:
|
|||
if not prompt:
|
||||
raise ValueError(f"Prompt {promptId} not found")
|
||||
|
||||
# Update prompt data using model
|
||||
updatedData = prompt.to_dict()
|
||||
updatedData.update(updateData)
|
||||
updatedPrompt = Prompt.from_dict(updatedData)
|
||||
|
||||
# Update prompt record
|
||||
self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
|
||||
# Update prompt record directly with the update data
|
||||
self.db.recordModify("prompts", promptId, updateData)
|
||||
|
||||
# Get updated prompt
|
||||
updatedPrompt = self.getPrompt(promptId)
|
||||
if not updatedPrompt:
|
||||
raise ValueError("Failed to retrieve updated prompt")
|
||||
|
||||
return updatedPrompt
|
||||
return updatedPrompt.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating prompt: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ from typing import List, Dict, Any, Optional
|
|||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from modules.shared.attributeUtils import Label, BaseModelWithUI
|
||||
# Import for label registration
|
||||
from modules.shared.attributeUtils import register_model_labels, ModelMixin
|
||||
|
||||
# CORE MODELS
|
||||
|
||||
class FileItem(BaseModelWithUI):
|
||||
class FileItem(BaseModel, ModelMixin):
|
||||
"""Data model for a file item"""
|
||||
id: int = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
mandateId: str = Field(description="ID of the mandate this file belongs to")
|
||||
filename: str = Field(description="Name of the file")
|
||||
mimeType: str = Field(description="MIME type of the file")
|
||||
|
|
@ -22,54 +23,54 @@ class FileItem(BaseModelWithUI):
|
|||
fileHash: str = Field(description="Hash of the file")
|
||||
fileSize: int = Field(description="Size of the file in bytes")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="File Item", translations={"en": "File Item", "fr": "Élément de fichier"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
|
||||
"filename": Label(default="Filename", translations={"en": "Filename", "fr": "Nom de fichier"}),
|
||||
"mimeType": Label(default="MIME Type", translations={"en": "MIME Type", "fr": "Type MIME"}),
|
||||
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du flux de travail"}),
|
||||
"fileHash": Label(default="File Hash", translations={"en": "File Hash", "fr": "Hash du fichier"}),
|
||||
"fileSize": Label(default="File Size", translations={"en": "File Size", "fr": "Taille du fichier"})
|
||||
# Register labels for FileItem
|
||||
register_model_labels(
|
||||
"FileItem",
|
||||
{"en": "File Item", "fr": "Élément de fichier"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"filename": {"en": "Filename", "fr": "Nom de fichier"},
|
||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
|
||||
"fileSize": {"en": "File Size", "fr": "Taille du fichier"}
|
||||
}
|
||||
)
|
||||
|
||||
class FileData(BaseModelWithUI):
|
||||
class FileData(BaseModel, ModelMixin):
|
||||
"""Data model for file data"""
|
||||
id: int = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
data: str = Field(description="File data content")
|
||||
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="File Data", translations={"en": "File Data", "fr": "Données de fichier"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"data": Label(default="Data", translations={"en": "Data", "fr": "Données"}),
|
||||
"base64Encoded": Label(default="Base64 Encoded", translations={"en": "Base64 Encoded", "fr": "Encodé en Base64"})
|
||||
# Register labels for FileData
|
||||
register_model_labels(
|
||||
"FileData",
|
||||
{"en": "File Data", "fr": "Données de fichier"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"data": {"en": "Data", "fr": "Données"},
|
||||
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"}
|
||||
}
|
||||
)
|
||||
|
||||
class Prompt(BaseModelWithUI):
|
||||
class Prompt(BaseModel, ModelMixin):
|
||||
"""Data model for a prompt"""
|
||||
id: int = Field(description="Primary key")
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
||||
mandateId: str = Field(description="ID of the mandate this prompt belongs to")
|
||||
content: str = Field(description="Content of the prompt")
|
||||
name: str = Field(description="Name of the prompt")
|
||||
|
||||
label: Label = Field(
|
||||
default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
|
||||
description="Label for the class"
|
||||
)
|
||||
|
||||
fieldLabels: Dict[str, Label] = {
|
||||
"id": Label(default="ID", translations={}),
|
||||
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
|
||||
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
|
||||
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"})
|
||||
# Register labels for Prompt
|
||||
register_model_labels(
|
||||
"Prompt",
|
||||
{"en": "Prompt", "fr": "Invite"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"content": {"en": "Content", "fr": "Contenu"},
|
||||
"name": {"en": "Name", "fr": "Nom"}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,35 +11,12 @@ import logging
|
|||
from modules.security.auth import limiter, getCurrentUser
|
||||
|
||||
# Import the attribute definition and helper functions
|
||||
from modules.interfaces.serviceAppModel import AttributeDefinition, User
|
||||
from modules.shared.attributeUtils import getModelClasses
|
||||
from modules.interfaces.serviceAppModel import User
|
||||
from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create a response model for better documentation
|
||||
class AttributeResponse(BaseModel):
|
||||
"""Response model for entity attributes"""
|
||||
attributes: List[AttributeDefinition]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"attributes": [
|
||||
{
|
||||
"name": "username",
|
||||
"label": "Username",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"placeholder": "Please enter username",
|
||||
"editable": True,
|
||||
"visible": True,
|
||||
"order": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Create a router for the attribute endpoints
|
||||
router = APIRouter(
|
||||
prefix="/api/attributes",
|
||||
|
|
@ -75,11 +52,11 @@ async def get_entity_attributes(
|
|||
|
||||
# Get model class and derive attributes from it
|
||||
modelClass = modelClasses[entityType]
|
||||
attributes = modelClass.getModelAttributeDefinitions()
|
||||
attribute_defs = getModelAttributeDefinitions(modelClass)
|
||||
|
||||
# Convert dictionary attributes to AttributeDefinition objects
|
||||
attribute_definitions = []
|
||||
for attr in attributes:
|
||||
for attr in attribute_defs["attributes"]:
|
||||
if isinstance(attr, dict) and attr.get('visible', True):
|
||||
attribute_definitions.append(AttributeDefinition(**attr))
|
||||
elif hasattr(attr, 'visible') and attr.visible:
|
||||
|
|
|
|||
164
modules/routes/routeDataConnections.py
Normal file
164
modules/routes/routeDataConnections.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""
|
||||
Connection routes for the backend API.
|
||||
Implements the endpoints for connection management.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import status
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus
|
||||
from modules.security.auth import getCurrentUser, limiter
|
||||
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/connections",
|
||||
tags=["Manage Connections"],
|
||||
responses={404: {"description": "Not found"}}
|
||||
)
|
||||
|
||||
@router.get("/", response_model=List[UserConnection])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_connections(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[UserConnection]:
|
||||
"""Get all connections for the current user or all connections if admin"""
|
||||
try:
|
||||
interface = getInterface(currentUser)
|
||||
if currentUser.privilege in ['admin', 'sysadmin']:
|
||||
# Admins can see all connections
|
||||
users = interface.getAllUsers()
|
||||
connections = []
|
||||
for user in users:
|
||||
connections.extend(user.connections)
|
||||
return connections
|
||||
else:
|
||||
# Regular users can only see their own connections
|
||||
return currentUser.connections
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting connections: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get connections: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{connectionId}/connect")
|
||||
@limiter.limit("10/minute")
|
||||
async def connect_service(
|
||||
request: Request,
|
||||
connectionId: str = Path(..., description="The ID of the connection to connect"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Connect to an external service"""
|
||||
try:
|
||||
interface = getInterface(currentUser)
|
||||
|
||||
# Find the connection
|
||||
connection = None
|
||||
if currentUser.privilege in ['admin', 'sysadmin']:
|
||||
# Admins can connect any connection
|
||||
users = interface.getAllUsers()
|
||||
for user in users:
|
||||
for conn in user.connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
if connection:
|
||||
break
|
||||
else:
|
||||
# Regular users can only connect their own connections
|
||||
for conn in currentUser.connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
)
|
||||
|
||||
# Initiate OAuth flow with state=connect
|
||||
auth_url = None
|
||||
if connection.authority == AuthAuthority.MSFT:
|
||||
auth_url = f"/api/msft/login?state=connect&connectionId={connectionId}"
|
||||
elif connection.authority == AuthAuthority.GOOGLE:
|
||||
auth_url = f"/api/google/login?state=connect&connectionId={connectionId}"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Unsupported authority: {connection.authority}"
|
||||
)
|
||||
|
||||
return {"authUrl": auth_url}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting service: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to connect service: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{connectionId}/disconnect")
|
||||
@limiter.limit("10/minute")
|
||||
async def disconnect_service(
|
||||
request: Request,
|
||||
connectionId: str = Path(..., description="The ID of the connection to disconnect"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Disconnect from an external service"""
|
||||
try:
|
||||
interface = getInterface(currentUser)
|
||||
|
||||
# Find the connection
|
||||
connection = None
|
||||
if currentUser.privilege in ['admin', 'sysadmin']:
|
||||
# Admins can disconnect any connection
|
||||
users = interface.getAllUsers()
|
||||
for user in users:
|
||||
for conn in user.connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
if connection:
|
||||
break
|
||||
else:
|
||||
# Regular users can only disconnect their own connections
|
||||
for conn in currentUser.connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
)
|
||||
|
||||
# Update connection status
|
||||
connection.status = ConnectionStatus.INACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
|
||||
# Update user record
|
||||
interface.db.recordModify("users", connection.userId, {
|
||||
"connections": [c.to_dict() for c in currentUser.connections]
|
||||
})
|
||||
|
||||
return {"message": "Service disconnected successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting service: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to disconnect service: {str(e)}"
|
||||
)
|
||||
|
|
@ -5,6 +5,10 @@ import logging
|
|||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass
|
||||
import io
|
||||
import inspect
|
||||
import importlib
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import auth module
|
||||
from modules.security.auth import limiter, getCurrentUser
|
||||
|
|
@ -12,8 +16,8 @@ from modules.security.auth import limiter, getCurrentUser
|
|||
# Import interfaces
|
||||
import modules.interfaces.serviceManagementClass as serviceManagementClass
|
||||
from modules.interfaces.serviceManagementModel import FileItem
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||
from modules.interfaces.serviceAppModel import AttributeDefinition, User
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
|
||||
from modules.interfaces.serviceAppModel import User
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -199,50 +203,33 @@ async def update_file(
|
|||
detail=str(e)
|
||||
)
|
||||
|
||||
@router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete("/{fileId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def delete_file(
|
||||
request: Request,
|
||||
fileId: str,
|
||||
fileId: str = Path(..., description="ID of the file to delete"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> JSONResponse:
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a file"""
|
||||
try:
|
||||
managementInterface = serviceManagementClass.getInterface(currentUser)
|
||||
|
||||
# Delete file via LucyDOM interface
|
||||
managementInterface.deleteFile(fileId)
|
||||
|
||||
# Return successful deletion without content (204 No Content)
|
||||
return JSONResponse({
|
||||
"message": "File deleted successfully"
|
||||
})
|
||||
|
||||
except serviceManagementClass.FileNotFoundError as e:
|
||||
logger.warning(f"File not found: {str(e)}")
|
||||
# Check if the file exists
|
||||
existingFile = managementInterface.getFile(fileId)
|
||||
if not existingFile:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
detail=f"File with ID {fileId} not found"
|
||||
)
|
||||
except serviceManagementClass.FilePermissionError as e:
|
||||
logger.warning(f"No permission to delete file: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e)
|
||||
)
|
||||
except serviceManagementClass.FileDeletionError as e:
|
||||
logger.error(f"Error deleting file: {str(e)}")
|
||||
|
||||
success = managementInterface.deleteFile(fileId)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting file: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error deleting file: {str(e)}"
|
||||
detail="Error deleting the file"
|
||||
)
|
||||
|
||||
return {"message": f"File with ID {fileId} successfully deleted"}
|
||||
|
||||
@router.get("/stats", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_file_stats(
|
||||
|
|
@ -281,18 +268,3 @@ async def get_file_stats(
|
|||
detail=f"Error retrieving file statistics: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/attributes", response_model=List[AttributeDefinition])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_file_attributes(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[AttributeDefinition]:
|
||||
"""
|
||||
Retrieves the attribute definitions for files.
|
||||
This can be used for dynamic form generation.
|
||||
|
||||
Returns:
|
||||
- A list of attribute definitions that can be used to generate forms
|
||||
"""
|
||||
# Get attributes from the FileItem model class
|
||||
return FileItem.getModelAttributeDefinitions()
|
||||
|
|
@ -6,17 +6,22 @@ Implements the endpoints for mandate management.
|
|||
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import status
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import inspect
|
||||
import importlib
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import auth module
|
||||
from modules.security.auth import limiter, getCurrentUser
|
||||
|
||||
# Import interfaces
|
||||
import modules.interfaces.serviceManagementClass as serviceManagementClass
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
|
||||
|
||||
# Import the model classes
|
||||
from modules.interfaces.serviceAppModel import AttributeDefinition, Mandate, User
|
||||
from modules.interfaces.serviceAppModel import Mandate, User
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -189,19 +194,3 @@ async def delete_mandate(
|
|||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete mandate: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/attributes", response_model=List[AttributeDefinition])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_mandate_attributes(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[AttributeDefinition]:
|
||||
"""
|
||||
Retrieves the attribute definitions for mandates.
|
||||
This can be used for dynamic form generation.
|
||||
|
||||
Returns:
|
||||
- A list of attribute definitions that can be used to generate forms
|
||||
"""
|
||||
# Get attributes from the Mandate model class
|
||||
return Mandate.getModelAttributeDefinitions()
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
from fastapi import APIRouter, HTTPException, Depends, Body, Query, Path, Request
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Query, Path, Request, Response
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import status
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import inspect
|
||||
import importlib
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import auth module
|
||||
from modules.security.auth import limiter, getCurrentUser
|
||||
|
|
@ -10,7 +14,8 @@ from modules.security.auth import limiter, getCurrentUser
|
|||
# Import interfaces
|
||||
import modules.interfaces.serviceManagementClass as serviceManagementClass
|
||||
from modules.interfaces.serviceManagementModel import Prompt
|
||||
from modules.interfaces.serviceAppModel import AttributeDefinition, User
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
|
||||
from modules.interfaces.serviceAppModel import User
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -31,7 +36,7 @@ async def get_prompts(
|
|||
"""Get all prompts"""
|
||||
managementInterface = serviceManagementClass.getInterface(currentUser)
|
||||
prompts = managementInterface.getAllPrompts()
|
||||
return [Prompt.from_dict(prompt) for prompt in prompts]
|
||||
return prompts
|
||||
|
||||
@router.post("", response_model=Prompt)
|
||||
@limiter.limit("10/minute")
|
||||
|
|
@ -44,13 +49,14 @@ async def create_prompt(
|
|||
managementInterface = serviceManagementClass.getInterface(currentUser)
|
||||
|
||||
# Convert Prompt to dict for interface
|
||||
prompt_data = prompt.to_dict()
|
||||
prompt_data = prompt.dict()
|
||||
|
||||
# Create prompt
|
||||
newPrompt = managementInterface.createPrompt(prompt_data)
|
||||
|
||||
# Set current time for createdAt if it exists in the model
|
||||
if "createdAt" in Prompt.getModelAttributeDefinitions() and hasattr(newPrompt, "createdAt"):
|
||||
promptAttributes = getModelAttributeDefinitions(Prompt)
|
||||
if "createdAt" in promptAttributes["attributes"] and hasattr(newPrompt, "createdAt"):
|
||||
newPrompt["createdAt"] = datetime.now().isoformat()
|
||||
|
||||
return Prompt.from_dict(newPrompt)
|
||||
|
|
@ -95,7 +101,7 @@ async def update_prompt(
|
|||
)
|
||||
|
||||
# Convert Prompt to dict for interface
|
||||
update_data = promptData.to_dict()
|
||||
update_data = promptData.dict()
|
||||
|
||||
# Update prompt
|
||||
updatedPrompt = managementInterface.updatePrompt(promptId, update_data)
|
||||
|
|
@ -134,19 +140,3 @@ async def delete_prompt(
|
|||
)
|
||||
|
||||
return {"message": f"Prompt with ID {promptId} successfully deleted"}
|
||||
|
||||
@router.get("/attributes", response_model=List[AttributeDefinition])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_prompt_attributes(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[AttributeDefinition]:
|
||||
"""
|
||||
Retrieves the attribute definitions for prompts.
|
||||
This can be used for dynamic form generation.
|
||||
|
||||
Returns:
|
||||
- A list of attribute definitions that can be used to generate forms
|
||||
"""
|
||||
# Get attributes from the Prompt model class
|
||||
return Prompt.getModelAttributeDefinitions()
|
||||
|
|
@ -18,8 +18,8 @@ import modules.interfaces.serviceManagementClass as serviceManagementClass
|
|||
from modules.security.auth import getCurrentUser, limiter, getCurrentUser
|
||||
|
||||
# Import the attribute definition and helper functions
|
||||
from modules.interfaces.serviceAppModel import User, AttributeDefinition as ServiceAppAttributeDefinition
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||
from modules.interfaces.serviceAppModel import User, AttributeDefinition
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -93,7 +93,8 @@ async def create_user(
|
|||
newUser = managementInterface.createUser(user_data)
|
||||
|
||||
# Set current time for createdAt if it exists in the model
|
||||
if "createdAt" in User.getModelAttributeDefinitions() and hasattr(newUser, "createdAt"):
|
||||
userAttributes = getModelAttributeDefinitions(User)
|
||||
if "createdAt" in userAttributes["attributes"] and hasattr(newUser, "createdAt"):
|
||||
newUser["createdAt"] = datetime.now().isoformat()
|
||||
|
||||
return User.from_dict(newUser)
|
||||
|
|
@ -139,34 +140,22 @@ async def delete_user(
|
|||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a user"""
|
||||
try:
|
||||
appInterface = serviceManagementClass.getInterface(currentUser)
|
||||
appInterface.deleteUser(userId)
|
||||
return {"message": f"User {userId} deleted successfully"}
|
||||
except ValueError as e:
|
||||
|
||||
# Check if the user exists
|
||||
existingUser = appInterface.getUser(userId)
|
||||
if not existingUser:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User with ID {userId} not found"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting user {userId}: {str(e)}")
|
||||
|
||||
success = appInterface.deleteUser(userId)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete user: {str(e)}"
|
||||
detail="Error deleting the user"
|
||||
)
|
||||
|
||||
@router.get("/attributes", response_model=List[ServiceAppAttributeDefinition])
|
||||
@limiter.limit("30/minute")
|
||||
async def get_user_attributes(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[ServiceAppAttributeDefinition]:
|
||||
"""
|
||||
Retrieves the attribute definitions for users.
|
||||
This can be used for dynamic form generation.
|
||||
return {"message": f"User with ID {userId} successfully deleted"}
|
||||
|
||||
Returns:
|
||||
- A list of attribute definitions that can be used to generate forms
|
||||
"""
|
||||
# Get attributes from the User model class
|
||||
return User.getModelAttributeDefinitions()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Routes for Google authentication.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body, Query
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
import logging
|
||||
import json
|
||||
|
|
@ -11,12 +11,13 @@ from datetime import datetime, timedelta
|
|||
from google.oauth2.credentials import Credentials
|
||||
from google_auth_oauthlib.flow import Flow
|
||||
from google.auth.transport.requests import Request as GoogleRequest
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
|
||||
from modules.interfaces.serviceAppModel import AuthAuthority, User
|
||||
from modules.interfaces.serviceAppTokens import GoogleToken
|
||||
from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
|
||||
from modules.security.auth import getCurrentUser, limiter
|
||||
from modules.shared.attributeUtils import ModelMixin
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -46,7 +47,11 @@ SCOPES = [
|
|||
|
||||
@router.get("/login")
|
||||
@limiter.limit("5/minute")
|
||||
async def login(request: Request) -> RedirectResponse:
|
||||
async def login(
|
||||
request: Request,
|
||||
state: str = Query("login", description="State parameter to distinguish between login and connection flows"),
|
||||
connectionId: Optional[str] = Query(None, description="Connection ID for connection flow")
|
||||
) -> RedirectResponse:
|
||||
"""Initiate Google login"""
|
||||
try:
|
||||
# Create OAuth flow
|
||||
|
|
@ -63,10 +68,14 @@ async def login(request: Request) -> RedirectResponse:
|
|||
scopes=SCOPES
|
||||
)
|
||||
|
||||
# Generate auth URL
|
||||
# Generate auth URL with state
|
||||
auth_url, _ = flow.authorization_url(
|
||||
access_type="offline",
|
||||
include_granted_scopes="true"
|
||||
include_granted_scopes="true",
|
||||
state=json.dumps({
|
||||
"type": state,
|
||||
"connectionId": connectionId
|
||||
})
|
||||
)
|
||||
|
||||
return RedirectResponse(auth_url)
|
||||
|
|
@ -79,9 +88,14 @@ async def login(request: Request) -> RedirectResponse:
|
|||
)
|
||||
|
||||
@router.get("/auth/callback")
|
||||
async def auth_callback(code: str, request: Request) -> HTMLResponse:
|
||||
async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse:
|
||||
"""Handle Google OAuth callback"""
|
||||
try:
|
||||
# Parse state
|
||||
state_data = json.loads(state)
|
||||
state_type = state_data.get("type", "login")
|
||||
connection_id = state_data.get("connectionId")
|
||||
|
||||
# Create OAuth flow
|
||||
flow = Flow.from_client_config(
|
||||
{
|
||||
|
|
@ -93,25 +107,47 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
|
|||
"redirect_uris": [REDIRECT_URI]
|
||||
}
|
||||
},
|
||||
scopes=SCOPES,
|
||||
redirect_uri=REDIRECT_URI
|
||||
scopes=SCOPES
|
||||
)
|
||||
|
||||
# Exchange code for token
|
||||
# Exchange code for credentials
|
||||
flow.fetch_token(code=code)
|
||||
credentials = flow.credentials
|
||||
|
||||
# Create token data
|
||||
token_data = {
|
||||
"access_token": credentials.token,
|
||||
"refresh_token": credentials.refresh_token,
|
||||
"token_type": credentials.token_type,
|
||||
"expires_at": credentials.expiry.timestamp()
|
||||
}
|
||||
# Get user info
|
||||
user_info_response = flow.oauth2session.get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
user_info = user_info_response.json()
|
||||
|
||||
# Save token data
|
||||
appInterface = getInterface()
|
||||
appInterface.saveToken("Google", token_data)
|
||||
if state_type == "login":
|
||||
# Handle login flow
|
||||
rootInterface = getRootInterface()
|
||||
user = rootInterface.getUserByUsername(user_info.get("email"))
|
||||
|
||||
if not user:
|
||||
# Create new user if doesn't exist
|
||||
user = rootInterface.createUser(
|
||||
username=user_info.get("email"),
|
||||
email=user_info.get("email"),
|
||||
fullName=user_info.get("name"),
|
||||
authenticationAuthority=AuthAuthority.GOOGLE,
|
||||
externalId=user_info.get("id"),
|
||||
externalUsername=user_info.get("email"),
|
||||
externalEmail=user_info.get("email")
|
||||
)
|
||||
|
||||
# Create token
|
||||
token = Token(
|
||||
userId=user.id,
|
||||
authority=AuthAuthority.GOOGLE,
|
||||
tokenAccess=credentials.token,
|
||||
tokenRefresh=credentials.refresh_token,
|
||||
tokenType=credentials.token_type,
|
||||
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None
|
||||
)
|
||||
|
||||
# Save token
|
||||
appInterface = getInterface(user)
|
||||
appInterface.saveToken(token)
|
||||
|
||||
# Return success page with token data
|
||||
return HTMLResponse(
|
||||
|
|
@ -124,7 +160,66 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
|
|||
window.opener.postMessage({{
|
||||
type: 'google_auth_success',
|
||||
access_token: {json.dumps(credentials.token)},
|
||||
token_data: {json.dumps(token_data)}
|
||||
token_data: {json.dumps(token.to_dict())}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
# Handle connection flow
|
||||
if not connection_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Connection ID is required for connection flow"
|
||||
)
|
||||
|
||||
# Get current user from session
|
||||
current_user = await getCurrentUser(request)
|
||||
if not current_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not authenticated"
|
||||
)
|
||||
|
||||
# Find and update connection
|
||||
interface = getInterface(current_user)
|
||||
connection = None
|
||||
for conn in current_user.connections:
|
||||
if conn.id == connection_id:
|
||||
connection = conn
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
)
|
||||
|
||||
# Update connection
|
||||
connection.status = ConnectionStatus.ACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
connection.expiresAt = credentials.expiry if credentials.expiry else None
|
||||
|
||||
# Update user record
|
||||
interface.db.recordModify("users", current_user.id, {
|
||||
"connections": [c.to_dict() for c in current_user.connections]
|
||||
})
|
||||
|
||||
# Return success page
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Successful</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_success',
|
||||
connectionId: {json.dumps(connection_id)}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
|
|
@ -154,7 +249,7 @@ async def get_current_user(
|
|||
logger.error(f"Error getting current user: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get current user: {str(e)}"
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
@router.post("/logout")
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ from pydantic import BaseModel
|
|||
# Import auth modules
|
||||
from modules.security.auth import createAccessToken, getCurrentUser, limiter
|
||||
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
|
||||
from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege
|
||||
from modules.interfaces.serviceAppTokens import LocalToken
|
||||
from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token
|
||||
from modules.shared.attributeUtils import ModelMixin
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -94,19 +94,23 @@ async def login(
|
|||
# Get user-specific interface for token operations
|
||||
userInterface = getInterface(user)
|
||||
|
||||
# Save token data
|
||||
token_data = {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"expires_at": expires_at.timestamp()
|
||||
}
|
||||
userInterface.saveToken("Local", token_data)
|
||||
# Create token
|
||||
token = Token(
|
||||
userId=user.id,
|
||||
authority=AuthAuthority.LOCAL,
|
||||
tokenAccess=access_token,
|
||||
tokenType="bearer",
|
||||
expiresAt=expires_at.timestamp()
|
||||
)
|
||||
|
||||
# Save token
|
||||
userInterface.saveToken(token)
|
||||
|
||||
# Create response data
|
||||
response_data = {
|
||||
"type": "local_auth_success",
|
||||
"access_token": access_token,
|
||||
"token_data": token_data
|
||||
"token_data": token.dict()
|
||||
}
|
||||
|
||||
return response_data
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Routes for Microsoft authentication.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body, Query
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
import logging
|
||||
import json
|
||||
|
|
@ -12,9 +12,9 @@ import msal
|
|||
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
|
||||
from modules.interfaces.serviceAppModel import AuthAuthority, User
|
||||
from modules.interfaces.serviceAppTokens import MsftToken
|
||||
from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
|
||||
from modules.security.auth import getCurrentUser, limiter
|
||||
from modules.shared.attributeUtils import ModelMixin
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -42,7 +42,11 @@ SCOPES = ["Mail.ReadWrite", "User.Read"]
|
|||
|
||||
@router.get("/login")
|
||||
@limiter.limit("5/minute")
|
||||
async def login(request: Request) -> RedirectResponse:
|
||||
async def login(
|
||||
request: Request,
|
||||
state: str = Query("login", description="State parameter to distinguish between login and connection flows"),
|
||||
connectionId: Optional[str] = Query(None, description="Connection ID for connection flow")
|
||||
) -> RedirectResponse:
|
||||
"""Initiate Microsoft login"""
|
||||
try:
|
||||
# Create MSAL app
|
||||
|
|
@ -52,10 +56,14 @@ async def login(request: Request) -> RedirectResponse:
|
|||
client_credential=CLIENT_SECRET
|
||||
)
|
||||
|
||||
# Generate auth URL
|
||||
# Generate auth URL with state
|
||||
auth_url = msal_app.get_authorization_request_url(
|
||||
scopes=SCOPES,
|
||||
redirect_uri=REDIRECT_URI
|
||||
redirect_uri=REDIRECT_URI,
|
||||
state=json.dumps({
|
||||
"type": state,
|
||||
"connectionId": connectionId
|
||||
})
|
||||
)
|
||||
|
||||
return RedirectResponse(auth_url)
|
||||
|
|
@ -68,9 +76,14 @@ async def login(request: Request) -> RedirectResponse:
|
|||
)
|
||||
|
||||
@router.get("/auth/callback")
|
||||
async def auth_callback(code: str, request: Request) -> HTMLResponse:
|
||||
async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse:
|
||||
"""Handle Microsoft OAuth callback"""
|
||||
try:
|
||||
# Parse state
|
||||
state_data = json.loads(state)
|
||||
state_type = state_data.get("type", "login")
|
||||
connection_id = state_data.get("connectionId")
|
||||
|
||||
# Create MSAL app
|
||||
msal_app = msal.ConfidentialClientApplication(
|
||||
CLIENT_ID,
|
||||
|
|
@ -91,17 +104,44 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
|
|||
status_code=400
|
||||
)
|
||||
|
||||
# Create token data
|
||||
token_data = {
|
||||
"access_token": token_response["access_token"],
|
||||
"refresh_token": token_response.get("refresh_token", ""),
|
||||
"token_type": token_response.get("token_type", "bearer"),
|
||||
"expires_at": datetime.now().timestamp() + token_response.get("expires_in", 0)
|
||||
}
|
||||
# Get user info from Microsoft
|
||||
user_info = msal_app.acquire_token_for_client(scopes=["User.Read"])
|
||||
if "error" in user_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user info from Microsoft"
|
||||
)
|
||||
|
||||
# Save token data
|
||||
appInterface = getInterface()
|
||||
appInterface.saveToken("Msft", token_data)
|
||||
if state_type == "login":
|
||||
# Handle login flow
|
||||
rootInterface = getRootInterface()
|
||||
user = rootInterface.getUserByUsername(user_info.get("preferred_username"))
|
||||
|
||||
if not user:
|
||||
# Create new user if doesn't exist
|
||||
user = rootInterface.createUser(
|
||||
username=user_info.get("preferred_username"),
|
||||
email=user_info.get("email"),
|
||||
fullName=user_info.get("name"),
|
||||
authenticationAuthority=AuthAuthority.MSFT,
|
||||
externalId=user_info.get("id"),
|
||||
externalUsername=user_info.get("preferred_username"),
|
||||
externalEmail=user_info.get("email")
|
||||
)
|
||||
|
||||
# Create token
|
||||
token = Token(
|
||||
userId=user.id,
|
||||
authority=AuthAuthority.MSFT,
|
||||
tokenAccess=token_response["access_token"],
|
||||
tokenRefresh=token_response.get("refresh_token", ""),
|
||||
tokenType=token_response.get("token_type", "bearer"),
|
||||
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0)
|
||||
)
|
||||
|
||||
# Save token
|
||||
appInterface = getInterface(user)
|
||||
appInterface.saveToken(token)
|
||||
|
||||
# Return success page with token data
|
||||
return HTMLResponse(
|
||||
|
|
@ -114,7 +154,66 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
|
|||
window.opener.postMessage({{
|
||||
type: 'msft_auth_success',
|
||||
access_token: {json.dumps(token_response["access_token"])},
|
||||
token_data: {json.dumps(token_data)}
|
||||
token_data: {json.dumps(token.to_dict())}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
# Handle connection flow
|
||||
if not connection_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Connection ID is required for connection flow"
|
||||
)
|
||||
|
||||
# Get current user from session
|
||||
current_user = await getCurrentUser(request)
|
||||
if not current_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not authenticated"
|
||||
)
|
||||
|
||||
# Find and update connection
|
||||
interface = getInterface(current_user)
|
||||
connection = None
|
||||
for conn in current_user.connections:
|
||||
if conn.id == connection_id:
|
||||
connection = conn
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
)
|
||||
|
||||
# Update connection
|
||||
connection.status = ConnectionStatus.ACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
connection.expiresAt = datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))
|
||||
|
||||
# Update user record
|
||||
interface.db.recordModify("users", current_user.id, {
|
||||
"connections": [c.to_dict() for c in current_user.connections]
|
||||
})
|
||||
|
||||
# Return success page
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Successful</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_success',
|
||||
connectionId: {json.dumps(connection_id)}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from modules.interfaces.serviceChatModel import (
|
|||
ChatDocument,
|
||||
UserInputRequest
|
||||
)
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
|
||||
from modules.interfaces.serviceAppModel import User
|
||||
|
||||
# Configure logger
|
||||
|
|
|
|||
|
|
@ -129,7 +129,11 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> User:
|
|||
# Ensure the user has the correct context
|
||||
if str(user.mandateId) != str(mandateId) or str(user.id) != str(userId):
|
||||
logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.mandateId}, id={user.id})")
|
||||
raise credentialsException
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User context has changed. Please log in again.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
|
|
|||
|
|
@ -3,129 +3,187 @@ Shared utilities for model attributes and labels.
|
|||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Type, ClassVar
|
||||
from typing import Dict, Any, List, Type, Optional, Union
|
||||
import inspect
|
||||
import importlib
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
class BaseModelWithUI(BaseModel):
|
||||
"""Base model class with UI support and common functionality"""
|
||||
|
||||
@classmethod
|
||||
def get_ui_schema(cls) -> Dict[str, Any]:
|
||||
"""Get UI schema for frontend"""
|
||||
return {
|
||||
"fields": cls.fieldLabels if hasattr(cls, 'fieldLabels') else {},
|
||||
"validations": cls.get_validations() if hasattr(cls, 'get_validations') else {}
|
||||
}
|
||||
class ModelMixin:
|
||||
"""Mixin class that provides serialization methods for Pydantic models."""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary with proper validation"""
|
||||
# Handle both Pydantic v1 and v2
|
||||
"""
|
||||
Convert a Pydantic model to a dictionary.
|
||||
Handles both Pydantic v1 and v2.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary representation of the model
|
||||
"""
|
||||
if hasattr(self, 'model_dump'):
|
||||
return self.model_dump() # Pydantic v2
|
||||
return self.dict() # Pydantic v1
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI':
|
||||
"""Create instance from dictionary with validation"""
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def getModelAttributeDefinitions(cls) -> List[Dict[str, Any]]:
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
|
||||
"""
|
||||
Get attribute definitions for this model class.
|
||||
Override this method in model classes to provide custom attribute definitions.
|
||||
Create a Pydantic model instance from a dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing the model data
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of attribute definitions
|
||||
ModelMixin: New instance of the model class
|
||||
"""
|
||||
return cls(**data)
|
||||
|
||||
# Define the AttributeDefinition class here instead of importing it
|
||||
class AttributeDefinition(BaseModel, ModelMixin):
|
||||
"""Definition of a model attribute with its metadata."""
|
||||
name: str
|
||||
type: str
|
||||
label: str
|
||||
description: Optional[str] = None
|
||||
required: bool = False
|
||||
default: Any = None
|
||||
options: Optional[List[Any]] = None
|
||||
validation: Optional[Dict[str, Any]] = None
|
||||
ui: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Global registry for model labels
|
||||
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
|
||||
|
||||
def to_dict(model: BaseModel) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert a Pydantic model to a dictionary.
|
||||
Handles both Pydantic v1 and v2.
|
||||
|
||||
Args:
|
||||
model: The Pydantic model instance to convert
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary representation of the model
|
||||
"""
|
||||
if hasattr(model, 'model_dump'):
|
||||
return model.model_dump() # Pydantic v2
|
||||
return model.dict() # Pydantic v1
|
||||
|
||||
def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
|
||||
"""
|
||||
Create a Pydantic model instance from a dictionary.
|
||||
|
||||
Args:
|
||||
model_class: The Pydantic model class to instantiate
|
||||
data: Dictionary containing the model data
|
||||
|
||||
Returns:
|
||||
BaseModel: New instance of the model class
|
||||
"""
|
||||
return model_class(**data)
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model class
|
||||
model_label: Dictionary mapping language codes to model labels
|
||||
e.g. {"en": "Prompt", "fr": "Invite"}
|
||||
labels: Dictionary mapping attribute names to their translations
|
||||
e.g. {"name": {"en": "Name", "fr": "Nom"}}
|
||||
"""
|
||||
MODEL_LABELS[model_name] = {
|
||||
"model": model_label,
|
||||
"attributes": labels
|
||||
}
|
||||
|
||||
def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
|
||||
"""
|
||||
Get labels for a model's attributes in the specified language.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model class
|
||||
language: Language code (default: "en")
|
||||
|
||||
Returns:
|
||||
Dictionary mapping attribute names to their labels in the specified language
|
||||
"""
|
||||
model_data = MODEL_LABELS.get(model_name, {})
|
||||
attribute_labels = model_data.get("attributes", {})
|
||||
|
||||
return {
|
||||
attr: translations.get(language, translations.get("en", attr))
|
||||
for attr, translations in attribute_labels.items()
|
||||
}
|
||||
|
||||
def get_model_label(model_name: str, language: str = "en") -> str:
|
||||
"""
|
||||
Get the label for a model in the specified language.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model class
|
||||
language: Language code (default: "en")
|
||||
|
||||
Returns:
|
||||
Model label in the specified language, or model name if no label exists
|
||||
"""
|
||||
model_data = MODEL_LABELS.get(model_name, {})
|
||||
model_label = model_data.get("model", {})
|
||||
return model_label.get(language, model_label.get("en", model_name))
|
||||
|
||||
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
||||
"""
|
||||
Get attribute definitions for a model class.
|
||||
|
||||
Args:
|
||||
modelClass: The model class to get attributes for
|
||||
userLanguage: Language code for translations (default: "en")
|
||||
|
||||
Returns:
|
||||
Dictionary containing model label and attribute definitions
|
||||
"""
|
||||
if not modelClass:
|
||||
return {}
|
||||
|
||||
attributes = []
|
||||
model_name = modelClass.__name__
|
||||
labels = get_model_labels(model_name, userLanguage)
|
||||
model_label = get_model_label(model_name, userLanguage)
|
||||
|
||||
# Handle both Pydantic v1 and v2
|
||||
if hasattr(cls, 'model_fields'): # Pydantic v2
|
||||
fields = cls.model_fields
|
||||
if hasattr(modelClass, 'model_fields'): # Pydantic v2
|
||||
fields = modelClass.model_fields
|
||||
for name, field in fields.items():
|
||||
attributes.append({
|
||||
"name": name,
|
||||
"type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
|
||||
"required": field.is_required() if hasattr(field, "is_required") else True,
|
||||
"description": field.description if hasattr(field, "description") else "",
|
||||
"label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
|
||||
"placeholder": f"Please enter {name}",
|
||||
"label": labels.get(name, name),
|
||||
"placeholder": f"Please enter {labels.get(name, name)}",
|
||||
"editable": True,
|
||||
"visible": True,
|
||||
"order": len(attributes)
|
||||
})
|
||||
else: # Pydantic v1
|
||||
fields = cls.__fields__
|
||||
fields = modelClass.__fields__
|
||||
for name, field in fields.items():
|
||||
attributes.append({
|
||||
"name": name,
|
||||
"type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
|
||||
"required": field.required,
|
||||
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
|
||||
"label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
|
||||
"placeholder": f"Please enter {name}",
|
||||
"label": labels.get(name, name),
|
||||
"placeholder": f"Please enter {labels.get(name, name)}",
|
||||
"editable": True,
|
||||
"visible": True,
|
||||
"order": len(attributes)
|
||||
})
|
||||
|
||||
return attributes
|
||||
|
||||
def getModelAttributes(modelClass):
|
||||
"""
|
||||
Get all attributes of a model class.
|
||||
|
||||
Args:
|
||||
modelClass: The model class to get attributes from
|
||||
|
||||
Returns:
|
||||
List[str]: List of attribute names
|
||||
"""
|
||||
return [attr for attr in dir(modelClass)
|
||||
if not callable(getattr(modelClass, attr))
|
||||
and not attr.startswith('_')
|
||||
and attr not in ('metadata', 'query', 'query_class', 'label', 'field_labels')]
|
||||
|
||||
class Label(BaseModel):
|
||||
"""
|
||||
Label for an attribute or a class with support for multiple languages.
|
||||
|
||||
Attributes:
|
||||
default: Default label text
|
||||
translations: Dictionary of translations for different languages
|
||||
"""
|
||||
default: str = Field(..., description="Default label text")
|
||||
translations: Dict[str, str] = Field(default_factory=dict, description="Translations for different languages")
|
||||
|
||||
class Config:
|
||||
title = "Label"
|
||||
description = "A label with support for multiple languages"
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"default": "Document",
|
||||
"translations": {
|
||||
"en": "Document",
|
||||
"fr": "Document"
|
||||
return {
|
||||
"model": model_label,
|
||||
"attributes": attributes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getLabel(self, language: str = None) -> str:
|
||||
"""
|
||||
Returns the label in the specified language, or the default value if not available.
|
||||
|
||||
Args:
|
||||
language: Language code to get the label for
|
||||
|
||||
Returns:
|
||||
str: Label text in the specified language or default
|
||||
"""
|
||||
if language and language in self.translations:
|
||||
return self.translations[language]
|
||||
return self.default
|
||||
|
||||
def getModelClasses() -> Dict[str, Type[BaseModel]]:
|
||||
"""
|
||||
|
|
@ -155,27 +213,24 @@ def getModelClasses() -> Dict[str, Type[BaseModel]]:
|
|||
|
||||
return modelClasses
|
||||
|
||||
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
||||
"""
|
||||
Get attribute definitions for model classes.
|
||||
If modelClass is provided, returns attributes for that specific class.
|
||||
If no modelClass is provided, returns attributes for all model classes.
|
||||
class AttributeResponse(BaseModel):
|
||||
"""Response model for entity attributes"""
|
||||
attributes: List[AttributeDefinition]
|
||||
|
||||
Args:
|
||||
modelClass: Optional specific model class to get attributes for
|
||||
userLanguage: Language code for translations (default: "en")
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary of model class names to their attribute definitions
|
||||
"""
|
||||
if modelClass:
|
||||
return getModelAttributes(modelClass)
|
||||
|
||||
# Get all model classes
|
||||
modelClasses = getModelClasses()
|
||||
|
||||
# Create dictionary of model class names to their attribute definitions
|
||||
return {
|
||||
name: getModelAttributes(cls)
|
||||
for name, cls in modelClasses.items()
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"attributes": [
|
||||
{
|
||||
"name": "username",
|
||||
"label": "Username",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"placeholder": "Please enter username",
|
||||
"editable": True,
|
||||
"visible": True,
|
||||
"order": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ from typing import Dict, Any, List, Optional
|
|||
from datetime import datetime
|
||||
from modules.interfaces.serviceChatModel import ChatDocument, ChatContent
|
||||
from modules.workflow.documentProcessor import getDocumentContents
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -46,43 +47,33 @@ class DocumentManager:
|
|||
self.service = service
|
||||
return True
|
||||
|
||||
async def extractContent(self, fileId: int) -> Optional[ChatDocument]:
|
||||
"""
|
||||
Extract content from a file.
|
||||
|
||||
Args:
|
||||
fileId: ID of the file to process
|
||||
|
||||
Returns:
|
||||
ChatDocument object with extracted content or None if processing failed
|
||||
"""
|
||||
async def extractContent(self, fileId: str) -> Optional[ChatDocument]:
|
||||
"""Extracts content from a file and creates a chat document."""
|
||||
try:
|
||||
# Get file metadata and content from service
|
||||
fileMetadata = await self.service.base.getFileMetadata(fileId)
|
||||
fileContent = await self.service.base.getFileContent(fileId)
|
||||
|
||||
if not fileMetadata or not fileContent:
|
||||
logger.error(f"Could not retrieve file data for fileId {fileId}")
|
||||
# Get file content
|
||||
fileContent = await self.getFileContent(fileId)
|
||||
if not fileContent:
|
||||
return None
|
||||
|
||||
# Extract content using documentProcessor
|
||||
contents = getDocumentContents(fileMetadata, fileContent)
|
||||
# Get file metadata
|
||||
fileMetadata = await self.getFileMetadata(fileId)
|
||||
if not fileMetadata:
|
||||
return None
|
||||
|
||||
# Create ChatDocument
|
||||
# Create chat document
|
||||
return ChatDocument(
|
||||
id=str(fileId), # Using fileId as document id
|
||||
id=str(uuid.uuid4()),
|
||||
fileId=fileId,
|
||||
filename=fileMetadata.get("name", "unknown"),
|
||||
filename=fileMetadata.get("name", "Unknown"),
|
||||
fileSize=fileMetadata.get("size", 0),
|
||||
mimeType=fileMetadata.get("mimeType", "application/octet-stream"),
|
||||
contents=contents
|
||||
content=fileContent.decode('utf-8', errors='ignore'),
|
||||
mimeType=fileMetadata.get("mimeType", "text/plain")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting content from file {fileId}: {str(e)}", exc_info=True)
|
||||
logger.error(f"Error extracting content from file {fileId}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def processFileIds(self, fileIds: List[int]) -> List[ChatDocument]:
|
||||
async def processFileIds(self, fileIds: List[str]) -> List[ChatDocument]:
|
||||
"""
|
||||
Process multiple files and extract their contents.
|
||||
|
||||
|
|
@ -103,34 +94,18 @@ class DocumentManager:
|
|||
continue
|
||||
return documents
|
||||
|
||||
async def getFileContent(self, fileId: int) -> Optional[bytes]:
|
||||
"""
|
||||
Get raw file content.
|
||||
|
||||
Args:
|
||||
fileId: ID of the file
|
||||
|
||||
Returns:
|
||||
File content as bytes or None if not found
|
||||
"""
|
||||
async def getFileContent(self, fileId: str) -> Optional[bytes]:
|
||||
"""Gets the content of a file."""
|
||||
try:
|
||||
return await self.service.base.getFileContent(fileId)
|
||||
return self.service.functions.getFileData(fileId)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting file content for {fileId}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def getFileMetadata(self, fileId: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get file metadata.
|
||||
|
||||
Args:
|
||||
fileId: ID of the file
|
||||
|
||||
Returns:
|
||||
File metadata dictionary or None if not found
|
||||
"""
|
||||
async def getFileMetadata(self, fileId: str) -> Optional[Dict[str, Any]]:
|
||||
"""Gets the metadata of a file."""
|
||||
try:
|
||||
return await self.service.base.getFileMetadata(fileId)
|
||||
return self.service.functions.getFile(fileId)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting file metadata for {fileId}: {str(e)}")
|
||||
return None
|
||||
|
|
@ -153,18 +128,10 @@ class DocumentManager:
|
|||
logger.error(f"Error saving file {filename}: {str(e)}")
|
||||
return None
|
||||
|
||||
async def deleteFile(self, fileId: int) -> bool:
|
||||
"""
|
||||
Delete a file.
|
||||
|
||||
Args:
|
||||
fileId: ID of the file to delete
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
async def deleteFile(self, fileId: str) -> bool:
|
||||
"""Deletes a file."""
|
||||
try:
|
||||
return await self.service.base.deleteFile(fileId)
|
||||
return self.service.functions.deleteFile(fileId)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting file {fileId}: {str(e)}")
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1259,18 +1259,15 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
|
|||
"stats": workflow.stats.to_dict()
|
||||
}
|
||||
|
||||
def _checkFileAccess(self, fileId: int) -> bool:
|
||||
"""Checks if the current user has access to a file."""
|
||||
def _checkFileAccess(self, fileId: str) -> bool:
|
||||
"""Checks if the current user has access to the file."""
|
||||
try:
|
||||
file = self.service.functions.getFile(fileId)
|
||||
if not file:
|
||||
return file is not None
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking file access: {str(e)}")
|
||||
return False
|
||||
|
||||
if file.get("mandateId") != self.functions.mandateId:
|
||||
logger.warning(f"File {fileId} does not belong to mandate {self.functions.mandateId}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Singleton factory for the WorkflowManager
|
||||
_workflowManagers = {}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ passlib==1.7.4
|
|||
argon2-cffi>=21.3.0 # Für Passwort-Hashing in gateway_interface.py
|
||||
google-auth-oauthlib==1.2.0 # Für Google OAuth
|
||||
google-auth==2.27.0 # Für Google Authentication
|
||||
google-api-python-client==2.170.0 # For Google API integration
|
||||
bcrypt==4.0.1 # For password hashing
|
||||
python-jose[cryptography]==3.3.0 # For JWT tokens
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue