self service date model for agents, msft interface complete

This commit is contained in:
ValueOn AG 2025-05-20 17:57:15 +02:00
parent db6b5d7985
commit fee9bb0151
35 changed files with 1976 additions and 2574 deletions

15
app.py
View file

@ -68,6 +68,14 @@ async def lifespan(app: FastAPI):
# Shutdown logic # Shutdown logic
logger.info("Application has been shut down") logger.info("Application has been shut down")
# START APP
app = FastAPI(
title="PowerOn | Data Platform API",
description=f"Backend API for the Multi-Agent Platform by ValueOn AG ({instanceLabel})",
lifespan=lifespan
)
# Parse CORS origins from environment variable # Parse CORS origins from environment variable
def get_allowed_origins(): def get_allowed_origins():
origins_str = APP_CONFIG.get("APP_ALLOWED_ORIGINS", "http://localhost:8080") origins_str = APP_CONFIG.get("APP_ALLOWED_ORIGINS", "http://localhost:8080")
@ -76,13 +84,6 @@ def get_allowed_origins():
logger.info(f"CORS allowed origins: {origins}") logger.info(f"CORS allowed origins: {origins}")
return origins return origins
# START APP
app = FastAPI(
title="PowerOn | Data Platform API",
description=f"Backend API for the Multi-Agent Platform by ValueOn AG ({instanceLabel})",
lifespan=lifespan
)
# CORS configuration using environment variables # CORS configuration using environment variables
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View file

@ -47,6 +47,6 @@ Agent_Coder_EXECUTION_TIMEOUT = 60
Agent_Coder_EXECUTION_RETRY = 5 Agent_Coder_EXECUTION_RETRY = 5
# Agent Mail configuration # Agent Mail configuration
Agent_Mail_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c Service_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Agent_Mail_MSFT_CLIENT_SECRET = Kxf8Q~2lJIteZ~JaI32kMf1lfaWKATqxXiNiFbzV Service_MSFT_CLIENT_SECRET = Kxf8Q~2lJIteZ~JaI32kMf1lfaWKATqxXiNiFbzV
Agent_Mail_MSFT_TENANT_ID = common Service_MSFT_TENANT_ID = common

View file

@ -17,6 +17,12 @@ DB_LUCYDOM_DATABASE=lucydom
DB_LUCYDOM_USER=dev_user DB_LUCYDOM_USER=dev_user
DB_LUCYDOM_PASSWORD_SECRET=dev_password DB_LUCYDOM_PASSWORD_SECRET=dev_password
# Database Configuration MSFT
DB_MSFT_HOST=D:/Temp/_powerondb
DB_MSFT_DATABASE=msft
DB_MSFT_USER=dev_user
DB_MSFT_PASSWORD_SECRET=dev_password
# Security Configuration # Security Configuration
APP_JWT_SECRET_SECRET=dev_jwt_secret_token APP_JWT_SECRET_SECRET=dev_jwt_secret_token
APP_TOKEN_EXPIRY=300 APP_TOKEN_EXPIRY=300
@ -35,4 +41,4 @@ APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5 APP_LOGGING_BACKUP_COUNT = 5
# Agent Mail # Agent Mail
Agent_Mail_MSFT_REDIRECT_URI = http://localhost:8000/api/msft/auth/callback Service_MSFT_REDIRECT_URI = http://localhost:8000/api/msft/auth/callback

View file

@ -17,6 +17,12 @@ DB_LUCYDOM_DATABASE=lucydom
DB_LUCYDOM_USER=dev_user DB_LUCYDOM_USER=dev_user
DB_LUCYDOM_PASSWORD_SECRET=prod_password DB_LUCYDOM_PASSWORD_SECRET=prod_password
# Database Configuration MSFT
DB_MSFT_HOST=/home/_powerondb
DB_MSFT_DATABASE=msft
DB_MSFT_USER=dev_user
DB_MSFT_PASSWORD_SECRET=dev_password
# Security Configuration # Security Configuration
APP_JWT_SECRET_SECRET=dev_jwt_secret_token APP_JWT_SECRET_SECRET=dev_jwt_secret_token
APP_TOKEN_EXPIRY=300 APP_TOKEN_EXPIRY=300
@ -34,5 +40,5 @@ APP_LOGGING_FILE_ENABLED = True
APP_LOGGING_ROTATION_SIZE = 10485760 APP_LOGGING_ROTATION_SIZE = 10485760
APP_LOGGING_BACKUP_COUNT = 5 APP_LOGGING_BACKUP_COUNT = 5
# Agent Mail # Service MSFT
Agent_Mail_MSFT_REDIRECT_URI = https://gateway.poweron-center.net/api/msft/auth/callback Service_MSFT_REDIRECT_URI = https://gateway.poweron-center.net/api/msft/auth/callback

View file

