Skip to content

Commit f1f8f82

Browse files
Merge pull request #1263 from escapade-mckv/ui-polishing
UI polishing
2 parents b84b184 + 58cfb0e commit f1f8f82

File tree

60 files changed

+1286
-5725
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1286
-5725
lines changed

backend/composio_integration/api.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from fastapi import APIRouter, HTTPException, Depends, Query
2-
from typing import Dict, Any, Optional, List
2+
from typing import Dict, Any, Optional
33
from pydantic import BaseModel
44
from uuid import uuid4
55
from utils.auth_utils import get_current_user_id_from_jwt
@@ -9,10 +9,8 @@
99

1010
from .composio_service import (
1111
get_integration_service,
12-
ComposioIntegrationService,
13-
ComposioIntegrationResult
1412
)
15-
from .toolkit_service import ToolkitInfo, ToolkitService, CategoryInfo, DetailedToolkitInfo
13+
from .toolkit_service import ToolkitService
1614
from .composio_profile_service import ComposioProfileService, ComposioProfile
1715

1816
router = APIRouter(prefix="/composio", tags=["composio"])
@@ -58,7 +56,7 @@ class ProfileResponse(BaseModel):
5856
display_name: str
5957
toolkit_slug: str
6058
toolkit_name: str
61-
mcp_url: str # The complete MCP URL
59+
mcp_url: str
6260
redirect_url: Optional[str] = None
6361
is_connected: bool
6462
is_default: bool
@@ -72,7 +70,7 @@ def from_composio_profile(cls, profile: ComposioProfile) -> "ProfileResponse":
7270
display_name=profile.display_name,
7371
toolkit_slug=profile.toolkit_slug,
7472
toolkit_name=profile.toolkit_name,
75-
mcp_url=profile.mcp_url, # Include the complete MCP URL
73+
mcp_url=profile.mcp_url,
7674
redirect_url=profile.redirect_url,
7775
is_connected=profile.is_connected,
7876
is_default=profile.is_default,

backend/composio_integration/composio_profile_service.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ComposioProfile:
2222
config_hash: str
2323
toolkit_slug: str
2424
toolkit_name: str
25-
mcp_url: str # The complete MCP URL
25+
mcp_url: str
2626
redirect_url: Optional[str] = None
2727
is_active: bool = True
2828
is_default: bool = False
@@ -57,36 +57,33 @@ def _build_config(
5757
self,
5858
toolkit_slug: str,
5959
toolkit_name: str,
60-
mcp_url: str, # The complete MCP URL
60+
mcp_url: str,
6161
redirect_url: Optional[str] = None,
6262
user_id: str = "default"
6363
) -> Dict[str, Any]:
6464
return {
6565
"type": "composio",
6666
"toolkit_slug": toolkit_slug,
6767
"toolkit_name": toolkit_name,
68-
"mcp_url": mcp_url, # Store the complete URL
68+
"mcp_url": mcp_url,
6969
"redirect_url": redirect_url,
7070
"user_id": user_id,
7171
"created_at": datetime.now(timezone.utc).isoformat()
7272
}
7373

7474
async def _generate_unique_profile_name(self, base_name: str, account_id: str, mcp_qualified_name: str, client) -> str:
75-
"""Generate a unique profile name by appending a number if needed"""
7675
original_name = base_name
7776
counter = 1
7877
current_name = base_name
7978

8079
while True:
81-
# Check if this name exists
8280
existing = await client.table('user_mcp_credential_profiles').select('profile_id').eq(
8381
'account_id', account_id
8482
).eq('mcp_qualified_name', mcp_qualified_name).eq('profile_name', current_name).execute()
8583

8684
if not existing.data:
8785
return current_name
8886

89-
# Generate next name
9087
counter += 1
9188
current_name = f"{original_name} ({counter})"
9289

