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

@ -188,8 +188,18 @@ export const ConnectorIndicator: FC = () => {
searchSpaceId={searchSpaceId}
connectedToolkits={
(connectors || [])
.filter((c: SearchSourceConnector) => c.connector_type === "COMPOSIO_CONNECTOR")
.map((c: SearchSourceConnector) => c.config?.toolkit_id as string)
.filter((c: SearchSourceConnector) =>
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)
}
onBack={handleBackFromComposio}

View file

@ -1,7 +1,5 @@
"use client";
import { ExternalLink, Info, Zap } from "lucide-react";
import Image from "next/image";
import type { FC } from "react";
import { Badge } from "@/components/ui/badge";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
@ -13,92 +11,13 @@ interface ComposioConfigProps {
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 }) => {
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 composioAccountId = connector.config?.composio_connected_account_id as string;
const toolkitInfo = getToolkitInfo(toolkitId);
return (
<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 */}
<div className="space-y-3">
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
@ -133,28 +52,6 @@ export const ComposioConfig: FC<ComposioConfigProps> = ({ connector }) => {
)}
</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>
);
};

View file

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

View file

@ -168,14 +168,28 @@ export const OTHER_CONNECTORS = [
},
] as const;
// Composio Connector (Single entry that opens toolkit selector)
// Composio Connectors - Individual entries for each supported toolkit
export const COMPOSIO_CONNECTORS = [
{
id: "composio-connector",
title: "Composio",
description: "Connect 100+ apps via Composio (Google, Slack, Notion, etc.)",
connectorType: EnumConnectorName.COMPOSIO_CONNECTOR,
// No authEndpoint - handled via toolkit selector view
id: "composio-googledrive",
title: "Google Drive",
description: "Search your Drive files via Composio",
connectorType: EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
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;

View file

@ -7,7 +7,7 @@ import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types
export const connectorPopupQueryParamsSchema = z.object({
modal: z.enum(["connectors"]).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(),
connectorId: z.string().optional(),
connectorType: z.string().optional(),

View file

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

View file

@ -4,7 +4,6 @@ import type { FC } from "react";
import { EnumConnectorName } from "@/contracts/enums/connector";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
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 { getDocumentCountForConnector } from "../utils/connector-document-mapping";
@ -29,13 +28,12 @@ interface AllConnectorsTabProps {
allConnectors: SearchSourceConnector[] | undefined;
documentTypeCounts?: Record<string, 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;
onCreateWebcrawler?: () => void;
onCreateYouTubeCrawler?: () => void;
onManage?: (connector: SearchSourceConnector) => void;
onViewAccountsList?: (connectorType: string, connectorTitle: string) => void;
onOpenComposio?: () => void;
}
export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
@ -51,7 +49,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
onCreateYouTubeCrawler,
onManage,
onViewAccountsList,
onOpenComposio,
}) => {
// Filter connectors based on search
const filteredOAuth = OAUTH_CONNECTORS.filter(
@ -79,23 +76,16 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
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 (
<div className="space-y-8">
{/* Quick Connect */}
{filteredOAuth.length > 0 && (
{/* Managed OAuth (Composio Integrations) */}
{filteredComposio.length > 0 && (
<section>
<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 className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredOAuth.map((connector) => {
{filteredComposio.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
@ -109,17 +99,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
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(
connector.connectorType,
@ -154,26 +133,57 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
</section>
)}
{/* Composio Integrations */}
{filteredComposio.length > 0 && onOpenComposio && (
{/* Quick Connect */}
{filteredOAuth.length > 0 && (
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">Managed OAuth</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>
<h3 className="text-sm font-semibold text-muted-foreground">Quick Connect</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredComposio.map((connector) => (
<ComposioConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorCount={composioConnectorCount}
onConnect={onOpenComposio}
/>
))}
{filteredOAuth.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find all connectors of this type
const typeConnectors =
isConnected && allConnectors
? 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>
</section>
)}

View file

@ -30,7 +30,10 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
// Special mappings (connector type differs from document type)
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",
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",
};
/**