2023-11-29 03:13:55 -08:00
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
jsonParse ,
type IExecuteFunctions ,
type INodeType ,
type INodeTypeDescription ,
type SupplyData ,
NodeOperationError ,
NodeConnectionType ,
} from 'n8n-workflow' ;
import { z } from 'zod' ;
import type { JSONSchema7 } from 'json-schema' ;
import { StructuredOutputParser } from 'langchain/output_parsers' ;
2024-03-07 02:36:36 -08:00
import { OutputParserException } from '@langchain/core/output_parsers' ;
2023-11-29 03:13:55 -08:00
import get from 'lodash/get' ;
2024-05-22 05:29:32 -07:00
import type { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox' ;
2023-11-29 03:13:55 -08:00
import { getConnectionHintNoticeField } from '../../../utils/sharedFields' ;
2024-04-29 04:59:55 -07:00
import { logWrapper } from '../../../utils/logWrapper' ;
2024-05-22 05:29:32 -07:00
import { generateSchema , getSandboxWithZod } from '../../../utils/schemaParsing' ;
import {
inputSchemaField ,
jsonSchemaExampleField ,
schemaTypeField ,
} from '../../../utils/descriptions' ;
2023-11-29 03:13:55 -08:00
const STRUCTURED_OUTPUT_KEY = '__structured__output' ;
const STRUCTURED_OUTPUT_OBJECT_KEY = '__structured__output__object' ;
const STRUCTURED_OUTPUT_ARRAY_KEY = '__structured__output__array' ;
2024-04-29 04:59:55 -07:00
export class N8nStructuredOutputParser < T extends z.ZodTypeAny > extends StructuredOutputParser < T > {
2023-11-29 03:13:55 -08:00
async parse ( text : string ) : Promise < z.infer < T > > {
try {
const parsed = ( await super . parse ( text ) ) as object ;
return (
2024-02-08 06:32:04 -08:00
get ( parsed , [ STRUCTURED_OUTPUT_KEY , STRUCTURED_OUTPUT_OBJECT_KEY ] ) ? ?
get ( parsed , [ STRUCTURED_OUTPUT_KEY , STRUCTURED_OUTPUT_ARRAY_KEY ] ) ? ?
2023-11-29 03:13:55 -08:00
get ( parsed , STRUCTURED_OUTPUT_KEY ) ? ?
parsed
) ;
} catch ( e ) {
// eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown
throw new OutputParserException ( ` Failed to parse. Text: " ${ text } ". Error: ${ e } ` , text ) ;
}
}
2024-04-29 04:59:55 -07:00
static async fromZedJsonSchema (
sandboxedSchema : JavaScriptSandbox ,
2024-02-29 23:41:45 -08:00
nodeVersion : number ,
2024-04-29 04:59:55 -07:00
) : Promise < StructuredOutputParser < z.ZodType < object , z.ZodTypeDef , object > >> {
const zodSchema = ( await sandboxedSchema . runCode ( ) ) as z . ZodSchema < object > ;
2023-11-29 03:13:55 -08:00
2024-02-29 23:41:45 -08:00
let returnSchema : z.ZodSchema < object > ;
if ( nodeVersion === 1 ) {
returnSchema = z . object ( {
[ STRUCTURED_OUTPUT_KEY ] : z
. object ( {
2024-04-29 04:59:55 -07:00
[ STRUCTURED_OUTPUT_OBJECT_KEY ] : zodSchema . optional ( ) ,
[ STRUCTURED_OUTPUT_ARRAY_KEY ] : z . array ( zodSchema ) . optional ( ) ,
2024-02-29 23:41:45 -08:00
} )
. describe (
` Wrapper around the output data. It can only contain ${ STRUCTURED_OUTPUT_OBJECT_KEY } or ${ STRUCTURED_OUTPUT_ARRAY_KEY } but never both. ` ,
)
. refine (
( data ) = > {
// Validate that one and only one of the properties exists
return (
Boolean ( data [ STRUCTURED_OUTPUT_OBJECT_KEY ] ) !==
Boolean ( data [ STRUCTURED_OUTPUT_ARRAY_KEY ] )
) ;
} ,
{
message :
'One and only one of __structured__output__object and __structured__output__array should be present.' ,
path : [ STRUCTURED_OUTPUT_KEY ] ,
} ,
) ,
} ) ;
} else {
returnSchema = z . object ( {
2024-04-29 04:59:55 -07:00
output : zodSchema.optional ( ) ,
2024-02-29 23:41:45 -08:00
} ) ;
}
2023-11-29 03:13:55 -08:00
return N8nStructuredOutputParser . fromZodSchema ( returnSchema ) ;
}
}
export class OutputParserStructured implements INodeType {
description : INodeTypeDescription = {
displayName : 'Structured Output Parser' ,
name : 'outputParserStructured' ,
icon : 'fa:code' ,
group : [ 'transform' ] ,
2024-05-22 05:29:32 -07:00
version : [ 1 , 1.1 , 1.2 ] ,
defaultVersion : 1.2 ,
2023-11-29 03:13:55 -08:00
description : 'Return data in a defined JSON format' ,
defaults : {
name : 'Structured Output Parser' ,
} ,
codex : {
alias : [ 'json' , 'zod' ] ,
categories : [ 'AI' ] ,
subcategories : {
AI : [ 'Output Parsers' ] ,
} ,
resources : {
primaryDocumentation : [
{
url : 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/' ,
} ,
] ,
} ,
} ,
// 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 . AiOutputParser ] ,
outputNames : [ 'Output Parser' ] ,
properties : [
getConnectionHintNoticeField ( [ NodeConnectionType . AiChain , NodeConnectionType . AiAgent ] ) ,
2024-05-22 05:29:32 -07:00
{ . . . schemaTypeField , displayOptions : { show : { '@version' : [ { _cnd : { gte : 1.2 } } ] } } } ,
{
. . . jsonSchemaExampleField ,
default : ` {
"state" : "California" ,
"cities" : [ "Los Angeles" , "San Francisco" , "San Diego" ]
} ` ,
} ,
{
. . . inputSchemaField ,
default : ` {
"type" : "object" ,
"properties" : {
"state" : {
"type" : "string"
} ,
"cities" : {
"type" : "array" ,
"items" : {
"type" : "string"
}
}
}
} ` ,
} ,
2023-11-29 03:13:55 -08:00
{
displayName : 'JSON Schema' ,
name : 'jsonSchema' ,
type : 'json' ,
description : 'JSON Schema to structure and validate the output against' ,
default : ` {
"type" : "object" ,
"properties" : {
"state" : {
"type" : "string"
} ,
"cities" : {
"type" : "array" ,
"items" : {
"type" : "string"
}
}
}
} ` ,
typeOptions : {
rows : 10 ,
} ,
required : true ,
2024-05-22 05:29:32 -07:00
displayOptions : {
show : {
'@version' : [ { _cnd : { lte : 1.1 } } ] ,
} ,
} ,
2023-11-29 03:13:55 -08:00
} ,
{
displayName :
'The schema has to be defined in the <a target="_blank" href="https://json-schema.org/">JSON Schema</a> format. Look at <a target="_blank" href="https://json-schema.org/learn/miscellaneous-examples.html">this</a> page for examples.' ,
name : 'notice' ,
type : 'notice' ,
default : '' ,
2024-05-22 05:29:32 -07:00
displayOptions : {
hide : {
schemaType : [ 'fromJson' ] ,
} ,
} ,
2023-11-29 03:13:55 -08:00
} ,
] ,
} ;
async supplyData ( this : IExecuteFunctions , itemIndex : number ) : Promise < SupplyData > {
2024-05-22 05:29:32 -07:00
const schemaType = this . getNodeParameter ( 'schemaType' , itemIndex , '' ) as 'fromJson' | 'manual' ;
// We initialize these even though one of them will always be empty
// it makes it easer to navigate the ternary operator
const jsonExample = this . getNodeParameter ( 'jsonSchemaExample' , itemIndex , '' ) as string ;
let inputSchema : string ;
2023-11-29 03:13:55 -08:00
2024-05-22 05:29:32 -07:00
if ( this . getNode ( ) . typeVersion <= 1.1 ) {
inputSchema = this . getNodeParameter ( 'jsonSchema' , itemIndex , '' ) as string ;
} else {
inputSchema = this . getNodeParameter ( 'inputSchema' , itemIndex , '' ) as string ;
2023-11-29 03:13:55 -08:00
}
2024-05-22 05:29:32 -07:00
const jsonSchema =
schemaType === 'fromJson' ? generateSchema ( jsonExample ) : jsonParse < JSONSchema7 > ( inputSchema ) ;
2023-11-29 03:13:55 -08:00
2024-05-22 05:29:32 -07:00
const zodSchemaSandbox = getSandboxWithZod ( this , jsonSchema , 0 ) ;
2024-04-29 04:59:55 -07:00
const nodeVersion = this . getNode ( ) . typeVersion ;
try {
const parser = await N8nStructuredOutputParser . fromZedJsonSchema (
2024-05-22 05:29:32 -07:00
zodSchemaSandbox ,
2024-04-29 04:59:55 -07:00
nodeVersion ,
) ;
return {
response : logWrapper ( parser , this ) ,
} ;
} catch ( error ) {
throw new NodeOperationError ( this . getNode ( ) , 'Error during parsing of JSON Schema.' ) ;
}
2023-11-29 03:13:55 -08:00
}
}