feat: add Composio connector types and enhance integration

- Introduced new enum values for Composio connectors: COMPOSIO_GOOGLE_DRIVE_CONNECTOR, COMPOSIO_GMAIL_CONNECTOR, and COMPOSIO_GOOGLE_CALENDAR_CONNECTOR.
- Updated database migration to add these new enum values to the relevant types.
- Refactored Composio integration logic to handle specific connector types, improving the management of connected accounts and indexing processes.
- Enhanced frontend components to support the new Composio connector types, including updated UI elements and connector configuration handling.
- Improved backend services to manage Composio connected accounts more effectively, including deletion and indexing tasks.
This commit is contained in:
Anish Sarkar 2026-01-22 22:33:28 +05:30
parent 3a1fa25a6f
commit be5715cfeb
19 changed files with 437 additions and 277 deletions

View file

@ -1,16 +1,21 @@
"""Add COMPOSIO_CONNECTOR to SearchSourceConnectorType and DocumentType enums """Add Composio connector types to SearchSourceConnectorType and DocumentType enums
Revision ID: 74 Revision ID: 74
Revises: 73 Revises: 73
Create Date: 2026-01-21 Create Date: 2026-01-21
This migration adds the COMPOSIO_CONNECTOR enum value to both: This migration adds the Composio connector enum values to both:
- searchsourceconnectortype (for connector type tracking) - searchsourceconnectortype (for connector type tracking)
- documenttype (for document type tracking) - documenttype (for document type tracking)
Composio is a managed OAuth integration service that allows connecting Composio is a managed OAuth integration service that allows connecting
to various third-party services (Google Drive, Gmail, Calendar, etc.) to various third-party services (Google Drive, Gmail, Calendar, etc.)
without requiring separate OAuth app verification. without requiring separate OAuth app verification.
This migration adds three specific connector types:
- COMPOSIO_GOOGLE_DRIVE_CONNECTOR
- COMPOSIO_GMAIL_CONNECTOR
- COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
""" """
from collections.abc import Sequence from collections.abc import Sequence
@ -23,55 +28,65 @@ down_revision: str | None = "73"
branch_labels: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None
# Define the ENUM type names and the new value # Define the ENUM type names and the new values
CONNECTOR_ENUM = "searchsourceconnectortype" CONNECTOR_ENUM = "searchsourceconnectortype"
CONNECTOR_NEW_VALUE = "COMPOSIO_CONNECTOR" CONNECTOR_NEW_VALUES = [
"COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
"COMPOSIO_GMAIL_CONNECTOR",
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
]
DOCUMENT_ENUM = "documenttype" DOCUMENT_ENUM = "documenttype"
DOCUMENT_NEW_VALUE = "COMPOSIO_CONNECTOR" DOCUMENT_NEW_VALUES = [
"COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
"COMPOSIO_GMAIL_CONNECTOR",
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
]
def upgrade() -> None: def upgrade() -> None:
"""Upgrade schema - add COMPOSIO_CONNECTOR to connector and document enums safely.""" """Upgrade schema - add Composio connector types to connector and document enums safely."""
# Add COMPOSIO_CONNECTOR to searchsourceconnectortype only if not exists # Add each Composio connector type to searchsourceconnectortype only if not exists
op.execute( for value in CONNECTOR_NEW_VALUES:
f""" op.execute(
DO $$ f"""
BEGIN DO $$
IF NOT EXISTS ( BEGIN
SELECT 1 FROM pg_enum IF NOT EXISTS (
WHERE enumlabel = '{CONNECTOR_NEW_VALUE}' SELECT 1 FROM pg_enum e
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = '{CONNECTOR_ENUM}') JOIN pg_type t ON e.enumtypid = t.oid
) THEN WHERE t.typname = '{CONNECTOR_ENUM}' AND e.enumlabel = '{value}'
ALTER TYPE {CONNECTOR_ENUM} ADD VALUE '{CONNECTOR_NEW_VALUE}'; ) THEN
END IF; ALTER TYPE {CONNECTOR_ENUM} ADD VALUE '{value}';
END$$; END IF;
""" END$$;
) """
)
# Add COMPOSIO_CONNECTOR to documenttype only if not exists # Add each Composio connector type to documenttype only if not exists
op.execute( for value in DOCUMENT_NEW_VALUES:
f""" op.execute(
DO $$ f"""
BEGIN DO $$
IF NOT EXISTS ( BEGIN
SELECT 1 FROM pg_enum IF NOT EXISTS (
WHERE enumlabel = '{DOCUMENT_NEW_VALUE}' SELECT 1 FROM pg_enum e
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = '{DOCUMENT_ENUM}') JOIN pg_type t ON e.enumtypid = t.oid
) THEN WHERE t.typname = '{DOCUMENT_ENUM}' AND e.enumlabel = '{value}'
ALTER TYPE {DOCUMENT_ENUM} ADD VALUE '{DOCUMENT_NEW_VALUE}'; ) THEN
END IF; ALTER TYPE {DOCUMENT_ENUM} ADD VALUE '{value}';
END$$; END IF;
""" END$$;
) """
)
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema - remove COMPOSIO_CONNECTOR from connector and document enums. """Downgrade schema - remove Composio connector types from connector and document enums.
Note: PostgreSQL does not support removing enum values directly. Note: PostgreSQL does not support removing enum values directly.
To properly downgrade, you would need to: To properly downgrade, you would need to:
1. Delete any rows using the COMPOSIO_CONNECTOR value 1. Delete any rows using the Composio connector type values
2. Create new enums without COMPOSIO_CONNECTOR 2. Create new enums without the Composio connector types
3. Alter the columns to use the new enums 3. Alter the columns to use the new enums
4. Drop the old enums 4. Drop the old enums

View file

@ -54,7 +54,9 @@ class DocumentType(str, Enum):
BOOKSTACK_CONNECTOR = "BOOKSTACK_CONNECTOR" BOOKSTACK_CONNECTOR = "BOOKSTACK_CONNECTOR"
CIRCLEBACK = "CIRCLEBACK" CIRCLEBACK = "CIRCLEBACK"
NOTE = "NOTE" NOTE = "NOTE"
COMPOSIO_CONNECTOR = "COMPOSIO_CONNECTOR" # Generic Composio integration COMPOSIO_GOOGLE_DRIVE_CONNECTOR = "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"
COMPOSIO_GMAIL_CONNECTOR = "COMPOSIO_GMAIL_CONNECTOR"
COMPOSIO_GOOGLE_CALENDAR_CONNECTOR = "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR"
class SearchSourceConnectorType(str, Enum): class SearchSourceConnectorType(str, Enum):
@ -82,7 +84,9 @@ class SearchSourceConnectorType(str, Enum):
BOOKSTACK_CONNECTOR = "BOOKSTACK_CONNECTOR" BOOKSTACK_CONNECTOR = "BOOKSTACK_CONNECTOR"
CIRCLEBACK_CONNECTOR = "CIRCLEBACK_CONNECTOR" CIRCLEBACK_CONNECTOR = "CIRCLEBACK_CONNECTOR"
MCP_CONNECTOR = "MCP_CONNECTOR" # Model Context Protocol - User-defined API tools MCP_CONNECTOR = "MCP_CONNECTOR" # Model Context Protocol - User-defined API tools
COMPOSIO_CONNECTOR = "COMPOSIO_CONNECTOR" # Generic Composio integration (Google, Slack, etc.) COMPOSIO_GOOGLE_DRIVE_CONNECTOR = "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"
COMPOSIO_GMAIL_CONNECTOR = "COMPOSIO_GMAIL_CONNECTOR"
COMPOSIO_GOOGLE_CALENDAR_CONNECTOR = "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR"
class LiteLLMProvider(str, Enum): class LiteLLMProvider(str, Enum):

View file

@ -19,6 +19,7 @@ from fastapi.responses import RedirectResponse
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.config import config from app.config import config
from app.db import ( from app.db import (
@ -30,15 +31,17 @@ from app.db import (
from app.services.composio_service import ( from app.services.composio_service import (
COMPOSIO_TOOLKIT_NAMES, COMPOSIO_TOOLKIT_NAMES,
INDEXABLE_TOOLKITS, INDEXABLE_TOOLKITS,
TOOLKIT_TO_CONNECTOR_TYPE,
ComposioService, ComposioService,
) )
from app.users import current_active_user from app.users import current_active_user
from app.utils.connector_naming import ( from app.utils.connector_naming import generate_unique_connector_name
check_duplicate_connector,
generate_unique_connector_name,
)
from app.utils.oauth_security import OAuthStateManager from app.utils.oauth_security import OAuthStateManager
# Note: We no longer use check_duplicate_connector for Composio connectors because
# Composio generates a new connected_account_id each time, even for the same Google account.
# Instead, we check for existing connectors by type/space/user and update them.
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@ -260,30 +263,65 @@ async def composio_callback(
"is_indexable": toolkit_id in INDEXABLE_TOOLKITS, "is_indexable": toolkit_id in INDEXABLE_TOOLKITS,
} }
# Check for duplicate connector # Get the specific connector type for this toolkit
# For Composio, we use toolkit_id + connected_account_id as unique identifier connector_type_str = TOOLKIT_TO_CONNECTOR_TYPE.get(toolkit_id)
identifier = final_connected_account_id or f"{toolkit_id}_{user_id}" if not connector_type_str:
raise HTTPException(
is_duplicate = await check_duplicate_connector( status_code=400,
session, detail=f"Unknown toolkit: {toolkit_id}. Available: {list(TOOLKIT_TO_CONNECTOR_TYPE.keys())}",
SearchSourceConnectorType.COMPOSIO_CONNECTOR,
space_id,
user_id,
identifier,
)
if is_duplicate:
logger.warning(
f"Duplicate Composio connector detected for user {user_id} with toolkit {toolkit_id}"
) )
connector_type = SearchSourceConnectorType(connector_type_str)
# Check for existing connector of the same type for this user/space
# When reconnecting, Composio gives a new connected_account_id, so we need to
# check by connector_type, user_id, and search_space_id instead of connected_account_id
existing_connector_result = await session.execute(
select(SearchSourceConnector).where(
SearchSourceConnector.connector_type == connector_type,
SearchSourceConnector.search_space_id == space_id,
SearchSourceConnector.user_id == user_id,
)
)
existing_connector = existing_connector_result.scalars().first()
if existing_connector:
# Delete the old Composio connected account before updating
old_connected_account_id = existing_connector.config.get("composio_connected_account_id")
if old_connected_account_id and old_connected_account_id != final_connected_account_id:
try:
deleted = await service.delete_connected_account(old_connected_account_id)
if deleted:
logger.info(
f"Deleted old Composio connected account {old_connected_account_id} "
f"before updating connector {existing_connector.id}"
)
else:
logger.warning(
f"Failed to delete old Composio connected account {old_connected_account_id}"
)
except Exception as delete_error:
# Log but don't fail - the old account may already be deleted
logger.warning(
f"Error deleting old Composio connected account {old_connected_account_id}: {delete_error!s}"
)
# Update existing connector with new connected_account_id
logger.info(
f"Updating existing Composio connector {existing_connector.id} with new connected_account_id {final_connected_account_id}"
)
existing_connector.config = connector_config
await session.commit()
await session.refresh(existing_connector)
return RedirectResponse( return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=duplicate_account&connector=composio-connector" url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=composio-connector&connectorId={existing_connector.id}"
) )
try: try:
# Generate a unique, user-friendly connector name # Generate a unique, user-friendly connector name
connector_name = await generate_unique_connector_name( connector_name = await generate_unique_connector_name(
session, session,
SearchSourceConnectorType.COMPOSIO_CONNECTOR, connector_type,
space_id, space_id,
user_id, user_id,
f"{toolkit_name} (Composio)", f"{toolkit_name} (Composio)",
@ -291,7 +329,7 @@ async def composio_callback(
db_connector = SearchSourceConnector( db_connector = SearchSourceConnector(
name=connector_name, name=connector_name,
connector_type=SearchSourceConnectorType.COMPOSIO_CONNECTOR, connector_type=connector_type,
config=connector_config, config=connector_config,
search_space_id=space_id, search_space_id=space_id,
user_id=user_id, user_id=user_id,

View file

@ -37,6 +37,7 @@ from app.db import (
async_session_maker, async_session_maker,
get_async_session, get_async_session,
) )
from app.services.composio_service import ComposioService
from app.schemas import ( from app.schemas import (
GoogleDriveIndexRequest, GoogleDriveIndexRequest,
MCPConnectorCreate, MCPConnectorCreate,
@ -529,6 +530,34 @@ async def delete_search_source_connector(
f"Failed to delete periodic schedule for connector {connector_id}" f"Failed to delete periodic schedule for connector {connector_id}"
) )
# For Composio connectors, also delete the connected account in Composio
composio_connector_types = [
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR,
]
if db_connector.connector_type in composio_connector_types:
composio_connected_account_id = db_connector.config.get("composio_connected_account_id")
if composio_connected_account_id and ComposioService.is_enabled():
try:
service = ComposioService()
deleted = await service.delete_connected_account(composio_connected_account_id)
if deleted:
logger.info(
f"Successfully deleted Composio connected account {composio_connected_account_id} "
f"for connector {connector_id}"
)
else:
logger.warning(
f"Failed to delete Composio connected account {composio_connected_account_id} "
f"for connector {connector_id}"
)
except Exception as composio_error:
# Log but don't fail the deletion - Composio account may already be deleted
logger.warning(
f"Error deleting Composio connected account {composio_connected_account_id}: {composio_error!s}"
)
await session.delete(db_connector) await session.delete(db_connector)
await session.commit() await session.commit()
return {"message": "Search source connector deleted successfully"} return {"message": "Search source connector deleted successfully"}
@ -868,7 +897,11 @@ async def index_connector_content(
) )
response_message = "Web page indexing started in the background." response_message = "Web page indexing started in the background."
elif connector.connector_type == SearchSourceConnectorType.COMPOSIO_CONNECTOR: elif connector.connector_type in [
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR,
]:
from app.tasks.celery_tasks.connector_tasks import ( from app.tasks.celery_tasks.connector_tasks import (
index_composio_connector_task, index_composio_connector_task,
) )
@ -2086,6 +2119,59 @@ async def run_bookstack_indexing(
) )
async def run_composio_indexing_with_new_session(
connector_id: int,
search_space_id: int,
user_id: str,
start_date: str,
end_date: str,
):
"""
Create a new session and run the Composio indexing task.
This prevents session leaks by creating a dedicated session for the background task.
"""
async with async_session_maker() as session:
await run_composio_indexing(
session, connector_id, search_space_id, user_id, start_date, end_date
)
async def run_composio_indexing(
session: AsyncSession,
connector_id: int,
search_space_id: int,
user_id: str,
start_date: str,
end_date: str,
):
"""
Run Composio connector indexing with real-time notifications.
This wraps the Composio indexer with the notification system so that
Electric SQL can sync indexing progress to the frontend in real-time.
Args:
session: Database session
connector_id: ID of the Composio connector
search_space_id: ID of the search space
user_id: ID of the user
start_date: Start date for indexing
end_date: End date for indexing
"""
from app.tasks.composio_indexer import index_composio_connector
await _run_indexing_with_notifications(
session=session,
connector_id=connector_id,
search_space_id=search_space_id,
user_id=user_id,
start_date=start_date,
end_date=end_date,
indexing_function=index_composio_connector,
update_timestamp_func=_update_connector_timestamp_by_id,
)
# ============================================================================= # =============================================================================
# MCP Connector Routes # MCP Connector Routes
# ============================================================================= # =============================================================================

View file

@ -39,6 +39,20 @@ COMPOSIO_TOOLKIT_NAMES = {
# Toolkits that support indexing (Phase 1: Google services only) # Toolkits that support indexing (Phase 1: Google services only)
INDEXABLE_TOOLKITS = {"googledrive", "gmail", "googlecalendar"} INDEXABLE_TOOLKITS = {"googledrive", "gmail", "googlecalendar"}
# Mapping of toolkit IDs to connector types
TOOLKIT_TO_CONNECTOR_TYPE = {
"googledrive": "COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
"gmail": "COMPOSIO_GMAIL_CONNECTOR",
"googlecalendar": "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
}
# Mapping of toolkit IDs to document types
TOOLKIT_TO_DOCUMENT_TYPE = {
"googledrive": "COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
"gmail": "COMPOSIO_GMAIL_CONNECTOR",
"googlecalendar": "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
}
class ComposioService: class ComposioService:
"""Service for interacting with Composio API.""" """Service for interacting with Composio API."""
@ -298,6 +312,26 @@ class ComposioService:
logger.error(f"Failed to list connections for user {user_id}: {e!s}") logger.error(f"Failed to list connections for user {user_id}: {e!s}")
return [] return []
async def delete_connected_account(self, connected_account_id: str) -> bool:
"""
Delete a connected account from Composio.
This permanently removes the connected account and revokes access tokens.
Args:
connected_account_id: The Composio connected account ID to delete.
Returns:
True if deletion was successful, False otherwise.
"""
try:
self.client.connected_accounts.delete(connected_account_id)
logger.info(f"Successfully deleted Composio connected account: {connected_account_id}")
return True
except Exception as e:
logger.error(f"Failed to delete Composio connected account {connected_account_id}: {e!s}")
return False
async def execute_tool( async def execute_tool(
self, self,
connected_account_id: str, connected_account_id: str,

View file

@ -793,11 +793,13 @@ async def _index_composio_connector(
start_date: str, start_date: str,
end_date: str, end_date: str,
): ):
"""Index Composio connector content with new session.""" """Index Composio connector content with new session and real-time notifications."""
# Import from tasks folder (not connector_indexers) to avoid circular import # Import from routes to use the notification-wrapped version
from app.tasks.composio_indexer import index_composio_connector from app.routes.search_source_connectors_routes import (
run_composio_indexing,
)
async with get_celery_session_maker()() as session: async with get_celery_session_maker()() as session:
await index_composio_connector( await run_composio_indexing(
session, connector_id, search_space_id, user_id, start_date, end_date session, connector_id, search_space_id, user_id, start_date, end_date
) )

View file

@ -23,7 +23,7 @@ from app.db import (
SearchSourceConnector, SearchSourceConnector,
SearchSourceConnectorType, SearchSourceConnectorType,
) )
from app.services.composio_service import INDEXABLE_TOOLKITS from app.services.composio_service import INDEXABLE_TOOLKITS, TOOLKIT_TO_DOCUMENT_TYPE
from app.services.llm_service import get_user_long_context_llm from app.services.llm_service import get_user_long_context_llm
from app.services.task_logging_service import TaskLoggingService from app.services.task_logging_service import TaskLoggingService
from app.utils.document_converters import ( from app.utils.document_converters import (
@ -58,15 +58,13 @@ async def check_document_by_unique_identifier(
async def get_connector_by_id( async def get_connector_by_id(
session: AsyncSession, connector_id: int, connector_type: SearchSourceConnectorType session: AsyncSession, connector_id: int, connector_type: SearchSourceConnectorType | None
) -> SearchSourceConnector | None: ) -> SearchSourceConnector | None:
"""Get a connector by ID and type from the database.""" """Get a connector by ID and optionally by type from the database."""
result = await session.execute( query = select(SearchSourceConnector).filter(SearchSourceConnector.id == connector_id)
select(SearchSourceConnector).filter( if connector_type is not None:
SearchSourceConnector.id == connector_id, query = query.filter(SearchSourceConnector.connector_type == connector_type)
SearchSourceConnector.connector_type == connector_type, result = await session.execute(query)
)
)
return result.scalars().first() return result.scalars().first()
@ -129,10 +127,23 @@ async def index_composio_connector(
) )
try: try:
# Get connector by id # Get connector by id - accept any Composio connector type
# We'll check the actual type after loading
connector = await get_connector_by_id( connector = await get_connector_by_id(
session, connector_id, SearchSourceConnectorType.COMPOSIO_CONNECTOR session, connector_id, None # Don't filter by type, we'll validate after
) )
# Validate it's a Composio connector
if connector and connector.connector_type not in [
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR,
]:
error_msg = f"Connector {connector_id} is not a Composio connector"
await task_logger.log_task_failure(
log_entry, error_msg, {"error_type": "InvalidConnectorType"}
)
return 0, error_msg
if not connector: if not connector:
error_msg = f"Composio connector with ID {connector_id} not found" error_msg = f"Composio connector with ID {connector_id} not found"
@ -276,7 +287,7 @@ async def _index_composio_google_drive(
await task_logger.log_task_success( await task_logger.log_task_success(
log_entry, success_msg, {"files_count": 0} log_entry, success_msg, {"files_count": 0}
) )
return 0, success_msg return 0, None # Return None (not error) when no items found - this is success with 0 items
logger.info(f"Found {len(all_files)} Google Drive files to index via Composio") logger.info(f"Found {len(all_files)} Google Drive files to index via Composio")
@ -299,8 +310,9 @@ async def _index_composio_google_drive(
continue continue
# Generate unique identifier hash # Generate unique identifier hash
document_type = DocumentType(TOOLKIT_TO_DOCUMENT_TYPE["googledrive"])
unique_identifier_hash = generate_unique_identifier_hash( unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.COMPOSIO_CONNECTOR, f"drive_{file_id}", search_space_id document_type, f"drive_{file_id}", search_space_id
) )
# Check if document exists # Check if document exists
@ -394,7 +406,7 @@ async def _index_composio_google_drive(
document = Document( document = Document(
search_space_id=search_space_id, search_space_id=search_space_id,
title=f"Drive: {file_name}", title=f"Drive: {file_name}",
document_type=DocumentType.COMPOSIO_CONNECTOR, document_type=DocumentType(TOOLKIT_TO_DOCUMENT_TYPE["googledrive"]),
document_metadata={ document_metadata={
"file_id": file_id, "file_id": file_id,
"file_name": file_name, "file_name": file_name,
@ -489,7 +501,7 @@ async def _index_composio_gmail(
await task_logger.log_task_success( await task_logger.log_task_success(
log_entry, success_msg, {"messages_count": 0} log_entry, success_msg, {"messages_count": 0}
) )
return 0, success_msg return 0, None # Return None (not error) when no items found - this is success with 0 items
logger.info(f"Found {len(messages)} Gmail messages to index via Composio") logger.info(f"Found {len(messages)} Gmail messages to index via Composio")
@ -530,8 +542,9 @@ async def _index_composio_gmail(
markdown_content = composio_connector.format_gmail_message_to_markdown(message) markdown_content = composio_connector.format_gmail_message_to_markdown(message)
# Generate unique identifier # Generate unique identifier
document_type = DocumentType(TOOLKIT_TO_DOCUMENT_TYPE["gmail"])
unique_identifier_hash = generate_unique_identifier_hash( unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.COMPOSIO_CONNECTOR, f"gmail_{message_id}", search_space_id document_type, f"gmail_{message_id}", search_space_id
) )
content_hash = generate_content_hash(markdown_content, search_space_id) content_hash = generate_content_hash(markdown_content, search_space_id)
@ -612,7 +625,7 @@ async def _index_composio_gmail(
document = Document( document = Document(
search_space_id=search_space_id, search_space_id=search_space_id,
title=f"Gmail: {subject}", title=f"Gmail: {subject}",
document_type=DocumentType.COMPOSIO_CONNECTOR, document_type=DocumentType(TOOLKIT_TO_DOCUMENT_TYPE["gmail"]),
document_metadata={ document_metadata={
"message_id": message_id, "message_id": message_id,
"subject": subject, "subject": subject,
@ -717,7 +730,7 @@ async def _index_composio_google_calendar(
await task_logger.log_task_success( await task_logger.log_task_success(
log_entry, success_msg, {"events_count": 0} log_entry, success_msg, {"events_count": 0}
) )
return 0, success_msg return 0, None # Return None (not error) when no items found - this is success with 0 items
logger.info(f"Found {len(events)} Google Calendar events to index via Composio") logger.info(f"Found {len(events)} Google Calendar events to index via Composio")
@ -738,8 +751,9 @@ async def _index_composio_google_calendar(
markdown_content = composio_connector.format_calendar_event_to_markdown(event) markdown_content = composio_connector.format_calendar_event_to_markdown(event)
# Generate unique identifier # Generate unique identifier
document_type = DocumentType(TOOLKIT_TO_DOCUMENT_TYPE["googlecalendar"])
unique_identifier_hash = generate_unique_identifier_hash( unique_identifier_hash = generate_unique_identifier_hash(
DocumentType.COMPOSIO_CONNECTOR, f"calendar_{event_id}", search_space_id document_type, f"calendar_{event_id}", search_space_id
) )
content_hash = generate_content_hash(markdown_content, search_space_id) content_hash = generate_content_hash(markdown_content, search_space_id)
@ -828,7 +842,7 @@ async def _index_composio_google_calendar(
document = Document( document = Document(
search_space_id=search_space_id, search_space_id=search_space_id,
title=f"Calendar: {summary}", title=f"Calendar: {summary}",
document_type=DocumentType.COMPOSIO_CONNECTOR, document_type=DocumentType(TOOLKIT_TO_DOCUMENT_TYPE["googlecalendar"]),
document_metadata={ document_metadata={
"event_id": event_id, "event_id": event_id,
"summary": summary, "summary": summary,

View file

@ -188,8 +188,18 @@ export const ConnectorIndicator: FC = () => {
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
connectedToolkits={ connectedToolkits={
(connectors || []) (connectors || [])
.filter((c: SearchSourceConnector) => c.connector_type === "COMPOSIO_CONNECTOR") .filter((c: SearchSourceConnector) =>
.map((c: SearchSourceConnector) => c.config?.toolkit_id as string) c.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
c.connector_type === "COMPOSIO_GMAIL_CONNECTOR" ||
c.connector_type === "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR"
)
.map((c: SearchSourceConnector) => {
// Map connector type back to toolkit_id
if (c.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR") return "googledrive";
if (c.connector_type === "COMPOSIO_GMAIL_CONNECTOR") return "gmail";
if (c.connector_type === "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR") return "googlecalendar";
return c.config?.toolkit_id as string;
})
.filter(Boolean) .filter(Boolean)
} }
onBack={handleBackFromComposio} onBack={handleBackFromComposio}

View file

@ -1,7 +1,5 @@
"use client"; "use client";
import { ExternalLink, Info, Zap } from "lucide-react";
import Image from "next/image";
import type { FC } from "react"; import type { FC } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
@ -13,92 +11,13 @@ interface ComposioConfigProps {
onNameChange?: (name: string) => void; onNameChange?: (name: string) => void;
} }
// Get toolkit display info
const getToolkitInfo = (toolkitId: string): { name: string; icon: string; description: string } => {
switch (toolkitId) {
case "googledrive":
return {
name: "Google Drive",
icon: "/connectors/google-drive.svg",
description: "Files and documents from Google Drive",
};
case "gmail":
return {
name: "Gmail",
icon: "/connectors/google-gmail.svg",
description: "Emails from Gmail",
};
case "googlecalendar":
return {
name: "Google Calendar",
icon: "/connectors/google-calendar.svg",
description: "Events from Google Calendar",
};
case "slack":
return {
name: "Slack",
icon: "/connectors/slack.svg",
description: "Messages from Slack",
};
case "notion":
return {
name: "Notion",
icon: "/connectors/notion.svg",
description: "Pages from Notion",
};
case "github":
return {
name: "GitHub",
icon: "/connectors/github.svg",
description: "Repositories from GitHub",
};
default:
return {
name: toolkitId,
icon: "/connectors/composio.svg",
description: "Connected via Composio",
};
}
};
export const ComposioConfig: FC<ComposioConfigProps> = ({ connector }) => { export const ComposioConfig: FC<ComposioConfigProps> = ({ connector }) => {
const toolkitId = connector.config?.toolkit_id as string; const toolkitId = connector.config?.toolkit_id as string;
const toolkitName = connector.config?.toolkit_name as string;
const isIndexable = connector.config?.is_indexable as boolean; const isIndexable = connector.config?.is_indexable as boolean;
const composioAccountId = connector.config?.composio_connected_account_id as string; const composioAccountId = connector.config?.composio_connected_account_id as string;
const toolkitInfo = getToolkitInfo(toolkitId);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Toolkit Info Card */}
<div className="rounded-xl border border-violet-500/20 bg-gradient-to-br from-violet-500/5 to-purple-500/5 p-4">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500/10 to-purple-500/10 border border-violet-500/20 shrink-0">
<Image
src={toolkitInfo.icon}
alt={toolkitInfo.name}
width={24}
height={24}
className="size-6"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-sm font-semibold">{toolkitName || toolkitInfo.name}</h3>
<Badge
variant="secondary"
className="text-[10px] px-1.5 py-0 h-5 bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20"
>
<Zap className="size-3 mr-0.5" />
Composio
</Badge>
</div>
<p className="text-xs text-muted-foreground">{toolkitInfo.description}</p>
</div>
</div>
</div>
{/* Connection Details */} {/* Connection Details */}
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider"> <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
@ -133,28 +52,6 @@ export const ComposioConfig: FC<ComposioConfigProps> = ({ connector }) => {
)} )}
</div> </div>
</div> </div>
{/* Info Banner */}
<div className="rounded-lg border border-border/50 bg-muted/30 p-3">
<div className="flex items-start gap-2.5">
<Info className="size-4 text-muted-foreground shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-xs text-muted-foreground leading-relaxed">
This connection uses Composio&apos;s managed OAuth, which means you don&apos;t need to
wait for app verification. Your data is securely accessed through Composio.
</p>
<a
href="https://composio.dev"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-violet-600 dark:text-violet-400 hover:underline"
>
Learn more about Composio
<ExternalLink className="size-3" />
</a>
</div>
</div>
</div>
</div> </div>
); );
}; };

View file

@ -74,7 +74,9 @@ export function getConnectorConfigComponent(
return CirclebackConfig; return CirclebackConfig;
case "MCP_CONNECTOR": case "MCP_CONNECTOR":
return MCPConfig; return MCPConfig;
case "COMPOSIO_CONNECTOR": case "COMPOSIO_GOOGLE_DRIVE_CONNECTOR":
case "COMPOSIO_GMAIL_CONNECTOR":
case "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR":
return ComposioConfig; return ComposioConfig;
// OAuth connectors (Gmail, Calendar, Airtable, Notion) and others don't need special config UI // OAuth connectors (Gmail, Calendar, Airtable, Notion) and others don't need special config UI
default: default:

View file

@ -168,14 +168,28 @@ export const OTHER_CONNECTORS = [
}, },
] as const; ] as const;
// Composio Connector (Single entry that opens toolkit selector) // Composio Connectors - Individual entries for each supported toolkit
export const COMPOSIO_CONNECTORS = [ export const COMPOSIO_CONNECTORS = [
{ {
id: "composio-connector", id: "composio-googledrive",
title: "Composio", title: "Google Drive",
description: "Connect 100+ apps via Composio (Google, Slack, Notion, etc.)", description: "Search your Drive files via Composio",
connectorType: EnumConnectorName.COMPOSIO_CONNECTOR, connectorType: EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
// No authEndpoint - handled via toolkit selector view authEndpoint: "/api/v1/auth/composio/connector/add/?toolkit_id=googledrive",
},
{
id: "composio-gmail",
title: "Gmail",
description: "Search through your emails via Composio",
connectorType: EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR,
authEndpoint: "/api/v1/auth/composio/connector/add/?toolkit_id=gmail",
},
{
id: "composio-googlecalendar",
title: "Google Calendar",
description: "Search through your events via Composio",
connectorType: EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR,
authEndpoint: "/api/v1/auth/composio/connector/add/?toolkit_id=googlecalendar",
}, },
] as const; ] as const;

View file

@ -7,7 +7,7 @@ import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types
export const connectorPopupQueryParamsSchema = z.object({ export const connectorPopupQueryParamsSchema = z.object({
modal: z.enum(["connectors"]).optional(), modal: z.enum(["connectors"]).optional(),
tab: z.enum(["all", "active"]).optional(), tab: z.enum(["all", "active"]).optional(),
view: z.enum(["configure", "edit", "connect", "youtube", "accounts", "mcp-list"]).optional(), view: z.enum(["configure", "edit", "connect", "youtube", "accounts", "mcp-list", "composio"]).optional(),
connector: z.string().optional(), connector: z.string().optional(),
connectorId: z.string().optional(), connectorId: z.string().optional(),
connectorType: z.string().optional(), connectorType: z.string().optional(),

View file

@ -26,7 +26,7 @@ import {
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queryClient } from "@/lib/query-client/client"; import { queryClient } from "@/lib/query-client/client";
import type { IndexingConfigState } from "../constants/connector-constants"; import type { IndexingConfigState } from "../constants/connector-constants";
import { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants"; import { COMPOSIO_CONNECTORS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants";
import { import {
dateRangeSchema, dateRangeSchema,
frequencyMinutesSchema, frequencyMinutesSchema,
@ -176,15 +176,24 @@ export const useConnectorDialog = () => {
} }
// Handle accounts view // Handle accounts view
if (params.view === "accounts" && params.connectorType && !viewingAccountsType) { if (params.view === "accounts" && params.connectorType) {
const oauthConnector = OAUTH_CONNECTORS.find( // Update state if not set, or if connectorType has changed
(c) => c.connectorType === params.connectorType const needsUpdate = !viewingAccountsType ||
); viewingAccountsType.connectorType !== params.connectorType;
if (oauthConnector) {
setViewingAccountsType({ if (needsUpdate) {
connectorType: oauthConnector.connectorType, // Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
connectorTitle: oauthConnector.title, const oauthConnector = OAUTH_CONNECTORS.find(
}); (c) => c.connectorType === params.connectorType
) || COMPOSIO_CONNECTORS.find(
(c) => c.connectorType === params.connectorType
);
if (oauthConnector) {
setViewingAccountsType({
connectorType: oauthConnector.connectorType,
connectorTitle: oauthConnector.title,
});
}
} }
} }
@ -293,6 +302,8 @@ export const useConnectorDialog = () => {
indexingConfig, indexingConfig,
connectingConnectorType, connectingConnectorType,
viewingAccountsType, viewingAccountsType,
viewingMCPList,
viewingComposio,
]); ]);
// Detect OAuth success / Failure and transition to config view // Detect OAuth success / Failure and transition to config view
@ -389,15 +400,19 @@ export const useConnectorDialog = () => {
// Handle OAuth connection // Handle OAuth connection
const handleConnectOAuth = useCallback( const handleConnectOAuth = useCallback(
async (connector: (typeof OAUTH_CONNECTORS)[number]) => { async (connector: (typeof OAUTH_CONNECTORS)[number] | (typeof COMPOSIO_CONNECTORS)[number]) => {
if (!searchSpaceId || !connector.authEndpoint) return; if (!searchSpaceId || !connector.authEndpoint) return;
// Set connecting state immediately to disable button and show spinner // Set connecting state immediately to disable button and show spinner
setConnectingId(connector.id); setConnectingId(connector.id);
try { try {
// Check if authEndpoint already has query parameters
const separator = connector.authEndpoint.includes("?") ? "&" : "?";
const url = `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}${separator}space_id=${searchSpaceId}`;
const response = await authenticatedFetch( const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, url,
{ method: "GET" } { method: "GET" }
); );
@ -799,23 +814,19 @@ export const useConnectorDialog = () => {
// Handle viewing accounts list for OAuth connector type // Handle viewing accounts list for OAuth connector type
const handleViewAccountsList = useCallback( const handleViewAccountsList = useCallback(
(connectorType: string, connectorTitle: string) => { (connectorType: string, _connectorTitle?: string) => {
if (!searchSpaceId) return; if (!searchSpaceId) return;
setViewingAccountsType({
connectorType,
connectorTitle,
});
// Update URL to show accounts view, preserving current tab // Update URL to show accounts view, preserving current tab
// The useEffect will handle setting viewingAccountsType based on URL params
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors"); url.searchParams.set("modal", "connectors");
url.searchParams.set("view", "accounts"); url.searchParams.set("view", "accounts");
url.searchParams.set("connectorType", connectorType); url.searchParams.set("connectorType", connectorType);
// Keep the current tab in URL so we can go back to it // Keep the current tab in URL so we can go back to it
window.history.pushState({ modal: true }, "", url.toString()); router.replace(url.pathname + url.search, { scroll: false });
}, },
[searchSpaceId] [searchSpaceId, router]
); );
// Handle going back from accounts list view // Handle going back from accounts list view
@ -839,8 +850,8 @@ export const useConnectorDialog = () => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors"); url.searchParams.set("modal", "connectors");
url.searchParams.set("view", "mcp-list"); url.searchParams.set("view", "mcp-list");
window.history.pushState({ modal: true }, "", url.toString()); router.replace(url.pathname + url.search, { scroll: false });
}, [searchSpaceId]); }, [searchSpaceId, router]);
// Handle going back from MCP list view // Handle going back from MCP list view
const handleBackFromMCPList = useCallback(() => { const handleBackFromMCPList = useCallback(() => {
@ -871,8 +882,8 @@ export const useConnectorDialog = () => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors"); url.searchParams.set("modal", "connectors");
url.searchParams.set("view", "composio"); url.searchParams.set("view", "composio");
window.history.pushState({ modal: true }, "", url.toString()); router.replace(url.pathname + url.search, { scroll: false });
}, [searchSpaceId]); }, [searchSpaceId, router]);
// Handle going back from Composio view // Handle going back from Composio view
const handleBackFromComposio = useCallback(() => { const handleBackFromComposio = useCallback(() => {
@ -1423,7 +1434,7 @@ export const useConnectorDialog = () => {
setIsDisconnecting(false); setIsDisconnecting(false);
} }
}, },
[editingConnector, searchSpaceId, deleteConnector, router] [editingConnector, searchSpaceId, deleteConnector, router, cameFromMCPList]
); );
// Handle quick index (index without date picker, uses backend defaults) // Handle quick index (index without date picker, uses backend defaults)
@ -1579,6 +1590,7 @@ export const useConnectorDialog = () => {
viewingAccountsType, viewingAccountsType,
viewingMCPList, viewingMCPList,
viewingComposio, viewingComposio,
connectingComposioToolkit,
// Setters // Setters
setSearchQuery, setSearchQuery,
@ -1616,8 +1628,6 @@ export const useConnectorDialog = () => {
setIndexingConnectorConfig, setIndexingConnectorConfig,
// Composio // Composio
viewingComposio,
connectingComposioToolkit,
handleOpenComposio, handleOpenComposio,
handleBackFromComposio, handleBackFromComposio,
handleConnectComposioToolkit, handleConnectComposioToolkit,

View file

@ -4,7 +4,6 @@ import type { FC } from "react";
import { EnumConnectorName } from "@/contracts/enums/connector"; import { EnumConnectorName } from "@/contracts/enums/connector";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { ConnectorCard } from "../components/connector-card"; import { ConnectorCard } from "../components/connector-card";
import { ComposioConnectorCard } from "../components/composio-connector-card";
import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS, COMPOSIO_CONNECTORS } from "../constants/connector-constants"; import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS, COMPOSIO_CONNECTORS } from "../constants/connector-constants";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
@ -29,13 +28,12 @@ interface AllConnectorsTabProps {
allConnectors: SearchSourceConnector[] | undefined; allConnectors: SearchSourceConnector[] | undefined;
documentTypeCounts?: Record<string, number>; documentTypeCounts?: Record<string, number>;
indexingConnectorIds?: Set<number>; indexingConnectorIds?: Set<number>;
onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number] | (typeof COMPOSIO_CONNECTORS)[number]) => void;
onConnectNonOAuth?: (connectorType: string) => void; onConnectNonOAuth?: (connectorType: string) => void;
onCreateWebcrawler?: () => void; onCreateWebcrawler?: () => void;
onCreateYouTubeCrawler?: () => void; onCreateYouTubeCrawler?: () => void;
onManage?: (connector: SearchSourceConnector) => void; onManage?: (connector: SearchSourceConnector) => void;
onViewAccountsList?: (connectorType: string, connectorTitle: string) => void; onViewAccountsList?: (connectorType: string, connectorTitle: string) => void;
onOpenComposio?: () => void;
} }
export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
@ -51,7 +49,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
onCreateYouTubeCrawler, onCreateYouTubeCrawler,
onManage, onManage,
onViewAccountsList, onViewAccountsList,
onOpenComposio,
}) => { }) => {
// Filter connectors based on search // Filter connectors based on search
const filteredOAuth = OAUTH_CONNECTORS.filter( const filteredOAuth = OAUTH_CONNECTORS.filter(
@ -79,23 +76,16 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
c.description.toLowerCase().includes(searchQuery.toLowerCase()) c.description.toLowerCase().includes(searchQuery.toLowerCase())
); );
// Count Composio connectors
const composioConnectorCount = allConnectors
? allConnectors.filter(
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.COMPOSIO_CONNECTOR
).length
: 0;
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* Quick Connect */} {/* Managed OAuth (Composio Integrations) */}
{filteredOAuth.length > 0 && ( {filteredComposio.length > 0 && (
<section> <section>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">Quick Connect</h3> <h3 className="text-sm font-semibold text-muted-foreground">Managed OAuth</h3>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredOAuth.map((connector) => { {filteredComposio.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType); const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id; const isConnecting = connectingId === connector.id;
@ -109,17 +99,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
const accountCount = typeConnectors.length; const accountCount = typeConnectors.length;
// Get the most recent last_indexed_at across all accounts
const mostRecentLastIndexed = typeConnectors.reduce<string | undefined>(
(latest, c) => {
if (!c.last_indexed_at) return latest;
if (!latest) return c.last_indexed_at;
return new Date(c.last_indexed_at) > new Date(latest)
? c.last_indexed_at
: latest;
},
undefined
);
const documentCount = getDocumentCountForConnector( const documentCount = getDocumentCountForConnector(
connector.connectorType, connector.connectorType,
@ -154,26 +133,57 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
</section> </section>
)} )}
{/* Composio Integrations */} {/* Quick Connect */}
{filteredComposio.length > 0 && onOpenComposio && ( {filteredOAuth.length > 0 && (
<section> <section>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">Managed OAuth</h3> <h3 className="text-sm font-semibold text-muted-foreground">Quick Connect</h3>
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-violet-500/10 text-violet-600 dark:text-violet-400 border border-violet-500/20 font-medium">
No verification needed
</span>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredComposio.map((connector) => ( {filteredOAuth.map((connector) => {
<ComposioConnectorCard const isConnected = connectedTypes.has(connector.connectorType);
key={connector.id} const isConnecting = connectingId === connector.id;
id={connector.id}
title={connector.title} // Find all connectors of this type
description={connector.description} const typeConnectors =
connectorCount={composioConnectorCount} isConnected && allConnectors
onConnect={onOpenComposio} ? allConnectors.filter(
/> (c: SearchSourceConnector) => c.connector_type === connector.connectorType
))} )
: [];
const accountCount = typeConnectors.length;
const documentCount = getDocumentCountForConnector(
connector.connectorType,
documentTypeCounts
);
// Check if any account is currently indexing
const isIndexing = typeConnectors.some((c) => indexingConnectorIds?.has(c.id));
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
accountCount={accountCount}
isIndexing={isIndexing}
onConnect={() => onConnectOAuth(connector)}
onManage={
isConnected && onViewAccountsList
? () => onViewAccountsList(connector.connectorType, connector.title)
: undefined
}
/>
);
})}
</div> </div>
</section> </section>
)} )}

View file

@ -30,7 +30,10 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
// Special mappings (connector type differs from document type) // Special mappings (connector type differs from document type)
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE", GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",
WEBCRAWLER_CONNECTOR: "CRAWLED_URL", WEBCRAWLER_CONNECTOR: "CRAWLED_URL",
COMPOSIO_CONNECTOR: "COMPOSIO_CONNECTOR", // Composio connectors map to their own document types
COMPOSIO_GOOGLE_DRIVE_CONNECTOR: "COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
COMPOSIO_GMAIL_CONNECTOR: "COMPOSIO_GMAIL_CONNECTOR",
COMPOSIO_GOOGLE_CALENDAR_CONNECTOR: "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
}; };
/** /**

View file

@ -24,5 +24,7 @@ export enum EnumConnectorName {
YOUTUBE_CONNECTOR = "YOUTUBE_CONNECTOR", YOUTUBE_CONNECTOR = "YOUTUBE_CONNECTOR",
CIRCLEBACK_CONNECTOR = "CIRCLEBACK_CONNECTOR", CIRCLEBACK_CONNECTOR = "CIRCLEBACK_CONNECTOR",
MCP_CONNECTOR = "MCP_CONNECTOR", MCP_CONNECTOR = "MCP_CONNECTOR",
COMPOSIO_CONNECTOR = "COMPOSIO_CONNECTOR", COMPOSIO_GOOGLE_DRIVE_CONNECTOR = "COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
COMPOSIO_GMAIL_CONNECTOR = "COMPOSIO_GMAIL_CONNECTOR",
COMPOSIO_GOOGLE_CALENDAR_CONNECTOR = "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
} }

View file

@ -66,8 +66,12 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
return <IconUsersGroup {...iconProps} />; return <IconUsersGroup {...iconProps} />;
case EnumConnectorName.MCP_CONNECTOR: case EnumConnectorName.MCP_CONNECTOR:
return <Image src="/connectors/modelcontextprotocol.svg" alt="MCP" {...imgProps} />; return <Image src="/connectors/modelcontextprotocol.svg" alt="MCP" {...imgProps} />;
case EnumConnectorName.COMPOSIO_CONNECTOR: case EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR:
return <Image src="/connectors/composio.svg" alt="Composio" {...imgProps} />; return <Image src="/connectors/google-drive.svg" alt="Google Drive" {...imgProps} />;
case EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR:
return <Image src="/connectors/google-gmail.svg" alt="Gmail" {...imgProps} />;
case EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
return <Image src="/connectors/google-calendar.svg" alt="Google Calendar" {...imgProps} />;
// Additional cases for non-enum connector types // Additional cases for non-enum connector types
case "YOUTUBE_CONNECTOR": case "YOUTUBE_CONNECTOR":
return <Image src="/connectors/youtube.svg" alt="YouTube" {...imgProps} />; return <Image src="/connectors/youtube.svg" alt="YouTube" {...imgProps} />;
@ -87,8 +91,12 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
return <File {...iconProps} />; return <File {...iconProps} />;
case "GOOGLE_DRIVE_FILE": case "GOOGLE_DRIVE_FILE":
return <File {...iconProps} />; return <File {...iconProps} />;
case "COMPOSIO_CONNECTOR": case "COMPOSIO_GOOGLE_DRIVE_CONNECTOR":
return <Image src="/connectors/composio.svg" alt="Composio" {...imgProps} />; return <Image src="/connectors/google-drive.svg" alt="Google Drive" {...imgProps} />;
case "COMPOSIO_GMAIL_CONNECTOR":
return <Image src="/connectors/google-gmail.svg" alt="Gmail" {...imgProps} />;
case "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR":
return <Image src="/connectors/google-calendar.svg" alt="Google Calendar" {...imgProps} />;
case "NOTE": case "NOTE":
return <FileText {...iconProps} />; return <FileText {...iconProps} />;
case "EXTENSION": case "EXTENSION":

View file

@ -27,7 +27,9 @@ export const searchSourceConnectorTypeEnum = z.enum([
"BOOKSTACK_CONNECTOR", "BOOKSTACK_CONNECTOR",
"CIRCLEBACK_CONNECTOR", "CIRCLEBACK_CONNECTOR",
"MCP_CONNECTOR", "MCP_CONNECTOR",
"COMPOSIO_CONNECTOR", "COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
"COMPOSIO_GMAIL_CONNECTOR",
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
]); ]);
export const searchSourceConnector = z.object({ export const searchSourceConnector = z.object({
@ -149,6 +151,13 @@ export const googleDriveIndexBody = z.object({
name: z.string(), name: z.string(),
}) })
), ),
indexing_options: z
.object({
max_files_per_folder: z.number().int().min(1).max(1000),
incremental_sync: z.boolean(),
include_subfolders: z.boolean(),
})
.optional(),
}); });
/** /**

View file

@ -25,7 +25,9 @@ export const documentTypeEnum = z.enum([
"CIRCLEBACK", "CIRCLEBACK",
"SURFSENSE_DOCS", "SURFSENSE_DOCS",
"NOTE", "NOTE",
"COMPOSIO_CONNECTOR", "COMPOSIO_GOOGLE_DRIVE_CONNECTOR",
"COMPOSIO_GMAIL_CONNECTOR",
"COMPOSIO_GOOGLE_CALENDAR_CONNECTOR",
]); ]);
export const document = z.object({ export const document = z.object({