466 lines
20 KiB
Python
466 lines
20 KiB
Python
import logging
|
|
import importlib
|
|
import pkgutil
|
|
import inspect
|
|
from typing import Dict, Any, List, Optional
|
|
from modules.interfaces.interfaceAppModel import User, UserConnection
|
|
from modules.interfaces.interfaceChatModel import (
|
|
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult,
|
|
ChatStat, ChatLog, ChatMessage, ChatWorkflow
|
|
)
|
|
from modules.interfaces.interfaceAiCalls import AiCalls
|
|
from modules.interfaces.interfaceChatObjects import getInterface as getChatObjects
|
|
from modules.interfaces.interfaceChatModel import ActionResult
|
|
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentObjects
|
|
from modules.interfaces.interfaceAppObjects import getInterface as getAppObjects
|
|
from modules.workflow.managerDocument import DocumentManager
|
|
from modules.workflow.methodBase import MethodBase
|
|
import uuid
|
|
import base64
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ServiceContainer:
|
|
"""Service container that provides access to all services and their functions"""
|
|
|
|
def __init__(self, currentUser: User, workflow: ChatWorkflow):
|
|
# Core services
|
|
self.user = currentUser
|
|
self.workflow = workflow
|
|
self.tasks = workflow.tasks
|
|
self.statusEnums = TaskStatus
|
|
self.currentTask = None # Initialize current task as None
|
|
|
|
# Initialize managers
|
|
self.interfaceChat = getChatObjects(currentUser)
|
|
self.interfaceComponent = getComponentObjects(currentUser)
|
|
self.interfaceApp = getAppObjects(currentUser)
|
|
self.interfaceAiCalls = AiCalls()
|
|
self.documentManager = DocumentManager(self)
|
|
|
|
# Initialize methods catalog
|
|
self.methods = {}
|
|
# Discover additional methods
|
|
self._discoverMethods()
|
|
|
|
def _discoverMethods(self):
|
|
"""Dynamically discover all method classes and their actions in modules.methods package"""
|
|
try:
|
|
# Import the methods package
|
|
methodsPackage = importlib.import_module('modules.methods')
|
|
|
|
# Discover all modules in the package
|
|
for _, name, isPkg in pkgutil.iter_modules(methodsPackage.__path__):
|
|
if not isPkg and name.startswith('method'):
|
|
try:
|
|
# Import the module
|
|
module = importlib.import_module(f'modules.methods.{name}')
|
|
|
|
# Find all classes in the module that inherit from MethodBase
|
|
for itemName, item in inspect.getmembers(module):
|
|
if (inspect.isclass(item) and
|
|
issubclass(item, MethodBase) and
|
|
item != MethodBase):
|
|
# Instantiate the method
|
|
methodInstance = item(self)
|
|
|
|
# Discover actions from public methods
|
|
actions = {}
|
|
for methodName, method in inspect.getmembers(type(methodInstance), predicate=inspect.iscoroutinefunction):
|
|
if not methodName.startswith('_') and methodName not in ['execute', 'validateParameters']:
|
|
# Bind the method to the instance
|
|
bound_method = method.__get__(methodInstance, type(methodInstance))
|
|
sig = inspect.signature(method)
|
|
params = {}
|
|
for paramName, param in sig.parameters.items():
|
|
if paramName not in ['self', 'authData']:
|
|
params[paramName] = {
|
|
'type': param.annotation if param.annotation != param.empty else Any,
|
|
'required': param.default == param.empty,
|
|
'description': param.default.__doc__ if hasattr(param.default, '__doc__') else None
|
|
}
|
|
actions[methodName] = {
|
|
'description': method.__doc__ or '',
|
|
'parameters': params,
|
|
'method': bound_method
|
|
}
|
|
|
|
# Add method instance with discovered actions
|
|
self.methods[methodInstance.name] = {
|
|
'instance': methodInstance,
|
|
'description': methodInstance.description,
|
|
'actions': actions
|
|
}
|
|
logger.info(f"Discovered method: {methodInstance.name} with {len(actions)} actions")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error loading method module {name}: {str(e)}", exc_info=True)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error discovering methods: {str(e)}")
|
|
|
|
# ===== Functions =====
|
|
|
|
def extractContent(self, prompt: str, document: ChatDocument) -> str:
|
|
"""Extract content from document using prompt"""
|
|
return self.documentManager.extractContentFromDocument(prompt, document)
|
|
|
|
def extractContentFromFileData(self, prompt: str, fileData: bytes, filename: str, mimeType: str, base64Encoded: bool = False) -> str:
|
|
"""Extract content from file data directly using prompt"""
|
|
return self.documentManager.extractContentFromFileData(prompt, fileData, filename, mimeType, base64Encoded)
|
|
|
|
def getMethodsCatalog(self) -> Dict[str, Any]:
|
|
"""Get catalog of available methods and their actions"""
|
|
catalog = {}
|
|
for methodName, method in self.methods.items():
|
|
catalog[methodName] = {
|
|
'description': method['description'],
|
|
'actions': {
|
|
actionName: {
|
|
'description': action['description'],
|
|
'parameters': action['parameters']
|
|
}
|
|
for actionName, action in method['actions'].items()
|
|
}
|
|
}
|
|
return catalog
|
|
|
|
def getMethodsList(self) -> List[str]:
|
|
"""Get list of available methods with their signatures"""
|
|
methodList = []
|
|
for methodName, method in self.methods.items():
|
|
for actionName, action in method['actions'].items():
|
|
# Get parameter types from action signature
|
|
paramTypes = []
|
|
for paramName, param in action['parameters'].items():
|
|
paramTypes.append(f"{paramName}:{param['type']}")
|
|
|
|
# Format: method.action([param1:type, param2:type]) # description
|
|
signature = f"{methodName}.{actionName}([{', '.join(paramTypes)}])"
|
|
if action['description']:
|
|
signature += f" # {action['description']}"
|
|
methodList.append(signature)
|
|
return methodList
|
|
|
|
def getDocumentReferenceList(self) -> Dict[str, List[Dict[str, str]]]:
|
|
"""Get list of document references sorted by datetime, categorized by chat round"""
|
|
chat_refs = []
|
|
history_refs = []
|
|
|
|
# Process messages in reverse order to find current chat round
|
|
for message in reversed(self.workflow.messages):
|
|
# Get document references from message
|
|
if message.documents:
|
|
# For messages with action context, use documentList reference
|
|
if message.actionId and message.documentsLabel:
|
|
doc_ref = self.getDocumentReferenceFromMessage(message)
|
|
doc_info = {
|
|
"documentReference": doc_ref,
|
|
"datetime": message.publishedAt
|
|
}
|
|
|
|
# Add to appropriate list based on message status
|
|
if message.status == "first":
|
|
chat_refs.append(doc_info)
|
|
break # Stop after finding first message
|
|
elif message.status == "step":
|
|
chat_refs.append(doc_info)
|
|
else:
|
|
history_refs.append(doc_info)
|
|
# For regular messages, use individual document references
|
|
else:
|
|
for doc in message.documents:
|
|
doc_ref = self.getDocumentReferenceFromChatDocument(doc)
|
|
doc_info = {
|
|
"documentReference": doc_ref,
|
|
"datetime": message.publishedAt
|
|
}
|
|
|
|
# Add to appropriate list based on message status
|
|
if message.status == "first":
|
|
chat_refs.append(doc_info)
|
|
break # Stop after finding first message
|
|
elif message.status == "step":
|
|
chat_refs.append(doc_info)
|
|
else:
|
|
history_refs.append(doc_info)
|
|
|
|
# Stop processing if we hit a first message
|
|
if message.status == "first":
|
|
break
|
|
|
|
# Sort both lists by datetime in descending order
|
|
chat_refs.sort(key=lambda x: x["datetime"], reverse=True)
|
|
history_refs.sort(key=lambda x: x["datetime"], reverse=True)
|
|
|
|
return {
|
|
"chat": chat_refs,
|
|
"history": history_refs
|
|
}
|
|
|
|
def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str:
|
|
"""Get document reference from ChatDocument"""
|
|
return f"document_{document.id}_{document.filename}"
|
|
|
|
def getDocumentReferenceFromMessage(self, message: ChatMessage) -> str:
|
|
"""Get document reference from ChatMessage with action context"""
|
|
if not message.actionId or not message.documentsLabel:
|
|
return None
|
|
|
|
# If documentsLabel already contains the full reference format, return it
|
|
if message.documentsLabel.startswith("documentList_"):
|
|
return message.documentsLabel
|
|
|
|
# Otherwise construct the reference
|
|
return f"documentList_{message.actionId}_{message.documentsLabel}"
|
|
|
|
def getChatDocumentsFromDocumentReference(self, documentReference: str) -> List[ChatDocument]:
|
|
"""Get ChatDocuments from document reference"""
|
|
try:
|
|
# Parse reference format
|
|
parts = documentReference.split('_', 2) # Split into max 3 parts
|
|
if len(parts) < 3:
|
|
return []
|
|
|
|
ref_type = parts[0]
|
|
ref_id = parts[1]
|
|
ref_label = parts[2] # Keep the full label
|
|
|
|
if ref_type == "document":
|
|
# Handle ChatDocument reference: document_<id>_<filename>
|
|
# Find document in workflow messages
|
|
for message in self.workflow.messages:
|
|
if message.documents:
|
|
for doc in message.documents:
|
|
if doc.id == ref_id:
|
|
return [doc]
|
|
|
|
elif ref_type == "documentList":
|
|
# Handle document list reference: documentList_<action.id>_<label>
|
|
# Find message with matching action ID and documents label
|
|
for message in self.workflow.messages:
|
|
if (message.actionId == ref_id and
|
|
message.documentsLabel == documentReference and # Compare full reference
|
|
message.documents):
|
|
return message.documents
|
|
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting documents from reference {documentReference}: {str(e)}")
|
|
return []
|
|
|
|
def getConnectionReferenceList(self) -> List[Dict[str, str]]:
|
|
"""Get list of all UserConnection objects as references"""
|
|
connections = []
|
|
# Get user connections through AppObjects interface
|
|
user_connections = self.interfaceApp.getUserConnections(self.user.id)
|
|
for conn in user_connections:
|
|
connections.append({
|
|
"connectionReference": f"connection_{conn.id}_{conn.authority}_{conn.externalUsername}",
|
|
"authority": conn.authority
|
|
})
|
|
# Sort by authority
|
|
return sorted(connections, key=lambda x: x["authority"])
|
|
|
|
def getConnectionReferenceFromUserConnection(self, connection: UserConnection) -> str:
|
|
"""Get connection reference from UserConnection"""
|
|
return f"connection_{connection.id}_{connection.authority}_{connection.externalUsername}"
|
|
|
|
def getUserConnectionFromConnectionReference(self, connectionReference: str) -> Optional[UserConnection]:
|
|
"""Get UserConnection from reference string"""
|
|
try:
|
|
# Parse reference format: connection_{id}_{authority}_{username}
|
|
parts = connectionReference.split('_')
|
|
if len(parts) != 4 or parts[0] != "connection":
|
|
return None
|
|
|
|
conn_id = parts[1]
|
|
authority = parts[2]
|
|
username = parts[3]
|
|
|
|
# Get user connections through AppObjects interface
|
|
user_connections = self.interfaceApp.getUserConnections(self.user.id)
|
|
|
|
# Find matching connection
|
|
for conn in user_connections:
|
|
if str(conn.id) == conn_id and conn.authority == authority and conn.externalUsername == username:
|
|
return conn
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing connection reference: {str(e)}")
|
|
return None
|
|
|
|
async def summarizeChat(self, messages: List[ChatMessage]) -> str:
|
|
"""
|
|
Summarize chat messages from last to first message with status="first"
|
|
|
|
Args:
|
|
messages: List of chat messages to summarize
|
|
|
|
Returns:
|
|
str: Summary of the chat in user's language
|
|
"""
|
|
try:
|
|
# Get messages from last to first, stopping at first message with status="first"
|
|
relevantMessages = []
|
|
for msg in reversed(messages):
|
|
relevantMessages.append(msg)
|
|
if msg.status == "first":
|
|
break
|
|
|
|
# Create prompt for AI
|
|
prompt = f"""You are an AI assistant providing a summary of a chat conversation.
|
|
Please respond in '{self.user.language}' language.
|
|
|
|
Chat History:
|
|
{chr(10).join(f"- {msg.message}" for msg in reversed(relevantMessages))}
|
|
|
|
Instructions:
|
|
1. Summarize the conversation's key points and outcomes
|
|
2. Be concise but informative
|
|
3. Use a professional but friendly tone
|
|
4. Focus on important decisions and next steps if any
|
|
|
|
Please provide a comprehensive summary of this conversation."""
|
|
|
|
# Get summary using AI
|
|
return await self.interfaceAiCalls.callAiTextBasic(prompt)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error summarizing chat: {str(e)}")
|
|
return f"Error summarizing chat: {str(e)}"
|
|
|
|
async def summarizeMessage(self, message: ChatMessage) -> str:
|
|
"""
|
|
Summarize a single chat message
|
|
|
|
Args:
|
|
message: Chat message to summarize
|
|
|
|
Returns:
|
|
str: Summary of the message in user's language
|
|
"""
|
|
try:
|
|
# Create prompt for AI
|
|
prompt = f"""You are an AI assistant providing a summary of a chat message.
|
|
Please respond in '{self.user.language}' language.
|
|
|
|
Message:
|
|
{message.message}
|
|
|
|
Instructions:
|
|
1. Summarize the key points of this message
|
|
2. Be concise but informative
|
|
3. Use a professional but friendly tone
|
|
4. Focus on important information and any actions needed
|
|
|
|
Please provide a clear summary of this message."""
|
|
|
|
# Get summary using AI
|
|
return await self.interfaceAiCalls.callAiTextBasic(prompt)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error summarizing message: {str(e)}")
|
|
return f"Error summarizing message: {str(e)}"
|
|
|
|
def callAiTextBasic(self, prompt: str, context: str = None) -> str:
|
|
"""Basic text processing using OpenAI"""
|
|
return self.interfaceAiCalls.callAiTextBasic(prompt, context)
|
|
|
|
def callAiTextAdvanced(self, prompt: str, context: str = None) -> str:
|
|
"""Advanced text processing using Anthropic"""
|
|
return self.interfaceAiCalls.callAiTextAdvanced(prompt, context)
|
|
|
|
def callAiImageBasic(self, prompt: str, imageData: bytes, mimeType: str) -> str:
|
|
"""Basic image processing using OpenAI"""
|
|
return self.interfaceAiCalls.callAiImageBasic(prompt, imageData, mimeType)
|
|
|
|
def callAiImageAdvanced(self, prompt: str, imageData: bytes, mimeType: str) -> str:
|
|
"""Advanced image processing using Anthropic"""
|
|
return self.interfaceAiCalls.callAiImageAdvanced(prompt, imageData, mimeType)
|
|
|
|
def getFileInfo(self, fileId: str) -> Dict[str, Any]:
|
|
"""Get file information"""
|
|
file_item = self.interfaceComponent.getFile(fileId)
|
|
if file_item:
|
|
return {
|
|
"id": file_item.id,
|
|
"filename": file_item.filename,
|
|
"size": file_item.fileSize,
|
|
"mimeType": file_item.mimeType,
|
|
"fileHash": file_item.fileHash,
|
|
"creationDate": file_item.creationDate
|
|
}
|
|
return None
|
|
|
|
def getFileData(self, fileId: str) -> bytes:
|
|
"""Get file data by ID"""
|
|
return self.interfaceComponent.getFileData(fileId)
|
|
|
|
def createFile(self, fileName: str, mimeType: str, content: str, base64encoded: bool = False) -> str:
|
|
"""Create new file and return its ID"""
|
|
# Convert content to bytes based on base64 flag
|
|
if base64encoded:
|
|
content_bytes = base64.b64decode(content)
|
|
else:
|
|
content_bytes = content.encode('utf-8')
|
|
|
|
# First create the file metadata
|
|
file_item = self.interfaceComponent.createFile(
|
|
name=fileName,
|
|
mimeType=mimeType,
|
|
size=len(content_bytes)
|
|
)
|
|
|
|
# Then store the file data
|
|
self.interfaceComponent.createFileData(file_item.id, content_bytes)
|
|
|
|
return file_item.id
|
|
|
|
def createDocument(self, fileName: str, mimeType: str, content: str, base64encoded: bool = True) -> ChatDocument:
|
|
"""Create document from file data object created by AI call"""
|
|
# First create the file and get its ID
|
|
file_id = self.createFile(fileName, mimeType, content, base64encoded)
|
|
|
|
# Get file info for metadata
|
|
file_info = self.interfaceComponent.getFile(file_id)
|
|
|
|
# Create document with file reference
|
|
return ChatDocument(
|
|
id=str(uuid.uuid4()),
|
|
fileId=file_id,
|
|
filename=fileName,
|
|
fileSize=file_info.fileSize,
|
|
mimeType=mimeType
|
|
)
|
|
|
|
async def executeAction(self, methodName: str, actionName: str, parameters: Dict[str, Any], authData: Optional[Dict[str, Any]] = None) -> ActionResult:
|
|
"""Execute a method action"""
|
|
try:
|
|
if methodName not in self.methods:
|
|
raise ValueError(f"Unknown method: {methodName}")
|
|
|
|
method = self.methods[methodName]
|
|
if actionName not in method['actions']:
|
|
raise ValueError(f"Unknown action: {actionName} for method {methodName}")
|
|
|
|
action = method['actions'][actionName]
|
|
|
|
# Execute the action
|
|
return await action['method'](parameters, authData)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error executing method {methodName}.{actionName}: {str(e)}")
|
|
raise
|
|
|
|
# Create singleton instance
|
|
serviceObject = None
|
|
|
|
def initializeServiceContainer(currentUser: User, workflow: ChatWorkflow) -> ServiceContainer:
|
|
"""Initialize the service container singleton"""
|
|
global serviceObject
|
|
if serviceObject is None:
|
|
serviceObject = ServiceContainer(currentUser, workflow)
|
|
return serviceObject
|