structuring action logic

This commit is contained in:
ValueOn AG 2025-06-12 17:52:51 +02:00
parent 8e63357190
commit eebd995d64
4 changed files with 545 additions and 269 deletions

View file

@ -42,7 +42,6 @@ class TaskStatus(str, Enum):
COMPLETED = "completed" COMPLETED = "completed"
FAILED = "failed" FAILED = "failed"
CANCELLED = "cancelled" CANCELLED = "cancelled"
ROLLED_BACK = "rolled_back"
# Register labels for TaskStatus # Register labels for TaskStatus
register_model_labels( register_model_labels(
@ -164,204 +163,131 @@ register_model_labels(
class TaskAction(BaseModel, ModelMixin): class TaskAction(BaseModel, ModelMixin):
"""Model for task actions""" """Model for task actions"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique action identifier") id: str = Field(..., description="Action ID")
method: str = Field(..., description="Method to execute") execMethod: str = Field(..., description="Method to execute")
action: str = Field(..., description="Action to perform") execAction: str = Field(..., description="Action to perform")
parameters: Dict[str, Any] = Field(default_factory=dict, description="Action parameters") execParameters: Dict[str, Any] = Field(default_factory=dict, description="Action parameters")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Current action status") execResultLabel: Optional[str] = Field(None, description="Label for the set of result documents")
retryCount: int = Field(default=0, description="Number of retry attempts") status: TaskStatus = Field(default=TaskStatus.PENDING, description="Action status")
retryMax: int = Field(default=3, description="Maximum number of retry attempts")
error: Optional[str] = Field(None, description="Error message if action failed") error: Optional[str] = Field(None, description="Error message if action failed")
startedAt: Optional[datetime] = Field(None, description="Action start timestamp") retryCount: int = Field(default=0, description="Number of retries attempted")
finishedAt: Optional[datetime] = Field(None, description="Action completion timestamp") retryMax: int = Field(default=3, description="Maximum number of retries")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), description="When the action was executed")
def start(self) -> None: def isSuccessful(self) -> bool:
"""Start the action""" """Check if action was successful"""
self.status = TaskStatus.RUNNING return self.status == TaskStatus.COMPLETED
self.startedAt = datetime.now(UTC)
def complete(self) -> None: def hasError(self) -> bool:
"""Mark action as completed""" """Check if action has an error"""
self.status = TaskStatus.COMPLETED return self.status == TaskStatus.FAILED
self.finishedAt = datetime.now(UTC)
def fail(self, error: str) -> None: def getErrorMessage(self) -> Optional[str]:
"""Mark action as failed""" """Get error message if any"""
self.status = TaskStatus.FAILED return self.error if self.hasError() else None
def setError(self, error: str) -> None:
"""Set action error"""
self.error = error self.error = error
self.finishedAt = datetime.now(UTC) self.status = TaskStatus.FAILED
def canRetry(self) -> bool: def setSuccess(self) -> None:
"""Check if action can be retried""" """Set action as successful"""
return self.retryCount < self.retryMax self.status = TaskStatus.COMPLETED
self.error = None
def incrementRetry(self) -> None:
"""Increment retry count"""
self.retryCount += 1
# Register labels for TaskAction # Register labels for TaskAction
register_model_labels( register_model_labels(
"TaskAction", "TaskAction",
{"en": "Task Action", "fr": "Action de tâche"}, {"en": "Task Action", "fr": "Action de tâche"},
{ {
"id": {"en": "ID", "fr": "ID"}, "id": {"en": "Action ID", "fr": "ID de l'action"},
"method": {"en": "Method", "fr": "Méthode"}, "execMethod": {"en": "Method", "fr": "Méthode"},
"action": {"en": "Action", "fr": "Action"}, "execAction": {"en": "Action", "fr": "Action"},
"parameters": {"en": "Parameters", "fr": "Paramètres"}, "execParameters": {"en": "Parameters", "fr": "Paramètres"},
"status": {"en": "Status", "fr": "Statut"}, "status": {"en": "Status", "fr": "Statut"},
"retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
"retryMax": {"en": "Max Retries", "fr": "Tentatives maximales"},
"error": {"en": "Error", "fr": "Erreur"}, "error": {"en": "Error", "fr": "Erreur"},
"startedAt": {"en": "Started At", "fr": "Démarré le"}, "retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
"finishedAt": {"en": "Finished At", "fr": "Terminé le"} "retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
"resultDocuments": {"en": "Result Documents", "fr": "Documents du résultat"},
"execResultLabel": {"en": "Document Label", "fr": "Label du document"},
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
"timestamp": {"en": "Timestamp", "fr": "Horodatage"}
} }
) )
class TaskItem(BaseModel, ModelMixin): class TaskItem(BaseModel, ModelMixin):
"""Model for tasks""" """Model for workflow tasks"""
id: str = Field(..., description="Unique task identifier") id: str = Field(..., description="Task ID")
workflowId: str = Field(..., description="Associated workflow ID") workflowId: str = Field(..., description="Workflow ID")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Current task status") userInput: str = Field(..., description="User input that triggered the task")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
error: Optional[str] = Field(None, description="Error message if task failed") error: Optional[str] = Field(None, description="Error message if task failed")
startedAt: Optional[datetime] = Field(None, description="Task start timestamp") startedAt: Optional[str] = Field(None, description="When the task started")
finishedAt: Optional[datetime] = Field(None, description="Task completion timestamp") finishedAt: Optional[str] = Field(None, description="When the task finished")
actionList: List[TaskAction] = Field(default_factory=list, description="List of actions to execute") actionList: List[TaskAction] = Field(default_factory=list, description="List of actions to execute")
documentsOutput: List[Dict[str, Any]] = Field(default_factory=list, description="Output documents") retryCount: int = Field(default=0, description="Number of retries attempted")
retryCount: int = Field(default=0, description="Number of retry attempts") retryMax: int = Field(default=3, description="Maximum number of retries")
retryMax: int = Field(default=3, description="Maximum number of retry attempts")
rollbackOnFailure: bool = Field(default=True, description="Whether to rollback on failure") rollbackOnFailure: bool = Field(default=True, description="Whether to rollback on failure")
dependencies: List[str] = Field(default_factory=list, description="List of dependent task IDs") dependencies: List[str] = Field(default_factory=list, description="List of task IDs this task depends on")
feedback: Optional[Dict[str, Any]] = Field(None, description="Task feedback data") feedback: Optional[str] = Field(None, description="Task feedback message")
processingTime: Optional[float] = Field(None, description="Total processing time in seconds")
resultLabels: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Map of result labels to their values")
def isCompleted(self) -> bool: def isSuccessful(self) -> bool:
"""Check if task is completed""" """Check if task was successful"""
return self.status == TaskStatus.COMPLETED return self.status == TaskStatus.COMPLETED
def isFailed(self) -> bool: def hasError(self) -> bool:
"""Check if task has failed""" """Check if task has an error"""
return self.status == TaskStatus.FAILED return self.status == TaskStatus.FAILED
def canRetry(self) -> bool: def getErrorMessage(self) -> Optional[str]:
"""Check if task can be retried""" """Get error message if any"""
return self.retryCount < self.retryMax return self.error if self.hasError() else None
def start(self) -> None: def getResultDocuments(self) -> List[ChatDocument]:
"""Start the task""" """Get all documents from all successful actions"""
self.status = TaskStatus.RUNNING documents = []
self.startedAt = datetime.now(UTC) for action in self.actionList:
if action.isSuccessful() and action.resultDocuments:
documents.extend(action.resultDocuments)
return documents
def complete(self) -> None: def getResultDocumentLabel(self) -> Optional[str]:
"""Mark task as completed""" """Get the label for the result documents"""
self.status = TaskStatus.COMPLETED for action in self.actionList:
self.finishedAt = datetime.now(UTC) if action.isSuccessful() and action.execResultLabel:
return action.execResultLabel
return None
def fail(self, error: str) -> None: def getResultLabel(self, label: str) -> Optional[Any]:
"""Mark task as failed""" """Get value for a specific result label"""
self.status = TaskStatus.FAILED return self.resultLabels.get(label) if self.resultLabels else None
self.error = error
self.finishedAt = datetime.now(UTC)
def cancel(self) -> None:
"""Cancel the task"""
self.status = TaskStatus.CANCELLED
self.finishedAt = datetime.now(UTC)
def rollback(self) -> None:
"""Mark task as rolled back"""
self.status = TaskStatus.ROLLED_BACK
self.finishedAt = datetime.now(UTC)
def incrementRetry(self) -> None:
"""Increment retry count"""
self.retryCount += 1
def addDependency(self, taskId: str) -> None:
"""Add a task dependency"""
if taskId not in self.dependencies:
self.dependencies.append(taskId)
def removeDependency(self, taskId: str) -> None:
"""Remove a task dependency"""
if taskId in self.dependencies:
self.dependencies.remove(taskId)
def addAction(self, action: TaskAction) -> None:
"""Add an action to the task"""
self.actionList.append(action)
def addDocumentOutput(self, document: Dict[str, Any]) -> None:
"""Add an output document"""
self.documentsOutput.append(document)
def setFeedback(self, feedback: Dict[str, Any]) -> None:
"""Set task feedback"""
self.feedback = feedback
# Register labels for TaskItem # Register labels for TaskItem
register_model_labels( register_model_labels(
"TaskItem", "TaskItem",
{"en": "Task", "fr": "Tâche"}, {"en": "Task", "fr": "Tâche"},
{ {
"id": {"en": "ID", "fr": "ID"}, "id": {"en": "Task ID", "fr": "ID de la tâche"},
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"}, "workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
"userInput": {"en": "User Input", "fr": "Entrée utilisateur"},
"status": {"en": "Status", "fr": "Statut"}, "status": {"en": "Status", "fr": "Statut"},
"error": {"en": "Error", "fr": "Erreur"}, "error": {"en": "Error", "fr": "Erreur"},
"startedAt": {"en": "Started At", "fr": "Démarré le"}, "startedAt": {"en": "Started At", "fr": "Démarré à"},
"finishedAt": {"en": "Finished At", "fr": "Terminé le"}, "finishedAt": {"en": "Finished At", "fr": "Terminé à"},
"actionList": {"en": "Action List", "fr": "Liste d'actions"}, "actionList": {"en": "Actions", "fr": "Actions"},
"documentsOutput": {"en": "Output Documents", "fr": "Documents de sortie"},
"retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"}, "retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
"retryMax": {"en": "Max Retries", "fr": "Tentatives maximales"}, "retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
"rollbackOnFailure": {"en": "Rollback on Failure", "fr": "Annulation en cas d'échec"}, "rollbackOnFailure": {"en": "Rollback On Failure", "fr": "Annuler en cas d'échec"},
"dependencies": {"en": "Dependencies", "fr": "Dépendances"}, "dependencies": {"en": "Dependencies", "fr": "Dépendances"},
"feedback": {"en": "Task Feedback", "fr": "Retour sur la tâche"}
}
)
class TaskResult(BaseModel, ModelMixin):
"""Model for task execution results"""
taskId: str = Field(..., description="ID of the task this result belongs to")
status: TaskStatus = Field(..., description="Result status")
success: bool = Field(..., description="Whether the task was successful")
error: Optional[str] = Field(None, description="Error message if task failed")
data: Optional[Dict[str, Any]] = Field(None, description="Result data")
documents: List[ChatDocument] = Field(default_factory=list, description="Output documents")
documentsLabel: Optional[str] = Field(None, description="Label for the set of documents")
feedback: Optional[str] = Field(None, description="Task feedback message")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC), description="When the result was created")
def isSuccessful(self) -> bool:
"""Check if result indicates success"""
return self.success and self.status == TaskStatus.COMPLETED
def hasError(self) -> bool:
"""Check if result has an error"""
return not self.success or self.status == TaskStatus.FAILED
def getErrorMessage(self) -> Optional[str]:
"""Get error message if any"""
return self.error if self.hasError() else None
# Register labels for TaskResult
register_model_labels(
"TaskResult",
{"en": "Task Result", "fr": "Résultat de la tâche"},
{
"taskId": {"en": "Task ID", "fr": "ID de la tâche"},
"status": {"en": "Status", "fr": "Statut"},
"success": {"en": "Success", "fr": "Succès"},
"error": {"en": "Error", "fr": "Erreur"},
"data": {"en": "Data", "fr": "Données"},
"documents": {"en": "Documents", "fr": "Documents"},
"feedback": {"en": "Feedback", "fr": "Retour"}, "feedback": {"en": "Feedback", "fr": "Retour"},
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"}, "processingTime": {"en": "Processing Time", "fr": "Temps de traitement"}
"timestamp": {"en": "Timestamp", "fr": "Horodatage"}
} }
) )
# ===== Message and Workflow Models =====
class ChatStat(BaseModel, ModelMixin): class ChatStat(BaseModel, ModelMixin):
"""Data model for chat statistics""" """Data model for chat statistics"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
@ -420,6 +346,7 @@ class ChatMessage(BaseModel, ModelMixin):
workflowId: str = Field(description="Foreign key to workflow") workflowId: str = Field(description="Foreign key to workflow")
parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading") parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading")
documents: List[ChatDocument] = Field(default_factory=list, description="Associated documents") documents: List[ChatDocument] = Field(default_factory=list, description="Associated documents")
documentsLabel: Optional[str] = Field(None, description="Label for the set of documents")
message: Optional[str] = Field(None, description="Message content") message: Optional[str] = Field(None, description="Message content")
role: str = Field(description="Role of the message sender") role: str = Field(description="Role of the message sender")
status: str = Field(description="Status of the message (first, step, last)") status: str = Field(description="Status of the message (first, step, last)")
@ -427,6 +354,9 @@ class ChatMessage(BaseModel, ModelMixin):
publishedAt: str = Field(description="When the message was published") publishedAt: str = Field(description="When the message was published")
stats: Optional[ChatStat] = Field(None, description="Statistics for this message") stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
success: Optional[bool] = Field(None, description="Whether the message processing was successful") success: Optional[bool] = Field(None, description="Whether the message processing was successful")
actionId: Optional[str] = Field(None, description="ID of the action that produced this message")
actionMethod: Optional[str] = Field(None, description="Method of the action that produced this message")
actionName: Optional[str] = Field(None, description="Name of the action that produced this message")
# Register labels for ChatMessage # Register labels for ChatMessage
register_model_labels( register_model_labels(
@ -437,13 +367,17 @@ register_model_labels(
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"}, "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
"parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"}, "parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
"documents": {"en": "Documents", "fr": "Documents"}, "documents": {"en": "Documents", "fr": "Documents"},
"documentsLabel": {"en": "Documents Label", "fr": "Label des documents"},
"message": {"en": "Message", "fr": "Message"}, "message": {"en": "Message", "fr": "Message"},
"role": {"en": "Role", "fr": "Rôle"}, "role": {"en": "Role", "fr": "Rôle"},
"status": {"en": "Status", "fr": "Statut"}, "status": {"en": "Status", "fr": "Statut"},
"sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"}, "sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
"publishedAt": {"en": "Published At", "fr": "Publié le"}, "publishedAt": {"en": "Published At", "fr": "Publié le"},
"stats": {"en": "Statistics", "fr": "Statistiques"}, "stats": {"en": "Statistics", "fr": "Statistiques"},
"success": {"en": "Success", "fr": "Succès"} "success": {"en": "Success", "fr": "Succès"},
"actionId": {"en": "Action ID", "fr": "ID de l'action"},
"actionMethod": {"en": "Action Method", "fr": "Méthode de l'action"},
"actionName": {"en": "Action Name", "fr": "Nom de l'action"}
} }
) )

