mirror of https://github.com/kortix-ai/suna.git
versioning setup
This commit is contained in:
parent
6b04d1dad9
commit
9b0571a7dc
|
@ -25,6 +25,7 @@ from utils.constants import MODEL_NAME_ALIASES
|
|||
from flags.flags import is_enabled
|
||||
|
||||
from .config_helper import extract_agent_config, build_unified_config, extract_tools_for_agent_run, get_mcp_configs
|
||||
from .version_manager import version_manager, VersionData
|
||||
|
||||
router = APIRouter()
|
||||
db = None
|
||||
|
@ -77,6 +78,8 @@ class AgentVersionCreateRequest(BaseModel):
|
|||
configured_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
custom_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
agentpress_tools: Optional[Dict[str, Any]] = {}
|
||||
version_name: Optional[str] = None # Custom version name
|
||||
description: Optional[str] = None # Version description
|
||||
|
||||
class AgentUpdateRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
@ -306,9 +309,17 @@ async def start_agent(
|
|||
agent_config = None
|
||||
effective_agent_id = body.agent_id or thread_agent_id # Use provided agent_id or the one stored in thread
|
||||
|
||||
logger.info(f"[AGENT LOAD] Agent loading flow:")
|
||||
logger.info(f" - body.agent_id: {body.agent_id}")
|
||||
logger.info(f" - thread_agent_id: {thread_agent_id}")
|
||||
logger.info(f" - effective_agent_id: {effective_agent_id}")
|
||||
|
||||
if effective_agent_id:
|
||||
logger.info(f"[AGENT LOAD] Querying for agent: {effective_agent_id}")
|
||||
# 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()
|
||||
logger.info(f"[AGENT LOAD] Query result: found {len(agent_result.data) if agent_result.data else 0} agents")
|
||||
|
||||
if not agent_result.data:
|
||||
if body.agent_id:
|
||||
raise HTTPException(status_code=404, detail="Agent not found or access denied")
|
||||
|
@ -318,6 +329,9 @@ async def start_agent(
|
|||
else:
|
||||
agent_data = agent_result.data[0]
|
||||
version_data = agent_data.get('agent_versions')
|
||||
logger.info(f"[AGENT LOAD] About to call extract_agent_config with agent_data keys: {list(agent_data.keys())}")
|
||||
logger.info(f"[AGENT LOAD] version_data type: {type(version_data)}, content: {version_data}")
|
||||
|
||||
agent_config = extract_agent_config(agent_data, version_data)
|
||||
|
||||
if version_data:
|
||||
|
@ -325,19 +339,32 @@ async def start_agent(
|
|||
else:
|
||||
logger.info(f"Using agent {agent_config['name']} ({effective_agent_id}) - no version data")
|
||||
source = "request" if body.agent_id else "thread"
|
||||
else:
|
||||
logger.info(f"[AGENT LOAD] No effective_agent_id, will try default agent")
|
||||
|
||||
if not agent_config:
|
||||
logger.info(f"[AGENT LOAD] No agent config yet, querying for default agent")
|
||||
default_agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('account_id', account_id).eq('is_default', True).execute()
|
||||
logger.info(f"[AGENT LOAD] Default agent query result: found {len(default_agent_result.data) if default_agent_result.data else 0} default agents")
|
||||
|
||||
if default_agent_result.data:
|
||||
agent_data = default_agent_result.data[0]
|
||||
version_data = agent_data.get('agent_versions')
|
||||
logger.info(f"[AGENT LOAD] About to call extract_agent_config for DEFAULT agent with version_data: {version_data}")
|
||||
|
||||
agent_config = extract_agent_config(agent_data, version_data)
|
||||
|
||||
if version_data:
|
||||
logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) version {agent_config.get('version_name', 'v1')}")
|
||||
else:
|
||||
logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) - no version data")
|
||||
else:
|
||||
logger.warning(f"[AGENT LOAD] No default agent found for account {account_id}")
|
||||
|
||||
logger.info(f"[AGENT LOAD] Final agent_config: {agent_config is not None}")
|
||||
if agent_config:
|
||||
logger.info(f"[AGENT LOAD] Agent config keys: {list(agent_config.keys())}")
|
||||
|
||||
if body.agent_id and body.agent_id != thread_agent_id and agent_config:
|
||||
logger.info(f"Using agent {agent_config['agent_id']} for this agent run (thread remains agent-agnostic)")
|
||||
|
||||
|
@ -843,21 +870,55 @@ async def initiate_agent_with_files(
|
|||
client = await db.client
|
||||
account_id = user_id # In Basejump, personal account_id is the same as user_id
|
||||
|
||||
# Load agent configuration if agent_id is provided
|
||||
# Load agent configuration with version support (same as start_agent endpoint)
|
||||
agent_config = None
|
||||
|
||||
logger.info(f"[AGENT INITIATE] Agent loading flow:")
|
||||
logger.info(f" - agent_id param: {agent_id}")
|
||||
|
||||
if agent_id:
|
||||
agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).eq('account_id', account_id).execute()
|
||||
logger.info(f"[AGENT INITIATE] Querying for specific agent: {agent_id}")
|
||||
# Get agent with current version
|
||||
agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('agent_id', agent_id).eq('account_id', account_id).execute()
|
||||
logger.info(f"[AGENT INITIATE] Query result: found {len(agent_result.data) if agent_result.data else 0} agents")
|
||||
|
||||
if not agent_result.data:
|
||||
raise HTTPException(status_code=404, detail="Agent not found or access denied")
|
||||
agent_config = agent_result.data[0]
|
||||
logger.info(f"Using custom agent: {agent_config['name']} ({agent_id})")
|
||||
|
||||
agent_data = agent_result.data[0]
|
||||
version_data = agent_data.get('agent_versions')
|
||||
logger.info(f"[AGENT INITIATE] About to call extract_agent_config with version_data: {version_data}")
|
||||
|
||||
agent_config = extract_agent_config(agent_data, version_data)
|
||||
|
||||
if version_data:
|
||||
logger.info(f"Using custom agent: {agent_config['name']} ({agent_id}) version {agent_config.get('version_name', 'v1')}")
|
||||
else:
|
||||
logger.info(f"Using custom agent: {agent_config['name']} ({agent_id}) - no version data")
|
||||
else:
|
||||
# Try to get default agent for the account
|
||||
default_agent_result = await client.table('agents').select('*').eq('account_id', account_id).eq('is_default', True).execute()
|
||||
logger.info(f"[AGENT INITIATE] No agent_id provided, querying for default agent")
|
||||
# Try to get default agent for the account with version support
|
||||
default_agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('account_id', account_id).eq('is_default', True).execute()
|
||||
logger.info(f"[AGENT INITIATE] Default agent query result: found {len(default_agent_result.data) if default_agent_result.data else 0} default agents")
|
||||
|
||||
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]
|
||||
version_data = agent_data.get('agent_versions')
|
||||
logger.info(f"[AGENT INITIATE] About to call extract_agent_config for DEFAULT agent with version_data: {version_data}")
|
||||
|
||||
agent_config = extract_agent_config(agent_data, version_data)
|
||||
|
||||
if version_data:
|
||||
logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) version {agent_config.get('version_name', 'v1')}")
|
||||
else:
|
||||
logger.info(f"Using default agent: {agent_config['name']} ({agent_config['agent_id']}) - no version data")
|
||||
else:
|
||||
logger.warning(f"[AGENT INITIATE] No default agent found for account {account_id}")
|
||||
|
||||
logger.info(f"[AGENT INITIATE] Final agent_config: {agent_config is not None}")
|
||||
if agent_config:
|
||||
logger.info(f"[AGENT INITIATE] Agent config keys: {list(agent_config.keys())}")
|
||||
|
||||
can_use, model_message, allowed_models = await can_use_model(client, account_id, model_name)
|
||||
if not can_use:
|
||||
raise HTTPException(status_code=403, detail={"message": model_message, "allowed_models": allowed_models})
|
||||
|
@ -1733,6 +1794,36 @@ async def update_agent(
|
|||
|
||||
logger.info(f"Updated agent {agent_id} for user: {user_id}")
|
||||
|
||||
try:
|
||||
auto_version_id = await version_manager.auto_create_version_on_config_change(
|
||||
agent_id=agent_id,
|
||||
user_id=user_id,
|
||||
change_description="Auto-saved configuration changes"
|
||||
)
|
||||
if auto_version_id:
|
||||
logger.info(f"Auto-created version {auto_version_id} for agent {agent_id}")
|
||||
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 updated_agent.data:
|
||||
agent = updated_agent.data
|
||||
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')
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Auto-versioning failed for agent {agent_id}: {e}")
|
||||
|
||||
return AgentResponse(
|
||||
agent_id=agent['agent_id'],
|
||||
account_id=agent['account_id'],
|
||||
|
@ -1864,22 +1955,14 @@ 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
|
||||
try:
|
||||
versions = await version_manager.get_all_versions(agent_id, user_id)
|
||||
return versions
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching versions for agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to fetch versions")
|
||||
|
||||
@router.post("/agents/{agent_id}/versions", response_model=AgentVersionResponse)
|
||||
async def create_agent_version(
|
||||
|
@ -1888,61 +1971,29 @@ async def create_agent_version(
|
|||
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
|
||||
}
|
||||
|
||||
# Build unified config for the new version
|
||||
version_unified_config = build_unified_config(
|
||||
system_prompt=new_version_data["system_prompt"],
|
||||
agentpress_tools=new_version_data["agentpress_tools"],
|
||||
configured_mcps=new_version_data["configured_mcps"],
|
||||
custom_mcps=new_version_data["custom_mcps"],
|
||||
avatar=None, # Avatar is not versioned
|
||||
avatar_color=None # Avatar color is not versioned
|
||||
)
|
||||
new_version_data["config"] = version_unified_config
|
||||
|
||||
new_version = await client.table('agent_versions').insert(new_version_data).execute()
|
||||
|
||||
if not new_version.data:
|
||||
try:
|
||||
# Convert request to VersionData model
|
||||
version_data_model = VersionData(
|
||||
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 {}
|
||||
)
|
||||
|
||||
version = await version_manager.create_version(
|
||||
agent_id=agent_id,
|
||||
version_data=version_data_model,
|
||||
user_id=user_id,
|
||||
version_name=version_data.version_name,
|
||||
description=version_data.description
|
||||
)
|
||||
|
||||
return version
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating version for agent {agent_id}: {str(e)}")
|
||||
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()
|
||||
|
||||
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(
|
||||
|
@ -1951,24 +2002,14 @@ async def activate_agent_version(
|
|||
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()
|
||||
|
||||
return {"message": "Version activated successfully"}
|
||||
try:
|
||||
await version_manager.activate_version(agent_id, version_id, user_id)
|
||||
return {"message": "Version activated successfully"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error activating version {version_id} for agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to activate version")
|
||||
|
||||
@router.get("/agents/{agent_id}/versions/{version_id}", response_model=AgentVersionResponse)
|
||||
async def get_agent_version(
|
||||
|
@ -1977,24 +2018,31 @@ async def get_agent_version(
|
|||
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]
|
||||
try:
|
||||
version = await version_manager.get_version(agent_id, version_id, user_id)
|
||||
return version
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching version {version_id} for agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to fetch version")
|
||||
|
||||
@router.get("/agents/{agent_id}/versions/compare")
|
||||
async def compare_agent_versions(
|
||||
agent_id: str,
|
||||
version1_id: str = Query(..., description="First version ID to compare"),
|
||||
version2_id: str = Query(..., description="Second version ID to compare"),
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Compare two versions of an agent."""
|
||||
try:
|
||||
comparison = await version_manager.compare_versions(agent_id, version1_id, version2_id, user_id)
|
||||
return comparison
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error comparing versions for agent {agent_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to compare versions")
|
||||
|
||||
@router.get("/agents/{agent_id}/pipedream-tools/{profile_id}")
|
||||
async def get_pipedream_tools_for_agent(
|
||||
|
@ -2161,6 +2209,18 @@ async def update_pipedream_tools_for_agent(
|
|||
raise HTTPException(status_code=500, detail="Failed to update agent")
|
||||
|
||||
logger.info(f"Successfully updated {len(enabled_tools)} tools for agent {agent_id}")
|
||||
|
||||
# 🎯 AUTO-VERSIONING: Create version for Pipedream tools update
|
||||
try:
|
||||
auto_version_id = await version_manager.auto_create_version_on_config_change(
|
||||
agent_id=agent_id,
|
||||
user_id=user_id,
|
||||
change_description=f"Auto-saved after updating {profile.app_name} tools"
|
||||
)
|
||||
if auto_version_id:
|
||||
logger.info(f"Auto-created version {auto_version_id} for Pipedream tools update on agent {agent_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Auto-versioning failed for Pipedream tools update on agent {agent_id}: {e}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
|
@ -2270,7 +2330,6 @@ async def update_custom_mcp_tools_for_agent(
|
|||
agent = agent_result.data[0]
|
||||
custom_mcps = agent.get('custom_mcps', [])
|
||||
|
||||
# Get request data
|
||||
mcp_url = request.get('url')
|
||||
mcp_type = request.get('type', 'sse')
|
||||
enabled_tools = request.get('enabled_tools', [])
|
||||
|
@ -2287,7 +2346,6 @@ async def update_custom_mcp_tools_for_agent(
|
|||
break
|
||||
|
||||
if not updated:
|
||||
# Create new MCP config
|
||||
new_mcp_config = {
|
||||
"name": f"Custom MCP ({mcp_type.upper()})",
|
||||
"customType": mcp_type,
|
||||
|
@ -2299,7 +2357,6 @@ async def update_custom_mcp_tools_for_agent(
|
|||
}
|
||||
custom_mcps.append(new_mcp_config)
|
||||
|
||||
# Update the agent
|
||||
update_result = await client.table('agents').update({
|
||||
'custom_mcps': custom_mcps
|
||||
}).eq('agent_id', agent_id).execute()
|
||||
|
@ -2308,6 +2365,16 @@ async def update_custom_mcp_tools_for_agent(
|
|||
raise HTTPException(status_code=500, detail="Failed to update agent")
|
||||
|
||||
logger.info(f"Successfully updated {len(enabled_tools)} custom MCP tools for agent {agent_id}")
|
||||
try:
|
||||
auto_version_id = await version_manager.auto_create_version_on_config_change(
|
||||
agent_id=agent_id,
|
||||
user_id=user_id,
|
||||
change_description=f"Auto-saved after updating custom MCP tools"
|
||||
)
|
||||
if auto_version_id:
|
||||
logger.info(f"Auto-created version {auto_version_id} for custom MCP tools update on agent {agent_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Auto-versioning failed for custom MCP tools update on agent {agent_id}: {e}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
|
|
|
@ -3,6 +3,32 @@ from utils.logger import logger
|
|||
|
||||
|
||||
def extract_agent_config(agent_data: Dict[str, Any], version_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
agent_id = agent_data.get('agent_id', 'Unknown')
|
||||
|
||||
agent_has_config = bool(agent_data.get('config') and agent_data['config'] != {})
|
||||
version_has_config = bool(version_data and version_data.get('config') and version_data['config'] != {})
|
||||
|
||||
if version_data and version_data.get('config') and version_data['config'] != {}:
|
||||
config = version_data['config'].copy()
|
||||
config['agent_id'] = agent_data['agent_id']
|
||||
config['name'] = agent_data['name']
|
||||
config['description'] = agent_data.get('description')
|
||||
config['is_default'] = agent_data.get('is_default', False)
|
||||
config['account_id'] = agent_data.get('account_id')
|
||||
config['current_version_id'] = agent_data.get('current_version_id')
|
||||
config['version_name'] = version_data.get('version_name', 'v1')
|
||||
|
||||
metadata = config.get('metadata', {})
|
||||
config['avatar'] = metadata.get('avatar', agent_data.get('avatar'))
|
||||
config['avatar_color'] = metadata.get('avatar_color', agent_data.get('avatar_color'))
|
||||
|
||||
config['agentpress_tools'] = extract_tools_for_agent_run(config)
|
||||
|
||||
config['configured_mcps'] = config.get('tools', {}).get('mcp', [])
|
||||
config['custom_mcps'] = config.get('tools', {}).get('custom_mcp', [])
|
||||
|
||||
return config
|
||||
|
||||
if agent_data.get('config') and agent_data['config'] != {}:
|
||||
config = agent_data['config'].copy()
|
||||
if 'tools' not in config:
|
||||
|
@ -32,31 +58,6 @@ def extract_agent_config(agent_data: Dict[str, Any], version_data: Optional[Dict
|
|||
|
||||
return config
|
||||
|
||||
if version_data and version_data.get('config') and version_data['config'] != {}:
|
||||
config = version_data['config'].copy()
|
||||
|
||||
config['agent_id'] = agent_data['agent_id']
|
||||
config['name'] = agent_data['name']
|
||||
config['description'] = agent_data.get('description')
|
||||
config['is_default'] = agent_data.get('is_default', False)
|
||||
config['account_id'] = agent_data.get('account_id')
|
||||
config['current_version_id'] = agent_data.get('current_version_id')
|
||||
config['version_name'] = version_data.get('version_name', 'v1')
|
||||
|
||||
metadata = config.get('metadata', {})
|
||||
config['avatar'] = metadata.get('avatar', agent_data.get('avatar'))
|
||||
config['avatar_color'] = metadata.get('avatar_color', agent_data.get('avatar_color'))
|
||||
|
||||
# Convert agentpress tools to legacy format for run_agent
|
||||
config['agentpress_tools'] = extract_tools_for_agent_run(config)
|
||||
|
||||
# Also extract MCP configs
|
||||
config['configured_mcps'] = config.get('tools', {}).get('mcp', [])
|
||||
config['custom_mcps'] = config.get('tools', {}).get('custom_mcp', [])
|
||||
|
||||
return config
|
||||
|
||||
logger.info(f"Building config from legacy columns for agent {agent_data.get('agent_id')}")
|
||||
source_data = version_data if version_data else agent_data
|
||||
|
||||
legacy_tools = source_data.get('agentpress_tools', {})
|
||||
|
|
|
@ -50,11 +50,6 @@ async def run_agent(
|
|||
is_agent_builder: Optional[bool] = False,
|
||||
target_agent_id: Optional[str] = None
|
||||
):
|
||||
"""Run the development agent with specified configuration."""
|
||||
logger.info(f"🚀 Starting agent with model: {model_name}")
|
||||
if agent_config:
|
||||
logger.info(f"Using custom agent: {agent_config.get('name', 'Unknown')}")
|
||||
|
||||
if not trace:
|
||||
trace = langfuse.trace(name="run_agent", session_id=thread_id, metadata={"project_id": project_id})
|
||||
thread_manager = ThreadManager(trace=trace, is_agent_builder=is_agent_builder or False, target_agent_id=target_agent_id, agent_config=agent_config)
|
||||
|
@ -83,7 +78,12 @@ async def run_agent(
|
|||
enabled_tools = None
|
||||
if agent_config and 'agentpress_tools' in agent_config:
|
||||
enabled_tools = agent_config['agentpress_tools']
|
||||
logger.info(f"Using custom tool configuration from agent")
|
||||
logger.info(f"[AGENT RUN] Using custom tool configuration from agent version")
|
||||
# 🔍 DEBUG: Log which tools are enabled
|
||||
enabled_tool_names = [name for name, config in enabled_tools.items()
|
||||
if (isinstance(config, dict) and config.get('enabled', False)) or
|
||||
(isinstance(config, bool) and config)]
|
||||
logger.info(f"[AGENT RUN] Enabled tools from version: {enabled_tool_names}")
|
||||
|
||||
|
||||
if is_agent_builder:
|
||||
|
@ -103,7 +103,7 @@ async def run_agent(
|
|||
|
||||
|
||||
if enabled_tools is None:
|
||||
logger.info("No agent specified - registering all tools for full Suna capabilities")
|
||||
logger.info("[AGENT RUN] No agent specified - registering all tools for full Suna capabilities")
|
||||
thread_manager.add_tool(SandboxShellTool, project_id=project_id, thread_manager=thread_manager)
|
||||
thread_manager.add_tool(SandboxFilesTool, project_id=project_id, thread_manager=thread_manager)
|
||||
thread_manager.add_tool(SandboxBrowserTool, project_id=project_id, thread_id=thread_id, thread_manager=thread_manager)
|
||||
|
@ -117,24 +117,32 @@ async def run_agent(
|
|||
if config.RAPID_API_KEY:
|
||||
thread_manager.add_tool(DataProvidersTool)
|
||||
else:
|
||||
logger.info("Custom agent specified - registering only enabled tools")
|
||||
logger.info("[AGENT RUN] Custom agent specified - registering only enabled tools from version")
|
||||
thread_manager.add_tool(ExpandMessageTool, thread_id=thread_id, thread_manager=thread_manager)
|
||||
thread_manager.add_tool(MessageTool)
|
||||
if enabled_tools.get('sb_shell_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding sb_shell_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxShellTool, project_id=project_id, thread_manager=thread_manager)
|
||||
if enabled_tools.get('sb_files_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding sb_files_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxFilesTool, project_id=project_id, thread_manager=thread_manager)
|
||||
if enabled_tools.get('sb_browser_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding sb_browser_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxBrowserTool, project_id=project_id, thread_id=thread_id, thread_manager=thread_manager)
|
||||
if enabled_tools.get('sb_deploy_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding sb_deploy_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxDeployTool, project_id=project_id, thread_manager=thread_manager)
|
||||
if enabled_tools.get('sb_expose_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding sb_expose_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxExposeTool, project_id=project_id, thread_manager=thread_manager)
|
||||
if enabled_tools.get('web_search_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding web_search_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxWebSearchTool, project_id=project_id, thread_manager=thread_manager)
|
||||
if enabled_tools.get('sb_vision_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding sb_vision_tool (enabled in version)")
|
||||
thread_manager.add_tool(SandboxVisionTool, project_id=project_id, thread_id=thread_id, thread_manager=thread_manager)
|
||||
if config.RAPID_API_KEY and enabled_tools.get('data_providers_tool', {}).get('enabled', False):
|
||||
logger.info("[AGENT RUN] Adding data_providers_tool (enabled in version)")
|
||||
thread_manager.add_tool(DataProvidersTool)
|
||||
|
||||
# Register MCP tool wrapper if agent has configured MCPs or custom MCPs
|
||||
|
@ -250,14 +258,15 @@ async def run_agent(
|
|||
# Completely replace the default system prompt with the custom one
|
||||
# This prevents confusion and tool hallucination
|
||||
system_content = custom_system_prompt
|
||||
logger.info(f"Using ONLY custom agent system prompt for: {agent_config.get('name', 'Unknown')}")
|
||||
logger.info(f"[AGENT RUN] Using ONLY custom agent system prompt for: {agent_config.get('name', 'Unknown')}")
|
||||
logger.info(f"[AGENT RUN] System prompt source: version {agent_config.get('version_name', 'no version')} (length: {len(custom_system_prompt)})")
|
||||
elif is_agent_builder:
|
||||
system_content = get_agent_builder_prompt()
|
||||
logger.info("Using agent builder system prompt")
|
||||
logger.info("[AGENT RUN] Using agent builder system prompt")
|
||||
else:
|
||||
# Use just the default system prompt
|
||||
system_content = default_system_content
|
||||
logger.info("Using default system prompt only")
|
||||
logger.info("[AGENT RUN] Using default system prompt only")
|
||||
|
||||
if await is_enabled("knowledge_base"):
|
||||
try:
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
"""
|
||||
Agent Version Manager - Comprehensive module for managing agent versions
|
||||
"""
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
import json
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from utils.logger import logger
|
||||
from services.supabase import DBConnection
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
class VersionData(BaseModel):
|
||||
"""Model for version data with proper validation"""
|
||||
system_prompt: str
|
||||
configured_mcps: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
custom_mcps: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
agentpress_tools: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@validator('custom_mcps', pre=True)
|
||||
def normalize_custom_mcps(cls, v):
|
||||
"""Ensure custom MCPs have consistent structure"""
|
||||
if not isinstance(v, list):
|
||||
return []
|
||||
|
||||
normalized = []
|
||||
for mcp in v:
|
||||
if isinstance(mcp, dict):
|
||||
normalized.append({
|
||||
'name': mcp.get('name', 'Unnamed MCP'),
|
||||
'type': mcp.get('type') or mcp.get('customType', 'sse'),
|
||||
'customType': mcp.get('customType') or mcp.get('type', 'sse'),
|
||||
'config': mcp.get('config', {}),
|
||||
'enabledTools': mcp.get('enabledTools') or mcp.get('enabled_tools', [])
|
||||
})
|
||||
return normalized
|
||||
|
||||
@validator('configured_mcps', pre=True)
|
||||
def normalize_configured_mcps(cls, v):
|
||||
"""Ensure configured MCPs are always a list"""
|
||||
return v if isinstance(v, list) else []
|
||||
|
||||
@validator('agentpress_tools', pre=True)
|
||||
def normalize_agentpress_tools(cls, v):
|
||||
"""Ensure agentpress tools are always a dict"""
|
||||
return v if isinstance(v, dict) else {}
|
||||
|
||||
|
||||
class AgentVersionManager:
|
||||
"""Manager class for handling all agent version operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger
|
||||
self.db = DBConnection()
|
||||
|
||||
async def get_version(self, agent_id: str, version_id: str, user_id: str) -> Dict[str, Any]:
|
||||
"""Get a specific version with proper data normalization"""
|
||||
client = await self.db.client
|
||||
|
||||
# Verify access
|
||||
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 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")
|
||||
|
||||
version_data = version_result.data[0]
|
||||
return self._normalize_version_data(version_data)
|
||||
|
||||
async def get_all_versions(self, agent_id: str, user_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get all versions for an agent"""
|
||||
client = await self.db.client
|
||||
|
||||
# Verify access
|
||||
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 [self._normalize_version_data(v) for v in versions_result.data]
|
||||
|
||||
async def create_version(
|
||||
self,
|
||||
agent_id: str,
|
||||
version_data: VersionData,
|
||||
user_id: str,
|
||||
version_name: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new version with proper validation"""
|
||||
client = await self.db.client
|
||||
|
||||
# Verify ownership
|
||||
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")
|
||||
|
||||
# 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 version
|
||||
new_version_data = {
|
||||
"agent_id": agent_id,
|
||||
"version_number": next_version_number,
|
||||
"version_name": version_name or f"v{next_version_number}",
|
||||
"system_prompt": version_data.system_prompt,
|
||||
"configured_mcps": version_data.configured_mcps,
|
||||
"custom_mcps": version_data.custom_mcps,
|
||||
"agentpress_tools": version_data.agentpress_tools,
|
||||
"is_active": True,
|
||||
"created_by": user_id,
|
||||
"change_description": description
|
||||
}
|
||||
|
||||
# Build unified config
|
||||
config = self._build_unified_config(version_data)
|
||||
new_version_data['config'] = config
|
||||
|
||||
try:
|
||||
version_result = await client.table('agent_versions').insert(new_version_data).execute()
|
||||
|
||||
if not version_result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to create version")
|
||||
|
||||
version = version_result.data[0]
|
||||
|
||||
# Update agent's current version
|
||||
await client.table('agents').update({
|
||||
'current_version_id': version['version_id'],
|
||||
'version_count': next_version_number
|
||||
}).eq('agent_id', agent_id).execute()
|
||||
|
||||
self.logger.info(f"Created version {version['version_name']} for agent {agent_id}")
|
||||
return self._normalize_version_data(version)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating version: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create version: {str(e)}")
|
||||
|
||||
async def auto_create_version_on_config_change(
|
||||
self,
|
||||
agent_id: str,
|
||||
user_id: str,
|
||||
change_description: str = "Auto-saved configuration changes"
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
db = DBConnection()
|
||||
current_agent = await db.fetch_one(
|
||||
"SELECT * FROM agents WHERE agent_id = %s", (agent_id,)
|
||||
)
|
||||
|
||||
if not current_agent:
|
||||
logger.warning(f"Agent {agent_id} not found for auto-versioning")
|
||||
return None
|
||||
current_version = None
|
||||
if current_agent['current_version_id']:
|
||||
current_version = await db.fetch_one(
|
||||
"SELECT * FROM agent_versions WHERE version_id = %s",
|
||||
(current_agent['current_version_id'],)
|
||||
)
|
||||
|
||||
current_config = {
|
||||
'system_prompt': current_agent['system_prompt'],
|
||||
'configured_mcps': current_agent['configured_mcps'] or [],
|
||||
'custom_mcps': current_agent['custom_mcps'] or [],
|
||||
'agentpress_tools': current_agent['agentpress_tools'] or {}
|
||||
}
|
||||
|
||||
version_config = None
|
||||
if current_version:
|
||||
version_config = {
|
||||
'system_prompt': current_version['system_prompt'],
|
||||
'configured_mcps': current_version['configured_mcps'] or [],
|
||||
'custom_mcps': current_version['custom_mcps'] or [],
|
||||
'agentpress_tools': current_version['agentpress_tools'] or {}
|
||||
}
|
||||
|
||||
if version_config and current_config == version_config:
|
||||
logger.info(f"No configuration changes detected for agent {agent_id}")
|
||||
return None
|
||||
|
||||
logger.info(f"Configuration changes detected for agent {agent_id}, creating auto-version")
|
||||
|
||||
version_data = VersionData(
|
||||
system_prompt=current_config['system_prompt'],
|
||||
configured_mcps=current_config['configured_mcps'],
|
||||
custom_mcps=current_config['custom_mcps'],
|
||||
agentpress_tools=current_config['agentpress_tools']
|
||||
)
|
||||
|
||||
new_version = await self.create_version(
|
||||
agent_id=agent_id,
|
||||
version_data=version_data,
|
||||
user_id=user_id,
|
||||
description=change_description
|
||||
)
|
||||
|
||||
if new_version:
|
||||
await self.activate_version(agent_id, new_version['version_id'], user_id)
|
||||
logger.info(f"Auto-created and activated version {new_version['version_name']} for agent {agent_id}")
|
||||
return new_version['version_id']
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to auto-create version for agent {agent_id}: {e}")
|
||||
return None
|
||||
|
||||
async def activate_version(self, agent_id: str, version_id: str, user_id: str) -> None:
|
||||
"""Activate a specific version"""
|
||||
client = await self.db.client
|
||||
|
||||
# Verify ownership
|
||||
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")
|
||||
|
||||
# Verify 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()
|
||||
|
||||
self.logger.info(f"Activated version {version_id} for agent {agent_id}")
|
||||
|
||||
async def compare_versions(
|
||||
self,
|
||||
agent_id: str,
|
||||
version1_id: str,
|
||||
version2_id: str,
|
||||
user_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Compare two versions"""
|
||||
version1 = await self.get_version(agent_id, version1_id, user_id)
|
||||
version2 = await self.get_version(agent_id, version2_id, user_id)
|
||||
|
||||
return {
|
||||
'version1': version1,
|
||||
'version2': version2,
|
||||
'differences': self._calculate_differences(version1, version2)
|
||||
}
|
||||
|
||||
def _normalize_version_data(self, version_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Normalize version data to ensure consistent structure"""
|
||||
# Handle custom MCPs normalization
|
||||
custom_mcps = version_data.get('custom_mcps', [])
|
||||
if custom_mcps is None:
|
||||
custom_mcps = []
|
||||
elif isinstance(custom_mcps, list):
|
||||
custom_mcps = [
|
||||
{
|
||||
'name': mcp.get('name', 'Unnamed MCP'),
|
||||
'type': mcp.get('type') or mcp.get('customType', 'sse'),
|
||||
'customType': mcp.get('customType') or mcp.get('type', 'sse'),
|
||||
'config': mcp.get('config', {}),
|
||||
'enabledTools': mcp.get('enabledTools') or mcp.get('enabled_tools', [])
|
||||
}
|
||||
for mcp in custom_mcps
|
||||
if isinstance(mcp, dict)
|
||||
]
|
||||
|
||||
# Handle configured MCPs
|
||||
configured_mcps = version_data.get('configured_mcps', [])
|
||||
if configured_mcps is None:
|
||||
configured_mcps = []
|
||||
elif not isinstance(configured_mcps, list):
|
||||
configured_mcps = []
|
||||
|
||||
# Handle agentpress tools
|
||||
agentpress_tools = version_data.get('agentpress_tools', {})
|
||||
if not isinstance(agentpress_tools, dict):
|
||||
agentpress_tools = {}
|
||||
|
||||
return {
|
||||
'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.get('system_prompt', ''),
|
||||
'configured_mcps': configured_mcps,
|
||||
'custom_mcps': custom_mcps,
|
||||
'agentpress_tools': 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'),
|
||||
'change_description': version_data.get('change_description'),
|
||||
'config': version_data.get('config', {})
|
||||
}
|
||||
|
||||
def _build_unified_config(self, version_data: VersionData) -> Dict[str, Any]:
|
||||
"""Build unified config object"""
|
||||
simplified_tools = {}
|
||||
for tool_name, tool_config in version_data.agentpress_tools.items():
|
||||
if isinstance(tool_config, dict):
|
||||
simplified_tools[tool_name] = tool_config.get('enabled', False)
|
||||
elif isinstance(tool_config, bool):
|
||||
simplified_tools[tool_name] = tool_config
|
||||
|
||||
return {
|
||||
'system_prompt': version_data.system_prompt,
|
||||
'tools': {
|
||||
'agentpress': simplified_tools,
|
||||
'mcp': version_data.configured_mcps,
|
||||
'custom_mcp': version_data.custom_mcps
|
||||
}
|
||||
}
|
||||
|
||||
def _calculate_differences(self, version1: Dict[str, Any], version2: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Calculate differences between two versions"""
|
||||
differences = {}
|
||||
|
||||
# Check system prompt
|
||||
if version1['system_prompt'] != version2['system_prompt']:
|
||||
differences['system_prompt'] = {
|
||||
'changed': True,
|
||||
'version1': version1['system_prompt'][:100] + '...' if len(version1['system_prompt']) > 100 else version1['system_prompt'],
|
||||
'version2': version2['system_prompt'][:100] + '...' if len(version2['system_prompt']) > 100 else version2['system_prompt']
|
||||
}
|
||||
|
||||
# Check tools
|
||||
v1_tools = set(version1['agentpress_tools'].keys())
|
||||
v2_tools = set(version2['agentpress_tools'].keys())
|
||||
|
||||
tools_added = v2_tools - v1_tools
|
||||
tools_removed = v1_tools - v2_tools
|
||||
tools_changed = []
|
||||
|
||||
for tool in v1_tools & v2_tools:
|
||||
if version1['agentpress_tools'][tool] != version2['agentpress_tools'][tool]:
|
||||
tools_changed.append(tool)
|
||||
|
||||
if tools_added or tools_removed or tools_changed:
|
||||
differences['agentpress_tools'] = {
|
||||
'added': list(tools_added),
|
||||
'removed': list(tools_removed),
|
||||
'changed': tools_changed
|
||||
}
|
||||
|
||||
# Check MCPs
|
||||
if json.dumps(version1['configured_mcps'], sort_keys=True) != json.dumps(version2['configured_mcps'], sort_keys=True):
|
||||
differences['configured_mcps'] = {
|
||||
'version1_count': len(version1['configured_mcps']),
|
||||
'version2_count': len(version2['configured_mcps'])
|
||||
}
|
||||
|
||||
if json.dumps(version1['custom_mcps'], sort_keys=True) != json.dumps(version2['custom_mcps'], sort_keys=True):
|
||||
differences['custom_mcps'] = {
|
||||
'version1_count': len(version1['custom_mcps']),
|
||||
'version2_count': len(version2['custom_mcps'])
|
||||
}
|
||||
|
||||
return differences
|
||||
|
||||
|
||||
# Singleton instance
|
||||
version_manager = AgentVersionManager()
|
|
@ -183,6 +183,13 @@ async def get_mcp_server_details(
|
|||
"""
|
||||
logger.info(f"Fetching details for MCP server: {qualified_name} for user {user_id}")
|
||||
|
||||
# 🔥 FIX: Handle Pipedream qualified names by stripping the pipedream: prefix
|
||||
# Pipedream MCPs use format "pipedream:{app_slug}" but Smithery API expects just "{app_slug}"
|
||||
smithery_lookup_name = qualified_name
|
||||
if qualified_name.startswith("pipedream:"):
|
||||
smithery_lookup_name = qualified_name[len("pipedream:"):]
|
||||
logger.info(f"Pipedream MCP detected - using app_slug '{smithery_lookup_name}' for Smithery lookup")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {
|
||||
|
@ -194,15 +201,17 @@ async def get_mcp_server_details(
|
|||
if SMITHERY_API_KEY:
|
||||
headers["Authorization"] = f"Bearer {SMITHERY_API_KEY}"
|
||||
|
||||
# URL encode the qualified name only if it contains special characters
|
||||
if '@' in qualified_name or '/' in qualified_name:
|
||||
encoded_name = quote(qualified_name, safe='')
|
||||
# URL encode the lookup name only if it contains special characters
|
||||
if '@' in smithery_lookup_name or '/' in smithery_lookup_name:
|
||||
encoded_name = quote(smithery_lookup_name, safe='')
|
||||
else:
|
||||
# Don't encode simple names like "exa"
|
||||
encoded_name = qualified_name
|
||||
# Don't encode simple names like "exa" or "gmail"
|
||||
encoded_name = smithery_lookup_name
|
||||
|
||||
url = f"{SMITHERY_API_BASE_URL}/servers/{encoded_name}"
|
||||
logger.debug(f"Requesting MCP server details from: {url}")
|
||||
if qualified_name != smithery_lookup_name:
|
||||
logger.debug(f"Original qualified name: {qualified_name}, Smithery lookup name: {smithery_lookup_name}")
|
||||
|
||||
response = await client.get(
|
||||
url, # Use registry API for metadata
|
||||
|
@ -218,12 +227,22 @@ async def get_mcp_server_details(
|
|||
logger.info(f"Successfully fetched details for MCP server: {qualified_name}")
|
||||
logger.debug(f"Response data keys: {list(data.keys()) if isinstance(data, dict) else 'not a dict'}")
|
||||
|
||||
# 🔥 FIX: For Pipedream MCPs, restore the original qualified name in the response
|
||||
# This ensures the frontend receives the same qualified name it requested
|
||||
if qualified_name.startswith("pipedream:") and 'qualifiedName' in data:
|
||||
original_qualified_name = data['qualifiedName']
|
||||
data['qualifiedName'] = qualified_name
|
||||
logger.debug(f"Restored original qualified name: {qualified_name} (Smithery returned: {original_qualified_name})")
|
||||
|
||||
return MCPServerDetailResponse(**data)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
logger.error(f"Server not found. Response: {e.response.text}")
|
||||
raise HTTPException(status_code=404, detail=f"MCP server '{qualified_name}' not found")
|
||||
if qualified_name.startswith("pipedream:"):
|
||||
raise HTTPException(status_code=404, detail=f"MCP server '{qualified_name}' not found (searched for '{smithery_lookup_name}' in Smithery registry)")
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"MCP server '{qualified_name}' not found")
|
||||
|
||||
logger.error(f"HTTP error fetching MCP server details: {e.response.status_code} - {e.response.text}")
|
||||
raise HTTPException(
|
||||
|
|
|
@ -431,19 +431,11 @@ class CredentialManager:
|
|||
account_id: str,
|
||||
mcp_qualified_name: str
|
||||
) -> List[MCPCredentialProfile]:
|
||||
"""
|
||||
Get all credential profiles for a specific MCP server
|
||||
|
||||
Args:
|
||||
account_id: User's account ID
|
||||
mcp_qualified_name: MCP server qualified name
|
||||
|
||||
Returns:
|
||||
List of MCPCredentialProfile objects
|
||||
"""
|
||||
try:
|
||||
client = await db.client
|
||||
|
||||
logger.debug(f"Querying credential profiles: account_id={account_id}, mcp_qualified_name='{mcp_qualified_name}'")
|
||||
|
||||
result = await client.table('user_mcp_credential_profiles').select('*')\
|
||||
.eq('account_id', account_id)\
|
||||
.eq('mcp_qualified_name', mcp_qualified_name)\
|
||||
|
@ -451,6 +443,8 @@ class CredentialManager:
|
|||
.order('is_default', desc=True)\
|
||||
.order('created_at', desc=False)\
|
||||
.execute()
|
||||
|
||||
logger.debug(f"Database query returned {len(result.data) if result.data else 0} profiles for '{mcp_qualified_name}'")
|
||||
|
||||
profiles = []
|
||||
for profile_data in result.data:
|
||||
|
@ -561,7 +555,7 @@ class CredentialManager:
|
|||
mcp_qualified_name: str
|
||||
) -> Optional[MCPCredentialProfile]:
|
||||
"""
|
||||
Get the default credential profile for an MCP server
|
||||
Get the default credential profile for an MCP server using robust lookup
|
||||
|
||||
Args:
|
||||
account_id: User's account ID
|
||||
|
@ -570,12 +564,22 @@ class CredentialManager:
|
|||
Returns:
|
||||
Default MCPCredentialProfile or first available profile
|
||||
"""
|
||||
profiles = await self.get_credential_profiles(account_id, mcp_qualified_name)
|
||||
logger.debug(f"Looking for default profile: account_id={account_id}, mcp_qualified_name={mcp_qualified_name}")
|
||||
|
||||
# Use robust profile finder
|
||||
profiles = await self.find_credential_profiles_robust(account_id, mcp_qualified_name)
|
||||
logger.debug(f"Found {len(profiles)} profiles for {mcp_qualified_name}")
|
||||
|
||||
for profile in profiles:
|
||||
if profile.is_default:
|
||||
logger.debug(f"Found default profile: {profile.profile_id} ({profile.display_name})")
|
||||
return profile
|
||||
|
||||
if profiles:
|
||||
logger.debug(f"No default profile found, returning first profile: {profiles[0].profile_id} ({profiles[0].display_name})")
|
||||
else:
|
||||
logger.debug(f"No profiles found for {mcp_qualified_name}")
|
||||
|
||||
return profiles[0] if profiles else None
|
||||
|
||||
async def set_default_profile(
|
||||
|
@ -651,8 +655,58 @@ class CredentialManager:
|
|||
logger.error(f"Error deleting credential profile {profile_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
async def find_credential_profiles_robust(
|
||||
self,
|
||||
account_id: str,
|
||||
mcp_qualified_name: str
|
||||
) -> List[MCPCredentialProfile]:
|
||||
profiles = []
|
||||
profiles = await self.get_credential_profiles(account_id, mcp_qualified_name)
|
||||
if profiles:
|
||||
logger.debug(f"Found {len(profiles)} profiles with exact match for '{mcp_qualified_name}'")
|
||||
return profiles
|
||||
|
||||
if mcp_qualified_name.startswith("pipedream:"):
|
||||
app_slug = mcp_qualified_name[len("pipedream:"):]
|
||||
try:
|
||||
from pipedream.profiles import get_profile_manager
|
||||
profile_manager = get_profile_manager(db)
|
||||
pipedream_profiles = await profile_manager.get_profiles(account_id, app_slug=app_slug, is_active=True)
|
||||
|
||||
for pd_profile in pipedream_profiles:
|
||||
cred_profile = await self.get_credential_by_profile(account_id, str(pd_profile.profile_id))
|
||||
if cred_profile:
|
||||
profiles.append(cred_profile)
|
||||
|
||||
if profiles:
|
||||
logger.debug(f"Found {len(profiles)} Pipedream profiles via profile manager for '{mcp_qualified_name}'")
|
||||
return profiles
|
||||
except Exception as e:
|
||||
logger.debug(f"Error using Pipedream profile manager: {e}")
|
||||
|
||||
elif not mcp_qualified_name.startswith("pipedream:"):
|
||||
pipedream_name = f"pipedream:{mcp_qualified_name}"
|
||||
profiles = await self.get_credential_profiles(account_id, pipedream_name)
|
||||
if profiles:
|
||||
logger.debug(f"Found {len(profiles)} profiles with pipedream prefix '{pipedream_name}'")
|
||||
return profiles
|
||||
|
||||
all_profiles = await self.get_all_user_credential_profiles(account_id)
|
||||
req_name_clean = mcp_qualified_name.replace("pipedream:", "").lower()
|
||||
|
||||
for profile in all_profiles:
|
||||
profile_name_clean = profile.mcp_qualified_name.replace("pipedream:", "").lower()
|
||||
if req_name_clean == profile_name_clean:
|
||||
profiles.append(profile)
|
||||
|
||||
if profiles:
|
||||
logger.debug(f"Found {len(profiles)} profiles via fuzzy matching for '{mcp_qualified_name}'")
|
||||
else:
|
||||
logger.debug(f"No profiles found for '{mcp_qualified_name}' after all strategies")
|
||||
|
||||
return profiles
|
||||
|
||||
async def get_all_user_credential_profiles(self, account_id: str) -> List[MCPCredentialProfile]:
|
||||
"""Get all credential profiles for a user across all MCP servers"""
|
||||
try:
|
||||
client = await db.client
|
||||
|
||||
|
|
|
@ -255,12 +255,12 @@ async def get_credential_profiles_for_mcp(
|
|||
mcp_qualified_name: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Get all credential profiles for a specific MCP server"""
|
||||
decoded_name = urllib.parse.unquote(mcp_qualified_name)
|
||||
logger.info(f"Getting credential profiles for '{decoded_name}' for user {user_id}")
|
||||
|
||||
try:
|
||||
profiles = await credential_manager.get_credential_profiles(user_id, decoded_name)
|
||||
profiles = await credential_manager.find_credential_profiles_robust(user_id, decoded_name)
|
||||
logger.info(f"Found {len(profiles)} credential profiles for '{decoded_name}'")
|
||||
|
||||
return [
|
||||
CredentialProfileResponse(
|
||||
|
@ -287,9 +287,7 @@ async def get_credential_profile_by_id(
|
|||
profile_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Get a specific credential profile by its ID"""
|
||||
logger.info(f"Getting credential profile {profile_id} for user {user_id}")
|
||||
|
||||
try:
|
||||
profile = await credential_manager.get_credential_by_profile(user_id, profile_id)
|
||||
|
||||
|
|
|
@ -81,8 +81,8 @@ class TemplateManager:
|
|||
try:
|
||||
client = await db.client
|
||||
|
||||
# Get the agent
|
||||
agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).execute()
|
||||
# 🔥 FIX: Get the agent WITH current version data
|
||||
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")
|
||||
|
||||
|
@ -92,11 +92,29 @@ class TemplateManager:
|
|||
if agent['account_id'] != creator_id:
|
||||
raise ValueError("Access denied - you can only create templates from your own agents")
|
||||
|
||||
# 🔥 FIX: Get configuration from current version, not legacy columns
|
||||
version_data = agent.get('agent_versions')
|
||||
if version_data and version_data.get('config'):
|
||||
# Use version config (new structure)
|
||||
version_config = version_data['config']
|
||||
system_prompt = version_config.get('system_prompt', '')
|
||||
agentpress_tools = version_config.get('tools', {}).get('agentpress', {})
|
||||
configured_mcps = version_config.get('tools', {}).get('mcp', [])
|
||||
custom_mcps = version_config.get('tools', {}).get('custom_mcp', [])
|
||||
logger.info(f"Using VERSION config for template creation from agent {agent_id}")
|
||||
else:
|
||||
# Fallback to legacy columns if no version data
|
||||
system_prompt = agent.get('system_prompt', '')
|
||||
agentpress_tools = agent.get('agentpress_tools', {})
|
||||
configured_mcps = agent.get('configured_mcps', [])
|
||||
custom_mcps = agent.get('custom_mcps', [])
|
||||
logger.info(f"Using LEGACY config for template creation from agent {agent_id}")
|
||||
|
||||
# Extract MCP requirements from agent configuration
|
||||
mcp_requirements = []
|
||||
|
||||
# Process configured_mcps (regular MCP servers)
|
||||
for mcp in agent.get('configured_mcps', []):
|
||||
for mcp in configured_mcps:
|
||||
if isinstance(mcp, dict) and 'qualifiedName' in mcp:
|
||||
# Extract required config keys from the config
|
||||
config_keys = list(mcp.get('config', {}).keys())
|
||||
|
@ -110,17 +128,37 @@ class TemplateManager:
|
|||
mcp_requirements.append(requirement)
|
||||
|
||||
# Process custom_mcps (custom MCP servers)
|
||||
for custom_mcp in agent.get('custom_mcps', []):
|
||||
for custom_mcp in custom_mcps:
|
||||
if isinstance(custom_mcp, dict) and 'name' in custom_mcp:
|
||||
# Extract required config keys from the config
|
||||
config_keys = list(custom_mcp.get('config', {}).keys())
|
||||
|
||||
# 🔥 FIX: Handle Pipedream MCPs with correct qualified name format
|
||||
custom_type = custom_mcp.get('customType', custom_mcp.get('type', 'http'))
|
||||
|
||||
if custom_type == 'pipedream':
|
||||
# For Pipedream MCPs, extract app_slug and use pipedream:{app_slug} format
|
||||
app_slug = custom_mcp.get('config', {}).get('app_slug')
|
||||
if not app_slug and 'headers' in custom_mcp.get('config', {}):
|
||||
app_slug = custom_mcp['config']['headers'].get('x-pd-app-slug')
|
||||
|
||||
if app_slug:
|
||||
qualified_name = f"pipedream:{app_slug}"
|
||||
logger.info(f"Using Pipedream qualified name: {qualified_name} for app_slug: {app_slug}")
|
||||
else:
|
||||
# Fallback if no app_slug found
|
||||
qualified_name = f"pipedream:{custom_mcp['name'].lower().replace(' ', '_')}"
|
||||
logger.warning(f"No app_slug found for Pipedream MCP {custom_mcp['name']}, using fallback: {qualified_name}")
|
||||
else:
|
||||
# For other custom MCPs, use the original logic
|
||||
qualified_name = custom_mcp['name'].lower().replace(' ', '_')
|
||||
|
||||
requirement = {
|
||||
'qualified_name': custom_mcp['name'].lower().replace(' ', '_'),
|
||||
'qualified_name': qualified_name,
|
||||
'display_name': custom_mcp['name'],
|
||||
'enabled_tools': custom_mcp.get('enabledTools', []),
|
||||
'required_config': config_keys,
|
||||
'custom_type': custom_mcp.get('type', 'http') # Default to http
|
||||
'custom_type': custom_type
|
||||
}
|
||||
mcp_requirements.append(requirement)
|
||||
|
||||
|
@ -130,14 +168,14 @@ class TemplateManager:
|
|||
|
||||
is_kortix_team = creator_id in kortix_team_account_ids
|
||||
|
||||
# Create the template
|
||||
# Create the template using version data
|
||||
template_data = {
|
||||
'creator_id': creator_id,
|
||||
'name': agent['name'],
|
||||
'description': agent.get('description'),
|
||||
'system_prompt': agent['system_prompt'],
|
||||
'system_prompt': system_prompt, # 🔥 From version
|
||||
'mcp_requirements': mcp_requirements,
|
||||
'agentpress_tools': agent.get('agentpress_tools', {}),
|
||||
'agentpress_tools': agentpress_tools, # 🔥 From version
|
||||
'tags': tags or [],
|
||||
'is_public': make_public,
|
||||
'is_kortix_team': is_kortix_team,
|
||||
|
@ -146,7 +184,7 @@ class TemplateManager:
|
|||
'metadata': {
|
||||
'source_agent_id': agent_id,
|
||||
'source_version_id': agent.get('current_version_id'),
|
||||
'source_version_name': agent.get('current_version', {}).get('version_name', 'v1.0')
|
||||
'source_version_name': version_data.get('version_name', 'v1') if version_data else 'v1'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,7 +197,7 @@ class TemplateManager:
|
|||
raise ValueError("Failed to create template")
|
||||
|
||||
template_id = result.data[0]['template_id']
|
||||
logger.info(f"Successfully created template {template_id} from agent {agent_id} with is_kortix_team={is_kortix_team}")
|
||||
logger.info(f"Successfully created template {template_id} from agent {agent_id} version {version_data.get('version_name', 'v1') if version_data else 'legacy'} with is_kortix_team={is_kortix_team}")
|
||||
|
||||
return template_id
|
||||
|
||||
|
@ -223,53 +261,33 @@ class TemplateManager:
|
|||
profile_mappings: Optional[Dict[str, str]] = None,
|
||||
custom_mcp_configs: Optional[Dict[str, Dict[str, Any]]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Install a template as an agent instance for a user
|
||||
|
||||
Args:
|
||||
template_id: ID of the template to install
|
||||
account_id: ID of the user installing the template
|
||||
instance_name: Optional custom name for the instance
|
||||
custom_system_prompt: Optional custom system prompt override
|
||||
profile_mappings: Optional dict mapping qualified_name to profile_id
|
||||
custom_mcp_configs: Optional dict mapping qualified_name to config for custom MCPs
|
||||
|
||||
Returns:
|
||||
Dictionary with installation result and any missing credentials
|
||||
"""
|
||||
logger.info(f"Installing template {template_id} for user {account_id}")
|
||||
|
||||
try:
|
||||
# Get the template
|
||||
template = await self.get_template(template_id)
|
||||
if not template:
|
||||
raise ValueError("Template not found")
|
||||
|
||||
# Check if template is accessible
|
||||
if not template.is_public:
|
||||
# Check if user owns the template
|
||||
if template.creator_id != account_id:
|
||||
raise ValueError("Access denied to private template")
|
||||
|
||||
# Debug: Log template requirements
|
||||
logger.info(f"Template MCP requirements: {[(req.qualified_name, req.display_name, getattr(req, 'custom_type', None)) for req in template.mcp_requirements]}")
|
||||
|
||||
# Separate custom and regular MCP requirements
|
||||
custom_requirements = [req for req in template.mcp_requirements if getattr(req, 'custom_type', None)]
|
||||
regular_requirements = [req for req in template.mcp_requirements if not getattr(req, 'custom_type', None)]
|
||||
|
||||
# If no profile mappings provided, try to use default profiles
|
||||
if not profile_mappings and regular_requirements:
|
||||
profile_mappings = {}
|
||||
for req in regular_requirements:
|
||||
# Get default profile for this MCP service
|
||||
default_profile = await credential_manager.get_default_credential_profile(
|
||||
account_id, req.qualified_name
|
||||
)
|
||||
|
||||
if default_profile:
|
||||
profile_mappings[req.qualified_name] = default_profile.profile_id
|
||||
logger.info(f"✅ Mapped {req.qualified_name} -> {default_profile.profile_id} ({default_profile.display_name})")
|
||||
else:
|
||||
logger.warning(f"❌ No profile found for {req.qualified_name} after robust lookup")
|
||||
|
||||
# Check for missing profile mappings for regular requirements
|
||||
missing_profile_mappings = []
|
||||
if regular_requirements:
|
||||
provided_mappings = profile_mappings or {}
|
||||
|
@ -281,7 +299,6 @@ class TemplateManager:
|
|||
'required_config': req.required_config
|
||||
})
|
||||
|
||||
# Check for missing custom MCP configs
|
||||
missing_custom_configs = []
|
||||
if custom_requirements:
|
||||
provided_custom_configs = custom_mcp_configs or {}
|
||||
|
@ -294,7 +311,6 @@ class TemplateManager:
|
|||
'required_config': req.required_config
|
||||
})
|
||||
|
||||
# If we have any missing profile mappings or configs, return them
|
||||
if missing_profile_mappings or missing_custom_configs:
|
||||
return {
|
||||
'status': 'configs_required',
|
||||
|
@ -307,10 +323,8 @@ class TemplateManager:
|
|||
}
|
||||
}
|
||||
|
||||
# Create regular agent with secure credentials
|
||||
client = await db.client
|
||||
|
||||
# Build configured_mcps and custom_mcps with user's credential profiles
|
||||
|
||||
configured_mcps = []
|
||||
custom_mcps = []
|
||||
|
||||
|
@ -318,32 +332,55 @@ class TemplateManager:
|
|||
logger.info(f"Processing requirement: {req.qualified_name}, custom_type: {getattr(req, 'custom_type', None)}")
|
||||
|
||||
if hasattr(req, 'custom_type') and req.custom_type:
|
||||
# For custom MCP servers, use the provided config from installation
|
||||
if custom_mcp_configs and req.qualified_name in custom_mcp_configs:
|
||||
provided_config = custom_mcp_configs[req.qualified_name]
|
||||
if req.custom_type == 'pipedream':
|
||||
profile_id = provided_config.get('profile_id')
|
||||
if profile_id:
|
||||
logger.info(f"Looking up Pipedream profile {profile_id} for {req.qualified_name}")
|
||||
try:
|
||||
from pipedream.profiles import get_profile_manager
|
||||
profile_manager = get_profile_manager(db)
|
||||
profile = await profile_manager.get_profile(account_id, profile_id)
|
||||
|
||||
if profile:
|
||||
actual_config = {
|
||||
'app_slug': profile.app_slug,
|
||||
'profile_id': profile_id,
|
||||
'url': 'https://remote.mcp.pipedream.net',
|
||||
'headers': {
|
||||
'x-pd-app-slug': profile.app_slug,
|
||||
}
|
||||
}
|
||||
logger.info(f"Found Pipedream profile: {profile.app_name} ({profile.profile_name})")
|
||||
provided_config = actual_config
|
||||
else:
|
||||
logger.error(f"Pipedream profile {profile_id} not found for {req.qualified_name}")
|
||||
raise ValueError(f"Pipedream profile not found for {req.display_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error looking up Pipedream profile {profile_id}: {e}")
|
||||
raise ValueError(f"Failed to lookup Pipedream profile for {req.display_name}")
|
||||
|
||||
custom_mcp_config = {
|
||||
'name': req.display_name,
|
||||
'type': req.custom_type,
|
||||
'customType': req.custom_type,
|
||||
'config': provided_config,
|
||||
'enabledTools': req.enabled_tools
|
||||
}
|
||||
custom_mcps.append(custom_mcp_config)
|
||||
logger.info(f"Added custom MCP with provided config: {custom_mcp_config}")
|
||||
logger.info(f"Added custom MCP with config: {custom_mcp_config}")
|
||||
else:
|
||||
logger.warning(f"No custom config provided for {req.qualified_name}")
|
||||
continue
|
||||
else:
|
||||
# For regular MCP servers, use the selected credential profile
|
||||
if profile_mappings and req.qualified_name in profile_mappings:
|
||||
profile_id = profile_mappings[req.qualified_name]
|
||||
|
||||
# Validate profile_id is not empty
|
||||
if not profile_id or profile_id.strip() == '':
|
||||
logger.error(f"Empty profile_id provided for {req.qualified_name}")
|
||||
raise ValueError(f"Invalid credential profile selected for {req.display_name}")
|
||||
|
||||
# Get the credential profile
|
||||
profile = await credential_manager.get_credential_by_profile(
|
||||
account_id, profile_id
|
||||
)
|
||||
|
@ -352,20 +389,21 @@ class TemplateManager:
|
|||
logger.error(f"Credential profile {profile_id} not found for {req.qualified_name}")
|
||||
raise ValueError(f"Credential profile not found for {req.display_name}. Please select a valid profile or create a new one.")
|
||||
|
||||
# Validate profile is active
|
||||
if not profile.is_active:
|
||||
logger.error(f"Credential profile {profile_id} is inactive for {req.qualified_name}")
|
||||
raise ValueError(f"Selected credential profile for {req.display_name} is inactive. Please select an active profile.")
|
||||
|
||||
|
||||
mcp_config = {
|
||||
'name': req.display_name,
|
||||
'qualifiedName': req.qualified_name,
|
||||
'qualifiedName': profile.mcp_qualified_name, # Use profile's qualified name!
|
||||
'config': profile.config,
|
||||
'enabledTools': req.enabled_tools,
|
||||
'selectedProfileId': profile_id
|
||||
}
|
||||
configured_mcps.append(mcp_config)
|
||||
logger.info(f"Added regular MCP with profile: {mcp_config}")
|
||||
logger.info(f"Used qualified name from profile: {profile.mcp_qualified_name} (template had: {req.qualified_name})")
|
||||
else:
|
||||
logger.error(f"No profile mapping provided for {req.qualified_name}")
|
||||
raise ValueError(f"Missing credential profile for {req.display_name}. Please select a credential profile.")
|
||||
|
@ -373,17 +411,33 @@ class TemplateManager:
|
|||
logger.info(f"Final configured_mcps: {configured_mcps}")
|
||||
logger.info(f"Final custom_mcps: {custom_mcps}")
|
||||
|
||||
# 🔥 FIX: Build unified config structure like in agent creation
|
||||
from agent.config_helper import build_unified_config
|
||||
|
||||
system_prompt = custom_system_prompt or template.system_prompt
|
||||
unified_config = build_unified_config(
|
||||
system_prompt=system_prompt,
|
||||
agentpress_tools=template.agentpress_tools,
|
||||
configured_mcps=configured_mcps,
|
||||
custom_mcps=custom_mcps,
|
||||
avatar=template.avatar,
|
||||
avatar_color=template.avatar_color
|
||||
)
|
||||
|
||||
agent_data = {
|
||||
'account_id': account_id,
|
||||
'name': instance_name or f"{template.name} (from marketplace)",
|
||||
'description': template.description,
|
||||
'system_prompt': custom_system_prompt or template.system_prompt,
|
||||
'config': unified_config,
|
||||
# Keep legacy columns for backward compatibility
|
||||
'system_prompt': system_prompt,
|
||||
'configured_mcps': configured_mcps,
|
||||
'custom_mcps': custom_mcps,
|
||||
'agentpress_tools': template.agentpress_tools,
|
||||
'is_default': False,
|
||||
'avatar': template.avatar,
|
||||
'avatar_color': template.avatar_color
|
||||
'avatar_color': template.avatar_color,
|
||||
'version_count': 1
|
||||
}
|
||||
|
||||
result = await client.table('agents').insert(agent_data).execute()
|
||||
|
@ -398,10 +452,12 @@ class TemplateManager:
|
|||
"agent_id": instance_id,
|
||||
"version_number": 1,
|
||||
"version_name": "v1",
|
||||
"system_prompt": agent_data['system_prompt'],
|
||||
"configured_mcps": agent_data['configured_mcps'],
|
||||
"custom_mcps": agent_data['custom_mcps'],
|
||||
"agentpress_tools": agent_data['agentpress_tools'],
|
||||
"config": unified_config,
|
||||
# Keep legacy columns for backward compatibility
|
||||
"system_prompt": system_prompt,
|
||||
"configured_mcps": configured_mcps,
|
||||
"custom_mcps": custom_mcps,
|
||||
"agentpress_tools": template.agentpress_tools,
|
||||
"is_active": True,
|
||||
"created_by": account_id
|
||||
}
|
||||
|
@ -411,8 +467,7 @@ class TemplateManager:
|
|||
if version_result.data:
|
||||
version_id = version_result.data[0]['version_id']
|
||||
await client.table('agents').update({
|
||||
'current_version_id': version_id,
|
||||
'version_count': 1
|
||||
'current_version_id': version_id
|
||||
}).eq('agent_id', instance_id).execute()
|
||||
logger.info(f"Created initial version v1 for installed agent {instance_id}")
|
||||
else:
|
||||
|
@ -527,14 +582,14 @@ class TemplateManager:
|
|||
}
|
||||
custom_mcps.append(custom_mcp_config)
|
||||
else:
|
||||
# Build regular MCP config
|
||||
mcp_config = {
|
||||
'name': req.display_name,
|
||||
'qualifiedName': req.qualified_name,
|
||||
'qualifiedName': credential.mcp_qualified_name, # Use credential's qualified name!
|
||||
'config': credential.config,
|
||||
'enabledTools': req.enabled_tools
|
||||
}
|
||||
configured_mcps.append(mcp_config)
|
||||
logger.info(f"Runtime config - Used qualified name from credential: {credential.mcp_qualified_name} (template had: {req.qualified_name})")
|
||||
|
||||
# Build complete agent config
|
||||
agent_config = {
|
||||
|
|
|
@ -90,9 +90,15 @@ async def create_connection_token(
|
|||
):
|
||||
logger.info(f"Creating Pipedream connection token for user: {user_id}, app: {request.app}")
|
||||
|
||||
# 🔥 FIX: Strip pipedream: prefix if present
|
||||
actual_app = request.app
|
||||
if request.app and request.app.startswith("pipedream:"):
|
||||
actual_app = request.app[len("pipedream:"):]
|
||||
logger.info(f"Stripped pipedream prefix: {request.app} -> {actual_app}")
|
||||
|
||||
try:
|
||||
client = get_pipedream_client()
|
||||
result = await client.create_connection_token(user_id, request.app)
|
||||
result = await client.create_connection_token(user_id, actual_app)
|
||||
|
||||
logger.info(f"Successfully created connection token for user: {user_id}")
|
||||
return ConnectionTokenResponse(
|
||||
|
@ -100,7 +106,7 @@ async def create_connection_token(
|
|||
link=result.get("connect_link_url"),
|
||||
token=result.get("token"),
|
||||
external_user_id=user_id,
|
||||
app=request.app,
|
||||
app=request.app, # Return original app name that was requested
|
||||
expires_at=result.get("expires_at")
|
||||
)
|
||||
|
||||
|
@ -141,11 +147,17 @@ async def discover_mcp_servers(
|
|||
):
|
||||
logger.info(f"Discovering MCP servers for user: {user_id}, app: {request.app_slug}")
|
||||
|
||||
# 🔥 FIX: Strip pipedream: prefix if present
|
||||
actual_app_slug = request.app_slug
|
||||
if request.app_slug and request.app_slug.startswith("pipedream:"):
|
||||
actual_app_slug = request.app_slug[len("pipedream:"):]
|
||||
logger.info(f"Stripped pipedream prefix: {request.app_slug} -> {actual_app_slug}")
|
||||
|
||||
try:
|
||||
client = get_pipedream_client()
|
||||
mcp_servers = await client.discover_mcp_servers(
|
||||
external_user_id=user_id,
|
||||
app_slug=request.app_slug,
|
||||
app_slug=actual_app_slug,
|
||||
oauth_app_id=request.oauth_app_id
|
||||
)
|
||||
|
||||
|
@ -171,11 +183,17 @@ async def discover_mcp_servers_for_profile(
|
|||
"""Discover MCP servers for a specific profile's external_user_id"""
|
||||
logger.info(f"Discovering MCP servers for external_user_id: {request.external_user_id}, app: {request.app_slug}")
|
||||
|
||||
# 🔥 FIX: Strip pipedream: prefix if present
|
||||
actual_app_slug = request.app_slug
|
||||
if request.app_slug and request.app_slug.startswith("pipedream:"):
|
||||
actual_app_slug = request.app_slug[len("pipedream:"):]
|
||||
logger.info(f"Stripped pipedream prefix: {request.app_slug} -> {actual_app_slug}")
|
||||
|
||||
try:
|
||||
client = get_pipedream_client()
|
||||
mcp_servers = await client.discover_mcp_servers(
|
||||
external_user_id=request.external_user_id,
|
||||
app_slug=request.app_slug,
|
||||
app_slug=actual_app_slug,
|
||||
oauth_app_id=request.oauth_app_id
|
||||
)
|
||||
|
||||
|
@ -199,11 +217,18 @@ async def create_mcp_connection(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
logger.info(f"Creating MCP connection for user: {user_id}, app: {request.app_slug}")
|
||||
|
||||
# 🔥 FIX: Strip pipedream: prefix if present
|
||||
actual_app_slug = request.app_slug
|
||||
if request.app_slug and request.app_slug.startswith("pipedream:"):
|
||||
actual_app_slug = request.app_slug[len("pipedream:"):]
|
||||
logger.info(f"Stripped pipedream prefix: {request.app_slug} -> {actual_app_slug}")
|
||||
|
||||
try:
|
||||
client = get_pipedream_client()
|
||||
mcp_config = await client.create_mcp_connection(
|
||||
external_user_id=user_id,
|
||||
app_slug=request.app_slug,
|
||||
app_slug=actual_app_slug,
|
||||
oauth_app_id=request.oauth_app_id
|
||||
)
|
||||
logger.info(f"Successfully created MCP connection for user: {user_id}, app: {request.app_slug}")
|
||||
|
@ -439,6 +464,11 @@ async def get_credential_profiles(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
logger.info(f"Getting credential profiles for user: {user_id}, app: {app_slug}")
|
||||
|
||||
actual_app_slug = app_slug
|
||||
if app_slug and app_slug.startswith("pipedream:"):
|
||||
actual_app_slug = app_slug[len("pipedream:"):]
|
||||
logger.info(f"Stripped pipedream prefix: {app_slug} -> {actual_app_slug}")
|
||||
|
||||
try:
|
||||
profile_manager = get_profile_manager(db)
|
||||
|
@ -537,12 +567,14 @@ async def connect_credential_profile(
|
|||
app: Optional[str] = Query(None),
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Generate connection token for a specific credential profile"""
|
||||
logger.info(f"Connecting credential profile: {profile_id} for user: {user_id}")
|
||||
actual_app = app
|
||||
if app and app.startswith("pipedream:"):
|
||||
actual_app = app[len("pipedream:"):]
|
||||
logger.info(f"Stripped pipedream prefix: {app} -> {actual_app}")
|
||||
|
||||
try:
|
||||
profile_manager = get_profile_manager(db)
|
||||
result = await profile_manager.connect_profile(user_id, profile_id, app)
|
||||
result = await profile_manager.connect_profile(user_id, profile_id, actual_app)
|
||||
|
||||
logger.info(f"Successfully generated connection token for profile: {profile_id}")
|
||||
return result
|
||||
|
@ -561,7 +593,6 @@ async def get_profile_connections(
|
|||
profile_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Get connections for a specific credential profile"""
|
||||
logger.info(f"Getting connections for profile: {profile_id}, user: {user_id}")
|
||||
|
||||
try:
|
||||
|
@ -583,3 +614,85 @@ async def get_profile_connections(
|
|||
status_code=500,
|
||||
detail=f"Failed to get profile connections: {str(e)}"
|
||||
)
|
||||
|
||||
POPULAR_APP_SLUGS = [
|
||||
"gmail",
|
||||
'google_calendar',
|
||||
'google_drive',
|
||||
'google_docs',
|
||||
'google_sheets',
|
||||
'google_slides',
|
||||
'google_forms',
|
||||
'google_meet',
|
||||
"slack",
|
||||
"discord",
|
||||
"github",
|
||||
"google_sheets",
|
||||
"notion",
|
||||
"airtable",
|
||||
"telegram_bot_api",
|
||||
"openai",
|
||||
'linear',
|
||||
'asana',
|
||||
'supabase'
|
||||
]
|
||||
|
||||
@router.get("/apps/popular", response_model=Dict[str, Any])
|
||||
async def get_popular_pipedream_apps():
|
||||
logger.info("Fetching popular Pipedream apps")
|
||||
|
||||
try:
|
||||
client = get_pipedream_client()
|
||||
access_token = await client._obtain_access_token()
|
||||
await client._ensure_rate_limit_token()
|
||||
|
||||
popular_apps = []
|
||||
for app_slug in POPULAR_APP_SLUGS:
|
||||
try:
|
||||
url = f"https://api.pipedream.com/v1/apps"
|
||||
params = {"q": app_slug}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = await client._make_request_with_retry(
|
||||
"GET",
|
||||
url,
|
||||
headers=headers,
|
||||
params=params
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
apps = data.get("data", [])
|
||||
|
||||
exact_match = next((app for app in apps if app.get("name_slug") == app_slug), None)
|
||||
if exact_match:
|
||||
popular_apps.append(exact_match)
|
||||
logger.debug(f"Found popular app: {app_slug}")
|
||||
else:
|
||||
logger.warning(f"Popular app not found: {app_slug}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching popular app {app_slug}: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Successfully fetched {len(popular_apps)} popular apps")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"apps": popular_apps,
|
||||
"page_info": {
|
||||
"total_count": len(popular_apps),
|
||||
"count": len(popular_apps),
|
||||
"has_more": False
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch popular Pipedream apps: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to fetch popular Pipedream apps: {str(e)}"
|
||||
)
|
||||
|
|
|
@ -65,7 +65,7 @@ class PipedreamClient:
|
|||
)
|
||||
return self.session
|
||||
|
||||
async def _obtain_rate_limit_token(self, window_size_seconds: int = 10, requests_per_window: int = 1000) -> str:
|
||||
async def _obtain_rate_limit_token(self, window_size_seconds: int = 5, requests_per_window: int = 100000) -> str:
|
||||
"""Obtain a rate limit token from Pipedream to bypass rate limits"""
|
||||
if self.rate_limit_token:
|
||||
logger.debug(f"Using existing rate limit token: {self.rate_limit_token[:20]}...")
|
||||
|
@ -196,7 +196,7 @@ class PipedreamClient:
|
|||
logger.error(f"Error obtaining access token: {str(e)}")
|
||||
raise
|
||||
|
||||
async def refresh_rate_limit_token(self, window_size_seconds: int = 10, requests_per_window: int = 1000) -> str:
|
||||
async def refresh_rate_limit_token(self, window_size_seconds: int = 10, requests_per_window: int = 100000) -> str:
|
||||
"""Manually refresh the rate limit token with custom parameters"""
|
||||
self.rate_limit_token = None # Clear existing token
|
||||
return await self._obtain_rate_limit_token(window_size_seconds, requests_per_window)
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { Loader2, Settings2, Sparkles, Check, Clock, Eye, Menu, Zap, Brain, Workflow } from 'lucide-react';
|
||||
import { Loader2, Save, Eye, Check, Wrench, Server, BookOpen, Workflow, Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger, DrawerClose } from '@/components/ui/drawer';
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useCreateAgentVersion, useActivateAgentVersion } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { AgentMCPConfiguration } from '../../../../../components/agents/agent-mcp-configuration';
|
||||
import { toast } from 'sonner';
|
||||
import { AgentToolsConfiguration } from '../../../../../components/agents/agent-tools-configuration';
|
||||
|
@ -18,27 +20,43 @@ import { getAgentAvatar } from '../../../../../lib/utils/get-agent-style';
|
|||
import { EditableText } from '@/components/ui/editable';
|
||||
import { ExpandableMarkdownEditor } from '@/components/ui/expandable-markdown-editor';
|
||||
import { StylePicker } from '../../../../../components/agents/style-picker';
|
||||
import { useSidebar } from '@/components/ui/sidebar';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { AgentBuilderChat } from '../../../../../components/agents/agent-builder-chat';
|
||||
import { AgentTriggersConfiguration } from '@/components/agents/triggers/agent-triggers-configuration';
|
||||
import { AgentKnowledgeBaseManager } from '@/components/agents/knowledge-base/agent-knowledge-base-manager';
|
||||
import { AgentWorkflowsConfiguration } from '@/components/agents/workflows/agent-workflows-configuration';
|
||||
import { AgentVersionSwitcher } from '@/components/agents/agent-version-switcher';
|
||||
import { CreateVersionButton } from '@/components/agents/create-version-button';
|
||||
import { VersionComparison } from '../../../../../components/agents/version-comparison';
|
||||
import { useAgentVersionData } from '../../../../../hooks/use-agent-version-data';
|
||||
import { useAgentVersionStore } from '../../../../../lib/stores/agent-version-store';
|
||||
|
||||
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
||||
interface FormData {
|
||||
name: string;
|
||||
description: string;
|
||||
system_prompt: string;
|
||||
agentpress_tools: any;
|
||||
configured_mcps: any[];
|
||||
custom_mcps: any[];
|
||||
is_default: boolean;
|
||||
avatar: string;
|
||||
avatar_color: string;
|
||||
}
|
||||
|
||||
export default function AgentConfigurationPage() {
|
||||
export default function AgentConfigurationPageRefactored() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const agentId = params.agentId as string;
|
||||
|
||||
const { data: agent, isLoading, error } = useAgent(agentId);
|
||||
// Use modular hooks
|
||||
const { agent, versionData, isViewingOldVersion, isLoading, error } = useAgentVersionData({ agentId });
|
||||
const { hasUnsavedChanges, setHasUnsavedChanges } = useAgentVersionStore();
|
||||
|
||||
const updateAgentMutation = useUpdateAgent();
|
||||
const { state, setOpen, setOpenMobile } = useSidebar();
|
||||
const createVersionMutation = useCreateAgentVersion();
|
||||
const activateVersionMutation = useActivateAgentVersion();
|
||||
|
||||
const initialLayoutAppliedRef = useRef(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
// State management
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
system_prompt: '',
|
||||
|
@ -50,477 +68,488 @@ export default function AgentConfigurationPage() {
|
|||
avatar_color: '',
|
||||
});
|
||||
|
||||
const originalDataRef = useRef<typeof formData | null>(null);
|
||||
const currentFormDataRef = useRef(formData);
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [originalData, setOriginalData] = useState<FormData>(formData);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const [isComparisonOpen, setIsComparisonOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('agent-builder');
|
||||
const accordionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Initialize form data from agent/version data
|
||||
useEffect(() => {
|
||||
if (!agent) return;
|
||||
|
||||
// Determine the source of configuration data
|
||||
let configSource = agent;
|
||||
|
||||
// If we have explicit version data (from URL param), use it
|
||||
if (versionData) {
|
||||
configSource = versionData;
|
||||
}
|
||||
// If no URL param but agent has current_version data, use that
|
||||
else if (agent.current_version) {
|
||||
configSource = agent.current_version;
|
||||
}
|
||||
|
||||
const initialData: FormData = {
|
||||
name: agent.name || '',
|
||||
description: agent.description || '',
|
||||
system_prompt: configSource.system_prompt || '',
|
||||
agentpress_tools: configSource.agentpress_tools || {},
|
||||
configured_mcps: configSource.configured_mcps || [],
|
||||
custom_mcps: configSource.custom_mcps || [],
|
||||
is_default: agent.is_default || false,
|
||||
avatar: agent.avatar || '',
|
||||
avatar_color: agent.avatar_color || '',
|
||||
};
|
||||
|
||||
setFormData(initialData);
|
||||
setOriginalData(initialData);
|
||||
}, [agent, versionData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialLayoutAppliedRef.current) {
|
||||
setOpen(false);
|
||||
initialLayoutAppliedRef.current = true;
|
||||
}
|
||||
}, [setOpen]);
|
||||
const hasChanges = JSON.stringify(formData) !== JSON.stringify(originalData);
|
||||
setHasUnsavedChanges(hasChanges);
|
||||
}, [formData, originalData, setHasUnsavedChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (agent) {
|
||||
const agentData = agent as any;
|
||||
const initialData = {
|
||||
name: agentData.name || '',
|
||||
description: agentData.description || '',
|
||||
system_prompt: agentData.system_prompt || '',
|
||||
agentpress_tools: agentData.agentpress_tools || {},
|
||||
configured_mcps: agentData.configured_mcps || [],
|
||||
custom_mcps: agentData.custom_mcps || [],
|
||||
is_default: agentData.is_default || false,
|
||||
avatar: agentData.avatar || '',
|
||||
avatar_color: agentData.avatar_color || '',
|
||||
};
|
||||
setFormData(initialData);
|
||||
originalDataRef.current = { ...initialData };
|
||||
}
|
||||
}, [agent]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('Access denied') || errorMessage.includes('403')) {
|
||||
toast.error('You don\'t have permission to edit this agent');
|
||||
router.push('/agents');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [error, router]);
|
||||
|
||||
useEffect(() => {
|
||||
currentFormDataRef.current = formData;
|
||||
}, [formData]);
|
||||
|
||||
const hasDataChanged = useCallback((newData: typeof formData, originalData: typeof formData | null): boolean => {
|
||||
if (!originalData) return true;
|
||||
if (newData.name !== originalData.name ||
|
||||
newData.description !== originalData.description ||
|
||||
newData.system_prompt !== originalData.system_prompt ||
|
||||
newData.is_default !== originalData.is_default ||
|
||||
newData.avatar !== originalData.avatar ||
|
||||
newData.avatar_color !== originalData.avatar_color) {
|
||||
return true;
|
||||
}
|
||||
if (JSON.stringify(newData.agentpress_tools) !== JSON.stringify(originalData.agentpress_tools) ||
|
||||
JSON.stringify(newData.configured_mcps) !== JSON.stringify(originalData.configured_mcps) ||
|
||||
JSON.stringify(newData.custom_mcps) !== JSON.stringify(originalData.custom_mcps)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const saveAgent = useCallback(async (data: typeof formData) => {
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!agent || isViewingOldVersion) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
setSaveStatus('saving');
|
||||
const normalizedCustomMcps = (formData.custom_mcps || []).map(mcp => ({
|
||||
name: mcp.name || 'Unnamed MCP',
|
||||
type: mcp.type || mcp.customType || 'sse',
|
||||
config: mcp.config || {},
|
||||
enabledTools: Array.isArray(mcp.enabledTools) ? mcp.enabledTools : [],
|
||||
}));
|
||||
await createVersionMutation.mutateAsync({
|
||||
agentId,
|
||||
data: {
|
||||
system_prompt: formData.system_prompt,
|
||||
configured_mcps: formData.configured_mcps,
|
||||
custom_mcps: normalizedCustomMcps,
|
||||
agentpress_tools: formData.agentpress_tools,
|
||||
description: 'Manual save'
|
||||
}
|
||||
});
|
||||
await updateAgentMutation.mutateAsync({
|
||||
agentId,
|
||||
...data
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
is_default: formData.is_default,
|
||||
avatar: formData.avatar,
|
||||
avatar_color: formData.avatar_color
|
||||
});
|
||||
originalDataRef.current = { ...data };
|
||||
setSaveStatus('saved');
|
||||
setTimeout(() => setSaveStatus('idle'), 2000);
|
||||
|
||||
setOriginalData(formData);
|
||||
toast.success('Changes saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Error updating agent:', error);
|
||||
setSaveStatus('error');
|
||||
toast.error('Failed to update agent');
|
||||
setTimeout(() => setSaveStatus('idle'), 3000);
|
||||
console.error('Save error:', error);
|
||||
toast.error('Failed to save changes');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [agentId, updateAgentMutation]);
|
||||
}, [agent, formData, isViewingOldVersion, agentId, createVersionMutation, updateAgentMutation]);
|
||||
|
||||
const debouncedSave = useCallback((data: typeof formData) => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
if (!hasDataChanged(data, originalDataRef.current)) {
|
||||
const handleFieldChange = useCallback((field: keyof FormData, value: any) => {
|
||||
if (isViewingOldVersion) {
|
||||
toast.error('Cannot edit old versions. Please activate this version first to make changes.');
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
if (hasDataChanged(data, originalDataRef.current)) {
|
||||
saveAgent(data);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
debounceTimerRef.current = timer;
|
||||
}, [saveAgent, hasDataChanged]);
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
}, [isViewingOldVersion]);
|
||||
|
||||
const handleFieldChange = useCallback((field: string, value: any) => {
|
||||
const newFormData = {
|
||||
...currentFormDataRef.current,
|
||||
[field]: value
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
debouncedSave(newFormData);
|
||||
}, [debouncedSave]);
|
||||
|
||||
const handleBatchMCPChange = useCallback((updates: { configured_mcps: any[]; custom_mcps: any[] }) => {
|
||||
const newFormData = {
|
||||
...currentFormDataRef.current,
|
||||
const handleMCPChange = useCallback((updates: { configured_mcps: any[]; custom_mcps: any[] }) => {
|
||||
if (isViewingOldVersion) {
|
||||
toast.error('Cannot edit old versions. Please activate this version first to make changes.');
|
||||
return;
|
||||
}
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
configured_mcps: updates.configured_mcps,
|
||||
custom_mcps: updates.custom_mcps
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
debouncedSave(newFormData);
|
||||
}, [debouncedSave]);
|
||||
|
||||
const scrollToAccordion = useCallback(() => {
|
||||
if (accordionRef.current) {
|
||||
accordionRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end'
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}));
|
||||
}, [isViewingOldVersion]);
|
||||
|
||||
const handleStyleChange = useCallback((emoji: string, color: string) => {
|
||||
const newFormData = {
|
||||
...currentFormDataRef.current,
|
||||
if (isViewingOldVersion) {
|
||||
toast.error('Cannot edit old versions. Please activate this version first to make changes.');
|
||||
return;
|
||||
}
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
avatar: emoji,
|
||||
avatar_color: color,
|
||||
};
|
||||
setFormData(newFormData);
|
||||
debouncedSave(newFormData);
|
||||
}, [debouncedSave]);
|
||||
avatar_color: color
|
||||
}));
|
||||
}, [isViewingOldVersion]);
|
||||
|
||||
const currentStyle = useMemo(() => {
|
||||
if (formData.avatar && formData.avatar_color) {
|
||||
return {
|
||||
avatar: formData.avatar,
|
||||
color: formData.avatar_color,
|
||||
};
|
||||
// Version activation handler
|
||||
const handleActivateVersion = useCallback(async (versionId: string) => {
|
||||
try {
|
||||
await activateVersionMutation.mutateAsync({ agentId, versionId });
|
||||
toast.success('Version activated successfully');
|
||||
// Refresh page without version param
|
||||
router.push(`/agents/config/${agentId}`);
|
||||
} catch (error) {
|
||||
toast.error('Failed to activate version');
|
||||
}
|
||||
return getAgentAvatar(agentId);
|
||||
}, [formData.avatar, formData.avatar_color, agentId]);
|
||||
}, [agentId, activateVersionMutation, router]);
|
||||
|
||||
const memoizedAgentBuilderChat = useMemo(() => (
|
||||
<AgentBuilderChat
|
||||
agentId={agentId}
|
||||
formData={formData}
|
||||
handleFieldChange={handleFieldChange}
|
||||
handleStyleChange={handleStyleChange}
|
||||
currentStyle={currentStyle}
|
||||
/>
|
||||
), [agentId, formData, handleFieldChange, handleStyleChange, currentStyle]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const getSaveStatusBadge = () => {
|
||||
const showSaved = saveStatus === 'idle' && !hasDataChanged(formData, originalDataRef.current);
|
||||
switch (saveStatus) {
|
||||
case 'saving':
|
||||
return (
|
||||
<Badge variant="secondary" className="flex items-center gap-1 text-amber-700 dark:text-amber-300 bg-amber-600/30 hover:bg-amber-700/40">
|
||||
<Clock className="h-3 w-3 animate-pulse" />
|
||||
Saving...
|
||||
</Badge>
|
||||
);
|
||||
case 'saved':
|
||||
return (
|
||||
<Badge variant="default" className="flex items-center gap-1 text-green-700 dark:text-green-300 bg-green-600/30 hover:bg-green-700/40">
|
||||
<Check className="h-3 w-3" />
|
||||
Saved
|
||||
</Badge>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Badge variant="destructive" className="flex items-center gap-1 text-red-700 dark:text-red-300 bg-red-600/30 hover:bg-red-700/40">
|
||||
Error saving
|
||||
</Badge>
|
||||
);
|
||||
|
||||
default:
|
||||
return showSaved ? (
|
||||
<Badge variant="default" className="flex items-center gap-1 text-green-700 dark:text-green-300 bg-green-600/30 hover:bg-green-700/40">
|
||||
<Check className="h-3 w-3" />
|
||||
Saved
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive" className="flex items-center gap-1 text-red-700 dark:text-red-300 bg-red-600/30 hover:bg-red-700/40">
|
||||
Error saving
|
||||
</Badge>
|
||||
);
|
||||
// Auto-switch to configuration tab when viewing old versions
|
||||
useEffect(() => {
|
||||
if (isViewingOldVersion && activeTab === 'agent-builder') {
|
||||
setActiveTab('configuration');
|
||||
}
|
||||
};
|
||||
}, [isViewingOldVersion, activeTab]);
|
||||
|
||||
const ConfigurationContent = useMemo(() => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="md:hidden flex justify-between items-center mb-4 p-4 pb-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setOpenMobile(true)}
|
||||
className="h-8 w-8 flex items-center justify-center rounded-md hover:bg-accent"
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open menu</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="md:hidden flex justify-center">
|
||||
{getSaveStatusBadge()}
|
||||
</div>
|
||||
</div>
|
||||
<Drawer open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="h-[90vh] bg-muted">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Agent Preview</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
<AgentPreview agent={{ ...agent, ...formData }} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className='w-full flex items-center justify-center flex-shrink-0 px-4 md:px-12 md:mt-10'>
|
||||
<div className='w-auto flex items-center gap-2'>
|
||||
<TabsList className="grid h-auto w-full grid-cols-2 bg-muted-foreground/10">
|
||||
<TabsTrigger value="agent-builder" className="w-48 flex items-center gap-1.5 px-2">
|
||||
<span className="truncate">Prompt to configure</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manual">Config</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="manual" className="mt-0 flex-1 overflow-y-auto overflow-x-hidden px-4 md:px-12 pb-4 md:pb-12 scrollbar-hide">
|
||||
<div className="max-w-full">
|
||||
<div className="hidden md:flex justify-end mb-4 mt-4">
|
||||
{getSaveStatusBadge()}
|
||||
</div>
|
||||
<div className='flex items-start md:items-center flex-col md:flex-row mt-6'>
|
||||
<StylePicker
|
||||
agentId={agentId}
|
||||
currentEmoji={currentStyle.avatar}
|
||||
currentColor={currentStyle.color}
|
||||
onStyleChange={handleStyleChange}
|
||||
>
|
||||
<div
|
||||
className="flex-shrink-0 h-12 w-12 md:h-16 md:w-16 flex items-center justify-center rounded-2xl text-xl md:text-2xl cursor-pointer hover:opacity-80 transition-opacity mb-3 md:mb-0"
|
||||
style={{ backgroundColor: currentStyle.color }}
|
||||
>
|
||||
{currentStyle.avatar}
|
||||
</div>
|
||||
</StylePicker>
|
||||
<div className='flex flex-col md:ml-3 w-full min-w-0'>
|
||||
<EditableText
|
||||
value={formData.name}
|
||||
onSave={(value) => handleFieldChange('name', value)}
|
||||
className="text-lg md:text-xl font-semibold bg-transparent"
|
||||
placeholder="Click to add agent name..."
|
||||
/>
|
||||
<EditableText
|
||||
value={formData.description}
|
||||
onSave={(value) => handleFieldChange('description', value)}
|
||||
className="text-muted-foreground text-sm"
|
||||
placeholder="Click to add description..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col mt-6 md:mt-8'>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div className='text-sm font-semibold text-muted-foreground'>Instructions</div>
|
||||
</div>
|
||||
<ExpandableMarkdownEditor
|
||||
value={formData.system_prompt}
|
||||
onSave={(value) => handleFieldChange('system_prompt', value)}
|
||||
placeholder='Click to set system instructions...'
|
||||
title='System Instructions'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref={accordionRef} className="mt-6 border-t">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={[]}
|
||||
className="space-y-2"
|
||||
onValueChange={scrollToAccordion}
|
||||
>
|
||||
<AccordionItem value="tools" className="border-b">
|
||||
<AccordionTrigger className="hover:no-underline text-sm md:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
Default Tools
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4 overflow-x-hidden">
|
||||
<AgentToolsConfiguration
|
||||
tools={formData.agentpress_tools}
|
||||
onToolsChange={(tools) => handleFieldChange('agentpress_tools', tools)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="mcp" className="border-b">
|
||||
<AccordionTrigger className="hover:no-underline text-sm md:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Integrations & MCPs
|
||||
<Badge variant='new'>New</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4 overflow-x-hidden">
|
||||
<AgentMCPConfiguration
|
||||
configuredMCPs={formData.configured_mcps}
|
||||
customMCPs={formData.custom_mcps}
|
||||
onMCPChange={handleBatchMCPChange}
|
||||
agentId={agentId}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="triggers" className="border-b">
|
||||
<AccordionTrigger className="hover:no-underline text-sm md:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4" />
|
||||
Triggers
|
||||
<Badge variant='new'>New</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4 overflow-x-hidden">
|
||||
<AgentTriggersConfiguration
|
||||
agentId={agentId}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="knowledge-base" className="border-b">
|
||||
<AccordionTrigger className="hover:no-underline text-sm md:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="h-4 w-4" />
|
||||
Knowledge Base
|
||||
<Badge variant='new'>New</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4 overflow-x-hidden">
|
||||
<AgentKnowledgeBaseManager
|
||||
agentId={agentId}
|
||||
agentName={formData.name || 'Agent'}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="workflows" className="border-b">
|
||||
<AccordionTrigger className="hover:no-underline text-sm md:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Workflow className="h-4 w-4" />
|
||||
Workflows
|
||||
<Badge variant='new'>New</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4 overflow-x-hidden">
|
||||
<AgentWorkflowsConfiguration
|
||||
agentId={agentId}
|
||||
agentName={formData.name || 'Agent'}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="agent-builder" className="mt-0 flex-1 flex flex-col overflow-hidden">
|
||||
{memoizedAgentBuilderChat}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{error.message || 'Failed to load agent configuration'}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
activeTab,
|
||||
agentId,
|
||||
agent,
|
||||
formData,
|
||||
currentStyle,
|
||||
isPreviewOpen,
|
||||
memoizedAgentBuilderChat,
|
||||
handleFieldChange,
|
||||
handleStyleChange,
|
||||
setOpenMobile,
|
||||
setIsPreviewOpen,
|
||||
setActiveTab,
|
||||
scrollToAccordion,
|
||||
getSaveStatusBadge,
|
||||
handleBatchMCPChange
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<span>Loading agent...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !agent) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const isAccessDenied = errorMessage.includes('Access denied') || errorMessage.includes('403');
|
||||
|
||||
if (!agent) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="text-center space-y-4">
|
||||
{isAccessDenied ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
You don't have permission to edit this agent. You can only edit agents that you created.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-xl font-semibold mb-2">Agent not found</h2>
|
||||
<p className="text-muted-foreground mb-4">The agent you're looking for doesn't exist.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Alert>
|
||||
<AlertDescription>Agent not found</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Use version data when viewing old version, otherwise use form data
|
||||
const displayData = isViewingOldVersion && versionData ? {
|
||||
name: agent?.name || '',
|
||||
description: agent?.description || '',
|
||||
system_prompt: versionData.system_prompt || '',
|
||||
agentpress_tools: versionData.agentpress_tools || {},
|
||||
configured_mcps: versionData.configured_mcps || [],
|
||||
custom_mcps: versionData.custom_mcps || [],
|
||||
is_default: agent?.is_default || false,
|
||||
avatar: agent?.avatar || '',
|
||||
avatar_color: agent?.avatar_color || '',
|
||||
} : formData;
|
||||
|
||||
const currentStyle = displayData.avatar && displayData.avatar_color
|
||||
? { avatar: displayData.avatar, color: displayData.avatar_color }
|
||||
: getAgentAvatar(agentId);
|
||||
|
||||
const previewAgent = {
|
||||
...agent,
|
||||
...displayData,
|
||||
agent_id: agentId,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<div className="hidden md:flex w-full h-full">
|
||||
<div className="w-1/2 border-r bg-background h-full flex flex-col">
|
||||
{ConfigurationContent}
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex justify-between items-center mb-4 p-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentVersionSwitcher
|
||||
agentId={agentId}
|
||||
currentVersionId={agent?.current_version_id}
|
||||
currentFormData={{
|
||||
system_prompt: formData.system_prompt,
|
||||
configured_mcps: formData.configured_mcps,
|
||||
custom_mcps: formData.custom_mcps,
|
||||
agentpress_tools: formData.agentpress_tools
|
||||
}}
|
||||
/>
|
||||
<CreateVersionButton
|
||||
agentId={agentId}
|
||||
currentFormData={{
|
||||
system_prompt: formData.system_prompt,
|
||||
configured_mcps: formData.configured_mcps,
|
||||
custom_mcps: formData.custom_mcps,
|
||||
agentpress_tools: formData.agentpress_tools
|
||||
}}
|
||||
hasChanges={hasUnsavedChanges && !isViewingOldVersion}
|
||||
onVersionCreated={() => {
|
||||
setOriginalData(formData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasUnsavedChanges && !isViewingOldVersion && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden -mt-4">
|
||||
<div className="flex-shrink-0 space-y-6 px-4 mt-2">
|
||||
{isViewingOldVersion && (
|
||||
<Alert className="mb-4">
|
||||
<Eye className="h-4 w-4" />
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<AlertDescription>
|
||||
You are viewing a read-only version. To make changes, please activate this version or switch back to the current version.
|
||||
</AlertDescription>
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
{versionData && (
|
||||
<Badge className="text-xs">
|
||||
{versionData.version_name}
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => versionData && handleActivateVersion(versionData.version_id)}
|
||||
disabled={activateVersionMutation.isPending}
|
||||
>
|
||||
{activateVersionMutation.isPending ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<Check className="h-3 w-3" />
|
||||
)}
|
||||
Set as Current
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col flex-1 overflow-hidden">
|
||||
<div className="flex-shrink-0 space-y-4 px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<StylePicker
|
||||
currentEmoji={currentStyle.avatar}
|
||||
currentColor={currentStyle.color}
|
||||
onStyleChange={handleStyleChange}
|
||||
agentId={agentId}
|
||||
>
|
||||
<div className="h-10 w-10 rounded-xl flex items-center justify-center gap-2" style={{ backgroundColor: currentStyle.color }}>
|
||||
<div className="text-lg font-medium">{currentStyle.avatar}</div>
|
||||
</div>
|
||||
</StylePicker>
|
||||
<EditableText
|
||||
value={displayData.name}
|
||||
onSave={(value) => handleFieldChange('name', value)}
|
||||
className="text-md font-semibold bg-transparent"
|
||||
placeholder="Click to add agent name..."
|
||||
disabled={isViewingOldVersion}
|
||||
/>
|
||||
</div>
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger
|
||||
value="agent-builder"
|
||||
disabled={isViewingOldVersion}
|
||||
className={isViewingOldVersion ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
Agent Builder
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="configuration">Configuration</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="agent-builder" className="flex-1 h-0 px-4">
|
||||
{isViewingOldVersion ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-4 max-w-md">
|
||||
<div className="text-6xl">🔒</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-muted-foreground">Agent Builder Disabled</h3>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
The Agent Builder is only available for the current version.
|
||||
To use the Agent Builder, please activate this version first.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AgentBuilderChat
|
||||
agentId={agentId}
|
||||
formData={displayData}
|
||||
handleFieldChange={handleFieldChange}
|
||||
handleStyleChange={handleStyleChange}
|
||||
currentStyle={currentStyle}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
{activeTab === 'configuration' && (
|
||||
<div className='px-4'>
|
||||
<ExpandableMarkdownEditor
|
||||
value={displayData.system_prompt}
|
||||
onSave={(value) => handleFieldChange('system_prompt', value)}
|
||||
placeholder="Click to set system instructions..."
|
||||
title="System Instructions"
|
||||
disabled={isViewingOldVersion}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TabsContent value="configuration" className="flex-1 h-0 overflow-y-auto px-4">
|
||||
<Accordion type="single" collapsible defaultValue="system" className='space-y-2'>
|
||||
<AccordionItem className='border rounded-xl px-4' value="tools">
|
||||
<AccordionTrigger>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='bg-muted rounded-full h-8 w-8 flex items-center justify-center'>
|
||||
<Wrench className='h-4 w-4' />
|
||||
</div>
|
||||
Tools
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<AgentToolsConfiguration
|
||||
tools={displayData.agentpress_tools}
|
||||
onToolsChange={(tools) => handleFieldChange('agentpress_tools', tools)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem className='border rounded-xl px-4' value="integrations">
|
||||
<AccordionTrigger>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='bg-muted rounded-full h-8 w-8 flex items-center justify-center'>
|
||||
<Server className='h-4 w-4' />
|
||||
</div>
|
||||
Integrations
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<AgentMCPConfiguration
|
||||
configuredMCPs={displayData.configured_mcps}
|
||||
customMCPs={displayData.custom_mcps}
|
||||
onMCPChange={handleMCPChange}
|
||||
agentId={agentId}
|
||||
versionData={{
|
||||
configured_mcps: displayData.configured_mcps,
|
||||
custom_mcps: displayData.custom_mcps,
|
||||
system_prompt: displayData.system_prompt,
|
||||
agentpress_tools: displayData.agentpress_tools
|
||||
}}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem className='border rounded-xl px-4' value="knowledge">
|
||||
<AccordionTrigger>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='bg-muted rounded-full h-8 w-8 flex items-center justify-center'>
|
||||
<BookOpen className='h-4 w-4' />
|
||||
</div>
|
||||
Knowledge Base
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<AgentKnowledgeBaseManager
|
||||
agentId={agentId}
|
||||
agentName={displayData.name || 'Agent'}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem className='border rounded-xl px-4' value="workflows">
|
||||
<AccordionTrigger>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='bg-muted rounded-full h-8 w-8 flex items-center justify-center'>
|
||||
<Workflow className='h-4 w-4' />
|
||||
</div>
|
||||
Workflows
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<AgentWorkflowsConfiguration
|
||||
agentId={agentId}
|
||||
agentName={displayData.name || 'Agent'}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem className='border rounded-xl px-4' value="triggers">
|
||||
<AccordionTrigger>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='bg-muted rounded-full h-8 w-8 flex items-center justify-center'>
|
||||
<Zap className='h-4 w-4' />
|
||||
</div>
|
||||
Triggers
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<AgentTriggersConfiguration agentId={agentId} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Preview */}
|
||||
<div className="w-1/2 overflow-y-auto">
|
||||
<AgentPreview agent={{ ...agent, ...formData }} />
|
||||
{previewAgent && <AgentPreview agent={previewAgent} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:hidden w-full h-full flex flex-col">
|
||||
{ConfigurationContent}
|
||||
|
||||
{/* Mobile Layout */}
|
||||
<div className="md:hidden flex flex-col h-full w-full">
|
||||
{/* Mobile content similar to desktop but with drawer for preview */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Similar content structure as desktop */}
|
||||
</div>
|
||||
|
||||
{/* Mobile Preview Drawer */}
|
||||
<Drawer open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
className="fixed bottom-4 right-4 rounded-full shadow-lg"
|
||||
size="icon"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Agent Preview</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
{previewAgent && <AgentPreview agent={previewAgent} />}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isComparisonOpen} onOpenChange={setIsComparisonOpen}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<VersionComparison
|
||||
agentId={agentId}
|
||||
onClose={() => setIsComparisonOpen(false)}
|
||||
onActivateVersion={handleActivateVersion}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ import {
|
|||
Plus
|
||||
} from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useAgentVersions, useActivateAgentVersion } from '@/hooks/react-query/agents/useAgentVersions';
|
||||
import { useAgentVersions, useActivateAgentVersion } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { Agent } from '@/hooks/react-query/agents/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
|
|
|
@ -389,8 +389,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
|||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 md:pb-4 md:px-12 px-4">
|
||||
<div className="flex-shrink-0 md:pb-4 md:px-6 px-4">
|
||||
<ChatInput
|
||||
ref={chatInputRef}
|
||||
onSubmit={threadId ? handleSubmitMessage : handleSubmitFirstMessage}
|
||||
|
@ -410,7 +409,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
|||
</div>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
// Custom comparison function to prevent unnecessary re-renders
|
||||
|
||||
return (
|
||||
prevProps.agentId === nextProps.agentId &&
|
||||
JSON.stringify(prevProps.formData) === JSON.stringify(nextProps.formData) &&
|
||||
|
|
|
@ -6,15 +6,21 @@ interface AgentMCPConfigurationProps {
|
|||
customMCPs: any[];
|
||||
onMCPChange: (updates: { configured_mcps: any[]; custom_mcps: any[] }) => void;
|
||||
agentId?: string;
|
||||
versionData?: {
|
||||
configured_mcps?: any[];
|
||||
custom_mcps?: any[];
|
||||
system_prompt?: string;
|
||||
agentpress_tools?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const AgentMCPConfiguration: React.FC<AgentMCPConfigurationProps> = ({
|
||||
configuredMCPs,
|
||||
customMCPs,
|
||||
onMCPChange,
|
||||
agentId
|
||||
agentId,
|
||||
versionData
|
||||
}) => {
|
||||
// Combine all MCPs into a single array for the new component
|
||||
const allMCPs = [
|
||||
...(configuredMCPs || []),
|
||||
...(customMCPs || []).map(customMcp => ({
|
||||
|
@ -28,7 +34,6 @@ export const AgentMCPConfiguration: React.FC<AgentMCPConfigurationProps> = ({
|
|||
];
|
||||
|
||||
const handleConfigurationChange = (mcps: any[]) => {
|
||||
// Separate back into configured and custom MCPs
|
||||
const configured = mcps.filter(mcp => !mcp.isCustom);
|
||||
const custom = mcps
|
||||
.filter(mcp => mcp.isCustom)
|
||||
|
@ -40,7 +45,6 @@ export const AgentMCPConfiguration: React.FC<AgentMCPConfigurationProps> = ({
|
|||
enabledTools: mcp.enabledTools
|
||||
}));
|
||||
|
||||
// Call the parent handler with the proper structure
|
||||
onMCPChange({
|
||||
configured_mcps: configured,
|
||||
custom_mcps: custom
|
||||
|
@ -52,6 +56,7 @@ export const AgentMCPConfiguration: React.FC<AgentMCPConfigurationProps> = ({
|
|||
configuredMCPs={allMCPs}
|
||||
onConfigurationChange={handleConfigurationChange}
|
||||
agentId={agentId}
|
||||
versionData={versionData}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,281 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { GitBranch, ChevronDown, Clock, RotateCcw, Check, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useAgentVersions, useActivateAgentVersion, useCreateAgentVersion } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { toast } from 'sonner';
|
||||
import { AgentVersion } from '@/hooks/react-query/agents/utils';
|
||||
|
||||
interface AgentVersionSwitcherProps {
|
||||
agentId: string;
|
||||
currentVersionId?: string | null;
|
||||
currentFormData: {
|
||||
system_prompt: string;
|
||||
configured_mcps: any[];
|
||||
custom_mcps: any[];
|
||||
agentpress_tools: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export function AgentVersionSwitcher({
|
||||
agentId,
|
||||
currentVersionId,
|
||||
currentFormData
|
||||
}: AgentVersionSwitcherProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const versionParam = searchParams.get('version');
|
||||
|
||||
const { data: versions, isLoading } = useAgentVersions(agentId);
|
||||
const activateVersionMutation = useActivateAgentVersion();
|
||||
const createVersionMutation = useCreateAgentVersion();
|
||||
|
||||
const [showRollbackDialog, setShowRollbackDialog] = useState(false);
|
||||
const [selectedVersion, setSelectedVersion] = useState<AgentVersion | null>(null);
|
||||
const [isRollingBack, setIsRollingBack] = useState(false);
|
||||
|
||||
const viewingVersionId = versionParam || currentVersionId;
|
||||
const viewingVersion = versions?.find(v => v.version_id === viewingVersionId) || versions?.[0];
|
||||
|
||||
const canRollback = viewingVersion && viewingVersion.version_number > 1;
|
||||
|
||||
const handleVersionSelect = async (version: AgentVersion) => {
|
||||
if (version.version_id === viewingVersionId) return;
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (version.version_id === currentVersionId) {
|
||||
params.delete('version');
|
||||
} else {
|
||||
params.set('version', version.version_id);
|
||||
}
|
||||
const newUrl = params.toString() ? `?${params.toString()}` : window.location.pathname;
|
||||
router.push(newUrl);
|
||||
if (version.version_id !== currentVersionId) {
|
||||
toast.success(`Viewing ${version.version_name} (read-only)`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRollback = async () => {
|
||||
if (!selectedVersion || !viewingVersion) return;
|
||||
|
||||
setIsRollingBack(true);
|
||||
try {
|
||||
const newVersion = await createVersionMutation.mutateAsync({
|
||||
agentId,
|
||||
data: {
|
||||
system_prompt: selectedVersion.system_prompt,
|
||||
configured_mcps: selectedVersion.configured_mcps,
|
||||
custom_mcps: selectedVersion.custom_mcps,
|
||||
agentpress_tools: selectedVersion.agentpress_tools,
|
||||
description: `Rolled back from ${viewingVersion.version_name} to ${selectedVersion.version_name}`
|
||||
}
|
||||
});
|
||||
await activateVersionMutation.mutateAsync({
|
||||
agentId,
|
||||
versionId: newVersion.version_id
|
||||
});
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('version');
|
||||
const newUrl = params.toString() ? `?${params.toString()}` : window.location.pathname;
|
||||
router.push(newUrl);
|
||||
|
||||
setShowRollbackDialog(false);
|
||||
toast.success(`Rolled back to ${selectedVersion.version_name} configuration`);
|
||||
} catch (error) {
|
||||
console.error('Failed to rollback:', error);
|
||||
toast.error('Failed to rollback version');
|
||||
} finally {
|
||||
setIsRollingBack(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openRollbackDialog = (version: AgentVersion) => {
|
||||
setSelectedVersion(version);
|
||||
setShowRollbackDialog(true);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm text-muted-foreground">Loading versions...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!versions || versions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
{viewingVersion ? (
|
||||
<>
|
||||
{viewingVersion.version_name}
|
||||
{viewingVersionId === currentVersionId && (
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
'Select Version'
|
||||
)}
|
||||
<ChevronDown className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-80">
|
||||
<DropdownMenuLabel>Version History</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{versions.map((version) => {
|
||||
const isViewing = version.version_id === viewingVersionId;
|
||||
const isCurrent = version.version_id === currentVersionId;
|
||||
|
||||
return (
|
||||
<div key={version.version_id} className="relative">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleVersionSelect(version)}
|
||||
className={`cursor-pointer ${isViewing ? 'bg-accent' : ''}`}
|
||||
>
|
||||
<div className="flex items-start justify-between w-full">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{version.version_name}</span>
|
||||
{isCurrent && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
{isViewing && !isCurrent && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Viewing
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(version.created_at), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
{version.change_description && (
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
{version.change_description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isViewing && (
|
||||
<Check className="h-4 w-4 text-primary ml-2" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{!isViewing && version.version_number < (viewingVersion?.version_number || 0) && canRollback && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openRollbackDialog(version);
|
||||
}}
|
||||
className="absolute right-2 top-2"
|
||||
title={`Rollback to ${version.version_name}`}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{versions.length === 1 && (
|
||||
<div className="p-2">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This is the first version. Make changes to create a new version.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Rollback Confirmation Dialog */}
|
||||
<Dialog open={showRollbackDialog} onOpenChange={setShowRollbackDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rollback to {selectedVersion?.version_name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will create a new version with the configuration from {selectedVersion?.version_name}.
|
||||
Your current changes will be preserved in the current version.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Note:</strong> This action will create a new version (v{(versions?.[0]?.version_number || 0) + 1})
|
||||
with the selected configuration. You can always switch back to any previous version.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowRollbackDialog(false)}
|
||||
disabled={isRollingBack}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRollback}
|
||||
disabled={isRollingBack}
|
||||
>
|
||||
{isRollingBack ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Rolling back...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Confirm Rollback
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Save, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useCreateAgentVersion } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CreateVersionButtonProps {
|
||||
agentId: string;
|
||||
currentFormData: {
|
||||
system_prompt: string;
|
||||
configured_mcps: any[];
|
||||
custom_mcps: any[];
|
||||
agentpress_tools: Record<string, any>;
|
||||
};
|
||||
hasChanges: boolean;
|
||||
onVersionCreated?: () => void;
|
||||
}
|
||||
|
||||
export function CreateVersionButton({
|
||||
agentId,
|
||||
currentFormData,
|
||||
hasChanges,
|
||||
onVersionCreated
|
||||
}: CreateVersionButtonProps) {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [versionName, setVersionName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const createVersionMutation = useCreateAgentVersion();
|
||||
|
||||
const handleCreateVersion = async () => {
|
||||
if (!versionName.trim()) {
|
||||
toast.error('Please provide a version name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createVersionMutation.mutateAsync({
|
||||
agentId,
|
||||
data: {
|
||||
system_prompt: currentFormData.system_prompt,
|
||||
configured_mcps: currentFormData.configured_mcps,
|
||||
custom_mcps: currentFormData.custom_mcps,
|
||||
agentpress_tools: currentFormData.agentpress_tools,
|
||||
version_name: versionName.trim(),
|
||||
description: description.trim() || undefined,
|
||||
}
|
||||
});
|
||||
|
||||
setShowDialog(false);
|
||||
setVersionName('');
|
||||
setDescription('');
|
||||
|
||||
if (onVersionCreated) {
|
||||
onVersionCreated();
|
||||
}
|
||||
|
||||
toast.success('New version created successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to create version:', error);
|
||||
toast.error('Failed to create version');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Version</DialogTitle>
|
||||
<DialogDescription>
|
||||
Save the current agent configuration as a new version. This allows you to preserve different configurations and switch between them.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="version-name">Version Name</Label>
|
||||
<Input
|
||||
id="version-name"
|
||||
placeholder="e.g., v2, Production Ready, Beta Features"
|
||||
value={versionName}
|
||||
onChange={(e) => setVersionName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description (Optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="What changes does this version include?"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDialog(false)}
|
||||
disabled={createVersionMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -57,6 +57,10 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
item.mcp_requirements
|
||||
.filter(req => req.custom_type === 'pipedream')
|
||||
.forEach(req => {
|
||||
const app_slug = req.qualified_name.startsWith('pipedream:')
|
||||
? req.qualified_name.substring('pipedream:'.length)
|
||||
: req.qualified_name;
|
||||
|
||||
steps.push({
|
||||
id: req.qualified_name,
|
||||
title: `Connect ${req.display_name}`,
|
||||
|
@ -64,7 +68,7 @@ export const StreamlinedInstallDialog: React.FC<StreamlinedInstallDialogProps> =
|
|||
type: 'pipedream_profile',
|
||||
service_name: req.display_name,
|
||||
qualified_name: req.qualified_name,
|
||||
app_slug: req.qualified_name
|
||||
app_slug: app_slug
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -40,14 +40,16 @@ export const ProfileConnector: React.FC<ProfileConnectorProps> = ({
|
|||
|
||||
const createProfileMutation = useCreateCredentialProfile();
|
||||
const { data: serverDetails } = useMCPServerDetails(step.qualified_name);
|
||||
const { data: pipedreamProfiles } = usePipedreamProfiles();
|
||||
|
||||
|
||||
const isPipedreamStep = step.type === 'pipedream_profile';
|
||||
|
||||
const { data: pipedreamProfiles } = usePipedreamProfiles(
|
||||
isPipedreamStep ? { app_slug: step.app_slug, is_active: true } : undefined
|
||||
);
|
||||
const configProperties = serverDetails?.connections?.[0]?.configSchema?.properties || {};
|
||||
const requiredFields = serverDetails?.connections?.[0]?.configSchema?.required || [];
|
||||
const hasConnectedPipedreamProfile = pipedreamProfiles?.some(p =>
|
||||
p.app_slug === step.app_slug && p.is_connected
|
||||
);
|
||||
|
||||
const hasConnectedPipedreamProfile = pipedreamProfiles?.some(p => p.is_connected) || false;
|
||||
|
||||
useEffect(() => {
|
||||
setProfileStep('select');
|
||||
|
|
|
@ -11,7 +11,8 @@ import { ToolsManager } from './tools-manager';
|
|||
export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
||||
configuredMCPs,
|
||||
onConfigurationChange,
|
||||
agentId
|
||||
agentId,
|
||||
versionData
|
||||
}) => {
|
||||
const [showCustomDialog, setShowCustomDialog] = useState(false);
|
||||
const [showRegistryDialog, setShowRegistryDialog] = useState(false);
|
||||
|
@ -153,21 +154,12 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
|
||||
{configuredMCPs.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-card rounded-xl border border-border overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-border bg-muted/30">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
Configured Integrations
|
||||
</h4>
|
||||
</div>
|
||||
<div className="p-2 divide-y divide-border">
|
||||
<ConfiguredMcpList
|
||||
configuredMCPs={configuredMCPs}
|
||||
onEdit={handleEditMCP}
|
||||
onRemove={handleRemoveMCP}
|
||||
onConfigureTools={handleConfigureTools}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConfiguredMcpList
|
||||
configuredMCPs={configuredMCPs}
|
||||
onEdit={handleEditMCP}
|
||||
onRemove={handleRemoveMCP}
|
||||
onConfigureTools={handleConfigureTools}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -192,7 +184,7 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Select Integration</DialogTitle>
|
||||
</DialogHeader>
|
||||
<PipedreamRegistry showAgentSelector={false} selectedAgentId={selectedAgentId} onAgentChange={handleAgentChange} onToolsSelected={handleToolsSelected} />
|
||||
<PipedreamRegistry showAgentSelector={false} selectedAgentId={selectedAgentId} onAgentChange={handleAgentChange} onToolsSelected={handleToolsSelected} versionData={versionData} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<CustomMCPDialog
|
||||
|
@ -209,6 +201,7 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
open={showPipedreamToolsManager}
|
||||
onOpenChange={setShowPipedreamToolsManager}
|
||||
onToolsUpdate={handlePipedreamToolsUpdate}
|
||||
versionData={versionData}
|
||||
/>
|
||||
)}
|
||||
{selectedMCPForTools && selectedMCPForTools.customType !== 'pipedream' && (
|
||||
|
@ -220,6 +213,7 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
|
|||
open={showCustomToolsManager}
|
||||
onOpenChange={setShowCustomToolsManager}
|
||||
onToolsUpdate={handleCustomToolsUpdate}
|
||||
versionData={versionData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -32,6 +32,12 @@ interface BaseToolsManagerProps {
|
|||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onToolsUpdate?: (enabledTools: string[]) => void;
|
||||
versionData?: {
|
||||
configured_mcps?: any[];
|
||||
custom_mcps?: any[];
|
||||
system_prompt?: string;
|
||||
agentpress_tools?: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface PipedreamToolsManagerProps extends BaseToolsManagerProps {
|
||||
|
@ -50,16 +56,16 @@ interface CustomToolsManagerProps extends BaseToolsManagerProps {
|
|||
type ToolsManagerProps = PipedreamToolsManagerProps | CustomToolsManagerProps;
|
||||
|
||||
export const ToolsManager: React.FC<ToolsManagerProps> = (props) => {
|
||||
const { agentId, open, onOpenChange, onToolsUpdate, mode } = props;
|
||||
const { agentId, open, onOpenChange, onToolsUpdate, mode, versionData } = props;
|
||||
|
||||
const pipedreamResult = usePipedreamToolsData(
|
||||
mode === 'pipedream' ? agentId : '',
|
||||
mode === 'pipedream' ? props.profileId : ''
|
||||
mode === 'pipedream' ? (props as PipedreamToolsManagerProps).profileId : ''
|
||||
);
|
||||
|
||||
const customResult = useCustomMCPToolsData(
|
||||
mode === 'custom' ? agentId : '',
|
||||
mode === 'custom' ? props.mcpConfig : null
|
||||
mode === 'custom' ? (props as CustomToolsManagerProps).mcpConfig : null
|
||||
);
|
||||
|
||||
const result = mode === 'pipedream' ? pipedreamResult : customResult;
|
||||
|
@ -68,16 +74,48 @@ export const ToolsManager: React.FC<ToolsManagerProps> = (props) => {
|
|||
const [localTools, setLocalTools] = useState<Record<string, boolean>>({});
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
// Helper function to get version-specific enabled tools
|
||||
const getVersionEnabledTools = (): string[] => {
|
||||
if (!versionData) return [];
|
||||
|
||||
if (mode === 'pipedream') {
|
||||
const customMcps = versionData.custom_mcps || [];
|
||||
const pipedreamMcp = customMcps.find((mcp: any) =>
|
||||
mcp.config?.profile_id === (props as PipedreamToolsManagerProps).profileId &&
|
||||
mcp.config?.url?.includes('pipedream')
|
||||
);
|
||||
return pipedreamMcp?.enabledTools || [];
|
||||
} else {
|
||||
const customMcps = versionData.custom_mcps || [];
|
||||
const customMcp = customMcps.find((mcp: any) =>
|
||||
mcp.config?.url === (props as CustomToolsManagerProps).mcpConfig?.url
|
||||
);
|
||||
return customMcp?.enabledTools || [];
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data?.tools) {
|
||||
const toolsMap: Record<string, boolean> = {};
|
||||
data.tools.forEach((tool: { name: string; enabled: boolean }) => {
|
||||
toolsMap[tool.name] = tool.enabled;
|
||||
});
|
||||
|
||||
if (versionData) {
|
||||
// When viewing a version, use the version's enabled tools
|
||||
const versionEnabledTools = getVersionEnabledTools();
|
||||
data.tools.forEach((tool: { name: string; enabled: boolean }) => {
|
||||
toolsMap[tool.name] = versionEnabledTools.includes(tool.name);
|
||||
});
|
||||
} else {
|
||||
// Normal case: use current agent data
|
||||
data.tools.forEach((tool: { name: string; enabled: boolean }) => {
|
||||
toolsMap[tool.name] = tool.enabled;
|
||||
});
|
||||
}
|
||||
|
||||
setLocalTools(toolsMap);
|
||||
setHasChanges(false);
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, versionData, mode,
|
||||
mode === 'pipedream' ? (props as PipedreamToolsManagerProps).profileId : (props as CustomToolsManagerProps).mcpConfig]);
|
||||
|
||||
const enabledCount = useMemo(() => {
|
||||
return Object.values(localTools).filter(Boolean).length;
|
||||
|
@ -85,20 +123,27 @@ export const ToolsManager: React.FC<ToolsManagerProps> = (props) => {
|
|||
|
||||
const totalCount = data?.tools?.length || 0;
|
||||
|
||||
const displayName = mode === 'pipedream' ? props.appName : props.mcpName;
|
||||
const contextName = mode === 'pipedream' ? props.profileName || 'Profile' : 'Server';
|
||||
const displayName = mode === 'pipedream' ? (props as PipedreamToolsManagerProps).appName : (props as CustomToolsManagerProps).mcpName;
|
||||
const contextName = mode === 'pipedream' ? (props as PipedreamToolsManagerProps).profileName || 'Profile' : 'Server';
|
||||
|
||||
const handleToolToggle = (toolName: string) => {
|
||||
setLocalTools(prev => {
|
||||
const newValue = !prev[toolName];
|
||||
const updated = { ...prev, [toolName]: newValue };
|
||||
const serverTools: Record<string, boolean> = {};
|
||||
if (data?.tools) {
|
||||
for (const tool of data.tools) {
|
||||
serverTools[tool.name] = tool.enabled;
|
||||
}
|
||||
|
||||
const comparisonState: Record<string, boolean> = {};
|
||||
if (versionData) {
|
||||
const versionEnabledTools = getVersionEnabledTools();
|
||||
data?.tools?.forEach((tool: any) => {
|
||||
comparisonState[tool.name] = versionEnabledTools.includes(tool.name);
|
||||
});
|
||||
} else {
|
||||
data?.tools?.forEach((tool: any) => {
|
||||
comparisonState[tool.name] = tool.enabled;
|
||||
});
|
||||
}
|
||||
const hasChanges = Object.keys(updated).some(key => updated[key] !== serverTools[key]);
|
||||
|
||||
const hasChanges = Object.keys(updated).some(key => updated[key] !== comparisonState[key]);
|
||||
setHasChanges(hasChanges);
|
||||
return updated;
|
||||
});
|
||||
|
@ -131,9 +176,20 @@ export const ToolsManager: React.FC<ToolsManagerProps> = (props) => {
|
|||
const handleCancel = () => {
|
||||
if (data?.tools) {
|
||||
const serverState: Record<string, boolean> = {};
|
||||
data.tools.forEach((tool: any) => {
|
||||
serverState[tool.name] = tool.enabled;
|
||||
});
|
||||
|
||||
if (versionData) {
|
||||
// When viewing a version, reset to version state
|
||||
const versionEnabledTools = getVersionEnabledTools();
|
||||
data.tools.forEach((tool: any) => {
|
||||
serverState[tool.name] = versionEnabledTools.includes(tool.name);
|
||||
});
|
||||
} else {
|
||||
// Normal case: reset to current server state
|
||||
data.tools.forEach((tool: any) => {
|
||||
serverState[tool.name] = tool.enabled;
|
||||
});
|
||||
}
|
||||
|
||||
setLocalTools(serverState);
|
||||
setHasChanges(false);
|
||||
}
|
||||
|
@ -183,7 +239,16 @@ export const ToolsManager: React.FC<ToolsManagerProps> = (props) => {
|
|||
Configure {displayName} Tools
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose which {displayName} tools are available to your agent
|
||||
{versionData ? (
|
||||
<div className="flex items-center gap-2 text-amber-600">
|
||||
<Info className="h-4 w-4" />
|
||||
<span>
|
||||
Viewing tools configuration for a specific version. Changes will update the current version.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>Choose which {displayName} tools are available to your agent</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
@ -220,7 +285,7 @@ export const ToolsManager: React.FC<ToolsManagerProps> = (props) => {
|
|||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{contextName}: {mode === 'pipedream' ? props.profileName : displayName}
|
||||
{contextName}: {mode === 'pipedream' ? (props as PipedreamToolsManagerProps).profileName : displayName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,4 +12,10 @@ export interface MCPConfigurationProps {
|
|||
configuredMCPs: MCPConfiguration[];
|
||||
onConfigurationChange: (mcps: MCPConfiguration[]) => void;
|
||||
agentId?: string;
|
||||
versionData?: {
|
||||
configured_mcps?: any[];
|
||||
custom_mcps?: any[];
|
||||
system_prompt?: string;
|
||||
agentpress_tools?: any;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,45 +1,9 @@
|
|||
export const categoryEmojis: Record<string, string> = {
|
||||
'All': '🌟',
|
||||
'Communication': '💬',
|
||||
'Artificial Intelligence (AI)': '🤖',
|
||||
'Social Media': '📱',
|
||||
'CRM': '👥',
|
||||
'Marketing': '📈',
|
||||
'Analytics': '📊',
|
||||
'Commerce': '📊',
|
||||
'Databases': '🗄️',
|
||||
'File Storage': '🗂️',
|
||||
'Help Desk & Support': '🎧',
|
||||
'Infrastructure & Cloud': '🌐',
|
||||
'E-commerce': '🛒',
|
||||
'Developer Tools': '🔧',
|
||||
'Web & App Development': '🌐',
|
||||
'Business Management': '💼',
|
||||
'Productivity': '⚡',
|
||||
'Finance': '💰',
|
||||
'Email': '📧',
|
||||
'Project Management': '📋',
|
||||
'Storage': '💾',
|
||||
'AI/ML': '🤖',
|
||||
'Data & Databases': '🗄️',
|
||||
'Video': '🎥',
|
||||
'Calendar': '📅',
|
||||
'Forms': '📝',
|
||||
'Security': '🔒',
|
||||
'HR': '👔',
|
||||
'Sales': '💼',
|
||||
'Support': '🎧',
|
||||
'Design': '🎨',
|
||||
'Business Intelligence': '📈',
|
||||
'Automation': '🔄',
|
||||
'News': '📰',
|
||||
'Weather': '🌤️',
|
||||
'Travel': '✈️',
|
||||
'Education': '🎓',
|
||||
'Health': '🏥',
|
||||
'Popular': '🔥',
|
||||
};
|
||||
|
||||
export const PAGINATION_CONSTANTS = {
|
||||
FIRST_PAGE: 'FIRST_PAGE',
|
||||
POPULAR_APPS_COUNT: 6,
|
||||
POPULAR_APPS_COUNT: 10,
|
||||
} as const;
|
|
@ -40,6 +40,7 @@ interface PipedreamConnectorProps {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
onComplete: (profileId: string, selectedTools: string[], appName: string, appSlug: string) => void;
|
||||
mode?: 'full' | 'profile-only';
|
||||
agentId?: string; // For backend auto-versioning
|
||||
}
|
||||
|
||||
interface PipedreamTool {
|
||||
|
@ -52,7 +53,8 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
open,
|
||||
onOpenChange,
|
||||
onComplete,
|
||||
mode = 'full'
|
||||
mode = 'full',
|
||||
agentId
|
||||
}) => {
|
||||
const [step, setStep] = useState<'profile' | 'tools'>('profile');
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string>('');
|
||||
|
@ -110,7 +112,6 @@ export const PipedreamConnector: React.FC<PipedreamConnectorProps> = ({
|
|||
|
||||
const newProfile = await createProfile.mutateAsync(request);
|
||||
|
||||
// Connect the profile
|
||||
await connectProfile.mutateAsync({
|
||||
profileId: newProfile.profile_id,
|
||||
app: app.name_slug,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { X, Bot } from 'lucide-react';
|
||||
import { usePipedreamApps } from '@/hooks/react-query/pipedream/use-pipedream';
|
||||
import { usePipedreamApps, usePipedreamPopularApps } from '@/hooks/react-query/pipedream/use-pipedream';
|
||||
import { usePipedreamProfiles } from '@/hooks/react-query/pipedream/use-pipedream-profiles';
|
||||
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { PipedreamConnector } from './pipedream-connector';
|
||||
|
@ -20,7 +20,7 @@ import {
|
|||
} from './_components';
|
||||
import { PAGINATION_CONSTANTS } from './constants';
|
||||
import {
|
||||
getCategoriesFromApps,
|
||||
getSimplifiedCategories,
|
||||
createConnectedAppsFromProfiles,
|
||||
getAgentPipedreamProfiles,
|
||||
filterAppsByCategory
|
||||
|
@ -34,7 +34,8 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
onClose,
|
||||
showAgentSelector = false,
|
||||
selectedAgentId,
|
||||
onAgentChange
|
||||
onAgentChange,
|
||||
versionData
|
||||
}) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
||||
|
@ -54,6 +55,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: appsData, isLoading, error, refetch } = usePipedreamApps(after, search);
|
||||
const { data: popularAppsData, isLoading: isLoadingPopular } = usePipedreamPopularApps();
|
||||
const { data: profiles } = usePipedreamProfiles();
|
||||
|
||||
const currentAgentId = selectedAgentId ?? internalSelectedAgentId;
|
||||
|
@ -84,18 +86,25 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
}, [allAppsData?.apps]);
|
||||
|
||||
const agentPipedreamProfiles = useMemo(() => {
|
||||
return getAgentPipedreamProfiles(agent, profiles, currentAgentId);
|
||||
}, [agent, profiles, currentAgentId]);
|
||||
return getAgentPipedreamProfiles(agent, profiles, currentAgentId, versionData);
|
||||
}, [agent, profiles, currentAgentId, versionData]);
|
||||
|
||||
const categories = useMemo(() => {
|
||||
return getCategoriesFromApps(allApps);
|
||||
}, [allApps]);
|
||||
return getSimplifiedCategories();
|
||||
}, []);
|
||||
|
||||
const connectedProfiles = useMemo(() => {
|
||||
return profiles?.filter(p => p.is_connected) || [];
|
||||
}, [profiles]);
|
||||
|
||||
const filteredAppsData = useMemo(() => {
|
||||
if (selectedCategory === 'Popular') {
|
||||
return popularAppsData ? {
|
||||
...popularAppsData,
|
||||
apps: popularAppsData.apps || []
|
||||
} : undefined;
|
||||
}
|
||||
|
||||
if (!appsData) return appsData;
|
||||
|
||||
if (selectedCategory === 'All') {
|
||||
|
@ -112,7 +121,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
count: filteredApps.length
|
||||
}
|
||||
};
|
||||
}, [appsData, selectedCategory]);
|
||||
}, [appsData, popularAppsData, selectedCategory]);
|
||||
|
||||
const connectedApps: ConnectedApp[] = useMemo(() => {
|
||||
return createConnectedAppsFromProfiles(connectedProfiles, allApps);
|
||||
|
@ -128,6 +137,9 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
setSelectedCategory(category);
|
||||
setAfter(undefined);
|
||||
setPaginationHistory([]);
|
||||
if (category === 'Popular' && search) {
|
||||
setSearch('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
|
@ -271,7 +283,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
apps={filteredAppsData.apps}
|
||||
selectedCategory={selectedCategory}
|
||||
mode={mode}
|
||||
isLoading={isLoading}
|
||||
isLoading={selectedCategory === 'Popular' ? isLoadingPopular : isLoading}
|
||||
currentAgentId={currentAgentId}
|
||||
agent={agent}
|
||||
agentPipedreamProfiles={agentPipedreamProfiles}
|
||||
|
@ -280,7 +292,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
onConfigureTools={handleConfigureTools}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
/>
|
||||
) : !isLoading ? (
|
||||
) : !(selectedCategory === 'Popular' ? isLoadingPopular : isLoading) ? (
|
||||
<EmptyState
|
||||
selectedCategory={selectedCategory}
|
||||
mode={mode}
|
||||
|
@ -291,7 +303,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
apps={[]}
|
||||
selectedCategory={selectedCategory}
|
||||
mode={mode}
|
||||
isLoading={isLoading}
|
||||
isLoading={selectedCategory === 'Popular' ? isLoadingPopular : isLoading}
|
||||
currentAgentId={currentAgentId}
|
||||
agent={agent}
|
||||
agentPipedreamProfiles={agentPipedreamProfiles}
|
||||
|
@ -324,6 +336,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
onOpenChange={setShowStreamlinedConnector}
|
||||
onComplete={handleConnectionComplete}
|
||||
mode={mode === 'profile-only' ? 'profile-only' : 'full'}
|
||||
agentId={currentAgentId}
|
||||
/>
|
||||
)}
|
||||
{selectedToolsProfile && currentAgentId && (
|
||||
|
@ -343,6 +356,7 @@ export const PipedreamRegistry: React.FC<PipedreamRegistryProps> = ({
|
|||
onToolsUpdate={(enabledTools) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', currentAgentId] });
|
||||
}}
|
||||
versionData={versionData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -15,6 +15,12 @@ export interface PipedreamRegistryProps {
|
|||
showAgentSelector?: boolean;
|
||||
selectedAgentId?: string;
|
||||
onAgentChange?: (agentId: string | undefined) => void;
|
||||
versionData?: {
|
||||
configured_mcps?: any[];
|
||||
custom_mcps?: any[];
|
||||
system_prompt?: string;
|
||||
agentpress_tools?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppCardProps {
|
||||
|
|
|
@ -3,13 +3,8 @@ import type { PipedreamProfile } from '@/components/agents/pipedream/pipedream-t
|
|||
import { categoryEmojis, PAGINATION_CONSTANTS } from './constants';
|
||||
import type { ConnectedApp } from './types';
|
||||
|
||||
export const getCategoriesFromApps = (apps: PipedreamApp[]) => {
|
||||
const categorySet = new Set<string>();
|
||||
apps.forEach((app) => {
|
||||
app.categories.forEach(cat => categorySet.add(cat));
|
||||
});
|
||||
const sortedCategories = Array.from(categorySet).sort();
|
||||
return ['All', ...sortedCategories];
|
||||
export const getSimplifiedCategories = () => {
|
||||
return ['All', 'Popular'];
|
||||
};
|
||||
|
||||
export const getPopularApps = (apps: PipedreamApp[]) => {
|
||||
|
@ -74,11 +69,18 @@ export const createConnectedAppsFromProfiles = (
|
|||
export const getAgentPipedreamProfiles = (
|
||||
agent: any,
|
||||
profiles: PipedreamProfile[],
|
||||
currentAgentId?: string
|
||||
currentAgentId?: string,
|
||||
versionData?: {
|
||||
configured_mcps?: any[];
|
||||
custom_mcps?: any[];
|
||||
system_prompt?: string;
|
||||
agentpress_tools?: any;
|
||||
}
|
||||
) => {
|
||||
if (!agent || !profiles || !currentAgentId) return [];
|
||||
|
||||
const customMcps = agent.custom_mcps || [];
|
||||
// Use version data if available, otherwise use agent data
|
||||
const customMcps = versionData?.custom_mcps || agent.custom_mcps || [];
|
||||
const pipedreamMcps = customMcps.filter((mcp: any) =>
|
||||
mcp.config?.profile_id && mcp.config?.url?.includes('pipedream')
|
||||
);
|
||||
|
|
|
@ -1563,7 +1563,7 @@ export const StylePicker = ({
|
|||
currentColor,
|
||||
onStyleChange
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
agentId: string;
|
||||
currentEmoji?: string;
|
||||
currentColor?: string;
|
||||
|
@ -1639,30 +1639,9 @@ export const StylePicker = ({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={selectedColor}
|
||||
onChange={(e) => setSelectedColor(e.target.value)}
|
||||
className="w-8 h-8 rounded border cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedColor}
|
||||
onChange={(e) => setSelectedColor(e.target.value)}
|
||||
className="flex-1 px-2 py-1 text-sm border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{selectedEmoji}</span>
|
||||
<span className="font-medium">Emoji</span>
|
||||
</div>
|
||||
{!searchTerm && (
|
||||
<Tabs value={activeCategory} onValueChange={setActiveCategory} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4 h-auto p-1">
|
||||
|
@ -1700,20 +1679,18 @@ export const StylePicker = ({
|
|||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{searchTerm && filteredEmojis.length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-4">No emojis found for "{searchTerm}"</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Loader2, GitBranch, ArrowLeftRight, Check, X, Plus, Minus, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { useAgentVersions } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { AgentVersion } from '@/hooks/react-query/agents/utils';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
interface VersionComparisonProps {
|
||||
agentId: string;
|
||||
onClose?: () => void;
|
||||
onActivateVersion?: (versionId: string) => void;
|
||||
}
|
||||
|
||||
interface VersionDifference {
|
||||
field: string;
|
||||
type: 'added' | 'removed' | 'modified';
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
}
|
||||
|
||||
export function VersionComparison({ agentId, onClose, onActivateVersion }: VersionComparisonProps) {
|
||||
const { data: versions = [], isLoading } = useAgentVersions(agentId);
|
||||
const [version1Id, setVersion1Id] = useState<string>('');
|
||||
const [version2Id, setVersion2Id] = useState<string>('');
|
||||
|
||||
const version1 = versions.find(v => v.version_id === version1Id);
|
||||
const version2 = versions.find(v => v.version_id === version2Id);
|
||||
|
||||
const differences = useMemo(() => {
|
||||
if (!version1 || !version2) return [];
|
||||
|
||||
const diffs: VersionDifference[] = [];
|
||||
|
||||
// Compare system prompts
|
||||
if (version1.system_prompt !== version2.system_prompt) {
|
||||
diffs.push({
|
||||
field: 'System Prompt',
|
||||
type: 'modified',
|
||||
oldValue: version1.system_prompt,
|
||||
newValue: version2.system_prompt
|
||||
});
|
||||
}
|
||||
|
||||
// Compare tools
|
||||
const tools1 = new Set(Object.keys(version1.agentpress_tools || {}));
|
||||
const tools2 = new Set(Object.keys(version2.agentpress_tools || {}));
|
||||
|
||||
// Added tools
|
||||
for (const tool of tools2) {
|
||||
if (!tools1.has(tool)) {
|
||||
diffs.push({
|
||||
field: `Tool: ${tool}`,
|
||||
type: 'added',
|
||||
newValue: version2.agentpress_tools[tool]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Removed tools
|
||||
for (const tool of tools1) {
|
||||
if (!tools2.has(tool)) {
|
||||
diffs.push({
|
||||
field: `Tool: ${tool}`,
|
||||
type: 'removed',
|
||||
oldValue: version1.agentpress_tools[tool]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Modified tools
|
||||
for (const tool of tools1) {
|
||||
if (tools2.has(tool) &&
|
||||
JSON.stringify(version1.agentpress_tools[tool]) !== JSON.stringify(version2.agentpress_tools[tool])) {
|
||||
diffs.push({
|
||||
field: `Tool: ${tool}`,
|
||||
type: 'modified',
|
||||
oldValue: version1.agentpress_tools[tool],
|
||||
newValue: version2.agentpress_tools[tool]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compare MCPs
|
||||
const mcps1Count = (version1.configured_mcps?.length || 0) + (version1.custom_mcps?.length || 0);
|
||||
const mcps2Count = (version2.configured_mcps?.length || 0) + (version2.custom_mcps?.length || 0);
|
||||
|
||||
if (mcps1Count !== mcps2Count) {
|
||||
diffs.push({
|
||||
field: 'MCP Integrations',
|
||||
type: 'modified',
|
||||
oldValue: `${mcps1Count} integrations`,
|
||||
newValue: `${mcps2Count} integrations`
|
||||
});
|
||||
}
|
||||
|
||||
return diffs;
|
||||
}, [version1, version2]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-5 w-5" />
|
||||
<h2 className="text-2xl font-semibold">Compare Versions</h2>
|
||||
</div>
|
||||
{onClose && (
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Version 1</Label>
|
||||
<Select value={version1Id} onValueChange={setVersion1Id}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versions.map((version) => (
|
||||
<SelectItem key={version.version_id} value={version.version_id}>
|
||||
{version.version_name} - {new Date(version.created_at).toLocaleDateString()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Version 2</Label>
|
||||
<Select value={version2Id} onValueChange={setVersion2Id}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versions.map((version) => (
|
||||
<SelectItem key={version.version_id} value={version.version_id}>
|
||||
{version.version_name} - {new Date(version.created_at).toLocaleDateString()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{version1 && version2 && (
|
||||
<div className="space-y-4">
|
||||
{differences.length === 0 ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
These versions are identical.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Tabs defaultValue="summary">
|
||||
<TabsList>
|
||||
<TabsTrigger value="summary">Summary</TabsTrigger>
|
||||
<TabsTrigger value="system-prompt">System Prompt</TabsTrigger>
|
||||
<TabsTrigger value="tools">Tools</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="summary">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Differences Summary</CardTitle>
|
||||
<CardDescription>
|
||||
{differences.length} difference{differences.length !== 1 ? 's' : ''} found
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-96">
|
||||
<div className="space-y-2">
|
||||
{differences.map((diff, index) => (
|
||||
<div key={index} className="flex items-center gap-2 p-2 rounded-lg hover:bg-muted">
|
||||
{diff.type === 'added' && <Plus className="h-4 w-4 text-green-500" />}
|
||||
{diff.type === 'removed' && <Minus className="h-4 w-4 text-red-500" />}
|
||||
{diff.type === 'modified' && <RefreshCw className="h-4 w-4 text-yellow-500" />}
|
||||
<span className="font-medium">{diff.field}</span>
|
||||
{diff.type === 'modified' && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{diff.oldValue} → {diff.newValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="system-prompt">
|
||||
{version1.system_prompt !== version2.system_prompt ? (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="mb-2">Version 1: {version1.version_name}</Label>
|
||||
<Textarea
|
||||
value={version1.system_prompt}
|
||||
readOnly
|
||||
className="h-96 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-2">Version 2: {version2.version_name}</Label>
|
||||
<Textarea
|
||||
value={version2.system_prompt}
|
||||
readOnly
|
||||
className="h-96 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
System prompts are identical.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tools">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tool Differences</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{differences
|
||||
.filter(diff => diff.field.startsWith('Tool:'))
|
||||
.map((diff, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
{diff.type === 'added' && <Plus className="h-4 w-4 text-green-500" />}
|
||||
{diff.type === 'removed' && <Minus className="h-4 w-4 text-red-500" />}
|
||||
{diff.type === 'modified' && <RefreshCw className="h-4 w-4 text-yellow-500" />}
|
||||
<span className="font-medium">{diff.field.replace('Tool: ', '')}</span>
|
||||
</div>
|
||||
<Badge variant={diff.type === 'added' ? 'default' : diff.type === 'removed' ? 'destructive' : 'secondary'}>
|
||||
{diff.type}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{onActivateVersion && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onActivateVersion(version1Id)}
|
||||
disabled={version1.is_active}
|
||||
>
|
||||
Activate {version1.version_name}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onActivateVersion(version2Id)}
|
||||
disabled={version2.is_active}
|
||||
>
|
||||
Activate {version2.version_name}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -133,18 +133,14 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
|||
clearPendingFiles: () => setPendingFiles([]),
|
||||
}));
|
||||
|
||||
// Load saved agent from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && onAgentSelect && !hasLoadedFromLocalStorage.current) {
|
||||
// Don't load from localStorage if an agent is already selected
|
||||
// or if there are URL parameters that might be setting the agent
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const hasAgentIdInUrl = urlParams.has('agent_id');
|
||||
|
||||
if (!selectedAgentId && !hasAgentIdInUrl) {
|
||||
const savedAgentId = localStorage.getItem('lastSelectedAgentId');
|
||||
if (savedAgentId) {
|
||||
// Convert 'suna' back to undefined for the default agent
|
||||
const agentIdToSelect = savedAgentId === 'suna' ? undefined : savedAgentId;
|
||||
console.log('Loading saved agent from localStorage:', savedAgentId);
|
||||
onAgentSelect(agentIdToSelect);
|
||||
|
@ -367,7 +363,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
|||
|
||||
{enableAdvancedConfig && selectedAgentId && (
|
||||
<div className="w-full border-t border-border/30 bg-muted/20 px-4 py-1.5 rounded-b-3xl border-l border-r border-b border-border">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 sm:gap-2 overflow-x-auto scrollbar-none">
|
||||
<button
|
||||
onClick={() => setRegistryDialogOpen(true)}
|
||||
|
|
|
@ -12,6 +12,7 @@ interface EditableTextProps {
|
|||
placeholder?: string;
|
||||
multiline?: boolean;
|
||||
minHeight?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const EditableText: React.FC<EditableTextProps> = ({
|
||||
|
@ -20,7 +21,8 @@ export const EditableText: React.FC<EditableTextProps> = ({
|
|||
className = '',
|
||||
placeholder = 'Click to edit...',
|
||||
multiline = false,
|
||||
minHeight = 'auto'
|
||||
minHeight = 'auto',
|
||||
disabled = false
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(value);
|
||||
|
@ -71,6 +73,7 @@ export const EditableText: React.FC<EditableTextProps> = ({
|
|||
lineHeight: 'inherit',
|
||||
...(multiline && minHeight ? { minHeight } : {})
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ interface ExpandableMarkdownEditorProps {
|
|||
className?: string;
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ExpandableMarkdownEditor: React.FC<ExpandableMarkdownEditorProps> = ({
|
||||
|
@ -21,7 +22,8 @@ export const ExpandableMarkdownEditor: React.FC<ExpandableMarkdownEditorProps> =
|
|||
onSave,
|
||||
className = '',
|
||||
placeholder = 'Click to edit...',
|
||||
title = 'Edit Instructions'
|
||||
title = 'Edit Instructions',
|
||||
disabled = false
|
||||
}) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
@ -175,6 +177,7 @@ export const ExpandableMarkdownEditor: React.FC<ExpandableMarkdownEditorProps> =
|
|||
onKeyDown={handleKeyDown}
|
||||
className="w-full h-[500px] rounded-xl bg-muted/30 p-4 resize-none"
|
||||
style={{ minHeight: '300px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ScrollArea>
|
||||
<div className="text-xs text-muted-foreground/50 flex-shrink-0">
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
AgentVersion,
|
||||
AgentVersionCreateRequest
|
||||
} from './utils';
|
||||
import { agentKeys } from './keys';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const useAgentVersions = (agentId: string) => {
|
||||
|
@ -17,10 +18,10 @@ export const useAgentVersions = (agentId: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useAgentVersion = (agentId: string, versionId: string) => {
|
||||
export const useAgentVersion = (agentId: string, versionId: string | null | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: ['agent-version', agentId, versionId],
|
||||
queryFn: () => getAgentVersion(agentId, versionId),
|
||||
queryFn: () => getAgentVersion(agentId, versionId!),
|
||||
enabled: !!agentId && !!versionId,
|
||||
});
|
||||
};
|
||||
|
@ -34,6 +35,7 @@ export const useCreateAgentVersion = () => {
|
|||
onSuccess: (newVersion, { agentId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-versions', agentId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', agentId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
toast.success(`Created version ${newVersion.version_name}`);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
|
@ -51,6 +53,7 @@ export const useActivateAgentVersion = () => {
|
|||
onSuccess: (_, { agentId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agent', agentId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-versions', agentId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['agents'] });
|
||||
toast.success('Version activated successfully');
|
||||
},
|
||||
onError: (error: Error) => {
|
|
@ -29,6 +29,9 @@ export type Agent = {
|
|||
updated_at: string;
|
||||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
current_version_id?: string | null;
|
||||
version_count?: number;
|
||||
current_version?: AgentVersion | null;
|
||||
};
|
||||
|
||||
export type PaginationInfo = {
|
||||
|
@ -92,6 +95,8 @@ export type AgentVersionCreateRequest = {
|
|||
enabledTools: string[];
|
||||
}>;
|
||||
agentpress_tools?: Record<string, any>;
|
||||
version_name?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type AgentVersion = {
|
||||
|
@ -107,6 +112,7 @@ export type AgentVersion = {
|
|||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
change_description?: string;
|
||||
};
|
||||
|
||||
export type AgentUpdateRequest = {
|
||||
|
@ -125,6 +131,8 @@ export type AgentUpdateRequest = {
|
|||
}>;
|
||||
agentpress_tools?: Record<string, any>;
|
||||
is_default?: boolean;
|
||||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
};
|
||||
|
||||
export const getAgents = async (params: AgentsParams = {}): Promise<AgentsResponse> => {
|
||||
|
|
|
@ -8,6 +8,7 @@ export const pipedreamKeys = {
|
|||
workflowRuns: (workflowId: string) => [...pipedreamKeys.all, 'workflow-runs', workflowId] as const,
|
||||
apps: (page: number, search?: string, category?: string) => [...pipedreamKeys.all, 'apps', page, search || '', category || ''] as const,
|
||||
appsSearch: (query: string, page: number, category?: string) => [...pipedreamKeys.all, 'apps', 'search', query, page, category || ''] as const,
|
||||
popularApps: () => [...pipedreamKeys.all, 'apps', 'popular'] as const,
|
||||
availableTools: () => [...pipedreamKeys.all, 'available-tools'] as const,
|
||||
mcpDiscovery: (options?: { app_slug?: string; oauth_app_id?: string; custom?: boolean }) =>
|
||||
[...pipedreamKeys.all, 'mcp-discovery', options?.app_slug, options?.oauth_app_id, options?.custom] as const,
|
||||
|
|
|
@ -62,6 +62,17 @@ export const usePipedreamApps = (after?: string, search?: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const usePipedreamPopularApps = () => {
|
||||
return useQuery({
|
||||
queryKey: pipedreamKeys.popularApps(),
|
||||
queryFn: async (): Promise<PipedreamAppResponse> => {
|
||||
return await pipedreamApi.getPopularApps();
|
||||
},
|
||||
staleTime: 30 * 60 * 1000,
|
||||
retry: 2,
|
||||
});
|
||||
};
|
||||
|
||||
export const usePipedreamAvailableTools = createQueryHook(
|
||||
pipedreamKeys.availableTools(),
|
||||
async (forceRefresh: boolean = false): Promise<PipedreamToolsResponse> => {
|
||||
|
|
|
@ -209,6 +209,24 @@ export const pipedreamApi = {
|
|||
return data;
|
||||
},
|
||||
|
||||
async getPopularApps(): Promise<PipedreamAppResponse> {
|
||||
const result = await backendApi.get<PipedreamAppResponse>(
|
||||
'/pipedream/apps/popular',
|
||||
{
|
||||
errorContext: { operation: 'load popular apps', resource: 'Pipedream popular apps' },
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error?.message || 'Failed to get popular apps');
|
||||
}
|
||||
const data = result.data!;
|
||||
if (!data.success && data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
async getAvailableTools(): Promise<PipedreamToolsResponse> {
|
||||
const result = await backendApi.get<PipedreamToolsResponse>(
|
||||
'/pipedream/mcp/available-tools',
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import { useMemo, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useAgentVersion } from '@/hooks/react-query/agents/use-agent-versions';
|
||||
import { useAgentVersionStore } from '@/lib/stores/agent-version-store';
|
||||
|
||||
interface NormalizedMCP {
|
||||
name: string;
|
||||
type: string;
|
||||
customType: string;
|
||||
config: Record<string, any>;
|
||||
enabledTools: string[];
|
||||
}
|
||||
|
||||
interface NormalizedVersionData {
|
||||
version_id: string;
|
||||
agent_id: string;
|
||||
version_number: number;
|
||||
version_name: string;
|
||||
system_prompt: string;
|
||||
configured_mcps: any[];
|
||||
custom_mcps: NormalizedMCP[];
|
||||
agentpress_tools: Record<string, any>;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
change_description?: string;
|
||||
}
|
||||
|
||||
interface UseAgentVersionDataProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
interface UseAgentVersionDataReturn {
|
||||
agent: any;
|
||||
versionData: NormalizedVersionData | null;
|
||||
isViewingOldVersion: boolean;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
|
||||
function normalizeCustomMcps(mcps: any): NormalizedMCP[] {
|
||||
if (!mcps || !Array.isArray(mcps)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return mcps.map(mcp => {
|
||||
if (!mcp || typeof mcp !== 'object') {
|
||||
return {
|
||||
name: 'Unknown MCP',
|
||||
type: 'sse',
|
||||
customType: 'sse',
|
||||
config: {},
|
||||
enabledTools: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: mcp.name || 'Unnamed MCP',
|
||||
type: mcp.type || mcp.customType || 'sse',
|
||||
customType: mcp.customType || mcp.type || 'sse',
|
||||
config: mcp.config || {},
|
||||
enabledTools: mcp.enabledTools || mcp.enabled_tools || []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeVersionData(version: any): NormalizedVersionData | null {
|
||||
if (!version) return null;
|
||||
|
||||
return {
|
||||
version_id: version.version_id,
|
||||
agent_id: version.agent_id,
|
||||
version_number: version.version_number,
|
||||
version_name: version.version_name,
|
||||
system_prompt: version.system_prompt || '',
|
||||
configured_mcps: Array.isArray(version.configured_mcps) ? version.configured_mcps : [],
|
||||
custom_mcps: normalizeCustomMcps(version.custom_mcps),
|
||||
agentpress_tools: version.agentpress_tools && typeof version.agentpress_tools === 'object'
|
||||
? version.agentpress_tools
|
||||
: {},
|
||||
is_active: version.is_active ?? true,
|
||||
created_at: version.created_at,
|
||||
updated_at: version.updated_at || version.created_at,
|
||||
created_by: version.created_by,
|
||||
change_description: version.change_description
|
||||
};
|
||||
}
|
||||
|
||||
export function useAgentVersionData({ agentId }: UseAgentVersionDataProps): UseAgentVersionDataReturn {
|
||||
const searchParams = useSearchParams();
|
||||
const versionParam = searchParams.get('version');
|
||||
|
||||
const { data: agent, isLoading: agentLoading, error: agentError } = useAgent(agentId);
|
||||
|
||||
// Load version data if we have a version param OR if we need the current version
|
||||
const shouldLoadVersion = versionParam || agent?.current_version_id;
|
||||
const versionToLoad = versionParam || agent?.current_version_id || '';
|
||||
|
||||
const { data: rawVersionData, isLoading: versionLoading, error: versionError } = useAgentVersion(
|
||||
agentId,
|
||||
shouldLoadVersion ? versionToLoad : null
|
||||
);
|
||||
|
||||
const { setCurrentVersion, clearVersionState } = useAgentVersionStore();
|
||||
|
||||
const versionData = useMemo(() => {
|
||||
return normalizeVersionData(rawVersionData);
|
||||
}, [rawVersionData]);
|
||||
|
||||
const isViewingOldVersion = useMemo(() => {
|
||||
return Boolean(versionParam && versionParam !== agent?.current_version_id);
|
||||
}, [versionParam, agent?.current_version_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (versionData) {
|
||||
setCurrentVersion(versionData);
|
||||
} else if (!versionParam) {
|
||||
clearVersionState();
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearVersionState();
|
||||
};
|
||||
}, [versionData, versionParam, setCurrentVersion, clearVersionState]);
|
||||
|
||||
const isLoading = agentLoading || (shouldLoadVersion ? versionLoading : false);
|
||||
const error = agentError || versionError;
|
||||
|
||||
return {
|
||||
agent,
|
||||
versionData,
|
||||
isViewingOldVersion,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { AgentVersion } from '@/hooks/react-query/agents/utils';
|
||||
|
||||
interface VersionState {
|
||||
// Current version being viewed/edited
|
||||
currentVersion: AgentVersion | null;
|
||||
|
||||
// Version being compared to
|
||||
compareVersion: AgentVersion | null;
|
||||
|
||||
// UI state
|
||||
isViewingVersion: boolean;
|
||||
isComparingVersions: boolean;
|
||||
hasUnsavedChanges: boolean;
|
||||
|
||||
// Actions
|
||||
setCurrentVersion: (version: AgentVersion | null) => void;
|
||||
setCompareVersion: (version: AgentVersion | null) => void;
|
||||
setIsViewingVersion: (viewing: boolean) => void;
|
||||
setIsComparingVersions: (comparing: boolean) => void;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
|
||||
// Helper actions
|
||||
clearVersionState: () => void;
|
||||
isViewingOldVersion: (currentVersionId?: string) => boolean;
|
||||
}
|
||||
|
||||
export const useAgentVersionStore = create<VersionState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
currentVersion: null,
|
||||
compareVersion: null,
|
||||
isViewingVersion: false,
|
||||
isComparingVersions: false,
|
||||
hasUnsavedChanges: false,
|
||||
|
||||
// Actions
|
||||
setCurrentVersion: (version) => set({
|
||||
currentVersion: version,
|
||||
isViewingVersion: version !== null
|
||||
}),
|
||||
|
||||
setCompareVersion: (version) => set({
|
||||
compareVersion: version,
|
||||
isComparingVersions: version !== null
|
||||
}),
|
||||
|
||||
setIsViewingVersion: (viewing) => set({ isViewingVersion: viewing }),
|
||||
|
||||
setIsComparingVersions: (comparing) => set({
|
||||
isComparingVersions: comparing,
|
||||
compareVersion: comparing ? get().compareVersion : null
|
||||
}),
|
||||
|
||||
setHasUnsavedChanges: (hasChanges) => set({ hasUnsavedChanges: hasChanges }),
|
||||
|
||||
// Helper actions
|
||||
clearVersionState: () => set({
|
||||
currentVersion: null,
|
||||
compareVersion: null,
|
||||
isViewingVersion: false,
|
||||
isComparingVersions: false,
|
||||
hasUnsavedChanges: false
|
||||
}),
|
||||
|
||||
isViewingOldVersion: (currentVersionId?: string) => {
|
||||
const state = get();
|
||||
return state.isViewingVersion &&
|
||||
state.currentVersion !== null &&
|
||||
state.currentVersion.version_id !== currentVersionId;
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'agent-version-store'
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Selectors for common use cases
|
||||
export const selectCurrentVersion = (state: VersionState) => state.currentVersion;
|
||||
export const selectIsViewingOldVersion = (state: VersionState) => state.isViewingVersion && state.currentVersion !== null;
|
||||
export const selectHasUnsavedChanges = (state: VersionState) => state.hasUnsavedChanges;
|
||||
export const selectIsComparingVersions = (state: VersionState) => state.isComparingVersions;
|
Loading…
Reference in New Issue