self service date model for agents, msft interface complete
This commit is contained in:
parent
db6b5d7985
commit
fee9bb0151
35 changed files with 1976 additions and 2574 deletions
15
app.py
15
app.py
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
10
env_prod.env
10
env_prod.env
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
self.client_id = None
|
def setDependencies(self, serviceBase=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",
|
# Get Microsoft token
|
||||||
"details": "Microsoft authentication required to create email drafts"
|
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."
|
||||||
|
|
@ -175,63 +140,39 @@ class AgentEmail(AgentBase):
|
||||||
"feedback": feedback,
|
"feedback": feedback,
|
||||||
"documents": documents
|
"documents": documents
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
""",
|
||||||
</html>
|
"base64Encoded": False,
|
||||||
"""
|
"metadata": {
|
||||||
|
"isText": True
|
||||||
return self.formatAgentDocumentOutput(
|
}
|
||||||
"microsoft_authentication.html",
|
}
|
||||||
html_content,
|
|
||||||
"text/html"
|
def _processInputDocuments(self, input_docs: List[Dict[str, Any]]) -> tuple:
|
||||||
)
|
|
||||||
|
|
||||||
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('{')
|
||||||
|
|
@ -320,7 +261,7 @@ class AgentEmail(AgentBase):
|
||||||
"plainBody": f"This email is regarding your request: {prompt}",
|
"plainBody": f"This email is regarding your request: {prompt}",
|
||||||
"htmlBody": f"<html><body><p>This email is regarding your request: {prompt}</p></body></html>"
|
"htmlBody": f"<html><body><p>This email is regarding your request: {prompt}</p></body></html>"
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error generating email template: {str(e)}")
|
logger.warning(f"Error generating email template: {str(e)}")
|
||||||
return {
|
return {
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -45,13 +46,13 @@ class AgentWebcrawler(AgentBase):
|
||||||
self.maxResults = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_RESULTS", "5"))
|
self.maxResults = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_RESULTS", "5"))
|
||||||
self.timeout = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_TIMEOUT", "30"))
|
self.timeout = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_TIMEOUT", "30"))
|
||||||
self.userAgent = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
self.userAgent = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,120 @@
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
Unified user access management function that filters data based on user privileges
|
Access control class for Gateway interface.
|
||||||
and adds access control attributes.
|
Handles user access management and permission checks.
|
||||||
|
|
||||||
Args:
|
|
||||||
currentUser: Current user information dictionary
|
|
||||||
table: Name of the table
|
|
||||||
recordset: Recordset to filter based on access rules
|
|
||||||
_mandateId: Current mandate ID
|
|
||||||
_userId: Current user ID
|
|
||||||
db: Database connector instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Filtered recordset with access control attributes
|
|
||||||
"""
|
"""
|
||||||
userPrivilege = currentUser.get("privilege", "user")
|
|
||||||
filtered_records = []
|
|
||||||
|
|
||||||
# Apply filtering based on privilege
|
def __init__(self, currentUser: Dict[str, Any], db):
|
||||||
if userPrivilege == "sysadmin":
|
"""Initialize with user context."""
|
||||||
filtered_records = recordset # System admins see all records
|
self.currentUser = currentUser
|
||||||
elif userPrivilege == "admin":
|
self._mandateId = currentUser.get("_mandateId")
|
||||||
# Admins see records in their mandate
|
self._userId = currentUser.get("id")
|
||||||
filtered_records = [r for r in recordset if r.get("_mandateId") == _mandateId]
|
|
||||||
else: # Regular users
|
|
||||||
# Users only see records they own within their mandate
|
|
||||||
filtered_records = [r for r in recordset
|
|
||||||
if r.get("_mandateId") == _mandateId and r.get("_userId") == _userId]
|
|
||||||
|
|
||||||
# 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 not self._mandateId or not self._userId:
|
||||||
if table == "mandates":
|
raise ValueError("Invalid user context: _mandateId and id are required")
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db)
|
self.db = db
|
||||||
record["_hideDelete"] = not _canModify(currentUser, "mandates", record_id, _mandateId, _userId, db)
|
|
||||||
elif table == "users":
|
|
||||||
record["_hideView"] = False # Everyone can view
|
|
||||||
record["_hideEdit"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db)
|
|
||||||
record["_hideDelete"] = not _canModify(currentUser, "users", record_id, _mandateId, _userId, db)
|
|
||||||
else:
|
|
||||||
# Default access control for other tables
|
|
||||||
record["_hideView"] = False
|
|
||||||
record["_hideEdit"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db)
|
|
||||||
record["_hideDelete"] = not _canModify(currentUser, table, record_id, _mandateId, _userId, db)
|
|
||||||
|
|
||||||
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 uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Checks if the current user can modify (create/update/delete) records in a table.
|
Unified user access management function that filters data based on user privileges
|
||||||
|
and adds access control attributes.
|
||||||
Args:
|
|
||||||
currentUser: Current user information dictionary
|
|
||||||
table: Name of the table
|
|
||||||
recordId: Optional record ID for specific record check
|
|
||||||
_mandateId: Current mandate ID
|
|
||||||
_userId: Current user ID
|
|
||||||
db: Database connector instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Boolean indicating permission
|
|
||||||
"""
|
|
||||||
userPrivilege = currentUser.get("privilege", "user")
|
|
||||||
|
|
||||||
# System admins can modify anything
|
|
||||||
if userPrivilege == "sysadmin":
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check specific record permissions
|
Args:
|
||||||
if recordId is not None:
|
table: Name of the table
|
||||||
# Get the record to check ownership
|
recordset: Recordset to filter based on access rules
|
||||||
records = db.getRecordset(table, recordFilter={"id": recordId})
|
|
||||||
if not records:
|
|
||||||
return False
|
|
||||||
|
|
||||||
record = records[0]
|
Returns:
|
||||||
|
Filtered recordset with access control attributes
|
||||||
|
"""
|
||||||
|
userPrivilege = self.currentUser.get("privilege", "user")
|
||||||
|
filtered_records = []
|
||||||
|
|
||||||
# Admins can modify anything in their mandate
|
# Apply filtering based on privilege
|
||||||
if userPrivilege == "admin" and record.get("_mandateId") == _mandateId:
|
if userPrivilege == "sysadmin":
|
||||||
# Exception: Can't modify Root mandate unless you are a sysadmin
|
filtered_records = recordset # System admins see all records
|
||||||
if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin":
|
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 records they own within their mandate
|
||||||
|
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 == "mandates":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self.canModify("mandates", record_id)
|
||||||
|
record["_hideDelete"] = not self.canModify("mandates", record_id)
|
||||||
|
elif table == "users":
|
||||||
|
record["_hideView"] = False # Everyone can view
|
||||||
|
record["_hideEdit"] = not self.canModify("users", record_id)
|
||||||
|
record["_hideDelete"] = not self.canModify("users", 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[int] = 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
|
return False
|
||||||
return True
|
|
||||||
|
record = records[0]
|
||||||
|
|
||||||
# Users can only modify their own records
|
# Admins can modify anything in their mandate
|
||||||
if (record.get("_mandateId") == _mandateId and
|
if userPrivilege == "admin" and record.get("_mandateId") == self._mandateId:
|
||||||
record.get("_userId") == _userId):
|
# Exception: Can't modify Root mandate unless you are a sysadmin
|
||||||
return True
|
if table == "mandates" and record.get("initialid") and userPrivilege != "sysadmin":
|
||||||
|
return False
|
||||||
return False
|
return True
|
||||||
else:
|
|
||||||
# For general table modify permission (e.g., create)
|
# Users can only modify their own records
|
||||||
# Admins can create anything in their mandate
|
if (record.get("_mandateId") == self._mandateId and
|
||||||
if userPrivilege == "admin":
|
record.get("_userId") == self._userId):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Regular users can create most entities
|
return False
|
||||||
if table == "mandates":
|
else:
|
||||||
return False # Regular users can't create mandates
|
# For general table modify permission (e.g., create)
|
||||||
return True
|
# Admins can create anything in their mandate
|
||||||
|
if userPrivilege == "admin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Regular users can create most entities
|
||||||
|
if table == "mandates":
|
||||||
|
return False # Regular users can't create mandates
|
||||||
|
return True
|
||||||
|
|
@ -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,40 +31,88 @@ 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."""
|
||||||
# Get configuration values with defaults
|
try:
|
||||||
dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data")
|
# Get configuration values with defaults
|
||||||
dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway")
|
dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data")
|
||||||
dbUser = APP_CONFIG.get("DB_GATEWAY_USER")
|
dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway")
|
||||||
dbPassword = APP_CONFIG.get("DB_GATEWAY_PASSWORD_SECRET")
|
dbUser = APP_CONFIG.get("DB_GATEWAY_USER")
|
||||||
|
dbPassword = APP_CONFIG.get("DB_GATEWAY_PASSWORD_SECRET")
|
||||||
# Ensure the database directory exists
|
|
||||||
os.makedirs(dbHost, exist_ok=True)
|
# Ensure the database directory exists
|
||||||
|
os.makedirs(dbHost, exist_ok=True)
|
||||||
self.db = DatabaseConnector(
|
|
||||||
dbHost=dbHost,
|
self.db = DatabaseConnector(
|
||||||
dbDatabase=dbDatabase,
|
dbHost=dbHost,
|
||||||
dbUser=dbUser,
|
dbDatabase=dbDatabase,
|
||||||
dbPassword=dbPassword,
|
dbUser=dbUser,
|
||||||
_mandateId=self._mandateId,
|
dbPassword=dbPassword,
|
||||||
_userId=self._userId
|
_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 _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."""
|
||||||
|
|
@ -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._initRootMandate()
|
||||||
|
# Update database context with new IDs
|
||||||
|
if self._mandateId and self._userId:
|
||||||
|
self.db.updateContext(self._mandateId, self._userId)
|
||||||
|
|
||||||
self._initAdminUser()
|
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,12 +210,12 @@ 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."""
|
||||||
return self.db.getInitialId(table)
|
return self.db.getInitialId(table)
|
||||||
|
|
||||||
def _getPasswordHash(self, password: str) -> str:
|
def _getPasswordHash(self, password: str) -> str:
|
||||||
"""Creates a hash for a password."""
|
"""Creates a hash for a password."""
|
||||||
return pwdContext.hash(password)
|
return pwdContext.hash(password)
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -420,8 +478,7 @@ class GatewayInterface:
|
||||||
if "hashedPassword" in authenticatedUser:
|
if "hashedPassword" in authenticatedUser:
|
||||||
del authenticatedUser["hashedPassword"]
|
del authenticatedUser["hashedPassword"]
|
||||||
|
|
||||||
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."""
|
||||||
|
|
@ -512,20 +569,81 @@ class GatewayInterface:
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
# Microsoft Login
|
||||||
|
|
||||||
|
def getMsftToken(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""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 GatewayInterface instances per context
|
|
||||||
_gatewayInterfaces = {}
|
|
||||||
|
|
||||||
def getGatewayInterface(_mandateId: str = None, _userId: str = None) -> GatewayInterface:
|
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]
|
return _gatewayInterfaces[contextKey]
|
||||||
|
|
||||||
# Initialize an instance with empty strings
|
|
||||||
getGatewayInterface('', '')
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
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]]:
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,85 +46,84 @@ 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()
|
self._initRecords()
|
||||||
|
|
||||||
|
# Initialize AI service
|
||||||
|
self.aiService = ChatService()
|
||||||
|
if not self.aiService:
|
||||||
|
logger.warning("AI service not available during LucydomInterface initialization")
|
||||||
|
|
||||||
# Initialize access control
|
# Initialize access control
|
||||||
self.access = LucyDOMAccess(self.currentUser, self._mandateId, self._userId)
|
self.access = LucydomAccess(self.currentUser, self.db)
|
||||||
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()
|
|
||||||
|
|
||||||
def _getCurrentUserInfo(self) -> Dict[str, Any]:
|
|
||||||
"""Gets information about the current user including privileges."""
|
|
||||||
# For production, you would get this from authentication
|
|
||||||
# For now return basic user info with default privilege
|
|
||||||
return {
|
|
||||||
"id": self._userId,
|
|
||||||
"_mandateId": self._mandateId,
|
|
||||||
"privilege": "user", # Default privilege level
|
|
||||||
"language": self.userLanguage
|
|
||||||
}
|
|
||||||
|
|
||||||
def _initializeDatabase(self):
|
def _initializeDatabase(self):
|
||||||
"""Initializes the database connection."""
|
"""Initializes the database connection."""
|
||||||
self.db = DatabaseConnector(
|
try:
|
||||||
dbHost=APP_CONFIG.get("DB_LUCYDOM_HOST"),
|
# Get configuration values with defaults
|
||||||
dbDatabase=APP_CONFIG.get("DB_LUCYDOM_DATABASE"),
|
dbHost = APP_CONFIG.get("DB_LUCYDOM_HOST", "data")
|
||||||
dbUser=APP_CONFIG.get("DB_LUCYDOM_USER"),
|
dbDatabase = APP_CONFIG.get("DB_LUCYDOM_DATABASE", "lucydom")
|
||||||
dbPassword=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"),
|
dbUser = APP_CONFIG.get("DB_LUCYDOM_USER")
|
||||||
_mandateId=self._mandateId,
|
dbPassword = APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET")
|
||||||
_userId=self._userId,
|
|
||||||
skipInitialIdLookup=True
|
# 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 _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."""
|
||||||
prompts = self.db.getRecordset("prompts")
|
prompts = self.db.getRecordset("prompts")
|
||||||
|
|
@ -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]]:
|
|
||||||
"""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
|
def getInterface(currentUser: Dict[str, Any]) -> 'LucydomInterface':
|
||||||
_lucydomInterfaces = {}
|
"""
|
||||||
|
Returns a LucydomInterface instance for the current user.
|
||||||
|
Handles initialization of database and records.
|
||||||
|
"""
|
||||||
|
# Get user context
|
||||||
|
mandateId = currentUser.get("_mandateId")
|
||||||
|
userId = currentUser.get("id")
|
||||||
|
|
||||||
|
if not mandateId or not userId:
|
||||||
|
raise ValueError("Invalid user context: _mandateId and id are required")
|
||||||
|
|
||||||
def getLucydomInterface(_mandateId: str = None, _userId: str = None) -> LucyDOMInterface:
|
# Create context key
|
||||||
"""
|
contextKey = f"{mandateId}_{userId}"
|
||||||
Returns a LucyDOMInterface instance for the specified context.
|
|
||||||
Ensures AI service is initialized and preserves it across instances.
|
|
||||||
"""
|
|
||||||
# For initialization, use empty strings instead of None
|
|
||||||
contextKey = f"{_mandateId or ''}_{_userId or ''}"
|
|
||||||
|
|
||||||
# Ensure AI service is initialized
|
# Create new instance if not exists
|
||||||
if _aiService is None:
|
|
||||||
initializeAIService()
|
|
||||||
|
|
||||||
# Create new instance if needed
|
|
||||||
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('', '')
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
113
modules/interfaces/msftAccess.py
Normal file
113
modules/interfaces/msftAccess.py
Normal 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
|
||||||
391
modules/interfaces/msftInterface.py
Normal file
391
modules/interfaces/msftInterface.py
Normal 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]
|
||||||
73
modules/interfaces/msftModel.py
Normal file
73
modules/interfaces/msftModel.py
Normal 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={})
|
||||||
|
}
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
raise HTTPException(
|
logger.error(f"Error getting mandates: {str(e)}")
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="Only system administrators can access all mandates"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get mandates
|
|
||||||
return context.interfaceData.getAllMandates()
|
|
||||||
|
|
||||||
@router.post("", response_model=Dict[str, Any])
|
|
||||||
async def createMandate(
|
|
||||||
mandate: Dict[str, Any] = Body(...),
|
|
||||||
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
|
|
||||||
):
|
|
||||||
"""Create a new mandate (only for SysAdmin users)"""
|
|
||||||
context = await getContext(currentUser)
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Mandate with ID {_mandateId} not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
return mandate
|
|
||||||
|
|
||||||
@router.put("/{_mandateId}", response_model=Dict[str, Any])
|
|
||||||
async def updateMandate(
|
|
||||||
_mandateId: str = Path(..., description="ID of the mandate to update"),
|
|
||||||
mandateData: Dict[str, Any] = Body(...),
|
|
||||||
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
|
|
||||||
):
|
|
||||||
"""Update a mandate."""
|
|
||||||
context = await getContext(currentUser)
|
|
||||||
|
|
||||||
# Get mandate
|
|
||||||
mandate = context.interfaceData.getMandate(_mandateId)
|
|
||||||
if not mandate:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Mandate with ID {_mandateId} not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Permission check
|
|
||||||
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 update this mandate"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dynamically filter attributes from the request into updateData
|
|
||||||
updateData = {}
|
|
||||||
for attr in mandateAttributes:
|
|
||||||
if attr in mandateData:
|
|
||||||
updateData[attr] = mandateData[attr]
|
|
||||||
|
|
||||||
# Update mandate
|
|
||||||
updatedMandate = context.interfaceData.updateMandate(_mandateId, mandateData)
|
|
||||||
return updatedMandate
|
|
||||||
|
|
||||||
@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(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Mandate with ID {_mandateId} not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Permission check
|
|
||||||
isAdmin = currentUser.get("privilege") == "admin"
|
|
||||||
isSysadmin = currentUser.get("privilege") == "sysadmin"
|
|
||||||
isOwnMandate = context._mandateId == _mandateId
|
|
||||||
|
|
||||||
if (isAdmin and not isOwnMandate) and not isSysadmin:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="No permission to delete this mandate"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete mandate
|
|
||||||
success = context.interfaceData.deleteMandate(_mandateId)
|
|
||||||
if not success:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"Error deleting mandate with ID {_mandateId}"
|
detail=f"Failed to get mandates: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
@router.get("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"])
|
||||||
|
async def get_mandate(
|
||||||
|
mandateId: str,
|
||||||
|
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
|
||||||
|
):
|
||||||
|
"""Get a specific mandate by ID"""
|
||||||
|
try:
|
||||||
|
myInterface = getInterface(currentUser)
|
||||||
|
mandate = myInterface.getMandateById(mandateId)
|
||||||
|
|
||||||
|
if not mandate:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Mandate {mandateId} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.post("/", response_model=Dict[str, Any], tags=["Mandates"])
|
||||||
|
async def create_mandate(
|
||||||
|
mandateData: Dict[str, Any],
|
||||||
|
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
|
||||||
|
):
|
||||||
|
"""Create a new mandate"""
|
||||||
|
try:
|
||||||
|
myInterface = getInterface(currentUser)
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
if not mandateData.get("name"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Mandate name is required"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter attributes based on model definition
|
||||||
|
filteredData = {}
|
||||||
|
for attr in mandateAttributes:
|
||||||
|
if attr in mandateData:
|
||||||
|
filteredData[attr] = mandateData[attr]
|
||||||
|
|
||||||
|
try:
|
||||||
|
createdMandate = myInterface.createMandate(**filteredData)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not createdMandate:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
myInterface.deleteMandate(mandateId)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)}"
|
||||||
|
)
|
||||||
|
|
@ -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"],
|
|
||||||
client_credential=app_config["client_credential"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build the auth URL with a random state
|
# Get login URL
|
||||||
state = secrets.token_urlsafe(32)
|
auth_url = msft.initiateLogin()
|
||||||
|
if not auth_url:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to initiate Microsoft login"
|
||||||
|
)
|
||||||
|
|
||||||
auth_url = msal_app.get_authorization_request_url(
|
logger.info("Redirecting to Microsoft login")
|
||||||
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)}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
logger.error(f"Error getting users: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to get users: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/{userId}", response_model=Dict[str, Any], tags=["Users"])
|
||||||
|
async def get_user(
|
||||||
|
userId: str,
|
||||||
|
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
|
||||||
|
):
|
||||||
|
"""Get a specific user by ID"""
|
||||||
|
try:
|
||||||
|
myInterface = getInterface(currentUser)
|
||||||
|
user = myInterface.getUserById(userId)
|
||||||
|
|
||||||
# Get root mandate and admin user IDs
|
if not user:
|
||||||
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_404_NOT_FOUND,
|
||||||
detail="System is not properly initialized with root mandate and admin user"
|
detail=f"User {userId} not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a new gateway interface instance with admin context
|
|
||||||
adminGateway = getGatewayInterface(rootMandateId, adminUserId)
|
|
||||||
|
|
||||||
# Set default values if not provided
|
|
||||||
if "language" not in userData:
|
|
||||||
userData["language"] = "en"
|
|
||||||
if "authenticationAuthority" not in userData:
|
|
||||||
userData["authenticationAuthority"] = "local"
|
|
||||||
|
|
||||||
# Validate authentication authority
|
return user
|
||||||
if userData["authenticationAuthority"] not in ["local", "microsoft"]:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=f"Invalid authentication authority: {userData['authenticationAuthority']}"
|
detail="Username and password are required"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate password for local authentication
|
# Filter attributes based on model definition
|
||||||
if userData["authenticationAuthority"] == "local":
|
filteredData = {}
|
||||||
if "password" not in userData:
|
for attr in userAttributes:
|
||||||
raise HTTPException(
|
if attr in userData:
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
filteredData[attr] = userData[attr]
|
||||||
detail="Password is required for local authentication"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the user
|
|
||||||
try:
|
try:
|
||||||
createdUser = adminGateway.createUser(
|
createdUser = myInterface.createUser(**filteredData)
|
||||||
username=userData["username"],
|
|
||||||
password=userData.get("password"),
|
|
||||||
email=userData.get("email"),
|
|
||||||
fullName=userData.get("fullName"),
|
|
||||||
language=userData["language"],
|
|
||||||
_mandateId=userData.get("_mandateId", rootMandateId),
|
|
||||||
authenticationAuthority=userData["authenticationAuthority"]
|
|
||||||
)
|
|
||||||
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
|
||||||
# User exists?
|
myInterface = getRootInterface()
|
||||||
userToUpdate = context.interfaceData.getUser(userId)
|
|
||||||
if not userToUpdate:
|
# Check if user exists
|
||||||
|
existingUser = myInterface.getUserById(userId)
|
||||||
|
if not existingUser:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"User {userId} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter attributes based on model definition
|
||||||
|
filteredData = {}
|
||||||
|
for attr in userAttributes:
|
||||||
|
if attr in userData:
|
||||||
|
filteredData[attr] = userData[attr]
|
||||||
|
|
||||||
|
# Update user data
|
||||||
|
try:
|
||||||
|
updatedUser = myInterface.updateUser(userId, **filteredData)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not updatedUser:
|
||||||
|
raise HTTPException(
|
||||||
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"User with ID {userId} not found"
|
detail=f"Failed to update user: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Permission check
|
|
||||||
isSelfUpdate = userId == str(context._userId)
|
|
||||||
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:
|
|
||||||
if attr in userData and attr != "id": # ID cannot be changed
|
|
||||||
updateData[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
|
|
||||||
updatedUser = context.interfaceData.updateUser(userId, updateData)
|
|
||||||
return updatedUser
|
|
||||||
|
|
||||||
@router.delete("/{userId}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{userId}", response_model=Dict[str, Any], tags=["Users"])
|
||||||
async def deleteUser(
|
async def delete_user(
|
||||||
userId: str = Path(..., description="ID of the user to delete"),
|
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
|
||||||
# User exists?
|
myInterface = getRootInterface()
|
||||||
userToDelete = context.interfaceData.getUser(userId)
|
|
||||||
if not userToDelete:
|
# Check if user exists
|
||||||
raise HTTPException(
|
existingUser = myInterface.getUserById(userId)
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
if not existingUser:
|
||||||
detail=f"User with ID {userId} not found"
|
raise HTTPException(
|
||||||
)
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"User {userId} not found"
|
||||||
# Permission check
|
)
|
||||||
isSelfDelete = userId == str(context._userId)
|
|
||||||
isAdmin = currentUser.get("privilege") == "admin"
|
# Delete user
|
||||||
isSysadmin = currentUser.get("privilege") == "sysadmin"
|
try:
|
||||||
sameMandate = userToDelete.get("_mandateId") == context._mandateId
|
myInterface.deleteUser(userId)
|
||||||
|
except ValueError as e:
|
||||||
if isSelfDelete:
|
raise HTTPException(
|
||||||
# User can delete themselves
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
pass
|
detail=str(e)
|
||||||
elif isAdmin and sameMandate:
|
)
|
||||||
# Admin can delete users of their own mandate
|
|
||||||
pass
|
return {"message": f"User {userId} deleted successfully"}
|
||||||
elif isSysadmin:
|
except HTTPException:
|
||||||
# SysAdmin can delete all users
|
raise
|
||||||
pass
|
except Exception as e:
|
||||||
else:
|
logger.error(f"Error deleting user {userId}: {str(e)}")
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="No permission to delete this user"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete user and all referenced objects
|
|
||||||
success = context.interfaceData.deleteUser(userId)
|
|
||||||
if not success:
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -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 all workflows for the mandate
|
# Get admin user for workflow operations
|
||||||
workflows = context.interfaceData.getWorkflowsByMandate(context._mandateId)
|
interfaceBase = getInterfaceLucydom(currentUser)
|
||||||
|
|
||||||
return workflows
|
# Get all workflows for the mandate
|
||||||
|
workflows = interfaceBase.getWorkflowsByMandate(currentUser.get("_mandateId"))
|
||||||
|
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:
|
||||||
# Create workflow
|
# Get admin user for workflow operations
|
||||||
newWorkflow = context.interfaceData.createWorkflow(workflow)
|
interfaceBase = getInterfaceLucydom(currentUser)
|
||||||
|
|
||||||
return newWorkflow
|
# Create workflow
|
||||||
|
newWorkflow = interfaceBase.createWorkflow(workflow)
|
||||||
|
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 workflow
|
# Get admin user for workflow operations
|
||||||
workflow = context.interfaceData.getWorkflow(workflowId)
|
interfaceBase = getInterfaceLucydom(currentUser)
|
||||||
if not workflow:
|
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
# Get workflow
|
||||||
|
workflow = interfaceBase.getWorkflow(workflowId)
|
||||||
# Check if user has access to this workflow
|
if not workflow:
|
||||||
if workflow.get("_userId") != context._userId:
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||||
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 workflow
|
# Get admin user for workflow operations
|
||||||
existingWorkflow = context.interfaceData.getWorkflow(workflowId)
|
interfaceBase = getInterfaceLucydom(currentUser)
|
||||||
if not existingWorkflow:
|
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
# Get workflow
|
||||||
|
existingWorkflow = interfaceBase.getWorkflow(workflowId)
|
||||||
# Check if user has access to this workflow
|
if not existingWorkflow:
|
||||||
if existingWorkflow.get("_userId") != context._userId:
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||||
raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
|
|
||||||
|
# Update workflow
|
||||||
# Update workflow
|
updatedWorkflow = interfaceBase.updateWorkflow(workflowId, workflow)
|
||||||
updatedWorkflow = context.interfaceData.updateWorkflow(workflowId, workflow)
|
return updatedWorkflow
|
||||||
|
except HTTPException:
|
||||||
return updatedWorkflow
|
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 workflow
|
# Get admin user for workflow operations
|
||||||
workflow = context.interfaceData.getWorkflow(workflowId)
|
interfaceBase = getInterfaceLucydom(currentUser)
|
||||||
if not workflow:
|
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
||||||
|
|
||||||
# Check if user has access to this workflow
|
|
||||||
if workflow.get("_userId") != context._userId:
|
|
||||||
raise HTTPException(status_code=403, detail="Not authorized to delete this workflow")
|
|
||||||
|
|
||||||
# Delete workflow
|
|
||||||
success = context.interfaceData.deleteWorkflow(workflowId)
|
|
||||||
if not success:
|
|
||||||
raise HTTPException(status_code=500, detail="Failed to delete workflow")
|
|
||||||
|
|
||||||
return {"status": "success"}
|
# Get workflow
|
||||||
|
workflow = interfaceBase.getWorkflow(workflowId)
|
||||||
|
if not workflow:
|
||||||
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||||
|
|
||||||
|
# Delete workflow
|
||||||
|
success = interfaceBase.deleteWorkflow(workflowId)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to delete workflow")
|
||||||
|
|
||||||
|
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
|
||||||
# Get workflow
|
interfaceBase = getInterfaceLucydom(currentUser)
|
||||||
workflow = context.interfaceData.getWorkflow(workflowId)
|
|
||||||
if not workflow:
|
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
|
||||||
|
|
||||||
# Check access
|
# Get workflow
|
||||||
if workflow.get("_userId") != context._userId:
|
workflow = interfaceBase.getWorkflow(workflowId)
|
||||||
raise HTTPException(status_code=403, detail="No access to this workflow")
|
if not workflow:
|
||||||
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||||
# Get file
|
|
||||||
file = context.interfaceData.getFile(fileId)
|
# Get file
|
||||||
if not file:
|
file = interfaceBase.getFile(fileId)
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
if not file:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
# Check file access
|
|
||||||
if file.get("_mandateId") != context._mandateId and file.get("_userId") != context._userId:
|
# Add file to workflow
|
||||||
raise HTTPException(status_code=403, detail="No access to this file")
|
success = interfaceBase.addFileToWorkflow(workflowId, fileId)
|
||||||
|
if not success:
|
||||||
# Add file to workflow
|
raise HTTPException(status_code=500, detail="Failed to add file to workflow")
|
||||||
success = context.interfaceData.addFileToWorkflow(workflowId, fileId)
|
|
||||||
if not success:
|
return {"status": "success"}
|
||||||
raise HTTPException(status_code=500, detail="Failed to add file to workflow")
|
except HTTPException:
|
||||||
|
raise
|
||||||
return {"status": "success"}
|
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)}"
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -183,4 +125,15 @@ async def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(getCurrentU
|
||||||
detail=f"Invalid authentication authority: {auth_authority}"
|
detail=f"Invalid authentication authority: {auth_authority}"
|
||||||
)
|
)
|
||||||
|
|
||||||
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)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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/"):
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue