Skip to content

Commit 3a9d8e2

Browse files
Merge pull request #1267 from escapade-mckv/main
UI fixes
2 parents 90372d3 + c56e2a2 commit 3a9d8e2

File tree

27 files changed

+1184
-133
lines changed

27 files changed

+1184
-133
lines changed

backend/agent/api.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,7 +1790,6 @@ async def import_agent_from_json(
17901790
request: JsonImportRequestModel,
17911791
user_id: str = Depends(get_current_user_id_from_jwt)
17921792
):
1793-
"""Import an agent from JSON with credential mappings"""
17941793
logger.info(f"Importing agent from JSON - user: {user_id}")
17951794

17961795
if not await is_enabled("custom_agents"):
@@ -1799,6 +1798,21 @@ async def import_agent_from_json(
17991798
detail="Custom agents currently disabled. This feature is not available at the moment."
18001799
)
18011800

1801+
client = await db.client
1802+
from .utils import check_agent_count_limit
1803+
limit_check = await check_agent_count_limit(client, user_id)
1804+
1805+
if not limit_check['can_create']:
1806+
error_detail = {
1807+
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
1808+
"current_count": limit_check['current_count'],
1809+
"limit": limit_check['limit'],
1810+
"tier_name": limit_check['tier_name'],
1811+
"error_code": "AGENT_LIMIT_EXCEEDED"
1812+
}
1813+
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
1814+
raise HTTPException(status_code=402, detail=error_detail)
1815+
18021816
try:
18031817
from agent.json_import_service import JsonImportService, JsonImportRequest
18041818
import_service = JsonImportService(db)
@@ -1840,6 +1854,20 @@ async def create_agent(
18401854
)
18411855
client = await db.client
18421856

