596 lines
No EOL
23 KiB
Python
596 lines
No EOL
23 KiB
Python
import logging
|
|
from typing import Dict, Any, Optional, List, Union
|
|
from datetime import datetime, UTC
|
|
import json
|
|
import uuid
|
|
|
|
from modules.interfaces.serviceAppModel import User
|
|
from modules.interfaces.serviceChatModel import (
|
|
TaskStatus, ChatDocument, TaskItem, TaskAction, TaskResult, ChatStat, ChatLog, ChatMessage, ChatWorkflow
|
|
)
|
|
from modules.workflow.serviceContainer import ServiceContainer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ChatManager:
|
|
"""Chat manager with improved AI integration and method handling"""
|
|
|
|
def __init__(self, currentUser: User):
|
|
self.currentUser = currentUser
|
|
self.service: ServiceContainer = None
|
|
|
|
# ===== Initialization and Setup =====
|
|
async def initialize(self, workflow: ChatWorkflow) -> None:
|
|
"""Initialize chat manager with workflow"""
|
|
self.workflow = workflow
|
|
self.service = ServiceContainer(self.currentUser, self.workflow)
|
|
|
|
# ===== Task Creation and Management =====
|
|
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()
|
|
|
|
# Process user input with AI
|
|
processedInput = await self._processUserInput(initialMessage.message, methodCatalog)
|
|
|
|
# Create actions from processed input
|
|
actions = await self._createActions(processedInput['actions'])
|
|
|
|
# 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()
|
|
}
|
|
|
|
# Create task using ChatInterface
|
|
return self.service.createTask(taskData)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating initial task: {str(e)}")
|
|
return None
|
|
|
|
async def createNextTask(self, workflow: ChatWorkflow, previousResult: TaskResult) -> Optional[TaskItem]:
|
|
"""Create next task based on previous result"""
|
|
try:
|
|
# Check if previous result was successful
|
|
if not previousResult.success:
|
|
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 using ChatInterface
|
|
return self.service.createTask(taskData)
|
|
|
|
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) -> TaskResult:
|
|
"""Execute a task and return its result"""
|
|
try:
|
|
# Create result object
|
|
result = TaskResult(
|
|
taskId=task.id,
|
|
status=task.status,
|
|
success=True,
|
|
timestamp=datetime.now(UTC)
|
|
)
|
|
|
|
# Start timing
|
|
startTime = datetime.now(UTC)
|
|
|
|
# Execute each action
|
|
for action in task.actionList:
|
|
try:
|
|
# Execute action
|
|
actionResult = await action.execute()
|
|
|
|
# Update action status
|
|
action.status = actionResult.status
|
|
if actionResult.error:
|
|
action.error = actionResult.error
|
|
|
|
except Exception as e:
|
|
logger.error(f"Action execution error: {str(e)}")
|
|
action.status = TaskStatus.FAILED
|
|
action.error = str(e)
|
|
|
|
# Calculate processing time
|
|
endTime = datetime.now(UTC)
|
|
result.processingTime = (endTime - startTime).total_seconds()
|
|
|
|
# Update task status
|
|
if all(action.status == TaskStatus.COMPLETED for action in task.actionList):
|
|
result.status = TaskStatus.COMPLETED
|
|
result.success = True
|
|
else:
|
|
result.status = TaskStatus.FAILED
|
|
result.success = False
|
|
result.error = "One or more actions failed"
|
|
|
|
# Generate feedback and documents if task completed successfully
|
|
if result.status == TaskStatus.COMPLETED:
|
|
# Generate feedback using AI
|
|
result.feedback = await self._processTaskResults(task)
|
|
|
|
# Create output documents
|
|
result.documents = await self._createOutputDocuments(task)
|
|
# Set documents label based on task input
|
|
result.documentsLabel = "TaskResult"
|
|
else:
|
|
result.feedback = f"Task failed: {result.error}"
|
|
|
|
# Update task in database
|
|
self.service.updateTask(task.id, {
|
|
"status": result.status,
|
|
"error": result.error,
|
|
"finishedAt": datetime.now(UTC).isoformat(),
|
|
"actionList": [action.dict() for action in task.actionList],
|
|
"documentsOutput": result.documents,
|
|
"feedback": result.feedback,
|
|
"documentsLabel": result.documentsLabel
|
|
})
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Task execution error: {str(e)}")
|
|
raise
|
|
|
|
async def parseTaskResult(self, workflow: ChatWorkflow, result: TaskResult) -> None:
|
|
"""Process and store task result in workflow"""
|
|
try:
|
|
# Find task in workflow
|
|
task = self.service.getTask(result.taskId)
|
|
if not task:
|
|
logger.error(f"Task {result.taskId} not found in workflow")
|
|
return
|
|
|
|
# Update task status
|
|
self.service.updateTask(task.id, {
|
|
"status": result.status,
|
|
"error": result.error,
|
|
"finishedAt": datetime.now(UTC).isoformat()
|
|
})
|
|
|
|
# Create feedback message if available
|
|
if result.feedback:
|
|
message = ChatMessage(
|
|
id=str(uuid.uuid4()),
|
|
workflowId=workflow.id,
|
|
role="assistant",
|
|
message=result.feedback,
|
|
status="step",
|
|
documents=result.documents
|
|
)
|
|
self.service.createWorkflowMessage(message.dict())
|
|
|
|
# Update workflow stats
|
|
if result.processingTime:
|
|
if not workflow.stats:
|
|
workflow.stats = ChatStat()
|
|
workflow.stats.processingTime = (workflow.stats.processingTime or 0) + result.processingTime
|
|
self.service.updateWorkflow(workflow.id, {"stats": workflow.stats.dict()})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing task result: {str(e)}")
|
|
raise
|
|
|
|
async def shouldContinue(self, workflow: ChatWorkflow) -> bool:
|
|
"""Determine if workflow should continue"""
|
|
try:
|
|
# Check if workflow is in a terminal state
|
|
if workflow.status in ["completed", "failed", "stopped"]:
|
|
return False
|
|
|
|
# Get all tasks for the workflow
|
|
tasks = self.service.tasks
|
|
|
|
# Check if there are any pending tasks
|
|
hasPendingTasks = any(t.status == TaskStatus.PENDING for t in tasks)
|
|
if not hasPendingTasks:
|
|
return False
|
|
|
|
# Check if any task is currently running
|
|
hasRunningTasks = any(t.status == TaskStatus.RUNNING for t in tasks)
|
|
if hasRunningTasks:
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking workflow continuation: {str(e)}")
|
|
return False
|
|
|
|
async def _summarizeWorkflow(self) -> str:
|
|
"""Summarize workflow history"""
|
|
if not self.workflow.messages:
|
|
return ""
|
|
|
|
prompt = f"""Summarize the following chat history:
|
|
{json.dumps([m.dict() for m in self.workflow.messages], indent=2)}
|
|
|
|
Please provide a concise summary focusing on:
|
|
1. Main objectives
|
|
2. Key actions taken
|
|
3. Current status
|
|
4. Any issues or blockers
|
|
"""
|
|
|
|
return await self.service.callAiBasic(prompt)
|
|
|
|
async def _analyzeTaskResults(self, task: TaskItem) -> Dict[str, Any]:
|
|
"""Analyze task results to determine next steps"""
|
|
# Get workflow summary
|
|
summary = await self._summarizeWorkflow()
|
|
|
|
# Generate prompt for analysis
|
|
prompt = f"""Based on the workflow summary and task results:
|
|
{summary}
|
|
|
|
Task: {task.userInput}
|
|
Status: {task.status}
|
|
Error: {task.error if task.error else 'None'}
|
|
|
|
Determine if the workflow is complete and what the next steps should be.
|
|
Return a JSON object with:
|
|
- isComplete: boolean
|
|
- objective: string
|
|
- nextActions: array of action objects
|
|
"""
|
|
|
|
# Get AI response
|
|
response = await self.service.callAiBasic(prompt)
|
|
|
|
# Parse response
|
|
return json.loads(response)
|
|
|
|
def _promptInstructions(self, methodCatalog: Dict[str, Any], isInitialTask: bool = False) -> str:
|
|
"""Generate common prompt instructions for task analysis"""
|
|
instructions = f"""Available Methods and Actions:
|
|
{json.dumps(methodCatalog, indent=2)}
|
|
|
|
"""
|
|
if isInitialTask:
|
|
instructions += """Please provide a JSON response with:
|
|
1. objective: The main goal or task to accomplish
|
|
2. actions: List of required actions with method and parameters
|
|
|
|
Example format:
|
|
{
|
|
"objective": "Search for documents about project X",
|
|
"actions": [
|
|
{
|
|
"method": "sharepoint",
|
|
"action": "search",
|
|
"parameters": {
|
|
"query": "project X",
|
|
"site": "projects"
|
|
}
|
|
}
|
|
]
|
|
}"""
|
|
else:
|
|
instructions += """Please provide a JSON response with:
|
|
1. isComplete: Whether the workflow is complete
|
|
2. nextActions: List of next actions needed (if any)
|
|
3. issues: Any issues or blockers identified
|
|
|
|
Example format:
|
|
{
|
|
"isComplete": false,
|
|
"nextActions": [
|
|
{
|
|
"method": "sharepoint",
|
|
"action": "read",
|
|
"parameters": {
|
|
"documentId": "doc123"
|
|
}
|
|
}
|
|
],
|
|
"issues": ["Need authentication for SharePoint"]
|
|
}
|
|
|
|
Note: Only use methods and actions that are available in the method catalog above."""
|
|
|
|
return instructions
|
|
|
|
async def _processUserInput(self, userInput: Dict[str, Any], methodCatalog: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Process user input with AI to extract objectives and actions"""
|
|
# Create prompt with available methods and actions
|
|
prompt = f"""Given the following user input and available methods/actions, extract the objective and required actions:
|
|
|
|
User Input: {userInput.get('message', '')}
|
|
|
|
{self._promptInstructions(methodCatalog, isInitialTask=True)}"""
|
|
|
|
# Call AI service
|
|
response = await self.service.callAiBasic(prompt)
|
|
return json.loads(response)
|
|
|
|
async def _createActions(self, actionsData: List[Dict[str, Any]]) -> List[TaskAction]:
|
|
"""Create action objects from processed input"""
|
|
actions = []
|
|
for actionData in actionsData:
|
|
try:
|
|
# Validate required fields
|
|
if not all(k in actionData for k in ['method', 'action']):
|
|
logger.warning(f"Skipping invalid action data: {actionData}")
|
|
continue
|
|
|
|
action = TaskAction(
|
|
id=f"action_{datetime.now(UTC).timestamp()}",
|
|
method=actionData['method'],
|
|
action=actionData['action'],
|
|
parameters=actionData.get('parameters', {}),
|
|
status=TaskStatus.PENDING,
|
|
retryCount=0,
|
|
retryMax=actionData.get('retryMax', 3)
|
|
)
|
|
actions.append(action)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating action: {str(e)}")
|
|
continue
|
|
|
|
return actions
|
|
|
|
async def _processTaskResults(self, task: TaskItem) -> str:
|
|
"""Process task results and generate feedback"""
|
|
try:
|
|
# Generate document prompt
|
|
docPrompt = self._generateDocumentPrompt(task.userInput)
|
|
|
|
# Get AI response for document generation
|
|
docResponse = await self.service.callAiBasic(docPrompt)
|
|
|
|
# Parse response into Task-Document objects
|
|
try:
|
|
taskDocs = json.loads(docResponse)
|
|
task.documentsOutput = taskDocs
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Error parsing document response: {str(e)}")
|
|
return f"Error processing results: {str(e)}"
|
|
|
|
# Generate feedback
|
|
feedback = await self.service.callAiBasic(
|
|
f"""Generate feedback for the completed task:
|
|
Task: {task.userInput}
|
|
Generated Documents: {len(task.documentsOutput)} files
|
|
|
|
Provide a concise summary of what was accomplished.
|
|
"""
|
|
)
|
|
|
|
return feedback
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing task results: {str(e)}")
|
|
return f"Error processing results: {str(e)}"
|
|
|
|
async def _createOutputDocuments(self, task: TaskItem) -> List[ChatDocument]:
|
|
"""Create output documents from task results"""
|
|
try:
|
|
fileIds = []
|
|
|
|
# Process each Task-Document from AI output
|
|
for taskDoc in task.documentsOutput:
|
|
# Store file in database
|
|
fileItem = self.service.createFile(
|
|
fileName=taskDoc.filename,
|
|
mimeType=taskDoc.mimeType,
|
|
content=taskDoc.data,
|
|
base64Encoded=taskDoc.base64Encoded
|
|
)
|
|
fileIds.append(fileItem.id)
|
|
|
|
# Convert all files to ChatDocuments in one call
|
|
if fileIds:
|
|
return await self.processFileIds(fileIds)
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating output documents: {str(e)}")
|
|
return []
|
|
|
|
async def processFileIds(self, fileIds: List[str]) -> List[ChatDocument]:
|
|
"""Process multiple files and extract their contents."""
|
|
documents = []
|
|
for fileId in fileIds:
|
|
# Get file metadata
|
|
fileInfo = self.service.getFileInfo(fileId)
|
|
if not fileInfo:
|
|
logger.warning(f"File metadata not found for {fileId}")
|
|
continue
|
|
|
|
# Create ChatDocument
|
|
document = ChatDocument(
|
|
id=str(uuid.uuid4()),
|
|
fileId=fileId,
|
|
filename=fileInfo.get("name", "Unknown"),
|
|
fileSize=fileInfo.get("size", 0),
|
|
mimeType=fileInfo.get("mimeType", "text/plain")
|
|
)
|
|
|
|
documents.append(document)
|
|
return documents
|
|
|
|
|
|
def _generateDocumentPrompt(self, task: str) -> str:
|
|
"""Generate a prompt for document generation"""
|
|
return f"""Generate output documents for the following task:
|
|
|
|
Task: {task}
|
|
|
|
For each document you need to generate, provide a Document object with the following structure:
|
|
{{
|
|
"filename": "string", # Filename with extension
|
|
"mimeType": "string", # MIME type of the file
|
|
"data": "string", # File content as text or base64
|
|
"base64Encoded": boolean # True if data is base64 encoded
|
|
}}
|
|
|
|
Rules:
|
|
1. For text files (txt, json, xml, etc.), provide content directly in the data field
|
|
2. For binary files (images, videos, etc.), encode content in base64 and set base64Encoded to true
|
|
3. Use appropriate MIME types (e.g., text/plain, image/jpeg, application/pdf)
|
|
4. Include file extensions in filenames
|
|
|
|
Return a JSON array of Document objects.
|
|
""" |