vibesurf 0.1.31__py3-none-any.whl → 0.1.33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +1 -1
- vibe_surf/agents/prompts/vibe_surf_prompt.py +6 -0
- vibe_surf/agents/report_writer_agent.py +50 -0
- vibe_surf/agents/vibe_surf_agent.py +56 -1
- vibe_surf/backend/api/composio.py +952 -0
- vibe_surf/backend/database/migrations/v005_add_composio_integration.sql +33 -0
- vibe_surf/backend/database/migrations/v006_add_credentials_table.sql +26 -0
- vibe_surf/backend/database/models.py +53 -1
- vibe_surf/backend/database/queries.py +312 -2
- vibe_surf/backend/main.py +28 -0
- vibe_surf/backend/shared_state.py +123 -9
- vibe_surf/chrome_extension/scripts/api-client.js +32 -0
- vibe_surf/chrome_extension/scripts/settings-manager.js +954 -1
- vibe_surf/chrome_extension/sidepanel.html +190 -0
- vibe_surf/chrome_extension/styles/settings-integrations.css +927 -0
- vibe_surf/chrome_extension/styles/settings-modal.css +7 -3
- vibe_surf/chrome_extension/styles/settings-responsive.css +37 -5
- vibe_surf/cli.py +98 -3
- vibe_surf/telemetry/__init__.py +60 -0
- vibe_surf/telemetry/service.py +112 -0
- vibe_surf/telemetry/views.py +156 -0
- vibe_surf/tools/browser_use_tools.py +90 -90
- vibe_surf/tools/composio_client.py +456 -0
- vibe_surf/tools/mcp_client.py +21 -2
- vibe_surf/tools/vibesurf_tools.py +290 -87
- vibe_surf/tools/views.py +16 -0
- vibe_surf/tools/website_api/youtube/client.py +35 -13
- vibe_surf/utils.py +13 -0
- {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/METADATA +11 -9
- {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/RECORD +35 -26
- {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Composio API endpoints for VibeSurf Backend
|
|
3
|
+
|
|
4
|
+
Handles Composio integration management including toolkit configuration,
|
|
5
|
+
OAuth flow handling, and API key validation.
|
|
6
|
+
"""
|
|
7
|
+
import pdb
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Depends
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
from typing import Dict, List, Optional, Any
|
|
13
|
+
import logging
|
|
14
|
+
import json
|
|
15
|
+
import asyncio
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
from ..database.manager import get_db_session
|
|
19
|
+
from ..database.queries import ComposioToolkitQueries, LLMProfileQueries, CredentialQueries
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/composio", tags=["composio"])
|
|
22
|
+
|
|
23
|
+
from vibe_surf.logger import get_logger
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
# Pydantic models for Composio API
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ComposioKeyVerifyRequest(BaseModel):
|
|
32
|
+
"""Request model for verifying Composio API key"""
|
|
33
|
+
api_key: str = Field(description="Composio API key to verify")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ComposioKeyVerifyResponse(BaseModel):
|
|
37
|
+
"""Response model for Composio API key verification"""
|
|
38
|
+
valid: bool
|
|
39
|
+
message: str
|
|
40
|
+
user_info: Optional[Dict[str, Any]] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ComposioToolkitResponse(BaseModel):
|
|
44
|
+
"""Response model for Composio toolkit data"""
|
|
45
|
+
id: str
|
|
46
|
+
name: str
|
|
47
|
+
slug: str
|
|
48
|
+
description: Optional[str] = None
|
|
49
|
+
logo: Optional[str] = None
|
|
50
|
+
app_url: Optional[str] = None
|
|
51
|
+
enabled: bool
|
|
52
|
+
tools: Optional[List] = None
|
|
53
|
+
connection_status: Optional[str] = None
|
|
54
|
+
created_at: str
|
|
55
|
+
updated_at: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ComposioToolkitListResponse(BaseModel):
|
|
59
|
+
"""Response model for toolkit list"""
|
|
60
|
+
toolkits: List[ComposioToolkitResponse]
|
|
61
|
+
total_count: int
|
|
62
|
+
synced_count: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ComposioToolkitToggleRequest(BaseModel):
|
|
66
|
+
"""Request model for enabling/disabling a toolkit"""
|
|
67
|
+
enabled: bool = Field(description="Whether to enable or disable the toolkit")
|
|
68
|
+
force_reauth: Optional[bool] = Field(default=False, description="Force re-authentication if already connected")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ComposioToolkitToggleResponse(BaseModel):
|
|
72
|
+
"""Response model for toolkit toggle operation"""
|
|
73
|
+
success: bool
|
|
74
|
+
message: str
|
|
75
|
+
enabled: bool
|
|
76
|
+
requires_oauth: bool = False
|
|
77
|
+
oauth_url: Optional[str] = None
|
|
78
|
+
connected: bool = False
|
|
79
|
+
connection_status: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ComposioToolsResponse(BaseModel):
|
|
83
|
+
"""Response model for toolkit tools"""
|
|
84
|
+
toolkit_slug: str
|
|
85
|
+
tools: List[Dict[str, Any]]
|
|
86
|
+
total_tools: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ComposioToolsUpdateRequest(BaseModel):
|
|
90
|
+
"""Request model for updating selected tools"""
|
|
91
|
+
selected_tools: Dict[str, bool] = Field(description="Mapping of tool_name to enabled status")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ComposioConnectionStatusResponse(BaseModel):
|
|
95
|
+
"""Response model for connection status"""
|
|
96
|
+
toolkit_slug: str
|
|
97
|
+
connected: bool
|
|
98
|
+
connection_id: Optional[str] = None
|
|
99
|
+
status: str
|
|
100
|
+
last_checked: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def _get_composio_api_key_from_db() -> Optional[str]:
|
|
104
|
+
"""Get Composio API key from database credentials table (encrypted)"""
|
|
105
|
+
try:
|
|
106
|
+
from .. import shared_state
|
|
107
|
+
|
|
108
|
+
if not shared_state.db_manager:
|
|
109
|
+
logger.warning("Database manager not available")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
async for db in shared_state.db_manager.get_session():
|
|
113
|
+
try:
|
|
114
|
+
api_key = await CredentialQueries.get_credential(db, "COMPOSIO_API_KEY")
|
|
115
|
+
return api_key
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Failed to retrieve Composio API key from database: {e}")
|
|
118
|
+
return None
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Database session error while retrieving Composio API key: {e}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def _store_composio_api_key_in_db(api_key: str) -> bool:
|
|
125
|
+
"""Store Composio API key in database credentials table (encrypted)"""
|
|
126
|
+
try:
|
|
127
|
+
from .. import shared_state
|
|
128
|
+
if not shared_state.db_manager:
|
|
129
|
+
logger.warning("Database manager not available")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
async for db in shared_state.db_manager.get_session():
|
|
133
|
+
try:
|
|
134
|
+
success = await CredentialQueries.store_credential(
|
|
135
|
+
db,
|
|
136
|
+
"COMPOSIO_API_KEY",
|
|
137
|
+
api_key,
|
|
138
|
+
"Composio API key for toolkit integrations"
|
|
139
|
+
)
|
|
140
|
+
if success:
|
|
141
|
+
await db.commit()
|
|
142
|
+
logger.info("✅ Composio API key stored successfully")
|
|
143
|
+
return success
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Failed to store Composio API key in database: {e}")
|
|
146
|
+
return False
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Database session error while storing Composio API key: {e}")
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def _get_composio_instance():
|
|
153
|
+
"""Get or create Composio instance from shared state"""
|
|
154
|
+
try:
|
|
155
|
+
from .. import shared_state
|
|
156
|
+
if shared_state.composio_instance is None:
|
|
157
|
+
# Try to get API key from database first
|
|
158
|
+
api_key = await _get_composio_api_key_from_db()
|
|
159
|
+
if not api_key:
|
|
160
|
+
# If no API key in database, Composio instance cannot be created
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
# Import Composio here to avoid circular imports
|
|
164
|
+
from composio import Composio
|
|
165
|
+
from composio_langchain import LangchainProvider
|
|
166
|
+
|
|
167
|
+
# Create Composio instance
|
|
168
|
+
shared_state.composio_instance = Composio(
|
|
169
|
+
api_key=api_key,
|
|
170
|
+
provider=LangchainProvider()
|
|
171
|
+
)
|
|
172
|
+
logger.info("✅ Composio instance created successfully")
|
|
173
|
+
|
|
174
|
+
return shared_state.composio_instance
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Failed to get Composio instance: {e}")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.get("/status")
|
|
181
|
+
async def get_composio_status(
|
|
182
|
+
db: AsyncSession = Depends(get_db_session)
|
|
183
|
+
):
|
|
184
|
+
"""
|
|
185
|
+
Get current Composio connection status without API validation
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
from .. import shared_state
|
|
189
|
+
logger.info("Checking Composio connection status")
|
|
190
|
+
|
|
191
|
+
# Check if we already have a valid Composio instance
|
|
192
|
+
if shared_state.composio_instance is not None:
|
|
193
|
+
# try:
|
|
194
|
+
# # Quick test to verify instance is still valid
|
|
195
|
+
# await asyncio.to_thread(lambda: shared_state.composio_instance.toolkits.get())
|
|
196
|
+
# return {
|
|
197
|
+
# "connected": True,
|
|
198
|
+
# "key_valid": True,
|
|
199
|
+
# "has_key": True,
|
|
200
|
+
# "message": "Composio is connected and ready",
|
|
201
|
+
# "instance_available": True
|
|
202
|
+
# }
|
|
203
|
+
# except Exception as e:
|
|
204
|
+
# logger.warning(f"Composio instance validation failed: {e}")
|
|
205
|
+
# # Instance is invalid, clear it
|
|
206
|
+
# shared_state.composio_instance = None
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"connected": True,
|
|
210
|
+
"key_valid": True,
|
|
211
|
+
"has_key": True,
|
|
212
|
+
"message": "Composio is connected and ready",
|
|
213
|
+
"instance_available": True
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# No valid instance, check if we have API key in database
|
|
217
|
+
api_key = await _get_composio_api_key_from_db()
|
|
218
|
+
|
|
219
|
+
if api_key:
|
|
220
|
+
# Try to create instance with stored API key
|
|
221
|
+
try:
|
|
222
|
+
from composio import Composio
|
|
223
|
+
from composio_langchain import LangchainProvider
|
|
224
|
+
|
|
225
|
+
temp_composio = Composio(
|
|
226
|
+
api_key=api_key,
|
|
227
|
+
provider=LangchainProvider()
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Test the instance
|
|
231
|
+
api_toolkits = await asyncio.to_thread(lambda: temp_composio.toolkits.get())
|
|
232
|
+
|
|
233
|
+
oauth2_toolkits = []
|
|
234
|
+
|
|
235
|
+
for toolkit in api_toolkits:
|
|
236
|
+
if hasattr(toolkit, 'auth_schemes') and 'OAUTH2' in toolkit.auth_schemes:
|
|
237
|
+
oauth2_toolkits.append(toolkit)
|
|
238
|
+
|
|
239
|
+
logger.info(f"Found {len(oauth2_toolkits)} OAuth2 toolkits from Composio API")
|
|
240
|
+
|
|
241
|
+
# Sync with database
|
|
242
|
+
for api_toolkit in oauth2_toolkits:
|
|
243
|
+
# Check if toolkit already exists
|
|
244
|
+
existing_toolkit = await ComposioToolkitQueries.get_toolkit_by_slug(db, api_toolkit.slug)
|
|
245
|
+
|
|
246
|
+
# Get metadata from toolkit
|
|
247
|
+
description = getattr(api_toolkit.meta, 'description', None) if hasattr(api_toolkit,
|
|
248
|
+
'meta') else None
|
|
249
|
+
logo = getattr(api_toolkit.meta, 'logo', None) if hasattr(api_toolkit, 'meta') else None
|
|
250
|
+
app_url = getattr(api_toolkit.meta, 'app_url', None) if hasattr(api_toolkit, 'meta') else None
|
|
251
|
+
|
|
252
|
+
if not existing_toolkit:
|
|
253
|
+
# Create new toolkit
|
|
254
|
+
toolkit_data = await ComposioToolkitQueries.create_toolkit(
|
|
255
|
+
db=db,
|
|
256
|
+
name=api_toolkit.name,
|
|
257
|
+
slug=api_toolkit.slug,
|
|
258
|
+
description=description,
|
|
259
|
+
logo=logo,
|
|
260
|
+
app_url=app_url,
|
|
261
|
+
enabled=False,
|
|
262
|
+
tools=None
|
|
263
|
+
)
|
|
264
|
+
logger.info(f"Created new toolkit: {api_toolkit.name}")
|
|
265
|
+
else:
|
|
266
|
+
# Update existing toolkit information (but keep enabled status and tools)
|
|
267
|
+
update_data = {
|
|
268
|
+
'name': api_toolkit.name,
|
|
269
|
+
'description': description,
|
|
270
|
+
'logo': logo,
|
|
271
|
+
'app_url': app_url
|
|
272
|
+
}
|
|
273
|
+
await ComposioToolkitQueries.update_toolkit_by_slug(db, api_toolkit.slug, update_data)
|
|
274
|
+
logger.debug(f"Updated existing toolkit: {api_toolkit.name}")
|
|
275
|
+
|
|
276
|
+
await db.commit()
|
|
277
|
+
|
|
278
|
+
# Store valid instance
|
|
279
|
+
shared_state.composio_instance = temp_composio
|
|
280
|
+
|
|
281
|
+
logger.info("✅ Composio instance recreated from stored API key")
|
|
282
|
+
return {
|
|
283
|
+
"connected": True,
|
|
284
|
+
"key_valid": True,
|
|
285
|
+
"has_key": True,
|
|
286
|
+
"message": "Composio connection restored from stored API key",
|
|
287
|
+
"instance_available": True
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning(f"Stored API key validation failed: {e}")
|
|
292
|
+
# Clear invalid stored key
|
|
293
|
+
shared_state.composio_instance = None
|
|
294
|
+
return {
|
|
295
|
+
"connected": False,
|
|
296
|
+
"key_valid": False,
|
|
297
|
+
"has_key": True,
|
|
298
|
+
"message": f"Stored API key is invalid: {str(e)}",
|
|
299
|
+
"instance_available": False
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# No API key in database
|
|
303
|
+
return {
|
|
304
|
+
"connected": False,
|
|
305
|
+
"key_valid": False,
|
|
306
|
+
"has_key": False,
|
|
307
|
+
"message": "No Composio API key configured",
|
|
308
|
+
"instance_available": False
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Failed to check Composio status: {e}")
|
|
313
|
+
return {
|
|
314
|
+
"connected": False,
|
|
315
|
+
"key_valid": False,
|
|
316
|
+
"has_key": False,
|
|
317
|
+
"message": f"Status check failed: {str(e)}",
|
|
318
|
+
"instance_available": False
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@router.post("/verify-key", response_model=ComposioKeyVerifyResponse)
|
|
323
|
+
async def verify_composio_api_key(
|
|
324
|
+
request: ComposioKeyVerifyRequest,
|
|
325
|
+
db: AsyncSession = Depends(get_db_session)
|
|
326
|
+
):
|
|
327
|
+
"""
|
|
328
|
+
Verify Composio API key validity and optionally store it
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
from .. import shared_state
|
|
332
|
+
logger.info("Verifying Composio API key")
|
|
333
|
+
|
|
334
|
+
# Import Composio here to avoid startup dependencies
|
|
335
|
+
from composio import Composio
|
|
336
|
+
from composio_langchain import LangchainProvider
|
|
337
|
+
|
|
338
|
+
# Create temporary Composio instance for verification
|
|
339
|
+
try:
|
|
340
|
+
temp_composio = Composio(
|
|
341
|
+
api_key=request.api_key,
|
|
342
|
+
provider=LangchainProvider()
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Test the API key by getting toolkits
|
|
346
|
+
toolkits = await asyncio.to_thread(lambda: temp_composio.toolkits.get(slug='gmail'))
|
|
347
|
+
|
|
348
|
+
# If we get here, the API key is valid
|
|
349
|
+
logger.info("✅ Composio API key verified successfully")
|
|
350
|
+
|
|
351
|
+
# Store the valid API key in database
|
|
352
|
+
store_success = await _store_composio_api_key_in_db(request.api_key)
|
|
353
|
+
|
|
354
|
+
# Update shared state with new Composio instance
|
|
355
|
+
shared_state.composio_instance = temp_composio
|
|
356
|
+
|
|
357
|
+
return ComposioKeyVerifyResponse(
|
|
358
|
+
valid=True,
|
|
359
|
+
message="API key verified successfully" + (" and stored in database" if store_success else ""),
|
|
360
|
+
user_info={"toolkits_count": len(toolkits) if toolkits else 0}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.warning(f"Composio API key verification failed: {e}")
|
|
365
|
+
return ComposioKeyVerifyResponse(
|
|
366
|
+
valid=False,
|
|
367
|
+
message=f"Invalid API key: {str(e)}"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Failed to verify Composio API key: {e}")
|
|
372
|
+
raise HTTPException(
|
|
373
|
+
status_code=500,
|
|
374
|
+
detail=f"Failed to verify Composio API key: {str(e)}"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@router.get("/toolkits", response_model=ComposioToolkitListResponse)
|
|
379
|
+
async def get_composio_toolkits(
|
|
380
|
+
sync_with_api: bool = False, # Changed default to False
|
|
381
|
+
db: AsyncSession = Depends(get_db_session)
|
|
382
|
+
):
|
|
383
|
+
"""
|
|
384
|
+
Get all OAuth2 toolkits from database and optionally sync with Composio API
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
from .. import shared_state
|
|
388
|
+
logger.info(f"Getting Composio toolkits (sync_with_api={sync_with_api})")
|
|
389
|
+
|
|
390
|
+
synced_count = 0
|
|
391
|
+
|
|
392
|
+
# Get all toolkits from database
|
|
393
|
+
db_toolkits = await ComposioToolkitQueries.list_toolkits(db, enabled_only=False)
|
|
394
|
+
# Convert to response format
|
|
395
|
+
toolkit_responses = []
|
|
396
|
+
for toolkit in db_toolkits:
|
|
397
|
+
# Parse tools JSON if present
|
|
398
|
+
tools_data = None
|
|
399
|
+
if toolkit.tools:
|
|
400
|
+
try:
|
|
401
|
+
tools_data = json.loads(toolkit.tools)
|
|
402
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
403
|
+
logger.warning(f"Failed to parse tools for toolkit {toolkit.slug}: {e}")
|
|
404
|
+
tools_data = None
|
|
405
|
+
|
|
406
|
+
toolkit_responses.append(ComposioToolkitResponse(
|
|
407
|
+
id=toolkit.id,
|
|
408
|
+
name=toolkit.name,
|
|
409
|
+
slug=toolkit.slug,
|
|
410
|
+
description=toolkit.description,
|
|
411
|
+
logo=toolkit.logo,
|
|
412
|
+
app_url=toolkit.app_url,
|
|
413
|
+
enabled=toolkit.enabled,
|
|
414
|
+
tools=tools_data,
|
|
415
|
+
connection_status="unknown", # Will be updated by connection status check
|
|
416
|
+
created_at=toolkit.created_at.isoformat(),
|
|
417
|
+
updated_at=toolkit.updated_at.isoformat()
|
|
418
|
+
))
|
|
419
|
+
logger.info(f"Found {len(toolkit_responses)} toolkits from Composio API")
|
|
420
|
+
return ComposioToolkitListResponse(
|
|
421
|
+
toolkits=toolkit_responses,
|
|
422
|
+
total_count=len(toolkit_responses),
|
|
423
|
+
synced_count=synced_count
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
except HTTPException:
|
|
427
|
+
raise
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Failed to get Composio toolkits: {e}")
|
|
430
|
+
raise HTTPException(
|
|
431
|
+
status_code=500,
|
|
432
|
+
detail=f"Failed to get Composio toolkits: {str(e)}"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@router.post("/toolkit/{slug}/toggle", response_model=ComposioToolkitToggleResponse)
|
|
437
|
+
async def toggle_composio_toolkit(
|
|
438
|
+
slug: str,
|
|
439
|
+
request: ComposioToolkitToggleRequest,
|
|
440
|
+
db: AsyncSession = Depends(get_db_session)
|
|
441
|
+
):
|
|
442
|
+
"""
|
|
443
|
+
Enable/disable a toolkit and handle OAuth flow if needed
|
|
444
|
+
"""
|
|
445
|
+
try:
|
|
446
|
+
logger.info(f"Toggling toolkit {slug} to enabled={request.enabled}")
|
|
447
|
+
|
|
448
|
+
# Get toolkit from database
|
|
449
|
+
toolkit = await ComposioToolkitQueries.get_toolkit_by_slug(db, slug)
|
|
450
|
+
if not toolkit:
|
|
451
|
+
raise HTTPException(
|
|
452
|
+
status_code=404,
|
|
453
|
+
detail=f"Toolkit '{slug}' not found"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Get Composio instance
|
|
457
|
+
composio = await _get_composio_instance()
|
|
458
|
+
if composio is None:
|
|
459
|
+
raise HTTPException(
|
|
460
|
+
status_code=400,
|
|
461
|
+
detail="Composio API key not configured. Please verify your API key first."
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
auth_url = None
|
|
465
|
+
connection_status = "disconnected"
|
|
466
|
+
entity_id = "default" # Use default entity ID
|
|
467
|
+
|
|
468
|
+
if request.enabled:
|
|
469
|
+
# Check if toolkit needs OAuth connection
|
|
470
|
+
try:
|
|
471
|
+
# Check for existing active connections using the new API
|
|
472
|
+
def _find_active_connection():
|
|
473
|
+
try:
|
|
474
|
+
connection_list = composio.connected_accounts.list(
|
|
475
|
+
user_ids=[entity_id],
|
|
476
|
+
toolkit_slugs=[slug.lower()]
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if connection_list and hasattr(connection_list, "items") and connection_list.items:
|
|
480
|
+
for connection in connection_list.items:
|
|
481
|
+
connection_id = getattr(connection, "id", None)
|
|
482
|
+
connection_status = getattr(connection, "status", None)
|
|
483
|
+
if connection_status == "ACTIVE" and connection_id:
|
|
484
|
+
return connection_id, connection_status
|
|
485
|
+
return None, None
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Error checking connections: {e}")
|
|
488
|
+
return None, None
|
|
489
|
+
|
|
490
|
+
connection_id, conn_status = await asyncio.to_thread(_find_active_connection)
|
|
491
|
+
|
|
492
|
+
if not connection_id or request.force_reauth:
|
|
493
|
+
# Need to create OAuth connection
|
|
494
|
+
try:
|
|
495
|
+
def _create_auth_connection():
|
|
496
|
+
try:
|
|
497
|
+
# Get or create auth config
|
|
498
|
+
auth_configs = composio.auth_configs.list(toolkit_slug=slug)
|
|
499
|
+
|
|
500
|
+
auth_config_id = None
|
|
501
|
+
if len(auth_configs.items) == 0:
|
|
502
|
+
# Create new auth config
|
|
503
|
+
auth_config_response = composio.auth_configs.create(
|
|
504
|
+
toolkit=slug,
|
|
505
|
+
options={"type": "use_composio_managed_auth"}
|
|
506
|
+
)
|
|
507
|
+
auth_config_id = auth_config_response.id if hasattr(auth_config_response,
|
|
508
|
+
'id') else auth_config_response
|
|
509
|
+
else:
|
|
510
|
+
# Use existing OAUTH2 auth config
|
|
511
|
+
for auth_config in auth_configs.items:
|
|
512
|
+
if auth_config.auth_scheme == "OAUTH2":
|
|
513
|
+
auth_config_id = auth_config.id
|
|
514
|
+
break
|
|
515
|
+
|
|
516
|
+
if not auth_config_id:
|
|
517
|
+
raise Exception("Could not find or create auth config")
|
|
518
|
+
|
|
519
|
+
# Initiate connection
|
|
520
|
+
connection_request = composio.connected_accounts.initiate(
|
|
521
|
+
user_id=entity_id,
|
|
522
|
+
auth_config_id=auth_config_id,
|
|
523
|
+
allow_multiple=True
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return getattr(connection_request, 'redirect_url', None)
|
|
527
|
+
|
|
528
|
+
except Exception as e:
|
|
529
|
+
logger.error(f"Error creating auth connection: {e}")
|
|
530
|
+
raise e
|
|
531
|
+
|
|
532
|
+
auth_url = await asyncio.to_thread(_create_auth_connection)
|
|
533
|
+
|
|
534
|
+
if auth_url:
|
|
535
|
+
connection_status = "pending_auth"
|
|
536
|
+
logger.info(f"Generated OAuth URL for {slug}: {auth_url}")
|
|
537
|
+
else:
|
|
538
|
+
logger.warning(f"No OAuth URL returned for {slug}")
|
|
539
|
+
connection_status = "error"
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
logger.error(f"Failed to create OAuth connection for {slug}: {e}")
|
|
543
|
+
connection_status = "error"
|
|
544
|
+
|
|
545
|
+
else:
|
|
546
|
+
connection_status = "connected"
|
|
547
|
+
logger.info(f"Toolkit {slug} already has active connection")
|
|
548
|
+
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.warning(f"Failed to check connections for {slug}: {e}")
|
|
551
|
+
connection_status = "unknown"
|
|
552
|
+
|
|
553
|
+
# If enabling and connected, fetch and save tools
|
|
554
|
+
if request.enabled and connection_status == "connected":
|
|
555
|
+
try:
|
|
556
|
+
entity_id = "default" # Use default entity ID
|
|
557
|
+
api_tools = await asyncio.to_thread(
|
|
558
|
+
lambda: composio.tools.get(user_id=entity_id, toolkits=[slug.lower()], limit=999)
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Convert to response format
|
|
562
|
+
tools_list = []
|
|
563
|
+
for tool in api_tools:
|
|
564
|
+
tools_list.append({
|
|
565
|
+
'name': tool.name,
|
|
566
|
+
'description': getattr(tool, 'description', ''),
|
|
567
|
+
'parameters': tool.args_schema.model_json_schema() if hasattr(tool, 'args_schema') else {},
|
|
568
|
+
'enabled': True # Default enabled
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
# Save tools to database
|
|
572
|
+
if tools_list:
|
|
573
|
+
try:
|
|
574
|
+
tools_json = json.dumps(tools_list)
|
|
575
|
+
await ComposioToolkitQueries.update_toolkit_tools(
|
|
576
|
+
db,
|
|
577
|
+
toolkit.id,
|
|
578
|
+
tools_json
|
|
579
|
+
)
|
|
580
|
+
logger.info(f"Synced {len(tools_list)} tools for toolkit {slug}")
|
|
581
|
+
except Exception as e:
|
|
582
|
+
logger.warning(f"Failed to save tools to database for toolkit {slug}: {e}")
|
|
583
|
+
|
|
584
|
+
except Exception as e:
|
|
585
|
+
logger.warning(f"Failed to fetch tools for toolkit {slug}: {e}")
|
|
586
|
+
|
|
587
|
+
# Update toolkit enabled status in database
|
|
588
|
+
update_data = {'enabled': request.enabled}
|
|
589
|
+
if request.enabled and connection_status == "connected" and 'tools_list' in locals():
|
|
590
|
+
# Also update tools if we fetched them
|
|
591
|
+
update_data['tools'] = json.dumps(tools_list) if tools_list else None
|
|
592
|
+
|
|
593
|
+
success = await ComposioToolkitQueries.update_toolkit_by_slug(
|
|
594
|
+
db,
|
|
595
|
+
slug,
|
|
596
|
+
update_data
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
if not success:
|
|
600
|
+
raise HTTPException(
|
|
601
|
+
status_code=500,
|
|
602
|
+
detail="Failed to update toolkit status in database"
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
await db.commit()
|
|
606
|
+
|
|
607
|
+
message = f"Toolkit '{toolkit.name}' {'enabled' if request.enabled else 'disabled'} successfully"
|
|
608
|
+
requires_oauth = auth_url is not None
|
|
609
|
+
is_connected = connection_status == "connected"
|
|
610
|
+
|
|
611
|
+
if auth_url:
|
|
612
|
+
message += ". Please complete OAuth authentication."
|
|
613
|
+
|
|
614
|
+
logger.info(f"✅ {message}")
|
|
615
|
+
|
|
616
|
+
return ComposioToolkitToggleResponse(
|
|
617
|
+
success=True,
|
|
618
|
+
message=message,
|
|
619
|
+
enabled=request.enabled,
|
|
620
|
+
requires_oauth=requires_oauth,
|
|
621
|
+
oauth_url=auth_url,
|
|
622
|
+
connected=is_connected,
|
|
623
|
+
connection_status=connection_status
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
except HTTPException:
|
|
627
|
+
raise
|
|
628
|
+
except Exception as e:
|
|
629
|
+
logger.error(f"Failed to toggle toolkit {slug}: {e}")
|
|
630
|
+
raise HTTPException(
|
|
631
|
+
status_code=500,
|
|
632
|
+
detail=f"Failed to toggle toolkit: {str(e)}"
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@router.get("/toolkit/{slug}/tools", response_model=ComposioToolsResponse)
|
|
637
|
+
async def get_toolkit_tools(
|
|
638
|
+
slug: str,
|
|
639
|
+
db: AsyncSession = Depends(get_db_session)
|
|
640
|
+
):
|
|
641
|
+
"""
|
|
642
|
+
Get available tools for a specific toolkit
|
|
643
|
+
First tries to get from database, if empty then fetches from API and saves to database
|
|
644
|
+
"""
|
|
645
|
+
try:
|
|
646
|
+
logger.info(f"Getting tools for toolkit {slug}")
|
|
647
|
+
|
|
648
|
+
# Get toolkit from database
|
|
649
|
+
toolkit = await ComposioToolkitQueries.get_toolkit_by_slug(db, slug)
|
|
650
|
+
if not toolkit:
|
|
651
|
+
raise HTTPException(
|
|
652
|
+
status_code=404,
|
|
653
|
+
detail=f"Toolkit '{slug}' not found"
|
|
654
|
+
)
|
|
655
|
+
# First, try to get tools from database
|
|
656
|
+
if toolkit.tools:
|
|
657
|
+
try:
|
|
658
|
+
tools_list = json.loads(toolkit.tools)
|
|
659
|
+
if tools_list:
|
|
660
|
+
logger.info(f"Found {len(tools_list)} tools for toolkit {slug} from database")
|
|
661
|
+
return ComposioToolsResponse(
|
|
662
|
+
toolkit_slug=slug,
|
|
663
|
+
tools=tools_list,
|
|
664
|
+
total_tools=len(tools_list)
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
668
|
+
logger.warning(f"Failed to parse existing tools from database for {slug}: {e}")
|
|
669
|
+
|
|
670
|
+
# If we reach here, either no tools in database or invalid format
|
|
671
|
+
# Get Composio instance and fetch from API
|
|
672
|
+
composio = await _get_composio_instance()
|
|
673
|
+
if composio is None:
|
|
674
|
+
raise HTTPException(
|
|
675
|
+
status_code=400,
|
|
676
|
+
detail="Composio API key not configured. Please verify your API key first."
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Get tools from Composio API
|
|
680
|
+
try:
|
|
681
|
+
entity_id = "default" # Use default entity ID
|
|
682
|
+
api_tools = await asyncio.to_thread(
|
|
683
|
+
lambda: composio.tools.get(user_id=entity_id, toolkits=[slug.lower()], limit=999)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Convert to response format
|
|
687
|
+
tools_list = []
|
|
688
|
+
for tool in api_tools:
|
|
689
|
+
tools_list.append({
|
|
690
|
+
'name': tool.name,
|
|
691
|
+
'description': getattr(tool, 'description', ''),
|
|
692
|
+
'parameters': tool.args_schema.model_json_schema() if hasattr(tool, 'args_schema') else {},
|
|
693
|
+
'enabled': True # Default enabled
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
# Save tools to database for future use
|
|
697
|
+
try:
|
|
698
|
+
tools_json = json.dumps(tools_list)
|
|
699
|
+
success = await ComposioToolkitQueries.update_toolkit_tools(
|
|
700
|
+
db,
|
|
701
|
+
toolkit.id,
|
|
702
|
+
tools_json
|
|
703
|
+
)
|
|
704
|
+
if success:
|
|
705
|
+
await db.commit()
|
|
706
|
+
else:
|
|
707
|
+
logger.warning(f"Failed to save tools to database for toolkit {slug}")
|
|
708
|
+
except Exception as e:
|
|
709
|
+
logger.warning(f"Failed to save tools to database for toolkit {slug}: {e}")
|
|
710
|
+
|
|
711
|
+
logger.info(f"Found {len(tools_list)} tools for toolkit {slug} from API and saved to database")
|
|
712
|
+
|
|
713
|
+
return ComposioToolsResponse(
|
|
714
|
+
toolkit_slug=slug,
|
|
715
|
+
tools=tools_list,
|
|
716
|
+
total_tools=len(tools_list)
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
except Exception as e:
|
|
720
|
+
logger.error(f"Failed to get tools for toolkit {slug} from API: {e}")
|
|
721
|
+
raise HTTPException(
|
|
722
|
+
status_code=500,
|
|
723
|
+
detail=f"Failed to get tools from Composio API: {str(e)}"
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
except HTTPException:
|
|
727
|
+
raise
|
|
728
|
+
except Exception as e:
|
|
729
|
+
logger.error(f"Failed to get toolkit tools for {slug}: {e}")
|
|
730
|
+
raise HTTPException(
|
|
731
|
+
status_code=500,
|
|
732
|
+
detail=f"Failed to get toolkit tools: {str(e)}"
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@router.post("/toolkit/{slug}/tools", response_model=ComposioToolsResponse)
|
|
737
|
+
async def update_toolkit_tools(
|
|
738
|
+
slug: str,
|
|
739
|
+
request: ComposioToolsUpdateRequest,
|
|
740
|
+
db: AsyncSession = Depends(get_db_session)
|
|
741
|
+
):
|
|
742
|
+
"""
|
|
743
|
+
Update selected tools for a toolkit
|
|
744
|
+
"""
|
|
745
|
+
try:
|
|
746
|
+
logger.info(f"Updating tools selection for toolkit {slug}")
|
|
747
|
+
logger.info(f"Request selected_tools: {request.selected_tools}")
|
|
748
|
+
|
|
749
|
+
# Get toolkit from database
|
|
750
|
+
toolkit = await ComposioToolkitQueries.get_toolkit_by_slug(db, slug)
|
|
751
|
+
if not toolkit:
|
|
752
|
+
raise HTTPException(
|
|
753
|
+
status_code=404,
|
|
754
|
+
detail=f"Toolkit '{slug}' not found"
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
logger.info(f"Found toolkit: {toolkit.name} (ID: {toolkit.id})")
|
|
758
|
+
logger.info(f"Existing tools in DB: {toolkit.tools}")
|
|
759
|
+
|
|
760
|
+
tools_list = []
|
|
761
|
+
if toolkit.tools:
|
|
762
|
+
try:
|
|
763
|
+
tools_list = json.loads(toolkit.tools)
|
|
764
|
+
logger.info(f"Parsed existing tools: {len(tools_list)} tools")
|
|
765
|
+
if tools_list:
|
|
766
|
+
for tool in tools_list:
|
|
767
|
+
original_enabled = tool.get('enabled', True)
|
|
768
|
+
new_enabled = request.selected_tools.get(tool['name'], True)
|
|
769
|
+
tool['enabled'] = new_enabled
|
|
770
|
+
logger.info(f"Tool {tool['name']}: {original_enabled} -> {new_enabled}")
|
|
771
|
+
except Exception as e:
|
|
772
|
+
logger.error(f"Failed to parse existing tools: {e}")
|
|
773
|
+
tools_list = []
|
|
774
|
+
|
|
775
|
+
if tools_list:
|
|
776
|
+
# Convert selected tools to JSON string
|
|
777
|
+
tools_json = json.dumps(tools_list)
|
|
778
|
+
logger.info(f"Tools JSON to save: {tools_json[:200]}...") # Log first 200 chars
|
|
779
|
+
else:
|
|
780
|
+
tools_json = ''
|
|
781
|
+
logger.info("No tools to save, using empty string")
|
|
782
|
+
|
|
783
|
+
# Update toolkit tools in database
|
|
784
|
+
logger.info(f"Calling update_toolkit_tools with toolkit_id: {toolkit.id}")
|
|
785
|
+
success = await ComposioToolkitQueries.update_toolkit_tools(
|
|
786
|
+
db,
|
|
787
|
+
toolkit.id,
|
|
788
|
+
tools_json
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
if not success:
|
|
792
|
+
logger.error(f"Failed to update toolkit tools in database for {slug}")
|
|
793
|
+
raise HTTPException(
|
|
794
|
+
status_code=500,
|
|
795
|
+
detail="Failed to update toolkit tools in database"
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
await db.commit()
|
|
799
|
+
logger.info(f"✅ Database commit successful for {slug}")
|
|
800
|
+
|
|
801
|
+
# Get updated tools count
|
|
802
|
+
enabled_count = sum(1 for enabled in request.selected_tools.values() if enabled)
|
|
803
|
+
total_count = len(request.selected_tools)
|
|
804
|
+
|
|
805
|
+
logger.info(f"✅ Updated tools selection for {slug}: {enabled_count}/{total_count} tools enabled")
|
|
806
|
+
|
|
807
|
+
# Return current tools (reuse the get endpoint logic)
|
|
808
|
+
result = await get_toolkit_tools(slug, db)
|
|
809
|
+
logger.info(f"Returning updated tools response for {slug}")
|
|
810
|
+
return result
|
|
811
|
+
|
|
812
|
+
except HTTPException:
|
|
813
|
+
raise
|
|
814
|
+
except Exception as e:
|
|
815
|
+
logger.error(f"Failed to update toolkit tools for {slug}: {e}")
|
|
816
|
+
raise HTTPException(
|
|
817
|
+
status_code=500,
|
|
818
|
+
detail=f"Failed to update toolkit tools: {str(e)}"
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@router.get("/toolkit/{slug}/connection-status", response_model=ComposioConnectionStatusResponse)
|
|
823
|
+
async def get_toolkit_connection_status(
|
|
824
|
+
slug: str,
|
|
825
|
+
db: AsyncSession = Depends(get_db_session)
|
|
826
|
+
):
|
|
827
|
+
"""
|
|
828
|
+
Check connection status for a specific toolkit
|
|
829
|
+
"""
|
|
830
|
+
try:
|
|
831
|
+
logger.info(f"Checking connection status for toolkit {slug}")
|
|
832
|
+
|
|
833
|
+
# Get toolkit from database
|
|
834
|
+
toolkit = await ComposioToolkitQueries.get_toolkit_by_slug(db, slug)
|
|
835
|
+
if not toolkit:
|
|
836
|
+
raise HTTPException(
|
|
837
|
+
status_code=404,
|
|
838
|
+
detail=f"Toolkit '{slug}' not found"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
# Get Composio instance
|
|
842
|
+
composio = await _get_composio_instance()
|
|
843
|
+
if composio is None:
|
|
844
|
+
return ComposioConnectionStatusResponse(
|
|
845
|
+
toolkit_slug=slug,
|
|
846
|
+
connected=False,
|
|
847
|
+
connection_id=None,
|
|
848
|
+
status="no_api_key",
|
|
849
|
+
last_checked=datetime.now().isoformat()
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Check connection status with Composio API
|
|
853
|
+
try:
|
|
854
|
+
entity_id = "default" # Use default entity ID
|
|
855
|
+
|
|
856
|
+
def _check_connection_status():
|
|
857
|
+
try:
|
|
858
|
+
connection_list = composio.connected_accounts.list(
|
|
859
|
+
user_ids=[entity_id],
|
|
860
|
+
toolkit_slugs=[slug.lower()]
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
if connection_list and hasattr(connection_list, "items") and connection_list.items:
|
|
864
|
+
for connection in connection_list.items:
|
|
865
|
+
connection_id = getattr(connection, "id", None)
|
|
866
|
+
connection_status = getattr(connection, "status", None)
|
|
867
|
+
if connection_status == "ACTIVE" and connection_id:
|
|
868
|
+
return connection_id, "connected"
|
|
869
|
+
return None, "disconnected"
|
|
870
|
+
except Exception as e:
|
|
871
|
+
logger.error(f"Error checking connection status: {e}")
|
|
872
|
+
return None, "error"
|
|
873
|
+
|
|
874
|
+
connection_id, status = await asyncio.to_thread(_check_connection_status)
|
|
875
|
+
|
|
876
|
+
return ComposioConnectionStatusResponse(
|
|
877
|
+
toolkit_slug=slug,
|
|
878
|
+
connected=(status == "connected"),
|
|
879
|
+
connection_id=connection_id,
|
|
880
|
+
status=status,
|
|
881
|
+
last_checked=datetime.now().isoformat()
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
except Exception as e:
|
|
885
|
+
logger.error(f"Failed to check connection status for {slug}: {e}")
|
|
886
|
+
return ComposioConnectionStatusResponse(
|
|
887
|
+
toolkit_slug=slug,
|
|
888
|
+
connected=False,
|
|
889
|
+
connection_id=None,
|
|
890
|
+
status="error",
|
|
891
|
+
last_checked=datetime.now().isoformat()
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
except HTTPException:
|
|
895
|
+
raise
|
|
896
|
+
except Exception as e:
|
|
897
|
+
logger.error(f"Failed to get connection status for {slug}: {e}")
|
|
898
|
+
raise HTTPException(
|
|
899
|
+
status_code=500,
|
|
900
|
+
detail=f"Failed to get connection status: {str(e)}"
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
# OAuth is now handled via browser popups using Composio's managed auth system
|
|
905
|
+
# No callback endpoint needed as authentication is handled in popup windows
|
|
906
|
+
|
|
907
|
+
# Health check endpoint
|
|
908
|
+
@router.get("/health")
|
|
909
|
+
async def composio_health_check():
|
|
910
|
+
"""
|
|
911
|
+
Check Composio integration health
|
|
912
|
+
"""
|
|
913
|
+
try:
|
|
914
|
+
composio = await _get_composio_instance()
|
|
915
|
+
|
|
916
|
+
if composio is None:
|
|
917
|
+
return {
|
|
918
|
+
"status": "no_api_key",
|
|
919
|
+
"message": "Composio API key not configured",
|
|
920
|
+
"timestamp": datetime.now().isoformat()
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
# Test API connection
|
|
924
|
+
# try:
|
|
925
|
+
# toolkits = await asyncio.to_thread(lambda: composio.toolkits.get())
|
|
926
|
+
# return {
|
|
927
|
+
# "status": "healthy",
|
|
928
|
+
# "message": "Composio API connection working",
|
|
929
|
+
# "toolkits_count": len(toolkits) if toolkits else 0,
|
|
930
|
+
# "timestamp": datetime.now().isoformat()
|
|
931
|
+
# }
|
|
932
|
+
# except Exception as e:
|
|
933
|
+
# return {
|
|
934
|
+
# "status": "api_error",
|
|
935
|
+
# "message": f"Composio API error: {str(e)}",
|
|
936
|
+
# "timestamp": datetime.now().isoformat()
|
|
937
|
+
# }
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
"status": "healthy",
|
|
941
|
+
"message": "Composio API connection working",
|
|
942
|
+
"toolkits_count": 0,
|
|
943
|
+
"timestamp": datetime.now().isoformat()
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
except Exception as e:
|
|
947
|
+
logger.error(f"Composio health check failed: {e}")
|
|
948
|
+
return {
|
|
949
|
+
"status": "error",
|
|
950
|
+
"message": f"Health check failed: {str(e)}",
|
|
951
|
+
"timestamp": datetime.now().isoformat()
|
|
952
|
+
}
|