refactored and properly separated self.chat and self.workflow

This commit is contained in:
ValueOn AG 2025-11-04 10:30:23 +01:00
parent 7ee5d4061b
commit 90663963ff
27 changed files with 380 additions and 357 deletions

View file

@ -83,7 +83,7 @@ class ManagerSyncDelta:
self.APP_ENV_TYPE = self.services.utils.configGet("APP_ENV_TYPE", "dev") self.APP_ENV_TYPE = self.services.utils.configGet("APP_ENV_TYPE", "dev")
self.JIRA_API_TOKEN = self.services.utils.configGet("Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET", "") self.JIRA_API_TOKEN = self.services.utils.configGet("Feature_SyncDelta_JIRA_DELTA_TOKEN_SECRET", "")
# Resolve SharePoint connection for the configured user id # Resolve SharePoint connection for the configured user id
self.sharepointConnection = self.services.workflow.getUserConnectionByExternalUsername("msft", self.SHAREPOINT_USER_ID) self.sharepointConnection = self.services.chat.getUserConnectionByExternalUsername("msft", self.SHAREPOINT_USER_ID)
if not self.sharepointConnection: if not self.sharepointConnection:
logger.error( logger.error(
f"No SharePoint connection found for user: {self.SHAREPOINT_USER_ID}" f"No SharePoint connection found for user: {self.SHAREPOINT_USER_ID}"

View file

@ -75,8 +75,8 @@ class Services:
from .serviceTicket.mainServiceTicket import TicketService from .serviceTicket.mainServiceTicket import TicketService
self.ticket = PublicService(TicketService(self)) self.ticket = PublicService(TicketService(self))
from .serviceWorkflow.mainServiceWorkflow import WorkflowService from .serviceChat.mainServiceChat import ChatService
self.workflow = PublicService(WorkflowService(self)) self.chat = PublicService(ChatService(self))
from .serviceUtils.mainServiceUtils import UtilsService from .serviceUtils.mainServiceUtils import UtilsService
self.utils = PublicService(UtilsService(self)) self.utils = PublicService(UtilsService(self))

View file

@ -205,11 +205,11 @@ Respond with ONLY a JSON object in this exact format:
# Update progress for iteration start # Update progress for iteration start
if operationId: if operationId:
if iteration == 1: if iteration == 1:
self.services.workflow.progressLogUpdate(operationId, 0.5, f"Starting AI call iteration {iteration}") self.services.chat.progressLogUpdate(operationId, 0.5, f"Starting AI call iteration {iteration}")
else: else:
# For continuation iterations, show progress incrementally # For continuation iterations, show progress incrementally
baseProgress = 0.5 + (min(iteration - 1, maxIterations) / maxIterations * 0.4) # Progress from 0.5 to 0.9 over maxIterations iterations baseProgress = 0.5 + (min(iteration - 1, maxIterations) / maxIterations * 0.4) # Progress from 0.5 to 0.9 over maxIterations iterations
self.services.workflow.progressLogUpdate(operationId, baseProgress, f"Continuing generation (iteration {iteration})") self.services.chat.progressLogUpdate(operationId, baseProgress, f"Continuing generation (iteration {iteration})")
# Build iteration prompt # Build iteration prompt
if len(allSections) > 0 and promptBuilder and promptArgs: if len(allSections) > 0 and promptBuilder and promptArgs:
@ -227,7 +227,7 @@ Respond with ONLY a JSON object in this exact format:
# Make AI call # Make AI call
try: try:
if operationId and iteration == 1: if operationId and iteration == 1:
self.services.workflow.progressLogUpdate(operationId, 0.51, "Calling AI model") self.services.chat.progressLogUpdate(operationId, 0.51, "Calling AI model")
request = AiCallRequest( request = AiCallRequest(
prompt=iterationPrompt, prompt=iterationPrompt,
context="", context="",
@ -246,10 +246,10 @@ Respond with ONLY a JSON object in this exact format:
# Update progress after AI call # Update progress after AI call
if operationId: if operationId:
if iteration == 1: if iteration == 1:
self.services.workflow.progressLogUpdate(operationId, 0.6, f"AI response received (iteration {iteration})") self.services.chat.progressLogUpdate(operationId, 0.6, f"AI response received (iteration {iteration})")
else: else:
progress = 0.6 + (min(iteration - 1, 10) * 0.03) progress = 0.6 + (min(iteration - 1, 10) * 0.03)
self.services.workflow.progressLogUpdate(operationId, progress, f"Processing response (iteration {iteration})") self.services.chat.progressLogUpdate(operationId, progress, f"Processing response (iteration {iteration})")
# Write raw AI response to debug file # Write raw AI response to debug file
if iteration == 1: if iteration == 1:
@ -258,8 +258,8 @@ Respond with ONLY a JSON object in this exact format:
self.services.utils.writeDebugFile(result, f"{debugPrefix}_response_iteration_{iteration}") self.services.utils.writeDebugFile(result, f"{debugPrefix}_response_iteration_{iteration}")
# Emit stats for this iteration # Emit stats for this iteration
self.services.workflow.storeWorkflowStat( self.services.chat.storeWorkflowStat(
self.services.currentWorkflow, self.services.workflow,
response, response,
f"ai.call.{debugPrefix}.iteration_{iteration}" f"ai.call.{debugPrefix}.iteration_{iteration}"
) )
@ -286,7 +286,7 @@ Respond with ONLY a JSON object in this exact format:
# Update progress after parsing # Update progress after parsing
if operationId: if operationId:
if extractedSections: if extractedSections:
self.services.workflow.progressLogUpdate(operationId, 0.65 + (min(iteration - 1, 10) * 0.025), f"Extracted {len(extractedSections)} sections (iteration {iteration})") self.services.chat.progressLogUpdate(operationId, 0.65 + (min(iteration - 1, 10) * 0.025), f"Extracted {len(extractedSections)} sections (iteration {iteration})")
if not extractedSections: if not extractedSections:
# If we're in continuation mode and JSON was incomplete, don't stop - continue to allow retry # If we're in continuation mode and JSON was incomplete, don't stop - continue to allow retry
@ -306,7 +306,7 @@ Respond with ONLY a JSON object in this exact format:
else: else:
# Done - build final result # Done - build final result
if operationId: if operationId:
self.services.workflow.progressLogUpdate(operationId, 0.95, f"Generation complete ({iteration} iterations, {len(allSections)} sections)") self.services.chat.progressLogUpdate(operationId, 0.95, f"Generation complete ({iteration} iterations, {len(allSections)} sections)")
logger.info(f"Generation complete after {iteration} iterations: {len(allSections)} sections") logger.info(f"Generation complete after {iteration} iterations: {len(allSections)} sections")
break break
@ -566,11 +566,11 @@ Respond with ONLY a JSON object in this exact format:
await self._ensureAiObjectsInitialized() await self._ensureAiObjectsInitialized()
# Create separate operationId for detailed progress tracking # Create separate operationId for detailed progress tracking
workflowId = self.services.currentWorkflow.id if self.services.currentWorkflow else f"no-workflow-{int(time.time())}" workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
aiOperationId = f"ai_documents_{workflowId}_{int(time.time())}" aiOperationId = f"ai_documents_{workflowId}_{int(time.time())}"
# Start progress tracking for this operation # Start progress tracking for this operation
self.services.workflow.progressLogStart( self.services.chat.progressLogStart(
aiOperationId, aiOperationId,
"AI call with documents", "AI call with documents",
"Document Generation", "Document Generation",
@ -580,7 +580,7 @@ Respond with ONLY a JSON object in this exact format:
try: try:
if options is None or (hasattr(options, 'operationType') and options.operationType is None): if options is None or (hasattr(options, 'operationType') and options.operationType is None):
# Use AI to determine parameters ONLY when truly needed (options=None OR operationType=None) # Use AI to determine parameters ONLY when truly needed (options=None OR operationType=None)
self.services.workflow.progressLogUpdate(aiOperationId, 0.1, "Analyzing prompt parameters") self.services.chat.progressLogUpdate(aiOperationId, 0.1, "Analyzing prompt parameters")
options = await self._analyzePromptAndCreateOptions(prompt) options = await self._analyzePromptAndCreateOptions(prompt)
# Check operationType FIRST - some operations need direct routing (before document generation checks) # Check operationType FIRST - some operations need direct routing (before document generation checks)
@ -591,7 +591,7 @@ Respond with ONLY a JSON object in this exact format:
if isImageRequest: if isImageRequest:
# Image generation uses generic call path but bypasses document generation pipeline # Image generation uses generic call path but bypasses document generation pipeline
self.services.workflow.progressLogUpdate(aiOperationId, 0.4, "Calling AI for image generation") self.services.chat.progressLogUpdate(aiOperationId, 0.4, "Calling AI for image generation")
# Call via generic path (no looping for images) # Call via generic path (no looping for images)
request = AiCallRequest( request = AiCallRequest(
@ -621,19 +621,19 @@ Respond with ONLY a JSON object in this exact format:
result = response.content result = response.content
# Emit stats for image generation # Emit stats for image generation
self.services.workflow.storeWorkflowStat( self.services.chat.storeWorkflowStat(
self.services.currentWorkflow, self.services.workflow,
response, response,
f"ai.generate.image" f"ai.generate.image"
) )
self.services.workflow.progressLogUpdate(aiOperationId, 0.9, "Image generated") self.services.chat.progressLogUpdate(aiOperationId, 0.9, "Image generated")
self.services.workflow.progressLogFinish(aiOperationId, True) self.services.chat.progressLogFinish(aiOperationId, True)
return result return result
else: else:
errorMsg = f"No image data returned: {response.content}" errorMsg = f"No image data returned: {response.content}"
logger.error(f"Error in AI image generation: {errorMsg}") logger.error(f"Error in AI image generation: {errorMsg}")
self.services.workflow.progressLogFinish(aiOperationId, False) self.services.chat.progressLogFinish(aiOperationId, False)
return {"success": False, "error": errorMsg} return {"success": False, "error": errorMsg}
# Handle WEB_SEARCH and WEB_CRAWL operations - route directly to connectors # Handle WEB_SEARCH and WEB_CRAWL operations - route directly to connectors
@ -645,7 +645,7 @@ Respond with ONLY a JSON object in this exact format:
# Web operations: prompt is already structured JSON (AiCallPromptWebSearch/WebCrawl) # Web operations: prompt is already structured JSON (AiCallPromptWebSearch/WebCrawl)
# Route directly through centralized AI call - model selector chooses appropriate connector # Route directly through centralized AI call - model selector chooses appropriate connector
# Connector parses the JSON prompt and executes the operation # Connector parses the JSON prompt and executes the operation
self.services.workflow.progressLogUpdate(aiOperationId, 0.4, f"Calling AI for {opType.name}") self.services.chat.progressLogUpdate(aiOperationId, 0.4, f"Calling AI for {opType.name}")
request = AiCallRequest( request = AiCallRequest(
prompt=prompt, # Pass raw JSON prompt unchanged - connector will parse it prompt=prompt, # Pass raw JSON prompt unchanged - connector will parse it
@ -658,19 +658,19 @@ Respond with ONLY a JSON object in this exact format:
# Extract result from response # Extract result from response
if response.content: if response.content:
# Emit stats for web operation # Emit stats for web operation
self.services.workflow.storeWorkflowStat( self.services.chat.storeWorkflowStat(
self.services.currentWorkflow, self.services.workflow,
response, response,
f"ai.{opType.name.lower()}" f"ai.{opType.name.lower()}"
) )
self.services.workflow.progressLogUpdate(aiOperationId, 0.9, f"{opType.name} completed") self.services.chat.progressLogUpdate(aiOperationId, 0.9, f"{opType.name} completed")
self.services.workflow.progressLogFinish(aiOperationId, True) self.services.chat.progressLogFinish(aiOperationId, True)
return response.content return response.content
else: else:
errorMsg = f"No content returned from {opType.name}: {response.content}" errorMsg = f"No content returned from {opType.name}: {response.content}"
logger.error(f"Error in {opType.name}: {errorMsg}") logger.error(f"Error in {opType.name}: {errorMsg}")
self.services.workflow.progressLogFinish(aiOperationId, False) self.services.chat.progressLogFinish(aiOperationId, False)
return {"success": False, "error": errorMsg} return {"success": False, "error": errorMsg}
# CRITICAL: For document generation with JSON templates, NEVER compress the prompt # CRITICAL: For document generation with JSON templates, NEVER compress the prompt
@ -685,13 +685,13 @@ Respond with ONLY a JSON object in this exact format:
if outputFormat: if outputFormat:
# Use unified generation method for all document generation # Use unified generation method for all document generation
if documents and len(documents) > 0: if documents and len(documents) > 0:
self.services.workflow.progressLogUpdate(aiOperationId, 0.2, f"Extracting content from {len(documents)} documents") self.services.chat.progressLogUpdate(aiOperationId, 0.2, f"Extracting content from {len(documents)} documents")
extracted_content = await self.callAiText(prompt, documents, options, aiOperationId) extracted_content = await self.callAiText(prompt, documents, options, aiOperationId)
else: else:
self.services.workflow.progressLogUpdate(aiOperationId, 0.2, "Preparing for direct generation") self.services.chat.progressLogUpdate(aiOperationId, 0.2, "Preparing for direct generation")
extracted_content = None extracted_content = None
self.services.workflow.progressLogUpdate(aiOperationId, 0.3, "Building generation prompt") self.services.chat.progressLogUpdate(aiOperationId, 0.3, "Building generation prompt")
from modules.services.serviceGeneration.subPromptBuilderGeneration import buildGenerationPrompt from modules.services.serviceGeneration.subPromptBuilderGeneration import buildGenerationPrompt
# First call without continuation context # First call without continuation context
generation_prompt = await buildGenerationPrompt(outputFormat, prompt, title, extracted_content, None) generation_prompt = await buildGenerationPrompt(outputFormat, prompt, title, extracted_content, None)
@ -704,7 +704,7 @@ Respond with ONLY a JSON object in this exact format:
"extracted_content": extracted_content "extracted_content": extracted_content
} }
self.services.workflow.progressLogUpdate(aiOperationId, 0.4, "Calling AI for content generation") self.services.chat.progressLogUpdate(aiOperationId, 0.4, "Calling AI for content generation")
generated_json = await self._callAiWithLooping( generated_json = await self._callAiWithLooping(
generation_prompt, generation_prompt,
options, options,
@ -714,7 +714,7 @@ Respond with ONLY a JSON object in this exact format:
aiOperationId aiOperationId
) )
self.services.workflow.progressLogUpdate(aiOperationId, 0.7, "Parsing generated JSON") self.services.chat.progressLogUpdate(aiOperationId, 0.7, "Parsing generated JSON")
# Parse the generated JSON (extract fenced/embedded JSON first) # Parse the generated JSON (extract fenced/embedded JSON first)
try: try:
extracted_json = self.services.utils.jsonExtractString(generated_json) extracted_json = self.services.utils.jsonExtractString(generated_json)
@ -728,10 +728,10 @@ Respond with ONLY a JSON object in this exact format:
# Write the problematic JSON to debug file # Write the problematic JSON to debug file
self.services.utils.writeDebugFile(generated_json, "failed_json_parsing") self.services.utils.writeDebugFile(generated_json, "failed_json_parsing")
self.services.workflow.progressLogFinish(aiOperationId, False) self.services.chat.progressLogFinish(aiOperationId, False)
return {"success": False, "error": f"Generated content is not valid JSON: {str(e)}"} return {"success": False, "error": f"Generated content is not valid JSON: {str(e)}"}
self.services.workflow.progressLogUpdate(aiOperationId, 0.8, f"Rendering to {outputFormat} format") self.services.chat.progressLogUpdate(aiOperationId, 0.8, f"Rendering to {outputFormat} format")
# Render to final format using the existing renderer # Render to final format using the existing renderer
try: try:
from modules.services.serviceGeneration.mainServiceGeneration import GenerationService from modules.services.serviceGeneration.mainServiceGeneration import GenerationService
@ -761,16 +761,16 @@ Respond with ONLY a JSON object in this exact format:
# Log AI response for debugging # Log AI response for debugging
self.services.utils.writeDebugFile(str(result), "document_generation_response", documents) self.services.utils.writeDebugFile(str(result), "document_generation_response", documents)
self.services.workflow.progressLogFinish(aiOperationId, True) self.services.chat.progressLogFinish(aiOperationId, True)
return result return result
except Exception as e: except Exception as e:
logger.error(f"Error rendering document: {str(e)}") logger.error(f"Error rendering document: {str(e)}")
self.services.workflow.progressLogFinish(aiOperationId, False) self.services.chat.progressLogFinish(aiOperationId, False)
return {"success": False, "error": f"Rendering failed: {str(e)}"} return {"success": False, "error": f"Rendering failed: {str(e)}"}
# Handle text calls (no output format specified) # Handle text calls (no output format specified)
self.services.workflow.progressLogUpdate(aiOperationId, 0.5, "Processing text call") self.services.chat.progressLogUpdate(aiOperationId, 0.5, "Processing text call")
if documents: if documents:
# Use document processing for text calls with documents # Use document processing for text calls with documents
result = await self.callAiText(prompt, documents, options, aiOperationId) result = await self.callAiText(prompt, documents, options, aiOperationId)
@ -778,12 +778,12 @@ Respond with ONLY a JSON object in this exact format:
# Use shared core function for direct text calls # Use shared core function for direct text calls
result = await self._callAiWithLooping(prompt, options, "text", None, None, aiOperationId) result = await self._callAiWithLooping(prompt, options, "text", None, None, aiOperationId)
self.services.workflow.progressLogFinish(aiOperationId, True) self.services.chat.progressLogFinish(aiOperationId, True)
return result return result
except Exception as e: except Exception as e:
logger.error(f"Error in callAiDocuments: {str(e)}") logger.error(f"Error in callAiDocuments: {str(e)}")
self.services.workflow.progressLogFinish(aiOperationId, False) self.services.chat.progressLogFinish(aiOperationId, False)
raise raise
async def callAiText( async def callAiText(

View file

@ -8,13 +8,13 @@ from modules.shared.progressLogger import ProgressLogger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WorkflowService: class ChatService:
"""Service class containing methods for document processing, chat operations, and workflow management""" """Service class containing methods for document processing, chat operations, and workflow management"""
def __init__(self, serviceCenter): def __init__(self, serviceCenter):
self.services = serviceCenter self.services = serviceCenter
self.user = serviceCenter.user self.user = serviceCenter.user
self.workflow = serviceCenter.workflow # self.services.workflow is now the ChatWorkflow object (stable during workflow execution)
self.interfaceDbChat = serviceCenter.interfaceDbChat self.interfaceDbChat = serviceCenter.interfaceDbChat
self.interfaceDbComponent = serviceCenter.interfaceDbComponent self.interfaceDbComponent = serviceCenter.interfaceDbComponent
self.interfaceDbApp = serviceCenter.interfaceDbApp self.interfaceDbApp = serviceCenter.interfaceDbApp
@ -23,11 +23,16 @@ class WorkflowService:
def getChatDocumentsFromDocumentList(self, documentList: List[str]) -> List[ChatDocument]: def getChatDocumentsFromDocumentList(self, documentList: List[str]) -> List[ChatDocument]:
"""Get ChatDocuments from a list of document references using all three formats.""" """Get ChatDocuments from a list of document references using all three formats."""
try: try:
workflow = self.services.currentWorkflow # Use self.services.workflow which is the ChatWorkflow object (stable during workflow execution)
workflow_id = workflow.id if workflow and hasattr(workflow, 'id') else 'NO_ID' workflow = self.services.workflow
workflow_obj_id = id(workflow) if workflow else None if not workflow:
logger.error("getChatDocumentsFromDocumentList: No workflow available (self.services.workflow is not set)")
return []
workflow_id = workflow.id if hasattr(workflow, 'id') else 'NO_ID'
workflow_obj_id = id(workflow)
logger.debug(f"getChatDocumentsFromDocumentList: input documentList = {documentList}") logger.debug(f"getChatDocumentsFromDocumentList: input documentList = {documentList}")
logger.debug(f"getChatDocumentsFromDocumentList: currentWorkflow.id = {workflow_id}, workflow object id = {workflow_obj_id}") logger.debug(f"getChatDocumentsFromDocumentList: using workflow.id = {workflow_id}, workflow object id = {workflow_obj_id}")
# Debug: list available messages with their labels and document names # Debug: list available messages with their labels and document names
try: try:
@ -59,6 +64,11 @@ class WorkflowService:
doc_id = parts[1] doc_id = parts[1]
# Find the document by ID # Find the document by ID
for message in workflow.messages: for message in workflow.messages:
# Validate message belongs to this workflow
msg_workflow_id = getattr(message, 'workflowId', None)
if msg_workflow_id and msg_workflow_id != workflow_id:
continue
if message.documents: if message.documents:
for doc in message.documents: for doc in message.documents:
if doc.id == doc_id: if doc.id == doc_id:
@ -75,6 +85,11 @@ class WorkflowService:
# First try to find the message by ID in the current workflow # First try to find the message by ID in the current workflow
message_found = None message_found = None
for message in workflow.messages: for message in workflow.messages:
# Validate message belongs to this workflow
msg_workflow_id = getattr(message, 'workflowId', None)
if msg_workflow_id and msg_workflow_id != workflow_id:
continue
if str(message.id) == message_id: if str(message.id) == message_id:
message_found = message message_found = message
break break
@ -96,6 +111,12 @@ class WorkflowService:
label = parts[1] label = parts[1]
message_found = None message_found = None
for message in workflow.messages: for message in workflow.messages:
# Validate message belongs to this workflow
msg_workflow_id = getattr(message, 'workflowId', None)
if msg_workflow_id and msg_workflow_id != workflow_id:
logger.warning(f"Message {message.id} has workflowId {msg_workflow_id} but belongs to workflow {workflow_id}. Skipping.")
continue
msg_label = getattr(message, 'documentsLabel', None) msg_label = getattr(message, 'documentsLabel', None)
if msg_label == label: if msg_label == label:
message_found = message message_found = message
@ -120,6 +141,12 @@ class WorkflowService:
# In case of retries, we want the NEWEST message (most recent publishedAt) # In case of retries, we want the NEWEST message (most recent publishedAt)
matching_messages = [] matching_messages = []
for message in workflow.messages: for message in workflow.messages:
# Validate message belongs to this workflow
msg_workflow_id = getattr(message, 'workflowId', None)
if msg_workflow_id and msg_workflow_id != workflow_id:
logger.debug(f"Skipping message {message.id} with workflowId {msg_workflow_id} (expected {workflow_id})")
continue
msg_documents_label = getattr(message, 'documentsLabel', '') msg_documents_label = getattr(message, 'documentsLabel', '')
# Check if this message's documentsLabel matches our reference # Check if this message's documentsLabel matches our reference
@ -358,10 +385,13 @@ class WorkflowService:
def getWorkflowContext(self) -> Dict[str, int]: def getWorkflowContext(self) -> Dict[str, int]:
"""Get current workflow context for document generation""" """Get current workflow context for document generation"""
try: try:
workflow = self.services.workflow
if not workflow:
return {'currentRound': 0, 'currentTask': 0, 'currentAction': 0}
return { return {
'currentRound': self.workflow.currentRound if hasattr(self.workflow, 'currentRound') else 0, 'currentRound': workflow.currentRound if hasattr(workflow, 'currentRound') else 0,
'currentTask': self.workflow.currentTask if hasattr(self.workflow, 'currentTask') else 0, 'currentTask': workflow.currentTask if hasattr(workflow, 'currentTask') else 0,
'currentAction': self.workflow.currentAction if hasattr(self.workflow, 'currentAction') else 0 'currentAction': workflow.currentAction if hasattr(workflow, 'currentAction') else 0
} }
except Exception as e: except Exception as e:
logger.error(f"Error getting workflow context: {str(e)}") logger.error(f"Error getting workflow context: {str(e)}")
@ -370,7 +400,10 @@ class WorkflowService:
def setWorkflowContext(self, roundNumber: int = None, taskNumber: int = None, actionNumber: int = None): def setWorkflowContext(self, roundNumber: int = None, taskNumber: int = None, actionNumber: int = None):
"""Set current workflow context for document generation and routing""" """Set current workflow context for document generation and routing"""
try: try:
workflow = self.services.currentWorkflow workflow = self.services.workflow
if not workflow:
logger.error("setWorkflowContext: No workflow available")
return
# Prepare update data # Prepare update data
update_data = {} update_data = {}
@ -396,15 +429,26 @@ class WorkflowService:
def getWorkflowStats(self) -> Dict[str, Any]: def getWorkflowStats(self) -> Dict[str, Any]:
"""Get comprehensive workflow statistics including current context""" """Get comprehensive workflow statistics including current context"""
try: try:
workflow = self.services.workflow
workflow_context = self.getWorkflowContext() workflow_context = self.getWorkflowContext()
if not workflow:
return {
'currentRound': workflow_context['currentRound'],
'currentTask': workflow_context['currentTask'],
'currentAction': workflow_context['currentAction'],
'totalTasks': 0,
'totalActions': 0,
'workflowStatus': 'unknown',
'workflowId': 'unknown'
}
return { return {
'currentRound': workflow_context['currentRound'], 'currentRound': workflow_context['currentRound'],
'currentTask': workflow_context['currentTask'], 'currentTask': workflow_context['currentTask'],
'currentAction': workflow_context['currentAction'], 'currentAction': workflow_context['currentAction'],
'totalTasks': self.workflow.totalTasks if hasattr(self.workflow, 'totalTasks') else 0, 'totalTasks': workflow.totalTasks if hasattr(workflow, 'totalTasks') else 0,
'totalActions': self.workflow.totalActions if hasattr(self.workflow, 'totalActions') else 0, 'totalActions': workflow.totalActions if hasattr(workflow, 'totalActions') else 0,
'workflowStatus': self.workflow.status if hasattr(self.workflow, 'status') else 'unknown', 'workflowStatus': workflow.status if hasattr(workflow, 'status') else 'unknown',
'workflowId': self.workflow.id if hasattr(self.workflow, 'id') else 'unknown' 'workflowId': workflow.id if hasattr(workflow, 'id') else 'unknown'
} }
except Exception as e: except Exception as e:
logger.error(f"Error getting workflow stats: {str(e)}") logger.error(f"Error getting workflow stats: {str(e)}")
@ -532,7 +576,9 @@ class WorkflowService:
def getDocumentCount(self) -> str: def getDocumentCount(self) -> str:
"""Get document count for task planning (matching old handlingTasks.py logic)""" """Get document count for task planning (matching old handlingTasks.py logic)"""
try: try:
workflow = self.services.currentWorkflow workflow = self.services.workflow
if not workflow:
return "No documents available"
# Count documents from all messages in the workflow (like old system) # Count documents from all messages in the workflow (like old system)
total_docs = 0 total_docs = 0
@ -551,7 +597,9 @@ class WorkflowService:
def getWorkflowHistoryContext(self) -> str: def getWorkflowHistoryContext(self) -> str:
"""Get workflow history context for task planning (matching old handlingTasks.py logic)""" """Get workflow history context for task planning (matching old handlingTasks.py logic)"""
try: try:
workflow = self.services.currentWorkflow workflow = self.services.workflow
if not workflow:
return "No previous round context available"
# Check if there are any previous rounds by looking for "first" messages # Check if there are any previous rounds by looking for "first" messages
has_previous_rounds = False has_previous_rounds = False
@ -594,8 +642,7 @@ class WorkflowService:
workflow_id = workflow.id if hasattr(workflow, 'id') else 'NO_ID' workflow_id = workflow.id if hasattr(workflow, 'id') else 'NO_ID'
workflow_obj_id = id(workflow) workflow_obj_id = id(workflow)
current_workflow_obj_id = id(self.services.currentWorkflow) if self.services.currentWorkflow else None logger.debug(f"getAvailableDocuments: workflow.id = {workflow_id}, workflow object id = {workflow_obj_id}")
logger.debug(f"getAvailableDocuments: workflow.id = {workflow_id}, workflow object id = {workflow_obj_id}, currentWorkflow object id = {current_workflow_obj_id}")
# Use the provided workflow object directly to avoid database reload issues # Use the provided workflow object directly to avoid database reload issues
# that can cause filename truncation. The workflow object should already be up-to-date. # that can cause filename truncation. The workflow object should already be up-to-date.
@ -832,22 +879,15 @@ class WorkflowService:
logger.error(f"Error getting connection reference list: {str(e)}") logger.error(f"Error getting connection reference list: {str(e)}")
return [] return []
def setCurrentWorkflow(self, workflow):
"""Set the current workflow reference for this service"""
self.workflow = workflow
# Reset progress logger for new workflow
self._progressLogger = None
def _getProgressLogger(self): def _getProgressLogger(self):
"""Get or create the progress logger instance""" """Get or create the progress logger instance"""
if self._progressLogger is None: if self._progressLogger is None:
# Use currentWorkflow from self.services instead of self.workflow (which is self) self._progressLogger = ProgressLogger(self.services)
workflow = getattr(self.services, 'currentWorkflow', None)
self._progressLogger = ProgressLogger(self, workflow)
return self._progressLogger return self._progressLogger
def createProgressLogger(self, workflow) -> ProgressLogger: def createProgressLogger(self) -> ProgressLogger:
return ProgressLogger(self, workflow) return ProgressLogger(self.services)
def progressLogStart(self, operationId: str, serviceName: str, actionName: str, context: str = ""): def progressLogStart(self, operationId: str, serviceName: str, actionName: str, context: str = ""):
"""Wrapper for ProgressLogger.startOperation""" """Wrapper for ProgressLogger.startOperation"""
@ -862,4 +902,5 @@ class WorkflowService:
def progressLogFinish(self, operationId: str, success: bool = True): def progressLogFinish(self, operationId: str, success: bool = True):
"""Wrapper for ProgressLogger.finishOperation""" """Wrapper for ProgressLogger.finishOperation"""
progressLogger = self._getProgressLogger() progressLogger = self._getProgressLogger()
return progressLogger.finishOperation(operationId, success) return progressLogger.finishOperation(operationId, success)

View file

@ -135,8 +135,8 @@ class ExtractionService:
errorCount=0 errorCount=0
) )
self.services.workflow.storeWorkflowStat( self.services.chat.storeWorkflowStat(
self.services.currentWorkflow, self.services.workflow,
aiResponse, aiResponse,
f"extraction.process.{doc.mimeType}" f"extraction.process.{doc.mimeType}"
) )
@ -422,9 +422,9 @@ class ExtractionService:
# Create operationId if not provided # Create operationId if not provided
if not operationId: if not operationId:
workflowId = self.services.currentWorkflow.id if self.services.currentWorkflow else f"no-workflow-{int(time.time())}" workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
operationId = f"ai_text_extract_{workflowId}_{int(time.time())}" operationId = f"ai_text_extract_{workflowId}_{int(time.time())}"
self.services.workflow.progressLogStart( self.services.chat.progressLogStart(
operationId, operationId,
"AI Text Extract", "AI Text Extract",
"Document Processing", "Document Processing",
@ -452,35 +452,35 @@ class ExtractionService:
# Extract content WITHOUT chunking # Extract content WITHOUT chunking
if operationId: if operationId:
self.services.workflow.progressLogUpdate(operationId, 0.1, f"Extracting content from {len(documents)} documents") self.services.chat.progressLogUpdate(operationId, 0.1, f"Extracting content from {len(documents)} documents")
extractionResult = self.extractContent(documents, extractionOptions) extractionResult = self.extractContent(documents, extractionOptions)
if not isinstance(extractionResult, list): if not isinstance(extractionResult, list):
if operationId: if operationId:
self.services.workflow.progressLogFinish(operationId, False) self.services.chat.progressLogFinish(operationId, False)
return "[Error: No extraction results]" return "[Error: No extraction results]"
# Process parts (not chunks) with model-aware AI calls # Process parts (not chunks) with model-aware AI calls
if operationId: if operationId:
self.services.workflow.progressLogUpdate(operationId, 0.3, f"Processing {len(extractionResult)} extracted content parts") self.services.chat.progressLogUpdate(operationId, 0.3, f"Processing {len(extractionResult)} extracted content parts")
partResults = await self._processPartsWithMapping(extractionResult, prompt, aiObjects, options, operationId) partResults = await self._processPartsWithMapping(extractionResult, prompt, aiObjects, options, operationId)
# Merge results using existing merging system # Merge results using existing merging system
if operationId: if operationId:
self.services.workflow.progressLogUpdate(operationId, 0.9, f"Merging {len(partResults)} part results") self.services.chat.progressLogUpdate(operationId, 0.9, f"Merging {len(partResults)} part results")
mergedContent = self._mergePartResults(partResults, options) mergedContent = self._mergePartResults(partResults, options)
# Save merged extraction content to debug # Save merged extraction content to debug
self.services.utils.writeDebugFile(mergedContent or '', "extraction_merged_text") self.services.utils.writeDebugFile(mergedContent or '', "extraction_merged_text")
if operationId: if operationId:
self.services.workflow.progressLogFinish(operationId, True) self.services.chat.progressLogFinish(operationId, True)
return mergedContent return mergedContent
except Exception as e: except Exception as e:
logger.error(f"Error in processDocumentsPerChunk: {str(e)}") logger.error(f"Error in processDocumentsPerChunk: {str(e)}")
if operationId: if operationId:
self.services.workflow.progressLogFinish(operationId, False) self.services.chat.progressLogFinish(operationId, False)
raise raise
async def _processPartsWithMapping( async def _processPartsWithMapping(
@ -539,21 +539,22 @@ class ExtractionService:
if operationId and totalParts > 0: if operationId and totalParts > 0:
processedCount[0] += 1 processedCount[0] += 1
progress = 0.3 + (processedCount[0] / totalParts * 0.6) # Progress from 0.3 to 0.9 progress = 0.3 + (processedCount[0] / totalParts * 0.6) # Progress from 0.3 to 0.9
self.services.workflow.progressLogUpdate(operationId, progress, f"Processing part {processedCount[0]}/{totalParts}") self.services.chat.progressLogUpdate(operationId, progress, f"Processing part {processedCount[0]}/{totalParts}")
# Create progress callback for chunking # Create progress callback for chunking
def chunkingProgressCallback(chunkProgress: float, status: str): def chunkingProgressCallback(chunkProgress: float, status: str):
"""Callback to log chunking progress as ChatLog entries""" """Callback to log chunking progress as ChatLog entries"""
if self.services.workflow and self.services.currentWorkflow: workflow = self.services.workflow
if workflow:
logData = { logData = {
"workflowId": self.services.currentWorkflow.id, "workflowId": workflow.id,
"message": "Service AI", "message": "Service AI",
"type": "info", "type": "info",
"status": status, "status": status,
"progress": chunkProgress "progress": chunkProgress
} }
try: try:
self.services.workflow.storeLog(self.services.currentWorkflow, logData) self.services.chat.storeLog(workflow, logData)
except Exception as e: except Exception as e:
logger.warning(f"Failed to store chunking progress log: {e}") logger.warning(f"Failed to store chunking progress log: {e}")

View file

@ -1,10 +1,7 @@
import logging import logging
import uuid import uuid
import time from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union, Tuple
from modules.datamodels.datamodelChat import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
from modules.datamodels.datamodelAi import AiCallResponse
from modules.aicore.aicoreModelRegistry import modelRegistry
from modules.services.serviceGeneration.subDocumentUtility import ( from modules.services.serviceGeneration.subDocumentUtility import (
getFileExtension, getFileExtension,
getMimeTypeFromExtension, getMimeTypeFromExtension,
@ -19,11 +16,10 @@ class GenerationService:
def __init__(self, serviceCenter=None): def __init__(self, serviceCenter=None):
# Directly use interfaces from the provided service center (no self.service calls) # Directly use interfaces from the provided service center (no self.service calls)
self.services = serviceCenter self.services = serviceCenter
self.interfaceDbComponent = getattr(serviceCenter, 'interfaceDbComponent', None) if serviceCenter else None self.interfaceDbComponent = serviceCenter.interfaceDbComponent
self.interfaceDbChat = getattr(serviceCenter, 'interfaceDbChat', None) if serviceCenter else None self.interfaceDbChat = serviceCenter.interfaceDbChat
self.workflow = getattr(serviceCenter, 'workflow', None) if serviceCenter else None
def processActionResultDocuments(self, action_result, action, workflow) -> List[Dict[str, Any]]: def processActionResultDocuments(self, actionResult, action) -> List[Dict[str, Any]]:
""" """
Process documents produced by AI actions and convert them to ChatDocument format. Process documents produced by AI actions and convert them to ChatDocument format.
This function handles AI-generated document data, not document references. This function handles AI-generated document data, not document references.
@ -31,7 +27,7 @@ class GenerationService:
""" """
try: try:
# Read documents from the standard documents field (not data.documents) # Read documents from the standard documents field (not data.documents)
documents = action_result.documents if action_result and hasattr(action_result, 'documents') else [] documents = actionResult.documents if actionResult and hasattr(actionResult, 'documents') else []
if not documents: if not documents:
return [] return []
@ -69,13 +65,13 @@ class GenerationService:
logger.error(f"Error processing single document: {str(e)}") logger.error(f"Error processing single document: {str(e)}")
return None return None
def createDocumentsFromActionResult(self, action_result, action, workflow, message_id=None) -> List[Any]: def createDocumentsFromActionResult(self, actionResult, action, workflow, message_id=None) -> List[Any]:
""" """
Create actual document objects from action result and store them in the system. Create actual document objects from action result and store them in the system.
Returns a list of created document objects with proper workflow context. Returns a list of created document objects with proper workflow context.
""" """
try: try:
processed_docs = self.processActionResultDocuments(action_result, action, workflow) processed_docs = self.processActionResultDocuments(actionResult, action)
createdDocuments = [] createdDocuments = []
for i, doc_data in enumerate(processed_docs): for i, doc_data in enumerate(processed_docs):

View file

@ -47,7 +47,7 @@ class WebService:
""" """
try: try:
# Step 1: AI intention analysis - extract URLs and parameters from prompt # Step 1: AI intention analysis - extract URLs and parameters from prompt
self.services.workflow.progressLogUpdate(operationId, 0.1, "Analyzing research intent") self.services.chat.progressLogUpdate(operationId, 0.1, "Analyzing research intent")
analysisResult = await self._analyzeResearchIntent(prompt, urls, country, language, researchDepth) analysisResult = await self._analyzeResearchIntent(prompt, urls, country, language, researchDepth)
@ -72,7 +72,7 @@ class WebService:
# Step 2: Search for URLs if needed (based on needsSearch flag) # Step 2: Search for URLs if needed (based on needsSearch flag)
if needsSearch and (not allUrls or len(allUrls) < maxNumberPages): if needsSearch and (not allUrls or len(allUrls) < maxNumberPages):
self.services.workflow.progressLogUpdate(operationId, 0.3, "Searching for URLs") self.services.chat.progressLogUpdate(operationId, 0.3, "Searching for URLs")
searchUrls = await self._performWebSearch( searchUrls = await self._performWebSearch(
instruction=instruction, instruction=instruction,
@ -84,7 +84,7 @@ class WebService:
# Add search URLs to the list # Add search URLs to the list
allUrls.extend(searchUrls) allUrls.extend(searchUrls)
self.services.workflow.progressLogUpdate(operationId, 0.5, f"Found {len(allUrls)} total URLs") self.services.chat.progressLogUpdate(operationId, 0.5, f"Found {len(allUrls)} total URLs")
# Step 3: Filter to maxNumberPages (simple cut, no intelligent filtering) # Step 3: Filter to maxNumberPages (simple cut, no intelligent filtering)
if len(allUrls) > maxNumberPages: if len(allUrls) > maxNumberPages:
@ -99,7 +99,7 @@ class WebService:
maxDepth = depthMap.get(finalResearchDepth.lower(), 2) maxDepth = depthMap.get(finalResearchDepth.lower(), 2)
# Step 5: Crawl all URLs # Step 5: Crawl all URLs
self.services.workflow.progressLogUpdate(operationId, 0.6, f"Crawling {len(allUrls)} URLs") self.services.chat.progressLogUpdate(operationId, 0.6, f"Crawling {len(allUrls)} URLs")
crawlResult = await self._performWebCrawl( crawlResult = await self._performWebCrawl(
instruction=instruction, instruction=instruction,
@ -107,7 +107,7 @@ class WebService:
maxDepth=maxDepth maxDepth=maxDepth
) )
self.services.workflow.progressLogUpdate(operationId, 0.9, "Consolidating results") self.services.chat.progressLogUpdate(operationId, 0.9, "Consolidating results")
# Return consolidated result # Return consolidated result
result = { result = {

View file

@ -13,15 +13,13 @@ logger = logging.getLogger(__name__)
class ProgressLogger: class ProgressLogger:
"""Centralized progress logger for workflow operations.""" """Centralized progress logger for workflow operations."""
def __init__(self, workflowService, workflow): def __init__(self, services):
"""Initialize progress logger. """Initialize progress logger.
Args: Args:
workflowService: WorkflowService instance for logging services: Services object for accessing chat service and workflow
workflow: Workflow object to get workflowId from
""" """
self.workflowService = workflowService self.services = services
self.workflow = workflow
self.activeOperations = {} self.activeOperations = {}
self.finishedOperations = set() # Track finished operations to avoid repeated warnings self.finishedOperations = set() # Track finished operations to avoid repeated warnings
@ -115,8 +113,13 @@ class ProgressLogger:
op = self.activeOperations[operationId] op = self.activeOperations[operationId]
message = f"Service {op['service']}" message = f"Service {op['service']}"
workflow = self.services.workflow
if not workflow:
logger.warning(f"Cannot log progress: no workflow available")
return
logData = { logData = {
"workflowId": self.workflow.id, "workflowId": workflow.id,
"message": message, "message": message,
"type": "info", "type": "info",
"status": status, "status": status,
@ -124,7 +127,7 @@ class ProgressLogger:
} }
try: try:
self.workflowService.storeLog(self.workflow, logData) self.services.chat.storeLog(workflow, logData)
except Exception as e: except Exception as e:
logger.error(f"Failed to store progress log: {e}") logger.error(f"Failed to store progress log: {e}")

View file

@ -43,11 +43,11 @@ class MethodAi(MethodBase):
""" """
try: try:
# Init progress logger # Init progress logger
workflowId = self.services.currentWorkflow.id if self.services.currentWorkflow else f"no-workflow-{int(time.time())}" workflowId = self.services.workflow.id if self.services.workflow else f"no-workflow-{int(time.time())}"
operationId = f"ai_process_{workflowId}_{int(time.time())}" operationId = f"ai_process_{workflowId}_{int(time.time())}"
# Start progress tracking # Start progress tracking
self.services.workflow.progressLogStart( self.services.chat.progressLogStart(
operationId, operationId,
"Generate", "Generate",
"AI Processing", "AI Processing",
@ -58,7 +58,7 @@ class MethodAi(MethodBase):
logger.info(f"aiPrompt extracted: '{aiPrompt}' (type: {type(aiPrompt)})") logger.info(f"aiPrompt extracted: '{aiPrompt}' (type: {type(aiPrompt)})")
# Update progress - preparing parameters # Update progress - preparing parameters
self.services.workflow.progressLogUpdate(operationId, 0.2, "Preparing parameters") self.services.chat.progressLogUpdate(operationId, 0.2, "Preparing parameters")
documentList = parameters.get("documentList", []) documentList = parameters.get("documentList", [])
if isinstance(documentList, str): if isinstance(documentList, str):
@ -79,17 +79,17 @@ class MethodAi(MethodBase):
logger.info(f"Using result type: {resultType} -> {output_extension}") logger.info(f"Using result type: {resultType} -> {output_extension}")
# Update progress - preparing documents # Update progress - preparing documents
self.services.workflow.progressLogUpdate(operationId, 0.3, "Preparing documents") self.services.chat.progressLogUpdate(operationId, 0.3, "Preparing documents")
# Get ChatDocuments for AI service - let AI service handle all document processing # Get ChatDocuments for AI service - let AI service handle all document processing
chatDocuments = [] chatDocuments = []
if documentList: if documentList:
chatDocuments = self.services.workflow.getChatDocumentsFromDocumentList(documentList) chatDocuments = self.services.chat.getChatDocumentsFromDocumentList(documentList)
if chatDocuments: if chatDocuments:
logger.info(f"Prepared {len(chatDocuments)} documents for AI processing") logger.info(f"Prepared {len(chatDocuments)} documents for AI processing")
# Update progress - preparing AI call # Update progress - preparing AI call
self.services.workflow.progressLogUpdate(operationId, 0.4, "Preparing AI call") self.services.chat.progressLogUpdate(operationId, 0.4, "Preparing AI call")
# Build options with only resultFormat - let service layer handle all other parameters # Build options with only resultFormat - let service layer handle all other parameters
output_format = output_extension.replace('.', '') or 'txt' output_format = output_extension.replace('.', '') or 'txt'
@ -99,7 +99,7 @@ class MethodAi(MethodBase):
) )
# Update progress - calling AI # Update progress - calling AI
self.services.workflow.progressLogUpdate(operationId, 0.6, "Calling AI") self.services.chat.progressLogUpdate(operationId, 0.6, "Calling AI")
result = await self.services.ai.callAiDocuments( result = await self.services.ai.callAiDocuments(
prompt=aiPrompt, prompt=aiPrompt,
@ -109,7 +109,7 @@ class MethodAi(MethodBase):
) )
# Update progress - processing result # Update progress - processing result
self.services.workflow.progressLogUpdate(operationId, 0.8, "Processing result") self.services.chat.progressLogUpdate(operationId, 0.8, "Processing result")
from modules.datamodels.datamodelChat import ActionDocument from modules.datamodels.datamodelChat import ActionDocument
@ -147,7 +147,7 @@ class MethodAi(MethodBase):
final_documents = [action_document] final_documents = [action_document]
# Complete progress tracking # Complete progress tracking
self.services.workflow.progressLogFinish(operationId, True) self.services.chat.progressLogFinish(operationId, True)
return ActionResult.isSuccess(documents=final_documents) return ActionResult.isSuccess(documents=final_documents)
@ -156,7 +156,7 @@ class MethodAi(MethodBase):
# Complete progress tracking with failure # Complete progress tracking with failure
try: try:
self.services.workflow.progressLogFinish(operationId, False) self.services.chat.progressLogFinish(operationId, False)
except: except:
pass # Don't fail on progress logging errors pass # Don't fail on progress logging errors
@ -186,10 +186,10 @@ class MethodAi(MethodBase):
return ActionResult.isFailure(error="Research prompt is required") return ActionResult.isFailure(error="Research prompt is required")
# Init progress logger # Init progress logger
operationId = f"web_research_{self.services.currentWorkflow.id}_{int(time.time())}" operationId = f"web_research_{self.services.workflow.id}_{int(time.time())}"
# Start progress tracking # Start progress tracking
self.services.workflow.progressLogStart( self.services.chat.progressLogStart(
operationId, operationId,
"Web Research", "Web Research",
"Searching and Crawling", "Searching and Crawling",
@ -207,7 +207,7 @@ class MethodAi(MethodBase):
) )
# Complete progress tracking # Complete progress tracking
self.services.workflow.progressLogFinish(operationId, True) self.services.chat.progressLogFinish(operationId, True)
# Get meaningful filename from research result (generated by intent analyzer) # Get meaningful filename from research result (generated by intent analyzer)
suggestedFilename = result.get("suggested_filename") suggestedFilename = result.get("suggested_filename")
@ -249,7 +249,7 @@ class MethodAi(MethodBase):
except Exception as e: except Exception as e:
logger.error(f"Error in web research: {str(e)}") logger.error(f"Error in web research: {str(e)}")
try: try:
self.services.workflow.progressLogFinish(operationId, False) self.services.chat.progressLogFinish(operationId, False)
except: except:
pass pass
return ActionResult.isFailure(error=str(e)) return ActionResult.isFailure(error=str(e))

View file

@ -187,7 +187,7 @@ class MethodBase:
try: try:
# Get workflow context from services if not provided # Get workflow context from services if not provided
if workflow_context is None and hasattr(self.services, 'workflow'): if workflow_context is None and hasattr(self.services, 'workflow'):
workflow_context = self.services.workflow.getWorkflowContext() workflow_context = self.services.chat.getWorkflowContext()
# Extract round, task, action numbers # Extract round, task, action numbers
round_num = workflow_context.get('currentRound', 0) if workflow_context else 0 round_num = workflow_context.get('currentRound', 0) if workflow_context else 0

View file

@ -36,7 +36,7 @@ class MethodOutlook(MethodBase):
logger.debug(f"Getting Microsoft connection for reference: {connectionReference}") logger.debug(f"Getting Microsoft connection for reference: {connectionReference}")
# Get the connection from the service # Get the connection from the service
userConnection = self.services.workflow.getUserConnectionFromConnectionReference(connectionReference) userConnection = self.services.chat.getUserConnectionFromConnectionReference(connectionReference)
if not userConnection: if not userConnection:
logger.error(f"Connection not found: {connectionReference}") logger.error(f"Connection not found: {connectionReference}")
return None return None
@ -44,7 +44,7 @@ class MethodOutlook(MethodBase):
logger.debug(f"Found connection: {userConnection.id}, status: {userConnection.status.value}, authority: {userConnection.authority.value}") logger.debug(f"Found connection: {userConnection.id}, status: {userConnection.status.value}, authority: {userConnection.authority.value}")
# Get a fresh token for this connection # Get a fresh token for this connection
token = self.services.workflow.getFreshConnectionToken(userConnection.id) token = self.services.chat.getFreshConnectionToken(userConnection.id)
if not token: if not token:
logger.error(f"Fresh token not found for connection: {userConnection.id}") logger.error(f"Fresh token not found for connection: {userConnection.id}")
logger.debug(f"Connection details: {userConnection}") logger.debug(f"Connection details: {userConnection}")
@ -1136,7 +1136,7 @@ class MethodOutlook(MethodBase):
# Prepare documents for AI processing # Prepare documents for AI processing
chatDocuments = [] chatDocuments = []
if documentList: if documentList:
chatDocuments = self.services.workflow.getChatDocumentsFromDocumentList(documentList) chatDocuments = self.services.chat.getChatDocumentsFromDocumentList(documentList)
# Create AI prompt for email composition # Create AI prompt for email composition
# Build document reference list for AI with expanded list contents when possible # Build document reference list for AI with expanded list contents when possible
@ -1144,13 +1144,12 @@ class MethodOutlook(MethodBase):
doc_list_text = "" doc_list_text = ""
if doc_references: if doc_references:
lines = ["Available_Document_References:"] lines = ["Available_Document_References:"]
workflow_obj = getattr(self.services, 'currentWorkflow', None)
for ref in doc_references: for ref in doc_references:
# Each item is a label: resolve to its document list and render contained items # Each item is a label: resolve to its document list and render contained items
list_docs = self.services.workflow.getChatDocumentsFromDocumentList([ref]) or [] list_docs = self.services.chat.getChatDocumentsFromDocumentList([ref]) or []
if list_docs: if list_docs:
for d in list_docs: for d in list_docs:
doc_ref_label = self.services.workflow.getDocumentReferenceFromChatDocument(d) doc_ref_label = self.services.chat.getDocumentReferenceFromChatDocument(d)
lines.append(f"- {doc_ref_label}") lines.append(f"- {doc_ref_label}")
else: else:
lines.append(" - (no documents)") lines.append(" - (no documents)")
@ -1216,7 +1215,7 @@ Return JSON:
if documentList: if documentList:
try: try:
available_refs = [documentList] if isinstance(documentList, str) else documentList available_refs = [documentList] if isinstance(documentList, str) else documentList
available_docs = self.services.workflow.getChatDocumentsFromDocumentList(available_refs) or [] available_docs = self.services.chat.getChatDocumentsFromDocumentList(available_refs) or []
except Exception: except Exception:
available_docs = [] available_docs = []
@ -1229,7 +1228,7 @@ Return JSON:
if ai_attachments: if ai_attachments:
try: try:
ai_refs = [ai_attachments] if isinstance(ai_attachments, str) else ai_attachments ai_refs = [ai_attachments] if isinstance(ai_attachments, str) else ai_attachments
ai_docs = self.services.workflow.getChatDocumentsFromDocumentList(ai_refs) or [] ai_docs = self.services.chat.getChatDocumentsFromDocumentList(ai_refs) or []
except Exception: except Exception:
ai_docs = [] ai_docs = []
@ -1239,15 +1238,15 @@ Return JSON:
if selected_docs: if selected_docs:
# Map selected ChatDocuments back to docItem references # Map selected ChatDocuments back to docItem references
documentList = [self.services.workflow.getDocumentReferenceFromChatDocument(d) for d in selected_docs] documentList = [self.services.chat.getDocumentReferenceFromChatDocument(d) for d in selected_docs]
logger.info(f"AI selected {len(documentList)} documents for attachment (resolved via ChatDocuments)") logger.info(f"AI selected {len(documentList)} documents for attachment (resolved via ChatDocuments)")
else: else:
# No intersection; use all available documents # No intersection; use all available documents
documentList = [self.services.workflow.getDocumentReferenceFromChatDocument(d) for d in available_docs] documentList = [self.services.chat.getDocumentReferenceFromChatDocument(d) for d in available_docs]
logger.warning("AI selected attachments not found in available documents, using all documents") logger.warning("AI selected attachments not found in available documents, using all documents")
else: else:
# No AI selection; use all available documents # No AI selection; use all available documents
documentList = [self.services.workflow.getDocumentReferenceFromChatDocument(d) for d in available_docs] documentList = [self.services.chat.getDocumentReferenceFromChatDocument(d) for d in available_docs]
logger.warning("AI did not specify attachments, using all available documents") logger.warning("AI did not specify attachments, using all available documents")
else: else:
logger.info("No documents provided in documentList; skipping attachment processing") logger.info("No documents provided in documentList; skipping attachment processing")
@ -1297,13 +1296,13 @@ Return JSON:
message["attachments"] = [] message["attachments"] = []
for attachment_ref in documentList: for attachment_ref in documentList:
# Get attachment document from service center # Get attachment document from service center
attachment_docs = self.services.workflow.getChatDocumentsFromDocumentList([attachment_ref]) attachment_docs = self.services.chat.getChatDocumentsFromDocumentList([attachment_ref])
if attachment_docs: if attachment_docs:
for doc in attachment_docs: for doc in attachment_docs:
file_id = getattr(doc, 'fileId', None) file_id = getattr(doc, 'fileId', None)
if file_id: if file_id:
try: try:
file_content = self.services.workflow.getFileData(file_id) file_content = self.services.chat.getFileData(file_id)
if file_content: if file_content:
if isinstance(file_content, bytes): if isinstance(file_content, bytes):
content_bytes = file_content content_bytes = file_content

View file

@ -31,7 +31,7 @@ class MethodSharepoint(MethodBase):
def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]: def _getMicrosoftConnection(self, connectionReference: str) -> Optional[Dict[str, Any]]:
"""Get Microsoft connection from connection reference and configure SharePoint service""" """Get Microsoft connection from connection reference and configure SharePoint service"""
try: try:
userConnection = self.services.workflow.getUserConnectionFromConnectionReference(connectionReference) userConnection = self.services.chat.getUserConnectionFromConnectionReference(connectionReference)
if not userConnection: if not userConnection:
logger.warning(f"No user connection found for reference: {connectionReference}") logger.warning(f"No user connection found for reference: {connectionReference}")
return None return None
@ -847,13 +847,13 @@ class MethodSharepoint(MethodBase):
try: try:
import json import json
# Resolve the reference label to get the actual document list # Resolve the reference label to get the actual document list
document_list = self.services.workflow.getChatDocumentsFromDocumentList([pathObject]) document_list = self.services.chat.getChatDocumentsFromDocumentList([pathObject])
if not document_list or len(document_list) == 0: if not document_list or len(document_list) == 0:
return ActionResult.isFailure(error=f"No document list found for reference: {pathObject}") return ActionResult.isFailure(error=f"No document list found for reference: {pathObject}")
# Get the first document's content (which should be the JSON) # Get the first document's content (which should be the JSON)
first_document = document_list[0] first_document = document_list[0]
file_data = self.services.workflow.getFileData(first_document.fileId) file_data = self.services.chat.getFileData(first_document.fileId)
if not file_data: if not file_data:
return ActionResult.isFailure(error=f"No file data found for document: {pathObject}") return ActionResult.isFailure(error=f"No file data found for document: {pathObject}")
@ -881,7 +881,7 @@ class MethodSharepoint(MethodBase):
# Get documents from reference - ensure documentList is a list, not a string # Get documents from reference - ensure documentList is a list, not a string
# documentList is already normalized above # documentList is already normalized above
chatDocuments = self.services.workflow.getChatDocumentsFromDocumentList(documentList) chatDocuments = self.services.chat.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return ActionResult.isFailure(error="No documents found for the provided reference") return ActionResult.isFailure(error="No documents found for the provided reference")
@ -1127,13 +1127,13 @@ class MethodSharepoint(MethodBase):
try: try:
import json import json
# Resolve the reference label to get the actual document list # Resolve the reference label to get the actual document list
document_list = self.services.workflow.getChatDocumentsFromDocumentList([pathObject]) document_list = self.services.chat.getChatDocumentsFromDocumentList([pathObject])
if not document_list or len(document_list) == 0: if not document_list or len(document_list) == 0:
return ActionResult.isFailure(error=f"No document list found for reference: {pathObject}") return ActionResult.isFailure(error=f"No document list found for reference: {pathObject}")
# Get the first document's content (which should be the JSON) # Get the first document's content (which should be the JSON)
first_document = document_list[0] first_document = document_list[0]
file_data = self.services.workflow.getFileData(first_document.fileId) file_data = self.services.chat.getFileData(first_document.fileId)
if not file_data: if not file_data:
return ActionResult.isFailure(error=f"No file data found for document: {pathObject}") return ActionResult.isFailure(error=f"No file data found for document: {pathObject}")
@ -1228,7 +1228,7 @@ class MethodSharepoint(MethodBase):
# Get documents from reference - ensure documentList is a list, not a string # Get documents from reference - ensure documentList is a list, not a string
if isinstance(documentList, str): if isinstance(documentList, str):
documentList = [documentList] # Convert string to list documentList = [documentList] # Convert string to list
chatDocuments = self.services.workflow.getChatDocumentsFromDocumentList(documentList) chatDocuments = self.services.chat.getChatDocumentsFromDocumentList(documentList)
if not chatDocuments: if not chatDocuments:
return ActionResult.isFailure(error="No documents found for the provided reference") return ActionResult.isFailure(error="No documents found for the provided reference")
@ -1318,7 +1318,7 @@ class MethodSharepoint(MethodBase):
for i, (chatDocument, fileName) in enumerate(zip(chatDocuments, fileNames)): for i, (chatDocument, fileName) in enumerate(zip(chatDocuments, fileNames)):
try: try:
fileId = chatDocument.fileId fileId = chatDocument.fileId
file_data = self.services.workflow.getFileData(fileId) file_data = self.services.chat.getFileData(fileId)
if not file_data: if not file_data:
logger.warning(f"File data not found for fileId: {fileId}") logger.warning(f"File data not found for fileId: {fileId}")
@ -1481,14 +1481,14 @@ class MethodSharepoint(MethodBase):
try: try:
import json import json
# Resolve the reference label to get the actual document list # Resolve the reference label to get the actual document list
document_list = self.services.workflow.getChatDocumentsFromDocumentList([pathObject]) document_list = self.services.chat.getChatDocumentsFromDocumentList([pathObject])
if not document_list or len(document_list) == 0: if not document_list or len(document_list) == 0:
return ActionResult.isFailure(error=f"No document list found for reference: {pathObject}") return ActionResult.isFailure(error=f"No document list found for reference: {pathObject}")
# Get the first document's content (which should be the JSON) # Get the first document's content (which should be the JSON)
first_document = document_list[0] first_document = document_list[0]
logger.info(f"Document fileId: {first_document.fileId}, fileName: {first_document.fileName}") logger.info(f"Document fileId: {first_document.fileId}, fileName: {first_document.fileName}")
file_data = self.services.workflow.getFileData(first_document.fileId) file_data = self.services.chat.getFileData(first_document.fileId)
if not file_data: if not file_data:
return ActionResult.isFailure(error=f"No file data found for document: {pathObject} (fileId: {first_document.fileId})") return ActionResult.isFailure(error=f"No file data found for document: {pathObject} (fileId: {first_document.fileId})")
logger.info(f"File data length: {len(file_data) if file_data else 0}") logger.info(f"File data length: {len(file_data) if file_data else 0}")

View file

@ -6,6 +6,7 @@ from typing import Dict, Any, List
from modules.datamodels.datamodelChat import ActionResult, ActionItem, TaskStep from modules.datamodels.datamodelChat import ActionResult, ActionItem, TaskStep
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.workflows.processing.shared.methodDiscovery import methods from modules.workflows.processing.shared.methodDiscovery import methods
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,20 +16,6 @@ class ActionExecutor:
def __init__(self, services): def __init__(self, services):
self.services = services self.services = services
def _checkWorkflowStopped(self, workflow):
"""Check if workflow has been stopped by user and raise exception if so"""
try:
# Get the current workflow status from the database to avoid stale data
current_workflow = self.services.interfaceDbChat.getWorkflow(workflow.id)
if current_workflow and current_workflow.status == "stopped":
logger.info("Workflow stopped by user, aborting action execution")
raise Exception("Workflow was stopped by user")
except Exception as e:
# If we can't get the current status due to other database issues, fall back to the in-memory object
logger.warning(f"Could not check current workflow status from database: {str(e)}")
if workflow and workflow.status == "stopped":
logger.info("Workflow stopped by user (from in-memory object), aborting action execution")
raise Exception("Workflow was stopped by user")
async def executeAction(self, methodName: str, actionName: str, parameters: Dict[str, Any]) -> ActionResult: async def executeAction(self, methodName: str, actionName: str, parameters: Dict[str, Any]) -> ActionResult:
"""Execute a method action""" """Execute a method action"""
@ -70,7 +57,7 @@ class ActionExecutor:
"""Execute a single action and return ActionResult with enhanced document processing""" """Execute a single action and return ActionResult with enhanced document processing"""
try: try:
# Check workflow status before executing action # Check workflow status before executing action
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Use passed indices or fallback to '?' # Use passed indices or fallback to '?'
taskNum = taskIndex if taskIndex is not None else '?' taskNum = taskIndex if taskIndex is not None else '?'
@ -94,7 +81,7 @@ class ActionExecutor:
logger.info(f"Expected formats: {action.expectedDocumentFormats}") logger.info(f"Expected formats: {action.expectedDocumentFormats}")
# Check workflow status before executing the action # Check workflow status before executing the action
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
result = await self.executeAction( result = await self.executeAction(
methodName=action.execMethod, methodName=action.execMethod,
@ -156,7 +143,7 @@ class ActionExecutor:
logger.error(f"Action failed: {result.error}") logger.error(f"Action failed: {result.error}")
# Create database log entry for action failure (write-through + bind) # Create database log entry for action failure (write-through + bind)
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": f"❌ **Task {taskNum}**❌ **Action {actionNum}/{totalActions}** failed: {result.error}", "message": f"❌ **Task {taskNum}**❌ **Action {actionNum}/{totalActions}** failed: {result.error}",
"type": "error", "type": "error",
"progress": 100 "progress": 100

View file

@ -5,6 +5,7 @@ import logging
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from modules.datamodels.datamodelChat import TaskPlan, TaskStep, ActionResult, ReviewResult from modules.datamodels.datamodelChat import TaskPlan, TaskStep, ActionResult, ReviewResult
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -14,26 +15,12 @@ class MessageCreator:
def __init__(self, services): def __init__(self, services):
self.services = services self.services = services
def _checkWorkflowStopped(self, workflow):
"""Check if workflow has been stopped by user and raise exception if so"""
try:
# Get the current workflow status from the database to avoid stale data
current_workflow = self.services.interfaceDbChat.getWorkflow(workflow.id)
if current_workflow and current_workflow.status == "stopped":
logger.info("Workflow stopped by user, aborting message creation")
raise Exception("Workflow was stopped by user")
except Exception as e:
# If we can't get the current status due to other database issues, fall back to the in-memory object
logger.warning(f"Could not check current workflow status from database: {str(e)}")
if workflow and workflow.status == "stopped":
logger.info("Workflow stopped by user (from in-memory object), aborting message creation")
raise Exception("Workflow was stopped by user")
async def createTaskPlanMessage(self, taskPlan: TaskPlan, workflow: ChatWorkflow): async def createTaskPlanMessage(self, taskPlan: TaskPlan, workflow: ChatWorkflow):
"""Create a chat message containing the task plan with user-friendly messages""" """Create a chat message containing the task plan with user-friendly messages"""
try: try:
# Check workflow status before creating message # Check workflow status before creating message
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Build task plan summary # Build task plan summary
taskSummary = f"📋 **Task Plan**\n\n" taskSummary = f"📋 **Task Plan**\n\n"
@ -67,7 +54,7 @@ class MessageCreator:
"taskProgress": "pending" "taskProgress": "pending"
} }
self.services.workflow.storeMessageWithDocuments(workflow, messageData, []) self.services.chat.storeMessageWithDocuments(workflow, messageData, [])
logger.info("Task plan message created successfully") logger.info("Task plan message created successfully")
except Exception as e: except Exception as e:
logger.error(f"Error creating task plan message: {str(e)}") logger.error(f"Error creating task plan message: {str(e)}")
@ -76,7 +63,7 @@ class MessageCreator:
"""Create a task start message for the user""" """Create a task start message for the user"""
try: try:
# Check workflow status before creating message # Check workflow status before creating message
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Create a task start message for the user # Create a task start message for the user
taskProgress = f"{taskIndex}/{totalTasks}" if totalTasks is not None else str(taskIndex) taskProgress = f"{taskIndex}/{totalTasks}" if totalTasks is not None else str(taskIndex)
@ -101,7 +88,7 @@ class MessageCreator:
if taskStep.userMessage: if taskStep.userMessage:
taskStartMessage["message"] += f"\n\n💬 {taskStep.userMessage}" taskStartMessage["message"] += f"\n\n💬 {taskStep.userMessage}"
self.services.workflow.storeMessageWithDocuments(workflow, taskStartMessage, []) self.services.chat.storeMessageWithDocuments(workflow, taskStartMessage, [])
logger.info(f"Task start message created for task {taskIndex}") logger.info(f"Task start message created for task {taskIndex}")
except Exception as e: except Exception as e:
logger.error(f"Error creating task start message: {str(e)}") logger.error(f"Error creating task start message: {str(e)}")
@ -112,7 +99,7 @@ class MessageCreator:
"""Create and store a message for the action result in the workflow with enhanced document processing""" """Create and store a message for the action result in the workflow with enhanced document processing"""
try: try:
# Check workflow status before creating action message # Check workflow status before creating action message
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
if resultLabel is None: if resultLabel is None:
resultLabel = action.execResultLabel resultLabel = action.execResultLabel
@ -124,8 +111,8 @@ class MessageCreator:
logger.info(f"Result label: {resultLabel} - No documents") logger.info(f"Result label: {resultLabel} - No documents")
# Get current workflow context and stats # Get current workflow context and stats
workflowContext = self.services.workflow.getWorkflowContext() workflowContext = self.services.chat.getWorkflowContext()
workflowStats = self.services.workflow.getWorkflowStats() workflowStats = self.services.chat.getWorkflowStats()
# Create a more meaningful message that includes task context # Create a more meaningful message that includes task context
taskObjective = taskStep.objective if taskStep else 'Unknown task' taskObjective = taskStep.objective if taskStep else 'Unknown task'
@ -191,7 +178,7 @@ class MessageCreator:
logger.info(f"Creating ERROR message: {messageText}") logger.info(f"Creating ERROR message: {messageText}")
logger.info(f"Message data: {messageData}") logger.info(f"Message data: {messageData}")
self.services.workflow.storeMessageWithDocuments(workflow, messageData, createdDocuments) self.services.chat.storeMessageWithDocuments(workflow, messageData, createdDocuments)
logger.info(f"Message created: {action.execMethod}.{action.execAction}") logger.info(f"Message created: {action.execMethod}.{action.execAction}")
except Exception as e: except Exception as e:
logger.error(f"Error creating action message: {str(e)}") logger.error(f"Error creating action message: {str(e)}")
@ -201,7 +188,7 @@ class MessageCreator:
"""Create a task completion message for the user""" """Create a task completion message for the user"""
try: try:
# Check workflow status before creating message # Check workflow status before creating message
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Create a task completion message for the user # Create a task completion message for the user
taskProgress = str(taskIndex) taskProgress = str(taskIndex)
@ -234,7 +221,7 @@ class MessageCreator:
"taskProgress": "success" "taskProgress": "success"
} }
self.services.workflow.storeMessageWithDocuments(workflow, taskCompletionMessage, []) self.services.chat.storeMessageWithDocuments(workflow, taskCompletionMessage, [])
logger.info(f"Task completion message created for task {taskIndex}") logger.info(f"Task completion message created for task {taskIndex}")
except Exception as e: except Exception as e:
logger.error(f"Error creating task completion message: {str(e)}") logger.error(f"Error creating task completion message: {str(e)}")
@ -243,7 +230,7 @@ class MessageCreator:
"""Create a retry message for the user""" """Create a retry message for the user"""
try: try:
# Check workflow status before creating message # Check workflow status before creating message
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Create retry message for user # Create retry message for user
retryMessage = { retryMessage = {
@ -261,7 +248,7 @@ class MessageCreator:
"taskProgress": "retry" "taskProgress": "retry"
} }
self.services.workflow.storeMessageWithDocuments(workflow, retryMessage, []) self.services.chat.storeMessageWithDocuments(workflow, retryMessage, [])
logger.info(f"Retry message created for task {taskIndex}") logger.info(f"Retry message created for task {taskIndex}")
except Exception as e: except Exception as e:
logger.error(f"Error creating retry message: {str(e)}") logger.error(f"Error creating retry message: {str(e)}")
@ -270,7 +257,7 @@ class MessageCreator:
"""Create an error message for the user""" """Create an error message for the user"""
try: try:
# Check workflow status before creating message # Check workflow status before creating message
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Create user-facing error message for task failure # Create user-facing error message for task failure
errorMessage = f"**Task {taskIndex}**\n\n'{taskStep.objective}' failed\n\n" errorMessage = f"**Task {taskIndex}**\n\n'{taskStep.objective}' failed\n\n"
@ -300,7 +287,7 @@ class MessageCreator:
"taskProgress": "fail" "taskProgress": "fail"
} }
self.services.workflow.storeMessageWithDocuments(workflow, messageData, []) self.services.chat.storeMessageWithDocuments(workflow, messageData, [])
logger.info(f"Error message created for task {taskIndex}") logger.info(f"Error message created for task {taskIndex}")
except Exception as e: except Exception as e:
logger.error(f"Error creating error message: {str(e)}") logger.error(f"Error creating error message: {str(e)}")

View file

@ -10,6 +10,7 @@ from modules.workflows.processing.shared.promptGenerationTaskplan import (
generateTaskPlanningPrompt generateTaskPlanningPrompt
) )
from modules.workflows.processing.adaptive import IntentAnalyzer from modules.workflows.processing.adaptive import IntentAnalyzer
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,26 +20,12 @@ class TaskPlanner:
def __init__(self, services): def __init__(self, services):
self.services = services self.services = services
def _checkWorkflowStopped(self, workflow):
"""Check if workflow has been stopped by user and raise exception if so"""
try:
# Get the current workflow status from the database to avoid stale data
current_workflow = self.services.interfaceDbChat.getWorkflow(workflow.id)
if current_workflow and current_workflow.status == "stopped":
logger.info("Workflow stopped by user, aborting task planning")
raise Exception("Workflow was stopped by user")
except Exception as e:
# If we can't get the current status due to other database issues, fall back to the in-memory object
logger.warning(f"Could not check current workflow status from database: {str(e)}")
if workflow and workflow.status == "stopped":
logger.info("Workflow stopped by user (from in-memory object), aborting task planning")
raise Exception("Workflow was stopped by user")
async def generateTaskPlan(self, userInput: str, workflow) -> TaskPlan: async def generateTaskPlan(self, userInput: str, workflow) -> TaskPlan:
"""Generate a high-level task plan for the workflow""" """Generate a high-level task plan for the workflow"""
try: try:
# Check workflow status before generating task plan # Check workflow status before generating task plan
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
logger.info(f"=== STARTING TASK PLAN GENERATION ===") logger.info(f"=== STARTING TASK PLAN GENERATION ===")
logger.info(f"Workflow ID: {workflow.id}") logger.info(f"Workflow ID: {workflow.id}")
@ -49,7 +36,7 @@ class TaskPlanner:
logger.info(f"Actual User Prompt: {actualUserPrompt}") logger.info(f"Actual User Prompt: {actualUserPrompt}")
# Check workflow status before calling AI service # Check workflow status before calling AI service
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Analyze user intent to obtain cleaned user objective for planning # Analyze user intent to obtain cleaned user objective for planning
# This intent will be reused for workflow-level validation in executeTask # This intent will be reused for workflow-level validation in executeTask

View file

@ -13,6 +13,7 @@ from modules.datamodels.datamodelChat import (
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
from modules.workflows.processing.modes.modeBase import BaseMode from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
from modules.workflows.processing.shared.executionState import TaskExecutionState from modules.workflows.processing.shared.executionState import TaskExecutionState
from modules.workflows.processing.shared.promptGenerationActionsActionplan import ( from modules.workflows.processing.shared.promptGenerationActionsActionplan import (
generateActionDefinitionPrompt, generateActionDefinitionPrompt,
@ -26,8 +27,8 @@ logger = logging.getLogger(__name__)
class ActionplanMode(BaseMode): class ActionplanMode(BaseMode):
"""Actionplan mode implementation - batch planning and sequential execution""" """Actionplan mode implementation - batch planning and sequential execution"""
def __init__(self, services, workflow): def __init__(self, services):
super().__init__(services, workflow) super().__init__(services)
# Initialize adaptive components for enhanced validation and learning # Initialize adaptive components for enhanced validation and learning
self.intentAnalyzer = IntentAnalyzer(services) self.intentAnalyzer = IntentAnalyzer(services)
self.learningEngine = LearningEngine() self.learningEngine = LearningEngine()
@ -42,7 +43,7 @@ class ActionplanMode(BaseMode):
"""Generate actions for a given task step using batch planning approach""" """Generate actions for a given task step using batch planning approach"""
try: try:
# Check workflow status before generating actions # Check workflow status before generating actions
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
retryInfo = f" (Retry #{enhancedContext.retryCount})" if enhancedContext and enhancedContext.retryCount > 0 else "" retryInfo = f" (Retry #{enhancedContext.retryCount})" if enhancedContext and enhancedContext.retryCount > 0 else ""
logger.info(f"Generating actions for task: {taskStep.objective}{retryInfo}") logger.info(f"Generating actions for task: {taskStep.objective}{retryInfo}")
@ -126,7 +127,7 @@ class ActionplanMode(BaseMode):
) )
# Check workflow status before calling AI service # Check workflow status before calling AI service
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Build prompt bundle (template + placeholders) # Build prompt bundle (template + placeholders)
bundle = generateActionDefinitionPrompt(self.services, actionContext) bundle = generateActionDefinitionPrompt(self.services, actionContext)
@ -262,7 +263,7 @@ class ActionplanMode(BaseMode):
# Update workflow context for this task # Update workflow context for this task
if taskIndex is not None: if taskIndex is not None:
self.services.workflow.setWorkflowContext(taskNumber=taskIndex) self.services.chat.setWorkflowContext(taskNumber=taskIndex)
# Create task start message # Create task start message
await self.messageCreator.createTaskStartMessage(taskStep, workflow, taskIndex, totalTasks) await self.messageCreator.createTaskStartMessage(taskStep, workflow, taskIndex, totalTasks)
@ -275,7 +276,7 @@ class ActionplanMode(BaseMode):
logger.info(f"Task execution attempt {attempt+1}/{maxRetries}") logger.info(f"Task execution attempt {attempt+1}/{maxRetries}")
# Check workflow status before starting task execution # Check workflow status before starting task execution
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Update retry context with current attempt information # Update retry context with current attempt information
if retryContext: if retryContext:
@ -300,7 +301,7 @@ class ActionplanMode(BaseMode):
actionResults = [] actionResults = []
for actionIdx, action in enumerate(actions): for actionIdx, action in enumerate(actions):
# Check workflow status before each action execution # Check workflow status before each action execution
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Update workflow object before executing action # Update workflow object before executing action
actionNumber = actionIdx + 1 actionNumber = actionIdx + 1
@ -330,7 +331,7 @@ class ActionplanMode(BaseMode):
if action.userMessage: if action.userMessage:
actionStartMessage["message"] += f"\n\n💬 {action.userMessage}" actionStartMessage["message"] += f"\n\n💬 {action.userMessage}"
self.services.workflow.storeMessageWithDocuments(workflow, actionStartMessage, []) self.services.chat.storeMessageWithDocuments(workflow, actionStartMessage, [])
logger.info(f"Action start message created for action {actionNumber}") logger.info(f"Action start message created for action {actionNumber}")
# Execute single action # Execute single action
@ -376,7 +377,7 @@ class ActionplanMode(BaseMode):
state.addFailedAction(result) state.addFailedAction(result)
# Check workflow status before review # Check workflow status before review
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
reviewResult = await self._reviewTaskCompletion(taskStep, actions, actionResults, workflow) reviewResult = await self._reviewTaskCompletion(taskStep, actions, actionResults, workflow)
success = reviewResult.status == 'success' success = reviewResult.status == 'success'
@ -479,7 +480,7 @@ class ActionplanMode(BaseMode):
"""Review task completion and determine success/failure/retry""" """Review task completion and determine success/failure/retry"""
try: try:
# Check workflow status before reviewing task completion # Check workflow status before reviewing task completion
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
logger.info(f"=== STARTING TASK COMPLETION REVIEW ===") logger.info(f"=== STARTING TASK COMPLETION REVIEW ===")
logger.info(f"Task: {taskStep.objective}") logger.info(f"Task: {taskStep.objective}")
@ -510,7 +511,7 @@ class ActionplanMode(BaseMode):
) )
# Check workflow status before calling AI service # Check workflow status before calling AI service
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Build prompt bundle for result review # Build prompt bundle for result review
bundle = generateResultReviewPrompt(reviewContext) bundle = generateResultReviewPrompt(reviewContext)

View file

@ -11,14 +11,15 @@ from modules.datamodels.datamodelChat import (
) )
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.workflows.processing.modes.modeBase import BaseMode from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AutomationMode(BaseMode): class AutomationMode(BaseMode):
"""Automation mode implementation - executes workflows from predefined plans""" """Automation mode implementation - executes workflows from predefined plans"""
def __init__(self, services, workflow): def __init__(self, services):
super().__init__(services, workflow) super().__init__(services)
# Store action lists for each task (mapped by task ID) # Store action lists for each task (mapped by task ID)
self.taskActionMap: Dict[str, List[Dict[str, Any]]] = {} self.taskActionMap: Dict[str, List[Dict[str, Any]]] = {}
logger.info("AutomationMode initialized - will use predefined plan from workflow") logger.info("AutomationMode initialized - will use predefined plan from workflow")
@ -192,12 +193,12 @@ class AutomationMode(BaseMode):
try: try:
# Check workflow status # Check workflow status
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Update workflow before executing task # Update workflow before executing task
if taskIndex is not None: if taskIndex is not None:
self._updateWorkflowBeforeExecutingTask(taskIndex) self._updateWorkflowBeforeExecutingTask(taskIndex)
self.services.workflow.setWorkflowContext(taskNumber=taskIndex) self.services.chat.setWorkflowContext(taskNumber=taskIndex)
# Create task start message # Create task start message
await self.messageCreator.createTaskStartMessage(taskStep, workflow, taskIndex, totalTasks) await self.messageCreator.createTaskStartMessage(taskStep, workflow, taskIndex, totalTasks)
@ -228,7 +229,7 @@ class AutomationMode(BaseMode):
actionResults = [] actionResults = []
for actionIdx, action in enumerate(actions): for actionIdx, action in enumerate(actions):
# Check workflow status before each action # Check workflow status before each action
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Update workflow before executing action # Update workflow before executing action
actionNumber = actionIdx + 1 actionNumber = actionIdx + 1
@ -256,7 +257,7 @@ class AutomationMode(BaseMode):
if action.userMessage: if action.userMessage:
actionStartMessage["message"] += f"\n\n💬 {action.userMessage}" actionStartMessage["message"] += f"\n\n💬 {action.userMessage}"
self.services.workflow.storeMessageWithDocuments(workflow, actionStartMessage, []) self.services.chat.storeMessageWithDocuments(workflow, actionStartMessage, [])
# Execute action # Execute action
result = await self.actionExecutor.executeSingleAction( result = await self.actionExecutor.executeSingleAction(

View file

@ -16,31 +16,13 @@ logger = logging.getLogger(__name__)
class BaseMode(ABC): class BaseMode(ABC):
"""Abstract base class for workflow execution modes""" """Abstract base class for workflow execution modes"""
def __init__(self, services, workflow): def __init__(self, services):
self.services = services self.services = services
self.workflow = workflow
self.taskPlanner = TaskPlanner(services) self.taskPlanner = TaskPlanner(services)
self.actionExecutor = ActionExecutor(services) self.actionExecutor = ActionExecutor(services)
self.messageCreator = MessageCreator(services) self.messageCreator = MessageCreator(services)
self.validator = WorkflowValidator(services) self.validator = WorkflowValidator(services)
def _checkWorkflowStopped(self, workflow):
"""Check if workflow has been stopped by user and raise exception if so"""
try:
# Get the current workflow status from the database to avoid stale data
current_workflow = self.services.interfaceDbChat.getWorkflow(workflow.id)
if current_workflow and current_workflow.status == "stopped":
logger.info("Workflow stopped by user, aborting execution")
raise Exception("Workflow was stopped by user")
except Exception as e:
# If this was the explicit stop signal, re-raise to abort immediately
if str(e) == "Workflow was stopped by user":
raise
# If we can't get the current status due to other database issues, fall back to the in-memory object
logger.warning(f"Could not check current workflow status from database: {str(e)}")
if workflow and workflow.status == "stopped":
logger.info("Workflow stopped by user (from in-memory object), aborting execution")
raise Exception("Workflow was stopped by user")
@abstractmethod @abstractmethod
async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext, async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext,

View file

@ -13,6 +13,7 @@ from modules.datamodels.datamodelChat import (
) )
from modules.datamodels.datamodelChat import ChatWorkflow from modules.datamodels.datamodelChat import ChatWorkflow
from modules.workflows.processing.modes.modeBase import BaseMode from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
from modules.workflows.processing.shared.executionState import TaskExecutionState, shouldContinue from modules.workflows.processing.shared.executionState import TaskExecutionState, shouldContinue
from modules.workflows.processing.shared.promptGenerationActionsDynamic import ( from modules.workflows.processing.shared.promptGenerationActionsDynamic import (
generateDynamicPlanSelectionPrompt, generateDynamicPlanSelectionPrompt,
@ -28,8 +29,8 @@ logger = logging.getLogger(__name__)
class DynamicMode(BaseMode): class DynamicMode(BaseMode):
"""Dynamic mode implementation - iterative plan-act-observe-refine loop""" """Dynamic mode implementation - iterative plan-act-observe-refine loop"""
def __init__(self, services, workflow): def __init__(self, services):
super().__init__(services, workflow) super().__init__(services)
# Initialize adaptive components # Initialize adaptive components
self.intentAnalyzer = IntentAnalyzer(services) self.intentAnalyzer = IntentAnalyzer(services)
self.learningEngine = LearningEngine() self.learningEngine = LearningEngine()
@ -87,7 +88,7 @@ class DynamicMode(BaseMode):
decision = None decision = None
while step <= state.max_steps: while step <= state.max_steps:
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Update workflow[currentAction] for UI # Update workflow[currentAction] for UI
self._updateWorkflowBeforeExecutingAction(step) self._updateWorkflowBeforeExecutingAction(step)
@ -250,7 +251,7 @@ class DynamicMode(BaseMode):
# Get available documents from the current workflow # Get available documents from the current workflow
try: try:
available_docs = self.services.workflow.getAvailableDocuments(self.services.currentWorkflow) available_docs = self.services.chat.getAvailableDocuments(self.services.workflow)
if not available_docs or available_docs == "No documents available": if not available_docs or available_docs == "No documents available":
logger.warning("No documents available for validation") logger.warning("No documents available for validation")
return return
@ -759,7 +760,7 @@ class DynamicMode(BaseMode):
"actionProgress": actionProgress "actionProgress": actionProgress
} }
self.services.workflow.storeMessageWithDocuments(workflow, messageData, []) self.services.chat.storeMessageWithDocuments(workflow, messageData, [])
except Exception as e: except Exception as e:
logger.error(f"Error creating Dynamic action message: {str(e)}") logger.error(f"Error creating Dynamic action message: {str(e)}")

View file

@ -117,12 +117,12 @@ def extractUserPrompt(context: Any) -> str:
return context.taskStep.objective or 'No request specified' return context.taskStep.objective or 'No request specified'
return 'No request specified' return 'No request specified'
def extractWorkflowHistory(service: Any, context: Any) -> str: def extractWorkflowHistory(service: Any) -> str:
"""Extract workflow history from context. Maps to {{KEY:WORKFLOW_HISTORY}} """Extract workflow history. Maps to {{KEY:WORKFLOW_HISTORY}}
Reverse-chronological, enriched with message summaries and document labels. Reverse-chronological, enriched with message summaries and document labels.
""" """
try: try:
history = getPreviousRoundContext(service, service.currentWorkflow) history = getPreviousRoundContext(service)
return history or "No previous workflow rounds available" return history or "No previous workflow rounds available"
except Exception as e: except Exception as e:
logger.error(f"Error getting workflow history: {str(e)}") logger.error(f"Error getting workflow history: {str(e)}")
@ -229,13 +229,14 @@ def getMessageSummary(msg) -> str:
except Exception: except Exception:
return "" return ""
def getPreviousRoundContext(services, workflow: Any) -> str: def getPreviousRoundContext(services) -> str:
"""Get enriched context: """Get enriched context:
- Reverse-chronological ordering - Reverse-chronological ordering
- Current round first (newest oldest), then older rounds - Current round first (newest oldest), then older rounds
- Only messages with documents summarized - Only messages with documents summarized
- Include available documents snapshot at end - Include available documents snapshot at end
""" """
workflow = services.workflow
try: try:
if not workflow: if not workflow:
return "No previous round context available" return "No previous round context available"
@ -268,7 +269,7 @@ def getPreviousRoundContext(services, workflow: Any) -> str:
# Include available documents snapshot at end # Include available documents snapshot at end
try: try:
if hasattr(services, 'workflow'): if hasattr(services, 'workflow'):
docs_index = services.workflow.getAvailableDocuments(workflow) docs_index = services.chat.getAvailableDocuments(workflow)
if docs_index and docs_index != "No documents available": if docs_index and docs_index != "No documents available":
doc_count = docs_index.count("docItem:") # Only count actual documents, not document list labels doc_count = docs_index.count("docItem:") # Only count actual documents, not document list labels
lines.append(f"Available documents: {doc_count}") lines.append(f"Available documents: {doc_count}")
@ -447,7 +448,7 @@ def extractLatestRefinementFeedback(context: Any) -> str:
def extractAvailableDocumentsSummary(service: Any, context: Any) -> str: def extractAvailableDocumentsSummary(service: Any, context: Any) -> str:
"""Summary of available documents (count only).""" """Summary of available documents (count only)."""
try: try:
documents = service.workflow.getAvailableDocuments(service.currentWorkflow) documents = service.chat.getAvailableDocuments(service.workflow)
if documents and documents != "No documents available": if documents and documents != "No documents available":
# Count only actual documents, not list labels # Count only actual documents, not list labels
doc_count = documents.count("docItem:") doc_count = documents.count("docItem:")
@ -460,7 +461,7 @@ def extractAvailableDocumentsSummary(service: Any, context: Any) -> str:
def extractAvailableDocumentsIndex(service: Any, context: Any) -> str: def extractAvailableDocumentsIndex(service: Any, context: Any) -> str:
"""Index of available documents with detailed references for parameter generation.""" """Index of available documents with detailed references for parameter generation."""
try: try:
return service.workflow.getAvailableDocuments(service.currentWorkflow) return service.chat.getAvailableDocuments(service.workflow)
except Exception as e: except Exception as e:
logger.error(f"Error getting document index: {str(e)}") logger.error(f"Error getting document index: {str(e)}")
return "No documents available" return "No documents available"

View file

@ -24,7 +24,7 @@ def generateActionDefinitionPrompt(services, context: Any) -> PromptBundle:
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False), PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True), PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False), PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False),
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True), PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services), summaryAllowed=True),
PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False), PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False),
PromptPlaceholder(label="USER_LANGUAGE", content=extractUserLanguage(services), summaryAllowed=False), PromptPlaceholder(label="USER_LANGUAGE", content=extractUserLanguage(services), summaryAllowed=False),
] ]

