mirror of https://github.com/kortix-ai/suna.git
668 lines
22 KiB
Python
668 lines
22 KiB
Python
from fastapi import APIRouter, HTTPException, Depends, Query
|
|
from typing import List, Dict, Any, Optional
|
|
from pydantic import BaseModel
|
|
from uuid import UUID
|
|
from datetime import datetime
|
|
|
|
from utils.logger import logger
|
|
from utils.auth_utils import get_current_user_id_from_jwt
|
|
from .facade import PipedreamManager
|
|
from .domain.entities import Profile, ConnectionStatus
|
|
from .domain.exceptions import (
|
|
ProfileNotFoundError, ProfileAlreadyExistsError,
|
|
ConnectionNotFoundError, AppNotFoundError, MCPConnectionError,
|
|
ValidationException, PipedreamException
|
|
)
|
|
|
|
router = APIRouter(prefix="/pipedream", tags=["pipedream"])
|
|
pipedream_manager: Optional[PipedreamManager] = None
|
|
|
|
|
|
def initialize(database):
|
|
global pipedream_manager
|
|
if pipedream_manager is None:
|
|
pipedream_manager = PipedreamManager(db=database, logger=logger)
|
|
|
|
|
|
class CreateConnectionTokenRequest(BaseModel):
|
|
app: Optional[str] = None
|
|
|
|
|
|
class ConnectionTokenResponse(BaseModel):
|
|
success: bool
|
|
link: Optional[str] = None
|
|
token: Optional[str] = None
|
|
external_user_id: str = ""
|
|
app: Optional[str] = None
|
|
expires_at: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
class ConnectionResponse(BaseModel):
|
|
success: bool
|
|
connections: List[Dict[str, Any]]
|
|
count: int
|
|
error: Optional[str] = None
|
|
|
|
|
|
class MCPDiscoveryRequest(BaseModel):
|
|
app_slug: Optional[str] = None
|
|
oauth_app_id: Optional[str] = None
|
|
|
|
|
|
class MCPProfileDiscoveryRequest(BaseModel):
|
|
external_user_id: str
|
|
app_slug: Optional[str] = None
|
|
oauth_app_id: Optional[str] = None
|
|
|
|
|
|
class MCPDiscoveryResponse(BaseModel):
|
|
success: bool
|
|
mcp_servers: List[Dict[str, Any]]
|
|
count: int
|
|
error: Optional[str] = None
|
|
|
|
|
|
class MCPConnectionRequest(BaseModel):
|
|
app_slug: str
|
|
oauth_app_id: Optional[str] = None
|
|
|
|
|
|
class MCPConnectionResponse(BaseModel):
|
|
success: bool
|
|
mcp_config: Optional[Dict[str, Any]] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
class ProfileRequest(BaseModel):
|
|
profile_name: str
|
|
app_slug: str
|
|
app_name: str
|
|
description: Optional[str] = None
|
|
is_default: bool = False
|
|
oauth_app_id: Optional[str] = None
|
|
enabled_tools: List[str] = []
|
|
external_user_id: Optional[str] = None
|
|
|
|
|
|
class UpdateProfileRequest(BaseModel):
|
|
profile_name: Optional[str] = None
|
|
display_name: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
is_default: Optional[bool] = None
|
|
enabled_tools: Optional[List[str]] = None
|
|
|
|
|
|
class ProfileResponse(BaseModel):
|
|
profile_id: UUID
|
|
account_id: UUID
|
|
mcp_qualified_name: str
|
|
profile_name: str
|
|
display_name: str
|
|
app_slug: str
|
|
app_name: str
|
|
external_user_id: str
|
|
enabled_tools: List[str]
|
|
is_active: bool
|
|
is_default: bool
|
|
is_connected: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
last_used_at: Optional[datetime] = None
|
|
|
|
@classmethod
|
|
def from_domain(cls, profile: Profile) -> 'ProfileResponse':
|
|
return cls(
|
|
profile_id=profile.profile_id,
|
|
account_id=profile.account_id,
|
|
mcp_qualified_name=profile.mcp_qualified_name,
|
|
profile_name=profile.profile_name.value,
|
|
display_name=profile.display_name,
|
|
app_slug=profile.app_slug.value,
|
|
app_name=profile.app_name,
|
|
external_user_id=profile.external_user_id.value,
|
|
enabled_tools=profile.enabled_tools,
|
|
is_active=profile.is_active,
|
|
is_default=profile.is_default,
|
|
is_connected=profile.is_connected,
|
|
created_at=profile.created_at,
|
|
updated_at=profile.updated_at,
|
|
last_used_at=profile.last_used_at
|
|
)
|
|
|
|
|
|
def _strip_pipedream_prefix(app_slug: Optional[str]) -> Optional[str]:
|
|
if app_slug and app_slug.startswith("pipedream:"):
|
|
return app_slug[len("pipedream:"):]
|
|
return app_slug
|
|
|
|
|
|
def _handle_pipedream_exception(e: Exception) -> HTTPException:
|
|
if isinstance(e, ProfileNotFoundError):
|
|
return HTTPException(status_code=404, detail=str(e))
|
|
elif isinstance(e, ProfileAlreadyExistsError):
|
|
return HTTPException(status_code=409, detail=str(e))
|
|
elif isinstance(e, ValidationException):
|
|
return HTTPException(status_code=400, detail=str(e))
|
|
elif isinstance(e, ConnectionNotFoundError):
|
|
return HTTPException(status_code=404, detail=str(e))
|
|
elif isinstance(e, AppNotFoundError):
|
|
return HTTPException(status_code=404, detail=str(e))
|
|
elif isinstance(e, MCPConnectionError):
|
|
return HTTPException(status_code=502, detail=str(e))
|
|
elif isinstance(e, PipedreamException):
|
|
return HTTPException(status_code=500, detail=str(e))
|
|
else:
|
|
return HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.post("/connection-token", response_model=ConnectionTokenResponse)
|
|
async def create_connection_token(
|
|
request: CreateConnectionTokenRequest,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Creating Pipedream connection token for user: {user_id}, app: {request.app}")
|
|
|
|
actual_app = _strip_pipedream_prefix(request.app)
|
|
|
|
try:
|
|
result = await pipedream_manager.create_connection_token(user_id, actual_app)
|
|
|
|
return ConnectionTokenResponse(
|
|
success=True,
|
|
link=result.get("connect_link_url"),
|
|
token=result.get("token"),
|
|
external_user_id=user_id,
|
|
app=actual_app,
|
|
expires_at=result.get("expires_at")
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create connection token: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/connections", response_model=ConnectionResponse)
|
|
async def get_user_connections(
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Getting connections for user: {user_id}")
|
|
|
|
try:
|
|
connections = await pipedream_manager.get_connections(user_id)
|
|
|
|
connection_data = []
|
|
for connection in connections:
|
|
connection_data.append({
|
|
"name": connection.app.name,
|
|
"name_slug": connection.app.slug.value,
|
|
"description": connection.app.description,
|
|
"category": connection.app.category,
|
|
"img_src": connection.app.logo_url,
|
|
"auth_type": connection.app.auth_type.value,
|
|
"verified": connection.app.is_verified,
|
|
"url": connection.app.url,
|
|
"tags": connection.app.tags,
|
|
"is_active": connection.is_active
|
|
})
|
|
|
|
return ConnectionResponse(
|
|
success=True,
|
|
connections=connection_data,
|
|
count=len(connection_data)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get connections: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.post("/mcp/discover", response_model=MCPDiscoveryResponse)
|
|
async def discover_mcp_servers(
|
|
request: MCPDiscoveryRequest,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Discovering MCP servers for user: {user_id}, app: {request.app_slug}")
|
|
|
|
actual_app_slug = _strip_pipedream_prefix(request.app_slug)
|
|
|
|
try:
|
|
servers = await pipedream_manager.discover_mcp_servers(user_id, actual_app_slug)
|
|
|
|
server_data = []
|
|
for server in servers:
|
|
tools_data = []
|
|
for tool in server.available_tools:
|
|
tools_data.append({
|
|
"name": tool.name,
|
|
"description": tool.description,
|
|
"inputSchema": tool.input_schema
|
|
})
|
|
|
|
server_data.append({
|
|
"app_slug": server.app_slug.value,
|
|
"app_name": server.app_name,
|
|
"server_url": server.server_url.value,
|
|
"project_id": server.project_id,
|
|
"environment": server.environment,
|
|
"external_user_id": server.external_user_id.value,
|
|
"oauth_app_id": server.oauth_app_id,
|
|
"status": server.status.value,
|
|
"available_tools": tools_data,
|
|
"error": server.error_message
|
|
})
|
|
|
|
return MCPDiscoveryResponse(
|
|
success=True,
|
|
mcp_servers=server_data,
|
|
count=len(server_data)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to discover MCP servers: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.post("/mcp/discover-profile", response_model=MCPDiscoveryResponse)
|
|
async def discover_mcp_servers_for_profile(
|
|
request: MCPProfileDiscoveryRequest,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Discovering MCP servers for profile: {request.external_user_id}")
|
|
|
|
actual_app_slug = _strip_pipedream_prefix(request.app_slug)
|
|
|
|
try:
|
|
servers = await pipedream_manager.discover_mcp_servers(request.external_user_id, actual_app_slug)
|
|
|
|
server_data = []
|
|
for server in servers:
|
|
tools_data = []
|
|
for tool in server.available_tools:
|
|
tools_data.append({
|
|
"name": tool.name,
|
|
"description": tool.description,
|
|
"inputSchema": tool.input_schema
|
|
})
|
|
|
|
server_data.append({
|
|
"app_slug": server.app_slug.value,
|
|
"app_name": server.app_name,
|
|
"server_url": server.server_url.value,
|
|
"project_id": server.project_id,
|
|
"environment": server.environment,
|
|
"external_user_id": server.external_user_id.value,
|
|
"oauth_app_id": server.oauth_app_id,
|
|
"status": server.status.value,
|
|
"available_tools": tools_data,
|
|
"error": server.error_message
|
|
})
|
|
|
|
return MCPDiscoveryResponse(
|
|
success=True,
|
|
mcp_servers=server_data,
|
|
count=len(server_data)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to discover MCP servers for profile: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.post("/mcp/connect", response_model=MCPConnectionResponse)
|
|
async def create_mcp_connection(
|
|
request: MCPConnectionRequest,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Creating MCP connection for user: {user_id}, app: {request.app_slug}")
|
|
|
|
actual_app_slug = _strip_pipedream_prefix(request.app_slug)
|
|
|
|
try:
|
|
server = await pipedream_manager.create_mcp_connection(
|
|
user_id,
|
|
actual_app_slug,
|
|
request.oauth_app_id
|
|
)
|
|
|
|
tools_data = []
|
|
for tool in server.available_tools:
|
|
tools_data.append({
|
|
"name": tool.name,
|
|
"description": tool.description,
|
|
"inputSchema": tool.input_schema
|
|
})
|
|
|
|
mcp_config = {
|
|
"app_slug": server.app_slug.value,
|
|
"app_name": server.app_name,
|
|
"server_url": server.server_url.value,
|
|
"project_id": server.project_id,
|
|
"environment": server.environment,
|
|
"external_user_id": server.external_user_id.value,
|
|
"oauth_app_id": server.oauth_app_id,
|
|
"status": server.status.value,
|
|
"available_tools": tools_data
|
|
}
|
|
|
|
return MCPConnectionResponse(
|
|
success=True,
|
|
mcp_config=mcp_config
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create MCP connection: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/apps", response_model=Dict[str, Any])
|
|
async def get_pipedream_apps(
|
|
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
|
q: Optional[str] = Query(None),
|
|
category: Optional[str] = Query(None)
|
|
):
|
|
logger.info(f"Fetching Pipedream apps: query='{q}', category='{category}'")
|
|
|
|
try:
|
|
result = await pipedream_manager.search_apps(
|
|
query=q,
|
|
category=category,
|
|
cursor=after
|
|
)
|
|
|
|
apps_data = []
|
|
for app in result.get("apps", []):
|
|
categories = []
|
|
if app.category and app.category != "Other":
|
|
categories.append(app.category)
|
|
if app.tags:
|
|
for tag in app.tags:
|
|
if tag and tag not in categories:
|
|
categories.append(tag)
|
|
if not categories and app.category:
|
|
categories.append(app.category)
|
|
|
|
apps_data.append({
|
|
"name": app.name,
|
|
"name_slug": app.slug.value,
|
|
"description": app.description,
|
|
"category": app.category,
|
|
"categories": categories,
|
|
"img_src": app.logo_url,
|
|
"auth_type": app.auth_type.value,
|
|
"verified": app.is_verified,
|
|
"url": app.url,
|
|
"tags": app.tags,
|
|
"featured_weight": app.featured_weight
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"apps": apps_data,
|
|
"page_info": result.get("page_info", {}),
|
|
"total_count": result.get("total_count", 0)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch Pipedream apps: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/apps/popular", response_model=Dict[str, Any])
|
|
async def get_popular_pipedream_apps():
|
|
logger.info("Fetching popular Pipedream apps")
|
|
|
|
try:
|
|
apps = await pipedream_manager.get_popular_apps(limit=100)
|
|
|
|
apps_data = []
|
|
for app in apps:
|
|
categories = []
|
|
if app.category and app.category != "Other":
|
|
categories.append(app.category)
|
|
if app.tags:
|
|
for tag in app.tags:
|
|
if tag and tag not in categories:
|
|
categories.append(tag)
|
|
|
|
if not categories and app.category:
|
|
categories.append(app.category)
|
|
|
|
apps_data.append({
|
|
"name": app.name,
|
|
"name_slug": app.slug.value,
|
|
"description": app.description,
|
|
"category": app.category,
|
|
"categories": categories,
|
|
"img_src": app.logo_url,
|
|
"auth_type": app.auth_type.value,
|
|
"verified": app.is_verified,
|
|
"url": app.url,
|
|
"tags": app.tags,
|
|
"featured_weight": app.featured_weight
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"apps": apps_data,
|
|
"page_info": {
|
|
"total_count": len(apps_data),
|
|
"count": len(apps_data),
|
|
"has_more": False
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch popular Pipedream apps: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/apps/{app_slug}/icon")
|
|
async def get_app_icon(app_slug: str):
|
|
logger.info(f"Fetching icon for app: {app_slug}")
|
|
try:
|
|
icon_url = await pipedream_manager.get_app_icon(app_slug)
|
|
if icon_url:
|
|
return {
|
|
"success": True,
|
|
"app_slug": app_slug,
|
|
"icon_url": icon_url
|
|
}
|
|
else:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Icon not found for app: {app_slug}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch icon for app {app_slug}: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.post("/profiles", response_model=ProfileResponse)
|
|
async def create_credential_profile(
|
|
request: ProfileRequest,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Creating credential profile for user: {user_id}, app: {request.app_slug}")
|
|
|
|
try:
|
|
profile = await pipedream_manager.create_profile(
|
|
account_id=user_id,
|
|
profile_name=request.profile_name,
|
|
app_slug=request.app_slug,
|
|
app_name=request.app_name,
|
|
description=request.description,
|
|
is_default=request.is_default,
|
|
oauth_app_id=request.oauth_app_id,
|
|
enabled_tools=request.enabled_tools,
|
|
external_user_id=request.external_user_id
|
|
)
|
|
|
|
return ProfileResponse.from_domain(profile)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create credential profile: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/profiles", response_model=List[ProfileResponse])
|
|
async def get_credential_profiles(
|
|
app_slug: Optional[str] = Query(None),
|
|
is_active: Optional[bool] = Query(None),
|
|
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 = _strip_pipedream_prefix(app_slug)
|
|
|
|
try:
|
|
profiles = await pipedream_manager.get_profiles(user_id, actual_app_slug, is_active)
|
|
|
|
return [ProfileResponse.from_domain(profile) for profile in profiles]
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get credential profiles: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/profiles/{profile_id}", response_model=ProfileResponse)
|
|
async def get_credential_profile(
|
|
profile_id: str,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Getting credential profile: {profile_id} for user: {user_id}")
|
|
|
|
try:
|
|
profile = await pipedream_manager.get_profile(user_id, profile_id)
|
|
|
|
if not profile:
|
|
raise ProfileNotFoundError(profile_id)
|
|
|
|
return ProfileResponse.from_domain(profile)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get credential profile: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.put("/profiles/{profile_id}", response_model=ProfileResponse)
|
|
async def update_credential_profile(
|
|
profile_id: str,
|
|
request: UpdateProfileRequest,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Updating credential profile: {profile_id} for user: {user_id}")
|
|
|
|
try:
|
|
profile = await pipedream_manager.update_profile(
|
|
account_id=user_id,
|
|
profile_id=profile_id,
|
|
profile_name=request.profile_name,
|
|
display_name=request.display_name,
|
|
is_active=request.is_active,
|
|
is_default=request.is_default,
|
|
enabled_tools=request.enabled_tools
|
|
)
|
|
|
|
return ProfileResponse.from_domain(profile)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to update credential profile: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.delete("/profiles/{profile_id}")
|
|
async def delete_credential_profile(
|
|
profile_id: str,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Deleting credential profile: {profile_id} for user: {user_id}")
|
|
|
|
try:
|
|
success = await pipedream_manager.delete_profile(user_id, profile_id)
|
|
|
|
if not success:
|
|
raise ProfileNotFoundError(profile_id)
|
|
|
|
return {"success": True, "message": "Profile deleted successfully"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete credential profile: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.post("/profiles/{profile_id}/connect")
|
|
async def connect_credential_profile(
|
|
profile_id: str,
|
|
app: Optional[str] = Query(None),
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Connecting credential profile: {profile_id} for user: {user_id}")
|
|
|
|
actual_app = _strip_pipedream_prefix(app)
|
|
|
|
try:
|
|
profile = await pipedream_manager.get_profile(user_id, profile_id)
|
|
if not profile:
|
|
raise ProfileNotFoundError(profile_id)
|
|
|
|
result = await pipedream_manager.create_connection_token(
|
|
profile.external_user_id.value,
|
|
actual_app or profile.app_slug.value
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"link": result.get("connect_link_url"),
|
|
"token": result.get("token"),
|
|
"expires_at": result.get("expires_at"),
|
|
"profile_id": profile_id,
|
|
"external_user_id": profile.external_user_id.value,
|
|
"app": actual_app or profile.app_slug.value
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect credential profile: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|
|
|
|
|
|
@router.get("/profiles/{profile_id}/connections")
|
|
async def get_profile_connections(
|
|
profile_id: str,
|
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
|
):
|
|
logger.info(f"Getting connections for profile: {profile_id}, user: {user_id}")
|
|
|
|
try:
|
|
profile = await pipedream_manager.get_profile(user_id, profile_id)
|
|
if not profile:
|
|
raise ProfileNotFoundError(profile_id)
|
|
|
|
connections = await pipedream_manager.get_connections(profile.external_user_id.value)
|
|
|
|
connection_data = []
|
|
for connection in connections:
|
|
connection_data.append({
|
|
"name": connection.app.name,
|
|
"name_slug": connection.app.slug.value,
|
|
"description": connection.app.description,
|
|
"category": connection.app.category,
|
|
"img_src": connection.app.logo_url,
|
|
"auth_type": connection.app.auth_type.value,
|
|
"verified": connection.app.is_verified,
|
|
"url": connection.app.url,
|
|
"tags": connection.app.tags,
|
|
"is_active": connection.is_active
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"connections": connection_data,
|
|
"count": len(connection_data)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get profile connections: {str(e)}")
|
|
raise _handle_pipedream_exception(e)
|