chore: auto save integrations

This commit is contained in:
Saumya 2025-08-27 00:00:44 +05:30
parent f713c51daf
commit d64f51ada4
10 changed files with 123 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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`);

View File

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

View File

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

View File

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

View File

@ -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);
},
}
)();
};

View File

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