diff --git a/backend/agent/api.py b/backend/agent/api.py index 4f051a14..4a793f1f 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -56,6 +56,26 @@ class AgentCreateRequest(BaseModel): avatar: Optional[str] = None avatar_color: Optional[str] = None +class AgentVersionResponse(BaseModel): + version_id: str + agent_id: str + version_number: int + version_name: str + system_prompt: str + configured_mcps: List[Dict[str, Any]] + custom_mcps: List[Dict[str, Any]] + agentpress_tools: Dict[str, Any] + is_active: bool + created_at: str + updated_at: str + created_by: Optional[str] = None + +class AgentVersionCreateRequest(BaseModel): + system_prompt: str + configured_mcps: Optional[List[Dict[str, Any]]] = [] + custom_mcps: Optional[List[Dict[str, Any]]] = [] + agentpress_tools: Optional[Dict[str, Any]] = {} + class AgentUpdateRequest(BaseModel): name: Optional[str] = None description: Optional[str] = None @@ -71,20 +91,23 @@ class AgentResponse(BaseModel): agent_id: str account_id: str name: str - description: Optional[str] + description: Optional[str] = None system_prompt: str configured_mcps: List[Dict[str, Any]] - custom_mcps: Optional[List[Dict[str, Any]]] = [] + custom_mcps: List[Dict[str, Any]] agentpress_tools: Dict[str, Any] is_default: bool + avatar: Optional[str] = None + avatar_color: Optional[str] = None + created_at: str + updated_at: Optional[str] = None is_public: Optional[bool] = False marketplace_published_at: Optional[str] = None download_count: Optional[int] = 0 tags: Optional[List[str]] = [] - avatar: Optional[str] - avatar_color: Optional[str] - created_at: str - updated_at: str + current_version_id: Optional[str] = None + version_count: Optional[int] = 1 + current_version: Optional[AgentVersionResponse] = None class PaginationInfo(BaseModel): page: int @@ -387,12 +410,13 @@ async def start_agent( if is_agent_builder: logger.info(f"Thread {thread_id} is in agent builder mode, target_agent_id: {target_agent_id}") - # Load agent configuration + # Load agent configuration with version support agent_config = None effective_agent_id = body.agent_id or thread_agent_id # Use provided agent_id or the one stored in thread if effective_agent_id: - agent_result = await client.table('agents').select('*').eq('agent_id', effective_agent_id).eq('account_id', account_id).execute() + # Get agent with current version + agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('agent_id', effective_agent_id).eq('account_id', account_id).execute() if not agent_result.data: if body.agent_id: raise HTTPException(status_code=404, detail="Agent not found or access denied") @@ -400,16 +424,53 @@ async def start_agent( logger.warning(f"Stored agent_id {effective_agent_id} not found, falling back to default") effective_agent_id = None else: - agent_config = agent_result.data[0] + agent_data = agent_result.data[0] + # Use version data if available, otherwise fall back to agent data (for backward compatibility) + if agent_data.get('agent_versions'): + version_data = agent_data['agent_versions'] + agent_config = { + 'agent_id': agent_data['agent_id'], + 'name': agent_data['name'], + 'description': agent_data.get('description'), + 'system_prompt': version_data['system_prompt'], + 'configured_mcps': version_data.get('configured_mcps', []), + 'custom_mcps': version_data.get('custom_mcps', []), + 'agentpress_tools': version_data.get('agentpress_tools', {}), + 'is_default': agent_data.get('is_default', False), + 'current_version_id': agent_data.get('current_version_id'), + 'version_name': version_data.get('version_name', 'v1') + } + logger.info(f"Using agent {agent_config['name']} ({effective_agent_id}) version {agent_config['version_name']}") + else: + # Backward compatibility - use agent data directly + agent_config = agent_data + logger.info(f"Using agent {agent_config['name']} ({effective_agent_id}) - no version data") source = "request" if body.agent_id else "thread" - logger.info(f"Using agent from {source}: {agent_config['name']} ({effective_agent_id})") # If no agent found yet, try to get default agent for the account if not agent_config: - default_agent_result = await client.table('agents').select('*').eq('account_id', account_id).eq('is_default', True).execute() + default_agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('account_id', account_id).eq('is_default', True).execute() if default_agent_result.data: - agent_config = default_agent_result.data[0] - logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']})") + agent_data = default_agent_result.data[0] + # Use version data if available + if agent_data.get('agent_versions'): + version_data = agent_data['agent_versions'] + agent_config = { + 'agent_id': agent_data['agent_id'], + 'name': agent_data['name'], + 'description': agent_data.get('description'), + 'system_prompt': version_data['system_prompt'], + 'configured_mcps': version_data.get('configured_mcps', []), + 'custom_mcps': version_data.get('custom_mcps', []), + 'agentpress_tools': version_data.get('agentpress_tools', {}), + 'is_default': agent_data.get('is_default', False), + 'current_version_id': agent_data.get('current_version_id'), + 'version_name': version_data.get('version_name', 'v1') + } + logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) version {agent_config['version_name']}") + else: + agent_config = agent_data + logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) - no version data") # Update thread's agent_id if a different agent was explicitly requested if body.agent_id and body.agent_id != thread_agent_id and agent_config: @@ -1092,8 +1153,8 @@ async def get_agents( # Calculate offset offset = (page - 1) * limit - # Start building the query - query = client.table('agents').select('*', count='exact').eq("account_id", user_id) + # Start building the query - include version data + query = client.table('agents').select('*, agent_versions!current_version_id(*)', count='exact').eq("account_id", user_id) # Apply search filter if search: @@ -1206,6 +1267,24 @@ async def get_agents( # Format the response agent_list = [] for agent in agents_data: + current_version = None + if agent.get('agent_versions'): + version_data = agent['agent_versions'] + current_version = AgentVersionResponse( + version_id=version_data['version_id'], + agent_id=version_data['agent_id'], + version_number=version_data['version_number'], + version_name=version_data['version_name'], + system_prompt=version_data['system_prompt'], + configured_mcps=version_data.get('configured_mcps', []), + custom_mcps=version_data.get('custom_mcps', []), + agentpress_tools=version_data.get('agentpress_tools', {}), + is_active=version_data.get('is_active', True), + created_at=version_data['created_at'], + updated_at=version_data.get('updated_at', version_data['created_at']), + created_by=version_data.get('created_by') + ) + agent_list.append(AgentResponse( agent_id=agent['agent_id'], account_id=agent['account_id'], @@ -1223,7 +1302,10 @@ async def get_agents( avatar=agent.get('avatar'), avatar_color=agent.get('avatar_color'), created_at=agent['created_at'], - updated_at=agent['updated_at'] + updated_at=agent['updated_at'], + current_version_id=agent.get('current_version_id'), + version_count=agent.get('version_count', 1), + current_version=current_version )) total_pages = (total_count + limit - 1) // limit @@ -1245,7 +1327,7 @@ async def get_agents( @router.get("/agents/{agent_id}", response_model=AgentResponse) async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)): - """Get a specific agent by ID. Only the owner can access non-public agents.""" + """Get a specific agent by ID with current version information. Only the owner can access non-public agents.""" if not await is_enabled("custom_agents"): raise HTTPException( status_code=403, @@ -1256,8 +1338,8 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr client = await db.client try: - # Get agent with access check - only owner or public agents - agent = await client.table('agents').select('*').eq("agent_id", agent_id).execute() + # Get agent with current version data + agent = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq("agent_id", agent_id).execute() if not agent.data: raise HTTPException(status_code=404, detail="Agent not found") @@ -1268,6 +1350,25 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr if agent_data['account_id'] != user_id and not agent_data.get('is_public', False): raise HTTPException(status_code=403, detail="Access denied") + # Prepare current version data + current_version = None + if agent_data.get('agent_versions'): + version_data = agent_data['agent_versions'] + current_version = AgentVersionResponse( + version_id=version_data['version_id'], + agent_id=version_data['agent_id'], + version_number=version_data['version_number'], + version_name=version_data['version_name'], + system_prompt=version_data['system_prompt'], + configured_mcps=version_data.get('configured_mcps', []), + custom_mcps=version_data.get('custom_mcps', []), + agentpress_tools=version_data.get('agentpress_tools', {}), + is_active=version_data.get('is_active', True), + created_at=version_data['created_at'], + updated_at=version_data.get('updated_at', version_data['created_at']), + created_by=version_data.get('created_by') + ) + return AgentResponse( agent_id=agent_data['agent_id'], account_id=agent_data['account_id'], @@ -1285,7 +1386,10 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr avatar=agent_data.get('avatar'), avatar_color=agent_data.get('avatar_color'), created_at=agent_data['created_at'], - updated_at=agent_data['updated_at'] + updated_at=agent_data.get('updated_at', agent_data['created_at']), + current_version_id=agent_data.get('current_version_id'), + version_count=agent_data.get('version_count', 1), + current_version=current_version ) except HTTPException: @@ -1299,7 +1403,7 @@ async def create_agent( agent_data: AgentCreateRequest, user_id: str = Depends(get_current_user_id_from_jwt) ): - """Create a new agent.""" + """Create a new agent with automatic v1 version.""" logger.info(f"Creating new agent for user: {user_id}") if not await is_enabled("custom_agents"): raise HTTPException( @@ -1313,12 +1417,7 @@ async def create_agent( if agent_data.is_default: await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute() - # enhanced_system_prompt = await enhance_system_prompt( - # agent_name=agent_data.name, - # description=agent_data.description or "", - # user_system_prompt=agent_data.system_prompt - # ) - + # Create the agent insert_data = { "account_id": user_id, "name": agent_data.name, @@ -1329,7 +1428,8 @@ async def create_agent( "agentpress_tools": agent_data.agentpress_tools or {}, "is_default": agent_data.is_default or False, "avatar": agent_data.avatar, - "avatar_color": agent_data.avatar_color + "avatar_color": agent_data.avatar_color, + "version_count": 1 } new_agent = await client.table('agents').insert(insert_data).execute() @@ -1338,7 +1438,42 @@ async def create_agent( raise HTTPException(status_code=500, detail="Failed to create agent") agent = new_agent.data[0] - logger.info(f"Created agent {agent['agent_id']} for user: {user_id}") + + # Create v1 version automatically + version_data = { + "agent_id": agent['agent_id'], + "version_number": 1, + "version_name": "v1", + "system_prompt": agent_data.system_prompt, + "configured_mcps": agent_data.configured_mcps or [], + "custom_mcps": agent_data.custom_mcps or [], + "agentpress_tools": agent_data.agentpress_tools or {}, + "is_active": True, + "created_by": user_id + } + + new_version = await client.table('agent_versions').insert(version_data).execute() + + if new_version.data: + version = new_version.data[0] + # Update agent with current version + await client.table('agents').update({ + "current_version_id": version['version_id'] + }).eq("agent_id", agent['agent_id']).execute() + + # Add version history entry + await client.table('agent_version_history').insert({ + "agent_id": agent['agent_id'], + "version_id": version['version_id'], + "action": "created", + "changed_by": user_id, + "change_description": "Initial version v1 created" + }).execute() + + agent['current_version_id'] = version['version_id'] + agent['current_version'] = version + + logger.info(f"Created agent {agent['agent_id']} with v1 for user: {user_id}") return AgentResponse( agent_id=agent['agent_id'], @@ -1357,7 +1492,10 @@ async def create_agent( avatar=agent.get('avatar'), avatar_color=agent.get('avatar_color'), created_at=agent['created_at'], - updated_at=agent['updated_at'] + updated_at=agent.get('updated_at', agent['created_at']), + current_version_id=agent.get('current_version_id'), + version_count=agent.get('version_count', 1), + current_version=agent.get('current_version') ) except HTTPException: @@ -1372,7 +1510,7 @@ async def update_agent( agent_data: AgentUpdateRequest, user_id: str = Depends(get_current_user_id_from_jwt) ): - """Update an existing agent.""" + """Update an existing agent. Creates a new version if system prompt, tools, or MCPs are changed.""" if not await is_enabled("custom_agents"): raise HTTPException( status_code=403, @@ -1382,34 +1520,41 @@ async def update_agent( client = await db.client try: - # First verify the agent exists and belongs to the user - existing_agent = await client.table('agents').select('*').eq("agent_id", agent_id).eq("account_id", user_id).maybe_single().execute() + # 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', {}) - # Prepare update data (only include fields that are not None) + # 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'): + 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', []): + 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', []): + 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', {}): + needs_new_version = True + version_changes['agentpress_tools'] = agent_data.agentpress_tools + + # Prepare update data for agent metadata (non-versioned fields) update_data = {} if agent_data.name is not None: update_data["name"] = agent_data.name if agent_data.description is not None: update_data["description"] = agent_data.description - if agent_data.system_prompt is not None: - # Enhance the system prompt using GPT-4o if it's being updated - # enhanced_system_prompt = await enhance_system_prompt( - # agent_name=agent_data.name or existing_data['name'], - # description=agent_data.description or existing_data.get('description', ''), - # user_system_prompt=agent_data.system_prompt - # ) - update_data["system_prompt"] = agent_data.system_prompt - if agent_data.configured_mcps is not None: - update_data["configured_mcps"] = agent_data.configured_mcps - if agent_data.custom_mcps is not None: - update_data["custom_mcps"] = agent_data.custom_mcps - if agent_data.agentpress_tools is not None: - update_data["agentpress_tools"] = agent_data.agentpress_tools if agent_data.is_default is not None: update_data["is_default"] = agent_data.is_default # If setting as default, unset other defaults first @@ -1420,23 +1565,89 @@ async def update_agent( if agent_data.avatar_color is not None: update_data["avatar_color"] = agent_data.avatar_color - if not update_data: - # No fields to update, return existing agent - agent = existing_agent.data - else: - # Update the agent + # Also update the agent table with the latest values (for backward compatibility) + if agent_data.system_prompt is not None: + update_data["system_prompt"] = agent_data.system_prompt + if agent_data.configured_mcps is not None: + update_data["configured_mcps"] = agent_data.configured_mcps + if agent_data.custom_mcps is not None: + update_data["custom_mcps"] = agent_data.custom_mcps + if agent_data.agentpress_tools is not None: + update_data["agentpress_tools"] = agent_data.agentpress_tools + + # 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: + 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() + + logger.info(f"Created new version v{next_version_number} for agent {agent_id}") + + # 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") - - # Fetch the updated agent data - updated_agent = await client.table('agents').select('*').eq("agent_id", agent_id).eq("account_id", user_id).maybe_single().execute() - - if not updated_agent.data: - raise HTTPException(status_code=500, detail="Failed to fetch updated agent") - - agent = updated_agent.data + + # 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() + + if not updated_agent.data: + raise HTTPException(status_code=500, detail="Failed to fetch updated agent") + + agent = updated_agent.data + + # Prepare current version response + current_version = None + if agent.get('agent_versions'): + version_data = agent['agent_versions'] + current_version = AgentVersionResponse( + version_id=version_data['version_id'], + agent_id=version_data['agent_id'], + version_number=version_data['version_number'], + version_name=version_data['version_name'], + system_prompt=version_data['system_prompt'], + configured_mcps=version_data.get('configured_mcps', []), + custom_mcps=version_data.get('custom_mcps', []), + agentpress_tools=version_data.get('agentpress_tools', {}), + is_active=version_data.get('is_active', True), + created_at=version_data['created_at'], + updated_at=version_data.get('updated_at', version_data['created_at']), + created_by=version_data.get('created_by') + ) logger.info(f"Updated agent {agent_id} for user: {user_id}") @@ -1457,7 +1668,10 @@ async def update_agent( avatar=agent.get('avatar'), avatar_color=agent.get('avatar_color'), created_at=agent['created_at'], - updated_at=agent['updated_at'] + updated_at=agent.get('updated_at', agent['created_at']), + current_version_id=agent.get('current_version_id'), + version_count=agent.get('version_count', 1), + current_version=current_version ) except HTTPException: @@ -1820,3 +2034,147 @@ async def get_agent_builder_chat_history( except Exception as e: logger.error(f"Error fetching agent builder chat history for agent {agent_id}: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to fetch chat history: {str(e)}") + +@router.get("/agents/{agent_id}/versions", response_model=List[AgentVersionResponse]) +async def get_agent_versions( + agent_id: str, + user_id: str = Depends(get_current_user_id_from_jwt) +): + """Get all versions of an agent.""" + client = await db.client + + # Check if user has access to this agent + agent_result = await client.table('agents').select("*").eq("agent_id", agent_id).execute() + if not agent_result.data: + raise HTTPException(status_code=404, detail="Agent not found") + + agent = agent_result.data[0] + if agent['account_id'] != user_id and not agent.get('is_public', False): + raise HTTPException(status_code=403, detail="Access denied") + + # Get all versions + versions_result = await client.table('agent_versions').select("*").eq("agent_id", agent_id).order("version_number", desc=True).execute() + + return versions_result.data + +@router.post("/agents/{agent_id}/versions", response_model=AgentVersionResponse) +async def create_agent_version( + agent_id: str, + version_data: AgentVersionCreateRequest, + user_id: str = Depends(get_current_user_id_from_jwt) +): + """Create a new version of an agent.""" + client = await db.client + + # Check if user owns this agent + agent_result = await client.table('agents').select("*").eq("agent_id", agent_id).eq("account_id", user_id).execute() + if not agent_result.data: + raise HTTPException(status_code=404, detail="Agent not found or access denied") + + agent = agent_result.data[0] + + # 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 + new_version_data = { + "agent_id": agent_id, + "version_number": next_version_number, + "version_name": f"v{next_version_number}", + "system_prompt": version_data.system_prompt, + "configured_mcps": version_data.configured_mcps or [], + "custom_mcps": version_data.custom_mcps or [], + "agentpress_tools": version_data.agentpress_tools or {}, + "is_active": True, + "created_by": user_id + } + + 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 version") + + version = new_version.data[0] + + # Update agent with new version + await client.table('agents').update({ + "current_version_id": version['version_id'], + "version_count": next_version_number + }).eq("agent_id", agent_id).execute() + + # Add version history entry + await client.table('agent_version_history').insert({ + "agent_id": agent_id, + "version_id": version['version_id'], + "action": "created", + "changed_by": user_id, + "change_description": f"New version v{next_version_number} created" + }).execute() + + logger.info(f"Created version v{next_version_number} for agent {agent_id}") + + return version + +@router.put("/agents/{agent_id}/versions/{version_id}/activate") +async def activate_agent_version( + agent_id: str, + version_id: str, + user_id: str = Depends(get_current_user_id_from_jwt) +): + """Switch agent to use a specific version.""" + client = await db.client + + # Check if user owns this agent + agent_result = await client.table('agents').select("*").eq("agent_id", agent_id).eq("account_id", user_id).execute() + if not agent_result.data: + raise HTTPException(status_code=404, detail="Agent not found or access denied") + + # Check if version exists + version_result = await client.table('agent_versions').select("*").eq("version_id", version_id).eq("agent_id", agent_id).execute() + if not version_result.data: + raise HTTPException(status_code=404, detail="Version not found") + + # Update agent's current version + await client.table('agents').update({ + "current_version_id": version_id + }).eq("agent_id", agent_id).execute() + + # Add version history entry + await client.table('agent_version_history').insert({ + "agent_id": agent_id, + "version_id": version_id, + "action": "activated", + "changed_by": user_id, + "change_description": f"Switched to version {version_result.data[0]['version_name']}" + }).execute() + + return {"message": "Version activated successfully"} + +@router.get("/agents/{agent_id}/versions/{version_id}", response_model=AgentVersionResponse) +async def get_agent_version( + agent_id: str, + version_id: str, + user_id: str = Depends(get_current_user_id_from_jwt) +): + """Get a specific version of an agent.""" + client = await db.client + + # Check if user has access to this agent + agent_result = await client.table('agents').select("*").eq("agent_id", agent_id).execute() + if not agent_result.data: + raise HTTPException(status_code=404, detail="Agent not found") + + agent = agent_result.data[0] + if agent['account_id'] != user_id and not agent.get('is_public', False): + raise HTTPException(status_code=403, detail="Access denied") + + # Get the specific version + version_result = await client.table('agent_versions').select("*").eq("version_id", version_id).eq("agent_id", agent_id).execute() + + if not version_result.data: + raise HTTPException(status_code=404, detail="Version not found") + + return version_result.data[0] diff --git a/backend/mcp_local/template_manager.py b/backend/mcp_local/template_manager.py index 52cba196..b8bfb5cc 100644 --- a/backend/mcp_local/template_manager.py +++ b/backend/mcp_local/template_manager.py @@ -39,6 +39,7 @@ class AgentTemplate: updated_at: datetime avatar: Optional[str] avatar_color: Optional[str] + metadata: Optional[Dict[str, Any]] = None @dataclass @@ -86,8 +87,8 @@ class TemplateManager: try: client = await db.client - # Get the existing agent - agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).execute() + # Get the existing agent with current version + agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('agent_id', agent_id).execute() if not agent_result.data: raise ValueError("Agent not found") @@ -122,18 +123,34 @@ class TemplateManager: logger.info(f"Created custom MCP requirement: {requirement}") mcp_requirements.append(requirement) + # Use version data if available, otherwise fall back to agent data + version_data = agent.get('agent_versions', {}) + if version_data: + system_prompt = version_data.get('system_prompt', agent['system_prompt']) + agentpress_tools = version_data.get('agentpress_tools', agent.get('agentpress_tools', {})) + version_name = version_data.get('version_name', 'v1') + else: + system_prompt = agent['system_prompt'] + agentpress_tools = agent.get('agentpress_tools', {}) + version_name = 'v1' + # Create template template_data = { 'creator_id': creator_id, 'name': agent['name'], 'description': agent.get('description'), - 'system_prompt': agent['system_prompt'], + 'system_prompt': system_prompt, 'mcp_requirements': mcp_requirements, - 'agentpress_tools': agent.get('agentpress_tools', {}), + 'agentpress_tools': agentpress_tools, 'tags': tags or [], 'is_public': make_public, 'avatar': agent.get('avatar'), - 'avatar_color': agent.get('avatar_color') + 'avatar_color': agent.get('avatar_color'), + 'metadata': { + 'source_agent_id': agent_id, + 'source_version_id': agent.get('current_version_id'), + 'source_version_name': version_name + } } if make_public: @@ -192,7 +209,8 @@ class TemplateManager: created_at=template_data['created_at'], updated_at=template_data['updated_at'], avatar=template_data.get('avatar'), - avatar_color=template_data.get('avatar_color') + avatar_color=template_data.get('avatar_color'), + metadata=template_data.get('metadata', {}) ) except Exception as e: diff --git a/backend/supabase/migrations/20250103000000_agent_versioning.sql b/backend/supabase/migrations/20250103000000_agent_versioning.sql new file mode 100644 index 00000000..31817ade --- /dev/null +++ b/backend/supabase/migrations/20250103000000_agent_versioning.sql @@ -0,0 +1,332 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS agent_versions ( + version_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + agent_id UUID NOT NULL REFERENCES agents(agent_id) ON DELETE CASCADE, + version_number INTEGER NOT NULL, + version_name VARCHAR(50) NOT NULL, + system_prompt TEXT NOT NULL, + configured_mcps JSONB DEFAULT '[]'::jsonb, + custom_mcps JSONB DEFAULT '[]'::jsonb, + agentpress_tools JSONB DEFAULT '{}'::jsonb, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID REFERENCES basejump.accounts(id), + + UNIQUE(agent_id, version_number), + UNIQUE(agent_id, version_name) +); + +-- Indexes for agent_versions +CREATE INDEX IF NOT EXISTS idx_agent_versions_agent_id ON agent_versions(agent_id); +CREATE INDEX IF NOT EXISTS idx_agent_versions_version_number ON agent_versions(version_number); +CREATE INDEX IF NOT EXISTS idx_agent_versions_is_active ON agent_versions(is_active); +CREATE INDEX IF NOT EXISTS idx_agent_versions_created_at ON agent_versions(created_at); + +-- Add current version tracking to agents table +ALTER TABLE agents ADD COLUMN IF NOT EXISTS current_version_id UUID REFERENCES agent_versions(version_id); +ALTER TABLE agents ADD COLUMN IF NOT EXISTS version_count INTEGER DEFAULT 1; + +-- Add index for current version +CREATE INDEX IF NOT EXISTS idx_agents_current_version ON agents(current_version_id); + +-- Add version tracking to threads (which version is being used in this thread) +ALTER TABLE threads ADD COLUMN IF NOT EXISTS agent_version_id UUID REFERENCES agent_versions(version_id); + +-- Add index for thread version +CREATE INDEX IF NOT EXISTS idx_threads_agent_version ON threads(agent_version_id); + +-- Track version changes and history +CREATE TABLE IF NOT EXISTS agent_version_history ( + history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + agent_id UUID NOT NULL REFERENCES agents(agent_id) ON DELETE CASCADE, + version_id UUID NOT NULL REFERENCES agent_versions(version_id) ON DELETE CASCADE, + action VARCHAR(50) NOT NULL, -- 'created', 'updated', 'activated', 'deactivated' + changed_by UUID REFERENCES basejump.accounts(id), + change_description TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Indexes for version history +CREATE INDEX IF NOT EXISTS idx_agent_version_history_agent_id ON agent_version_history(agent_id); +CREATE INDEX IF NOT EXISTS idx_agent_version_history_version_id ON agent_version_history(version_id); +CREATE INDEX IF NOT EXISTS idx_agent_version_history_created_at ON agent_version_history(created_at); + +-- Update updated_at timestamp for agent_versions +CREATE OR REPLACE FUNCTION update_agent_versions_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_agent_versions_updated_at + BEFORE UPDATE ON agent_versions + FOR EACH ROW + EXECUTE FUNCTION update_agent_versions_updated_at(); + +-- Enable RLS on new tables +ALTER TABLE agent_versions ENABLE ROW LEVEL SECURITY; +ALTER TABLE agent_version_history ENABLE ROW LEVEL SECURITY; + +-- Policies for agent_versions +CREATE POLICY agent_versions_select_policy ON agent_versions + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM agents + WHERE agents.agent_id = agent_versions.agent_id + AND ( + agents.is_public = TRUE OR + basejump.has_role_on_account(agents.account_id) + ) + ) + ); + +CREATE POLICY agent_versions_insert_policy ON agent_versions + FOR INSERT + WITH CHECK ( + EXISTS ( + SELECT 1 FROM agents + WHERE agents.agent_id = agent_versions.agent_id + AND basejump.has_role_on_account(agents.account_id, 'owner') + ) + ); + +CREATE POLICY agent_versions_update_policy ON agent_versions + FOR UPDATE + USING ( + EXISTS ( + SELECT 1 FROM agents + WHERE agents.agent_id = agent_versions.agent_id + AND basejump.has_role_on_account(agents.account_id, 'owner') + ) + ); + +CREATE POLICY agent_versions_delete_policy ON agent_versions + FOR DELETE + USING ( + EXISTS ( + SELECT 1 FROM agents + WHERE agents.agent_id = agent_versions.agent_id + AND basejump.has_role_on_account(agents.account_id, 'owner') + ) + ); + +-- Policies for agent_version_history +CREATE POLICY agent_version_history_select_policy ON agent_version_history + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM agents + WHERE agents.agent_id = agent_version_history.agent_id + AND basejump.has_role_on_account(agents.account_id) + ) + ); + +CREATE POLICY agent_version_history_insert_policy ON agent_version_history + FOR INSERT + WITH CHECK ( + EXISTS ( + SELECT 1 FROM agents + WHERE agents.agent_id = agent_version_history.agent_id + AND basejump.has_role_on_account(agents.account_id, 'owner') + ) + ); + +-- Function to migrate existing agents to versioned system +CREATE OR REPLACE FUNCTION migrate_agents_to_versioned() +RETURNS void +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +DECLARE + v_agent RECORD; + v_version_id UUID; +BEGIN + -- For each existing agent, create a v1 version + FOR v_agent IN SELECT * FROM agents WHERE current_version_id IS NULL + LOOP + -- Create v1 version with current agent data + INSERT INTO agent_versions ( + agent_id, + version_number, + version_name, + system_prompt, + configured_mcps, + custom_mcps, + agentpress_tools, + is_active, + created_by + ) VALUES ( + v_agent.agent_id, + 1, + 'v1', + v_agent.system_prompt, + v_agent.configured_mcps, + v_agent.custom_mcps, + v_agent.agentpress_tools, + TRUE, + v_agent.account_id + ) RETURNING version_id INTO v_version_id; + + -- Update agent with current version + UPDATE agents + SET current_version_id = v_version_id, + version_count = 1 + WHERE agent_id = v_agent.agent_id; + + -- Add history entry + INSERT INTO agent_version_history ( + agent_id, + version_id, + action, + changed_by, + change_description + ) VALUES ( + v_agent.agent_id, + v_version_id, + 'created', + v_agent.account_id, + 'Initial version created from existing agent' + ); + END LOOP; +END; +$$; + +-- Function to create a new version of an agent +CREATE OR REPLACE FUNCTION create_agent_version( + p_agent_id UUID, + p_system_prompt TEXT, + p_configured_mcps JSONB DEFAULT '[]'::jsonb, + p_custom_mcps JSONB DEFAULT '[]'::jsonb, + p_agentpress_tools JSONB DEFAULT '{}'::jsonb, + p_created_by UUID DEFAULT NULL +) +RETURNS UUID +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +DECLARE + v_version_id UUID; + v_version_number INTEGER; + v_version_name VARCHAR(50); +BEGIN + -- Check if user has permission + IF NOT EXISTS ( + SELECT 1 FROM agents + WHERE agent_id = p_agent_id + AND basejump.has_role_on_account(account_id, 'owner') + ) THEN + RAISE EXCEPTION 'Agent not found or access denied'; + END IF; + + -- Get next version number + SELECT COALESCE(MAX(version_number), 0) + 1 INTO v_version_number + FROM agent_versions + WHERE agent_id = p_agent_id; + + -- Generate version name + v_version_name := 'v' || v_version_number; + + -- Create new version + INSERT INTO agent_versions ( + agent_id, + version_number, + version_name, + system_prompt, + configured_mcps, + custom_mcps, + agentpress_tools, + is_active, + created_by + ) VALUES ( + p_agent_id, + v_version_number, + v_version_name, + p_system_prompt, + p_configured_mcps, + p_custom_mcps, + p_agentpress_tools, + TRUE, + p_created_by + ) RETURNING version_id INTO v_version_id; + + -- Update agent version count + UPDATE agents + SET version_count = v_version_number, + current_version_id = v_version_id + WHERE agent_id = p_agent_id; + + -- Add history entry + INSERT INTO agent_version_history ( + agent_id, + version_id, + action, + changed_by, + change_description + ) VALUES ( + p_agent_id, + v_version_id, + 'created', + p_created_by, + 'New version ' || v_version_name || ' created' + ); + + RETURN v_version_id; +END; +$$; + +-- Function to switch agent to a different version +CREATE OR REPLACE FUNCTION switch_agent_version( + p_agent_id UUID, + p_version_id UUID, + p_changed_by UUID DEFAULT NULL +) +RETURNS void +SECURITY DEFINER +LANGUAGE plpgsql +AS $$ +BEGIN + -- Check if user has permission and version exists + IF NOT EXISTS ( + SELECT 1 FROM agents a + JOIN agent_versions av ON a.agent_id = av.agent_id + WHERE a.agent_id = p_agent_id + AND av.version_id = p_version_id + AND basejump.has_role_on_account(a.account_id, 'owner') + ) THEN + RAISE EXCEPTION 'Agent/version not found or access denied'; + END IF; + + -- Update current version + UPDATE agents + SET current_version_id = p_version_id + WHERE agent_id = p_agent_id; + + -- Add history entry + INSERT INTO agent_version_history ( + agent_id, + version_id, + action, + changed_by, + change_description + ) VALUES ( + p_agent_id, + p_version_id, + 'activated', + p_changed_by, + 'Switched to this version' + ); +END; +$$; + +-- ===================================================== +-- 9. RUN MIGRATION +-- ===================================================== +-- Migrate existing agents to versioned system +SELECT migrate_agents_to_versioned(); + +COMMIT; \ No newline at end of file diff --git a/backend/supabase/migrations/20250103000001_agent_template_metadata.sql b/backend/supabase/migrations/20250103000001_agent_template_metadata.sql new file mode 100644 index 00000000..e7509c2d --- /dev/null +++ b/backend/supabase/migrations/20250103000001_agent_template_metadata.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb; +CREATE INDEX IF NOT EXISTS idx_agent_templates_metadata ON agent_templates USING gin(metadata); + +COMMIT; \ No newline at end of file diff --git a/frontend/src/app/(dashboard)/agents/_components/AgentVersionManager.tsx b/frontend/src/app/(dashboard)/agents/_components/AgentVersionManager.tsx new file mode 100644 index 00000000..ca07b582 --- /dev/null +++ b/frontend/src/app/(dashboard)/agents/_components/AgentVersionManager.tsx @@ -0,0 +1,202 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; +import { + Clock, + GitBranch, + CheckCircle2, + ArrowUpRight, + History, + Plus +} from 'lucide-react'; +import { formatDistanceToNow } from 'date-fns'; +import { useAgentVersions, useActivateAgentVersion } from '@/hooks/react-query/agents/useAgentVersions'; +import { Agent, AgentVersion } from '../_types'; +import { cn } from '@/lib/utils'; + +interface AgentVersionManagerProps { + agent: Agent; + onCreateVersion?: () => void; +} + +export function AgentVersionManager({ agent, onCreateVersion }: AgentVersionManagerProps) { + const { data: versions, isLoading } = useAgentVersions(agent.agent_id); + const activateVersion = useActivateAgentVersion(); + const [selectedVersion, setSelectedVersion] = useState(null); + + if (isLoading) { + return ( + + +
+
+
+
+
+ ); + } + + const currentVersion = versions?.find(v => v.version_id === agent.current_version_id); + const versionHistory = versions?.sort((a, b) => b.version_number - a.version_number) || []; + + const handleActivateVersion = (versionId: string) => { + activateVersion.mutate({ + agentId: agent.agent_id, + versionId + }); + }; + + return ( + + +
+
+ + + Version Management + + + Manage different versions of your agent configuration + +
+ {onCreateVersion && ( + + )} +
+
+ + + + Current Version + Version History + + + + {currentVersion ? ( +
+
+
+ + {currentVersion.version_name} + + + Active version + +
+ +
+ +
+
+ Created + {formatDistanceToNow(new Date(currentVersion.created_at), { addSuffix: true })} +
+
+ Tools + {Object.keys(currentVersion.agentpress_tools || {}).length} enabled +
+
+ MCP Servers + {(currentVersion.configured_mcps?.length || 0) + (currentVersion.custom_mcps?.length || 0)} +
+
+
+ ) : ( +
+ No version information available +
+ )} +
+ + + +
+ {versionHistory.map((version, index) => { + const isActive = version.version_id === agent.current_version_id; + const isSelected = version.version_id === selectedVersion; + + return ( +
setSelectedVersion(version.version_id)} + > +
+
+
+ + {version.version_name} + + {isActive && ( + + Current + + )} +
+

+ Created {formatDistanceToNow(new Date(version.created_at), { addSuffix: true })} +

+
+ + {!isActive && ( + + )} +
+ + {isSelected && ( +
+
+
+ Tools + {Object.keys(version.agentpress_tools || {}).length} enabled +
+
+ MCP Servers + {(version.configured_mcps?.length || 0) + (version.custom_mcps?.length || 0)} +
+
+
+ )} +
+ ); + })} + + {versionHistory.length === 0 && ( +
+ +

No version history available

+
+ )} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx b/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx index 4b8cb2a2..a76c532a 100644 --- a/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Settings, Trash2, Star, MessageCircle, Wrench, Globe, GlobeLock, Download, Shield, AlertTriangle } from 'lucide-react'; +import { Settings, Trash2, Star, MessageCircle, Wrench, Globe, GlobeLock, Download, Shield, AlertTriangle, GitBranch } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { Dialog, DialogContent, DialogTitle, DialogHeader, DialogDescription } from '@/components/ui/dialog'; @@ -26,6 +26,13 @@ interface Agent { avatar?: string; avatar_color?: string; template_id?: string; + current_version_id?: string; + version_count?: number; + current_version?: { + version_id: string; + version_name: string; + version_number: number; + }; } interface AgentsGridProps { @@ -76,6 +83,12 @@ const AgentModal = ({ agent, isOpen, onClose, onCustomize, onChat, onPublish, on

{agent.name}

+ {agent.current_version && ( + + + {agent.current_version.version_name} + + )} {agent.is_public && ( @@ -281,6 +294,12 @@ export const AgentsGrid = ({

{agent.name}

+ {agent.current_version && ( + + + {agent.current_version.version_name} + + )} {agent.is_public && ( 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 58bed502..baa9e1fb 100644 --- a/frontend/src/app/(dashboard)/agents/_components/update-agent-dialog.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/update-agent-dialog.tsx @@ -6,13 +6,15 @@ import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Loader2, Search, Save, Settings2, Sparkles } from 'lucide-react'; +import { Loader2, Search, Save, Settings2, Sparkles, GitBranch } from 'lucide-react'; import { toast } from 'sonner'; import { Skeleton } from '@/components/ui/skeleton'; import { DEFAULT_AGENTPRESS_TOOLS, getToolDisplayName } from '../_data/tools'; import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents'; import { MCPConfiguration } from './mcp-configuration'; import { MCPConfigurationNew } from './mcp/mcp-configuration-new'; +import { AgentVersionManager } from './AgentVersionManager'; +import { Badge } from '@/components/ui/badge'; interface AgentUpdateRequest { name?: string; @@ -208,8 +210,13 @@ export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdate
- + Edit Agent + {(agent as any).current_version && ( + + {(agent as any).current_version.version_name} + + )} Modify your agent's configuration and capabilities @@ -281,6 +288,12 @@ export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdate MCP Servers + + + Versions + @@ -372,37 +385,60 @@ export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdate onConfigurationChange={handleMCPConfigurationChange} /> + + + { + // When creating a new version, save current changes first + handleSubmit(); + }} + /> +
-
- - +
+ {/* Show notice if changes will create a new version */} + {agent && (formData.system_prompt !== agent.system_prompt || + JSON.stringify(formData.configured_mcps) !== JSON.stringify(agent.configured_mcps) || + JSON.stringify(formData.custom_mcps) !== JSON.stringify(agent.custom_mcps) || + JSON.stringify(formData.agentpress_tools) !== JSON.stringify(agent.agentpress_tools)) && ( +
+ + These changes will create a new version of your agent +
+ )} + +
+ + +
diff --git a/frontend/src/app/(dashboard)/agents/_types/index.ts b/frontend/src/app/(dashboard)/agents/_types/index.ts index 71968cde..902083ef 100644 --- a/frontend/src/app/(dashboard)/agents/_types/index.ts +++ b/frontend/src/app/(dashboard)/agents/_types/index.ts @@ -9,6 +9,21 @@ export interface FilterOptions { selectedTools: string[]; } +export interface AgentVersion { + version_id: string; + agent_id: string; + version_number: number; + version_name: string; + system_prompt: string; + configured_mcps?: Array<{ name: string }>; + custom_mcps?: Array; + agentpress_tools?: Record; + is_active: boolean; + created_at: string; + updated_at: string; + created_by?: string; +} + export interface Agent { agent_id: string; name: string; @@ -18,6 +33,9 @@ export interface Agent { updated_at?: string; configured_mcps?: Array<{ name: string }>; agentpress_tools?: Record; + current_version_id?: string; + version_count?: number; + current_version?: AgentVersion; } export interface MutationState { diff --git a/frontend/src/app/(dashboard)/agents/_utils/_avatar-generator.ts b/frontend/src/app/(dashboard)/agents/_utils/_avatar-generator.ts new file mode 100644 index 00000000..6a991fce --- /dev/null +++ b/frontend/src/app/(dashboard)/agents/_utils/_avatar-generator.ts @@ -0,0 +1,100 @@ +const AGENT_EMOJIS = [ + '๐Ÿค–', '๐Ÿง ', '๐Ÿ’ก', '๐Ÿš€', 'โšก', '๐Ÿ”ฎ', '๐ŸŽฏ', '๐Ÿ›ก๏ธ', '๐Ÿ”ง', '๐ŸŽจ', + '๐Ÿ“Š', '๐Ÿ“ˆ', '๐Ÿ”', '๐ŸŒŸ', 'โœจ', '๐ŸŽช', '๐ŸŽญ', '๐ŸŽจ', '๐ŸŽฏ', '๐ŸŽฒ', + '๐Ÿงฉ', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ—บ๏ธ', '๐Ÿงญ', 'โš™๏ธ', '๐Ÿ› ๏ธ', '๐Ÿ”ฉ', '๐Ÿ”—', '๐Ÿ“ก', + '๐ŸŒ', '๐Ÿ’ป', '๐Ÿ–ฅ๏ธ', '๐Ÿ“ฑ', 'โŒจ๏ธ', '๐Ÿ–ฑ๏ธ', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐Ÿ“€', '๐Ÿ—„๏ธ', + '๐Ÿ“‚', '๐Ÿ“', '๐Ÿ—‚๏ธ', '๐Ÿ“‹', '๐Ÿ“Œ', '๐Ÿ“', '๐Ÿ“Ž', '๐Ÿ–‡๏ธ', '๐Ÿ“', '๐Ÿ“', + 'โœ‚๏ธ', '๐Ÿ–Š๏ธ', '๐Ÿ–‹๏ธ', 'โœ’๏ธ', '๐Ÿ–Œ๏ธ', '๐Ÿ–๏ธ', '๐Ÿ“', 'โœ๏ธ', '๐Ÿ”', '๐Ÿ”’', + '๐Ÿ”“', '๐Ÿ”', '๐Ÿ”‘', '๐Ÿ—๏ธ', '๐Ÿ”จ', 'โ›๏ธ', 'โš’๏ธ', '๐Ÿ›ก๏ธ', '๐Ÿน', '๐ŸŽฏ', + '๐ŸŽฐ', '๐ŸŽฎ', '๐Ÿ•น๏ธ', '๐ŸŽฒ', 'โ™ ๏ธ', 'โ™ฅ๏ธ', 'โ™ฆ๏ธ', 'โ™ฃ๏ธ', '๐Ÿƒ', '๐Ÿ€„', + '๐ŸŽด', '๐ŸŽญ', '๐Ÿ–ผ๏ธ', '๐ŸŽจ', '๐Ÿงต', '๐Ÿงถ', '๐ŸŽธ', '๐ŸŽน', '๐ŸŽบ', '๐ŸŽป', + '๐Ÿฅ', '๐ŸŽฌ', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽต', '๐ŸŽถ', '๐ŸŽ™๏ธ', '๐ŸŽš๏ธ', '๐ŸŽ›๏ธ', + '๐Ÿ“ป', '๐Ÿ“บ', '๐Ÿ“ท', '๐Ÿ“น', '๐Ÿ“ฝ๏ธ', '๐ŸŽž๏ธ', '๐Ÿ“ž', 'โ˜Ž๏ธ', '๐Ÿ“Ÿ', '๐Ÿ“ ', + '๐Ÿ’Ž', '๐Ÿ’', '๐Ÿ†', '๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰', '๐Ÿ…', '๐ŸŽ–๏ธ', '๐Ÿต๏ธ', '๐ŸŽ—๏ธ', + '๐ŸŽซ', '๐ŸŽŸ๏ธ', '๐ŸŽช', '๐ŸŽญ', '๐ŸŽจ', '๐ŸŽฌ', '๐ŸŽค', '๐ŸŽง', '๐ŸŽผ', '๐ŸŽน', + '๐Ÿฆพ', '๐Ÿฆฟ', '๐Ÿฆด', '๐Ÿ‘๏ธ', '๐Ÿง ', '๐Ÿซ€', '๐Ÿซ', '๐Ÿฆท', '๐Ÿฆด', '๐Ÿ‘€' +]; + +const AVATAR_COLORS = [ + '#FF6B6B', // Red + '#4ECDC4', // Teal + '#45B7D1', // Sky Blue + '#96CEB4', // Mint Green + '#FECA57', // Yellow + '#FF9FF3', // Pink + '#54A0FF', // Blue + '#48DBFB', // Light Blue + '#1DD1A1', // Emerald + '#00D2D3', // Cyan + '#5F27CD', // Purple + '#341F97', // Dark Purple + '#EE5A24', // Orange + '#F368E0', // Magenta + '#FF6348', // Coral + '#7BED9F', // Light Green + '#70A1FF', // Periwinkle + '#5352ED', // Indigo + '#3742FA', // Royal Blue + '#2ED573', // Green + '#1E90FF', // Dodger Blue + '#FF1744', // Red Accent + '#D500F9', // Purple Accent + '#00E676', // Green Accent + '#FF6D00', // Orange Accent + '#00B8D4', // Cyan Accent + '#6C5CE7', // Soft Purple + '#A29BFE', // Lavender + '#FD79A8', // Rose + '#FDCB6E', // Mustard + '#6C5CE7', // Violet + '#00B894', // Mint + '#00CEC9', // Turquoise + '#0984E3', // Blue + '#6C5CE7', // Purple + '#A29BFE', // Light Purple + '#74B9FF', // Light Blue + '#81ECEC', // Light Cyan + '#55A3FF', // Sky + '#FD79A8', // Pink + '#FDCB6E', // Yellow + '#FF7675', // Light Red + '#E17055', // Terra Cotta + '#FAB1A0', // Peach + '#74B9FF', // Powder Blue + '#A29BFE', // Periwinkle + '#DFE6E9', // Light Gray + '#B2BEC3', // Gray + '#636E72', // Dark Gray +]; + +export function generateRandomEmoji(): string { + const randomIndex = Math.floor(Math.random() * AGENT_EMOJIS.length); + return AGENT_EMOJIS[randomIndex]; +} + +export function generateRandomAvatarColor(): string { + const randomIndex = Math.floor(Math.random() * AVATAR_COLORS.length); + return AVATAR_COLORS[randomIndex]; +} + +export function generateRandomAvatar(): { avatar: string; avatar_color: string } { + return { + avatar: generateRandomEmoji(), + avatar_color: generateRandomAvatarColor(), + }; +} + +export function generateAvatarFromSeed(seed: string): { avatar: string; avatar_color: string } { + let hash = 0; + for (let i = 0; i < seed.length; i++) { + const char = seed.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + const emojiIndex = Math.abs(hash) % AGENT_EMOJIS.length; + const colorIndex = Math.abs(hash >> 8) % AVATAR_COLORS.length; + return { + avatar: AGENT_EMOJIS[emojiIndex], + avatar_color: AVATAR_COLORS[colorIndex], + }; +} \ No newline at end of file diff --git a/frontend/src/app/(dashboard)/agents/page.tsx b/frontend/src/app/(dashboard)/agents/page.tsx index 112e46ad..145cdf58 100644 --- a/frontend/src/app/(dashboard)/agents/page.tsx +++ b/frontend/src/app/(dashboard)/agents/page.tsx @@ -17,6 +17,7 @@ import { useRouter } from 'next/navigation'; import { DEFAULT_AGENTPRESS_TOOLS } from './_data/tools'; import { AgentsParams } from '@/hooks/react-query/agents/utils'; import { useFeatureFlags } from '@/lib/feature-flags'; +import { generateRandomAvatar } from './_utils/_avatar-generator'; type ViewMode = 'grid' | 'list'; type SortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count'; @@ -156,10 +157,14 @@ export default function AgentsPage() { const handleCreateNewAgent = async () => { try { + const { avatar, avatar_color } = generateRandomAvatar(); + const defaultAgentData = { name: 'New Agent', description: 'A newly created agent', system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.', + avatar, + avatar_color, configured_mcps: [], agentpress_tools: Object.fromEntries( Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [ diff --git a/frontend/src/app/(dashboard)/marketplace/my-templates/page.tsx b/frontend/src/app/(dashboard)/marketplace/my-templates/page.tsx index 48de857d..c1d30c8e 100644 --- a/frontend/src/app/(dashboard)/marketplace/my-templates/page.tsx +++ b/frontend/src/app/(dashboard)/marketplace/my-templates/page.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState } from 'react'; -import { Globe, GlobeLock, Download, Calendar, User, Tags, Loader2, AlertTriangle, Plus } from 'lucide-react'; +import { Globe, GlobeLock, Download, Calendar, User, Tags, Loader2, AlertTriangle, Plus, GitBranch } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; @@ -66,21 +66,6 @@ export default function MyTemplatesPage() {

- -
- - - - - - -
{isLoading ? ( @@ -128,32 +113,18 @@ export default function MyTemplatesPage() {
{avatar}
-
- {template.is_public ? ( -
- - Public -
- ) : ( -
- - Private -
- )} - {template.is_public && ( -
- - {template.download_count || 0} -
- )} -
-

{template.name}

+ {template.metadata?.source_version_name && ( + + + {template.metadata.source_version_name} + + )}

{template.description || 'No description available'} @@ -179,12 +150,6 @@ export default function MyTemplatesPage() { Created {new Date(template.created_at).toLocaleDateString()}

- {template.marketplace_published_at && ( -
- - Published {new Date(template.marketplace_published_at).toLocaleDateString()} -
- )}
@@ -198,12 +163,12 @@ export default function MyTemplatesPage() { > {isUnpublishing ? ( <> - + Unpublishing... ) : ( <> - + Make Private )} diff --git a/frontend/src/app/(dashboard)/marketplace/page.tsx b/frontend/src/app/(dashboard)/marketplace/page.tsx index eb07be0c..a1354727 100644 --- a/frontend/src/app/(dashboard)/marketplace/page.tsx +++ b/frontend/src/app/(dashboard)/marketplace/page.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useMemo } from 'react'; -import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle } from 'lucide-react'; +import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle, GitBranch } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -40,6 +40,11 @@ interface MarketplaceTemplate { required_config: string[]; custom_type?: 'sse' | 'http'; }>; + metadata?: { + source_agent_id?: string; + source_version_id?: string; + source_version_name?: string; + }; } interface SetupStep { @@ -524,6 +529,7 @@ export default function MarketplacePage() { avatar_color: template.avatar_color, template_id: template.template_id, mcp_requirements: template.mcp_requirements, + metadata: template.metadata, }); }); } @@ -785,6 +791,12 @@ export default function MarketplacePage() {

{item.name}

+ {item.metadata?.source_version_name && ( + + + {item.metadata.source_version_name} + + )}

{item.description || 'No description available'} diff --git a/frontend/src/components/sidebar/sidebar-left.tsx b/frontend/src/components/sidebar/sidebar-left.tsx index 958b9fe9..4dbfb56d 100644 --- a/frontend/src/components/sidebar/sidebar-left.tsx +++ b/frontend/src/components/sidebar/sidebar-left.tsx @@ -141,9 +141,6 @@ export function SidebarLeft({ Agent Playground - - New - @@ -156,9 +153,6 @@ export function SidebarLeft({ Marketplace - - New - @@ -171,9 +165,6 @@ export function SidebarLeft({ Credentials - - New - diff --git a/frontend/src/hooks/react-query/agents/useAgentVersions.ts b/frontend/src/hooks/react-query/agents/useAgentVersions.ts new file mode 100644 index 00000000..ac7e57ca --- /dev/null +++ b/frontend/src/hooks/react-query/agents/useAgentVersions.ts @@ -0,0 +1,60 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + getAgentVersions, + createAgentVersion, + activateAgentVersion, + getAgentVersion, + AgentVersion, + AgentVersionCreateRequest +} from './utils'; +import { toast } from 'sonner'; + +export const useAgentVersions = (agentId: string) => { + return useQuery({ + queryKey: ['agent-versions', agentId], + queryFn: () => getAgentVersions(agentId), + enabled: !!agentId, + }); +}; + +export const useAgentVersion = (agentId: string, versionId: string) => { + return useQuery({ + queryKey: ['agent-version', agentId, versionId], + queryFn: () => getAgentVersion(agentId, versionId), + enabled: !!agentId && !!versionId, + }); +}; + +export const useCreateAgentVersion = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ agentId, data }: { agentId: string; data: AgentVersionCreateRequest }) => + createAgentVersion(agentId, data), + onSuccess: (newVersion, { agentId }) => { + queryClient.invalidateQueries({ queryKey: ['agent-versions', agentId] }); + queryClient.invalidateQueries({ queryKey: ['agent', agentId] }); + toast.success(`Created version ${newVersion.version_name}`); + }, + onError: (error: Error) => { + toast.error(error.message || 'Failed to create version'); + }, + }); +}; + +export const useActivateAgentVersion = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ agentId, versionId }: { agentId: string; versionId: string }) => + activateAgentVersion(agentId, versionId), + onSuccess: (_, { agentId }) => { + queryClient.invalidateQueries({ queryKey: ['agent', agentId] }); + queryClient.invalidateQueries({ queryKey: ['agent-versions', agentId] }); + toast.success('Version activated successfully'); + }, + onError: (error: Error) => { + toast.error(error.message || 'Failed to activate version'); + }, + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/react-query/agents/utils.ts b/frontend/src/hooks/react-query/agents/utils.ts index 5478ccc4..77f7213e 100644 --- a/frontend/src/hooks/react-query/agents/utils.ts +++ b/frontend/src/hooks/react-query/agents/utils.ts @@ -79,6 +79,36 @@ export type AgentCreateRequest = { is_default?: boolean; }; +export type AgentVersionCreateRequest = { + system_prompt: string; + configured_mcps?: Array<{ + name: string; + config: Record; + }>; + custom_mcps?: Array<{ + name: string; + type: 'json' | 'sse'; + config: Record; + enabledTools: string[]; + }>; + agentpress_tools?: Record; +}; + +export type AgentVersion = { + version_id: string; + agent_id: string; + version_number: number; + version_name: string; + system_prompt: string; + configured_mcps: Array; + custom_mcps: Array; + agentpress_tools: Record; + is_active: boolean; + created_at: string; + updated_at: string; + created_by?: string; +}; + export type AgentUpdateRequest = { name?: string; description?: string; @@ -458,4 +488,153 @@ export const startAgentBuilderChat = async ( throw err; } }; + +export const getAgentVersions = async (agentId: string): Promise => { + try { + const agentPlaygroundEnabled = await isFlagEnabled('custom_agents'); + if (!agentPlaygroundEnabled) { + throw new Error('Custom agents is not enabled'); + } + const supabase = createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + throw new Error('You must be logged in to get agent versions'); + } + + const response = await fetch(`${API_URL}/agents/${agentId}/versions`, { + headers: { + 'Authorization': `Bearer ${session.access_token}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const versions = await response.json(); + console.log('[API] Fetched agent versions:', agentId, versions.length); + return versions; + } catch (err) { + console.error('Error fetching agent versions:', err); + throw err; + } +}; + +export const createAgentVersion = async ( + agentId: string, + data: AgentVersionCreateRequest +): Promise => { + try { + const agentPlaygroundEnabled = await isFlagEnabled('custom_agents'); + if (!agentPlaygroundEnabled) { + throw new Error('Custom agents is not enabled'); + } + const supabase = createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + throw new Error('You must be logged in to create agent version'); + } + + const response = await fetch(`${API_URL}/agents/${agentId}/versions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.access_token}`, + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const version = await response.json(); + console.log('[API] Created agent version:', version.version_id); + return version; + } catch (err) { + console.error('Error creating agent version:', err); + throw err; + } +}; + +export const activateAgentVersion = async ( + agentId: string, + versionId: string +): Promise => { + try { + const agentPlaygroundEnabled = await isFlagEnabled('custom_agents'); + if (!agentPlaygroundEnabled) { + throw new Error('Custom agents is not enabled'); + } + const supabase = createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + throw new Error('You must be logged in to activate agent version'); + } + + const response = await fetch( + `${API_URL}/agents/${agentId}/versions/${versionId}/activate`, + { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${session.access_token}`, + }, + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + console.log('[API] Activated agent version:', versionId); + } catch (err) { + console.error('Error activating agent version:', err); + throw err; + } +}; + +export const getAgentVersion = async ( + agentId: string, + versionId: string +): Promise => { + try { + const agentPlaygroundEnabled = await isFlagEnabled('custom_agents'); + if (!agentPlaygroundEnabled) { + throw new Error('Custom agents is not enabled'); + } + const supabase = createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + throw new Error('You must be logged in to get agent version'); + } + + const response = await fetch( + `${API_URL}/agents/${agentId}/versions/${versionId}`, + { + headers: { + 'Authorization': `Bearer ${session.access_token}`, + }, + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const version = await response.json(); + console.log('[API] Fetched agent version:', version.version_id); + return version; + } catch (err) { + console.error('Error fetching agent version:', err); + throw err; + } +}; \ No newline at end of file diff --git a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts index 8010e855..b0ebc2ab 100644 --- a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts +++ b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts @@ -44,6 +44,11 @@ export interface AgentTemplate { creator_name?: string; avatar?: string; avatar_color?: string; + metadata?: { + source_agent_id?: string; + source_version_id?: string; + source_version_name?: string; + }; } export interface MCPRequirement {