diff --git a/backend/agent/agent_builder_prompt.py b/backend/agent/agent_builder_prompt.py index a29f8723..e8854bee 100644 --- a/backend/agent/agent_builder_prompt.py +++ b/backend/agent/agent_builder_prompt.py @@ -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 diff --git a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py index 3b75ec9a..09983426 100644 --- a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py +++ b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py @@ -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, diff --git a/backend/agent/tools/agent_builder_tools/trigger_tool.py b/backend/agent/tools/agent_builder_tools/trigger_tool.py index d8a792b7..aacd138f 100644 --- a/backend/agent/tools/agent_builder_tools/trigger_tool.py +++ b/backend/agent/tools/agent_builder_tools/trigger_tool.py @@ -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(''' - - - gmail - - - ''') - 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: gmail - gmail + + [toolkit_slug] + GMAIL_NEW_GMAIL_MESSAGE profile_123 @@ -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)}") diff --git a/frontend/src/components/agents/triggers/one-click-integrations.tsx b/frontend/src/components/agents/triggers/one-click-integrations.tsx index bda48733..1fbad8bb 100644 --- a/frontend/src/components/agents/triggers/one-click-integrations.tsx +++ b/frontend/src/components/agents/triggers/one-click-integrations.tsx @@ -171,14 +171,14 @@ export const OneClickIntegrations: React.FC = ({ ); })} - {config.ENV_MODE !== EnvMode.PRODUCTION && } + {configuringSchedule && (