mirror of https://github.com/kortix-ai/suna.git
feat: enable app triggers
This commit is contained in:
parent
d79adf8b95
commit
ec8989bb21
|
@ -70,18 +70,17 @@ Build structured, repeatable processes:
|
|||
- **`activate_workflow`**: Enable/disable workflow execution
|
||||
|
||||
### ⏰ Trigger Management
|
||||
Schedule automatic execution{f''' and event-based triggers''' if config.ENV_MODE != EnvMode.PRODUCTION else ""}:
|
||||
Schedule automatic execution and event-based triggers:
|
||||
- **`create_scheduled_trigger`**: Set up cron-based scheduling
|
||||
- **`get_scheduled_triggers`**: View all scheduled tasks
|
||||
- **`delete_scheduled_trigger`**: Remove scheduled tasks
|
||||
- **`toggle_scheduled_trigger`**: Enable/disable scheduled execution
|
||||
{f'''
|
||||
Event-based triggers (Composio):
|
||||
|
||||
Event/APP-based triggers (Composio):
|
||||
- **`list_event_trigger_apps`**: Discover apps with available event triggers
|
||||
- **`list_app_event_triggers`**: List triggers for a specific app (includes config schema)
|
||||
- **`list_event_profiles`**: List connected profiles to get `profile_id` and `connected_account_id`
|
||||
- **`get_credential_profiles`**: List connected profiles to get `profile_id` and `connected_account_id`
|
||||
- **`create_event_trigger`**: Create an event trigger by passing `slug`, `profile_id`, `connected_account_id`, `trigger_config`, and route (`agent` or `workflow`). If route is `agent`, pass `agent_prompt`; if `workflow`, pass `workflow_id` (and optional `workflow_input`).
|
||||
''' if config.ENV_MODE != EnvMode.PRODUCTION else ""}
|
||||
|
||||
### 📊 Agent Management
|
||||
- **`get_current_agent_config`**: Review current setup and capabilities
|
||||
|
|
|
@ -48,6 +48,9 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
formatted_profiles = []
|
||||
for profile in profiles:
|
||||
formatted_profiles.append({
|
||||
"profile_id": profile.profile_id,
|
||||
"connected_account_id": getattr(profile, 'connected_account_id', None),
|
||||
"account_id": profile.account_id,
|
||||
"profile_name": profile.profile_name,
|
||||
"display_name": profile.display_name,
|
||||
"toolkit_slug": profile.toolkit_slug,
|
||||
|
|
|
@ -408,10 +408,9 @@ class TriggerTool(AgentBuilderBaseTool):
|
|||
logger.error(f"Error toggling scheduled trigger: {str(e)}")
|
||||
return self.fail_response("Error toggling scheduled trigger")
|
||||
|
||||
# ===== EVENT-BASED TRIGGERS (Non-Production Only) =====
|
||||
# ===== EVENT-BASED TRIGGERS =====
|
||||
|
||||
# Event trigger methods - only available in non-production environments
|
||||
if config.ENV_MODE != EnvMode.PRODUCTION:
|
||||
# Event trigger methods - available in all environments
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
@ -443,8 +442,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
except Exception as e:
|
||||
logger.error(f"Error listing event trigger apps: {e}")
|
||||
return self.fail_response("Error listing apps")
|
||||
|
||||
TriggerTool.list_event_trigger_apps = list_event_trigger_apps
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
|
@ -485,62 +482,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
except Exception as e:
|
||||
logger.error(f"Error listing triggers for app {toolkit_slug}: {e}")
|
||||
return self.fail_response("Error listing triggers")
|
||||
|
||||
TriggerTool.list_app_event_triggers = list_app_event_triggers
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_event_profiles",
|
||||
"description": "List connected Composio profiles for a toolkit. Use this to get profile_id and connected_account_id before creating a trigger.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"toolkit_slug": {
|
||||
"type": "string",
|
||||
"description": "Toolkit slug, e.g. 'gmail'"
|
||||
}
|
||||
},
|
||||
"required": ["toolkit_slug"]
|
||||
}
|
||||
}
|
||||
})
|
||||
@usage_example('''
|
||||
<function_calls>
|
||||
<invoke name="list_event_profiles">
|
||||
<parameter name="toolkit_slug">gmail</parameter>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
''')
|
||||
async def list_event_profiles(self, toolkit_slug: str) -> ToolResult:
|
||||
try:
|
||||
client = await self.db.client
|
||||
agent_rows = await client.table('agents').select('account_id').eq('agent_id', self.agent_id).execute()
|
||||
if not agent_rows.data:
|
||||
return self.fail_response("Agent not found")
|
||||
account_id = agent_rows.data[0]['account_id']
|
||||
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profiles = await profile_service.get_profiles(account_id, toolkit_slug)
|
||||
|
||||
items = []
|
||||
for p in profiles:
|
||||
items.append({
|
||||
"profile_name": p.profile_name,
|
||||
"display_name": p.display_name,
|
||||
"is_connected": p.is_connected
|
||||
})
|
||||
|
||||
return self.success_response({
|
||||
"message": f"Found {len(items)} profile(s) for {toolkit_slug}",
|
||||
"items": items,
|
||||
"total": len(items)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing event profiles: {e}")
|
||||
return self.fail_response("Error listing profiles")
|
||||
|
||||
TriggerTool.list_event_profiles = list_event_profiles
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
|
@ -568,7 +509,9 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
<function_calls>
|
||||
<invoke name="list_event_trigger_apps"></invoke>
|
||||
<invoke name="list_app_event_triggers"><parameter name="toolkit_slug">gmail</parameter></invoke>
|
||||
<invoke name="list_event_profiles"><parameter name="toolkit_slug">gmail</parameter></invoke>
|
||||
<invoke name="get_credential_profiles">
|
||||
<parameter name="toolkit_slug">[toolkit_slug]</parameter>
|
||||
</invoke>
|
||||
<invoke name="create_event_trigger">
|
||||
<parameter name="slug">GMAIL_NEW_GMAIL_MESSAGE</parameter>
|
||||
<parameter name="profile_id">profile_123</parameter>
|
||||
|
@ -598,15 +541,25 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
if route == "agent" and not agent_prompt:
|
||||
return self.fail_response("agent_prompt is required when route is 'agent'")
|
||||
|
||||
# Resolve composio user id and connected account id from profile
|
||||
# Get profile config
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profile_cfg = await profile_service.get_profile_config(profile_id)
|
||||
composio_user_id = profile_cfg.get("user_id")
|
||||
try:
|
||||
profile_config = await profile_service.get_profile_config(profile_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get profile config: {e}")
|
||||
return self.fail_response(f"Failed to get profile config: {str(e)}")
|
||||
|
||||
composio_user_id = profile_config.get("user_id")
|
||||
if not composio_user_id:
|
||||
return self.fail_response("Composio profile is missing user_id")
|
||||
if not connected_account_id:
|
||||
connected_account_id = profile_cfg.get("connected_account_id")
|
||||
|
||||
# Get toolkit_slug and build qualified_name
|
||||
toolkit_slug = profile_config.get("toolkit_slug")
|
||||
if not toolkit_slug and slug:
|
||||
toolkit_slug = slug.split('_')[0].lower() if '_' in slug else 'composio'
|
||||
qualified_name = f'composio.{toolkit_slug}' if toolkit_slug and toolkit_slug != 'composio' else 'composio'
|
||||
|
||||
# API setup
|
||||
api_base = os.getenv("COMPOSIO_API_BASE", "https://backend.composio.dev").rstrip("/")
|
||||
api_key = os.getenv("COMPOSIO_API_KEY")
|
||||
if not api_key:
|
||||
|
@ -646,47 +599,35 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
coerced_config[key] = ",".join(str(x) for x in val)
|
||||
elif not isinstance(val, str):
|
||||
coerced_config[key] = str(val)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to coerce config key {key}: {e}")
|
||||
pass
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch trigger schema: {e}")
|
||||
pass
|
||||
|
||||
# Upsert trigger instance with webhook
|
||||
base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000").rstrip("/")
|
||||
secret = os.getenv("COMPOSIO_WEBHOOK_SECRET", "")
|
||||
webhook_headers: Dict[str, str] = {"X-Composio-Secret": secret} if secret else {}
|
||||
vercel_bypass = os.getenv("VERCEL_PROTECTION_BYPASS_KEY", "")
|
||||
if vercel_bypass:
|
||||
webhook_headers["X-Vercel-Protection-Bypass"] = vercel_bypass
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
# Build request body (simplified like in API)
|
||||
body = {
|
||||
"user_id": composio_user_id,
|
||||
"userId": composio_user_id,
|
||||
"trigger_config": coerced_config,
|
||||
"triggerConfig": coerced_config,
|
||||
"webhook": {
|
||||
"url": f"{base_url}/api/composio/webhook",
|
||||
"headers": webhook_headers,
|
||||
"method": "POST",
|
||||
},
|
||||
}
|
||||
if connected_account_id:
|
||||
body["connectedAccountId"] = connected_account_id
|
||||
body["connected_account_id"] = connected_account_id
|
||||
body["connectedAccountIds"] = [connected_account_id]
|
||||
body["connected_account_ids"] = [connected_account_id]
|
||||
|
||||
# Upsert trigger instance
|
||||
upsert_url = f"{api_base}/api/v3/trigger_instances/{slug}/upsert"
|
||||
async with httpx.AsyncClient(timeout=20) as http_client:
|
||||
resp = await http_client.post(upsert_url, headers=headers, json=body)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPStatusError:
|
||||
except httpx.HTTPStatusError as e:
|
||||
ct = resp.headers.get("content-type", "")
|
||||
detail = resp.json() if "application/json" in ct else resp.text
|
||||
return self.fail_response("Composio upsert error")
|
||||
logger.error(f"Composio upsert error - status: {resp.status_code}, detail: {detail}")
|
||||
return self.fail_response(f"Composio upsert error: {detail}")
|
||||
created = resp.json()
|
||||
|
||||
# Extract trigger ID (same logic as API)
|
||||
def _extract_id(obj: Dict[str, Any]) -> Optional[str]:
|
||||
if not isinstance(obj, dict):
|
||||
return None
|
||||
|
@ -700,6 +641,7 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
)
|
||||
if cand:
|
||||
return cand
|
||||
# Nested shapes
|
||||
for k in ("trigger", "trigger_instance", "triggerInstance", "data", "result"):
|
||||
nested = obj.get(k)
|
||||
if isinstance(nested, dict):
|
||||
|
@ -714,49 +656,47 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
|
||||
composio_trigger_id = _extract_id(created) if isinstance(created, dict) else None
|
||||
|
||||
if not composio_trigger_id:
|
||||
# fallback to list active
|
||||
try:
|
||||
params_lookup: Dict[str, Any] = {"limit": 50, "slug": slug, "userId": composio_user_id}
|
||||
if connected_account_id:
|
||||
params_lookup["connectedAccountId"] = connected_account_id
|
||||
list_url = f"{api_base}/api/v3/trigger_instances/active"
|
||||
async with httpx.AsyncClient(timeout=15) as http_client:
|
||||
lr = await http_client.get(list_url, headers=headers, params=params_lookup)
|
||||
if lr.status_code == 200:
|
||||
ldata = lr.json()
|
||||
items = ldata.get("items") if isinstance(ldata, dict) else (ldata if isinstance(ldata, list) else [])
|
||||
if items:
|
||||
composio_trigger_id = _extract_id(items[0] if isinstance(items[0], dict) else {})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not composio_trigger_id:
|
||||
return self.fail_response("Failed to get Composio trigger id from response")
|
||||
|
||||
# Build Suna trigger and save
|
||||
|
||||
# Build Suna trigger config (same as API)
|
||||
suna_config: Dict[str, Any] = {
|
||||
"provider_id": "composio",
|
||||
"composio_trigger_id": composio_trigger_id,
|
||||
"trigger_slug": slug,
|
||||
"execution_type": route,
|
||||
"qualified_name": qualified_name,
|
||||
"execution_type": route if route in ("agent", "workflow") else "agent",
|
||||
"profile_id": profile_id,
|
||||
}
|
||||
if route == "agent":
|
||||
if suna_config["execution_type"] == "agent":
|
||||
if agent_prompt:
|
||||
suna_config["agent_prompt"] = agent_prompt
|
||||
else:
|
||||
if not workflow_id:
|
||||
return self.fail_response("workflow_id is required for workflow route")
|
||||
suna_config["workflow_id"] = workflow_id
|
||||
if workflow_input:
|
||||
suna_config["workflow_input"] = workflow_input
|
||||
|
||||
|
||||
# Create Suna trigger
|
||||
trigger_svc = get_trigger_service(self.db)
|
||||
trigger = await trigger_svc.create_trigger(
|
||||
agent_id=self.agent_id,
|
||||
provider_id="composio",
|
||||
name=name or slug,
|
||||
config=suna_config,
|
||||
description=f"Composio event: {slug}"
|
||||
)
|
||||
try:
|
||||
trigger = await trigger_svc.create_trigger(
|
||||
agent_id=self.agent_id,
|
||||
provider_id="composio",
|
||||
name=name or slug,
|
||||
config=suna_config,
|
||||
description=f"Composio event: {slug}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create Suna trigger: {e}")
|
||||
return self.fail_response(f"Failed to create Suna trigger: {str(e)}")
|
||||
|
||||
# Sync triggers to version config
|
||||
try:
|
||||
await self._sync_workflows_to_version_config()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to sync triggers to version config: {e}")
|
||||
|
||||
message = f"Event trigger '{trigger.name}' created successfully.\n"
|
||||
message += f"Route: {route}. "
|
||||
|
@ -765,12 +705,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
else:
|
||||
message += "Agent execution configured."
|
||||
|
||||
# Sync triggers to version config
|
||||
try:
|
||||
await self._sync_workflows_to_version_config()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to sync triggers to version config: {e}")
|
||||
|
||||
return self.success_response({
|
||||
"message": message,
|
||||
"trigger": {
|
||||
|
@ -780,7 +714,5 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
|
|||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating event trigger: {e}", exc_info=True)
|
||||
return self.fail_response("Error creating event trigger")
|
||||
|
||||
TriggerTool.create_event_trigger = create_event_trigger
|
||||
logger.error(f"Exception in create_event_trigger: {e}", exc_info=True)
|
||||
return self.fail_response(f"Error creating event trigger: {str(e)}")
|
||||
|
|
|
@ -171,14 +171,14 @@ export const OneClickIntegrations: React.FC<OneClickIntegrationsProps> = ({
|
|||
</Button>
|
||||
);
|
||||
})}
|
||||
{config.ENV_MODE !== EnvMode.PRODUCTION && <Button
|
||||
<Button
|
||||
variant="default"
|
||||
size='sm'
|
||||
onClick={() => setShowEventDialog(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<PlugZap className="h-4 w-4" /> App-based Trigger
|
||||
</Button>}
|
||||
</Button>
|
||||
</div>
|
||||
<EventBasedTriggerDialog open={showEventDialog} onOpenChange={setShowEventDialog} agentId={agentId} />
|
||||
{configuringSchedule && (
|
||||
|
|
Loading…
Reference in New Issue