617 lines
No EOL
23 KiB
Python
617 lines
No EOL
23 KiB
Python
"""
|
|
Chat Manager Module for managing chat workflows and agent handovers.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, List, Optional, Union
|
|
from datetime import datetime, UTC
|
|
import uuid
|
|
import json
|
|
from dataclasses import dataclass
|
|
from modules.interfaces.serviceChatModel import (
|
|
ChatLog, ChatMessage, ChatDocument, UserInputRequest, ChatWorkflow,
|
|
AgentHandover
|
|
)
|
|
from modules.workflow.agentManager import getAgentManager
|
|
from modules.workflow.documentManager import getDocumentManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ChatManager:
|
|
"""Manager for chat workflows and agent handovers."""
|
|
|
|
_instance = None
|
|
|
|
@classmethod
|
|
def getInstance(cls):
|
|
"""Return a singleton instance of the chat manager."""
|
|
if cls._instance is None:
|
|
cls._instance = cls()
|
|
return cls._instance
|
|
|
|
# Core functions
|
|
|
|
def __init__(self):
|
|
"""Initialize the chat manager."""
|
|
if ChatManager._instance is not None:
|
|
raise RuntimeError("Singleton instance already exists - use getInstance()")
|
|
|
|
self.service = None
|
|
self.agentManager = getAgentManager()
|
|
self.documentManager = getDocumentManager()
|
|
|
|
def initialize(self, workflow: ChatWorkflow):
|
|
"""
|
|
Initialize the manager with an optional workflow object.
|
|
|
|
Args:
|
|
workflow: Optional ChatWorkflow object to initialize with
|
|
"""
|
|
# Initialize managers
|
|
self.agentManager.initialize(self.service)
|
|
self.documentManager.initialize(self.service)
|
|
|
|
# Add basic references to service
|
|
self.service.workflow = workflow
|
|
self.service.logAdd = self.logAdd
|
|
|
|
self.service.user = {
|
|
'id': None,
|
|
'name': None,
|
|
'language': 'en'
|
|
}
|
|
self.service.functions = {
|
|
'forEach': lambda items, action: [action(item) for item in items],
|
|
'while': lambda condition, action: [action() for _ in iter(lambda: condition(), False)]
|
|
}
|
|
self.service.model = {
|
|
'callAiBasic': self._callAiBasic,
|
|
'callAiComplex': self._callAiComplex,
|
|
'callAiImage': self._callAiImage
|
|
}
|
|
|
|
# Initialize document operations
|
|
self.service.document = {
|
|
'extract': self.documentManager.extractContent,
|
|
'convertFileRefToFileId': self.documentManager.convertFileRefToId,
|
|
'convertFileIdToFileRef': self.documentManager.convertFileIdToRef,
|
|
'convertDataFormat': self.documentManager.convertDataFormat,
|
|
'agentInputFilesCreate': self.documentManager.createAgentInputFileList,
|
|
'agentOutputFilesSave': self.documentManager.saveAgentOutputFiles
|
|
}
|
|
|
|
# Initialize data access
|
|
from modules.workflow.dataAccessFunctions import get_data_access
|
|
self.service.data = get_data_access().to_service_object()
|
|
|
|
return True
|
|
|
|
def createInitialHandover(self, userInput: UserInputRequest) -> AgentHandover:
|
|
"""
|
|
Create the initial handover object from user input.
|
|
|
|
Args:
|
|
userInput: User input request
|
|
|
|
Returns:
|
|
Initial handover object
|
|
"""
|
|
try:
|
|
# Create initial handover
|
|
handover = AgentHandover(
|
|
promptUserInitial=userInput.message,
|
|
documentsUserInitial=userInput.listFileId or [],
|
|
startedAt=datetime.now(UTC).isoformat()
|
|
)
|
|
|
|
# Process user input documents
|
|
if handover.documentsUserInitial:
|
|
handover.documentsInput = handover.documentsUserInitial
|
|
|
|
# Set initial prompt for next agent
|
|
handover.promptForNextAgent = handover.promptUserInitial
|
|
|
|
return handover
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating initial handover: {str(e)}")
|
|
return AgentHandover(status="failed", error=str(e))
|
|
|
|
async def defineNextHandover(self, currentHandover: AgentHandover) -> Optional[AgentHandover]:
|
|
"""
|
|
Define the next handover object for agent transition.
|
|
|
|
Args:
|
|
currentHandover: Current handover object
|
|
|
|
Returns:
|
|
Next handover object or None if no next agent
|
|
"""
|
|
try:
|
|
# Get available agents
|
|
availableAgents = self.agentManager.getAgentInfos()
|
|
if not availableAgents:
|
|
logger.warning("No available agents found")
|
|
return None
|
|
|
|
# Create next handover object
|
|
nextHandover = AgentHandover(
|
|
promptUserInitial=currentHandover.promptUserInitial,
|
|
documentsUserInitial=currentHandover.documentsUserInitial,
|
|
startedAt=datetime.now(UTC).isoformat()
|
|
)
|
|
|
|
# If this is the first handover, use initial documents
|
|
if not currentHandover.promptFromFinishedAgent:
|
|
nextHandover.documentsInput = currentHandover.documentsUserInitial
|
|
nextHandover.promptForNextAgent = currentHandover.promptUserInitial
|
|
else:
|
|
# Use output documents from previous agent
|
|
nextHandover.documentsInput = currentHandover.documentsOutput
|
|
nextHandover.promptForNextAgent = currentHandover.promptFromFinishedAgent
|
|
|
|
# Select next agent based on available agents and current state
|
|
nextAgent = await self._selectNextAgent(availableAgents, nextHandover)
|
|
if not nextAgent:
|
|
logger.info("No suitable next agent found")
|
|
return None
|
|
|
|
nextHandover.nextAgent = nextAgent['name']
|
|
return nextHandover
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error defining next handover: {str(e)}")
|
|
return None
|
|
|
|
async def _selectNextAgent(self, availableAgents: List[Dict[str, Any]], handover: AgentHandover) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Select the next agent using AI analysis of the current state and requirements.
|
|
|
|
Args:
|
|
availableAgents: List of available agents
|
|
handover: Current handover object
|
|
|
|
Returns:
|
|
Selected agent or None if no suitable agent
|
|
"""
|
|
try:
|
|
if not availableAgents:
|
|
logger.warning("No available agents found")
|
|
return None
|
|
|
|
# Get current workflow state
|
|
workflow = self.service.workflow
|
|
if not workflow:
|
|
logger.error("No workflow context available")
|
|
return None
|
|
|
|
# Detect user language if not already set
|
|
if not workflow.userLanguage:
|
|
workflow.userLanguage = await self._detectUserLanguage(handover.promptUserInitial)
|
|
|
|
# Get workflow summary for context
|
|
workflow_summary = await self.workflowSummarize(ChatMessage(
|
|
id=str(uuid.uuid4()),
|
|
workflowId=workflow.id,
|
|
role="user",
|
|
message=handover.promptUserInitial
|
|
))
|
|
|
|
# Prepare context for AI analysis
|
|
context = {
|
|
"current_state": {
|
|
"previous_agent": handover.currentAgent,
|
|
"status": handover.status,
|
|
"error": handover.error,
|
|
"user_language": workflow.userLanguage,
|
|
"input_documents": handover.documentsInput or [],
|
|
"output_documents": handover.documentsOutput or [],
|
|
"required_capabilities": handover.requiredCapabilities or []
|
|
},
|
|
"conversation_history": workflow_summary,
|
|
"available_agents": [
|
|
{
|
|
"name": agent.get("name", ""),
|
|
"capabilities": agent.get("capabilities", {}),
|
|
"description": agent.get("description", "")
|
|
}
|
|
for agent in availableAgents
|
|
]
|
|
}
|
|
|
|
# Create prompt for AI to analyze and select next agent
|
|
prompt = f"""
|
|
Analyze the current workflow state, conversation history, and available agents to determine the most suitable next agent.
|
|
Consider the following factors:
|
|
1. Previous agent's status and any errors
|
|
2. Required capabilities for the task
|
|
3. Document type compatibility
|
|
4. Language requirements
|
|
5. Agent's capabilities and specializations
|
|
6. Conversation history and context
|
|
|
|
Current State:
|
|
{json.dumps(context['current_state'], indent=2)}
|
|
|
|
Conversation History:
|
|
{context['conversation_history']}
|
|
|
|
Available Agents:
|
|
{json.dumps(context['available_agents'], indent=2)}
|
|
|
|
Return a JSON object with the following structure:
|
|
{{
|
|
"selected_agent": "name of the most suitable agent",
|
|
"reasoning": "brief explanation of why this agent was selected",
|
|
"required_capabilities": ["list", "of", "required", "capabilities"],
|
|
"potential_risks": ["list", "of", "potential", "issues"],
|
|
"task": {{
|
|
"description": "clear description of what the agent needs to do",
|
|
"input_format": {{
|
|
"documents": ["list", "of", "required", "input", "documents"],
|
|
"data": ["list", "of", "required", "data", "fields"]
|
|
}},
|
|
"output_format": {{
|
|
"documents": ["list", "of", "expected", "output", "documents"],
|
|
"data": ["list", "of", "expected", "output", "fields"]
|
|
}},
|
|
"requirements": [
|
|
"list of specific requirements",
|
|
"format requirements",
|
|
"quality requirements"
|
|
],
|
|
"constraints": [
|
|
"list of constraints",
|
|
"time limits",
|
|
"resource limits"
|
|
]
|
|
}},
|
|
"prompt_template": "template for the agent's prompt with placeholders for dynamic content"
|
|
}}
|
|
|
|
Format your response as a valid JSON object.
|
|
"""
|
|
|
|
# Get AI's analysis and selection
|
|
response = await self._callAiComplex(prompt)
|
|
|
|
try:
|
|
analysis = json.loads(response)
|
|
selected_agent_name = analysis.get('selected_agent')
|
|
|
|
# Find the selected agent in available agents
|
|
selected_agent = next(
|
|
(agent for agent in availableAgents if agent.get('name') == selected_agent_name),
|
|
None
|
|
)
|
|
|
|
if selected_agent:
|
|
logger.info(f"AI selected agent {selected_agent_name}: {analysis.get('reasoning')}")
|
|
# Update handover with AI's analysis
|
|
handover.requiredCapabilities = analysis.get('required_capabilities', [])
|
|
handover.analysis = {
|
|
'reasoning': analysis.get('reasoning'),
|
|
'potential_risks': analysis.get('potential_risks', []),
|
|
'task': analysis.get('task', {}),
|
|
'prompt_template': analysis.get('prompt_template', '')
|
|
}
|
|
return selected_agent
|
|
else:
|
|
logger.warning(f"AI selected agent {selected_agent_name} not found in available agents")
|
|
return None
|
|
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Error parsing AI response: {str(e)}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error selecting next agent: {str(e)}")
|
|
return None
|
|
|
|
async def processNextAgent(self, handover: AgentHandover) -> AgentHandover:
|
|
"""
|
|
Process the next agent in the workflow.
|
|
|
|
Args:
|
|
handover: Current handover object
|
|
|
|
Returns:
|
|
Updated handover object
|
|
"""
|
|
try:
|
|
# Get agent instance
|
|
agent = self.agentManager.getAgent(handover.nextAgent)
|
|
if not agent:
|
|
handover.update_status("failed", f"Agent {handover.nextAgent} not found")
|
|
return handover
|
|
|
|
# Set current agent
|
|
handover.currentAgent = handover.nextAgent
|
|
handover.nextAgent = None
|
|
|
|
# Execute agent
|
|
response = await agent.execute(handover)
|
|
|
|
# Update handover with results
|
|
if response.success:
|
|
handover.update_status("success")
|
|
handover.documentsOutput = response.message.documents if response.message else []
|
|
handover.promptFromFinishedAgent = response.message.message if response.message else ""
|
|
else:
|
|
handover.update_status("failed", response.error)
|
|
|
|
return handover
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing next agent: {str(e)}")
|
|
handover.update_status("failed", str(e))
|
|
return handover
|
|
|
|
# Agent functions
|
|
|
|
async def _callAiBasic(self, prompt: str, context: Dict[str, Any] = None) -> str:
|
|
"""Call basic AI model."""
|
|
try:
|
|
response = await self.service.base.callAi(prompt, context or {}, model="aiBase")
|
|
return response
|
|
except Exception as e:
|
|
logger.error(f"Error calling basic AI: {str(e)}")
|
|
return ""
|
|
|
|
async def _callAiComplex(self, prompt: str, context: Dict[str, Any] = None) -> str:
|
|
"""Call complex AI model."""
|
|
try:
|
|
response = await self.service.base.callAi(prompt, context or {}, model="aiComplex")
|
|
return response
|
|
except Exception as e:
|
|
logger.error(f"Error calling complex AI: {str(e)}")
|
|
return ""
|
|
|
|
async def _callAiImage(self, prompt: str, context: Dict[str, Any] = None) -> str:
|
|
"""Call image AI model."""
|
|
try:
|
|
response = await self.service.base.callAi(prompt, context or {}, model="aiImage")
|
|
return response
|
|
except Exception as e:
|
|
logger.error(f"Error calling image AI: {str(e)}")
|
|
return ""
|
|
|
|
def logAdd(self, message: str, level: str = "info",
|
|
progress: Optional[int] = None) -> str:
|
|
"""
|
|
Add a log entry to the workflow.
|
|
|
|
Args:
|
|
message: Log message
|
|
level: Log level (info, warning, error)
|
|
progress: Optional progress percentage
|
|
|
|
Returns:
|
|
str: ID of the created log entry
|
|
"""
|
|
workflow = self.service.workflow
|
|
try:
|
|
# Generate log ID
|
|
logId = str(uuid.uuid4())
|
|
|
|
# Create log entry
|
|
logEntry = ChatLog(
|
|
id=logId,
|
|
workflowId=workflow.id,
|
|
message=message,
|
|
level=level,
|
|
progress=progress,
|
|
timestamp=datetime.now().isoformat()
|
|
)
|
|
|
|
# Add to workflow logs
|
|
workflow.logs.append(logEntry)
|
|
|
|
# Also log to Python logger
|
|
logLevel = getattr(logging, level.upper())
|
|
logger.log(logLevel, f"[Workflow {workflow.id}] {message}")
|
|
|
|
# Save to database
|
|
self.chatManager.saveWorkflowLog(workflow.id, logEntry.to_dict())
|
|
|
|
return logId
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding log entry: {str(e)}")
|
|
return ""
|
|
|
|
async def chatMessageToWorkflow(self, role: str, agent: Union[str, Dict[str, Any]], chatMessage: UserInputRequest) -> ChatMessage:
|
|
"""
|
|
Integrates chat message input into a Message object including files with complete contents.
|
|
|
|
Args:
|
|
role: Role of the message sender (e.g., 'user', 'assistant')
|
|
agent: Agent name or configuration
|
|
chatMessage: UserInputRequest object containing message data and file references
|
|
|
|
Returns:
|
|
ChatMessage object with complete file contents
|
|
"""
|
|
try:
|
|
# Process additional files with complete contents
|
|
additionalFileIds = chatMessage.listFileId or []
|
|
additionalFiles = await self.processFileIds(additionalFileIds)
|
|
|
|
# Create message object
|
|
message = ChatMessage(
|
|
id=str(uuid.uuid4()),
|
|
workflowId=self.service.workflow.id,
|
|
role=role,
|
|
agentName=agent if isinstance(agent, str) else agent.get("name", ""),
|
|
message=chatMessage.message,
|
|
documents=additionalFiles,
|
|
status="completed",
|
|
startedAt=datetime.now().isoformat()
|
|
)
|
|
|
|
return message
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating workflow message: {str(e)}")
|
|
raise
|
|
|
|
async def sendFinalMessage(self, handover: AgentHandover) -> ChatMessage:
|
|
"""
|
|
Send final message to user with workflow results.
|
|
|
|
Args:
|
|
handover: Final handover object
|
|
|
|
Returns:
|
|
Final message to user
|
|
"""
|
|
try:
|
|
# Create final message content from handover
|
|
messageContent = handover.promptFromFinishedAgent
|
|
if handover.status == "failed":
|
|
messageContent = f"Workflow failed: {handover.error}"
|
|
|
|
# Add summary of generated documents
|
|
if handover.documentsOutput:
|
|
messageContent += "\n\nGenerated documents:"
|
|
for doc in handover.documentsOutput:
|
|
messageContent += f"\n- {doc.get('name', 'Unknown')}"
|
|
|
|
# Create message object
|
|
finalMessage = ChatMessage(
|
|
id=str(uuid.uuid4()),
|
|
workflowId=self.service.workflow.id,
|
|
agentName="Workflow Manager",
|
|
message=messageContent,
|
|
role="assistant",
|
|
status="completed",
|
|
sequenceNr=0,
|
|
startedAt=datetime.now(UTC).isoformat(),
|
|
finishedAt=datetime.now(UTC).isoformat(),
|
|
success=handover.status == "success",
|
|
documents=handover.documentsOutput
|
|
)
|
|
|
|
return finalMessage
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending final message: {str(e)}")
|
|
return ChatMessage(
|
|
id=str(uuid.uuid4()),
|
|
workflowId=self.service.workflow.id,
|
|
agentName="Workflow Manager",
|
|
message=f"Error in workflow: {str(e)}",
|
|
role="system",
|
|
status="error",
|
|
sequenceNr=0,
|
|
startedAt=datetime.now(UTC).isoformat(),
|
|
finishedAt=datetime.now(UTC).isoformat(),
|
|
success=False
|
|
)
|
|
|
|
async def workflowSummarize(self, messageUser: ChatMessage) -> str:
|
|
"""
|
|
Creates a summary of the workflow without the current user message.
|
|
|
|
Args:
|
|
messageUser: Current user message
|
|
|
|
Returns:
|
|
Summary of the workflow
|
|
"""
|
|
if not self.service.workflow or "messages" not in self.service.workflow or not self.service.workflow["messages"]:
|
|
return "" # First message
|
|
|
|
# Go through messages in chronological order
|
|
messages = sorted(self.service.workflow["messages"], key=lambda m: m.get("sequenceNo", 0), reverse=False)
|
|
|
|
summaryParts = []
|
|
for message in messages:
|
|
if message["id"] != messageUser["id"]:
|
|
messageSummary = await self.messageSummarize(message)
|
|
summaryParts.append(messageSummary)
|
|
|
|
return "\n\n".join(summaryParts)
|
|
|
|
async def messageSummarize(self, message: ChatMessage) -> str:
|
|
"""
|
|
Creates a summary of a message including its documents.
|
|
|
|
Args:
|
|
message: Message to summarize
|
|
|
|
Returns:
|
|
Summary of the message
|
|
"""
|
|
role = message.role
|
|
agentName = message.agentName
|
|
content = message.content
|
|
|
|
try:
|
|
# Use the serviceBase for language-aware AI calls
|
|
prompt = f"Create a very concise summary (2-3 sentences, maximum 300 characters) of the following message:\n\n{content}"
|
|
contentSummary = await self._callAiBasic(prompt)
|
|
except Exception as e:
|
|
logger.error(f"Error creating summary: {str(e)}")
|
|
contentSummary = content[:200] + "..."
|
|
|
|
# Summarize documents
|
|
docsSummary = ""
|
|
if "documents" in message and message["documents"]:
|
|
docsList = []
|
|
for i, doc in enumerate(message["documents"]):
|
|
docName = self.getFilename(doc)
|
|
docsList.append(docName)
|
|
if docsList:
|
|
docsSummary = "\nDocuments:" + "\n- ".join(docsList)
|
|
|
|
return f"[{role} {agentName}]: {contentSummary}{docsSummary}"
|
|
|
|
def getFilename(self, document: ChatDocument) -> str:
|
|
"""
|
|
Gets the filename from a document by combining name and extension.
|
|
|
|
Args:
|
|
document: Document object
|
|
|
|
Returns:
|
|
Filename with extension
|
|
"""
|
|
name = document.name
|
|
ext = document.ext
|
|
if ext:
|
|
return f"{name}.{ext}"
|
|
return name
|
|
|
|
async def _detectUserLanguage(self, text: str) -> str:
|
|
"""
|
|
Detects the language of user input using AI.
|
|
|
|
Args:
|
|
text: User input text to analyze
|
|
|
|
Returns:
|
|
Language code (e.g., 'en', 'de', 'fr')
|
|
"""
|
|
try:
|
|
# Use basic AI model for language detection
|
|
prompt = f"""
|
|
Analyze the following text and identify its language.
|
|
Return only the ISO 639-1 language code (e.g., 'en' for English, 'de' for German).
|
|
|
|
Text: {text}
|
|
"""
|
|
response = await self._callAiBasic(prompt)
|
|
# Clean and validate response
|
|
lang_code = response.strip().lower()
|
|
# Basic validation of common language codes
|
|
valid_codes = {'en', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'ru', 'zh', 'ja', 'ko'}
|
|
return lang_code if lang_code in valid_codes else 'en'
|
|
except Exception as e:
|
|
logger.error(f"Error detecting language: {str(e)}")
|
|
return 'en' # Default to English on error
|
|
|
|
|
|
# Singleton factory for the chat manager
|
|
def getChatManager():
|
|
return ChatManager.getInstance() |