From ecf23255d2aef93c22e635ab4a630c87e3d32cac Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 28 May 2025 01:51:10 +0200
Subject: [PATCH] base for integration test
---
app.py | 23 +-
env_dev.env | 2 +-
env_prod.env | 2 +-
modules/agents/agentCoach.py | 98 +++----
modules/agents/agentCoder.py | 114 ++++----
modules/agents/agentEmail.py | 98 +++++--
modules/connectors/connectorDbJson.py | 14 +-
modules/interfaces/serviceAppAccess.py | 10 +-
modules/interfaces/serviceAppClass.py | 137 +++++++---
modules/interfaces/serviceAppModel.py | 13 +-
modules/interfaces/serviceAppTokens.py | 27 +-
modules/interfaces/serviceChatClass.py | 101 ++++----
modules/interfaces/serviceChatModel.py | 48 ++--
modules/interfaces/serviceManagementAccess.py | 22 +-
modules/interfaces/serviceManagementClass.py | 64 +++--
modules/routes/routeAdmin.py | 14 +-
modules/routes/routeAttributes.py | 27 +-
modules/routes/routeDataFiles.py | 127 +++++----
modules/routes/routeDataMandates.py | 117 +++++----
modules/routes/routeDataPrompts.py | 44 ++--
modules/routes/routeDataUsers.py | 179 ++++++-------
modules/routes/routeSecurityGoogle.py | 54 ++--
modules/routes/routeSecurityLocal.py | 122 ++++++---
modules/routes/routeSecurityMsft.py | 54 ++--
modules/routes/routeWorkflows.py | 244 +++++++++---------
modules/security/auth.py | 30 ++-
modules/shared/attributeUtils.py | 51 +++-
modules/shared/configuration.py | 31 +--
modules/workflow/agentManager.py | 2 +-
modules/workflow/agentRegistry.py | 155 -----------
modules/workflow/workflowManager.py | 36 +--
notes/changelog.txt | 11 +
32 files changed, 1095 insertions(+), 976 deletions(-)
delete mode 100644 modules/workflow/agentRegistry.py
diff --git a/app.py b/app.py
index 376afe8a..2b6e1c4c 100644
--- a/app.py
+++ b/app.py
@@ -13,19 +13,20 @@ import pathlib
from modules.shared.configuration import APP_CONFIG
def initLogging():
+ """Initialize logging with configuration from APP_CONFIG"""
# Get log level from config (default to INFO if not found)
logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING")
logLevel = getattr(logging, logLevelName)
- # Create formatters
+ # Create formatters - using single line format
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")
)
- # File formatter with more detailed error information
+ # File formatter with more detailed error information but still single line
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")
)
@@ -66,17 +67,22 @@ def initLogging():
# Configure the root logger
logging.basicConfig(
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"),
- 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
noisyLoggers = ["httpx", "urllib3", "asyncio", "fastapi.security.oauth2"]
for loggerName in noisyLoggers:
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
initLogging()
@@ -143,6 +149,9 @@ app.include_router(promptRouter)
from modules.routes.routeWorkflows import router as 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
app.include_router(msftRouter)
diff --git a/env_dev.env b/env_dev.env
index 53d85460..8e5f8572 100644
--- a/env_dev.env
+++ b/env_dev.env
@@ -8,7 +8,7 @@ APP_API_URL = http://localhost:8000
# Database Configuration for Application
DB_APP_HOST=D:/Temp/_powerondb
DB_APP_DATABASE=app
-DB_APPY_USER=dev_user
+DB_APP_USER=dev_user
DB_APP_PASSWORD_SECRET=dev_password
# Database Configuration Chat
diff --git a/env_prod.env b/env_prod.env
index 694a0b2e..ed6dfc52 100644
--- a/env_prod.env
+++ b/env_prod.env
@@ -8,7 +8,7 @@ APP_API_URL = https://gateway.poweron-center.net
# Database Configuration Application
DB_APP_HOST=/home/_powerondb
DB_APP_DATABASE=app
-DB_APPY_USER=dev_user
+DB_APP_USER=dev_user
DB_APP_PASSWORD_SECRET=dev_password
# Database Configuration Chat
diff --git a/modules/agents/agentCoach.py b/modules/agents/agentCoach.py
index e8a6ea54..799ed8f5 100644
--- a/modules/agents/agentCoach.py
+++ b/modules/agents/agentCoach.py
@@ -7,8 +7,10 @@ import logging
from typing import Dict, Any, List
import json
from datetime import datetime
+import uuid
from modules.workflow.agentBase import AgentBase
+from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
logger = logging.getLogger(__name__)
@@ -36,21 +38,21 @@ class AgentCoach(AgentBase):
"""Set external dependencies for the agent."""
self.setService(serviceBase)
- async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
+ async def processTask(self, task: Task) -> Dict[str, Any]:
"""
Process a task by directly using AI to provide answers or content based on extracted data.
Args:
- task: Task dictionary with prompt, inputDocuments, outputSpecifications
+ task: Task object with prompt, inputDocuments, outputSpecifications
Returns:
Dictionary with feedback and documents
"""
try:
# Extract task information
- prompt = task.get("prompt", "")
- inputDocuments = task.get("inputDocuments", [])
- outputSpecs = task.get("outputSpecifications", [])
+ prompt = task.prompt
+ inputDocuments = task.filesInput
+ outputSpecs = task.filesOutput
# Check AI service
if not self.service or not self.service.base:
@@ -113,7 +115,7 @@ class AgentCoach(AgentBase):
"documents": []
}
- def _collectExtractedData(self, documents: List[Dict[str, Any]]) -> str:
+ def _collectExtractedData(self, documents: List[ChatDocument]) -> str:
"""
Collect extracted data from input documents.
@@ -126,16 +128,16 @@ class AgentCoach(AgentBase):
contextParts = []
for doc in documents:
- docName = doc.get("name", "unnamed")
- if doc.get("ext"):
- docName = f"{docName}.{doc.get('ext')}"
+ docName = doc.name
+ if doc.ext:
+ docName = f"{docName}.{doc.ext}"
contextParts.append(f"\n\n--- {docName} ---\n")
# Process contents, focusing on dataExtracted field
- for content in doc.get("contents", []):
- if content.get("dataExtracted"):
- contextParts.append(content.get("dataExtracted", ""))
+ for content in doc.contents:
+ if content.data:
+ contextParts.append(content.data)
return "\n".join(contextParts)
@@ -208,7 +210,7 @@ class AgentCoach(AgentBase):
}
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.
@@ -221,7 +223,7 @@ class AgentCoach(AgentBase):
taskUnderstanding: Task understanding from analysis
Returns:
- Document object
+ ChatDocument object
"""
# Determine content type based on format
contentType = self._getContentType(outputFormat)
@@ -248,52 +250,52 @@ class AgentCoach(AgentBase):
4. Be comprehensive but focused
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:
- # Build system prompt based on format
- systemPrompt = f"You create {outputFormat} format content based on requests and extracted data. Provide only the content in valid {outputFormat} format."
-
- # Generate content with AI
+ # Get content from AI
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}
])
- # Process content based on format
- if outputFormat in ["json", "csv"]:
- # For structured formats, extract from code blocks if present
- content = self._extractFromCodeBlocks(content, outputFormat)
-
- # Validate JSON if needed
- if outputFormat == "json":
- try:
- json.loads(content)
- except:
- logger.warning("Invalid JSON generated, attempting to fix")
- # Try to extract just the JSON portion
- jsonStart = content.find('{')
- jsonEnd = content.rfind('}') + 1
- if jsonStart >= 0 and jsonEnd > jsonStart:
- content = content[jsonStart:jsonEnd]
+ # Extract content from code blocks if present
+ content = self._extractFromCodeBlocks(content, 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 "{title}{title}
{content}"
-
- return self.formatAgentDocumentOutput(outputLabel, content, contentType)
+ # Create document object
+ return ChatDocument(
+ id=str(uuid.uuid4()),
+ name=outputLabel.split('.')[0],
+ ext=outputFormat,
+ data=content,
+ contents=[
+ ChatContent(
+ name="main",
+ data=content,
+ summary=description,
+ metadata={"format": outputFormat}
+ )
+ ]
+ )
except Exception as e:
logger.error(f"Error generating document: {str(e)}")
-
- # Create error document
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:
"""
diff --git a/modules/agents/agentCoder.py b/modules/agents/agentCoder.py
index e0bf78bf..dffac935 100644
--- a/modules/agents/agentCoder.py
+++ b/modules/agents/agentCoder.py
@@ -4,7 +4,7 @@ Provides code generation, execution, and improvement capabilities.
"""
import logging
-from typing import Dict, Any, List, Tuple
+from typing import Dict, Any, List, Tuple, Optional
import json
import os
import sys
@@ -14,9 +14,11 @@ import shutil
import venv
import importlib.util
from datetime import datetime
+import uuid
from modules.workflow.agentBase import AgentBase
from modules.shared.configuration import APP_CONFIG
+from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
logger = logging.getLogger(__name__)
@@ -46,7 +48,7 @@ class AgentCoder(AgentBase):
"""Set external dependencies for the agent."""
self.setService(serviceBase)
- async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
+ async def processTask(self, task: Task) -> Dict[str, Any]:
"""
Process a task and perform code development/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.
Args:
- task: Task dictionary with prompt, inputDocuments, outputSpecifications
+ task: Task object with prompt, inputDocuments, outputSpecifications
Returns:
Dictionary with feedback and documents
"""
# 1. Extract task information
- prompt = task.get("prompt", "")
- inputDocuments = task.get("inputDocuments", [])
- outputSpecs = task.get("outputSpecifications", [])
+ prompt = task.prompt
+ inputDocuments = task.filesInput
+ outputSpecs = task.filesOutput
# Check if AI service is available
if not self.service or not self.service.base:
@@ -79,39 +81,39 @@ class AgentCoder(AgentBase):
for doc in inputDocuments:
# 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
- docData = doc.get('data', '')
+ docData = doc.data
if docData:
isBase64 = True # Assume base64 encoded for document data
documentData.append([filename, docData, isBase64])
# Process contents for different uses
- if doc.get('contents'):
- for content in doc.get('contents', []):
- contentName = content.get('name', 'unnamed')
+ if doc.contents:
+ for content in doc.contents:
+ contentName = content.name
# For AI-extracted data (quick completion)
- if content.get('dataExtracted'):
+ if content.data:
contentExtraction.append({
"filename": filename,
"contentName": contentName,
- "contentData": content.get('dataExtracted', ''),
- "contentType": content.get('contentType', ''),
- "summary": content.get('summary', '')
+ "contentData": content.data,
+ "contentType": content.contentType,
+ "summary": content.summary
})
# For raw content data
- if content.get('data'):
- rawData = content.get('data', '')
- isBase64 = content.get('metadata', {}).get('base64Encoded', False)
+ if content.data:
+ rawData = content.data
+ isBase64 = content.metadata.get('base64Encoded', False) if content.metadata else False
contentData.append({
"filename": filename,
"contentName": contentName,
"data": rawData,
"isBase64": isBase64,
- "contentType": content.get('contentType', '')
+ "contentType": content.contentType
})
# 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
# This is needed since formatAgentDocumentOutput might determine a different value
if isinstance(base64Encoded, bool):
- doc["base64Encoded"] = base64Encoded
+ doc.base64Encoded = base64Encoded
documents.append(doc)
createdOutputs.add(finalLabel)
@@ -248,36 +250,14 @@ class AgentCoder(AgentBase):
# Not properly structured - log warning
logger.warning(f"Skipping improperly formatted result for '{label}'. Results must include 'content' field.")
else:
- # No result dictionary found
- logger.warning("No valid result dictionary found or it's not properly formatted")
-
- # 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)
- 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}"
+ # Handle non-dictionary results
+ logger.warning("Execution result is not a dictionary. Creating a single output document.")
+ doc = self.formatAgentDocumentOutput("result.txt", str(resultData), "text/plain")
+ documents.append(doc)
+ # 8. Return results
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
}
@@ -393,7 +373,7 @@ Return ONLY Python code without explanations or markdown.
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.
@@ -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
specsJson = json.dumps(outputSpecs)
- dataJson = json.dumps(contentExtraction)
+ dataJson = json.dumps([doc.dict() for doc in contentExtraction])
checkPrompt = f"""
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
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.
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])
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
def getAgentCoder():
diff --git a/modules/agents/agentEmail.py b/modules/agents/agentEmail.py
index 308ea3e6..6c6e2f5f 100644
--- a/modules/agents/agentEmail.py
+++ b/modules/agents/agentEmail.py
@@ -5,8 +5,12 @@ Handles email-related tasks using Microsoft Graph API.
import logging
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.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
logger = logging.getLogger(__name__)
@@ -30,7 +34,7 @@ class AgentEmail(AgentBase):
"""Set external dependencies for the agent."""
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.
@@ -48,9 +52,9 @@ class AgentEmail(AgentBase):
"""
try:
# Extract task information
- prompt = task.get("prompt", "")
- inputDocuments = task.get("inputDocuments", [])
- outputSpecs = task.get("outputSpecifications", [])
+ prompt = task.prompt
+ inputDocuments = task.filesInput
+ outputSpecs = task.filesOutput
# Check AI service
if not self.service.base:
@@ -148,26 +152,39 @@ class AgentEmail(AgentBase):
"documents": []
}
- def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]:
+ def _createFrontendAuthTriggerDocument(self) -> ChatDocument:
"""Create a document that triggers Microsoft authentication in the frontend."""
- return {
- "name": "microsoft_auth",
- "ext": "html",
- "mimeType": "text/html",
- "data": """
+ return ChatDocument(
+ id=str(uuid.uuid4()),
+ name="microsoft_auth",
+ ext="html",
+ data="""
Microsoft Authentication Required
Please click the button below to authenticate with Microsoft:
""",
- "base64Encoded": False,
- "metadata": {
- "isText": True
- }
- }
+ contents=[
+ ChatContent(
+ name="main",
+ data="""
+
+
Microsoft Authentication Required
+
Please click the button below to authenticate with Microsoft:
+
+
+ """,
+ summary="Microsoft authentication trigger page",
+ metadata={
+ "contentType": "text/html",
+ "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.
@@ -181,22 +198,22 @@ class AgentEmail(AgentBase):
attachments = []
for doc in input_docs:
- docName = doc.get("name", "unnamed")
- if doc.get("ext"):
- docName = f"{docName}.{doc.get('ext')}"
+ docName = doc.name
+ if doc.ext:
+ docName = f"{docName}.{doc.ext}"
# Add document name to contents
documentContents.append(f"\n\n--- {docName} ---\n")
# Process document data directly
- if doc.get("data"):
+ if doc.data:
# Add to attachments with proper metadata
attachments.append({
"name": docName,
"document": {
- "data": doc["data"],
- "mimeType": doc.get("mimeType", "application/octet-stream"),
- "base64Encoded": doc.get("base64Encoded", False)
+ "data": doc.data,
+ "mimeType": doc.contents[0].metadata.get("contentType", "application/octet-stream") if doc.contents else "application/octet-stream",
+ "base64Encoded": doc.contents[0].metadata.get("base64Encoded", False) if doc.contents else False
}
})
documentContents.append(f"Document attached: {docName}")
@@ -205,6 +222,39 @@ class AgentEmail(AgentBase):
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]:
"""
Generate email template using AI.
diff --git a/modules/connectors/connectorDbJson.py b/modules/connectors/connectorDbJson.py
index 18ebc48c..878392d0 100644
--- a/modules/connectors/connectorDbJson.py
+++ b/modules/connectors/connectorDbJson.py
@@ -409,11 +409,17 @@ class DatabaseConnector:
with open(recordPath, 'w', encoding='utf-8') as f:
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):
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 isinstance(self._tablesCache[table], list):
self._tablesCache[table].append(recordData)
@@ -526,9 +532,7 @@ class DatabaseConnector:
"""Returns the initial ID for a table."""
systemData = self._loadSystemTable()
initialId = systemData.get(table)
- logger.debug(f"Database '{self.dbDatabase}': Table: {systemData}, Initial ID for table '{table}' is {initialId}")
- if initialId is None:
- logger.debug(f"No initial ID found for table {table}")
+ logger.debug(f"Initial ID for table '{table}': {initialId}")
return initialId
def getAllInitialIds(self) -> Dict[str, str]:
diff --git a/modules/interfaces/serviceAppAccess.py b/modules/interfaces/serviceAppAccess.py
index efcbcdee..182a3055 100644
--- a/modules/interfaces/serviceAppAccess.py
+++ b/modules/interfaces/serviceAppAccess.py
@@ -4,7 +4,7 @@ Access control for the Application.
from typing import Dict, Any, List, Optional
from datetime import datetime
-from modules.interfaces.serviceAppModel import UserPrivilege, Session
+from modules.interfaces.serviceAppModel import UserPrivilege, Session, User
class AppAccess:
"""
@@ -12,12 +12,12 @@ class AppAccess:
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."""
self.currentUser = currentUser
- self.mandateId = currentUser.get("mandateId")
- self.userId = currentUser.get("id")
- self.privilege = currentUser.get("privilege", UserPrivilege.USER)
+ self.userId = currentUser.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")
diff --git a/modules/interfaces/serviceAppClass.py b/modules/interfaces/serviceAppClass.py
index ae298165..a614d282 100644
--- a/modules/interfaces/serviceAppClass.py
+++ b/modules/interfaces/serviceAppClass.py
@@ -37,12 +37,12 @@ class GatewayInterface:
Manages users and mandates.
"""
- def __init__(self, currentUser: Dict[str, Any] = None):
+ def __init__(self, currentUser: Optional[User] = None):
"""Initializes the Gateway Interface."""
# Initialize variables
- self.currentUser = currentUser
- self.userId = currentUser.get("id") if currentUser else None
- self.mandateId = currentUser.get("mandateId") if currentUser else None
+ 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
@@ -55,24 +55,24 @@ class GatewayInterface:
if currentUser:
self.setUserContext(currentUser)
- def setUserContext(self, currentUser: Dict[str, Any]):
+ def setUserContext(self, currentUser: User):
"""Sets the user context for the interface."""
if not currentUser:
logger.info("Initializing interface without user context")
return
- self.currentUser = currentUser
- self.userId = currentUser.get("id")
- self.mandateId = currentUser.get("mandateId")
+ self.currentUser = currentUser # Store User object directly
+ self.userId = currentUser.id
+ self.mandateId = currentUser.mandateId
if not self.userId or not self.mandateId:
raise ValueError("Invalid user context: id and mandateId are required")
# 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
- 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}")
@@ -115,7 +115,7 @@ class GatewayInterface:
name="Root",
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']}")
# Register the initial ID
@@ -142,7 +142,7 @@ class GatewayInterface:
hashedPassword=self._getPasswordHash("The 1st Poweron Admin"), # Use a secure password in production!
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']}")
# Register the initial ID
@@ -219,10 +219,10 @@ class GatewayInterface:
return None
# Find user by username
- for user in users:
- if user.get("username") == username:
+ for user_dict in users:
+ if user_dict.get("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}")
return None
@@ -270,7 +270,7 @@ class GatewayInterface:
user.connections.append(connection)
# 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
@@ -290,7 +290,7 @@ class GatewayInterface:
user.connections = [c for c in user.connections if c.id != connectionId]
# 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:
logger.error(f"Error removing user connection: {str(e)}")
@@ -317,8 +317,11 @@ class GatewayInterface:
raise ValueError("User does not have local authentication enabled")
# Get the full user record with password hash for verification
- userWithPassword = UserInDB(**self.db.getRecordset("users", recordFilter={"id": user.id})[0])
- if not self._verifyPassword(password, userWithPassword.hashedPassword):
+ userRecord = self.db.getRecordset("users", recordFilter={"id": user.id})[0]
+ if not userRecord.get("hashedPassword"):
+ raise ValueError("User has no password set")
+
+ if not self._verifyPassword(password, userRecord["hashedPassword"]):
raise ValueError("Invalid password")
return user
@@ -331,6 +334,18 @@ class GatewayInterface:
externalEmail: str = None) -> User:
"""Create a new user with optional external connection"""
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
userData = UserInDB(
username=username,
@@ -365,9 +380,11 @@ class GatewayInterface:
if not createdUser or len(createdUser) == 0:
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:
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])
@@ -387,7 +404,7 @@ class GatewayInterface:
raise ValueError(f"User {userId} not found")
# Update user data using model
- updatedData = user.model_dump()
+ updatedData = user.to_dict()
updatedData.update(updateData)
updatedUser = User.from_dict(updatedData)
@@ -488,7 +505,7 @@ class GatewayInterface:
raise ValueError(f"Mandate {mandateId} not found")
# Update mandate data using model
- updatedData = mandate.model_dump()
+ updatedData = mandate.to_dict()
updatedData.update(updateData)
updatedMandate = Mandate.from_dict(updatedData)
@@ -529,20 +546,65 @@ class GatewayInterface:
logger.error(f"Error deleting 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
-def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface:
+def getInterface(currentUser: User) -> GatewayInterface:
"""
Returns a GatewayInterface instance for the current user.
Handles initialization of database and records.
"""
- mandateId = currentUser.get("mandateId")
- userId = currentUser.get("id")
- if not mandateId or not userId:
- raise ValueError("Invalid user context: mandateId and id are required")
+ if not currentUser:
+ raise ValueError("Invalid user context: user is required")
# Create context key
- contextKey = f"{mandateId}_{userId}"
+ contextKey = f"{currentUser.mandateId}_{currentUser.id}"
# Create new instance if not exists
if contextKey not in _gatewayInterfaces:
@@ -550,24 +612,27 @@ def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface:
return _gatewayInterfaces[contextKey]
-def getRootUser() -> Dict[str, Any]:
+def getRootUser() -> User:
"""
Returns the root user from the database.
This is the user with the initial ID in the users table.
"""
try:
- readInterface = getInterface()
- # Get the initial user ID
- initialUserId = readInterface.db.getInitialId("users")
+ # Create a temporary interface without user context
+ tempInterface = GatewayInterface()
+
+ # Get the initial user directly
+ initialUserId = tempInterface.db.getInitialId("users")
if not initialUserId:
raise ValueError("No initial user ID found in database")
- # Get the user record
- users = readInterface.db.getRecordset("users", recordFilter={"id": initialUserId})
+ users = tempInterface.db.getRecordset("users", recordFilter={"id": initialUserId})
if not users:
- raise ValueError(f"Root user with ID {initialUserId} not found in database")
+ raise ValueError("Initial user not found in database")
- return users[0]
+ # Convert to User model and return the model instance
+ return User.from_dict(users[0])
+
except Exception as e:
logger.error(f"Error getting root user: {str(e)}")
raise ValueError(f"Failed to get root user: {str(e)}")
diff --git a/modules/interfaces/serviceAppModel.py b/modules/interfaces/serviceAppModel.py
index 2ed97791..1eb98f1d 100644
--- a/modules/interfaces/serviceAppModel.py
+++ b/modules/interfaces/serviceAppModel.py
@@ -10,6 +10,17 @@ from enum import Enum
from modules.shared.attributeUtils import Label, BaseModelWithUI
+class AttributeDefinition(BaseModel):
+ """Definition of an attribute for UI forms"""
+ name: str = Field(..., description="Name of the attribute")
+ label: str = Field(..., description="Display label for the attribute")
+ type: str = Field(..., description="Type of the attribute (string, number, boolean, etc.)")
+ required: bool = Field(default=False, description="Whether the attribute is required")
+ placeholder: Optional[str] = Field(None, description="Placeholder text for the input")
+ editable: bool = Field(default=True, description="Whether the attribute can be edited")
+ visible: bool = Field(default=True, description="Whether the attribute should be visible in forms")
+ order: int = Field(default=0, description="Order in which to display the attribute")
+
class AuthAuthority(str, Enum):
"""Authentication authorities"""
LOCAL = "local"
@@ -151,7 +162,7 @@ class User(BaseModelWithUI):
disabled: bool = Field(default=False, description="Indicates whether the user is disabled")
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
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")
label: Label = Field(
diff --git a/modules/interfaces/serviceAppTokens.py b/modules/interfaces/serviceAppTokens.py
index 7b2a8ea4..68170861 100644
--- a/modules/interfaces/serviceAppTokens.py
+++ b/modules/interfaces/serviceAppTokens.py
@@ -24,29 +24,4 @@ class LocalToken(BaseModel):
"""Local authentication token model"""
access_token: str
token_type: str = "bearer"
- 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
\ No newline at end of file
+ expires_at: float
\ No newline at end of file
diff --git a/modules/interfaces/serviceChatClass.py b/modules/interfaces/serviceChatClass.py
index 16cd0fc0..e7e4d3c6 100644
--- a/modules/interfaces/serviceChatClass.py
+++ b/modules/interfaces/serviceChatClass.py
@@ -16,8 +16,9 @@ from modules.interfaces.serviceChatAccess import ChatAccess
from modules.interfaces.serviceChatModel import (
ChatContent, ChatDocument, ChatStat, ChatMessage,
ChatLog, ChatWorkflow, Agent, AgentResponse,
- TaskItem, TaskPlan, UserInputRequest
+ Task, TaskPlan, UserInputRequest
)
+from modules.interfaces.serviceAppModel import User
# DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbJson import DatabaseConnector
@@ -57,44 +58,42 @@ class ChatInterface:
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."""
+ # 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
self._initializeDatabase()
- # Initialize standard records if needed
- self._initRecords()
+ # Set user context if provided
+ if currentUser:
+ self.setUserContext(currentUser)
- # Initialize variables
- 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]):
+ def setUserContext(self, currentUser: User):
"""Sets the user context for the interface."""
if not currentUser:
logger.info("Initializing interface without user context")
return
- self.currentUser = currentUser
- self.userId = currentUser.get("id")
- self.mandateId = currentUser.get("mandateId")
- if not self.userId:
- raise ValueError("Invalid user context: id is required")
+ self.currentUser = currentUser # Store User object directly
+ self.userId = currentUser.id
+ self.mandateId = currentUser.mandateId
+
+ if not self.userId or not self.mandateId:
+ raise ValueError("Invalid user context: id and mandateId are required")
# 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
- self.access = ChatAccess(self.currentUser, self.db)
+ self.access = ChatAccess(self.currentUser, self.db) # Convert to dict only when needed
- # Initialize AI service
- self.aiService = ChatService()
-
- logger.debug(f"User context set: userId={self.userId}")
-
+ logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
+
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
@@ -353,7 +352,7 @@ class ChatInterface:
# 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."""
# Check workflow access first
workflow = self.getWorkflow(workflowId)
@@ -362,7 +361,7 @@ class ChatInterface:
# Get messages for this workflow
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:
"""Creates a message for a workflow if user has access."""
@@ -639,7 +638,7 @@ class ChatInterface:
# 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."""
# Check workflow access first
workflow = self.getWorkflow(workflowId)
@@ -647,7 +646,7 @@ class ChatInterface:
return []
# 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:
"""Creates a log entry for a workflow if user has access."""
@@ -695,17 +694,17 @@ class ChatInterface:
return None
# 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 ChatLog(**createdLog)
# 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."""
try:
- workflowId = workflow.get("id")
+ workflowId = workflow.id
if not workflowId:
return False
@@ -722,12 +721,12 @@ class ChatInterface:
# Extract only the database-relevant workflow fields
workflowDbData = {
"id": workflowId,
- "mandateId": workflow.get("mandateId", self.currentUser.get("mandateId")),
- "name": workflow.get("name", f"Workflow {workflowId}"),
- "status": workflow.get("status", "completed"),
- "startedAt": workflow.get("startedAt", self._getCurrentTimestamp()),
- "lastActivity": workflow.get("lastActivity", self._getCurrentTimestamp()),
- "dataStats": workflow.get("dataStats", {})
+ "mandateId": workflow.mandateId,
+ "name": workflow.name,
+ "status": workflow.status,
+ "startedAt": workflow.startedAt,
+ "lastActivity": workflow.lastActivity,
+ "dataStats": workflow.stats.dict() if workflow.stats else {}
}
# Check if workflow already exists
@@ -802,7 +801,7 @@ class ChatInterface:
logger.error(f"Error saving workflow state: {str(e)}")
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."""
try:
# Check workflow access
@@ -852,21 +851,19 @@ class ChatInterface:
return None
-def getInterface(currentUser: Dict[str, Any] = None) -> 'ChatInterface':
+def getInterface(currentUser: Optional[User] = None) -> 'ChatInterface':
"""
- Returns a ChatInterface instance.
- If currentUser is provided, initializes with user context.
- Otherwise, returns an instance with only database access.
+ Returns a ChatInterface instance for the current user.
+ Handles initialization of database and records.
"""
+ 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
- if "default" not in _chatInterfaces:
- _chatInterfaces["default"] = ChatInterface()
+ if contextKey not in _chatInterfaces:
+ _chatInterfaces[contextKey] = ChatInterface(currentUser)
- interface = _chatInterfaces["default"]
-
- if currentUser:
- interface.setUserContext(currentUser)
- else:
- logger.info("Returning interface without user context")
-
- return interface
\ No newline at end of file
+ return _chatInterfaces[contextKey]
\ No newline at end of file
diff --git a/modules/interfaces/serviceChatModel.py b/modules/interfaces/serviceChatModel.py
index 6fcd9b89..6bedd288 100644
--- a/modules/interfaces/serviceChatModel.py
+++ b/modules/interfaces/serviceChatModel.py
@@ -67,6 +67,23 @@ class ChatMessage(BaseModelWithUI):
stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
success: Optional[bool] = Field(None, description="Whether the message processing was successful")
+class Task(BaseModelWithUI):
+ """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):
"""Data model for a chat workflow"""
id: str = Field(description="Primary key")
@@ -79,7 +96,7 @@ class ChatWorkflow(BaseModelWithUI):
logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs")
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow")
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(
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")
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):
"""Data model for a task plan"""
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"""
prompt: str = Field(description="Prompt for the user")
listFileId: List[int] = Field(default_factory=list, description="List of file IDs")
- userLanguage: str = Field(description="User's preferred language")
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/modules/interfaces/serviceManagementAccess.py b/modules/interfaces/serviceManagementAccess.py
index b8db3239..7ae713dc 100644
--- a/modules/interfaces/serviceManagementAccess.py
+++ b/modules/interfaces/serviceManagementAccess.py
@@ -3,7 +3,12 @@ Access control module for Management interface.
Handles user access management and permission checks.
"""
+import logging
from typing import Dict, Any, List, Optional
+from modules.interfaces.serviceAppModel import User
+
+# Configure logger
+logger = logging.getLogger(__name__)
class ManagementAccess:
"""
@@ -11,15 +16,12 @@ class ManagementAccess:
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."""
self.currentUser = currentUser
- self.mandateId = currentUser.get("mandateId")
- self.userId = currentUser.get("id")
-
- if not self.mandateId or not self.userId:
- raise ValueError("Invalid user context: mandateId and userId are required")
-
+ self.userId = currentUser.id
+ self.mandateId = currentUser.mandateId
+ self.privilege = currentUser.privilege
self.db = db
def uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@@ -34,8 +36,8 @@ class ManagementAccess:
Returns:
Filtered recordset with access control attributes
"""
- userPrivilege = self.currentUser.get("privilege", "user")
- print("DEBUG: User privilege:", userPrivilege, self.currentUser.get("username"),self.currentUser.get("email"))
+ userPrivilege = self.privilege
+ logger.debug(f"User privilege: {userPrivilege}, username: {self.currentUser.username}, email: {self.currentUser.email}")
filtered_records = []
# Apply filtering based on privilege
@@ -98,7 +100,7 @@ class ManagementAccess:
Returns:
Boolean indicating permission
"""
- userPrivilege = self.currentUser.get("privilege", "user")
+ userPrivilege = self.privilege
# System admins can modify anything
if userPrivilege == "sysadmin":
diff --git a/modules/interfaces/serviceManagementClass.py b/modules/interfaces/serviceManagementClass.py
index 7e525607..d1801626 100644
--- a/modules/interfaces/serviceManagementClass.py
+++ b/modules/interfaces/serviceManagementClass.py
@@ -76,7 +76,7 @@ class ServiceManagement:
logger.info("Initializing interface without user context")
return
- self.currentUser = currentUser
+ self.currentUser = currentUser # Store User object directly
self.userId = currentUser.id
if not self.userId:
@@ -249,48 +249,44 @@ class ServiceManagement:
filteredPrompts = self._uam("prompts", prompts)
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."""
if not self._canModify("prompts"):
raise PermissionError("No permission to create prompts")
- promptData = Prompt(
- content=content,
- name=name,
- createdAt=self._getCurrentTimestamp()
- )
-
+ # Create prompt record
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")
+
+ return createdRecord
- def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Prompt:
+ def updatePrompt(self, promptId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates a prompt if user has access."""
- # Check if the prompt exists and user has access
- prompt = self.getPrompt(promptId)
- if not prompt:
- raise ValueError(f"Prompt {promptId} not found")
+ try:
+ # Get prompt
+ prompt = self.getPrompt(promptId)
+ if not prompt:
+ 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
- updatedData = prompt.model_dump()
- if content is not None:
- updatedData["content"] = content
- if name is not None:
- updatedData["name"] = name
+ # Update prompt data using model
+ updatedData = prompt.to_dict()
+ updatedData.update(updateData)
+ updatedPrompt = Prompt.from_dict(updatedData)
- updatedPrompt = Prompt.from_dict(updatedData)
-
- # Update prompt
- self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
-
- # Get updated prompt
- updatedPrompt = self.getPrompt(promptId)
- if not updatedPrompt:
- raise ValueError("Failed to retrieve updated prompt")
-
- return updatedPrompt
+ # Update prompt record
+ self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
+
+ # Get updated prompt
+ updatedPrompt = self.getPrompt(promptId)
+ if not updatedPrompt:
+ raise ValueError("Failed to retrieve updated prompt")
+
+ 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:
"""Deletes a prompt if user has access."""
diff --git a/modules/routes/routeAdmin.py b/modules/routes/routeAdmin.py
index c63b4ff5..1e3156d7 100644
--- a/modules/routes/routeAdmin.py
+++ b/modules/routes/routeAdmin.py
@@ -1,13 +1,15 @@
-from fastapi import APIRouter, Response, Depends
+from fastapi import APIRouter, Response, Depends, Request
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import os
import logging
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.security.auth import limiter, getCurrentUser
+from modules.interfaces.serviceAppModel import User
router = APIRouter(
prefix="",
@@ -33,7 +35,7 @@ router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), nam
@router.get("/")
@limiter.limit("30/minute")
-async def root():
+async def root(request: Request) -> Dict[str, str]:
"""API status endpoint"""
return {
"status": "online",
@@ -43,7 +45,7 @@ async def root():
@router.get("/api/environment")
@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"""
return {
"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}")
@limiter.limit("60/minute")
-async def options_route(fullPath: str):
+async def options_route(request: Request, fullPath: str) -> Response:
return Response(status_code=200)
@router.get("/favicon.ico")
@limiter.limit("30/minute")
-async def favicon():
+async def favicon(request: Request) -> FileResponse:
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
diff --git a/modules/routes/routeAttributes.py b/modules/routes/routeAttributes.py
index e3d82452..53082129 100644
--- a/modules/routes/routeAttributes.py
+++ b/modules/routes/routeAttributes.py
@@ -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 fastapi import status
import inspect
@@ -11,7 +11,7 @@ import logging
from modules.security.auth import limiter, getCurrentUser
# 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
# Configure logger
@@ -50,9 +50,9 @@ router = APIRouter(
@router.get("/{entityType}", response_model=AttributeResponse)
@limiter.limit("30/minute")
async def get_entity_attributes(
- entityType: str = Path(..., description="Type of entity (e.g. prompt)"),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ entityType: str = Path(..., description="Type of entity (e.g. prompt)")
+) -> AttributeResponse:
"""
Retrieves the attribute definitions for a specific entity.
This can be used for dynamic form generation.
@@ -63,9 +63,6 @@ async def get_entity_attributes(
Returns:
- 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
modelClasses = getModelClasses()
@@ -80,13 +77,21 @@ async def get_entity_attributes(
modelClass = modelClasses[entityType]
attributes = modelClass.getModelAttributeDefinitions()
- # Return only visible attributes
- return AttributeResponse(attributes=[attr for attr in attributes if attr.visible])
+ # Convert dictionary attributes to AttributeDefinition objects
+ 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}")
@limiter.limit("60/minute")
async def options_entity_attributes(
+ request: Request,
entityType: str = Path(..., description="Type of entity (e.g. prompt)")
-):
+) -> Response:
"""Handle OPTIONS request for CORS preflight"""
return Response(status_code=200)
\ No newline at end of file
diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py
index 0ab1dafb..651c92c7 100644
--- a/modules/routes/routeDataFiles.py
+++ b/modules/routes/routeDataFiles.py
@@ -1,5 +1,5 @@
-from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response
-from fastapi.responses import JSONResponse
+from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response, Body
+from fastapi.responses import JSONResponse, FileResponse
from typing import List, Dict, Any, Optional
import logging
from datetime import datetime, timezone
@@ -11,8 +11,9 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass
-from modules.interfaces.serviceManagementModel import FileItem, getModelAttributeDefinitions
-from modules.interfaces.serviceAppModel import AttributeDefinition
+from modules.interfaces.serviceManagementModel import FileItem
+from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.interfaces.serviceAppModel import AttributeDefinition, User
# Configure logger
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")
-async def get_files(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
- """Get all available files"""
+async def get_files(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> List[FileItem]:
+ """Get all files"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -44,19 +48,20 @@ async def get_files(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
files = managementInterface.getAllFiles()
return [FileItem(**file) for file in files]
except Exception as e:
- logger.error(f"Error retrieving files: {str(e)}")
+ logger.error(f"Error getting files: {str(e)}")
raise HTTPException(
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)
@limiter.limit("10/minute")
async def upload_file(
+ request: Request,
file: UploadFile = File(...),
workflowId: Optional[str] = Form(None),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ currentUser: User = Depends(getCurrentUser)
+) -> JSONResponse:
"""Upload a file"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -82,7 +87,10 @@ async def upload_file(
fileMeta["workflowId"] = workflowId
# Successful response
- return fileMeta
+ return JSONResponse({
+ "message": "File uploaded successfully",
+ "file": fileMeta
+ })
except serviceManagementClass.FileStorageError as e:
logger.error(f"Error during file upload (storage): {str(e)}")
@@ -97,28 +105,26 @@ async def upload_file(
detail=f"Error during file upload: {str(e)}"
)
-@router.get("/{fileId}")
+@router.get("/{fileId}", response_model=FileItem)
@limiter.limit("30/minute")
async def get_file(
- fileId: str,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
- """Returns a file by its ID for download"""
+ request: Request,
+ fileId: str = Path(..., description="ID of the file"),
+ currentUser: User = Depends(getCurrentUser)
+) -> FileItem:
+ """Get a file"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
# Get file via LucyDOM interface from the database
- fileData = managementInterface.downloadFile(fileId)
-
- # Return file
- headers = {
- "Content-Disposition": f'attachment; filename="{fileData["name"]}"'
- }
- return Response(
- content=fileData["content"],
- media_type=fileData["contentType"],
- headers=headers
- )
+ fileData = managementInterface.getFile(fileId)
+ if not fileData:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"File with ID {fileId} not found"
+ )
+
+ return FileItem(**fileData)
except serviceManagementClass.FileNotFoundError as e:
logger.warning(f"File not found: {str(e)}")
@@ -145,53 +151,62 @@ async def get_file(
detail=f"Error retrieving file: {str(e)}"
)
-@router.put("/{file_id}", response_model=FileItem)
+@router.put("/{fileId}", response_model=FileItem)
@limiter.limit("10/minute")
async def update_file(
- file_id: str,
- file_data: FileItem,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
- """
- Update file metadata
- """
+ request: Request,
+ fileId: str = Path(..., description="ID of the file to update"),
+ file_info: Dict[str, Any] = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+) -> FileItem:
+ """Update file info"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
# Get the file from the database
- file = managementInterface.getFile(file_id)
+ file = managementInterface.getFile(fileId)
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
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,
+ detail="Not authorized to update this file"
+ )
- # Convert FileItem to dict for interface
- update_data = file_data.model_dump()
-
# Update the file
- result = managementInterface.updateFile(file_id, update_data)
+ result = managementInterface.updateFile(fileId, file_info)
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
- updatedFile = managementInterface.getFile(file_id)
+ updatedFile = managementInterface.getFile(fileId)
return FileItem(**updatedFile)
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"Error updating file: {str(e)}")
- raise HTTPException(status_code=500, detail=str(e))
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=str(e)
+ )
@router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT)
@limiter.limit("10/minute")
async def delete_file(
+ request: Request,
fileId: str,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
- """Deletes a file by its ID from the database"""
+ currentUser: User = Depends(getCurrentUser)
+) -> JSONResponse:
+ """Delete a file"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -199,7 +214,9 @@ async def delete_file(
managementInterface.deleteFile(fileId)
# 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:
logger.warning(f"File not found: {str(e)}")
@@ -229,8 +246,9 @@ async def delete_file(
@router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute")
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"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -266,8 +284,9 @@ async def get_file_stats(
@router.get("/attributes", response_model=List[AttributeDefinition])
@limiter.limit("30/minute")
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.
This can be used for dynamic form generation.
diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py
index d6309cdb..1cd20547 100644
--- a/modules/routes/routeDataMandates.py
+++ b/modules/routes/routeDataMandates.py
@@ -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 typing import List, Dict, Any, Optional
from fastapi import status
@@ -8,10 +13,10 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass
-from modules.interfaces.serviceManagementModel import Mandate, getModelAttributeDefinitions
+from modules.shared.attributeUtils import getModelAttributeDefinitions
# Import the model classes
-from modules.interfaces.serviceAppModel import AttributeDefinition
+from modules.interfaces.serviceAppModel import AttributeDefinition, Mandate, User
# Configure logger
logger = logging.getLogger(__name__)
@@ -26,13 +31,17 @@ router = APIRouter(
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")
-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"""
try:
- appInterface = serviceManagementClass.getInterface(currentUser)
- return appInterface.getMandates()
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+ mandates = managementInterface.getMandates()
+ return [Mandate.from_dict(mandate) for mandate in mandates]
except Exception as e:
logger.error(f"Error getting mandates: {str(e)}")
raise HTTPException(
@@ -40,24 +49,25 @@ async def get_mandates(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
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")
async def get_mandate(
- mandateId: str,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ mandateId: str = Path(..., description="ID of the mandate"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Mandate:
"""Get a specific mandate by ID"""
try:
- appInterface = serviceManagementClass.getInterface(currentUser)
- mandate = appInterface.getMandateById(mandateId)
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+ mandate = managementInterface.getMandate(mandateId)
if not mandate:
raise HTTPException(
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:
raise
except Exception as e:
@@ -67,31 +77,30 @@ async def get_mandate(
detail=f"Failed to get mandate: {str(e)}"
)
-@router.post("/", response_model=Mandate, tags=["Mandates"])
+@router.post("/", response_model=Mandate)
@limiter.limit("10/minute")
async def create_mandate(
- mandateData: Mandate,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ mandateData: Mandate = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+) -> Mandate:
"""Create a new mandate"""
try:
- appInterface = serviceManagementClass.getInterface(currentUser)
-
- try:
- createdMandate = appInterface.createMandate(mandateData)
- except ValueError as e:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=str(e)
- )
-
- if not createdMandate:
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+
+ # Convert Mandate to dict for interface
+ mandate_data = mandateData.to_dict()
+
+ # Create mandate
+ newMandate = managementInterface.createMandate(mandate_data)
+
+ if not newMandate:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create mandate"
)
- return createdMandate
+ return Mandate.from_dict(newMandate)
except HTTPException:
raise
except Exception as e:
@@ -101,41 +110,39 @@ async def create_mandate(
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")
async def update_mandate(
- mandateId: str,
- mandateData: Mandate,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ mandateId: str = Path(..., description="ID of the mandate to update"),
+ mandateData: Mandate = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+) -> Mandate:
"""Update an existing mandate"""
try:
- appInterface = serviceManagementClass.getInterface(currentUser)
+ managementInterface = serviceManagementClass.getInterface(currentUser)
# Check if mandate exists
- existingMandate = appInterface.getMandateById(mandateId)
+ existingMandate = managementInterface.getMandate(mandateId)
if not existingMandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Mandate {mandateId} not found"
- )
-
- # Update mandate data
- try:
- updatedMandate = appInterface.updateMandate(mandateId, mandateData)
- except ValueError as e:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=str(e)
+ detail=f"Mandate with ID {mandateId} not found"
)
+ # Convert Mandate to dict for interface
+ update_data = mandateData.to_dict()
+
+ # Update mandate
+ updatedMandate = managementInterface.updateMandate(mandateId, update_data)
+
if not updatedMandate:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update mandate"
)
- return updatedMandate
+ return Mandate.from_dict(updatedMandate)
except HTTPException:
raise
except Exception as e:
@@ -145,12 +152,13 @@ async def update_mandate(
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")
async def delete_mandate(
- mandateId: str,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ mandateId: str = Path(..., description="ID of the mandate to delete"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
"""Delete a mandate"""
try:
appInterface = serviceManagementClass.getInterface(currentUser)
@@ -185,8 +193,9 @@ async def delete_mandate(
@router.get("/attributes", response_model=List[AttributeDefinition])
@limiter.limit("30/minute")
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.
This can be used for dynamic form generation.
diff --git a/modules/routes/routeDataPrompts.py b/modules/routes/routeDataPrompts.py
index c6ba1a04..72ecfea8 100644
--- a/modules/routes/routeDataPrompts.py
+++ b/modules/routes/routeDataPrompts.py
@@ -10,7 +10,7 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import Prompt
-from modules.interfaces.serviceAppModel import AttributeDefinition
+from modules.interfaces.serviceAppModel import AttributeDefinition, User
# Configure logger
logger = logging.getLogger(__name__)
@@ -25,24 +25,26 @@ router = APIRouter(
@router.get("", response_model=List[Prompt])
@limiter.limit("30/minute")
async def get_prompts(
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> List[Prompt]:
"""Get all prompts"""
managementInterface = serviceManagementClass.getInterface(currentUser)
prompts = managementInterface.getAllPrompts()
- return [Prompt(**prompt) for prompt in prompts]
+ return [Prompt.from_dict(prompt) for prompt in prompts]
@router.post("", response_model=Prompt)
@limiter.limit("10/minute")
async def create_prompt(
+ request: Request,
prompt: Prompt,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ currentUser: User = Depends(getCurrentUser)
+) -> Prompt:
"""Create a new prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser)
# Convert Prompt to dict for interface
- prompt_data = prompt.model_dump()
+ prompt_data = prompt.to_dict()
# Create prompt
newPrompt = managementInterface.createPrompt(prompt_data)
@@ -51,14 +53,15 @@ async def create_prompt(
if "createdAt" in Prompt.getModelAttributeDefinitions() and hasattr(newPrompt, "createdAt"):
newPrompt["createdAt"] = datetime.now().isoformat()
- return Prompt(**newPrompt)
+ return Prompt.from_dict(newPrompt)
@router.get("/{promptId}", response_model=Prompt)
@limiter.limit("30/minute")
async def get_prompt(
+ request: Request,
promptId: str = Path(..., description="ID of the prompt"),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ currentUser: User = Depends(getCurrentUser)
+) -> Prompt:
"""Get a specific prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -70,15 +73,16 @@ async def get_prompt(
detail=f"Prompt with ID {promptId} not found"
)
- return Prompt(**prompt)
+ return Prompt.from_dict(prompt)
@router.put("/{promptId}", response_model=Prompt)
@limiter.limit("10/minute")
async def update_prompt(
+ request: Request,
promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Prompt = Body(...),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ currentUser: User = Depends(getCurrentUser)
+) -> Prompt:
"""Update an existing prompt"""
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -91,7 +95,7 @@ async def update_prompt(
)
# Convert Prompt to dict for interface
- update_data = promptData.model_dump()
+ update_data = promptData.to_dict()
# Update prompt
updatedPrompt = managementInterface.updatePrompt(promptId, update_data)
@@ -102,14 +106,15 @@ async def update_prompt(
detail="Error updating the prompt"
)
- return Prompt(**updatedPrompt)
+ return Prompt.from_dict(updatedPrompt)
@router.delete("/{promptId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_prompt(
+ request: Request,
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"""
managementInterface = serviceManagementClass.getInterface(currentUser)
@@ -133,8 +138,9 @@ async def delete_prompt(
@router.get("/attributes", response_model=List[AttributeDefinition])
@limiter.limit("30/minute")
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.
This can be used for dynamic form generation.
diff --git a/modules/routes/routeDataUsers.py b/modules/routes/routeDataUsers.py
index 0497fbeb..89aa0b05 100644
--- a/modules/routes/routeDataUsers.py
+++ b/modules/routes/routeDataUsers.py
@@ -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 typing import List, Dict, Any, Optional
from fastapi import status
@@ -13,8 +18,8 @@ import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import the attribute definition and helper functions
-from modules.interfaces.serviceManagementModel import User, AttributeDefinition, getModelAttributeDefinitions
-from modules.interfaces.serviceAppModel import AttributeDefinition as ServiceAppAttributeDefinition
+from modules.interfaces.serviceAppModel import User, AttributeDefinition as ServiceAppAttributeDefinition
+from modules.shared.attributeUtils import getModelAttributeDefinitions
# Configure logger
logger = logging.getLogger(__name__)
@@ -25,12 +30,17 @@ router = APIRouter(
responses={404: {"description": "Not found"}}
)
-@router.get("/", response_model=List[Dict[str, Any]], tags=["Users"])
-async def get_users(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
+@router.get("/", response_model=List[User])
+@limiter.limit("30/minute")
+async def get_users(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> List[User]:
"""Get all users in the current mandate"""
try:
- appInterface = serviceManagementClass.getInterface(currentUser)
- return appInterface.getUsers()
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+ users = managementInterface.getUsers()
+ return [User.from_dict(user) for user in users]
except Exception as e:
logger.error(f"Error getting users: {str(e)}")
raise HTTPException(
@@ -38,23 +48,25 @@ async def get_users(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
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(
- userId: str,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ userId: str = Path(..., description="ID of the user"),
+ currentUser: User = Depends(getCurrentUser)
+) -> User:
"""Get a specific user by ID"""
try:
- appInterface = serviceManagementClass.getInterface(currentUser)
- user = appInterface.getUserById(userId)
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+ user = managementInterface.getUser(userId)
if not user:
raise HTTPException(
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:
raise
except Exception as e:
@@ -64,98 +76,68 @@ async def get_user(
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(
- userData: User,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ user: User,
+ currentUser: User = Depends(getCurrentUser)
+) -> User:
"""Create a new user"""
- try:
- # Get interface for user creation
- appInterface = serviceManagementClass.getInterface(currentUser)
-
- try:
- # Convert User model to dict and pass to createUser
- 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:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to create user"
- )
-
- return createdUser
- except HTTPException:
- raise
- 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)}"
- )
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+
+ # Convert User to dict for interface
+ user_data = user.to_dict()
+
+ # Create user
+ newUser = managementInterface.createUser(user_data)
+
+ # Set current time for createdAt if it exists in the model
+ if "createdAt" in User.getModelAttributeDefinitions() and hasattr(newUser, "createdAt"):
+ newUser["createdAt"] = datetime.now().isoformat()
+
+ return User.from_dict(newUser)
-@router.put("/{userId}", response_model=User, tags=["Users"])
+@router.put("/{userId}", response_model=User)
+@limiter.limit("10/minute")
async def update_user(
- userId: str,
- userData: User,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ userId: str = Path(..., description="ID of the user to update"),
+ userData: User = Body(...),
+ currentUser: User = Depends(getCurrentUser)
+) -> User:
"""Update an existing user"""
- try:
- # Get interface for user updates
- appInterface = serviceManagementClass.getInterface(currentUser)
-
- # Check if user exists
- existingUser = appInterface.getUserById(userId)
- if not existingUser:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"User {userId} not found"
- )
-
- # Update user data
- try:
- updatedUser = appInterface.updateUser(userId, userData)
- except ValueError as e:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=str(e)
- )
-
- if not updatedUser:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to update user"
- )
-
- return updatedUser
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"Error updating user {userId}: {str(e)}")
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+
+ # Check if the user exists
+ existingUser = managementInterface.getUser(userId)
+ if not existingUser:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"User with ID {userId} not found"
+ )
+
+ # Convert User to dict for interface
+ update_data = userData.to_dict()
+
+ # Update user
+ updatedUser = managementInterface.updateUser(userId, update_data)
+
+ if not updatedUser:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update user: {str(e)}"
+ detail="Error updating the user"
)
+
+ return User.from_dict(updatedUser)
-@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(
- userId: str,
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ request: Request,
+ userId: str = Path(..., description="ID of the user to delete"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
"""Delete a user"""
try:
appInterface = serviceManagementClass.getInterface(currentUser)
@@ -176,8 +158,9 @@ async def delete_user(
@router.get("/attributes", response_model=List[ServiceAppAttributeDefinition])
@limiter.limit("30/minute")
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.
This can be used for dynamic form generation.
diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py
index 107f82c7..1a73c7b5 100644
--- a/modules/routes/routeSecurityGoogle.py
+++ b/modules/routes/routeSecurityGoogle.py
@@ -10,12 +10,12 @@ from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
-from google.auth.transport.requests import Request
+from google.auth.transport.requests import Request as GoogleRequest
from modules.shared.configuration import APP_CONFIG
-from modules.interfaces.serviceAppClass import getInterface
-from modules.interfaces.serviceAppModel import AuthAuthority
-from modules.interfaces.serviceAppTokens import GoogleToken, saveToken
+from modules.interfaces.serviceAppClass import getInterface, getRootInterface
+from modules.interfaces.serviceAppModel import AuthAuthority, User
+from modules.interfaces.serviceAppTokens import GoogleToken
from modules.security.auth import getCurrentUser, limiter
# Configure logger
@@ -46,7 +46,7 @@ SCOPES = [
@router.get("/login")
@limiter.limit("5/minute")
-async def login():
+async def login(request: Request) -> RedirectResponse:
"""Initiate Google login"""
try:
# Create OAuth flow
@@ -79,7 +79,7 @@ async def login():
)
@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"""
try:
# Create OAuth flow
@@ -111,7 +111,7 @@ async def auth_callback(code: str, request: Request):
# Save token data
appInterface = getInterface()
- saveToken(appInterface, "Google", token_data)
+ appInterface.saveToken("Google", token_data)
# Return success page with token data
return HTMLResponse(
@@ -141,24 +141,36 @@ async def auth_callback(code: str, request: Request):
detail=f"Authentication failed: {str(e)}"
)
-@router.post("/logout")
+@router.get("/me", response_model=User)
@limiter.limit("30/minute")
-async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
- """Logout from Google"""
+async def get_current_user(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> User:
+ """Get current user information"""
try:
- # Get user interface
- appInterface = getInterface()
-
- # Revoke all sessions for the user
- appInterface.revokeAllUserSessions(currentUser.get("id"))
-
- return JSONResponse({
- "message": "Successfully logged out from Google"
- })
-
+ return currentUser
+ except Exception as e:
+ logger.error(f"Error getting current user: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Failed to get current user: {str(e)}"
+ )
+
+@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:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Logout failed: {str(e)}"
+ detail=f"Failed to logout: {str(e)}"
)
\ No newline at end of file
diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py
index 6d2ffd73..4c0dce4a 100644
--- a/modules/routes/routeSecurityLocal.py
+++ b/modules/routes/routeSecurityLocal.py
@@ -2,18 +2,19 @@
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
import logging
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
-from fastapi.responses import JSONResponse
+from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse
+from pydantic import BaseModel
# Import auth modules
from modules.security.auth import createAccessToken, getCurrentUser, limiter
-from modules.interfaces.serviceAppClass import getInterface
-from modules.interfaces.serviceAppModel import User, AuthAuthority
-from modules.interfaces.serviceAppTokens import LocalToken, saveToken
+from modules.interfaces.serviceAppClass import getInterface, getRootInterface
+from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege
+from modules.interfaces.serviceAppTokens import LocalToken
# Configure logger
logger = logging.getLogger(__name__)
@@ -36,7 +37,7 @@ router = APIRouter(
async def login(
request: Request,
formData: OAuth2PasswordRequestForm = Depends(),
-):
+) -> Dict[str, Any]:
"""Get access token for local user authentication"""
try:
# Validate CSRF token
@@ -47,11 +48,22 @@ async def login(
detail="CSRF token missing"
)
- # Get gateway interface
- appInterface = getInterface()
-
+ # Get gateway interface with root privileges for authentication
+ 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
- user = appInterface.authenticateLocalUser(
+ user = rootInterface.authenticateLocalUser(
username=formData.username,
password=formData.password
)
@@ -62,7 +74,7 @@ async def login(
detail="Invalid username or password",
headers={"WWW-Authenticate": "Bearer"},
)
-
+
# Create token data
token_data = {
"sub": user.username,
@@ -79,13 +91,16 @@ async def login(
detail="Failed to create access token"
)
+ # Get user-specific interface for token operations
+ userInterface = getInterface(user)
+
# Save token data
token_data = {
"access_token": access_token,
"token_type": "bearer",
"expires_at": expires_at.timestamp()
}
- saveToken(appInterface, "Local", token_data)
+ userInterface.saveToken("Local", token_data)
# Create response data
response_data = {
@@ -115,11 +130,16 @@ async def login(
)
@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."""
try:
- # Get gateway interface
- appInterface = getInterface()
+ # Get gateway interface with root privileges since this is a public endpoint
+ appInterface = getRootInterface()
# Get default mandate ID
defaultMandateId = appInterface.getInitialId("mandates")
@@ -129,22 +149,28 @@ async def register_user(userData: User):
detail="No default mandate found"
)
- # Create user with default mandate
- user = appInterface.createUser(
+ # Set the mandate ID on the interface
+ appInterface.mandateId = defaultMandateId
+
+ # Create user with individual parameters
+ newUser = appInterface.createUser(
username=userData.username,
- password=userData.password,
+ password=password, # Pass the plain text password - createUser will hash it
email=userData.email,
- mandateId=defaultMandateId, # Use default mandate instead of userData.mandateId
- authenticationAuthority=AuthAuthority.LOCAL
+ fullName=userData.fullName,
+ language=userData.language,
+ disabled=userData.disabled,
+ privilege=userData.privilege,
+ authenticationAuthority=userData.authenticationAuthority
)
- if not user:
+ if not newUser:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to register user"
)
- return user
+ return newUser
except ValueError as e:
raise HTTPException(
@@ -158,22 +184,32 @@ async def register_user(userData: User):
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")
-async def read_user_me(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
- """Get current user information"""
- return currentUser
+async def read_user_me(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> User:
+ """Get current user info"""
+ try:
+ 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")
@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"""
try:
- # Get user interface
- appInterface = getInterface()
+ # Get user interface with current user context
+ appInterface = getInterface(currentUser)
# Revoke all sessions for the user
- appInterface.revokeAllUserSessions(currentUser.get("id"))
+ appInterface.revokeAllUserSessions(currentUser.id)
return JSONResponse({
"message": "Successfully logged out"
@@ -186,15 +222,31 @@ async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
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(
+ request: Request,
username: str,
authenticationAuthority: str = "local"
-):
- """Check if a username is available for registration"""
+) -> Dict[str, Any]:
+ """Check if a username is available for registration."""
try:
- interfaceRoot = getInterface()
- return interfaceRoot.checkUsernameAvailability(username, authenticationAuthority)
+ # Get root interface
+ 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:
logger.error(f"Error checking username availability: {str(e)}")
raise HTTPException(
diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py
index cf55f420..3776ee55 100644
--- a/modules/routes/routeSecurityMsft.py
+++ b/modules/routes/routeSecurityMsft.py
@@ -2,7 +2,7 @@
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
import logging
import json
@@ -11,9 +11,9 @@ from datetime import datetime, timedelta
import msal
from modules.shared.configuration import APP_CONFIG
-from modules.interfaces.serviceAppClass import getInterface
-from modules.interfaces.serviceAppModel import AuthAuthority
-from modules.interfaces.serviceAppTokens import MsftToken, saveToken
+from modules.interfaces.serviceAppClass import getInterface, getRootInterface
+from modules.interfaces.serviceAppModel import AuthAuthority, User
+from modules.interfaces.serviceAppTokens import MsftToken
from modules.security.auth import getCurrentUser, limiter
# Configure logger
@@ -42,7 +42,7 @@ SCOPES = ["Mail.ReadWrite", "User.Read"]
@router.get("/login")
@limiter.limit("5/minute")
-async def login():
+async def login(request: Request) -> RedirectResponse:
"""Initiate Microsoft login"""
try:
# Create MSAL app
@@ -68,7 +68,7 @@ async def login():
)
@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"""
try:
# Create MSAL app
@@ -101,7 +101,7 @@ async def auth_callback(code: str, request: Request):
# Save token data
appInterface = getInterface()
- saveToken(appInterface, "Msft", token_data)
+ appInterface.saveToken("Msft", token_data)
# Return success page with token data
return HTMLResponse(
@@ -131,24 +131,36 @@ async def auth_callback(code: str, request: Request):
detail=f"Authentication failed: {str(e)}"
)
-@router.post("/logout")
+@router.get("/me", response_model=User)
@limiter.limit("30/minute")
-async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
- """Logout from Microsoft"""
+async def get_current_user(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> User:
+ """Get current user information"""
try:
- # Get user interface
- appInterface = getInterface()
-
- # Revoke all sessions for the user
- appInterface.revokeAllUserSessions(currentUser.get("id"))
-
- return JSONResponse({
- "message": "Successfully logged out from Microsoft"
- })
-
+ return currentUser
+ except Exception as e:
+ logger.error(f"Error getting current user: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Failed to get current user: {str(e)}"
+ )
+
+@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:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Logout failed: {str(e)}"
+ detail=f"Failed to logout: {str(e)}"
)
diff --git a/modules/routes/routeWorkflows.py b/modules/routes/routeWorkflows.py
index 8cea957c..82d6e54f 100644
--- a/modules/routes/routeWorkflows.py
+++ b/modules/routes/routeWorkflows.py
@@ -7,14 +7,16 @@ import os
import json
import logging
from typing import List, Dict, Any, Optional
-from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status
-from datetime import datetime
+from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status, Request
+from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
+from datetime import datetime, timedelta
# Import auth modules
from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceChatClass as serviceChatClass
+from modules.interfaces.serviceChatClass import getInterface
# Import workflow manager
from modules.workflow.workflowManager import getWorkflowManager
@@ -26,10 +28,10 @@ from modules.interfaces.serviceChatModel import (
ChatLog,
ChatStat,
ChatDocument,
- UserInputRequest,
- Workflow,
- getModelAttributeDefinitions
+ UserInputRequest
)
+from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.interfaces.serviceAppModel import User
# Configure logger
logger = logging.getLogger(__name__)
@@ -58,34 +60,32 @@ def createServiceContainer(currentUser: Dict[str, Any]):
return service
# API Endpoint for getting all workflows
-@router.get("", response_model=List[ChatWorkflow])
+@router.get("/list", response_model=List[ChatWorkflow])
@limiter.limit("30/minute")
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."""
try:
- # Get service container
- service = createServiceContainer(currentUser)
-
- # Retrieve workflows for the user
- workflows = service.base.getWorkflowsByUser(currentUser["id"])
- return [ChatWorkflow(**workflow) for workflow in workflows]
+ appInterface = getInterface(currentUser)
+ return appInterface.getAllWorkflows()
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(
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
@router.post("/start", response_model=ChatWorkflow)
@limiter.limit("10/minute")
async def start_workflow(
+ request: Request,
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
userInput: UserInputRequest = Body(...),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ currentUser: User = Depends(getCurrentUser)
+) -> ChatWorkflow:
"""
Starts a new workflow or continues an existing one.
Corresponds to State 1 in the state machine documentation.
@@ -100,19 +100,23 @@ async def start_workflow(
# Start or continue workflow
workflow = await workflowManager.workflowStart(userInput, workflowId)
- return workflow
+ return ChatWorkflow(**workflow)
except Exception as 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
@router.post("/{workflowId}/stop", response_model=ChatWorkflow)
@limiter.limit("10/minute")
async def stop_workflow(
+ request: Request,
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."""
try:
# Get service container
@@ -124,19 +128,23 @@ async def stop_workflow(
# Stop workflow
workflow = await workflowManager.workflowStop(workflowId)
- return workflow
+ return ChatWorkflow(**workflow)
except Exception as 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
@router.delete("/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_workflow(
+ request: Request,
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."""
try:
# Get service container
@@ -183,9 +191,10 @@ async def delete_workflow(
@router.get("/{workflowId}/status", response_model=ChatWorkflow)
@limiter.limit("30/minute")
async def get_workflow_status(
+ request: Request,
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."""
try:
# Get service container
@@ -213,10 +222,11 @@ async def get_workflow_status(
@router.get("/{workflowId}/logs", response_model=List[ChatLog])
@limiter.limit("30/minute")
async def get_workflow_logs(
+ request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
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."""
try:
# Get service container
@@ -255,10 +265,11 @@ async def get_workflow_logs(
@router.get("/{workflowId}/messages", response_model=List[ChatMessage])
@limiter.limit("30/minute")
async def get_workflow_messages(
+ request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
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."""
try:
# Get service container
@@ -277,20 +288,12 @@ async def get_workflow_messages(
# Apply selective data transfer if messageId is provided
if messageId:
- # Find the index of the specified message based on messageIds array
- messageIds = workflow.get("messageIds", [])
- if messageId in messageIds:
- messageIndex = messageIds.index(messageId)
- # Return messages from this index onwards based on the messageIds order
- 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]
+ # Find the index of the message with the given ID
+ messageIndex = next((i for i, msg in enumerate(allMessages) if msg.get("id") == messageId), -1)
+ if messageIndex >= 0:
+ # Return only messages after the specified message
+ return [ChatMessage(**msg) for msg in allMessages[messageIndex + 1:]]
- # Sort messages by sequenceNo
- allMessages.sort(key=lambda x: x.get("sequenceNo", 0))
return [ChatMessage(**msg) for msg in allMessages]
except HTTPException:
raise
@@ -306,16 +309,17 @@ async def get_workflow_messages(
@router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_workflow_message(
+ request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
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."""
try:
# Get service container
service = createServiceContainer(currentUser)
- # Verify workflow exists and belongs to user
+ # Verify workflow exists
workflow = service.base.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
@@ -355,17 +359,18 @@ async def delete_workflow_message(
@router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_file_from_message(
+ request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"),
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."""
try:
# Get service container
service = createServiceContainer(currentUser)
- # Verify workflow exists and belongs to user
+ # Verify workflow exists
workflow = service.base.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
@@ -402,89 +407,24 @@ async def delete_file_from_message(
@router.get("/files/{fileId}/preview", response_model=ChatDocument)
@limiter.limit("30/minute")
async def preview_file(
+ request: Request,
fileId: str = Path(..., description="ID of the file to preview"),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
- """Get file metadata and a preview of the file content."""
+ currentUser: User = Depends(getCurrentUser)
+) -> ChatDocument:
+ """Preview a file's content."""
try:
# Get service container
service = createServiceContainer(currentUser)
- # Get file metadata
- file = service.base.getFile(fileId)
- if not file:
+ # Get file document
+ document = service.base.getFileDocument(fileId)
+ if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found"
)
- # Get file data (limited for preview)
- 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/")
- }
- }]
- )
+ return ChatDocument(**document)
except HTTPException:
raise
except Exception as e:
@@ -497,9 +437,10 @@ async def preview_file(
@router.get("/files/{fileId}/download")
@limiter.limit("30/minute")
async def download_file(
+ request: Request,
fileId: str = Path(..., description="ID of the file to download"),
- currentUser: Dict[str, Any] = Depends(getCurrentUser)
-):
+ currentUser: User = Depends(getCurrentUser)
+) -> Response:
"""Download a file."""
try:
# Get service container
@@ -529,3 +470,54 @@ async def download_file(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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)}"
+ )
diff --git a/modules/security/auth.py b/modules/security/auth.py
index a2ad51b3..a1f31591 100644
--- a/modules/security/auth.py
+++ b/modules/security/auth.py
@@ -14,7 +14,7 @@ from slowapi.util import get_remote_address
from modules.shared.configuration import APP_CONFIG
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
SECRET_KEY = APP_CONFIG.get("APP_JWT_SECRET_SECRET")
@@ -72,7 +72,7 @@ def createRefreshToken(data: dict) -> Tuple[str, datetime]:
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.
@@ -80,7 +80,7 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
token: JWT Token from the Authorization header
Returns:
- User data
+ User model instance
Raises:
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")
raise credentialsException
- if user.get("disabled", False):
+ if user.disabled:
logger.warning(f"User {username} is disabled")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
# Ensure the user has the correct context
- if str(user.get("mandateId")) != str(mandateId) or str(user.get("id")) != str(userId):
- logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.get('mandateId')}, id={user.get('id')})")
+ if str(user.mandateId) != str(mandateId) or str(user.id) != str(userId):
+ logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.mandateId}, id={user.id})")
raise credentialsException
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."""
- if currentUser.get("disabled", False):
+ if currentUser.disabled:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User is disabled"
@@ -155,7 +155,17 @@ def createUserSession(userId: str, tokenId: str, request: Request) -> Session:
)
# 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
@@ -172,7 +182,7 @@ def logAuthEvent(userId: str, eventType: str, details: Dict[str, Any], request:
)
# 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:
"""Validate a user session."""
diff --git a/modules/shared/attributeUtils.py b/modules/shared/attributeUtils.py
index f6db84aa..f63e8531 100644
--- a/modules/shared/attributeUtils.py
+++ b/modules/shared/attributeUtils.py
@@ -21,7 +21,10 @@ class BaseModelWithUI(BaseModel):
def to_dict(self) -> Dict[str, Any]:
"""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
def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI':
@@ -29,23 +32,47 @@ class BaseModelWithUI(BaseModel):
return cls(**data)
@classmethod
- def getModelAttributeDefinitions(cls) -> Dict[str, Any]:
+ def getModelAttributeDefinitions(cls) -> List[Dict[str, Any]]:
"""
Get attribute definitions for this model class.
Override this method in model classes to provide custom attribute definitions.
Returns:
- Dict[str, Any]: Dictionary of attribute definitions
+ List[Dict[str, Any]]: List of attribute definitions
"""
- return {
- name: {
- "type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
- "required": field.is_required() if hasattr(field, "is_required") else True,
- "description": field.description if hasattr(field, "description") else "",
- "label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name
- }
- for name, field in cls.model_fields.items()
- }
+ attributes = []
+
+ # 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),
+ "required": field.is_required() if hasattr(field, "is_required") else True,
+ "description": field.description if hasattr(field, "description") else "",
+ "label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
+ "placeholder": f"Please enter {name}",
+ "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):
"""
diff --git a/modules/shared/configuration.py b/modules/shared/configuration.py
index f7e9b81f..e526674e 100644
--- a/modules/shared/configuration.py
+++ b/modules/shared/configuration.py
@@ -10,7 +10,14 @@ import logging
from typing import Any, Dict, Optional
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__)
class Configuration:
@@ -34,14 +41,11 @@ class Configuration:
def _loadConfig(self):
"""Load configuration from config.ini file in flattened format"""
- # Find config.ini file (look in current directory and parent directory)
- configPath = Path('config.ini')
+ # Find config.ini file in the gateway directory
+ configPath = Path(__file__).parent.parent.parent / 'config.ini'
if not configPath.exists():
- # Try in parent directory
- configPath = Path('../config.ini')
- if not configPath.exists():
- logger.warning(f"Configuration file not found at {configPath.absolute()}")
- return
+ logger.warning(f"Configuration file not found at {configPath.absolute()}")
+ return
self._configFilePath = configPath
currentMtime = os.path.getmtime(configPath)
@@ -75,14 +79,11 @@ class Configuration:
def _loadEnv(self):
"""Load environment variables from .env file"""
- # Find .env file (look in current directory and parent directory)
- envPath = Path('.env')
+ # Find .env file in the gateway directory
+ envPath = Path(__file__).parent.parent.parent / 'env_dev.env'
if not envPath.exists():
- # Try in parent directory
- envPath = Path('../.env')
- if not envPath.exists():
- logger.warning(f"Environment file not found at {envPath.absolute()}")
- return
+ logger.warning(f"Environment file not found at {envPath.absolute()}")
+ return
self._envFilePath = envPath
currentMtime = os.path.getmtime(envPath)
diff --git a/modules/workflow/agentManager.py b/modules/workflow/agentManager.py
index 0b97adca..fba60ac5 100644
--- a/modules/workflow/agentManager.py
+++ b/modules/workflow/agentManager.py
@@ -192,7 +192,7 @@ class AgentManager:
performance={},
progress=0.0
),
- Task(**{**task.model_dump(), "status": "failed", "error": error_msg})
+ Task(**{**task.to_dict(), "status": "failed", "error": error_msg})
)
try:
diff --git a/modules/workflow/agentRegistry.py b/modules/workflow/agentRegistry.py
deleted file mode 100644
index 7677f9e7..00000000
--- a/modules/workflow/agentRegistry.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/modules/workflow/workflowManager.py b/modules/workflow/workflowManager.py
index 28294960..79d6d56f 100644
--- a/modules/workflow/workflowManager.py
+++ b/modules/workflow/workflowManager.py
@@ -20,7 +20,7 @@ from modules.workflow.taskManager import getTaskManager
from modules.workflow.documentManager import getDocumentManager
from modules.interfaces.serviceChatModel import (
UserInputRequest, ChatWorkflow, ChatMessage, ChatLog,
- ChatDocument, ChatStat, Workflow, Task, AgentResponse
+ ChatDocument, ChatStat, Task, AgentResponse, AgentProfile
)
# Configure logger
@@ -360,8 +360,8 @@ class WorkflowManager:
self.service.functions.updateWorkflow(workflow.id, {
"status": workflow.status,
"lastActivity": workflow.lastActivity,
- "stats": workflow.stats.model_dump(),
- "messages": [msg.model_dump() for msg in workflow.messages]
+ "stats": workflow.stats.to_dict(),
+ "messages": [msg.to_dict() for msg in workflow.messages]
})
return workflow
@@ -380,7 +380,7 @@ class WorkflowManager:
self.service.functions.updateWorkflow(workflow.id, {
"status": "failed",
"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)
@@ -421,7 +421,7 @@ class WorkflowManager:
)
# Save to database - only the workflow metadata
- workflowDb = workflow.model_dump()
+ workflowDb = workflow.to_dict()
self.service.functions.createWorkflow(workflowDb)
self.logAdd(workflow, GLOBAL_WORKFLOW_LABELS["workflowStatusMessages"]["init"], level="info", progress=0)
@@ -456,7 +456,7 @@ class WorkflowManager:
"status": workflow.status,
"lastActivity": workflow.lastActivity,
"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)
@@ -623,7 +623,7 @@ JSON_OUTPUT = {{
logger.debug(f"PROJECT MANAGER Planning answer: {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).
Uses the new Task and AgentResponse models.
@@ -660,7 +660,7 @@ JSON_OUTPUT = {{
# Update in database
self.service.functions.updateWorkflow(workflow.id, {
- "stats": workflow.stats.model_dump()
+ "stats": workflow.stats.to_dict()
})
# Log the agent response
@@ -685,7 +685,7 @@ JSON_OUTPUT = {{
self.logAdd(workflow, errorMsg, level="error")
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).
@@ -857,7 +857,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
# Update workflow in database
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
@@ -931,7 +931,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow.stats.tokensUsed += tokensUsed
# Create ChatMessage object
- chatMessage = ChatMessage(**message.model_dump())
+ chatMessage = ChatMessage(**message.to_dict())
# Add message to workflow
workflow.messages.append(chatMessage)
@@ -947,13 +947,13 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
workflow.lastActivity = currentTime
# 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
workflowUpdate = {
"lastActivity": currentTime,
"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)
@@ -1018,7 +1018,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
logger.log(logLevel, f"[Workflow {workflow.id}] {message}")
# Save to database
- self.service.functions.saveWorkflowLog(workflow.id, logEntry.model_dump())
+ self.service.functions.saveWorkflowLog(workflow.id, logEntry.to_dict())
return logId
@@ -1109,7 +1109,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
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.
@@ -1171,7 +1171,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
logger.info(f"Available documents: {len(availableDocs)}")
return availableDocs
- def agentProfiles(self) -> List[Dict[str, Any]]:
+ def agentProfiles(self) -> List[AgentProfile]:
"""
Gets information about all available agents.
@@ -1247,7 +1247,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"userLanguage": "en"
}
- def _createWorkflowData(self, workflow: ChatWorkflow) -> Dict[str, Any]:
+ def _createWorkflowData(self, workflow: ChatWorkflow) -> ChatWorkflow:
"""Creates a workflow data structure."""
return {
"mandateId": self.functions.mandateId,
@@ -1256,7 +1256,7 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"status": workflow.status,
"startedAt": workflow.startedAt,
"lastActivity": workflow.lastActivity,
- "stats": workflow.stats.model_dump()
+ "stats": workflow.stats.to_dict()
}
def _checkFileAccess(self, fileId: int) -> bool:
diff --git a/notes/changelog.txt b/notes/changelog.txt
index 1f195f36..b128aa28 100644
--- a/notes/changelog.txt
+++ b/notes/changelog.txt
@@ -1,5 +1,13 @@
....................... 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:
- register
@@ -8,6 +16,9 @@ Walkthroughs:
- management pages
- workflow
+Install a Test environment with same prod_env
+- add CORS url names to prod_env
+-
----------------------- OPEN