Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
204 lines
5.7 KiB
TypeScript
204 lines
5.7 KiB
TypeScript
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<string, any> = Record<string, any>,
|
|
> extends JsonOutputParser<T> {
|
|
async parse(text: string): Promise<T> {
|
|
// 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<string | Record<string, unknown>> {
|
|
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<unknown[]> {
|
|
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];
|
|
}
|