chore(dev): ux refactor for marketplace

This commit is contained in:
Soumyadas15 2025-06-09 19:03:47 +05:30
parent 120b3974b3
commit d603125311
16 changed files with 1456 additions and 150 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string | null>(null);
if (isLoading) {
return (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
</CardContent>
</Card>
);
}
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 (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<GitBranch className="h-5 w-5" />
Version Management
</CardTitle>
<CardDescription>
Manage different versions of your agent configuration
</CardDescription>
</div>
{onCreateVersion && (
<Button onClick={onCreateVersion} size="sm">
<Plus className="h-4 w-4 mr-2" />
New Version
</Button>
)}
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="current" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="current">Current Version</TabsTrigger>
<TabsTrigger value="history">Version History</TabsTrigger>
</TabsList>
<TabsContent value="current" className="space-y-4">
{currentVersion ? (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Badge variant="default" className="text-sm">
{currentVersion.version_name}
</Badge>
<span className="text-sm text-muted-foreground">
Active version
</span>
</div>
<CheckCircle2 className="h-5 w-5 text-green-500" />
</div>
<div className="grid gap-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Created</span>
<span>{formatDistanceToNow(new Date(currentVersion.created_at), { addSuffix: true })}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tools</span>
<span>{Object.keys(currentVersion.agentpress_tools || {}).length} enabled</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">MCP Servers</span>
<span>{(currentVersion.configured_mcps?.length || 0) + (currentVersion.custom_mcps?.length || 0)}</span>
</div>
</div>
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No version information available
</div>
)}
</TabsContent>
<TabsContent value="history" className="space-y-4">
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-3">
{versionHistory.map((version, index) => {
const isActive = version.version_id === agent.current_version_id;
const isSelected = version.version_id === selectedVersion;
return (
<div
key={version.version_id}
className={cn(
"p-4 rounded-lg border cursor-pointer transition-colors",
isActive && "border-primary bg-primary/5",
!isActive && "hover:bg-muted/50",
isSelected && !isActive && "bg-muted"
)}
onClick={() => setSelectedVersion(version.version_id)}
>
<div className="flex items-start justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant={isActive ? "default" : "secondary"}>
{version.version_name}
</Badge>
{isActive && (
<Badge variant="outline" className="text-xs">
Current
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
Created {formatDistanceToNow(new Date(version.created_at), { addSuffix: true })}
</p>
</div>
{!isActive && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
handleActivateVersion(version.version_id);
}}
disabled={activateVersion.isPending}
>
<ArrowUpRight className="h-4 w-4 mr-1" />
Activate
</Button>
)}
</div>
{isSelected && (
<div className="mt-3 pt-3 border-t space-y-2">
<div className="grid gap-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Tools</span>
<span>{Object.keys(version.agentpress_tools || {}).length} enabled</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">MCP Servers</span>
<span>{(version.configured_mcps?.length || 0) + (version.custom_mcps?.length || 0)}</span>
</div>
</div>
</div>
)}
</div>
);
})}
{versionHistory.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<History className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p>No version history available</p>
</div>
)}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}

View File

