From 99a2e9af181b2d05d8e64d8544ee4719ecbbeea4 Mon Sep 17 00:00:00 2001 From: Saumya Date: Wed, 6 Aug 2025 00:43:33 +0530 Subject: [PATCH 1/4] custom agent preview --- .../composio_integration/toolkit_service.py | 12 - .../agents/composio/composio-registry.tsx | 12 - .../custom-agents-page/marketplace-tab.tsx | 36 ++- .../components/agents/installation/types.ts | 1 + .../marketplace-agent-preview-dialog.tsx | 282 ++++++++++++++++++ .../react-query/composio/use-composio.ts | 14 + frontend/src/lib/api-client.ts | 17 +- 7 files changed, 340 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/agents/marketplace-agent-preview-dialog.tsx diff --git a/backend/composio_integration/toolkit_service.py b/backend/composio_integration/toolkit_service.py index 64870dca..91253cd2 100644 --- a/backend/composio_integration/toolkit_service.py +++ b/backend/composio_integration/toolkit_service.py @@ -29,24 +29,12 @@ class ToolkitService: popular_categories = [ {"id": "popular", "name": "Popular"}, {"id": "productivity", "name": "Productivity"}, - {"id": "ai", "name": "AI"}, {"id": "crm", "name": "CRM"}, {"id": "marketing", "name": "Marketing"}, - {"id": "email", "name": "Email"}, {"id": "analytics", "name": "Analytics"}, - {"id": "automation", "name": "Automation"}, {"id": "communication", "name": "Communication"}, {"id": "project-management", "name": "Project Management"}, - {"id": "e-commerce", "name": "E-commerce"}, - {"id": "social-media", "name": "Social Media"}, - {"id": "payments", "name": "Payments"}, - {"id": "finance", "name": "Finance"}, - {"id": "developer-tools", "name": "Developer Tools"}, - {"id": "api", "name": "API"}, - {"id": "notifications", "name": "Notifications"}, {"id": "scheduling", "name": "Scheduling"}, - {"id": "data-analytics", "name": "Data Analytics"}, - {"id": "customer-support", "name": "Customer Support"} ] categories = [CategoryInfo(**cat) for cat in popular_categories] diff --git a/frontend/src/components/agents/composio/composio-registry.tsx b/frontend/src/components/agents/composio/composio-registry.tsx index a5c07134..0a16dcbb 100644 --- a/frontend/src/components/agents/composio/composio-registry.tsx +++ b/frontend/src/components/agents/composio/composio-registry.tsx @@ -18,24 +18,12 @@ import { AgentSelector } from '../../thread/chat-input/agent-selector'; const CATEGORY_EMOJIS: Record = { 'popular': '🔥', 'productivity': '📊', - 'ai': '🤖', 'crm': '👥', 'marketing': '📢', - 'email': '📧', 'analytics': '📈', - 'automation': '⚡', 'communication': '💬', 'project-management': '📋', - 'e-commerce': '🛒', - 'social-media': '📱', - 'payments': '💳', - 'finance': '💰', - 'developer-tools': '🛠️', - 'api': '🔌', - 'notifications': '🔔', 'scheduling': '📅', - 'data-analytics': '📊', - 'customer-support': '🎧' }; diff --git a/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx b/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx index 1231604a..fc14e74f 100644 --- a/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx +++ b/frontend/src/components/agents/custom-agents-page/marketplace-tab.tsx @@ -1,11 +1,12 @@ 'use client'; -import React from 'react'; +import React, { useState } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { SearchBar } from './search-bar'; import { MarketplaceSectionHeader } from './marketplace-section-header'; import { AgentCard } from './agent-card'; +import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog'; import type { MarketplaceTemplate } from '@/components/agents/installation/types'; interface MarketplaceTabProps { @@ -41,6 +42,25 @@ export const MarketplaceTab = ({ getItemStyling, currentUserId }: MarketplaceTabProps) => { + const [previewAgent, setPreviewAgent] = useState(null); + const [previewDialogOpen, setPreviewDialogOpen] = useState(false); + + const handleAgentClick = (item: MarketplaceTemplate) => { + setPreviewAgent(item); + setPreviewDialogOpen(true); + }; + + const handlePreviewClose = () => { + setPreviewDialogOpen(false); + setPreviewAgent(null); + }; + + const handleInstallFromPreview = (agent: MarketplaceTemplate) => { + onInstallClick(agent); + setPreviewDialogOpen(false); + setPreviewAgent(null); + }; + return (
@@ -107,7 +127,7 @@ export const MarketplaceTab = ({ isActioning={installingItemId === item.id} onPrimaryAction={onInstallClick} onDeleteAction={onDeleteTemplate} - onClick={() => onInstallClick(item)} + onClick={() => handleAgentClick(item)} currentUserId={currentUserId} /> ))} @@ -126,7 +146,7 @@ export const MarketplaceTab = ({ isActioning={installingItemId === item.id} onPrimaryAction={onInstallClick} onDeleteAction={onDeleteTemplate} - onClick={() => onInstallClick(item)} + onClick={() => handleAgentClick(item)} currentUserId={currentUserId} /> ))} @@ -145,7 +165,7 @@ export const MarketplaceTab = ({ isActioning={installingItemId === item.id} onPrimaryAction={onInstallClick} onDeleteAction={onDeleteTemplate} - onClick={() => onInstallClick(item)} + onClick={() => handleAgentClick(item)} currentUserId={currentUserId} /> ))} @@ -154,6 +174,14 @@ export const MarketplaceTab = ({
)}
+ + ); }; \ No newline at end of file diff --git a/frontend/src/components/agents/installation/types.ts b/frontend/src/components/agents/installation/types.ts index 43e75fc9..f67b4655 100644 --- a/frontend/src/components/agents/installation/types.ts +++ b/frontend/src/components/agents/installation/types.ts @@ -12,6 +12,7 @@ export interface MarketplaceTemplate { avatar_color?: string; template_id: string; is_kortix_team?: boolean; + agentpress_tools?: Record; mcp_requirements?: Array<{ qualified_name: string; display_name: string; diff --git a/frontend/src/components/agents/marketplace-agent-preview-dialog.tsx b/frontend/src/components/agents/marketplace-agent-preview-dialog.tsx new file mode 100644 index 00000000..4aac6e75 --- /dev/null +++ b/frontend/src/components/agents/marketplace-agent-preview-dialog.tsx @@ -0,0 +1,282 @@ +'use client'; + +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Bot, Download, Wrench, Plug, Tag, User, Calendar, Loader2 } from 'lucide-react'; +import type { MarketplaceTemplate } from '@/components/agents/installation/types'; +import { AGENTPRESS_TOOL_DEFINITIONS } from '@/components/agents/tools'; +import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio'; +import { usePipedreamAppIcon } from '@/hooks/react-query/pipedream/use-pipedream'; + +interface MarketplaceAgentPreviewDialogProps { + agent: MarketplaceTemplate | null; + isOpen: boolean; + onClose: () => void; + onInstall: (agent: MarketplaceTemplate) => void; + isInstalling?: boolean; +} + +const extractAppInfo = (qualifiedName: string, customType?: string) => { + if (customType === 'pipedream') { + const qualifiedMatch = qualifiedName.match(/^pipedream_([^_]+)_/); + if (qualifiedMatch) { + return { type: 'pipedream', slug: qualifiedMatch[1] }; + } + } + + if (customType === 'composio') { + if (qualifiedName.startsWith('composio.')) { + const extractedSlug = qualifiedName.substring(9); + if (extractedSlug) { + return { type: 'composio', slug: extractedSlug }; + } + } + } + + return null; +}; + +const IntegrationLogo: React.FC<{ + qualifiedName: string; + displayName: string; + customType?: string; +}> = ({ qualifiedName, displayName, customType }) => { + const appInfo = extractAppInfo(qualifiedName, customType); + + const { data: pipedreamIconData } = usePipedreamAppIcon( + appInfo?.type === 'pipedream' ? appInfo.slug : '', + { enabled: appInfo?.type === 'pipedream' } + ); + + const { data: composioIconData } = useComposioToolkitIcon( + appInfo?.type === 'composio' ? appInfo.slug : '', + { enabled: appInfo?.type === 'composio' } + ); + + let logoUrl: string | undefined; + if (appInfo?.type === 'pipedream') { + logoUrl = pipedreamIconData?.icon_url; + } else if (appInfo?.type === 'composio') { + logoUrl = composioIconData?.icon_url; + } + + const firstLetter = displayName.charAt(0).toUpperCase(); + + return ( +
+ {logoUrl ? ( + {displayName} { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + target.nextElementSibling?.classList.remove('hidden'); + }} + /> + ) : null} +
+ {firstLetter} +
+
+ ); +}; + +export const MarketplaceAgentPreviewDialog: React.FC = ({ + agent, + isOpen, + onClose, + onInstall, + isInstalling = false +}) => { + if (!agent) return null; + + const { avatar, avatar_color } = agent; + const isSunaAgent = agent.is_kortix_team || false; + + const tools = agent.mcp_requirements || []; + const integrations = tools.filter(tool => !tool.custom_type || tool.custom_type !== 'sse'); + const customTools = tools.filter(tool => tool.custom_type === 'sse'); + + const agentpressTools = Object.entries(agent.agentpress_tools || {}) + .filter(([_, enabled]) => enabled) + .map(([toolName]) => toolName); + + const handleInstall = () => { + onInstall(agent); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getAppDisplayName = (qualifiedName: string) => { + if (qualifiedName.includes('_')) { + const parts = qualifiedName.split('_'); + return parts[parts.length - 1].replace(/\b\w/g, l => l.toUpperCase()); + } + return qualifiedName.replace(/\b\w/g, l => l.toUpperCase()); + }; + + return ( + + + + Agent Preview +
+
+ {avatar || '🤖'} +
+
+
+ +
+
+
+
+

+ {agent.name} +

+
+
+ + {agent.creator_name || 'Unknown'} +
+
+ + {agent.download_count} downloads +
+
+ + {formatDate(agent.created_at)} +
+
+
+
+

+ {agent.description || 'No description available'} +

+ {agent.tags && agent.tags.length > 0 && ( +
+ +
+ {agent.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} +
+
+ {integrations.length > 0 && ( + + +
+ +

Integrations

+
+
+ {integrations.map((integration, index) => ( + + + + {integration.display_name || getAppDisplayName(integration.qualified_name)} + + + ))} +
+
+
+ )} + {customTools.length > 0 && ( + + +
+ +

Custom Tools

+
+
+ {customTools.map((tool, index) => ( + + + + {tool.display_name || getAppDisplayName(tool.qualified_name)} + + + ))} +
+
+
+ )} + {agentpressTools.length === 0 && tools.length === 0 && ( + + + +

+ This agent uses basic functionality without external integrations or specialized tools. +

+
+
+ )} +
+
+
+ + +
+
+
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/hooks/react-query/composio/use-composio.ts b/frontend/src/hooks/react-query/composio/use-composio.ts index e89f0333..2e6f0108 100644 --- a/frontend/src/hooks/react-query/composio/use-composio.ts +++ b/frontend/src/hooks/react-query/composio/use-composio.ts @@ -37,6 +37,20 @@ export const useComposioToolkits = (search?: string, category?: string) => { }); }; +export const useComposioToolkitIcon = (toolkitSlug: string, options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: ['composio', 'toolkit-icon', toolkitSlug], + queryFn: async (): Promise<{ success: boolean; icon_url?: string }> => { + const result = await composioApi.getToolkitIcon(toolkitSlug); + console.log(`🎨 Composio Icon for ${toolkitSlug}:`, result); + return result; + }, + enabled: options?.enabled !== undefined ? options.enabled : !!toolkitSlug, + staleTime: 60 * 60 * 1000, + retry: 2, + }); +}; + export const useCreateComposioProfile = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts index 717aaa50..720ebe5a 100644 --- a/frontend/src/lib/api-client.ts +++ b/frontend/src/lib/api-client.ts @@ -85,19 +85,24 @@ export const apiClient = { clearTimeout(timeoutId); if (!response.ok) { - const error: ApiError = new Error(`HTTP ${response.status}: ${response.statusText}`); - error.status = response.status; - error.response = response; + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + let errorData: any = null; try { - const errorData = await response.json(); - error.details = errorData; + errorData = await response.json(); if (errorData.message) { - error.message = errorData.message; + errorMessage = errorData.message; } } catch { } + const error: ApiError = new Error(errorMessage); + error.status = response.status; + error.response = response; + if (errorData) { + error.details = errorData; + } + if (showErrors) { handleApiError(error, errorContext); } From 50b6ad518921d663c4f664c16760c6c5501e354e Mon Sep 17 00:00:00 2001 From: Saumya Date: Wed, 6 Aug 2025 11:18:06 +0530 Subject: [PATCH 2/4] improve composio UC --- .../credential_profile_tool.py | 2 - .../connected_account_service.py | 22 +- .../mcp_server_service.py | 10 +- .../(dashboard)/settings/credentials/page.tsx | 2 +- .../composio/composio-connections-section.tsx | 37 +- .../agents/composio/composio-connector.tsx | 111 +++-- .../agents/composio/composio-registry.tsx | 381 +++++++++++++++--- .../composio/composio-tools-manager.tsx | 10 +- 8 files changed, 448 insertions(+), 127 deletions(-) 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 eed3e0b2..4a662d44 100644 --- a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py +++ b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py @@ -121,8 +121,6 @@ class CredentialProfileTool(AgentBuilderBaseTool): save_as_profile=True ) - print("[DEBUG] create_credential_profile result:", result) - response_data = { "message": f"Successfully created credential profile '{profile_name}' for {result.toolkit.name}", "profile": { diff --git a/backend/composio_integration/connected_account_service.py b/backend/composio_integration/connected_account_service.py index d124e082..f7797a16 100644 --- a/backend/composio_integration/connected_account_service.py +++ b/backend/composio_integration/connected_account_service.py @@ -26,42 +26,33 @@ class ConnectedAccountService: self.client = ComposioClient.get_client(api_key) def _extract_deprecated_value(self, deprecated_obj) -> Optional[bool]: - """Extract boolean value from Composio SDK's Deprecated object""" if deprecated_obj is None: return None - # If it's already a boolean, return it if isinstance(deprecated_obj, bool): return deprecated_obj - - # If it's a Composio Deprecated object, try to extract meaningful info + if hasattr(deprecated_obj, '__dict__'): - # Check if it has any deprecation info - if so, consider it deprecated deprecated_dict = deprecated_obj.__dict__ if deprecated_dict: return True - # Default to not deprecated return False def _extract_val_dict(self, val_obj) -> Dict[str, Any]: - """Extract dictionary from Composio SDK's val object""" if val_obj is None: return {} - # If it's already a dict, return it if isinstance(val_obj, dict): return val_obj - # If it's a Pydantic model, convert it to dict if hasattr(val_obj, 'model_dump'): return val_obj.model_dump() elif hasattr(val_obj, 'dict'): return val_obj.dict() elif hasattr(val_obj, '__dict__'): return val_obj.__dict__ - - # Fallback to empty dict + return {} async def create_connected_account( @@ -85,16 +76,13 @@ class ConnectedAccountService: } ) - # Access Pydantic model attributes directly connection_data_obj = getattr(response, 'connection_data', None) if not connection_data_obj: - # Try alternative attribute names connection_data_obj = getattr(response, 'connectionData', None) if connection_data_obj and hasattr(connection_data_obj, '__dict__'): connection_data_dict = connection_data_obj.__dict__ - # Extract val field properly - it might be a Pydantic object val_obj = connection_data_dict.get('val', {}) val_dict = self._extract_val_dict(val_obj) @@ -105,7 +93,6 @@ class ConnectedAccountService: else: connection_data = ConnectionState() - # Handle the deprecated field properly deprecated_obj = getattr(response, 'deprecated', None) deprecated_value = self._extract_deprecated_value(deprecated_obj) @@ -136,7 +123,6 @@ class ConnectedAccountService: if not response: return None - # Access Pydantic model attributes directly connection_data_obj = getattr(response, 'connection_data', None) if not connection_data_obj: connection_data_obj = getattr(response, 'connectionData', None) @@ -144,7 +130,6 @@ class ConnectedAccountService: if connection_data_obj and hasattr(connection_data_obj, '__dict__'): connection_data_dict = connection_data_obj.__dict__ - # Extract val field properly - it might be a Pydantic object val_obj = connection_data_dict.get('val', {}) val_dict = self._extract_val_dict(val_obj) @@ -155,7 +140,6 @@ class ConnectedAccountService: else: connection_data = ConnectionState() - # Handle the deprecated field properly deprecated_obj = getattr(response, 'deprecated', None) deprecated_value = self._extract_deprecated_value(deprecated_obj) @@ -213,7 +197,6 @@ class ConnectedAccountService: if connection_data_obj and hasattr(connection_data_obj, '__dict__'): connection_data_dict = connection_data_obj.__dict__ - # Extract val field properly - it might be a Pydantic object val_obj = connection_data_dict.get('val', {}) val_dict = self._extract_val_dict(val_obj) @@ -224,7 +207,6 @@ class ConnectedAccountService: else: connection_data = ConnectionState() - # Handle the deprecated field properly deprecated_obj = getattr(item, 'deprecated', None) deprecated_value = self._extract_deprecated_value(deprecated_obj) diff --git a/backend/composio_integration/mcp_server_service.py b/backend/composio_integration/mcp_server_service.py index 35055f5d..80bf12e3 100644 --- a/backend/composio_integration/mcp_server_service.py +++ b/backend/composio_integration/mcp_server_service.py @@ -142,18 +142,15 @@ class MCPServerService: if user_ids: request_data["user_ids"] = user_ids - - # Try different possible API paths + try: response = self.client.mcp.generate_mcp_url(**request_data) except AttributeError: try: response = self.client.mcp.generate.url(**request_data) except AttributeError: - # Fallback: try direct method call response = self.client.generate_mcp_url(**request_data) - # Access Pydantic model attributes directly mcp_url_response = MCPUrlResponse( mcp_url=response.mcp_url, connected_account_urls=getattr(response, 'connected_account_urls', []), @@ -171,17 +168,14 @@ class MCPServerService: try: logger.info(f"Fetching MCP server: {mcp_server_id}") - # Try different possible API paths try: response = self.client.mcp.get(mcp_server_id) except AttributeError: - # Fallback: try direct method call response = self.client.get_mcp_server(mcp_server_id) if not response: return None - # Access Pydantic model attributes directly commands_obj = getattr(response, 'commands', None) commands = MCPCommands( @@ -211,11 +205,9 @@ class MCPServerService: try: logger.info("Listing MCP servers") - # Try different possible API paths try: response = self.client.mcp.list() except AttributeError: - # Fallback: try direct method call response = self.client.list_mcp_servers() mcp_servers = [] diff --git a/frontend/src/app/(dashboard)/settings/credentials/page.tsx b/frontend/src/app/(dashboard)/settings/credentials/page.tsx index e933d3c1..c22fc160 100644 --- a/frontend/src/app/(dashboard)/settings/credentials/page.tsx +++ b/frontend/src/app/(dashboard)/settings/credentials/page.tsx @@ -44,7 +44,7 @@ export default function AppProfilesPage() {
- Composio Credentials + App Credentials
diff --git a/frontend/src/components/agents/composio/composio-connections-section.tsx b/frontend/src/components/agents/composio/composio-connections-section.tsx index bc2e79d7..0de6ba57 100644 --- a/frontend/src/components/agents/composio/composio-connections-section.tsx +++ b/frontend/src/components/agents/composio/composio-connections-section.tsx @@ -35,8 +35,10 @@ import { XCircle, } from 'lucide-react'; import { useComposioCredentialsProfiles, useComposioMcpUrl } from '@/hooks/react-query/composio/use-composio-profiles'; +import { ComposioRegistry } from './composio-registry'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; +import { useQueryClient } from '@tanstack/react-query'; import type { ComposioProfileSummary, ComposioToolkitGroup } from '@/hooks/react-query/composio/utils'; interface ComposioConnectionsSectionProps { @@ -367,6 +369,8 @@ export const ComposioConnectionsSection: React.FC { const { data: toolkits, isLoading, error } = useComposioCredentialsProfiles(); const [searchQuery, setSearchQuery] = useState(''); + const [showRegistry, setShowRegistry] = useState(false); + const queryClient = useQueryClient(); const filteredToolkits = useMemo(() => { if (!toolkits || !searchQuery.trim()) return toolkits || []; @@ -405,6 +409,12 @@ export const ComposioConnectionsSection: React.FC { + setShowRegistry(false); + queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] }); + toast.success(`Successfully connected ${appName}!`); + }; + if (isLoading) { return (
@@ -448,15 +458,13 @@ export const ComposioConnectionsSection: React.FC
-

No Composio Connections

+

No Connections

- You haven't connected any Composio applications yet. + You haven't connected any applications yet.

-
@@ -477,7 +485,7 @@ export const ComposioConnectionsSection: React.FC
); }; \ No newline at end of file diff --git a/frontend/src/components/agents/composio/composio-connector.tsx b/frontend/src/components/agents/composio/composio-connector.tsx index 77d804db..9cbbc74b 100644 --- a/frontend/src/components/agents/composio/composio-connector.tsx +++ b/frontend/src/components/agents/composio/composio-connector.tsx @@ -88,7 +88,7 @@ const getStepIndex = (step: Step): number => { const StepIndicator = ({ currentStep, mode }: { currentStep: Step; mode: 'full' | 'profile-only' }) => { const currentIndex = getStepIndex(currentStep); const visibleSteps = mode === 'profile-only' - ? stepConfigs.filter(step => step.id !== Step.ToolsSelection) + ? stepConfigs.filter(step => step.id !== Step.ToolsSelection && step.id !== Step.ProfileSelect) : stepConfigs; const visibleCurrentIndex = visibleSteps.findIndex(step => step.id === currentStep); @@ -274,7 +274,7 @@ export const ComposioConnector: React.FC = ({ useEffect(() => { if (open) { - setStep(Step.ProfileSelect); + setStep(mode === 'profile-only' ? Step.ProfileCreate : Step.ProfileSelect); setProfileName(`${app.name} Profile`); setSelectedProfileId(''); setSelectedProfile(null); @@ -287,7 +287,7 @@ export const ComposioConnector: React.FC = ({ setAvailableTools([]); setToolsError(null); } - }, [open, app.name]); + }, [open, app.name, mode]); useEffect(() => { if (step === Step.ToolsSelection && selectedProfile) { @@ -485,7 +485,11 @@ export const ComposioConnector: React.FC = ({ const handleBack = () => { switch (step) { case Step.ProfileCreate: - navigateToStep(Step.ProfileSelect); + if (mode === 'profile-only') { + onOpenChange(false); + } else { + navigateToStep(Step.ProfileSelect); + } break; case Step.Connecting: navigateToStep(Step.ProfileCreate); @@ -590,30 +594,61 @@ export const ComposioConnector: React.FC = ({ transition={{ duration: 0.3, ease: "easeInOut" }} className="space-y-6" > -
- - + + + + + {existingProfiles.map((profile) => ( + +
+ {app.logo ? ( + {app.name} + ) : ( +
+ {app.name.charAt(0)} +
+ )} +
+
{profile.profile_name}
+
+ Created {formatDistanceToNow(new Date(profile.created_at), { addSuffix: true })} +
+
-
- - ))} - -
- - Create New Profile + + ))} + + +
+ )} +
+
+
+ + {existingProfiles.length > 0 ? 'or' : ''} + +
+
+ +
@@ -624,14 +659,16 @@ export const ComposioConnector: React.FC = ({ > Cancel - + {existingProfiles.length > 0 && ( + + )}
)} @@ -754,11 +791,11 @@ export const ComposioConnector: React.FC = ({ className="text-center py-8" >
-
- +
+
-
+

Successfully Connected!

Your {app.name} integration is ready. diff --git a/frontend/src/components/agents/composio/composio-registry.tsx b/frontend/src/components/agents/composio/composio-registry.tsx index 0a16dcbb..0184eab1 100644 --- a/frontend/src/components/agents/composio/composio-registry.tsx +++ b/frontend/src/components/agents/composio/composio-registry.tsx @@ -4,16 +4,19 @@ import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Search, Zap, X } from 'lucide-react'; +import { Search, Zap, X, Settings, ChevronDown, ChevronUp } from 'lucide-react'; import { useComposioToolkits, useComposioCategories } from '@/hooks/react-query/composio/use-composio'; import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles'; -import { useAgent } from '@/hooks/react-query/agents/use-agents'; +import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents'; import { ComposioConnector } from './composio-connector'; +import { ComposioToolsManager } from './composio-tools-manager'; import type { ComposioToolkit, ComposioProfile } from '@/hooks/react-query/composio/utils'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { useQueryClient } from '@tanstack/react-query'; import { AgentSelector } from '../../thread/chat-input/agent-selector'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { Switch } from '@/components/ui/switch'; const CATEGORY_EMOJIS: Record = { 'popular': '🔥', @@ -26,6 +29,16 @@ const CATEGORY_EMOJIS: Record = { 'scheduling': '📅', }; +interface ConnectedApp { + toolkit: ComposioToolkit; + profile: ComposioProfile; + mcpConfig: { + name: string; + type: string; + config: Record; + enabledTools: string[]; + }; +} interface ComposioRegistryProps { onToolsSelected?: (profileId: string, selectedTools: string[], appName: string, appSlug: string) => void; @@ -37,6 +50,52 @@ interface ComposioRegistryProps { onAgentChange?: (agentId: string | undefined) => void; } +// Helper function to get agent-specific connected apps +const getAgentConnectedApps = ( + agent: any, + profiles: ComposioProfile[], + toolkits: ComposioToolkit[] +): ConnectedApp[] => { + if (!agent?.custom_mcps || !profiles?.length || !toolkits?.length) return []; + + const connectedApps: ConnectedApp[] = []; + + agent.custom_mcps.forEach((mcpConfig: any) => { + // Check if this is a Composio MCP by looking for profile_id in config + if (mcpConfig.config?.profile_id) { + const profile = profiles.find(p => p.profile_id === mcpConfig.config.profile_id); + const toolkit = toolkits.find(t => t.slug === profile?.toolkit_slug); + + if (profile && toolkit) { + connectedApps.push({ + toolkit, + profile, + mcpConfig + }); + } + } + }); + + return connectedApps; +}; + +// Helper function to check if an app is connected to the agent +const isAppConnectedToAgent = ( + agent: any, + appSlug: string, + profiles: ComposioProfile[] +): boolean => { + if (!agent?.custom_mcps) return false; + + return agent.custom_mcps.some((mcpConfig: any) => { + if (mcpConfig.config?.profile_id) { + const profile = profiles.find(p => p.profile_id === mcpConfig.config.profile_id); + return profile?.toolkit_slug === appSlug; + } + return false; + }); +}; + const AppCardSkeleton = () => (

@@ -57,16 +116,100 @@ const AppCardSkeleton = () => (
); -const AppCard = ({ app, profiles, onConnect, onConfigure }: { +const ConnectedAppSkeleton = () => ( +
+
+ +
+ + +
+ +
+
+ +
+
+); + +const ConnectedAppCard = ({ + connectedApp, + onToggleTools, + onConfigure, + onManageTools, + isUpdating +}: { + connectedApp: ConnectedApp; + onToggleTools: (profileId: string, enabled: boolean) => void; + onConfigure: (app: ComposioToolkit, profile: ComposioProfile) => void; + onManageTools: (connectedApp: ConnectedApp) => void; + isUpdating: boolean; +}) => { + const { toolkit, profile, mcpConfig } = connectedApp; + const hasEnabledTools = mcpConfig.enabledTools && mcpConfig.enabledTools.length > 0; + + return ( +
+
+ {toolkit.logo ? ( + {toolkit.name} + ) : ( +
+ {toolkit.name.charAt(0)} +
+ )} +
+

{toolkit.name}

+

+ Connected as "{profile.profile_name}" +

+
+
+ +
+
+ +
+
+
+
+ {hasEnabledTools ? `${mcpConfig.enabledTools.length} tools enabled` : 'Connected (no tools)'} +
+
+
+
+ ); +}; + +const AppCard = ({ app, profiles, onConnect, onConfigure, isConnectedToAgent, currentAgentId, mode }: { app: ComposioToolkit; profiles: ComposioProfile[]; onConnect: () => void; onConfigure: (profile: ComposioProfile) => void; + isConnectedToAgent: boolean; + currentAgentId?: string; + mode?: 'full' | 'profile-only'; }) => { const connectedProfiles = profiles.filter(p => p.is_connected); + const canConnect = mode === 'profile-only' ? true : (!isConnectedToAgent && currentAgentId); return ( -
0 ? () => onConfigure(connectedProfiles[0]) : onConnect} className="group border bg-card hover:bg-muted rounded-2xl p-4 transition-all duration-200"> +
0 ? () => onConfigure(connectedProfiles[0]) : onConnect) : undefined} + className={cn( + "group border bg-card rounded-2xl p-4 transition-all duration-200", + canConnect ? "hover:bg-muted cursor-pointer" : "opacity-60 cursor-not-allowed" + )} + >
{app.logo ? ( {app.name} @@ -99,11 +242,32 @@ const AppCard = ({ app, profiles, onConnect, onConfigure }: { )}
- {connectedProfiles.length > 0 && ( + {mode === 'profile-only' ? ( +
+
+
+ {connectedProfiles.length > 0 ? `${connectedProfiles.length} existing profile${connectedProfiles.length !== 1 ? 's' : ''}` : 'Click to connect'} +
+
+ ) : isConnectedToAgent ? ( +
+
+
+ Connected to this agent +
+
+ ) : connectedProfiles.length > 0 ? (
- Connected ({connectedProfiles.length}) + Profile available ({connectedProfiles.length}) +
+
+ ) : ( +
+
+
+ Not connected
)} @@ -125,16 +289,20 @@ export const ComposioRegistry: React.FC = ({ const [selectedCategory, setSelectedCategory] = useState(''); const [selectedApp, setSelectedApp] = useState(null); const [showConnector, setShowConnector] = useState(false); + const [showConnectedApps, setShowConnectedApps] = useState(true); + const [showToolsManager, setShowToolsManager] = useState(false); + const [selectedConnectedApp, setSelectedConnectedApp] = useState(null); const [internalSelectedAgentId, setInternalSelectedAgentId] = useState(selectedAgentId); const queryClient = useQueryClient(); const { data: categoriesData, isLoading: isLoadingCategories } = useComposioCategories(); const { data: toolkits, isLoading } = useComposioToolkits(search, selectedCategory); - const { data: profiles } = useComposioProfiles(); + const { data: profiles, isLoading: isLoadingProfiles } = useComposioProfiles(); const currentAgentId = selectedAgentId ?? internalSelectedAgentId; - const { data: agent } = useAgent(currentAgentId || ''); + const { data: agent, isLoading: isLoadingAgent } = useAgent(currentAgentId || ''); + const { mutate: updateAgent, isPending: isUpdatingAgent } = useUpdateAgent(); const handleAgentSelect = (agentId: string | undefined) => { if (onAgentChange) { @@ -157,13 +325,20 @@ export const ComposioRegistry: React.FC = ({ return grouped; }, [profiles]); + const connectedApps = useMemo(() => { + if (!currentAgentId || !agent) return []; + return getAgentConnectedApps(agent, profiles || [], toolkits?.toolkits || []); + }, [agent, profiles, toolkits, currentAgentId]); + + const isLoadingConnectedApps = currentAgentId && (isLoadingAgent || isLoadingProfiles || isLoading); + const filteredToolkits = useMemo(() => { if (!toolkits?.toolkits) return []; return toolkits.toolkits; }, [toolkits]); const handleConnect = (app: ComposioToolkit) => { - if (!currentAgentId && showAgentSelector) { + if (mode !== 'profile-only' && !currentAgentId && showAgentSelector) { toast.error('Please select an agent first'); return; } @@ -172,7 +347,7 @@ export const ComposioRegistry: React.FC = ({ }; const handleConfigure = (app: ComposioToolkit, profile: ComposioProfile) => { - if (!currentAgentId) { + if (mode !== 'profile-only' && !currentAgentId) { toast.error('Please select an agent first'); return; } @@ -180,6 +355,37 @@ export const ComposioRegistry: React.FC = ({ setShowConnector(true); }; + const handleToggleTools = (profileId: string, enabled: boolean) => { + if (!currentAgentId || !agent) return; + + const updatedCustomMcps = agent.custom_mcps?.map((mcpConfig: any) => { + if (mcpConfig.config?.profile_id === profileId) { + return { + ...mcpConfig, + enabledTools: enabled ? mcpConfig.enabledTools || [] : [] + }; + } + return mcpConfig; + }) || []; + + updateAgent({ + agentId: currentAgentId, + custom_mcps: updatedCustomMcps + }, { + onSuccess: () => { + toast.success(enabled ? 'Tools enabled' : 'Tools disabled'); + }, + onError: (error: any) => { + toast.error(error.message || 'Failed to update tools'); + } + }); + }; + + const handleManageTools = (connectedApp: ConnectedApp) => { + setSelectedConnectedApp(connectedApp); + setShowToolsManager(true); + }; + const handleConnectionComplete = (profileId: string, appName: string, appSlug: string) => { setShowConnector(false); queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] }); @@ -251,9 +457,14 @@ export const ComposioRegistry: React.FC = ({
-

App Integrations

+

+ {mode === 'profile-only' ? 'Connect New App' : 'App Integrations'} +

- Connect your favorite apps powered by Composio + {mode === 'profile-only' + ? 'Create a connection profile for your favorite apps' + : `Connect your favorite apps with ${currentAgentId ? 'this agent' : 'your agent'}` + }

@@ -264,11 +475,6 @@ export const ComposioRegistry: React.FC = ({ isSunaAgent={agent?.metadata?.is_suna_default} /> )} - {onClose && ( - - )}
@@ -301,42 +507,103 @@ export const ComposioRegistry: React.FC = ({
-
- {isLoading ? ( -
- {Array.from({ length: 12 }).map((_, i) => ( - - ))} -
- ) : filteredToolkits.length === 0 ? ( -
-
- -
-

No apps found

-

- {search ? `No apps match "${search}"` : 'No apps available in this category'} -

-
- ) : ( -
- {filteredToolkits.map((app) => ( - handleConnect(app)} - onConfigure={(profile) => handleConfigure(app, profile)} - /> - ))} -
+
+ {currentAgentId && ( + + +
+
+

Connected to this agent

+ {isLoadingConnectedApps ? ( + + ) : connectedApps.length > 0 && ( + + {connectedApps.length} + + )} +
+ {showConnectedApps ? ( + + ) : ( + + )} +
+
+ + {isLoadingConnectedApps ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : connectedApps.length === 0 ? ( +
+
+ +
+

No connected apps

+

Connect apps below to manage tools for this agent.

+
+ ) : ( +
+ {connectedApps.map((connectedApp) => ( + + ))} +
+ )} +
+
)} +
+

+ {currentAgentId ? 'Available Apps' : 'Browse Apps'} +

+ + {isLoading ? ( +
+ {Array.from({ length: 12 }).map((_, i) => ( + + ))} +
+ ) : filteredToolkits.length === 0 ? ( +
+
+ +
+

No apps found

+

+ {search ? `No apps match "${search}"` : 'No apps available in this category'} +

+
+ ) : ( +
+ {filteredToolkits.map((app) => ( + handleConnect(app)} + onConfigure={(profile) => handleConfigure(app, profile)} + isConnectedToAgent={isAppConnectedToAgent(agent, app.slug, profiles || [])} + currentAgentId={currentAgentId} + mode={mode} + /> + ))} +
+ )} +
- {selectedApp && ( = ({ open={showConnector} onOpenChange={setShowConnector} onComplete={handleConnectionComplete} + mode={mode} + /> + )} + + {selectedConnectedApp && currentAgentId && ( + { + queryClient.invalidateQueries({ queryKey: ['agents', currentAgentId] }); + }} /> )}
diff --git a/frontend/src/components/agents/composio/composio-tools-manager.tsx b/frontend/src/components/agents/composio/composio-tools-manager.tsx index 34370c8e..14150db6 100644 --- a/frontend/src/components/agents/composio/composio-tools-manager.tsx +++ b/frontend/src/components/agents/composio/composio-tools-manager.tsx @@ -11,6 +11,7 @@ import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Search, Save, AlertCircle, Zap, CheckCircle2, Settings2, Loader2 } from 'lucide-react'; import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles'; +import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio'; import { backendApi } from '@/lib/api-client'; import { toast } from 'sonner'; import { useQueryClient } from '@tanstack/react-query'; @@ -129,6 +130,9 @@ export const ComposioToolsManager: React.FC = ({ const { data: profiles } = useComposioProfiles(); const currentProfile = profileInfo || profiles?.find(p => p.profile_id === profileId); + const { data: iconData } = useComposioToolkitIcon(currentProfile?.toolkit_slug || '', { + enabled: !!currentProfile?.toolkit_slug + }); const filteredTools = useMemo(() => { if (!searchTerm) return availableTools; @@ -241,11 +245,11 @@ export const ComposioToolsManager: React.FC = ({
- {appLogo ? ( + {iconData?.icon_url || appLogo ? ( {currentProfile?.toolkit_name} ) : (
From 3b977e961ef180ce5c27cc14d3432be3c6363f29 Mon Sep 17 00:00:00 2001 From: Saumya Date: Wed, 6 Aug 2025 11:31:10 +0530 Subject: [PATCH 3/4] chore: improve integrations manager ux --- .../credential_profile_tool.py | 89 +------------------ backend/composio_integration/api.py | 15 ++-- .../composio_integration/composio_service.py | 2 +- .../connected_account_service.py | 2 +- 4 files changed, 15 insertions(+), 93 deletions(-) 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 4a662d44..4876c66a 100644 --- a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py +++ b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py @@ -1,4 +1,5 @@ from typing import Optional, List +from uuid import uuid4 from agentpress.tool import ToolResult, openapi_schema, usage_example from agentpress.thread_manager import ThreadManager from .base_tool import AgentBuilderBaseTool @@ -110,14 +111,16 @@ class CredentialProfileTool(AgentBuilderBaseTool): ) -> ToolResult: try: account_id = await self._get_current_account_id() + integration_user_id = str(uuid4()) + logger.info(f"Generated integration user_id: {integration_user_id} for account: {account_id}") integration_service = get_integration_service(db_connection=self.db) result = await integration_service.integrate_toolkit( toolkit_slug=toolkit_slug, account_id=account_id, + user_id=integration_user_id, profile_name=profile_name, display_name=display_name or profile_name, - user_id=account_id, save_as_profile=True ) @@ -146,90 +149,6 @@ class CredentialProfileTool(AgentBuilderBaseTool): except Exception as e: return self.fail_response(f"Error creating credential profile: {str(e)}") - # @openapi_schema({ - # "type": "function", - # "function": { - # "name": "check_profile_connection", - # "description": "Check the connection status of a credential profile and get available tools if connected.", - # "parameters": { - # "type": "object", - # "properties": { - # "profile_id": { - # "type": "string", - # "description": "The ID of the credential profile to check" - # } - # }, - # "required": ["profile_id"] - # } - # } - # }) - # @usage_example(''' - # - # - # profile-uuid-123 - # - # - # ''') - # async def check_profile_connection(self, profile_id: str) -> ToolResult: - # try: - # from uuid import UUID - # from pipedream.connection_service import ExternalUserId - # account_id = await self._get_current_account_id() - # profile_service = ComposioProfileService(self.db) - - # profile = await profile_service.get_profile(UUID(account_id), UUID(profile_id)) - # if not profile: - # return self.fail_response("Credential profile not found") - - # external_user_id = ExternalUserId(profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id)) - # connection_service = ConnectionService(self.db) - # raw_connections = await connection_service.get_connections_for_user(external_user_id) - # connections = [] - # for conn in raw_connections: - # connections.append({ - # "external_user_id": conn.external_user_id.value if hasattr(conn.external_user_id, 'value') else str(conn.external_user_id), - # "app_slug": conn.app.slug.value if hasattr(conn.app.slug, 'value') else str(conn.app.slug), - # "app_name": conn.app.name, - # "created_at": conn.created_at.isoformat() if conn.created_at else None, - # "updated_at": conn.updated_at.isoformat() if conn.updated_at else None, - # "is_active": conn.is_active - # }) - - # response_data = { - # "profile_name": profile.display_name, - # "app_name": profile.app_name, - # "app_slug": profile.app_slug.value if hasattr(profile.app_slug, 'value') else str(profile.app_slug), - # "external_user_id": profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id), - # "is_connected": profile.is_connected, - # "connections": connections, - # "connection_count": len(connections) - # } - - # if profile.is_connected and connections: - # try: - # from pipedream.mcp_service import ConnectionStatus, ExternalUserId, AppSlug - # external_user_id = ExternalUserId(profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id)) - # app_slug = AppSlug(profile.app_slug.value if hasattr(profile.app_slug, 'value') else str(profile.app_slug)) - # servers = await mcp_service.discover_servers_for_user(external_user_id, app_slug) - # connected_servers = [s for s in servers if s.status == ConnectionStatus.CONNECTED] - # if connected_servers: - # tools = [t.name for t in connected_servers[0].available_tools] - # response_data["available_tools"] = tools - # response_data["tool_count"] = len(tools) - # response_data["message"] = f"Profile '{profile.display_name}' is connected with {len(tools)} available tools" - # else: - # response_data["message"] = f"Profile '{profile.display_name}' is connected but no MCP tools are available yet" - # except Exception as mcp_error: - # logger.error(f"Error getting MCP tools for profile: {mcp_error}") - # response_data["message"] = f"Profile '{profile.display_name}' is connected but could not retrieve MCP tools" - # else: - # response_data["message"] = f"Profile '{profile.display_name}' is not connected yet" - - # return self.success_response(response_data) - - # except Exception as e: - # return self.fail_response(f"Error checking profile connection: {str(e)}") - @openapi_schema({ "type": "function", "function": { diff --git a/backend/composio_integration/api.py b/backend/composio_integration/api.py index 6018e09e..ca4d7725 100644 --- a/backend/composio_integration/api.py +++ b/backend/composio_integration/api.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query from typing import Dict, Any, Optional, List from pydantic import BaseModel +from uuid import uuid4 from utils.auth_utils import get_current_user_id_from_jwt from utils.logger import logger from services.supabase import DBConnection @@ -16,11 +17,9 @@ from .composio_profile_service import ComposioProfileService, ComposioProfile router = APIRouter(prefix="/composio", tags=["composio"]) -# Global database connection db: Optional[DBConnection] = None def initialize(database: DBConnection): - """Initialize the composio API with database connection""" global db db = database @@ -29,7 +28,6 @@ class IntegrateToolkitRequest(BaseModel): toolkit_slug: str profile_name: Optional[str] = None display_name: Optional[str] = None - user_id: Optional[str] = "default" mcp_server_name: Optional[str] = None save_as_profile: bool = True @@ -49,7 +47,6 @@ class CreateProfileRequest(BaseModel): toolkit_slug: str profile_name: str display_name: Optional[str] = None - user_id: Optional[str] = "default" mcp_server_name: Optional[str] = None is_default: bool = False @@ -137,13 +134,16 @@ async def integrate_toolkit( current_user_id: str = Depends(get_current_user_id_from_jwt) ) -> IntegrationStatusResponse: try: + integration_user_id = str(uuid4()) + logger.info(f"Generated integration user_id: {integration_user_id} for account: {current_user_id}") + service = get_integration_service(db_connection=db) result = await service.integrate_toolkit( toolkit_slug=request.toolkit_slug, account_id=current_user_id, + user_id=integration_user_id, profile_name=request.profile_name, display_name=request.display_name, - user_id=request.user_id or current_user_id, mcp_server_name=request.mcp_server_name, save_as_profile=request.save_as_profile ) @@ -171,13 +171,16 @@ async def create_profile( current_user_id: str = Depends(get_current_user_id_from_jwt) ) -> ProfileResponse: try: + integration_user_id = str(uuid4()) + logger.info(f"Generated integration user_id: {integration_user_id} for account: {current_user_id}") + service = get_integration_service(db_connection=db) result = await service.integrate_toolkit( toolkit_slug=request.toolkit_slug, account_id=current_user_id, + user_id=integration_user_id, profile_name=request.profile_name, display_name=request.display_name, - user_id=request.user_id or current_user_id, mcp_server_name=request.mcp_server_name, save_as_profile=True ) diff --git a/backend/composio_integration/composio_service.py b/backend/composio_integration/composio_service.py index f6c99cf0..3f9f19c3 100644 --- a/backend/composio_integration/composio_service.py +++ b/backend/composio_integration/composio_service.py @@ -36,9 +36,9 @@ class ComposioIntegrationService: self, toolkit_slug: str, account_id: str, + user_id: str, profile_name: Optional[str] = None, display_name: Optional[str] = None, - user_id: str = "default", mcp_server_name: Optional[str] = None, save_as_profile: bool = True ) -> ComposioIntegrationResult: diff --git a/backend/composio_integration/connected_account_service.py b/backend/composio_integration/connected_account_service.py index f7797a16..2c6981f1 100644 --- a/backend/composio_integration/connected_account_service.py +++ b/backend/composio_integration/connected_account_service.py @@ -58,7 +58,7 @@ class ConnectedAccountService: async def create_connected_account( self, auth_config_id: str, - user_id: str = "default" + user_id: str ) -> ConnectedAccount: try: logger.info(f"Creating connected account for auth_config: {auth_config_id}, user: {user_id}") From 87f6adc8cf87d6f526d801f823b7ed721966d17a Mon Sep 17 00:00:00 2001 From: Saumya Date: Wed, 6 Aug 2025 11:56:46 +0530 Subject: [PATCH 4/4] composio integrations UX improvement --- backend/agent/suna/config.py | 1 + .../agents/mcp/mcp-configuration-new.tsx | 2 - .../components/workflows/workflow-builder.tsx | 6 +- .../workflows/workflow-definitions.ts | 3 +- .../workflows/workflow-side-panel.tsx | 124 ++++-------------- 5 files changed, 30 insertions(+), 106 deletions(-) diff --git a/backend/agent/suna/config.py b/backend/agent/suna/config.py index 6817e5d3..75db8bd6 100644 --- a/backend/agent/suna/config.py +++ b/backend/agent/suna/config.py @@ -11,6 +11,7 @@ class SunaConfig: DEFAULT_TOOLS = { "sb_shell_tool": True, + "sb_files_tool": True, "sb_browser_tool": True, "sb_deploy_tool": True, "sb_expose_tool": True, diff --git a/frontend/src/components/agents/mcp/mcp-configuration-new.tsx b/frontend/src/components/agents/mcp/mcp-configuration-new.tsx index ddde3c19..829a93e6 100644 --- a/frontend/src/components/agents/mcp/mcp-configuration-new.tsx +++ b/frontend/src/components/agents/mcp/mcp-configuration-new.tsx @@ -90,8 +90,6 @@ export const MCPConfigurationNew: React.FC = ({ const handleToolsSelected = (profileId: string, selectedTools: string[], appName: string, appSlug: string) => { console.log('Tools selected:', { profileId, selectedTools, appName, appSlug }); setShowRegistryDialog(false); - // ComposioRegistry handles all the actual configuration internally - // We need to refresh the agent data to show updated configuration queryClient.invalidateQueries({ queryKey: ['agents'] }); queryClient.invalidateQueries({ queryKey: ['agent', selectedAgentId] }); queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] }); diff --git a/frontend/src/components/workflows/workflow-builder.tsx b/frontend/src/components/workflows/workflow-builder.tsx index b779c659..3cdc5a6c 100644 --- a/frontend/src/components/workflows/workflow-builder.tsx +++ b/frontend/src/components/workflows/workflow-builder.tsx @@ -152,10 +152,9 @@ export function WorkflowBuilder({
{editableSteps.length === 0 ? ( - // Empty state
-
+

@@ -165,13 +164,12 @@ export function WorkflowBuilder({ Add steps to create a workflow that guides your agent through tasks.

) : ( - // Steps list - uses the children array from root node
{ if (stepType.id === 'credentials_profile') { - setShowPipedreamRegistry(true); + setShowComposioRegistry(true); } else if (stepType.id === 'mcp_configuration') { setShowCustomMCPDialog(true); } else { @@ -113,88 +117,25 @@ export function WorkflowSidePanel({ } }; - const handlePipedreamToolsSelected = async (profileId: string, selectedTools: string[], appName: string, appSlug: string) => { - try { - const pipedreamMCP = { - name: appName, - qualifiedName: `pipedream_${appSlug}_${profileId}`, - config: { - url: 'https://remote.mcp.pipedream.net', - headers: { - 'x-pd-app-slug': appSlug, - }, - profile_id: profileId - }, - enabledTools: selectedTools, - selectedProfileId: profileId - }; - - // Update agent with new MCP - const existingCustomMCPs = agent?.custom_mcps || []; - const nonPipedreamMCPs = existingCustomMCPs.filter((mcp: any) => - mcp.type !== 'pipedream' || mcp.config?.profile_id !== profileId - ); - - await updateAgentMutation.mutateAsync({ - agentId: agentId!, - custom_mcps: [ - ...nonPipedreamMCPs, - { - name: appName, - type: 'pipedream', - config: pipedreamMCP.config, - enabledTools: selectedTools - } as any - ] - }); - - // Create a step for the credentials profile - const credentialsStep: ConditionalStep = { - id: `credentials-${Date.now()}`, - name: `${appName} Credentials`, - description: `Credentials profile for ${appName} integration`, - type: 'instruction', - config: { - step_type: 'credentials_profile', - tool_name: selectedTools[0] || `${appName} Profile`, - profile_id: profileId, - app_name: appName, - app_slug: appSlug - }, - order: 0, - enabled: true, - children: [] - }; - - onCreateStep({ - id: 'credentials_profile', - name: credentialsStep.name, - description: credentialsStep.description, - icon: 'Key', - category: 'configuration', - config: credentialsStep.config - }); - - // Invalidate queries to refresh tools - queryClient.invalidateQueries({ queryKey: ['agent', agentId] }); - queryClient.invalidateQueries({ queryKey: ['agent-tools', agentId] }); - - // Trigger parent tools update - if (onToolsUpdate) { - onToolsUpdate(); - } - - setShowPipedreamRegistry(false); - toast.success(`Added ${appName} credentials profile!`); - } catch (error) { - toast.error('Failed to add integration'); + const handleComposioToolsSelected = (profileId: string, selectedTools: string[], appName: string, appSlug: string) => { + console.log('Tools selected:', { profileId, selectedTools, appName, appSlug }); + setShowComposioRegistry(false); + // ComposioRegistry handles all the actual configuration internally + // We need to refresh the agent data to show updated configuration + queryClient.invalidateQueries({ queryKey: ['agents'] }); + queryClient.invalidateQueries({ queryKey: ['agent', agentId] }); + queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] }); + + if (onToolsUpdate) { + onToolsUpdate(); } + + toast.success(`Connected ${appName} integration!`); }; const handleCustomMCPSave = async (customConfig: any) => { try { const existingCustomMCPs = agent?.custom_mcps || []; - await updateAgentMutation.mutateAsync({ agentId: agentId!, custom_mcps: [ @@ -208,7 +149,6 @@ export function WorkflowSidePanel({ ] }); - // Create a step for the MCP configuration const mcpStep: ConditionalStep = { id: `mcp-${Date.now()}`, name: `${customConfig.name} MCP`, @@ -302,7 +242,7 @@ export function WorkflowSidePanel({ onClick={() => handleStepTypeClick(stepType)} className="w-full p-3 text-left border border-border rounded-2xl hover:bg-muted/50 transition-colors" > -
+
@@ -472,30 +412,18 @@ export function WorkflowSidePanel({ {renderContent()} - - {/* Pipedream Registry Dialog */} - - - - + + + + App Integrations - { }} - onToolsSelected={handlePipedreamToolsSelected} - versionData={versionData ? { - configured_mcps: versionData.configured_mcps || [], - custom_mcps: versionData.custom_mcps || [], - system_prompt: versionData.system_prompt || '', - agentpress_tools: versionData.agentpress_tools || {} - } : undefined} - versionId={versionData?.version_id || 'current'} + onClose={() => setShowComposioRegistry(false)} + onToolsSelected={handleComposioToolsSelected} /> - - {/* Custom MCP Dialog */}