prod azure 1.0.11
This commit is contained in:
parent
443387f394
commit
b7c2fa8647
34 changed files with 3705 additions and 1820 deletions
3
app.py
3
app.py
|
|
@ -197,5 +197,8 @@ app.include_router(promptRouter)
|
|||
from routes.routeWorkflows import router as workflowRouter
|
||||
app.include_router(workflowRouter)
|
||||
|
||||
from routes.routeMsft import router as msftRouter
|
||||
app.include_router(msftRouter)
|
||||
|
||||
#if __name__ == "__main__":
|
||||
# uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
|
@ -45,3 +45,8 @@ Agent_Webcrawler_SERPAPI_USER_AGENT = Mozilla/5.0 (Windows NT 10.0; Win64; x64)
|
|||
Agent_Coder_INSTALL_TIMEOUT = 180
|
||||
Agent_Coder_EXECUTION_TIMEOUT = 60
|
||||
Agent_Coder_EXECUTION_RETRY = 5
|
||||
|
||||
# Agent Mail configuration
|
||||
Agent_Mail_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
|
||||
Agent_Mail_MSFT_CLIENT_SECRET = Kxf8Q~2lJIteZ~JaI32kMf1lfaWKATqxXiNiFbzV
|
||||
Agent_Mail_MSFT_TENANT_ID = common
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# System Configuration
|
||||
APP_ENV_TYPE = dev
|
||||
APP_ENV_LABEL = Development Instance Patrick
|
||||
APP_CALL=uvicorn app:app --host 0.0.0.0 --port 8000
|
||||
APP_API_URL = http://localhost:8080
|
||||
|
||||
# Database Configuration System
|
||||
DB_SYSTEM_HOST=D:/Temp/_powerondb
|
||||
|
|
@ -33,3 +33,6 @@ APP_LOGGING_CONSOLE_ENABLED = True
|
|||
APP_LOGGING_FILE_ENABLED = True
|
||||
APP_LOGGING_ROTATION_SIZE = 10485760
|
||||
APP_LOGGING_BACKUP_COUNT = 5
|
||||
|
||||
# Agent Mail
|
||||
Agent_Mail_MSFT_REDIRECT_URI = http://localhost:8000/api/msft/auth/callback
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# System Configuration
|
||||
APP_ENV_TYPE = prod
|
||||
APP_ENV_LABEL = Production Instance
|
||||
APP_CALL=uvicorn app:app --host 0.0.0.0 --port 8000
|
||||
APP_API_URL = https://gateway.poweron-center.net
|
||||
|
||||
# Database Configuration System
|
||||
DB_SYSTEM_HOST=/home/_powerondb
|
||||
|
|
@ -33,3 +33,6 @@ APP_LOGGING_CONSOLE_ENABLED = True
|
|||
APP_LOGGING_FILE_ENABLED = True
|
||||
APP_LOGGING_ROTATION_SIZE = 10485760
|
||||
APP_LOGGING_BACKUP_COUNT = 5
|
||||
|
||||
# Agent Mail
|
||||
Agent_Mail_MSFT_REDIRECT_URI = https://gateway.poweron-center.net/api/msft/auth/callback
|
||||
|
|
|
|||
|
|
@ -1,471 +0,0 @@
|
|||
"""
|
||||
Interface to the Gateway system.
|
||||
Manages users and mandates for authentication.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
import importlib
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from connectors.connectorDbJson import DatabaseConnector
|
||||
from modules.configuration import APP_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Password-Hashing
|
||||
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||
|
||||
|
||||
class GatewayInterface:
|
||||
"""
|
||||
Interface to the Gateway system.
|
||||
Manages users and mandates.
|
||||
"""
|
||||
|
||||
def __init__(self, mandateId: int = None, userId: int = None):
|
||||
"""
|
||||
Initializes the Gateway Interface with optional mandate and user context.
|
||||
|
||||
Args:
|
||||
mandateId: ID of the current mandate (optional)
|
||||
userId: ID of the current user (optional)
|
||||
"""
|
||||
# Context can be empty during initialization
|
||||
self.mandateId = mandateId
|
||||
self.userId = userId
|
||||
|
||||
# Import data model module
|
||||
try:
|
||||
self.modelModule = importlib.import_module("modules.gatewayModel")
|
||||
logger.info("gatewayModel successfully imported")
|
||||
except ImportError as e:
|
||||
logger.error(f"Error importing gatewayModel: {e}")
|
||||
raise
|
||||
|
||||
# Initialize database
|
||||
self._initializeDatabase()
|
||||
|
||||
def _initializeDatabase(self):
|
||||
"""
|
||||
Initializes the database with minimal objects
|
||||
"""
|
||||
|
||||
self.db = DatabaseConnector(
|
||||
dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
|
||||
dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
|
||||
dbUser=APP_CONFIG.get("DB_SYSTEM_USER"),
|
||||
dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"),
|
||||
mandateId=self.mandateId if self.mandateId else 0,
|
||||
userId=self.userId if self.userId else 0
|
||||
)
|
||||
|
||||
# Create Root mandate if needed
|
||||
existingMandateId = self.getInitialId("mandates")
|
||||
mandates = self.db.getRecordset("mandates")
|
||||
if existingMandateId is None or not mandates:
|
||||
logger.info("Creating Root mandate")
|
||||
rootMandate = {
|
||||
"name": "Root",
|
||||
"language": "de"
|
||||
}
|
||||
createdMandate = self.db.recordCreate("mandates", rootMandate)
|
||||
logger.info(f"Root mandate created with ID {createdMandate['id']}")
|
||||
|
||||
# Update mandate context
|
||||
self.mandateId = createdMandate['id']
|
||||
self.userId = createdMandate['userId']
|
||||
|
||||
# Recreate connector with correct context
|
||||
self.db = DatabaseConnector(
|
||||
dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
|
||||
dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
|
||||
dbUser=APP_CONFIG.get("DB_SYSTEM_USER"),
|
||||
dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"),
|
||||
mandateId=self.mandateId,
|
||||
userId=self.userId
|
||||
)
|
||||
|
||||
# Create Admin user if needed
|
||||
existingUserId = self.getInitialId("users")
|
||||
users = self.db.getRecordset("users")
|
||||
if existingUserId is None or not users:
|
||||
logger.info("Creating Admin user")
|
||||
adminUser = {
|
||||
"mandateId": self.mandateId,
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"fullName": "Administrator",
|
||||
"disabled": False,
|
||||
"language": "de",
|
||||
"privilege": "sysadmin", # SysAdmin privilege
|
||||
"hashedPassword": self._getPasswordHash("admin") # Use a secure password in production!
|
||||
}
|
||||
createdUser = self.db.recordCreate("users", adminUser)
|
||||
logger.info(f"Admin user created with ID {createdUser['id']}")
|
||||
|
||||
# Update user context
|
||||
self.userId = createdUser['id']
|
||||
|
||||
# Recreate connector with correct context
|
||||
self.db = DatabaseConnector(
|
||||
dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"),
|
||||
dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"),
|
||||
dbUser=APP_CONFIG.get("DB_SYSTEM_USER"),
|
||||
dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"),
|
||||
mandateId=self.mandateId,
|
||||
userId=self.userId
|
||||
)
|
||||
|
||||
def getInitialId(self, table: str) -> Optional[int]:
|
||||
"""Returns the initial ID for a table"""
|
||||
return self.db.getInitialId(table)
|
||||
|
||||
def _getPasswordHash(self, password: str) -> str:
|
||||
"""Creates a hash for a password"""
|
||||
return pwdContext.hash(password)
|
||||
|
||||
def _verifyPassword(self, plainPassword: str, hashedPassword: str) -> bool:
|
||||
"""Checks if the password matches the hash"""
|
||||
return pwdContext.verify(plainPassword, hashedPassword)
|
||||
|
||||
def _getCurrentTimestamp(self) -> str:
|
||||
"""Returns the current timestamp in ISO format"""
|
||||
from datetime import datetime
|
||||
return datetime.now().isoformat()
|
||||
|
||||
# Mandate methods
|
||||
|
||||
def getAllMandates(self) -> List[Dict[str, Any]]:
|
||||
"""Returns all mandates"""
|
||||
return self.db.getRecordset("mandates")
|
||||
|
||||
def getMandate(self, mandateId: int) -> Optional[Dict[str, Any]]:
|
||||
"""Returns a mandate by its ID"""
|
||||
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
|
||||
if mandates:
|
||||
return mandates[0]
|
||||
return None
|
||||
|
||||
def createMandate(self, name: str, language: str = "de") -> Dict[str, Any]:
|
||||
"""Creates a new mandate"""
|
||||
mandateData = {
|
||||
"name": name,
|
||||
"language": language
|
||||
}
|
||||
|
||||
return self.db.recordCreate("mandates", mandateData)
|
||||
|
||||
def updateMandate(self, mandateId: int, mandateData: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Updates an existing mandate
|
||||
|
||||
Args:
|
||||
mandateId: The ID of the mandate to update
|
||||
mandateData: The mandate data to update
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The updated mandate data
|
||||
|
||||
Raises:
|
||||
ValueError: If the mandate is not found
|
||||
"""
|
||||
# Check if the mandate exists
|
||||
mandate = self.getMandate(mandateId)
|
||||
if not mandate:
|
||||
raise ValueError(f"Mandate with ID {mandateId} not found")
|
||||
|
||||
# Update the mandate
|
||||
updatedMandate = self.db.recordModify("mandates", mandateId, mandateData)
|
||||
|
||||
return updatedMandate
|
||||
|
||||
def deleteMandate(self, mandateId: int) -> bool:
|
||||
"""
|
||||
Deletes a mandate and all associated users and data
|
||||
|
||||
Args:
|
||||
mandateId: The ID of the mandate to delete
|
||||
|
||||
Returns:
|
||||
bool: True if the mandate was successfully deleted, otherwise False
|
||||
"""
|
||||
# Check if the mandate exists
|
||||
mandate = self.getMandate(mandateId)
|
||||
if not mandate:
|
||||
return False
|
||||
|
||||
# Check if it's the initial mandate
|
||||
initialMandateId = self.getInitialId("mandates")
|
||||
if initialMandateId is not None and mandateId == initialMandateId:
|
||||
logger.warning(f"Attempt to delete the Root mandate was prevented")
|
||||
return False
|
||||
|
||||
# Find all users of the mandate
|
||||
users = self.getUsersByMandate(mandateId)
|
||||
|
||||
# Delete all users of the mandate and their associated data
|
||||
for user in users:
|
||||
self.deleteUser(user["id"])
|
||||
|
||||
# Delete the mandate
|
||||
success = self.db.recordDelete("mandates", mandateId)
|
||||
|
||||
if success:
|
||||
logger.info(f"Mandate with ID {mandateId} was successfully deleted")
|
||||
else:
|
||||
logger.error(f"Error deleting mandate with ID {mandateId}")
|
||||
|
||||
return success
|
||||
|
||||
# User methods
|
||||
|
||||
def getAllUsers(self) -> List[Dict[str, Any]]:
|
||||
"""Returns all users"""
|
||||
users = self.db.getRecordset("users")
|
||||
# Remove password hashes from the response
|
||||
for user in users:
|
||||
if "hashedPassword" in user:
|
||||
del user["hashedPassword"]
|
||||
return users
|
||||
|
||||
def getUsersByMandate(self, mandateId: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns all users of a specific mandate
|
||||
|
||||
Args:
|
||||
mandateId: The ID of the mandate
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of users in the mandate
|
||||
"""
|
||||
users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId})
|
||||
# Remove password hashes from the response
|
||||
for user in users:
|
||||
if "hashedPassword" in user:
|
||||
del user["hashedPassword"]
|
||||
return users
|
||||
|
||||
def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
"""Returns a user by username"""
|
||||
users = self.db.getRecordset("users")
|
||||
for user in users:
|
||||
if user.get("username") == username:
|
||||
return user
|
||||
return None
|
||||
|
||||
def getUser(self, userId: int) -> Optional[Dict[str, Any]]:
|
||||
"""Returns a user by ID"""
|
||||
users = self.db.getRecordset("users", recordFilter={"id": userId})
|
||||
if users:
|
||||
user = users[0]
|
||||
# Remove password hash from the API response
|
||||
if "hashedPassword" in user:
|
||||
userCopy = user.copy()
|
||||
del userCopy["hashedPassword"]
|
||||
return userCopy
|
||||
return user
|
||||
return None
|
||||
|
||||
def createUser(self, username: str, password: str, email: str = None,
|
||||
fullName: str = None, language: str = "de", mandateId: int = None,
|
||||
disabled: bool = False, privilege: str = "user") -> Dict[str, Any]:
|
||||
"""
|
||||
Creates a new user
|
||||
|
||||
Args:
|
||||
username: The username
|
||||
password: The password
|
||||
email: The email address (optional)
|
||||
fullName: The full name (optional)
|
||||
language: The preferred language (default: "de")
|
||||
mandateId: The ID of the mandate (optional)
|
||||
disabled: Whether the user is disabled (default: False)
|
||||
privilege: The privilege level (default: "user")
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created user data
|
||||
|
||||
Raises:
|
||||
ValueError: If the username already exists
|
||||
"""
|
||||
# Check if the username already exists
|
||||
existingUser = self.getUserByUsername(username)
|
||||
if existingUser:
|
||||
raise ValueError(f"User '{username}' already exists")
|
||||
|
||||
# Use the provided mandateId or the current context
|
||||
userMandateId = mandateId if mandateId is not None else self.mandateId
|
||||
|
||||
userData = {
|
||||
"mandateId": userMandateId,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"fullName": fullName,
|
||||
"disabled": disabled,
|
||||
"language": language,
|
||||
"privilege": privilege,
|
||||
"hashedPassword": self._getPasswordHash(password)
|
||||
}
|
||||
|
||||
createdUser = self.db.recordCreate("users", userData)
|
||||
|
||||
# Remove password hash from the response
|
||||
if "hashedPassword" in createdUser:
|
||||
del createdUser["hashedPassword"]
|
||||
|
||||
return createdUser
|
||||
|
||||
def authenticateUser(self, username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Authenticates a user by username and password
|
||||
|
||||
Args:
|
||||
username: The username
|
||||
password: The password
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: The user data or None if authentication fails
|
||||
"""
|
||||
user = self.getUserByUsername(username)
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if not self._verifyPassword(password, user.get("hashedPassword", "")):
|
||||
return None
|
||||
|
||||
# Check if the user is disabled
|
||||
if user.get("disabled", False):
|
||||
return None
|
||||
|
||||
# Create a copy without password hash
|
||||
authenticatedUser = {**user}
|
||||
if "hashedPassword" in authenticatedUser:
|
||||
del authenticatedUser["hashedPassword"]
|
||||
|
||||
return authenticatedUser
|
||||
|
||||
def updateUser(self, userId: int, userData: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Updates a user
|
||||
|
||||
Args:
|
||||
userId: The ID of the user to update
|
||||
userData: The user data to update
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The updated user data
|
||||
|
||||
Raises:
|
||||
ValueError: If the user is not found
|
||||
"""
|
||||
# Get the current user with password hash (directly from DB)
|
||||
users = self.db.getRecordset("users", recordFilter={"id": userId})
|
||||
if not users:
|
||||
raise ValueError(f"User with ID {userId} not found")
|
||||
|
||||
user = users[0]
|
||||
|
||||
# If the password is being changed, hash it
|
||||
if "password" in userData:
|
||||
userData["hashedPassword"] = self._getPasswordHash(userData["password"])
|
||||
del userData["password"]
|
||||
|
||||
# Update the user
|
||||
updatedUser = self.db.recordModify("users", userId, userData)
|
||||
|
||||
# Remove password hash from the response
|
||||
if "hashedPassword" in updatedUser:
|
||||
del updatedUser["hashedPassword"]
|
||||
|
||||
return updatedUser
|
||||
|
||||
def disableUser(self, userId: int) -> Dict[str, Any]:
|
||||
"""Disables a user"""
|
||||
return self.updateUser(userId, {"disabled": True})
|
||||
|
||||
def enableUser(self, userId: int) -> Dict[str, Any]:
|
||||
"""Enables a user"""
|
||||
return self.updateUser(userId, {"disabled": False})
|
||||
|
||||
def _deleteUserReferencedData(self, userId: int) -> None:
|
||||
"""
|
||||
Deletes all data associated with a user
|
||||
|
||||
Args:
|
||||
userId: The ID of the user
|
||||
"""
|
||||
# Here all tables are searched and all entries referencing this user are deleted
|
||||
|
||||
# Delete user attributes
|
||||
try:
|
||||
attributes = self.db.getRecordset("attributes", recordFilter={"userId": userId})
|
||||
for attribute in attributes:
|
||||
self.db.recordDelete("attributes", attribute["id"])
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting attributes for user {userId}: {e}")
|
||||
|
||||
# Other tables that might reference the user
|
||||
# (Depending on the application's database structure)
|
||||
|
||||
logger.info(f"All referenced data for user {userId} has been deleted")
|
||||
|
||||
def deleteUser(self, userId: int) -> bool:
|
||||
"""
|
||||
Deletes a user and all associated data
|
||||
|
||||
Args:
|
||||
userId: The ID of the user to delete
|
||||
|
||||
Returns:
|
||||
bool: True if the user was successfully deleted, otherwise False
|
||||
"""
|
||||
# Check if the user exists
|
||||
users = self.db.getRecordset("users", recordFilter={"id": userId})
|
||||
if not users:
|
||||
return False
|
||||
|
||||
# Check if it's the initial user
|
||||
initialUserId = self.getInitialId("users")
|
||||
if initialUserId is not None and userId == initialUserId:
|
||||
logger.warning("Attempt to delete the Root Admin was prevented")
|
||||
return False
|
||||
|
||||
# Delete all data associated with the user
|
||||
self._deleteUserReferencedData(userId)
|
||||
|
||||
# Delete the user
|
||||
success = self.db.recordDelete("users", userId)
|
||||
|
||||
if success:
|
||||
logger.info(f"User with ID {userId} was successfully deleted")
|
||||
else:
|
||||
logger.error(f"Error deleting user with ID {userId}")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
# Singleton factory for GatewayInterface instances per context
|
||||
_gatewayInterfaces = {}
|
||||
|
||||
def getGatewayInterface(mandateId: int = None, userId: int = None) -> GatewayInterface:
|
||||
"""
|
||||
Returns a GatewayInterface instance for the specified context.
|
||||
Reuses existing instances.
|
||||
|
||||
Args:
|
||||
mandateId: ID of the mandate
|
||||
userId: ID of the user
|
||||
|
||||
Returns:
|
||||
GatewayInterface instance
|
||||
"""
|
||||
contextKey = f"{mandateId}_{userId}"
|
||||
if contextKey not in _gatewayInterfaces:
|
||||
_gatewayInterfaces[contextKey] = GatewayInterface(mandateId, userId)
|
||||
return _gatewayInterfaces[contextKey]
|
||||
|
||||
# Initialize the interface
|
||||
getGatewayInterface()
|
||||
File diff suppressed because it is too large
Load diff
656
modules/agentEmail.py
Normal file
656
modules/agentEmail.py
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
"""
|
||||
Email agent for creating draft emails with Microsoft Graph API.
|
||||
Creates HTML-formatted email templates with attachments based on input documents.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import msal
|
||||
import requests
|
||||
from typing import Dict, Any, List, Optional
|
||||
from modules.configuration import APP_CONFIG
|
||||
|
||||
from modules.workflowAgentsRegistry import AgentBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AgentEmail(AgentBase):
|
||||
"""AI-driven agent for creating email templates and drafts using Microsoft Graph API"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the email agent"""
|
||||
super().__init__()
|
||||
self.name = "email"
|
||||
self.label = "Email Templates"
|
||||
self.description = "Creates email templates with HTML-formatted body and attachments from input documents"
|
||||
self.capabilities = [
|
||||
"emailDrafting",
|
||||
"contentFormatting",
|
||||
"htmlTemplates",
|
||||
"documentAttachment",
|
||||
"msftGraphIntegration"
|
||||
]
|
||||
|
||||
# Initialize configuration
|
||||
self.client_id = None
|
||||
self.client_secret = None
|
||||
self.tenant_id = None
|
||||
self.redirect_uri = None
|
||||
self.authority = None
|
||||
self.scopes = ["Mail.ReadWrite", "User.Read"]
|
||||
|
||||
# Token storage directory
|
||||
self.token_dir = './token_storage'
|
||||
if not os.path.exists(self.token_dir):
|
||||
os.makedirs(self.token_dir)
|
||||
logger.info(f"Created token storage directory: {self.token_dir}")
|
||||
|
||||
def setDependencies(self, mydom=None):
|
||||
"""Set external dependencies for the agent."""
|
||||
self.mydom = mydom
|
||||
self._loadConfiguration()
|
||||
|
||||
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]:
|
||||
"""
|
||||
Process a task by creating an email template based on input documents.
|
||||
Sends a login request to the frontend if Microsoft authentication is required.
|
||||
|
||||
Args:
|
||||
task: Task dictionary with prompt, inputDocuments, outputSpecifications
|
||||
|
||||
Returns:
|
||||
Dictionary with feedback and documents
|
||||
"""
|
||||
try:
|
||||
# Extract task information
|
||||
prompt = task.get("prompt", "")
|
||||
inputDocuments = task.get("inputDocuments", [])
|
||||
|
||||
# Check AI service
|
||||
if not self.mydom:
|
||||
return {
|
||||
"feedback": "The Email agent requires an AI service to function.",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
# Check Microsoft authentication status
|
||||
user_info, access_token = self._getCurrentUserToken()
|
||||
|
||||
# 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 {
|
||||
"feedback": "⚠️ Microsoft authentication required. Please complete the authentication process when prompted.",
|
||||
"documents": [auth_instructions],
|
||||
"log": {
|
||||
"message": "doMsftLogin",
|
||||
"type": "system",
|
||||
"details": "Microsoft authentication required to create email drafts"
|
||||
}
|
||||
}
|
||||
|
||||
# Extract document data from input
|
||||
documentContents, attachments = self._processInputDocuments(inputDocuments)
|
||||
|
||||
# Generate email subject and body using AI
|
||||
emailTemplate = await self._generateEmailTemplate(prompt, documentContents)
|
||||
|
||||
# Create HTML preview of the email
|
||||
htmlPreview = self._createHtmlPreview(emailTemplate)
|
||||
|
||||
# Attempt to create a draft email using Microsoft Graph API
|
||||
draft_result, user_email = self._createDraftEmail(
|
||||
emailTemplate["recipient"],
|
||||
emailTemplate["subject"],
|
||||
emailTemplate["htmlBody"],
|
||||
attachments
|
||||
)
|
||||
|
||||
# Prepare output documents
|
||||
documents = []
|
||||
|
||||
# Add HTML preview document
|
||||
previewDoc = self.formatAgentDocumentOutput(
|
||||
"email_preview.html",
|
||||
htmlPreview,
|
||||
"text/html"
|
||||
)
|
||||
documents.append(previewDoc)
|
||||
|
||||
# Add email template as JSON for reference
|
||||
templateJson = json.dumps(emailTemplate, indent=2)
|
||||
templateDoc = self.formatAgentDocumentOutput(
|
||||
"email_template.json",
|
||||
templateJson,
|
||||
"application/json"
|
||||
)
|
||||
documents.append(templateDoc)
|
||||
|
||||
# Prepare feedback message
|
||||
if draft_result:
|
||||
feedback = f"Email draft created successfully for {user_email}. The subject is: '{emailTemplate['subject']}'"
|
||||
if attachments:
|
||||
feedback += f" with {len(attachments)} attachment(s)"
|
||||
feedback += ". You can open and edit it in your Outlook draft folder."
|
||||
else:
|
||||
feedback = "Email template created but could not save as draft. HTML preview and template are available as documents."
|
||||
|
||||
return {
|
||||
"feedback": feedback,
|
||||
"documents": documents
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in email creation: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"feedback": f"Error creating email template: {str(e)}",
|
||||
"documents": []
|
||||
}
|
||||
|
||||
def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a simple document that explains authentication is required.
|
||||
This document is minimal as the actual authentication will be handled by frontend.
|
||||
|
||||
Returns:
|
||||
Document dictionary
|
||||
"""
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return self.formatAgentDocumentOutput(
|
||||
"microsoft_authentication.html",
|
||||
html_content,
|
||||
"text/html"
|
||||
)
|
||||
|
||||
def _processInputDocuments(self, documents: List[Dict[str, Any]]) -> tuple:
|
||||
"""
|
||||
Process input documents to extract content and prepare attachments.
|
||||
|
||||
Args:
|
||||
documents: List of input documents
|
||||
|
||||
Returns:
|
||||
Tuple of (document content text, list of attachments)
|
||||
"""
|
||||
documentContents = []
|
||||
attachments = []
|
||||
|
||||
for doc in documents:
|
||||
docName = doc.get("name", "unnamed")
|
||||
if doc.get("ext"):
|
||||
docName = f"{docName}.{doc.get('ext')}"
|
||||
|
||||
# Add document name to contents
|
||||
documentContents.append(f"\n\n--- {docName} ---\n")
|
||||
|
||||
# Process contents
|
||||
hasAttachment = False
|
||||
for content in doc.get("contents", []):
|
||||
# Add extracted text to document contents
|
||||
if content.get("dataExtracted"):
|
||||
documentContents.append(content.get("dataExtracted", ""))
|
||||
|
||||
# Prepare attachment if it has content data
|
||||
if content.get("data"):
|
||||
# Check if this content should be an attachment
|
||||
# Typically files like PDFs, images, etc.
|
||||
contentType = content.get("contentType", "")
|
||||
if (not contentType.startswith("text/") or
|
||||
contentType in ["application/pdf", "application/msword"]):
|
||||
hasAttachment = True
|
||||
|
||||
# If document has content to attach, add to attachments
|
||||
if hasAttachment:
|
||||
attachments.append({
|
||||
"name": docName,
|
||||
"document": doc
|
||||
})
|
||||
|
||||
return "\n".join(documentContents), attachments
|
||||
|
||||
async def _generateEmailTemplate(self, prompt: str, documentContents: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate email template using AI.
|
||||
|
||||
Args:
|
||||
prompt: The task prompt
|
||||
documentContents: Extracted document content
|
||||
|
||||
Returns:
|
||||
Email template dictionary with recipient, subject, body
|
||||
"""
|
||||
emailPrompt = f"""
|
||||
Create an email based on the following request:
|
||||
|
||||
REQUEST: {prompt}
|
||||
|
||||
DOCUMENT CONTENTS:
|
||||
{documentContents[:2000]}... (truncated if longer)
|
||||
|
||||
Generate an email template with:
|
||||
1. A relevant recipient (use placeholder or derive from content if possible)
|
||||
2. A concise but descriptive subject line
|
||||
3. A professional HTML-formatted email body
|
||||
4. Appropriate greeting and closing
|
||||
|
||||
Format your response as JSON with these fields:
|
||||
- recipient: email address
|
||||
- subject: subject line
|
||||
- plainBody: plain text version
|
||||
- htmlBody: HTML formatted version
|
||||
|
||||
Only return valid JSON. No preamble or explanations.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = await self.mydom.callAi([
|
||||
{"role": "system", "content": "You are an email template specialist. Respond with valid JSON only."},
|
||||
{"role": "user", "content": emailPrompt}
|
||||
], produceUserAnswer=True)
|
||||
|
||||
# Extract JSON from response
|
||||
jsonStart = response.find('{')
|
||||
jsonEnd = response.rfind('}') + 1
|
||||
|
||||
if jsonStart >= 0 and jsonEnd > jsonStart:
|
||||
template = json.loads(response[jsonStart:jsonEnd])
|
||||
return template
|
||||
else:
|
||||
# Fallback if JSON not found
|
||||
return {
|
||||
"recipient": "recipient@example.com",
|
||||
"subject": "Information Regarding Your Request",
|
||||
"plainBody": f"This email is regarding your request: {prompt}",
|
||||
"htmlBody": f"<html><body><p>This email is regarding your request: {prompt}</p></body></html>"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error generating email template: {str(e)}")
|
||||
return {
|
||||
"recipient": "recipient@example.com",
|
||||
"subject": "Information Regarding Your Request",
|
||||
"plainBody": f"This email is regarding your request: {prompt}",
|
||||
"htmlBody": f"<html><body><p>This email is regarding your request: {prompt}</p></body></html>"
|
||||
}
|
||||
|
||||
def _createHtmlPreview(self, emailTemplate: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Create an HTML preview of the email template.
|
||||
|
||||
Args:
|
||||
emailTemplate: Email template dictionary
|
||||
|
||||
Returns:
|
||||
HTML string for preview
|
||||
"""
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: {emailTemplate.get('subject', 'Email Template')}</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
|
||||
.email-container {{ max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }}
|
||||
.email-header {{ background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }}
|
||||
.email-content {{ padding: 20px; }}
|
||||
.email-footer {{ background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }}
|
||||
.field {{ margin-bottom: 10px; }}
|
||||
.field-label {{ font-weight: bold; color: #555; }}
|
||||
.email-body {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>{emailTemplate.get('recipient', 'recipient@example.com')}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>{emailTemplate.get('subject', 'No Subject')}</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
{emailTemplate.get('htmlBody', '<p>No content</p>')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
def _getCurrentUserToken(self):
|
||||
"""
|
||||
Get the current user's token from the token store.
|
||||
Does not attempt to initiate authentication flow.
|
||||
|
||||
Returns:
|
||||
Tuple of (user info, access token) or (None, None) if no valid token
|
||||
"""
|
||||
try:
|
||||
# Check if we have any token files
|
||||
if not os.path.exists(self.token_dir) or not os.listdir(self.token_dir):
|
||||
logger.warning("No token files found. User needs to authenticate with Microsoft.")
|
||||
return None, None
|
||||
|
||||
# Find the most recently modified token file
|
||||
token_files = [os.path.join(self.token_dir, f) for f in os.listdir(self.token_dir) if f.endswith('.json')]
|
||||
if not token_files:
|
||||
return None, None
|
||||
|
||||
most_recent = max(token_files, key=os.path.getmtime)
|
||||
user_id = os.path.basename(most_recent).split('.')[0]
|
||||
|
||||
# Load the token
|
||||
token_data = self._loadTokenFromFile(user_id)
|
||||
if not token_data or not token_data.get("access_token"):
|
||||
logger.warning(f"No valid token data for user {user_id}")
|
||||
return None, None
|
||||
|
||||
# Get user info from token
|
||||
user_info = self._getUserInfoFromToken(token_data["access_token"])
|
||||
if not user_info:
|
||||
# Try to refresh the token
|
||||
if self._refreshToken(user_id):
|
||||
# Load the refreshed token
|
||||
token_data = self._loadTokenFromFile(user_id)
|
||||
if token_data and token_data.get("access_token"):
|
||||
user_info = self._getUserInfoFromToken(token_data["access_token"])
|
||||
if user_info:
|
||||
return user_info, token_data["access_token"]
|
||||
|
||||
logger.warning(f"Could not get user info for user {user_id}")
|
||||
return None, None
|
||||
|
||||
return user_info, token_data["access_token"]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting current user token: {str(e)}")
|
||||
return None, None
|
||||
|
||||
def _loadTokenFromFile(self, user_id):
|
||||
"""Load token data from a file"""
|
||||
filename = os.path.join(self.token_dir, f"{user_id}.json")
|
||||
if os.path.exists(filename):
|
||||
try:
|
||||
with open(filename, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading token file: {str(e)}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def _getUserInfoFromToken(self, access_token):
|
||||
"""Get user information using the access token"""
|
||||
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 _refreshToken(self, user_id):
|
||||
"""Refresh the access token using the stored refresh token"""
|
||||
token_data = self._loadTokenFromFile(user_id)
|
||||
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 tokens in storage
|
||||
token_data["access_token"] = result["access_token"]
|
||||
if "refresh_token" in result:
|
||||
token_data["refresh_token"] = result["refresh_token"]
|
||||
|
||||
# Save the updated token
|
||||
filename = os.path.join(self.token_dir, f"{user_id}.json")
|
||||
try:
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(token_data, f)
|
||||
logger.info(f"Token saved for user: {user_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving token file: {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 with fixed attachment handling.
|
||||
Directly uses the document's data attribute for attachments.
|
||||
|
||||
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
|
||||
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:
|
||||
# Get the document object
|
||||
doc = attachment.get('document', {})
|
||||
file_name = attachment.get('name', 'attachment.file')
|
||||
|
||||
logger.info(f"Processing attachment: {file_name}")
|
||||
|
||||
# Directly access the data attribute from the document
|
||||
if 'data' in doc:
|
||||
file_content = doc['data']
|
||||
is_base64 = doc.get('base64Encoded', False)
|
||||
|
||||
# Determine content type
|
||||
content_type = "application/octet-stream"
|
||||
if 'mimeType' in doc:
|
||||
content_type = doc['mimeType']
|
||||
elif 'contentType' in doc:
|
||||
content_type = doc['contentType']
|
||||
|
||||
# Check if we need to encode the content
|
||||
if not is_base64:
|
||||
logger.info(f"Base64 encoding content for {file_name}")
|
||||
if isinstance(file_content, str):
|
||||
try:
|
||||
# Check if already valid base64
|
||||
base64.b64decode(file_content)
|
||||
logger.info("Content appears to be valid base64 already")
|
||||
except:
|
||||
# Not valid base64, encode it
|
||||
logger.info("Encoding string content to base64")
|
||||
file_content = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
|
||||
elif isinstance(file_content, bytes):
|
||||
logger.info("Encoding bytes content to base64")
|
||||
file_content = base64.b64encode(file_content).decode('utf-8')
|
||||
|
||||
# Add attachment to email data
|
||||
logger.info(f"Adding attachment: {file_name} ({content_type})")
|
||||
attachment_data = {
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
'name': file_name,
|
||||
'contentType': content_type,
|
||||
'contentBytes': file_content
|
||||
}
|
||||
email_data['attachments'].append(attachment_data)
|
||||
logger.info(f"Successfully added attachment: {file_name}")
|
||||
else:
|
||||
logger.warning(f"Document does not contain 'data' attribute: {file_name}")
|
||||
# Try to find data in the fileId
|
||||
if 'fileId' in doc:
|
||||
logger.info(f"Found fileId: {doc['fileId']} - could implement fileId-based attachment lookup here")
|
||||
# Future enhancement: implement file lookup by fileId
|
||||
|
||||
# Try to create draft using drafts folder endpoint (Option 1)
|
||||
try:
|
||||
logger.info("Attempting to create draft email using drafts folder 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', []))}")
|
||||
|
||||
response = requests.post(
|
||||
'https://graph.microsoft.com/v1.0/me/mailFolders/drafts/messages',
|
||||
headers=headers,
|
||||
json=email_data
|
||||
)
|
||||
|
||||
if response.status_code >= 200 and response.status_code < 300:
|
||||
logger.info("Successfully created draft email using drafts folder endpoint")
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Drafts folder method failed: {response.status_code} - {response.text}")
|
||||
|
||||
# Try fallback method with messages endpoint (Option 2)
|
||||
logger.info("Trying fallback with messages endpoint")
|
||||
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 also failed: {response.status_code} - {response.text}")
|
||||
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()
|
||||
|
|
@ -145,7 +145,13 @@ class LucyDOMInterface:
|
|||
"userId": self.userId,
|
||||
"content": "Gib mir die ersten 1000 Primzahlen",
|
||||
"name": "Code: Primzahlen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mandateId": self.mandateId,
|
||||
"userId": self.userId,
|
||||
"content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
|
||||
"name": "Mail: Vorbereitung"
|
||||
},
|
||||
]
|
||||
|
||||
# Create prompts
|
||||
|
|
|
|||
|
|
@ -43,3 +43,6 @@ python-dotenv==1.0.0
|
|||
|
||||
## Dependencies for trio (used by httpx)
|
||||
sortedcontainers>=2.4.0 # Required by trio
|
||||
|
||||
## MSFT Integration
|
||||
msal==1.24.1
|
||||
|
|
|
|||
405
routes/routeMsft.py
Normal file
405
routes/routeMsft.py
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
import msal
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from modules.auth import getCurrentActiveUser, getUserContext
|
||||
from modules.configuration import APP_CONFIG
|
||||
from modules.lucydomInterface import getLucydomInterface
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create router for Microsoft Auth endpoints
|
||||
router = APIRouter(
|
||||
prefix="/api/msft",
|
||||
tags=["Microsoft"],
|
||||
responses={
|
||||
404: {"description": "Not found"},
|
||||
400: {"description": "Bad request"},
|
||||
401: {"description": "Unauthorized"},
|
||||
403: {"description": "Forbidden"},
|
||||
500: {"description": "Internal server error"}
|
||||
}
|
||||
)
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# Create a simple file-based token storage
|
||||
TOKEN_DIR = './token_storage'
|
||||
if not os.path.exists(TOKEN_DIR):
|
||||
os.makedirs(TOKEN_DIR)
|
||||
logger.info(f"Created token storage directory: {TOKEN_DIR}")
|
||||
|
||||
def save_token_to_file(user_id: str, token_data: Dict[str, Any]):
|
||||
"""Save token data to a file"""
|
||||
filename = os.path.join(TOKEN_DIR, f"{user_id}.json")
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(token_data, f)
|
||||
logger.info(f"Token saved for user: {user_id}")
|
||||
|
||||
def load_token_from_file(user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load token data from a file"""
|
||||
filename = os.path.join(TOKEN_DIR, f"{user_id}.json")
|
||||
if os.path.exists(filename):
|
||||
with open(filename, 'r') as f:
|
||||
return json.load(f)
|
||||
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
|
||||
|
||||
def refresh_token(user_id: str) -> bool:
|
||||
"""Refresh the access token using the stored refresh token"""
|
||||
token_data = load_token_from_file(user_id)
|
||||
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"]
|
||||
|
||||
save_token_to_file(user_id, token_data)
|
||||
logger.info("Access token refreshed successfully")
|
||||
return True
|
||||
|
||||
def silent_login(user_id: str) -> bool:
|
||||
"""Try to silently log in a user using their refresh token"""
|
||||
token_data = load_token_from_file(user_id)
|
||||
if not token_data or not token_data.get("refresh_token"):
|
||||
logger.info(f"No refresh token found for user: {user_id}")
|
||||
return False
|
||||
|
||||
# Try to refresh the token
|
||||
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"]
|
||||
|
||||
save_token_to_file(user_id, token_data)
|
||||
|
||||
return True
|
||||
|
||||
@router.get("/login")
|
||||
async def login():
|
||||
# Modified implementation without requiring current user
|
||||
try:
|
||||
# Create a confidential client application
|
||||
msal_app = msal.ConfidentialClientApplication(
|
||||
app_config["client_id"],
|
||||
authority=app_config["authority"],
|
||||
client_credential=app_config["client_credential"]
|
||||
)
|
||||
|
||||
# Build the auth URL
|
||||
auth_url = msal_app.get_authorization_request_url(
|
||||
SCOPES,
|
||||
state="anonymous-user", # Use a general state since we don't have user context
|
||||
redirect_uri=app_config["redirect_uri"]
|
||||
)
|
||||
|
||||
logger.info(f"Redirecting to Microsoft login: {auth_url[:60]}...")
|
||||
return RedirectResponse(auth_url)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initiating Microsoft login: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error initiating Microsoft login: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/auth/callback")
|
||||
async def auth_callback(request: Request, code: str = None, state: str = None):
|
||||
"""Handle callback from Microsoft login"""
|
||||
try:
|
||||
# Log callback for debugging
|
||||
logger.info("Received callback from Microsoft login")
|
||||
|
||||
if not code:
|
||||
logger.error("No authorization code received in callback")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"message": "No authorization code received"}
|
||||
)
|
||||
|
||||
# Extract user and mandate info from state if available
|
||||
user_id = None
|
||||
mandate_id = None
|
||||
|
||||
if state and state != "anonymous-user":
|
||||
try:
|
||||
mandate_id, user_id = state.split(":")
|
||||
logger.info(f"State contains mandate_id: {mandate_id}, user_id: {user_id}")
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid state format: {state}")
|
||||
# Generate a generic user ID if state is invalid
|
||||
user_id = f"user_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
else:
|
||||
# For anonymous authentication, create a generic user ID
|
||||
logger.info("Anonymous authentication (no user context)")
|
||||
user_id = f"user_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
|
||||
# Create a confidential client application
|
||||
msal_app = msal.ConfidentialClientApplication(
|
||||
app_config["client_id"],
|
||||
authority=app_config["authority"],
|
||||
client_credential=app_config["client_credential"]
|
||||
)
|
||||
|
||||
# Get tokens using the authorization code
|
||||
result = msal_app.acquire_token_by_authorization_code(
|
||||
code,
|
||||
scopes=SCOPES,
|
||||
redirect_uri=app_config["redirect_uri"]
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
logger.error(f"Error acquiring token: {result.get('error')}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"message": f"Error acquiring token: {result.get('error_description', result.get('error'))}"}
|
||||
)
|
||||
|
||||
# Store user information
|
||||
user_info = {}
|
||||
if "id_token_claims" in result:
|
||||
user_info = {
|
||||
"name": result["id_token_claims"].get("name", ""),
|
||||
"email": result["id_token_claims"].get("preferred_username", ""),
|
||||
}
|
||||
|
||||
# If we have user info from the token, use that for user_id
|
||||
token_user_id = result["id_token_claims"].get("oid") or result["id_token_claims"].get("sub")
|
||||
if token_user_id:
|
||||
user_id = token_user_id
|
||||
elif not user_id and user_info.get("email"):
|
||||
# Fall back to email-based ID if no other ID is available
|
||||
user_id = user_info.get("email", "user").replace("@", "_").replace(".", "_")
|
||||
|
||||
# Save tokens to file
|
||||
token_data = {
|
||||
"access_token": result["access_token"],
|
||||
"refresh_token": result.get("refresh_token", ""),
|
||||
"user_info": user_info,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Ensure token directory exists
|
||||
if not os.path.exists(TOKEN_DIR):
|
||||
os.makedirs(TOKEN_DIR)
|
||||
|
||||
# Save token to file
|
||||
token_file = os.path.join(TOKEN_DIR, f"{user_id}.json")
|
||||
with open(token_file, 'w') as f:
|
||||
json.dump(token_data, f)
|
||||
|
||||
logger.info(f"User authenticated: {user_info.get('email', 'unknown')}")
|
||||
|
||||
# Create a success page
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Authentication Successful</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; text-align: center; }
|
||||
.success-container { max-width: 600px; margin: 0 auto; }
|
||||
h1 { color: #0078d4; }
|
||||
.success-icon { font-size: 72px; color: #107c10; margin: 20px 0; }
|
||||
.button { display: inline-block; background-color: #0078d4; color: white;
|
||||
padding: 10px 20px; text-decoration: none; border-radius: 4px;
|
||||
font-weight: bold; margin-top: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="success-container">
|
||||
<h1>Authentication Successful</h1>
|
||||
<div class="success-icon">✓</div>
|
||||
<p>You have successfully authenticated with Microsoft.</p>
|
||||
<p>You can now close this tab and return to the application.</p>
|
||||
<p>Your email templates will now be able to create drafts in your mailbox.</p>
|
||||
<a href="javascript:window.close()" class="button">Close Window</a>
|
||||
</div>
|
||||
<script>
|
||||
// Attempt to notify the opener window that authentication is complete
|
||||
if (window.opener && !window.opener.closed) {
|
||||
try {
|
||||
window.opener.postMessage({ type: 'msft_auth_complete', success: true }, '*');
|
||||
} catch (e) {
|
||||
console.error('Error notifying opener:', e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
else:
|
||||
logger.warning("No id_token_claims found in result")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"message": "Failed to retrieve user information"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"message": f"Error in auth callback: {str(e)}"}
|
||||
)
|
||||
|
||||
@router.get("/status")
|
||||
async def auth_status(
|
||||
msft_user_id: Optional[str] = Cookie(None),
|
||||
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
|
||||
):
|
||||
"""Check Microsoft authentication status"""
|
||||
try:
|
||||
# Get user ID
|
||||
if not msft_user_id:
|
||||
mandateId, userId = await getUserContext(currentUser)
|
||||
user_id = str(userId)
|
||||
else:
|
||||
user_id = msft_user_id
|
||||
|
||||
# Check if user has a token
|
||||
token_data = load_token_from_file(user_id)
|
||||
if not token_data:
|
||||
return JSONResponse(
|
||||
content={"authenticated": False, "message": "Not authenticated with Microsoft"}
|
||||
)
|
||||
|
||||
# Check if token is valid
|
||||
if not verify_token(token_data.get("access_token", "")):
|
||||
# Try to refresh token
|
||||
if refresh_token(user_id):
|
||||
token_data = load_token_from_file(user_id)
|
||||
user_info = token_data.get("user_info", {})
|
||||
return JSONResponse(
|
||||
content={
|
||||
"authenticated": True,
|
||||
"message": "Token refreshed successfully",
|
||||
"user": user_info
|
||||
}
|
||||
)
|
||||
else:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"authenticated": False,
|
||||
"message": "Token expired and couldn't be refreshed"
|
||||
}
|
||||
)
|
||||
|
||||
# Token is valid, return user info
|
||||
user_info = token_data.get("user_info", {})
|
||||
return JSONResponse(
|
||||
content={
|
||||
"authenticated": True,
|
||||
"message": "Authenticated with Microsoft",
|
||||
"user": user_info
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking auth status: {str(e)}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"message": f"Error checking auth status: {str(e)}"}
|
||||
)
|
||||
42
static/10_email_preview.html
Normal file
42
static/10_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Verschiebung des Meetings auf Freitag</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>peter.muster@domain.com</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Verschiebung des Meetings auf Freitag</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser neue Termin für Sie passt.</p><p>Vielen Dank für Ihr Verständnis.</p><p>Mit freundlichen Grüßen,<br>[Ihr Name]</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/11_email_template.json
Normal file
6
static/11_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "peter.muster@domain.com",
|
||||
"subject": "Verschiebung des Meetings auf Freitag",
|
||||
"plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser neue Termin f\u00fcr Sie passt.\n\nVielen Dank f\u00fcr Ihr Verst\u00e4ndnis.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
|
||||
"htmlBody": "<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser neue Termin f\u00fcr Sie passt.</p><p>Vielen Dank f\u00fcr Ihr Verst\u00e4ndnis.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>[Ihr Name]</p>"
|
||||
}
|
||||
42
static/12_email_preview.html
Normal file
42
static/12_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Anfrage zur Terminverschiebung</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>peter.muster@domain.com</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Anfrage zur Terminverschiebung</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, diese Nachricht trifft Sie wohl. Ich schreibe Ihnen, um eine Verschiebung unseres Termins von 10 Uhr auf Freitag zu erbitten. Bitte lassen Sie mich wissen, ob dies für Sie möglich ist.</p><p>Vielen Dank im Voraus für Ihre Flexibilität.</p><p>Mit freundlichen Grüßen,<br>[Ihr Name]</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/13_email_template.json
Normal file
6
static/13_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "peter.muster@domain.com",
|
||||
"subject": "Anfrage zur Terminverschiebung",
|
||||
"plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, diese Nachricht trifft Sie wohl. Ich schreibe Ihnen, um eine Verschiebung unseres Termins von 10 Uhr auf Freitag zu erbitten. Bitte lassen Sie mich wissen, ob dies f\u00fcr Sie m\u00f6glich ist.\n\nVielen Dank im Voraus f\u00fcr Ihre Flexibilit\u00e4t.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
|
||||
"htmlBody": "<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, diese Nachricht trifft Sie wohl. Ich schreibe Ihnen, um eine Verschiebung unseres Termins von 10 Uhr auf Freitag zu erbitten. Bitte lassen Sie mich wissen, ob dies f\u00fcr Sie m\u00f6glich ist.</p><p>Vielen Dank im Voraus f\u00fcr Ihre Flexibilit\u00e4t.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>[Ihr Name]</p>"
|
||||
}
|
||||
47
static/14_microsoft_authentication.html
Normal file
47
static/14_microsoft_authentication.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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; }
|
||||
.step { margin-bottom: 20px; }
|
||||
.step-number { display: inline-block; width: 30px; height: 30px; background-color: #0078d4; color: white; border-radius: 50%; text-align: center; line-height: 30px; margin-right: 10px; font-weight: bold; }
|
||||
.auth-button { display: inline-block; background-color: #0078d4; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; margin: 20px 0; }
|
||||
.auth-button:hover { background-color: #106ebe; }
|
||||
.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. Follow these steps:</p>
|
||||
|
||||
<div class="step">
|
||||
<span class="step-number">1</span>
|
||||
<strong>Click the authentication link below</strong>
|
||||
</div>
|
||||
|
||||
<a href="http://localhost:8080/api/msft/login" class="auth-button" target="_blank">Authenticate with Microsoft</a>
|
||||
|
||||
<div class="step">
|
||||
<span class="step-number">2</span>
|
||||
<strong>Sign in with your Microsoft account</strong> and grant the required permissions
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<span class="step-number">3</span>
|
||||
<strong>Return to this application</strong> and run the email agent again after completing authentication
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
28
static/15_microsoft_authentication.html
Normal file
28
static/15_microsoft_authentication.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
42
static/16_email_preview.html
Normal file
42
static/16_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Verschiebung des Meetings auf Freitag</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>peter.muster@domain.com</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Verschiebung des Meetings auf Freitag</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting um 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser Termin für Sie passt.</p><p>Vielen Dank für Ihr Verständnis.</p><p>Mit freundlichen Grüßen,</p><p>[Ihr Name]</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/17_email_template.json
Normal file
6
static/17_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "peter.muster@domain.com",
|
||||
"subject": "Verschiebung des Meetings auf Freitag",
|
||||
"plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting um 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser Termin f\u00fcr Sie passt.\n\nVielen Dank f\u00fcr Ihr Verst\u00e4ndnis.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
|
||||
"htmlBody": "<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting um 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob dieser Termin f\u00fcr Sie passt.</p><p>Vielen Dank f\u00fcr Ihr Verst\u00e4ndnis.</p><p>Mit freundlichen Gr\u00fc\u00dfen,</p><p>[Ihr Name]</p>"
|
||||
}
|
||||
48
static/18_generated_code.py
Normal file
48
static/18_generated_code.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
inputFiles = [] # DO NOT CHANGE THIS LINE
|
||||
|
||||
# REQUIREMENTS:
|
||||
|
||||
import json
|
||||
import csv
|
||||
from io import StringIO
|
||||
|
||||
def is_prime(n):
|
||||
if n <= 1:
|
||||
return False
|
||||
if n <= 3:
|
||||
return True
|
||||
if n % 2 == 0 or n % 3 == 0:
|
||||
return False
|
||||
i = 5
|
||||
while i * i <= n:
|
||||
if n % i == 0 or n % (i + 2) == 0:
|
||||
return False
|
||||
i += 6
|
||||
return True
|
||||
|
||||
def generate_primes(limit):
|
||||
primes = []
|
||||
num = 2
|
||||
while len(primes) < limit:
|
||||
if is_prime(num):
|
||||
primes.append(num)
|
||||
num += 1
|
||||
return primes
|
||||
|
||||
primes = generate_primes(1000)
|
||||
|
||||
output = StringIO()
|
||||
csv_writer = csv.writer(output)
|
||||
for prime in primes:
|
||||
csv_writer.writerow([prime])
|
||||
|
||||
result = {
|
||||
"prime_numbers.csv": {
|
||||
"content": output.getvalue(),
|
||||
"base64Encoded": False,
|
||||
"contentType": "text/csv"
|
||||
}
|
||||
}
|
||||
|
||||
import json
|
||||
print(json.dumps(result))
|
||||
19
static/19_execution_history.json
Normal file
19
static/19_execution_history.json
Normal file
File diff suppressed because one or more lines are too long
1000
static/20_prime_numbers.csv
Normal file
1000
static/20_prime_numbers.csv
Normal file
File diff suppressed because it is too large
Load diff
42
static/21_email_preview.html
Normal file
42
static/21_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Prime Numbers CSV</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>recipient@example.com</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Prime Numbers CSV</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Sehr geehrte Damen und Herren,</p><p>anbei finden Sie die Datei <strong>'prime_numbers.csv'</strong>, die die Liste der Primzahlen enthält.</p><p>Mit freundlichen Grüßen,<br>Ihr Team</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/22_email_template.json
Normal file
6
static/22_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "recipient@example.com",
|
||||
"subject": "Prime Numbers CSV",
|
||||
"plainBody": "Sehr geehrte Damen und Herren,\n\nanbei finden Sie die Datei 'prime_numbers.csv', die die Liste der Primzahlen enth\u00e4lt.\n\nMit freundlichen Gr\u00fc\u00dfen,\nIhr Team",
|
||||
"htmlBody": "<p>Sehr geehrte Damen und Herren,</p><p>anbei finden Sie die Datei <strong>'prime_numbers.csv'</strong>, die die Liste der Primzahlen enth\u00e4lt.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>Ihr Team</p>"
|
||||
}
|
||||
933
static/23_documentProcessor.py
Normal file
933
static/23_documentProcessor.py
Normal file
|
|
@ -0,0 +1,933 @@
|
|||
"""
|
||||
Module for extracting content from various file formats.
|
||||
Provides specialized functions for processing text, PDF, Office documents, images, etc.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import io
|
||||
from typing import Dict, Any, List, Optional, Union, Tuple
|
||||
import base64
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Optional imports - only loaded when needed
|
||||
pdfExtractorLoaded = False
|
||||
officeExtractorLoaded = False
|
||||
imageProcessorLoaded = False
|
||||
|
||||
def getDocumentContents(fileMetadata: Dict[str, Any], fileContent: bytes) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Main function for extracting content from a file based on its MIME type.
|
||||
Delegates to specialized extraction functions.
|
||||
|
||||
Args:
|
||||
fileMetadata: File metadata (Name, MIME type, etc.)
|
||||
fileContent: Binary data of the file
|
||||
|
||||
Returns:
|
||||
List of Document-Content objects with metadata and base64Encoded flag
|
||||
"""
|
||||
try:
|
||||
mimeType = fileMetadata.get("mimeType", "application/octet-stream")
|
||||
fileName = fileMetadata.get("name", "unknown")
|
||||
|
||||
logger.info(f"Extracting content from file '{fileName}' (MIME type: {mimeType})")
|
||||
|
||||
# Extract content based on MIME type
|
||||
contents = []
|
||||
|
||||
# Text-based formats (excluding CSV which has its own handler)
|
||||
if mimeType == "text/csv":
|
||||
contents.extend(extractCsvContent(fileName, fileContent))
|
||||
|
||||
# Then handle other text-based formats
|
||||
elif mimeType.startswith("text/") or mimeType in [
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/javascript",
|
||||
"application/x-python"
|
||||
]:
|
||||
contents.extend(extractTextContent(fileName, fileContent, mimeType))
|
||||
|
||||
# SVG Files
|
||||
elif mimeType == "image/svg+xml":
|
||||
contents.extend(extractSvgContent(fileName, fileContent))
|
||||
|
||||
# Images
|
||||
elif mimeType.startswith("image/"):
|
||||
contents.extend(extractImageContent(fileName, fileContent, mimeType))
|
||||
|
||||
# PDF Documents
|
||||
elif mimeType == "application/pdf":
|
||||
contents.extend(extractPdfContent(fileName, fileContent))
|
||||
|
||||
# Word Documents
|
||||
elif mimeType in [
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword"
|
||||
]:
|
||||
contents.extend(extractWordContent(fileName, fileContent, mimeType))
|
||||
|
||||
# Excel Documents
|
||||
elif mimeType in [
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel"
|
||||
]:
|
||||
contents.extend(extractExcelContent(fileName, fileContent, mimeType))
|
||||
|
||||
# PowerPoint Documents
|
||||
elif mimeType in [
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.ms-powerpoint"
|
||||
]:
|
||||
contents.extend(extractPowerpointContent(fileName, fileContent, mimeType))
|
||||
|
||||
# Binary data as fallback for unknown formats
|
||||
else:
|
||||
contents.extend(extractBinaryContent(fileName, fileContent, mimeType))
|
||||
|
||||
# Fallback when no content could be extracted
|
||||
if not contents:
|
||||
logger.warning(f"No content extracted from file '{fileName}', using binary fallback")
|
||||
|
||||
# Convert binary content to base64
|
||||
encoded_data = base64.b64encode(fileContent).decode('utf-8')
|
||||
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": '1_undefined',
|
||||
"ext": os.path.splitext(fileName)[1][1:] if os.path.splitext(fileName)[1] else "bin",
|
||||
"contentType": mimeType,
|
||||
"data": encoded_data,
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False
|
||||
}
|
||||
})
|
||||
|
||||
# Add generic attributes for all documents
|
||||
for content in contents:
|
||||
# Make sure all content items have the base64Encoded flag
|
||||
if "base64Encoded" not in content:
|
||||
if isinstance(content.get("data"), bytes):
|
||||
# Convert bytes to base64
|
||||
content["data"] = base64.b64encode(content["data"]).decode('utf-8')
|
||||
content["base64Encoded"] = True
|
||||
else:
|
||||
# Assume text content if not explicitly marked
|
||||
content["base64Encoded"] = False
|
||||
|
||||
# Maintain backward compatibility with old "base64Encoded" flag in metadata
|
||||
if "metadata" not in content:
|
||||
content["metadata"] = {}
|
||||
|
||||
# Set base64Encoded in metadata for backward compatibility
|
||||
content["metadata"]["base64Encoded"] = content["base64Encoded"]
|
||||
|
||||
logger.info(f"Successfully extracted {len(contents)} content items from file '{fileName}'")
|
||||
return contents
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during content extraction: {str(e)}")
|
||||
# Fallback on error - return original data
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": fileMetadata.get("name", "unknown"),
|
||||
"ext": os.path.splitext(fileMetadata.get("name", ""))[1][1:] if os.path.splitext(fileMetadata.get("name", ""))[1] else "bin",
|
||||
"contentType": fileMetadata.get("mimeType", "application/octet-stream"),
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"base64Encoded": True # For backward compatibility
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
def _loadPdfExtractor():
|
||||
"""Loads PDF extraction libraries when needed"""
|
||||
global pdfExtractorLoaded
|
||||
if not pdfExtractorLoaded:
|
||||
try:
|
||||
global PyPDF2, fitz
|
||||
import PyPDF2
|
||||
import fitz # PyMuPDF for more extensive PDF processing
|
||||
pdfExtractorLoaded = True
|
||||
logger.info("PDF extraction libraries successfully loaded")
|
||||
except ImportError as e:
|
||||
logger.warning(f"PDF extraction libraries could not be loaded: {e}")
|
||||
|
||||
def _loadOfficeExtractor():
|
||||
"""Loads Office document extraction libraries when needed"""
|
||||
global officeExtractorLoaded
|
||||
if not officeExtractorLoaded:
|
||||
try:
|
||||
global docx, openpyxl
|
||||
import docx # python-docx for Word documents
|
||||
import openpyxl # for Excel files
|
||||
officeExtractorLoaded = True
|
||||
logger.info("Office extraction libraries successfully loaded")
|
||||
except ImportError as e:
|
||||
logger.warning(f"Office extraction libraries could not be loaded: {e}")
|
||||
|
||||
def _loadImageProcessor():
|
||||
"""Loads image processing libraries when needed"""
|
||||
global imageProcessorLoaded
|
||||
if not imageProcessorLoaded:
|
||||
try:
|
||||
global PIL, Image
|
||||
from PIL import Image
|
||||
imageProcessorLoaded = True
|
||||
logger.info("Image processing libraries successfully loaded")
|
||||
except ImportError as e:
|
||||
logger.warning(f"Image processing libraries could not be loaded: {e}")
|
||||
|
||||
def extractTextContent(fileName: str, fileContent: bytes, mimeType: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts text from text files.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
mimeType: MIME type of the file
|
||||
|
||||
Returns:
|
||||
List of Text-Content objects with base64Encoded = False
|
||||
"""
|
||||
try:
|
||||
# Keep original file extension
|
||||
fileExtension = os.path.splitext(fileName)[1][1:] if os.path.splitext(fileName)[1] else "txt"
|
||||
|
||||
# Extract text content
|
||||
textContent = fileContent.decode('utf-8')
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_text", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": "text/plain",
|
||||
"data": textContent,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True
|
||||
}
|
||||
}]
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"Could not decode text from file '{fileName}' as UTF-8, trying alternative encodings")
|
||||
try:
|
||||
# Try alternative encodings
|
||||
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
|
||||
try:
|
||||
textContent = fileContent.decode(encoding)
|
||||
logger.info(f"Text successfully decoded with encoding {encoding}")
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_text", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": "text/plain",
|
||||
"data": textContent,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"encoding": encoding
|
||||
}
|
||||
}]
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
# Fallback to binary data if no encoding works
|
||||
logger.warning(f"Could not decode text, using binary data")
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False
|
||||
}
|
||||
}]
|
||||
except Exception as e:
|
||||
logger.error(f"Error in alternative text decoding: {str(e)}")
|
||||
# Return binary data as fallback
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False
|
||||
}
|
||||
}]
|
||||
|
||||
def extractCsvContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts content from CSV files.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
|
||||
Returns:
|
||||
List of CSV-Content objects with base64Encoded = False
|
||||
"""
|
||||
try:
|
||||
# Extract text content
|
||||
csvContent = fileContent.decode('utf-8')
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_csv", # Simplified naming
|
||||
"ext": "csv",
|
||||
"contentType": "text/csv",
|
||||
"data": csvContent,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"format": "csv"
|
||||
}
|
||||
}]
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"Could not decode CSV from file '{fileName}' as UTF-8, trying alternative encodings")
|
||||
try:
|
||||
# Try alternative encodings for CSV
|
||||
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
|
||||
try:
|
||||
csvContent = fileContent.decode(encoding)
|
||||
logger.info(f"CSV successfully decoded with encoding {encoding}")
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_csv", # Simplified naming
|
||||
"ext": "csv",
|
||||
"contentType": "text/csv",
|
||||
"data": csvContent,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"encoding": encoding,
|
||||
"format": "csv"
|
||||
}
|
||||
}]
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
# Fallback to binary data
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": "csv",
|
||||
"contentType": "text/csv",
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False
|
||||
}
|
||||
}]
|
||||
except Exception as e:
|
||||
logger.error(f"Error in alternative CSV decoding: {str(e)}")
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": "csv",
|
||||
"contentType": "text/csv",
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False
|
||||
}
|
||||
}]
|
||||
|
||||
def extractSvgContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts content from SVG files.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
|
||||
Returns:
|
||||
List of SVG-Content objects with dual text/image metadata
|
||||
"""
|
||||
contents = []
|
||||
|
||||
try:
|
||||
# Extract SVG as text content (XML)
|
||||
svgText = fileContent.decode('utf-8')
|
||||
|
||||
# Check if it's actually SVG by looking for the SVG tag
|
||||
if "<svg" in svgText.lower():
|
||||
# SVG is both text (XML) and an image
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_svg", # Simplified naming
|
||||
"ext": "svg",
|
||||
"contentType": "image/svg+xml",
|
||||
"data": svgText,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True, # SVG is text-based (XML)
|
||||
"format": "svg",
|
||||
"isImage": True # But also represents an image
|
||||
}
|
||||
})
|
||||
else:
|
||||
# Doesn't appear to be a valid SVG file
|
||||
logger.warning(f"File '{fileName}' has SVG extension but does not contain SVG markup")
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_text",
|
||||
"ext": "svg",
|
||||
"contentType": "text/plain",
|
||||
"data": svgText,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"format": "text"
|
||||
}
|
||||
})
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"Could not decode SVG from file '{fileName}' as UTF-8, trying alternative encodings")
|
||||
try:
|
||||
# Try alternative encodings
|
||||
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
|
||||
try:
|
||||
svgText = fileContent.decode(encoding)
|
||||
if "<svg" in svgText.lower():
|
||||
logger.info(f"SVG successfully decoded with encoding {encoding}")
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_svg", # Simplified naming
|
||||
"ext": "svg",
|
||||
"contentType": "image/svg+xml",
|
||||
"data": svgText,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"format": "svg",
|
||||
"isImage": True,
|
||||
"encoding": encoding
|
||||
}
|
||||
})
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
# Fallback to binary data if no encoding works
|
||||
if not contents:
|
||||
logger.warning(f"Could not decode SVG text, using binary data")
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": "svg",
|
||||
"contentType": "image/svg+xml",
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "svg",
|
||||
"isImage": True
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in alternative SVG decoding: {str(e)}")
|
||||
# Return binary data as fallback
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": "svg",
|
||||
"contentType": "image/svg+xml",
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "svg",
|
||||
"isImage": True
|
||||
}
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def extractImageContent(fileName: str, fileContent: bytes, mimeType: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts content from image files and optionally generates metadata descriptions.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
mimeType: MIME type of the file
|
||||
|
||||
Returns:
|
||||
List of Image-Content objects with base64Encoded = True
|
||||
"""
|
||||
|
||||
# Extract file extension from MIME type or filename
|
||||
fileExtension = mimeType.split('/')[-1]
|
||||
if fileExtension == "jpeg":
|
||||
fileExtension = "jpg"
|
||||
|
||||
# If possible, analyze image and extract metadata
|
||||
imageMetadata = {
|
||||
"isText": False,
|
||||
"format": "image"
|
||||
}
|
||||
imageDescription = None
|
||||
|
||||
try:
|
||||
_loadImageProcessor()
|
||||
if imageProcessorLoaded and fileContent and len(fileContent) > 0:
|
||||
with io.BytesIO(fileContent) as imgStream:
|
||||
try:
|
||||
img = Image.open(imgStream)
|
||||
# Check if the image was actually loaded
|
||||
img.verify()
|
||||
# To safely continue working, reload
|
||||
imgStream.seek(0)
|
||||
img = Image.open(imgStream)
|
||||
imageMetadata.update({
|
||||
"format": img.format,
|
||||
"mode": img.mode,
|
||||
"width": img.width,
|
||||
"height": img.height
|
||||
})
|
||||
# Extract EXIF data if available
|
||||
if hasattr(img, '_getexif') and callable(img._getexif):
|
||||
exif = img._getexif()
|
||||
if exif:
|
||||
exifData = {}
|
||||
for tagId, value in exif.items():
|
||||
exifData[f"tag_{tagId}"] = str(value)
|
||||
imageMetadata["exif"] = exifData
|
||||
|
||||
# Generate image description
|
||||
imageDescription = f"Image ({img.width}x{img.height}, {img.format}, {img.mode})"
|
||||
except Exception as innerE:
|
||||
logger.warning(f"Error processing image: {str(innerE)}")
|
||||
imageMetadata["error"] = str(innerE)
|
||||
imageDescription = f"Image (unable to process: {str(innerE)})"
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not extract image metadata: {str(e)}")
|
||||
imageMetadata["error"] = str(e)
|
||||
|
||||
# Convert binary image to base64
|
||||
encoded_data = base64.b64encode(fileContent).decode('utf-8')
|
||||
|
||||
# Return image content
|
||||
contents = [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_image", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": encoded_data,
|
||||
"base64Encoded": True,
|
||||
"metadata": imageMetadata
|
||||
}]
|
||||
|
||||
# If image description available, add as additional text content
|
||||
if imageDescription:
|
||||
contents.append({
|
||||
"sequenceNr": 2,
|
||||
"name": "2_text_image_info", # Simplified naming with label
|
||||
"ext": "txt",
|
||||
"contentType": "text/plain",
|
||||
"data": imageDescription,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"imageDescription": True
|
||||
}
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def extractPdfContent(fileName: str, fileContent: bytes) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts text and images from PDF files.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
|
||||
Returns:
|
||||
List of PDF-Content objects (text and images) with appropriate base64Encoded flags
|
||||
"""
|
||||
contents = []
|
||||
extractedContentFound = False
|
||||
|
||||
try:
|
||||
# Load PDF extraction libraries
|
||||
_loadPdfExtractor()
|
||||
if not pdfExtractorLoaded:
|
||||
logger.warning("PDF extraction not possible: Libraries not available")
|
||||
# Add original file as binary content
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_pdf", # Simplified naming
|
||||
"ext": "pdf",
|
||||
"contentType": "application/pdf",
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "pdf"
|
||||
}
|
||||
})
|
||||
return contents
|
||||
|
||||
# Extract text with PyPDF2
|
||||
extractedText = ""
|
||||
pdfMetadata = {}
|
||||
with io.BytesIO(fileContent) as pdfStream:
|
||||
pdfReader = PyPDF2.PdfReader(pdfStream)
|
||||
|
||||
# Extract metadata
|
||||
pdfInfo = pdfReader.metadata or {}
|
||||
for key, value in pdfInfo.items():
|
||||
if key.startswith('/'):
|
||||
pdfMetadata[key[1:]] = value
|
||||
else:
|
||||
pdfMetadata[key] = value
|
||||
|
||||
# Extract text from all pages
|
||||
for pageNum in range(len(pdfReader.pages)):
|
||||
page = pdfReader.pages[pageNum]
|
||||
pageText = page.extract_text()
|
||||
if pageText:
|
||||
extractedText += f"--- Page {pageNum + 1} ---\n{pageText}\n\n"
|
||||
|
||||
# If text was found, add as separate content
|
||||
if extractedText.strip():
|
||||
extractedContentFound = True
|
||||
contents.append({
|
||||
"sequenceNr": len(contents) + 1,
|
||||
"name": f"{len(contents) + 1}_text", # Simplified naming
|
||||
"ext": "txt",
|
||||
"contentType": "text/plain",
|
||||
"data": extractedText,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"source": "pdf",
|
||||
"pages": len(pdfReader.pages),
|
||||
"pdfMetadata": pdfMetadata
|
||||
}
|
||||
})
|
||||
|
||||
# Extract images with PyMuPDF (fitz)
|
||||
try:
|
||||
with io.BytesIO(fileContent) as pdfStream:
|
||||
doc = fitz.open(stream=pdfStream, filetype="pdf")
|
||||
imageCount = 0
|
||||
|
||||
for pageNum in range(len(doc)):
|
||||
page = doc[pageNum]
|
||||
imageList = page.get_images(full=True)
|
||||
|
||||
for imgIndex, imgInfo in enumerate(imageList):
|
||||
try:
|
||||
imageCount += 1
|
||||
xref = imgInfo[0]
|
||||
baseImage = doc.extract_image(xref)
|
||||
imageBytes = baseImage["image"]
|
||||
imageExt = baseImage["ext"]
|
||||
|
||||
# Add image as content - encode as base64
|
||||
extractedContentFound = True
|
||||
contents.append({
|
||||
"sequenceNr": len(contents) + 1,
|
||||
"name": f"{len(contents) + 1}_image_page{pageNum+1}_{imgIndex+1}", # Simplified naming with label
|
||||
"ext": imageExt,
|
||||
"contentType": f"image/{imageExt}",
|
||||
"data": base64.b64encode(imageBytes).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"source": "pdf",
|
||||
"page": pageNum + 1,
|
||||
"index": imgIndex
|
||||
}
|
||||
})
|
||||
except Exception as imgE:
|
||||
logger.warning(f"Error extracting image {imgIndex} on page {pageNum + 1}: {str(imgE)}")
|
||||
|
||||
# Close document
|
||||
doc.close()
|
||||
|
||||
except Exception as imgExtractE:
|
||||
logger.warning(f"Error extracting images from PDF: {str(imgExtractE)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in PDF extraction: {str(e)}")
|
||||
|
||||
# If no content was extracted, add the original PDF
|
||||
if not extractedContentFound:
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_pdf", # Simplified naming
|
||||
"ext": "pdf",
|
||||
"contentType": "application/pdf",
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "pdf"
|
||||
}
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def extractWordContent(fileName: str, fileContent: bytes, mimeType: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts text and images from Word documents.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
mimeType: MIME type of the file
|
||||
|
||||
Returns:
|
||||
List of Word-Content objects (text and possibly images) with appropriate base64Encoded flags
|
||||
"""
|
||||
contents = []
|
||||
extractedContentFound = False
|
||||
|
||||
# Determine file extension
|
||||
fileExtension = "docx" if mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" else "doc"
|
||||
|
||||
try:
|
||||
# Load Office extraction libraries
|
||||
_loadOfficeExtractor()
|
||||
if not officeExtractorLoaded:
|
||||
logger.warning("Word extraction not possible: Libraries not available")
|
||||
# Add original file as binary content
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_word", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "word"
|
||||
}
|
||||
})
|
||||
return contents
|
||||
|
||||
# Only supports DOCX (newer format)
|
||||
if mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
with io.BytesIO(fileContent) as docxStream:
|
||||
doc = docx.Document(docxStream)
|
||||
|
||||
# Extract text
|
||||
fullText = []
|
||||
for para in doc.paragraphs:
|
||||
fullText.append(para.text)
|
||||
|
||||
# Extract tables
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
rowText = []
|
||||
for cell in row.cells:
|
||||
rowText.append(cell.text)
|
||||
fullText.append(" | ".join(rowText))
|
||||
|
||||
extractedText = "\n\n".join(fullText)
|
||||
|
||||
# Add extracted text as content
|
||||
if extractedText.strip():
|
||||
extractedContentFound = True
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_text", # Simplified naming
|
||||
"ext": "txt",
|
||||
"contentType": "text/plain",
|
||||
"data": extractedText,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"source": "docx",
|
||||
"paragraphCount": len(doc.paragraphs),
|
||||
"tableCount": len(doc.tables)
|
||||
}
|
||||
})
|
||||
else:
|
||||
logger.warning(f"Extraction from old Word format (DOC) not supported")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Word extraction: {str(e)}")
|
||||
|
||||
# If no content was extracted, add the original document
|
||||
if not extractedContentFound:
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_word", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "word"
|
||||
}
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def extractExcelContent(fileName: str, fileContent: bytes, mimeType: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts table data from Excel files.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
mimeType: MIME type of the file
|
||||
|
||||
Returns:
|
||||
List of Excel-Content objects with appropriate base64Encoded flags
|
||||
"""
|
||||
contents = []
|
||||
extractedContentFound = False
|
||||
|
||||
# Determine file extension
|
||||
fileExtension = "xlsx" if mimeType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" else "xls"
|
||||
|
||||
try:
|
||||
# Load Office extraction libraries
|
||||
_loadOfficeExtractor()
|
||||
if not officeExtractorLoaded:
|
||||
logger.warning("Excel extraction not possible: Libraries not available")
|
||||
# Add original file as binary content
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_excel", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "excel"
|
||||
}
|
||||
})
|
||||
return contents
|
||||
|
||||
# Only supports XLSX (newer format)
|
||||
if mimeType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
||||
with io.BytesIO(fileContent) as xlsxStream:
|
||||
workbook = openpyxl.load_workbook(xlsxStream, data_only=True)
|
||||
|
||||
# Extract each worksheet as separate CSV content
|
||||
for sheetIndex, sheetName in enumerate(workbook.sheetnames):
|
||||
sheet = workbook[sheetName]
|
||||
|
||||
# Format data as CSV
|
||||
csvRows = []
|
||||
for row in sheet.iter_rows():
|
||||
csvRow = []
|
||||
for cell in row:
|
||||
value = cell.value
|
||||
if value is None:
|
||||
csvRow.append("")
|
||||
else:
|
||||
csvRow.append(str(value).replace('"', '""'))
|
||||
csvRows.append(','.join(f'"{cell}"' for cell in csvRow))
|
||||
|
||||
csvContent = "\n".join(csvRows)
|
||||
|
||||
# Add as CSV content
|
||||
if csvContent.strip():
|
||||
extractedContentFound = True
|
||||
sheetSafeName = sheetName.replace(" ", "_").replace("/", "_").replace("\\", "_")
|
||||
contents.append({
|
||||
"sequenceNr": len(contents) + 1,
|
||||
"name": f"{len(contents) + 1}_csv_{sheetSafeName}", # Simplified naming with sheet label
|
||||
"ext": "csv",
|
||||
"contentType": "text/csv",
|
||||
"data": csvContent,
|
||||
"base64Encoded": False,
|
||||
"metadata": {
|
||||
"isText": True,
|
||||
"source": "xlsx",
|
||||
"sheet": sheetName,
|
||||
"format": "csv"
|
||||
}
|
||||
})
|
||||
else:
|
||||
logger.warning(f"Extraction from old Excel format (XLS) not supported")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Excel extraction: {str(e)}")
|
||||
|
||||
# If no content was extracted, add the original document
|
||||
if not extractedContentFound:
|
||||
contents.append({
|
||||
"sequenceNr": 1,
|
||||
"name": "1_excel", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "excel"
|
||||
}
|
||||
})
|
||||
|
||||
return contents
|
||||
|
||||
def extractPowerpointContent(fileName: str, fileContent: bytes, mimeType: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts content from PowerPoint presentations.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
mimeType: MIME type of the file
|
||||
|
||||
Returns:
|
||||
List of PowerPoint-Content objects with base64Encoded = True
|
||||
"""
|
||||
# For PowerPoint, we currently only return the original binary file
|
||||
# A complete extraction would require more specialized libraries
|
||||
fileExtension = "pptx" if mimeType == "application/vnd.openxmlformats-officedocument.presentationml.presentation" else "ppt"
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_powerpoint", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "powerpoint"
|
||||
}
|
||||
}]
|
||||
|
||||
def extractBinaryContent(fileName: str, fileContent: bytes, mimeType: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fallback for binary files where no specific extraction is possible.
|
||||
|
||||
Args:
|
||||
fileName: Name of the file
|
||||
fileContent: Binary data of the file
|
||||
mimeType: MIME type of the file
|
||||
|
||||
Returns:
|
||||
List with a binary Content object with base64Encoded = True
|
||||
"""
|
||||
fileExtension = os.path.splitext(fileName)[1][1:] if os.path.splitext(fileName)[1] else "bin"
|
||||
return [{
|
||||
"sequenceNr": 1,
|
||||
"name": "1_binary", # Simplified naming
|
||||
"ext": fileExtension,
|
||||
"contentType": mimeType,
|
||||
"data": base64.b64encode(fileContent).decode('utf-8'),
|
||||
"base64Encoded": True,
|
||||
"metadata": {
|
||||
"isText": False,
|
||||
"format": "binary"
|
||||
}
|
||||
}]
|
||||
123
static/24_defAttributes.py
Normal file
123
static/24_defAttributes.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# Define the model for attribute definitions
|
||||
class AttributeDefinition(BaseModel):
|
||||
name: str
|
||||
label: str
|
||||
type: str
|
||||
required: bool = False
|
||||
placeholder: Optional[str] = None
|
||||
defaultValue: Optional[Any] = None
|
||||
options: Optional[List[Dict[str, Any]]] = None
|
||||
editable: bool = True
|
||||
visible: bool = True
|
||||
order: int = 0
|
||||
validation: Optional[Dict[str, Any]] = None
|
||||
helpText: Optional[str] = None
|
||||
|
||||
# Helper classes for type mapping
|
||||
typeMappings = {
|
||||
"int": "number",
|
||||
"str": "string",
|
||||
"float": "number",
|
||||
"bool": "boolean",
|
||||
"List[int]": "array",
|
||||
"List[str]": "array",
|
||||
"Dict[str, Any]": "object",
|
||||
"Optional[str]": "string",
|
||||
"Optional[int]": "number",
|
||||
"Optional[Dict[str, Any]]": "object"
|
||||
}
|
||||
|
||||
# Special field types based on naming conventions
|
||||
specialFieldTypes = {
|
||||
"content": "textarea",
|
||||
"description": "textarea",
|
||||
"instructions": "textarea",
|
||||
"password": "password",
|
||||
"email": "email",
|
||||
"workspaceId": "select",
|
||||
"agentId": "select",
|
||||
"type": "select"
|
||||
}
|
||||
|
||||
# Function to convert a Pydantic model into attribute definitions
|
||||
def getModelAttributes(modelClass, userLanguage="de"):
|
||||
"""
|
||||
Converts a Pydantic model into a list of AttributeDefinition objects
|
||||
"""
|
||||
attributes = []
|
||||
|
||||
# Go through all fields in the model
|
||||
for i, (fieldName, field) in enumerate(modelClass.__fields__.items()):
|
||||
# Skip internal fields
|
||||
if fieldName.startswith('_') or fieldName in ["label", "fieldLabels"]:
|
||||
continue
|
||||
|
||||
# Determine the field type
|
||||
fieldType = typeMappings.get(str(field.type_), "string")
|
||||
|
||||
# Check for special field types
|
||||
if fieldName in specialFieldTypes:
|
||||
fieldType = specialFieldTypes[fieldName]
|
||||
|
||||
# Get the label (if available)
|
||||
fieldLabel = fieldName.replace('_', ' ').capitalize()
|
||||
if hasattr(modelClass, 'fieldLabels') and fieldName in modelClass.fieldLabels:
|
||||
labelObj = modelClass.fieldLabels[fieldName]
|
||||
fieldLabel = labelObj.getLabel(userLanguage)
|
||||
|
||||
# Determine default values and required status
|
||||
required = field.required
|
||||
defaultValue = field.default if not field.required else None
|
||||
|
||||
# Check for validation rules
|
||||
validation = None
|
||||
if field.validators:
|
||||
validation = {"hasValidators": True}
|
||||
|
||||
# Placeholder text
|
||||
placeholder = f"Please enter {fieldLabel}"
|
||||
|
||||
# Special options for Select fields
|
||||
options = None
|
||||
if fieldType == "select":
|
||||
if fieldName == "type" and modelClass.__name__ == "Agent":
|
||||
options = [
|
||||
{"value": "Analysis", "label": "Analysis"},
|
||||
{"value": "Transformation", "label": "Transformation"},
|
||||
{"value": "Generation", "label": "Generation"},
|
||||
{"value": "Classification", "label": "Classification"},
|
||||
{"value": "Custom", "label": "Custom"}
|
||||
]
|
||||
|
||||
# Extract description from Field object
|
||||
description = None
|
||||
# Try to get description from various possible sources
|
||||
if hasattr(field, 'field_info') and hasattr(field.field_info, 'description'):
|
||||
description = field.field_info.description
|
||||
elif hasattr(field, 'description'):
|
||||
description = field.description
|
||||
elif hasattr(field, 'schema') and hasattr(field.schema, 'description'):
|
||||
description = field.schema.description
|
||||
|
||||
# Create attribute definition
|
||||
attrDef = AttributeDefinition(
|
||||
name=fieldName,
|
||||
label=fieldLabel,
|
||||
type=fieldType,
|
||||
required=required,
|
||||
placeholder=placeholder,
|
||||
defaultValue=defaultValue,
|
||||
options=options,
|
||||
editable=fieldName not in ["id", "mandateId", "userId", "createdAt", "uploadDate"],
|
||||
visible=fieldName not in ["hashedPassword", "mandateId", "userId"],
|
||||
order=i,
|
||||
validation=validation,
|
||||
helpText=description or "" # Set empty string as default value if no description found
|
||||
)
|
||||
|
||||
attributes.append(attrDef)
|
||||
|
||||
return attributes
|
||||
42
static/25_email_preview.html
Normal file
42
static/25_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Attached: documentProcessor.py and defAttributes.py</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>recipient@example.com</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Attached: documentProcessor.py and defAttributes.py</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<html><body><p>Sehr geehrte Damen und Herren,</p><p>anbei finden Sie die angeforderten Dokumente <strong>documentProcessor.py</strong> und <strong>defAttributes.py</strong>. Bitte zögern Sie nicht, sich bei Fragen oder weiteren Anliegen an uns zu wenden.</p><p>Mit freundlichen Grüßen,<br>Ihr Team</p></body></html>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/26_email_template.json
Normal file
6
static/26_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "recipient@example.com",
|
||||
"subject": "Attached: documentProcessor.py and defAttributes.py",
|
||||
"plainBody": "Sehr geehrte Damen und Herren,\n\nanbei finden Sie die angeforderten Dokumente 'documentProcessor.py' und 'defAttributes.py'. Bitte z\u00f6gern Sie nicht, sich bei Fragen oder weiteren Anliegen an uns zu wenden.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\nIhr Team",
|
||||
"htmlBody": "<html><body><p>Sehr geehrte Damen und Herren,</p><p>anbei finden Sie die angeforderten Dokumente <strong>documentProcessor.py</strong> und <strong>defAttributes.py</strong>. Bitte z\u00f6gern Sie nicht, sich bei Fragen oder weiteren Anliegen an uns zu wenden.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>Ihr Team</p></body></html>"
|
||||
}
|
||||
42
static/27_email_preview.html
Normal file
42
static/27_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Angehängt: documentProcessor.py und defAttributes.py</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>team@example.com</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Angehängt: documentProcessor.py und defAttributes.py</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<!DOCTYPE html><html><head><meta charset="UTF-8"><style>body { background-color: #f4f4f4; font-family: Arial, sans-serif; } .email-container { background-color: #ffffff; border: 1px solid #dddddd; border-radius: 5px; margin: 20px auto; padding: 20px; max-width: 600px; } .email-header { background-color: #eeeeee; padding: 10px; text-align: center; font-weight: bold; } .email-content { margin: 20px 0; } .email-footer { font-size: 12px; color: #888888; text-align: center; }</style></head><body><div class="email-container"><div class="email-header">E-Mail-Vorschau</div><div class="email-content"><p>Liebe Teammitglieder,</p><p>im Anhang finden Sie die Dateien <strong>documentProcessor.py</strong> und <strong>defAttributes.py</strong>. Bitte überprüfen Sie diese und geben Sie mir Ihr Feedback.</p><p>Mit freundlichen Grüßen,<br>[Ihr Name]</p></div><div class="email-footer">Dies ist eine Vorschau der E-Mail und kann in verschiedenen E-Mail-Clients unterschiedlich angezeigt werden.</div></div></body></html>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/28_email_template.json
Normal file
6
static/28_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "team@example.com",
|
||||
"subject": "Angeh\u00e4ngt: documentProcessor.py und defAttributes.py",
|
||||
"plainBody": "Liebe Teammitglieder,\n\nim Anhang finden Sie die Dateien documentProcessor.py und defAttributes.py. Bitte \u00fcberpr\u00fcfen Sie diese und geben Sie mir Ihr Feedback.\n\nMit freundlichen Gr\u00fc\u00dfen,\n[Ihr Name]",
|
||||
"htmlBody": "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><style>body { background-color: #f4f4f4; font-family: Arial, sans-serif; } .email-container { background-color: #ffffff; border: 1px solid #dddddd; border-radius: 5px; margin: 20px auto; padding: 20px; max-width: 600px; } .email-header { background-color: #eeeeee; padding: 10px; text-align: center; font-weight: bold; } .email-content { margin: 20px 0; } .email-footer { font-size: 12px; color: #888888; text-align: center; }</style></head><body><div class=\"email-container\"><div class=\"email-header\">E-Mail-Vorschau</div><div class=\"email-content\"><p>Liebe Teammitglieder,</p><p>im Anhang finden Sie die Dateien <strong>documentProcessor.py</strong> und <strong>defAttributes.py</strong>. Bitte \u00fcberpr\u00fcfen Sie diese und geben Sie mir Ihr Feedback.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>[Ihr Name]</p></div><div class=\"email-footer\">Dies ist eine Vorschau der E-Mail und kann in verschiedenen E-Mail-Clients unterschiedlich angezeigt werden.</div></div></body></html>"
|
||||
}
|
||||
42
static/6_email_preview.html
Normal file
42
static/6_email_preview.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Verspätete Ankunft morgen</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>i.dittrich@valueon.ch</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Verspätete Ankunft morgen</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<p>Hallo Ida,</p><p>ich wollte dich nur kurz informieren, dass ich morgen etwas später ankommen werde. Ich hoffe, das ist in Ordnung.</p><p>Bis dann!</p><p>Viele Grüße</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/7_email_template.json
Normal file
6
static/7_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "i.dittrich@valueon.ch",
|
||||
"subject": "Versp\u00e4tete Ankunft morgen",
|
||||
"plainBody": "Hallo Ida,\n\nich wollte dich nur kurz informieren, dass ich morgen etwas sp\u00e4ter ankommen werde. Ich hoffe, das ist in Ordnung.\n\nBis dann!\n\nViele Gr\u00fc\u00dfe",
|
||||
"htmlBody": "<p>Hallo Ida,</p><p>ich wollte dich nur kurz informieren, dass ich morgen etwas sp\u00e4ter ankommen werde. Ich hoffe, das ist in Ordnung.</p><p>Bis dann!</p><p>Viele Gr\u00fc\u00dfe</p>"
|
||||
}
|
||||
74
static/8_email_preview.html
Normal file
74
static/8_email_preview.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Verspätete Ankunft morgen</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
|
||||
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field-label { font-weight: bold; color: #555; }
|
||||
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h2>Email Template Preview</h2>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<div class="field">
|
||||
<div class="field-label">To:</div>
|
||||
<div>i.dittrich@valueon.ch</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Subject:</div>
|
||||
<div>Verspätete Ankunft morgen</div>
|
||||
</div>
|
||||
<div class="email-body">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Preview: Verspätete Ankunft morgen</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4; }
|
||||
.email-container { max-width: 600px; margin: 20px auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
|
||||
.email-header { background-color: #007BFF; color: #ffffff; padding: 10px; text-align: center; }
|
||||
.email-content { padding: 20px; }
|
||||
.email-footer { font-size: 12px; color: #777777; text-align: center; padding: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>Email Template Preview</h1>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<p><strong>To:</strong> i.dittrich@valueon.ch</p>
|
||||
<p><strong>Subject:</strong> Verspätete Ankunft morgen</p>
|
||||
<div class="email-body">
|
||||
<p>Hallo Ida,</p>
|
||||
<p>ich wollte dich nur kurz informieren, dass ich morgen etwas später ankommen werde. Ich hoffe, das ist in Ordnung.</p>
|
||||
<p>Bis dann!</p>
|
||||
<p>Viele Grüße</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>Dies ist eine Vorschau des E-Mail-Templates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-footer">
|
||||
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
static/9_email_template.json
Normal file
6
static/9_email_template.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recipient": "i.dittrich@valueon.ch",
|
||||
"subject": "Versp\u00e4tete Ankunft morgen",
|
||||
"plainBody": "Hallo Ida,\n\nich wollte dich nur kurz informieren, dass ich morgen etwas sp\u00e4ter ankommen werde. Ich hoffe, das ist in Ordnung.\n\nBis dann!\n\nViele Gr\u00fc\u00dfe",
|
||||
"htmlBody": "<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Email Preview: Versp\u00e4tete Ankunft morgen</title>\n<style>\n body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4; }\n .email-container { max-width: 600px; margin: 20px auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }\n .email-header { background-color: #007BFF; color: #ffffff; padding: 10px; text-align: center; }\n .email-content { padding: 20px; }\n .email-footer { font-size: 12px; color: #777777; text-align: center; padding: 10px; }\n</style>\n</head>\n<body>\n<div class=\"email-container\">\n <div class=\"email-header\">\n <h1>Email Template Preview</h1>\n </div>\n <div class=\"email-content\">\n <p><strong>To:</strong> i.dittrich@valueon.ch</p>\n <p><strong>Subject:</strong> Versp\u00e4tete Ankunft morgen</p>\n <div class=\"email-body\">\n <p>Hallo Ida,</p>\n <p>ich wollte dich nur kurz informieren, dass ich morgen etwas sp\u00e4ter ankommen werde. Ich hoffe, das ist in Ordnung.</p>\n <p>Bis dann!</p>\n <p>Viele Gr\u00fc\u00dfe</p>\n </div>\n </div>\n <div class=\"email-footer\">\n <p>Dies ist eine Vorschau des E-Mail-Templates.</p>\n </div>\n</div>\n</body>\n</html>"
|
||||
}
|
||||
1
token_storage/7d08aab9-a170-4975-8898-bc7e0a95488e.json
Normal file
1
token_storage/7d08aab9-a170-4975-8898-bc7e0a95488e.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"access_token": "eyJ0eXAiOiJKV1QiLCJub25jZSI6ImMyMkF0TDR3NzdGSEluczFRVlFKSFFfMWEzN1I1WmtZZFJ5NmhLNElaY00iLCJhbGciOiJSUzI1NiIsIng1dCI6IkNOdjBPSTNSd3FsSEZFVm5hb01Bc2hDSDJYRSIsImtpZCI6IkNOdjBPSTNSd3FsSEZFVm5hb01Bc2hDSDJYRSJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC82YTUxYWFlYi0yNDY3LTQxODYtOTUwNC0yYTA1YWVkYzU5MWYvIiwiaWF0IjoxNzQ2NTc0ODAwLCJuYmYiOjE3NDY1NzQ4MDAsImV4cCI6MTc0NjU4MDE0MSwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsicDEiXSwiYWlvIjoiQWFRQVcvOFpBQUFBM3ZjTDcyMktKd0lmbk4yQnFxMW5lQWVlSjMwMlA2VEg3QVFPTzJKRG85UU5UdlFqaFRZTEd6QURWb0VOSkN4OStZSHZNUjBGUWxPUlN0QWZLUVFBVzJacFJaME02YlhidS9rb0d2cFIyKy92OGhQOTZySnl3b3ZzZlFTZzh3c2MrSndNa0NZQ2djU3VnWkRmcUxNMS9ZYlpyVnFPQUNzeUpQdTJpTFlwbWpOZStKNXhQSUc4eE1KdlZlbStRbFkrS0RrNW5DcSs5R2t2eU1zVEk4d2duQT09IiwiYW1yIjpbInB3ZCIsInJzYSIsIm1mYSJdLCJhcHBfZGlzcGxheW5hbWUiOiJQTSBUZXN0IC0gRW1haWwgRHJhZnQiLCJhcHBpZCI6ImM3ZTcxMTJkLTYxZGMtNGYzYS04Y2QzLTA4Y2M0Y2Q3NTA0YyIsImFwcGlkYWNyIjoiMSIsImRldmljZWlkIjoiOWE0YTM2OWEtNjBhOS00NjdlLWFjNTktODdkZGQyMDUxZGU5IiwiZmFtaWx5X25hbWUiOiJNb3RzY2giLCJnaXZlbl9uYW1lIjoiUGF0cmljayIsImlkdHlwIjoidXNlciIsImlwYWRkciI6IjE3OC4xOTcuMjIyLjE0OCIsIm5hbWUiOiJQYXRyaWNrIE1vdHNjaCIsIm9pZCI6IjdkMDhhYWI5LWExNzAtNDk3NS04ODk4LWJjN2UwYTk1NDg4ZSIsInBsYXRmIjoiMyIsInB1aWQiOiIxMDAzN0ZGRThDREQ2QTgyIiwicmgiOiIxLkFRc0E2NnBSYW1ja2hrR1ZCQ29GcnR4Wkh3TUFBQUFBQUFBQXdBQUFBQUFBQUFDRUFEQUxBQS4iLCJzY3AiOiJNYWlsLlJlYWRXcml0ZSBvcGVuaWQgcHJvZmlsZSBVc2VyLlJlYWQgZW1haWwiLCJzaWQiOiIyOTI0ZTgxMS0xMTM1LTQ0ZTItOGUxYi1kMmU2YmVhZmI3ZTUiLCJzaWduaW5fc3RhdGUiOlsia21zaSJdLCJzdWIiOiJJZzBpcDN4YWRiTGl1S3piRmd3VmhOSU1fRHpHMHdweGlFRmIySll1Y240IiwidGVuYW50X3JlZ2lvbl9zY29wZSI6IkVVIiwidGlkIjoiNmE1MWFhZWItMjQ2Ny00MTg2LTk1MDQtMmEwNWFlZGM1OTFmIiwidW5pcXVlX25hbWUiOiJwLm1vdHNjaEB2YWx1ZW9uLmNoIiwidXBuIjoicC5tb3RzY2hAdmFsdWVvbi5jaCIsInV0aSI6IndScENlYUtKNUVtWXdDN0xUbTJIQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjE1OGMwNDdhLWM5MDctNDU1Ni1iN2VmLTQ0NjU1MWE2YjVmNyIsIjliODk1ZDkyLTJjZDMtNDRjNy05ZDAyLWE2YWMyZDVlYTVjMyIsImNmMWMzOGU1LTM2MjEtNDAwNC1hN2NiLTg3OTYyNGRjZWQ3YyIsIjlmMDYyMDRkLTczYzEtNGQ0Yy04ODBhLTZlZGI5MDYwNmZkOCIsIjg5MmM1ODQyLWE5YTYtNDYzYS04MDQxLTcyYWEwOGNhM2NmNiIsImI3OWZiZjRkLTNlZjktNDY4OS04MTQzLTc2YjE5NGU4NTUwOSJdLCJ4bXNfZnRkIjoiSUhCXzdUNG9NLUpoejdrTnNjOGhNVnpPazIyZnV3cmdCYkRqZnlKb2xhY0JjM2RsWkdWdVl5MWtjMjF6IiwieG1zX2lkcmVsIjoiMjggMSIsInhtc19zdCI6eyJzdWIiOiJSMnZEMEcxbW1hWVJDN0pZV2NJU1pXMktEUGdOQmpCTEZsNmVMQUJfUFVNIn0sInhtc190Y2R0IjoxNDE4MjE0NTAxLCJ4bXNfdGRiciI6IkVVIn0.ESBS6AJiQaHa7xd59iZDBPvg66EJYEvrxLibqv8WM5edqNN0BMk3G7OFeDdivgf5BWCoRnDVsUII5S1Rb7eo-5nmWGC3xDq4uLzZ-ilOv4K2xErUUjU5x_tcpN67UtskBkb0LdwrTMzcmlc43iwkLGFlhKAELg07LuyGjxdjTN0izVc02eQL2-Z0mQVNI9ipUKeU40whmvDlI66nwnZM8lCRb7CU1g8_tgaQGqTmDrGbILz3zSNdyxPkzVag7g4c6nfs15bGZr3Vu_ouaz9zB3cdIFQ5vhI8Gb0IqOHZQvtmQ5zSDZ62Z0c2GBDEy7Zq0GZCxuBvmJjHPd3EoT9yXw", "refresh_token": "1.AQsA66pRamckhkGVBCoFrtxZHy0R58fcYTpPjNMIzEzXUEyEADALAA.AgABAwEAAABVrSpeuWamRam2jAF1XRQEAwDs_wUA9P8nMmZ5--kLI0YXEAYqwORMNt9GxWnIx_Y4RwBXPDOzc1RtmnjOmrpqlhoL1QYzpyfe2OKizkyx7z5q-NSA3gweH-kgKALAh0iVpZi6e3IqqW8igDjAtIwVMHS5HuV7-7WMQVILYJ9LfIap-CXvAIyDd0-3DkbjMc7T1V9xPKFIyDQN6TPNuojeW_oKZNd_EsBr2vyZjWjgmTVzb1sZwmNQFao0ZOcDpW5H6H2HXxL_pB6wk0K8ppBYNLj1bC_g1Si3htREBWC7W7UfngqENdaAkfBhH5UKoS73aarJZ9eZv-wGgAD-aDLubx2C5wRaLXWPpENMWK8st3Or9tWVfBDrFgU2JZ_UCnfd8gPKcpi-_qepFcb3FncdAsMpCQQLvXs7O4qSlBlD8QE6m2JrbrjtC5U14ZPoPekUwnd9V-m5wWCIpxBw1sMy04BxSx2xg9EziZ-_VjoIKEvB6m3A8uWXUDIYeki9QeNKl47es6wIEdUV1hmf8MSjtWfUK_azwvkIMWYSNiJ42hN74jBxxYfa7bDxajpTiu8iV75zuQv9kqR9mS2-lTvtH1Y9_qYtYQDQ4ldB0GaRx62-SasO9IUl_ugpyljA4lue7cblaGf-yrU3wjdbylGUr7t6XN3v4_yXs2_yQ5knfWsnUulxfvUfWMFzFwtYONI8eAiOmra1nYjnDAxydUyPJrquwvPge7T4Jlo8o2BvQJnwSy9NZTuD_1RZMu54LAw67FCe5HKKNDaZHtCrB_B1Qq0oEgLY9CiXF5RQj-78UUfBPLrhpFBsROU5Q9Q77tRTz36zI3WK6sZ4nTukQsPSILIBrG_6W7BTKl3l4vykNAT4u159QWIxONKU_1-E2XEZ3r9xc2ik1TFq-bX0-FhFtKtJvzqD-w239KUwn6Pc3vjk7OHZ7WRK3pABC_AubvNep3h8hbnaefNaTaNhGy2aossM8jYo4oy_tS6p1yhuKzM4et4ZBQJaWZBSZ-TYVFK9r7f6jMF3gaeD2RKBq5ZGGCaLOJLnCT1bSn0ThFRS5xlKMvLxH8qddwU4", "user_info": {"name": "Patrick Motsch", "email": "p.motsch@valueon.ch"}, "timestamp": "2025-05-07T01:45:00.286453"}
|
||||
Loading…
Reference in a new issue