View file

@ -32,7 +32,7 @@ def generateDynamicPlanSelectionPrompt(services, context: Any, learningEngine=No
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True), PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False), PromptPlaceholder(label="AVAILABLE_METHODS", content=extractAvailableMethods(services), summaryAllowed=False),
# Provide enriched history context for Stage 1 to craft parametersContext # Provide enriched history context for Stage 1 to craft parametersContext
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True), PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services), summaryAllowed=True),
# Provide deterministic indexes so the planner can choose exact labels # Provide deterministic indexes so the planner can choose exact labels
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_INDEX", content=extractAvailableDocumentsIndex(services, context), summaryAllowed=True), PromptPlaceholder(label="AVAILABLE_DOCUMENTS_INDEX", content=extractAvailableDocumentsIndex(services, context), summaryAllowed=True),
PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False), PromptPlaceholder(label="AVAILABLE_CONNECTIONS_INDEX", content=extractAvailableConnectionsIndex(services), summaryAllowed=False),

View file

@ -23,7 +23,7 @@ def generateTaskPlanningPrompt(services, context: Any) -> PromptBundle:
placeholders: List[PromptPlaceholder] = [ placeholders: List[PromptPlaceholder] = [
PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False), PromptPlaceholder(label="USER_PROMPT", content=extractUserPrompt(context), summaryAllowed=False),
PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True), PromptPlaceholder(label="AVAILABLE_DOCUMENTS_SUMMARY", content=extractAvailableDocumentsSummary(services, context), summaryAllowed=True),
PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services, context), summaryAllowed=True), PromptPlaceholder(label="WORKFLOW_HISTORY", content=extractWorkflowHistory(services), summaryAllowed=True),
PromptPlaceholder(label="USER_LANGUAGE", content=userLanguage, summaryAllowed=False), PromptPlaceholder(label="USER_LANGUAGE", content=userLanguage, summaryAllowed=False),
] ]