View file

@ -3,6 +3,7 @@ from typing import Dict, Any, Optional, List, Union
from datetime import datetime, UTC from datetime import datetime, UTC
import json import json
import uuid import uuid
import time
from modules.interfaces.serviceAppModel import User from modules.interfaces.serviceAppModel import User
from modules.interfaces.serviceChatModel import ( from modules.interfaces.serviceChatModel import (
@ -50,7 +51,10 @@ class ChatManager:
} }
# Create task using ChatInterface # Create task using ChatInterface
return self.service.createTask(taskData) task = self.service.createTask(taskData)
if task:
self.service.currentTask = task
return task
except Exception as e: except Exception as e:
logger.error(f"Error creating initial task: {str(e)}") logger.error(f"Error creating initial task: {str(e)}")
@ -81,7 +85,10 @@ class ChatManager:
} }
# Create task using ChatInterface # Create task using ChatInterface
return self.service.createTask(taskData) task = self.service.createTask(taskData)
if task:
self.service.currentTask = task
return task
except Exception as e: except Exception as e:
logger.error(f"Error creating next task: {str(e)}") logger.error(f"Error creating next task: {str(e)}")
@ -221,111 +228,215 @@ Document: {document.filename} ({document.mimeType})
return "" return ""
# ===== Task Execution and Processing ===== # ===== Task Execution and Processing =====
async def executeTask(self, task: TaskItem) -> TaskResult: async def executeTask(self, task: TaskItem) -> TaskItem:
"""Execute a task and return its result""" """Execute a task with its list of actions"""
try: try:
# Create result object
result = TaskResult(
taskId=task.id,
status=task.status,
success=True,
timestamp=datetime.now(UTC)
)
# Start timing # Start timing
startTime = datetime.now(UTC) start_time = time.time()
task.startedAt = datetime.now(UTC).isoformat()
# Execute each action task.status = TaskStatus.RUNNING
# Execute each action in sequence
for action in task.actionList: for action in task.actionList:
try: try:
# Execute action # Execute action
actionResult = await action.execute() action_start = time.time()
result = await self._executeAction(action)
# Update action status action.processingTime = time.time() - action_start
action.status = actionResult.status
if actionResult.error:
action.error = actionResult.error
except Exception as e:
logger.error(f"Action execution error: {str(e)}")
action.status = TaskStatus.FAILED
action.error = str(e)
# Calculate processing time
endTime = datetime.now(UTC)
result.processingTime = (endTime - startTime).total_seconds()
# Update task status
if all(action.status == TaskStatus.COMPLETED for action in task.actionList):
result.status = TaskStatus.COMPLETED
result.success = True
else:
result.status = TaskStatus.FAILED
result.success = False
result.error = "One or more actions failed"
# Generate feedback and documents if task completed successfully
if result.status == TaskStatus.COMPLETED:
# Generate feedback using AI
result.feedback = await self._processTaskResults(task)
# Create output documents
result.documents = await self._createOutputDocuments(task)
# Set documents label based on task input
result.documentsLabel = "TaskResult"
else:
result.feedback = f"Task failed: {result.error}"
# Update task in database
self.service.updateTask(task.id, {
"status": result.status,
"error": result.error,
"finishedAt": datetime.now(UTC).isoformat(),
"actionList": [action.dict() for action in task.actionList],
"documentsOutput": result.documents,
"feedback": result.feedback,
"documentsLabel": result.documentsLabel
})
return result
except Exception as e:
logger.error(f"Task execution error: {str(e)}")
raise
async def parseTaskResult(self, workflow: ChatWorkflow, result: TaskResult) -> None: # Validate result
if not result.success:
error_msg = result.error or "Unknown error"
action.setError(error_msg)
# Create error message
error_message = ChatMessage(
workflowId=task.workflowId,
message=f"{action.execMethod}.{action.execAction}.error: {error_msg}",
role="system",
status="step",
sequenceNr=0, # Will be set by workflow
publishedAt=datetime.now(UTC).isoformat(),
success=False,
actionId=action.id,
actionMethod=action.execMethod,
actionName=action.execAction
)
# Add error message to workflow
await self._addMessageToWorkflow(task.workflowId, error_message)
# If action failed and we have retries left, retry
if action.retryCount < action.retryMax:
action.retryCount += 1
continue
# If we're out of retries, fail the task
task.error = f"Action {action.id} failed after {action.retryCount} retries: {error_msg}"
task.status = TaskStatus.FAILED
return task
# Process successful result
action.setSuccess()
# Set result label from AI response if provided
if result.data.get("resultLabel"):
action.execResultLabel = result.data["resultLabel"]
# Create result message with documents if any
if result.data.get("documents"):
# Store AI-generated documents in database and create ChatDocuments
documents = []
for doc in result.data["documents"]:
# Create document (which also creates the file)
document = await self.service.createDocument(
fileName=doc["filename"],
mimeType=doc["mimeType"],
content=doc["content"],
base64encoded=doc["base64Encoded"]
)
documents.append(document)
# Create success message with documents
success_message = ChatMessage(
workflowId=task.workflowId,
message=f"{action.execMethod}.{action.execAction}",
role="system",
status="step",
sequenceNr=0, # Will be set by workflow
publishedAt=datetime.now(UTC).isoformat(),
documents=documents,
documentsLabel=action.execResultLabel, # Use the label from action
success=True,
actionId=action.id,
actionMethod=action.execMethod,
actionName=action.execAction
)
# Add success message to workflow
await self._addMessageToWorkflow(task.workflowId, success_message)
# Store result labels
if result.data.get("labels"):
task.resultLabels.update(result.data["labels"])
except Exception as e:
error_msg = str(e)
action.setError(error_msg)
# Create error message
error_message = ChatMessage(
workflowId=task.workflowId,
message=f"{action.execMethod}.{action.execAction}.error: {error_msg}",
role="system",
status="step",
sequenceNr=0, # Will be set by workflow
publishedAt=datetime.now(UTC).isoformat(),
success=False,
actionId=action.id,
actionMethod=action.execMethod,
actionName=action.execAction
)
# Add error message to workflow
await self._addMessageToWorkflow(task.workflowId, error_message)
# If action failed and we have retries left, retry
if action.retryCount < action.retryMax:
action.retryCount += 1
continue
# If we're out of retries, fail the task
task.error = f"Action {action.id} failed after {action.retryCount} retries: {error_msg}"
task.status = TaskStatus.FAILED
return task
# Check if all actions were successful
if all(action.isSuccessful() for action in task.actionList):
task.status = TaskStatus.COMPLETED
task.feedback = "Task completed successfully"
# Create chat message with results
message = ChatMessage(
workflowId=task.workflowId,
message=task.feedback,
role="system",
status="last",
sequenceNr=0, # Will be set by workflow
publishedAt=datetime.now(UTC).isoformat(),
success=True
)
# Add message to workflow
await self._addMessageToWorkflow(task.workflowId, message)
else:
# If any action failed, fail the task
task.status = TaskStatus.FAILED
task.error = "One or more actions failed"
# Create error message
error_message = ChatMessage(
workflowId=task.workflowId,
message=f"Task failed: {task.getErrorMessage()}",
role="system",
status="last",
sequenceNr=0, # Will be set by workflow
publishedAt=datetime.now(UTC).isoformat(),
success=False
)
# Add error message to workflow
await self._addMessageToWorkflow(task.workflowId, error_message)
# Calculate processing time
task.processingTime = time.time() - start_time
task.finishedAt = datetime.now(UTC).isoformat()
return task
except Exception as e:
# Handle unexpected errors
task.status = TaskStatus.FAILED
task.error = str(e)
task.finishedAt = datetime.now(UTC).isoformat()
# Create error message
error_message = ChatMessage(
workflowId=task.workflowId,
message=f"Task failed with unexpected error: {task.getErrorMessage()}",
role="system",
status="last",
sequenceNr=0, # Will be set by workflow
publishedAt=datetime.now(UTC).isoformat(),
success=False
)
# Add error message to workflow
await self._addMessageToWorkflow(task.workflowId, error_message)
return task
async def parseTaskResult(self, workflow: ChatWorkflow, task: TaskItem) -> None:
"""Process and store task result in workflow""" """Process and store task result in workflow"""
try: try:
# Find task in workflow
task = self.service.getTask(result.taskId)
if not task:
logger.error(f"Task {result.taskId} not found in workflow")
return
# Update task status
self.service.updateTask(task.id, {
"status": result.status,
"error": result.error,
"finishedAt": datetime.now(UTC).isoformat()
})
# Create feedback message if available # Create feedback message if available
if result.feedback: if task.feedback:
message = ChatMessage( message = ChatMessage(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
workflowId=workflow.id, workflowId=workflow.id,
role="assistant", role="assistant",
message=result.feedback, message=task.feedback,
status="step", status="step",
documents=result.documents documents=task.getResultDocuments()
) )
self.service.createWorkflowMessage(message.dict()) self.service.createWorkflowMessage(message.dict())
# Update workflow stats # Update workflow stats
if result.processingTime: if task.processingTime:
if not workflow.stats: if not workflow.stats:
workflow.stats = ChatStat() workflow.stats = ChatStat()
workflow.stats.processingTime = (workflow.stats.processingTime or 0) + result.processingTime workflow.stats.processingTime = (workflow.stats.processingTime or 0) + task.processingTime
self.service.updateWorkflow(workflow.id, {"stats": workflow.stats.dict()}) self.service.updateWorkflow(workflow.id, {"stats": workflow.stats.dict()})
except Exception as e: except Exception as e:
@ -593,4 +704,78 @@ Rules:
4. Include file extensions in filenames 4. Include file extensions in filenames
Return a JSON array of Document objects. Return a JSON array of Document objects.
""" """
def _createTaskDefinitionPrompt(self, userInput: str, workflow: ChatWorkflow) -> str:
"""Create prompt for task definition"""
# Get available methods
methodList = self.service.getMethodsList()
# Get workflow history
messageSummary = self.service.getMessageSummary(workflow.messages[-1] if workflow.messages else None)
# Get available documents and connections
docRefs = self.service.getDocumentReferenceList()
connRefs = self.service.getConnectionReferenceList()
prompt = f"""
Task Definition for: {userInput}
Available Methods:
{chr(10).join(f"- {method}" for method in methodList)}
Workflow History:
{chr(10).join(f"- {msg['message']}" for msg in messageSummary.get('chat', []))}
Available Documents:
{chr(10).join(f"- {doc['documentReference']} ({doc['datetime']})" for doc in docRefs.get('chat', []))}
Available Connections:
{chr(10).join(f"- {conn}" for conn in connRefs)}
Instructions:
1. Result Format (JSON):
{{
"status": "pending|running|completed|failed",
"feedback": "string explaining what was done and what needs to be done next",
"actions": [
{{
"method": "string",
"action": "string",
"parameters": {{
"param1": "value1",
"param2": "value2"
}},
"resultLabel": "documentList_<uuid>_<label>"
}}
]
}}
2. Available Data:
- Use only provided document references (format: document_<id>_<filename> or documentList_<action.id>_<label>)
- Use only provided connection references
- Use result labels from previous actions in the same task
3. Method Usage Rules:
- Syntax: method.action([parameter:type])->resultLabel:type
- resultLabel format: documentList_<uuid>_<label>
- Actions must be in processing sequence
- Parameters must be from:
* Available document references
* Available connection references
* Result labels from previous actions
4. Result Labels:
- Use consistent naming for related documents
- Include descriptive labels for document sets
- Labels will be used to track document sets in messages
5. Error Handling:
- Include validation for each action
- Specify retry behavior if needed
- Provide clear error messages
- Errors will be recorded in messages with .error: suffix
Please provide the task definition in JSON format following these rules.
"""
return prompt

