mirror of https://github.com/kortix-ai/suna.git
Merge pull request #855 from escapade-mckv/ux-improvements
chore(ui): sync custom agents config with credentials profile
This commit is contained in:
commit
8a0a8f37b1
|
@ -1658,32 +1658,83 @@ async def update_agent(
|
|||
client = await db.client
|
||||
|
||||
try:
|
||||
# First verify the agent exists and belongs to the user, get with current version
|
||||
existing_agent = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq("agent_id", agent_id).eq("account_id", user_id).maybe_single().execute()
|
||||
|
||||
if not existing_agent.data:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
existing_data = existing_agent.data
|
||||
current_version_data = existing_data.get('agent_versions', {})
|
||||
current_version_data = existing_data.get('agent_versions')
|
||||
|
||||
if current_version_data is None:
|
||||
logger.info(f"Agent {agent_id} has no version data, creating initial version")
|
||||
try:
|
||||
initial_version_data = {
|
||||
"agent_id": agent_id,
|
||||
"version_number": 1,
|
||||
"version_name": "v1",
|
||||
"system_prompt": existing_data.get('system_prompt', ''),
|
||||
"configured_mcps": existing_data.get('configured_mcps', []),
|
||||
"custom_mcps": existing_data.get('custom_mcps', []),
|
||||
"agentpress_tools": existing_data.get('agentpress_tools', {}),
|
||||
"is_active": True,
|
||||
"created_by": user_id
|
||||
}
|
||||
|
||||
version_result = await client.table('agent_versions').insert(initial_version_data).execute()
|
||||
|
||||
if version_result.data:
|
||||
version_id = version_result.data[0]['version_id']
|
||||
|
||||
await client.table('agents').update({
|
||||
'current_version_id': version_id,
|
||||
'version_count': 1
|
||||
}).eq('agent_id', agent_id).execute()
|
||||
current_version_data = initial_version_data
|
||||
logger.info(f"Created initial version for agent {agent_id}")
|
||||
else:
|
||||
current_version_data = {
|
||||
'system_prompt': existing_data.get('system_prompt', ''),
|
||||
'configured_mcps': existing_data.get('configured_mcps', []),
|
||||
'custom_mcps': existing_data.get('custom_mcps', []),
|
||||
'agentpress_tools': existing_data.get('agentpress_tools', {})
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create initial version for agent {agent_id}: {e}")
|
||||
current_version_data = {
|
||||
'system_prompt': existing_data.get('system_prompt', ''),
|
||||
'configured_mcps': existing_data.get('configured_mcps', []),
|
||||
'custom_mcps': existing_data.get('custom_mcps', []),
|
||||
'agentpress_tools': existing_data.get('agentpress_tools', {})
|
||||
}
|
||||
|
||||
# Check if we need to create a new version (if system prompt, tools, or MCPs are changing)
|
||||
needs_new_version = False
|
||||
version_changes = {}
|
||||
|
||||
if agent_data.system_prompt is not None and agent_data.system_prompt != current_version_data.get('system_prompt'):
|
||||
def values_different(new_val, old_val):
|
||||
if new_val is None:
|
||||
return False
|
||||
import json
|
||||
try:
|
||||
new_json = json.dumps(new_val, sort_keys=True) if new_val is not None else None
|
||||
old_json = json.dumps(old_val, sort_keys=True) if old_val is not None else None
|
||||
return new_json != old_json
|
||||
except (TypeError, ValueError):
|
||||
return new_val != old_val
|
||||
|
||||
if values_different(agent_data.system_prompt, current_version_data.get('system_prompt')):
|
||||
needs_new_version = True
|
||||
version_changes['system_prompt'] = agent_data.system_prompt
|
||||
|
||||
if agent_data.configured_mcps is not None and agent_data.configured_mcps != current_version_data.get('configured_mcps', []):
|
||||
if values_different(agent_data.configured_mcps, current_version_data.get('configured_mcps', [])):
|
||||
needs_new_version = True
|
||||
version_changes['configured_mcps'] = agent_data.configured_mcps
|
||||
|
||||
if agent_data.custom_mcps is not None and agent_data.custom_mcps != current_version_data.get('custom_mcps', []):
|
||||
if values_different(agent_data.custom_mcps, current_version_data.get('custom_mcps', [])):
|
||||
needs_new_version = True
|
||||
version_changes['custom_mcps'] = agent_data.custom_mcps
|
||||
|
||||
if agent_data.agentpress_tools is not None and agent_data.agentpress_tools != current_version_data.get('agentpress_tools', {}):
|
||||
if values_different(agent_data.agentpress_tools, current_version_data.get('agentpress_tools', {})):
|
||||
needs_new_version = True
|
||||
version_changes['agentpress_tools'] = agent_data.agentpress_tools
|
||||
|
||||
|
@ -1716,49 +1767,69 @@ async def update_agent(
|
|||
# Create new version if needed
|
||||
new_version_id = None
|
||||
if needs_new_version:
|
||||
# Get next version number
|
||||
versions_result = await client.table('agent_versions').select('version_number').eq('agent_id', agent_id).order('version_number', desc=True).limit(1).execute()
|
||||
next_version_number = 1
|
||||
if versions_result.data:
|
||||
next_version_number = versions_result.data[0]['version_number'] + 1
|
||||
|
||||
# Create new version with current data merged with changes
|
||||
new_version_data = {
|
||||
"agent_id": agent_id,
|
||||
"version_number": next_version_number,
|
||||
"version_name": f"v{next_version_number}",
|
||||
"system_prompt": version_changes.get('system_prompt', current_version_data.get('system_prompt')),
|
||||
"configured_mcps": version_changes.get('configured_mcps', current_version_data.get('configured_mcps', [])),
|
||||
"custom_mcps": version_changes.get('custom_mcps', current_version_data.get('custom_mcps', [])),
|
||||
"agentpress_tools": version_changes.get('agentpress_tools', current_version_data.get('agentpress_tools', {})),
|
||||
"is_active": True,
|
||||
"created_by": user_id
|
||||
}
|
||||
|
||||
new_version = await client.table('agent_versions').insert(new_version_data).execute()
|
||||
|
||||
if new_version.data:
|
||||
try:
|
||||
# Get next version number
|
||||
versions_result = await client.table('agent_versions').select('version_number').eq('agent_id', agent_id).order('version_number', desc=True).limit(1).execute()
|
||||
next_version_number = 1
|
||||
if versions_result.data:
|
||||
next_version_number = versions_result.data[0]['version_number'] + 1
|
||||
|
||||
# Validate version data before creating
|
||||
new_version_data = {
|
||||
"agent_id": agent_id,
|
||||
"version_number": next_version_number,
|
||||
"version_name": f"v{next_version_number}",
|
||||
"system_prompt": version_changes.get('system_prompt', current_version_data.get('system_prompt', '')),
|
||||
"configured_mcps": version_changes.get('configured_mcps', current_version_data.get('configured_mcps', [])),
|
||||
"custom_mcps": version_changes.get('custom_mcps', current_version_data.get('custom_mcps', [])),
|
||||
"agentpress_tools": version_changes.get('agentpress_tools', current_version_data.get('agentpress_tools', {})),
|
||||
"is_active": True,
|
||||
"created_by": user_id
|
||||
}
|
||||
|
||||
# Validate system prompt is not empty
|
||||
if not new_version_data["system_prompt"] or new_version_data["system_prompt"].strip() == '':
|
||||
raise HTTPException(status_code=400, detail="System prompt cannot be empty")
|
||||
|
||||
new_version = await client.table('agent_versions').insert(new_version_data).execute()
|
||||
|
||||
if not new_version.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to create new agent version")
|
||||
|
||||
new_version_id = new_version.data[0]['version_id']
|
||||
update_data['current_version_id'] = new_version_id
|
||||
update_data['version_count'] = next_version_number
|
||||
|
||||
# Add version history entry
|
||||
await client.table('agent_version_history').insert({
|
||||
"agent_id": agent_id,
|
||||
"version_id": new_version_id,
|
||||
"action": "created",
|
||||
"changed_by": user_id,
|
||||
"change_description": f"New version v{next_version_number} created from update"
|
||||
}).execute()
|
||||
try:
|
||||
await client.table('agent_version_history').insert({
|
||||
"agent_id": agent_id,
|
||||
"version_id": new_version_id,
|
||||
"action": "created",
|
||||
"changed_by": user_id,
|
||||
"change_description": f"New version v{next_version_number} created from update"
|
||||
}).execute()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create version history entry: {e}")
|
||||
|
||||
logger.info(f"Created new version v{next_version_number} for agent {agent_id}")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating new version for agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create new agent version: {str(e)}")
|
||||
|
||||
# Update the agent if there are changes
|
||||
if update_data:
|
||||
update_result = await client.table('agents').update(update_data).eq("agent_id", agent_id).eq("account_id", user_id).execute()
|
||||
|
||||
if not update_result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to update agent")
|
||||
try:
|
||||
update_result = await client.table('agents').update(update_data).eq("agent_id", agent_id).eq("account_id", user_id).execute()
|
||||
|
||||
if not update_result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to update agent - no rows affected")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update agent: {str(e)}")
|
||||
|
||||
# Fetch the updated agent data with version info
|
||||
updated_agent = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq("agent_id", agent_id).eq("account_id", user_id).maybe_single().execute()
|
||||
|
|
|
@ -338,26 +338,37 @@ class TemplateManager:
|
|||
if profile_mappings and req.qualified_name in profile_mappings:
|
||||
profile_id = profile_mappings[req.qualified_name]
|
||||
|
||||
# Validate profile_id is not empty
|
||||
if not profile_id or profile_id.strip() == '':
|
||||
logger.error(f"Empty profile_id provided for {req.qualified_name}")
|
||||
raise ValueError(f"Invalid credential profile selected for {req.display_name}")
|
||||
|
||||
# Get the credential profile
|
||||
profile = await credential_manager.get_credential_by_profile(
|
||||
account_id, profile_id
|
||||
)
|
||||
|
||||
if not profile:
|
||||
logger.warning(f"Credential profile not found for {req.qualified_name}")
|
||||
continue
|
||||
logger.error(f"Credential profile {profile_id} not found for {req.qualified_name}")
|
||||
raise ValueError(f"Credential profile not found for {req.display_name}. Please select a valid profile or create a new one.")
|
||||
|
||||
# Validate profile is active
|
||||
if not profile.is_active:
|
||||
logger.error(f"Credential profile {profile_id} is inactive for {req.qualified_name}")
|
||||
raise ValueError(f"Selected credential profile for {req.display_name} is inactive. Please select an active profile.")
|
||||
|
||||
mcp_config = {
|
||||
'name': req.display_name,
|
||||
'qualifiedName': req.qualified_name,
|
||||
'config': profile.config,
|
||||
'enabledTools': req.enabled_tools
|
||||
'enabledTools': req.enabled_tools,
|
||||
'selectedProfileId': profile_id
|
||||
}
|
||||
configured_mcps.append(mcp_config)
|
||||
logger.info(f"Added regular MCP with profile: {mcp_config}")
|
||||
else:
|
||||
logger.warning(f"No profile mapping provided for {req.qualified_name}")
|
||||
continue
|
||||
logger.error(f"No profile mapping provided for {req.qualified_name}")
|
||||
raise ValueError(f"Missing credential profile for {req.display_name}. Please select a credential profile.")
|
||||
|
||||
logger.info(f"Final configured_mcps: {configured_mcps}")
|
||||
logger.info(f"Final custom_mcps: {custom_mcps}")
|
||||
|
@ -381,8 +392,42 @@ class TemplateManager:
|
|||
raise ValueError("Failed to create agent")
|
||||
|
||||
instance_id = result.data[0]['agent_id']
|
||||
|
||||
# Update template download count
|
||||
|
||||
try:
|
||||
initial_version_data = {
|
||||
"agent_id": instance_id,
|
||||
"version_number": 1,
|
||||
"version_name": "v1",
|
||||
"system_prompt": agent_data['system_prompt'],
|
||||
"configured_mcps": agent_data['configured_mcps'],
|
||||
"custom_mcps": agent_data['custom_mcps'],
|
||||
"agentpress_tools": agent_data['agentpress_tools'],
|
||||
"is_active": True,
|
||||
"created_by": account_id
|
||||
}
|
||||
|
||||
version_result = await client.table('agent_versions').insert(initial_version_data).execute()
|
||||
|
||||
if version_result.data:
|
||||
version_id = version_result.data[0]['version_id']
|
||||
await client.table('agents').update({
|
||||
'current_version_id': version_id,
|
||||
'version_count': 1
|
||||
}).eq('agent_id', instance_id).execute()
|
||||
await client.table('agent_version_history').insert({
|
||||
"agent_id": instance_id,
|
||||
"version_id": version_id,
|
||||
"action": "created",
|
||||
"changed_by": account_id,
|
||||
"change_description": "Initial version created from template installation"
|
||||
}).execute()
|
||||
|
||||
logger.info(f"Created initial version v1 for installed agent {instance_id}")
|
||||
else:
|
||||
logger.warning(f"Failed to create initial version for agent {instance_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create initial version for agent {instance_id}: {e}")
|
||||
await client.table('agent_templates')\
|
||||
.update({'download_count': template.download_count + 1})\
|
||||
.eq('template_id', template_id).execute()
|
||||
|
|
|
@ -21,8 +21,9 @@ const MCPConfigurationItem: React.FC<{
|
|||
const { data: profiles = [] } = useCredentialProfilesForMcp(mcp.qualifiedName);
|
||||
const selectedProfile = profiles.find(p => p.profile_id === mcp.selectedProfileId);
|
||||
|
||||
const hasDirectConfig = mcp.config && Object.keys(mcp.config).length > 0;
|
||||
const hasCredentialProfile = !!mcp.selectedProfileId && !!selectedProfile;
|
||||
const needsConfiguration = !hasCredentialProfile;
|
||||
const needsConfiguration = !hasCredentialProfile && !hasDirectConfig && !mcp.isCustom;
|
||||
|
||||
return (
|
||||
<Card className="p-3">
|
||||
|
@ -50,7 +51,13 @@ const MCPConfigurationItem: React.FC<{
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{needsConfiguration && !mcp.isCustom && (
|
||||
{hasDirectConfig && !hasCredentialProfile && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Key className="h-3 w-3 text-green-600" />
|
||||
<span className="text-green-600 font-medium">Configured</span>
|
||||
</div>
|
||||
)}
|
||||
{needsConfiguration && (
|
||||
<div className="flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3 text-amber-600" />
|
||||
<span className="text-amber-600">Needs config</span>
|
||||
|
|
|
@ -156,18 +156,38 @@ export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdate
|
|||
return;
|
||||
}
|
||||
|
||||
if (!agentId) return;
|
||||
if (!agentId) {
|
||||
toast.error('Invalid agent ID');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAgentMutation.mutateAsync({
|
||||
agentId,
|
||||
...formData
|
||||
});
|
||||
|
||||
toast.success('Agent updated successfully!');
|
||||
onOpenChange(false);
|
||||
onAgentUpdated?.();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Error updating agent:', error);
|
||||
// Error handling is managed by the mutation hook
|
||||
|
||||
if (error.message?.includes('System prompt cannot be empty')) {
|
||||
toast.error('System prompt cannot be empty');
|
||||
} else if (error.message?.includes('Failed to create new agent version')) {
|
||||
toast.error('Failed to create new version. Please try again.');
|
||||
} else if (error.message?.includes('Failed to update agent')) {
|
||||
toast.error('Failed to update agent. Please check your configuration and try again.');
|
||||
} else if (error.message?.includes('Agent not found')) {
|
||||
toast.error('Agent not found. It may have been deleted.');
|
||||
onOpenChange(false);
|
||||
} else if (error.message?.includes('Access denied')) {
|
||||
toast.error('You do not have permission to update this agent.');
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
toast.error(error.message || 'Failed to update agent. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -714,6 +714,34 @@ export default function MarketplacePage() {
|
|||
setInstallingItemId(item.id);
|
||||
|
||||
try {
|
||||
if (!instanceName || instanceName.trim() === '') {
|
||||
toast.error('Please provide a name for the agent');
|
||||
return;
|
||||
}
|
||||
|
||||
const regularRequirements = item.mcp_requirements?.filter(req => !req.custom_type) || [];
|
||||
const missingProfiles = regularRequirements.filter(req =>
|
||||
!profileMappings || !profileMappings[req.qualified_name] || profileMappings[req.qualified_name].trim() === ''
|
||||
);
|
||||
|
||||
if (missingProfiles.length > 0) {
|
||||
const missingNames = missingProfiles.map(req => req.display_name).join(', ');
|
||||
toast.error(`Please select credential profiles for: ${missingNames}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const customRequirements = item.mcp_requirements?.filter(req => req.custom_type) || [];
|
||||
const missingCustomConfigs = customRequirements.filter(req =>
|
||||
!customMcpConfigs || !customMcpConfigs[req.qualified_name] ||
|
||||
req.required_config.some(field => !customMcpConfigs[req.qualified_name][field]?.trim())
|
||||
);
|
||||
|
||||
if (missingCustomConfigs.length > 0) {
|
||||
const missingNames = missingCustomConfigs.map(req => req.display_name).join(', ');
|
||||
toast.error(`Please provide all required configuration for: ${missingNames}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await installTemplateMutation.mutateAsync({
|
||||
template_id: item.template_id,
|
||||
instance_name: instanceName,
|
||||
|
@ -722,17 +750,35 @@ export default function MarketplacePage() {
|
|||
});
|
||||
|
||||
if (result.status === 'installed') {
|
||||
toast.success('Agent installed successfully!');
|
||||
toast.success(`Agent "${instanceName}" installed successfully!`);
|
||||
setShowInstallDialog(false);
|
||||
} else if (result.status === 'configs_required') {
|
||||
toast.error('Please provide all required configurations');
|
||||
return;
|
||||
} else {
|
||||
toast.error('Unexpected response from server. Please try again.');
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Installation error:', error);
|
||||
|
||||
// Handle specific error types
|
||||
if (error.message?.includes('already in your library')) {
|
||||
toast.error('This agent is already in your library');
|
||||
} else if (error.message?.includes('Credential profile not found')) {
|
||||
toast.error('One or more selected credential profiles could not be found. Please refresh and try again.');
|
||||
} else if (error.message?.includes('Missing credential profile')) {
|
||||
toast.error('Please select credential profiles for all required services');
|
||||
} else if (error.message?.includes('Invalid credential profile')) {
|
||||
toast.error('One or more selected credential profiles are invalid. Please select valid profiles.');
|
||||
} else if (error.message?.includes('inactive')) {
|
||||
toast.error('One or more selected credential profiles are inactive. Please select active profiles.');
|
||||
} else if (error.message?.includes('Template not found')) {
|
||||
toast.error('This agent template is no longer available');
|
||||
} else if (error.message?.includes('Access denied')) {
|
||||
toast.error('You do not have permission to install this agent');
|
||||
} else {
|
||||
toast.error(error.message || 'Failed to install agent');
|
||||
toast.error(error.message || 'Failed to install agent. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setInstallingItemId(null);
|
||||
|
|
|
@ -411,7 +411,7 @@ export default function CredentialsPage() {
|
|||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowAddDialog(true)} className="h-9">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
<Plus className="h-4 w-4" />
|
||||
Create First Profile
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -352,8 +352,17 @@ export function CredentialProfileSelector({
|
|||
<Select
|
||||
value={selectedProfileId || ''}
|
||||
onValueChange={(value) => {
|
||||
const profile = profiles.find(p => p.profile_id === value);
|
||||
onProfileSelect(value || null, profile || null);
|
||||
if (value && value.trim() !== '') {
|
||||
const profile = profiles.find(p => p.profile_id === value);
|
||||
if (profile) {
|
||||
onProfileSelect(value, profile);
|
||||
} else {
|
||||
console.error('Selected profile not found:', value);
|
||||
toast.error('Selected profile not found. Please refresh and try again.');
|
||||
}
|
||||
} else {
|
||||
onProfileSelect(null, null);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue