Custom Providers

This guide provides a comprehensive walkthrough of creating custom providers for the Composio SDK, enabling integration with different AI frameworks and platforms.

Provider Architecture

The Composio SDK uses a provider architecture to adapt tools for different AI frameworks. The provider handles:

  1. Tool Format Transformation: Converting Composio tools into formats compatible with specific AI platforms
  2. Tool Execution: Managing the flow of tool execution and results
  3. Platform-Specific Integration: Providing helper methods for seamless integration

Types of Providers

There are two types of providers:

  1. Non-Agentic Providers: Transform tools for platforms that don’t have their own agency (e.g., OpenAI)
  2. Agentic Providers: Transform tools for platforms that have their own agency (e.g., LangChain, AutoGPT)

Provider Class Hierarchy

BaseProvider (Abstract)
├── BaseNonAgenticProvider (Abstract)
│ └── OpenAIProvider (Concrete)
│ └── [Your Custom Non-Agentic Provider] (Concrete)
└── BaseAgenticProvider (Abstract)
└── [Your Custom Agentic Provider] (Concrete)

Creating a Non-Agentic Provider

Non-agentic providers implement the BaseNonAgenticProvider abstract class:

1import { BaseNonAgenticProvider, Tool } from '@composio/core';
2
3// Define your tool format
4interface MyAITool {
5 name: string;
6 description: string;
7 parameters: {
8 type: string;
9 properties: Record<string, unknown>;
10 required?: string[];
11 };
12}
13
14// Define your tool collection format
15type MyAIToolCollection = MyAITool[];
16
17// Create your provider
18export class MyAIProvider extends BaseNonAgenticProvider<MyAIToolCollection, MyAITool> {
19 // Required: Unique provider name for telemetry
20 readonly name = 'my-ai-platform';
21
22 // Required: Method to transform a single tool
23 override wrapTool(tool: Tool): MyAITool {
24 return {
25 name: tool.slug,
26 description: tool.description || '',
27 parameters: {
28 type: 'object',
29 properties: tool.inputParameters?.properties || {},
30 required: tool.inputParameters?.required || [],
31 },
32 };
33 }
34
35 // Required: Method to transform a collection of tools
36 override wrapTools(tools: Tool[]): MyAIToolCollection {
37 return tools.map(tool => this.wrapTool(tool));
38 }
39
40 // Optional: Custom helper methods for your AI platform
41 async executeMyAIToolCall(
42 userId: string,
43 toolCall: {
44 name: string;
45 arguments: Record<string, unknown>;
46 }
47 ): Promise<string> {
48 // Use the built-in executeTool method
49 const result = await this.executeTool(toolCall.name, {
50 userId,
51 arguments: toolCall.arguments,
52 });
53
54 return JSON.stringify(result.data);
55 }
56}

Creating an Agentic Provider

Agentic providers implement the BaseAgenticProvider abstract class:

1import { BaseAgenticProvider, Tool, ExecuteToolFn } from '@composio/core';
2
3// Define your tool format
4interface AgentTool {
5 name: string;
6 description: string;
7 execute: (args: Record<string, unknown>) => Promise<unknown>;
8 schema: Record<string, unknown>;
9}
10
11// Define your tool collection format
12interface AgentToolkit {
13 tools: AgentTool[];
14 createAgent: (config: Record<string, unknown>) => unknown;
15}
16
17// Create your provider
18export class MyAgentProvider extends BaseAgenticProvider<AgentToolkit, AgentTool> {
19 // Required: Unique provider name for telemetry
20 readonly name = 'my-agent-platform';
21
22 // Required: Method to transform a single tool with execute function
23 override wrapTool(tool: Tool, executeToolFn: ExecuteToolFn): AgentTool {
24 return {
25 name: tool.slug,
26 description: tool.description || '',
27 schema: tool.inputParameters || {},
28 execute: async (args: Record<string, unknown>) => {
29 const result = await executeToolFn(tool.slug, args);
30 if (!result.successful) {
31 throw new Error(result.error || 'Tool execution failed');
32 }
33 return result.data;
34 },
35 };
36 }
37
38 // Required: Method to transform a collection of tools with execute function
39 override wrapTools(tools: Tool[], executeToolFn: ExecuteToolFn): AgentToolkit {
40 const agentTools = tools.map(tool => this.wrapTool(tool, executeToolFn));
41
42 return {
43 tools: agentTools,
44 createAgent: config => {
45 // Create an agent using the tools
46 return {
47 run: async (prompt: string) => {
48 // Implementation depends on your agent framework
49 console.log(`Running agent with prompt: ${prompt}`);
50 // The agent would use the tools.execute method to run tools
51 },
52 };
53 },
54 };
55 }
56
57 // Optional: Custom helper methods for your agent platform
58 async runAgent(agentToolkit: AgentToolkit, prompt: string): Promise<unknown> {
59 const agent = agentToolkit.createAgent({});
60 return await agent.run(prompt);
61 }
62}

