1
1
import { DynamicStructuredTool } from '@langchain/core/tools' ;
2
2
import type {
3
3
IExecuteFunctions ,
4
+ INode ,
4
5
INodeParameters ,
5
6
INodeType ,
6
7
ISupplyDataFunctions ,
8
+ ITaskDataConnections ,
7
9
} from 'n8n-workflow' ;
8
10
import { jsonParse , NodeConnectionType , NodeOperationError } from 'n8n-workflow' ;
9
11
import { z } from 'zod' ;
@@ -16,22 +18,25 @@ interface FromAIArgument {
16
18
defaultValue ?: string | number | boolean | Record < string , unknown > ;
17
19
}
18
20
21
+ type ParserOptions = {
22
+ node : INode ;
23
+ nodeType : INodeType ;
24
+ contextFactory : ( runIndex : number , inputData : ITaskDataConnections ) => ISupplyDataFunctions ;
25
+ } ;
26
+
19
27
/**
20
28
* AIParametersParser
21
29
*
22
30
* This class encapsulates the logic for parsing node parameters, extracting $fromAI calls,
23
31
* generating Zod schemas, and creating LangChain tools.
24
32
*/
25
33
class AIParametersParser {
26
- private ctx : ISupplyDataFunctions ;
34
+ private runIndex = 0 ;
27
35
28
36
/**
29
37
* Constructs an instance of AIParametersParser.
30
- * @param ctx The execution context.
31
38
*/
32
- constructor ( ctx : ISupplyDataFunctions ) {
33
- this . ctx = ctx ;
34
- }
39
+ constructor ( private readonly options : ParserOptions ) { }
35
40
36
41
/**
37
42
* Generates a Zod schema based on the provided FromAIArgument placeholder.
@@ -162,14 +167,14 @@ class AIParametersParser {
162
167
} catch ( error ) {
163
168
// If parsing fails, throw an ApplicationError with details
164
169
throw new NodeOperationError (
165
- this . ctx . getNode ( ) ,
170
+ this . options . node ,
166
171
`Failed to parse $fromAI arguments: ${ argsString } : ${ error } ` ,
167
172
) ;
168
173
}
169
174
} else {
170
175
// Log an error if parentheses are unbalanced
171
176
throw new NodeOperationError (
172
- this . ctx . getNode ( ) ,
177
+ this . options . node ,
173
178
`Unbalanced parentheses while parsing $fromAI call: ${ str . slice ( startIndex ) } ` ,
174
179
) ;
175
180
}
@@ -254,7 +259,7 @@ class AIParametersParser {
254
259
const type = cleanArgs ?. [ 2 ] || 'string' ;
255
260
256
261
if ( ! [ 'string' , 'number' , 'boolean' , 'json' ] . includes ( type . toLowerCase ( ) ) ) {
257
- throw new NodeOperationError ( this . ctx . getNode ( ) , `Invalid type: ${ type } ` ) ;
262
+ throw new NodeOperationError ( this . options . node , `Invalid type: ${ type } ` ) ;
258
263
}
259
264
260
265
return {
@@ -315,13 +320,12 @@ class AIParametersParser {
315
320
316
321
/**
317
322
* Creates a DynamicStructuredTool from a node.
318
- * @param node The node type.
319
- * @param nodeParameters The parameters of the node.
320
323
* @returns A DynamicStructuredTool instance.
321
324
*/
322
- public createTool ( node : INodeType , nodeParameters : INodeParameters ) : DynamicStructuredTool {
325
+ public createTool ( ) : DynamicStructuredTool {
326
+ const { node, nodeType } = this . options ;
323
327
const collectedArguments : FromAIArgument [ ] = [ ] ;
324
- this . traverseNodeParameters ( nodeParameters , collectedArguments ) ;
328
+ this . traverseNodeParameters ( node . parameters , collectedArguments ) ;
325
329
326
330
// Validate each collected argument
327
331
const nameValidationRegex = / ^ [ a - z A - Z 0 - 9 _ - ] { 1 , 64 } $ / ;
@@ -331,7 +335,7 @@ class AIParametersParser {
331
335
const isEmptyError = 'You must specify a key when using $fromAI()' ;
332
336
const isInvalidError = `Parameter key \`${ argument . key } \` is invalid` ;
333
337
const error = new Error ( argument . key . length === 0 ? isEmptyError : isInvalidError ) ;
334
- throw new NodeOperationError ( this . ctx . getNode ( ) , error , {
338
+ throw new NodeOperationError ( node , error , {
335
339
description :
336
340
'Invalid parameter key, must be between 1 and 64 characters long and only contain letters, numbers, underscores, and hyphens' ,
337
341
} ) ;
@@ -348,7 +352,7 @@ class AIParametersParser {
348
352
) {
349
353
// If not, throw an error for inconsistent duplicate keys
350
354
throw new NodeOperationError (
351
- this . ctx . getNode ( ) ,
355
+ node ,
352
356
`Duplicate key '${ argument . key } ' found with different description or type` ,
353
357
{
354
358
description :
@@ -378,37 +382,38 @@ class AIParametersParser {
378
382
} , { } ) ;
379
383
380
384
const schema = z . object ( schemaObj ) . required ( ) ;
381
- const description = this . getDescription ( node , nodeParameters ) ;
382
- const nodeName = this . ctx . getNode ( ) . name . replace ( / / g, '_' ) ;
383
- const name = nodeName || node . description . name ;
385
+ const description = this . getDescription ( nodeType , node . parameters ) ;
386
+ const nodeName = node . name . replace ( / / g, '_' ) ;
387
+ const name = nodeName || nodeType . description . name ;
384
388
385
389
const tool = new DynamicStructuredTool ( {
386
390
name,
387
391
description,
388
392
schema,
389
- func : async ( functionArgs : z . infer < typeof schema > ) => {
390
- const { index } = this . ctx . addInputData ( NodeConnectionType . AiTool , [
391
- [ { json : functionArgs } ] ,
392
- ] ) ;
393
+ func : async ( toolArgs : z . infer < typeof schema > ) => {
394
+ const context = this . options . contextFactory ( this . runIndex , { } ) ;
395
+ context . addInputData ( NodeConnectionType . AiTool , [ [ { json : toolArgs } ] ] ) ;
393
396
394
397
try {
395
398
// Execute the node with the proxied context
396
- const result = await node . execute ?. bind ( this . ctx as IExecuteFunctions ) ( ) ;
399
+ const result = await nodeType . execute ?. call ( context as IExecuteFunctions ) ;
397
400
398
401
// Process and map the results
399
402
const mappedResults = result ?. [ 0 ] ?. flatMap ( ( item ) => item . json ) ;
400
403
401
404
// Add output data to the context
402
- this . ctx . addOutputData ( NodeConnectionType . AiTool , index , [
405
+ context . addOutputData ( NodeConnectionType . AiTool , this . runIndex , [
403
406
[ { json : { response : mappedResults } } ] ,
404
407
] ) ;
405
408
406
409
// Return the stringified results
407
410
return JSON . stringify ( mappedResults ) ;
408
411
} catch ( error ) {
409
- const nodeError = new NodeOperationError ( this . ctx . getNode ( ) , error as Error ) ;
410
- this . ctx . addOutputData ( NodeConnectionType . AiTool , index , nodeError ) ;
412
+ const nodeError = new NodeOperationError ( this . options . node , error as Error ) ;
413
+ context . addOutputData ( NodeConnectionType . AiTool , this . runIndex , nodeError ) ;
411
414
return 'Error during node execution: ' + nodeError . description ;
415
+ } finally {
416
+ this . runIndex ++ ;
412
417
}
413
418
} ,
414
419
} ) ;
@@ -421,20 +426,8 @@ class AIParametersParser {
421
426
* Converts node into LangChain tool by analyzing node parameters,
422
427
* identifying placeholders using the $fromAI function, and generating a Zod schema. It then creates
423
428
* a DynamicStructuredTool that can be used in LangChain workflows.
424
- *
425
- * @param ctx The execution context.
426
- * @param node The node type.
427
- * @param nodeParameters The parameters of the node.
428
- * @returns An object containing the DynamicStructuredTool instance.
429
429
*/
430
- export function createNodeAsTool (
431
- ctx : ISupplyDataFunctions ,
432
- node : INodeType ,
433
- nodeParameters : INodeParameters ,
434
- ) {
435
- const parser = new AIParametersParser ( ctx ) ;
436
-
437
- return {
438
- response : parser . createTool ( node , nodeParameters ) ,
439
- } ;
430
+ export function createNodeAsTool ( options : ParserOptions ) {
431
+ const parser = new AIParametersParser ( options ) ;
432
+ return { response : parser . createTool ( ) } ;
440
433
}
0 commit comments