suna/backend/triggers/execution_service.py

574 lines
22 KiB
Python

import json
import uuid
from datetime import datetime, timezone
from typing import Dict, Any, Tuple
from services.supabase import DBConnection
from services import redis
from utils.logger import logger, structlog
from utils.config import config
from run_agent_background import run_agent_background
from .trigger_service import TriggerEvent, TriggerResult
from .utils import format_workflow_for_llm
class ExecutionService:
def __init__(self, db_connection: DBConnection):
self._db = db_connection
self._session_manager = SessionManager(db_connection)
self._agent_executor = AgentExecutor(db_connection, self._session_manager)
self._workflow_executor = WorkflowExecutor(db_connection, self._session_manager)
async def execute_trigger_result(
self,
agent_id: str,
trigger_result: TriggerResult,
trigger_event: TriggerEvent
) -> Dict[str, Any]:
try:
logger.info(f"Executing trigger for agent {agent_id}: workflow={trigger_result.should_execute_workflow}, agent={trigger_result.should_execute_agent}")
if trigger_result.should_execute_workflow:
return await self._workflow_executor.execute_workflow(
agent_id=agent_id,
workflow_id=trigger_result.workflow_id,
workflow_input=trigger_result.workflow_input or {},
trigger_result=trigger_result,
trigger_event=trigger_event
)
else:
return await self._agent_executor.execute_agent(
agent_id=agent_id,
trigger_result=trigger_result,
trigger_event=trigger_event
)
except Exception as e:
logger.error(f"Failed to execute trigger result: {e}")
return {
"success": False,
"error": str(e),
"message": "Failed to execute trigger"
}
class SessionManager:
def __init__(self, db_connection: DBConnection):
self._db = db_connection
async def create_agent_session(
self,
agent_id: str,
agent_config: Dict[str, Any],
trigger_event: TriggerEvent
) -> Tuple[str, str]:
client = await self._db.client
project_id = str(uuid.uuid4())
thread_id = str(uuid.uuid4())
account_id = agent_config.get('account_id')
placeholder_name = f"Trigger: {agent_config.get('name', 'Agent')} - {trigger_event.trigger_id[:8]}"
await client.table('projects').insert({
"project_id": project_id,
"account_id": account_id,
"name": placeholder_name,
"created_at": datetime.now(timezone.utc).isoformat()
}).execute()
await self._create_sandbox_for_project(project_id)
await client.table('threads').insert({
"thread_id": thread_id,
"project_id": project_id,
"account_id": account_id,
"created_at": datetime.now(timezone.utc).isoformat()
}).execute()
logger.info(f"Created agent session: project={project_id}, thread={thread_id}")
return thread_id, project_id
async def create_workflow_session(
self,
account_id: str,
workflow_id: str,
workflow_name: str
) -> Tuple[str, str]:
client = await self._db.client
project_id = str(uuid.uuid4())
thread_id = str(uuid.uuid4())
await client.table('projects').insert({
"project_id": project_id,
"account_id": account_id,
"name": f"Workflow: {workflow_name}",
"created_at": datetime.now(timezone.utc).isoformat()
}).execute()
await self._create_sandbox_for_project(project_id)
await client.table('threads').insert({
"thread_id": thread_id,
"project_id": project_id,
"account_id": account_id,
"created_at": datetime.now(timezone.utc).isoformat(),
"metadata": {
"workflow_execution": True,
"workflow_id": workflow_id,
"workflow_name": workflow_name
}
}).execute()
logger.info(f"Created workflow session: project={project_id}, thread={thread_id}")
return thread_id, project_id
async def _create_sandbox_for_project(self, project_id: str) -> None:
client = await self._db.client
try:
from sandbox.sandbox import create_sandbox, delete_sandbox
sandbox_pass = str(uuid.uuid4())
sandbox = await create_sandbox(sandbox_pass, project_id)
sandbox_id = sandbox.id
vnc_link = await sandbox.get_preview_link(6080)
website_link = await sandbox.get_preview_link(8080)
vnc_url = self._extract_url(vnc_link)
website_url = self._extract_url(website_link)
token = self._extract_token(vnc_link)
update_result = await client.table('projects').update({
'sandbox': {
'id': sandbox_id,
'pass': sandbox_pass,
'vnc_preview': vnc_url,
'sandbox_url': website_url,
'token': token
}
}).eq('project_id', project_id).execute()
if not update_result.data:
await delete_sandbox(sandbox_id)
raise Exception("Database update failed")
except Exception as e:
await client.table('projects').delete().eq('project_id', project_id).execute()
raise Exception(f"Failed to create sandbox: {str(e)}")
def _extract_url(self, link) -> str:
if hasattr(link, 'url'):
return link.url
return str(link).split("url='")[1].split("'")[0]
def _extract_token(self, link) -> str:
if hasattr(link, 'token'):
return link.token
if "token='" in str(link):
return str(link).split("token='")[1].split("'")[0]
return None
class AgentExecutor:
def __init__(self, db_connection: DBConnection, session_manager: SessionManager):
self._db = db_connection
self._session_manager = session_manager
async def execute_agent(
self,
agent_id: str,
trigger_result: TriggerResult,
trigger_event: TriggerEvent
) -> Dict[str, Any]:
try:
agent_config = await self._get_agent_config(agent_id)
if not agent_config:
raise ValueError(f"Agent {agent_id} not found")
thread_id, project_id = await self._session_manager.create_agent_session(
agent_id, agent_config, trigger_event
)
await self._create_initial_message(
thread_id, trigger_result.agent_prompt, trigger_result.execution_variables
)
agent_run_id = await self._start_agent_execution(
thread_id, project_id, agent_config, trigger_result.execution_variables
)
return {
"success": True,
"thread_id": thread_id,
"agent_run_id": agent_run_id,
"message": "Agent execution started successfully"
}
except Exception as e:
logger.error(f"Failed to execute agent {agent_id}: {e}")
return {
"success": False,
"error": str(e),
"message": "Failed to start agent execution"
}
async def _get_agent_config(self, agent_id: str) -> Dict[str, Any]:
try:
client = await self._db.client
agent_result = await client.table('agents').select('account_id, name').eq('agent_id', agent_id).execute()
if not agent_result.data:
return None
agent_data = agent_result.data[0]
from agent.versioning.version_service import get_version_service
version_service = await get_version_service()
active_version = await version_service.get_active_version(agent_id, "system")
if not active_version:
return {
'agent_id': agent_id,
'account_id': agent_data.get('account_id'),
'name': agent_data.get('name', 'Unknown Agent'),
'system_prompt': 'You are a helpful AI assistant.',
'configured_mcps': [],
'custom_mcps': [],
'agentpress_tools': {},
}
return {
'agent_id': agent_id,
'account_id': agent_data.get('account_id'),
'name': agent_data.get('name', 'Unknown Agent'),
'system_prompt': active_version.system_prompt,
'configured_mcps': active_version.configured_mcps,
'custom_mcps': active_version.custom_mcps,
'agentpress_tools': active_version.agentpress_tools if isinstance(active_version.agentpress_tools, dict) else {},
'current_version_id': active_version.version_id,
'version_name': active_version.version_name
}
except Exception as e:
logger.warning(f"Failed to get agent config using versioning system: {e}")
return None
async def _create_initial_message(
self,
thread_id: str,
prompt: str,
trigger_data: Dict[str, Any]
) -> None:
client = await self._db.client
message_payload = {"role": "user", "content": prompt}
await client.table('messages').insert({
"message_id": str(uuid.uuid4()),
"thread_id": thread_id,
"type": "user",
"is_llm_message": True,
"content": json.dumps(message_payload),
"created_at": datetime.now(timezone.utc).isoformat()
}).execute()
async def _start_agent_execution(
self,
thread_id: str,
project_id: str,
agent_config: Dict[str, Any],
trigger_variables: Dict[str, Any]
) -> str:
client = await self._db.client
model_name = "anthropic/claude-sonnet-4-20250514"
agent_run = await client.table('agent_runs').insert({
"thread_id": thread_id,
"status": "running",
"started_at": datetime.now(timezone.utc).isoformat(),
"agent_id": agent_config.get('agent_id'),
"agent_version_id": agent_config.get('current_version_id'),
"metadata": {
"model_name": model_name,
"enable_thinking": False,
"reasoning_effort": "low",
"enable_context_manager": True,
"trigger_execution": True,
"trigger_variables": trigger_variables
}
}).execute()
agent_run_id = agent_run.data[0]['id']
await self._register_agent_run(agent_run_id)
run_agent_background.send(
agent_run_id=agent_run_id,
thread_id=thread_id,
instance_id="trigger_executor",
project_id=project_id,
model_name=model_name,
enable_thinking=False,
reasoning_effort="low",
stream=False,
enable_context_manager=True,
agent_config=agent_config,
is_agent_builder=False,
target_agent_id=None,
request_id=structlog.contextvars.get_contextvars().get('request_id'),
)
logger.info(f"Started agent execution: {agent_run_id}")
return agent_run_id
async def _register_agent_run(self, agent_run_id: str) -> None:
try:
instance_key = f"active_run:trigger_executor:{agent_run_id}"
await redis.set(instance_key, "running", ex=redis.REDIS_KEY_TTL)
except Exception as e:
logger.warning(f"Failed to register agent run in Redis: {e}")
class WorkflowExecutor:
def __init__(self, db_connection: DBConnection, session_manager: SessionManager):
self._db = db_connection
self._session_manager = session_manager
async def execute_workflow(
self,
agent_id: str,
workflow_id: str,
workflow_input: Dict[str, Any],
trigger_result: TriggerResult,
trigger_event: TriggerEvent
) -> Dict[str, Any]:
try:
workflow_config, steps_json = await self._get_workflow_data(workflow_id, agent_id)
agent_config, account_id = await self._get_agent_data(agent_id)
enhanced_agent_config = await self._enhance_agent_config_for_workflow(
agent_config, workflow_config, steps_json, workflow_input
)
thread_id, project_id = await self._session_manager.create_workflow_session(
account_id, workflow_id, workflow_config['name']
)
await self._validate_workflow_execution(account_id)
await self._create_workflow_message(thread_id, workflow_config, workflow_input)
agent_run_id = await self._start_workflow_agent_execution(
thread_id, project_id, enhanced_agent_config
)
return {
"success": True,
"thread_id": thread_id,
"agent_run_id": agent_run_id,
"message": "Workflow execution started successfully"
}
except Exception as e:
logger.error(f"Failed to execute workflow {workflow_id}: {e}")
return {
"success": False,
"error": str(e),
"message": "Failed to start workflow execution"
}
async def _get_workflow_data(self, workflow_id: str, agent_id: str) -> Tuple[Dict[str, Any], list]:
client = await self._db.client
workflow_result = await client.table('agent_workflows').select('*').eq('id', workflow_id).eq('agent_id', agent_id).execute()
if not workflow_result.data:
raise ValueError(f"Workflow {workflow_id} not found for agent {agent_id}")
workflow_config = workflow_result.data[0]
if workflow_config['status'] != 'active':
raise ValueError(f"Workflow {workflow_id} is not active")
steps_json = workflow_config.get('steps', [])
return workflow_config, steps_json
async def _get_agent_data(self, agent_id: str) -> Tuple[Dict[str, Any], str]:
try:
client = await self._db.client
agent_result = await client.table('agents').select('account_id, name').eq('agent_id', agent_id).execute()
if not agent_result.data:
raise ValueError(f"Agent {agent_id} not found")
agent_data = agent_result.data[0]
account_id = agent_data['account_id']
from agent.versioning.version_service import get_version_service
version_service = await get_version_service()
active_version = await version_service.get_active_version(agent_id, "system")
if not active_version:
raise ValueError(f"No active version found for agent {agent_id}")
agent_config = {
'agent_id': agent_id,
'name': agent_data.get('name', 'Unknown Agent'),
'system_prompt': active_version.system_prompt,
'configured_mcps': active_version.configured_mcps,
'custom_mcps': active_version.custom_mcps,
'agentpress_tools': active_version.agentpress_tools if isinstance(active_version.agentpress_tools, dict) else {},
'current_version_id': active_version.version_id,
'version_name': active_version.version_name
}
return agent_config, account_id
except Exception as e:
raise ValueError(f"Failed to get agent configuration: {str(e)}")
async def _enhance_agent_config_for_workflow(
self,
agent_config: Dict[str, Any],
workflow_config: Dict[str, Any],
steps_json: list,
workflow_input: Dict[str, Any]
) -> Dict[str, Any]:
available_tools = self._get_available_tools(agent_config)
workflow_prompt = format_workflow_for_llm(
workflow_config=workflow_config,
steps=steps_json,
input_data=workflow_input,
available_tools=available_tools
)
enhanced_config = agent_config.copy()
enhanced_config['system_prompt'] = f"""{agent_config['system_prompt']}
--- WORKFLOW EXECUTION MODE ---
{workflow_prompt}"""
return enhanced_config
def _get_available_tools(self, agent_config: Dict[str, Any]) -> list:
available_tools = []
agentpress_tools = agent_config.get('agentpress_tools', {})
tool_mapping = {
'sb_shell_tool': ['execute_command'],
'sb_files_tool': ['create_file', 'str_replace', 'full_file_rewrite', 'delete_file'],
'sb_browser_tool': ['browser_navigate_to', 'browser_take_screenshot'],
'sb_vision_tool': ['see_image'],
'sb_deploy_tool': ['deploy'],
'sb_expose_tool': ['expose_port'],
'web_search_tool': ['web_search'],
'data_providers_tool': ['get_data_provider_endpoints', 'execute_data_provider_call']
}
for tool_key, tool_names in tool_mapping.items():
tool_config = agentpress_tools.get(tool_key, False)
if isinstance(tool_config, bool):
if tool_config:
available_tools.extend(tool_names)
elif isinstance(tool_config, dict):
if tool_config.get('enabled', False):
available_tools.extend(tool_names)
all_mcps = []
if agent_config.get('configured_mcps'):
all_mcps.extend(agent_config['configured_mcps'])
if agent_config.get('custom_mcps'):
all_mcps.extend(agent_config['custom_mcps'])
for mcp in all_mcps:
enabled_tools_list = mcp.get('enabledTools', [])
available_tools.extend(enabled_tools_list)
return available_tools
async def _validate_workflow_execution(self, account_id: str) -> None:
from services.billing import check_billing_status, can_use_model
client = await self._db.client
model_name = config.MODEL_TO_USE or "anthropic/claude-sonnet-4-20250514"
can_use, model_message, _ = await can_use_model(client, account_id, model_name)
if not can_use:
raise Exception(f"Model access denied: {model_message}")
can_run, billing_message, _ = await check_billing_status(client, account_id)
if not can_run:
raise Exception(f"Billing check failed: {billing_message}")
async def _create_workflow_message(
self,
thread_id: str,
workflow_config: Dict[str, Any],
workflow_input: Dict[str, Any]
) -> None:
client = await self._db.client
message_content = f"Execute workflow: {workflow_config['name']}\n\nInput: {json.dumps(workflow_input) if workflow_input else 'None'}"
await client.table('messages').insert({
"message_id": str(uuid.uuid4()),
"thread_id": thread_id,
"type": "user",
"is_llm_message": True,
"content": json.dumps({"role": "user", "content": message_content}),
"created_at": datetime.now(timezone.utc).isoformat()
}).execute()
async def _start_workflow_agent_execution(
self,
thread_id: str,
project_id: str,
agent_config: Dict[str, Any]
) -> str:
client = await self._db.client
model_name = config.MODEL_TO_USE or "anthropic/claude-sonnet-4-20250514"
agent_run = await client.table('agent_runs').insert({
"thread_id": thread_id,
"status": "running",
"started_at": datetime.now(timezone.utc).isoformat(),
"agent_id": agent_config.get('agent_id'),
"agent_version_id": agent_config.get('current_version_id')
}).execute()
agent_run_id = agent_run.data[0]['id']
await self._register_workflow_run(agent_run_id)
run_agent_background.send(
agent_run_id=agent_run_id,
thread_id=thread_id,
instance_id=getattr(config, 'INSTANCE_ID', 'default'),
project_id=project_id,
model_name=model_name,
enable_thinking=False,
reasoning_effort='medium',
stream=False,
enable_context_manager=True,
agent_config=agent_config,
is_agent_builder=False,
target_agent_id=None,
request_id=None,
)
logger.info(f"Started workflow agent execution: {agent_run_id}")
return agent_run_id
async def _register_workflow_run(self, agent_run_id: str) -> None:
try:
instance_id = getattr(config, 'INSTANCE_ID', 'default')
instance_key = f"active_run:{instance_id}:{agent_run_id}"
await redis.set(instance_key, "running", ex=redis.REDIS_KEY_TTL)
except Exception as e:
logger.warning(f"Failed to register workflow run in Redis: {e}")
def get_execution_service(db_connection: DBConnection) -> ExecutionService:
return ExecutionService(db_connection)