Full Stack Chat App

In this example, you will learn how to build a chatbot that:

  • Lets users connect their various apps to the chatbot using the Composio SDK.
  • Uses the Vercel provider in the Composio SDK to handle and execute tool calls from the LLM.

This page gives a high-level overview of the Composio SDK and how it is used in the GitHub repository: composiohq/chat.

You can find the demo live here.

Prerequisites

Ensure you’ve followed the README.md in the composiohq/chat repository to set the project locally.

Creating auth configs

For all the apps you want to connect to the chatbot, you need to create their respective auth configs.

Learn how to create auth configs here.

Once done, your auth configs will be available in the Composio dashboard.

Auth configs

Save auth config IDs to environment variables

For this project, the auth config IDs should be saved the the environment variables with the NEXT_PUBLIC_ prefix.

.env.local
$NEXT_PUBLIC_GMAIL_AUTH_CONFIG_ID=ac_1234567890
>NEXT_PUBLIC_GITHUB_AUTH_CONFIG_ID=ac_1234567890

Create a Composio client instance

We create a Composio client instance for server-side operations like, API routes, server components, etc.

lib/service/composio.ts
1import { Composio } from '@composio/core';
2import { VercelProvider } from '@composio/vercel';
3
4const composio = new Composio({
5 apiKey: process.env.COMPOSIO_API_KEY,
6 provider: new VercelProvider(),
7});
8
9export default composio;

Creating an API for fetching toolkits

The Composio SDK is meant to be used only in server-side code. For client-side functionality, we create API endpoints in the /app/api/ directory.

In order to list the toolkits and their connection status, we create a Next.js API route to fetch the toolkits using Composio SDK.

1. Listing connected accounts

First, we fetch all connected accounts for a user and create a mapping of toolkit slugs to their connection IDs:

app/api/composio/toolkits.ts
1export async function GET() {
2 // ... auth logic ...
3
4 // List connected accounts to get connection IDs for each toolkit
5 const connectedAccounts = await composio.connectedAccounts.list({
6 userIds: [session.user.id],
7 });
8
9 const connectedToolkitMap = new Map();
10 connectedAccounts.items.forEach(account => {
11 connectedToolkitMap.set(account.toolkit.slug.toUpperCase(), account.id);
12 });
13
14 // ... continue with toolkit fetching ...
15}

2. Fetching toolkit data and building response

Next, we fetch toolkit information for each supported toolkit and combine it with the connection status:

app/api/composio/toolkits.ts
1export async function GET() {
2 // ... auth logic ...
3 // ... connected accounts mapping ...
4
5 const SUPPORTED_TOOLKITS = ['GMAIL', 'GOOGLECALENDAR', 'GITHUB', 'NOTION'];
6
7 // Fetch toolkit data from slugs
8 const toolkitPromises = SUPPORTED_TOOLKITS.map(async slug => {
9 const toolkit = await composio.toolkits.get(slug);
10 const connectionId = connectedToolkitMap.get(slug.toUpperCase());
11
12 return {
13 name: toolkit.name,
14 slug: toolkit.slug,
15 description: toolkit.meta?.description,
16 logo: toolkit.meta?.logo,
17 categories: toolkit.meta?.categories,
18 isConnected: !!connectionId,
19 connectionId: connectionId || undefined,
20 };
21 });
22
23 const toolkits = await Promise.all(toolkitPromises);
24 return NextResponse.json({ toolkits });
25}

Managing connections

Users need to connect and disconnect their accounts from the chatbot to enable tool usage. When users click “Connect” on a toolkit, we initiate an OAuth flow, and when they click “Disconnect”, we remove their connection.

1. Initiating a connection

When a user wants to connect their account, we create a connection request that redirects them to the OAuth provider:

app/api/connections/initiate/route.ts
1export async function POST(request: Request) {
2 // ... auth and validation ...
3
4 const { authConfigId } = requestBody;
5
6 // Initiate connection with Composio
7 const connectionRequest = await composio.connectedAccounts.initiate(
8 session.user.id,
9 authConfigId
10 );
11
12 return NextResponse.json({
13 redirectUrl: connectionRequest.redirectUrl,
14 connectionId: connectionRequest.id,
15 });
16}

2. Checking connection status

After initiating a connection, we need to wait for the OAuth flow to complete. We check the connection status to know when it’s ready to use:

app/api/connections/status/route.ts
1export async function GET(request: Request) {
2 // ... auth and validation ...
3
4 const connectionId = searchParams.get('connectionId');
5
6 // Wait for connection to complete
7 const connection = await composio.connectedAccounts.waitForConnection(connectionId);
8
9 return NextResponse.json({
10 id: connection.id,
11 status: connection.status,
12 authConfig: connection.authConfig,
13 data: connection.data,
14 });
15}

3. Deleting a connection

When a user wants to disconnect their account, we remove the connection using the connection ID:

app/api/connections/delete/route.ts
1export async function DELETE(request: Request) {
2 // ... auth and validation ...
3
4 const connectionId = searchParams.get('connectionId');
5
6 // Delete the connection
7 await composio.connectedAccounts.delete(connectionId);
8
9 return NextResponse.json({
10 success: true,
11 message: 'Connection deleted successfully',
12 });
13}

Working with tools

Once users have connected their accounts, we need to track which toolkits are enabled and fetch the corresponding tools for the LLM.

1. Tracking enabled toolkits

We keep track of which toolkits the user has enabled in the chat interface:

components/chat.tsx
1const { ... } = useChat({
2 // ... other config ...
3 experimental_prepareRequestBody: (body) => {
4 // Get current toolbar state
5 const currentToolbarState = toolbarStateRef.current;
6 const enabledToolkits = Array.from(
7 currentToolbarState.enabledToolkitsWithStatus.entries(),
8 ).map(([slug, isConnected]) => ({ slug, isConnected }));
9
10 return {
11 // ... other fields ...
12 enabledToolkits,
13 };
14 },
15 // ... other handlers ...
16 });

2. Fetching tools for enabled toolkits

We fetch Composio tools based on the enabled toolkit slugs:

lib/ai/tools/composio.ts
1export async function getComposioTools(userId: string, toolkitSlugs: string[]) {
2 // ... validation ...
3
4 const tools = await composio.tools.get(userId, {
5 toolkits: toolkitSlugs,
6 });
7 return tools || {};
8}
app/api/chat.ts
1export async function POST(request: Request) {
2 // ... auth and parsing ...
3
4 const toolkitSlugs = enabledToolkits?.map(t => t.slug) || [];
5
6 const composioTools = await getComposioTools(session.user.id, toolkitSlugs);
7
8 const result = streamText({
9 // ... model config ...
10 tools: {
11 ...composioTools,
12 },
13 });
14}

Bonus: Creating custom component to show tool calls

By default, tool calls appear as raw JSON in the chat interface. To create a better user experience, we can build custom components that display tool calls with proper formatting and loading states.

You can find the ToolCall component at components/tool-call.tsx. Here’s how to integrate it into your message rendering:

components/messages.tsx
1if (type === 'tool-invocation') {
2 const { toolInvocation } = part;
3 const { toolName, toolCallId, state, args, result } = toolInvocation;
4
5 if (state === 'call') {
6 return (
7 <ToolCall
8 key={toolCallId}
9 toolName={toolName}
10 args={args}
11 isLoading={true}
12 />
13 );
14 }
15
16 if (state === 'result') {
17 return (
18 <ToolCall
19 key={toolCallId}
20 toolName={toolName}
21 args={args}
22 result={result}
23 isLoading={false}
24 />
25 );
26 }
27}