@ -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
<h2 className="text-xl font-semibold text-foreground">
{agent.name}
</h2>
{agent.current_version && (
<Badge variant="outline" className="text-xs">
<GitBranch className="h-3 w-3" />
{agent.current_version.version_name}
</Badge>
)}
{agent.is_public && (
<Badge variant="outline" className="text-xs">
<Shield className="h-3 w-3" />
@ -281,6 +294,12 @@ export const AgentsGrid = ({
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
{agent.name}
</h3>
{agent.current_version && (
<Badge variant="outline" className="text-xs shrink-0">
<GitBranch className="h-3 w-3" />
{agent.current_version.version_name}
</Badge>
)}
{agent.is_public && (
<Badge variant="outline" className="text-xs shrink-0">
<Shield className="h-3 w-3" />

View File

@ -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
<DialogHeader className="px-6 py-4 border-b flex-shrink-0">
<div className="flex items-center justify-between">
<div>
<DialogTitle className="text-xl font-semibold">
<DialogTitle className="text-xl font-semibold flex items-center gap-2">
Edit Agent
{(agent as any).current_version && (
<Badge variant="secondary" className="text-xs">
{(agent as any).current_version.version_name}
</Badge>
)}
</DialogTitle>
<DialogDescription className="text-sm mt-1">
Modify your agent's configuration and capabilities
@ -281,6 +288,12 @@ export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdate
<Sparkles className="h-4 w-4" />
MCP Servers
</TabsTrigger>
<TabsTrigger
value="versions"
>
<GitBranch className="h-4 w-4" />
Versions
</TabsTrigger>
</TabsList>
<TabsContent value="tools" className="flex-1 flex flex-col m-0 min-h-0">
@ -372,37 +385,60 @@ export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdate
onConfigurationChange={handleMCPConfigurationChange}
/>
</TabsContent>
<TabsContent value="versions" className="flex-1 m-0 p-6 overflow-y-auto">
<AgentVersionManager
agent={agent as any}
onCreateVersion={() => {
// When creating a new version, save current changes first
handleSubmit();
}}
/>
</TabsContent>
</Tabs>
</div>
</div>
</div>
<div className="px-6 border-t py-4 flex-shrink-0">
<div className="flex justify-end gap-3">
<Button
variant="outline"
onClick={handleCancel}
disabled={updateAgentMutation.isPending}
className="px-6"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={updateAgentMutation.isPending || !formData.name?.trim() || !formData.system_prompt?.trim()}
>
{updateAgentMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving Changes
</>
) : (
<>
<Save className="h-4 w-4" />
Save Changes
</>
)}
</Button>
<div className="space-y-3">
{/* 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)) && (
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 px-3 py-2 rounded-md">
<GitBranch className="h-4 w-4" />
<span>These changes will create a new version of your agent</span>
</div>
)}
<div className="flex justify-end gap-3">
<Button
variant="outline"
onClick={handleCancel}
disabled={updateAgentMutation.isPending}
className="px-6"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={updateAgentMutation.isPending || !formData.name?.trim() || !formData.system_prompt?.trim()}
>
{updateAgentMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving Changes
</>
) : (
<>
<Save className="h-4 w-4" />
Save Changes
</>
)}
</Button>
</div>
</div>
</div>
</DialogContent>

View File

@ -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<any>;
agentpress_tools?: Record<string, any>;
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<string, any>;
current_version_id?: string;
version_count?: number;
current_version?: AgentVersion;
}
export interface MutationState {

View File

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

View File

@ -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]) => [

View File

@ -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() {
</p>
</div>
</div>
<div className="flex gap-4">
<Link href="/marketplace/secure">
<Button variant="outline">
<Globe className="h-4 w-4 mr-2" />
Browse Marketplace
</Button>
</Link>
<Link href="/settings/credentials">
<Button variant="outline">
<User className="h-4 w-4 mr-2" />
Manage Credentials
</Button>
</Link>
</div>
</div>
{isLoading ? (
@ -128,32 +113,18 @@ export default function MyTemplatesPage() {
<div className="text-4xl">
{avatar}
</div>
<div className="absolute top-3 right-3 flex gap-2">
{template.is_public ? (
<div className="flex items-center gap-1 bg-green-500/20 backdrop-blur-sm px-2 py-1 rounded-full">
<Globe className="h-3 w-3 text-green-400" />
<span className="text-green-400 text-xs font-medium">Public</span>
</div>
) : (
<div className="flex items-center gap-1 bg-gray-500/20 backdrop-blur-sm px-2 py-1 rounded-full">
<GlobeLock className="h-3 w-3 text-gray-400" />
<span className="text-gray-400 text-xs font-medium">Private</span>
</div>
)}
{template.is_public && (
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
<Download className="h-3 w-3 text-white" />
<span className="text-white text-xs font-medium">{template.download_count || 0}</span>
</div>
)}
</div>
</div>
<div className="p-4 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
{template.name}
</h3>
{template.metadata?.source_version_name && (
<Badge variant="secondary" className="text-xs shrink-0">
<GitBranch className="h-3 w-3" />
{template.metadata.source_version_name}
</Badge>
)}
</div>
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
{template.description || 'No description available'}
@ -179,12 +150,6 @@ export default function MyTemplatesPage() {
<Calendar className="h-3 w-3" />
<span>Created {new Date(template.created_at).toLocaleDateString()}</span>
</div>
{template.marketplace_published_at && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Globe className="h-3 w-3" />
<span>Published {new Date(template.marketplace_published_at).toLocaleDateString()}</span>
</div>
)}
</div>
<div className="mt-auto">
@ -198,12 +163,12 @@ export default function MyTemplatesPage() {
>
{isUnpublishing ? (
<>
<Loader2 className="h-3 w-3 animate-spin mr-2" />
<Loader2 className="h-3 w-3 animate-spin" />
Unpublishing...
</>
) : (
<>
<GlobeLock className="h-3 w-3 mr-2" />
<GlobeLock className="h-3 w-3" />
Make Private
</>
)}

View File

@ -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() {
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
{item.name}
</h3>
{item.metadata?.source_version_name && (
<Badge variant="secondary" className="text-xs shrink-0">
<GitBranch className="h-3 w-3" />
{item.metadata.source_version_name}
</Badge>
)}
</div>
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
{item.description || 'No description available'}

View File

@ -141,9 +141,6 @@ export function SidebarLeft({
<Bot className="h-4 w-4 mr-2" />
<span className="flex items-center justify-between w-full">
Agent Playground
<Badge variant="new">
New
</Badge>
</span>
</SidebarMenuButton>
</Link>
@ -156,9 +153,6 @@ export function SidebarLeft({
<Store className="h-4 w-4 mr-2" />
<span className="flex items-center justify-between w-full">
Marketplace
<Badge variant="new">
New
</Badge>
</span>
</SidebarMenuButton>
</Link>
@ -171,9 +165,6 @@ export function SidebarLeft({
<Key className="h-4 w-4 mr-2" />
<span className="flex items-center justify-between w-full">
Credentials
<Badge variant="new">
New
</Badge>
</span>
</SidebarMenuButton>
</Link>

View File

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

View File

@ -79,6 +79,36 @@ export type AgentCreateRequest = {
is_default?: boolean;
};
export type AgentVersionCreateRequest = {
system_prompt: string;
configured_mcps?: Array<{
name: string;
config: Record<string, any>;
}>;
custom_mcps?: Array<{
name: string;
type: 'json' | 'sse';
config: Record<string, any>;
enabledTools: string[];
}>;
agentpress_tools?: Record<string, any>;
};
export type AgentVersion = {
version_id: string;
agent_id: string;
version_number: number;
version_name: string;
system_prompt: string;
configured_mcps: Array<any>;
custom_mcps: Array<any>;
agentpress_tools: Record<string, any>;
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<AgentVersion[]> => {
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<AgentVersion> => {
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<void> => {
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<AgentVersion> => {
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;
}
};

View File

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