diff --git a/backend/agent/api.py b/backend/agent/api.py index a3db33ea..2187e0ee 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -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() diff --git a/backend/mcp_local/template_manager.py b/backend/mcp_local/template_manager.py index 4b270049..bfaec271 100644 --- a/backend/mcp_local/template_manager.py +++ b/backend/mcp_local/template_manager.py @@ -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() diff --git a/frontend/src/app/(dashboard)/agents/_components/mcp/configured-mcp-list.tsx b/frontend/src/app/(dashboard)/agents/_components/mcp/configured-mcp-list.tsx index d05681b9..5edd68a2 100644 --- a/frontend/src/app/(dashboard)/agents/_components/mcp/configured-mcp-list.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/mcp/configured-mcp-list.tsx @@ -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 ( @@ -50,7 +51,13 @@ const MCPConfigurationItem: React.FC<{ )} - {needsConfiguration && !mcp.isCustom && ( + {hasDirectConfig && !hasCredentialProfile && ( +
+ + Configured +
+ )} + {needsConfiguration && (
Needs config diff --git a/frontend/src/app/(dashboard)/agents/_components/update-agent-dialog.tsx b/frontend/src/app/(dashboard)/agents/_components/update-agent-dialog.tsx index 13e804d6..0c2ab953 100644 --- a/frontend/src/app/(dashboard)/agents/_components/update-agent-dialog.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/update-agent-dialog.tsx @@ -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.'); + } } }; diff --git a/frontend/src/app/(dashboard)/marketplace/page.tsx b/frontend/src/app/(dashboard)/marketplace/page.tsx index 372086eb..43faf088 100644 --- a/frontend/src/app/(dashboard)/marketplace/page.tsx +++ b/frontend/src/app/(dashboard)/marketplace/page.tsx @@ -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); diff --git a/frontend/src/app/(dashboard)/settings/credentials/page.tsx b/frontend/src/app/(dashboard)/settings/credentials/page.tsx index 85af035b..b4a44f5a 100644 --- a/frontend/src/app/(dashboard)/settings/credentials/page.tsx +++ b/frontend/src/app/(dashboard)/settings/credentials/page.tsx @@ -411,7 +411,7 @@ export default function CredentialsPage() {

diff --git a/frontend/src/components/workflows/CredentialProfileSelector.tsx b/frontend/src/components/workflows/CredentialProfileSelector.tsx index 26c8f971..ca588f78 100644 --- a/frontend/src/components/workflows/CredentialProfileSelector.tsx +++ b/frontend/src/components/workflows/CredentialProfileSelector.tsx @@ -352,8 +352,17 @@ export function CredentialProfileSelector({