root, files, prompts running

This commit is contained in:
ValueOn AG 2025-05-30 03:41:24 +02:00
parent 8c9492715a
commit fc662fbe59
5 changed files with 265 additions and 131 deletions

View file

@ -31,27 +31,6 @@ logger = logging.getLogger(__name__)
# Singleton factory for Chat instances with AI service per context # Singleton factory for Chat instances with AI service per context
_chatInterfaces = {} _chatInterfaces = {}
# Custom exceptions for file handling
class FileError(Exception):
"""Base class for file handling exceptions."""
pass
class FileNotFoundError(FileError):
"""Exception raised when a file is not found."""
pass
class FileStorageError(FileError):
"""Exception raised when there's an error storing a file."""
pass
class FilePermissionError(FileError):
"""Exception raised when there's a permission issue with a file."""
pass
class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file."""
pass
class ChatInterface: class ChatInterface:
""" """
Interface to Chat database and AI Connectors. Interface to Chat database and AI Connectors.

View file

@ -323,7 +323,7 @@ class ServiceManagement:
raise PermissionError("No permission to create prompts") raise PermissionError("No permission to create prompts")
# Create prompt record # Create prompt record
createdRecord = self.db.recordCreate("prompts", promptData.to_dict()) createdRecord = self.db.recordCreate("prompts", promptData)
if not createdRecord or not createdRecord.get("id"): if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create prompt record") raise ValueError("Failed to create prompt record")
@ -369,15 +369,23 @@ class ServiceManagement:
"""Calculates a SHA-256 hash for the file content""" """Calculates a SHA-256 hash for the file content"""
return hashlib.sha256(fileContent).hexdigest() return hashlib.sha256(fileContent).hexdigest()
def checkForDuplicateFile(self, fileHash: str) -> Optional[Dict[str, Any]]: def checkForDuplicateFile(self, fileHash: str) -> Optional[FileItem]:
"""Checks if a file with the same hash already exists for the current user and mandate.""" """Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={ files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash, "fileHash": fileHash,
"mandateId": self.currentUser.get("mandateId"), "mandateId": self.currentUser.mandateId,
"_createdBy": self.currentUser.get("id") "_createdBy": self.currentUser.id
}) })
if files: if files:
return files[0] return FileItem(
id=files[0]["id"],
mandateId=files[0]["mandateId"],
filename=files[0]["filename"],
mimeType=files[0]["mimeType"],
workflowId=files[0]["workflowId"],
fileHash=files[0]["fileHash"],
fileSize=files[0]["fileSize"]
)
return None return None
def getMimeType(self, filename: str) -> str: def getMimeType(self, filename: str) -> str:
@ -412,34 +420,85 @@ class ServiceManagement:
# File methods - metadata-based operations # File methods - metadata-based operations
def getAllFiles(self) -> List[Dict[str, Any]]: def getAllFiles(self) -> List[FileItem]:
"""Returns files based on user access level.""" """Returns files based on user access level."""
allFiles = self.db.getRecordset("files") allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles) filteredFiles = self._uam("files", allFiles)
# Convert database records to FileItem instances
fileItems = []
for file in filteredFiles:
try:
# Get creation date from record or use current time
creationDate = file.get("creationDate")
if not creationDate:
creationDate = datetime.now().isoformat()
fileItem = FileItem(
id=file.get("id"),
mandateId=file.get("mandateId"),
filename=file.get("filename"),
mimeType=file.get("mimeType"),
workflowId=file.get("workflowId"),
fileHash=file.get("fileHash"),
fileSize=file.get("fileSize"),
creationDate=creationDate
)
fileItems.append(fileItem)
except Exception as e:
logger.warning(f"Skipping invalid file record: {str(e)}")
continue
return fileItems
def getFile(self, fileId: str) -> Optional[Dict[str, Any]]: 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."""
files = self.db.getRecordset("files", recordFilter={"id": fileId}) files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files: if not files:
return None return None
filteredFiles = self._uam("files", files) filteredFiles = self._uam("files", files)
return filteredFiles[0] if filteredFiles else None if not filteredFiles:
return None
file = filteredFiles[0]
try:
# Get creation date from record or use current time
creationDate = file.get("creationDate")
if not creationDate:
creationDate = datetime.now().isoformat()
return FileItem(
id=file.get("id"),
mandateId=file.get("mandateId"),
filename=file.get("filename"),
mimeType=file.get("mimeType"),
workflowId=file.get("workflowId"),
fileHash=file.get("fileHash"),
fileSize=file.get("fileSize"),
creationDate=creationDate
)
except Exception as e:
logger.error(f"Error converting file record: {str(e)}")
return None
def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> Dict[str, Any]: def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> FileItem:
"""Creates a new file entry if user has permission.""" """Creates a new file entry if user has permission."""
if not self._canModify("files"): if not self._canModify("files"):
raise PermissionError("No permission to create files") raise PermissionError("No permission to create files")
fileData = { # Create FileItem instance
"mandateId": self.currentUser.get("mandateId"), fileItem = FileItem(
"name": name, mandateId=self.currentUser.mandateId,
"mimeType": mimeType, filename=name,
"size": size, mimeType=mimeType,
"fileHash": fileHash, fileSize=size,
"creationDate": self._getCurrentTimestamp() fileHash=fileHash
} )
return self.db.recordCreate("files", fileData)
# Store in database
self.db.recordCreate("files", fileItem.to_dict())
return fileItem
def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]: def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates file metadata if user has access.""" """Updates file metadata if user has access."""
@ -467,10 +526,10 @@ class ServiceManagement:
raise PermissionError(f"No permission to delete file {fileId}") raise PermissionError(f"No permission to delete file {fileId}")
# Check for other references to this file (by hash) # Check for other references to this file (by hash)
fileHash = file.get("fileHash") fileHash = file.fileHash
if fileHash: if fileHash:
otherReferences = [f for f in self.db.getRecordset("files", recordFilter={"fileHash": fileHash}) otherReferences = [f for f in self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
if f.get("id") != fileId] if f["id"] != fileId]
# Only delete associated fileData if no other references exist # Only delete associated fileData if no other references exist
if not otherReferences: if not otherReferences:
@ -507,7 +566,7 @@ class ServiceManagement:
return False return False
# Determine if this is a text-based format # Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream") mimeType = file.mimeType
isTextFormat = isTextMimeType(mimeType) isTextFormat = isTextMimeType(mimeType)
base64Encoded = False base64Encoded = False
@ -581,6 +640,52 @@ class ServiceManagement:
logger.error(f"Error processing file data for {fileId}: {str(e)}") logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None return None
def getFilePreview(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a preview of the file content if user has access."""
try:
# Get file metadata
file = self.getFile(fileId)
if not file:
logger.warning(f"No access to file ID {fileId}")
return None
# Get file content
fileContent = self.getFileData(fileId)
if not fileContent:
logger.warning(f"No content found for file ID {fileId}")
return None
# Determine if content is text based on MIME type
isText = file.mimeType.startswith(('text/', 'application/json', 'application/xml', 'application/javascript'))
# For text content, decode to string
if isText:
try:
content = fileContent.decode('utf-8')
encoding = 'utf-8'
except UnicodeDecodeError:
try:
content = fileContent.decode('latin-1')
encoding = 'latin-1'
except:
content = fileContent
encoding = None
else:
content = fileContent
encoding = None
return {
"content": content,
"mimeType": file.mimeType,
"filename": file.filename,
"isText": isText,
"encoding": encoding,
"size": len(fileContent)
}
except Exception as e:
logger.error(f"Error getting file preview for {fileId}: {str(e)}")
return None
def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool: def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool:
"""Updates file data if user has access.""" """Updates file data if user has access."""
# Check file access # Check file access
@ -597,7 +702,7 @@ class ServiceManagement:
import base64 import base64
# Determine if this is a text-based format # Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream") mimeType = file.mimeType
isTextFormat = isTextMimeType(mimeType) isTextFormat = isTextMimeType(mimeType)
base64Encoded = False base64Encoded = False
@ -667,7 +772,7 @@ class ServiceManagement:
logger.error(f"Error updating data for file {fileId}: {str(e)}") logger.error(f"Error updating data for file {fileId}: {str(e)}")
return False return False
def saveUploadedFile(self, fileContent: bytes, fileName: str) -> Dict[str, Any]: def saveUploadedFile(self, fileContent: bytes, fileName: str) -> FileItem:
"""Saves an uploaded file if user has permission.""" """Saves an uploaded file if user has permission."""
try: try:
# Check file creation permission # Check file creation permission
@ -687,7 +792,7 @@ class ServiceManagement:
# Check for duplicate within same user/mandate # Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash) existingFile = self.checkForDuplicateFile(fileHash)
if existingFile: if existingFile:
logger.debug(f"Duplicate found for {fileName}: {existingFile['id']}") logger.debug(f"Duplicate found for {fileName}: {existingFile.id}")
return existingFile return existingFile
# Determine MIME type and size # Determine MIME type and size
@ -696,7 +801,7 @@ class ServiceManagement:
# Save metadata # Save metadata
logger.debug(f"Saving file metadata to database for file: {fileName}") logger.debug(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile( fileItem = self.createFile(
name=fileName, name=fileName,
mimeType=mimeType, mimeType=mimeType,
size=fileSize, size=fileSize,
@ -705,10 +810,10 @@ class ServiceManagement:
# Save binary data # Save binary data
logger.debug(f"Saving file content to database for file: {fileName}") logger.debug(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent) self.createFileData(fileItem.id, fileContent)
logger.debug(f"File upload process completed for: {fileName}") logger.debug(f"File upload process completed for: {fileName}")
return dbFile return fileItem
except Exception as e: except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True) logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
@ -731,9 +836,9 @@ class ServiceManagement:
return { return {
"id": fileId, "id": fileId,
"name": file.get("name", f"file_{fileId}"), "name": file.filename,
"contentType": file.get("mimeType", "application/octet-stream"), "contentType": file.mimeType,
"size": file.get("size", len(fileContent)), "size": file.fileSize,
"content": fileContent "content": fileContent
} }
except FileNotFoundError as e: except FileNotFoundError as e:

View file

@ -4,7 +4,7 @@ Updated to match the Entity Relation Diagram structure.
""" """
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional, Union
from datetime import datetime from datetime import datetime
import uuid import uuid
@ -22,6 +22,14 @@ class FileItem(BaseModel, ModelMixin):
workflowId: Optional[str] = Field(None, description="Foreign key to workflow") workflowId: Optional[str] = Field(None, description="Foreign key to workflow")
fileHash: str = Field(description="Hash of the file") fileHash: str = Field(description="Hash of the file")
fileSize: int = Field(description="Size of the file in bytes") fileSize: int = Field(description="Size of the file in bytes")
creationDate: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Date when the file was created")
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary with proper datetime handling"""
data = super().to_dict()
if isinstance(data.get("creationDate"), datetime):
data["creationDate"] = data["creationDate"].isoformat()
return data
# Register labels for FileItem # Register labels for FileItem
register_model_labels( register_model_labels(
@ -34,7 +42,40 @@ register_model_labels(
"mimeType": {"en": "MIME Type", "fr": "Type MIME"}, "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"}, "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"}, "fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
"fileSize": {"en": "File Size", "fr": "Taille du fichier"} "fileSize": {"en": "File Size", "fr": "Taille du fichier"},
"creationDate": {"en": "Creation Date", "fr": "Date de création"}
}
)
class FilePreview(BaseModel, ModelMixin):
"""Data model for file preview"""
content: Union[str, bytes] = Field(description="File content (text or binary)")
mimeType: str = Field(description="MIME type of the file")
filename: str = Field(description="Original filename")
isText: bool = Field(description="Whether the content is text (True) or binary (False)")
encoding: Optional[str] = Field(None, description="Text encoding if content is text")
size: int = Field(description="Size of the content in bytes")
def to_dict(self) -> Dict[str, Any]:
"""Convert model to dictionary with proper content handling"""
data = super().to_dict()
# Convert bytes to base64 string if content is binary
if isinstance(data.get("content"), bytes):
import base64
data["content"] = base64.b64encode(data["content"]).decode('utf-8')
return data
# Register labels for FilePreview
register_model_labels(
"FilePreview",
{"en": "File Preview", "fr": "Aperçu du fichier"},
{
"content": {"en": "Content", "fr": "Contenu"},
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"filename": {"en": "Filename", "fr": "Nom de fichier"},
"isText": {"en": "Is Text", "fr": "Est du texte"},
"encoding": {"en": "Encoding", "fr": "Encodage"},
"size": {"en": "Size", "fr": "Taille"}
} }
) )