View file

@ -0,0 +1,46 @@
"""
State Tools
Shared utilities for workflow state management and validation.
"""
import logging
from typing import Any
logger = logging.getLogger(__name__)
class WorkflowStoppedException(Exception):
"""Exception raised when a workflow is stopped by the user."""
pass
def checkWorkflowStopped(services: Any) -> None:
"""
Check if workflow has been stopped by user and raise exception if so.
Args:
services: Services object with workflow and interfaceDbChat for fresh status check
Raises:
WorkflowStoppedException: If workflow status is "stopped"
"""
workflow = services.workflow
if not workflow:
return
try:
# Get the current workflow status from the database to avoid stale data
currentWorkflow = services.interfaceDbChat.getWorkflow(workflow.id)
if currentWorkflow and currentWorkflow.status == "stopped":
logger.info("Workflow stopped by user, aborting operation")
raise WorkflowStoppedException("Workflow was stopped by user")
except WorkflowStoppedException:
# Re-raise the stop signal immediately
raise
except Exception as e:
# If we can't get the current status due to other database issues, fall back to the in-memory object
logger.warning(f"Could not check current workflow status from database: {str(e)}")
if workflow and workflow.status == "stopped":
logger.info("Workflow stopped by user (from in-memory object), aborting operation")
raise WorkflowStoppedException("Workflow was stopped by user")

