Skip to content

Commit b29523d

Browse files
Feature/add composio tool (#3722)
* feat: add composio tool * fix: improve error handling & field description * update composio tools for refresh and sorting --------- Co-authored-by: Henry <[email protected]>
1 parent d6b3546 commit b29523d

File tree

7 files changed

+34163
-33879
lines changed

7 files changed

+34163
-33879
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { INodeParams, INodeCredential } from '../src/Interface'
2+
3+
class ComposioApi implements INodeCredential {
4+
label: string
5+
name: string
6+
version: number
7+
inputs: INodeParams[]
8+
9+
constructor() {
10+
this.label = 'Composio API'
11+
this.name = 'composioApi'
12+
this.version = 1.0
13+
this.inputs = [
14+
{
15+
label: 'Composio API Key',
16+
name: 'composioApi',
17+
type: 'password'
18+
}
19+
]
20+
}
21+
}
22+
23+
module.exports = { credClass: ComposioApi }
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { Tool } from '@langchain/core/tools'
2+
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface'
3+
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
4+
import { LangchainToolSet } from 'composio-core'
5+
6+
class ComposioTool extends Tool {
7+
name = 'composio'
8+
description = 'Tool for interacting with Composio applications and performing actions'
9+
toolset: any
10+
appName: string
11+
actions: string[]
12+
13+
constructor(toolset: any, appName: string, actions: string[]) {
14+
super()
15+
this.toolset = toolset
16+
this.appName = appName
17+
this.actions = actions
18+
}
19+
20+
async _call(input: string): Promise<string> {
21+
try {
22+
return `Executed action on ${this.appName} with input: ${input}`
23+
} catch (error) {
24+
return 'Failed to execute action'
25+
}
26+
}
27+
}
28+
29+
class Composio_Tools implements INode {
30+
label: string
31+
name: string
32+
version: number
33+
type: string
34+
icon: string
35+
category: string
36+
description: string
37+
baseClasses: string[]
38+
credential: INodeParams
39+
inputs: INodeParams[]
40+
41+
constructor() {
42+
this.label = 'Composio'
43+
this.name = 'composio'
44+
this.version = 1.0
45+
this.type = 'Composio'
46+
this.icon = 'composio.svg'
47+
this.category = 'Tools'
48+
this.description = 'Toolset with over 250+ Apps for building AI-powered applications'
49+
this.baseClasses = [this.type, ...getBaseClasses(ComposioTool)]
50+
this.credential = {
51+
label: 'Connect Credential',
52+
name: 'credential',
53+
type: 'credential',
54+
credentialNames: ['composioApi']
55+
}
56+
this.inputs = [
57+
{
58+
label: 'App Name',
59+
name: 'appName',
60+
type: 'asyncOptions',
61+
loadMethod: 'listApps',
62+
description: 'Select the app to connect with',
63+
refresh: true
64+
},
65+
{
66+
label: 'Auth Status',
67+
name: 'authStatus',
68+
type: 'asyncOptions',
69+
loadMethod: 'authStatus',
70+
placeholder: 'Connection status will appear here',
71+
refresh: true
72+
},
73+
{
74+
label: 'Actions to Use',
75+
name: 'actions',
76+
type: 'asyncOptions',
77+
loadMethod: 'listActions',
78+
description: 'Select the actions you want to use',
79+
refresh: true
80+
}
81+
]
82+
}
83+
84+
//@ts-ignore
85+
loadMethods = {
86+
listApps: async (nodeData: INodeData, options?: ICommonObject): Promise<INodeOptionsValue[]> => {
87+
try {
88+
const credentialData = await getCredentialData(nodeData.credential ?? '', options ?? {})
89+
const composioApiKey = getCredentialParam('composioApi', credentialData, nodeData)
90+
91+
if (!composioApiKey) {
92+
return [
93+
{
94+
label: 'API Key Required',
95+
name: 'placeholder',
96+
description: 'Enter Composio API key in the credential field'
97+
}
98+
]
99+
}
100+
101+
const toolset = new LangchainToolSet({ apiKey: composioApiKey })
102+
const apps = await toolset.client.apps.list()
103+
apps.sort((a: any, b: any) => a.name.localeCompare(b.name))
104+
105+
return apps.map(({ name, ...rest }) => ({
106+
label: name.toUpperCase(),
107+
name: name,
108+
description: rest.description || name
109+
}))
110+
} catch (error) {
111+
console.error('Error loading apps:', error)
112+
return [
113+
{
114+
label: 'Error Loading Apps',
115+
name: 'error',
116+
description: 'Failed to load apps. Please check your API key and try again'
117+
}
118+
]
119+
}
120+
},
121+
listActions: async (nodeData: INodeData, options?: ICommonObject): Promise<INodeOptionsValue[]> => {
122+
try {
123+
const credentialData = await getCredentialData(nodeData.credential ?? '', options ?? {})
124+
const composioApiKey = getCredentialParam('composioApi', credentialData, nodeData)
125+
const appName = nodeData.inputs?.appName as string
126+
127+
if (!composioApiKey) {
128+
return [
129+
{
130+
label: 'API Key Required',
131+
name: 'placeholder',
132+
description: 'Enter Composio API key in the credential field'
133+
}
134+
]
135+
}
136+
137+
if (!appName) {
138+
return [
139+
{
140+
label: 'Select an App first',
141+
name: 'placeholder',
142+
description: 'Select an app from the dropdown to view available actions'
143+
}
144+
]
145+
}
146+
147+
const toolset = new LangchainToolSet({ apiKey: composioApiKey })
148+
const actions = await toolset.getTools({ apps: [appName] })
149+
actions.sort((a: any, b: any) => a.name.localeCompare(b.name))
150+
151+
return actions.map(({ name, ...rest }) => ({
152+
label: name.toUpperCase(),
153+
name: name,
154+
description: rest.description || name
155+
}))
156+
} catch (error) {
157+
console.error('Error loading actions:', error)
158+
return [
159+
{
160+
label: 'Error Loading Actions',
161+
name: 'error',
162+
description: 'Failed to load actions. Please check your API key and try again'
163+
}
164+
]
165+
}
166+
},
167+
authStatus: async (nodeData: INodeData, options?: ICommonObject): Promise<INodeOptionsValue[]> => {
168+
const credentialData = await getCredentialData(nodeData.credential ?? '', options ?? {})
169+
const composioApiKey = getCredentialParam('composioApi', credentialData, nodeData)
170+
const appName = nodeData.inputs?.appName as string
171+
172+
if (!composioApiKey) {
173+
return [
174+
{
175+
label: 'API Key Required',
176+
name: 'placeholder',
177+
description: 'Enter Composio API key in the credential field'
178+
}
179+
]
180+
}
181+
182+
if (!appName) {
183+
return [
184+
{
185+
label: 'Select an App first',
186+
name: 'placeholder',
187+
description: 'Select an app from the dropdown to view available actions'
188+
}
189+
]
190+
}
191+
192+
const toolset = new LangchainToolSet({ apiKey: composioApiKey })
193+
const authStatus = await toolset.client.getEntity('default').getConnection({ app: appName.toLowerCase() })
194+
return [
195+
{
196+
label: authStatus ? 'Connected' : 'Not Connected',
197+
name: authStatus ? 'Connected' : 'Not Connected',
198+
description: authStatus ? 'Selected app has an active connection' : 'Please connect the app on app.composio.dev'
199+
}
200+
]
201+
}
202+
}
203+
204+
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
205+
if (!nodeData.inputs) nodeData.inputs = {}
206+
207+
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
208+
const composioApiKey = getCredentialParam('composioApi', credentialData, nodeData)
209+
210+
if (!composioApiKey) {
211+
nodeData.inputs = {
212+
appName: undefined,
213+
authStatus: '',
214+
actions: []
215+
}
216+
throw new Error('API Key Required')
217+
}
218+
219+
const toolset = new LangchainToolSet({ apiKey: composioApiKey })
220+
const tools = await toolset.getTools({ actions: [nodeData.inputs?.actions as string] })
221+
return tools
222+
}
223+
}
224+
225+
module.exports = { nodeClass: Composio_Tools }
Lines changed: 11 additions & 0 deletions
Loading

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"cheerio": "^1.0.0-rc.12",
7575
"chromadb": "^1.5.11",
7676
"cohere-ai": "^7.7.5",
77+
"composio-core": "^0.4.7",
7778
"couchbase": "4.4.1",
7879
"crypto-js": "^4.1.1",
7980
"css-what": "^6.1.0",