1857+
from .utils import check_agent_count_limit
1858+
limit_check = await check_agent_count_limit(client, user_id)
1859+
1860+
if not limit_check['can_create']:
1861+
error_detail = {
1862+
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
1863+
"current_count": limit_check['current_count'],
1864+
"limit": limit_check['limit'],
1865+
"tier_name": limit_check['tier_name'],
1866+
"error_code": "AGENT_LIMIT_EXCEEDED"
1867+
}
1868+
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
1869+
raise HTTPException(status_code=402, detail=error_detail)
1870+
18431871
try:
18441872
if agent_data.is_default:
18451873
await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute()
@@ -1897,6 +1925,10 @@ async def create_agent(
18971925
await client.table('agents').delete().eq('agent_id', agent['agent_id']).execute()
18981926
raise HTTPException(status_code=500, detail="Failed to create initial version")
18991927

1928+
# Invalidate agent count cache after successful creation
1929+
from utils.cache import Cache
1930+
await Cache.invalidate(f"agent_count_limit:{user_id}")
1931+
19001932
logger.info(f"Created agent {agent['agent_id']} with v1 for user: {user_id}")
19011933
return AgentResponse(
19021934
agent_id=agent['agent_id'],
@@ -2298,7 +2330,20 @@ async def delete_agent(agent_id: str, user_id: str = Depends(get_current_user_id
22982330
if agent['is_default']:
22992331
raise HTTPException(status_code=400, detail="Cannot delete default agent")
23002332

2301-
await client.table('agents').delete().eq('agent_id', agent_id).execute()
2333+
if agent.get('metadata', {}).get('is_suna_default', False):
2334+
raise HTTPException(status_code=400, detail="Cannot delete Suna default agent")
2335+
2336+
delete_result = await client.table('agents').delete().eq('agent_id', agent_id).execute()
2337+
2338+
if not delete_result.data:
2339+
logger.warning(f"No agent was deleted for agent_id: {agent_id}, user_id: {user_id}")
2340+
raise HTTPException(status_code=403, detail="Unable to delete agent - permission denied or agent not found")
2341+
2342+
try:
2343+
from utils.cache import Cache
2344+
await Cache.invalidate(f"agent_count_limit:{user_id}")
2345+
except Exception as cache_error:
2346+
logger.warning(f"Cache invalidation failed for user {user_id}: {str(cache_error)}")
23022347

23032348
logger.info(f"Successfully deleted agent: {agent_id}")
23042349
return {"message": "Agent deleted successfully"}

backend/agent/json_import_service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ async def import_json(self, request: JsonImportRequest) -> JsonImportResult:
114114
request.custom_system_prompt or json_data.get('system_prompt', '')
115115
)
116116

117+
from utils.cache import Cache
118+
await Cache.invalidate(f"agent_count_limit:{request.account_id}")
119+
117120
logger.info(f"Successfully imported agent {agent_id} from JSON")
118121

119122
return JsonImportResult(

backend/agent/utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,63 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]:
138138
'running_count': 0,
139139
'running_thread_ids': []
140140
}
141+
142+
143+
async def check_agent_count_limit(client, account_id: str) -> Dict[str, Any]:
144+
try:
145+
try:
146+
result = await Cache.get(f"agent_count_limit:{account_id}")
147+
if result:
148+
logger.debug(f"Cache hit for agent count limit: {account_id}")
149+
return result
150+
except Exception as cache_error:
151+
logger.warning(f"Cache read failed for agent count limit {account_id}: {str(cache_error)}")
152+
153+
agents_result = await client.table('agents').select('agent_id, metadata').eq('account_id', account_id).execute()
154+
155+
non_suna_agents = []
156+
for agent in agents_result.data or []:
157+
metadata = agent.get('metadata', {}) or {}
158+
is_suna_default = metadata.get('is_suna_default', False)
159+
if not is_suna_default:
160+
non_suna_agents.append(agent)
161+
162+
current_count = len(non_suna_agents)
163+
logger.debug(f"Account {account_id} has {current_count} custom agents (excluding Suna defaults)")
164+
165+
try:
166+
from services.billing import get_subscription_tier
167+
tier_name = await get_subscription_tier(client, account_id)
168+
logger.debug(f"Account {account_id} subscription tier: {tier_name}")
169+
except Exception as billing_error:
170+
logger.warning(f"Could not get subscription tier for {account_id}: {str(billing_error)}, defaulting to free")
171+
tier_name = 'free'
172+
173+
agent_limit = config.AGENT_LIMITS.get(tier_name, config.AGENT_LIMITS['free'])
174+
175+
can_create = current_count < agent_limit
176+
177+
result = {
178+
'can_create': can_create,
179+
'current_count': current_count,
180+
'limit': agent_limit,
181+
'tier_name': tier_name
182+
}
183+
184+
try:
185+
await Cache.set(f"agent_count_limit:{account_id}", result, ttl=300)
186+
except Exception as cache_error:
187+
logger.warning(f"Cache write failed for agent count limit {account_id}: {str(cache_error)}")
188+
189+
logger.info(f"Account {account_id} has {current_count}/{agent_limit} agents (tier: {tier_name}) - can_create: {can_create}")
190+
191+
return result
192+
193+
except Exception as e:
194+
logger.error(f"Error checking agent count limit for account {account_id}: {str(e)}", exc_info=True)
195+
return {
196+
'can_create': True,
197+
'current_count': 0,
198+
'limit': config.AGENT_LIMITS['free'],
199+
'tier_name': 'free'
200+
}

backend/composio_integration/api.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .composio_service import (
1111
get_integration_service,
1212
)
13-
from .toolkit_service import ToolkitService
13+
from .toolkit_service import ToolkitService, ToolsListResponse
1414
from .composio_profile_service import ComposioProfileService, ComposioProfile
1515

1616
router = APIRouter(prefix="/composio", tags=["composio"])
@@ -21,15 +21,13 @@ def initialize(database: DBConnection):
2121
global db
2222
db = database
2323

24-
2524
class IntegrateToolkitRequest(BaseModel):
2625
toolkit_slug: str
2726
profile_name: Optional[str] = None
2827
display_name: Optional[str] = None
2928
mcp_server_name: Optional[str] = None
3029
save_as_profile: bool = True
3130

32-
3331
class IntegrationStatusResponse(BaseModel):
3432
status: str
3533
toolkit: str
@@ -40,7 +38,6 @@ class IntegrationStatusResponse(BaseModel):
4038
profile_id: Optional[str] = None
4139
redirect_url: Optional[str] = None
4240

43-
4441
class CreateProfileRequest(BaseModel):
4542
toolkit_slug: str
4643
profile_name: str
@@ -49,6 +46,10 @@ class CreateProfileRequest(BaseModel):
4946
is_default: bool = False
5047
initiation_fields: Optional[Dict[str, str]] = None
5148

49+
class ToolsListRequest(BaseModel):
50+
toolkit_slug: str
51+
limit: int = 50
52+
cursor: Optional[str] = None
5253

5354
class ProfileResponse(BaseModel):
5455
profile_id: str
@@ -406,6 +407,35 @@ async def get_toolkit_icon(
406407
raise HTTPException(status_code=500, detail="Internal server error")
407408

408409

410+
@router.post("/tools/list")
411+
async def list_toolkit_tools(
412+
request: ToolsListRequest,
413+
current_user_id: str = Depends(get_current_user_id_from_jwt)
414+
):
415+
try:
416+
logger.info(f"User {current_user_id} requesting tools for toolkit: {request.toolkit_slug}")
417+
418+
toolkit_service = ToolkitService()
419+
tools_response = await toolkit_service.get_toolkit_tools(
420+
toolkit_slug=request.toolkit_slug,
421+
limit=request.limit,
422+
cursor=request.cursor
423+
)
424+
425+
return {
426+
"success": True,
427+
"tools": [tool.dict() for tool in tools_response.items],
428+
"total_items": tools_response.total_items,
429+
"current_page": tools_response.current_page,
430+
"total_pages": tools_response.total_pages,
431+
"next_cursor": tools_response.next_cursor
432+
}
433+
434+
except Exception as e:
435+
logger.error(f"Failed to list toolkit tools for {request.toolkit_slug}: {e}", exc_info=True)
436+
raise HTTPException(status_code=500, detail=f"Failed to get toolkit tools: {str(e)}")
437+
438+
409439
@router.get("/health")
410440
async def health_check() -> Dict[str, str]:
411441
try:

backend/composio_integration/toolkit_service.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ class DetailedToolkitInfo(BaseModel):
4848
base_url: Optional[str] = None
4949

5050

51+
class ParameterSchema(BaseModel):
52+
properties: Dict[str, Any] = {}
53+
required: Optional[List[str]] = None
54+
55+
56+
class ToolInfo(BaseModel):
57+
slug: str
58+
name: str
59+
description: str
60+
version: str
61+
input_parameters: ParameterSchema = ParameterSchema()
62+
output_parameters: ParameterSchema = ParameterSchema()
63+
scopes: List[str] = []
64+
tags: List[str] = []
65+
no_auth: bool = False
66+
67+
68+
class ToolsListResponse(BaseModel):
69+
items: List[ToolInfo]
70+
next_cursor: Optional[str] = None
71+
total_items: int
72+
current_page: int = 1
73+
total_pages: int = 1
74+
75+
5176
class ToolkitService:
5277
def __init__(self, api_key: Optional[str] = None):
5378
self.client = ComposioClient.get_client(api_key)
@@ -388,4 +413,80 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
388413

389414
except Exception as e:
390415
logger.error(f"Failed to get detailed toolkit info for {toolkit_slug}: {e}", exc_info=True)
391-
return None
416+
return None
417+
418+
async def get_toolkit_tools(self, toolkit_slug: str, limit: int = 50, cursor: Optional[str] = None) -> ToolsListResponse:
419+
try:
420+
logger.info(f"Fetching tools for toolkit: {toolkit_slug}")
421+
422+
params = {
423+
"limit": limit,
424+
"toolkit_slug": toolkit_slug
425+
}
426+
427+
if cursor:
428+
params["cursor"] = cursor
429+
430+
tools_response = self.client.tools.list(**params)
431+
432+
if hasattr(tools_response, '__dict__'):
433+
response_data = tools_response.__dict__
434+
else:
435+
response_data = tools_response
436+
437+
items = response_data.get('items', [])
438+
439+
tools = []
440+
for item in items:
441+
if hasattr(item, '__dict__'):
442+
tool_data = item.__dict__
443+
elif hasattr(item, '_asdict'):
444+
tool_data = item._asdict()
445+
else:
446+
tool_data = item
447+
448+
input_params_raw = tool_data.get("input_parameters", {})
449+
output_params_raw = tool_data.get("output_parameters", {})
450+
451+
input_parameters = ParameterSchema()
452+
if isinstance(input_params_raw, dict):
453+
input_parameters.properties = input_params_raw.get("properties", input_params_raw)
454+
input_parameters.required = input_params_raw.get("required")
455+
456+
output_parameters = ParameterSchema()
457+
if isinstance(output_params_raw, dict):
458+
output_parameters.properties = output_params_raw.get("properties", output_params_raw)
459+
output_parameters.required = output_params_raw.get("required")
460+
461+
tool = ToolInfo(
462+
slug=tool_data.get("slug", ""),
463+
name=tool_data.get("name", ""),
464+
description=tool_data.get("description", ""),
465+
version=tool_data.get("version", "1.0.0"),
466+
input_parameters=input_parameters,
467+
output_parameters=output_parameters,
468+
scopes=tool_data.get("scopes", []),
469+
tags=tool_data.get("tags", []),
470+
no_auth=tool_data.get("no_auth", False)
471+
)
472+
tools.append(tool)
473+
474+
result = ToolsListResponse(
475+
items=tools,
476+
total_items=response_data.get("total_items", len(tools)),
477+
total_pages=response_data.get("total_pages", 1),
478+
current_page=response_data.get("current_page", 1),
479+
next_cursor=response_data.get("next_cursor")
480+
)
481+
482+
logger.info(f"Successfully fetched {len(tools)} tools for toolkit {toolkit_slug}")
483+
return result
484+
485+
except Exception as e:
486+
logger.error(f"Failed to get tools for toolkit {toolkit_slug}: {e}", exc_info=True)
487+
return ToolsListResponse(
488+
items=[],
489+
total_items=0,
490+
current_page=1,
491+
total_pages=1
492+
)

0 commit comments

Comments
 (0)