@ -36,8 +36,9 @@ class AgentAnalyst(AgentBase):
# Set default visualization settings # Set default visualization settings
plt.style.use('seaborn-v0_8-whitegrid') plt.style.use('seaborn-v0_8-whitegrid')
def setDependencies(self, mydom=None): def setDependencies(self, serviceBase=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
""" """
@ -56,7 +57,7 @@ class AgentAnalyst(AgentBase):
workflow = task.get("context", {}).get("workflow", {}) workflow = task.get("context", {}).get("workflow", {})
# Check AI service # Check AI service
if not self.mydom: if not self.service or not self.service.base:
return { return {
"feedback": "The Analyst agent requires an AI service to function effectively.", "feedback": "The Analyst agent requires an AI service to function effectively.",
"documents": [] "documents": []
@ -244,7 +245,7 @@ class AgentAnalyst(AgentBase):
try: try:
# Get analysis plan from AI # Get analysis plan from AI
response = await self.mydom.callAi([ response = await self.service.base.callAi([
{"role": "system", "content": "You are a data analysis expert. Create detailed analysis plans. Respond with valid JSON only."}, {"role": "system", "content": "You are a data analysis expert. Create detailed analysis plans. Respond with valid JSON only."},
{"role": "user", "content": analysisPrompt} {"role": "user", "content": analysisPrompt}
], produceUserAnswer=True) ], produceUserAnswer=True)
@ -366,7 +367,7 @@ class AgentAnalyst(AgentBase):
""" """
# Get analysis plan from AI # Get analysis plan from AI
response = await self.mydom.callAi([ response = await self.service.base.callAi([
{"role": "system", "content": "You are a data analysis expert. Create detailed analysis plans. Respond with valid JSON only."}, {"role": "system", "content": "You are a data analysis expert. Create detailed analysis plans. Respond with valid JSON only."},
{"role": "user", "content": analysisPrompt} {"role": "user", "content": analysisPrompt}
], produceUserAnswer=True) ], produceUserAnswer=True)
@ -457,7 +458,7 @@ class AgentAnalyst(AgentBase):
if not vizRecommendations: if not vizRecommendations:
# Generate visualization recommendations if none provided # Generate visualization recommendations if none provided
self.mydom.logAdd(analysisPlan.get("workflowId"), "Generating visualization recommendations...", level="info", progress=50) self.service.base.logAdd(analysisPlan.get("workflowId"), "Generating visualization recommendations...", level="info", progress=50)
vizPrompt = f""" vizPrompt = f"""
Based on this data and task, recommend appropriate visualizations. Based on this data and task, recommend appropriate visualizations.
@ -481,7 +482,7 @@ class AgentAnalyst(AgentBase):
}} }}
""" """
response = await self.mydom.callAi([ response = await self.service.base.callAi([
{"role": "system", "content": "You are a data visualization expert. Recommend appropriate visualizations based on the data and task."}, {"role": "system", "content": "You are a data visualization expert. Recommend appropriate visualizations based on the data and task."},
{"role": "user", "content": vizPrompt} {"role": "user", "content": vizPrompt}
]) ])
@ -566,7 +567,7 @@ class AgentAnalyst(AgentBase):
try: try:
# Get visualization code from AI # Get visualization code from AI
vizCode = await self.mydom.callAi([ vizCode = await self.service.base.callAi([
{"role": "system", "content": "You are a data visualization expert. Provide only executable Python code."}, {"role": "system", "content": "You are a data visualization expert. Provide only executable Python code."},
{"role": "user", "content": vizPrompt} {"role": "user", "content": vizPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)
@ -696,7 +697,7 @@ class AgentAnalyst(AgentBase):
try: try:
# Get data processing code from AI # Get data processing code from AI
dataCode = await self.mydom.callAi([ dataCode = await self.service.base.callAi([
{"role": "system", "content": "You are a data processing expert. Provide only executable Python code."}, {"role": "system", "content": "You are a data processing expert. Provide only executable Python code."},
{"role": "user", "content": dataPrompt} {"role": "user", "content": dataPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)
@ -817,7 +818,7 @@ class AgentAnalyst(AgentBase):
try: try:
# Get document content from AI # Get document content from AI
documentContent = await self.mydom.callAi([ documentContent = await self.service.base.callAi([
{"role": "system", "content": f"You are a data analysis expert creating a {formatType} document."}, {"role": "system", "content": f"You are a data analysis expert creating a {formatType} document."},
{"role": "user", "content": analysisPrompt} {"role": "user", "content": analysisPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)

View file

@ -32,8 +32,9 @@ class AgentCoach(AgentBase):
"structuredOutput" "structuredOutput"
] ]
def setDependencies(self, mydom=None): def setDependencies(self, serviceBase=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
""" """
@ -52,7 +53,7 @@ class AgentCoach(AgentBase):
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.get("outputSpecifications", [])
# Check AI service # Check AI service
if not self.mydom: if not self.service or not self.service.base:
return { return {
"feedback": "The Coach agent requires an AI service to function.", "feedback": "The Coach agent requires an AI service to function.",
"documents": [] "documents": []
@ -171,7 +172,8 @@ class AgentCoach(AgentBase):
""" """
try: try:
response = await self.mydom.callAi([ # Get task understanding from AI
response = await self.service.base.callAi([
{"role": "system", "content": "You are a task analysis expert. Respond with valid JSON only."}, {"role": "system", "content": "You are a task analysis expert. Respond with valid JSON only."},
{"role": "user", "content": analysisPrompt} {"role": "user", "content": analysisPrompt}
]) ])
@ -254,7 +256,7 @@ class AgentCoach(AgentBase):
systemPrompt = f"You create {outputFormat} format content based on requests and extracted data. Provide only the content in valid {outputFormat} format." systemPrompt = f"You create {outputFormat} format content based on requests and extracted data. Provide only the content in valid {outputFormat} format."
# Generate content with AI # Generate content with AI
content = await self.mydom.callAi([ content = await self.service.base.callAi([
{"role": "system", "content": systemPrompt}, {"role": "system", "content": systemPrompt},
{"role": "user", "content": generationPrompt} {"role": "user", "content": generationPrompt}
]) ])

View file

@ -42,8 +42,9 @@ class AgentCoder(AgentBase):
self.executionRetryLimit = int(APP_CONFIG.get("Agent_Coder_EXECUTION_RETRY")) # max retries self.executionRetryLimit = int(APP_CONFIG.get("Agent_Coder_EXECUTION_RETRY")) # max retries
self.tempDir = None self.tempDir = None
def setDependencies(self, mydom=None): def setDependencies(self, serviceBase=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
""" """
@ -64,7 +65,7 @@ class AgentCoder(AgentBase):
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.get("outputSpecifications", [])
# Check if AI service is available # Check if AI service is available
if not self.mydom: if not self.service or not self.service.base:
logger.error("No AI service configured for the Coder agent") logger.error("No AI service configured for the Coder agent")
return { return {
"feedback": "The Coder agent is not properly configured.", "feedback": "The Coder agent is not properly configured.",
@ -373,7 +374,7 @@ Return ONLY Python code without explanations or markdown.
] ]
try: try:
improvedContent = await self.mydom.callAi(messages, temperature=0.2) improvedContent = await self.service.base.callAi(messages, temperature=0.2)
# Extract code and requirements # Extract code and requirements
improvedCode = self._cleanCode(improvedContent) improvedCode = self._cleanCode(improvedContent)
@ -451,7 +452,7 @@ Only return valid JSON. Your entire response must be parseable as JSON.
try: try:
# Use a lower temperature for more deterministic response # Use a lower temperature for more deterministic response
response = await self.mydom.callAi(messages, produceUserAnswer = True, temperature=0.1) response = await self.service.base.callAi(messages, produceUserAnswer = True, temperature=0.1)
# Parse response as JSON # Parse response as JSON
if response: if response:
@ -578,7 +579,7 @@ Return ONLY Python code without explanations or markdown.
{"role": "user", "content": aiPrompt} {"role": "user", "content": aiPrompt}
] ]
generatedContent = await self.mydom.callAi(messages, temperature=0.1) generatedContent = await self.service.base.callAi(messages, temperature=0.1)
# Extract code and requirements # Extract code and requirements
code = self._cleanCode(generatedContent) code = self._cleanCode(generatedContent)

View file

@ -30,8 +30,9 @@ class AgentDocumentation(AgentBase):
"knowledge_organization" "knowledge_organization"
] ]
def setDependencies(self, mydom=None): def setDependencies(self, serviceBase=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
""" """
@ -50,7 +51,7 @@ class AgentDocumentation(AgentBase):
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.get("outputSpecifications", [])
# Check AI service # Check AI service
if not self.mydom: if not self.service or not self.service.base:
return { return {
"feedback": "The Documentation agent requires an AI service to function.", "feedback": "The Documentation agent requires an AI service to function.",
"documents": [] "documents": []
@ -204,7 +205,7 @@ class AgentDocumentation(AgentBase):
""" """
try: try:
response = await self.mydom.callAi([ response = await self.service.base.callAi([
{"role": "system", "content": "You are a documentation expert. Respond with valid JSON only."}, {"role": "system", "content": "You are a documentation expert. Respond with valid JSON only."},
{"role": "user", "content": analysisPrompt} {"role": "user", "content": analysisPrompt}
]) ])
@ -374,7 +375,7 @@ This introduction should:
The introduction should be professional and engaging, but short and precise, formatted according to {formatType} standards. do not add details, which are not requested by the Task Context. The introduction should be professional and engaging, but short and precise, formatted according to {formatType} standards. do not add details, which are not requested by the Task Context.
""" """
introduction = await self.mydom.callAi([ introduction = await self.service.base.callAi([
{"role": "system", "content": f"You are a documentation expert creating an introduction in {formatType} format."}, {"role": "system", "content": f"You are a documentation expert creating an introduction in {formatType} format."},
{"role": "user", "content": introPrompt} {"role": "user", "content": introPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)
@ -400,7 +401,7 @@ The introduction should be professional and engaging, but short and precise, for
Keep the summary focused and impactful, approximately 200-300 words. Keep the summary focused and impactful, approximately 200-300 words.
""" """
executiveSummary = await self.mydom.callAi([ executiveSummary = await self.service.base.callAi([
{"role": "system", "content": f"You are a documentation expert creating an executive summary in {formatType} format."}, {"role": "system", "content": f"You are a documentation expert creating an executive summary in {formatType} format."},
{"role": "user", "content": summaryPrompt} {"role": "user", "content": summaryPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)
@ -450,7 +451,7 @@ This section should:
Be thorough in your coverage of this section, providing substantive content focussing on the Task content. Be thorough in your coverage of this section, providing substantive content focussing on the Task content.
""" """
sectionContent = await self.mydom.callAi([ sectionContent = await self.service.base.callAi([
{"role": "system", "content": f"You are a documentation expert creating detailed content for the {sectionTitle} section."}, {"role": "system", "content": f"You are a documentation expert creating detailed content for the {sectionTitle} section."},
{"role": "user", "content": sectionPrompt} {"role": "user", "content": sectionPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)
@ -477,7 +478,7 @@ Be thorough in your coverage of this section, providing substantive content focu
The conclusion should be professional and impactful, formatted according to {formatType} standards. The conclusion should be professional and impactful, formatted according to {formatType} standards.
""" """
conclusion = await self.mydom.callAi([ conclusion = await self.service.base.callAi([
{"role": "system", "content": f"You are a documentation expert creating a conclusion in {formatType} format."}, {"role": "system", "content": f"You are a documentation expert creating a conclusion in {formatType} format."},
{"role": "user", "content": conclusionPrompt} {"role": "user", "content": conclusionPrompt}
], produceUserAnswer = True) ], produceUserAnswer = True)

View file

@ -1,83 +1,50 @@
""" """
Email agent for generating and sending emails. Email Agent Module.
Provides email template generation and sending capabilities. Handles email-related tasks using Microsoft Graph API.
""" """
import logging import logging
from typing import Dict, Any, List, Tuple
import json import json
import os from typing import Dict, Any, List, Optional
import requests from ..workflow.agentBase import AgentBase
import base64
from datetime import datetime
import re
from bs4 import BeautifulSoup
import msal
from modules.shared.configuration import APP_CONFIG
from modules.workflow.agentBase import AgentBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AgentEmail(AgentBase): class AgentEmail(AgentBase):
"""AI-driven agent for creating email templates and drafts using Microsoft Graph API""" """Agent for handling email-related tasks."""
def __init__(self): def __init__(self):
"""Initialize the email agent""" """Initialize the email agent."""
super().__init__() super().__init__()
self.name = "email" self.name = "email"
self.label = "Email Templates" self.label = "Email Agent"
self.description = "Creates email templates with HTML-formatted body and attachments from input documents" self.description = "Handles email composition and sending using Microsoft Graph API"
self.capabilities = [ self.capabilities = [
"emailDrafting", "email_composition",
"contentFormatting", "email_draft_creation",
"htmlTemplates", "email_template_generation"
"documentAttachment",
"msftGraphIntegration"
] ]
self.serviceBase = None
# Initialize configuration def setDependencies(self, serviceBase=None):
self.client_id = None
self.client_secret = None
self.tenant_id = None
self.redirect_uri = None
self.authority = None
self.scopes = ["Mail.ReadWrite", "User.Read"]
# API base URL for Microsoft authentication
self.api_base_url = APP_CONFIG.get("APP_API_URL", "(no-url)")
def setDependencies(self, mydom=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self._loadConfiguration() self.serviceBase = serviceBase
def _loadConfiguration(self):
"""Load Microsoft Graph API configuration from config files"""
try:
self.client_id = APP_CONFIG.get("Agent_Mail_MSFT_CLIENT_ID")
self.client_secret = APP_CONFIG.get("Agent_Mail_MSFT_CLIENT_SECRET")
self.tenant_id = APP_CONFIG.get("Agent_Mail_MSFT_TENANT_ID", "common")
self.redirect_uri = APP_CONFIG.get("Agent_Mail_MSFT_REDIRECT_URI")
# Set authority URL
self.authority = f"https://login.microsoftonline.com/{self.tenant_id}"
logger.info(f"Email agent initialized with tenant ID: {self.tenant_id}")
logger.info(f"Redirect URI: {self.redirect_uri}")
except Exception as e:
logger.error(f"Error loading Microsoft Graph configuration: {str(e)}")
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
""" """
Process a task by creating an email template based on input documents. Process an email-related task.
Sends a login request to the frontend if Microsoft authentication is required.
Args: Args:
task: Task dictionary with prompt, inputDocuments, outputSpecifications task: Task object containing:
- prompt: Instructions for the agent
- inputDocuments: List of documents to process
- outputSpecifications: List of required output documents
- context: Additional context including workflow info
Returns: Returns:
Dictionary with feedback and documents Dictionary containing:
- feedback: Text response explaining what was done
- documents: List of created documents
""" """
try: try:
# Extract task information # Extract task information
@ -86,29 +53,27 @@ class AgentEmail(AgentBase):
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.get("outputSpecifications", [])
# Check AI service # Check AI service
if not self.mydom: if not self.service.base:
return { return {
"feedback": "The Email agent requires an AI service to function.", "feedback": "The Email agent requires an AI service to function.",
"documents": [] "documents": []
} }
# Check Microsoft authentication status # Check if Microsoft connector is available
user_info, access_token = self._getCurrentUserToken() if not hasattr(self.service, 'msft'):
# If not authenticated, trigger frontend authentication flow
if not user_info or not access_token:
# Create authentication instruction document
auth_instructions = self._createFrontendAuthTriggerDocument()
# Return feedback with authentication trigger log for frontend
return { return {
"feedback": "⚠️ Microsoft authentication required. Please complete the authentication process when prompted.", "feedback": "Microsoft connector not available. Please ensure Microsoft integration is properly configured.",
"documents": [auth_instructions], "documents": []
"log": {
"message": "doMsftLogin",
"type": "system",
"details": "Microsoft authentication required to create email drafts"
} }
# Get Microsoft token
token_data = self.service.msft.getMsftToken()
if not token_data:
# Create authentication trigger document
auth_doc = self._createFrontendAuthTriggerDocument()
return {
"feedback": "Microsoft authentication required. Please authenticate to continue.",
"documents": [auth_doc]
} }
# Extract document data from input # Extract document data from input
@ -121,7 +86,7 @@ class AgentEmail(AgentBase):
htmlPreview = self._createHtmlPreview(emailTemplate) htmlPreview = self._createHtmlPreview(emailTemplate)
# Attempt to create a draft email using Microsoft Graph API # Attempt to create a draft email using Microsoft Graph API
draft_result, user_email = self._createDraftEmail( draft_result = self.service.msft.createDraftEmail(
emailTemplate["recipient"], emailTemplate["recipient"],
emailTemplate["subject"], emailTemplate["subject"],
emailTemplate["htmlBody"], emailTemplate["htmlBody"],
@ -164,7 +129,7 @@ class AgentEmail(AgentBase):
# Prepare feedback message # Prepare feedback message
if draft_result: if draft_result:
feedback = f"Email draft created successfully for {user_email}. The subject is: '{emailTemplate['subject']}'" feedback = f"Email draft created successfully for {emailTemplate.get('recipient')}. The subject is: '{emailTemplate['subject']}'"
if attachments: if attachments:
feedback += f" with {len(attachments)} attachment(s)" feedback += f" with {len(attachments)} attachment(s)"
feedback += ". You can open and edit it in your Outlook draft folder." feedback += ". You can open and edit it in your Outlook draft folder."
@ -177,61 +142,37 @@ class AgentEmail(AgentBase):
} }
except Exception as e: except Exception as e:
logger.error(f"Error in email creation: {str(e)}", exc_info=True) logger.error(f"Error in email agent: {str(e)}")
return { return {
"feedback": f"Error creating email template: {str(e)}", "feedback": f"Error processing email task: {str(e)}",
"documents": [] "documents": []
} }
def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]: def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]:
""" """Create a document that triggers Microsoft authentication in the frontend."""
Create a simple document that explains authentication is required. return {
This document is minimal as the actual authentication will be handled by frontend. "name": "microsoft_auth",
"ext": "html",
Returns: "mimeType": "text/html",
Document dictionary "data": """
""" <div>
html_content = """ <h2>Microsoft Authentication Required</h2>
<!DOCTYPE html> <p>Please click the button below to authenticate with Microsoft:</p>
<html> <button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
<head>
<meta charset="UTF-8">
<title>Microsoft Authentication Required</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background-color: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 30px; }
h1 { color: #0078d4; margin-top: 0; }
.note { background-color: #fff4e5; border-left: 4px solid #ff8c00; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<h1>Microsoft Authentication Required</h1>
<p>To create email templates and drafts, you need to authenticate with your Microsoft account.</p>
<p>The application will now initiate the Microsoft authentication process. Please follow the instructions in the authentication window.</p>
<div class="note">
<p><strong>Note:</strong> You only need to authenticate once. Your session will be remembered for future email operations.</p>
</div> </div>
</div> """,
</body> "base64Encoded": False,
</html> "metadata": {
""" "isText": True
}
}
return self.formatAgentDocumentOutput( def _processInputDocuments(self, input_docs: List[Dict[str, Any]]) -> tuple:
"microsoft_authentication.html",
html_content,
"text/html"
)
def _processInputDocuments(self, documents: List[Dict[str, Any]]) -> tuple:
""" """
Process input documents to extract content and prepare attachments. Process input documents to extract content and prepare attachments.
Args: Args:
documents: List of input documents input_docs: List of input documents
Returns: Returns:
Tuple of (document content text, list of attachments) Tuple of (document content text, list of attachments)
@ -239,7 +180,7 @@ class AgentEmail(AgentBase):
documentContents = [] documentContents = []
attachments = [] attachments = []
for doc in documents: for doc in input_docs:
docName = doc.get("name", "unnamed") docName = doc.get("name", "unnamed")
if doc.get("ext"): if doc.get("ext"):
docName = f"{docName}.{doc.get('ext')}" docName = f"{docName}.{doc.get('ext')}"
@ -299,10 +240,10 @@ class AgentEmail(AgentBase):
""" """
try: try:
response = await self.mydom.callAi([ response = await self.service.base.callAi([
{"role": "system", "content": "You are an email template specialist. Create professional emails. Respond with valid JSON only."}, {"role": "system", "content": "You are an email template specialist. Create professional emails. Respond with valid JSON only."},
{"role": "user", "content": emailPrompt} {"role": "user", "content": emailPrompt}
], produceUserAnswer=True) ])
# Extract JSON from response # Extract JSON from response
jsonStart = response.find('{') jsonStart = response.find('{')
@ -384,260 +325,6 @@ class AgentEmail(AgentBase):
""" """
return html return html
def _getCurrentUserToken(self) -> tuple: def getAgentEmail() -> AgentEmail:
""" """Factory function to create and return an EmailAgent instance."""
Get the current user's Microsoft token using the current user context.
Returns tuple of (user_info, access_token) or (None, None) if not authenticated.
"""
try:
if not self.mydom:
logger.error("No mydom interface available")
return None, None
# Get token data from database using LucyDOMInterface
token_data = self.mydom.getMsftToken()
if not token_data:
logger.info("No Microsoft token found for user")
return None, None
# Verify token is still valid
if not self._verifyToken(token_data.get("access_token")):
logger.info("Token invalid, attempting refresh")
if not self._refreshToken(token_data):
logger.info("Token refresh failed")
return None, None
# Get updated token data after refresh
token_data = self.mydom.getMsftToken()
# Get user info from token data
user_info = token_data.get("user_info")
if not user_info:
# If user_info is not in token_data, try to get it from the token
headers = {
'Authorization': f'Bearer {token_data.get("access_token", "")}',
'Content-Type': 'application/json'
}
try:
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
if response.status_code == 200:
user_data = response.json()
user_info = {
"name": user_data.get("displayName", ""),
"email": user_data.get("userPrincipalName", ""),
"id": user_data.get("id", "")
}
# Update token data with user info
token_data["user_info"] = user_info
self.mydom.saveMsftToken(token_data)
logger.info(f"Retrieved and stored user info for {user_info.get('name', 'Unknown User')}")
else:
logger.warning(f"Failed to get user info: {response.status_code} - {response.text}")
return None, None
except Exception as e:
logger.error(f"Error getting user info: {str(e)}")
return None, None
logger.info(f"Retrieved user info for {user_info.get('name', 'Unknown User')}")
return user_info, token_data.get("access_token")
except Exception as e:
logger.error(f"Error getting current user token: {str(e)}")
return None, None
def _verifyToken(self, token: str) -> bool:
"""Verify the access token is valid"""
try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
return response.status_code == 200
except Exception as e:
logger.error(f"Error verifying token: {str(e)}")
return False
def _refreshToken(self, token_data: Dict[str, Any]) -> bool:
"""Refresh the access token using the stored refresh token"""
try:
if not token_data or not token_data.get("refresh_token"):
logger.warning("No refresh token available")
return False
msal_app = msal.ConfidentialClientApplication(
self.client_id,
authority=self.authority,
client_credential=self.client_secret
)
result = msal_app.acquire_token_by_refresh_token(
token_data["refresh_token"],
scopes=self.scopes
)
if "error" in result:
logger.error(f"Error refreshing token: {result.get('error')}")
return False
# Update token data
token_data["access_token"] = result["access_token"]
if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"]
# Save updated token
self.mydom.saveMsftToken(token_data)
logger.info("Access token refreshed successfully")
return True
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
return False
def _createDraftEmail(self, recipient, subject, body, attachments=None):
"""Create a draft email using Microsoft Graph API"""
try:
# Get current user token
user_info, access_token = self._getCurrentUserToken()
if not user_info or not access_token:
logger.warning("No authenticated user found, cannot create draft email")
return False, None
# Create draft email using Graph API
email_result = self._createGraphDraftEmail(access_token, recipient, subject, body, attachments)
if email_result:
return True, user_info.get("email")
else:
return False, user_info.get("email")
except Exception as e:
logger.error(f"Error in creating draft email: {str(e)}")
return False, None
def _createGraphDraftEmail(self, access_token, recipient, subject, body, attachments=None):
"""
Create a draft email using Microsoft Graph API.
Treats all files as binary attachments without content analysis.
Args:
access_token: Microsoft Graph access token
recipient: Email recipient
subject: Email subject
body: HTML body of the email
attachments: List of attachments
Returns:
Draft result or None if failed
"""
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
# Prepare email data with proper structure
email_data = {
'subject': subject,
'body': {
'contentType': 'HTML',
'content': body
},
'toRecipients': [
{
'emailAddress': {
'address': recipient
}
}
]
}
# Add attachments if available
if attachments and len(attachments) > 0:
email_data['attachments'] = []
for attachment in attachments:
doc = attachment.get('document', {})
file_name = attachment.get('name', 'attachment.file')
logger.info(f"Processing attachment: {file_name}")
# Get the document data directly
file_content = doc.get('data')
if not file_content:
logger.warning(f"No data found for attachment: {file_name}")
continue
# Get content type from document metadata
mime_type = doc.get('mimeType', 'application/octet-stream')
is_base64 = doc.get('base64Encoded', False)
# Handle content encoding
try:
if is_base64:
# Content is already base64 encoded
content_bytes = file_content
else:
# Content needs to be base64 encoded
if isinstance(file_content, str):
# For text files, encode the string to bytes first
content_bytes = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
elif isinstance(file_content, bytes):
# For binary files, encode directly
content_bytes = base64.b64encode(file_content).decode('utf-8')
else:
logger.warning(f"Unexpected content type for {file_name}")
continue
# Calculate size from decoded content
decoded_size = len(base64.b64decode(content_bytes))
# Add attachment to email data
logger.info(f"Adding attachment: {file_name} ({mime_type}, size: {decoded_size} bytes)")
attachment_data = {
'@odata.type': '#microsoft.graph.fileAttachment',
'name': file_name,
'contentType': mime_type,
'contentBytes': content_bytes,
'isInline': False,
'size': decoded_size
}
email_data['attachments'].append(attachment_data)
logger.info(f"Successfully added attachment: {file_name}")
except Exception as e:
logger.error(f"Error processing attachment {file_name}: {str(e)}")
continue
# Try to create draft using drafts folder endpoint
try:
logger.info("Attempting to create draft email using messages endpoint")
logger.info(f"Email data structure: subject={subject}, recipient={recipient}, " +
f"has_attachments={bool(email_data.get('attachments'))}, " +
f"attachment_count={len(email_data.get('attachments', []))}")
# Create the draft message
response = requests.post(
'https://graph.microsoft.com/v1.0/me/messages',
headers=headers,
json=email_data
)
if response.status_code >= 200 and response.status_code < 300:
logger.info("Successfully created draft email using messages endpoint")
return response.json()
else:
logger.error(f"Messages endpoint method failed: {response.status_code} - {response.text}")
logger.error(f"Request headers: {headers}")
logger.error(f"Request body: {json.dumps(email_data, indent=2)}")
return None
except Exception as e:
logger.error(f"Exception creating draft email: {str(e)}", exc_info=True)
return None
# Factory function for the Email agent
def getAgentEmail():
"""Returns an instance of the Email agent."""
return AgentEmail() return AgentEmail()

View file

@ -7,6 +7,7 @@ import logging
import json import json
import re import re
import time import time
import os
from typing import Dict, Any, List from typing import Dict, Any, List
from urllib.parse import quote_plus, unquote from urllib.parse import quote_plus, unquote
@ -23,17 +24,17 @@ class AgentWebcrawler(AgentBase):
"""AI-driven agent for web research and information retrieval""" """AI-driven agent for web research and information retrieval"""
def __init__(self): def __init__(self):
"""Initialize the webcrawler agent""" """Initialize the web crawler agent"""
super().__init__() super().__init__()
self.name = "webcrawler" self.name = "webcrawler"
self.label = "Web-Research" self.label = "Web Crawler"
self.description = "Conducts web research and collects information from online sources" self.description = "Gathers and analyzes web content using AI with multi-step research"
self.capabilities = [ self.capabilities = [
"webSearch", "web_research",
"informationRetrieval", "content_gathering",
"dataCollection", "data_extraction",
"searchResultsAnalysis", "information_synthesis",
"webpageContentExtraction" "source_verification"
] ]
# Web crawling configuration # Web crawling configuration
@ -49,9 +50,9 @@ class AgentWebcrawler(AgentBase):
if not self.srcApikey: if not self.srcApikey:
logger.error("SerpAPI key not configured") logger.error("SerpAPI key not configured")
def setDependencies(self, serviceBase=None):
def setDependencies(self, mydom=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
""" """
@ -66,13 +67,14 @@ class AgentWebcrawler(AgentBase):
try: try:
# Extract task information # Extract task information
prompt = task.get("prompt", "") prompt = task.get("prompt", "")
inputDocuments = task.get("inputDocuments", [])
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.get("outputSpecifications", [])
workflow = task.get("context", {}).get("workflow", {}) workflow = task.get("context", {}).get("workflow", {})
# Check AI service # Check AI service
if not self.mydom: if not self.service or not self.service.base:
return { return {
"feedback": "The Webcrawler agent requires an AI service to function effectively.", "feedback": "The Web Crawler agent requires an AI service to function.",
"documents": [] "documents": []
} }
@ -147,10 +149,10 @@ class AgentWebcrawler(AgentBase):
try: try:
# Get research plan from AI # Get research plan from AI
response = await self.mydom.callAi([ response = await self.service.base.callAi([
{"role": "system", "content": "You are a web research planning expert. Create precise research plans. Respond with valid JSON only."}, {"role": "system", "content": "You are a research expert. Respond with valid JSON only."},
{"role": "user", "content": researchPrompt} {"role": "user", "content": researchPrompt}
], produceUserAnswer=True) ])
# Extract JSON # Extract JSON
jsonStart = response.find('{') jsonStart = response.find('{')
@ -316,10 +318,10 @@ class AgentWebcrawler(AgentBase):
""" """
# Get summary from AI # Get summary from AI
summary = await self.mydom.callAi([ summary = await self.service.base.callAi([
{"role": "system", "content": "You are a web content summarization expert. Create concise summaries."}, {"role": "system", "content": "You are a research expert. Respond with valid JSON only."},
{"role": "user", "content": summaryPrompt} {"role": "user", "content": summaryPrompt}
], produceUserAnswer=True) ])
# Add summary to result # Add summary to result
result["summary"] = summary.strip() result["summary"] = summary.strip()
@ -465,8 +467,8 @@ class AgentWebcrawler(AgentBase):
try: try:
# Generate report with AI # Generate report with AI
reportContent = await self.mydom.callAi([ reportContent = await self.service.base.callAi([
{"role": "system", "content": f"You create professional research reports in {templateFormat} format."}, {"role": "system", "content": "You are a research expert. Respond with valid JSON only."},
{"role": "user", "content": reportPrompt} {"role": "user", "content": reportPrompt}
]) ])
@ -616,10 +618,10 @@ class AgentWebcrawler(AgentBase):
if not self.srcApikey: if not self.srcApikey:
return [] return []
# Get user language from mydom if available # Get user language from serviceBase if available
userLanguage = "en" # Default language userLanguage = "en" # Default language
if self.mydom.userLanguage: if self.service.base.userLanguage:
userLanguage = self.mydom.userLanguage userLanguage = self.service.base.userLanguage
try: try:
# Format the search request for SerpAPI # Format the search request for SerpAPI

View file

@ -562,7 +562,7 @@ class DatabaseConnector:
"""Returns the initial ID for a table.""" """Returns the initial ID for a table."""
systemData = self._loadSystemTable() systemData = self._loadSystemTable()
initialId = systemData.get(table) initialId = systemData.get(table)
logger.debug(f"Database '{self.dbDatabase}': Initial ID for table '{table}' is {initialId}") logger.debug(f"Database '{self.dbDatabase}': Table: {systemData}, Initial ID for table '{table}' is {initialId}")
if initialId is None: if initialId is None:
logger.debug(f"No initial ID found for table {table}") logger.debug(f"No initial ID found for table {table}")
return initialId return initialId

View file

@ -1,27 +1,40 @@
""" """
Access control functions for the Gateway system. Access control module for Gateway interface.
Manages user access and permissions. Handles user access management and permission checks.
""" """
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]], _mandateId: int, _userId: int, db) -> List[Dict[str, Any]]: class GatewayAccess:
"""
Access control class for Gateway interface.
Handles user access management and permission checks.
"""
def __init__(self, currentUser: Dict[str, Any], db):
"""Initialize with user context."""
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
self.db = db
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 Unified user access management function that filters data based on user privileges
and adds access control attributes. and adds access control attributes.
Args: Args:
currentUser: Current user information dictionary
table: Name of the table table: Name of the table
recordset: Recordset to filter based on access rules recordset: Recordset to filter based on access rules
_mandateId: Current mandate ID
_userId: Current user ID
db: Database connector instance
Returns: Returns:
Filtered recordset with access control attributes Filtered recordset with access control attributes
""" """
userPrivilege = currentUser.get("privilege", "user") userPrivilege = self.currentUser.get("privilege", "user")
filtered_records = [] filtered_records = []
# Apply filtering based on privilege # Apply filtering based on privilege
@ -29,11 +42,11 @@ def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]
filtered_records = recordset # System admins see all records filtered_records = recordset # System admins see all records
elif userPrivilege == "admin": elif userPrivilege == "admin":
# Admins see records in their mandate # Admins see records in their mandate
filtered_records = [r for r in recordset if r.get("_mandateId") == _mandateId] filtered_records = [r for r in recordset if r.get("_mandateId") == self._mandateId]
else: # Regular users else: # Regular users
# Users only see records they own within their mandate # Users only see records they own within their mandate
filtered_records = [r for r in recordset filtered_records = [r for r in recordset
if r.get("_mandateId") == _mandateId and r.get("_userId") == _userId] if r.get("_mandateId") == self._mandateId and r.get("_userId") == self._userId]
# Add access control attributes to each record # Add access control attributes to each record
for record in filtered_records: for record in filtered_records:
@ -42,36 +55,32 @@ def _uam(currentUser: Dict[str, Any], table: str, recordset: List[Dict[str, Any]
# Set access control flags based on user permissions # Set access control flags based on user permissions
if table == "mandates": if table == "mandates":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db) record["_hideEdit"] = not self.canModify("mandates", record_id)
record["_hideDelete"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db) record["_hideDelete"] = not self.canModify("mandates", record_id)
elif table == "users": elif table == "users":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db) record["_hideEdit"] = not self.canModify("users", record_id)
record["_hideDelete"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db) record["_hideDelete"] = not self.canModify("users", record_id)
else: else:
# Default access control for other tables # Default access control for other tables
record["_hideView"] = False record["_hideView"] = False
record["_hideEdit"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db) record["_hideEdit"] = not self.canModify(table, record_id)
record["_hideDelete"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db) record["_hideDelete"] = not self.canModify(table, record_id)
return filtered_records return filtered_records
def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int] = None, _mandateId: int = None, _userId: int = None, db = None) -> bool: def canModify(self, table: str, recordId: Optional[int] = None) -> bool:
""" """
Checks if the current user can modify (create/update/delete) records in a table. Checks if the current user can modify (create/update/delete) records in a table.
Args: Args:
currentUser: Current user information dictionary
table: Name of the table table: Name of the table
recordId: Optional record ID for specific record check recordId: Optional record ID for specific record check
_mandateId: Current mandate ID
_userId: Current user ID
db: Database connector instance
Returns: Returns:
Boolean indicating permission Boolean indicating permission
""" """
userPrivilege = currentUser.get("privilege", "user") userPrivilege = self.currentUser.get("privilege", "user")
# System admins can modify anything # System admins can modify anything
if userPrivilege == "sysadmin": if userPrivilege == "sysadmin":
@ -80,22 +89,22 @@ def _canModify(currentUser: Dict[str, Any], table: str, recordId: Optional[int]
# Check specific record permissions # Check specific record permissions
if recordId is not None: if recordId is not None:
# Get the record to check ownership # Get the record to check ownership
records = db.getRecordset(table, recordFilter={"id": recordId}) records = self.db.getRecordset(table, recordFilter={"id": recordId})
if not records: if not records:
return False return False
record = records[0] record = records[0]
# Admins can modify anything in their mandate # Admins can modify anything in their mandate
if userPrivilege == "admin" and record.get("_mandateId") == _mandateId: if userPrivilege == "admin" and record.get("_mandateId") == self._mandateId:
# Exception: Can't modify Root mandate unless you are a sysadmin # Exception: Can't modify Root mandate unless you are a sysadmin
if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin": if table == "mandates" and record.get("initialid") and userPrivilege != "sysadmin":
return False return False
return True return True
# Users can only modify their own records # Users can only modify their own records
if (record.get("_mandateId") == _mandateId and if (record.get("_mandateId") == self._mandateId and
record.get("_userId") == _userId): record.get("_userId") == self._userId):
return True return True
return False return False

View file

@ -3,18 +3,24 @@ Interface to the Gateway system.
Manages users and mandates for authentication. Manages users and mandates for authentication.
""" """
from datetime import datetime
import os import os
import logging import logging
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
import importlib import importlib
import json
from passlib.context import CryptContext from passlib.context import CryptContext
from modules.connectors.connectorDbJson import DatabaseConnector from modules.connectors.connectorDbJson import DatabaseConnector
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.gatewayAccess import _uam, _canModify from modules.interfaces.gatewayAccess import GatewayAccess
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Singleton factory for GatewayInterface instances per context
_gatewayInterfaces = {}
# Password-Hashing # Password-Hashing
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto") pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
@ -25,23 +31,63 @@ class GatewayInterface:
Manages users and mandates. Manages users and mandates.
""" """
def __init__(self, _mandateId: str = None, _userId: str = None): def __init__(self, currentUser: Dict[str, Any]):
"""Initializes the Gateway Interface with optional mandate and user context.""" """Initializes the Gateway Interface with user context."""
# Context can be empty during initialization
self._mandateId = _mandateId # Ensure valid DB
self._userId = _userId
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid initial context: _mandateId and id are required")
# Initialize database # Initialize database
self._initializeDatabase() self._initializeDatabase()
# Load user information
self.currentUser = self._getCurrentUserInfo()
# Initialize standard records if needed # Initialize standard records if needed
self._initRecords() self._initRecords()
# Set user context
if currentUser.get("id") == "-1":
logger.debug(f"Initializing GatewayInterface with Root User")
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
self._initializeDatabase()
mandateId = self.getInitialId("mandates")
userId = self.getInitialId("users")
currentUser = {
"_mandateId": mandateId,
"id": userId
}
logger.debug(f"Initializing GatewayInterface with rootUser={currentUser}")
else:
logger.debug(f"Initializing GatewayInterface with currentUser={currentUser}")
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Add language settings
self.userLanguage = currentUser.get("language", "en") # Default user language
# Initialize database
self._initializeDatabase()
# Initialize access control
self.access = GatewayAccess(self.currentUser, self.db)
def _initializeDatabase(self): def _initializeDatabase(self):
"""Initializes the database connection.""" """Initializes the database connection."""
try:
# Get configuration values with defaults # Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data") dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway") dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway")
@ -60,6 +106,14 @@ class GatewayInterface:
_userId=self._userId _userId=self._userId
) )
# Set context
self.db.updateContext(self._mandateId, self._userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _getCurrentUserInfo(self) -> Optional[Dict[str, Any]]: def _getCurrentUserInfo(self) -> Optional[Dict[str, Any]]:
"""Returns information about the current user.""" """Returns information about the current user."""
if not self._userId: if not self._userId:
@ -72,9 +126,13 @@ class GatewayInterface:
def _initRecords(self): def _initRecords(self):
"""Initializes standard records in the database if they don't exist.""" """Initializes standard records in the database if they don't exist."""
self._initRootMandate()
self._initAdminUser()
self._initRootMandate()
# Update database context with new IDs
if self._mandateId and self._userId:
self.db.updateContext(self._mandateId, self._userId)
self._initAdminUser()
# Update database context with new IDs # Update database context with new IDs
if self._mandateId and self._userId: if self._mandateId and self._userId:
self.db.updateContext(self._mandateId, self._userId) self.db.updateContext(self._mandateId, self._userId)
@ -90,7 +148,7 @@ class GatewayInterface:
logger.info("Creating Root mandate") logger.info("Creating Root mandate")
rootMandate = { rootMandate = {
"name": "Root", "name": "Root",
"language": "de" "language": "en"
} }
createdMandate = self.db.recordCreate("mandates", rootMandate) createdMandate = self.db.recordCreate("mandates", rootMandate)
logger.info(f"Root mandate created with ID {createdMandate['id']}") logger.info(f"Root mandate created with ID {createdMandate['id']}")
@ -113,7 +171,7 @@ class GatewayInterface:
"email": "admin@example.com", "email": "admin@example.com",
"fullName": "Administrator", "fullName": "Administrator",
"disabled": False, "disabled": False,
"language": "de", "language": "en",
"privilege": "sysadmin", "privilege": "sysadmin",
"authenticationAuthority": "local", "authenticationAuthority": "local",
"hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production! "hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production!
@ -139,7 +197,7 @@ class GatewayInterface:
Returns: Returns:
Filtered recordset with access control attributes Filtered recordset with access control attributes
""" """
return _uam(self.currentUser, table, recordset, self._mandateId, self._userId, self.db) return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool: def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
""" """
@ -152,7 +210,7 @@ class GatewayInterface:
Returns: Returns:
Boolean indicating permission Boolean indicating permission
""" """
return _canModify(self.currentUser, table, recordId, self._mandateId, self._userId, self.db) return self.access.canModify(table, recordId)
def getInitialId(self, table: str) -> Optional[str]: def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table.""" """Returns the initial ID for a table."""
@ -187,7 +245,7 @@ class GatewayInterface:
filteredMandates = self._uam("mandates", mandates) filteredMandates = self._uam("mandates", mandates)
return filteredMandates[0] if filteredMandates else None return filteredMandates[0] if filteredMandates else None
def createMandate(self, name: str, language: str = "de") -> Dict[str, Any]: def createMandate(self, name: str, language: str = "en") -> Dict[str, Any]:
"""Creates a new mandate if user has permission.""" """Creates a new mandate if user has permission."""
if not self._canModify("mandates"): if not self._canModify("mandates"):
raise PermissionError("No permission to create mandates") raise PermissionError("No permission to create mandates")
@ -322,7 +380,7 @@ class GatewayInterface:
return user return user
def createUser(self, username: str, password: str = None, email: str = None, fullName: str = None, def createUser(self, username: str, password: str = None, email: str = None, fullName: str = None,
language: str = "de", _mandateId: int = None, disabled: bool = False, language: str = "en", _mandateId: int = None, disabled: bool = False,
privilege: str = "user", authenticationAuthority: str = "local") -> Dict[str, Any]: privilege: str = "user", authenticationAuthority: str = "local") -> Dict[str, Any]:
"""Create a new user""" """Create a new user"""
try: try:
@ -422,7 +480,6 @@ class GatewayInterface:
return authenticatedUser return authenticatedUser
def updateUser(self, _userId: str, userData: Dict[str, Any]) -> Dict[str, Any]: def updateUser(self, _userId: str, userData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a user if current user has permission.""" """Updates a user if current user has permission."""
# Check if the user exists and current user has access # Check if the user exists and current user has access
@ -512,20 +569,81 @@ class GatewayInterface:
return success return success
# Microsoft Login
# Singleton factory for GatewayInterface instances per context def getMsftToken(self) -> Optional[Dict[str, Any]]:
_gatewayInterfaces = {} """Get Microsoft token data for the current user from database"""
try:
# Get token from database using current user's mandateId and userId
tokens = self.db.getRecordset("msftTokens", recordFilter={
"_mandateId": self._mandateId,
"_userId": self._userId
})
def getGatewayInterface(_mandateId: str = None, _userId: str = None) -> GatewayInterface: if tokens and len(tokens) > 0:
token_data = json.loads(tokens[0]["token_data"])
logger.debug(f"Retrieved Microsoft token for user {self._userId}")
return token_data
else:
logger.debug(f"No Microsoft token found for user {self._userId}")
return None
except Exception as e:
logger.error(f"Error retrieving Microsoft token: {str(e)}")
return None
def saveMsftToken(self, token_data: Dict[str, Any]) -> bool:
"""Save Microsoft token data for the current user to database"""
try:
# Check if token already exists
tokens = self.db.getRecordset("msftTokens", recordFilter={
"_mandateId": self._mandateId,
"_userId": self._userId
})
if tokens and len(tokens) > 0:
# Update existing token
token_id = tokens[0]["id"]
updated_data = {
"token_data": json.dumps(token_data),
"updated_at": datetime.now().isoformat()
}
self.db.recordModify("msftTokens", token_id, updated_data)
logger.debug(f"Updated Microsoft token for user {self._userId}")
else:
# Create new token with UUID
new_token = {
"_mandateId": self._mandateId,
"_userId": self._userId,
"token_data": json.dumps(token_data),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.db.recordCreate("msftTokens", new_token)
logger.debug(f"Saved new Microsoft token for user {self._userId}")
return True
except Exception as e:
logger.error(f"Error saving Microsoft token: {str(e)}")
return False
def getInterface(currentUser: Dict[str, Any]) -> 'GatewayInterface':
""" """
Returns a GatewayInterface instance for the specified context. Returns a GatewayInterface instance for the current user.
Reuses existing instances. Handles initialization of database and records.
""" """
# For initialization, use empty strings instead of None mandateId = currentUser.get("_mandateId")
contextKey = f"{_mandateId or ''}_{_userId or ''}" userId = currentUser.get("id")
if not mandateId or not userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Create context key
contextKey = f"{mandateId}_{userId}"
# Create new instance if not exists
if contextKey not in _gatewayInterfaces: if contextKey not in _gatewayInterfaces:
_gatewayInterfaces[contextKey] = GatewayInterface(_mandateId or '', _userId or '') _gatewayInterfaces[contextKey] = GatewayInterface(currentUser)
return _gatewayInterfaces[contextKey]
# Initialize an instance with empty strings return _gatewayInterfaces[contextKey]
getGatewayInterface('', '')

View file

@ -6,6 +6,12 @@ from typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
import uuid import uuid
# Get all attributes of the model
def getModelAttributes(modelClass):
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): class Label(BaseModel):
"""Label for an attribute or a class with support for multiple languages""" """Label for an attribute or a class with support for multiple languages"""

View file

@ -5,19 +5,24 @@ Handles user access management and permission checks.
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
class LucyDOMAccess: class LucydomAccess:
""" """
Access control class for LucyDOM interface. Access control class for LucyDOM interface.
Handles user access management and permission checks. Handles user access management and permission checks.
""" """
def __init__(self, currentUser: Dict[str, Any], _mandateId: int, _userId: int): def __init__(self, currentUser: Dict[str, Any], db):
"""Initialize with user context.""" """Initialize with user context."""
self.currentUser = currentUser self.currentUser = currentUser
self._mandateId = _mandateId self._mandateId = currentUser.get("_mandateId")
self._userId = _userId self._userId = currentUser.get("id")
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
self.db = db
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 Unified user access management function that filters data based on user privileges
and adds access control attributes. and adds access control attributes.
@ -30,6 +35,7 @@ class LucyDOMAccess:
Filtered recordset with access control attributes Filtered recordset with access control attributes
""" """
userPrivilege = self.currentUser.get("privilege", "user") userPrivilege = self.currentUser.get("privilege", "user")
print("DEBUG: User privilege:", userPrivilege, self.currentUser.get("username"),self.currentUser.get("email"))
filtered_records = [] filtered_records = []
# Apply filtering based on privilege # Apply filtering based on privilege
@ -54,40 +60,34 @@ class LucyDOMAccess:
# Set access control flags based on user permissions # Set access control flags based on user permissions
if table == "prompts": if table == "prompts":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
# Only allow modification of own prompts or if admin/sysadmin record["_hideEdit"] = not self.canModify("prompts", record_id)
can_modify = ( record["_hideDelete"] = not self.canModify("prompts", record_id)
userPrivilege == "sysadmin" or
(userPrivilege == "admin" and record.get("_mandateId") == self._mandateId) or
(record.get("_mandateId") == self._mandateId and record.get("_userId") == self._userId)
)
record["_hideEdit"] = not can_modify
record["_hideDelete"] = not can_modify
elif table == "files": elif table == "files":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("files", record_id) record["_hideEdit"] = not self.canModify("files", record_id)
record["_hideDelete"] = not self._canModify("files", record_id) record["_hideDelete"] = not self.canModify("files", record_id)
record["_hideDownload"] = not self._canModify("files", record_id) record["_hideDownload"] = not self.canModify("files", record_id)
elif table == "workflows": elif table == "workflows":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("workflows", record_id) record["_hideEdit"] = not self.canModify("workflows", record_id)
record["_hideDelete"] = not self._canModify("workflows", record_id) record["_hideDelete"] = not self.canModify("workflows", record_id)
elif table == "workflowMessages": elif table == "workflowMessages":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("workflows", record.get("workflowId")) record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId"))
record["_hideDelete"] = not self._canModify("workflows", record.get("workflowId")) record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId"))
elif table == "workflowLogs": elif table == "workflowLogs":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self._canModify("workflows", record.get("workflowId")) record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId"))
record["_hideDelete"] = not self._canModify("workflows", record.get("workflowId")) record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId"))
else: else:
# Default access control for other tables # Default access control for other tables
record["_hideView"] = False record["_hideView"] = False
record["_hideEdit"] = not self._canModify(table, record_id) record["_hideEdit"] = not self.canModify(table, record_id)
record["_hideDelete"] = not self._canModify(table, record_id) record["_hideDelete"] = not self.canModify(table, record_id)
return filtered_records return filtered_records
def _canModify(self, table: str, recordId: Optional[int] = None) -> bool: def canModify(self, table: str, recordId: Optional[int] = None) -> bool:
""" """
Checks if the current user can modify (create/update/delete) records in a table. Checks if the current user can modify (create/update/delete) records in a table.

View file

@ -9,12 +9,10 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
import importlib
import hashlib import hashlib
import json
from modules.shared.mimeUtils import isTextMimeType, determineContentEncoding from modules.shared.mimeUtils import isTextMimeType
from modules.interfaces.lucydomAccess import LucyDOMAccess from modules.interfaces.lucydomAccess import LucydomAccess
# DYNAMIC PART: Connectors to the Interface # DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbJson import DatabaseConnector from modules.connectors.connectorDbJson import DatabaseConnector
@ -24,23 +22,8 @@ from modules.connectors.connectorAiOpenai import ChatService
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Initialize AI service at module level # Singleton factory for Lucydom instances with AI service per context
_aiService = None _lucydomInterfaces = {}
def initializeAIService():
"""Initialize the AI service for the LucyDOM interface."""
global _aiService
if _aiService is None:
try:
_aiService = ChatService()
logger.info("AI service initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize AI service: {str(e)}")
_aiService = None
return _aiService
# Initialize AI service when module is imported
initializeAIService()
# Custom exceptions for file handling # Custom exceptions for file handling
class FileError(Exception): class FileError(Exception):
@ -63,84 +46,83 @@ class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file.""" """Exception raised when there's an error deleting a file."""
pass pass
from modules.security.auth import getInitialContext class LucydomInterface:
class LucyDOMInterface:
""" """
Interface to the LucyDOM database. Interface to the LucyDOM database.
Uses the JSON connector for data access. Uses the JSON connector for data access.
""" """
def __init__(self, _mandateId: str, _userId: str): def __init__(self, currentUser: Dict[str, Any]):
"""Initializes the LucyDOM Interface with mandate and user context.""" """Initializes the LucyDOM Interface with user context."""
logger.debug(f"Initializing LucyDOMInterface with mandateId={_mandateId}, userId={_userId}") logger.debug(f"Initializing LucydomInterface with currentUser={currentUser}")
self._mandateId = _mandateId
self._userId = _userId # Ensure valid user context
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Add language settings # Add language settings
self.userLanguage = "en" # Default user language self.userLanguage = currentUser.get("language", "en") # Default user language
# Set AI service from module-level instance # Initialize database
self.aiService = _aiService
if not self.aiService:
logger.warning("AI service not available during LucyDOMInterface initialization")
# Initialize database connector
self._initializeDatabase() self._initializeDatabase()
# Load user information # Initialize standard records
self.currentUser = self._getCurrentUserInfo()
# Initialize access control
self.access = LucyDOMAccess(self.currentUser, self._mandateId, self._userId)
self.access.db = self.db # Share database connection
# Get initial IDs if not provided
if not self._mandateId or not self._userId:
logger.debug("No context provided, getting initial context from auth")
self._mandateId, self._userId = getInitialContext()
logger.debug(f"Retrieved initial context: mandate={self._mandateId}, user={self._userId}")
if self._mandateId and self._userId:
self.db.updateContext(self._mandateId, self._userId)
logger.debug(f"Updated database context with initial IDs")
else:
logger.warning("No initial context available from auth")
# Initialize standard records if needed
self._initRecords() self._initRecords()
def _getCurrentUserInfo(self) -> Dict[str, Any]: # Initialize AI service
"""Gets information about the current user including privileges.""" self.aiService = ChatService()
# For production, you would get this from authentication if not self.aiService:
# For now return basic user info with default privilege logger.warning("AI service not available during LucydomInterface initialization")
return {
"id": self._userId, # Initialize access control
"_mandateId": self._mandateId, self.access = LucydomAccess(self.currentUser, self.db)
"privilege": "user", # Default privilege level
"language": self.userLanguage
}
def _initializeDatabase(self): def _initializeDatabase(self):
"""Initializes the database connection.""" """Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_LUCYDOM_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_LUCYDOM_DATABASE", "lucydom")
dbUser = APP_CONFIG.get("DB_LUCYDOM_USER")
dbPassword = APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector( self.db = DatabaseConnector(
dbHost=APP_CONFIG.get("DB_LUCYDOM_HOST"), dbHost=dbHost,
dbDatabase=APP_CONFIG.get("DB_LUCYDOM_DATABASE"), dbDatabase=dbDatabase,
dbUser=APP_CONFIG.get("DB_LUCYDOM_USER"), dbUser=dbUser,
dbPassword=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"), dbPassword=dbPassword,
_mandateId=self._mandateId, _mandateId=self._mandateId,
_userId=self._userId, _userId=self._userId
skipInitialIdLookup=True
) )
# Set context
self.db.updateContext(self._mandateId, self._userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _initRecords(self): def _initRecords(self):
"""Initializes standard records in the database if they don't exist.""" """Initializes standard records in the database if they don't exist."""
# Only initialize prompts if we have valid context try:
if self._mandateId and self._userId: # Initialize standard prompts
logger.debug(f"Initializing prompts with context: mandate={self._mandateId}, user={self._userId}")
self._initializeStandardPrompts() self._initializeStandardPrompts()
else:
logger.warning("Skipping prompt initialization - no valid context available") # Add other record initializations here
logger.info("Standard records initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize standard records: {str(e)}")
raise
def _initializeStandardPrompts(self): def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist.""" """Creates standard prompts if they don't exist."""
@ -187,11 +169,11 @@ class LucyDOMInterface:
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module.""" """Delegate to access control module."""
return self.access._uam(table, recordset) return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool: def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module.""" """Delegate to access control module."""
return self.access._canModify(table, recordId) return self.access.canModify(table, recordId)
# Language support method # Language support method
@ -205,7 +187,7 @@ class LucyDOMInterface:
async def callAi(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str: async def callAi(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str:
"""Enhanced AI service call with language support.""" """Enhanced AI service call with language support."""
if not self.aiService: if not self.aiService:
logger.error("AI service not set in LucyDOMInterface") logger.error("AI service not set in LucydomInterface")
return "Error: AI service not available" return "Error: AI service not available"
# Add language instruction for user-facing responses # Add language instruction for user-facing responses
@ -230,7 +212,7 @@ class LucyDOMInterface:
async def callAi4Image(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str: async def callAi4Image(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str:
"""Enhanced AI service call with language support.""" """Enhanced AI service call with language support."""
if not self.aiService: if not self.aiService:
logger.error("AI service not set in LucyDOMInterface") logger.error("AI service not set in LucydomInterface")
return "Error: AI service not available" return "Error: AI service not available"
return await self.aiService.analyzeImage(imageData, mimeType, prompt) return await self.aiService.analyzeImage(imageData, mimeType, prompt)
@ -1237,85 +1219,24 @@ class LucyDOMInterface:
logger.error(f"Error loading workflow state: {str(e)}") logger.error(f"Error loading workflow state: {str(e)}")
return None return None
# Microsoft Login
def getMsftToken(self) -> Optional[Dict[str, Any]]: def getInterface(currentUser: Dict[str, Any]) -> 'LucydomInterface':
"""Get Microsoft token data for the current user from database"""
try:
# Get token from database using current user's mandateId and userId
tokens = self.db.getRecordset("msftTokens", recordFilter={
"_mandateId": self._mandateId,
"_userId": self._userId
})
if tokens and len(tokens) > 0:
token_data = json.loads(tokens[0]["token_data"])
logger.debug(f"Retrieved Microsoft token for user {self._userId}")
return token_data
else:
logger.debug(f"No Microsoft token found for user {self._userId}")
return None
except Exception as e:
logger.error(f"Error retrieving Microsoft token: {str(e)}")
return None
def saveMsftToken(self, token_data: Dict[str, Any]) -> bool:
"""Save Microsoft token data for the current user to database"""
try:
# Check if token already exists
tokens = self.db.getRecordset("msftTokens", recordFilter={
"_mandateId": self._mandateId,
"_userId": self._userId
})
if tokens and len(tokens) > 0:
# Update existing token
token_id = tokens[0]["id"]
updated_data = {
"token_data": json.dumps(token_data),
"updated_at": datetime.now().isoformat()
}
self.db.recordModify("msftTokens", token_id, updated_data)
logger.debug(f"Updated Microsoft token for user {self._userId}")
else:
# Create new token with UUID
new_token = {
"_mandateId": self._mandateId,
"_userId": self._userId,
"token_data": json.dumps(token_data),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.db.recordCreate("msftTokens", new_token)
logger.debug(f"Saved new Microsoft token for user {self._userId}")
return True
except Exception as e:
logger.error(f"Error saving Microsoft token: {str(e)}")
return False
# Singleton factory for LucyDOMInterface instances per context
_lucydomInterfaces = {}
def getLucydomInterface(_mandateId: str = None, _userId: str = None) -> LucyDOMInterface:
""" """
Returns a LucyDOMInterface instance for the specified context. Returns a LucydomInterface instance for the current user.
Ensures AI service is initialized and preserves it across instances. Handles initialization of database and records.
""" """
# For initialization, use empty strings instead of None # Get user context
contextKey = f"{_mandateId or ''}_{_userId or ''}" mandateId = currentUser.get("_mandateId")
userId = currentUser.get("id")
# Ensure AI service is initialized if not mandateId or not userId:
if _aiService is None: raise ValueError("Invalid user context: _mandateId and id are required")
initializeAIService()
# Create new instance if needed # Create context key
contextKey = f"{mandateId}_{userId}"
# Create new instance if not exists
if contextKey not in _lucydomInterfaces: if contextKey not in _lucydomInterfaces:
_lucydomInterfaces[contextKey] = LucyDOMInterface(_mandateId or '', _userId or '') _lucydomInterfaces[contextKey] = LucydomInterface(currentUser)
return _lucydomInterfaces[contextKey] return _lucydomInterfaces[contextKey]
# Initialize default instance with empty strings
getLucydomInterface('', '')

View file

@ -1,265 +0,0 @@
"""
LucyDOM model classes for the workflow and document system.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
class Label(BaseModel):
"""Label for an attribute or a class with support for multiple languages"""
default: str
translations: Dict[str, str] = {}
def getLabel(self, language: str = None):
"""Returns the label in the specified language, or the default value if not available"""
if language and language in self.translations:
return self.translations[language]
return self.default
class Prompt(BaseModel):
"""Data model for a prompt"""
id: int = Field(description="Unique ID of the prompt")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the creator")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Display name of the prompt")
label: Label = Field(
default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
"name": Label(default="Name", translations={"en": "Label", "fr": "Nom"})
}
class FileItem(BaseModel):
"""Data model for a file"""
id: int = Field(description="Unique ID of the data object")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the creator")
name: str = Field(description="Name of the data object")
mimeType: str = Field(description="Type of the data object MIME type")
size: Optional[int] = Field(None, description="Size of the data object in bytes")
fileHash: str = Field(description="Hash code for deduplication")
creationDate: Optional[str] = Field(None, description="Upload date")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
label: Label = Field(
default=Label(default="Data Object", translations={"en": "Data Object", "fr": "Objet de données"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"mimeType": Label(default="Type", translations={"en": "Type", "fr": "Type"}),
"size": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
"fileHash": Label(default="File Hash", translations={"en": "Hash", "fr": "Hash"}),
"creationDate": Label(default="Upload date", translations={"en": "Upload date", "fr": "Date de téléchargement"}),
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"})
}
class FileData(BaseModel):
"""Data model for file content"""
id: int = Field(description="Unique ID of the data object")
data: str = Field(description="content of the file, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
class MsftToken(BaseModel):
"""Data model for Microsoft authentication tokens"""
id: int = Field(description="Unique ID of the token")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the user")
token_data: str = Field(description="JSON string containing the token data")
created_at: str = Field(description="Timestamp when the token was created")
updated_at: str = Field(description="Timestamp when the token was last updated")
label: Label = Field(
default=Label(default="Microsoft Token", translations={"en": "Microsoft Token", "fr": "Jeton Microsoft"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"token_data": Label(default="Token Data", translations={"en": "Token Data", "fr": "Données du jeton"}),
"created_at": Label(default="Created At", translations={"en": "Created At", "fr": "Créé le"}),
"updated_at": Label(default="Updated At", translations={"en": "Updated At", "fr": "Mis à jour le"})
}
# Workflow model classes
class DocumentContent(BaseModel):
"""Content of a document in the workflow"""
sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
name: str = Field(description="Designation")
ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
mimeType: str = Field(description="MIME type")
summary: str = Field(description="Summary of the file content")
data: str = Field(description="Actual content, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content, such as isText flag, format information, encoding, etc.")
class Document(BaseModel):
"""Document in the workflow - References a file directly in the database"""
id: str = Field(description="Unique ID of the document")
name: str = Field(description="Name of the data object")
ext: str = Field(description="Extension of the data object")
fileId: int = Field(description="ID of the referenced file in the database")
mimeType: str = Field(description="MIME type")
data: str = Field(description="Content of the data as text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
contents: List[DocumentContent] = Field(description="Document contents")
class DataStats(BaseModel):
"""Statistics for performance and data usage"""
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Token count (for AI models)")
bytesSent: Optional[int] = Field(None, description="Bytes sent")
bytesReceived: Optional[int] = Field(None, description="Bytes received")
class WorkflowMessage(BaseModel):
"""Message object in the workflow"""
id: str = Field(description="Unique ID of the message")
workflowId: str = Field(description="Reference to the parent workflow")
parentMessageId: Optional[str] = Field(None, description="Reference to the replied message")
startedAt: str = Field(description="Timestamp for message creation")
finishedAt: Optional[str] = Field(None, description="Timestamp for message completion")
sequenceNo: int = Field(description="Sequence number for sorting")
status: str = Field(description="Status of the message ('first', 'step', 'last')")
role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
dataStats: Optional[DataStats] = Field(None, description="Statistics")
documents: Optional[List[Document]] = Field(None, description="Documents in this message (references to files in the database)")
content: Optional[str] = Field(None, description="Text content of the message")
agentName: Optional[str] = Field(None, description="Name of the agent used")
class WorkflowLog(BaseModel):
"""Log entry for a workflow"""
id: str = Field(description="Unique ID of the log entry")
workflowId: str = Field(description="ID of the associated workflow")
message: str = Field(description="Log message content")
type: str = Field(description="Type of log ('info', 'warning', 'error')")
timestamp: str = Field(description="Timestamp of the log entry")
agentName: str = Field(description="Name of the agent that created the log")
status: str = Field(description="Status of the workflow at log time")
progress: Optional[int] = Field(None, description="Progress value (0-100)")
mandateId: Optional[int] = Field(None, description="ID of the mandate")
userId: Optional[int] = Field(None, description="ID of the user")
class Workflow(BaseModel):
"""Workflow object for multi-agent system"""
id: str = Field(description="Unique ID of the workflow")
name: Optional[str] = Field(None, description="Name of the workflow")
mandateId: int = Field(description="ID of the mandate")
userId: int = Field(description="ID of the user")
status: str = Field(description="Status of the workflow ('running', 'completed', 'failed', 'stopped')")
startedAt: str = Field(description="Start timestamp")
lastActivity: str = Field(description="Timestamp of the last activity")
dataStats: Optional[Dict[str, Any]] = Field(None, description="Total statistics")
currentRound: int = Field(default=1, description="Current round/iteration of the workflow")
messageIds: List[str] = Field(default=[], description="List of message IDs in this workflow")
messages: List[WorkflowMessage] = Field(default=[], description="Message history (in-memory representation)")
logs: List[WorkflowLog] = Field(default=[], description="Log entries (in-memory representation)")
# Agent and Workflow Task Models
class AgentResult(BaseModel):
"""Result structure returned by agent processing"""
feedback: str = Field(description="Text response explaining what the agent did")
documents: List[Document] = Field(default=[], description="List of document objects created by the agent")
label: Label = Field(
default=Label(default="Agent Result", translations={"en": "Agent Result", "fr": "Résultat d'agent"}),
description="Label for the class"
)
class AgentInfo(BaseModel):
"""Information about an agent's capabilities"""
name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent's functionality")
capabilities: List[str] = Field(default=[], description="List of agent capabilities")
label: Label = Field(
default=Label(default="Agent Information", translations={"en": "Agent Information", "fr": "Information d'agent"}),
description="Label for the class"
)
class InputDocument(BaseModel):
"""Input document specification for a task"""
label: str = Field(description="Document label in the format 'filename.ext'")
fileId: Optional[int] = Field(None, description="ID of the existing document if referring to one")
contentPart: str = Field(default="", description="Content part to focus on, empty string for all contents")
prompt: str = Field(description="AI prompt to describe what data to extract from the file")
class OutputDocument(BaseModel):
"""Output document specification for a task"""
label: str = Field(description="Document label in the format 'filename.ext'")
prompt: str = Field(description="AI prompt to describe the content of the file")
class TaskItem(BaseModel):
"""Individual task in the workplan"""
agent: str = Field(description="Name of an available agent")
prompt: str = Field(description="Specific instructions to the agent, that he knows what to do with which documents and which output to provide")
outputDocuments: List[OutputDocument] = Field(default=[], description="List of required output documents")
inputDocuments: List[InputDocument] = Field(default=[], description="List of input documents to process")
label: Label = Field(
default=Label(default="Task Item", translations={"en": "Task Item", "fr": "Élément de tâche"}),
description="Label for the class"
)
class TaskPlan(BaseModel):
"""Work plan created by project manager"""
objFinalDocuments: List[str] = Field(default=[], description="List of required result documents")
objWorkplan: List[TaskItem] = Field(default=[], description="Plan for executing agents")
objUserResponse: str = Field(description="Response to the user explaining the plan")
userLanguage: str = Field(default="en", description="Language code of the user's request")
label: Label = Field(
default=Label(default="Task Plan", translations={"en": "Task Plan", "fr": "Plan de tâches"}),
description="Label for the class"
)
class WorkflowStatus(BaseModel):
"""Workflow status messages"""
init: str = Field(default="Workflow initialized")
running: str = Field(default="Running workflow")
waiting: str = Field(default="Waiting for input")
completed: str = Field(default="Workflow completed successfully")
stopped: str = Field(default="Workflow stopped by user")
failed: str = Field(default="Error in workflow")
label: Label = Field(
default=Label(default="Workflow Status", translations={"en": "Workflow Status", "fr": "État du workflow"}),
description="Label for the class"
)
# Request models for the API
class UserInputRequest(BaseModel):
"""Request for user input to a running workflow"""
prompt: str = Field(description="Message from the user")
listFileId: List[int] = Field(default=[], description="List of FileItem IDs")

View file

@ -7,6 +7,12 @@ from typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
import uuid import uuid
# Get all attributes of the model
def getModelAttributes(modelClass):
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')]
# CORE MODELS # CORE MODELS
@ -73,23 +79,6 @@ class FileData(BaseModel):
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded") base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any") workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
class UserInputRequest(BaseModel):
"""Request for user input to a running workflow"""
prompt: str = Field(description="Message from the user")
listFileId: List[str] = Field(default=[], description="List of FileItem IDs")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for the request")
class MsftToken(BaseModel):
"""Data model for Microsoft authentication tokens"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the token")
tokenData: str = Field(description="JSON string containing the token data")
expiresAt: datetime = Field(description="Expiration date and time")
refreshToken: Optional[str] = Field(None, description="Refresh token if available")
scope: str = Field(description="Token scope")
# WORKFLOW MODELS # WORKFLOW MODELS
class ChatContent(BaseModel): class ChatContent(BaseModel):
@ -194,3 +183,11 @@ class TaskPlan(BaseModel):
taskItems: List[TaskItem] = Field(default=[], description="Plan for executing agents") taskItems: List[TaskItem] = Field(default=[], description="Plan for executing agents")
userResponse: str = Field(description="Response to the user explaining the plan") userResponse: str = Field(description="Response to the user explaining the plan")
userLanguage: str = Field(default="en", description="Language code of the user's request") userLanguage: str = Field(default="en", description="Language code of the user's request")
class UserInputRequest(BaseModel):
"""Request for user input to a running workflow"""
prompt: str = Field(description="Message from the user")
listFileId: List[str] = Field(default=[], description="List of FileItem IDs")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for the request")

View file

@ -0,0 +1,113 @@
"""
Access control module for Microsoft interface.
Handles user access management and permission checks for Microsoft tokens.
"""
from typing import Dict, Any, List, Optional
class MsftAccess:
"""
Access control class for Microsoft interface.
Handles user access management and permission checks for Microsoft tokens.
"""
def __init__(self, currentUser: Dict[str, Any], db):
"""Initialize with user context."""
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
self.db = db
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
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
userPrivilege = self.currentUser.get("privilege", "user")
filtered_records = []
# Apply filtering based on privilege
if userPrivilege == "sysadmin":
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]
else: # Regular users
# Users only see their own Microsoft tokens
filtered_records = [r for r in recordset
if r.get("_mandateId") == self._mandateId and r.get("_userId") == self._userId]
# Add access control attributes to each record
for record in filtered_records:
record_id = record.get("id")
# Set access control flags based on user permissions
if table == "msftTokens":
record["_hideView"] = False # Everyone can view their own tokens
record["_hideEdit"] = not self.canModify("msftTokens", record_id)
record["_hideDelete"] = not self.canModify("msftTokens", record_id)
else:
# Default access control for other tables
record["_hideView"] = False
record["_hideEdit"] = not self.canModify(table, record_id)
record["_hideDelete"] = not self.canModify(table, record_id)
return filtered_records
def canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
userPrivilege = self.currentUser.get("privilege", "user")
# System admins can modify anything
if userPrivilege == "sysadmin":
return True
# Check specific record permissions
if recordId is not None:
# Get the record to check ownership
records = self.db.getRecordset(table, recordFilter={"id": recordId})
if not records:
return False
record = records[0]
# Admins can modify anything in their mandate
if userPrivilege == "admin" and record.get("_mandateId") == self._mandateId:
return True
# Users can only modify their own Microsoft tokens
if (record.get("_mandateId") == self._mandateId and
record.get("_userId") == self._userId):
return True
return False
else:
# For general table modify permission (e.g., create)
# Admins can create anything in their mandate
if userPrivilege == "admin":
return True
# Regular users can create their own Microsoft tokens
if table == "msftTokens":
return True
return False

View file

@ -0,0 +1,391 @@
"""
Microsoft interface for handling Microsoft authentication and Graph API operations.
"""
import logging
import json
import requests
import base64
import msal
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
import secrets
import os
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.msftModel import MsftToken, MsftUserInfo
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.interfaces.msftAccess import MsftAccess
logger = logging.getLogger(__name__)
# Singleton factory for MsftInterface instances per context
_msftInterfaces = {}
class MsftInterface:
"""Interface for Microsoft authentication and Graph API operations"""
def __init__(self, currentUser: Dict[str, Any]):
"""Initialize the Microsoft interface"""
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Initialize configuration
self.client_id = APP_CONFIG.get("Service_MSFT_CLIENT_ID")
self.client_secret = APP_CONFIG.get("Service_MSFT_CLIENT_SECRET")
self.tenant_id = APP_CONFIG.get("Service_MSFT_TENANT_ID", "common")
self.redirect_uri = APP_CONFIG.get("Service_MSFT_REDIRECT_URI")
self.authority = f"https://login.microsoftonline.com/{self.tenant_id}"
self.scopes = ["Mail.ReadWrite", "User.Read"]
# Initialize database
self._initializeDatabase()
# Initialize access control
self.access = MsftAccess(self.currentUser, self.db)
# Initialize MSAL application
self.msal_app = msal.ConfidentialClientApplication(
self.client_id,
authority=self.authority,
client_credential=self.client_secret
)
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_MSFT_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_MSFT_DATABASE", "msft")
dbUser = APP_CONFIG.get("DB_MSFT_USER")
dbPassword = APP_CONFIG.get("DB_MSFT_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
_mandateId=self._mandateId,
_userId=self._userId
)
# Set context
self.db.updateContext(self._mandateId, self._userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
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
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
return self.access.canModify(table, recordId)
def getMsftToken(self) -> Optional[Dict[str, Any]]:
"""Get Microsoft token for current user"""
try:
tokens = self.db.getRecordset("msftTokens", recordFilter={
"_mandateId": self._mandateId,
"_userId": self._userId
})
if not tokens:
return None
# Apply access control
filtered_tokens = self._uam("msftTokens", tokens)
if not filtered_tokens:
return None
return filtered_tokens[0]
except Exception as e:
logger.error(f"Error getting Microsoft token: {str(e)}")
return None
def saveMsftToken(self, token_data: Dict[str, Any]) -> bool:
"""Save Microsoft token data"""
try:
# Check if user can modify tokens
if not self._canModify("msftTokens"):
raise PermissionError("No permission to save Microsoft token")
# Add user and mandate IDs to token data
token_data["_mandateId"] = self._mandateId
token_data["_userId"] = self._userId
# Check if token already exists
existing_token = self.getMsftToken()
if existing_token:
# Update existing token
return self.db.recordUpdate("msftTokens", existing_token["id"], token_data)
else:
# Create new token record
return self.db.recordCreate("msftTokens", token_data)
except Exception as e:
logger.error(f"Error saving Microsoft token: {str(e)}")
return False
def deleteMsftToken(self) -> bool:
"""Delete Microsoft token for current user"""
try:
if not self._canModify("msftTokens"):
raise PermissionError("No permission to delete Microsoft token")
existing_token = self.getMsftToken()
if existing_token:
return self.db.recordDelete("msftTokens", existing_token["id"])
return True
except Exception as e:
logger.error(f"Error deleting Microsoft token: {str(e)}")
return False
def getCurrentUserToken(self) -> tuple:
"""Get current user's Microsoft token and info"""
try:
token_data = self.getMsftToken()
if not token_data:
return None, None
# Verify token is still valid
if not self.verifyToken(token_data.get("access_token")):
if not self.refreshToken(token_data):
return None, None
token_data = self.getMsftToken()
user_info = token_data.get("user_info")
if not user_info:
user_info = self.getUserInfoFromToken(token_data.get("access_token"))
if user_info:
token_data["user_info"] = user_info
self.saveMsftToken(token_data)
return user_info, token_data.get("access_token")
except Exception as e:
logger.error(f"Error getting current user token: {str(e)}")
return None, None
def verifyToken(self, token: str) -> bool:
"""Verify the access token is valid"""
try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
return response.status_code == 200
except Exception as e:
logger.error(f"Error verifying token: {str(e)}")
return False
def refreshToken(self, token_data: Dict[str, Any]) -> bool:
"""Refresh the access token using the stored refresh token"""
try:
if not token_data or not token_data.get("refresh_token"):
return False
result = self.msal_app.acquire_token_by_refresh_token(
token_data["refresh_token"],
scopes=self.scopes
)
if "error" in result:
logger.error(f"Error refreshing token: {result.get('error')}")
return False
token_data["access_token"] = result["access_token"]
if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"]
return self.saveMsftToken(token_data)
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
return False
def getUserInfoFromToken(self, access_token: str) -> Optional[Dict[str, Any]]:
"""Get user information using the access token"""
try:
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
if response.status_code == 200:
user_data = response.json()
return {
"name": user_data.get("displayName", ""),
"email": user_data.get("userPrincipalName", ""),
"id": user_data.get("id", "")
}
return None
except Exception as e:
logger.error(f"Error getting user info: {str(e)}")
return None
def createDraftEmail(self, recipient: str, subject: str, body: str, attachments: List[Dict[str, Any]] = None) -> bool:
"""Create a draft email using Microsoft Graph API"""
try:
user_info, access_token = self.getCurrentUserToken()
if not user_info or not access_token:
return False
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
email_data = {
'subject': subject,
'body': {
'contentType': 'HTML',
'content': body
},
'toRecipients': [
{
'emailAddress': {
'address': recipient
}
}
]
}
if attachments:
email_data['attachments'] = []
for attachment in attachments:
doc = attachment.get('document', {})
file_name = attachment.get('name', 'attachment.file')
file_content = doc.get('data')
if not file_content:
continue
mime_type = doc.get('mimeType', 'application/octet-stream')
is_base64 = doc.get('base64Encoded', False)
try:
if is_base64:
content_bytes = file_content
else:
if isinstance(file_content, str):
content_bytes = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
elif isinstance(file_content, bytes):
content_bytes = base64.b64encode(file_content).decode('utf-8')
else:
continue
decoded_size = len(base64.b64decode(content_bytes))
attachment_data = {
'@odata.type': '#microsoft.graph.fileAttachment',
'name': file_name,
'contentType': mime_type,
'contentBytes': content_bytes,
'isInline': False,
'size': decoded_size
}
email_data['attachments'].append(attachment_data)
except Exception as e:
logger.error(f"Error processing attachment {file_name}: {str(e)}")
continue
response = requests.post(
'https://graph.microsoft.com/v1.0/me/messages',
headers=headers,
json=email_data
)
return response.status_code >= 200 and response.status_code < 300
except Exception as e:
logger.error(f"Error creating draft email: {str(e)}")
return False
def initiateLogin(self) -> str:
"""Initiate Microsoft login flow"""
try:
state = secrets.token_urlsafe(32)
auth_url = self.msal_app.get_authorization_request_url(
self.scopes,
state=state,
redirect_uri=self.redirect_uri
)
return auth_url
except Exception as e:
logger.error(f"Error initiating login: {str(e)}")
return None
def handleAuthCallback(self, code: str) -> Optional[Dict[str, Any]]:
"""Handle Microsoft OAuth callback"""
try:
token_response = self.msal_app.acquire_token_by_authorization_code(
code,
self.scopes,
redirect_uri=self.redirect_uri
)
if "error" in token_response:
logger.error(f"Token acquisition failed: {token_response['error']}")
return None
user_info = self.getUserInfoFromToken(token_response["access_token"])
if not user_info:
return None
token_response["user_info"] = user_info
return token_response
except Exception as e:
logger.error(f"Error handling auth callback: {str(e)}")
return None
def getInterface(currentUser: Dict[str, Any]) -> MsftInterface:
"""
Returns a MsftInterface instance for the current user.
Handles initialization of database and records.
"""
mandateId = currentUser.get("_mandateId")
userId = currentUser.get("id")
if not mandateId or not userId:
raise ValueError("Invalid user context: _mandateId and id are required")
# Create context key
contextKey = f"{mandateId}_{userId}"
# Create new instance if not exists
if contextKey not in _msftInterfaces:
_msftInterfaces[contextKey] = MsftInterface(currentUser)
return _msftInterfaces[contextKey]

View file

@ -0,0 +1,73 @@
"""
Data models for Microsoft integration.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
# Get all attributes of the model
def getModelAttributes(modelClass):
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"""
default: str
translations: Dict[str, str] = {}
def getLabel(self, language: str = None):
"""Returns the label in the specified language, or the default value if not available"""
if language and language in self.translations:
return self.translations[language]
return self.default
class MsftToken(BaseModel):
"""Data model for Microsoft authentication token"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the token")
access_token: str = Field(description="Microsoft access token")
refresh_token: str = Field(description="Microsoft refresh token")
expires_in: int = Field(description="Token expiration time in seconds")
token_type: str = Field(description="Type of token (usually 'bearer')")
expires_at: float = Field(description="Timestamp when token expires")
user_info: Optional[Dict[str, Any]] = Field(None, description="User information from Microsoft")
_mandateId: str = Field(description="Mandate ID associated with the token")
_userId: str = Field(description="User ID associated with the token")
label: Label = Field(
default=Label(default="Microsoft Token", translations={"en": "Microsoft Token", "fr": "Jeton Microsoft"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"access_token": Label(default="Access Token", translations={"en": "Access Token", "fr": "Jeton d'accès"}),
"refresh_token": Label(default="Refresh Token", translations={"en": "Refresh Token", "fr": "Jeton de rafraîchissement"}),
"expires_in": Label(default="Expires In", translations={"en": "Expires In", "fr": "Expire dans"}),
"token_type": Label(default="Token Type", translations={"en": "Token Type", "fr": "Type de jeton"}),
"expires_at": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire à"}),
"user_info": Label(default="User Info", translations={"en": "User Info", "fr": "Info utilisateur"}),
"_mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"_userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"})
}
class MsftUserInfo(BaseModel):
"""Data model for Microsoft user information"""
name: str = Field(description="User's display name")
email: str = Field(description="User's email address")
id: str = Field(description="User's Microsoft ID")
label: Label = Field(
default=Label(default="Microsoft User Info", translations={"en": "Microsoft User Info", "fr": "Info utilisateur Microsoft"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"email": Label(default="Email", translations={"en": "Email", "fr": "E-mail"}),
"id": Label(default="ID", translations={})
}

View file

@ -6,7 +6,7 @@ import importlib
import os import os
from pydantic import BaseModel from pydantic import BaseModel
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.security.auth import getCurrentActiveUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.shared.defAttributes import AttributeDefinition, getModelAttributes from modules.shared.defAttributes import AttributeDefinition, getModelAttributes
@ -43,7 +43,7 @@ router = APIRouter(
) )
@router.get("/{entityType}", response_model=List[AttributeDefinition]) @router.get("/{entityType}", response_model=List[AttributeDefinition])
async def getEntityAttributes( async def get_entity_attributes(
entityType: str = Path(..., description="Type of entity (e.g. prompt)"), entityType: str = Path(..., description="Type of entity (e.g. prompt)"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
@ -51,11 +51,8 @@ async def getEntityAttributes(
Retrieves the attribute definitions for a specific entity. Retrieves the attribute definitions for a specific entity.
This can be used for dynamic form generation. This can be used for dynamic form generation.
""" """
# Authentication and user context
mandateId, userId = await getUserContext(currentUser)
# Determine preferred language of the user # Determine preferred language of the user
userLanguage = currentUser.get("language", "de") userLanguage = currentUser.get("language", "en")
# Get model classes dynamically # Get model classes dynamically
modelClasses = getModelClasses() modelClasses = getModelClasses()
@ -75,7 +72,7 @@ async def getEntityAttributes(
return [attr for attr in attributes if attr.visible] return [attr for attr in attributes if attr.visible]
@router.options("/{entityType}") @router.options("/{entityType}")
async def optionsEntityAttributes( async def options_entity_attributes(
entityType: str = Path(..., description="Type of entity (e.g. prompt)") entityType: str = Path(..., description="Type of entity (e.g. prompt)")
): ):
"""Handle OPTIONS request for CORS preflight""" """Handle OPTIONS request for CORS preflight"""

View file

@ -2,50 +2,23 @@ from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, P
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from dataclasses import dataclass from dataclasses import dataclass
import io import io
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.security.auth import getCurrentActiveUser
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
# Import interfaces # Import interfaces
from modules.interfaces.lucydomInterface import getLucydomInterface, FileError, FileNotFoundError, FileStorageError, FilePermissionError, FileDeletionError from modules.interfaces.lucydomInterface import getInterface, FileError, FileNotFoundError, FileStorageError, FilePermissionError, FileDeletionError
from modules.interfaces.lucydomModel import FileItem from modules.interfaces.lucydomModel import FileItem, getModelAttributes
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Get all attributes of the model
def getModelAttributes(modelClass):
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')]
# Model attributes for FileItem # Model attributes for FileItem
fileAttributes = getModelAttributes(FileItem) fileAttributes = getModelAttributes(FileItem)
class AppContext:
def __init__(self, mandateId: int, userId: int):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""
Creates a central context object with all required interfaces
Args:
currentUser: Current user from authentication
Returns:
AppContext object with all required connections
"""
_mandateId, _userId = await getUserContext(currentUser)
return AppContext(_mandateId, _userId)
# Create router for file endpoints # Create router for file endpoints
router = APIRouter( router = APIRouter(
prefix="/api/files", prefix="/api/files",
@ -60,13 +33,13 @@ router = APIRouter(
) )
@router.get("", response_model=List[Dict[str, Any]]) @router.get("", response_model=List[Dict[str, Any]])
async def getFiles(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def get_files(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get all available files""" """Get all available files"""
try: try:
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Get all files generically - only metadata, no binary data # Get all files generically - only metadata, no binary data
files = context.interfaceData.getAllFiles() files = myInterface.getAllFiles()
return files return files
except Exception as e: except Exception as e:
logger.error(f"Error retrieving files: {str(e)}") logger.error(f"Error retrieving files: {str(e)}")
@ -76,14 +49,14 @@ async def getFiles(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
) )
@router.post("/upload", status_code=status.HTTP_201_CREATED) @router.post("/upload", status_code=status.HTTP_201_CREATED)
async def uploadFile( async def upload_file(
file: UploadFile = File(...), file: UploadFile = File(...),
workflowId: Optional[str] = Form(None), workflowId: Optional[str] = Form(None),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Upload a file""" """Upload a file"""
try: try:
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Read file # Read file
fileContent = await file.read() fileContent = await file.read()
@ -97,12 +70,12 @@ async def uploadFile(
) )
# Save file via LucyDOM interface in the database # Save file via LucyDOM interface in the database
fileMeta = context.interfaceData.saveUploadedFile(fileContent, file.filename) fileMeta = myInterface.saveUploadedFile(fileContent, file.filename)
# If workflowId is provided, update the file information # If workflowId is provided, update the file information
if workflowId: if workflowId:
updateData = {"workflowId": workflowId} updateData = {"workflowId": workflowId}
context.interfaceData.updateFile(fileMeta["id"], updateData) myInterface.updateFile(fileMeta["id"], updateData)
fileMeta["workflowId"] = workflowId fileMeta["workflowId"] = workflowId
# Successful response # Successful response
@ -122,16 +95,16 @@ async def uploadFile(
) )
@router.get("/{fileId}") @router.get("/{fileId}")
async def getFile( async def get_file(
fileId: str, fileId: str,
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Returns a file by its ID for download""" """Returns a file by its ID for download"""
try: try:
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Get file via LucyDOM interface from the database # Get file via LucyDOM interface from the database
fileData = context.interfaceData.downloadFile(fileId) fileData = myInterface.downloadFile(fileId)
# Return file # Return file
headers = { headers = {
@ -168,17 +141,57 @@ async def getFile(
detail=f"Error retrieving file: {str(e)}" detail=f"Error retrieving file: {str(e)}"
) )
@router.put("/{file_id}")
async def update_file(
file_id: str,
file_data: FileItem,
current_user: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
Update file metadata
"""
try:
myInterface = getInterface(current_user)
# Get the file from the database
file = myInterface.getFile(file_id)
if not file:
raise HTTPException(status_code=404, detail="File not found")
# Check if user has access to the file
if file.get("userId", 0) != current_user.get("id", 0):
raise HTTPException(status_code=403, detail="Not authorized to update this file")
# Update file metadata
update_data = file_data.dict(exclude_unset=True)
update_data["modified_at"] = datetime.now(timezone.utc)
# Update in database
result = myInterface.updateFile(file_id, update_data)
if not result:
raise HTTPException(status_code=500, detail="Failed to update file")
# Get updated file
updated_file = myInterface.getFile(file_id)
return updated_file
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"Error updating file: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteFile( async def delete_file(
fileId: str, fileId: str,
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Deletes a file by its ID from the database""" """Deletes a file by its ID from the database"""
try: try:
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Delete file via LucyDOM interface # Delete file via LucyDOM interface
context.interfaceData.deleteFile(fileId) myInterface.deleteFile(fileId)
# Return successful deletion without content (204 No Content) # Return successful deletion without content (204 No Content)
return Response(status_code=status.HTTP_204_NO_CONTENT) return Response(status_code=status.HTTP_204_NO_CONTENT)
@ -209,15 +222,15 @@ async def deleteFile(
) )
@router.get("/stats", response_model=Dict[str, Any]) @router.get("/stats", response_model=Dict[str, Any])
async def getFileStats( async def get_file_stats(
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Returns statistics about the stored files""" """Returns statistics about the stored files"""
try: try:
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Get all files - metadata only # Get all files - metadata only
allFiles = context.interfaceData.getAllFiles() allFiles = myInterface.getAllFiles()
# Calculate statistics # Calculate statistics
totalFiles = len(allFiles) totalFiles = len(allFiles)

View file

@ -12,11 +12,11 @@ from modules.shared.configuration import APP_CONFIG
from modules.security.auth import ( from modules.security.auth import (
createAccessToken, createAccessToken,
getCurrentActiveUser, getCurrentActiveUser,
getUserContext, getRootInterface,
ACCESS_TOKEN_EXPIRE_MINUTES ACCESS_TOKEN_EXPIRE_MINUTES
) )
import modules.interfaces.gatewayModel as gatewayModel import modules.interfaces.gatewayModel as gatewayModel
from modules.interfaces.gatewayInterface import getGatewayInterface from modules.interfaces.gatewayInterface import getInterface
router = APIRouter() router = APIRouter()
@ -40,11 +40,11 @@ async def root():
return {"status": "online", "message": "Data Platform API is active"} return {"status": "online", "message": "Data Platform API is active"}
@router.get("/api/test", tags=["General"]) @router.get("/api/test", tags=["General"])
async def getTest(): async def get_test():
return f"Status: OK. Alowed origins: {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}" return f"Status: OK. Alowed origins: {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}"
@router.options("/{fullPath:path}", tags=["General"]) @router.options("/{fullPath:path}", tags=["General"])
async def optionsRoute(fullPath: str): async def options_route(fullPath: str):
return Response(status_code=200) return Response(status_code=200)
@router.get("/api/environment", tags=["General"]) @router.get("/api/environment", tags=["General"])
@ -58,24 +58,13 @@ async def get_environment():
} }
@router.post("/api/token", response_model=gatewayModel.Token, tags=["General"]) @router.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()): async def login_for_access_token(formData: OAuth2PasswordRequestForm = Depends()):
# Get root mandate and admin user IDs
adminGateway = getGatewayInterface()
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="System is not properly initialized with root mandate and admin user"
)
# Create a new gateway interface instance with admin context # Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId) myInterface = getRootInterface()
try: try:
# Authenticate user # Authenticate user
user = adminGateway.authenticateUser(formData.username, formData.password) user = myInterface.authenticateUser(formData.username, formData.password)
# Create token with mandate ID and user ID # Create token with mandate ID and user ID
accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) accessTokenExpires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
@ -110,30 +99,17 @@ async def loginForAccessToken(formData: OAuth2PasswordRequestForm = Depends()):
) )
@router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"]) @router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
async def readUserMe(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def read_user_me(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
return currentUser return currentUser
@router.post("/api/users/register", response_model=Dict[str, Any], tags=["General"]) @router.post("/api/users/register", response_model=Dict[str, Any], tags=["General"])
async def registerUser(userData: Dict[str, Any]): async def register_user(userData: Dict[str, Any]):
"""Register a new user.""" """Register a new user."""
try: try:
logger.info("Received registration request") logger.debug("Received registration request")
logger.info(f"Raw userData type: {type(userData)}")
logger.info(f"Raw userData content: {userData}")
# Get root mandate and admin user IDs
adminGateway = getGatewayInterface()
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="System is not properly initialized with root mandate and admin user"
)
# Create a new gateway interface instance with admin context # Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId) myInterface = getRootInterface()
# Check required fields # Check required fields
if not userData or not isinstance(userData, dict): if not userData or not isinstance(userData, dict):
@ -148,79 +124,56 @@ async def registerUser(userData: Dict[str, Any]):
detail="Username and password are required" detail="Username and password are required"
) )
# Create user data with mandate ID # Create user data in same mandate as admin user
userData = { userData = {
"username": userData["username"], "username": userData["username"],
"password": userData["password"], "password": userData["password"],
"email": userData.get("email"), "email": userData.get("email"),
"fullName": userData.get("fullName"), "fullName": userData.get("fullName"),
"language": userData.get("language", "de"), "language": userData.get("language", "en"),
"_mandateId": rootMandateId,
"disabled": False, "disabled": False,
"privilege": "user" "privilege": "user"
} }
# Create the user # Create the user
createdUser = adminGateway.createUser(**userData) try:
createdUser = myInterface.createUser(**userData)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Verify the user was created
if not createdUser: if not createdUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user" detail="Failed to create user"
) )
# Clear the users table from cache to ensure fresh data
if hasattr(adminGateway.db, '_tablesCache') and "users" in adminGateway.db._tablesCache:
del adminGateway.db._tablesCache["users"]
# Return the created user (without password)
if "hashedPassword" in createdUser:
del createdUser["hashedPassword"]
return createdUser return createdUser
except ValueError as e:
logger.error(f"ValueError during registration: {str(e)}") except HTTPException:
raise HTTPException( raise
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except PermissionError as e:
logger.error(f"PermissionError during registration: {str(e)}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=str(e)
)
except Exception as e: except Exception as e:
logger.error(f"Error during user registration: {str(e)}") logger.error(f"Error in user registration: {str(e)}")
logger.error(f"Error type: {type(e)}")
logger.error(f"Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No details available'}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to register user" detail=f"Registration failed: {str(e)}"
) )
@router.get("/api/user/available", response_model=Dict[str, Any], tags=["General"]) @router.get("/api/user/available", response_model=Dict[str, Any], tags=["General"])
async def checkUsernameAvailability( async def check_username_availability(
username: str, username: str,
authenticationAuthority: str = "local" authenticationAuthority: str = "local"
): ):
"""Check if a username is available for registration""" """Check if a username is available for registration"""
try: try:
# Get root mandate and admin user IDs # Create a new gateway interface instance with root context
adminGateway = getGatewayInterface() myInterface = getRootInterface()
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="System is not properly initialized with root mandate and admin user"
)
# Create a new gateway interface instance with admin context
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
# Check if user exists # Check if user exists
existingUser = adminGateway.getUserByUsername(username) existingUser = myInterface.getUserByUsername(username)
if not existingUser: if not existingUser:
return {"available": True} return {"available": True}

View file

@ -1,187 +1,189 @@
from fastapi import APIRouter, HTTPException, Depends, Body, Path from fastapi import APIRouter, HTTPException, Depends, Body, status
from typing import List, Dict, Any from typing import Dict, Any, List
from fastapi import status import logging
from datetime import datetime
from dataclasses import dataclass
# Import interfaces from modules.security.auth import getCurrentActiveUser
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.interfaces.gatewayInterface import getInterface
from modules.interfaces.gatewayInterface import getGatewayInterface from modules.interfaces.gatewayModel import Mandate, getModelAttributes
from modules.interfaces.gatewayModel import Mandate
# Determine all attributes of the model # Configure logger
def getModelAttributes(modelClass): logger = logging.getLogger(__name__)
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')]
# Model attributes for Mandate # Model attributes for Mandate
mandateAttributes = getModelAttributes(Mandate) mandateAttributes = getModelAttributes(Mandate)
class AppContext:
def __init__(self, mandateId: int, userId: int):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getGatewayInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
mandateId, userId = await getUserContext(currentUser)
return AppContext(mandateId, userId)
# Create router for mandate endpoints
router = APIRouter( router = APIRouter(
prefix="/api/mandates", prefix="/api/mandates",
tags=["Mandates"], tags=["Mandates"],
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("", response_model=List[Dict[str, Any]]) @router.get("/", response_model=List[Dict[str, Any]], tags=["Mandates"])
async def getMandates(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def get_mandates(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get all available mandates (only for SysAdmin users)""" """Get all mandates"""
context = await getContext(currentUser) try:
myInterface = getInterface(currentUser)
# Permission check return myInterface.getMandates()
if currentUser.get("privilege") != "sysadmin": except Exception as e:
logger.error(f"Error getting mandates: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Only system administrators can access all mandates" detail=f"Failed to get mandates: {str(e)}"
) )
# Get mandates @router.get("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"])
return context.interfaceData.getAllMandates() async def get_mandate(
mandateId: str,
@router.post("", response_model=Dict[str, Any])
async def createMandate(
mandate: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Create a new mandate (only for SysAdmin users)""" """Get a specific mandate by ID"""
context = await getContext(currentUser) try:
myInterface = getInterface(currentUser)
mandate = myInterface.getMandateById(mandateId)
# Permission check
if currentUser.get("privilege") != "sysadmin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only system administrators can create mandates"
)
# Set attributes from the request dynamically
mandateData = {}
for attr in mandateAttributes:
if attr in mandate:
mandateData[attr] = mandate[attr]
# Default values for missing fields
mandateData.setdefault("name", "New Mandate")
mandateData.setdefault("language", "de")
# Create mandate
newMandate = context.interfaceData.createMandate(**mandateData)
return newMandate
@router.get("/{_mandateId}", response_model=Dict[str, Any])
async def getMandate(
_mandateId: str = Path(..., description="ID of the mandate"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a mandate by ID."""
context = await getContext(currentUser)
# Permission check
# Admin can only see their own mandate, SysAdmin can see all
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to access this mandate"
)
# Get mandate
mandate = context.interfaceData.getMandate(_mandateId)
if not mandate: if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate with ID {_mandateId} not found" detail=f"Mandate {mandateId} not found"
) )
return mandate return mandate
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting mandate {mandateId}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get mandate: {str(e)}"
)
@router.put("/{_mandateId}", response_model=Dict[str, Any]) @router.post("/", response_model=Dict[str, Any], tags=["Mandates"])
async def updateMandate( async def create_mandate(
_mandateId: str = Path(..., description="ID of the mandate to update"), mandateData: Dict[str, Any],
mandateData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Update a mandate.""" """Create a new mandate"""
context = await getContext(currentUser) try:
myInterface = getInterface(currentUser)
# Get mandate # Check required fields
mandate = context.interfaceData.getMandate(_mandateId) if not mandateData.get("name"):
if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Mandate with ID {_mandateId} not found" detail="Mandate name is required"
) )
# Permission check # Filter attributes based on model definition
isAdmin = currentUser.get("privilege") == "admin" filteredData = {}
isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to update this mandate"
)
# Dynamically filter attributes from the request into updateData
updateData = {}
for attr in mandateAttributes: for attr in mandateAttributes:
if attr in mandateData: if attr in mandateData:
updateData[attr] = mandateData[attr] filteredData[attr] = mandateData[attr]
# Update mandate try:
updatedMandate = context.interfaceData.updateMandate(_mandateId, mandateData) createdMandate = myInterface.createMandate(**filteredData)
return updatedMandate except ValueError as e:
@router.delete("/{_mandateId}", status_code=status.HTTP_204_NO_CONTENT)
async def deleteMandate(
_mandateId: str = Path(..., description="ID of the mandate to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a mandate."""
context = await getContext(currentUser)
# Get mandate
mandate = context.interfaceData.getMandate(_mandateId)
if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Mandate with ID {_mandateId} not found" detail=str(e)
) )
# Permission check if not createdMandate:
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
isOwnMandate = context._mandateId == _mandateId
if (isAdmin and not isOwnMandate) and not isSysadmin:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="No permission to delete this mandate" detail="Failed to create mandate"
)
return createdMandate
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating mandate: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create mandate: {str(e)}"
)
@router.put("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"])
async def update_mandate(
mandateId: str,
mandateData: Dict[str, Any],
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Update an existing mandate"""
try:
myInterface = getInterface(currentUser)
# Check if mandate exists
existingMandate = myInterface.getMandateById(mandateId)
if not existingMandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate {mandateId} not found"
)
# Filter attributes based on model definition
filteredData = {}
for attr in mandateAttributes:
if attr in mandateData:
filteredData[attr] = mandateData[attr]
# Update mandate data
try:
updatedMandate = myInterface.updateMandate(mandateId, **filteredData)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
if not updatedMandate:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update mandate"
)
return updatedMandate
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating mandate {mandateId}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update mandate: {str(e)}"
)
@router.delete("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"])
async def delete_mandate(
mandateId: str,
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a mandate"""
try:
myInterface = getInterface(currentUser)
# Check if mandate exists
existingMandate = myInterface.getMandateById(mandateId)
if not existingMandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate {mandateId} not found"
) )
# Delete mandate # Delete mandate
success = context.interfaceData.deleteMandate(_mandateId) try:
if not success: myInterface.deleteMandate(mandateId)
except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Error deleting mandate with ID {_mandateId}" detail=str(e)
) )
return None return {"message": f"Mandate {mandateId} deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting mandate {mandateId}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete mandate: {str(e)}"
)

View file

@ -1,16 +1,12 @@
from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie, Body
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import msal
import logging import logging
import json import json
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
import secrets
from modules.security.auth import getCurrentActiveUser, getUserContext, createAccessToken, ACCESS_TOKEN_EXPIRE_MINUTES from modules.security.auth import getCurrentActiveUser, createAccessToken, ACCESS_TOKEN_EXPIRE_MINUTES, getRootInterface
from modules.shared.configuration import APP_CONFIG from modules.interfaces.msftInterface import getInterface as getMsftInterface
from modules.interfaces.lucydomInterface import getLucydomInterface
from modules.interfaces.gatewayInterface import getGatewayInterface
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,189 +24,22 @@ router = APIRouter(
} }
) )
# Azure AD configuration - load from config
CLIENT_ID = APP_CONFIG.get("Agent_Mail_MSFT_CLIENT_ID")
CLIENT_SECRET = APP_CONFIG.get("Agent_Mail_MSFT_CLIENT_SECRET")
TENANT_ID = APP_CONFIG.get("Agent_Mail_MSFT_TENANT_ID", "common") # Use 'common' for multi-tenant
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["Mail.ReadWrite", "User.Read"]
REDIRECT_URI = APP_CONFIG.get("Agent_Mail_MSFT_REDIRECT_URI")
# Initialize MSAL application
app_config = {
"client_id": CLIENT_ID,
"client_credential": CLIENT_SECRET,
"authority": AUTHORITY,
"redirect_uri": REDIRECT_URI
}
async def save_token_to_file(token_data, currentUser: Dict[str, Any]):
"""Save token data to database using LucyDOMInterface"""
try:
# Get current user context
_mandateId, _userId = await getUserContext(currentUser)
if not _mandateId or not _userId:
logger.error("No user context available for token storage")
return False
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
_mandateId=_mandateId,
_userId=_userId
)
if not mydom:
logger.error("No LucyDOM interface available for token storage")
return False
# Ensure user info is preserved
if "user_info" not in token_data:
# Try to get user info from the token
user_info = get_user_info_from_token(token_data.get("access_token", ""))
if user_info:
token_data["user_info"] = user_info
# Save token to database
success = mydom.saveMsftToken(token_data)
if success:
logger.info("Token saved successfully to database")
return True
else:
logger.error("Failed to save token to database")
return False
except Exception as e:
logger.error(f"Error saving token: {str(e)}")
return False
async def load_token_from_file(currentUser: Dict[str, Any]):
"""Load token data from database using LucyDOMInterface"""
try:
# Get current user context
_mandateId, _userId = await getUserContext(currentUser)
if not _mandateId or not _userId:
logger.error("No user context available for token retrieval")
return None
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
_mandateId=_mandateId,
_userId=_userId
)
if not mydom:
logger.error("No LucyDOM interface available for token retrieval")
return None
# Get token from database
token_data = mydom.getMsftToken()
if token_data:
logger.info("Token loaded successfully from database")
return token_data
else:
logger.info("No token found in database")
return None
except Exception as e:
logger.error(f"Error loading token: {str(e)}")
return None
def get_user_info_from_token(access_token: str) -> Optional[Dict[str, Any]]:
"""Get user information using the access token"""
import requests
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
try:
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
if response.status_code == 200:
user_data = response.json()
return {
"name": user_data.get("displayName", ""),
"email": user_data.get("userPrincipalName", ""),
"id": user_data.get("id", "")
}
else:
logger.error(f"Error getting user info: {response.status_code} - {response.text}")
return None
except Exception as e:
logger.error(f"Exception getting user info: {str(e)}")
return None
def verify_token(token: str) -> bool:
"""Verify the access token is valid"""
import requests
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
try:
logger.info("Verifying token validity...")
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
if response.status_code == 200:
logger.info("Token verification successful")
return True
else:
logger.error(f"Token verification failed: {response.status_code} - {response.text}")
return False
except Exception as e:
logger.error(f"Exception verifying token: {str(e)}")
return False
async def refresh_token(user_id: str, currentUser: Dict[str, Any]) -> bool:
"""Refresh the access token using the stored refresh token"""
token_data = await load_token_from_file(currentUser)
if not token_data or not token_data.get("refresh_token"):
logger.warning("No refresh token available")
return False
msal_app = msal.ConfidentialClientApplication(
app_config["client_id"],
authority=app_config["authority"],
client_credential=app_config["client_credential"]
)
result = msal_app.acquire_token_by_refresh_token(
token_data["refresh_token"],
scopes=SCOPES
)
if "error" in result:
logger.error(f"Error refreshing token: {result.get('error')}")
return False
# Update tokens in storage
token_data["access_token"] = result["access_token"]
if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"]
await save_token_to_file(token_data, currentUser)
logger.info("Access token refreshed successfully")
return True
@router.get("/login") @router.get("/login")
async def login(): async def login():
"""Initiate Microsoft login for the current user""" """Initiate Microsoft login for the current user"""
try: try:
# Create a confidential client application # Get Microsoft interface
msal_app = msal.ConfidentialClientApplication( msft = getMsftInterface({"_mandateId": "root", "id": "root"})
app_config["client_id"],
authority=app_config["authority"], # Get login URL
client_credential=app_config["client_credential"] auth_url = msft.initiateLogin()
if not auth_url:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to initiate Microsoft login"
) )
# Build the auth URL with a random state logger.info("Redirecting to Microsoft login")
state = secrets.token_urlsafe(32)
auth_url = msal_app.get_authorization_request_url(
SCOPES,
state=state, # Use random state
redirect_uri=app_config["redirect_uri"]
)
logger.info(f"Redirecting to Microsoft login")
return RedirectResponse(auth_url) return RedirectResponse(auth_url)
except Exception as e: except Exception as e:
@ -224,22 +53,12 @@ async def login():
async def auth_callback(code: str, state: str, request: Request): async def auth_callback(code: str, state: str, request: Request):
"""Handle Microsoft OAuth callback""" """Handle Microsoft OAuth callback"""
try: try:
# Create a confidential client application # Get Microsoft interface
msal_app = msal.ConfidentialClientApplication( msft = getMsftInterface({"_mandateId": "root", "id": "root"})
app_config["client_id"],
authority=app_config["authority"],
client_credential=app_config["client_credential"]
)
# Exchange the authorization code for tokens # Handle auth callback
token_response = msal_app.acquire_token_by_authorization_code( token_response = msft.handleAuthCallback(code)
code, if not token_response:
SCOPES,
redirect_uri=app_config["redirect_uri"]
)
if "error" in token_response:
logger.error(f"Token acquisition failed: {token_response['error']}")
return HTMLResponse( return HTMLResponse(
content=""" content="""
<html> <html>
@ -262,38 +81,11 @@ async def auth_callback(code: str, state: str, request: Request):
status_code=400 status_code=400
) )
# Get user info from the token
user_info = get_user_info_from_token(token_response["access_token"])
if not user_info:
logger.error("Failed to get user info from token")
return HTMLResponse(
content="""
<html>
<head>
<title>Authentication Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Authentication Failed</h1>
<p>Could not retrieve user information.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
)
# Get gateway interface for user operations # Get gateway interface for user operations
gateway = getGatewayInterface() gateway = getRootInterface()
# Check if user exists # Check if user exists
user = gateway.getUserByUsername(user_info["email"]) user = gateway.getUserByUsername(token_response["user_info"]["email"])
# If user doesn't exist, create a new user in the default mandate # If user doesn't exist, create a new user in the default mandate
if not user: if not user:
@ -305,16 +97,16 @@ async def auth_callback(code: str, state: str, request: Request):
# Create new user with Microsoft authentication # Create new user with Microsoft authentication
user = gateway.createUser( user = gateway.createUser(
username=user_info["email"], username=token_response["user_info"]["email"],
email=user_info["email"], email=token_response["user_info"]["email"],
fullName=user_info.get("name", user_info["email"]), fullName=token_response["user_info"].get("name", token_response["user_info"]["email"]),
_mandateId=rootMandateId, _mandateId=rootMandateId,
authenticationAuthority="microsoft" authenticationAuthority="microsoft"
) )
logger.info(f"Created new user for Microsoft account: {user_info['email']}") logger.info(f"Created new user for Microsoft account: {token_response['user_info']['email']}")
# Verify user was created by retrieving it # Verify user was created by retrieving it
user = gateway.getUserByUsername(user_info["email"]) user = gateway.getUserByUsername(token_response["user_info"]["email"])
if not user: if not user:
raise ValueError("Failed to retrieve created user") raise ValueError("Failed to retrieve created user")
@ -354,9 +146,6 @@ async def auth_callback(code: str, state: str, request: Request):
expiresDelta=access_token_expires expiresDelta=access_token_expires
) )
# Add user info to token response
token_response["user_info"] = user_info
# Store tokens in session storage for the frontend to pick up # Store tokens in session storage for the frontend to pick up
response = HTMLResponse( response = HTMLResponse(
content=f""" content=f"""
@ -370,7 +159,7 @@ async def auth_callback(code: str, state: str, request: Request):
</head> </head>
<body> <body>
<h1 class="success">Authentication Successful</h1> <h1 class="success">Authentication Successful</h1>
<p>Welcome, {user_info.get('name', 'User')}!</p> <p>Welcome, {token_response['user_info'].get('name', 'User')}!</p>
<p>This window will close automatically.</p> <p>This window will close automatically.</p>
<script> <script>
// Store token data in session storage // Store token data in session storage
@ -380,7 +169,7 @@ async def auth_callback(code: str, state: str, request: Request):
if (window.opener) {{ if (window.opener) {{
window.opener.postMessage({{ window.opener.postMessage({{
type: 'msft_auth_success', type: 'msft_auth_success',
user: {json.dumps(user_info)}, user: {json.dumps(token_response['user_info'])},
token_data: {json.dumps(token_response)}, token_data: {json.dumps(token_response)},
access_token: "{access_token}" access_token: "{access_token}"
}}, '*'); }}, '*');
@ -406,47 +195,18 @@ async def auth_callback(code: str, state: str, request: Request):
async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Check Microsoft authentication status""" """Check Microsoft authentication status"""
try: try:
# Get current user context # Get Microsoft interface
_mandateId, _userId = await getUserContext(currentUser) msft = getMsftInterface(currentUser)
if not _mandateId or not _userId:
logger.info("No user context found") # Get current user token and info
user_info, access_token = msft.getCurrentUserToken()
if not user_info or not access_token:
return JSONResponse({ return JSONResponse({
"authenticated": False, "authenticated": False,
"message": "Not authenticated with Microsoft" "message": "Not authenticated with Microsoft"
}) })
# Check if we have a token for the current user
token_data = await load_token_from_file(currentUser)
if not token_data:
logger.info(f"No token data found for user {_userId}")
return JSONResponse({
"authenticated": False,
"message": "Not authenticated with Microsoft"
})
# Verify token is still valid and get user info
user_info = get_user_info_from_token(token_data["access_token"])
if not user_info:
logger.info("Token invalid, attempting refresh")
# Try to refresh the token
if not await refresh_token(_userId, currentUser):
logger.info("Token refresh failed")
return JSONResponse({
"authenticated": False,
"message": "Token expired and refresh failed"
})
# Reload token data after refresh
token_data = await load_token_from_file(currentUser)
# Get user info again after refresh
user_info = get_user_info_from_token(token_data["access_token"])
if not user_info:
return JSONResponse({
"authenticated": False,
"message": "Could not get user info after token refresh"
})
logger.info(f"User {user_info.get('name')} is authenticated")
return JSONResponse({ return JSONResponse({
"authenticated": True, "authenticated": True,
"user": user_info "user": user_info
@ -459,152 +219,15 @@ async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser
"message": f"Error checking authentication status: {str(e)}" "message": f"Error checking authentication status: {str(e)}"
}) })
@router.post("/logout")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Logout from Microsoft"""
try:
# Get current user context
_mandateId, _userId = await getUserContext(currentUser)
if not _mandateId or not _userId:
return JSONResponse({
"message": "Not authenticated with Microsoft"
})
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
_mandateId=_mandateId,
_userId=_userId
)
if not mydom:
return JSONResponse({
"message": "Not authenticated with Microsoft"
})
# Remove token from database
tokens = mydom.db.getRecordset("msftTokens", recordFilter={
"_mandateId": _mandateId,
"_userId": _userId
})
if tokens and len(tokens) > 0:
mydom.db.recordDelete("msftTokens", tokens[0]["id"])
logger.info(f"Removed Microsoft token for user {_userId}")
return JSONResponse({
"message": "Successfully logged out from Microsoft"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)
@router.get("/token")
async def get_access_token(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get the current user's access token for Microsoft Graph API"""
try:
# Check if we have a token for the current user
token_data = await load_token_from_file(currentUser)
if not token_data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated with Microsoft"
)
# Verify token is still valid
if not verify_token(token_data["access_token"]):
# Try to refresh the token
if not await refresh_token(currentUser["id"], currentUser):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired and refresh failed"
)
# Reload token data after refresh
token_data = await load_token_from_file(currentUser)
return JSONResponse({
"access_token": token_data["access_token"]
})
except Exception as e:
logger.error(f"Error getting access token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting access token: {str(e)}"
)
@router.post("/token")
async def get_backend_token(request: Request):
"""Convert MSAL token to backend token"""
try:
# Get the authorization header
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid authorization header"
)
# Extract the MSAL token
msal_token = auth_header.split(' ')[1]
# Verify the MSAL token and get user info
user_info = get_user_info_from_token(msal_token)
if not user_info:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid MSAL token"
)
# Get the user from the database using the email
gateway = getGatewayInterface()
user = gateway.getUserByUsername(user_info["email"])
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not registered in the system"
)
# Create backend token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = createAccessToken(
data={
"sub": user["username"],
"_mandateId": user["_mandateId"]
},
expiresDelta=access_token_expires
)
return {
"accessToken": access_token,
"tokenType": "bearer",
"user": {
"username": user["username"],
"email": user["email"],
"fullName": user.get("fullName", ""),
"_mandateId": user["_mandateId"]
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in MSAL token conversion: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error processing MSAL token: {str(e)}"
)
@router.post("/save-token") @router.post("/save-token")
async def save_token(token_data: Dict[str, Any], currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def save_token(token_data: Dict[str, Any], currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Save Microsoft token data from frontend""" """Save Microsoft token data from frontend"""
try: try:
# Save token to database # Get Microsoft interface
success = await save_token_to_file(token_data, currentUser) msft = getMsftInterface(currentUser)
# Save token
success = msft.saveMsftToken(token_data)
if success: if success:
return JSONResponse({ return JSONResponse({
@ -623,3 +246,45 @@ async def save_token(token_data: Dict[str, Any], currentUser: Dict[str, Any] = D
"success": False, "success": False,
"message": f"Error saving token: {str(e)}" "message": f"Error saving token: {str(e)}"
}) })
@router.post("/logout")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Logout from Microsoft"""
try:
# Get Microsoft interface
msft = getMsftInterface(currentUser)
# Delete token
success = msft.db.deleteToken(currentUser["id"])
if success:
return JSONResponse({
"message": "Successfully logged out from Microsoft"
})
else:
return JSONResponse({
"message": "Failed to logout from Microsoft"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)
@router.get("/token")
async def get_token(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get Microsoft token for current user."""
try:
# Get Microsoft interface
msft = getMsftInterface(currentUser)
# Get token
token = msft.getMsftToken()
if token:
return {"token": token}
return {"error": "No token found"}
except Exception as e:
logger.error(f"Error getting token: {str(e)}")
return {"error": str(e)}

View file

@ -2,35 +2,17 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Query, Path
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
from datetime import datetime from datetime import datetime
from dataclasses import dataclass
# Import auth module # Import auth module
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.security.auth import getCurrentActiveUser
# Import interfaces # Import interface
from modules.interfaces.lucydomInterface import getLucydomInterface from modules.interfaces.lucydomInterface import getInterface
from modules.interfaces.lucydomModel import Prompt from modules.interfaces.lucydomModel import Prompt, getModelAttributes
# Get all attributes of the model
def getModelAttributes(modelClass):
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')]
# Model attributes for Prompt # Model attributes for Prompt
promptAttributes = getModelAttributes(Prompt) promptAttributes = getModelAttributes(Prompt)
class AppContext:
def __init__(self, mandateId: str, userId: str):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
mandateId, userId = await getUserContext(currentUser)
return AppContext(mandateId, userId)
# Create router for prompt endpoints # Create router for prompt endpoints
router = APIRouter( router = APIRouter(
prefix="/api/prompts", prefix="/api/prompts",
@ -39,29 +21,27 @@ router = APIRouter(
) )
@router.get("", response_model=List[Dict[str, Any]]) @router.get("", response_model=List[Dict[str, Any]])
async def getPrompts( async def get_prompts(
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Get all prompts""" """Get all prompts"""
context = await getContext(currentUser) myInterface = getInterface(currentUser)
return myInterface.getAllPrompts()
# Retrieve prompts
return context.interfaceData.getAllPrompts()
@router.post("", response_model=Dict[str, Any]) @router.post("", response_model=Dict[str, Any])
async def createPrompt( async def create_prompt(
prompt: Dict[str, Any] = Body(...), prompt: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Create a new prompt""" """Create a new prompt"""
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Required fields with default values # Required fields with default values
content = prompt.get("content", "") content = prompt.get("content", "")
name = prompt.get("name", "New Prompt") name = prompt.get("name", "New Prompt")
# Create prompt # Create prompt
newPrompt = context.interfaceData.createPrompt( newPrompt = myInterface.createPrompt(
content=content, content=content,
name=name name=name
) )
@ -73,15 +53,15 @@ async def createPrompt(
return newPrompt return newPrompt
@router.get("/{promptId}", response_model=Dict[str, Any]) @router.get("/{promptId}", response_model=Dict[str, Any])
async def getPrompt( async def get_prompt(
promptId: str = Path(..., description="ID of the prompt"), promptId: str = Path(..., description="ID of the prompt"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Get a specific prompt""" """Get a specific prompt"""
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Get prompt # Get prompt
prompt = context.interfaceData.getPrompt(promptId) prompt = myInterface.getPrompt(promptId)
if not prompt: if not prompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -91,16 +71,16 @@ async def getPrompt(
return prompt return prompt
@router.put("/{promptId}", response_model=Dict[str, Any]) @router.put("/{promptId}", response_model=Dict[str, Any])
async def updatePrompt( async def update_prompt(
promptId: str = Path(..., description="ID of the prompt to update"), promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Dict[str, Any] = Body(...), promptData: Dict[str, Any] = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Update an existing prompt""" """Update an existing prompt"""
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Check if the prompt exists # Check if the prompt exists
existingPrompt = context.interfaceData.getPrompt(promptId) existingPrompt = myInterface.getPrompt(promptId)
if not existingPrompt: if not existingPrompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -112,7 +92,7 @@ async def updatePrompt(
name = promptData.get("name") name = promptData.get("name")
# Update prompt # Update prompt
updatedPrompt = context.interfaceData.updatePrompt( updatedPrompt = myInterface.updatePrompt(
promptId=promptId, promptId=promptId,
content=content, content=content,
name=name name=name
@ -127,22 +107,22 @@ async def updatePrompt(
return updatedPrompt return updatedPrompt
@router.delete("/{promptId}", response_model=Dict[str, Any]) @router.delete("/{promptId}", response_model=Dict[str, Any])
async def deletePrompt( async def delete_prompt(
promptId: str = Path(..., description="ID of the prompt to delete"), promptId: str = Path(..., description="ID of the prompt to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Delete a prompt""" """Delete a prompt"""
context = await getContext(currentUser) myInterface = getInterface(currentUser)
# Check if the prompt exists # Check if the prompt exists
existingPrompt = context.interfaceData.getPrompt(promptId) existingPrompt = myInterface.getPrompt(promptId)
if not existingPrompt: if not existingPrompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prompt with ID {promptId} not found" detail=f"Prompt with ID {promptId} not found"
) )
success = context.interfaceData.deletePrompt(promptId) success = myInterface.deletePrompt(promptId)
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View file

@ -2,392 +2,196 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
from datetime import datetime from datetime import datetime
from dataclasses import dataclass
import logging import logging
import time
import traceback
# Import auth module # Import auth module
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.security.auth import getCurrentActiveUser, getRootInterface
# Import interfaces # Import interfaces
from modules.interfaces.gatewayInterface import getGatewayInterface from modules.interfaces.gatewayInterface import getInterface
from modules.interfaces.gatewayModel import User from modules.interfaces.gatewayModel import User, getModelAttributes
# Set up logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Determine all attributes of the model
def getModelAttributes(modelClass):
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')]
# Model attributes for User # Model attributes for User
userAttributes = getModelAttributes(User) userAttributes = getModelAttributes(User)
@dataclass
class AppContext:
"""Context object for all required connections and user information"""
_mandateId: int
_userId: int
interfaceData: Any # Gateway Interface
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""Creates a central context object with all required connections"""
_mandateId, _userId = await getUserContext(currentUser)
interfaceData = getGatewayInterface(_mandateId, _userId)
return AppContext(
_mandateId=_mandateId,
_userId=_userId,
interfaceData=interfaceData
)
# Create router for user endpoints
router = APIRouter( router = APIRouter(
prefix="/api/users", prefix="/api/users",
tags=["Users"], tags=["Users"],
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("", response_model=List[Dict[str, Any]]) @router.get("/", response_model=List[Dict[str, Any]], tags=["Users"])
async def getUsers(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def get_users(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get all available users (only for Admin/SysAdmin users)""" """Get all users in the current mandate"""
context = await getContext(currentUser)
# Permission check
if currentUser.get("privilege") not in ["admin", "sysadmin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to access the user list"
)
# Admin sees only users of own mandate, SysAdmin sees all
if currentUser.get("privilege") == "admin":
return context.interfaceData.getUsersByMandate(context._mandateId)
else: # sysadmin
return context.interfaceData.getAllUsers()
@router.post("/register", response_model=Dict[str, Any])
async def registerUser(request: Request):
"""Register a new user."""
try: try:
# Get request data myInterface = getInterface(currentUser)
userData = await request.json() return myInterface.getUsers()
except Exception as e:
# Get root mandate and admin user IDs logger.error(f"Error getting users: {str(e)}")
adminGateway = getGatewayInterface()
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="System is not properly initialized with root mandate and admin user" detail=f"Failed to get users: {str(e)}"
) )
# Create a new gateway interface instance with admin context @router.get("/{userId}", response_model=Dict[str, Any], tags=["Users"])
adminGateway = getGatewayInterface(rootMandateId, adminUserId) async def get_user(
userId: str,
# Set default values if not provided currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
if "language" not in userData: ):
userData["language"] = "en" """Get a specific user by ID"""
if "authenticationAuthority" not in userData:
userData["authenticationAuthority"] = "local"
# Validate authentication authority
if userData["authenticationAuthority"] not in ["local", "microsoft"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid authentication authority: {userData['authenticationAuthority']}"
)
# Validate password for local authentication
if userData["authenticationAuthority"] == "local":
if "password" not in userData:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password is required for local authentication"
)
# Create the user
try: try:
createdUser = adminGateway.createUser( myInterface = getInterface(currentUser)
username=userData["username"], user = myInterface.getUserById(userId)
password=userData.get("password"),
email=userData.get("email"), if not user:
fullName=userData.get("fullName"), raise HTTPException(
language=userData["language"], status_code=status.HTTP_404_NOT_FOUND,
_mandateId=userData.get("_mandateId", rootMandateId), detail=f"User {userId} not found"
authenticationAuthority=userData["authenticationAuthority"]
) )
return user
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting user {userId}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get user: {str(e)}"
)
@router.post("/", response_model=Dict[str, Any], tags=["Users"])
async def create_user(
userData: Dict[str, Any],
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Create a new user"""
try:
# Get admin user for user creation
myInterface = getRootInterface()
# Check required fields
if not userData.get("username") or not userData.get("password"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username and password are required"
)
# Filter attributes based on model definition
filteredData = {}
for attr in userAttributes:
if attr in userData:
filteredData[attr] = userData[attr]
try:
createdUser = myInterface.createUser(**filteredData)
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e) detail=str(e)
) )
# Verify the user was created
if not createdUser: if not createdUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user" detail="Failed to create user"
) )
# For local authentication, verify password was stored
if userData["authenticationAuthority"] == "local":
if "hashedPassword" not in createdUser:
logger.error("Password not stored in user record")
# Try to delete the user
try:
adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully deleted user after password storage failure")
except Exception as e:
logger.error(f"Failed to delete user after password storage failure: {str(e)}")
raise HTTPException(status_code=500, detail="Password storage failed")
logger.info("User verification successful")
# Test authentication
try:
authResult = adminGateway.authenticateUser(userData["username"], userData["password"])
if not authResult:
logger.error("Authentication test failed after user creation")
# Try to delete the user
try:
adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully deleted user after authentication test failure")
except Exception as e:
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
raise HTTPException(status_code=500, detail="Authentication test failed")
except ValueError as e:
logger.error(f"Authentication test failed: {str(e)}")
# Try to delete the user
try:
adminGateway.deleteUser(createdUser["id"])
logger.info("Successfully deleted user after authentication test failure")
except Exception as e:
logger.error(f"Failed to delete user after authentication test failure: {str(e)}")
raise HTTPException(status_code=500, detail=f"Authentication test failed: {str(e)}")
logger.info("Authentication test successful")
# Remove sensitive data from response
if "hashedPassword" in createdUser:
del createdUser["hashedPassword"]
return createdUser return createdUser
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Unexpected error during user registration: {str(e)}") logger.error(f"Error creating user: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Registration failed: {str(e)}" detail=f"Failed to create user: {str(e)}"
) )
@router.post("/register-with-msal", response_model=Dict[str, Any]) @router.put("/{userId}", response_model=Dict[str, Any], tags=["Users"])
async def registerUserWithMsal(userData: dict = Body(...)): async def update_user(
"""Register a new user using Microsoft authentication""" userId: str,
# Add debug logging userData: Dict[str, Any],
import logging
logger = logging.getLogger(__name__)
logger.info(f"MSAL Registration request data: {userData}")
# Get the initial IDs for mandate and admin user
adminGateway = getGatewayInterface()
# Get ID of the root mandate - we'll use this for new users
rootMandateId = adminGateway.getInitialId("mandates")
adminUserId = adminGateway.getInitialId("users")
if not rootMandateId or not adminUserId:
raise HTTPException(
status_code=500,
detail="System is not properly initialized with root mandate and admin user"
)
# Use a gateway with admin context for user creation
gateway = getGatewayInterface(rootMandateId, adminUserId)
if "username" not in userData:
raise HTTPException(status_code=400, detail="Username required")
try:
# Create user data with a random password since it won't be used
import secrets
random_password = secrets.token_urlsafe(32)
# Create user with required fields
newUser = gateway.createUser(
username=userData["username"],
password=random_password, # Random password since MSAL auth will be used
email=userData.get("email"),
fullName=userData.get("fullName"),
language=userData.get("language", "de"),
_mandateId=rootMandateId,
disabled=False,
privilege="user"
)
return newUser
except ValueError as e:
logger.error(f"ValueError in MSAL registration: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except PermissionError as e:
logger.error(f"PermissionError in MSAL registration: {str(e)}")
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
import traceback
logger.error(f"Unexpected error in MSAL registration: {str(e)}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"MSAL Registration failed: {str(e)}")
@router.get("/{userId}", response_model=Dict[str, Any])
async def getUser(
userId: str = Path(..., description="ID of the user"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a specific user"""
context = await getContext(currentUser)
# Initialize gateway interface with user context
userToGet = context.interfaceData.getUser(userId)
if not userToGet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {userId} not found"
)
# Permission check
# User can only view themselves, Admin only users of their own mandate, SysAdmin all
if userId == str(context._userId):
# User can view themselves
pass
elif currentUser.get("privilege") == "admin" and userToGet.get("_mandateId") == context._mandateId:
# Admin can view users of their own mandate
pass
elif currentUser.get("privilege") == "sysadmin":
# SysAdmin can view all users
pass
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to view this user"
)
return userToGet
@router.put("/{userId}", response_model=Dict[str, Any])
async def updateUser(
userId: str = Path(..., description="ID of the user to update"),
userData: Dict[str, Any] = Body(..., description="Updated user data"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Update an existing user""" """Update an existing user"""
context = await getContext(currentUser) try:
# Get admin user for user updates
myInterface = getRootInterface()
# User exists? # Check if user exists
userToUpdate = context.interfaceData.getUser(userId) existingUser = myInterface.getUserById(userId)
if not userToUpdate: if not existingUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {userId} not found" detail=f"User {userId} not found"
) )
# Permission check # Filter attributes based on model definition
isSelfUpdate = userId == str(context._userId) filteredData = {}
isAdmin = currentUser.get("privilege") == "admin"
isSysadmin = currentUser.get("privilege") == "sysadmin"
sameMandate = userToUpdate.get("_mandateId") == context._mandateId
# Filter allowed fields based on permission level
allowedFields = {"username", "email", "fullName", "language"}
sensitiveFields = {"_mandateId", "disabled", "privilege"}
# Check if sensitive fields should be changed
sensitiveUpdate = any(field in userData for field in sensitiveFields)
if isSelfUpdate and sensitiveUpdate:
# Normal users cannot change their sensitive data
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to change sensitive user data"
)
elif isAdmin and sensitiveUpdate and not sameMandate:
# Admins can only change sensitive data for users of their own mandate
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to change sensitive data for users of other mandates"
)
elif not (isSelfUpdate or (isAdmin and sameMandate) or isSysadmin):
# No permission for other cases
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="No permission to update this user"
)
# Dynamically filter attributes from the request
updateData = {}
for attr in userAttributes: for attr in userAttributes:
if attr in userData and attr != "id": # ID cannot be changed if attr in userData:
updateData[attr] = userData[attr] filteredData[attr] = userData[attr]
# Remove disallowed fields for normal users
if not (isAdmin or isSysadmin):
updateData = {k: v for k, v in updateData.items() if k in allowedFields}
# Update user data # Update user data
updatedUser = context.interfaceData.updateUser(userId, updateData) try:
return updatedUser updatedUser = myInterface.updateUser(userId, **filteredData)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.delete("/{userId}", status_code=status.HTTP_204_NO_CONTENT) if not updatedUser:
async def deleteUser( raise HTTPException(
userId: str = Path(..., description="ID of the user to delete"), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user"
)
return updatedUser
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating user {userId}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update user: {str(e)}"
)
@router.delete("/{userId}", response_model=Dict[str, Any], tags=["Users"])
async def delete_user(
userId: str,
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Delete a user""" """Delete a user"""
context = await getContext(currentUser) try:
# Get admin user for user deletion
myInterface = getRootInterface()
# User exists? # Check if user exists
userToDelete = context.interfaceData.getUser(userId) existingUser = myInterface.getUserById(userId)
if not userToDelete: if not existingUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {userId} not found" detail=f"User {userId} not found"
) )
# Permission check # Delete user
isSelfDelete = userId == str(context._userId) try:
isAdmin = currentUser.get("privilege") == "admin" myInterface.deleteUser(userId)
isSysadmin = currentUser.get("privilege") == "sysadmin" except ValueError as e:
sameMandate = userToDelete.get("_mandateId") == context._mandateId
if isSelfDelete:
# User can delete themselves
pass
elif isAdmin and sameMandate:
# Admin can delete users of their own mandate
pass
elif isSysadmin:
# SysAdmin can delete all users
pass
else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_400_BAD_REQUEST,
detail="No permission to delete this user" detail=str(e)
) )
# Delete user and all referenced objects return {"message": f"User {userId} deleted successfully"}
success = context.interfaceData.deleteUser(userId) except HTTPException:
if not success: raise
except Exception as e:
logger.error(f"Error deleting user {userId}: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting user with ID {userId}" detail=f"Failed to delete user: {str(e)}"
) )
return None

