mirror of https://github.com/kortix-ai/suna.git
chore: auto save integrations
This commit is contained in:
parent
f713c51daf
commit
d64f51ada4
|
@ -120,6 +120,7 @@ class AgentUpdateRequest(BaseModel):
|
|||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
replace_mcps: Optional[bool] = None
|
||||
|
||||
class AgentResponse(BaseModel):
|
||||
agent_id: str
|
||||
|
@ -145,7 +146,7 @@ class AgentResponse(BaseModel):
|
|||
current_version: Optional[AgentVersionResponse] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
from utils.pagination import PaginationService, PaginationParams, PaginatedResponse
|
||||
from utils.pagination import PaginationParams
|
||||
|
||||
class PaginationInfo(BaseModel):
|
||||
current_page: int
|
||||
|
@ -2105,16 +2106,32 @@ async def update_agent(
|
|||
print(f"[DEBUG] update_agent: Prepared update_data with icon fields - icon_name={update_data.get('icon_name')}, icon_color={update_data.get('icon_color')}, icon_background={update_data.get('icon_background')}")
|
||||
|
||||
current_system_prompt = agent_data.system_prompt if agent_data.system_prompt is not None else current_version_data.get('system_prompt', '')
|
||||
current_configured_mcps = agent_data.configured_mcps if agent_data.configured_mcps is not None else current_version_data.get('configured_mcps', [])
|
||||
|
||||
if agent_data.configured_mcps is not None:
|
||||
if agent_data.replace_mcps:
|
||||
current_configured_mcps = agent_data.configured_mcps
|
||||
logger.debug(f"Replacing configured MCPs for agent {agent_id}: {current_configured_mcps}")
|
||||
else:
|
||||
current_configured_mcps = agent_data.configured_mcps
|
||||
else:
|
||||
current_configured_mcps = current_version_data.get('configured_mcps', [])
|
||||
|
||||
# Handle custom MCPs - either replace or merge based on the flag
|
||||
if agent_data.custom_mcps is not None:
|
||||
current_custom_mcps = merge_custom_mcps(
|
||||
current_version_data.get('custom_mcps', []),
|
||||
agent_data.custom_mcps
|
||||
)
|
||||
if agent_data.replace_mcps:
|
||||
# Replace mode: use the provided list as-is
|
||||
current_custom_mcps = agent_data.custom_mcps
|
||||
logger.debug(f"Replacing custom MCPs for agent {agent_id}: {current_custom_mcps}")
|
||||
else:
|
||||
# Merge mode: merge with existing MCPs (default behavior)
|
||||
current_custom_mcps = merge_custom_mcps(
|
||||
current_version_data.get('custom_mcps', []),
|
||||
agent_data.custom_mcps
|
||||
)
|
||||
logger.debug(f"Merging custom MCPs for agent {agent_id}")
|
||||
else:
|
||||
current_custom_mcps = current_version_data.get('custom_mcps', [])
|
||||
|
||||
|
||||
current_agentpress_tools = agent_data.agentpress_tools if agent_data.agentpress_tools is not None else current_version_data.get('agentpress_tools', {})
|
||||
current_avatar = agent_data.avatar if agent_data.avatar is not None else existing_data.get('avatar')
|
||||
current_avatar_color = agent_data.avatar_color if agent_data.avatar_color is not None else existing_data.get('avatar_color')
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
|
|||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
|
||||
import { useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useUpdateAgentMCPs } from '@/hooks/react-query/agents/use-update-agent-mcps';
|
||||
import { useCreateAgentVersion, useActivateAgentVersion } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
|
@ -23,7 +24,6 @@ import { useAgentConfigTour } from '@/hooks/use-agent-config-tour';
|
|||
import Joyride, { CallBackProps, STATUS, Step } from 'react-joyride';
|
||||
import { TourConfirmationDialog } from '@/components/tour/TourConfirmationDialog';
|
||||
|
||||
// Tour steps for agent configuration
|
||||
const agentConfigTourSteps: Step[] = [
|
||||
{
|
||||
target: '[data-tour="agent-header"]',
|
||||
|
@ -90,7 +90,6 @@ const agentConfigTourSteps: Step[] = [
|
|||
},
|
||||
];
|
||||
|
||||
// Form data interface
|
||||
interface FormData {
|
||||
name: string;
|
||||
description: string;
|
||||
|
@ -119,6 +118,7 @@ function AgentConfigurationContent() {
|
|||
const { setHasUnsavedChanges } = useAgentVersionStore();
|
||||
|
||||
const updateAgentMutation = useUpdateAgent();
|
||||
const updateAgentMCPsMutation = useUpdateAgentMCPs();
|
||||
const createVersionMutation = useCreateAgentVersion();
|
||||
const activateVersionMutation = useActivateAgentVersion();
|
||||
const exportMutation = useExportAgent();
|
||||
|
@ -199,12 +199,41 @@ function AgentConfigurationContent() {
|
|||
}, []);
|
||||
|
||||
const handleMCPChange = useCallback((updates: { configured_mcps: any[]; custom_mcps: any[] }) => {
|
||||
const previousConfiguredMcps = formData.configured_mcps;
|
||||
const previousCustomMcps = formData.custom_mcps;
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
configured_mcps: updates.configured_mcps,
|
||||
custom_mcps: updates.custom_mcps
|
||||
configured_mcps: updates.configured_mcps || [],
|
||||
custom_mcps: updates.custom_mcps || []
|
||||
}));
|
||||
}, []);
|
||||
|
||||
updateAgentMCPsMutation.mutate({
|
||||
agentId,
|
||||
configured_mcps: updates.configured_mcps || [],
|
||||
custom_mcps: updates.custom_mcps || [],
|
||||
replace_mcps: true
|
||||
}, {
|
||||
onSuccess: (updatedAgent) => {
|
||||
setOriginalData(prev => ({
|
||||
...prev,
|
||||
configured_mcps: updates.configured_mcps || [],
|
||||
custom_mcps: updates.custom_mcps || []
|
||||
}));
|
||||
|
||||
toast.success('MCP configuration updated');
|
||||
},
|
||||
onError: (error) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
configured_mcps: previousConfiguredMcps,
|
||||
custom_mcps: previousCustomMcps
|
||||
}));
|
||||
toast.error('Failed to update MCP configuration');
|
||||
console.error('MCP update error:', error);
|
||||
}
|
||||
});
|
||||
}, [agentId, updateAgentMCPsMutation, formData.configured_mcps, formData.custom_mcps]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
exportMutation.mutate(agentId);
|
||||
|
|
|
@ -45,18 +45,23 @@ export const AgentMCPConfiguration: React.FC<AgentMCPConfigurationProps> = ({
|
|||
};
|
||||
}
|
||||
|
||||
// Map 'sse' backend type to 'http' for frontend display
|
||||
const displayType = customMcp.type === 'sse' ? 'http' : (customMcp.type || customMcp.customType);
|
||||
|
||||
return {
|
||||
name: customMcp.name,
|
||||
qualifiedName: customMcp.qualifiedName || `custom_${customMcp.type || customMcp.customType}_${customMcp.name.replace(' ', '_').toLowerCase()}`,
|
||||
qualifiedName: customMcp.qualifiedName || `custom_${displayType}_${customMcp.name.replace(' ', '_').toLowerCase()}`,
|
||||
config: customMcp.config,
|
||||
enabledTools: customMcp.enabledTools,
|
||||
isCustom: true,
|
||||
customType: customMcp.type || customMcp.customType
|
||||
customType: displayType
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
const handleConfigurationChange = (mcps: any[]) => {
|
||||
console.log('[AgentMCPConfiguration] Configuration changed:', mcps);
|
||||
|
||||
const configured = mcps.filter(mcp => !mcp.isCustom);
|
||||
const custom = mcps
|
||||
.filter(mcp => mcp.isCustom)
|
||||
|
@ -71,15 +76,20 @@ export const AgentMCPConfiguration: React.FC<AgentMCPConfigurationProps> = ({
|
|||
};
|
||||
}
|
||||
|
||||
// Map 'http' to 'sse' for backend compatibility
|
||||
const backendType = mcp.customType === 'http' ? 'sse' : mcp.customType;
|
||||
|
||||
return {
|
||||
name: mcp.name,
|
||||
type: mcp.customType,
|
||||
type: backendType,
|
||||
customType: mcp.customType,
|
||||
config: mcp.config,
|
||||
enabledTools: mcp.enabledTools
|
||||
};
|
||||
});
|
||||
|
||||
console.log('[AgentMCPConfiguration] Sending to parent - configured:', configured, 'custom:', custom);
|
||||
|
||||
onMCPChange({
|
||||
configured_mcps: configured,
|
||||
custom_mcps: custom
|
||||
|
|
|
@ -643,7 +643,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<h4 className="font-medium text-foreground">Connect to {app.name}</h4>
|
||||
<p className="text-xs text-muted-foreground">Choose an existing profile or create a new connection</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => setShowToolsManager(!showToolsManager)}>
|
||||
<Button variant="outline" size="sm" onClick={() => setShowToolsManager(!showToolsManager)} type="button">
|
||||
{showToolsManager ? 'Hide' : 'View'} Tools
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -810,6 +810,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="px-6"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
@ -823,6 +824,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
}}
|
||||
disabled={!selectedConnectionType || (selectedConnectionType === 'existing' && !selectedProfileId)}
|
||||
className="px-8 min-w-[120px]"
|
||||
type="button"
|
||||
>
|
||||
{selectedConnectionType === 'new' ? (
|
||||
<>
|
||||
|
@ -919,6 +921,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
onClick={handleBack}
|
||||
disabled={isCreating}
|
||||
className="flex-1"
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
|
@ -927,6 +930,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
onClick={handleCreateProfile}
|
||||
disabled={isCreating || isLoadingToolkitDetails || !profileName.trim()}
|
||||
className="flex-1"
|
||||
type="button"
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
|
@ -983,6 +987,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<Button
|
||||
onClick={handleAuthComplete}
|
||||
className="w-full"
|
||||
type="button"
|
||||
>
|
||||
I've Completed Authentication
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
@ -1078,6 +1083,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
|
@ -1085,6 +1091,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<Button
|
||||
onClick={handleSaveTools}
|
||||
className="min-w-[80px]"
|
||||
type="button"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
Save Tools
|
||||
|
|
|
@ -7,14 +7,14 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||
import { Search, Zap, X, Settings, ChevronDown, ChevronUp, Loader2, Server } from 'lucide-react';
|
||||
import { useComposioCategories, useComposioToolkitsInfinite } from '@/hooks/react-query/composio/use-composio';
|
||||
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
|
||||
import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useUpdateAgentMCPs } from '@/hooks/react-query/agents/use-update-agent-mcps';
|
||||
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 { CustomMCPDialog } from '../mcp/custom-mcp-dialog';
|
||||
|
||||
|
@ -168,6 +168,7 @@ const ConnectedAppCard = ({
|
|||
size="sm"
|
||||
onClick={() => onManageTools(connectedApp)}
|
||||
disabled={isUpdating}
|
||||
type="button"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
|
@ -311,7 +312,7 @@ export const ComposioRegistry: React.FC<ComposioRegistryProps> = ({
|
|||
|
||||
const currentAgentId = selectedAgentId ?? internalSelectedAgentId;
|
||||
const { data: agent, isLoading: isLoadingAgent } = useAgent(currentAgentId || '');
|
||||
const { mutate: updateAgent, isPending: isUpdatingAgent } = useUpdateAgent();
|
||||
const { mutate: updateAgent, isPending: isUpdatingAgent } = useUpdateAgentMCPs(); // Use the MCP-specific hook
|
||||
|
||||
const handleAgentSelect = (agentId: string | undefined) => {
|
||||
if (onAgentChange) {
|
||||
|
@ -429,7 +430,8 @@ export const ComposioRegistry: React.FC<ComposioRegistryProps> = ({
|
|||
return new Promise((resolve, reject) => {
|
||||
updateAgent({
|
||||
agentId: currentAgentId,
|
||||
custom_mcps: updatedCustomMcps
|
||||
custom_mcps: updatedCustomMcps,
|
||||
replace_mcps: true // Use replace mode to ensure proper updates
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(`Custom MCP "${customConfig.name}" added successfully`);
|
||||
|
|
|
@ -127,6 +127,7 @@ const MCPConfigurationItem: React.FC<{
|
|||
className="h-8 w-8 p-0"
|
||||
onClick={() => onConfigureTools(index)}
|
||||
title="Configure tools"
|
||||
type="button"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
|
@ -137,6 +138,7 @@ const MCPConfigurationItem: React.FC<{
|
|||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
onClick={() => onRemove(index)}
|
||||
title="Remove integration"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
@ -196,7 +198,7 @@ export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
|
|||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
className="bg-destructive hover:bg-destructive/90 text-white"
|
||||
>
|
||||
Remove Integration
|
||||
</AlertDialogAction>
|
||||
|
|
|
@ -407,15 +407,16 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
<DialogFooter className="flex-shrink-0 pt-4">
|
||||
{step === 'tools' ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleBack} disabled={isSaving}>
|
||||
<Button variant="outline" onClick={handleBack} disabled={isSaving} type="button">
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving} type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleToolsNext}
|
||||
disabled={selectedTools.size === 0 || isSaving}
|
||||
type="button"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
|
@ -429,12 +430,13 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={validateAndDiscoverTools}
|
||||
disabled={!configText.trim() || !manualServerName.trim() || isValidating}
|
||||
type="button"
|
||||
>
|
||||
{isValidating ? (
|
||||
<>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Zap, Server, Store } from 'lucide-react'
|
||||
import { Zap, Server, Store, Settings } from 'lucide-react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { MCPConfigurationProps, MCPConfiguration as MCPConfigurationType } from './types';
|
||||
import { ConfiguredMcpList } from './configured-mcp-list';
|
||||
|
@ -29,10 +29,6 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
const [selectedAgentId, setSelectedAgentId] = useState<string | undefined>(agentId);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedAgentId(agentId);
|
||||
}, [agentId]);
|
||||
|
||||
const handleAgentChange = (newAgentId: string | undefined) => {
|
||||
setSelectedAgentId(newAgentId);
|
||||
};
|
||||
|
@ -64,8 +60,7 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
};
|
||||
|
||||
const handleRemoveMCP = (index: number) => {
|
||||
const newMCPs = [...configuredMCPs];
|
||||
newMCPs.splice(index, 1);
|
||||
const newMCPs = configuredMCPs.filter((_, i) => i !== index);
|
||||
onConfigurationChange(newMCPs);
|
||||
};
|
||||
|
||||
|
@ -84,8 +79,9 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
|
||||
const handleToolsSelected = (profileId: string, selectedTools: string[], appName: string, appSlug: string) => {
|
||||
setShowRegistryDialog(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', selectedAgentId] });
|
||||
if (selectedAgentId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['agents', 'detail', selectedAgentId] });
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['composio', 'profiles'] });
|
||||
toast.success(`Connected ${appName} integration!`);
|
||||
};
|
||||
|
@ -107,11 +103,11 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => setShowRegistryDialog(true)} size="sm" variant="default" className="gap-2">
|
||||
<Button onClick={() => setShowRegistryDialog(true)} size="sm" variant="default" className="gap-2" type="button">
|
||||
<Store className="h-4 w-4" />
|
||||
Browse Apps
|
||||
</Button>
|
||||
<Button onClick={() => setShowCustomDialog(true)} size="sm" variant="outline" className="gap-2">
|
||||
<Button onClick={() => setShowCustomDialog(true)} size="sm" variant="outline" className="gap-2" type="button">
|
||||
<Server className="h-4 w-4" />
|
||||
Custom MCP
|
||||
</Button>
|
||||
|
@ -155,8 +151,9 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
onToolsSelected={handleToolsSelected}
|
||||
onClose={() => {
|
||||
setShowRegistryDialog(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', selectedAgentId] });
|
||||
if (selectedAgentId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['agents', 'detail', selectedAgentId] });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { createMutationHook } from '@/hooks/use-query';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AgentUpdateRequest, updateAgent } from './utils';
|
||||
import { agentKeys } from './keys';
|
||||
|
||||
export const useUpdateAgentMCPs = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return createMutationHook(
|
||||
({ agentId, ...data }: { agentId: string } & AgentUpdateRequest) =>
|
||||
updateAgent(agentId, data),
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.setQueryData(agentKeys.detail(variables.agentId), data);
|
||||
},
|
||||
}
|
||||
)();
|
||||
};
|
|
@ -163,6 +163,8 @@ export type AgentUpdateRequest = {
|
|||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
// MCP replacement flag
|
||||
replace_mcps?: boolean;
|
||||
};
|
||||
|
||||
export const getAgents = async (params: AgentsParams = {}): Promise<AgentsResponse> => {
|
||||
|
|
Loading…
Reference in New Issue