base for integration test

This commit is contained in:
ValueOn AG 2025-05-28 01:51:10 +02:00
parent fe27f51ebb
commit ecf23255d2
32 changed files with 1095 additions and 976 deletions

23
app.py
View file

@ -13,19 +13,20 @@ import pathlib
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
def initLogging(): def initLogging():
"""Initialize logging with configuration from APP_CONFIG"""
# Get log level from config (default to INFO if not found) # Get log level from config (default to INFO if not found)
logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING") logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING")
logLevel = getattr(logging, logLevelName) logLevel = getattr(logging, logLevelName)
# Create formatters # Create formatters - using single line format
consoleFormatter = logging.Formatter( consoleFormatter = logging.Formatter(
fmt=APP_CONFIG.get("APP_LOGGING_FORMAT", "%(asctime)s - %(levelname)s - %(name)s - %(message)s"), fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S") datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
) )
# File formatter with more detailed error information # File formatter with more detailed error information but still single line
fileFormatter = logging.Formatter( fileFormatter = logging.Formatter(
fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s - %(pathname)s:%(lineno)d\n%(funcName)s\n%(exc_info)s", fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s - %(pathname)s:%(lineno)d - %(funcName)s",
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S") datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
) )
@ -66,17 +67,22 @@ def initLogging():
# Configure the root logger # Configure the root logger
logging.basicConfig( logging.basicConfig(
level=logLevel, level=logLevel,
format=APP_CONFIG.get("APP_LOGGING_FORMAT", "%(asctime)s - %(levelname)s - %(name)s - %(message)s"), format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S"), datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S"),
handlers=handlers handlers=handlers,
force=True # Force reconfiguration of the root logger
) )
# Silence noisy third-party libraries - use the same level as the root logger # Silence noisy third-party libraries - use the same level as the root logger
noisyLoggers = ["httpx", "urllib3", "asyncio", "fastapi.security.oauth2"] noisyLoggers = ["httpx", "urllib3", "asyncio", "fastapi.security.oauth2"]
for loggerName in noisyLoggers: for loggerName in noisyLoggers:
logging.getLogger(loggerName).setLevel(logLevel) logging.getLogger(loggerName).setLevel(logLevel)
# Log the current logging configuration
logger = logging.getLogger(__name__)
logger.info(f"Logging initialized with level {logLevelName}")
logger.info(f"Log file: {logFile if APP_CONFIG.get('APP_LOGGING_FILE_ENABLED', True) else 'disabled'}")
logger.info(f"Console logging: {'enabled' if APP_CONFIG.get('APP_LOGGING_CONSOLE_ENABLED', True) else 'disabled'}")
# Initialize logging # Initialize logging
initLogging() initLogging()
@ -143,6 +149,9 @@ app.include_router(promptRouter)
from modules.routes.routeWorkflows import router as workflowRouter from modules.routes.routeWorkflows import router as workflowRouter
app.include_router(workflowRouter) app.include_router(workflowRouter)
from modules.routes.routeSecurityLocal import router as localRouter
app.include_router(localRouter)
from modules.routes.routeSecurityMsft import router as msftRouter from modules.routes.routeSecurityMsft import router as msftRouter
app.include_router(msftRouter) app.include_router(msftRouter)

View file

@ -8,7 +8,7 @@ APP_API_URL = http://localhost:8000
# Database Configuration for Application # Database Configuration for Application
DB_APP_HOST=D:/Temp/_powerondb DB_APP_HOST=D:/Temp/_powerondb
DB_APP_DATABASE=app DB_APP_DATABASE=app
DB_APPY_USER=dev_user DB_APP_USER=dev_user
DB_APP_PASSWORD_SECRET=dev_password DB_APP_PASSWORD_SECRET=dev_password
# Database Configuration Chat # Database Configuration Chat

View file

@ -8,7 +8,7 @@ APP_API_URL = https://gateway.poweron-center.net
# Database Configuration Application # Database Configuration Application
DB_APP_HOST=/home/_powerondb DB_APP_HOST=/home/_powerondb
DB_APP_DATABASE=app DB_APP_DATABASE=app
DB_APPY_USER=dev_user DB_APP_USER=dev_user
DB_APP_PASSWORD_SECRET=dev_password DB_APP_PASSWORD_SECRET=dev_password
# Database Configuration Chat # Database Configuration Chat

View file

@ -7,8 +7,10 @@ import logging
from typing import Dict, Any, List from typing import Dict, Any, List
import json import json
from datetime import datetime from datetime import datetime
import uuid
from modules.workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,21 +38,21 @@ class AgentCoach(AgentBase):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase) self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Task) -> Dict[str, Any]:
""" """
Process a task by directly using AI to provide answers or content based on extracted data. Process a task by directly using AI to provide answers or content based on extracted data.
Args: Args:
task: Task dictionary with prompt, inputDocuments, outputSpecifications task: Task object with prompt, inputDocuments, outputSpecifications
Returns: Returns:
Dictionary with feedback and documents Dictionary with feedback and documents
""" """
try: try:
# Extract task information # Extract task information
prompt = task.get("prompt", "") prompt = task.prompt
inputDocuments = task.get("inputDocuments", []) inputDocuments = task.filesInput
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.filesOutput
# Check AI service # Check AI service
if not self.service or not self.service.base: if not self.service or not self.service.base:
@ -113,7 +115,7 @@ class AgentCoach(AgentBase):
"documents": [] "documents": []
} }
def _collectExtractedData(self, documents: List[Dict[str, Any]]) -> str: def _collectExtractedData(self, documents: List[ChatDocument]) -> str:
""" """
Collect extracted data from input documents. Collect extracted data from input documents.
@ -126,16 +128,16 @@ class AgentCoach(AgentBase):
contextParts = [] contextParts = []
for doc in documents: for doc in documents:
docName = doc.get("name", "unnamed") docName = doc.name
if doc.get("ext"): if doc.ext:
docName = f"{docName}.{doc.get('ext')}" docName = f"{docName}.{doc.ext}"
contextParts.append(f"\n\n--- {docName} ---\n") contextParts.append(f"\n\n--- {docName} ---\n")
# Process contents, focusing on dataExtracted field # Process contents, focusing on dataExtracted field
for content in doc.get("contents", []): for content in doc.contents:
if content.get("dataExtracted"): if content.data:
contextParts.append(content.get("dataExtracted", "")) contextParts.append(content.data)
return "\n".join(contextParts) return "\n".join(contextParts)
@ -208,7 +210,7 @@ class AgentCoach(AgentBase):
} }
async def _generateDocument(self, prompt: str, context: str, outputLabel: str, async def _generateDocument(self, prompt: str, context: str, outputLabel: str,
outputFormat: str, description: str, taskUnderstanding: Dict) -> Dict[str, Any]: outputFormat: str, description: str, taskUnderstanding: Dict) -> ChatDocument:
""" """
Generate a document based on the request and extracted data. Generate a document based on the request and extracted data.
@ -221,7 +223,7 @@ class AgentCoach(AgentBase):
taskUnderstanding: Task understanding from analysis taskUnderstanding: Task understanding from analysis
Returns: Returns:
Document object ChatDocument object
""" """
# Determine content type based on format # Determine content type based on format
contentType = self._getContentType(outputFormat) contentType = self._getContentType(outputFormat)
@ -248,52 +250,52 @@ class AgentCoach(AgentBase):
4. Be comprehensive but focused 4. Be comprehensive but focused
5. Include appropriate formatting, structure, and organization 5. Include appropriate formatting, structure, and organization
Your response should be in valid {outputFormat} format without explanations or markdown formatting around it. Only return the content. No explanations or additional text.
""" """
try: try:
# Build system prompt based on format # Get content from AI
systemPrompt = f"You create {outputFormat} format content based on requests and extracted data. Provide only the content in valid {outputFormat} format."
# Generate content with AI
content = await self.service.base.callAi([ content = await self.service.base.callAi([
{"role": "system", "content": systemPrompt}, {"role": "system", "content": f"You are a content generation expert. Create content in {outputFormat} format."},
{"role": "user", "content": generationPrompt} {"role": "user", "content": generationPrompt}
]) ])
# Process content based on format # Extract content from code blocks if present
if outputFormat in ["json", "csv"]:
# For structured formats, extract from code blocks if present
content = self._extractFromCodeBlocks(content, outputFormat) content = self._extractFromCodeBlocks(content, outputFormat)
# Validate JSON if needed # Create document object
if outputFormat == "json": return ChatDocument(
try: id=str(uuid.uuid4()),
json.loads(content) name=outputLabel.split('.')[0],
except: ext=outputFormat,
logger.warning("Invalid JSON generated, attempting to fix") data=content,
# Try to extract just the JSON portion contents=[
jsonStart = content.find('{') ChatContent(
jsonEnd = content.rfind('}') + 1 name="main",
if jsonStart >= 0 and jsonEnd > jsonStart: data=content,
content = content[jsonStart:jsonEnd] summary=description,
metadata={"format": outputFormat}
# Ensure proper structure for markdown/HTML )
if outputFormat in ["md", "markdown"] and not content.strip().startswith("#"): ]
title = "Response" )
content = f"# {title}\n\n{content}"
elif outputFormat == "html" and not "<html" in content.lower():
title = "Response"
content = f"<html><head><title>{title}</title></head><body><h1>{title}</h1>{content}</body></html>"
return self.formatAgentDocumentOutput(outputLabel, content, contentType)
except Exception as e: except Exception as e:
logger.error(f"Error generating document: {str(e)}") logger.error(f"Error generating document: {str(e)}")
# Create error document
errorContent = self._createErrorContent(str(e), outputFormat) errorContent = self._createErrorContent(str(e), outputFormat)
return self.formatAgentDocumentOutput(outputLabel, errorContent, contentType) return ChatDocument(
id=str(uuid.uuid4()),
name=outputLabel.split('.')[0],
ext=outputFormat,
data=errorContent,
contents=[
ChatContent(
name="error",
data=errorContent,
summary="Error generating content",
metadata={"format": outputFormat, "error": str(e)}
)
]
)
def _getContentType(self, outputFormat: str) -> str: def _getContentType(self, outputFormat: str) -> str:
""" """

View file

@ -4,7 +4,7 @@ Provides code generation, execution, and improvement capabilities.
""" """
import logging import logging
from typing import Dict, Any, List, Tuple from typing import Dict, Any, List, Tuple, Optional
import json import json
import os import os
import sys import sys
@ -14,9 +14,11 @@ import shutil
import venv import venv
import importlib.util import importlib.util
from datetime import datetime from datetime import datetime
import uuid
from modules.workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +48,7 @@ class AgentCoder(AgentBase):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.setService(serviceBase) self.setService(serviceBase)
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Task) -> Dict[str, Any]:
""" """
Process a task and perform code development/execution. Process a task and perform code development/execution.
First checks if the task can be completed without code execution, First checks if the task can be completed without code execution,
@ -54,15 +56,15 @@ class AgentCoder(AgentBase):
Enhanced to ensure all generated documents are included in output. Enhanced to ensure all generated documents are included in output.
Args: Args:
task: Task dictionary with prompt, inputDocuments, outputSpecifications task: Task object with prompt, inputDocuments, outputSpecifications
Returns: Returns:
Dictionary with feedback and documents Dictionary with feedback and documents
""" """
# 1. Extract task information # 1. Extract task information
prompt = task.get("prompt", "") prompt = task.prompt
inputDocuments = task.get("inputDocuments", []) inputDocuments = task.filesInput
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.filesOutput
# Check if AI service is available # Check if AI service is available
if not self.service or not self.service.base: if not self.service or not self.service.base:
@ -79,39 +81,39 @@ class AgentCoder(AgentBase):
for doc in inputDocuments: for doc in inputDocuments:
# Create proper filename from name and ext # Create proper filename from name and ext
filename = f"{doc.get('name')}.{doc.get('ext')}" if doc.get('ext') else doc.get('name') filename = f"{doc.name}.{doc.ext}" if doc.ext else doc.name
# Add main document data to documentData if it exists # Add main document data to documentData if it exists
docData = doc.get('data', '') docData = doc.data
if docData: if docData:
isBase64 = True # Assume base64 encoded for document data isBase64 = True # Assume base64 encoded for document data
documentData.append([filename, docData, isBase64]) documentData.append([filename, docData, isBase64])
# Process contents for different uses # Process contents for different uses
if doc.get('contents'): if doc.contents:
for content in doc.get('contents', []): for content in doc.contents:
contentName = content.get('name', 'unnamed') contentName = content.name
# For AI-extracted data (quick completion) # For AI-extracted data (quick completion)
if content.get('dataExtracted'): if content.data:
contentExtraction.append({ contentExtraction.append({
"filename": filename, "filename": filename,
"contentName": contentName, "contentName": contentName,
"contentData": content.get('dataExtracted', ''), "contentData": content.data,
"contentType": content.get('contentType', ''), "contentType": content.contentType,
"summary": content.get('summary', '') "summary": content.summary
}) })
# For raw content data # For raw content data
if content.get('data'): if content.data:
rawData = content.get('data', '') rawData = content.data
isBase64 = content.get('metadata', {}).get('base64Encoded', False) isBase64 = content.metadata.get('base64Encoded', False) if content.metadata else False
contentData.append({ contentData.append({
"filename": filename, "filename": filename,
"contentName": contentName, "contentName": contentName,
"data": rawData, "data": rawData,
"isBase64": isBase64, "isBase64": isBase64,
"contentType": content.get('contentType', '') "contentType": content.contentType
}) })
# Also add to documentData for code execution if not already added # Also add to documentData for code execution if not already added
@ -239,7 +241,7 @@ class AgentCoder(AgentBase):
# Override the base64Encoded flag with the value from the result # Override the base64Encoded flag with the value from the result
# This is needed since formatAgentDocumentOutput might determine a different value # This is needed since formatAgentDocumentOutput might determine a different value
if isinstance(base64Encoded, bool): if isinstance(base64Encoded, bool):
doc["base64Encoded"] = base64Encoded doc.base64Encoded = base64Encoded
documents.append(doc) documents.append(doc)
createdOutputs.add(finalLabel) createdOutputs.add(finalLabel)
@ -248,36 +250,14 @@ class AgentCoder(AgentBase):
# Not properly structured - log warning # Not properly structured - log warning
logger.warning(f"Skipping improperly formatted result for '{label}'. Results must include 'content' field.") logger.warning(f"Skipping improperly formatted result for '{label}'. Results must include 'content' field.")
else: else:
# No result dictionary found # Handle non-dictionary results
logger.warning("No valid result dictionary found or it's not properly formatted") logger.warning("Execution result is not a dictionary. Creating a single output document.")
doc = self.formatAgentDocumentOutput("result.txt", str(resultData), "text/plain")
# If no valid documents were created from the result dictionary but we have output specifications
if len(documents) <= 2 and outputSpecs: # Only code.py and history.json exist
logger.warning("No valid documents created from result dictionary, using execution output for specifications")
# Default to execution output
output = executionResult.get("output", "")
for spec in outputSpecs:
label = spec.get("label", "output.txt")
# Create basic document from output
doc = self.formatAgentDocumentOutput(label, output, "text/plain")
documents.append(doc) documents.append(doc)
logger.info(f"Created document from output specification: {label}")
if retryCount > 0:
feedback = f"Code executed successfully after {retryCount + 1} attempts. Generated {len(documents) - 2} output files."
else:
feedback = f"Code executed successfully. Generated {len(documents) - 2} output files."
else:
# Execution failed
error = executionResult.get("error", "Unknown error")
documents.append(self.formatAgentDocumentOutput("execution_error.txt", f"Error executing code:\n\n{error}", "text/plain"))
if retryCount > 0:
feedback = f"Error during code execution after {retryCount + 1} attempts: {error}"
else:
feedback = f"Error during code execution: {error}"
# 8. Return results
return { return {
"feedback": feedback, "feedback": "Code execution completed successfully." if executionResult.get("success", False) else f"Code execution failed: {executionResult.get('error', 'Unknown error')}",
"documents": documents "documents": documents
} }
@ -393,7 +373,7 @@ Return ONLY Python code without explanations or markdown.
return None, [] return None, []
async def _checkQuickCompletion(self, prompt: str, contentExtraction: List[Dict], outputSpecs: List[Dict]) -> Dict: async def _checkQuickCompletion(self, prompt: str, contentExtraction: List[ChatDocument], outputSpecs: List[Dict[str, Any]]) -> Dict[str, Any]:
""" """
Check if the task can be completed without writing and executing code. Check if the task can be completed without writing and executing code.
@ -411,7 +391,7 @@ Return ONLY Python code without explanations or markdown.
# Create a prompt for the AI to check if this can be completed directly # Create a prompt for the AI to check if this can be completed directly
specsJson = json.dumps(outputSpecs) specsJson = json.dumps(outputSpecs)
dataJson = json.dumps(contentExtraction) dataJson = json.dumps([doc.dict() for doc in contentExtraction])
checkPrompt = f""" checkPrompt = f"""
Analyze this task and determine if it can be completed directly without writing code. Analyze this task and determine if it can be completed directly without writing code.
@ -478,7 +458,7 @@ Only return valid JSON. Your entire response must be parseable as JSON.
# Default to requiring code execution # Default to requiring code execution
return None return None
async def _generateCode(self, prompt: str, outputSpecs: List[Dict[str, Any]] = None) -> Tuple[str, List[str]]: async def _generateCode(self, prompt: str, outputSpecs: List[ChatDocument] = None) -> Tuple[str, List[str]]:
""" """
Generate Python code from a prompt with the inputFiles placeholder. Generate Python code from a prompt with the inputFiles placeholder.
Enhanced to emphasize proper result output handling with correct document structure. Enhanced to emphasize proper result output handling with correct document structure.
@ -1019,6 +999,38 @@ Return ONLY Python code without explanations or markdown.
cleanedCode = '\n'.join(lines[startIndex:endIndex]) cleanedCode = '\n'.join(lines[startIndex:endIndex])
return cleanedCode.strip() return cleanedCode.strip()
def formatAgentDocumentOutput(self, filename: str, content: str, contentType: str) -> ChatDocument:
"""
Format a document for agent output.
Args:
filename: Output filename
content: Document content
contentType: MIME type of the content
Returns:
ChatDocument object
"""
# Split filename into name and extension
name, ext = os.path.splitext(filename)
if ext.startswith('.'):
ext = ext[1:]
# Create document object
return ChatDocument(
id=str(uuid.uuid4()),
name=name,
ext=ext,
data=content,
contents=[
ChatContent(
name="main",
data=content,
summary=f"Generated {filename}",
metadata={"contentType": contentType}
)
]
)
# Factory function for the Coder agent # Factory function for the Coder agent
def getAgentCoder(): def getAgentCoder():