View file

@ -8,16 +8,16 @@ import json
import logging import logging
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
# Import interfaces # Import interfaces
from modules.interfaces.lucydomInterface import getLucydomInterface from modules.interfaces.lucydomInterface import getInterface as getInterfaceLucydom
from modules.security.auth import getCurrentActiveUser, getUserContext from modules.interfaces.msftInterface import getInterface as getInterfaceMsft
from modules.security.auth import getCurrentActiveUser
from modules.workflow.workflowManager import getWorkflowManager from modules.workflow.workflowManager import getWorkflowManager
# Import models # Import models
from modules.interfaces.lucydomModel import UserInputRequest from modules.interfaces import lucydomModel as Models
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,47 +29,34 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
class AppContext:
def __init__(self, mandateId: int, userId: int):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
mandateId, userId = await getUserContext(currentUser)
return AppContext(mandateId, userId)
# State 1: Workflow Initialization endpoint # State 1: Workflow Initialization endpoint
@router.post("/start", response_model=Dict[str, Any]) @router.post("/start", response_model=Dict[str, Any])
async def startWorkflow( async def startWorkflow(
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"), workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
userInput: UserInputRequest = Body(...), userInput: Models.UserInputRequest = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """
Starts a new workflow or continues an existing one. Starts a new workflow or continues an existing one.
Corresponds to State 1 in the state machine documentation. Corresponds to State 1 in the state machine documentation.
Args:
workflowId: Optional ID of an existing workflow to continue
userInput: User input with prompt and optional file list
currentUser: Authenticated user
Returns:
Dictionary with workflow ID and status
""" """
context = await getContext(currentUser)
try: try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
interfaceMsft = getInterfaceMsft(currentUser)
# Convert the user input to a dictionary # Convert the user input to a dictionary
userInputDict = { userInputDict = {
"prompt": userInput.prompt, "prompt": userInput.prompt,
"listFileId": userInput.listFileId "listFileId": userInput.listFileId
} }
# Get workflow manager with interface
workflowManager = await getWorkflowManager(interfaceBase, interfaceMsft)
# Start or continue workflow using the workflow manager # Start or continue workflow using the workflow manager
workflow = await getWorkflowManager(context._mandateId, context._userId).workflowStart(userInputDict, workflowId) workflow = await workflowManager.workflowStart(userInputDict, workflowId)
logger.info("User Input received. Answer:",workflow) logger.info("User Input received. Answer:", workflow)
return { return {
"id": workflow.get("id"), "id": workflow.get("id"),
@ -89,36 +76,29 @@ async def stopWorkflow(
workflowId: str = Path(..., description="ID of the workflow to stop"), workflowId: str = Path(..., description="ID of the workflow to stop"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Stops a running workflow."""
Stops a running workflow.
Corresponds to State 8 in the state machine documentation.
Args:
workflowId: ID of the workflow to stop
currentUser: Authenticated user
Returns:
Dictionary with status information
"""
context = await getContext(currentUser)
try: try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
interfaceMsft = getInterfaceMsft(currentUser)
# Verify workflow exists and belongs to user # Verify workflow exists and belongs to user
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
if workflow.get("_userId") != context._userId: if workflow.get("_userId") != currentUser["id"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to stop this workflow" detail="You don't have permission to stop this workflow"
) )
# Stop the workflow # Stop the workflow
stoppedWorkflow = getWorkflowManager(context._mandateId, context._userId).workflowStop(workflowId) workflowManager = await getWorkflowManager(interfaceBase, interfaceMsft)
stoppedWorkflow = await workflowManager.workflowStop(workflowId)
return { return {
"id": workflowId, "id": workflowId,
@ -126,7 +106,6 @@ async def stopWorkflow(
"message": "Workflow has been stopped" "message": "Workflow has been stopped"
} }
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error stopping workflow: {str(e)}", exc_info=True) logger.error(f"Error stopping workflow: {str(e)}", exc_info=True)
@ -141,22 +120,13 @@ async def deleteWorkflow(
workflowId: str = Path(..., description="ID of the workflow to delete"), workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Deletes a workflow and its associated data."""
Deletes a workflow and its associated data.
Corresponds to State 11 in the state machine documentation.
Args:
workflowId: ID of the workflow to delete
currentUser: Authenticated user
Returns:
Dictionary with status information
"""
context = await getContext(currentUser)
try: try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists # Verify workflow exists
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -164,14 +134,14 @@ async def deleteWorkflow(
) )
# Check if user has permission to delete # Check if user has permission to delete
if workflow.get("_userId") != context._userId: if workflow.get("_userId") != currentUser["id"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to delete this workflow" detail="You don't have permission to delete this workflow"
) )
# Delete workflow # Delete workflow
success = context.interfaceData.deleteWorkflow(workflowId) success = interfaceBase.deleteWorkflow(workflowId)
if not success: if not success:
raise HTTPException( raise HTTPException(
@ -184,7 +154,6 @@ async def deleteWorkflow(
"message": "Workflow and associated data deleted successfully" "message": "Workflow and associated data deleted successfully"
} }
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error deleting workflow: {str(e)}", exc_info=True) logger.error(f"Error deleting workflow: {str(e)}", exc_info=True)
@ -198,20 +167,13 @@ async def deleteWorkflow(
async def listWorkflows( async def listWorkflows(
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """List all workflows for the current user."""
List all workflows for the current user.
Args:
currentUser: Authenticated user
Returns:
List of workflow objects
"""
context = await getContext(currentUser)
try: try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Retrieve workflows for the user # Retrieve workflows for the user
workflows = context.interfaceData.getWorkflowsByUser(context._userId) workflows = interfaceBase.getWorkflowsByUser(currentUser["id"])
return workflows return workflows
except Exception as e: except Exception as e:
logger.error(f"Error listing workflows: {str(e)}", exc_info=True) logger.error(f"Error listing workflows: {str(e)}", exc_info=True)
@ -226,21 +188,13 @@ async def getWorkflowStatus(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Get the current status of a workflow."""
Get the current status of a workflow.
Args:
workflowId: ID of the workflow
currentUser: Authenticated user
Returns:
Dictionary with workflow status information
"""
context = await getContext(currentUser)
try: try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Retrieve workflow # Retrieve workflow
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -260,7 +214,6 @@ async def getWorkflowStatus(
return statusInfo return statusInfo
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error getting workflow status: {str(e)}", exc_info=True) logger.error(f"Error getting workflow status: {str(e)}", exc_info=True)
@ -276,23 +229,13 @@ async def getWorkflowLogs(
logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"), logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Get logs for a workflow with support for selective data transfer."""
Get logs for a workflow with support for selective data transfer.
If logId is provided, returns only logs with IDs equal to or newer than the specified ID.
Args:
workflowId: ID of the workflow
logId: Optional ID to get only newer logs
currentUser: Authenticated user
Returns:
List of log entries
"""
context = await getContext(currentUser)
try: try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists # Verify workflow exists
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -300,20 +243,18 @@ async def getWorkflowLogs(
) )
# Get all logs # Get all logs
allLogs = context.interfaceData.getWorkflowLogs(workflowId) allLogs = interfaceBase.getWorkflowLogs(workflowId)
# Apply selective data transfer if logId is provided # Apply selective data transfer if logId is provided
if logId: if logId:
# Find the index of the specified log # Find the index of the log with the given ID
logIndex = next((i for i, log in enumerate(allLogs) if log.get("id") == logId), None) logIndex = next((i for i, log in enumerate(allLogs) if log.get("id") == logId), -1)
if logIndex >= 0:
if logIndex is not None: # Return only logs after the specified log
# Return logs from this index onwards return allLogs[logIndex + 1:]
return allLogs[logIndex:]
return allLogs return allLogs
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error getting workflow logs: {str(e)}", exc_info=True) logger.error(f"Error getting workflow logs: {str(e)}", exc_info=True)
@ -329,23 +270,13 @@ async def getWorkflowMessages(
messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"), messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Get messages for a workflow with support for selective data transfer."""
Get messages for a workflow with support for selective data transfer.
If messageId is provided, returns only messages with IDs equal to or newer than the specified ID.
Args:
workflowId: ID of the workflow
messageId: Optional ID to get only newer messages
currentUser: Authenticated user
Returns:
List of message objects
"""
context = await getContext(currentUser)
try: try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists # Verify workflow exists
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -353,7 +284,7 @@ async def getWorkflowMessages(
) )
# Get all messages # Get all messages
allMessages = context.interfaceData.getWorkflowMessages(workflowId) allMessages = interfaceBase.getWorkflowMessages(workflowId)
# Apply selective data transfer if messageId is provided # Apply selective data transfer if messageId is provided
if messageId: if messageId:
@ -373,7 +304,6 @@ async def getWorkflowMessages(
allMessages.sort(key=lambda x: x.get("sequenceNo", 0)) allMessages.sort(key=lambda x: x.get("sequenceNo", 0))
return allMessages return allMessages
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error getting workflow messages: {str(e)}", exc_info=True) logger.error(f"Error getting workflow messages: {str(e)}", exc_info=True)
@ -390,36 +320,21 @@ async def deleteWorkflowMessage(
messageId: str = Path(..., description="ID of the message to delete"), messageId: str = Path(..., description="ID of the message to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Delete a message from a workflow."""
Delete a message from a workflow.
Args:
workflowId: ID of the workflow
messageId: ID of the message to delete
currentUser: Authenticated user
Returns:
Dictionary with status information
"""
context = await getContext(currentUser)
try: try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists and belongs to user # Verify workflow exists and belongs to user
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow"
)
# Delete the message # Delete the message
success = context.interfaceData.deleteWorkflowMessage(workflowId, messageId) success = interfaceBase.deleteWorkflowMessage(workflowId, messageId)
if not success: if not success:
raise HTTPException( raise HTTPException(
@ -431,7 +346,7 @@ async def deleteWorkflowMessage(
messageIds = workflow.get("messageIds", []) messageIds = workflow.get("messageIds", [])
if messageId in messageIds: if messageId in messageIds:
messageIds.remove(messageId) messageIds.remove(messageId)
context.interfaceData.updateWorkflow(workflowId, {"messageIds": messageIds}) interfaceBase.updateWorkflow(workflowId, {"messageIds": messageIds})
return { return {
"workflowId": workflowId, "workflowId": workflowId,
@ -439,7 +354,6 @@ async def deleteWorkflowMessage(
"message": "Message deleted successfully" "message": "Message deleted successfully"
} }
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error deleting message: {str(e)}", exc_info=True) logger.error(f"Error deleting message: {str(e)}", exc_info=True)
@ -455,38 +369,21 @@ async def deleteFileFromMessage(
fileId: str = Path(..., description="ID of the file to delete"), fileId: str = Path(..., description="ID of the file to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Delete a file reference from a message in a workflow."""
Delete a file reference from a message in a workflow.
The file itself is not deleted from the database, only the reference in the message.
Args:
workflowId: ID of the workflow
messageId: ID of the message
fileId: ID of the file to delete
currentUser: Authenticated user
Returns:
Dictionary with status information
"""
context = await getContext(currentUser)
try: try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists and belongs to user # Verify workflow exists and belongs to user
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
if workflow.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to modify this workflow"
)
# Delete file reference from message # Delete file reference from message
success = context.interfaceData.deleteFileFromMessage(workflowId, messageId, fileId) success = interfaceBase.deleteFileFromMessage(workflowId, messageId, fileId)
if not success: if not success:
raise HTTPException( raise HTTPException(
@ -501,7 +398,6 @@ async def deleteFileFromMessage(
"message": "File reference deleted successfully" "message": "File reference deleted successfully"
} }
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error deleting file reference: {str(e)}", exc_info=True) logger.error(f"Error deleting file reference: {str(e)}", exc_info=True)
@ -517,36 +413,21 @@ async def previewFile(
fileId: str = Path(..., description="ID of the file to preview"), fileId: str = Path(..., description="ID of the file to preview"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Get file metadata and a preview of the file content."""
Get file metadata and a preview of the file content.
Args:
fileId: ID of the file
currentUser: Authenticated user
Returns:
Dictionary with file metadata and preview content
"""
context = await getContext(currentUser)
try: try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get file metadata # Get file metadata
file = context.interfaceData.getFile(fileId) file = interfaceBase.getFile(fileId)
if not file: if not file:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found" detail=f"File with ID {fileId} not found"
) )
# Check if file belongs to user or their mandate
if file.get("_mandateId") != context._mandateId and file.get("_userId") != context._userId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this file"
)
# Get file data (limited for preview) # Get file data (limited for preview)
fileData = context.interfaceData.getFileData(fileId) fileData = interfaceBase.getFileData(fileId)
if fileData is None: if fileData is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -564,7 +445,7 @@ async def previewFile(
previewData = None previewData = None
# Get base64Encoded flag from database # Get base64Encoded flag from database
fileDataEntries = context.interfaceData.db.getRecordset("fileData", recordFilter={"id": fileId}) fileDataEntries = interfaceBase.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries and "base64Encoded" in fileDataEntries[0]: if fileDataEntries and "base64Encoded" in fileDataEntries[0]:
# Use the flag from the database # Use the flag from the database
base64Encoded = fileDataEntries[0]["base64Encoded"] base64Encoded = fileDataEntries[0]["base64Encoded"]
@ -606,7 +487,6 @@ async def previewFile(
"base64Encoded": base64Encoded "base64Encoded": base64Encoded
} }
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error previewing file: {str(e)}", exc_info=True) logger.error(f"Error previewing file: {str(e)}", exc_info=True)
@ -620,21 +500,13 @@ async def downloadFile(
fileId: str = Path(..., description="ID of the file to download"), fileId: str = Path(..., description="ID of the file to download"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
""" """Download a file."""
Download a file.
Args:
fileId: ID of the file
currentUser: Authenticated user
Returns:
File data with appropriate headers
"""
context = await getContext(currentUser)
try: try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get file data # Get file data
fileInfo = context.interfaceData.downloadFile(fileId) fileInfo = interfaceBase.downloadFile(fileId)
if not fileInfo: if not fileInfo:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -650,7 +522,6 @@ async def downloadFile(
} }
) )
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions
raise raise
except Exception as e: except Exception as e:
logger.error(f"Error downloading file: {str(e)}", exc_info=True) logger.error(f"Error downloading file: {str(e)}", exc_info=True)
@ -661,42 +532,65 @@ async def downloadFile(
@router.get("/workflows", response_model=List[Dict[str, Any]]) @router.get("/workflows", response_model=List[Dict[str, Any]])
async def getWorkflows(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): async def getWorkflows(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
context = await getContext(currentUser) """Get all workflows for the mandate."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get all workflows for the mandate # Get all workflows for the mandate
workflows = context.interfaceData.getWorkflowsByMandate(context._mandateId) workflows = interfaceBase.getWorkflowsByMandate(currentUser.get("_mandateId"))
return workflows return workflows
except Exception as e:
logger.error(f"Error getting workflows: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting workflows: {str(e)}"
)
@router.post("/workflows", response_model=Dict[str, Any]) @router.post("/workflows", response_model=Dict[str, Any])
async def createWorkflow( async def createWorkflow(
workflow: Dict[str, Any], workflow: Dict[str, Any],
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
context = await getContext(currentUser) """Create a new workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Create workflow # Create workflow
newWorkflow = context.interfaceData.createWorkflow(workflow) newWorkflow = interfaceBase.createWorkflow(workflow)
return newWorkflow return newWorkflow
except Exception as e:
logger.error(f"Error creating workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error creating workflow: {str(e)}"
)
@router.get("/workflows/{workflowId}", response_model=Dict[str, Any]) @router.get("/workflows/{workflowId}", response_model=Dict[str, Any])
async def getWorkflow( async def getWorkflow(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
context = await getContext(currentUser) """Get a specific workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow # Get workflow
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found") raise HTTPException(status_code=404, detail="Workflow not found")
# Check if user has access to this workflow
if workflow.get("_userId") != context._userId:
raise HTTPException(status_code=403, detail="Not authorized to access this workflow")
return workflow return workflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting workflow: {str(e)}"
)
@router.put("/workflows/{workflowId}", response_model=Dict[str, Any]) @router.put("/workflows/{workflowId}", response_model=Dict[str, Any])
async def updateWorkflow( async def updateWorkflow(
@ -704,44 +598,57 @@ async def updateWorkflow(
workflowId: str = Path(..., description="ID of the workflow to update"), workflowId: str = Path(..., description="ID of the workflow to update"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
context = await getContext(currentUser) """Update an existing workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow # Get workflow
existingWorkflow = context.interfaceData.getWorkflow(workflowId) existingWorkflow = interfaceBase.getWorkflow(workflowId)
if not existingWorkflow: if not existingWorkflow:
raise HTTPException(status_code=404, detail="Workflow not found") raise HTTPException(status_code=404, detail="Workflow not found")
# Check if user has access to this workflow
if existingWorkflow.get("_userId") != context._userId:
raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
# Update workflow # Update workflow
updatedWorkflow = context.interfaceData.updateWorkflow(workflowId, workflow) updatedWorkflow = interfaceBase.updateWorkflow(workflowId, workflow)
return updatedWorkflow return updatedWorkflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating workflow: {str(e)}"
)
@router.delete("/workflows/{workflowId}") @router.delete("/workflows/{workflowId}")
async def deleteWorkflow( async def deleteWorkflow(
workflowId: str = Path(..., description="ID of the workflow to delete"), workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
context = await getContext(currentUser) """Delete a workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow # Get workflow
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found") raise HTTPException(status_code=404, detail="Workflow not found")
# Check if user has access to this workflow
if workflow.get("_userId") != context._userId:
raise HTTPException(status_code=403, detail="Not authorized to delete this workflow")
# Delete workflow # Delete workflow
success = context.interfaceData.deleteWorkflow(workflowId) success = interfaceBase.deleteWorkflow(workflowId)
if not success: if not success:
raise HTTPException(status_code=500, detail="Failed to delete workflow") raise HTTPException(status_code=500, detail="Failed to delete workflow")
return {"status": "success"} return {"status": "success"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting workflow: {str(e)}"
)
@router.post("/workflows/{workflowId}/files/{fileId}") @router.post("/workflows/{workflowId}/files/{fileId}")
async def addFileToWorkflow( async def addFileToWorkflow(
@ -750,29 +657,31 @@ async def addFileToWorkflow(
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
): ):
"""Add a file to a workflow.""" """Add a file to a workflow."""
context = await getContext(currentUser) try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow # Get workflow
workflow = context.interfaceData.getWorkflow(workflowId) workflow = interfaceBase.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found") raise HTTPException(status_code=404, detail="Workflow not found")
# Check access
if workflow.get("_userId") != context._userId:
raise HTTPException(status_code=403, detail="No access to this workflow")
# Get file # Get file
file = context.interfaceData.getFile(fileId) file = interfaceBase.getFile(fileId)
if not file: if not file:
raise HTTPException(status_code=404, detail="File not found") raise HTTPException(status_code=404, detail="File not found")
# Check file access
if file.get("_mandateId") != context._mandateId and file.get("_userId") != context._userId:
raise HTTPException(status_code=403, detail="No access to this file")
# Add file to workflow # Add file to workflow
success = context.interfaceData.addFileToWorkflow(workflowId, fileId) success = interfaceBase.addFileToWorkflow(workflowId, fileId)
if not success: if not success:
raise HTTPException(status_code=500, detail="Failed to add file to workflow") raise HTTPException(status_code=500, detail="Failed to add file to workflow")
return {"status": "success"} return {"status": "success"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error adding file to workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error adding file to workflow: {str(e)}"
)

View file

@ -10,7 +10,7 @@ from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt from jose import JWTError, jwt
import logging import logging
from modules.interfaces.gatewayInterface import getGatewayInterface from modules.interfaces.gatewayInterface import getInterface
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
# Get Config Data # Get Config Data
@ -47,7 +47,7 @@ def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> s
return encodedJwt return encodedJwt
async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]: def _getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
""" """
Extracts and validates the current user from the JWT token. Extracts and validates the current user from the JWT token.
@ -88,7 +88,7 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
raise credentialsException raise credentialsException
# Initialize Gateway Interface with context # Initialize Gateway Interface with context
gateway = getGatewayInterface(_mandateId, _userId) gateway = getRootInterface()
# Retrieve user from database # Retrieve user from database
user = gateway.getUserByUsername(username) user = gateway.getUserByUsername(username)
@ -111,65 +111,7 @@ async def getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
return user return user
async def getUserContext(currentUser: Dict[str, Any]) -> Tuple[str, str]: def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(_getCurrentUser)) -> Dict[str, Any]:
"""
Extracts the mandate ID and user ID from the current user.
Args:
currentUser: The current user
Returns:
Tuple of (_mandateId, _userId) as strings
Raises:
HTTPException: If mandate or user ID is missing
"""
# Extract _mandateId
_mandateId = currentUser.get("_mandateId")
if not _mandateId:
logger.error("No _mandateId found in currentUser")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing mandate context"
)
# Extract _userId
_userId = currentUser.get("id") # Note: using 'id' instead of '_userId'
if not _userId:
logger.error("No _userId found in currentUser")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing user context"
)
return str(_mandateId), str(_userId)
def getInitialContext() -> tuple[str, str]:
"""
Returns the initial mandate and user IDs from the gateway.
This is used by other interfaces to get their context.
Returns:
tuple[str, str]: (_mandateId, _userId) or (None, None) if not available
"""
gateway = getGatewayInterface()
mandateId = gateway.getInitialId("mandates")
userId = gateway.getInitialId("users")
return mandateId, userId
async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, Any]:
"""
Gets the current active user and verifies their authentication authority.
Args:
currentUser: The current user from getCurrentUser
Returns:
The current user data
Raises:
HTTPException: If user is disabled or has invalid authentication authority
"""
if currentUser.get("disabled", False): if currentUser.get("disabled", False):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
@ -184,3 +126,14 @@ async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentU
) )
return currentUser return currentUser
def getRootInterface() -> Dict[str, Any]:
try:
return getInterface(currentUser={"id": "-1", "_mandateId": "-1"})
except Exception as e:
logger.error(f"Error getting root access: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get root access: {str(e)}"
)

View file

@ -26,18 +26,18 @@ class AgentBase:
self.description = "Base agent functionality" self.description = "Base agent functionality"
self.capabilities = [] self.capabilities = []
self.workflowManager = None self.workflowManager = None
self.mydom = None self.service = None
def setWorkflowManager(self, workflowManager): def setWorkflowManager(self, workflowManager):
"""Set the workflow manager reference.""" """Set the workflow manager reference."""
self.workflowManager = workflowManager self.workflowManager = workflowManager
# Also set mydom reference from workflow manager # Also set service reference from workflow manager
if workflowManager and hasattr(workflowManager, 'mydom'): if workflowManager and hasattr(workflowManager, 'service'):
self.mydom = workflowManager.mydom self.service = workflowManager.service
def setMydom(self, mydom): def setService(self, service):
"""Set the LucyDOM interface reference.""" """Set the service container reference."""
self.mydom = mydom self.service = service
def getAgentInfo(self) -> Dict[str, Any]: def getAgentInfo(self) -> Dict[str, Any]:
""" """

View file

@ -32,13 +32,13 @@ class AgentRegistry:
self.agents = {} self.agents = {}
self._loadAgents() self._loadAgents()
def initialize(self, mydom=None, workflowManager=None): def initialize(self, service=None, workflowManager=None):
"""Initialize or update the registry with workflow manager and LucyDOM references.""" """Initialize or update the registry with workflow manager and service references."""
for agent in self.agents.values(): for agent in self.agents.values():
if workflowManager and hasattr(agent, 'setWorkflowManager'): if workflowManager and hasattr(agent, 'setWorkflowManager'):
agent.setWorkflowManager(workflowManager) agent.setWorkflowManager(workflowManager)
elif mydom and hasattr(agent, 'setMydom'): if service and hasattr(agent, 'setService'):
agent.setMydom(mydom) agent.setService(service)
def _loadAgents(self): def _loadAgents(self):
"""Load all available agents from modules.""" """Load all available agents from modules."""

View file

@ -994,7 +994,7 @@ def processFile(self, fileContent: bytes, fileName: str, fileMetadata: Dict[str,
try: try:
# Get file extension and MIME type # Get file extension and MIME type
fileExtension = os.path.splitext(fileName)[1].lower()[1:] fileExtension = os.path.splitext(fileName)[1].lower()[1:]
mimeType = fileMetadata.get("mimeType", self.mydom.getMimeType(fileName)) if fileMetadata else self.mydom.getMimeType(fileName) mimeType = fileMetadata.get("mimeType", self.serviceBase.getMimeType(fileName)) if fileMetadata else self.serviceBase.getMimeType(fileName)
# Process based on file type # Process based on file type
if mimeType.startswith("image/"): if mimeType.startswith("image/"):

View file

@ -7,18 +7,15 @@ import asyncio
import os import os
import logging import logging
import json import json
import re
import uuid import uuid
import base64 import base64
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, Union, Tuple from typing import Dict, Any, List, Optional, Union, Tuple
import time import time
from modules.shared.mimeUtils import isTextMimeType, determineContentEncoding from modules.shared.mimeUtils import isTextMimeType
# Required imports # Required imports
from modules.workflow.agentRegistry import getAgentRegistry from modules.workflow.agentRegistry import getAgentRegistry
from modules.interfaces.lucydomInterface import getLucydomInterface as domInterface
from modules.workflow.documentProcessor import getDocumentContents from modules.workflow.documentProcessor import getDocumentContents
# Configure logger # Configure logger
@ -43,20 +40,25 @@ class WorkflowStoppedException(Exception):
class WorkflowManager: class WorkflowManager:
"""Manages the execution of workflows and their associated agents.""" """Manages the execution of workflows and their associated agents."""
def __init__(self, _mandateId: str, _userId: str): def __init__(self, interfaceBase, interfaceMsft):
"""Initialize the workflow manager with mandate and user context.""" """Initialize the workflow manager with interface."""
self._mandateId = _mandateId # Create service container
self._userId = _userId self.service = type('ServiceContainer', (), {
self.mydom = domInterface(_mandateId, _userId) 'base': interfaceBase,
'msft': interfaceMsft
})
self._mandateId = interfaceBase._mandateId
self._userId = interfaceBase._userId
self.agentRegistry = getAgentRegistry() self.agentRegistry = getAgentRegistry()
self.agentRegistry.initialize(mydom=self.mydom, workflowManager=self) self.agentRegistry.initialize(service=self.service, workflowManager=self)
def workflowStart(self, workflowId: str, workflowData: dict) -> dict: def workflowStart(self, workflowId: str, workflowData: dict) -> dict:
"""Start a new workflow with the given ID and data.""" """Start a new workflow with the given ID and data."""
try: try:
# Update the LucyDOM interface with current user context # Update the LucyDOM interface with current user context
self.mydom._mandateId = self._mandateId self.service.base._mandateId = self._mandateId
self.mydom._userId = self._userId self.service.base._userId = self._userId
# Initialize workflow state # Initialize workflow state
workflowState = { workflowState = {
@ -111,7 +113,7 @@ class WorkflowManager:
### Forces exit ### Forces exit
def checkExitCriteria(self, workflow: Dict[str, Any]): def checkExitCriteria(self, workflow: Dict[str, Any]):
current_workflow = self.mydom.loadWorkflowState(workflow["id"]) current_workflow = self.service.base.loadWorkflowState(workflow["id"])
if current_workflow["status"] in ["stopped", "failed"]: if current_workflow["status"] in ["stopped", "failed"]:
self.logAdd(workflow, f"Workflow processing terminated due to status: {current_workflow['status']}", level="info") self.logAdd(workflow, f"Workflow processing terminated due to status: {current_workflow['status']}", level="info")
# Raise an exception to stop execution # Raise an exception to stop execution
@ -128,7 +130,7 @@ class WorkflowManager:
Returns: Returns:
Updated workflow with status="stopped" Updated workflow with status="stopped"
""" """
workflow = self.mydom.loadWorkflowState(workflowId) workflow = self.service.base.loadWorkflowState(workflowId)
if not workflow: if not workflow:
return {"error": "Workflow not found", "status": "failed"} return {"error": "Workflow not found", "status": "failed"}
@ -137,7 +139,7 @@ class WorkflowManager:
workflow["lastActivity"] = datetime.now().isoformat() workflow["lastActivity"] = datetime.now().isoformat()
# Update in database # Update in database
self.mydom.updateWorkflow(workflowId, { self.service.base.updateWorkflow(workflowId, {
"status": workflow["status"], "status": workflow["status"],
"lastActivity": workflow["lastActivity"] "lastActivity": workflow["lastActivity"]
}) })
@ -172,10 +174,10 @@ class WorkflowManager:
objWorkplan = projectManagerResponse.get("objWorkplan", []) objWorkplan = projectManagerResponse.get("objWorkplan", [])
objUserResponse = projectManagerResponse.get("objUserResponse", "") objUserResponse = projectManagerResponse.get("objUserResponse", "")
# Get detected language and set it in the mydom interface # Get detected language and set it in the serviceBase interface
self.checkExitCriteria(workflow) self.checkExitCriteria(workflow)
userLanguage = projectManagerResponse.get("userLanguage", "en") userLanguage = projectManagerResponse.get("userLanguage", "en")
self.mydom.setUserLanguage(userLanguage) self.service.base.setUserLanguage(userLanguage)
# Save the response as a message in the workflow and add log entries # Save the response as a message in the workflow and add log entries
self.checkExitCriteria(workflow) self.checkExitCriteria(workflow)
@ -276,7 +278,7 @@ class WorkflowManager:
workflow["dataStats"]["processingTime"] = endTime - startTime workflow["dataStats"]["processingTime"] = endTime - startTime
# Update in database # Update in database
self.mydom.updateWorkflow(workflow["id"], { self.service.base.updateWorkflow(workflow["id"], {
"status": "failed", "status": "failed",
"lastActivity": workflow["lastActivity"], "lastActivity": workflow["lastActivity"],
"dataStats": workflow["dataStats"] "dataStats": workflow["dataStats"]
@ -297,7 +299,7 @@ class WorkflowManager:
""" """
currentTime = datetime.now().isoformat() currentTime = datetime.now().isoformat()
workflowExist=self.mydom.getWorkflow(workflowId) workflowExist=self.service.base.getWorkflow(workflowId)
if workflowId is None or not workflowExist: if workflowId is None or not workflowExist:
# Create new workflow # Create new workflow
newWorkflowId = str(uuid.uuid4()) if workflowId is None else workflowId newWorkflowId = str(uuid.uuid4()) if workflowId is None else workflowId
@ -334,14 +336,14 @@ class WorkflowManager:
"lastActivity": workflow["lastActivity"], "lastActivity": workflow["lastActivity"],
"messageIds": workflow["messageIds"] # Include messageIds "messageIds": workflow["messageIds"] # Include messageIds
} }
self.mydom.createWorkflow(workflowDb) self.service.base.createWorkflow(workflowDb)
self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["init"], level="info", progress=0) self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["init"], level="info", progress=0)
logger.debug(f"CHECK DATA {workflow}") logger.debug(f"CHECK DATA {workflow}")
return workflow return workflow
else: else:
# State 10: Workflow Resumption - Load existing workflow # State 10: Workflow Resumption - Load existing workflow
workflow = self.mydom.loadWorkflowState(workflowId) workflow = self.service.base.loadWorkflowState(workflowId)
# Ensure messageIds exists # Ensure messageIds exists
if "messageIds" not in workflow: if "messageIds" not in workflow:
@ -349,7 +351,7 @@ class WorkflowManager:
workflow["messageIds"] = [msg["id"] for msg in workflow.get("messages", [])] workflow["messageIds"] = [msg["id"] for msg in workflow.get("messages", [])]
# Update in database # Update in database
self.mydom.updateWorkflow(workflowId, {"messageIds": workflow["messageIds"]}) self.service.base.updateWorkflow(workflowId, {"messageIds": workflow["messageIds"]})
# Update status and increment round counter # Update status and increment round counter
workflow["status"] = "running" workflow["status"] = "running"
@ -380,7 +382,7 @@ class WorkflowManager:
"currentRound": workflow["currentRound"], "currentRound": workflow["currentRound"],
"dataStats": workflow["dataStats"] # Include updated dataStats "dataStats": workflow["dataStats"] # Include updated dataStats
} }
self.mydom.updateWorkflow(workflowId, workflowUpdate) self.service.base.updateWorkflow(workflowId, workflowUpdate)
self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["running"], level="info", progress=0) self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["running"], level="info", progress=0)
return workflow return workflow
@ -406,7 +408,7 @@ class WorkflowManager:
workflow["lastActivity"] = workflowUpdate["lastActivity"] workflow["lastActivity"] = workflowUpdate["lastActivity"]
# Save workflow state to database - only relevant fields # Save workflow state to database - only relevant fields
self.mydom.updateWorkflow(workflow["id"], workflowUpdate) self.service.base.updateWorkflow(workflow["id"], workflowUpdate)
self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["completed"], level="info", progress=100) self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["completed"], level="info", progress=100)
return workflow return workflow
@ -528,9 +530,9 @@ JSON_OUTPUT = {{
4. If you use label for an existing file 4. If you use label for an existing file
""" """
# Call the AI service through mydom for language support # Call the AI service through serviceBase for language support
logger.debug(f"PROJECT MANAGER Planning prompt: {prompt}") logger.debug(f"PROJECT MANAGER Planning prompt: {prompt}")
projectManagerOutput = await self.mydom.callAi([ projectManagerOutput = await self.service.base.callAi([
{ {
"role": "system", "role": "system",
"content": "You are an experienced project manager who analyzes user requests and creates work plans. You pay very careful attention to ensure that all document dependencies are correct and that no non-existent documents are defined as inputs. The output follows strictly the specified format." "content": "You are an experienced project manager who analyzes user requests and creates work plans. You pay very careful attention to ensure that all document dependencies are correct and that no non-existent documents are defined as inputs. The output follows strictly the specified format."
@ -608,7 +610,7 @@ JSON_OUTPUT = {{
"workflowRound": workflow.get("currentRound", 1), "workflowRound": workflow.get("currentRound", 1),
"agentType": agentName, "agentType": agentName,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"language": self.mydom.userLanguage # Pass language to agent "language": self.service.base.userLanguage # Pass language to agent
} }
} }
@ -618,7 +620,7 @@ JSON_OUTPUT = {{
logger.debug("TASK: "+self.parseJson2text(agentTask)) logger.debug("TASK: "+self.parseJson2text(agentTask))
# Ensure AI service is available # Ensure AI service is available
if not self.mydom.aiService: if not self.service.base.aiService:
logger.error("AI service not available in LucyDOM interface") logger.error("AI service not available in LucyDOM interface")
self.logAdd(workflow, "Error: AI service not available", level="error") self.logAdd(workflow, "Error: AI service not available", level="error")
return [] return []
@ -661,7 +663,7 @@ JSON_OUTPUT = {{
workflow['dataStats']['processingTime'] += (endTime - startTime) workflow['dataStats']['processingTime'] += (endTime - startTime)
# Update in database # Update in database
self.mydom.updateWorkflow(workflow["id"], { self.service.base.updateWorkflow(workflow["id"], {
"dataStats": workflow['dataStats'] "dataStats": workflow['dataStats']
}) })
@ -725,8 +727,8 @@ JSON_OUTPUT = {{
matchingDocuments.append(docRef) matchingDocuments.append(docRef)
break break
# Use the mydom for language-aware AI calls # Use the serviceBase for language-aware AI calls
finalPrompt = await self.mydom.callAi([ finalPrompt = await self.service.base.callAi([
{"role": "system", "content": "You are a project manager, who delivers results to a user."}, {"role": "system", "content": "You are a project manager, who delivers results to a user."},
{"role": "user", "content": f""" {"role": "user", "content": f"""
Give the final short feedback to the user with reference to the initial statement (objUserResponse). Inform him about the list of filesDelivered. You do not need to send the files, this is handled separately. If in the list of filesDelivered some files_promised would be missing, just give a comment on this, otherwise task is now completed successfully. Give the final short feedback to the user with reference to the initial statement (objUserResponse). Inform him about the list of filesDelivered. You do not need to send the files, this is handled separately. If in the list of filesDelivered some files_promised would be missing, just give a comment on this, otherwise task is now completed successfully.
@ -791,8 +793,8 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
content = message.get("content", "") content = message.get("content", "")
try: try:
# Use the mydom for language-aware AI calls # Use the serviceBase for language-aware AI calls
contentSummary = await self.mydom.callAi([ contentSummary = await self.service.base.callAi([
{"role": "system", "content": f"You are a chat message summarizer. Create a very concise summary (2-3 sentences, maximum 300 characters)"}, {"role": "system", "content": f"You are a chat message summarizer. Create a very concise summary (2-3 sentences, maximum 300 characters)"},
{"role": "user", "content": content} {"role": "user", "content": content}
]) ])
@ -883,7 +885,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow['dataStats']['tokensUsed'] += tokensUsed workflow['dataStats']['tokensUsed'] += tokensUsed
# Update in database # Update in database
self.mydom.updateWorkflow(workflow["id"], { self.service.base.updateWorkflow(workflow["id"], {
"dataStats": workflow['dataStats'] "dataStats": workflow['dataStats']
}) })
@ -907,7 +909,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
for fileId in fileIds: for fileId in fileIds:
try: try:
# Check if the file exists # Check if the file exists
file = self.mydom.getFile(fileId) file = self.service.base.getFile(fileId)
if not file: if not file:
logger.warning(f"File with ID {fileId} not found") logger.warning(f"File with ID {fileId} not found")
continue continue
@ -918,7 +920,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
continue continue
# Load file content # Load file content
fileContent = self.mydom.getFileData(fileId) fileContent = self.service.base.getFileData(fileId)
if fileContent is None: if fileContent is None:
logger.warning(f"No content found for file with ID {fileId}") logger.warning(f"No content found for file with ID {fileId}")
continue continue
@ -928,7 +930,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
isTextFormat = isTextMimeType(mimeType) isTextFormat = isTextMimeType(mimeType)
# Get file data from database # Get file data from database
fileDataEntries = self.mydom.db.getRecordset("fileData", recordFilter={"id": fileId}) fileDataEntries = self.service.base.db.getRecordset("fileData", recordFilter={"id": fileId})
base64Encoded = False base64Encoded = False
if fileDataEntries and "base64Encoded" in fileDataEntries[0]: if fileDataEntries and "base64Encoded" in fileDataEntries[0]:
@ -1110,13 +1112,13 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
if isBase64: if isBase64:
try: try:
# Pass base64 encoded data directly to callAi4Image # Pass base64 encoded data directly to callAi4Image
return await self.mydom.callAi4Image(data, content.get("mimeType", "application/octet-stream"), imagePrompt) return await self.service.base.callAi4Image(data, content.get("mimeType", "application/octet-stream"), imagePrompt)
except Exception as e: except Exception as e:
logger.error(f"Error processing base64 content: {str(e)}") logger.error(f"Error processing base64 content: {str(e)}")
return f"Error processing content: {str(e)}" return f"Error processing content: {str(e)}"
else: else:
# For non-base64 content, use callAi # For non-base64 content, use callAi
return await self.mydom.callAi([ return await self.service.base.callAi([
{"role": "system", "content": "You are a content analyzer. Extract relevant information from the provided content."}, {"role": "system", "content": "You are a content analyzer. Extract relevant information from the provided content."},
{"role": "user", "content": f"{textPrompt}\n\nContent:\n{data}"} {"role": "user", "content": f"{textPrompt}\n\nContent:\n{data}"}
], produceUserAnswer=True) ], produceUserAnswer=True)
@ -1211,7 +1213,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow["lastActivity"] = currentTime workflow["lastActivity"] = currentTime
# Save to database - first the message itself # Save to database - first the message itself
self.mydom.createWorkflowMessage(message) self.service.base.createWorkflowMessage(message)
# Then save the workflow with updated references and statistics # Then save the workflow with updated references and statistics
workflowUpdate = { workflowUpdate = {
@ -1219,7 +1221,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"messageIds": workflow["messageIds"], "messageIds": workflow["messageIds"],
"dataStats": workflow["dataStats"] # Include updated statistics "dataStats": workflow["dataStats"] # Include updated statistics
} }
self.mydom.updateWorkflow(workflow["id"], workflowUpdate) self.service.base.updateWorkflow(workflow["id"], workflowUpdate)
return message return message
@ -1306,7 +1308,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow["logs"].append(logEntry) workflow["logs"].append(logEntry)
# Save in database # Save in database
self.mydom.createWorkflowLog(logEntry) self.service.base.createWorkflowLog(logEntry)
# Also log in logger # Also log in logger
if level == "info": if level == "info":
@ -1375,10 +1377,10 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
fileContent = data fileContent = data
# Determine MIME type based on extension # Determine MIME type based on extension
mimeType = self.mydom.getMimeType(f"{base_name}.{ext}") mimeType = self.service.base.getMimeType(f"{base_name}.{ext}")
# Create file metadata # Create file metadata
fileMeta = self.mydom.createFile( fileMeta = self.service.base.createFile(
name=base_name, name=base_name,
mimeType=mimeType, mimeType=mimeType,
size=len(fileContent) size=len(fileContent)
@ -1386,7 +1388,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
if fileMeta and "id" in fileMeta: if fileMeta and "id" in fileMeta:
# Save file content # Save file content
if self.mydom.createFileData(fileMeta["id"], fileContent): if self.service.base.createFileData(fileMeta["id"], fileContent):
fileIds.append(fileMeta["id"]) fileIds.append(fileMeta["id"])
logger.info(f"Saved document '{base_name}.{ext}' with file ID: {fileMeta['id']} (base64Encoded: {base64Encoded})") logger.info(f"Saved document '{base_name}.{ext}' with file ID: {fileMeta['id']} (base64Encoded: {base64Encoded})")
else: else:
@ -1553,7 +1555,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
def _checkFileAccess(self, fileId: int) -> bool: def _checkFileAccess(self, fileId: int) -> bool:
"""Checks if the current user has access to a file.""" """Checks if the current user has access to a file."""
file = self.mydom.getFile(fileId) file = self.service.base.getFile(fileId)
if not file: if not file:
return False return False
@ -1568,9 +1570,23 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
_workflowManagers = {} _workflowManagers = {}
_workflowManagerLastAccess = {} # Track last access time for cleanup _workflowManagerLastAccess = {} # Track last access time for cleanup
def getWorkflowManager(_mandateId: str = '', _userId: str = '') -> WorkflowManager: async def getWorkflowManager(interfaceBase, interfaceMsft) -> WorkflowManager:
"""Get a workflow manager instance with the specified context.""" """Get or create a workflow manager instance."""
return WorkflowManager(_mandateId=_mandateId, _userId=_userId) contextKey = f"{interfaceBase._mandateId}_{interfaceBase._userId}"
# Check if we have a cached instance
if contextKey in _workflowManagers:
_workflowManagerLastAccess[contextKey] = time.time()
return _workflowManagers[contextKey]
# Create new instance
manager = WorkflowManager(interfaceBase, interfaceMsft)
# Cache the instance
_workflowManagers[contextKey] = manager
_workflowManagerLastAccess[contextKey] = time.time()
return manager
def cleanupWorkflowManager(_mandateId: int, _userId: int) -> None: def cleanupWorkflowManager(_mandateId: int, _userId: int) -> None:
""" """

View file

@ -1,5 +1,45 @@
....................... TASKS ....................... TASKS
mandateid necessary??
cleanup:
all routes to have mandateAttributes = getModelAttributes(Mandate) --> include
adapt workflow route, that handover is with myInterface, not with usersetc.
i want to refactor all routes route*.py to initiate in the same way.
- routes which need gatewayInterface, to import it as: from modules.interfaces.gatewayInterface import getInterface
- routes which need lucydomInterface, to import it as: from modules.interfaces.lucydomInterface import getInterface
- gatewayInterface and lucydomInterface are automatically initialized, and do the following things:
- check and ensurem that database is available
- initializeDatabase
- initializeRecords
- each route, which needs authentication, has: "currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)" as parameter
- it calls "myInterface = getInterface(currentUser)", to have an instance of the according interface with applied userid and mandateid to use
- in lucydomInterface there is also attached "aiService", an instance of connector_ai_openai
like this all routes have the same basic data and can do their work
can you start with route "prompt"?
for all created records for all created records
- to add _createdAt (datetime) and _modifiedAt (datetime), initially _createdAt=_modifiedAt - to add _createdAt (datetime) and _modifiedAt (datetime), initially _createdAt=_modifiedAt