View file

@ -13,6 +13,8 @@ from modules.interfaces.serviceChatClass import getInterface as getChatInterface
from modules.interfaces.serviceManagementClass import getInterface as getFileInterface from modules.interfaces.serviceManagementClass import getInterface as getFileInterface
from modules.workflow.managerDocument import DocumentManager from modules.workflow.managerDocument import DocumentManager
from modules.methods.methodBase import MethodBase from modules.methods.methodBase import MethodBase
import uuid
import base64
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,6 +27,7 @@ class ServiceContainer:
self.workflow = workflow self.workflow = workflow
self.tasks = workflow.tasks self.tasks = workflow.tasks
self.statusEnums = TaskStatus self.statusEnums = TaskStatus
self.currentTask = None # Initialize current task as None
# Initialize managers # Initialize managers
self.interfaceChat = getChatInterface(currentUser) self.interfaceChat = getChatInterface(currentUser)
@ -77,27 +80,129 @@ class ServiceContainer:
return self.methods return self.methods
def getMethodsList(self) -> List[str]: def getMethodsList(self) -> List[str]:
"""Get list of available methods""" """Get list of available methods with their signatures"""
return list(self.methods.keys()) methodList = []
for methodName, method in self.methods.items():
for actionName, action in method.actions.items():
# Get parameter types and return type from action signature
paramTypes = []
for paramName, param in action.parameters.items():
paramTypes.append(f"{paramName}:{param.type}")
# Format: method.action([param1:type, param2:type])->resultLabel:type # description
signature = f"{methodName}.{actionName}([{', '.join(paramTypes)}])->{action.resultLabel}:{action.resultType}"
if action.description:
signature += f" # {action.description}"
methodList.append(signature)
return methodList
def getDocumentReferenceList(self) -> Dict[str, List[Dict[str, str]]]: def getDocumentReferenceList(self) -> Dict[str, List[Dict[str, str]]]:
"""Get list of document references sorted by datetime""" """Get list of document references sorted by datetime, categorized by chat round"""
chat_refs = []
history_refs = []
# Process messages in reverse order to find current chat round
for message in reversed(self.workflow.messages):
# Get document references from message
if message.documents:
# For messages with action context, use documentList reference
if message.actionId and message.documentsLabel:
doc_ref = self.getDocumentReferenceFromMessage(message)
doc_info = {
"documentReference": doc_ref,
"datetime": message.publishedAt
}
# Add to appropriate list based on message status
if message.status == "first":
chat_refs.append(doc_info)
break # Stop after finding first message
elif message.status == "step":
chat_refs.append(doc_info)
else:
history_refs.append(doc_info)
# For regular messages, use individual document references
else:
for doc in message.documents:
doc_ref = self.getDocumentReferenceFromChatDocument(doc)
doc_info = {
"documentReference": doc_ref,
"datetime": message.publishedAt
}
# Add to appropriate list based on message status
if message.status == "first":
chat_refs.append(doc_info)
break # Stop after finding first message
elif message.status == "step":
chat_refs.append(doc_info)
else:
history_refs.append(doc_info)
# Stop processing if we hit a first message
if message.status == "first":
break
# Sort both lists by datetime in descending order
chat_refs.sort(key=lambda x: x["datetime"], reverse=True)
history_refs.sort(key=lambda x: x["datetime"], reverse=True)
return { return {
"chat": self._getChatDocumentReferences(), "chat": chat_refs,
"history": self._getHistoryDocumentReferences() "history": history_refs
} }
def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str: def getDocumentReferenceFromChatDocument(self, document: ChatDocument) -> str:
"""Get document reference from ChatDocument""" """Get document reference from ChatDocument"""
return f"document_{document.id}_{document.filename}" return f"document_{document.id}_{document.filename}"
def getDocumentReferenceFromTaskResult(self, result: TaskResult) -> str: def getDocumentReferenceFromMessage(self, message: ChatMessage) -> str:
"""Get document reference from TaskResult""" """Get document reference from ChatMessage with action context"""
return f"documentList_{result.id}_{result.documentsLabel}" if not message.actionId or not message.documentsLabel:
return None
# If documentsLabel already contains the full reference format, return it
if message.documentsLabel.startswith("documentList_"):
return message.documentsLabel
# Otherwise construct the reference
return f"documentList_{message.actionId}_{message.documentsLabel}"
def getChatDocumentsFromDocumentReference(self, documentReference: str) -> List[ChatDocument]: def getChatDocumentsFromDocumentReference(self, documentReference: str) -> List[ChatDocument]:
"""Get ChatDocuments from document reference""" """Get ChatDocuments from document reference"""
return self.documentManager.getDocumentsByReference(documentReference) try:
# Parse reference format
parts = documentReference.split('_', 2) # Split into max 3 parts
if len(parts) < 3:
return []
ref_type = parts[0]
ref_id = parts[1]
ref_label = parts[2] # Keep the full label
if ref_type == "document":
# Handle ChatDocument reference: document_<id>_<filename>
# Find document in workflow messages
for message in self.workflow.messages:
if message.documents:
for doc in message.documents:
if doc.id == ref_id:
return [doc]
elif ref_type == "documentList":
# Handle document list reference: documentList_<action.id>_<label>
# Find message with matching action ID and documents label
for message in self.workflow.messages:
if (message.actionId == ref_id and
message.documentsLabel == documentReference and # Compare full reference
message.documents):
return message.documents
return []
except Exception as e:
logger.error(f"Error getting documents from reference {documentReference}: {str(e)}")
return []
def getConnectionReferenceList(self) -> List[Dict[str, str]]: def getConnectionReferenceList(self) -> List[Dict[str, str]]:
"""Get list of connection references sorted by authority""" """Get list of connection references sorted by authority"""
@ -130,9 +235,42 @@ class ServiceContainer:
"""Call AI image service""" """Call AI image service"""
return self.interfaceAi.callAiImage(imageData, mimeType, prompt) return self.interfaceAi.callAiImage(imageData, mimeType, prompt)
def createFile(self, fileName: str, mimeType: str, content: bytes, base64encoded: bool = False) -> Dict[str, Any]: def createFile(self, fileName: str, mimeType: str, content: str, base64encoded: bool = False) -> str:
"""Create new file""" """Create new file and return its ID"""
return self.interfaceFiles.createFile(fileName, mimeType, content, base64encoded) # Convert content to bytes based on base64 flag
if base64encoded:
content_bytes = base64.b64decode(content)
else:
content_bytes = content.encode('utf-8')
# First create the file metadata
file_item = self.interfaceFiles.createFile(
name=fileName,
mimeType=mimeType,
size=len(content_bytes)
)
# Then store the file data
self.interfaceFiles.createFileData(file_item.id, content_bytes)
return file_item.id
def createDocument(self, fileName: str, mimeType: str, content: str, base64encoded: bool = True) -> ChatDocument:
"""Create document from file data object created by AI call"""
# First create the file and get its ID
file_id = self.createFile(fileName, mimeType, content, base64encoded)
# Get file info for metadata
file_info = self.interfaceFiles.getFile(file_id)
# Create document with file reference
return ChatDocument(
id=str(uuid.uuid4()),
fileId=file_id,
filename=fileName,
fileSize=file_info.fileSize,
mimeType=mimeType
)
def getFileInfo(self, fileId: str) -> Dict[str, Any]: def getFileInfo(self, fileId: str) -> Dict[str, Any]:
"""Get file information""" """Get file information"""

