From 03c17d750617e1aca4cf10642ae4fd620688cda4 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Fri, 13 Jun 2025 00:41:51 +0200 Subject: [PATCH] methods done --- app.py | 2 +- modules/connectors/connectorAiAnthropic.py | 139 +-- modules/connectors/connectorAiOpenai.py | 4 +- modules/interfaces/interfaceAi.py | 38 - modules/interfaces/interfaceAiCalls.py | 141 +++ ...viceAppAccess.py => interfaceAppAccess.py} | 2 +- ...erviceAppModel.py => interfaceAppModel.py} | 0 ...viceAppClass.py => interfaceAppObjects.py} | 30 +- ...ceChatAccess.py => interfaceChatAccess.py} | 2 +- ...viceChatModel.py => interfaceChatModel.py} | 0 ...ceChatClass.py => interfaceChatObjects.py} | 14 +- ...tAccess.py => interfaceComponentAccess.py} | 4 +- ...entModel.py => interfaceComponentModel.py} | 0 ...tClass.py => interfaceComponentObjects.py} | 20 +- modules/methods/methodBase.py | 11 +- modules/methods/methodCoder.py | 81 +- modules/methods/methodDocument.py | 209 ++-- modules/methods/methodExcel.py | 188 ++++ modules/methods/methodOperator.py | 4 +- modules/methods/methodOutlook.py | 294 +++--- modules/methods/methodPowerpoint.py | 447 ++++----- modules/methods/methodSharepoint.py | 466 ++++----- modules/methods/methodWeb.py | 569 +++-------- modules/routes/routeAdmin.py | 2 +- modules/routes/routeAttributes.py | 2 +- modules/routes/routeDataConnections.py | 4 +- modules/routes/routeDataFiles.py | 34 +- modules/routes/routeDataMandates.py | 14 +- modules/routes/routeDataPrompts.py | 16 +- modules/routes/routeDataUsers.py | 14 +- modules/routes/routeSecurityGoogle.py | 4 +- modules/routes/routeSecurityLocal.py | 4 +- modules/routes/routeSecurityMsft.py | 4 +- modules/routes/routeWorkflows.py | 58 +- modules/security/auth.py | 4 +- modules/workflow/managerChat.py | 930 ++++++------------ modules/workflow/managerDocument.py | 2 +- modules/workflow/managerWorkflow.py | 8 +- modules/workflow/processorDocument.py | 71 +- modules/workflow/serviceContainer.py | 339 ++++--- notes/changelog.txt | 84 +- notes/data_specification.md | 16 +- notes/methodbased_specification.md | 8 +- 43 files changed, 1761 insertions(+), 2522 deletions(-) delete mode 100644 modules/interfaces/interfaceAi.py create mode 100644 modules/interfaces/interfaceAiCalls.py rename modules/interfaces/{serviceAppAccess.py => interfaceAppAccess.py} (99%) rename modules/interfaces/{serviceAppModel.py => interfaceAppModel.py} (100%) rename modules/interfaces/{serviceAppClass.py => interfaceAppObjects.py} (97%) rename modules/interfaces/{serviceChatAccess.py => interfaceChatAccess.py} (98%) rename modules/interfaces/{serviceChatModel.py => interfaceChatModel.py} (100%) rename modules/interfaces/{serviceChatClass.py => interfaceChatObjects.py} (99%) rename modules/interfaces/{serviceManagementAccess.py => interfaceComponentAccess.py} (99%) rename modules/interfaces/{serviceManagementModel.py => interfaceComponentModel.py} (100%) rename modules/interfaces/{serviceManagementClass.py => interfaceComponentObjects.py} (98%) create mode 100644 modules/methods/methodExcel.py diff --git a/app.py b/app.py index ed80f158..05b54a9c 100644 --- a/app.py +++ b/app.py @@ -105,7 +105,7 @@ async def lifespan(app: FastAPI): logger.info("Application is starting up") # Initialize root interface to ensure database is properly set up - from modules.interfaces.serviceAppClass import getRootInterface + from modules.interfaces.interfaceAppObjects import getRootInterface getRootInterface() yield diff --git a/modules/connectors/connectorAiAnthropic.py b/modules/connectors/connectorAiAnthropic.py index 9f452790..fce51e97 100644 --- a/modules/connectors/connectorAiAnthropic.py +++ b/modules/connectors/connectorAiAnthropic.py @@ -17,7 +17,7 @@ def loadConfigData(): "maxTokens": int(APP_CONFIG.get('Connector_AiAnthropic_MAX_TOKENS')) } -class ChatService: +class AiAnthropic: """Connector for communication with the Anthropic API.""" def __init__(self): @@ -39,7 +39,7 @@ class ChatService: logger.info(f"Anthropic Connector initialized with model: {self.modelName}") - async def callApi(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> Dict[str, Any]: + async def callAiBasic(self, messages: List[Dict[str, Any]], temperature: float = None, maxTokens: int = None) -> Dict[str, Any]: """ Calls the Anthropic API with the given messages. @@ -49,15 +49,12 @@ class ChatService: maxTokens: Maximum number of tokens in the response Returns: - The response converted to OpenAI format + The response in OpenAI format Raises: HTTPException: For errors in API communication """ try: - # Convert OpenAI format to Anthropic format - formattedMessages = self._convertToAnthropicFormat(messages) - # Use parameters from configuration if none were overridden if temperature is None: temperature = self.config.get("temperature", 0.2) @@ -68,7 +65,7 @@ class ChatService: # Create Anthropic API payload payload = { "model": self.modelName, - "messages": formattedMessages, + "messages": messages, "temperature": temperature, "max_tokens": maxTokens } @@ -82,110 +79,44 @@ class ChatService: logger.error(f"Anthropic API error: {response.status_code} - {response.text}") raise HTTPException(status_code=500, detail="Error communicating with Anthropic API") - # Convert response from Anthropic format to OpenAI format + # Parse response anthropicResponse = response.json() - openaiFormattedResponse = self._convertToOpenaiFormat(anthropicResponse) - return openaiFormattedResponse + # Extract content from response + content = "" + if "content" in anthropicResponse: + if isinstance(anthropicResponse["content"], list): + # Content is a list of parts (in newer API versions) + for part in anthropicResponse["content"]: + if part.get("type") == "text": + content += part.get("text", "") + else: + # Direct content as string (in older API versions) + content = anthropicResponse["content"] + + # Return in OpenAI format + return { + "id": anthropicResponse.get("id", ""), + "object": "chat.completion", + "created": anthropicResponse.get("created", 0), + "model": anthropicResponse.get("model", self.modelName), + "choices": [ + { + "message": { + "role": "assistant", + "content": content + }, + "index": 0, + "finish_reason": "stop" + } + ] + } except Exception as e: logger.error(f"Error calling Anthropic API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Anthropic API: {str(e)}") - def _convertToAnthropicFormat(self, openaiMessages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - Converts messages from OpenAI format to Anthropic format. - - OpenAI uses: - [{"role": "system", "content": "..."}, - {"role": "user", "content": "..."}, - {"role": "assistant", "content": "..."}] - - Anthropic uses: - [{"role": "user", "content": "..."}, - {"role": "assistant", "content": "..."}] - - Note: Anthropic has no direct system message equivalent, - so we add system messages to the first user message. - """ - anthropicMessages = [] - systemContent = "" - - # First extract all system messages - for msg in openaiMessages: - if msg.get("role") == "system": - systemContent += msg.get("content", "") + "\n\n" - - # Convert the remaining messages - for msg in openaiMessages: - role = msg.get("role") - content = msg.get("content", "") - - # Skip system messages (already extracted) - if role == "system": - continue - - # For the first user message: prepend system content if available - if role == "user" and systemContent and not any(m.get("role") == "user" for m in anthropicMessages): - if isinstance(content, str): - content = systemContent + content - elif isinstance(content, list): - # If content is an array (for multimodal messages) - textParts = [] - for part in content: - if part.get("type") == "text": - textParts.append(part) - - if textParts: - # Create a new text part with combined content - textParts[0] = { - "type": "text", - "text": systemContent + textParts[0].get("text", "") - } - - # Anthropic only supports "user" and "assistant" roles - if role not in ["user", "assistant"]: - role = "user" - - anthropicMessages.append({"role": role, "content": content}) - - return anthropicMessages - - def _convertToOpenaiFormat(self, anthropicResponse: Dict[str, Any]) -> Dict[str, Any]: - """ - Converts a response from Anthropic format to OpenAI format. - """ - # Extract content from Anthropic response - content = "" - if "content" in anthropicResponse: - if isinstance(anthropicResponse["content"], list): - # Content is a list of parts (in newer API versions) - for part in anthropicResponse["content"]: - if part.get("type") == "text": - content += part.get("text", "") - else: - # Direct content as string (in older API versions) - content = anthropicResponse["content"] - - # Create OpenAI-formatted response - return { - "id": anthropicResponse.get("id", ""), - "object": "chat.completion", - "created": anthropicResponse.get("created", 0), - "model": anthropicResponse.get("model", self.modelName), - "choices": [ - { - "message": { - "role": "assistant", - "content": content - }, - "index": 0, - "finish_reason": "stop" - } - ] - } - - async def analyzeImage(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str: + async def callAiImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str: """ Analyzes an image using Anthropic's vision capabilities. diff --git a/modules/connectors/connectorAiOpenai.py b/modules/connectors/connectorAiOpenai.py index 1014137d..847380cd 100644 --- a/modules/connectors/connectorAiOpenai.py +++ b/modules/connectors/connectorAiOpenai.py @@ -18,7 +18,7 @@ def loadConfigData(): "maxTokens": int(APP_CONFIG.get('Connector_AiOpenai_MAX_TOKENS')) } -class AiConnector: +class AiOpenai: """Connector for communication with the OpenAI API.""" def __init__(self): @@ -85,7 +85,7 @@ class AiConnector: logger.error(f"Error calling OpenAI API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling OpenAI API: {str(e)}") - async def callAiImage(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str: + async def callAiImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str: """ Analyzes an image with the OpenAI Vision API. diff --git a/modules/interfaces/interfaceAi.py b/modules/interfaces/interfaceAi.py deleted file mode 100644 index 5c19ffb0..00000000 --- a/modules/interfaces/interfaceAi.py +++ /dev/null @@ -1,38 +0,0 @@ -import logging -from typing import Dict, Any, List, Union -from modules.connectors.connectorAiOpenai import AiConnector - -logger = logging.getLogger(__name__) - -class AiInterface: - """Interface for AI service interactions""" - - def __init__(self): - self.service = AiConnector() - - async def callAiBasic(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str: - """Enhanced AI service call with language support.""" - - # Add language instruction for user-facing responses - if produceUserAnswer and hasattr(self, 'userLanguage') and self.userLanguage: - ltext = f"Please respond in '{self.userLanguage}' language." - if messages and messages[0]["role"] == "system": - if "language" not in messages[0]["content"].lower(): - messages[0]["content"] = f"{ltext} {messages[0]['content']}" - else: - # Insert a system message with language instruction - messages.insert(0, { - "role": "system", - "content": ltext - }) - - # Call the AI service - return await self.service.callAiBasic(messages, temperature=temperature) - - async def callAiImage(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str: - """Enhanced AI service call with language support.""" - if not self.service: - logger.error("AI service not set in AiInterface") - return "Error: AI service not available" - return await self.service.callAiImage(imageData, mimeType, prompt) - diff --git a/modules/interfaces/interfaceAiCalls.py b/modules/interfaces/interfaceAiCalls.py new file mode 100644 index 00000000..79a662af --- /dev/null +++ b/modules/interfaces/interfaceAiCalls.py @@ -0,0 +1,141 @@ +import logging +from typing import Dict, Any, List, Union, Optional +from modules.connectors.connectorAiOpenai import AiOpenai +from modules.connectors.connectorAiAnthropic import AiAnthropic + +logger = logging.getLogger(__name__) + +class AiCalls: + """Interface for AI service interactions""" + + def __init__(self): + self.openaiService = AiOpenai() + self.anthropicService = AiAnthropic() + + async def callAiTextBasic(self, prompt: str, context: Optional[str] = None) -> str: + """ + Basic text processing using OpenAI. + + Args: + prompt: The user prompt to process + context: Optional system context/prompt + + Returns: + The AI response as text + """ + # Prepare messages in OpenAI format + messages = [] + + # Add system message if context provided + if context: + messages.append({ + "role": "system", + "content": context + }) + + # Add user message + messages.append({ + "role": "user", + "content": prompt + }) + + # Add language instruction for user-facing responses + if hasattr(self, 'userLanguage') and self.userLanguage: + ltext = f"Please respond in '{self.userLanguage}' language." + if messages and messages[0]["role"] == "system": + if "language" not in messages[0]["content"].lower(): + messages[0]["content"] = f"{ltext} {messages[0]['content']}" + else: + messages.insert(0, { + "role": "system", + "content": ltext + }) + + try: + return await self.openaiService.callAiBasic(messages) + except Exception as e: + logger.error(f"Error in OpenAI call: {str(e)}") + return f"Error: {str(e)}" + + async def callAiTextAdvanced(self, prompt: str, context: Optional[str] = None) -> str: + """ + Advanced text processing using Anthropic. + + Args: + prompt: The user prompt to process + context: Optional system context/prompt + + Returns: + The AI response as text + """ + # Prepare messages in OpenAI format + messages = [] + + # Add system message if context provided + if context: + messages.append({ + "role": "system", + "content": context + }) + + # Add user message + messages.append({ + "role": "user", + "content": prompt + }) + + # Add language instruction for user-facing responses + if hasattr(self, 'userLanguage') and self.userLanguage: + ltext = f"Please respond in '{self.userLanguage}' language." + if messages and messages[0]["role"] == "system": + if "language" not in messages[0]["content"].lower(): + messages[0]["content"] = f"{ltext} {messages[0]['content']}" + else: + messages.insert(0, { + "role": "system", + "content": ltext + }) + + try: + response = await self.anthropicService.callAiBasic(messages) + return response["choices"][0]["message"]["content"] + except Exception as e: + logger.error(f"Error in Anthropic call: {str(e)}") + return f"Error: {str(e)}" + + async def callAiImageBasic(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str: + """ + Basic image processing using OpenAI. + + Args: + prompt: The prompt for image analysis + imageData: The image data (file path or bytes) + mimeType: Optional MIME type of the image + + Returns: + The AI response as text + """ + try: + return await self.openaiService.callAiImage(imageData, mimeType, prompt) + except Exception as e: + logger.error(f"Error in OpenAI image call: {str(e)}") + return f"Error: {str(e)}" + + async def callAiImageAdvanced(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str: + """ + Advanced image processing using Anthropic. + + Args: + prompt: The prompt for image analysis + imageData: The image data (file path or bytes) + mimeType: Optional MIME type of the image + + Returns: + The AI response as text + """ + try: + return await self.anthropicService.callAiImage(prompt, imageData, mimeType) + except Exception as e: + logger.error(f"Error in Anthropic image call: {str(e)}") + return f"Error: {str(e)}" + diff --git a/modules/interfaces/serviceAppAccess.py b/modules/interfaces/interfaceAppAccess.py similarity index 99% rename from modules/interfaces/serviceAppAccess.py rename to modules/interfaces/interfaceAppAccess.py index a7b301f8..262ab3eb 100644 --- a/modules/interfaces/serviceAppAccess.py +++ b/modules/interfaces/interfaceAppAccess.py @@ -5,7 +5,7 @@ Access control for the Application. import logging from typing import Dict, Any, List, Optional from datetime import datetime -from modules.interfaces.serviceAppModel import UserPrivilege, Session, User +from modules.interfaces.interfaceAppModel import UserPrivilege, Session, User # Configure logger logger = logging.getLogger(__name__) diff --git a/modules/interfaces/serviceAppModel.py b/modules/interfaces/interfaceAppModel.py similarity index 100% rename from modules/interfaces/serviceAppModel.py rename to modules/interfaces/interfaceAppModel.py diff --git a/modules/interfaces/serviceAppClass.py b/modules/interfaces/interfaceAppObjects.py similarity index 97% rename from modules/interfaces/serviceAppClass.py rename to modules/interfaces/interfaceAppObjects.py index fc5e4791..92c55834 100644 --- a/modules/interfaces/serviceAppClass.py +++ b/modules/interfaces/interfaceAppObjects.py @@ -14,8 +14,8 @@ import uuid from modules.connectors.connectorDbJson import DatabaseConnector from modules.shared.configuration import APP_CONFIG -from modules.interfaces.serviceAppAccess import AppAccess -from modules.interfaces.serviceAppModel import ( +from modules.interfaces.interfaceAppAccess import AppAccess +from modules.interfaces.interfaceAppModel import ( User, Mandate, UserInDB, UserConnection, Session, AuthEvent, AuthAuthority, UserPrivilege, ConnectionStatus, Token, LocalToken, GoogleToken, MsftToken @@ -24,16 +24,16 @@ from modules.shared.attributeUtils import ModelMixin logger = logging.getLogger(__name__) -# Singleton factory for GatewayInterface instances per context +# Singleton factory for AppObjects instances per context _gatewayInterfaces = {} # Root interface instance -_rootGatewayInterface = None +_rootAppObjects = None # Password-Hashing pwdContext = CryptContext(schemes=["argon2"], deprecated="auto") -class GatewayInterface: +class AppObjects: """ Interface to the Gateway system. Manages users and mandates. @@ -793,9 +793,9 @@ class GatewayInterface: # Public Methods -def getInterface(currentUser: User) -> GatewayInterface: +def getInterface(currentUser: User) -> AppObjects: """ - Returns a GatewayInterface instance for the current user. + Returns a AppObjects instance for the current user. Handles initialization of database and records. """ if not currentUser: @@ -806,7 +806,7 @@ def getInterface(currentUser: User) -> GatewayInterface: # Create new instance if not exists if contextKey not in _gatewayInterfaces: - _gatewayInterfaces[contextKey] = GatewayInterface(currentUser) + _gatewayInterfaces[contextKey] = AppObjects(currentUser) return _gatewayInterfaces[contextKey] @@ -817,7 +817,7 @@ def getRootUser() -> User: """ try: # Create a temporary interface without user context - tempInterface = GatewayInterface() + tempInterface = AppObjects() # Get the initial user directly initialUserId = tempInterface.db.getInitialId("users") @@ -835,15 +835,15 @@ def getRootUser() -> User: logger.error(f"Error getting root user: {str(e)}") raise ValueError(f"Failed to get root user: {str(e)}") -def getRootInterface() -> GatewayInterface: +def getRootInterface() -> AppObjects: """ - Returns a GatewayInterface instance with root privileges. + Returns a AppObjects instance with root privileges. This is used for initial setup and user creation. """ - global _rootGatewayInterface + global _rootAppObjects - if _rootGatewayInterface is None: + if _rootAppObjects is None: rootUser = getRootUser() - _rootGatewayInterface = GatewayInterface(rootUser) + _rootAppObjects = AppObjects(rootUser) - return _rootGatewayInterface + return _rootAppObjects diff --git a/modules/interfaces/serviceChatAccess.py b/modules/interfaces/interfaceChatAccess.py similarity index 98% rename from modules/interfaces/serviceChatAccess.py rename to modules/interfaces/interfaceChatAccess.py index cd9428ff..22961874 100644 --- a/modules/interfaces/serviceChatAccess.py +++ b/modules/interfaces/interfaceChatAccess.py @@ -4,7 +4,7 @@ Handles user access management and permission checks. """ from typing import Dict, Any, List, Optional -from modules.interfaces.serviceAppModel import User, UserPrivilege +from modules.interfaces.interfaceAppModel import User, UserPrivilege class ChatAccess: """ diff --git a/modules/interfaces/serviceChatModel.py b/modules/interfaces/interfaceChatModel.py similarity index 100% rename from modules/interfaces/serviceChatModel.py rename to modules/interfaces/interfaceChatModel.py diff --git a/modules/interfaces/serviceChatClass.py b/modules/interfaces/interfaceChatObjects.py similarity index 99% rename from modules/interfaces/serviceChatClass.py rename to modules/interfaces/interfaceChatObjects.py index eb730e58..d939c394 100644 --- a/modules/interfaces/serviceChatClass.py +++ b/modules/interfaces/interfaceChatObjects.py @@ -12,11 +12,11 @@ from typing import Dict, Any, List, Optional, Union import hashlib import asyncio -from modules.interfaces.serviceChatAccess import ChatAccess -from modules.interfaces.serviceChatModel import ( +from modules.interfaces.interfaceChatAccess import ChatAccess +from modules.interfaces.interfaceChatModel import ( TaskStatus, UserInputRequest, ChatDocument, TaskItem, ChatStat, ChatLog, ChatMessage, ChatWorkflow, TaskAction ) -from modules.interfaces.serviceAppModel import User +from modules.interfaces.interfaceAppModel import User # DYNAMIC PART: Connectors to the Interface from modules.connectors.connectorDbJson import DatabaseConnector @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) # Singleton factory for Chat instances _chatInterfaces = {} -class ChatInterface: +class ChatObjects: """ Interface to Chat database and AI Connectors. Uses the JSON connector for data access with added language support. @@ -1025,9 +1025,9 @@ class ChatInterface: logger.error(f"Error deleting task: {str(e)}") return False -def getInterface(currentUser: Optional[User] = None) -> 'ChatInterface': +def getInterface(currentUser: Optional[User] = None) -> 'ChatObjects': """ - Returns a ChatInterface instance for the current user. + Returns a ChatObjects instance for the current user. Handles initialization of database and records. """ if not currentUser: @@ -1038,6 +1038,6 @@ def getInterface(currentUser: Optional[User] = None) -> 'ChatInterface': # Create new instance if not exists if contextKey not in _chatInterfaces: - _chatInterfaces[contextKey] = ChatInterface(currentUser) + _chatInterfaces[contextKey] = ChatObjects(currentUser) return _chatInterfaces[contextKey] \ No newline at end of file diff --git a/modules/interfaces/serviceManagementAccess.py b/modules/interfaces/interfaceComponentAccess.py similarity index 99% rename from modules/interfaces/serviceManagementAccess.py rename to modules/interfaces/interfaceComponentAccess.py index ef842b2f..6db839af 100644 --- a/modules/interfaces/serviceManagementAccess.py +++ b/modules/interfaces/interfaceComponentAccess.py @@ -5,12 +5,12 @@ Handles user access management and permission checks. import logging from typing import Dict, Any, List, Optional -from modules.interfaces.serviceAppModel import User +from modules.interfaces.interfaceAppModel import User # Configure logger logger = logging.getLogger(__name__) -class ManagementAccess: +class ComponentAccess: """ Access control class for Management interface. Handles user access management and permission checks. diff --git a/modules/interfaces/serviceManagementModel.py b/modules/interfaces/interfaceComponentModel.py similarity index 100% rename from modules/interfaces/serviceManagementModel.py rename to modules/interfaces/interfaceComponentModel.py diff --git a/modules/interfaces/serviceManagementClass.py b/modules/interfaces/interfaceComponentObjects.py similarity index 98% rename from modules/interfaces/serviceManagementClass.py rename to modules/interfaces/interfaceComponentObjects.py index e478ac18..87ec4e7c 100644 --- a/modules/interfaces/serviceManagementClass.py +++ b/modules/interfaces/interfaceComponentObjects.py @@ -11,11 +11,11 @@ from typing import Dict, Any, List, Optional, Union import hashlib -from modules.interfaces.serviceManagementAccess import ManagementAccess -from modules.interfaces.serviceManagementModel import ( +from modules.interfaces.interfaceComponentAccess import ComponentAccess +from modules.interfaces.interfaceComponentModel import ( FilePreview, Prompt, FileItem, FileData ) -from modules.interfaces.serviceAppModel import User, Mandate, UserPrivilege +from modules.interfaces.interfaceAppModel import User, Mandate, UserPrivilege # DYNAMIC PART: Connectors to the Interface from modules.connectors.connectorDbJson import DatabaseConnector @@ -49,7 +49,7 @@ class FileDeletionError(FileError): """Exception raised when there's an error deleting a file.""" pass -class ServiceManagement: +class ComponentObjects: """ Interface to Management database and AI Connectors. Uses the JSON connector for data access with added language support. @@ -60,7 +60,7 @@ class ServiceManagement: # Initialize variables first self.currentUser: Optional[User] = None self.userId: Optional[str] = None - self.access: Optional[ManagementAccess] = None # Will be set when user context is provided + self.access: Optional[ComponentAccess] = None # Will be set when user context is provided self.aiService: Optional[ChatService] = None # Will be set when user context is provided # Initialize database @@ -85,7 +85,7 @@ class ServiceManagement: self.userLanguage = currentUser.language # Default user language # Initialize access control with user context - self.access = ManagementAccess(self.currentUser, self.db) + self.access = ComponentAccess(self.currentUser, self.db) # Initialize AI service self.aiService = ChatService() @@ -143,7 +143,7 @@ class ServiceManagement: return # Get the root interface to access the initial mandate ID - from modules.interfaces.serviceAppClass import getRootInterface + from modules.interfaces.interfaceAppObjects import getRootInterface rootInterface = getRootInterface() # Get initial mandate ID through the root interface @@ -887,15 +887,15 @@ class ServiceManagement: raise FileError(f"Error downloading file: {str(e)}") -def getInterface(currentUser: Optional[User] = None) -> 'ServiceManagement': +def getInterface(currentUser: Optional[User] = None) -> 'ComponentObjects': """ - Returns a ServiceManagement instance. + Returns a ComponentObjects instance. If currentUser is provided, initializes with user context. Otherwise, returns an instance with only database access. """ # Create new instance if not exists if "default" not in _instancesManagement: - _instancesManagement["default"] = ServiceManagement() + _instancesManagement["default"] = ComponentObjects() interface = _instancesManagement["default"] diff --git a/modules/methods/methodBase.py b/modules/methods/methodBase.py index 7952b68e..824be505 100644 --- a/modules/methods/methodBase.py +++ b/modules/methods/methodBase.py @@ -3,10 +3,19 @@ from typing import Dict, List, Optional, Any, Literal from datetime import datetime, UTC from pydantic import BaseModel, Field import logging -from modules.interfaces.serviceChatModel import MethodResult +from modules.interfaces.interfaceChatModel import MethodResult +from functools import wraps logger = logging.getLogger(__name__) +def action(func): + """Decorator to mark a method as an available action""" + @wraps(func) + async def wrapper(self, *args, **kwargs): + return await func(self, *args, **kwargs) + wrapper.is_action = True + return wrapper + class MethodBase: """Base class for all methods""" diff --git a/modules/methods/methodCoder.py b/modules/methods/methodCoder.py index 7b9a1755..c99b93fb 100644 --- a/modules/methods/methodCoder.py +++ b/modules/methods/methodCoder.py @@ -3,85 +3,20 @@ import logging import ast import re -from modules.methods.methodBase import MethodBase, MethodResult +from modules.methods.methodBase import MethodBase, MethodResult, action logger = logging.getLogger(__name__) class MethodCoder(MethodBase): """Coder method implementation for code operations""" - def __init__(self): - super().__init__() + def __init__(self, serviceContainer: Any): + super().__init__(serviceContainer) self.name = "coder" self.description = "Handle code operations like analysis, generation, and refactoring" - - @property - def actions(self) -> Dict[str, Dict[str, Any]]: - """Available actions and their parameters""" - return { - "analyze": { - "description": "Analyze code structure and quality", - "retryMax": 2, - "timeout": 30, - "parameters": { - "code": {"type": "string", "required": True}, - "language": {"type": "string", "required": False}, - "metrics": {"type": "array", "items": "string", "required": False} - } - }, - "generate": { - "description": "Generate code based on requirements", - "retryMax": 2, - "timeout": 60, - "parameters": { - "requirements": {"type": "string", "required": True}, - "language": {"type": "string", "required": False}, - "style": {"type": "string", "required": False} - } - }, - "refactor": { - "description": "Refactor code for better quality", - "retryMax": 2, - "timeout": 60, - "parameters": { - "code": {"type": "string", "required": True}, - "language": {"type": "string", "required": False}, - "improvements": {"type": "array", "items": "string", "required": False} - } - } - } - async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: - """Execute coder method""" - try: - # Validate parameters - if not await self.validateParameters(action, parameters): - return self._createResult( - success=False, - data={"error": f"Invalid parameters for {action}"} - ) - - # Execute action - if action == "analyze": - return await self._analyzeCode(parameters) - elif action == "generate": - return await self._generateCode(parameters) - elif action == "refactor": - return await self._refactorCode(parameters) - else: - return self._createResult( - success=False, - data={"error": f"Unknown action: {action}"} - ) - - except Exception as e: - logger.error(f"Error executing coder {action}: {e}") - return self._createResult( - success=False, - data={"error": str(e)} - ) - - async def _analyzeCode(self, parameters: Dict[str, Any]) -> MethodResult: + @action + async def analyze(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: """Analyze code structure and quality""" try: code = parameters["code"] @@ -173,7 +108,8 @@ class MethodCoder(MethodBase): data={"error": f"Analysis failed: {str(e)}"} ) - async def _generateCode(self, parameters: Dict[str, Any]) -> MethodResult: + @action + async def generate(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: """Generate code based on requirements""" try: requirements = parameters["requirements"] @@ -216,7 +152,8 @@ class MethodCoder(MethodBase): data={"error": f"Generation failed: {str(e)}"} ) - async def _refactorCode(self, parameters: Dict[str, Any]) -> MethodResult: + @action + async def refactor(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: """Refactor code for better quality""" try: code = parameters["code"] diff --git a/modules/methods/methodDocument.py b/modules/methods/methodDocument.py index 1f669d21..34bafb43 100644 --- a/modules/methods/methodDocument.py +++ b/modules/methods/methodDocument.py @@ -7,14 +7,14 @@ import logging from typing import Dict, Any, List, Optional from datetime import datetime -from modules.interfaces.serviceChatModel import ( +from modules.interfaces.interfaceChatModel import ( ChatDocument, TaskDocument, ExtractedContent, ContentItem ) from modules.workflow.managerDocument import DocumentManager -from modules.methods.methodBase import MethodBase +from modules.methods.methodBase import MethodBase, MethodResult, action logger = logging.getLogger(__name__) @@ -25,100 +25,80 @@ class MethodDocument(MethodBase): """Initialize the document method""" super().__init__(serviceContainer) self.documentManager = DocumentManager(serviceContainer) - - async def process(self, action: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + + @action + async def extract(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: """ - Process document operations + Extract content from document Args: - action: The action to perform - parameters: Action parameters - - Returns: - Dictionary containing the operation result - - Raises: - ValueError: If action is not supported + parameters: + documentId: ID of the document to extract from + documentType: Type of document + extractionType: Type of extraction to perform """ try: - if action == "extract": - return await self._extractContent(parameters) - elif action == "analyze": - return await self._analyzeDocument(parameters) - elif action == "summarize": - return await self._summarizeDocument(parameters) + documentId = parameters["documentId"] + documentType = parameters.get("documentType", "text") + extractionType = parameters.get("extractionType", "full") + + # Get document from service + document = await self.service.interfaceComponent.getDocument(documentId) + if not document: + return self._createResult( + success=False, + data={"error": f"Document not found: {documentId}"} + ) + + # Extract content based on type + if documentType == "text": + content = await self.documentManager.extractTextContent(document, extractionType) + elif documentType == "table": + content = await self.documentManager.extractTableContent(document, extractionType) + elif documentType == "image": + content = await self.documentManager.extractImageContent(document, extractionType) else: - raise ValueError(f"Unsupported action: {action}") - except Exception as e: - logger.error(f"Error processing document action {action}: {str(e)}") - raise - - async def _extractContent(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - """ - Extract content from a document - - Args: - parameters: Dictionary containing: - - documentId: ID of the document to process - - documentType: Type of document ('ChatDocument' or 'TaskDocument') - - Returns: - Dictionary containing extracted content - """ - try: - documentId = parameters.get("documentId") - documentType = parameters.get("documentType", "ChatDocument") + return self._createResult( + success=False, + data={"error": f"Unsupported document type: {documentType}"} + ) - if not documentId: - raise ValueError("documentId is required") - - # Get document from database - if documentType == "ChatDocument": - document = await self._getChatDocument(documentId) - if not document: - raise ValueError(f"ChatDocument {documentId} not found") - extracted = await self.documentManager.extractFromChatDocument(document) - else: - document = await self._getTaskDocument(documentId) - if not document: - raise ValueError(f"TaskDocument {documentId} not found") - extracted = await self.documentManager.extractFromTaskDocument(document) - - return { - "success": True, - "content": extracted.dict(), - "metadata": await self.documentManager.getDocumentMetadata(document) - } + return self._createResult( + success=True, + data={ + "documentId": documentId, + "type": documentType, + "content": content + } + ) except Exception as e: logger.error(f"Error extracting content: {str(e)}") - return { - "success": False, - "error": str(e) - } - - async def _analyzeDocument(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def analyze(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: """ Analyze document content Args: - parameters: Dictionary containing: - - documentId: ID of the document to analyze - - documentType: Type of document - - analysisType: Type of analysis to perform - - Returns: - Dictionary containing analysis results + parameters: + documentId: ID of the document to analyze + documentType: Type of document + analysisType: Type of analysis to perform """ try: # Extract content first - contentResult = await self._extractContent(parameters) - if not contentResult["success"]: + contentResult = await self.extract(parameters) + if not contentResult.success: return contentResult # Perform analysis based on type analysisType = parameters.get("analysisType", "basic") - content = ExtractedContent(**contentResult["content"]) + content = ExtractedContent(**contentResult.data["content"]) if analysisType == "basic": # Basic analysis: count items, calculate statistics @@ -134,64 +114,71 @@ class MethodDocument(MethodBase): stats["itemTypes"][itemType] = 0 stats["itemTypes"][itemType] += 1 - return { - "success": True, - "analysis": stats - } + return self._createResult( + success=True, + data={ + "documentId": parameters["documentId"], + "analysis": stats + } + ) else: - raise ValueError(f"Unsupported analysis type: {analysisType}") + return self._createResult( + success=False, + data={"error": f"Unsupported analysis type: {analysisType}"} + ) except Exception as e: logger.error(f"Error analyzing document: {str(e)}") - return { - "success": False, - "error": str(e) - } - - async def _summarizeDocument(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def summarize(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: """ - Generate document summary + Summarize document content Args: - parameters: Dictionary containing: - - documentId: ID of the document to summarize - - documentType: Type of document - - summaryType: Type of summary to generate - - Returns: - Dictionary containing summary + parameters: + documentId: ID of the document to summarize + documentType: Type of document + summaryType: Type of summary to generate """ try: # Extract content first - contentResult = await self._extractContent(parameters) - if not contentResult["success"]: + contentResult = await self.extract(parameters) + if not contentResult.success: return contentResult # Generate summary based on type summaryType = parameters.get("summaryType", "basic") - content = ExtractedContent(**contentResult["content"]) + content = ExtractedContent(**contentResult.data["content"]) if summaryType == "basic": # Basic summary: concatenate all text content - summary = "\n".join( - item.data for item in content.contents - if item.label == "main" - ) + summary = "\n".join(item.content for item in content.contents if item.content) - return { - "success": True, - "summary": summary - } + return self._createResult( + success=True, + data={ + "documentId": parameters["documentId"], + "summary": summary + } + ) else: - raise ValueError(f"Unsupported summary type: {summaryType}") + return self._createResult( + success=False, + data={"error": f"Unsupported summary type: {summaryType}"} + ) except Exception as e: logger.error(f"Error summarizing document: {str(e)}") - return { - "success": False, - "error": str(e) - } - + return self._createResult( + success=False, + data={"error": str(e)} + ) + async def _getChatDocument(self, documentId: str) -> Optional[ChatDocument]: """Get ChatDocument from database""" try: diff --git a/modules/methods/methodExcel.py b/modules/methods/methodExcel.py new file mode 100644 index 00000000..898c25bd --- /dev/null +++ b/modules/methods/methodExcel.py @@ -0,0 +1,188 @@ +""" +Excel method module. +Handles Excel operations using the Excel service. +""" + +import logging +from typing import Dict, Any, List, Optional +from datetime import datetime + +from modules.interfaces.interfaceExcel import ExcelService +from modules.methods.methodBase import MethodBase, MethodResult, action + +logger = logging.getLogger(__name__) + +class MethodExcel(MethodBase): + """Excel method implementation""" + + def __init__(self, serviceContainer): + """Initialize the Excel method""" + super().__init__(serviceContainer) + self.excelService = ExcelService(serviceContainer) + + @action + async def read(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Read data from Excel file + + Args: + parameters: + fileId: ID of the Excel file + sheetName: Name of the sheet to read + range: Cell range to read (e.g. "A1:B10") + """ + try: + fileId = parameters["fileId"] + sheetName = parameters.get("sheetName", "Sheet1") + range = parameters.get("range") + + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: + return self._createResult( + success=False, + data={"error": f"File not found: {fileId}"} + ) + + # Read data from Excel + data = await self.excelService.readData(file, sheetName, range) + + return self._createResult( + success=True, + data={ + "fileId": fileId, + "sheetName": sheetName, + "range": range, + "data": data + } + ) + + except Exception as e: + logger.error(f"Error reading Excel file: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def write(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Write data to Excel file + + Args: + parameters: + fileId: ID of the Excel file + sheetName: Name of the sheet to write to + range: Cell range to write to (e.g. "A1:B10") + data: Data to write + """ + try: + fileId = parameters["fileId"] + sheetName = parameters.get("sheetName", "Sheet1") + range = parameters.get("range") + data = parameters["data"] + + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: + return self._createResult( + success=False, + data={"error": f"File not found: {fileId}"} + ) + + # Write data to Excel + await self.excelService.writeData(file, sheetName, range, data) + + return self._createResult( + success=True, + data={ + "fileId": fileId, + "sheetName": sheetName, + "range": range + } + ) + + except Exception as e: + logger.error(f"Error writing to Excel file: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def create(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Create new Excel file + + Args: + parameters: + fileName: Name of the new file + sheets: List of sheet configurations + """ + try: + fileName = parameters["fileName"] + sheets = parameters.get("sheets", [{"name": "Sheet1"}]) + + # Create new Excel file + file = await self.excelService.createFile(fileName, sheets) + + return self._createResult( + success=True, + data={ + "fileId": file.id, + "fileName": fileName, + "sheets": sheets + } + ) + + except Exception as e: + logger.error(f"Error creating Excel file: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def format(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Format Excel cells + + Args: + parameters: + fileId: ID of the Excel file + sheetName: Name of the sheet to format + range: Cell range to format (e.g. "A1:B10") + format: Format configuration + """ + try: + fileId = parameters["fileId"] + sheetName = parameters.get("sheetName", "Sheet1") + range = parameters.get("range") + format = parameters["format"] + + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: + return self._createResult( + success=False, + data={"error": f"File not found: {fileId}"} + ) + + # Apply formatting + await self.excelService.formatCells(file, sheetName, range, format) + + return self._createResult( + success=True, + data={ + "fileId": fileId, + "sheetName": sheetName, + "range": range + } + ) + + except Exception as e: + logger.error(f"Error formatting Excel cells: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) \ No newline at end of file diff --git a/modules/methods/methodOperator.py b/modules/methods/methodOperator.py index 9082ccf5..9e211272 100644 --- a/modules/methods/methodOperator.py +++ b/modules/methods/methodOperator.py @@ -2,7 +2,7 @@ from typing import Dict, List, Any, Optional from datetime import datetime, UTC import logging from .methodBase import MethodBase -from modules.interfaces.serviceChatModel import MethodResult +from modules.interfaces.interfaceChatModel import MethodResult logger = logging.getLogger(__name__) @@ -159,7 +159,7 @@ class MethodOperator(MethodBase): full_prompt += f"\nDocument: {content['document']}\n{content['content']}\n" # Call AI service - response = await self.service.callAiBasic(full_prompt) + response = await self.service.callAiTextBasic(full_prompt) return self._createResult( success=True, diff --git a/modules/methods/methodOutlook.py b/modules/methods/methodOutlook.py index 68dd07d0..48bfa702 100644 --- a/modules/methods/methodOutlook.py +++ b/modules/methods/methodOutlook.py @@ -1,202 +1,176 @@ -from typing import Dict, Any, Optional -import logging -from datetime import datetime, UTC -from O365 import Account, MSGraphProtocol +""" +Outlook method module. +Handles Outlook operations using the Outlook service. +""" -from modules.methods.methodBase import MethodBase, MethodResult -from modules.models.userConnection import UserConnection +import logging +from typing import Dict, Any, List, Optional +from datetime import datetime + +from modules.interfaces.interfaceOutlook import OutlookService +from modules.methods.methodBase import MethodBase, MethodResult, action logger = logging.getLogger(__name__) class MethodOutlook(MethodBase): - """Outlook method implementation for email operations""" + """Outlook method implementation""" - def __init__(self): - super().__init__() - self.name = "outlook" - self.description = "Handle Outlook email operations like reading and sending emails" + def __init__(self, serviceContainer): + """Initialize the Outlook method""" + super().__init__(serviceContainer) + self.outlookService = OutlookService(serviceContainer) + + @action + async def readMails(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Read emails from Outlook - @property - def actions(self) -> Dict[str, Dict[str, Any]]: - """Available actions and their parameters""" - return { - "readMails": { - "description": "Read emails from Outlook", - "retryMax": 2, - "timeout": 30, - "parameters": { - "folder": {"type": "string", "required": False}, - "query": {"type": "string", "required": False}, - "maxResults": {"type": "number", "required": False}, - "includeAttachments": {"type": "boolean", "required": False} - } - }, - "sendMail": { - "description": "Send email through Outlook", - "retryMax": 2, - "timeout": 30, - "parameters": { - "to": {"type": "array", "items": "string", "required": True}, - "subject": {"type": "string", "required": True}, - "body": {"type": "string", "required": True}, - "cc": {"type": "array", "items": "string", "required": False}, - "bcc": {"type": "array", "items": "string", "required": False}, - "attachments": {"type": "array", "items": "string", "required": False} - } - } - } - - async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: - """Execute Outlook method""" - try: - # Validate parameters - if not await self.validateParameters(action, parameters): - return self._createResult( - success=False, - data={"error": f"Invalid parameters for {action}"} - ) - - # Get UserConnection from auth_data - if not authData or "userConnection" not in authData: - return self._createResult( - success=False, - data={"error": "UserConnection required for Outlook operations"} - ) - - userConnection: UserConnection = authData["userConnection"] - - # Execute action - if action == "readMails": - return await self._readMails(parameters, userConnection) - elif action == "sendMail": - return await self._sendMail(parameters, userConnection) - else: - return self._createResult( - success=False, - data={"error": f"Unknown action: {action}"} - ) - - except Exception as e: - logger.error(f"Error executing Outlook {action}: {e}") - return self._createResult( - success=False, - data={"error": str(e)} - ) - - async def _readMails(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Read emails from Outlook""" + Args: + parameters: + folder: Folder to read from (default: inbox) + query: Search query + maxResults: Maximum number of results + includeAttachments: Whether to include attachments + """ try: folder = parameters.get("folder", "inbox") query = parameters.get("query") maxResults = parameters.get("maxResults", 10) includeAttachments = parameters.get("includeAttachments", False) - # Create Outlook account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() + # Read emails + emails = await self.outlookService.readEmails( + folder=folder, + query=query, + maxResults=maxResults, + includeAttachments=includeAttachments ) - # Get mailbox - mailbox = account.mailbox() - - # Get folder - targetFolder = mailbox.folder(folder_name=folder) - - # Get messages - if query: - messages = targetFolder.get_messages(query=query, limit=maxResults) - else: - messages = targetFolder.get_messages(limit=maxResults) - - # Process messages - results = [] - for message in messages: - msgData = { - "id": message.object_id, - "subject": message.subject, - "from": message.sender.address, - "to": [to.address for to in message.to], - "cc": [cc.address for cc in message.cc], - "received": message.received.strftime("%Y-%m-%d %H:%M:%S"), - "body": message.body, - "hasAttachments": message.has_attachments - } - - if includeAttachments and message.has_attachments: - attachments = [] - for attachment in message.attachments: - attachments.append({ - "name": attachment.name, - "contentType": attachment.content_type, - "size": attachment.size - }) - msgData["attachments"] = attachments - - results.append(msgData) - return self._createResult( success=True, data={ "folder": folder, "query": query, - "messages": results + "emails": emails } ) + except Exception as e: - logger.error(f"Error reading Outlook emails: {e}") + logger.error(f"Error reading emails: {str(e)}") return self._createResult( success=False, - data={"error": f"Read failed: {str(e)}"} + data={"error": str(e)} ) - - async def _sendMail(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Send email through Outlook""" + + @action + async def sendMail(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Send email using Outlook + + Args: + parameters: + to: List of recipient email addresses + subject: Email subject + body: Email body + attachments: List of attachment file IDs + """ try: - toAddresses = parameters["to"] + to = parameters["to"] subject = parameters["subject"] body = parameters["body"] - ccAddresses = parameters.get("cc", []) - bccAddresses = parameters.get("bcc", []) attachments = parameters.get("attachments", []) - # Create Outlook account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() + # Send email + messageId = await self.outlookService.sendEmail( + to=to, + subject=subject, + body=body, + attachments=attachments ) - # Get mailbox - mailbox = account.mailbox() - - # Create new message - message = mailbox.new_message() - message.to.add(toAddresses) - if ccAddresses: - message.cc.add(ccAddresses) - if bccAddresses: - message.bcc.add(bccAddresses) - message.subject = subject - message.body = body - - # Add attachments - for attachmentPath in attachments: - message.attachments.add(attachmentPath) - - # Send message - message.send() - return self._createResult( success=True, data={ - "to": toAddresses, - "subject": subject, - "sent": datetime.now(UTC).isoformat() + "messageId": messageId, + "to": to, + "subject": subject } ) + except Exception as e: - logger.error(f"Error sending Outlook email: {e}") + logger.error(f"Error sending email: {str(e)}") return self._createResult( success=False, - data={"error": f"Send failed: {str(e)}"} + data={"error": str(e)} + ) + + @action + async def createFolder(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Create folder in Outlook + + Args: + parameters: + name: Folder name + parentFolder: Parent folder ID (optional) + """ + try: + name = parameters["name"] + parentFolder = parameters.get("parentFolder") + + # Create folder + folderId = await self.outlookService.createFolder( + name=name, + parentFolder=parentFolder + ) + + return self._createResult( + success=True, + data={ + "folderId": folderId, + "name": name, + "parentFolder": parentFolder + } + ) + + except Exception as e: + logger.error(f"Error creating folder: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def moveMail(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Move email to different folder + + Args: + parameters: + messageId: ID of the message to move + targetFolder: ID of the target folder + """ + try: + messageId = parameters["messageId"] + targetFolder = parameters["targetFolder"] + + # Move email + await self.outlookService.moveEmail( + messageId=messageId, + targetFolder=targetFolder + ) + + return self._createResult( + success=True, + data={ + "messageId": messageId, + "targetFolder": targetFolder + } + ) + + except Exception as e: + logger.error(f"Error moving email: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} ) \ No newline at end of file diff --git a/modules/methods/methodPowerpoint.py b/modules/methods/methodPowerpoint.py index 26ccf458..d5a90a4e 100644 --- a/modules/methods/methodPowerpoint.py +++ b/modules/methods/methodPowerpoint.py @@ -1,375 +1,260 @@ -from typing import Dict, Any, Optional -import logging -import os -from pathlib import Path +""" +PowerPoint method module. +Handles PowerPoint operations using the PowerPoint service. +""" -from modules.methods.methodBase import MethodBase, MethodResult -from modules.models.userConnection import UserConnection -from modules.models.account import Account -from modules.protocols.msGraphProtocol import MSGraphProtocol +import logging +from typing import Dict, Any, List, Optional +from datetime import datetime + +from modules.interfaces.interfacePowerpoint import PowerpointService +from modules.methods.methodBase import MethodBase, MethodResult, action logger = logging.getLogger(__name__) class MethodPowerpoint(MethodBase): - """Powerpoint method implementation for PowerPoint operations""" + """PowerPoint method implementation""" - def __init__(self): - super().__init__() - self.name = "powerpoint" - self.description = "Handle PowerPoint operations like reading, writing, and converting presentations" + def __init__(self, serviceContainer): + """Initialize the PowerPoint method""" + super().__init__(serviceContainer) + self.powerpointService = PowerpointService(serviceContainer) + + @action + async def read(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Read PowerPoint presentation - @property - def actions(self) -> Dict[str, Dict[str, Any]]: - """Available actions and their parameters""" - return { - "read": { - "description": "Read PowerPoint presentation content", - "retryMax": 2, - "timeout": 30, - "parameters": { - "path": {"type": "string", "required": True}, - "format": {"type": "string", "required": False}, - "includeNotes": {"type": "boolean", "required": False} - } - }, - "write": { - "description": "Write content to PowerPoint presentation", - "retryMax": 2, - "timeout": 60, - "parameters": { - "path": {"type": "string", "required": True}, - "content": {"type": "object", "required": True}, - "template": {"type": "string", "required": False} - } - }, - "convert": { - "description": "Convert PowerPoint presentation between formats", - "retryMax": 2, - "timeout": 60, - "parameters": { - "sourcePath": {"type": "string", "required": True}, - "targetPath": {"type": "string", "required": True}, - "sourceFormat": {"type": "string", "required": False}, - "targetFormat": {"type": "string", "required": False} - } - }, - "createPresentation": { - "description": "Create a new PowerPoint presentation", - "retryMax": 2, - "timeout": 60, - "parameters": { - "title": {"type": "string", "required": True}, - "template": {"type": "string", "required": False} - } - }, - "addSlide": { - "description": "Add a new slide to presentation", - "retryMax": 2, - "timeout": 60, - "parameters": { - "presentationId": {"type": "string", "required": True}, - "layout": {"type": "string", "required": False}, - "title": {"type": "string", "required": False} - } - }, - "addContent": { - "description": "Add content to a slide", - "retryMax": 2, - "timeout": 60, - "parameters": { - "presentationId": {"type": "string", "required": True}, - "slideId": {"type": "string", "required": True}, - "contentType": {"type": "string", "required": True}, - "content": {"type": "object", "required": True}, - "position": {"type": "object", "required": False} - } - } - } - - async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: - """Execute PowerPoint method""" + Args: + parameters: + fileId: ID of the PowerPoint file + includeSlides: Whether to include slide content + """ try: - # Validate parameters - if not await self.validateParameters(action, parameters): + fileId = parameters["fileId"] + includeSlides = parameters.get("includeSlides", True) + + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: return self._createResult( success=False, - data={"error": f"Invalid parameters for {action}"} + data={"error": f"File not found: {fileId}"} ) - # Get UserConnection from auth_data - if not authData or "userConnection" not in authData: - return self._createResult( - success=False, - data={"error": "UserConnection required for PowerPoint operations"} - ) + # Read presentation + presentation = await self.powerpointService.readPresentation(file, includeSlides) - userConnection: UserConnection = authData["userConnection"] + return self._createResult( + success=True, + data={ + "fileId": fileId, + "presentation": presentation + } + ) - # Execute action - if action == "createPresentation": - return await self._createPresentation(parameters, userConnection) - elif action == "addSlide": - return await self._addSlide(parameters, userConnection) - elif action == "addContent": - return await self._addContent(parameters, userConnection) - else: - return self._createResult( - success=False, - data={"error": f"Unknown action: {action}"} - ) - except Exception as e: - logger.error(f"Error executing PowerPoint {action}: {e}") + logger.error(f"Error reading PowerPoint: {str(e)}") return self._createResult( success=False, data={"error": str(e)} ) - - async def _read_presentation(self, parameters: Dict[str, Any], authData: Dict[str, Any]) -> MethodResult: - """Read PowerPoint presentation content""" + + @action + async def write(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Write PowerPoint presentation + + Args: + parameters: + fileId: ID of the PowerPoint file + slides: List of slide configurations + """ try: - path = Path(parameters["path"]) - if not path.exists(): + fileId = parameters["fileId"] + slides = parameters["slides"] + + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: return self._createResult( success=False, - data={"error": f"File not found: {path}"} + data={"error": f"File not found: {fileId}"} ) - # Determine format if not specified - format = parameters.get("format") - if not format: - format = path.suffix[1:] if path.suffix else "pptx" + # Write presentation + await self.powerpointService.writePresentation(file, slides) - # TODO: Implement PowerPoint reading using Microsoft Graph API - # This is a placeholder implementation return self._createResult( success=True, data={ - "path": str(path), - "format": format, - "slides": [ - { - "number": 1, - "title": "Example Slide", - "content": "Example content", - "notes": "Example notes" if parameters.get("includeNotes", False) else None - } - ] + "fileId": fileId, + "slideCount": len(slides) } ) + except Exception as e: - logger.error(f"Error reading presentation: {e}") + logger.error(f"Error writing PowerPoint: {str(e)}") return self._createResult( success=False, - data={"error": f"Read failed: {str(e)}"} + data={"error": str(e)} ) - - async def _write_presentation(self, parameters: Dict[str, Any], authData: Dict[str, Any]) -> MethodResult: - """Write content to PowerPoint presentation""" + + @action + async def convert(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Convert PowerPoint to other format + + Args: + parameters: + fileId: ID of the PowerPoint file + format: Target format (pdf, png, etc.) + """ try: - path = Path(parameters["path"]) + fileId = parameters["fileId"] + format = parameters["format"] - # Create directory if it doesn't exist - path.parent.mkdir(parents=True, exist_ok=True) - - # Determine format if not specified - format = parameters.get("format") - if not format: - format = path.suffix[1:] if path.suffix else "pptx" - - # TODO: Implement PowerPoint writing using Microsoft Graph API - # This is a placeholder implementation - return self._createResult( - success=True, - data={ - "path": str(path), - "format": format, - "slides": len(parameters["content"].get("slides", [])) - } - ) - except Exception as e: - logger.error(f"Error writing presentation: {e}") - return self._createResult( - success=False, - data={"error": f"Write failed: {str(e)}"} - ) - - async def _convert_presentation(self, parameters: Dict[str, Any], authData: Dict[str, Any]) -> MethodResult: - """Convert PowerPoint presentation between formats""" - try: - source_path = Path(parameters["sourcePath"]) - target_path = Path(parameters["targetPath"]) - - if not source_path.exists(): + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: return self._createResult( success=False, - data={"error": f"Source file not found: {source_path}"} + data={"error": f"File not found: {fileId}"} ) - # Determine formats if not specified - source_format = parameters.get("sourceFormat") - if not source_format: - source_format = source_path.suffix[1:] if source_path.suffix else "pptx" + # Convert presentation + convertedFile = await self.powerpointService.convertPresentation(file, format) - target_format = parameters.get("targetFormat") - if not target_format: - target_format = target_path.suffix[1:] if target_path.suffix else "pptx" - - # TODO: Implement PowerPoint conversion using Microsoft Graph API - # This is a placeholder implementation return self._createResult( success=True, data={ - "sourcePath": str(source_path), - "targetPath": str(target_path), - "sourceFormat": source_format, - "targetFormat": target_format + "fileId": fileId, + "format": format, + "convertedFileId": convertedFile.id } ) + except Exception as e: - logger.error(f"Error converting presentation: {e}") + logger.error(f"Error converting PowerPoint: {str(e)}") return self._createResult( success=False, - data={"error": f"Conversion failed: {str(e)}"} + data={"error": str(e)} ) - - async def _createPresentation(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Create a new PowerPoint presentation""" + + @action + async def createPresentation(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Create new PowerPoint presentation + + Args: + parameters: + fileName: Name of the new file + template: Template ID (optional) + """ try: - title = parameters["title"] + fileName = parameters["fileName"] template = parameters.get("template") - # Create PowerPoint account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() - ) - - # Get drive - drive = account.drive() - # Create presentation - if template: - # Copy template - templateFile = drive.get_item_by_path(template) - newFile = templateFile.copy(f"{title}.pptx") - else: - # Create blank presentation - newFile = drive.create_file( - name=f"{title}.pptx", - content_type="application/vnd.openxmlformats-officedocument.presentationml.presentation" - ) + file = await self.powerpointService.createPresentation(fileName, template) return self._createResult( success=True, data={ - "id": newFile.object_id, - "name": newFile.name, - "webUrl": newFile.web_url + "fileId": file.id, + "fileName": fileName, + "template": template } ) + except Exception as e: - logger.error(f"Error creating PowerPoint presentation: {e}") + logger.error(f"Error creating PowerPoint: {str(e)}") return self._createResult( success=False, - data={"error": f"Create failed: {str(e)}"} + data={"error": str(e)} ) - - async def _addSlide(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Add a new slide to presentation""" + + @action + async def addSlide(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Add slide to presentation + + Args: + parameters: + fileId: ID of the PowerPoint file + layout: Slide layout + content: Slide content + """ try: - presentationId = parameters["presentationId"] + fileId = parameters["fileId"] layout = parameters.get("layout", "title") - title = parameters.get("title") + content = parameters.get("content", {}) - # Create PowerPoint account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() - ) - - # Get drive - drive = account.drive() - - # Get presentation - presentation = drive.get_item_by_id(presentationId) + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: + return self._createResult( + success=False, + data={"error": f"File not found: {fileId}"} + ) # Add slide - slide = presentation.add_slide(layout=layout) - if title: - slide.title = title + slideId = await self.powerpointService.addSlide(file, layout, content) return self._createResult( success=True, data={ - "slideId": slide.object_id, - "layout": layout, - "title": title + "fileId": fileId, + "slideId": slideId, + "layout": layout } ) + except Exception as e: - logger.error(f"Error adding PowerPoint slide: {e}") + logger.error(f"Error adding slide: {str(e)}") return self._createResult( success=False, - data={"error": f"Add slide failed: {str(e)}"} + data={"error": str(e)} ) - - async def _addContent(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Add content to a slide""" + + @action + async def addContent(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Add content to slide + + Args: + parameters: + fileId: ID of the PowerPoint file + slideId: ID of the slide + content: Content to add + """ try: - presentationId = parameters["presentationId"] + fileId = parameters["fileId"] slideId = parameters["slideId"] - contentType = parameters["contentType"] content = parameters["content"] - position = parameters.get("position", {"x": 0, "y": 0}) - # Create PowerPoint account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() - ) - - # Get drive - drive = account.drive() - - # Get presentation and slide - presentation = drive.get_item_by_id(presentationId) - slide = presentation.get_slide(slideId) - - # Add content based on type - if contentType == "text": - shape = slide.add_text_box( - text=content, - left=position["x"], - top=position["y"] + # Get file from service + file = await self.service.interfaceComponent.getFile(fileId) + if not file: + return self._createResult( + success=False, + data={"error": f"File not found: {fileId}"} ) - elif contentType == "image": - shape = slide.add_picture( - image_path=content, - left=position["x"], - top=position["y"] - ) - elif contentType == "table": - shape = slide.add_table( - rows=content["rows"], - cols=content["cols"], - left=position["x"], - top=position["y"] - ) - else: - raise ValueError(f"Unsupported content type: {contentType}") + + # Add content + await self.powerpointService.addContent(file, slideId, content) return self._createResult( success=True, data={ - "shapeId": shape.object_id, - "contentType": contentType, - "position": position + "fileId": fileId, + "slideId": slideId } ) + except Exception as e: - logger.error(f"Error adding PowerPoint content: {e}") + logger.error(f"Error adding content: {str(e)}") return self._createResult( success=False, - data={"error": f"Add content failed: {str(e)}"} + data={"error": str(e)} ) \ No newline at end of file diff --git a/modules/methods/methodSharepoint.py b/modules/methods/methodSharepoint.py index e75fbd6f..d4eda812 100644 --- a/modules/methods/methodSharepoint.py +++ b/modules/methods/methodSharepoint.py @@ -1,389 +1,269 @@ -from typing import Dict, Any, Optional -import logging -from datetime import datetime, UTC -from office365.runtime.auth.user_credential import UserCredential -from office365.sharepoint.client_context import ClientContext -from office365.sharepoint.files.file import File -from office365.sharepoint.lists.list import List -from office365.sharepoint.lists.list_creation_information import ListCreationInformation +""" +SharePoint method module. +Handles SharePoint operations using the SharePoint service. +""" -from modules.methods.methodBase import MethodBase, MethodResult -from modules.models.userConnection import UserConnection +import logging +from typing import Dict, Any, List, Optional +from datetime import datetime + +from modules.interfaces.interfaceSharepoint import SharepointService +from modules.methods.methodBase import MethodBase, MethodResult, action logger = logging.getLogger(__name__) class MethodSharepoint(MethodBase): - """SharePoint method implementation for document operations""" + """SharePoint method implementation""" - def __init__(self): - super().__init__() - self.name = "sharepoint" - self.description = "Handle SharePoint document operations like search, read, and write" + def __init__(self, serviceContainer): + """Initialize the SharePoint method""" + super().__init__(serviceContainer) + self.sharepointService = SharepointService(serviceContainer) + + @action + async def search(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Search SharePoint content - @property - def actions(self) -> Dict[str, Dict[str, Any]]: - """Available actions and their parameters""" - return { - "search": { - "description": "Search SharePoint documents", - "retryMax": 3, - "timeout": 30, - "parameters": { - "query": {"type": "string", "required": True}, - "siteUrl": {"type": "string", "required": True}, - "listName": {"type": "string", "required": False}, - "maxResults": {"type": "number", "required": False} - } - }, - "read": { - "description": "Read SharePoint document content", - "retryMax": 2, - "timeout": 30, - "parameters": { - "fileUrl": {"type": "string", "required": True}, - "siteUrl": {"type": "string", "required": True} - } - }, - "write": { - "description": "Write content to SharePoint document", - "retryMax": 2, - "timeout": 30, - "parameters": { - "fileUrl": {"type": "string", "required": True}, - "siteUrl": {"type": "string", "required": True}, - "content": {"type": "string", "required": True}, - "contentType": {"type": "string", "required": False} - } - }, - "readList": { - "description": "Read items from SharePoint list", - "retryMax": 2, - "timeout": 30, - "parameters": { - "siteUrl": {"type": "string", "required": True}, - "listName": {"type": "string", "required": True}, - "query": {"type": "string", "required": False}, - "fields": {"type": "array", "required": False} - } - }, - "writeList": { - "description": "Write items to SharePoint list", - "retryMax": 2, - "timeout": 30, - "parameters": { - "siteUrl": {"type": "string", "required": True}, - "listName": {"type": "string", "required": True}, - "items": {"type": "array", "required": True} - } - }, - "createList": { - "description": "Create a new SharePoint list", - "retryMax": 2, - "timeout": 30, - "parameters": { - "siteUrl": {"type": "string", "required": True}, - "listName": {"type": "string", "required": True}, - "description": {"type": "string", "required": False}, - "template": {"type": "string", "required": False}, - "fields": {"type": "array", "required": False} - } - } - } - - async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: - """Execute SharePoint method""" + Args: + parameters: + query: Search query + siteId: Site ID to search in + contentType: Content type to search for + maxResults: Maximum number of results + """ try: - # Validate parameters - if not await self.validateParameters(action, parameters): - return self._createResult( - success=False, - data={"error": f"Invalid parameters for {action}"} - ) - - # Get UserConnection from auth_data - if not authData or "userConnection" not in authData: - return self._createResult( - success=False, - data={"error": "UserConnection required for SharePoint operations"} - ) - - userConnection: UserConnection = authData["userConnection"] - - # Execute action - if action == "search": - return await self._search_documents(parameters, userConnection) - elif action == "read": - return await self._read_document(parameters, userConnection) - elif action == "write": - return await self._write_document(parameters, userConnection) - elif action == "readList": - return await self._readList(parameters, userConnection) - elif action == "writeList": - return await self._writeList(parameters, userConnection) - elif action == "createList": - return await self._createList(parameters, userConnection) - else: - return self._createResult( - success=False, - data={"error": f"Unknown action: {action}"} - ) - - except Exception as e: - logger.error(f"Error executing SharePoint {action}: {e}") - return self._createResult( - success=False, - data={"error": str(e)} - ) - - async def _search_documents(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Search SharePoint documents""" - try: - siteUrl = parameters["siteUrl"] query = parameters["query"] - listName = parameters.get("listName") + siteId = parameters.get("siteId") + contentType = parameters.get("contentType") maxResults = parameters.get("maxResults", 10) - # Create SharePoint context - ctx = ClientContext(siteUrl).with_credentials( - UserCredential(userConnection.authToken, userConnection.refreshToken) + # Search content + results = await self.sharepointService.searchContent( + query=query, + siteId=siteId, + contentType=contentType, + maxResults=maxResults ) - # Search in specific list or entire site - if listName: - targetList = ctx.web.lists.get_by_title(listName) - items = targetList.items.filter(f"Title eq '{query}'").top(maxResults).get().execute_query() - results = [{ - "title": item.properties["Title"], - "url": item.properties["FileRef"], - "modified": item.properties["Modified"], - "created": item.properties["Created"] - } for item in items] - else: - # Search entire site - search_results = ctx.search(query).execute_query() - results = [{ - "title": result.properties["Title"], - "url": result.properties["Path"], - "modified": result.properties["LastModifiedTime"], - "created": result.properties["Created"] - } for result in search_results[:maxResults]] - return self._createResult( success=True, data={ "query": query, + "siteId": siteId, + "contentType": contentType, "results": results } ) + except Exception as e: - logger.error(f"Error searching SharePoint documents: {e}") + logger.error(f"Error searching SharePoint: {str(e)}") return self._createResult( success=False, - data={"error": f"Search failed: {str(e)}"} + data={"error": str(e)} ) - - async def _read_document(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Read SharePoint document content""" + + @action + async def read(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Read SharePoint item + + Args: + parameters: + itemId: ID of the item to read + siteId: Site ID containing the item + listId: List ID containing the item + """ try: - siteUrl = parameters["siteUrl"] - fileUrl = parameters["fileUrl"] + itemId = parameters["itemId"] + siteId = parameters.get("siteId") + listId = parameters.get("listId") - # Create SharePoint context - ctx = ClientContext(siteUrl).with_credentials( - UserCredential(userConnection.authToken, userConnection.refreshToken) + # Read item + item = await self.sharepointService.readItem( + itemId=itemId, + siteId=siteId, + listId=listId ) - # Get file - file = ctx.web.get_file_by_server_relative_url(fileUrl) - file_content = file.read().execute_query() - return self._createResult( success=True, data={ - "url": fileUrl, - "content": file_content.content.decode('utf-8'), - "modified": file.properties["TimeLastModified"], - "size": file.properties["Length"] + "itemId": itemId, + "siteId": siteId, + "listId": listId, + "item": item } ) + except Exception as e: - logger.error(f"Error reading SharePoint document: {e}") + logger.error(f"Error reading SharePoint item: {str(e)}") return self._createResult( success=False, - data={"error": f"Read failed: {str(e)}"} + data={"error": str(e)} ) - - async def _write_document(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Write content to SharePoint document""" + + @action + async def write(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Write SharePoint item + + Args: + parameters: + siteId: Site ID to write to + listId: List ID to write to + item: Item data to write + """ try: - siteUrl = parameters["siteUrl"] - fileUrl = parameters["fileUrl"] - content = parameters["content"] - contentType = parameters.get("contentType", "text/plain") + siteId = parameters["siteId"] + listId = parameters["listId"] + item = parameters["item"] - # Create SharePoint context - ctx = ClientContext(siteUrl).with_credentials( - UserCredential(userConnection.authToken, userConnection.refreshToken) + # Write item + itemId = await self.sharepointService.writeItem( + siteId=siteId, + listId=listId, + item=item ) - # Get or create file - try: - file = ctx.web.get_file_by_server_relative_url(fileUrl) - except: - # Create new file - folderUrl = "/".join(fileUrl.split("/")[:-1]) - fileName = fileUrl.split("/")[-1] - folder = ctx.web.get_folder_by_server_relative_url(folderUrl) - file = folder.upload_file(fileName, content.encode('utf-8')).execute_query() - - # Update file content - file.write(content.encode('utf-8')).execute_query() - return self._createResult( success=True, data={ - "url": fileUrl, - "modified": datetime.now(UTC).isoformat(), - "size": len(content.encode('utf-8')) + "siteId": siteId, + "listId": listId, + "itemId": itemId } ) + except Exception as e: - logger.error(f"Error writing SharePoint document: {e}") + logger.error(f"Error writing SharePoint item: {str(e)}") return self._createResult( success=False, - data={"error": f"Write failed: {str(e)}"} + data={"error": str(e)} ) - - async def _readList(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Read items from SharePoint list""" + + @action + async def readList(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Read SharePoint list + + Args: + parameters: + listId: ID of the list to read + siteId: Site ID containing the list + query: Query to filter items + maxResults: Maximum number of results + """ try: - siteUrl = parameters["siteUrl"] - listName = parameters["listName"] + listId = parameters["listId"] + siteId = parameters.get("siteId") query = parameters.get("query") - fields = parameters.get("fields", ["*"]) + maxResults = parameters.get("maxResults", 100) - # Create SharePoint account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() + # Read list + items = await self.sharepointService.readList( + listId=listId, + siteId=siteId, + query=query, + maxResults=maxResults ) - # Get site - site = account.get_site(siteUrl) - - # Get list - list = site.get_list(listName) - - # Get items - if query: - items = list.get_items(query=query, fields=fields) - else: - items = list.get_items(fields=fields) - return self._createResult( success=True, data={ - "siteUrl": siteUrl, - "listName": listName, + "listId": listId, + "siteId": siteId, "items": items } ) + except Exception as e: - logger.error(f"Error reading SharePoint list: {e}") + logger.error(f"Error reading SharePoint list: {str(e)}") return self._createResult( success=False, - data={"error": f"Read failed: {str(e)}"} + data={"error": str(e)} ) - - async def _writeList(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Write items to SharePoint list""" + + @action + async def writeList(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Write multiple items to SharePoint list + + Args: + parameters: + siteId: Site ID to write to + listId: List ID to write to + items: List of item data to write + """ try: - siteUrl = parameters["siteUrl"] - listName = parameters["listName"] + siteId = parameters["siteId"] + listId = parameters["listId"] items = parameters["items"] - # Create SharePoint account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() + # Write items + itemIds = await self.sharepointService.writeList( + siteId=siteId, + listId=listId, + items=items ) - # Get site - site = account.get_site(siteUrl) - - # Get list - list = site.get_list(listName) - - # Add items - results = [] - for item in items: - result = list.add_item(item) - results.append({ - "id": result.id, - "status": "success" - }) - return self._createResult( success=True, data={ - "siteUrl": siteUrl, - "listName": listName, - "results": results + "siteId": siteId, + "listId": listId, + "itemIds": itemIds } ) + except Exception as e: - logger.error(f"Error writing to SharePoint list: {e}") + logger.error(f"Error writing SharePoint list: {str(e)}") return self._createResult( success=False, - data={"error": f"Write failed: {str(e)}"} + data={"error": str(e)} ) - - async def _createList(self, parameters: Dict[str, Any], userConnection: UserConnection) -> MethodResult: - """Create a new SharePoint list""" + + @action + async def createList(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Create SharePoint list + + Args: + parameters: + siteId: Site ID to create list in + name: Name of the list + description: List description + template: List template + fields: List field definitions + """ try: - siteUrl = parameters["siteUrl"] - listName = parameters["listName"] + siteId = parameters["siteId"] + name = parameters["name"] description = parameters.get("description") - template = parameters.get("template", "generic") + template = parameters.get("template", "genericList") fields = parameters.get("fields", []) - # Create SharePoint account - account = Account( - credentials=(userConnection.authToken, userConnection.refreshToken), - protocol=MSGraphProtocol() - ) - - # Get site - site = account.get_site(siteUrl) - # Create list - list = site.create_list( - name=listName, + listId = await self.sharepointService.createList( + siteId=siteId, + name=name, description=description, - template=template + template=template, + fields=fields ) - # Add fields - for field in fields: - list.add_field( - name=field["name"], - field_type=field["type"], - required=field.get("required", False), - description=field.get("description") - ) - return self._createResult( success=True, data={ - "siteUrl": siteUrl, - "listName": listName, - "id": list.id, - "webUrl": list.web_url + "siteId": siteId, + "listId": listId, + "name": name } ) + except Exception as e: - logger.error(f"Error creating SharePoint list: {e}") + logger.error(f"Error creating SharePoint list: {str(e)}") return self._createResult( success=False, - data={"error": f"Create failed: {str(e)}"} + data={"error": str(e)} ) \ No newline at end of file diff --git a/modules/methods/methodWeb.py b/modules/methods/methodWeb.py index 9398741c..13e4b2e8 100644 --- a/modules/methods/methodWeb.py +++ b/modules/methods/methodWeb.py @@ -1,468 +1,177 @@ -from typing import Dict, Any, Optional -import logging -import aiohttp -import asyncio -from bs4 import BeautifulSoup -from urllib.parse import urljoin, urlparse -import re -from datetime import datetime, UTC -import requests -import time -import json +""" +Web method module. +Handles web operations using the web service. +""" -from modules.methods.methodBase import MethodBase, MethodResult -from modules.shared.configuration import APP_CONFIG +import logging +from typing import Dict, Any, List, Optional +from datetime import datetime + +from modules.interfaces.interfaceWeb import WebService +from modules.methods.methodBase import MethodBase, MethodResult, action logger = logging.getLogger(__name__) class MethodWeb(MethodBase): - """Web method implementation for web operations""" + """Web method implementation""" - def __init__(self): - super().__init__() - self.name = "web" - self.description = "Handle web operations like search, crawl, and content extraction" + def __init__(self, serviceContainer): + """Initialize the web method""" + super().__init__(serviceContainer) + self.webService = WebService(serviceContainer) + + @action + async def search(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Search web content - # Web crawling configuration from agentWebcrawler - self.srcApikey = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_APIKEY", "") - self.srcEngine = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_ENGINE", "google") - self.srcCountry = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_COUNTRY", "auto") - self.maxResults = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_MAX_SEARCH_RESULTS", "5")) - self.timeout = int(APP_CONFIG.get("Agent_Webcrawler_SERPAPI_TIMEOUT", "30")) - self.userAgent = APP_CONFIG.get("Agent_Webcrawler_SERPAPI_USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - - if not self.srcApikey: - logger.error("SerpAPI key not configured") - - @property - def actions(self) -> Dict[str, Dict[str, Any]]: - """Available actions and their parameters""" - return { - "search": { - "description": "Search web content", - "retryMax": 3, - "timeout": 30, - "parameters": { - "query": {"type": "string", "required": True}, - "maxResults": {"type": "number", "required": False}, - "filters": {"type": "object", "required": False}, - "searchEngine": {"type": "string", "required": False} - } - }, - "crawl": { - "description": "Crawl web pages", - "retryMax": 2, - "timeout": 60, - "parameters": { - "url": {"type": "string", "required": True}, - "depth": {"type": "number", "required": False}, - "followLinks": {"type": "boolean", "required": False}, - "includeImages": {"type": "boolean", "required": False}, - "respectRobots": {"type": "boolean", "required": False} - } - }, - "extract": { - "description": "Extract content from web page", - "retryMax": 2, - "timeout": 30, - "parameters": { - "url": {"type": "string", "required": True}, - "selectors": {"type": "array", "items": "string", "required": False}, - "format": {"type": "string", "required": False}, - "includeMetadata": {"type": "boolean", "required": False} - } - } - } - - async def execute(self, action: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: - """Execute web method""" - try: - # Validate parameters - if not await self.validateParameters(action, parameters): - return self._createResult( - success=False, - data={"error": f"Invalid parameters for {action}"} - ) - - # Execute action - if action == "fetchUrl": - return await self._fetchUrl(parameters) - elif action == "parseContent": - return await self._parseContent(parameters) - elif action == "extractData": - return await self._extractData(parameters) - else: - return self._createResult( - success=False, - data={"error": f"Unknown action: {action}"} - ) - - except Exception as e: - logger.error(f"Error executing web {action}: {e}") - return self._createResult( - success=False, - data={"error": str(e)} - ) - - async def _fetchUrl(self, parameters: Dict[str, Any]) -> MethodResult: - """Fetch content from URL""" - try: - url = parameters["url"] - method = parameters.get("method", "GET") - headers = parameters.get("headers", {}) - data = parameters.get("data") - timeout = parameters.get("timeout", 30) - - async with aiohttp.ClientSession() as session: - async with session.request( - method=method, - url=url, - headers=headers, - data=data, - timeout=timeout - ) as response: - content = await response.text() - return self._createResult( - success=True, - data={ - "url": url, - "status": response.status, - "headers": dict(response.headers), - "content": content - } - ) - except Exception as e: - logger.error(f"Error fetching URL: {e}") - return self._createResult( - success=False, - data={"error": f"Fetch failed: {str(e)}"} - ) - - async def _parseContent(self, parameters: Dict[str, Any]) -> MethodResult: - """Parse web content""" - try: - content = parameters["content"] - contentType = parameters.get("contentType", "html") - - if contentType == "html": - soup = BeautifulSoup(content, "html.parser") - return self._createResult( - success=True, - data={ - "type": "html", - "title": soup.title.string if soup.title else None, - "text": soup.get_text(), - "links": [a.get("href") for a in soup.find_all("a", href=True)], - "images": [img.get("src") for img in soup.find_all("img", src=True)] - } - ) - elif contentType == "json": - data = json.loads(content) - return self._createResult( - success=True, - data={ - "type": "json", - "data": data - } - ) - else: - raise ValueError(f"Unsupported content type: {contentType}") - except Exception as e: - logger.error(f"Error parsing content: {e}") - return self._createResult( - success=False, - data={"error": f"Parse failed: {str(e)}"} - ) - - async def _extractData(self, parameters: Dict[str, Any]) -> MethodResult: - """Extract data from web content""" - try: - content = parameters["content"] - contentType = parameters.get("contentType", "html") - selectors = parameters["selectors"] - - if contentType == "html": - soup = BeautifulSoup(content, "html.parser") - results = {} - - for key, selector in selectors.items(): - elements = soup.select(selector) - if len(elements) == 1: - results[key] = elements[0].get_text().strip() - else: - results[key] = [el.get_text().strip() for el in elements] - - return self._createResult( - success=True, - data={ - "type": "html", - "results": results - } - ) - elif contentType == "json": - data = json.loads(content) - results = {} - - for key, path in selectors.items(): - value = data - for part in path.split("."): - if isinstance(value, dict): - value = value.get(part) - elif isinstance(value, list) and part.isdigit(): - value = value[int(part)] - else: - value = None - break - results[key] = value - - return self._createResult( - success=True, - data={ - "type": "json", - "results": results - } - ) - else: - raise ValueError(f"Unsupported content type: {contentType}") - except Exception as e: - logger.error(f"Error extracting data: {e}") - return self._createResult( - success=False, - data={"error": f"Extract failed: {str(e)}"} - ) - - async def _search_web(self, parameters: Dict[str, Any]) -> MethodResult: - """Search web content""" + Args: + parameters: + query: Search query + engine: Search engine to use (google, bing) + maxResults: Maximum number of results + """ try: query = parameters["query"] + engine = parameters.get("engine", "google") maxResults = parameters.get("maxResults", 10) - filters = parameters.get("filters", {}) - searchEngine = parameters.get("searchEngine", "google") - # Implement search using different engines - if searchEngine.lower() == "google": - # Use Google Custom Search API - # TODO: Implement Google Custom Search API integration - results = await self._google_search(query, maxResults, filters) - elif searchEngine.lower() == "bing": - # Use Bing Web Search API - # TODO: Implement Bing Web Search API integration - results = await self._bing_search(query, maxResults, filters) - else: - return self._createResult( - success=False, - data={"error": f"Unsupported search engine: {searchEngine}"} - ) + # Search web + results = await self.webService.searchContent( + query=query, + engine=engine, + maxResults=maxResults + ) return self._createResult( success=True, data={ "query": query, - "engine": searchEngine, + "engine": engine, "results": results } ) + except Exception as e: - logger.error(f"Error searching web: {e}") + logger.error(f"Error searching web: {str(e)}") return self._createResult( success=False, - data={"error": f"Search failed: {str(e)}"} + data={"error": str(e)} ) - - async def _google_search(self, query: str, max_results: int, filters: Dict[str, Any]) -> list: - """Search using Google Custom Search API""" - # TODO: Implement Google Custom Search API - # This is a placeholder implementation - return [ - { - "title": "Example Result", - "url": "https://example.com", - "snippet": "Example search result snippet", - "source": "google" - } - ] - - async def _bing_search(self, query: str, max_results: int, filters: Dict[str, Any]) -> list: - """Search using Bing Web Search API""" - # TODO: Implement Bing Web Search API - # This is a placeholder implementation - return [ - { - "title": "Example Result", - "url": "https://example.com", - "snippet": "Example search result snippet", - "source": "bing" - } - ] - - async def _crawl_page(self, parameters: Dict[str, Any]) -> MethodResult: - """Crawl web pages""" + + @action + async def crawl(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Crawl web page + + Args: + parameters: + url: URL to crawl + depth: Crawl depth + followLinks: Whether to follow links + extractContent: Whether to extract content + """ try: url = parameters["url"] depth = parameters.get("depth", 1) followLinks = parameters.get("followLinks", False) - includeImages = parameters.get("includeImages", False) - respectRobots = parameters.get("respectRobots", True) + extractContent = parameters.get("extractContent", True) - # Check robots.txt if required - if respectRobots: - if not await self._check_robots_txt(url): - return self._createResult( - success=False, - data={"error": "Crawling not allowed by robots.txt"} - ) + # Crawl page + results = await self.webService.crawlPage( + url=url, + depth=depth, + followLinks=followLinks, + extractContent=extractContent + ) + + return self._createResult( + success=True, + data={ + "url": url, + "depth": depth, + "results": results + } + ) - # Crawl the page - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - if response.status == 200: - html = await response.text() - soup = BeautifulSoup(html, 'html.parser') - - # Extract basic information - result = { - "url": url, - "title": soup.title.string if soup.title else None, - "description": self._get_meta_description(soup), - "links": [], - "images": [] if includeImages else None, - "text": soup.get_text(strip=True), - "crawled": datetime.now(UTC).isoformat() - } - - # Extract links if followLinks is True - if followLinks: - baseUrl = url - for link in soup.find_all('a'): - href = link.get('href') - if href: - absoluteUrl = urljoin(baseUrl, href) - if self._is_valid_url(absoluteUrl): - result["links"].append({ - "url": absoluteUrl, - "text": link.get_text(strip=True) - }) - - # Extract images if includeImages is True - if includeImages: - for img in soup.find_all('img'): - src = img.get('src') - if src: - absoluteSrc = urljoin(url, src) - result["images"].append({ - "url": absoluteSrc, - "alt": img.get('alt', ''), - "title": img.get('title', '') - }) - - return self._createResult( - success=True, - data=result - ) - else: - return self._createResult( - success=False, - data={"error": f"Failed to fetch URL: {response.status}"} - ) except Exception as e: - logger.error(f"Error crawling page: {e}") + logger.error(f"Error crawling web page: {str(e)}") return self._createResult( success=False, - data={"error": f"Crawl failed: {str(e)}"} + data={"error": str(e)} ) - - def _get_meta_description(self, soup: BeautifulSoup) -> Optional[str]: - """Extract meta description from HTML""" - metaDesc = soup.find('meta', attrs={'name': 'description'}) - if metaDesc: - return metaDesc.get('content') - return None - - def _is_valid_url(self, url: str) -> bool: - """Check if URL is valid""" + + @action + async def extract(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Extract content from web page + + Args: + parameters: + url: URL to extract from + selectors: CSS selectors to extract + format: Output format (text, html, json) + """ try: - result = urlparse(url) - return all([result.scheme, result.netloc]) - except: - return False - - async def _check_robots_txt(self, url: str) -> bool: - """Check if URL is allowed by robots.txt""" - try: - parsedUrl = urlparse(url) - robotsUrl = f"{parsedUrl.scheme}://{parsedUrl.netloc}/robots.txt" + url = parameters["url"] + selectors = parameters.get("selectors", ["body"]) + format = parameters.get("format", "text") - async with aiohttp.ClientSession() as session: - async with session.get(robotsUrl, headers={"User-Agent": self.userAgent}, timeout=self.timeout) as response: - if response.status == 200: - robotsContent = await response.text() - - # Parse robots.txt content - userAgent = "*" # Default to all user agents - disallowPaths = [] - - for line in robotsContent.splitlines(): - line = line.strip().lower() - if line.startswith("user-agent:"): - userAgent = line[11:].strip() - elif line.startswith("disallow:") and userAgent in ["*", self.userAgent.lower()]: - path = line[9:].strip() - if path: - disallowPaths.append(path) - - # Check if URL path is disallowed - urlPath = parsedUrl.path - for disallowPath in disallowPaths: - if urlPath.startswith(disallowPath): - return False - - return True - else: - # If robots.txt doesn't exist, assume crawling is allowed - return True - - except Exception as e: - logger.warning(f"Error checking robots.txt for {url}: {str(e)}") - # If there's an error, assume crawling is allowed - return True - - def _detect_language(self, soup: BeautifulSoup) -> str: - """Detect page language""" - try: - # Try to get language from HTML lang attribute - if soup.html and soup.html.get('lang'): - return soup.html.get('lang') + # Extract content + content = await self.webService.extractContent( + url=url, + selectors=selectors, + format=format + ) - # Try to get language from meta tag - metaLang = soup.find('meta', attrs={'http-equiv': 'content-language'}) - if metaLang: - return metaLang.get('content', 'en') - - # Try to get language from meta charset - metaCharset = soup.find('meta', attrs={'charset': True}) - if metaCharset: - charset = metaCharset.get('charset', '').lower() - if 'utf-8' in charset: - return 'en' # Default to English for UTF-8 - - # Try to detect language from content - # This is a simple heuristic based on common words - text = soup.get_text().lower() - commonWords = { - 'en': ['the', 'and', 'of', 'to', 'in', 'is', 'that', 'for', 'it', 'with'], - 'es': ['el', 'la', 'los', 'las', 'de', 'y', 'en', 'que', 'por', 'con'], - 'fr': ['le', 'la', 'les', 'de', 'et', 'en', 'que', 'pour', 'avec', 'dans'], - 'de': ['der', 'die', 'das', 'und', 'in', 'den', 'von', 'zu', 'für', 'mit'] - } - - wordCounts = {lang: sum(1 for word in words if f' {word} ' in f' {text} ') - for lang, words in commonWords.items()} - - if wordCounts: - return max(wordCounts.items(), key=lambda x: x[1])[0] - - return 'en' # Default to English if no language detected + return self._createResult( + success=True, + data={ + "url": url, + "format": format, + "content": content + } + ) except Exception as e: - logger.warning(f"Error detecting language: {str(e)}") - return 'en' # Default to English on error \ No newline at end of file + logger.error(f"Error extracting web content: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) + + @action + async def validate(self, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> MethodResult: + """ + Validate web page + + Args: + parameters: + url: URL to validate + checks: List of checks to perform + """ + try: + url = parameters["url"] + checks = parameters.get("checks", ["accessibility", "seo", "performance"]) + + # Validate page + results = await self.webService.validatePage( + url=url, + checks=checks + ) + + return self._createResult( + success=True, + data={ + "url": url, + "checks": checks, + "results": results + } + ) + + except Exception as e: + logger.error(f"Error validating web page: {str(e)}") + return self._createResult( + success=False, + data={"error": str(e)} + ) \ No newline at end of file diff --git a/modules/routes/routeAdmin.py b/modules/routes/routeAdmin.py index 1e3156d7..f895521c 100644 --- a/modules/routes/routeAdmin.py +++ b/modules/routes/routeAdmin.py @@ -9,7 +9,7 @@ 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 +from modules.interfaces.interfaceAppModel import User router = APIRouter( prefix="", diff --git a/modules/routes/routeAttributes.py b/modules/routes/routeAttributes.py index 0f9fec0f..45a759e1 100644 --- a/modules/routes/routeAttributes.py +++ b/modules/routes/routeAttributes.py @@ -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 User +from modules.interfaces.interfaceAppModel import User from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition # Configure logger diff --git a/modules/routes/routeDataConnections.py b/modules/routes/routeDataConnections.py index 0ac46005..d7831d44 100644 --- a/modules/routes/routeDataConnections.py +++ b/modules/routes/routeDataConnections.py @@ -10,9 +10,9 @@ from datetime import datetime import logging import json -from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus +from modules.interfaces.interfaceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus from modules.security.auth import getCurrentUser, limiter -from modules.interfaces.serviceAppClass import getInterface, getRootInterface +from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface # Configure logger logger = logging.getLogger(__name__) diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py index 7860fec3..26f63bd0 100644 --- a/modules/routes/routeDataFiles.py +++ b/modules/routes/routeDataFiles.py @@ -14,10 +14,10 @@ from pydantic import BaseModel from modules.security.auth import limiter, getCurrentUser # Import interfaces -import modules.interfaces.serviceManagementClass as serviceManagementClass -from modules.interfaces.serviceManagementModel import FileItem, FilePreview +import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects +from modules.interfaces.interfaceComponentModel import FileItem, FilePreview from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition -from modules.interfaces.serviceAppModel import User +from modules.interfaces.interfaceAppModel import User # Configure logger logger = logging.getLogger(__name__) @@ -46,7 +46,7 @@ async def get_files( ) -> List[FileItem]: """Get all files""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get all files generically - only metadata, no binary data files = managementInterface.getAllFiles() @@ -70,17 +70,17 @@ async def upload_file( ) -> JSONResponse: """Upload a file""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Read file fileContent = await file.read() # Check size limits - maxSize = int(serviceManagementClass.APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in bytes + maxSize = int(interfaceComponentObjects.APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in bytes if len(fileContent) > maxSize: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, - detail=f"File too large. Maximum size: {serviceManagementClass.APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" + detail=f"File too large. Maximum size: {interfaceComponentObjects.APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" ) # Save file via LucyDOM interface in the database @@ -101,7 +101,7 @@ async def upload_file( "file": fileMeta }) - except serviceManagementClass.FileStorageError as e: + except interfaceComponentObjects.FileStorageError as e: logger.error(f"Error during file upload (storage): {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -123,7 +123,7 @@ async def get_file( ) -> FileItem: """Get a file""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get file via LucyDOM interface from the database fileData = managementInterface.getFile(fileId) @@ -135,19 +135,19 @@ async def get_file( return FileItem(**fileData) - except serviceManagementClass.FileNotFoundError as e: + except interfaceComponentObjects.FileNotFoundError as e: logger.warning(f"File not found: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) - except serviceManagementClass.FilePermissionError as e: + except interfaceComponentObjects.FilePermissionError as e: logger.warning(f"No permission for file: {str(e)}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e) ) - except serviceManagementClass.FileError as e: + except interfaceComponentObjects.FileError as e: logger.error(f"Error retrieving file: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -170,7 +170,7 @@ async def update_file( ) -> FileItem: """Update file info""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get the file from the database file = managementInterface.getFile(fileId) @@ -216,7 +216,7 @@ async def delete_file( currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a file""" - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Check if the file exists existingFile = managementInterface.getFile(fileId) @@ -243,7 +243,7 @@ async def get_file_stats( ) -> Dict[str, Any]: """Returns statistics about the stored files""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get all files - metadata only allFiles = managementInterface.getAllFiles() @@ -282,7 +282,7 @@ async def download_file( ) -> Response: """Download a file""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get file data fileData = managementInterface.getFile(fileId) @@ -326,7 +326,7 @@ async def preview_file( ) -> FilePreview: """Preview a file's content""" try: - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get file preview preview = managementInterface.getFilePreview(fileId) diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py index c2aebea2..a6be8e8d 100644 --- a/modules/routes/routeDataMandates.py +++ b/modules/routes/routeDataMandates.py @@ -17,11 +17,11 @@ from pydantic import BaseModel from modules.security.auth import limiter, getCurrentUser # Import interfaces -import modules.interfaces.serviceAppClass as serviceAppClass +import modules.interfaces.interfaceAppObjects as interfaceAppObjects from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition # Import the model classes -from modules.interfaces.serviceAppModel import Mandate, User +from modules.interfaces.interfaceAppModel import Mandate, User # Configure logger logger = logging.getLogger(__name__) @@ -44,7 +44,7 @@ async def get_mandates( ) -> List[Mandate]: """Get all mandates""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) mandates = appInterface.getAllMandates() return mandates except Exception as e: @@ -63,7 +63,7 @@ async def get_mandate( ) -> Mandate: """Get a specific mandate by ID""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) mandate = appInterface.getMandate(mandateId) if not mandate: @@ -91,7 +91,7 @@ async def create_mandate( ) -> Mandate: """Create a new mandate""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Create mandate newMandate = appInterface.createMandate( @@ -125,7 +125,7 @@ async def update_mandate( ) -> Mandate: """Update an existing mandate""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Check if mandate exists existingMandate = appInterface.getMandate(mandateId) @@ -163,7 +163,7 @@ async def delete_mandate( ) -> Dict[str, Any]: """Delete a mandate""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Check if mandate exists existingMandate = appInterface.getMandate(mandateId) diff --git a/modules/routes/routeDataPrompts.py b/modules/routes/routeDataPrompts.py index 6f54fc9c..b4809151 100644 --- a/modules/routes/routeDataPrompts.py +++ b/modules/routes/routeDataPrompts.py @@ -12,10 +12,10 @@ from pydantic import BaseModel from modules.security.auth import limiter, getCurrentUser # Import interfaces -import modules.interfaces.serviceManagementClass as serviceManagementClass -from modules.interfaces.serviceManagementModel import Prompt +import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects +from modules.interfaces.interfaceComponentModel import Prompt from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition -from modules.interfaces.serviceAppModel import User +from modules.interfaces.interfaceAppModel import User # Configure logger logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ async def get_prompts( currentUser: User = Depends(getCurrentUser) ) -> List[Prompt]: """Get all prompts""" - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) prompts = managementInterface.getAllPrompts() return prompts @@ -46,7 +46,7 @@ async def create_prompt( currentUser: User = Depends(getCurrentUser) ) -> Prompt: """Create a new prompt""" - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Convert Prompt to dict for interface prompt_data = prompt.dict() @@ -64,7 +64,7 @@ async def get_prompt( currentUser: User = Depends(getCurrentUser) ) -> Prompt: """Get a specific prompt""" - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Get prompt prompt = managementInterface.getPrompt(promptId) @@ -85,7 +85,7 @@ async def update_prompt( currentUser: User = Depends(getCurrentUser) ) -> Prompt: """Update an existing prompt""" - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Check if the prompt exists existingPrompt = managementInterface.getPrompt(promptId) @@ -117,7 +117,7 @@ async def delete_prompt( currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a prompt""" - managementInterface = serviceManagementClass.getInterface(currentUser) + managementInterface = interfaceComponentObjects.getInterface(currentUser) # Check if the prompt exists existingPrompt = managementInterface.getPrompt(promptId) diff --git a/modules/routes/routeDataUsers.py b/modules/routes/routeDataUsers.py index bcaeead4..9d8be813 100644 --- a/modules/routes/routeDataUsers.py +++ b/modules/routes/routeDataUsers.py @@ -14,11 +14,11 @@ import os from pydantic import BaseModel # Import interfaces and models -import modules.interfaces.serviceAppClass as serviceAppClass +import modules.interfaces.interfaceAppObjects as interfaceAppObjects from modules.security.auth import getCurrentUser, limiter, getCurrentUser # Import the attribute definition and helper functions -from modules.interfaces.serviceAppModel import User, AttributeDefinition +from modules.interfaces.interfaceAppModel import User, AttributeDefinition from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse # Configure logger @@ -39,7 +39,7 @@ async def get_users( ) -> List[User]: """Get all users in the current mandate""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # If mandateId is provided, use it, otherwise use the current user's mandate targetMandateId = mandateId or currentUser.mandateId # Get all users without filtering by enabled status @@ -61,7 +61,7 @@ async def get_user( ) -> User: """Get a specific user by ID""" try: - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Get user without filtering by enabled status user = appInterface.getUser(userId) @@ -89,7 +89,7 @@ async def create_user( currentUser: User = Depends(getCurrentUser) ) -> User: """Create a new user""" - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Convert User to dict for interface user_dict = user_data.dict() @@ -108,7 +108,7 @@ async def update_user( currentUser: User = Depends(getCurrentUser) ) -> User: """Update an existing user""" - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Check if the user exists existingUser = appInterface.getUser(userId) @@ -140,7 +140,7 @@ async def delete_user( currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a user""" - appInterface = serviceAppClass.getInterface(currentUser) + appInterface = interfaceAppObjects.getInterface(currentUser) # Check if the user exists existingUser = appInterface.getUser(userId) diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py index 944b2fa7..d4f69108 100644 --- a/modules/routes/routeSecurityGoogle.py +++ b/modules/routes/routeSecurityGoogle.py @@ -14,8 +14,8 @@ from google.auth.transport.requests import Request as GoogleRequest from googleapiclient.discovery import build from modules.shared.configuration import APP_CONFIG -from modules.interfaces.serviceAppClass import getInterface, getRootInterface -from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection +from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface +from modules.interfaces.interfaceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection from modules.security.auth import getCurrentUser, limiter from modules.shared.attributeUtils import ModelMixin diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py index 441975b8..4d53d216 100644 --- a/modules/routes/routeSecurityLocal.py +++ b/modules/routes/routeSecurityLocal.py @@ -12,8 +12,8 @@ from pydantic import BaseModel # Import auth modules from modules.security.auth import createAccessToken, getCurrentUser, limiter -from modules.interfaces.serviceAppClass import getInterface, getRootInterface -from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token +from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface +from modules.interfaces.interfaceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token from modules.shared.attributeUtils import ModelMixin # Configure logger diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py index 8b54c580..59a2898e 100644 --- a/modules/routes/routeSecurityMsft.py +++ b/modules/routes/routeSecurityMsft.py @@ -12,8 +12,8 @@ import msal import httpx from modules.shared.configuration import APP_CONFIG -from modules.interfaces.serviceAppClass import getInterface, getRootInterface -from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection +from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface +from modules.interfaces.interfaceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection from modules.security.auth import getCurrentUser, limiter, createAccessToken from modules.shared.attributeUtils import ModelMixin diff --git a/modules/routes/routeWorkflows.py b/modules/routes/routeWorkflows.py index a96ca2b4..a5258f9d 100644 --- a/modules/routes/routeWorkflows.py +++ b/modules/routes/routeWorkflows.py @@ -15,11 +15,11 @@ from datetime import datetime, timedelta from modules.security.auth import limiter, getCurrentUser # Import interfaces -import modules.interfaces.serviceChatClass as serviceChatClass -from modules.interfaces.serviceChatClass import getInterface +import modules.interfaces.interfaceChatObjects as interfaceChatObjects +from modules.interfaces.interfaceChatObjects import getInterface # Import models -from modules.interfaces.serviceChatModel import ( +from modules.interfaces.interfaceChatModel import ( ChatWorkflow, ChatMessage, ChatLog, @@ -28,7 +28,7 @@ from modules.interfaces.serviceChatModel import ( UserInputRequest ) from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse -from modules.interfaces.serviceAppModel import User +from modules.interfaces.interfaceAppModel import User # Configure logger logger = logging.getLogger(__name__) @@ -44,7 +44,7 @@ router = APIRouter( ) def getServiceChat(currentUser: User): - return serviceChatClass.getInterface(currentUser) + return interfaceChatObjects.getInterface(currentUser) # Consolidated endpoint for getting all workflows @router.get("/", response_model=List[ChatWorkflow]) @@ -104,10 +104,10 @@ async def get_workflow_status( """Get the current status of a workflow.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) # Retrieve workflow - workflow = serviceChat.getWorkflow(workflowId) + workflow = interfaceChat.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -136,10 +136,10 @@ async def get_workflow_logs( """Get logs for a workflow with support for selective data transfer.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) # Verify workflow exists - workflow = serviceChat.getWorkflow(workflowId) + workflow = interfaceChat.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -147,7 +147,7 @@ async def get_workflow_logs( ) # Get all logs - allLogs = serviceChat.getWorkflowLogs(workflowId) + allLogs = interfaceChat.getWorkflowLogs(workflowId) # Apply selective data transfer if logId is provided if logId: @@ -179,10 +179,10 @@ async def get_workflow_messages( """Get messages for a workflow with support for selective data transfer.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) # Verify workflow exists - workflow = serviceChat.getWorkflow(workflowId) + workflow = interfaceChat.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -190,7 +190,7 @@ async def get_workflow_messages( ) # Get all messages - allMessages = serviceChat.getWorkflowMessages(workflowId) + allMessages = interfaceChat.getWorkflowMessages(workflowId) # Apply selective data transfer if messageId is provided if messageId: @@ -225,10 +225,10 @@ async def start_workflow( """ try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) - # Start or continue workflow using ChatInterface - workflow = await serviceChat.workflowStart(currentUser, userInput, workflowId) + # Start or continue workflow using ChatObjects + workflow = await interfaceChat.workflowStart(currentUser, userInput, workflowId) return ChatWorkflow(**workflow) @@ -250,10 +250,10 @@ async def stop_workflow( """Stops a running workflow.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) - # Stop workflow using ChatInterface - workflow = await serviceChat.workflowStop(workflowId) + # Stop workflow using ChatObjects + workflow = await interfaceChat.workflowStop(workflowId) return ChatWorkflow(**workflow) @@ -275,10 +275,10 @@ async def delete_workflow( """Deletes a workflow and its associated data.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) # Verify workflow exists - workflow = serviceChat.getWorkflow(workflowId) + workflow = interfaceChat.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -293,7 +293,7 @@ async def delete_workflow( ) # Delete workflow - success = serviceChat.deleteWorkflow(workflowId) + success = interfaceChat.deleteWorkflow(workflowId) if not success: raise HTTPException( @@ -328,10 +328,10 @@ async def delete_workflow_message( """Delete a message from a workflow.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) # Verify workflow exists - workflow = serviceChat.getWorkflow(workflowId) + workflow = interfaceChat.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -339,7 +339,7 @@ async def delete_workflow_message( ) # Delete the message - success = serviceChat.deleteWorkflowMessage(workflowId, messageId) + success = interfaceChat.deleteWorkflowMessage(workflowId, messageId) if not success: raise HTTPException( @@ -351,7 +351,7 @@ async def delete_workflow_message( messageIds = workflow.get("messageIds", []) if messageId in messageIds: messageIds.remove(messageId) - serviceChat.updateWorkflow(workflowId, {"messageIds": messageIds}) + interfaceChat.updateWorkflow(workflowId, {"messageIds": messageIds}) return { "workflowId": workflowId, @@ -379,10 +379,10 @@ async def delete_file_from_message( """Delete a file reference from a message in a workflow.""" try: # Get service container - serviceChat = getServiceChat(currentUser) + interfaceChat = getServiceChat(currentUser) # Verify workflow exists - workflow = serviceChat.getWorkflow(workflowId) + workflow = interfaceChat.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -390,7 +390,7 @@ async def delete_file_from_message( ) # Delete file reference from message - success = serviceChat.deleteFileFromMessage(workflowId, messageId, fileId) + success = interfaceChat.deleteFileFromMessage(workflowId, messageId, fileId) if not success: raise HTTPException( diff --git a/modules/security/auth.py b/modules/security/auth.py index 6eeb7bde..4eb405f0 100644 --- a/modules/security/auth.py +++ b/modules/security/auth.py @@ -13,8 +13,8 @@ from slowapi import Limiter 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, User +from modules.interfaces.interfaceAppObjects import getRootInterface +from modules.interfaces.interfaceAppModel import Session, AuthEvent, UserPrivilege, User # Get Config Data SECRET_KEY = APP_CONFIG.get("APP_JWT_SECRET_SECRET") diff --git a/modules/workflow/managerChat.py b/modules/workflow/managerChat.py index a605bc91..ed91dade 100644 --- a/modules/workflow/managerChat.py +++ b/modules/workflow/managerChat.py @@ -5,8 +5,8 @@ import json import uuid import time -from modules.interfaces.serviceAppModel import User -from modules.interfaces.serviceChatModel import ( +from modules.interfaces.interfaceAppModel import User +from modules.interfaces.interfaceChatModel import ( TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult, ChatStat, ChatLog, ChatMessage, ChatWorkflow ) from modules.workflow.serviceContainer import ServiceContainer @@ -19,6 +19,7 @@ class ChatManager: def __init__(self, currentUser: User): self.currentUser = currentUser self.service: ServiceContainer = None + self.workflow: ChatWorkflow = None # ===== Initialization and Setup ===== async def initialize(self, workflow: ChatWorkflow) -> None: @@ -30,30 +31,62 @@ class ChatManager: async def createInitialTask(self, workflow: ChatWorkflow, initialMessage: ChatMessage) -> Optional[TaskItem]: """Create the initial task from the first message""" try: - # Get available methods and their actions - methodCatalog = self.service.getMethodsCatalog() + # Create task definition prompt + prompt = await self._createTaskDefinitionPrompt(initialMessage.message, workflow) - # Process user input with AI - processedInput = await self._processUserInput(initialMessage.message, methodCatalog) + # Get AI response + response = await self.service.callAiTextAdvanced(prompt) - # Create actions from processed input - actions = await self._createActions(processedInput['actions']) + # Parse response + try: + taskDef = json.loads(response) + except json.JSONDecodeError: + logger.error(f"Invalid JSON in task definition: {response}") + return None - # Create task data - taskData = { - "workflowId": workflow.id, - "userInput": processedInput['objective'], - "dataList": initialMessage.documents, - "actionList": [action.dict() for action in actions], - "status": TaskStatus.PENDING, - "startedAt": datetime.now(UTC).isoformat(), - "updatedAt": datetime.now(UTC).isoformat() - } + # Validate task definition + if not isinstance(taskDef, dict): + logger.error("Task definition must be a JSON object") + return None + + requiredFields = ["status", "feedback", "actions"] + for field in requiredFields: + if field not in taskDef: + logger.error(f"Missing required field: {field}") + return None + + if not isinstance(taskDef["actions"], list): + logger.error("Actions must be a list") + return None + + # Create task + task = TaskItem( + id=str(uuid.uuid4()), + workflow=workflow, + userInput=initialMessage.message, + status=taskDef["status"], + feedback=taskDef["feedback"], + actions=[] + ) + + # Add actions + for actionDef in taskDef["actions"]: + if not isinstance(actionDef, dict): + continue + + requiredFields = ["method", "action", "parameters"] + if not all(field in actionDef for field in requiredFields): + continue + + action = TaskAction( + id=str(uuid.uuid4()), + method=actionDef["method"], + action=actionDef["action"], + parameters=actionDef["parameters"], + resultLabel=actionDef.get("resultLabel") + ) + task.actions.append(action) - # Create task using ChatInterface - task = self.service.createTask(taskData) - if task: - self.service.currentTask = task return task except Exception as e: @@ -68,369 +101,153 @@ class ChatManager: logger.error(f"Previous task failed: {previousResult.error}") return None - # Extract task data from previous result - taskData = previousResult.data - if not taskData: - logger.error("No task data in previous result") - return None - - # Create task data - taskData = { - "workflowId": workflow.id, - "userInput": taskData.get('objective', ''), - "actionList": [action.dict() for action in await self._createActions(taskData.get('actions', []))], - "status": TaskStatus.PENDING, - "startedAt": datetime.now(UTC).isoformat(), - "updatedAt": datetime.now(UTC).isoformat() - } + # Create task definition prompt + prompt = await self._createTaskDefinitionPrompt(previousResult.feedback, workflow) + + # Get AI response + response = await self.service.callAiTextAdvanced(prompt) + + # Parse response + try: + taskDef = json.loads(response) + except json.JSONDecodeError: + logger.error(f"Invalid JSON in task definition: {response}") + return None + + # Validate task definition + if not isinstance(taskDef, dict): + logger.error("Task definition must be a JSON object") + return None + + requiredFields = ["status", "feedback", "actions"] + for field in requiredFields: + if field not in taskDef: + logger.error(f"Missing required field: {field}") + return None + + if not isinstance(taskDef["actions"], list): + logger.error("Actions must be a list") + return None + + # Create task + task = TaskItem( + id=str(uuid.uuid4()), + workflow=workflow, + userInput=previousResult.feedback, + status=taskDef["status"], + feedback=taskDef["feedback"], + actions=[] + ) + + # Add actions + for actionDef in taskDef["actions"]: + if not isinstance(actionDef, dict): + continue + + requiredFields = ["method", "action", "parameters"] + if not all(field in actionDef for field in requiredFields): + continue + + action = TaskAction( + id=str(uuid.uuid4()), + method=actionDef["method"], + action=actionDef["action"], + parameters=actionDef["parameters"], + resultLabel=actionDef.get("resultLabel") + ) + task.actions.append(action) - # Create task using ChatInterface - task = self.service.createTask(taskData) - if task: - self.service.currentTask = task return task except Exception as e: logger.error(f"Error creating next task: {str(e)}") return None - async def identifyNextTask(self, workflow: ChatWorkflow) -> TaskResult: - """Identify the next task based on workflow state""" - try: - # Get workflow summary - summary = await self._summarizeWorkflow() - - # Generate prompt for next task - prompt = f"""Based on the workflow summary: - {summary} - - Determine what the next task should be. - Return a JSON object with: - - objective: The main goal or task to accomplish - - actions: List of required actions with method and parameters - """ - - # Get AI response - response = await self.service.callAiBasic(prompt) - - # Parse response - try: - result = json.loads(response) - return TaskResult( - taskId=f"analysis_{datetime.now(UTC).timestamp()}", - status=TaskStatus.COMPLETED, - success=True, - timestamp=datetime.now(UTC), - data=result, - documentsLabel="Task Results" - ) - except json.JSONDecodeError as e: - logger.error(f"Error parsing AI response: {str(e)}") - return TaskResult( - taskId=f"analysis_{datetime.now(UTC).timestamp()}", - status=TaskStatus.FAILED, - success=False, - timestamp=datetime.now(UTC), - error=f"Error parsing AI response: {str(e)}", - documentsLabel="Task Error" - ) - - except Exception as e: - logger.error(f"Error identifying next task: {str(e)}") - return TaskResult( - taskId=f"analysis_{datetime.now(UTC).timestamp()}", - status=TaskStatus.FAILED, - success=False, - timestamp=datetime.now(UTC), - error=f"Error identifying next task: {str(e)}", - documentsLabel="Task Error" - ) - - - async def generateWorkflowFeedback(self, workflow: ChatWorkflow) -> str: - """ - Generates a final feedback message for the workflow in the user's language. - - Args: - workflow: The completed workflow to generate feedback for - - Returns: - str: The generated feedback message - """ - try: - # Get workflow summary - workflowSummary = { - "status": workflow.status, - "totalMessages": len(workflow.messages), - "totalDocuments": sum(len(msg.documents) for msg in workflow.messages), - "duration": (datetime.now(UTC) - datetime.fromisoformat(workflow.startedAt)).total_seconds() - } - - # Get user language from service - userLanguage = self.service.user.language - - # Prepare messages for AI context - messages = [ - { - "role": "system", - "content": f"You are an AI assistant providing a summary of a completed workflow. " - f"Please respond in '{userLanguage}' language. " - f"Summarize the workflow's activities, outcomes, and any important points. " - f"Be concise but informative. Use a professional but friendly tone." - }, - { - "role": "user", - "content": f"Please provide a summary of this workflow:\n" - f"Status: {workflowSummary['status']}\n" - f"Total Messages: {workflowSummary['totalMessages']}\n" - f"Total Documents: {workflowSummary['totalDocuments']}\n" - f"Duration: {workflowSummary['duration']:.1f} seconds" - } - ] - - # Add relevant workflow messages for context - for msg in workflow.messages: - if msg.role == "user" or msg.status in ["first", "last"]: - messages.append({ - "role": msg.role, - "content": msg.message - }) - - # Generate feedback using AI - feedback = await self.service.callAi(messages, produceUserAnswer=True, temperature=0.7) - - return feedback - - except Exception as e: - logger.error(f"Error generating workflow feedback: {str(e)}") - return "Workflow completed successfully." - - def _generatePrompt(self, task: str, document: ChatDocument, examples: List[Dict[str, str]] = None) -> str: - """Generate a prompt based on task and document""" - try: - # Create base prompt - prompt = f"""Task: {task} -Document: {document.filename} ({document.mimeType}) - -""" - - # Add examples if provided - if examples: - prompt += "\nExamples:\n" - for example in examples: - prompt += f"Input: {example.get('input', '')}\n" - prompt += f"Output: {example.get('output', '')}\n\n" - - return prompt - - except Exception as e: - logger.error(f"Error generating prompt: {str(e)}") - return "" - - # ===== Task Execution and Processing ===== async def executeTask(self, task: TaskItem) -> TaskItem: - """Execute a task with its list of actions""" + """Execute a task's actions""" try: - # Start timing - start_time = time.time() - task.startedAt = datetime.now(UTC).isoformat() - task.status = TaskStatus.RUNNING + # Execute each action + for action in task.actions: + # Create action prompt + prompt = f"""Execute the following action: - # Execute each action in sequence - for action in task.actionList: +Action: {action.method}.{action.action} +Parameters: {json.dumps(action.parameters)} + +Please provide a JSON response with: +1. result: The result of the action +2. resultLabel: A label for the result (format: documentList__