table rendering with proper api controled rendering

This commit is contained in:
ValueOn AG 2025-11-01 23:00:55 +01:00
parent 37aad732f5
commit 4d3ca3342a
11 changed files with 1065 additions and 144 deletions

View file

@ -0,0 +1,72 @@
"""
Pagination models for server-side pagination, sorting, and filtering.
All models use camelStyle naming convention for consistency with frontend.
"""
from typing import List, Dict, Any, Optional, Generic, TypeVar
from pydantic import BaseModel, Field
import math
T = TypeVar('T')
class SortField(BaseModel):
"""
Single sort field configuration.
"""
field: str = Field(..., description="Field name to sort by")
direction: str = Field(..., description="Sort direction: 'asc' or 'desc'")
class PaginationParams(BaseModel):
"""
Complete pagination state including page, sorting, and filters.
"""
page: int = Field(ge=1, description="Current page number (1-based)")
pageSize: int = Field(ge=1, le=1000, description="Number of items per page")
sort: List[SortField] = Field(default_factory=list, description="List of sort fields in priority order")
filters: Optional[Dict[str, Any]] = Field(default=None, description="Filter criteria (structure TBD for future implementation)")
class PaginationRequest(BaseModel):
"""
Pagination request parameters sent from frontend to backend.
All fields are optional. If pagination=None, no pagination is applied.
"""
pagination: Optional[PaginationParams] = None
class PaginatedResult(BaseModel):
"""
Internal result structure from interface layer.
Used when pagination is requested.
"""
items: List[Any]
totalItems: int
totalPages: int # Calculated as: math.ceil(totalItems / pageSize)
class PaginationMetadata(BaseModel):
"""
Pagination metadata returned to frontend for rendering controls.
Contains all information needed to render pagination UI and handle user interactions.
"""
currentPage: int = Field(..., description="Current page number (1-based)")
pageSize: int = Field(..., description="Number of items per page")
totalItems: int = Field(..., description="Total number of items across all pages (after filters)")
totalPages: int = Field(..., description="Total number of pages (calculated from totalItems / pageSize)")
sort: List[SortField] = Field(..., description="Current sort configuration applied")
filters: Optional[Dict[str, Any]] = Field(default=None, description="Current filters applied (for future use)")
class PaginatedResponse(BaseModel, Generic[T]):
"""
Response containing paginated data and metadata.
"""
items: List[T] = Field(..., description="Array of items for current page")
pagination: Optional[PaginationMetadata] = Field(..., description="Pagination metadata (None if pagination not applied)")
class Config:
arbitrary_types_allowed = True

View file