Using Your Custom Provider

After creating your provider, use it with the Composio SDK:

1import { Composio } from '@composio/core';
2import { MyAIProvider } from './my-ai-provider';
3
4// Create your provider instance
5const myProvider = new MyAIProvider();
6
7// Initialize Composio with your provider
8const composio = new Composio({
9 apiKey: 'your-composio-api-key',
10 provider: myProvider,
11});
12
13// Get tools - they will be transformed by your provider
14const tools = await composio.tools.get('default', {
15 toolkits: ['github'],
16});
17
18// Use the tools with your AI platform
19console.log(tools); // These will be in your custom format

Provider State and Context

Your provider can maintain state and context:

1export class StatefulProvider extends BaseNonAgenticProvider<ToolCollection, Tool> {
2 readonly name = 'stateful-provider';
3
4 // Provider state
5 private requestCount = 0;
6 private toolCache = new Map<string, any>();
7 private config: ProviderConfig;
8
9 constructor(config: ProviderConfig) {
10 super();
11 this.config = config;
12 }
13
14 override wrapTool(tool: Tool): ProviderTool {
15 this.requestCount++;
16
17 // Use the provider state/config
18 const enhancedTool = {
19 // Transform the tool
20 name: this.config.useUpperCase ? tool.slug.toUpperCase() : tool.slug,
21 description: tool.description,
22 schema: tool.inputParameters,
23 };
24
25 // Cache the transformed tool
26 this.toolCache.set(tool.slug, enhancedTool);
27
28 return enhancedTool;
29 }
30
31 override wrapTools(tools: Tool[]): ProviderToolCollection {
32 return tools.map(tool => this.wrapTool(tool));
33 }
34
35 // Custom methods that use provider state
36 getRequestCount(): number {
37 return this.requestCount;
38 }
39
40 getCachedTool(slug: string): ProviderTool | undefined {
41 return this.toolCache.get(slug);
42 }
43}

Advanced: Provider Composition

You can compose functionality by extending existing providers:

1import { OpenAIProvider } from '@composio/openai';
2
3// Extend the OpenAI provider with custom functionality
4export class EnhancedOpenAIProvider extends OpenAIProvider {
5 // Add properties
6 private analytics = {
7 toolCalls: 0,
8 errors: 0,
9 };
10
11 // Override methods to add functionality
12 override async executeToolCall(userId, tool, options, modifiers) {
13 this.analytics.toolCalls++;
14
15 try {
16 // Call the parent implementation
17 const result = await super.executeToolCall(userId, tool, options, modifiers);
18 return result;
19 } catch (error) {
20 this.analytics.errors++;
21 throw error;
22 }
23 }
24
25 // Add new methods
26 getAnalytics() {
27 return this.analytics;
28 }
29
30 async executeWithRetry(userId, tool, options, modifiers, maxRetries = 3) {
31 let attempts = 0;
32 let lastError;
33
34 while (attempts < maxRetries) {
35 try {
36 return await this.executeToolCall(userId, tool, options, modifiers);
37 } catch (error) {
38 lastError = error;
39 attempts++;
40 await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
41 }
42 }
43
44 throw lastError;
45 }
46}

Example: Anthropic Claude Provider

Here’s a more complete example for Anthropic’s Claude:

