2023-11-29 03:13:55 -08:00
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type {
IExecuteFunctions ,
INodeType ,
INodeTypeDescription ,
SupplyData ,
ExecutionError ,
2024-09-10 04:48:44 -07:00
IDataObject ,
2023-11-29 03:13:55 -08:00
} from 'n8n-workflow' ;
2024-08-02 07:00:09 -07:00
2024-09-10 04:48:44 -07:00
import { jsonParse , NodeConnectionType , NodeOperationError } from 'n8n-workflow' ;
2023-11-29 03:13:55 -08:00
import type { Sandbox } from 'n8n-nodes-base/dist/nodes/Code/Sandbox' ;
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox' ;
import { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox' ;
import { PythonSandbox } from 'n8n-nodes-base/dist/nodes/Code/PythonSandbox' ;
2024-09-10 04:48:44 -07:00
import { DynamicStructuredTool , DynamicTool } from '@langchain/core/tools' ;
2023-11-29 03:13:55 -08:00
import { getConnectionHintNoticeField } from '../../../utils/sharedFields' ;
2024-09-10 04:48:44 -07:00
import {
inputSchemaField ,
jsonSchemaExampleField ,
schemaTypeField ,
} from '../../../utils/descriptions' ;
import { generateSchema , getSandboxWithZod } from '../../../utils/schemaParsing' ;
import type { JSONSchema7 } from 'json-schema' ;
import type { DynamicZodObject } from '../../../types/zod.types' ;
2023-11-29 03:13:55 -08:00
export class ToolCode implements INodeType {
description : INodeTypeDescription = {
2024-07-23 07:40:28 -07:00
displayName : 'Code Tool' ,
2023-11-29 03:13:55 -08:00
name : 'toolCode' ,
icon : 'fa:code' ,
group : [ 'transform' ] ,
2024-03-14 01:03:33 -07:00
version : [ 1 , 1.1 ] ,
2023-11-29 03:13:55 -08:00
description : 'Write a tool in JS or Python' ,
defaults : {
2024-07-23 07:40:28 -07:00
name : 'Code Tool' ,
2023-11-29 03:13:55 -08:00
} ,
codex : {
categories : [ 'AI' ] ,
subcategories : {
AI : [ 'Tools' ] ,
2024-07-23 07:40:28 -07:00
Tools : [ 'Recommended Tools' ] ,
2023-11-29 03:13:55 -08:00
} ,
resources : {
primaryDocumentation : [
{
url : 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolcode/' ,
} ,
] ,
} ,
} ,
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs : [ ] ,
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs : [ NodeConnectionType . AiTool ] ,
outputNames : [ 'Tool' ] ,
properties : [
getConnectionHintNoticeField ( [ NodeConnectionType . AiAgent ] ) ,
2024-02-15 00:15:58 -08:00
{
displayName :
'See an example of a conversational agent with custom tool written in JavaScript <a href="/templates/1963" target="_blank">here</a>.' ,
name : 'noticeTemplateExample' ,
type : 'notice' ,
default : '' ,
} ,
2023-11-29 03:13:55 -08:00
{
displayName : 'Name' ,
name : 'name' ,
type : 'string' ,
default : '' ,
placeholder : 'My_Tool' ,
2024-03-14 01:03:33 -07:00
displayOptions : {
show : {
'@version' : [ 1 ] ,
} ,
} ,
} ,
{
displayName : 'Name' ,
name : 'name' ,
type : 'string' ,
default : '' ,
placeholder : 'e.g. My_Tool' ,
validateType : 'string-alphanumeric' ,
description :
'The name of the function to be called, could contain letters, numbers, and underscores only' ,
displayOptions : {
show : {
'@version' : [ { _cnd : { gte : 1.1 } } ] ,
} ,
} ,
2023-11-29 03:13:55 -08:00
} ,
{
displayName : 'Description' ,
name : 'description' ,
type : 'string' ,
default : '' ,
placeholder :
'Call this tool to get a random color. The input should be a string with comma separted names of colors to exclude.' ,
typeOptions : {
rows : 3 ,
} ,
} ,
{
displayName : 'Language' ,
name : 'language' ,
type : 'options' ,
noDataExpression : true ,
options : [
{
name : 'JavaScript' ,
value : 'javaScript' ,
} ,
{
name : 'Python (Beta)' ,
value : 'python' ,
} ,
] ,
default : 'javaScript' ,
} ,
{
displayName : 'JavaScript' ,
name : 'jsCode' ,
type : 'string' ,
displayOptions : {
show : {
language : [ 'javaScript' ] ,
} ,
} ,
typeOptions : {
2024-02-15 02:35:50 -08:00
editor : 'jsEditor' ,
2023-11-29 03:13:55 -08:00
} ,
2024-02-15 00:15:58 -08:00
default :
'// Example: convert the incoming query to uppercase and return it\nreturn query.toUpperCase()' ,
2023-11-29 03:13:55 -08:00
// TODO: Add proper text here later
hint : 'You can access the input the tool receives via the input property "query". The returned value should be a single string.' ,
2024-02-15 00:15:58 -08:00
// eslint-disable-next-line n8n-nodes-base/node-param-description-missing-final-period
description : 'E.g. Converts any text to uppercase' ,
2023-11-29 03:13:55 -08:00
noDataExpression : true ,
} ,
{
displayName : 'Python' ,
name : 'pythonCode' ,
type : 'string' ,
displayOptions : {
show : {
language : [ 'python' ] ,
} ,
} ,
typeOptions : {
2024-02-15 02:35:50 -08:00
editor : 'codeNodeEditor' , // TODO: create a separate `pythonEditor` component
2023-11-29 03:13:55 -08:00
editorLanguage : 'python' ,
} ,
2024-02-15 00:15:58 -08:00
default :
'# Example: convert the incoming query to uppercase and return it\nreturn query.upper()' ,
2023-11-29 03:13:55 -08:00
// TODO: Add proper text here later
hint : 'You can access the input the tool receives via the input property "query". The returned value should be a single string.' ,
2024-02-15 00:15:58 -08:00
// eslint-disable-next-line n8n-nodes-base/node-param-description-missing-final-period
description : 'E.g. Converts any text to uppercase' ,
2023-11-29 03:13:55 -08:00
noDataExpression : true ,
} ,
2024-09-10 04:48:44 -07:00
{
displayName : 'Specify Input Schema' ,
name : 'specifyInputSchema' ,
type : 'boolean' ,
description :
'Whether to specify the schema for the function. This would require the LLM to provide the input in the correct format and would validate it against the schema.' ,
noDataExpression : true ,
default : false ,
} ,
{ . . . schemaTypeField , displayOptions : { show : { specifyInputSchema : [ true ] } } } ,
jsonSchemaExampleField ,
inputSchemaField ,
2023-11-29 03:13:55 -08:00
] ,
} ;
async supplyData ( this : IExecuteFunctions , itemIndex : number ) : Promise < SupplyData > {
const node = this . getNode ( ) ;
const workflowMode = this . getMode ( ) ;
const name = this . getNodeParameter ( 'name' , itemIndex ) as string ;
const description = this . getNodeParameter ( 'description' , itemIndex ) as string ;
2024-09-10 04:48:44 -07:00
const useSchema = this . getNodeParameter ( 'specifyInputSchema' , itemIndex ) as boolean ;
2023-11-29 03:13:55 -08:00
const language = this . getNodeParameter ( 'language' , itemIndex ) as string ;
let code = '' ;
if ( language === 'javaScript' ) {
code = this . getNodeParameter ( 'jsCode' , itemIndex ) as string ;
} else {
code = this . getNodeParameter ( 'pythonCode' , itemIndex ) as string ;
}
2024-09-10 04:48:44 -07:00
const getSandbox = ( query : string | IDataObject , index = 0 ) = > {
2023-11-29 03:13:55 -08:00
const context = getSandboxContext . call ( this , index ) ;
context . query = query ;
let sandbox : Sandbox ;
if ( language === 'javaScript' ) {
sandbox = new JavaScriptSandbox ( context , code , index , this . helpers ) ;
} else {
sandbox = new PythonSandbox ( context , code , index , this . helpers ) ;
}
sandbox . on (
'output' ,
workflowMode === 'manual'
? this . sendMessageToUI . bind ( this )
: ( . . . args : unknown [ ] ) = >
console . log ( ` [Workflow " ${ this . getWorkflow ( ) . id } "][Node " ${ node . name } "] ` , . . . args ) ,
) ;
return sandbox ;
} ;
2024-09-10 04:48:44 -07:00
const runFunction = async ( query : string | IDataObject ) : Promise < string > = > {
2023-11-29 03:13:55 -08:00
const sandbox = getSandbox ( query , itemIndex ) ;
2024-01-17 07:08:50 -08:00
return await ( sandbox . runCode ( ) as Promise < string > ) ;
2023-11-29 03:13:55 -08:00
} ;
2024-09-10 04:48:44 -07:00
const toolHandler = async ( query : string | IDataObject ) : Promise < string > = > {
const { index } = this . addInputData ( NodeConnectionType . AiTool , [ [ { json : { query } } ] ] ) ;
let response : string = '' ;
let executionError : ExecutionError | undefined ;
try {
response = await runFunction ( query ) ;
} catch ( error : unknown ) {
executionError = new NodeOperationError ( this . getNode ( ) , error as ExecutionError ) ;
response = ` There was an error: " ${ executionError . message } " ` ;
}
if ( typeof response === 'number' ) {
response = ( response as number ) . toString ( ) ;
}
if ( typeof response !== 'string' ) {
// TODO: Do some more testing. Issues here should actually fail the workflow
executionError = new NodeOperationError ( this . getNode ( ) , 'Wrong output type returned' , {
description : ` The response property should be a string, but it is an ${ typeof response } ` ,
} ) ;
response = ` There was an error: " ${ executionError . message } " ` ;
}
if ( executionError ) {
void this . addOutputData ( NodeConnectionType . AiTool , index , executionError ) ;
} else {
void this . addOutputData ( NodeConnectionType . AiTool , index , [ [ { json : { response } } ] ] ) ;
}
return response ;
} ;
const commonToolOptions = {
name ,
description ,
func : toolHandler ,
} ;
let tool : DynamicTool | DynamicStructuredTool | undefined = undefined ;
if ( useSchema ) {
try {
// We initialize these even though one of them will always be empty
// it makes it easier to navigate the ternary operator
const jsonExample = this . getNodeParameter ( 'jsonSchemaExample' , itemIndex , '' ) as string ;
const inputSchema = this . getNodeParameter ( 'inputSchema' , itemIndex , '' ) as string ;
const schemaType = this . getNodeParameter ( 'schemaType' , itemIndex ) as 'fromJson' | 'manual' ;
const jsonSchema =
schemaType === 'fromJson'
? generateSchema ( jsonExample )
: jsonParse < JSONSchema7 > ( inputSchema ) ;
const zodSchemaSandbox = getSandboxWithZod ( this , jsonSchema , 0 ) ;
const zodSchema = ( await zodSchemaSandbox . runCode ( ) ) as DynamicZodObject ;
tool = new DynamicStructuredTool < typeof zodSchema > ( {
schema : zodSchema ,
. . . commonToolOptions ,
} ) ;
} catch ( error ) {
throw new NodeOperationError (
this . getNode ( ) ,
'Error during parsing of JSON Schema. \n ' + error ,
) ;
}
} else {
tool = new DynamicTool ( commonToolOptions ) ;
}
2023-11-29 03:13:55 -08:00
return {
2024-09-10 04:48:44 -07:00
response : tool ,
2023-11-29 03:13:55 -08:00
} ;
}
}