suna/backend/triggers/services/execution_service.py

447 lines
17 KiB
Python
Raw Normal View History

2025-07-14 19:49:18 +08:00
import uuid
from typing import Dict, Any
from datetime import datetime, timezone
from ..domain.entities import TriggerResult, TriggerEvent
from services.supabase import DBConnection
from services import redis
from utils.logger import logger, structlog
from run_agent_background import run_agent_background
class TriggerExecutionService:
def __init__(self, db_connection: DBConnection):
self._db = db_connection
self._agent_executor = AgentExecutor(db_connection)
self._workflow_executor = WorkflowExecutor(db_connection)
async def execute_trigger_result(
self,
agent_id: str,
trigger_result: TriggerResult,
trigger_event: TriggerEvent
) -> Dict[str, Any]:
try:
2025-07-14 21:10:24 +08:00
logger.info(f"Trigger execution: should_execute_agent={trigger_result.should_execute_agent}, should_execute_workflow={trigger_result.should_execute_workflow}")
logger.info(f"Trigger result type: {type(trigger_result)}")
logger.info(f"Workflow ID: {trigger_result.workflow_id}")
# FORCE AGENT EXECUTION ONLY - disable workflows for webhook triggers
if trigger_event.trigger_type.value == "webhook":
logger.info(f"Webhook trigger detected - forcing agent execution for agent: {agent_id}")
# Override any workflow settings for webhook triggers
trigger_result.should_execute_workflow = False
trigger_result.should_execute_agent = True
return await self._agent_executor.execute_triggered_agent(
agent_id=agent_id,
trigger_result=trigger_result,
trigger_event=trigger_event
)
2025-07-14 19:49:18 +08:00
if trigger_result.should_execute_workflow:
2025-07-14 21:10:24 +08:00
logger.info(f"Executing workflow: {trigger_result.workflow_id}")
2025-07-14 19:49:18 +08:00
workflow_id = trigger_result.workflow_id
workflow_input = trigger_result.workflow_input or {}
return await self._workflow_executor.execute_triggered_workflow(
agent_id=agent_id,
workflow_id=workflow_id,
workflow_input=workflow_input,
trigger_result=trigger_result,
trigger_event=trigger_event
)
else:
2025-07-14 21:10:24 +08:00
logger.info(f"Executing agent: {agent_id}")
2025-07-14 19:49:18 +08:00
return await self._agent_executor.execute_triggered_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 AgentExecutor:
def __init__(self, db_connection: DBConnection):
self._db = db_connection
async def execute_triggered_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._create_trigger_thread(
agent_id=agent_id,
agent_config=agent_config,
trigger_event=trigger_event,
trigger_result=trigger_result
)
await self._create_initial_message(
thread_id=thread_id,
prompt=trigger_result.agent_prompt,
2025-07-14 21:10:24 +08:00
trigger_data=trigger_result.execution_variables.variables,
agent_id=agent_id,
agent_config=agent_config
2025-07-14 19:49:18 +08:00
)
agent_run_id = await self._start_agent_execution(
thread_id=thread_id,
project_id=project_id,
agent_config=agent_config,
trigger_variables=trigger_result.execution_variables.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 triggered 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]:
client = await self._db.client
result = await client.table('agents').select('*').eq('agent_id', agent_id).execute()
return result.data[0] if result.data else None
async def _create_trigger_thread(
self,
agent_id: str,
agent_config: Dict[str, Any],
trigger_event: TriggerEvent,
trigger_result: TriggerResult
) -> tuple[str, str]:
client = await self._db.client
2025-07-14 21:10:24 +08:00
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]}"
project = await client.table('projects').insert({
"project_id": project_id,
"account_id": account_id,
"name": placeholder_name,
"created_at": datetime.now(timezone.utc).isoformat()
}).execute()
logger.info(f"Created new project for trigger: {project_id}")
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
logger.info(f"Created new sandbox {sandbox_id} for trigger project {project_id}")
vnc_link = await sandbox.get_preview_link(6080)
website_link = await sandbox.get_preview_link(8080)
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link).split("url='")[1].split("'")[0]
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
token = None
if hasattr(vnc_link, 'token'):
token = vnc_link.token
elif "token='" in str(vnc_link):
token = str(vnc_link).split("token='")[1].split("'")[0]
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:
logger.error(f"Failed to update trigger project {project_id} with sandbox {sandbox_id}")
try:
await delete_sandbox(sandbox_id)
except Exception as e:
logger.error(f"Error deleting sandbox: {str(e)}")
raise Exception("Database update failed")
except Exception as e:
logger.error(f"Error creating sandbox for trigger: {str(e)}")
await client.table('projects').delete().eq('project_id', project_id).execute()
raise Exception(f"Failed to create sandbox: {str(e)}")
2025-07-14 19:49:18 +08:00
thread_data = {
2025-07-14 21:10:24 +08:00
"thread_id": thread_id,
"project_id": project_id,
"account_id": account_id,
"created_at": datetime.now(timezone.utc).isoformat()
2025-07-14 19:49:18 +08:00
}
thread = await client.table('threads').insert(thread_data).execute()
2025-07-14 21:10:24 +08:00
logger.info(f"Created new thread for trigger: {thread_id}")
2025-07-14 19:49:18 +08:00
return thread_id, project_id
async def _create_initial_message(
self,
thread_id: str,
prompt: str,
2025-07-14 21:10:24 +08:00
trigger_data: Dict[str, Any],
agent_id: str,
agent_config: Dict[str, Any]
2025-07-14 19:49:18 +08:00
) -> str:
client = await self._db.client
2025-07-14 21:10:24 +08:00
import json
message_payload = {"role": "user", "content": prompt}
2025-07-14 19:49:18 +08:00
message_data = {
2025-07-14 21:10:24 +08:00
"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()
2025-07-14 19:49:18 +08:00
}
message = await client.table('messages').insert(message_data).execute()
return message.data[0]['message_id']
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
2025-07-14 21:10:24 +08:00
logger.info(f"Using project {project_id} with pre-created sandbox for trigger execution")
2025-07-14 19:49:18 +08:00
model_name = "anthropic/claude-sonnet-4-20250514"
2025-07-14 21:10:24 +08:00
agent_run = await client.table('agent_runs').insert({
2025-07-14 19:49:18 +08:00
"thread_id": thread_id,
"status": "running",
"started_at": datetime.now(timezone.utc).isoformat(),
2025-07-14 21:10:24 +08:00
"agent_id": agent_config.get('agent_id') if agent_config else None,
"agent_version_id": agent_config.get('current_version_id') if agent_config else None,
2025-07-14 19:49:18 +08:00
"metadata": {
"model_name": model_name,
"enable_thinking": False,
"reasoning_effort": "low",
"enable_context_manager": True,
"trigger_execution": True,
"trigger_variables": trigger_variables
}
2025-07-14 21:10:24 +08:00
}).execute()
2025-07-14 19:49:18 +08:00
agent_run_id = agent_run.data[0]['id']
instance_id = "trigger_executor"
instance_key = f"active_run:{instance_id}:{agent_run_id}"
try:
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 ({instance_key}): {str(e)}")
request_id = structlog.contextvars.get_contextvars().get('request_id')
run_agent_background.send(
agent_run_id=agent_run_id,
thread_id=thread_id,
instance_id=instance_id,
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=request_id,
)
logger.info(f"Started background agent execution for trigger (run_id: {agent_run_id})")
return agent_run_id
class WorkflowExecutor:
def __init__(self, db_connection: DBConnection):
self._db = db_connection
async def execute_triggered_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 = await self._get_workflow_config(workflow_id)
if not workflow_config:
raise ValueError(f"Workflow {workflow_id} not found")
if workflow_config['status'] != 'active':
raise ValueError(f"Workflow {workflow_id} is not active")
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._create_workflow_thread(
agent_id=agent_id,
workflow_id=workflow_id,
agent_config=agent_config,
workflow_config=workflow_config,
trigger_event=trigger_event
)
execution_id = await self._create_workflow_execution(
workflow_id=workflow_id,
agent_id=agent_id,
thread_id=thread_id,
workflow_input=workflow_input,
trigger_event=trigger_event
)
await self._create_workflow_message(
thread_id=thread_id,
workflow_config=workflow_config,
workflow_input=workflow_input,
2025-07-14 21:10:24 +08:00
trigger_data=trigger_result.execution_variables.variables,
agent_id=agent_id,
agent_config=agent_config
2025-07-14 19:49:18 +08:00
)
return {
"success": True,
"thread_id": thread_id,
"execution_id": execution_id,
"message": "Workflow execution started successfully"
}
except Exception as e:
logger.error(f"Failed to execute triggered workflow {workflow_id}: {e}")
return {
"success": False,
"error": str(e),
"message": "Failed to start workflow execution"
}
async def _get_workflow_config(self, workflow_id: str) -> Dict[str, Any]:
client = await self._db.client
2025-07-14 21:10:24 +08:00
# Use agent_workflows table like in workflows.py
result = await client.table('agent_workflows').select('*').eq('id', workflow_id).execute()
2025-07-14 19:49:18 +08:00
return result.data[0] if result.data else None
async def _get_agent_config(self, agent_id: str) -> Dict[str, Any]:
client = await self._db.client
result = await client.table('agents').select('*').eq('agent_id', agent_id).execute()
return result.data[0] if result.data else None
async def _create_workflow_thread(
self,
agent_id: str,
workflow_id: str,
agent_config: Dict[str, Any],
workflow_config: Dict[str, Any],
trigger_event: TriggerEvent
) -> tuple[str, str]:
client = await self._db.client
thread_data = {
'thread_id': str(uuid.uuid4()),
2025-07-14 21:10:24 +08:00
'account_id': agent_config.get('account_id'),
2025-07-14 19:49:18 +08:00
'project_id': agent_config.get('project_id'),
2025-07-14 21:10:24 +08:00
'is_public': False,
'created_at': datetime.now(timezone.utc).isoformat()
2025-07-14 19:49:18 +08:00
}
thread = await client.table('threads').insert(thread_data).execute()
thread_id = thread.data[0]['thread_id']
project_id = thread.data[0]['project_id']
return thread_id, project_id
async def _create_workflow_execution(
self,
workflow_id: str,
agent_id: str,
thread_id: str,
workflow_input: Dict[str, Any],
trigger_event: TriggerEvent
) -> str:
client = await self._db.client
execution_data = {
'execution_id': str(uuid.uuid4()),
'workflow_id': workflow_id,
'agent_id': agent_id,
'thread_id': thread_id,
'status': 'running',
'input_data': workflow_input,
'started_at': datetime.now(timezone.utc).isoformat(),
'metadata': {
'trigger_id': trigger_event.trigger_id,
'trigger_type': trigger_event.trigger_type.value,
'created_by_trigger': True
}
}
execution = await client.table('workflow_executions').insert(execution_data).execute()
return execution.data[0]['execution_id']
async def _create_workflow_message(
self,
thread_id: str,
workflow_config: Dict[str, Any],
workflow_input: Dict[str, Any],
2025-07-14 21:10:24 +08:00
trigger_data: Dict[str, Any],
agent_id: str,
agent_config: Dict[str, Any]
2025-07-14 19:49:18 +08:00
) -> str:
client = await self._db.client
prompt = workflow_config.get('initial_prompt', 'Execute workflow with provided input')
message_data = {
'message_id': str(uuid.uuid4()),
'thread_id': thread_id,
2025-07-14 21:10:24 +08:00
'type': 'human',
'is_llm_message': False,
'content': {
'role': 'user',
'message': prompt
},
'agent_id': agent_id,
2025-07-14 19:49:18 +08:00
'created_at': datetime.now(timezone.utc).isoformat(),
'metadata': {
'workflow_input': workflow_input,
'trigger_data': trigger_data,
'created_by_trigger': True,
'is_workflow_message': True
}
}
2025-07-14 21:10:24 +08:00
if agent_config.get('current_version_id'):
message_data['agent_version_id'] = agent_config['current_version_id']
2025-07-14 19:49:18 +08:00
message = await client.table('messages').insert(message_data).execute()
return message.data[0]['message_id']