1import { BaseNonAgenticProvider, Tool } from '@composio/core';
2import Anthropic from '@anthropic-ai/sdk';
3
4interface ClaudeTool {
5 name: string;
6 description: string;
7 input_schema: {
8 type: string;
9 properties: Record<string, unknown>;
10 required?: string[];
11 };
12}
13
14type ClaudeToolCollection = ClaudeTool[];
15
16export class ClaudeProvider extends BaseNonAgenticProvider<ClaudeToolCollection, ClaudeTool> {
17 readonly name = 'claude';
18 private client: Anthropic;
19
20 constructor(apiKey: string) {
21 super();
22 this.client = new Anthropic({
23 apiKey,
24 });
25 }
26
27 override wrapTool(tool: Tool): ClaudeTool {
28 return {
29 name: tool.slug,
30 description: tool.description || '',
31 input_schema: {
32 type: 'object',
33 properties: tool.inputParameters?.properties || {},
34 required: tool.inputParameters?.required || [],
35 },
36 };
37 }
38
39 override wrapTools(tools: Tool[]): ClaudeToolCollection {
40 return tools.map(tool => this.wrapTool(tool));
41 }
42
43 // Helper method to create a Claude message with tools
44 async createMessage(prompt: string, tools: ClaudeToolCollection, userId: string) {
45 const response = await this.client.messages.create({
46 model: 'claude-3-opus-20240229',
47 max_tokens: 1024,
48 system: 'You are a helpful assistant with access to tools.',
49 messages: [{ role: 'user', content: prompt }],
50 tools,
51 });
52
53 // Process tool calls if any
54 if (
55 response.content.some(
56 content => content.type === 'tool_use' && 'name' in content && 'input' in content
57 )
58 ) {
59 const toolResponses = await Promise.all(
60 response.content
61 .filter(content => content.type === 'tool_use')
62 .map(async (content: any) => {
63 const result = await this.executeTool(content.name, {
64 userId,
65 arguments: content.input,
66 });
67
68 return {
69 type: 'tool_result',
70 tool_use_id: content.id,
71 content: JSON.stringify(result.data),
72 };
73 })
74 );
75
76 // Continue the conversation with tool results
77 const followupResponse = await this.client.messages.create({
78 model: 'claude-3-opus-20240229',
79 max_tokens: 1024,
80 system: 'You are a helpful assistant with access to tools.',
81 messages: [
82 { role: 'user', content: prompt },
83 { role: 'assistant', content: response.content },
84 { role: 'user', content: toolResponses },
85 ],
86 tools,
87 });
88
89 return followupResponse;
90 }
91
92 return response;
93 }
94}

Example: LangChain Provider

Here’s an example for LangChain:

1import { BaseAgenticProvider, Tool, ExecuteToolFn } from '@composio/core';
2import { DynamicTool } from 'langchain/tools';
3import { ChatOpenAI } from 'langchain/chat_models/openai';
4import { initializeAgentExecutorWithOptions } from 'langchain/agents';
5
6interface LangChainTool extends DynamicTool {
7 name: string;
8 description: string;
9 func: (input: Record<string, unknown>) => Promise<string>;
10}
11
12interface LangChainToolkit {
13 tools: LangChainTool[];
14 createExecutor: (options: { model: string }) => Promise<any>;
15}
16
17export class LangChainProvider extends BaseAgenticProvider<LangChainToolkit, LangChainTool> {
18 readonly name = 'langchain';
19
20 override wrapTool(tool: Tool, executeToolFn: ExecuteToolFn): LangChainTool {
21 return new DynamicTool({
22 name: tool.slug,
23 description: tool.description || '',
24 func: async (input: string) => {
25 try {
26 // Parse input from string to object
27 const args = typeof input === 'string' ? JSON.parse(input) : input;
28
29 // Execute the tool
30 const result = await executeToolFn(tool.slug, args);
31
32 if (!result.successful) {
33 throw new Error(result.error || 'Tool execution failed');
34 }
35
36 // Return the result
37 return JSON.stringify(result.data);
38 } catch (error) {
39 return `Error: ${error.message}`;
40 }
41 },
42 }) as LangChainTool;
43 }
44
45 override wrapTools(tools: Tool[], executeToolFn: ExecuteToolFn): LangChainToolkit {
46 const langchainTools = tools.map(tool => this.wrapTool(tool, executeToolFn));
47
48 return {
49 tools: langchainTools,
50 createExecutor: async ({ model }) => {
51 const llm = new ChatOpenAI({
52 modelName: model || 'gpt-4',
53 temperature: 0,
54 });
55
56 return await initializeAgentExecutorWithOptions(langchainTools, llm, {
57 agentType: 'chat-zero-shot-react-description',
58 verbose: true,
59 });
60 },
61 };
62 }
63
64 // Helper method to run the agent
65 async runAgent(toolkit: LangChainToolkit, prompt: string, model = 'gpt-4'): Promise<string> {
66 const executor = await toolkit.createExecutor({ model });
67 const result = await executor.call({ input: prompt });
68 return result.output;
69 }
70}

Best Practices

  1. Keep providers focused: Each provider should integrate with one specific platform
  2. Handle errors gracefully: Catch and transform errors from tool execution
  3. Follow platform conventions: Adopt naming and structural conventions of the target platform
  4. Optimize for performance: Cache transformed tools when possible
  5. Add helper methods: Provide convenient methods for common platform-specific operations
  6. Provide clear documentation: Document your provider’s unique features and usage
  7. Use telemetry: Set a meaningful provider name for telemetry insights