View file

@ -5,8 +5,12 @@ Handles email-related tasks using Microsoft Graph API.
import logging import logging
import json import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional, Tuple
import uuid
import os
from modules.workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,7 +34,7 @@ class AgentEmail(AgentBase):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
self.serviceBase = serviceBase self.serviceBase = serviceBase
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]: async def processTask(self, task: Task) -> Dict[str, Any]:
""" """
Process an email-related task. Process an email-related task.
@ -48,9 +52,9 @@ class AgentEmail(AgentBase):
""" """
try: try:
# Extract task information # Extract task information
prompt = task.get("prompt", "") prompt = task.prompt
inputDocuments = task.get("inputDocuments", []) inputDocuments = task.filesInput
outputSpecs = task.get("outputSpecifications", []) outputSpecs = task.filesOutput
# Check AI service # Check AI service
if not self.service.base: if not self.service.base:
@ -148,26 +152,39 @@ class AgentEmail(AgentBase):
"documents": [] "documents": []
} }
def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]: def _createFrontendAuthTriggerDocument(self) -> ChatDocument:
"""Create a document that triggers Microsoft authentication in the frontend.""" """Create a document that triggers Microsoft authentication in the frontend."""
return { return ChatDocument(
"name": "microsoft_auth", id=str(uuid.uuid4()),
"ext": "html", name="microsoft_auth",
"mimeType": "text/html", ext="html",
"data": """ data="""
<div> <div>
<h2>Microsoft Authentication Required</h2> <h2>Microsoft Authentication Required</h2>
<p>Please click the button below to authenticate with Microsoft:</p> <p>Please click the button below to authenticate with Microsoft:</p>
<button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button> <button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
</div> </div>
""", """,
"base64Encoded": False, contents=[
"metadata": { ChatContent(
name="main",
data="""
<div>
<h2>Microsoft Authentication Required</h2>
<p>Please click the button below to authenticate with Microsoft:</p>
<button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
</div>
""",
summary="Microsoft authentication trigger page",
metadata={
"contentType": "text/html",
"isText": True "isText": True
} }
} )
]
)
def _processInputDocuments(self, input_docs: List[Dict[str, Any]]) -> tuple: def _processInputDocuments(self, input_docs: List[ChatDocument]) -> Tuple[str, List[Dict[str, Any]]]:
""" """
Process input documents to extract content and prepare attachments. Process input documents to extract content and prepare attachments.
@ -181,22 +198,22 @@ class AgentEmail(AgentBase):
attachments = [] attachments = []
for doc in input_docs: for doc in input_docs:
docName = doc.get("name", "unnamed") docName = doc.name
if doc.get("ext"): if doc.ext:
docName = f"{docName}.{doc.get('ext')}" docName = f"{docName}.{doc.ext}"
# Add document name to contents # Add document name to contents
documentContents.append(f"\n\n--- {docName} ---\n") documentContents.append(f"\n\n--- {docName} ---\n")
# Process document data directly # Process document data directly
if doc.get("data"): if doc.data:
# Add to attachments with proper metadata # Add to attachments with proper metadata
attachments.append({ attachments.append({
"name": docName, "name": docName,
"document": { "document": {
"data": doc["data"], "data": doc.data,
"mimeType": doc.get("mimeType", "application/octet-stream"), "mimeType": doc.contents[0].metadata.get("contentType", "application/octet-stream") if doc.contents else "application/octet-stream",
"base64Encoded": doc.get("base64Encoded", False) "base64Encoded": doc.contents[0].metadata.get("base64Encoded", False) if doc.contents else False
} }
}) })
documentContents.append(f"Document attached: {docName}") documentContents.append(f"Document attached: {docName}")
@ -205,6 +222,39 @@ class AgentEmail(AgentBase):
return "\n".join(documentContents), attachments return "\n".join(documentContents), attachments
def formatAgentDocumentOutput(self, filename: str, content: str, contentType: str) -> ChatDocument:
"""
Format a document for agent output.
Args:
filename: Output filename
content: Document content
contentType: MIME type of the content
Returns:
ChatDocument object
"""
# Split filename into name and extension
name, ext = os.path.splitext(filename)
if ext.startswith('.'):
ext = ext[1:]
# Create document object
return ChatDocument(
id=str(uuid.uuid4()),
name=name,
ext=ext,
data=content,
contents=[
ChatContent(
name="main",
data=content,
summary=f"Generated {filename}",
metadata={"contentType": contentType}
)
]
)
async def _generateEmailTemplate(self, prompt: str, documentContents: str) -> Dict[str, Any]: async def _generateEmailTemplate(self, prompt: str, documentContents: str) -> Dict[str, Any]:
""" """
Generate email template using AI. Generate email template using AI.

View file

@ -409,11 +409,17 @@ class DatabaseConnector:
with open(recordPath, 'w', encoding='utf-8') as f: with open(recordPath, 'w', encoding='utf-8') as f:
json.dump(recordData, f, indent=2, ensure_ascii=False) json.dump(recordData, f, indent=2, ensure_ascii=False)
# Save metadata # Update metadata with new record ID
if recordData["id"] not in metadata["recordIds"]:
metadata["recordIds"].append(recordData["id"])
metadata["recordIds"].sort()
# Save updated metadata
if not self._saveTableMetadata(table, metadata): if not self._saveTableMetadata(table, metadata):
raise ValueError(f"Error saving metadata for table {table}") raise ValueError(f"Error saving metadata for table {table}")
# Update cache safely # Update both caches
self._tableMetadataCache[table] = metadata
if table in self._tablesCache: if table in self._tablesCache:
if isinstance(self._tablesCache[table], list): if isinstance(self._tablesCache[table], list):
self._tablesCache[table].append(recordData) self._tablesCache[table].append(recordData)
@ -526,9 +532,7 @@ class DatabaseConnector:
"""Returns the initial ID for a table.""" """Returns the initial ID for a table."""
systemData = self._loadSystemTable() systemData = self._loadSystemTable()
initialId = systemData.get(table) initialId = systemData.get(table)
logger.debug(f"Database '{self.dbDatabase}': Table: {systemData}, Initial ID for table '{table}' is {initialId}") logger.debug(f"Initial ID for table '{table}': {initialId}")
if initialId is None:
logger.debug(f"No initial ID found for table {table}")
return initialId return initialId
def getAllInitialIds(self) -> Dict[str, str]: def getAllInitialIds(self) -> Dict[str, str]:

View file

@ -4,7 +4,7 @@ Access control for the Application.
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from datetime import datetime from datetime import datetime
from modules.interfaces.serviceAppModel import UserPrivilege, Session from modules.interfaces.serviceAppModel import UserPrivilege, Session, User
class AppAccess: class AppAccess:
""" """
@ -12,12 +12,12 @@ class AppAccess:
Handles user access management and permission checks. Handles user access management and permission checks.
""" """
def __init__(self, currentUser: Dict[str, Any], db): def __init__(self, currentUser: User, db):
"""Initialize with user context.""" """Initialize with user context."""
self.currentUser = currentUser self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId") self.userId = currentUser.id
self.userId = currentUser.get("id") self.mandateId = currentUser.mandateId
self.privilege = currentUser.get("privilege", UserPrivilege.USER) self.privilege = currentUser.privilege
if not self.mandateId or not self.userId: if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and userId are required") raise ValueError("Invalid user context: mandateId and userId are required")

View file

