feat: enable app triggers

This commit is contained in:
Vukasin 2025-08-28 17:59:30 +02:00 committed by marko-kraemer
parent 06df493157
commit b92cb3ce01
4 changed files with 70 additions and 136 deletions

View File

@ -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

View File

@ -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,

View File

@ -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": {
@ -444,8 +443,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
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",
"function": {
@ -486,62 +483,6 @@ if config.ENV_MODE != EnvMode.PRODUCTION:
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",
"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)}")

View File

@ -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 && (