View file

@ -1,4 +1,25 @@
Prompt for task definition: TODO
- documenthandling to review with document-file-filedata --> redundant and too complex --> to have ChatDocument with label and reference to fileid with function to get metadate directly from file object
- prompt for task definition to fix
- implement all functions from service object correctly
- result parsing: execute task actions by using data references stepwise, all documents in TaskResults to save as ChatDocuments, to be available for next action.
- action execution: to use conversion functions, user DocumentReference and ConnectionReference
ADAPT:
can you analyse the following specification:
- clear what is required?
- clear what to do?
- questions?
please give a summarized feedback before implementing
How does the task process need to work:
- specification for task-prompt below ensures, that a json with actions is produced, where references are clear, always having uuid integrated, only valid references
- ai call result to be validated against json specification before proceeding, proper error handling for retry or abort depending on error (e.g. is ai down then to abort, if ai error to proceed with error to potentially fix it, etc.)
- then to process the action list.
Resulting specification Prompt for task definition:
- original user input (summary) - original user input (summary)
- prompt for task to do - prompt for task to do
- method list - method list
@ -9,16 +30,14 @@ Prompt for task definition:
- rules for result: status (enum), feedback (to tell what is done and what needed next. Only to to the tasks, which are possible with the available methods, referencing and data, rest to do in next round) - rules for result: status (enum), feedback (to tell what is done and what needed next. Only to to the tasks, which are possible with the available methods, referencing and data, rest to do in next round)
- available data to use: list of documentReference, list of connectionReference - available data to use: list of documentReference, list of connectionReference
- methods usage: - methods usage:
- syntax: method.action([parameter:type])->resultLabel:type - syntax: method.action([parameter:type])->resultLabel:type (reference note: resultLabel to be stored in TaskAction.execResultLabel for later use)
- resultLabel to be in format documentList_<generated uuid>_<generated label>
- sequence of method.action to be the sequence ot processing - sequence of method.action to be the sequence ot processing
- as parameter only to use available items of documentReference and connectionReference or resultLabel of a previous method.action - as parameter only to use available items of documentReference or connectionReference or resultLabel from a previous method.action
- required result format: json for TaskResult, nothing else - required result format: json, nothing else
TODO FINALIZE:
- result parsing: execute task actions by using data references stepwise, all documents in TaskResults to save as ChatDocuments, to be available for next action.
- action execution: to use conversion functions, user DocumentReference and ConnectionReference
service (ServiceContainer) service (ServiceContainer)
user <-- currentUser user <-- currentUser
@ -37,11 +56,11 @@ service (ServiceContainer)
sharepoint.download([connectionReference:str, filepath:str])->documentReference:str sharepoint.download([connectionReference:str, filepath:str])->documentReference:str
functions functions
extractContent(prompt,documentReference):str <- managerDocument.extractContent(prompt,FilePreview) ok extractContent(prompt,documentReference):str <- managerDocument.extractContent(prompt,FilePreview)
getMethodsCatalog():{...} <- to import dynamically all methods from the files "method*.py" in folder modules/methods ok getMethodsCatalog():{...} <- to import dynamically all methods from the files "method*.py" in folder modules/methods
getMethodsList():[str] <- to transform result from getMethodsCatalog into a list with the method items in format "method.action([parameter:type])->resultLabel:type" ok getMethodsList():[str] <- to transform result from getMethodsCatalog into a list with the method items in format "method.action([parameter:type])->resultLabel:type # description"
getDocumentReferenceList():{"chat":[{"documentReference":str,"datetime":str}],"history":[{"documentReference":str,"datetime":str}]} sorted by datetime desc ok getDocumentReferenceList():{"chat":[{"documentReference":str,"datetime":str}],"history":[{"documentReference":str,"datetime":str}]} sorted by datetime desc
getDocumentReferenceFromChatDocument(ChatDocument):"document_"+ChatDocument.id+"_"+ChatDocument.filename getDocumentReferenceFromChatDocument(ChatDocument):"document_"+ChatDocument.id+"_"+ChatDocument.filename
getDocumentReferenceFromTaskResult(TaskResult):"documentList_"+TaskResult.id+"_"+TaskResult.documentsLabel getDocumentReferenceFromTaskResult(TaskResult):"documentList_"+TaskResult.id+"_"+TaskResult.documentsLabel
getChatDocumentsFromDocumentReference(documentReference):List[ChatDocuments] getChatDocumentsFromDocumentReference(documentReference):List[ChatDocuments]