View file

@ -9,45 +9,27 @@ from modules.workflows.processing.modes.modeBase import BaseMode
from modules.workflows.processing.modes.modeActionplan import ActionplanMode from modules.workflows.processing.modes.modeActionplan import ActionplanMode
from modules.workflows.processing.modes.modeDynamic import DynamicMode from modules.workflows.processing.modes.modeDynamic import DynamicMode
from modules.workflows.processing.modes.modeAutomation import AutomationMode from modules.workflows.processing.modes.modeAutomation import AutomationMode
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WorkflowStoppedException(Exception):
"""Exception raised when a workflow is stopped by the user."""
pass
class WorkflowProcessor: class WorkflowProcessor:
"""Main workflow processor that delegates to appropriate mode implementations""" """Main workflow processor that delegates to appropriate mode implementations"""
def __init__(self, services, workflow=None): def __init__(self, services):
self.services = services self.services = services
self.workflow = workflow self.mode = self._createMode(services.workflow.workflowMode)
self.mode = self._createMode(workflow.workflowMode)
def _createMode(self, workflowMode: WorkflowModeEnum) -> BaseMode: def _createMode(self, workflowMode: WorkflowModeEnum) -> BaseMode:
"""Create the appropriate mode implementation based on workflow mode""" """Create the appropriate mode implementation based on workflow mode"""
if workflowMode == WorkflowModeEnum.WORKFLOW_DYNAMIC: if workflowMode == WorkflowModeEnum.WORKFLOW_DYNAMIC:
return DynamicMode(self.services, self.workflow) return DynamicMode(self.services)
elif workflowMode == WorkflowModeEnum.WORKFLOW_ACTIONPLAN: elif workflowMode == WorkflowModeEnum.WORKFLOW_ACTIONPLAN:
return ActionplanMode(self.services, self.workflow) return ActionplanMode(self.services)
elif workflowMode == WorkflowModeEnum.WORKFLOW_AUTOMATION: elif workflowMode == WorkflowModeEnum.WORKFLOW_AUTOMATION:
return AutomationMode(self.services, self.workflow) return AutomationMode(self.services)
else: else:
raise ValueError(f"Invalid workflow mode: {workflowMode}") raise ValueError(f"Invalid workflow mode: {workflowMode}")
def _checkWorkflowStopped(self, workflow):
"""Check if workflow has been stopped by user and raise exception if so"""
try:
# Get the current workflow status from the database to avoid stale data
current_workflow = self.services.interfaceDbChat.getWorkflow(workflow.id)
if current_workflow and current_workflow.status == "stopped":
logger.info("Workflow stopped by user, aborting processing")
raise WorkflowStoppedException("Workflow was stopped by user")
except Exception as e:
# If we can't get the current status due to other database issues, fall back to the in-memory object
logger.warning(f"Could not check current workflow status from database: {str(e)}")
if workflow and workflow.status == "stopped":
logger.info("Workflow stopped by user (from in-memory object), aborting processing")
raise WorkflowStoppedException("Workflow was stopped by user")
async def generateTaskPlan(self, userInput: str, workflow: ChatWorkflow) -> TaskPlan: async def generateTaskPlan(self, userInput: str, workflow: ChatWorkflow) -> TaskPlan:
"""Generate a high-level task plan for the workflow""" """Generate a high-level task plan for the workflow"""
@ -58,10 +40,10 @@ class WorkflowProcessor:
try: try:
# Check workflow status before generating task plan # Check workflow status before generating task plan
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Start progress tracking # Start progress tracking
self.services.workflow.progressLogStart( self.services.chat.progressLogStart(
operationId, operationId,
"Workflow Planning", "Workflow Planning",
"Task Plan Generation", "Task Plan Generation",
@ -78,25 +60,25 @@ class WorkflowProcessor:
logger.info(f"Workflow Mode: {modeValue}") logger.info(f"Workflow Mode: {modeValue}")
# Update progress - generating task plan # Update progress - generating task plan
self.services.workflow.progressLogUpdate(operationId, 0.3, "Analyzing input") self.services.chat.progressLogUpdate(operationId, 0.3, "Analyzing input")
# Delegate to the appropriate mode # Delegate to the appropriate mode
taskPlan = await self.mode.generateTaskPlan(userInput, workflow) taskPlan = await self.mode.generateTaskPlan(userInput, workflow)
# Update progress - creating task plan message # Update progress - creating task plan message
self.services.workflow.progressLogUpdate(operationId, 0.8, "Creating plan") self.services.chat.progressLogUpdate(operationId, 0.8, "Creating plan")
# Create task plan message # Create task plan message
await self.mode.createTaskPlanMessage(taskPlan, workflow) await self.mode.createTaskPlanMessage(taskPlan, workflow)
# Complete progress tracking # Complete progress tracking
self.services.workflow.progressLogFinish(operationId, True) self.services.chat.progressLogFinish(operationId, True)
return taskPlan return taskPlan
except Exception as e: except Exception as e:
logger.error(f"Error in generateTaskPlan: {str(e)}") logger.error(f"Error in generateTaskPlan: {str(e)}")
# Complete progress tracking with failure # Complete progress tracking with failure
self.services.workflow.progressLogFinish(operationId, False) self.services.chat.progressLogFinish(operationId, False)
raise raise
async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext, async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext,
@ -109,10 +91,10 @@ class WorkflowProcessor:
try: try:
# Check workflow status before executing task # Check workflow status before executing task
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Start progress tracking # Start progress tracking
self.services.workflow.progressLogStart( self.services.chat.progressLogStart(
operationId, operationId,
"Workflow Execution", "Workflow Execution",
"Task Execution", "Task Execution",
@ -125,19 +107,19 @@ class WorkflowProcessor:
logger.info(f"Mode: {modeValue}") logger.info(f"Mode: {modeValue}")
# Update progress - executing task # Update progress - executing task
self.services.workflow.progressLogUpdate(operationId, 0.2, "Executing") self.services.chat.progressLogUpdate(operationId, 0.2, "Executing")
# Delegate to the appropriate mode # Delegate to the appropriate mode
result = await self.mode.executeTask(taskStep, workflow, context, taskIndex, totalTasks) result = await self.mode.executeTask(taskStep, workflow, context, taskIndex, totalTasks)
# Complete progress tracking # Complete progress tracking
self.services.workflow.progressLogFinish(operationId, True) self.services.chat.progressLogFinish(operationId, True)
return result return result
except Exception as e: except Exception as e:
logger.error(f"Error in executeTask: {str(e)}") logger.error(f"Error in executeTask: {str(e)}")
# Complete progress tracking with failure # Complete progress tracking with failure
self.services.workflow.progressLogFinish(operationId, False) self.services.chat.progressLogFinish(operationId, False)
raise raise
async def generateActionItems(self, taskStep: TaskStep, workflow: ChatWorkflow, async def generateActionItems(self, taskStep: TaskStep, workflow: ChatWorkflow,
@ -145,7 +127,7 @@ class WorkflowProcessor:
"""Generate actions for a task step using the appropriate mode""" """Generate actions for a task step using the appropriate mode"""
try: try:
# Check workflow status before generating actions # Check workflow status before generating actions
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
logger.info(f"=== STARTING ACTION GENERATION ===") logger.info(f"=== STARTING ACTION GENERATION ===")
logger.info(f"Task: {taskStep.objective}") logger.info(f"Task: {taskStep.objective}")
@ -287,7 +269,7 @@ class WorkflowProcessor:
"""Prepare task handover data for workflow coordination""" """Prepare task handover data for workflow coordination"""
try: try:
# Check workflow status before preparing task handover # Check workflow status before preparing task handover
self._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
# Log handover status summary # Log handover status summary
status = taskResult.status if taskResult else 'unknown' status = taskResult.status if taskResult else 'unknown'

View file

@ -12,7 +12,8 @@ from modules.datamodels.datamodelChat import (
WorkflowModeEnum WorkflowModeEnum
) )
from modules.datamodels.datamodelChat import TaskContext from modules.datamodels.datamodelChat import TaskContext
from modules.workflows.processing.workflowProcessor import WorkflowProcessor, WorkflowStoppedException from modules.workflows.processing.workflowProcessor import WorkflowProcessor
from modules.workflows.processing.shared.stateTools import WorkflowStoppedException, checkWorkflowStopped
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,22 +35,22 @@ class WorkflowManager:
currentTime = self.services.utils.timestampGetUtc() currentTime = self.services.utils.timestampGetUtc()
if workflowId: if workflowId:
workflow = self.services.workflow.getWorkflow(workflowId) workflow = self.services.chat.getWorkflow(workflowId)
if not workflow: if not workflow:
raise ValueError(f"Workflow {workflowId} not found") raise ValueError(f"Workflow {workflowId} not found")
# Store workflow in services for reference # Store workflow in services for reference (this is the ChatWorkflow object)
self.services.currentWorkflow = workflow self.services.workflow = workflow
if workflow.status == "running": if workflow.status == "running":
logger.info(f"Stopping running workflow {workflowId} before processing new prompt") logger.info(f"Stopping running workflow {workflowId} before processing new prompt")
workflow.status = "stopped" workflow.status = "stopped"
workflow.lastActivity = currentTime workflow.lastActivity = currentTime
self.services.workflow.updateWorkflow(workflowId, { self.services.chat.updateWorkflow(workflowId, {
"status": "stopped", "status": "stopped",
"lastActivity": currentTime "lastActivity": currentTime
}) })
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": "Workflow stopped for new prompt", "message": "Workflow stopped for new prompt",
"type": "info", "type": "info",
"status": "stopped", "status": "stopped",
@ -57,7 +58,7 @@ class WorkflowManager:
}) })
newRound = workflow.currentRound + 1 newRound = workflow.currentRound + 1
self.services.workflow.updateWorkflow(workflowId, { self.services.chat.updateWorkflow(workflowId, {
"status": "running", "status": "running",
"lastActivity": currentTime, "lastActivity": currentTime,
"currentRound": newRound, "currentRound": newRound,
@ -70,7 +71,7 @@ class WorkflowManager:
workflow.currentRound = newRound workflow.currentRound = newRound
workflow.workflowMode = workflowMode workflow.workflowMode = workflowMode
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": f"Workflow resumed (round {workflow.currentRound}) with mode: {workflowMode}", "message": f"Workflow resumed (round {workflow.currentRound}) with mode: {workflowMode}",
"type": "info", "type": "info",
"status": "running", "status": "running",
@ -94,14 +95,15 @@ class WorkflowManager:
"maxSteps": 5 if workflowMode == WorkflowModeEnum.WORKFLOW_DYNAMIC else 1, # Set maxSteps for Dynamic mode "maxSteps": 5 if workflowMode == WorkflowModeEnum.WORKFLOW_DYNAMIC else 1, # Set maxSteps for Dynamic mode
} }
workflow = self.services.workflow.createWorkflow(workflowData) workflow = self.services.chat.createWorkflow(workflowData)
logger.info(f"Created workflow with mode: {getattr(workflow, 'workflowMode', 'NOT_SET')}") logger.info(f"Created workflow with mode: {getattr(workflow, 'workflowMode', 'NOT_SET')}")
logger.info(f"Workflow data passed: {workflowData.get('workflowMode', 'NOT_IN_DATA')}") logger.info(f"Workflow data passed: {workflowData.get('workflowMode', 'NOT_IN_DATA')}")
self.services.currentWorkflow = workflow # Store workflow in services (this is the ChatWorkflow object)
self.services.workflow = workflow
# Start workflow processing asynchronously # Start workflow processing asynchronously
asyncio.create_task(self._workflowProcess(userInput, workflow)) asyncio.create_task(self._workflowProcess(userInput))
return workflow return workflow
except Exception as e: except Exception as e:
@ -111,17 +113,20 @@ class WorkflowManager:
async def workflowStop(self, workflowId: str) -> ChatWorkflow: async def workflowStop(self, workflowId: str) -> ChatWorkflow:
"""Stops a running workflow.""" """Stops a running workflow."""
try: try:
workflow = self.services.workflow.getWorkflow(workflowId) workflow = self.services.chat.getWorkflow(workflowId)
if not workflow: if not workflow:
raise ValueError(f"Workflow {workflowId} not found") raise ValueError(f"Workflow {workflowId} not found")
# Store workflow in services (this is the ChatWorkflow object)
self.services.workflow = workflow
workflow.status = "stopped" workflow.status = "stopped"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflowId, { self.services.chat.updateWorkflow(workflowId, {
"status": "stopped", "status": "stopped",
"lastActivity": workflow.lastActivity "lastActivity": workflow.lastActivity
}) })
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": "Workflow stopped", "message": "Workflow stopped",
"type": "warning", "type": "warning",
"status": "stopped", "status": "stopped",
@ -134,34 +139,35 @@ class WorkflowManager:
# Main processor # Main processor
async def _workflowProcess(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> None: async def _workflowProcess(self, userInput: UserInputRequest) -> None:
"""Process a workflow with user input""" """Process a workflow with user input"""
try: try:
# Store the current user prompt in services for easy access throughout the workflow # Store the current user prompt in services for easy access throughout the workflow
self.services.rawUserPrompt = userInput.prompt self.services.rawUserPrompt = userInput.prompt
self.services.currentUserPrompt = userInput.prompt self.services.currentUserPrompt = userInput.prompt
# Update the workflow service with the current workflow context # Reset progress logger for new workflow
self.services.workflow.setCurrentWorkflow(workflow) self.services.chat._progressLogger = None
self.workflowProcessor = WorkflowProcessor(self.services, workflow) self.workflowProcessor = WorkflowProcessor(self.services)
await self._sendFirstMessage(userInput, workflow) await self._sendFirstMessage(userInput)
task_plan = await self._planTasks(userInput, workflow) task_plan = await self._planTasks(userInput)
await self._executeTasks(task_plan, workflow) await self._executeTasks(task_plan)
await self._processWorkflowResults(workflow) await self._processWorkflowResults()
except WorkflowStoppedException: except WorkflowStoppedException:
self._handleWorkflowStop(workflow) self._handleWorkflowStop()
except Exception as e: except Exception as e:
self._handleWorkflowError(workflow, e) self._handleWorkflowError(e)
# Helper functions # Helper functions
async def _sendFirstMessage(self, userInput: UserInputRequest, workflow: ChatWorkflow) -> None: async def _sendFirstMessage(self, userInput: UserInputRequest) -> None:
"""Send first message to start workflow""" """Send first message to start workflow"""
try: try:
self.workflowProcessor._checkWorkflowStopped(workflow) workflow = self.services.workflow
checkWorkflowStopped(self.services)
# Create initial message using interface # Create initial message using interface
# For first user message, include round info in the user context label # For first user message, include round info in the user context label
@ -286,7 +292,7 @@ class WorkflowManager:
self.services.interfaceDbComponent.createFileData(fileItem.id, contentBytes) self.services.interfaceDbComponent.createFileData(fileItem.id, contentBytes)
# Collect file info # Collect file info
fileInfo = self.services.workflow.getFileInfo(fileItem.id) fileInfo = self.services.chat.getFileInfo(fileItem.id)
from modules.datamodels.datamodelChat import ChatDocument from modules.datamodels.datamodelChat import ChatDocument
doc = ChatDocument( doc = ChatDocument(
fileId=fileItem.id, fileId=fileItem.id,
@ -310,14 +316,15 @@ class WorkflowManager:
logger.warning(f"Failed to process user fileIds: {e}") logger.warning(f"Failed to process user fileIds: {e}")
# Finally, persist and bind the first message with combined documents (context + user) # Finally, persist and bind the first message with combined documents (context + user)
self.services.workflow.storeMessageWithDocuments(workflow, messageData, createdDocs) self.services.chat.storeMessageWithDocuments(workflow, messageData, createdDocs)
except Exception as e: except Exception as e:
logger.error(f"Error sending first message: {str(e)}") logger.error(f"Error sending first message: {str(e)}")
raise raise
async def _planTasks(self, userInput: UserInputRequest, workflow: ChatWorkflow): async def _planTasks(self, userInput: UserInputRequest):
"""Generate task plan for workflow execution""" """Generate task plan for workflow execution"""
workflow = self.services.workflow
handling = self.workflowProcessor handling = self.workflowProcessor
# Generate task plan first (shared for both modes) # Generate task plan first (shared for both modes)
taskPlan = await handling.generateTaskPlan(userInput.prompt, workflow) taskPlan = await handling.generateTaskPlan(userInput.prompt, workflow)
@ -328,8 +335,9 @@ class WorkflowManager:
logger.info(f"Executing workflow mode={workflowMode} with {len(taskPlan.tasks)} tasks") logger.info(f"Executing workflow mode={workflowMode} with {len(taskPlan.tasks)} tasks")
return taskPlan return taskPlan
async def _executeTasks(self, taskPlan, workflow: ChatWorkflow) -> None: async def _executeTasks(self, taskPlan) -> None:
"""Execute all tasks in the task plan and update workflow status.""" """Execute all tasks in the task plan and update workflow status."""
workflow = self.services.workflow
handling = self.workflowProcessor handling = self.workflowProcessor
totalTasks = len(taskPlan.tasks) totalTasks = len(taskPlan.tasks)
allTaskResults: List = [] allTaskResults: List = []
@ -377,11 +385,12 @@ class WorkflowManager:
workflow.status = "completed" workflow.status = "completed"
return None return None
async def _processWorkflowResults(self, workflow: ChatWorkflow) -> None: async def _processWorkflowResults(self) -> None:
"""Process workflow results based on workflow status and create appropriate messages""" """Process workflow results based on workflow status and create appropriate messages"""
try: try:
workflow = self.services.workflow
try: try:
self.workflowProcessor._checkWorkflowStopped(workflow) checkWorkflowStopped(self.services)
except WorkflowStoppedException: except WorkflowStoppedException:
logger.info(f"Workflow {workflow.id} was stopped during result processing") logger.info(f"Workflow {workflow.id} was stopped during result processing")
@ -403,12 +412,12 @@ class WorkflowManager:
"taskProgress": "stopped", "taskProgress": "stopped",
"actionProgress": "stopped" "actionProgress": "stopped"
} }
self.services.workflow.storeMessageWithDocuments(workflow, stoppedMessage, []) self.services.chat.storeMessageWithDocuments(workflow, stoppedMessage, [])
# Update workflow status to stopped # Update workflow status to stopped
workflow.status = "stopped" workflow.status = "stopped"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "stopped", "status": "stopped",
"lastActivity": workflow.lastActivity "lastActivity": workflow.lastActivity
}) })
@ -433,12 +442,12 @@ class WorkflowManager:
"taskProgress": "stopped", "taskProgress": "stopped",
"actionProgress": "stopped" "actionProgress": "stopped"
} }
self.services.workflow.storeMessageWithDocuments(workflow, stoppedMessage, []) self.services.chat.storeMessageWithDocuments(workflow, stopped_message, [])
# Update workflow status to stopped # Update workflow status to stopped
workflow.status = "stopped" workflow.status = "stopped"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "stopped", "status": "stopped",
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"totalTasks": workflow.totalTasks, "totalTasks": workflow.totalTasks,
@ -446,7 +455,7 @@ class WorkflowManager:
}) })
# Add stopped log entry # Add stopped log entry
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": "Workflow stopped by user", "message": "Workflow stopped by user",
"type": "warning", "type": "warning",
"status": "stopped", "status": "stopped",
@ -472,12 +481,12 @@ class WorkflowManager:
"taskProgress": "fail", "taskProgress": "fail",
"actionProgress": "fail" "actionProgress": "fail"
} }
self.services.workflow.storeMessageWithDocuments(workflow, errorMessage, []) self.services.chat.storeMessageWithDocuments(workflow, errorMessage, [])
# Update workflow status to failed # Update workflow status to failed
workflow.status = "failed" workflow.status = "failed"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "failed", "status": "failed",
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"totalTasks": workflow.totalTasks, "totalTasks": workflow.totalTasks,
@ -485,7 +494,7 @@ class WorkflowManager:
}) })
# Add failed log entry # Add failed log entry
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": "Workflow failed: Unknown error", "message": "Workflow failed: Unknown error",
"type": "error", "type": "error",
"status": "failed", "status": "failed",
@ -494,7 +503,7 @@ class WorkflowManager:
return return
# For successful workflows, send detailed completion message # For successful workflows, send detailed completion message
await self._sendLastMessage(workflow) await self._sendLastMessage()
except Exception as e: except Exception as e:
logger.error(f"Error processing workflow results: {str(e)}") logger.error(f"Error processing workflow results: {str(e)}")
@ -516,28 +525,29 @@ class WorkflowManager:
"taskProgress": "fail", "taskProgress": "fail",
"actionProgress": "fail" "actionProgress": "fail"
} }
self.services.workflow.storeMessageWithDocuments(workflow, errorMessage, []) self.services.chat.storeMessageWithDocuments(workflow, error_message, [])
# Update workflow status to failed # Update workflow status to failed
workflow.status = "failed" workflow.status = "failed"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "failed", "status": "failed",
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"totalTasks": workflow.totalTasks, "totalTasks": workflow.totalTasks,
"totalActions": workflow.totalActions "totalActions": workflow.totalActions
}) })
async def _sendLastMessage(self, workflow: ChatWorkflow) -> None: async def _sendLastMessage(self) -> None:
"""Send last message to complete workflow (only for successful workflows)""" """Send last message to complete workflow (only for successful workflows)"""
try: try:
workflow = self.services.workflow
# Safety check: ensure this is only called for successful workflows # Safety check: ensure this is only called for successful workflows
if workflow.status in ['stopped', 'failed']: if workflow.status in ['stopped', 'failed']:
logger.warning(f"Attempted to send last message for {workflow.status} workflow {workflow.id}") logger.warning(f"Attempted to send last message for {workflow.status} workflow {workflow.id}")
return return
# Generate feedback # Generate feedback
feedback = await self._generateWorkflowFeedback(workflow) feedback = await self._generateWorkflowFeedback()
# Create last message using interface # Create last message using interface
messageData = { messageData = {
@ -559,20 +569,20 @@ class WorkflowManager:
} }
# Create message using interface # Create message using interface
self.services.workflow.storeMessageWithDocuments(workflow, messageData, []) self.services.chat.storeMessageWithDocuments(workflow, messageData, [])
# Update workflow status to completed # Update workflow status to completed
workflow.status = "completed" workflow.status = "completed"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
# Update workflow in database # Update workflow in database
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "completed", "status": "completed",
"lastActivity": workflow.lastActivity "lastActivity": workflow.lastActivity
}) })
# Add completion log entry # Add completion log entry
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": "Workflow completed", "message": "Workflow completed",
"type": "success", "type": "success",
"status": "completed", "status": "completed",
@ -583,10 +593,11 @@ class WorkflowManager:
logger.error(f"Error sending last message: {str(e)}") logger.error(f"Error sending last message: {str(e)}")
raise raise
async def _generateWorkflowFeedback(self, workflow: ChatWorkflow) -> str: async def _generateWorkflowFeedback(self) -> str:
"""Generate feedback message for workflow completion""" """Generate feedback message for workflow completion"""
try: try:
self.workflowProcessor._checkWorkflowStopped(workflow) workflow = self.services.workflow
checkWorkflowStopped(self.services)
# Count messages by role # Count messages by role
userMessages = [msg for msg in workflow.messages if msg.role == 'user'] userMessages = [msg for msg in workflow.messages if msg.role == 'user']
@ -610,14 +621,15 @@ class WorkflowManager:
logger.error(f"Error generating workflow feedback: {str(e)}") logger.error(f"Error generating workflow feedback: {str(e)}")
return "Workflow processing completed." return "Workflow processing completed."
def _handleWorkflowStop(self, workflow: ChatWorkflow) -> None: def _handleWorkflowStop(self) -> None:
"""Handle workflow stop exception""" """Handle workflow stop exception"""
workflow = self.services.workflow
logger.info("Workflow stopped by user") logger.info("Workflow stopped by user")
# Update workflow status to stopped # Update workflow status to stopped
workflow.status = "stopped" workflow.status = "stopped"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "stopped", "status": "stopped",
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"totalTasks": workflow.totalTasks, "totalTasks": workflow.totalTasks,
@ -642,24 +654,25 @@ class WorkflowManager:
"taskProgress": "pending", "taskProgress": "pending",
"actionProgress": "pending" "actionProgress": "pending"
} }
self.services.workflow.storeMessageWithDocuments(workflow, stopped_message, []) self.services.chat.storeMessageWithDocuments(workflow, stopped_message, [])
# Add log entry # Add log entry
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": "Workflow stopped by user", "message": "Workflow stopped by user",
"type": "warning", "type": "warning",
"status": "stopped", "status": "stopped",
"progress": 100 "progress": 100
}) })
def _handleWorkflowError(self, workflow: ChatWorkflow, error: Exception) -> None: def _handleWorkflowError(self, error: Exception) -> None:
"""Handle workflow error exception""" """Handle workflow error exception"""
workflow = self.services.workflow
logger.error(f"Workflow processing error: {str(error)}") logger.error(f"Workflow processing error: {str(error)}")
# Update workflow status to failed # Update workflow status to failed
workflow.status = "failed" workflow.status = "failed"
workflow.lastActivity = self.services.utils.timestampGetUtc() workflow.lastActivity = self.services.utils.timestampGetUtc()
self.services.workflow.updateWorkflow(workflow.id, { self.services.chat.updateWorkflow(workflow.id, {
"status": "failed", "status": "failed",
"lastActivity": workflow.lastActivity, "lastActivity": workflow.lastActivity,
"totalTasks": workflow.totalTasks, "totalTasks": workflow.totalTasks,
@ -684,10 +697,10 @@ class WorkflowManager:
"taskProgress": "fail", "taskProgress": "fail",
"actionProgress": "fail" "actionProgress": "fail"
} }
self.services.workflow.storeMessageWithDocuments(workflow, error_message, []) self.services.chat.storeMessageWithDocuments(workflow, error_message, [])
# Add error log entry # Add error log entry
self.services.workflow.storeLog(workflow, { self.services.chat.storeLog(workflow, {
"message": f"Workflow failed: {str(error)}", "message": f"Workflow failed: {str(error)}",
"type": "error", "type": "error",
"status": "failed", "status": "failed",
@ -701,8 +714,8 @@ class WorkflowManager:
documents = [] documents = []
for fileId in fileIds: for fileId in fileIds:
try: try:
# Get file info from unified workflow service # Get file info from chat service
fileInfo = self.services.workflow.getFileInfo(fileId) fileInfo = self.services.chat.getFileInfo(fileId)
if fileInfo: if fileInfo:
# Create document directly with all file attributes # Create document directly with all file attributes
document = ChatDocument( document = ChatDocument(

View file

@ -214,11 +214,6 @@ class MethodAiOperationsTester:
print(f"Debug: services id: {id(self.services)}") print(f"Debug: services id: {id(self.services)}")
print(f"Debug: methodAi.services id: {id(self.methodAi.services)}") print(f"Debug: methodAi.services id: {id(self.methodAi.services)}")
# Final safety check: ensure methodAi.services has the workflow
if hasattr(self.methodAi, 'services') and not self.methodAi.services.currentWorkflow:
print(f"⚠️ Fixing: Setting workflow in methodAi.services...")
self.methodAi.services.currentWorkflow = self.services.currentWorkflow
actionResult = await self.methodAi.process(parameters) actionResult = await self.methodAi.process(parameters)
endTime = asyncio.get_event_loop().time() endTime = asyncio.get_event_loop().time()