packages/components/src/Interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export interface INodeParams {
9393
hint?: Record<string, string>
9494
tabIdentifier?: string
9595
tabs?: Array<INodeParams>
96+
refresh?: boolean
9697
}
9798

9899
export interface INodeExecutionData {

packages/ui/src/views/canvas/NodeInputHandler.jsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Popper, Box, Typography, Tooltip, IconButton, Button, TextField } from
99
import { useGridApiContext } from '@mui/x-data-grid'
1010
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
1111
import { tooltipClasses } from '@mui/material/Tooltip'
12-
import { IconArrowsMaximize, IconEdit, IconAlertTriangle, IconBulb } from '@tabler/icons-react'
12+
import { IconArrowsMaximize, IconEdit, IconAlertTriangle, IconBulb, IconRefresh } from '@tabler/icons-react'
1313
import { Tabs } from '@mui/base/Tabs'
1414
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
1515

@@ -738,7 +738,7 @@ const NodeInputHandler = ({
738738
{inputParam.type === 'asyncOptions' && (
739739
<>
740740
{data.inputParams.length === 1 && <div style={{ marginTop: 10 }} />}
741-
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
741+
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row', alignContent: 'center' }}>
742742
<AsyncDropdown
743743
disabled={disabled}
744744
name={inputParam.name}
@@ -758,6 +758,16 @@ const NodeInputHandler = ({
758758
<IconEdit />
759759
</IconButton>
760760
)}
761+
{inputParam.refresh && (
762+
<IconButton
763+
title='Refresh'
764+
color='primary'
765+
size='small'
766+
onClick={() => setReloadTimestamp(Date.now().toString())}
767+
>
768+
<IconRefresh />
769+
</IconButton>
770+
)}
761771
</div>
762772
</>
763773
)}

0 commit comments

Comments
 (0)