@ -37,12 +37,12 @@ class GatewayInterface:
Manages users and mandates. Manages users and mandates.
""" """
def __init__(self, currentUser: Dict[str, Any] = None): def __init__(self, currentUser: Optional[User] = None):
"""Initializes the Gateway Interface.""" """Initializes the Gateway Interface."""
# Initialize variables # Initialize variables
self.currentUser = currentUser self.currentUser = currentUser # Store User object directly
self.userId = currentUser.get("id") if currentUser else None self.userId = currentUser.id if currentUser else None
self.mandateId = currentUser.get("mandateId") if currentUser else None self.mandateId = currentUser.mandateId if currentUser else None
self.access = None # Will be set when user context is provided self.access = None # Will be set when user context is provided
# Initialize database # Initialize database
@ -55,24 +55,24 @@ class GatewayInterface:
if currentUser: if currentUser:
self.setUserContext(currentUser) self.setUserContext(currentUser)
def setUserContext(self, currentUser: Dict[str, Any]): def setUserContext(self, currentUser: User):
"""Sets the user context for the interface.""" """Sets the user context for the interface."""
if not currentUser: if not currentUser:
logger.info("Initializing interface without user context") logger.info("Initializing interface without user context")
return return
self.currentUser = currentUser self.currentUser = currentUser # Store User object directly
self.userId = currentUser.get("id") self.userId = currentUser.id
self.mandateId = currentUser.get("mandateId") self.mandateId = currentUser.mandateId
if not self.userId or not self.mandateId: if not self.userId or not self.mandateId:
raise ValueError("Invalid user context: id and mandateId are required") raise ValueError("Invalid user context: id and mandateId are required")
# Add language settings # Add language settings
self.userLanguage = currentUser.get("language", "en") # Default user language self.userLanguage = currentUser.language # Default user language
# Initialize access control with user context # Initialize access control with user context
self.access = AppAccess(self.currentUser, self.db) self.access = AppAccess(self.currentUser, self.db) # Convert to dict only when needed
logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}") logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
@ -115,7 +115,7 @@ class GatewayInterface:
name="Root", name="Root",
language="en" language="en"
) )
createdMandate = self.db.recordCreate("mandates", rootMandate.model_dump()) createdMandate = self.db.recordCreate("mandates", rootMandate.to_dict())
logger.info(f"Root mandate created with ID {createdMandate['id']}") logger.info(f"Root mandate created with ID {createdMandate['id']}")
# Register the initial ID # Register the initial ID
@ -142,7 +142,7 @@ class GatewayInterface:
hashedPassword=self._getPasswordHash("The 1st Poweron Admin"), # Use a secure password in production! hashedPassword=self._getPasswordHash("The 1st Poweron Admin"), # Use a secure password in production!
connections=[] connections=[]
) )
createdUser = self.db.recordCreate("users", adminUser.model_dump()) createdUser = self.db.recordCreate("users", adminUser.to_dict())
logger.info(f"Admin user created with ID {createdUser['id']}") logger.info(f"Admin user created with ID {createdUser['id']}")
# Register the initial ID # Register the initial ID
@ -219,10 +219,10 @@ class GatewayInterface:
return None return None
# Find user by username # Find user by username
for user in users: for user_dict in users:
if user.get("username") == username: if user_dict.get("username") == username:
logger.info(f"Found user with username {username}") logger.info(f"Found user with username {username}")
return User.from_dict(user) return User.from_dict(user_dict)
logger.info(f"No user found with username {username}") logger.info(f"No user found with username {username}")
return None return None
@ -270,7 +270,7 @@ class GatewayInterface:
user.connections.append(connection) user.connections.append(connection)
# Update user record # Update user record
self.db.recordModify("users", userId, {"connections": [c.model_dump() for c in user.connections]}) self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
return connection return connection
@ -290,7 +290,7 @@ class GatewayInterface:
user.connections = [c for c in user.connections if c.id != connectionId] user.connections = [c for c in user.connections if c.id != connectionId]
# Update user record # Update user record
self.db.recordModify("users", userId, {"connections": [c.model_dump() for c in user.connections]}) self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
except Exception as e: except Exception as e:
logger.error(f"Error removing user connection: {str(e)}") logger.error(f"Error removing user connection: {str(e)}")
@ -317,8 +317,11 @@ class GatewayInterface:
raise ValueError("User does not have local authentication enabled") raise ValueError("User does not have local authentication enabled")
# Get the full user record with password hash for verification # Get the full user record with password hash for verification
userWithPassword = UserInDB(**self.db.getRecordset("users", recordFilter={"id": user.id})[0]) userRecord = self.db.getRecordset("users", recordFilter={"id": user.id})[0]
if not self._verifyPassword(password, userWithPassword.hashedPassword): if not userRecord.get("hashedPassword"):
raise ValueError("User has no password set")
if not self._verifyPassword(password, userRecord["hashedPassword"]):
raise ValueError("Invalid password") raise ValueError("Invalid password")
return user return user
@ -331,6 +334,18 @@ class GatewayInterface:
externalEmail: str = None) -> User: externalEmail: str = None) -> User:
"""Create a new user with optional external connection""" """Create a new user with optional external connection"""
try: try:
# Ensure username is a string
username = str(username).strip()
# Validate password for local authentication
if authenticationAuthority == AuthAuthority.LOCAL:
if not password:
raise ValueError("Password is required for local authentication")
if not isinstance(password, str):
raise ValueError("Password must be a string")
if not password.strip():
raise ValueError("Password cannot be empty")
# Create user data using UserInDB model # Create user data using UserInDB model
userData = UserInDB( userData = UserInDB(
username=username, username=username,
@ -365,9 +380,11 @@ class GatewayInterface:
if not createdUser or len(createdUser) == 0: if not createdUser or len(createdUser) == 0:
raise ValueError("Failed to retrieve created user") raise ValueError("Failed to retrieve created user")
# Clear users table from cache # Clear both table and metadata caches
if hasattr(self.db, '_tablesCache') and "users" in self.db._tablesCache: if hasattr(self.db, '_tablesCache') and "users" in self.db._tablesCache:
del self.db._tablesCache["users"] del self.db._tablesCache["users"]
if hasattr(self.db, '_tableMetadataCache') and "users" in self.db._tableMetadataCache:
del self.db._tableMetadataCache["users"]
return User.from_dict(createdUser[0]) return User.from_dict(createdUser[0])
@ -387,7 +404,7 @@ class GatewayInterface:
raise ValueError(f"User {userId} not found") raise ValueError(f"User {userId} not found")
# Update user data using model # Update user data using model
updatedData = user.model_dump() updatedData = user.to_dict()
updatedData.update(updateData) updatedData.update(updateData)
updatedUser = User.from_dict(updatedData) updatedUser = User.from_dict(updatedData)
@ -488,7 +505,7 @@ class GatewayInterface:
raise ValueError(f"Mandate {mandateId} not found") raise ValueError(f"Mandate {mandateId} not found")
# Update mandate data using model # Update mandate data using model
updatedData = mandate.model_dump() updatedData = mandate.to_dict()
updatedData.update(updateData) updatedData.update(updateData)
updatedMandate = Mandate.from_dict(updatedData) updatedMandate = Mandate.from_dict(updatedData)
@ -529,20 +546,65 @@ class GatewayInterface:
logger.error(f"Error deleting mandate: {str(e)}") logger.error(f"Error deleting mandate: {str(e)}")
raise ValueError(f"Failed to delete mandate: {str(e)}") raise ValueError(f"Failed to delete mandate: {str(e)}")
def _getInitialUser(self) -> Optional[Dict[str, Any]]:
"""Get the initial user record directly from database without access control."""
try:
initialUserId = self.db.getInitialId("users")
if not initialUserId:
return None
users = self.db.getRecordset("users", recordFilter={"id": initialUserId})
return users[0] if users else None
except Exception as e:
logger.error(f"Error getting initial user: {str(e)}")
return None
def checkUsernameAvailability(self, checkData: Dict[str, Any]) -> Dict[str, Any]:
"""Checks if a username is available for registration."""
try:
username = checkData.get("username")
authenticationAuthority = checkData.get("authenticationAuthority", "local")
if not username:
return {
"available": False,
"message": "Username is required"
}
# Get user by username
user = self.getUserByUsername(username)
# Check if user exists (User model instance)
if user is not None:
return {
"available": False,
"message": "Username is already taken"
}
return {
"available": True,
"message": "Username is available"
}
except Exception as e:
logger.error(f"Error checking username availability: {str(e)}")
return {
"available": False,
"message": f"Error checking username availability: {str(e)}"
}
# Public Methods # Public Methods
def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface: def getInterface(currentUser: User) -> GatewayInterface:
""" """
Returns a GatewayInterface instance for the current user. Returns a GatewayInterface instance for the current user.
Handles initialization of database and records. Handles initialization of database and records.
""" """
mandateId = currentUser.get("mandateId") if not currentUser:
userId = currentUser.get("id") raise ValueError("Invalid user context: user is required")
if not mandateId or not userId:
raise ValueError("Invalid user context: mandateId and id are required")
# Create context key # Create context key
contextKey = f"{mandateId}_{userId}" contextKey = f"{currentUser.mandateId}_{currentUser.id}"
# Create new instance if not exists # Create new instance if not exists
if contextKey not in _gatewayInterfaces: if contextKey not in _gatewayInterfaces:
@ -550,24 +612,27 @@ def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface:
return _gatewayInterfaces[contextKey] return _gatewayInterfaces[contextKey]
def getRootUser() -> Dict[str, Any]: def getRootUser() -> User:
""" """
Returns the root user from the database. Returns the root user from the database.
This is the user with the initial ID in the users table. This is the user with the initial ID in the users table.
""" """
try: try:
readInterface = getInterface() # Create a temporary interface without user context
# Get the initial user ID tempInterface = GatewayInterface()
initialUserId = readInterface.db.getInitialId("users")
# Get the initial user directly
initialUserId = tempInterface.db.getInitialId("users")
if not initialUserId: if not initialUserId:
raise ValueError("No initial user ID found in database") raise ValueError("No initial user ID found in database")
# Get the user record users = tempInterface.db.getRecordset("users", recordFilter={"id": initialUserId})
users = readInterface.db.getRecordset("users", recordFilter={"id": initialUserId})
if not users: if not users:
raise ValueError(f"Root user with ID {initialUserId} not found in database") raise ValueError("Initial user not found in database")
# Convert to User model and return the model instance
return User.from_dict(users[0])
return users[0]
except Exception as e: except Exception as e:
logger.error(f"Error getting root user: {str(e)}") logger.error(f"Error getting root user: {str(e)}")
raise ValueError(f"Failed to get root user: {str(e)}") raise ValueError(f"Failed to get root user: {str(e)}")

View file

@ -10,6 +10,17 @@ from enum import Enum
from modules.shared.attributeUtils import Label, BaseModelWithUI from modules.shared.attributeUtils import Label, BaseModelWithUI
class AttributeDefinition(BaseModel):
"""Definition of an attribute for UI forms"""
name: str = Field(..., description="Name of the attribute")
label: str = Field(..., description="Display label for the attribute")
type: str = Field(..., description="Type of the attribute (string, number, boolean, etc.)")
required: bool = Field(default=False, description="Whether the attribute is required")
placeholder: Optional[str] = Field(None, description="Placeholder text for the input")
editable: bool = Field(default=True, description="Whether the attribute can be edited")
visible: bool = Field(default=True, description="Whether the attribute should be visible in forms")
order: int = Field(default=0, description="Order in which to display the attribute")
class AuthAuthority(str, Enum): class AuthAuthority(str, Enum):
"""Authentication authorities""" """Authentication authorities"""
LOCAL = "local" LOCAL = "local"
@ -151,7 +162,7 @@ class User(BaseModelWithUI):
disabled: bool = Field(default=False, description="Indicates whether the user is disabled") disabled: bool = Field(default=False, description="Indicates whether the user is disabled")
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level") privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority") authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
mandateId: str = Field(description="ID of the mandate this user belongs to") mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections") connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
label: Label = Field( label: Label = Field(

View file

@ -25,28 +25,3 @@ class LocalToken(BaseModel):
access_token: str access_token: str
token_type: str = "bearer" token_type: str = "bearer"
expires_at: float expires_at: float
# Token management functions
def saveToken(interface, tokenType: str, tokenData: dict) -> bool:
"""Save token data for a specific service"""
try:
return interface.saveToken(f"tokens{tokenType}", tokenData)
except Exception as e:
logger.error(f"Error saving {tokenType} token: {str(e)}")
return False
def getToken(interface, tokenType: str) -> Optional[dict]:
"""Get token data for a specific service"""
try:
return interface.getToken(f"tokens{tokenType}")
except Exception as e:
logger.error(f"Error getting {tokenType} token: {str(e)}")
return None
def deleteToken(interface, tokenType: str) -> bool:
"""Delete token data for a specific service"""
try:
return interface.deleteToken(f"tokens{tokenType}")
except Exception as e:
logger.error(f"Error deleting {tokenType} token: {str(e)}")
return False

View file

@ -16,8 +16,9 @@ from modules.interfaces.serviceChatAccess import ChatAccess
from modules.interfaces.serviceChatModel import ( from modules.interfaces.serviceChatModel import (
ChatContent, ChatDocument, ChatStat, ChatMessage, ChatContent, ChatDocument, ChatStat, ChatMessage,
ChatLog, ChatWorkflow, Agent, AgentResponse, ChatLog, ChatWorkflow, Agent, AgentResponse,
TaskItem, TaskPlan, UserInputRequest Task, TaskPlan, UserInputRequest
) )
from modules.interfaces.serviceAppModel import User
# DYNAMIC PART: Connectors to the Interface # DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbJson import DatabaseConnector from modules.connectors.connectorDbJson import DatabaseConnector
@ -57,43 +58,41 @@ class ChatInterface:
Uses the JSON connector for data access with added language support. Uses the JSON connector for data access with added language support.
""" """
def __init__(self): def __init__(self, currentUser: Optional[User] = None):
"""Initializes the Chat Interface.""" """Initializes the Chat Interface."""
# Initialize variables
self.currentUser = currentUser # Store User object directly
self.userId = currentUser.id if currentUser else None
self.mandateId = currentUser.mandateId if currentUser else None
self.access = None # Will be set when user context is provided
# Initialize database # Initialize database
self._initializeDatabase() self._initializeDatabase()
# Initialize standard records if needed # Set user context if provided
self._initRecords() if currentUser:
self.setUserContext(currentUser)
# Initialize variables def setUserContext(self, currentUser: User):
self.currentUser = None
self.userId = None
self.mandateId = None
self.access = None # Will be set when user context is provided
self.aiService = None # Will be set when user context is provided
def setUserContext(self, currentUser: Dict[str, Any]):
"""Sets the user context for the interface.""" """Sets the user context for the interface."""
if not currentUser: if not currentUser:
logger.info("Initializing interface without user context") logger.info("Initializing interface without user context")
return return
self.currentUser = currentUser self.currentUser = currentUser # Store User object directly
self.userId = currentUser.get("id") self.userId = currentUser.id
self.mandateId = currentUser.get("mandateId") self.mandateId = currentUser.mandateId
if not self.userId:
raise ValueError("Invalid user context: id is required") if not self.userId or not self.mandateId:
raise ValueError("Invalid user context: id and mandateId are required")
# Add language settings # Add language settings
self.userLanguage = currentUser.get("language", "en") # Default user language self.userLanguage = currentUser.language # Default user language
# Initialize access control with user context # Initialize access control with user context
self.access = ChatAccess(self.currentUser, self.db) self.access = ChatAccess(self.currentUser, self.db) # Convert to dict only when needed
# Initialize AI service logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
self.aiService = ChatService()
logger.debug(f"User context set: userId={self.userId}")
def _initializeDatabase(self): def _initializeDatabase(self):
"""Initializes the database connection.""" """Initializes the database connection."""
@ -353,7 +352,7 @@ class ChatInterface:
# Workflow Messages # Workflow Messages
def getWorkflowMessages(self, workflowId: str) -> List[Dict[str, Any]]: def getWorkflowMessages(self, workflowId: str) -> List[ChatMessage]:
"""Returns messages for a workflow if user has access to the workflow.""" """Returns messages for a workflow if user has access to the workflow."""
# Check workflow access first # Check workflow access first
workflow = self.getWorkflow(workflowId) workflow = self.getWorkflow(workflowId)
@ -362,7 +361,7 @@ class ChatInterface:
# Get messages for this workflow # Get messages for this workflow
messages = self.db.getRecordset("workflowMessages", recordFilter={"workflowId": workflowId}) messages = self.db.getRecordset("workflowMessages", recordFilter={"workflowId": workflowId})
return messages # No further filtering needed since workflow access is already checked return [ChatMessage(**msg) for msg in messages]
def createWorkflowMessage(self, messageData: Dict[str, Any]) -> ChatMessage: def createWorkflowMessage(self, messageData: Dict[str, Any]) -> ChatMessage:
"""Creates a message for a workflow if user has access.""" """Creates a message for a workflow if user has access."""
@ -639,7 +638,7 @@ class ChatInterface:
# Workflow Logs # Workflow Logs
def getWorkflowLogs(self, workflowId: str) -> List[Dict[str, Any]]: def getWorkflowLogs(self, workflowId: str) -> List[ChatLog]:
"""Returns logs for a workflow if user has access to the workflow.""" """Returns logs for a workflow if user has access to the workflow."""
# Check workflow access first # Check workflow access first
workflow = self.getWorkflow(workflowId) workflow = self.getWorkflow(workflowId)
@ -647,7 +646,7 @@ class ChatInterface:
return [] return []
# Get logs for this workflow # Get logs for this workflow
return self.db.getRecordset("workflowLogs", recordFilter={"workflowId": workflowId}) return [ChatLog(**log) for log in self.db.getRecordset("workflowLogs", recordFilter={"workflowId": workflowId})]
def createWorkflowLog(self, logData: Dict[str, Any]) -> ChatLog: def createWorkflowLog(self, logData: Dict[str, Any]) -> ChatLog:
"""Creates a log entry for a workflow if user has access.""" """Creates a log entry for a workflow if user has access."""
@ -695,17 +694,17 @@ class ChatInterface:
return None return None
# Create log in database # Create log in database
createdLog = self.db.recordCreate("workflowLogs", log_model.model_dump()) createdLog = self.db.recordCreate("workflowLogs", log_model.to_dict())
# Return validated ChatLog instance # Return validated ChatLog instance
return ChatLog(**createdLog) return ChatLog(**createdLog)
# Workflow Management # Workflow Management
def saveWorkflowState(self, workflow: Dict[str, Any], saveMessages: bool = True, saveLogs: bool = True) -> bool: def saveWorkflowState(self, workflow: ChatWorkflow, saveMessages: bool = True, saveLogs: bool = True) -> bool:
"""Saves workflow state if user has access.""" """Saves workflow state if user has access."""
try: try:
workflowId = workflow.get("id") workflowId = workflow.id
if not workflowId: if not workflowId:
return False return False
@ -722,12 +721,12 @@ class ChatInterface:
# Extract only the database-relevant workflow fields # Extract only the database-relevant workflow fields
workflowDbData = { workflowDbData = {
"id": workflowId, "id": workflowId,
"mandateId": workflow.get("mandateId", self.currentUser.get("mandateId")), "mandateId": workflow.mandateId,
"name": workflow.get("name", f"Workflow {workflowId}"), "name": workflow.name,
"status": workflow.get("status", "completed"), "status": workflow.status,
"startedAt": workflow.get("startedAt", self._getCurrentTimestamp()), "startedAt": workflow.startedAt,
"lastActivity": workflow.get("lastActivity", self._getCurrentTimestamp()), "lastActivity": workflow.lastActivity,
"dataStats": workflow.get("dataStats", {}) "dataStats": workflow.stats.dict() if workflow.stats else {}
} }
# Check if workflow already exists # Check if workflow already exists
@ -802,7 +801,7 @@ class ChatInterface:
logger.error(f"Error saving workflow state: {str(e)}") logger.error(f"Error saving workflow state: {str(e)}")
return False return False
def loadWorkflowState(self, workflowId: str) -> Optional[Dict[str, Any]]: def loadWorkflowState(self, workflowId: str) -> Optional[ChatWorkflow]:
"""Loads workflow state if user has access.""" """Loads workflow state if user has access."""
try: try:
# Check workflow access # Check workflow access
@ -852,21 +851,19 @@ class ChatInterface:
return None return None
def getInterface(currentUser: Dict[str, Any] = None) -> 'ChatInterface': def getInterface(currentUser: Optional[User] = None) -> 'ChatInterface':
""" """
Returns a ChatInterface instance. Returns a ChatInterface instance for the current user.
If currentUser is provided, initializes with user context. Handles initialization of database and records.
Otherwise, returns an instance with only database access.
""" """
if not currentUser:
raise ValueError("Invalid user context: user is required")
# Create context key
contextKey = f"{currentUser.mandateId}_{currentUser.id}"
# Create new instance if not exists # Create new instance if not exists
if "default" not in _chatInterfaces: if contextKey not in _chatInterfaces:
_chatInterfaces["default"] = ChatInterface() _chatInterfaces[contextKey] = ChatInterface(currentUser)
interface = _chatInterfaces["default"] return _chatInterfaces[contextKey]
if currentUser:
interface.setUserContext(currentUser)
else:
logger.info("Returning interface without user context")
return interface

View file

@ -67,6 +67,23 @@ class ChatMessage(BaseModelWithUI):
stats: Optional[ChatStat] = Field(None, description="Statistics for this message") stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
success: Optional[bool] = Field(None, description="Whether the message processing was successful") success: Optional[bool] = Field(None, description="Whether the message processing was successful")
class Task(BaseModelWithUI):
"""Data model for a task"""
id: str = Field(description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
agentName: str = Field(description="Name of the agent assigned to this task")
status: str = Field(description="Current status of the task")
progress: float = Field(description="Task progress (0-100)")
prompt: str = Field(description="Prompt for the task")
userLanguage: str = Field(description="User's preferred language")
filesInput: List[str] = Field(default_factory=list, description="Input files")
filesOutput: List[str] = Field(default_factory=list, description="Output files")
result: Optional[ChatMessage] = Field(None, description="Task result message")
error: Optional[str] = Field(None, description="Error message if failed")
startedAt: str = Field(description="When the task started")
finishedAt: Optional[str] = Field(None, description="When the task finished")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
class ChatWorkflow(BaseModelWithUI): class ChatWorkflow(BaseModelWithUI):
"""Data model for a chat workflow""" """Data model for a chat workflow"""
id: str = Field(description="Primary key") id: str = Field(description="Primary key")
@ -79,7 +96,7 @@ class ChatWorkflow(BaseModelWithUI):
logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs") logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs")
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow") messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow")
stats: Optional[ChatStat] = Field(None, description="Workflow statistics") stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
tasks: List['Task'] = Field(default_factory=list, description="List of tasks in the workflow") tasks: List[Task] = Field(default_factory=list, description="List of tasks in the workflow")
label: Label = Field( label: Label = Field(
default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Flux de travail de chat"}), default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Flux de travail de chat"}),
@ -117,23 +134,6 @@ class AgentResponse(BaseModelWithUI):
performance: Dict[str, Any] = Field(default_factory=dict, description="Performance metrics") performance: Dict[str, Any] = Field(default_factory=dict, description="Performance metrics")
progress: float = Field(description="Task progress (0-100)") progress: float = Field(description="Task progress (0-100)")
class Task(BaseModelWithUI):
"""Data model for a task"""
id: str = Field(description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
agentName: str = Field(description="Name of the agent assigned to this task")
status: str = Field(description="Current status of the task")
progress: float = Field(description="Task progress (0-100)")
prompt: str = Field(description="Prompt for the task")
userLanguage: str = Field(description="User's preferred language")
filesInput: List[str] = Field(default_factory=list, description="Input files")
filesOutput: List[str] = Field(default_factory=list, description="Output files")
result: Optional[ChatMessage] = Field(None, description="Task result message")
error: Optional[str] = Field(None, description="Error message if failed")
startedAt: str = Field(description="When the task started")
finishedAt: Optional[str] = Field(None, description="When the task finished")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
class TaskPlan(BaseModelWithUI): class TaskPlan(BaseModelWithUI):
"""Data model for a task plan""" """Data model for a task plan"""
fileList: List[str] = Field(default_factory=list, description="List of files") fileList: List[str] = Field(default_factory=list, description="List of files")
@ -145,4 +145,14 @@ class UserInputRequest(BaseModelWithUI):
"""Data model for a user input request""" """Data model for a user input request"""
prompt: str = Field(description="Prompt for the user") prompt: str = Field(description="Prompt for the user")
listFileId: List[int] = Field(default_factory=list, description="List of file IDs") listFileId: List[int] = Field(default_factory=list, description="List of file IDs")
userLanguage: str = Field(description="User's preferred language") userLanguage: str = Field(default="en", description="User's preferred language")
class AgentProfile(BaseModel):
"""Model for agent profile information."""
id: str
name: str
description: str
capabilities: List[str] = Field(default_factory=list)
isAvailable: bool = True
lastActive: Optional[datetime] = None
stats: Optional[Dict[str, Any]] = None

View file

@ -3,7 +3,12 @@ Access control module for Management interface.
Handles user access management and permission checks. Handles user access management and permission checks.
""" """
import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from modules.interfaces.serviceAppModel import User
# Configure logger
logger = logging.getLogger(__name__)
class ManagementAccess: class ManagementAccess:
""" """
@ -11,15 +16,12 @@ class ManagementAccess:
Handles user access management and permission checks. Handles user access management and permission checks.
""" """
def __init__(self, currentUser: Dict[str, Any], db): def __init__(self, currentUser: User, db):
"""Initialize with user context.""" """Initialize with user context."""
self.currentUser = currentUser self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId") self.userId = currentUser.id
self.userId = currentUser.get("id") self.mandateId = currentUser.mandateId
self.privilege = currentUser.privilege
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and userId are required")
self.db = db self.db = db
def uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@ -34,8 +36,8 @@ class ManagementAccess:
Returns: Returns:
Filtered recordset with access control attributes Filtered recordset with access control attributes
""" """
userPrivilege = self.currentUser.get("privilege", "user") userPrivilege = self.privilege
print("DEBUG: User privilege:", userPrivilege, self.currentUser.get("username"),self.currentUser.get("email")) logger.debug(f"User privilege: {userPrivilege}, username: {self.currentUser.username}, email: {self.currentUser.email}")
filtered_records = [] filtered_records = []
# Apply filtering based on privilege # Apply filtering based on privilege
@ -98,7 +100,7 @@ class ManagementAccess:
Returns: Returns:
Boolean indicating permission Boolean indicating permission
""" """
userPrivilege = self.currentUser.get("privilege", "user") userPrivilege = self.privilege
# System admins can modify anything # System admins can modify anything
if userPrivilege == "sysadmin": if userPrivilege == "sysadmin":

View file

@ -76,7 +76,7 @@ class ServiceManagement:
logger.info("Initializing interface without user context") logger.info("Initializing interface without user context")
return return
self.currentUser = currentUser self.currentUser = currentUser # Store User object directly
self.userId = currentUser.id self.userId = currentUser.id
if not self.userId: if not self.userId:
@ -249,40 +249,32 @@ class ServiceManagement:
filteredPrompts = self._uam("prompts", prompts) filteredPrompts = self._uam("prompts", prompts)
return Prompt.from_dict(filteredPrompts[0]) if filteredPrompts else None return Prompt.from_dict(filteredPrompts[0]) if filteredPrompts else None
def createPrompt(self, content: str, name: str) -> Prompt: def createPrompt(self, promptData: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a new prompt if user has permission.""" """Creates a new prompt if user has permission."""
if not self._canModify("prompts"): if not self._canModify("prompts"):
raise PermissionError("No permission to create prompts") raise PermissionError("No permission to create prompts")
promptData = Prompt( # Create prompt record
content=content,
name=name,
createdAt=self._getCurrentTimestamp()
)
createdRecord = self.db.recordCreate("prompts", promptData.to_dict()) createdRecord = self.db.recordCreate("prompts", promptData.to_dict())
return Prompt.from_dict(createdRecord) if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create prompt record")
def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Prompt: return createdRecord
def updatePrompt(self, promptId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a prompt if user has access.""" """Updates a prompt if user has access."""
# Check if the prompt exists and user has access try:
# Get prompt
prompt = self.getPrompt(promptId) prompt = self.getPrompt(promptId)
if not prompt: if not prompt:
raise ValueError(f"Prompt {promptId} not found") raise ValueError(f"Prompt {promptId} not found")
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to update prompt {promptId}")
# Update prompt data using model # Update prompt data using model
updatedData = prompt.model_dump() updatedData = prompt.to_dict()
if content is not None: updatedData.update(updateData)
updatedData["content"] = content
if name is not None:
updatedData["name"] = name
updatedPrompt = Prompt.from_dict(updatedData) updatedPrompt = Prompt.from_dict(updatedData)
# Update prompt # Update prompt record
self.db.recordModify("prompts", promptId, updatedPrompt.to_dict()) self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
# Get updated prompt # Get updated prompt
@ -292,6 +284,10 @@ class ServiceManagement:
return updatedPrompt return updatedPrompt
except Exception as e:
logger.error(f"Error updating prompt: {str(e)}")
raise ValueError(f"Failed to update prompt: {str(e)}")
def deletePrompt(self, promptId: str) -> bool: def deletePrompt(self, promptId: str) -> bool:
"""Deletes a prompt if user has access.""" """Deletes a prompt if user has access."""
# Check if the prompt exists and user has access # Check if the prompt exists and user has access

View file

@ -1,13 +1,15 @@
from fastapi import APIRouter, Response, Depends from fastapi import APIRouter, Response, Depends, Request
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import os import os
import logging import logging
from pathlib import Path as FilePath from pathlib import Path as FilePath
from typing import Dict, Any from typing import Dict, Any, List
from fastapi import HTTPException, status
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
from modules.interfaces.serviceAppModel import User
router = APIRouter( router = APIRouter(
prefix="", prefix="",
@ -33,7 +35,7 @@ router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), nam
@router.get("/") @router.get("/")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def root(): async def root(request: Request) -> Dict[str, str]:
"""API status endpoint""" """API status endpoint"""
return { return {
"status": "online", "status": "online",
@ -43,7 +45,7 @@ async def root():
@router.get("/api/environment") @router.get("/api/environment")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_environment(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def get_environment(request: Request, currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, str]:
"""Get environment configuration for frontend""" """Get environment configuration for frontend"""
return { return {
"apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""), "apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""),
@ -54,10 +56,10 @@ async def get_environment(currentUser: Dict[str, Any] = Depends(getCurrentUser))
@router.options("/{fullPath:path}") @router.options("/{fullPath:path}")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def options_route(fullPath: str): async def options_route(request: Request, fullPath: str) -> Response:
return Response(status_code=200) return Response(status_code=200)
@router.get("/favicon.ico") @router.get("/favicon.ico")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def favicon(): async def favicon(request: Request) -> FileResponse:
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon") return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")

View file

@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends, Path, Response from fastapi import APIRouter, HTTPException, Depends, Path, Response, Request
from typing import List, Dict, Any from typing import List, Dict, Any
from fastapi import status from fastapi import status
import inspect import inspect
@ -11,7 +11,7 @@ import logging
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.interfaces.serviceAppModel import AttributeDefinition from modules.interfaces.serviceAppModel import AttributeDefinition, User
from modules.shared.attributeUtils import getModelClasses from modules.shared.attributeUtils import getModelClasses
# Configure logger # Configure logger
@ -50,9 +50,9 @@ router = APIRouter(
@router.get("/{entityType}", response_model=AttributeResponse) @router.get("/{entityType}", response_model=AttributeResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_entity_attributes( async def get_entity_attributes(
entityType: str = Path(..., description="Type of entity (e.g. prompt)"), request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) entityType: str = Path(..., description="Type of entity (e.g. prompt)")
): ) -> AttributeResponse:
""" """
Retrieves the attribute definitions for a specific entity. Retrieves the attribute definitions for a specific entity.
This can be used for dynamic form generation. This can be used for dynamic form generation.
@ -63,9 +63,6 @@ async def get_entity_attributes(
Returns: Returns:
- A list of attribute definitions that can be used to generate forms - A list of attribute definitions that can be used to generate forms
""" """
# Determine preferred language of the user
userLanguage = currentUser.get("language", "en")
# Get model classes dynamically # Get model classes dynamically
modelClasses = getModelClasses() modelClasses = getModelClasses()
@ -80,13 +77,21 @@ async def get_entity_attributes(
modelClass = modelClasses[entityType] modelClass = modelClasses[entityType]
attributes = modelClass.getModelAttributeDefinitions() attributes = modelClass.getModelAttributeDefinitions()
# Return only visible attributes # Convert dictionary attributes to AttributeDefinition objects
return AttributeResponse(attributes=[attr for attr in attributes if attr.visible]) attribute_definitions = []
for attr in attributes:
if isinstance(attr, dict) and attr.get('visible', True):
attribute_definitions.append(AttributeDefinition(**attr))
elif hasattr(attr, 'visible') and attr.visible:
attribute_definitions.append(attr)
return AttributeResponse(attributes=attribute_definitions)
@router.options("/{entityType}") @router.options("/{entityType}")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def options_entity_attributes( async def options_entity_attributes(
request: Request,
entityType: str = Path(..., description="Type of entity (e.g. prompt)") entityType: str = Path(..., description="Type of entity (e.g. prompt)")
): ) -> Response:
"""Handle OPTIONS request for CORS preflight""" """Handle OPTIONS request for CORS preflight"""
return Response(status_code=200) return Response(status_code=200)

View file

@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response, Body
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, FileResponse
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
@ -11,8 +11,9 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import FileItem, getModelAttributeDefinitions from modules.interfaces.serviceManagementModel import FileItem
from modules.interfaces.serviceAppModel import AttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions
from modules.interfaces.serviceAppModel import AttributeDefinition, User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,10 +34,13 @@ router = APIRouter(
} }
) )
@router.get("", response_model=List[FileItem]) @router.get("/list", response_model=List[FileItem])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_files(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def get_files(
"""Get all available files""" request: Request,
currentUser: User = Depends(getCurrentUser)
) -> List[FileItem]:
"""Get all files"""
try: try:
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -44,19 +48,20 @@ async def get_files(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
files = managementInterface.getAllFiles() files = managementInterface.getAllFiles()
return [FileItem(**file) for file in files] return [FileItem(**file) for file in files]
except Exception as e: except Exception as e:
logger.error(f"Error retrieving files: {str(e)}") logger.error(f"Error getting files: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error retrieving files: {str(e)}" detail=f"Failed to get files: {str(e)}"
) )
@router.post("/upload", status_code=status.HTTP_201_CREATED) @router.post("/upload", status_code=status.HTTP_201_CREATED)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def upload_file( async def upload_file(
request: Request,
file: UploadFile = File(...), file: UploadFile = File(...),
workflowId: Optional[str] = Form(None), workflowId: Optional[str] = Form(None),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> JSONResponse:
"""Upload a file""" """Upload a file"""
try: try:
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -82,7 +87,10 @@ async def upload_file(
fileMeta["workflowId"] = workflowId fileMeta["workflowId"] = workflowId
# Successful response # Successful response
return fileMeta return JSONResponse({
"message": "File uploaded successfully",
"file": fileMeta
})
except serviceManagementClass.FileStorageError as e: except serviceManagementClass.FileStorageError as e:
logger.error(f"Error during file upload (storage): {str(e)}") logger.error(f"Error during file upload (storage): {str(e)}")
@ -97,29 +105,27 @@ async def upload_file(
detail=f"Error during file upload: {str(e)}" detail=f"Error during file upload: {str(e)}"
) )
@router.get("/{fileId}") @router.get("/{fileId}", response_model=FileItem)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_file( async def get_file(
fileId: str, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) fileId: str = Path(..., description="ID of the file"),
): currentUser: User = Depends(getCurrentUser)
"""Returns a file by its ID for download""" ) -> FileItem:
"""Get a file"""
try: try:
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get file via LucyDOM interface from the database # Get file via LucyDOM interface from the database
fileData = managementInterface.downloadFile(fileId) fileData = managementInterface.getFile(fileId)
if not fileData:
# Return file raise HTTPException(
headers = { status_code=status.HTTP_404_NOT_FOUND,
"Content-Disposition": f'attachment; filename="{fileData["name"]}"' detail=f"File with ID {fileId} not found"
}
return Response(
content=fileData["content"],
media_type=fileData["contentType"],
headers=headers
) )
return FileItem(**fileData)
except serviceManagementClass.FileNotFoundError as e: except serviceManagementClass.FileNotFoundError as e:
logger.warning(f"File not found: {str(e)}") logger.warning(f"File not found: {str(e)}")
raise HTTPException( raise HTTPException(
@ -145,53 +151,62 @@ async def get_file(
detail=f"Error retrieving file: {str(e)}" detail=f"Error retrieving file: {str(e)}"
) )
@router.put("/{file_id}", response_model=FileItem) @router.put("/{fileId}", response_model=FileItem)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_file( async def update_file(
file_id: str, request: Request,
file_data: FileItem, fileId: str = Path(..., description="ID of the file to update"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) file_info: Dict[str, Any] = Body(...),
): currentUser: User = Depends(getCurrentUser)
""" ) -> FileItem:
Update file metadata """Update file info"""
"""
try: try:
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get the file from the database # Get the file from the database
file = managementInterface.getFile(file_id) file = managementInterface.getFile(fileId)
if not file: if not file:
raise HTTPException(status_code=404, detail="File not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found"
)
# Check if user has access to the file # Check if user has access to the file
if file.get("userId", 0) != currentUser.get("id", 0): if file.get("userId", 0) != currentUser.get("id", 0):
raise HTTPException(status_code=403, detail="Not authorized to update this file") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
# Convert FileItem to dict for interface detail="Not authorized to update this file"
update_data = file_data.model_dump() )
# Update the file # Update the file
result = managementInterface.updateFile(file_id, update_data) result = managementInterface.updateFile(fileId, file_info)
if not result: if not result:
raise HTTPException(status_code=500, detail="Failed to update file") raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update file"
)
# Get updated file and convert to FileItem # Get updated file and convert to FileItem
updatedFile = managementInterface.getFile(file_id) updatedFile = managementInterface.getFile(fileId)
return FileItem(**updatedFile) return FileItem(**updatedFile)
except HTTPException as he: except HTTPException as he:
raise he raise he
except Exception as e: except Exception as e:
logger.error(f"Error updating file: {str(e)}") logger.error(f"Error updating file: {str(e)}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_file( async def delete_file(
request: Request,
fileId: str, fileId: str,
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> JSONResponse:
"""Deletes a file by its ID from the database""" """Delete a file"""
try: try:
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -199,7 +214,9 @@ async def delete_file(
managementInterface.deleteFile(fileId) managementInterface.deleteFile(fileId)
# Return successful deletion without content (204 No Content) # Return successful deletion without content (204 No Content)
return Response(status_code=status.HTTP_204_NO_CONTENT) return JSONResponse({
"message": "File deleted successfully"
})
except serviceManagementClass.FileNotFoundError as e: except serviceManagementClass.FileNotFoundError as e:
logger.warning(f"File not found: {str(e)}") logger.warning(f"File not found: {str(e)}")
@ -229,8 +246,9 @@ async def delete_file(
@router.get("/stats", response_model=Dict[str, Any]) @router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_file_stats( async def get_file_stats(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Returns statistics about the stored files""" """Returns statistics about the stored files"""
try: try:
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -266,8 +284,9 @@ async def get_file_stats(
@router.get("/attributes", response_model=List[AttributeDefinition]) @router.get("/attributes", response_model=List[AttributeDefinition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_file_attributes( async def get_file_attributes(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> List[AttributeDefinition]:
""" """
Retrieves the attribute definitions for files. Retrieves the attribute definitions for files.
This can be used for dynamic form generation. This can be used for dynamic form generation.

View file

@ -1,3 +1,8 @@
"""
Mandate routes for the backend API.
Implements the endpoints for mandate management.
"""
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
@ -8,10 +13,10 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import Mandate, getModelAttributeDefinitions from modules.shared.attributeUtils import getModelAttributeDefinitions
# Import the model classes # Import the model classes
from modules.interfaces.serviceAppModel import AttributeDefinition from modules.interfaces.serviceAppModel import AttributeDefinition, Mandate, User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,13 +31,17 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("/", response_model=List[Dict[str, Any]], tags=["Mandates"]) @router.get("/", response_model=List[Mandate])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_mandates(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def get_mandates(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> List[Mandate]:
"""Get all mandates""" """Get all mandates"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
return appInterface.getMandates() mandates = managementInterface.getMandates()
return [Mandate.from_dict(mandate) for mandate in mandates]
except Exception as e: except Exception as e:
logger.error(f"Error getting mandates: {str(e)}") logger.error(f"Error getting mandates: {str(e)}")
raise HTTPException( raise HTTPException(
@ -40,24 +49,25 @@ async def get_mandates(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
detail=f"Failed to get mandates: {str(e)}" detail=f"Failed to get mandates: {str(e)}"
) )
@router.get("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"]) @router.get("/{mandateId}", response_model=Mandate)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_mandate( async def get_mandate(
mandateId: str, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) mandateId: str = Path(..., description="ID of the mandate"),
): currentUser: User = Depends(getCurrentUser)
) -> Mandate:
"""Get a specific mandate by ID""" """Get a specific mandate by ID"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
mandate = appInterface.getMandateById(mandateId) mandate = managementInterface.getMandate(mandateId)
if not mandate: if not mandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate {mandateId} not found" detail=f"Mandate with ID {mandateId} not found"
) )
return mandate return Mandate.from_dict(mandate)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -67,31 +77,30 @@ async def get_mandate(
detail=f"Failed to get mandate: {str(e)}" detail=f"Failed to get mandate: {str(e)}"
) )
@router.post("/", response_model=Mandate, tags=["Mandates"]) @router.post("/", response_model=Mandate)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_mandate( async def create_mandate(
mandateData: Mandate, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) mandateData: Mandate = Body(...),
): currentUser: User = Depends(getCurrentUser)
) -> Mandate:
"""Create a new mandate""" """Create a new mandate"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
try: # Convert Mandate to dict for interface
createdMandate = appInterface.createMandate(mandateData) mandate_data = mandateData.to_dict()
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
if not createdMandate: # Create mandate
newMandate = managementInterface.createMandate(mandate_data)
if not newMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create mandate" detail="Failed to create mandate"
) )
return createdMandate return Mandate.from_dict(newMandate)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -101,33 +110,31 @@ async def create_mandate(
detail=f"Failed to create mandate: {str(e)}" detail=f"Failed to create mandate: {str(e)}"
) )
@router.put("/{mandateId}", response_model=Mandate, tags=["Mandates"]) @router.put("/{mandateId}", response_model=Mandate)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_mandate( async def update_mandate(
mandateId: str, request: Request,
mandateData: Mandate, mandateId: str = Path(..., description="ID of the mandate to update"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) mandateData: Mandate = Body(...),
): currentUser: User = Depends(getCurrentUser)
) -> Mandate:
"""Update an existing mandate""" """Update an existing mandate"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Check if mandate exists # Check if mandate exists
existingMandate = appInterface.getMandateById(mandateId) existingMandate = managementInterface.getMandate(mandateId)
if not existingMandate: if not existingMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Mandate {mandateId} not found" detail=f"Mandate with ID {mandateId} not found"
) )
# Update mandate data # Convert Mandate to dict for interface
try: update_data = mandateData.to_dict()
updatedMandate = appInterface.updateMandate(mandateId, mandateData)
except ValueError as e: # Update mandate
raise HTTPException( updatedMandate = managementInterface.updateMandate(mandateId, update_data)
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
if not updatedMandate: if not updatedMandate:
raise HTTPException( raise HTTPException(
@ -135,7 +142,7 @@ async def update_mandate(
detail="Failed to update mandate" detail="Failed to update mandate"
) )
return updatedMandate return Mandate.from_dict(updatedMandate)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -145,12 +152,13 @@ async def update_mandate(
detail=f"Failed to update mandate: {str(e)}" detail=f"Failed to update mandate: {str(e)}"
) )
@router.delete("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"]) @router.delete("/{mandateId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_mandate( async def delete_mandate(
mandateId: str, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) mandateId: str = Path(..., description="ID of the mandate to delete"),
): currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Delete a mandate""" """Delete a mandate"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
@ -185,8 +193,9 @@ async def delete_mandate(
@router.get("/attributes", response_model=List[AttributeDefinition]) @router.get("/attributes", response_model=List[AttributeDefinition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_mandate_attributes( async def get_mandate_attributes(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> List[AttributeDefinition]:
""" """
Retrieves the attribute definitions for mandates. Retrieves the attribute definitions for mandates.
This can be used for dynamic form generation. This can be used for dynamic form generation.

View file

@ -10,7 +10,7 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import Prompt from modules.interfaces.serviceManagementModel import Prompt
from modules.interfaces.serviceAppModel import AttributeDefinition from modules.interfaces.serviceAppModel import AttributeDefinition, User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,24 +25,26 @@ router = APIRouter(
@router.get("", response_model=List[Prompt]) @router.get("", response_model=List[Prompt])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_prompts( async def get_prompts(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> List[Prompt]:
"""Get all prompts""" """Get all prompts"""
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
prompts = managementInterface.getAllPrompts() prompts = managementInterface.getAllPrompts()
return [Prompt(**prompt) for prompt in prompts] return [Prompt.from_dict(prompt) for prompt in prompts]
@router.post("", response_model=Prompt) @router.post("", response_model=Prompt)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_prompt( async def create_prompt(
request: Request,
prompt: Prompt, prompt: Prompt,
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Prompt:
"""Create a new prompt""" """Create a new prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Convert Prompt to dict for interface # Convert Prompt to dict for interface
prompt_data = prompt.model_dump() prompt_data = prompt.to_dict()
# Create prompt # Create prompt
newPrompt = managementInterface.createPrompt(prompt_data) newPrompt = managementInterface.createPrompt(prompt_data)
@ -51,14 +53,15 @@ async def create_prompt(
if "createdAt" in Prompt.getModelAttributeDefinitions() and hasattr(newPrompt, "createdAt"): if "createdAt" in Prompt.getModelAttributeDefinitions() and hasattr(newPrompt, "createdAt"):
newPrompt["createdAt"] = datetime.now().isoformat() newPrompt["createdAt"] = datetime.now().isoformat()
return Prompt(**newPrompt) return Prompt.from_dict(newPrompt)
@router.get("/{promptId}", response_model=Prompt) @router.get("/{promptId}", response_model=Prompt)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_prompt( async def get_prompt(
request: Request,
promptId: str = Path(..., description="ID of the prompt"), promptId: str = Path(..., description="ID of the prompt"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Prompt:
"""Get a specific prompt""" """Get a specific prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -70,15 +73,16 @@ async def get_prompt(
detail=f"Prompt with ID {promptId} not found" detail=f"Prompt with ID {promptId} not found"
) )
return Prompt(**prompt) return Prompt.from_dict(prompt)
@router.put("/{promptId}", response_model=Prompt) @router.put("/{promptId}", response_model=Prompt)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_prompt( async def update_prompt(
request: Request,
promptId: str = Path(..., description="ID of the prompt to update"), promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Prompt = Body(...), promptData: Prompt = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Prompt:
"""Update an existing prompt""" """Update an existing prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -91,7 +95,7 @@ async def update_prompt(
) )
# Convert Prompt to dict for interface # Convert Prompt to dict for interface
update_data = promptData.model_dump() update_data = promptData.to_dict()
# Update prompt # Update prompt
updatedPrompt = managementInterface.updatePrompt(promptId, update_data) updatedPrompt = managementInterface.updatePrompt(promptId, update_data)
@ -102,14 +106,15 @@ async def update_prompt(
detail="Error updating the prompt" detail="Error updating the prompt"
) )
return Prompt(**updatedPrompt) return Prompt.from_dict(updatedPrompt)
@router.delete("/{promptId}", response_model=Dict[str, Any]) @router.delete("/{promptId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_prompt( async def delete_prompt(
request: Request,
promptId: str = Path(..., description="ID of the prompt to delete"), promptId: str = Path(..., description="ID of the prompt to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Dict[str, Any]:
"""Delete a prompt""" """Delete a prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
@ -133,8 +138,9 @@ async def delete_prompt(
@router.get("/attributes", response_model=List[AttributeDefinition]) @router.get("/attributes", response_model=List[AttributeDefinition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_prompt_attributes( async def get_prompt_attributes(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> List[AttributeDefinition]:
""" """
Retrieves the attribute definitions for prompts. Retrieves the attribute definitions for prompts.
This can be used for dynamic form generation. This can be used for dynamic form generation.

View file

@ -1,3 +1,8 @@
"""
User routes for the backend API.
Implements the endpoints for user management.
"""
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
@ -13,8 +18,8 @@ import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.security.auth import getCurrentUser, limiter, getCurrentUser from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.interfaces.serviceManagementModel import User, AttributeDefinition, getModelAttributeDefinitions from modules.interfaces.serviceAppModel import User, AttributeDefinition as ServiceAppAttributeDefinition
from modules.interfaces.serviceAppModel import AttributeDefinition as ServiceAppAttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,12 +30,17 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("/", response_model=List[Dict[str, Any]], tags=["Users"]) @router.get("/", response_model=List[User])
async def get_users(currentUser: Dict[str, Any] = Depends(getCurrentUser)): @limiter.limit("30/minute")
async def get_users(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> List[User]:
"""Get all users in the current mandate""" """Get all users in the current mandate"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
return appInterface.getUsers() users = managementInterface.getUsers()
return [User.from_dict(user) for user in users]
except Exception as e: except Exception as e:
logger.error(f"Error getting users: {str(e)}") logger.error(f"Error getting users: {str(e)}")
raise HTTPException( raise HTTPException(
@ -38,23 +48,25 @@ async def get_users(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
detail=f"Failed to get users: {str(e)}" detail=f"Failed to get users: {str(e)}"
) )
@router.get("/{userId}", response_model=Dict[str, Any], tags=["Users"]) @router.get("/{userId}", response_model=User)
@limiter.limit("30/minute")
async def get_user( async def get_user(
userId: str, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) userId: str = Path(..., description="ID of the user"),
): currentUser: User = Depends(getCurrentUser)
) -> User:
"""Get a specific user by ID""" """Get a specific user by ID"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
user = appInterface.getUserById(userId) user = managementInterface.getUser(userId)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {userId} not found" detail=f"User with ID {userId} not found"
) )
return user return User.from_dict(user)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -64,98 +76,68 @@ async def get_user(
detail=f"Failed to get user: {str(e)}" detail=f"Failed to get user: {str(e)}"
) )
@router.post("/", response_model=User, tags=["Users"]) @router.post("", response_model=User)
@limiter.limit("10/minute")
async def create_user( async def create_user(
userData: User, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) user: User,
): currentUser: User = Depends(getCurrentUser)
) -> User:
"""Create a new user""" """Create a new user"""
try: managementInterface = serviceManagementClass.getInterface(currentUser)
# Get interface for user creation
appInterface = serviceManagementClass.getInterface(currentUser)
try: # Convert User to dict for interface
# Convert User model to dict and pass to createUser user_data = user.to_dict()
createdUser = appInterface.createUser(
username=userData.username,
email=userData.email,
fullName=userData.fullName,
language=userData.language,
disabled=userData.disabled,
privilege=userData.privilege,
authenticationAuthority=userData.authenticationAuthority
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
if not createdUser: # Create user
raise HTTPException( newUser = managementInterface.createUser(user_data)
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user"
)
return createdUser # Set current time for createdAt if it exists in the model
except HTTPException: if "createdAt" in User.getModelAttributeDefinitions() and hasattr(newUser, "createdAt"):
raise newUser["createdAt"] = datetime.now().isoformat()
except Exception as e:
logger.error(f"Error creating user: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create user: {str(e)}"
)
@router.put("/{userId}", response_model=User, tags=["Users"]) return User.from_dict(newUser)
@router.put("/{userId}", response_model=User)
@limiter.limit("10/minute")
async def update_user( async def update_user(
userId: str, request: Request,
userData: User, userId: str = Path(..., description="ID of the user to update"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) userData: User = Body(...),
): currentUser: User = Depends(getCurrentUser)
) -> User:
"""Update an existing user""" """Update an existing user"""
try: managementInterface = serviceManagementClass.getInterface(currentUser)
# Get interface for user updates
appInterface = serviceManagementClass.getInterface(currentUser)
# Check if user exists # Check if the user exists
existingUser = appInterface.getUserById(userId) existingUser = managementInterface.getUser(userId)
if not existingUser: if not existingUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"User {userId} not found" detail=f"User with ID {userId} not found"
) )
# Update user data # Convert User to dict for interface
try: update_data = userData.to_dict()
updatedUser = appInterface.updateUser(userId, userData)
except ValueError as e: # Update user
raise HTTPException( updatedUser = managementInterface.updateUser(userId, update_data)
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
if not updatedUser: if not updatedUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user" detail="Error updating the user"
) )
return updatedUser return User.from_dict(updatedUser)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating user {userId}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update user: {str(e)}"
)
@router.delete("/{userId}", response_model=Dict[str, Any], tags=["Users"]) @router.delete("/{userId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_user( async def delete_user(
userId: str, request: Request,
currentUser: Dict[str, Any] = Depends(getCurrentUser) userId: str = Path(..., description="ID of the user to delete"),
): currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Delete a user""" """Delete a user"""
try: try:
appInterface = serviceManagementClass.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
@ -176,8 +158,9 @@ async def delete_user(
@router.get("/attributes", response_model=List[ServiceAppAttributeDefinition]) @router.get("/attributes", response_model=List[ServiceAppAttributeDefinition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_user_attributes( async def get_user_attributes(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> List[ServiceAppAttributeDefinition]:
""" """
Retrieves the attribute definitions for users. Retrieves the attribute definitions for users.
This can be used for dynamic form generation. This can be used for dynamic form generation.

View file

@ -10,12 +10,12 @@ from typing import Dict, Any, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow from google_auth_oauthlib.flow import Flow
from google.auth.transport.requests import Request from google.auth.transport.requests import Request as GoogleRequest
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import AuthAuthority from modules.interfaces.serviceAppModel import AuthAuthority, User
from modules.interfaces.serviceAppTokens import GoogleToken, saveToken from modules.interfaces.serviceAppTokens import GoogleToken
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
# Configure logger # Configure logger
@ -46,7 +46,7 @@ SCOPES = [
@router.get("/login") @router.get("/login")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def login(): async def login(request: Request) -> RedirectResponse:
"""Initiate Google login""" """Initiate Google login"""
try: try:
# Create OAuth flow # Create OAuth flow
@ -79,7 +79,7 @@ async def login():
) )
@router.get("/auth/callback") @router.get("/auth/callback")
async def auth_callback(code: str, request: Request): async def auth_callback(code: str, request: Request) -> HTMLResponse:
"""Handle Google OAuth callback""" """Handle Google OAuth callback"""
try: try:
# Create OAuth flow # Create OAuth flow
@ -111,7 +111,7 @@ async def auth_callback(code: str, request: Request):
# Save token data # Save token data
appInterface = getInterface() appInterface = getInterface()
saveToken(appInterface, "Google", token_data) appInterface.saveToken("Google", token_data)
# Return success page with token data # Return success page with token data
return HTMLResponse( return HTMLResponse(
@ -141,24 +141,36 @@ async def auth_callback(code: str, request: Request):
detail=f"Authentication failed: {str(e)}" detail=f"Authentication failed: {str(e)}"
) )
@router.post("/logout") @router.get("/me", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def get_current_user(
"""Logout from Google""" request: Request,
currentUser: User = Depends(getCurrentUser)
) -> User:
"""Get current user information"""
try: try:
# Get user interface return currentUser
appInterface = getInterface() except Exception as e:
logger.error(f"Error getting current user: {str(e)}")
# Revoke all sessions for the user raise HTTPException(
appInterface.revokeAllUserSessions(currentUser.get("id")) status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get current user: {str(e)}"
return JSONResponse({ )
"message": "Successfully logged out from Google"
})
@router.post("/logout")
@limiter.limit("10/minute")
async def logout(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Logout current user"""
try:
appInterface = getInterface(currentUser)
appInterface.logout()
return {"message": "Logged out successfully"}
except Exception as e: except Exception as e:
logger.error(f"Error during logout: {str(e)}") logger.error(f"Error during logout: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}" detail=f"Failed to logout: {str(e)}"
) )

View file

@ -2,18 +2,19 @@
Routes for local security and authentication. Routes for local security and authentication.
""" """
from fastapi import APIRouter, HTTPException, status, Depends, Request from fastapi import APIRouter, HTTPException, status, Depends, Request, Response, Body
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
import logging import logging
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse
from pydantic import BaseModel
# Import auth modules # Import auth modules
from modules.security.auth import createAccessToken, getCurrentUser, limiter from modules.security.auth import createAccessToken, getCurrentUser, limiter
from modules.interfaces.serviceAppClass import getInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import User, AuthAuthority from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege
from modules.interfaces.serviceAppTokens import LocalToken, saveToken from modules.interfaces.serviceAppTokens import LocalToken
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,7 +37,7 @@ router = APIRouter(
async def login( async def login(
request: Request, request: Request,
formData: OAuth2PasswordRequestForm = Depends(), formData: OAuth2PasswordRequestForm = Depends(),
): ) -> Dict[str, Any]:
"""Get access token for local user authentication""" """Get access token for local user authentication"""
try: try:
# Validate CSRF token # Validate CSRF token
@ -47,11 +48,22 @@ async def login(
detail="CSRF token missing" detail="CSRF token missing"
) )
# Get gateway interface # Get gateway interface with root privileges for authentication
appInterface = getInterface() rootInterface = getRootInterface()
# Get default mandate ID
defaultMandateId = rootInterface.getInitialId("mandates")
if not defaultMandateId:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="No default mandate found"
)
# Set the mandate ID on the interface
rootInterface.mandateId = defaultMandateId
# Authenticate user # Authenticate user
user = appInterface.authenticateLocalUser( user = rootInterface.authenticateLocalUser(
username=formData.username, username=formData.username,
password=formData.password password=formData.password
) )
@ -79,13 +91,16 @@ async def login(
detail="Failed to create access token" detail="Failed to create access token"
) )
# Get user-specific interface for token operations
userInterface = getInterface(user)
# Save token data # Save token data
token_data = { token_data = {
"access_token": access_token, "access_token": access_token,
"token_type": "bearer", "token_type": "bearer",
"expires_at": expires_at.timestamp() "expires_at": expires_at.timestamp()
} }
saveToken(appInterface, "Local", token_data) userInterface.saveToken("Local", token_data)
# Create response data # Create response data
response_data = { response_data = {
@ -115,11 +130,16 @@ async def login(
) )
@router.post("/register", response_model=User) @router.post("/register", response_model=User)
async def register_user(userData: User): @limiter.limit("10/minute")
async def register_user(
request: Request,
userData: User = Body(...),
password: str = Body(..., embed=True)
) -> User:
"""Register a new local user.""" """Register a new local user."""
try: try:
# Get gateway interface # Get gateway interface with root privileges since this is a public endpoint
appInterface = getInterface() appInterface = getRootInterface()
# Get default mandate ID # Get default mandate ID
defaultMandateId = appInterface.getInitialId("mandates") defaultMandateId = appInterface.getInitialId("mandates")
@ -129,22 +149,28 @@ async def register_user(userData: User):
detail="No default mandate found" detail="No default mandate found"
) )
# Create user with default mandate # Set the mandate ID on the interface
user = appInterface.createUser( appInterface.mandateId = defaultMandateId
# Create user with individual parameters
newUser = appInterface.createUser(
username=userData.username, username=userData.username,
password=userData.password, password=password, # Pass the plain text password - createUser will hash it
email=userData.email, email=userData.email,
mandateId=defaultMandateId, # Use default mandate instead of userData.mandateId fullName=userData.fullName,
authenticationAuthority=AuthAuthority.LOCAL language=userData.language,
disabled=userData.disabled,
privilege=userData.privilege,
authenticationAuthority=userData.authenticationAuthority
) )
if not user: if not newUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to register user" detail="Failed to register user"
) )
return user return newUser
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
@ -158,22 +184,32 @@ async def register_user(userData: User):
detail=f"Failed to register user: {str(e)}" detail=f"Failed to register user: {str(e)}"
) )
@router.get("/me", response_model=Dict[str, Any]) @router.get("/me", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def read_user_me(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def read_user_me(
"""Get current user information""" request: Request,
currentUser: User = Depends(getCurrentUser)
) -> User:
"""Get current user info"""
try:
return currentUser return currentUser
except Exception as e:
logger.error(f"Error getting user me: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get current user: {str(e)}"
)
@router.post("/logout") @router.post("/logout")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def logout(request: Request, currentUser: User = Depends(getCurrentUser)) -> JSONResponse:
"""Logout from local authentication""" """Logout from local authentication"""
try: try:
# Get user interface # Get user interface with current user context
appInterface = getInterface() appInterface = getInterface(currentUser)
# Revoke all sessions for the user # Revoke all sessions for the user
appInterface.revokeAllUserSessions(currentUser.get("id")) appInterface.revokeAllUserSessions(currentUser.id)
return JSONResponse({ return JSONResponse({
"message": "Successfully logged out" "message": "Successfully logged out"
@ -186,15 +222,31 @@ async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
detail=f"Logout failed: {str(e)}" detail=f"Logout failed: {str(e)}"
) )
@router.get("/available", response_model=Dict[str, Any]) @router.get("/available")
@limiter.limit("10/minute")
async def check_username_availability( async def check_username_availability(
request: Request,
username: str, username: str,
authenticationAuthority: str = "local" authenticationAuthority: str = "local"
): ) -> Dict[str, Any]:
"""Check if a username is available for registration""" """Check if a username is available for registration."""
try: try:
interfaceRoot = getInterface() # Get root interface
return interfaceRoot.checkUsernameAvailability(username, authenticationAuthority) appInterface = getRootInterface()
# Use the interface's method to check availability
result = appInterface.checkUsernameAvailability({
"username": username,
"authenticationAuthority": authenticationAuthority
})
return {
"username": username,
"authenticationAuthority": authenticationAuthority,
"available": result["available"],
"message": result["message"]
}
except Exception as e: except Exception as e:
logger.error(f"Error checking username availability: {str(e)}") logger.error(f"Error checking username availability: {str(e)}")
raise HTTPException( raise HTTPException(

View file

@ -2,7 +2,7 @@
Routes for Microsoft authentication. Routes for Microsoft authentication.
""" """
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging import logging
import json import json
@ -11,9 +11,9 @@ from datetime import datetime, timedelta
import msal import msal
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import AuthAuthority from modules.interfaces.serviceAppModel import AuthAuthority, User
from modules.interfaces.serviceAppTokens import MsftToken, saveToken from modules.interfaces.serviceAppTokens import MsftToken
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
# Configure logger # Configure logger
@ -42,7 +42,7 @@ SCOPES = ["Mail.ReadWrite", "User.Read"]
@router.get("/login") @router.get("/login")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def login(): async def login(request: Request) -> RedirectResponse:
"""Initiate Microsoft login""" """Initiate Microsoft login"""
try: try:
# Create MSAL app # Create MSAL app
@ -68,7 +68,7 @@ async def login():
) )
@router.get("/auth/callback") @router.get("/auth/callback")
async def auth_callback(code: str, request: Request): async def auth_callback(code: str, request: Request) -> HTMLResponse:
"""Handle Microsoft OAuth callback""" """Handle Microsoft OAuth callback"""
try: try:
# Create MSAL app # Create MSAL app
@ -101,7 +101,7 @@ async def auth_callback(code: str, request: Request):
# Save token data # Save token data
appInterface = getInterface() appInterface = getInterface()
saveToken(appInterface, "Msft", token_data) appInterface.saveToken("Msft", token_data)
# Return success page with token data # Return success page with token data
return HTMLResponse( return HTMLResponse(
@ -131,24 +131,36 @@ async def auth_callback(code: str, request: Request):
detail=f"Authentication failed: {str(e)}" detail=f"Authentication failed: {str(e)}"
) )
@router.post("/logout") @router.get("/me", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)): async def get_current_user(
"""Logout from Microsoft""" request: Request,
currentUser: User = Depends(getCurrentUser)
) -> User:
"""Get current user information"""
try: try:
# Get user interface return currentUser
appInterface = getInterface() except Exception as e:
logger.error(f"Error getting current user: {str(e)}")
# Revoke all sessions for the user raise HTTPException(
appInterface.revokeAllUserSessions(currentUser.get("id")) status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get current user: {str(e)}"
return JSONResponse({ )
"message": "Successfully logged out from Microsoft"
})
@router.post("/logout")
@limiter.limit("10/minute")
async def logout(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Logout current user"""
try:
appInterface = getInterface(currentUser)
appInterface.logout()
return {"message": "Logged out successfully"}
except Exception as e: except Exception as e:
logger.error(f"Error during logout: {str(e)}") logger.error(f"Error during logout: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}" detail=f"Failed to logout: {str(e)}"
) )

View file

@ -7,14 +7,16 @@ import os
import json import json
import logging import logging
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status, Request
from datetime import datetime from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from datetime import datetime, timedelta
# Import auth modules # Import auth modules
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceChatClass as serviceChatClass import modules.interfaces.serviceChatClass as serviceChatClass
from modules.interfaces.serviceChatClass import getInterface
# Import workflow manager # Import workflow manager
from modules.workflow.workflowManager import getWorkflowManager from modules.workflow.workflowManager import getWorkflowManager
@ -26,10 +28,10 @@ from modules.interfaces.serviceChatModel import (
ChatLog, ChatLog,
ChatStat, ChatStat,
ChatDocument, ChatDocument,
UserInputRequest, UserInputRequest
Workflow,
getModelAttributeDefinitions
) )
from modules.shared.attributeUtils import getModelAttributeDefinitions
from modules.interfaces.serviceAppModel import User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,34 +60,32 @@ def createServiceContainer(currentUser: Dict[str, Any]):
return service return service
# API Endpoint for getting all workflows # API Endpoint for getting all workflows
@router.get("", response_model=List[ChatWorkflow]) @router.get("/list", response_model=List[ChatWorkflow])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def list_workflows( async def list_workflows(
currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request,
): currentUser: User = Depends(getCurrentUser)
) -> List[ChatWorkflow]:
"""List all workflows for the current user.""" """List all workflows for the current user."""
try: try:
# Get service container appInterface = getInterface(currentUser)
service = createServiceContainer(currentUser) return appInterface.getAllWorkflows()
# Retrieve workflows for the user
workflows = service.base.getWorkflowsByUser(currentUser["id"])
return [ChatWorkflow(**workflow) for workflow in workflows]
except Exception as e: except Exception as e:
logger.error(f"Error listing workflows: {str(e)}", exc_info=True) logger.error(f"Error listing workflows: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error listing workflows: {str(e)}" detail=f"Failed to list workflows: {str(e)}"
) )
# State 1: Workflow Initialization endpoint # State 1: Workflow Initialization endpoint
@router.post("/start", response_model=ChatWorkflow) @router.post("/start", response_model=ChatWorkflow)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def start_workflow( async def start_workflow(
request: Request,
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"), workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
userInput: UserInputRequest = Body(...), userInput: UserInputRequest = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> ChatWorkflow:
""" """
Starts a new workflow or continues an existing one. Starts a new workflow or continues an existing one.
Corresponds to State 1 in the state machine documentation. Corresponds to State 1 in the state machine documentation.
@ -100,19 +100,23 @@ async def start_workflow(
# Start or continue workflow # Start or continue workflow
workflow = await workflowManager.workflowStart(userInput, workflowId) workflow = await workflowManager.workflowStart(userInput, workflowId)
return workflow return ChatWorkflow(**workflow)
except Exception as e: except Exception as e:
logger.error(f"Error in start_workflow: {str(e)}") logger.error(f"Error in start_workflow: {str(e)}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
# State 8: Workflow Stopped endpoint # State 8: Workflow Stopped endpoint
@router.post("/{workflowId}/stop", response_model=ChatWorkflow) @router.post("/{workflowId}/stop", response_model=ChatWorkflow)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def stop_workflow( async def stop_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow to stop"), workflowId: str = Path(..., description="ID of the workflow to stop"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> ChatWorkflow:
"""Stops a running workflow.""" """Stops a running workflow."""
try: try:
# Get service container # Get service container
@ -124,19 +128,23 @@ async def stop_workflow(
# Stop workflow # Stop workflow
workflow = await workflowManager.workflowStop(workflowId) workflow = await workflowManager.workflowStop(workflowId)
return workflow return ChatWorkflow(**workflow)
except Exception as e: except Exception as e:
logger.error(f"Error in stop_workflow: {str(e)}") logger.error(f"Error in stop_workflow: {str(e)}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
# State 11: Workflow Reset/Deletion endpoint # State 11: Workflow Reset/Deletion endpoint
@router.delete("/{workflowId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_workflow( async def delete_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow to delete"), workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Dict[str, Any]:
"""Deletes a workflow and its associated data.""" """Deletes a workflow and its associated data."""
try: try:
# Get service container # Get service container
@ -183,9 +191,10 @@ async def delete_workflow(
@router.get("/{workflowId}/status", response_model=ChatWorkflow) @router.get("/{workflowId}/status", response_model=ChatWorkflow)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_workflow_status( async def get_workflow_status(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> ChatWorkflow:
"""Get the current status of a workflow.""" """Get the current status of a workflow."""
try: try:
# Get service container # Get service container
@ -213,10 +222,11 @@ async def get_workflow_status(
@router.get("/{workflowId}/logs", response_model=List[ChatLog]) @router.get("/{workflowId}/logs", response_model=List[ChatLog])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_workflow_logs( async def get_workflow_logs(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"), logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> List[ChatLog]:
"""Get logs for a workflow with support for selective data transfer.""" """Get logs for a workflow with support for selective data transfer."""
try: try:
# Get service container # Get service container
@ -255,10 +265,11 @@ async def get_workflow_logs(
@router.get("/{workflowId}/messages", response_model=List[ChatMessage]) @router.get("/{workflowId}/messages", response_model=List[ChatMessage])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_workflow_messages( async def get_workflow_messages(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"), messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> List[ChatMessage]:
"""Get messages for a workflow with support for selective data transfer.""" """Get messages for a workflow with support for selective data transfer."""
try: try:
# Get service container # Get service container
@ -277,20 +288,12 @@ async def get_workflow_messages(
# Apply selective data transfer if messageId is provided # Apply selective data transfer if messageId is provided
if messageId: if messageId:
# Find the index of the specified message based on messageIds array # Find the index of the message with the given ID
messageIds = workflow.get("messageIds", []) messageIndex = next((i for i, msg in enumerate(allMessages) if msg.get("id") == messageId), -1)
if messageId in messageIds: if messageIndex >= 0:
messageIndex = messageIds.index(messageId) # Return only messages after the specified message
# Return messages from this index onwards based on the messageIds order return [ChatMessage(**msg) for msg in allMessages[messageIndex + 1:]]
filteredMessages = []
for msgId in messageIds[messageIndex:]:
message = next((msg for msg in allMessages if msg.get("id") == msgId), None)
if message:
filteredMessages.append(message)
return [ChatMessage(**msg) for msg in filteredMessages]
# Sort messages by sequenceNo
allMessages.sort(key=lambda x: x.get("sequenceNo", 0))
return [ChatMessage(**msg) for msg in allMessages] return [ChatMessage(**msg) for msg in allMessages]
except HTTPException: except HTTPException:
raise raise
@ -306,16 +309,17 @@ async def get_workflow_messages(
@router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_workflow_message( async def delete_workflow_message(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message to delete"), messageId: str = Path(..., description="ID of the message to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Dict[str, Any]:
"""Delete a message from a workflow.""" """Delete a message from a workflow."""
try: try:
# Get service container # Get service container
service = createServiceContainer(currentUser) service = createServiceContainer(currentUser)
# Verify workflow exists and belongs to user # Verify workflow exists
workflow = service.base.getWorkflow(workflowId) workflow = service.base.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
@ -355,17 +359,18 @@ async def delete_workflow_message(
@router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_file_from_message( async def delete_file_from_message(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"), messageId: str = Path(..., description="ID of the message"),
fileId: str = Path(..., description="ID of the file to delete"), fileId: str = Path(..., description="ID of the file to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Dict[str, Any]:
"""Delete a file reference from a message in a workflow.""" """Delete a file reference from a message in a workflow."""
try: try:
# Get service container # Get service container
service = createServiceContainer(currentUser) service = createServiceContainer(currentUser)
# Verify workflow exists and belongs to user # Verify workflow exists
workflow = service.base.getWorkflow(workflowId) workflow = service.base.getWorkflow(workflowId)
if not workflow: if not workflow:
raise HTTPException( raise HTTPException(
@ -402,89 +407,24 @@ async def delete_file_from_message(
@router.get("/files/{fileId}/preview", response_model=ChatDocument) @router.get("/files/{fileId}/preview", response_model=ChatDocument)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def preview_file( async def preview_file(
request: Request,
fileId: str = Path(..., description="ID of the file to preview"), fileId: str = Path(..., description="ID of the file to preview"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> ChatDocument:
"""Get file metadata and a preview of the file content.""" """Preview a file's content."""
try: try:
# Get service container # Get service container
service = createServiceContainer(currentUser) service = createServiceContainer(currentUser)
# Get file metadata # Get file document
file = service.base.getFile(fileId) document = service.base.getFileDocument(fileId)
if not file: if not document:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found" detail=f"File with ID {fileId} not found"
) )
# Get file data (limited for preview) return ChatDocument(**document)
fileData = service.base.getFileData(fileId)
if fileData is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File data not found for file ID {fileId}"
)
# For text-based files, return a preview of the content
mimeType = file.get("mimeType", "application/octet-stream")
isText = mimeType.startswith("text/") or mimeType in [
"application/json",
"application/xml",
"application/javascript"
]
previewData = None
# Get base64Encoded flag from database
fileDataEntries = service.base.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries and "base64Encoded" in fileDataEntries[0]:
# Use the flag from the database
base64Encoded = fileDataEntries[0]["base64Encoded"]
else:
# Determine based on file type (fallback for older data)
base64Encoded = not isText
if isText:
# Convert to string without trim for preview
if isinstance(fileData, bytes):
try:
filePreview = fileData.decode('utf-8')
previewData = filePreview
except UnicodeDecodeError:
# Try other encodings
for encoding in ['latin-1', 'cp1252', 'iso-8859-1']:
try:
filePreview = fileData.decode(encoding)
previewData = filePreview
break
except UnicodeDecodeError:
continue
# For images, return base64 encoded data
if mimeType.startswith("image/"):
import base64
previewData = base64.b64encode(fileData).decode('utf-8')
base64Encoded = True
# Create ChatDocument instance
return ChatDocument(
id=fileId,
fileId=fileId,
fileName=file.get("name"),
fileSize=file.get("size"),
mimeType=mimeType,
contents=[{
"sequenceNr": 1,
"name": file.get("name"),
"mimeType": mimeType,
"data": previewData,
"metadata": {
"base64Encoded": base64Encoded,
"isPreviewable": isText or mimeType.startswith("image/")
}
}]
)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -497,9 +437,10 @@ async def preview_file(
@router.get("/files/{fileId}/download") @router.get("/files/{fileId}/download")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def download_file( async def download_file(
request: Request,
fileId: str = Path(..., description="ID of the file to download"), fileId: str = Path(..., description="ID of the file to download"),
currentUser: Dict[str, Any] = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ) -> Response:
"""Download a file.""" """Download a file."""
try: try:
# Get service container # Get service container
@ -529,3 +470,54 @@ async def download_file(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading file: {str(e)}" detail=f"Error downloading file: {str(e)}"
) )
@router.get("/workflows", response_model=List[ChatWorkflow])
@limiter.limit("30/minute")
async def get_workflows(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> List[ChatWorkflow]:
"""Get all workflows for current user"""
try:
# Get workflow interface with current user context
workflowInterface = getInterface(currentUser)
# Get workflows
workflows = workflowInterface.getWorkflows()
return workflows
except Exception as e:
logger.error(f"Error getting workflows: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get workflows: {str(e)}"
)
@router.get("/workflows/{workflow_id}", response_model=ChatWorkflow)
@limiter.limit("30/minute")
async def get_workflow(
request: Request,
workflow_id: str,
currentUser: User = Depends(getCurrentUser)
) -> ChatWorkflow:
"""Get workflow by ID"""
try:
# Get workflow interface with current user context
workflowInterface = getInterface(currentUser)
# Get workflow
workflow = workflowInterface.getWorkflow(workflow_id)
if not workflow:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Workflow not found"
)
return workflow
except Exception as e:
logger.error(f"Error getting workflow: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get workflow: {str(e)}"
)

View file

@ -14,7 +14,7 @@ from slowapi.util import get_remote_address
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getRootInterface from modules.interfaces.serviceAppClass import getRootInterface
from modules.interfaces.serviceAppModel import Session, AuthEvent, UserPrivilege from modules.interfaces.serviceAppModel import Session, AuthEvent, UserPrivilege, User
# Get Config Data # Get Config Data
SECRET_KEY = APP_CONFIG.get("APP_JWT_SECRET_SECRET") SECRET_KEY = APP_CONFIG.get("APP_JWT_SECRET_SECRET")
@ -72,7 +72,7 @@ def createRefreshToken(data: dict) -> Tuple[str, datetime]:
return encodedJwt, expire return encodedJwt, expire
def _getUserBase(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]: def _getUserBase(token: str = Depends(oauth2Scheme)) -> User:
""" """
Extracts and validates the current user from the JWT token. Extracts and validates the current user from the JWT token.
@ -80,7 +80,7 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
token: JWT Token from the Authorization header token: JWT Token from the Authorization header
Returns: Returns:
User data User model instance
Raises: Raises:
HTTPException: For invalid token or user HTTPException: For invalid token or user
@ -122,20 +122,20 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
logger.warning(f"User {username} not found") logger.warning(f"User {username} not found")
raise credentialsException raise credentialsException
if user.get("disabled", False): if user.disabled:
logger.warning(f"User {username} is disabled") logger.warning(f"User {username} is disabled")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
# Ensure the user has the correct context # Ensure the user has the correct context
if str(user.get("mandateId")) != str(mandateId) or str(user.get("id")) != str(userId): if str(user.mandateId) != str(mandateId) or str(user.id) != str(userId):
logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.get('mandateId')}, id={user.get('id')})") logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.mandateId}, id={user.id})")
raise credentialsException raise credentialsException
return user return user
def getCurrentUser(currentUser: Dict[str, Any] = Depends(_getUserBase)) -> Dict[str, Any]: def getCurrentUser(currentUser: User = Depends(_getUserBase)) -> User:
"""Get current active user with additional validation.""" """Get current active user with additional validation."""
if currentUser.get("disabled", False): if currentUser.disabled:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="User is disabled" detail="User is disabled"
@ -155,7 +155,17 @@ def createUserSession(userId: str, tokenId: str, request: Request) -> Session:
) )
# Save session to database # Save session to database
appInterface.db.recordCreate("sessions", session.model_dump()) appInterface.db.recordCreate("sessions", session.to_dict())
# Log auth event
event = AuthEvent(
userId=userId,
eventType="login",
details={"method": "local"},
ipAddress=request.client.host if request.client else None,
userAgent=request.headers.get("user-agent")
)
appInterface.db.recordCreate("auth_events", event.to_dict())
return session return session
@ -172,7 +182,7 @@ def logAuthEvent(userId: str, eventType: str, details: Dict[str, Any], request:
) )
# Save event to database # Save event to database
appInterface.db.recordCreate("auth_events", event.model_dump()) appInterface.db.recordCreate("auth_events", event.to_dict())
def validateSession(sessionId: str) -> bool: def validateSession(sessionId: str) -> bool:
"""Validate a user session.""" """Validate a user session."""

View file

@ -21,7 +21,10 @@ class BaseModelWithUI(BaseModel):
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary with proper validation""" """Convert to dictionary with proper validation"""
return self.model_dump() # Handle both Pydantic v1 and v2
if hasattr(self, 'model_dump'):
return self.model_dump() # Pydantic v2
return self.dict() # Pydantic v1
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI': def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI':
@ -29,23 +32,47 @@ class BaseModelWithUI(BaseModel):
return cls(**data) return cls(**data)
@classmethod @classmethod
def getModelAttributeDefinitions(cls) -> Dict[str, Any]: def getModelAttributeDefinitions(cls) -> List[Dict[str, Any]]:
""" """
Get attribute definitions for this model class. Get attribute definitions for this model class.
Override this method in model classes to provide custom attribute definitions. Override this method in model classes to provide custom attribute definitions.
Returns: Returns:
Dict[str, Any]: Dictionary of attribute definitions List[Dict[str, Any]]: List of attribute definitions
""" """
return { attributes = []
name: {
# Handle both Pydantic v1 and v2
if hasattr(cls, 'model_fields'): # Pydantic v2
fields = cls.model_fields
for name, field in fields.items():
attributes.append({
"name": name,
"type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation), "type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
"required": field.is_required() if hasattr(field, "is_required") else True, "required": field.is_required() if hasattr(field, "is_required") else True,
"description": field.description if hasattr(field, "description") else "", "description": field.description if hasattr(field, "description") else "",
"label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name "label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
} "placeholder": f"Please enter {name}",
for name, field in cls.model_fields.items() "editable": True,
} "visible": True,
"order": len(attributes)
})
else: # Pydantic v1
fields = cls.__fields__
for name, field in fields.items():
attributes.append({
"name": name,
"type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
"required": field.required,
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
"label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
"placeholder": f"Please enter {name}",
"editable": True,
"visible": True,
"order": len(attributes)
})
return attributes
def getModelAttributes(modelClass): def getModelAttributes(modelClass):
""" """

View file

@ -10,7 +10,14 @@ import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from pathlib import Path from pathlib import Path
# Set up logging # Set up basic logging for configuration loading
logging.basicConfig(
level=logging.WARNING,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
handlers=[logging.StreamHandler()]
)
# Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Configuration: class Configuration:
@ -34,11 +41,8 @@ class Configuration:
def _loadConfig(self): def _loadConfig(self):
"""Load configuration from config.ini file in flattened format""" """Load configuration from config.ini file in flattened format"""
# Find config.ini file (look in current directory and parent directory) # Find config.ini file in the gateway directory
configPath = Path('config.ini') configPath = Path(__file__).parent.parent.parent / 'config.ini'
if not configPath.exists():
# Try in parent directory
configPath = Path('../config.ini')
if not configPath.exists(): if not configPath.exists():
logger.warning(f"Configuration file not found at {configPath.absolute()}") logger.warning(f"Configuration file not found at {configPath.absolute()}")
return return
@ -75,11 +79,8 @@ class Configuration:
def _loadEnv(self): def _loadEnv(self):
"""Load environment variables from .env file""" """Load environment variables from .env file"""
# Find .env file (look in current directory and parent directory) # Find .env file in the gateway directory
envPath = Path('.env') envPath = Path(__file__).parent.parent.parent / 'env_dev.env'
if not envPath.exists():
# Try in parent directory
envPath = Path('../.env')
if not envPath.exists(): if not envPath.exists():
logger.warning(f"Environment file not found at {envPath.absolute()}") logger.warning(f"Environment file not found at {envPath.absolute()}")
return return

View file

@ -192,7 +192,7 @@ class AgentManager:
performance={}, performance={},
progress=0.0 progress=0.0
), ),
Task(**{**task.model_dump(), "status": "failed", "error": error_msg}) Task(**{**task.to_dict(), "status": "failed", "error": error_msg})
) )
try: try:

View file

@ -1,155 +0,0 @@
"""
Agent Registry Module for managing and initializing agents.
"""
import os
import logging
import importlib
from typing import Dict, Any, List, Optional
from modules.workflow.agentBase import AgentBase
logger = logging.getLogger(__name__)
class AgentRegistry:
"""Central registry for all available agents in the system."""
_instance = None
@classmethod
def getInstance(cls):
"""Return a singleton instance of the agent registry."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
"""Initialize the agent registry."""
if AgentRegistry._instance is not None:
raise RuntimeError("Singleton instance already exists - use getInstance()")
self.agents: Dict[str, AgentBase] = {}
self._loadAgents()
def initialize(self, service=None):
"""Initialize or update the registry with workflow manager and service references."""
if service:
# Validate required interfaces
required_interfaces = ['base', 'msft', 'google']
missing_interfaces = []
for interface in required_interfaces:
if not hasattr(service, interface):
missing_interfaces.append(interface)
if missing_interfaces:
logger.warning(f"Service container missing required interfaces: {', '.join(missing_interfaces)}")
return False
# Initialize agents with service
for agent in self.agents.values():
if service and hasattr(agent, 'setService'):
agent.setService(service)
return True
def _loadAgents(self):
"""Load all available agents from modules."""
logger.info("Loading agent modules...")
# List of agent modules to load
agentModules = []
agentDir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "agents")
# Search the directory for agent modules
for filename in os.listdir(agentDir):
if filename.startswith("agent") and filename.endswith(".py"):
agentModules.append(filename[0:-3]) # Remove .py extension
if not agentModules:
logger.warning("No agent modules found")
return
logger.info(f"{len(agentModules)} agent modules found")
# Load each agent module
for moduleName in agentModules:
try:
# Import the module
module = importlib.import_module(f"modules.agents.{moduleName}")
# Look for agent class or get_*_agent function
agentName = moduleName.split("agent")[-1]
className = f"Agent{agentName}"
getterName = f"getAgent{agentName}"
agent = None
# Try to get the agent via the get*Agent function
if hasattr(module, getterName):
getterFunc = getattr(module, getterName)
agent = getterFunc()
logger.info(f"Agent '{agent.name}' loaded via {getterName}()")
# Alternatively, try to instantiate the agent directly
elif hasattr(module, className):
agentClass = getattr(module, className)
agent = agentClass()
logger.info(f"Agent '{agent.name}' directly instantiated")
if agent:
# Register the agent
self.registerAgent(agent)
else:
logger.warning(f"No agent class or getter function found in module {moduleName}")
except ImportError as e:
logger.error(f"Module {moduleName} could not be imported: {e}")
except Exception as e:
logger.error(f"Error loading agent from module {moduleName}: {e}")
def registerAgent(self, agent):
"""
Register an agent in the registry.
Args:
agent: The agent to register
"""
agentId = getattr(agent, 'name', "unknown_agent")
self.agents[agentId] = agent
logger.debug(f"Agent '{agent.name}' registered")
def getAgent(self, agentIdentifier: str):
"""
Return an agent instance
Args:
agentIdentifier: ID or type of the desired agent
Returns:
Agent instance or None if not found
"""
if agentIdentifier in self.agents:
return self.agents[agentIdentifier]
logger.error(f"Agent with identifier '{agentIdentifier}' not found")
return None
def getAllAgents(self) -> Dict[str, AgentBase]:
"""
Get all registered agents.
Returns:
Dictionary mapping agent names to agent instances
"""
return self.agents.copy()
def getAgentInfos(self) -> List[Dict[str, Any]]:
"""Return information about all registered agents."""
agentInfos = []
seenAgents = set()
for agent in self.agents.values():
if agent not in seenAgents:
agentInfos.append(agent.getAgentInfo())
seenAgents.add(agent)
return agentInfos
# Singleton factory for the agent registry
def getAgentRegistry():
return AgentRegistry.getInstance()

View file

@ -20,7 +20,7 @@ from modules.workflow.taskManager import getTaskManager
from modules.workflow.documentManager import getDocumentManager from modules.workflow.documentManager import getDocumentManager
from modules.interfaces.serviceChatModel import ( from modules.interfaces.serviceChatModel import (
UserInputRequest, ChatWorkflow, ChatMessage, ChatLog, UserInputRequest, ChatWorkflow, ChatMessage, ChatLog,
ChatDocument, ChatStat, Workflow, Task, AgentResponse ChatDocument, ChatStat, Task, AgentResponse, AgentProfile
) )
# Configure logger # Configure logger
@ -360,8 +360,8 @@ class WorkflowManager:
self.service.functions.updateWorkflow(workflow.id, { self.service.functions.updateWorkflow(workflow.id, {
"status": workflow.status, "status": workflow.status,
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"stats": workflow.stats.model_dump(), "stats": workflow.stats.to_dict(),
"messages": [msg.model_dump() for msg in workflow.messages] "messages": [msg.to_dict() for msg in workflow.messages]
}) })
return workflow return workflow
@ -380,7 +380,7 @@ class WorkflowManager:
self.service.functions.updateWorkflow(workflow.id, { self.service.functions.updateWorkflow(workflow.id, {
"status": "failed", "status": "failed",
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"stats": workflow.stats.model_dump() "stats": workflow.stats.to_dict()
}) })
self.logAdd(workflow, f"Workflow failed: {str(e)}", level="error", progress=100) self.logAdd(workflow, f"Workflow failed: {str(e)}", level="error", progress=100)
@ -421,7 +421,7 @@ class WorkflowManager:
) )
# Save to database - only the workflow metadata # Save to database - only the workflow metadata
workflowDb = workflow.model_dump() workflowDb = workflow.to_dict()
self.service.functions.createWorkflow(workflowDb) self.service.functions.createWorkflow(workflowDb)
self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["init"], level="info", progress=0) self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["init"], level="info", progress=0)
@ -456,7 +456,7 @@ class WorkflowManager:
"status": workflow.status, "status": workflow.status,
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"currentRound": workflow.currentRound, "currentRound": workflow.currentRound,
"stats": workflow.stats.model_dump() # Include updated stats "stats": workflow.stats.to_dict() # Include updated stats
} }
self.service.functions.updateWorkflow(workflowId, workflowUpdate) self.service.functions.updateWorkflow(workflowId, workflowUpdate)
@ -623,7 +623,7 @@ JSON_OUTPUT = {{
logger.debug(f"PROJECT MANAGER Planning answer: {projectManagerOutput}") logger.debug(f"PROJECT MANAGER Planning answer: {projectManagerOutput}")
return self.parseJsonResponse(projectManagerOutput) return self.parseJsonResponse(projectManagerOutput)
async def agentProcessing(self, task: Dict[str, Any], workflow: ChatWorkflow) -> List[Dict[str, Any]]: async def agentProcessing(self, task: Task, workflow: ChatWorkflow) -> List[ChatDocument]:
""" """
Process a single agent task from the workflow (State 5: Agent Execution). Process a single agent task from the workflow (State 5: Agent Execution).
Uses the new Task and AgentResponse models. Uses the new Task and AgentResponse models.
@ -660,7 +660,7 @@ JSON_OUTPUT = {{
# Update in database # Update in database
self.service.functions.updateWorkflow(workflow.id, { self.service.functions.updateWorkflow(workflow.id, {
"stats": workflow.stats.model_dump() "stats": workflow.stats.to_dict()
}) })
# Log the agent response # Log the agent response
@ -685,7 +685,7 @@ JSON_OUTPUT = {{
self.logAdd(workflow, errorMsg, level="error") self.logAdd(workflow, errorMsg, level="error")
return [] return []
async def generateFinalMessage(self, objUserResponse: str, objFinalDocuments: List[str], objResults: List[Dict[str, Any]]) -> Dict[str, Any]: async def generateFinalMessage(self, objUserResponse: str, objFinalDocuments: List[str], objResults: List[Dict[str, Any]]) -> ChatMessage:
""" """
Creates the final response message with review of promised and delivered documents (State 6: Final Response Generation). Creates the final response message with review of promised and delivered documents (State 6: Final Response Generation).
@ -857,7 +857,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
# Update workflow in database # Update workflow in database
self.service.functions.updateWorkflow(workflow.id, { self.service.functions.updateWorkflow(workflow.id, {
"messages": [msg.model_dump() for msg in workflow.messages] "messages": [msg.to_dict() for msg in workflow.messages]
}) })
return messageObject return messageObject
@ -931,7 +931,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow.stats.tokensUsed += tokensUsed workflow.stats.tokensUsed += tokensUsed
# Create ChatMessage object # Create ChatMessage object
chatMessage = ChatMessage(**message.model_dump()) chatMessage = ChatMessage(**message.to_dict())
# Add message to workflow # Add message to workflow
workflow.messages.append(chatMessage) workflow.messages.append(chatMessage)
@ -947,13 +947,13 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow.lastActivity = currentTime workflow.lastActivity = currentTime
# Save to database - first the message itself # Save to database - first the message itself
self.service.functions.createWorkflowMessage(chatMessage.model_dump()) self.service.functions.createWorkflowMessage(chatMessage.to_dict())
# Then save the workflow with updated references and statistics # Then save the workflow with updated references and statistics
workflowUpdate = { workflowUpdate = {
"lastActivity": currentTime, "lastActivity": currentTime,
"messageIds": workflow.messageIds, "messageIds": workflow.messageIds,
"stats": workflow.stats.model_dump() # Include updated statistics "stats": workflow.stats.to_dict() # Include updated statistics
} }
self.service.functions.updateWorkflow(workflow.id, workflowUpdate) self.service.functions.updateWorkflow(workflow.id, workflowUpdate)
@ -1018,7 +1018,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
logger.log(logLevel, f"[Workflow {workflow.id}] {message}") logger.log(logLevel, f"[Workflow {workflow.id}] {message}")
# Save to database # Save to database
self.service.functions.saveWorkflowLog(workflow.id, logEntry.model_dump()) self.service.functions.saveWorkflowLog(workflow.id, logEntry.to_dict())
return logId return logId
@ -1109,7 +1109,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
return fileIds return fileIds
def getAvailableDocuments(self, workflow: ChatWorkflow, messageUser: ChatMessage) -> List[Dict[str, Any]]: def getAvailableDocuments(self, workflow: ChatWorkflow, messageUser: ChatMessage) -> List[ChatDocument]:
""" """
Determines all currently available documents from user input and already generated documents. Determines all currently available documents from user input and already generated documents.
@ -1171,7 +1171,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
logger.info(f"Available documents: {len(availableDocs)}") logger.info(f"Available documents: {len(availableDocs)}")
return availableDocs return availableDocs
def agentProfiles(self) -> List[Dict[str, Any]]: def agentProfiles(self) -> List[AgentProfile]:
""" """
Gets information about all available agents. Gets information about all available agents.
@ -1247,7 +1247,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"userLanguage": "en" "userLanguage": "en"
} }
def _createWorkflowData(self, workflow: ChatWorkflow) -> Dict[str, Any]: def _createWorkflowData(self, workflow: ChatWorkflow) -> ChatWorkflow:
"""Creates a workflow data structure.""" """Creates a workflow data structure."""
return { return {
"mandateId": self.functions.mandateId, "mandateId": self.functions.mandateId,
@ -1256,7 +1256,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"status": workflow.status, "status": workflow.status,
"startedAt": workflow.startedAt, "startedAt": workflow.startedAt,
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"stats": workflow.stats.model_dump() "stats": workflow.stats.to_dict()
} }
def _checkFileAccess(self, fileId: int) -> bool: def _checkFileAccess(self, fileId: int) -> bool:

View file

@ -1,5 +1,13 @@
....................... TASKS ....................... TASKS
Agents and Manager:
- To adapt prompts to match document handling, done by agents
- agents to use service object and to work stepwise:
1. to extract document content with prompts
2. to run ai propmt with integrated content-data in the prompt, including document reference (name, id)
3. to analyse success and to give back instruction to task manager
4. task manager to add a task based on agents result and feedback
- document extraction to have error handling for big documents. if document too large, then to get content in pieces - depending on document type
Walkthroughs: Walkthroughs:
- register - register
@ -8,6 +16,9 @@ Walkthroughs:
- management pages - management pages
- workflow - workflow
Install a Test environment with same prod_env
- add CORS url names to prod_env
-
----------------------- OPEN ----------------------- OPEN