@@ -96,7 +93,7 @@ async def create_profile(
9693
profile_name: str,
9794
toolkit_slug: str,
9895
toolkit_name: str,
99-
mcp_url: str, # The complete MCP URL from Composio
96+
mcp_url: str,
10097
redirect_url: Optional[str] = None,
10198
user_id: str = "default",
10299
is_default: bool = False
@@ -118,21 +115,18 @@ async def create_profile(
118115

119116
client = await self.db.client
120117

121-
# Generate unique profile name if needed
122118
unique_profile_name = await self._generate_unique_profile_name(
123119
profile_name, account_id, mcp_qualified_name, client
124120
)
125121

126122
if unique_profile_name != profile_name:
127123
logger.info(f"Generated unique profile name: {unique_profile_name} (original: {profile_name})")
128124

129-
# Set other profiles as non-default if this is default
130125
if is_default:
131126
await client.table('user_mcp_credential_profiles').update({
132127
'is_default': False
133128
}).eq('account_id', account_id).eq('mcp_qualified_name', mcp_qualified_name).execute()
134129

135-
# Insert new profile
136130
result = await client.table('user_mcp_credential_profiles').insert({
137131
'profile_id': profile_id,
138132
'account_id': account_id,
@@ -220,7 +214,6 @@ async def get_mcp_url_for_runtime(self, profile_id: str) -> str:
220214

221215
profile_data = result.data[0]
222216

223-
# Decrypt the config to get the MCP URL
224217
config = self._decrypt_config(profile_data['encrypted_config'])
225218

226219
if config.get('type') != 'composio':
@@ -238,7 +231,6 @@ async def get_mcp_url_for_runtime(self, profile_id: str) -> str:
238231
raise
239232

240233
async def get_profile_config(self, profile_id: str) -> Dict[str, Any]:
241-
"""Get decrypted config for a profile"""
242234
try:
243235
client = await self.db.client
244236

@@ -270,7 +262,6 @@ async def get_profiles(self, account_id: str, toolkit_slug: Optional[str] = None
270262

271263
profiles = []
272264
for row in result.data:
273-
# Decrypt and parse config
274265
config = self._decrypt_config(row['encrypted_config'])
275266

276267
profile = ComposioProfile(

backend/composio_integration/mcp_server_service.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,21 @@ def __init__(self, api_key: Optional[str] = None):
3636
self.client = ComposioClient.get_client(api_key)
3737

3838
def _generate_cuid(self, length: int = 8) -> str:
39-
"""Generate a random CUID with letters and numbers only"""
4039
return ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length))
4140

4241
def _generate_server_name(self, toolkit_name: str) -> str:
43-
"""Generate a valid server name: appname-cuid (4-30 chars, letters/numbers/spaces/hyphens only)"""
44-
# Clean the toolkit name: remove invalid chars, convert to lowercase
4542
clean_name = ''.join(c.lower() if c.isalnum() else '-' for c in toolkit_name)
46-
clean_name = clean_name.strip('-') # Remove leading/trailing hyphens
43+
clean_name = clean_name.strip('-')
4744

48-
# Generate CUID
4945
cuid = self._generate_cuid(8)
50-
51-
# Create server name
46+
5247
server_name = f"{clean_name}-{cuid}"
5348

54-
# Ensure it's within 4-30 characters
5549
if len(server_name) > 30:
56-
# Truncate the app name to fit
57-
max_app_name_length = 30 - len(cuid) - 1 # -1 for the hyphen
50+
max_app_name_length = 30 - len(cuid) - 1
5851
clean_name = clean_name[:max_app_name_length]
5952
server_name = f"{clean_name}-{cuid}"
6053

61-
# Ensure minimum length
6254
if len(server_name) < 4:
6355
server_name = f"app-{cuid}"
6456

@@ -82,22 +74,19 @@ async def create_mcp_server(
8274

8375
logger.info(f"Using MCP server name: {name}")
8476

85-
# Try different possible API paths
8677
try:
8778
response = self.client.mcp.create(
8879
auth_config_ids=auth_config_ids,
8980
name=name,
9081
allowed_tools=allowed_tools
9182
)
9283
except AttributeError:
93-
# Fallback: try direct method call
9484
response = self.client.create_mcp_server(
9585
auth_config_ids=auth_config_ids,
9686
name=name,
9787
allowed_tools=allowed_tools
9888
)
99-
100-
# Access Pydantic model attributes directly
89+
10190
commands_obj = getattr(response, 'commands', None)
10291

10392
commands = MCPCommands(

backend/composio_integration/toolkit_service.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ async def list_toolkits(self, limit: int = 500, cursor: Optional[str] = None, ca
163163
)
164164
toolkits.append(toolkit)
165165

166-
# Return pagination response structure
167166
result = {
168167
"items": toolkits,
169168
"total_items": response_data.get("total_items", len(toolkits)),
@@ -209,9 +208,9 @@ async def search_toolkits(self, query: str, category: Optional[str] = None, limi
209208
result = {
210209
"items": limited_results,
211210
"total_items": len(filtered_toolkits),
212-
"total_pages": 1, # For search, we'll keep it simple with one page
211+
"total_pages": 1,
213212
"current_page": 1,
214-
"next_cursor": None # For search, we don't use cursor pagination
213+
"next_cursor": None
215214
}
216215

217216
logger.info(f"Found {len(filtered_toolkits)} toolkits with OAUTH2 in both auth schemes matching query: {query}" + (f" in category {category}" if category else ""))
@@ -262,7 +261,6 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
262261

263262
logger.info(f"Raw toolkit response for {toolkit_slug}: {toolkit_response}")
264263

265-
# Parse the response
266264
meta = toolkit_dict.get('meta', {})
267265
if hasattr(meta, '__dict__'):
268266
meta = meta.__dict__
@@ -272,13 +270,12 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
272270
name=toolkit_dict.get('name', ''),
273271
description=meta.get('description', '') if isinstance(meta, dict) else getattr(meta, 'description', ''),
274272
logo=meta.get('logo') if isinstance(meta, dict) else getattr(meta, 'logo', None),
275-
tags=[], # Will populate from meta if available
273+
tags=[],
276274
auth_schemes=toolkit_dict.get('composio_managed_auth_schemes', []),
277275
categories=[],
278276
base_url=toolkit_dict.get('base_url')
279277
)
280278

281-
# Parse categories properly
282279
categories_data = meta.get('categories', []) if isinstance(meta, dict) else getattr(meta, 'categories', [])
283280
detailed_toolkit.categories = [
284281
cat.get('name', '') if isinstance(cat, dict) else getattr(cat, 'name', '')
@@ -287,18 +284,15 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
287284

288285
logger.info(f"Parsed basic toolkit info: {detailed_toolkit}")
289286

290-
# Parse auth_config_details
291287
auth_config_details = []
292288
raw_auth_configs = toolkit_dict.get('auth_config_details', [])
293289

294290
for config in raw_auth_configs:
295-
# Handle both dict and object formats
296291
if hasattr(config, '__dict__'):
297292
config_dict = config.__dict__
298293
else:
299294
config_dict = config
300295

301-
# Extract fields - this could be a nested object
302296
fields_obj = config_dict.get('fields')
303297
if hasattr(fields_obj, '__dict__'):
304298
fields_dict = fields_obj.__dict__
@@ -307,7 +301,6 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
307301

308302
auth_fields = {}
309303

310-
# Parse each field type (auth_config_creation, connected_account_initiation, etc.)
311304
for field_type, field_type_obj in fields_dict.items():
312305
auth_fields[field_type] = {}
313306

@@ -316,7 +309,6 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
316309
else:
317310
field_type_dict = field_type_obj or {}
318311

319-
# Parse required and optional fields
320312
for requirement_level in ['required', 'optional']:
321313
field_list = field_type_dict.get(requirement_level, [])
322314

@@ -329,7 +321,7 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
329321

330322
auth_config_fields.append(AuthConfigField(
331323
name=field_dict.get('name', ''),
332-
displayName=field_dict.get('display_name', ''), # Note: display_name not displayName
324+
displayName=field_dict.get('display_name', ''),
333325
type=field_dict.get('type', 'string'),
334326
description=field_dict.get('description'),
335327
required=field_dict.get('required', False),
@@ -346,10 +338,8 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
346338

347339
detailed_toolkit.auth_config_details = auth_config_details
348340

349-
# Extract connected_account_initiation fields specifically
350341
connected_account_initiation = None
351342
for config in raw_auth_configs:
352-
# Handle both dict and object formats
353343
if hasattr(config, '__dict__'):
354344
config_dict = config.__dict__
355345
else:
@@ -398,4 +388,4 @@ async def get_detailed_toolkit_info(self, toolkit_slug: str) -> Optional[Detaile
398388

399389
except Exception as e:
400390
logger.error(f"Failed to get detailed toolkit info for {toolkit_slug}: {e}", exc_info=True)
401-
return None
391+
return None

frontend/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=""
44
NEXT_PUBLIC_BACKEND_URL="http://localhost:8000/api"
55
NEXT_PUBLIC_URL="http://localhost:3000"
66
NEXT_PUBLIC_GOOGLE_CLIENT_ID=""
7+
NEXT_PUBLIC_POSTHOG_KEY=""
8+
79
OPENAI_API_KEY=""
810
KORTIX_ADMIN_API_KEY=""
911

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@hookform/resolvers": "^5.0.1",
2020
"@next/third-parties": "^15.3.1",
2121
"@number-flow/react": "^0.5.7",
22-
"@pipedream/sdk": "^1.7.0",
22+
2323
"@radix-ui/react-accordion": "^1.2.11",
2424
"@radix-ui/react-alert-dialog": "^1.1.11",
2525
"@radix-ui/react-avatar": "^1.1.4",

frontend/src/app/(dashboard)/agents/page.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ export default function AgentsPage() {
358358
}
359359

360360
const customRequirements = item.mcp_requirements?.filter(req =>
361-
req.custom_type && req.custom_type !== 'pipedream'
361+
req.custom_type
362362
) || [];
363363
const missingCustomConfigs = customRequirements.filter(req =>
364364
!customMcpConfigs || !customMcpConfigs[req.qualified_name] ||
@@ -371,20 +371,6 @@ export default function AgentsPage() {
371371
return;
372372
}
373373

374-
const pipedreamRequirements = item.mcp_requirements?.filter(req =>
375-
req.custom_type === 'pipedream'
376-
) || [];
377-
const missingPipedreamConfigs = pipedreamRequirements.filter(req =>
378-
!customMcpConfigs || !customMcpConfigs[req.qualified_name] ||
379-
!customMcpConfigs[req.qualified_name].profile_id
380-
);
381-
382-
if (missingPipedreamConfigs.length > 0) {
383-
const missingNames = missingPipedreamConfigs.map(req => req.display_name).join(', ');
384-
toast.error(`Please select Pipedream profiles for: ${missingNames}`);
385-
return;
386-
}
387-
388374
const result = await installTemplateMutation.mutateAsync({
389375
template_id: item.template_id,
390376
instance_name: instanceName,
@@ -566,7 +552,6 @@ export default function AgentsPage() {
566552
</div>
567553
</div>
568554
<div className="container mx-auto max-w-7xl px-4 py-2">
569-
{/* Fixed height container to prevent layout shifts on tab change */}
570555
<div className="w-full min-h-[calc(100vh-300px)]">
571556
{activeTab === "my-agents" && (
572557
<MyAgentsTab

0 commit comments

Comments
 (0)