View file

@ -1,6 +1,6 @@
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response, Body from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response, Body
from fastapi.responses import JSONResponse, FileResponse from fastapi.responses import JSONResponse, FileResponse
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional, Union
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from dataclasses import dataclass from dataclasses import dataclass
@ -15,7 +15,7 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import FileItem from modules.interfaces.serviceManagementModel import FileItem, FilePreview
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
from modules.interfaces.serviceAppModel import User from modules.interfaces.serviceAppModel import User
@ -50,7 +50,9 @@ async def get_files(
# Get all files generically - only metadata, no binary data # Get all files generically - only metadata, no binary data
files = managementInterface.getAllFiles() files = managementInterface.getAllFiles()
return [FileItem(**file) for file in files]
# Return files directly since they are already FileItem objects
return files
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(
@ -82,13 +84,16 @@ async def upload_file(
) )
# Save file via LucyDOM interface in the database # Save file via LucyDOM interface in the database
fileMeta = managementInterface.saveUploadedFile(fileContent, file.filename) fileItem = managementInterface.saveUploadedFile(fileContent, file.filename)
# If workflowId is provided, update the file information # If workflowId is provided, update the file information
if workflowId: if workflowId:
updateData = {"workflowId": workflowId} updateData = {"workflowId": workflowId}
managementInterface.updateFile(fileMeta["id"], updateData) managementInterface.updateFile(fileItem.id, updateData)
fileMeta["workflowId"] = workflowId fileItem.workflowId = workflowId
# Convert FileItem to dictionary for JSON response
fileMeta = fileItem.to_dict()
# Successful response # Successful response
return JSONResponse({ return JSONResponse({
@ -245,12 +250,12 @@ async def get_file_stats(
# Calculate statistics # Calculate statistics
totalFiles = len(allFiles) totalFiles = len(allFiles)
totalSize = sum(file.get("size", 0) for file in allFiles) totalSize = sum(file.fileSize for file in allFiles)
# Group by file type # Group by file type
fileTypes = {} fileTypes = {}
for file in allFiles: for file in allFiles:
fileType = file.get("mimeType", "unknown").split("/")[0] fileType = file.mimeType.split("/")[0]
if fileType not in fileTypes: if fileType not in fileTypes:
fileTypes[fileType] = 0 fileTypes[fileType] = 0
fileTypes[fileType] += 1 fileTypes[fileType] += 1
@ -268,3 +273,76 @@ async def get_file_stats(
detail=f"Error retrieving file statistics: {str(e)}" detail=f"Error retrieving file statistics: {str(e)}"
) )
@router.get("/{fileId}/download")
@limiter.limit("30/minute")
async def download_file(
request: Request,
fileId: str = Path(..., description="ID of the file to download"),
currentUser: User = Depends(getCurrentUser)
) -> Response:
"""Download a file"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
# Get file data
fileData = managementInterface.getFile(fileId)
if not fileData:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found"
)
# Get file content
fileContent = managementInterface.getFileData(fileId)
if not fileContent:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File content not found for ID {fileId}"
)
# Return file as response
return Response(
content=fileContent,
media_type=fileData.mimeType,
headers={
"Content-Disposition": f"attachment; filename={fileData.filename}"
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error downloading file: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading file: {str(e)}"
)
@router.get("/{fileId}/preview", response_model=FilePreview)
@limiter.limit("30/minute")
async def preview_file(
request: Request,
fileId: str = Path(..., description="ID of the file to preview"),
currentUser: User = Depends(getCurrentUser)
) -> FilePreview:
"""Preview a file's content"""
try:
managementInterface = serviceManagementClass.getInterface(currentUser)
# Get file preview
preview = managementInterface.getFilePreview(fileId)
if not preview:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found or no content available"
)
return FilePreview(**preview)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error previewing file: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error previewing file: {str(e)}"
)

View file

@ -402,75 +402,6 @@ async def delete_file_from_message(
detail=f"Error deleting file reference: {str(e)}" detail=f"Error deleting file reference: {str(e)}"
) )
# File preview and download routes
@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: User = Depends(getCurrentUser)
) -> ChatDocument:
"""Preview a file's content."""
try:
# Get service container
service = createServiceContainer(currentUser)
# 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"
)
return ChatDocument(**document)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error previewing file: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error previewing file: {str(e)}"
)
@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: User = Depends(getCurrentUser)
) -> Response:
"""Download a file."""
try:
# Get service container
service = createServiceContainer(currentUser)
# Get file data
fileInfo = service.base.downloadFile(fileId)
if not fileInfo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"File with ID {fileId} not found"
)
# Return file as response
return Response(
content=fileInfo["content"],
media_type=fileInfo["contentType"],
headers={
"Content-Disposition": f"attachment; filename={fileInfo['name']}"
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error downloading file: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error downloading file: {str(e)}"
)
@router.get("/workflows", response_model=List[ChatWorkflow]) @router.get("/workflows", response_model=List[ChatWorkflow])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_workflows( async def get_workflows(