suna/backend/workflows/executor.py

247 lines
10 KiB
Python

import asyncio
import uuid
import json
from datetime import datetime, timezone
from typing import Dict, Any, Optional, AsyncGenerator
from .models import WorkflowDefinition, WorkflowExecution
from agent.run import run_agent
from services.supabase import DBConnection
from utils.logger import logger
class WorkflowExecutor:
"""Executes workflows using the AgentPress agent system."""
def __init__(self, db: DBConnection):
self.db = db
async def execute_workflow(
self,
workflow: WorkflowDefinition,
variables: Optional[Dict[str, Any]] = None,
thread_id: Optional[str] = None,
project_id: Optional[str] = None
) -> AsyncGenerator[Dict[str, Any], None]:
"""
Execute a workflow definition.
V1 Implementation: Generates a system prompt from the workflow
and executes it as a single agent call.
"""
if not thread_id:
thread_id = str(uuid.uuid4())
if not project_id:
project_id = workflow.project_id
logger.info(f"Executing workflow {workflow.name} (ID: {workflow.id}) in thread {thread_id}")
execution = WorkflowExecution(
id=str(uuid.uuid4()),
workflow_id=workflow.id or str(uuid.uuid4()),
status="running",
started_at=datetime.now(timezone.utc),
trigger_type="MANUAL",
trigger_data={},
variables=variables or {}
)
try:
await self._store_execution(execution)
await self._create_workflow_thread(thread_id, project_id, workflow, variables)
if not workflow.steps:
raise ValueError("Workflow has no steps defined")
main_step = workflow.steps[0]
system_prompt = main_step.config.get("system_prompt", "")
if variables:
variables_text = "\n\n## Workflow Variables\n"
variables_text += "The following variables are available for this workflow execution:\n"
for key, value in variables.items():
variables_text += f"- **{key}**: {value}\n"
variables_text += "\nUse these variables as needed during workflow execution.\n"
system_prompt += variables_text
agent_config = {
"name": f"Workflow Agent: {workflow.name}",
"description": workflow.description or "Generated workflow agent",
"system_prompt": system_prompt,
"agentpress_tools": {
"sb_files_tool": {"enabled": True, "description": "File operations"},
"message_tool": {"enabled": True, "description": "Send messages"},
"expand_msg_tool": {"enabled": True, "description": "Expand messages"}
},
"configured_mcps": [],
"custom_mcps": []
}
async for response in run_agent(
thread_id=thread_id,
project_id=project_id,
stream=True,
model_name="anthropic/claude-3-5-sonnet-latest",
enable_thinking=False,
reasoning_effort="low",
enable_context_manager=True,
agent_config=agent_config,
max_iterations=5
):
yield self._transform_agent_response_to_workflow_update(response, execution.id)
if response.get('type') == 'status':
status = response.get('status')
if status in ['completed', 'failed', 'stopped']:
execution.status = status.lower()
execution.completed_at = datetime.now(timezone.utc)
if status == 'failed':
execution.error = response.get('message', 'Workflow execution failed')
await self._update_execution(execution)
break
if execution.status == "running":
execution.status = "completed"
execution.completed_at = datetime.now(timezone.utc)
await self._update_execution(execution)
yield {
"type": "workflow_status",
"execution_id": execution.id,
"status": "completed",
"message": "Workflow completed successfully"
}
except Exception as e:
logger.error(f"Error executing workflow {workflow.id}: {e}")
execution.status = "failed"
execution.completed_at = datetime.now(timezone.utc)
execution.error = str(e)
await self._update_execution(execution)
yield {
"type": "workflow_status",
"execution_id": execution.id,
"status": "failed",
"error": str(e)
}
def _transform_agent_response_to_workflow_update(
self,
agent_response: Dict[str, Any],
execution_id: str
) -> Dict[str, Any]:
"""Transform agent response into workflow execution update."""
workflow_response = {
**agent_response,
"execution_id": execution_id,
"source": "workflow_executor"
}
if agent_response.get('type') == 'assistant':
workflow_response['type'] = 'workflow_step'
workflow_response['step_name'] = 'workflow_execution'
elif agent_response.get('type') == 'tool_call':
workflow_response['type'] = 'workflow_tool_call'
elif agent_response.get('type') == 'tool_result':
workflow_response['type'] = 'workflow_tool_result'
return workflow_response
async def _store_execution(self, execution: WorkflowExecution):
"""Store workflow execution in database."""
try:
client = await self.db.client
logger.info(f"Execution {execution.id} handled by API endpoint")
except Exception as e:
logger.error(f"Failed to store workflow execution: {e}")
async def _update_execution(self, execution: WorkflowExecution):
"""Update workflow execution in database."""
try:
client = await self.db.client
update_data = {
"status": execution.status,
"completed_at": execution.completed_at.isoformat() if execution.completed_at else None,
"error": execution.error
}
await client.table('workflow_executions').update(update_data).eq('id', execution.id).execute()
logger.info(f"Updated workflow execution {execution.id} status to {execution.status}")
except Exception as e:
logger.error(f"Failed to update workflow execution: {e}")
async def get_execution_status(self, execution_id: str) -> Optional[WorkflowExecution]:
"""Get the status of a workflow execution."""
try:
client = await self.db.client
result = await client.table('workflow_executions').select('*').eq('id', execution_id).execute()
if result.data:
data = result.data[0]
return WorkflowExecution(
id=data['id'],
workflow_id=data['workflow_id'],
status=data['status'],
started_at=datetime.fromisoformat(data['started_at']) if data['started_at'] else None,
completed_at=datetime.fromisoformat(data['completed_at']) if data['completed_at'] else None,
trigger_type=data.get('triggered_by', 'MANUAL'),
trigger_data={},
variables=data.get('execution_context', {}),
error=data.get('error')
)
return None
except Exception as e:
logger.error(f"Failed to get execution status: {e}")
return None
async def _create_workflow_thread(
self,
thread_id: str,
project_id: str,
workflow: WorkflowDefinition,
variables: Optional[Dict[str, Any]] = None
):
"""Create a thread in the database for workflow execution."""
try:
client = await self.db.client
project_result = await client.table('projects').select('account_id').eq('project_id', project_id).execute()
if not project_result.data:
raise ValueError(f"Project {project_id} not found")
account_id = project_result.data[0]['account_id']
thread_data = {
"thread_id": thread_id,
"project_id": project_id,
"account_id": account_id,
"created_at": datetime.now(timezone.utc).isoformat()
}
await client.table('threads').insert(thread_data).execute()
initial_message = f"Execute the workflow: {workflow.name}"
if workflow.description:
initial_message += f"\n\nDescription: {workflow.description}"
if variables:
initial_message += f"\n\nVariables: {json.dumps(variables, indent=2)}"
message_data = {
"message_id": str(uuid.uuid4()),
"thread_id": thread_id,
"type": "user",
"is_llm_message": True,
"content": json.dumps({"role": "user", "content": initial_message}),
"created_at": datetime.now(timezone.utc).isoformat()
}
await client.table('messages').insert(message_data).execute()
logger.info(f"Created workflow thread {thread_id} for workflow {workflow.id}")
except Exception as e:
logger.error(f"Failed to create workflow thread: {e}")
raise