import type { Tool } from '@langchain/classic/tools'; import type { BaseLanguageModel } from '@langchain/core/language_models/base'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseLLMOutputParser } from '@langchain/core/output_parsers'; import { JsonOutputParser, StringOutputParser } from '@langchain/core/output_parsers'; import type { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts'; import type { Runnable } from '@langchain/core/runnables'; import type { IExecuteFunctions } from 'n8n-workflow'; import { isChatInstance } from '@n8n/ai-utilities'; import { getTracingConfig } from '@utils/tracing'; import { createPromptTemplate, getAgentStepsParser } from './promptUtils'; import type { ChainExecutionParams } from './types'; export class NaiveJsonOutputParser< T extends Record = Record, > extends JsonOutputParser { async parse(text: string): Promise { // First try direct JSON parsing try { const directParsed = JSON.parse(text); return directParsed as T; } catch (e) { // If fails, fall back to JsonOutputParser parser return await super.parse(text); } } } /** * Type guard to check if the LLM has a modelKwargs property(OpenAI) */ export function isModelWithResponseFormat( llm: BaseLanguageModel, ): llm is BaseLanguageModel & { modelKwargs: { response_format: { type: string } } } { return ( 'modelKwargs' in llm && !!llm.modelKwargs && typeof llm.modelKwargs === 'object' && 'response_format' in llm.modelKwargs ); } export function isModelInThinkingMode( llm: BaseLanguageModel, ): llm is BaseLanguageModel & { lc_kwargs: { invocationKwargs: { thinking: { type: string } } } } { return ( 'lc_kwargs' in llm && 'invocationKwargs' in llm.lc_kwargs && typeof llm.lc_kwargs.invocationKwargs === 'object' && 'thinking' in llm.lc_kwargs.invocationKwargs && llm.lc_kwargs.invocationKwargs.thinking.type === 'enabled' ); } /** * Type guard to check if the LLM has a format property(Ollama) */ export function isModelWithFormat( llm: BaseLanguageModel, ): llm is BaseLanguageModel & { format: string } { return 'format' in llm && typeof llm.format !== 'undefined'; } /** * Determines if an LLM is configured to output JSON and returns the appropriate output parser */ export function getOutputParserForLLM( llm: BaseChatModel | BaseLanguageModel, ): BaseLLMOutputParser> { if (isModelWithResponseFormat(llm) && llm.modelKwargs?.response_format?.type === 'json_object') { return new NaiveJsonOutputParser(); } if (isModelWithFormat(llm) && llm.format === 'json') { return new NaiveJsonOutputParser(); } if (isModelInThinkingMode(llm)) { return new NaiveJsonOutputParser(); } // For example Mistral's Magistral models (LmChatMistralCloud node) if (llm.metadata?.output_format === 'json') { return new NaiveJsonOutputParser(); } return new StringOutputParser(); } /** * Creates a simple chain for LLMs without output parsers */ async function executeSimpleChain({ context, llm, query, prompt, }: { context: IExecuteFunctions; llm: BaseChatModel | BaseLanguageModel; query: string; prompt: ChatPromptTemplate | PromptTemplate; }) { const outputParser = getOutputParserForLLM(llm); const chain = prompt.pipe(llm).pipe(outputParser).withConfig(getTracingConfig(context)); // Execute the chain const response = await chain.invoke({ query, signal: context.getExecutionCancelSignal(), }); // Ensure response is always returned as an array return [response]; } // Some models nodes, like OpenAI, can define built-in tools in their metadata function withBuiltInTools(llm: BaseChatModel | BaseLanguageModel) { const modelTools = (llm.metadata?.tools as Tool[]) ?? []; if (modelTools.length && isChatInstance(llm) && llm.bindTools) { return llm.bindTools(modelTools); } return llm; } function prepareLlm( llm: BaseLanguageModel | BaseChatModel, fallbackLlm?: BaseLanguageModel | BaseChatModel | null, ) { const mainLlm = withBuiltInTools(llm); if (fallbackLlm) { return mainLlm.withFallbacks([withBuiltInTools(fallbackLlm)]); } return mainLlm; } /** * Creates and executes an LLM chain with the given prompt and optional output parsers */ export async function executeChain({ context, itemIndex, query, llm, outputParser, messages, fallbackLlm, }: ChainExecutionParams): Promise { const version = context.getNode().typeVersion; const model = prepareLlm(llm, fallbackLlm) as BaseChatModel | BaseLanguageModel; // If no output parsers provided, use a simple chain with basic prompt template if (!outputParser) { const promptTemplate = await createPromptTemplate({ context, itemIndex, llm, messages, query, }); return await executeSimpleChain({ context, llm: model, query, prompt: promptTemplate, }); } const formatInstructions = outputParser.getFormatInstructions(); // Create a prompt template with format instructions const promptWithInstructions = await createPromptTemplate({ context, itemIndex, llm, messages, formatInstructions, query, }); let chain: Runnable<{ query: string }>; if (version >= 1.9) { // use getAgentStepsParser to have more robust output parsing chain = promptWithInstructions .pipe(model) .pipe(getAgentStepsParser(outputParser)) .withConfig(getTracingConfig(context)); } else { chain = promptWithInstructions .pipe(model) .pipe(outputParser) .withConfig(getTracingConfig(context)); } const response = await chain.invoke({ query }, { signal: context.getExecutionCancelSignal() }); // Ensure response is always returned as an array // eslint-disable-next-line @typescript-eslint/no-unsafe-return return Array.isArray(response) ? response : [response]; }