Adds composio-sdk/ with SKILL.md, AGENTS.md, and 18 rule files covering Tool Router, direct execution, triggers, and auth patterns. Source: composiohq/skills Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.3 KiB
9.3 KiB
title, impact, description, tags
| title | impact | description | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|
| Building Chat UIs with Tool Router | HIGH | Best practices for building chat applications with toolkit selection, connection management, and session handling |
|
Building Chat UIs with Tool Router
Build chat applications with Tool Router using Vercel AI SDK, create sessions per message with dynamic configuration, and provide toolkit selection and connection management UI.
Recommended: Vercel AI SDK
- Native streaming support
- React hooks for chat interfaces
- Built-in UI components
- Excellent DX with Tool Router
❌ Incorrect - Sharing Sessions Without Config
// DON'T: Reuse sessions without proper configuration
const globalSession = await composio.create('default', {
toolkits: ['gmail'] // Hard-coded toolkits
});
app.post('/api/chat', async (req, res) => {
// ❌ No user isolation
// ❌ No per-message configuration
// ❌ Can't change toolkits dynamically
const tools = await globalSession.tools();
});
✅ Correct - Session Per Message
// DO: Create sessions per message with proper config
import { Composio } from '@composio/core';
import { VercelProvider } from '@composio/vercel';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const composio = new Composio({ provider: new VercelProvider() });
app.post('/api/chat', async (req, res) => {
const { userId, message, selectedToolkits } = req.body;
// Create new session for this message
const session = await composio.create(userId, {
toolkits: selectedToolkits, // User-selected toolkits
manageConnections: true
});
const tools = await session.tools();
const stream = await streamText({
model: openai('gpt-4o'),
messages: [{ role: 'user', content: message }],
tools,
maxSteps: 10
});
return stream.toDataStreamResponse();
});
Toolkit Selection UI
List All Available Toolkits
Create a session without toolkit filters to show all available toolkits:
// API endpoint to list all toolkits
app.post('/api/toolkits', async (req, res) => {
const { userId } = req.body;
// No toolkits parameter = all toolkits available
const session = await composio.create(userId);
const toolkits = await session.toolkits();
res.json(toolkits.map(tk => ({
slug: tk.slug,
name: tk.name,
description: tk.description,
logo: tk.logo,
isConnected: tk.connectedAccounts.length > 0
})));
});
React Component
export function ToolkitSelector({ userId, onSelect }: Props) {
const [toolkits, setToolkits] = useState<Toolkit[]>([]);
const [selected, setSelected] = useState<string[]>([]);
useEffect(() => {
fetch('/api/toolkits', {
method: 'POST',
body: JSON.stringify({ userId })
}).then(res => res.json()).then(setToolkits);
}, [userId]);
return (
<div className="toolkit-grid">
{toolkits.map(tk => (
<div
key={tk.slug}
className={selected.includes(tk.slug) ? 'selected' : ''}
onClick={() => setSelected(prev =>
prev.includes(tk.slug) ? prev.filter(s => s !== tk.slug) : [...prev, tk.slug]
)}
>
<img src={tk.logo} alt={tk.name} />
<h3>{tk.name}</h3>
{tk.isConnected && <span>✓ Connected</span>}
</div>
))}
<button onClick={() => onSelect(selected)}>Use Selected</button>
</div>
);
}
Connection Management UI
Authorize Toolkits
// API endpoint to start connection flow
app.post('/api/connect', async (req, res) => {
const { userId, toolkitSlug } = req.body;
const session = await composio.create(userId, {
toolkits: [toolkitSlug]
});
const auth = await session.authorize({
toolkit: toolkitSlug,
redirectUrl: `${process.env.APP_URL}/auth/callback`
});
res.json({ redirectUrl: auth.redirectUrl });
});
React Component
export function ConnectedAccounts({ userId }: Props) {
const [toolkits, setToolkits] = useState<Toolkit[]>([]);
const handleConnect = async (slug: string) => {
const res = await fetch('/api/connect', {
method: 'POST',
body: JSON.stringify({ userId, toolkitSlug: slug })
});
const { redirectUrl } = await res.json();
window.location.href = redirectUrl;
};
return (
<div>
{toolkits.map(tk => (
<div key={tk.slug}>
<h3>{tk.name}</h3>
{tk.isConnected ? (
<button onClick={() => handleDisconnect(tk.slug)}>Disconnect</button>
) : (
<button onClick={() => handleConnect(tk.slug)}>Connect</button>
)}
</div>
))}
</div>
);
}
Connected Account Sharing
Connected accounts are shared between sessions (tied to user ID and auth configs, not individual sessions).
// Both sessions use the same Gmail connected account
const session1 = await composio.create('user_123', { toolkits: ['gmail'] });
const session2 = await composio.create('user_123', { toolkits: ['gmail', 'slack'] });
// ✅ Connected accounts shared across sessions
// ✅ No need to reconnect for each session
Override Connected Accounts
// Override which connected account to use
const session = await composio.create('user_123', {
toolkits: ['gmail'],
connectedAccounts: {
gmail: 'conn_specific_account_id' // Use specific account
}
});
Override Auth Config
// Override which auth config to use
const session = await composio.create('user_123', {
toolkits: ['gmail'],
authConfig: {
gmail: 'auth_config_custom_id' // Use custom auth config
}
});
Complete Chat Application
// app/api/chat/route.ts
import { Composio } from '@composio/core';
import { VercelProvider } from '@composio/vercel';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const composio = new Composio({ provider: new VercelProvider() });
export async function POST(req: Request) {
const { userId, messages, selectedToolkits } = await req.json();
const session = await composio.create(userId, {
toolkits: selectedToolkits,
manageConnections: true
});
const tools = await session.tools();
const result = await streamText({
model: openai('gpt-4o'),
messages,
tools,
maxSteps: 10
});
return result.toDataStreamResponse();
}
// app/page.tsx - Chat UI
'use client';
import { useChat } from 'ai/react';
import { useState } from 'react';
export default function ChatPage() {
const [selectedToolkits, setSelectedToolkits] = useState(['gmail']);
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/chat',
body: { userId: 'user_123', selectedToolkits }
});
return (
<div>
<ToolkitSelector
userId="user_123"
selected={selectedToolkits}
onSelect={setSelectedToolkits}
/>
<div className="messages">
{messages.map(m => (
<div key={m.id} className={m.role}>{m.content}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}
Manual Tool Operations (Advanced)
For custom workflows, you can manually fetch and execute tools instead of using sessions.
Manual Tool Fetching
// Fetch raw tool metadata
const tools = await composio.tools.getRawComposioTools({
toolkits: ['gmail', 'slack'],
important: true
});
Manual Tool Execution
// Execute tools directly
const result = await composio.tools.execute('GMAIL_SEND_EMAIL', {
userId: 'user_123',
arguments: { to: 'test@example.com', subject: 'Hello' },
version: '15082025_00' // Version REQUIRED for manual execution
});
if (!result.successful) {
console.error('Failed:', result.error);
}
When to Use Manual Approach
| Use Case | Recommended Approach |
|---|---|
| Chat UIs, agents, streaming | ✅ session.tools() |
| Custom workflows, catalogs | ✅ Manual fetch/execute |
Reference: See Fetching Tools and Tool Execution for detailed manual operation guides.
Best Practices
- Create Sessions Per Message - Fresh session with config for each interaction
- Let Users Select Toolkits - Dynamic toolkit configuration via UI
- Show Connection Status - Display which toolkits are connected
- Handle Authorization - Use
session.authorize()for auth flows - Enable Connection Management - Set
manageConnections: true
Key Principles
- Vercel AI SDK - Best framework for chat UIs
- Session per message - Fresh sessions with config
- No toolkit filter - List all by creating session without toolkits
- Shared connections - Connected accounts shared across sessions
- Override when needed - Use
connectedAccountsorauthConfigfor special cases