@ -4,7 +4,8 @@ Manages users and mandates for authentication.
""" """
import logging import logging
from typing import Dict, Any, List, Optional import math
from typing import Dict, Any, List, Optional, Union
from passlib.context import CryptContext from passlib.context import CryptContext
import uuid import uuid
@ -26,6 +27,7 @@ from modules.datamodels.datamodelNeutralizer import (
DataNeutraliserConfig, DataNeutraliserConfig,
DataNeutralizerAttributes, DataNeutralizerAttributes,
) )
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -233,6 +235,57 @@ class AppObjects:
""" """
return self.access.canModify(model_class, recordId) return self.access.canModify(model_class, recordId)
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Apply filter criteria to records (implementation for future filtering)."""
# TODO: Implement filtering logic when needed
return records
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
if not sortFields:
return records
# Start with a copy to avoid modifying original
sortedRecords = list(records)
# Sort from least significant to most significant field (reverse order)
# Python's sort is stable, so this creates proper multi-level sorting
for sortField in reversed(sortFields):
# Handle both dict and object formats
if isinstance(sortField, dict):
fieldName = sortField.get("field")
direction = sortField.get("direction", "asc")
else:
fieldName = getattr(sortField, "field", None)
direction = getattr(sortField, "direction", "asc")
if not fieldName:
continue
isDesc = (direction == "desc")
def sortKey(record):
value = record.get(fieldName)
# Handle None values - place them at the end for both directions
if value is None:
# Use a special value that sorts last
return (1, "") # (is_none_flag, empty_value) - sorts after (0, ...)
else:
# Return tuple with type indicator for proper comparison
if isinstance(value, (int, float)):
return (0, value)
elif isinstance(value, str):
return (0, value)
elif isinstance(value, bool):
return (0, value)
else:
return (0, str(value))
# Sort with reverse parameter for descending
sortedRecords.sort(key=sortKey, reverse=isDesc)
return sortedRecords
def getInitialId(self, model_class: type) -> Optional[str]: def getInitialId(self, model_class: type) -> Optional[str]:
"""Returns the initial ID for a table.""" """Returns the initial ID for a table."""
return self.db.getInitialId(model_class) return self.db.getInitialId(model_class)
@ -247,14 +300,52 @@ class AppObjects:
# User methods # User methods
def getUsersByMandate(self, mandateId: str) -> List[User]: def getUsersByMandate(self, mandateId: str, pagination: Optional[PaginationParams] = None) -> Union[List[User], PaginatedResult]:
"""Returns users for a specific mandate if user has access.""" """
Returns users for a specific mandate if user has access.
Supports optional pagination, sorting, and filtering.
Args:
mandateId: The mandate ID to get users for
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[User]
If pagination is provided: PaginatedResult with items and metadata
"""
# Get users for this mandate # Get users for this mandate
users = self.db.getRecordset(UserInDB, recordFilter={"mandateId": mandateId}) users = self.db.getRecordset(UserInDB, recordFilter={"mandateId": mandateId})
filteredUsers = self._uam(UserInDB, users) filteredUsers = self._uam(UserInDB, users)
# Convert to User models # If no pagination requested, return all items
return [User(**user) for user in filteredUsers] if pagination is None:
return [User(**user) for user in filteredUsers]
# Apply filtering (if filters provided)
if pagination.filters:
filteredUsers = self._applyFilters(filteredUsers, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination.sort:
filteredUsers = self._applySorting(filteredUsers, pagination.sort)
# Count total items after filters
totalItems = len(filteredUsers)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedUsers = filteredUsers[startIdx:endIdx]
# Convert to model objects
items = [User(**user) for user in pagedUsers]
return PaginatedResult(
items=items,
totalItems=totalItems,
totalPages=totalPages
)
def getUserByUsername(self, username: str) -> Optional[User]: def getUserByUsername(self, username: str) -> Optional[User]:
"""Returns a user by username.""" """Returns a user by username."""
@ -638,11 +729,50 @@ class AppObjects:
# Mandate methods # Mandate methods
def getAllMandates(self) -> List[Mandate]: def getAllMandates(self, pagination: Optional[PaginationParams] = None) -> Union[List[Mandate], PaginatedResult]:
"""Returns all mandates based on user access level.""" """
Returns all mandates based on user access level.
Supports optional pagination, sorting, and filtering.
Args:
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[Mandate]
If pagination is provided: PaginatedResult with items and metadata
"""
allMandates = self.db.getRecordset(Mandate) allMandates = self.db.getRecordset(Mandate)
filteredMandates = self._uam(Mandate, allMandates) filteredMandates = self._uam(Mandate, allMandates)
return [Mandate(**mandate) for mandate in filteredMandates]
# If no pagination requested, return all items
if pagination is None:
return [Mandate(**mandate) for mandate in filteredMandates]
# Apply filtering (if filters provided)
if pagination.filters:
filteredMandates = self._applyFilters(filteredMandates, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination.sort:
filteredMandates = self._applySorting(filteredMandates, pagination.sort)
# Count total items after filters
totalItems = len(filteredMandates)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedMandates = filteredMandates[startIdx:endIdx]
# Convert to model objects
items = [Mandate(**mandate) for mandate in pagedMandates]
return PaginatedResult(
items=items,
totalItems=totalItems,
totalPages=totalPages
)
def getMandate(self, mandateId: str) -> Optional[Mandate]: def getMandate(self, mandateId: str) -> Optional[Mandate]:
"""Returns a mandate by ID if user has access.""" """Returns a mandate by ID if user has access."""

View file

@ -3,24 +3,16 @@ Interface to LucyDOM database and AI Connectors.
Uses the JSON connector for data access with added language support. Uses the JSON connector for data access with added language support.
""" """
import os
import logging import logging
import uuid import uuid
from datetime import datetime, UTC, timezone import math
from typing import Dict, Any, List, Optional, Union, get_origin, get_args from typing import Dict, Any, List, Optional, Union
import asyncio import asyncio
from modules.interfaces.interfaceDbChatAccess import ChatAccess from modules.interfaces.interfaceDbChatAccess import ChatAccess
from modules.datamodels.datamodelChat import ( from modules.datamodels.datamodelChat import (
ActionItem,
TaskResult,
TaskItem,
TaskStatus,
ActionResult
)
from modules.datamodels.datamodelChat import (
UserInputRequest,
ChatDocument, ChatDocument,
ChatStat, ChatStat,
ChatLog, ChatLog,
@ -32,6 +24,7 @@ from modules.datamodels.datamodelUam import User
# DYNAMIC PART: Connectors to the Interface # DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.shared.timezoneUtils import getUtcTimestamp from modules.shared.timezoneUtils import getUtcTimestamp
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
# Basic Configurations # Basic Configurations
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
@ -197,6 +190,56 @@ class ChatObjects:
"""Delegate to access control module.""" """Delegate to access control module."""
return self.access.canModify(model_class, recordId) return self.access.canModify(model_class, recordId)
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Apply filter criteria to records (implementation for future filtering)."""
# TODO: Implement filtering logic when needed
return records
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
if not sortFields:
return records
# Start with a copy to avoid modifying original
sortedRecords = list(records)
# Sort from least significant to most significant field (reverse order)
# Python's sort is stable, so this creates proper multi-level sorting
for sortField in reversed(sortFields):
# Handle both dict and object formats
if isinstance(sortField, dict):
fieldName = sortField.get("field")
direction = sortField.get("direction", "asc")
else:
fieldName = getattr(sortField, "field", None)
direction = getattr(sortField, "direction", "asc")
if not fieldName:
continue
isDesc = (direction == "desc")
def sortKey(record):
value = record.get(fieldName)
# Handle None values - place them at the end for both directions
if value is None:
# Use a special value that sorts last
return (1, "") # (is_none_flag, empty_value) - sorts after (0, ...)
else:
# Return tuple with type indicator for proper comparison
if isinstance(value, (int, float)):
return (0, value)
elif isinstance(value, str):
return (0, value)
elif isinstance(value, bool):
return (0, value)
else:
return (0, str(value))
# Sort with reverse parameter for descending
sortedRecords.sort(key=sortKey, reverse=isDesc)
return sortedRecords
# Utilities # Utilities
@ -208,10 +251,47 @@ class ChatObjects:
# Workflow methods # Workflow methods
def getWorkflows(self) -> List[Dict[str, Any]]: def getWorkflows(self, pagination: Optional[PaginationParams] = None) -> Union[List[Dict[str, Any]], PaginatedResult]:
"""Returns workflows based on user access level.""" """
Returns workflows based on user access level.
Supports optional pagination, sorting, and filtering.
Args:
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[Dict[str, Any]]
If pagination is provided: PaginatedResult with items and metadata
"""
allWorkflows = self.db.getRecordset(ChatWorkflow) allWorkflows = self.db.getRecordset(ChatWorkflow)
return self._uam(ChatWorkflow, allWorkflows) filteredWorkflows = self._uam(ChatWorkflow, allWorkflows)
# If no pagination requested, return all items
if pagination is None:
return filteredWorkflows
# Apply filtering (if filters provided)
if pagination.filters:
filteredWorkflows = self._applyFilters(filteredWorkflows, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination.sort:
filteredWorkflows = self._applySorting(filteredWorkflows, pagination.sort)
# Count total items after filters
totalItems = len(filteredWorkflows)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedWorkflows = filteredWorkflows[startIdx:endIdx]
return PaginatedResult(
items=pagedWorkflows,
totalItems=totalItems,
totalPages=totalPages
)
def getWorkflow(self, workflowId: str) -> Optional[ChatWorkflow]: def getWorkflow(self, workflowId: str) -> Optional[ChatWorkflow]:
"""Returns a workflow by ID if user has access.""" """Returns a workflow by ID if user has access."""
@ -389,26 +469,118 @@ class ChatObjects:
# Message methods # Message methods
def getMessages(self, workflowId: str) -> List[ChatMessage]: def getMessages(self, workflowId: str, pagination: Optional[PaginationParams] = None) -> Union[List[ChatMessage], PaginatedResult]:
"""Returns messages for a workflow if user has access to the workflow.""" """
Returns messages for a workflow if user has access to the workflow.
Supports optional pagination, sorting, and filtering.
Args:
workflowId: The workflow ID to get messages for
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[ChatMessage]
If pagination is provided: PaginatedResult with items and metadata
"""
# Check workflow access first (without calling getWorkflow to avoid circular reference) # Check workflow access first (without calling getWorkflow to avoid circular reference)
workflows = self.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId}) workflows = self.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
if not workflows: if not workflows:
return [] if pagination is None:
return []
return PaginatedResult(items=[], totalItems=0, totalPages=0)
filteredWorkflows = self._uam(ChatWorkflow, workflows) filteredWorkflows = self._uam(ChatWorkflow, workflows)
if not filteredWorkflows: if not filteredWorkflows:
return [] if pagination is None:
return []
return PaginatedResult(items=[], totalItems=0, totalPages=0)
# Get messages for this workflow from normalized table # Get messages for this workflow from normalized table
messages = self.db.getRecordset(ChatMessage, recordFilter={"workflowId": workflowId}) messages = self.db.getRecordset(ChatMessage, recordFilter={"workflowId": workflowId})
# Sort messages by publishedAt timestamp to ensure chronological order # Convert raw messages to dict format for sorting/filtering
messages.sort(key=lambda x: x.get("publishedAt", x.get("timestamp", "0"))) messageDicts = []
for msg in messages:
messageDicts.append({
"id": msg.get("id"),
"workflowId": msg.get("workflowId"),
"parentMessageId": msg.get("parentMessageId"),
"documentsLabel": msg.get("documentsLabel"),
"message": msg.get("message"),
"role": msg.get("role", "assistant"),
"status": msg.get("status", "step"),
"sequenceNr": msg.get("sequenceNr", 0),
"publishedAt": msg.get("publishedAt", msg.get("timestamp", getUtcTimestamp())),
"success": msg.get("success"),
"actionId": msg.get("actionId"),
"actionMethod": msg.get("actionMethod"),
"actionName": msg.get("actionName"),
"roundNumber": msg.get("roundNumber"),
"taskNumber": msg.get("taskNumber"),
"actionNumber": msg.get("actionNumber"),
"taskProgress": msg.get("taskProgress"),
"actionProgress": msg.get("actionProgress")
})
# Apply default sorting by publishedAt if no sort specified
if pagination is None or not pagination.sort:
messageDicts.sort(key=lambda x: x.get("publishedAt", getUtcTimestamp()))
# Apply filtering (if filters provided)
if pagination and pagination.filters:
messageDicts = self._applyFilters(messageDicts, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination and pagination.sort:
messageDicts = self._applySorting(messageDicts, pagination.sort)
# If no pagination requested, return all items
if pagination is None:
# Convert messages to ChatMessage objects and load documents
chat_messages = []
for msg in messageDicts:
# Load documents from normalized documents table
documents = self.getDocuments(msg["id"])
# Create ChatMessage object with loaded documents
chat_message = ChatMessage(
id=msg["id"],
workflowId=msg["workflowId"],
parentMessageId=msg.get("parentMessageId"),
documents=documents,
documentsLabel=msg.get("documentsLabel"),
message=msg.get("message"),
role=msg.get("role", "assistant"),
status=msg.get("status", "step"),
sequenceNr=msg.get("sequenceNr", 0),
publishedAt=msg.get("publishedAt", getUtcTimestamp()),
success=msg.get("success"),
actionId=msg.get("actionId"),
actionMethod=msg.get("actionMethod"),
actionName=msg.get("actionName"),
roundNumber=msg.get("roundNumber"),
taskNumber=msg.get("taskNumber"),
actionNumber=msg.get("actionNumber"),
taskProgress=msg.get("taskProgress"),
actionProgress=msg.get("actionProgress")
)
chat_messages.append(chat_message)
return chat_messages
# Count total items after filters
totalItems = len(messageDicts)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedMessageDicts = messageDicts[startIdx:endIdx]
# Convert messages to ChatMessage objects and load documents # Convert messages to ChatMessage objects and load documents
chat_messages = [] chat_messages = []
for msg in messages: for msg in pagedMessageDicts:
# Load documents from normalized documents table # Load documents from normalized documents table
documents = self.getDocuments(msg["id"]) documents = self.getDocuments(msg["id"])
@ -437,8 +609,11 @@ class ChatObjects:
chat_messages.append(chat_message) chat_messages.append(chat_message)
return PaginatedResult(
return chat_messages items=chat_messages,
totalItems=totalItems,
totalPages=totalPages
)
def createMessage(self, messageData: Dict[str, Any]) -> ChatMessage: def createMessage(self, messageData: Dict[str, Any]) -> ChatMessage:
"""Creates a message for a workflow if user has access.""" """Creates a message for a workflow if user has access."""
@ -765,24 +940,84 @@ class ChatObjects:
# Log methods # Log methods
def getLogs(self, workflowId: str) -> List[ChatLog]: def getLogs(self, workflowId: str, pagination: Optional[PaginationParams] = None) -> Union[List[ChatLog], PaginatedResult]:
"""Returns logs for a workflow if user has access to the workflow.""" """
Returns logs for a workflow if user has access to the workflow.
Supports optional pagination, sorting, and filtering.
Args:
workflowId: The workflow ID to get logs for
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[ChatLog]
If pagination is provided: PaginatedResult with items and metadata
"""
# Check workflow access first (without calling getWorkflow to avoid circular reference) # Check workflow access first (without calling getWorkflow to avoid circular reference)
workflows = self.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId}) workflows = self.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
if not workflows: if not workflows:
return [] if pagination is None:
return []
return PaginatedResult(items=[], totalItems=0, totalPages=0)
filteredWorkflows = self._uam(ChatWorkflow, workflows) filteredWorkflows = self._uam(ChatWorkflow, workflows)
if not filteredWorkflows: if not filteredWorkflows:
return [] if pagination is None:
return []
return PaginatedResult(items=[], totalItems=0, totalPages=0)
# Get logs for this workflow from normalized table # Get logs for this workflow from normalized table
logs = self.db.getRecordset(ChatLog, recordFilter={"workflowId": workflowId}) logs = self.db.getRecordset(ChatLog, recordFilter={"workflowId": workflowId})
# Sort logs by timestamp (Unix timestamps) # Convert raw logs to dict format for sorting/filtering
logs.sort(key=lambda x: float(x.get("timestamp", 0))) logDicts = []
for log in logs:
logDicts.append({
"id": log.get("id"),
"workflowId": log.get("workflowId"),
"message": log.get("message"),
"type": log.get("type"),
"timestamp": log.get("timestamp", getUtcTimestamp()),
"agentName": log.get("agentName"),
"status": log.get("status"),
"progress": log.get("progress"),
"mandateId": log.get("mandateId"),
"userId": log.get("userId")
})
return [ChatLog(**log) for log in logs] # Apply default sorting by timestamp if no sort specified
if pagination is None or not pagination.sort:
logDicts.sort(key=lambda x: float(x.get("timestamp", 0)))
# Apply filtering (if filters provided)
if pagination and pagination.filters:
logDicts = self._applyFilters(logDicts, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination and pagination.sort:
logDicts = self._applySorting(logDicts, pagination.sort)
# If no pagination requested, return all items
if pagination is None:
return [ChatLog(**log) for log in logDicts]
# Count total items after filters
totalItems = len(logDicts)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedLogDicts = logDicts[startIdx:endIdx]
# Convert to model objects
items = [ChatLog(**log) for log in pagedLogDicts]
return PaginatedResult(
items=items,
totalItems=totalItems,
totalPages=totalPages
)
def createLog(self, logData: Dict[str, Any]) -> ChatLog: def createLog(self, logData: Dict[str, Any]) -> ChatLog:
"""Creates a log entry for a workflow if user has access.""" """Creates a log entry for a workflow if user has access."""

View file

@ -7,7 +7,8 @@ import os
import logging import logging
import base64 import base64
import hashlib import hashlib
from typing import Dict, Any, List, Optional import math
from typing import Dict, Any, List, Optional, Union
from modules.connectors.connectorDbPostgre import DatabaseConnector from modules.connectors.connectorDbPostgre import DatabaseConnector
from modules.interfaces.interfaceDbComponentAccess import ComponentAccess from modules.interfaces.interfaceDbComponentAccess import ComponentAccess
@ -17,6 +18,7 @@ from modules.datamodels.datamodelVoice import VoiceSettings
from modules.datamodels.datamodelUam import User, Mandate from modules.datamodels.datamodelUam import User, Mandate
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import getUtcTimestamp from modules.shared.timezoneUtils import getUtcTimestamp
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -244,6 +246,56 @@ class ComponentObjects:
"""Delegate to access control module.""" """Delegate to access control module."""
return self.access.canModify(model_class, recordId) return self.access.canModify(model_class, recordId)
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Apply filter criteria to records (implementation for future filtering)."""
# TODO: Implement filtering logic when needed
return records
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
if not sortFields:
return records
# Start with a copy to avoid modifying original
sortedRecords = list(records)
# Sort from least significant to most significant field (reverse order)
# Python's sort is stable, so this creates proper multi-level sorting
for sortField in reversed(sortFields):
# Handle both dict and object formats
if isinstance(sortField, dict):
fieldName = sortField.get("field")
direction = sortField.get("direction", "asc")
else:
fieldName = getattr(sortField, "field", None)
direction = getattr(sortField, "direction", "asc")
if not fieldName:
continue
isDesc = (direction == "desc")
def sortKey(record):
value = record.get(fieldName)
# Handle None values - place them at the end for both directions
if value is None:
# Use a special value that sorts last
return (1, "") # (is_none_flag, empty_value) - sorts after (0, ...)
else:
# Return tuple with type indicator for proper comparison
if isinstance(value, (int, float)):
return (0, value)
elif isinstance(value, str):
return (0, value)
elif isinstance(value, bool):
return (0, value)
else:
return (0, str(value))
# Sort with reverse parameter for descending
sortedRecords.sort(key=sortKey, reverse=isDesc)
return sortedRecords
# Utilities # Utilities
@ -255,18 +307,57 @@ class ComponentObjects:
# Prompt methods # Prompt methods
def getAllPrompts(self) -> List[Prompt]: def getAllPrompts(self, pagination: Optional[PaginationParams] = None) -> Union[List[Prompt], PaginatedResult]:
"""Returns prompts based on user access level.""" """
Returns prompts based on user access level.
Supports optional pagination, sorting, and filtering.
Args:
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[Prompt]
If pagination is provided: PaginatedResult with items and metadata
"""
try: try:
allPrompts = self.db.getRecordset(Prompt) allPrompts = self.db.getRecordset(Prompt)
filteredPrompts = self._uam(Prompt, allPrompts) filteredPrompts = self._uam(Prompt, allPrompts)
# Convert to Prompt objects # If no pagination requested, return all items
return [Prompt(**prompt) for prompt in filteredPrompts] if pagination is None:
return [Prompt(**prompt) for prompt in filteredPrompts]
# Apply filtering (if filters provided)
if pagination.filters:
filteredPrompts = self._applyFilters(filteredPrompts, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination.sort:
filteredPrompts = self._applySorting(filteredPrompts, pagination.sort)
# Count total items after filters
totalItems = len(filteredPrompts)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedPrompts = filteredPrompts[startIdx:endIdx]
# Convert to model objects
items = [Prompt(**prompt) for prompt in pagedPrompts]
return PaginatedResult(
items=items,
totalItems=totalItems,
totalPages=totalPages
)
except Exception as e: except Exception as e:
logger.error(f"Error getting prompts: {str(e)}") logger.error(f"Error getting prompts: {str(e)}")
return [] if pagination is None:
return []
return PaginatedResult(items=[], totalItems=0, totalPages=0)
def getPrompt(self, promptId: str) -> Optional[Prompt]: def getPrompt(self, promptId: str) -> Optional[Prompt]:
"""Returns a prompt by ID if user has access.""" """Returns a prompt by ID if user has access."""
@ -454,39 +545,79 @@ class ComponentObjects:
# File methods - metadata-based operations # File methods - metadata-based operations
def getAllFiles(self) -> List[FileItem]: def getAllFiles(self, pagination: Optional[PaginationParams] = None) -> Union[List[FileItem], PaginatedResult]:
"""Returns files based on user access level.""" """
Returns files based on user access level.
Supports optional pagination, sorting, and filtering.
Args:
pagination: Optional pagination parameters. If None, returns all items.
Returns:
If pagination is None: List[FileItem]
If pagination is provided: PaginatedResult with items and metadata
"""
allFiles = self.db.getRecordset(FileItem) allFiles = self.db.getRecordset(FileItem)
filteredFiles = self._uam(FileItem, allFiles) filteredFiles = self._uam(FileItem, allFiles)
# Convert database records to FileItem instances # Convert database records to FileItem instances (for both paginated and non-paginated)
fileItems = [] def convertFileItems(files):
for file in filteredFiles: fileItems = []
try: for file in files:
# Ensure proper values, use defaults for invalid data try:
creationDate = file.get("creationDate") # Ensure proper values, use defaults for invalid data
if creationDate is None or not isinstance(creationDate, (int, float)) or creationDate <= 0: creationDate = file.get("creationDate")
creationDate = getUtcTimestamp() if creationDate is None or not isinstance(creationDate, (int, float)) or creationDate <= 0:
creationDate = getUtcTimestamp()
fileName = file.get("fileName")
if not fileName or fileName == "None": fileName = file.get("fileName")
continue # Skip records with invalid fileName if not fileName or fileName == "None":
continue # Skip records with invalid fileName
fileItem = FileItem(
id=file.get("id"), fileItem = FileItem(
mandateId=file.get("mandateId"), id=file.get("id"),
fileName=fileName, mandateId=file.get("mandateId"),
mimeType=file.get("mimeType"), fileName=fileName,
fileHash=file.get("fileHash"), mimeType=file.get("mimeType"),
fileSize=file.get("fileSize"), fileHash=file.get("fileHash"),
creationDate=creationDate fileSize=file.get("fileSize"),
) creationDate=creationDate
fileItems.append(fileItem) )
except Exception as e: fileItems.append(fileItem)
logger.warning(f"Skipping invalid file record: {str(e)}") except Exception as e:
continue logger.warning(f"Skipping invalid file record: {str(e)}")
continue
return fileItems return fileItems
# If no pagination requested, return all items
if pagination is None:
return convertFileItems(filteredFiles)
# Apply filtering (if filters provided)
if pagination.filters:
filteredFiles = self._applyFilters(filteredFiles, pagination.filters)
# Apply sorting (in order of sortFields)
if pagination.sort:
filteredFiles = self._applySorting(filteredFiles, pagination.sort)
# Count total items after filters
totalItems = len(filteredFiles)
totalPages = math.ceil(totalItems / pagination.pageSize) if totalItems > 0 else 0
# Apply pagination (skip/limit)
startIdx = (pagination.page - 1) * pagination.pageSize
endIdx = startIdx + pagination.pageSize
pagedFiles = filteredFiles[startIdx:endIdx]
# Convert to model objects
items = convertFileItems(pagedFiles)
return PaginatedResult(
items=items,
totalItems=totalItems,
totalPages=totalPages
)
def getFile(self, fileId: str) -> Optional[FileItem]: def getFile(self, fileId: str) -> Optional[FileItem]:
"""Returns a file by ID if user has access.""" """Returns a file by ID if user has access."""

View file

@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, P
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import logging import logging
import json
# Import auth module # Import auth module
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
@ -11,6 +12,7 @@ import modules.interfaces.interfaceDbComponentObjects as interfaceDbComponentObj
from modules.datamodels.datamodelFiles import FileItem, FilePreview from modules.datamodels.datamodelFiles import FileItem, FilePreview
from modules.shared.attributeUtils import getModelAttributeDefinitions from modules.shared.attributeUtils import getModelAttributeDefinitions
from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,21 +33,61 @@ router = APIRouter(
} }
) )
@router.get("/list", response_model=List[FileItem]) @router.get("/list", response_model=PaginatedResponse[FileItem])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_files( async def get_files(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[FileItem]: ) -> PaginatedResponse[FileItem]:
"""Get all files""" """
Get files with optional pagination, sorting, and filtering.
Query Parameters:
- pagination: JSON-encoded PaginationParams object, or None for no pagination
Examples:
- GET /api/files/list (no pagination - returns all items)
- GET /api/files/list?pagination={"page":1,"pageSize":10,"sort":[]}
- GET /api/files/list?pagination={"page":2,"pageSize":20,"sort":[{"field":"fileName","direction":"asc"}]}
"""
try: try:
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbComponentObjects.getInterface(currentUser) managementInterface = interfaceDbComponentObjects.getInterface(currentUser)
result = managementInterface.getAllFiles(pagination=paginationParams)
# Get all files generically - only metadata, no binary data # If pagination was requested, result is PaginatedResult
files = managementInterface.getAllFiles() # If no pagination, result is List[FileItem]
if paginationParams:
# Return files directly since they are already FileItem objects return PaginatedResponse(
return files items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
except HTTPException:
raise
except Exception as e: except Exception as e:
logger.error(f"Error getting files: {str(e)}") logger.error(f"Error getting files: {str(e)}")
raise HTTPException( raise HTTPException(

View file

@ -3,10 +3,11 @@ Mandate routes for the backend API.
Implements the endpoints for mandate management. Implements the endpoints for mandate management.
""" """
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response, Query
from typing import List, Dict, Any from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
import logging import logging
import json
# Import auth module # Import auth module
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
@ -17,6 +18,7 @@ from modules.shared.attributeUtils import getModelAttributeDefinitions
# Import the model classes # Import the model classes
from modules.datamodels.datamodelUam import Mandate, User from modules.datamodels.datamodelUam import Mandate, User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,17 +33,60 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("/", response_model=List[Mandate]) @router.get("/", response_model=PaginatedResponse[Mandate])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_mandates( async def get_mandates(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[Mandate]: ) -> PaginatedResponse[Mandate]:
"""Get all mandates""" """
Get mandates with optional pagination, sorting, and filtering.
Query Parameters:
- pagination: JSON-encoded PaginationParams object, or None for no pagination
Examples:
- GET /api/mandates/ (no pagination - returns all items)
- GET /api/mandates/?pagination={"page":1,"pageSize":10,"sort":[]}
"""
try: try:
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
appInterface = interfaceDbAppObjects.getInterface(currentUser) appInterface = interfaceDbAppObjects.getInterface(currentUser)
mandates = appInterface.getAllMandates() result = appInterface.getAllMandates(pagination=paginationParams)
return mandates
# If pagination was requested, result is PaginatedResult
# If no pagination, result is List[Mandate]
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
except HTTPException:
raise
except Exception as e: except Exception as e:
logger.error(f"Error getting mandates: {str(e)}") logger.error(f"Error getting mandates: {str(e)}")
raise HTTPException( raise HTTPException(

View file

@ -1,7 +1,8 @@
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query
from typing import List, Dict, Any from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
import logging import logging
import json
# Import auth module # Import auth module
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
@ -10,6 +11,7 @@ from modules.security.auth import limiter, getCurrentUser
import modules.interfaces.interfaceDbComponentObjects as interfaceDbComponentObjects import modules.interfaces.interfaceDbComponentObjects as interfaceDbComponentObjects
from modules.datamodels.datamodelUtils import Prompt from modules.datamodels.datamodelUtils import Prompt
from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,16 +23,58 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("", response_model=List[Prompt]) @router.get("", response_model=PaginatedResponse[Prompt])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_prompts( async def get_prompts(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[Prompt]: ) -> PaginatedResponse[Prompt]:
"""Get all prompts""" """
Get prompts with optional pagination, sorting, and filtering.
Query Parameters:
- pagination: JSON-encoded PaginationParams object, or None for no pagination
Examples:
- GET /api/prompts (no pagination - returns all items)
- GET /api/prompts?pagination={"page":1,"pageSize":10,"sort":[]}
- GET /api/prompts?pagination={"page":2,"pageSize":20,"sort":[{"field":"name","direction":"asc"}]}
"""
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbComponentObjects.getInterface(currentUser) managementInterface = interfaceDbComponentObjects.getInterface(currentUser)
prompts = managementInterface.getAllPrompts() result = managementInterface.getAllPrompts(pagination=paginationParams)
return prompts
# If pagination was requested, result is PaginatedResult
# If no pagination, result is List[Prompt]
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
@router.post("", response_model=Prompt) @router.post("", response_model=Prompt)
@limiter.limit("10/minute") @limiter.limit("10/minute")

View file

@ -3,10 +3,11 @@ User routes for the backend API.
Implements the endpoints for user management. Implements the endpoints for user management.
""" """
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response, Query
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
import logging import logging
import json
# Import interfaces and models # Import interfaces and models
import modules.interfaces.interfaceDbAppObjects as interfaceDbAppObjects import modules.interfaces.interfaceDbAppObjects as interfaceDbAppObjects
@ -14,6 +15,7 @@ from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.datamodels.datamodelUam import User, UserPrivilege from modules.datamodels.datamodelUam import User, UserPrivilege
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,21 +26,65 @@ router = APIRouter(
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("/", response_model=List[User]) @router.get("/", response_model=PaginatedResponse[User])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_users( async def get_users(
request: Request, request: Request,
mandateId: Optional[str] = None, mandateId: Optional[str] = Query(None, description="Mandate ID to filter users"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[User]: ) -> PaginatedResponse[User]:
"""Get all users in the current mandate""" """
Get users with optional pagination, sorting, and filtering.
Query Parameters:
- mandateId: Optional mandate ID to filter users
- pagination: JSON-encoded PaginationParams object, or None for no pagination
Examples:
- GET /api/users/ (no pagination - returns all users)
- GET /api/users/?pagination={"page":1,"pageSize":10,"sort":[]}
"""
try: try:
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
appInterface = interfaceDbAppObjects.getInterface(currentUser) appInterface = interfaceDbAppObjects.getInterface(currentUser)
# If mandateId is provided, use it, otherwise use the current user's mandate # If mandateId is provided, use it, otherwise use the current user's mandate
targetMandateId = mandateId or currentUser.mandateId targetMandateId = mandateId or currentUser.mandateId
# Get all users without filtering by enabled status # Get users with optional pagination
users = appInterface.getUsersByMandate(targetMandateId) result = appInterface.getUsersByMandate(targetMandateId, pagination=paginationParams)
return users
# If pagination was requested, result is PaginatedResult
# If no pagination, result is List[User]
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
except HTTPException:
raise
except Exception as e: except Exception as e:
logger.error(f"Error getting users: {str(e)}") logger.error(f"Error getting users: {str(e)}")
raise HTTPException( raise HTTPException(

View file

@ -325,9 +325,17 @@ async def get_available_languages(currentUser: User = Depends(getCurrentUser)):
@router.get("/voices") @router.get("/voices")
async def get_available_voices( async def get_available_voices(
languageCode: Optional[str] = None, languageCode: Optional[str] = None,
language_code: Optional[str] = None, # Accept both camelCase and snake_case
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
): ):
"""Get available voices from Google Cloud Text-to-Speech.""" """
Get available voices from Google Cloud Text-to-Speech.
Accepts languageCode (camelCase) or language_code (snake_case) query parameter.
"""
# Use language_code if provided (frontend sends this), otherwise use languageCode
if language_code:
languageCode = language_code
try: try:
logger.info(f"🎤 Getting available voices, language filter: {languageCode}") logger.info(f"🎤 Getting available voices, language filter: {languageCode}")

View file

@ -4,6 +4,7 @@ Implements the endpoints for workflow management according to the state machine.
""" """
import logging import logging
import json
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status, Request from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status, Request
@ -24,6 +25,7 @@ from modules.datamodels.datamodelChat import (
) )
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
# Configure logger # Configure logger
@ -43,18 +45,51 @@ def getServiceChat(currentUser: User):
return interfaceDbChatObjects.getInterface(currentUser) return interfaceDbChatObjects.getInterface(currentUser)
# Consolidated endpoint for getting all workflows # Consolidated endpoint for getting all workflows
@router.get("/", response_model=List[ChatWorkflow]) @router.get("/", response_model=PaginatedResponse[ChatWorkflow])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflows( async def get_workflows(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[ChatWorkflow]: ) -> PaginatedResponse[ChatWorkflow]:
"""Get all workflows for the current user.""" """
Get workflows with optional pagination, sorting, and filtering.
Query Parameters:
- pagination: JSON-encoded PaginationParams object, or None for no pagination
Examples:
- GET /api/workflows/ (no pagination - returns all workflows)
- GET /api/workflows/?pagination={"page":1,"pageSize":10,"sort":[]}
"""
try: try:
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
appInterface = getInterface(currentUser) appInterface = getInterface(currentUser)
workflows_data = appInterface.getWorkflows() result = appInterface.getWorkflows(pagination=paginationParams)
# Convert raw dictionaries to ChatWorkflow objects by loading each workflow properly # Convert raw dictionaries to ChatWorkflow objects by loading each workflow properly
# If pagination was requested, result is PaginatedResult with items as dicts
# If no pagination, result is List[Dict]
if paginationParams:
workflows_data = result.items
totalItems = result.totalItems
totalPages = result.totalPages
else:
workflows_data = result
totalItems = len(result)
totalPages = 1
workflows = [] workflows = []
for workflow_data in workflows_data: for workflow_data in workflows_data:
try: try:
@ -67,7 +102,25 @@ async def get_workflows(
# Skip invalid workflows instead of failing the entire request # Skip invalid workflows instead of failing the entire request
continue continue
return workflows if paginationParams:
return PaginatedResponse(
items=workflows,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=totalItems,
totalPages=totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=workflows,
pagination=None
)
except HTTPException:
raise
except Exception as e: except Exception as e:
logger.error(f"Error getting workflows: {str(e)}") logger.error(f"Error getting workflows: {str(e)}")
raise HTTPException( raise HTTPException(
@ -185,16 +238,36 @@ async def get_workflow_status(
) )
# API Endpoint for workflow logs with selective data transfer # API Endpoint for workflow logs with selective data transfer
@router.get("/{workflowId}/logs", response_model=List[ChatLog]) @router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflow_logs( async def get_workflow_logs(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"), logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs (legacy selective data transfer)"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[ChatLog]: ) -> PaginatedResponse[ChatLog]:
"""Get logs for a workflow with support for selective data transfer.""" """
Get logs for a workflow with optional pagination, sorting, and filtering.
Also supports legacy selective data transfer via logId parameter.
Query Parameters:
- logId: Optional log ID for selective data transfer (returns only logs after this ID)
- pagination: JSON-encoded PaginationParams object, or None for no pagination
"""
try: try:
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
# Get service center # Get service center
interfaceDbChat = getServiceChat(currentUser) interfaceDbChat = getServiceChat(currentUser)
@ -206,18 +279,43 @@ async def get_workflow_logs(
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
# Get all logs # Get logs with optional pagination
allLogs = interfaceDbChat.getLogs(workflowId) result = interfaceDbChat.getLogs(workflowId, pagination=paginationParams)
# Apply selective data transfer if logId is provided # Handle legacy selective data transfer if logId is provided (takes precedence over pagination)
if logId: if logId:
# If pagination was requested, result is PaginatedResult, otherwise List[ChatLog]
allLogs = result.items if paginationParams else result
# Find the index of the log with the given ID # Find the index of the log with the given ID
logIndex = next((i for i, log in enumerate(allLogs) if log.id == logId), -1) logIndex = next((i for i, log in enumerate(allLogs) if log.id == logId), -1)
if logIndex >= 0: if logIndex >= 0:
# Return only logs after the specified log # Return only logs after the specified log
return allLogs[logIndex + 1:] filteredLogs = allLogs[logIndex + 1:]
return PaginatedResponse(
items=filteredLogs,
pagination=None
)
return allLogs # If pagination was requested, result is PaginatedResult
# If no pagination, result is List[ChatLog]
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -228,16 +326,36 @@ async def get_workflow_logs(
) )
# API Endpoint for workflow messages with selective data transfer # API Endpoint for workflow messages with selective data transfer
@router.get("/{workflowId}/messages", response_model=List[ChatMessage]) @router.get("/{workflowId}/messages", response_model=PaginatedResponse[ChatMessage])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflow_messages( async def get_workflow_messages(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"), messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages (legacy selective data transfer)"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[ChatMessage]: ) -> PaginatedResponse[ChatMessage]:
"""Get messages for a workflow with support for selective data transfer.""" """
Get messages for a workflow with optional pagination, sorting, and filtering.
Also supports legacy selective data transfer via messageId parameter.
Query Parameters:
- messageId: Optional message ID for selective data transfer (returns only messages after this ID)
- pagination: JSON-encoded PaginationParams object, or None for no pagination
"""
try: try:
# Parse pagination parameter
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
# Get service center # Get service center
interfaceDbChat = getServiceChat(currentUser) interfaceDbChat = getServiceChat(currentUser)
@ -249,19 +367,43 @@ async def get_workflow_messages(
detail=f"Workflow with ID {workflowId} not found" detail=f"Workflow with ID {workflowId} not found"
) )
# Get all messages # Get messages with optional pagination
allMessages = interfaceDbChat.getMessages(workflowId) result = interfaceDbChat.getMessages(workflowId, pagination=paginationParams)
# Apply selective data transfer if messageId is provided # Handle legacy selective data transfer if messageId is provided (takes precedence over pagination)
if messageId: if messageId:
# If pagination was requested, result is PaginatedResult, otherwise List[ChatMessage]
allMessages = result.items if paginationParams else result
# Find the index of the message with the given ID # Find the index of the message with the given ID
messageIndex = next((i for i, msg in enumerate(allMessages) if msg.id == messageId), -1) messageIndex = next((i for i, msg in enumerate(allMessages) if msg.id == messageId), -1)
if messageIndex >= 0: if messageIndex >= 0:
# Return only messages after the specified message # Return only messages after the specified message
filteredMessages = allMessages[messageIndex + 1:] filteredMessages = allMessages[messageIndex + 1:]
return filteredMessages return PaginatedResponse(
items=filteredMessages,
pagination=None
)
return allMessages # If pagination was requested, result is PaginatedResult
# If no pagination, result is List[ChatMessage]
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:

View file

@ -7,6 +7,9 @@ from typing import Dict, Any, List, Type, Optional
import inspect import inspect
import importlib import importlib
import os import os
import logging
logger = logging.getLogger(__name__)
# Define the AttributeDefinition class here instead of importing it # Define the AttributeDefinition class here instead of importing it
@ -107,7 +110,9 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
fields = modelClass.model_fields fields = modelClass.model_fields
for name, field in fields.items(): for name, field in fields.items():
# Extract frontend metadata from field info # Extract frontend metadata from field info
field_info = field.field_info if hasattr(field, "field_info") else None # In Pydantic v2, the field from model_fields.items() IS the FieldInfo object
# FieldInfo objects have the 'extra' dict directly on them
field_info = field
# Check both direct attributes and extra field for frontend metadata # Check both direct attributes and extra field for frontend metadata
frontend_type = None frontend_type = None
frontend_readonly = False frontend_readonly = False
@ -115,7 +120,7 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
frontend_options = None frontend_options = None
if field_info: if field_info:
# Try direct attributes first # Try direct attributes first (though these won't exist for custom kwargs)
frontend_type = getattr(field_info, "frontend_type", None) frontend_type = getattr(field_info, "frontend_type", None)
frontend_readonly = getattr(field_info, "frontend_readonly", False) frontend_readonly = getattr(field_info, "frontend_readonly", False)
frontend_required = getattr( frontend_required = getattr(
@ -123,22 +128,43 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
) )
frontend_options = getattr(field_info, "frontend_options", None) frontend_options = getattr(field_info, "frontend_options", None)
# If not found, check extra field # If not found, check json_schema_extra (Pydantic v2 stores custom kwargs here)
if hasattr(field_info, "extra") and field_info.extra: if frontend_type is None and hasattr(field_info, "json_schema_extra"):
if frontend_type is None: json_extra = field_info.json_schema_extra
frontend_type = field_info.extra.get("frontend_type") if isinstance(json_extra, dict):
frontend_type = json_extra.get("frontend_type")
elif callable(json_extra):
# If it's a callable, we can't easily extract from it, skip
pass
# Check extra field (Pydantic v2 stores custom kwargs here)
# This is the main location where frontend_type is stored
if hasattr(field_info, "extra"):
extra_dict = field_info.extra
if isinstance(extra_dict, dict):
if frontend_type is None:
frontend_type = extra_dict.get("frontend_type")
# Also extract other fields from extra if it exists
if hasattr(field_info, "extra") and isinstance(field_info.extra, dict):
extra_dict = field_info.extra
if not frontend_readonly: if not frontend_readonly:
frontend_readonly = field_info.extra.get( frontend_readonly = extra_dict.get("frontend_readonly", False)
"frontend_readonly", False if frontend_required == field.is_required():
) frontend_required = extra_dict.get("frontend_required", frontend_required)
if (
frontend_required == field.is_required()
): # Only override if we didn't get it from direct attribute
frontend_required = field_info.extra.get(
"frontend_required", frontend_required
)
if frontend_options is None: if frontend_options is None:
frontend_options = field_info.extra.get("frontend_options") frontend_options = extra_dict.get("frontend_options")
# Also check json_schema_extra for other fields
if hasattr(field_info, "json_schema_extra"):
json_extra = field_info.json_schema_extra
if isinstance(json_extra, dict):
if not frontend_readonly and "frontend_readonly" in json_extra:
frontend_readonly = json_extra.get("frontend_readonly", False)
if frontend_required == field.is_required() and "frontend_required" in json_extra:
frontend_required = json_extra.get("frontend_required", frontend_required)
if frontend_options is None and "frontend_options" in json_extra:
frontend_options = json_extra.get("frontend_options")
# Use frontend type if available, otherwise fall back to Python type # Use frontend type if available, otherwise fall back to Python type
field_type = ( field_type = (