n8n-nodes-teamorouter/nodes/shared/langchainModules.ts

76 lines
2.5 KiB
TypeScript

import { dirname, join } from 'node:path';
/**
* n8n bundles `@langchain/openai` and `@n8n/ai-utilities` inside its own installation, and those
* modules are only reachable by a plain `require` when the package was installed as a community
* node (npm installs it below `~/.n8n/nodes`). When the package is mounted as a custom extension
* the resolution starts from our own folder, so we additionally look next to n8n itself
* (the directory of `require.main.filename` is n8n's CLI entry point).
*/
function candidateModuleDirs(): string[] {
const dirs = new Set<string>();
const addChain = (start: string | undefined): void => {
if (!start) return;
let current = start;
for (;;) {
dirs.add(join(current, 'node_modules'));
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
};
addChain(__dirname);
addChain(process.cwd());
if (require.main?.filename) addChain(dirname(require.main.filename));
return [...dirs];
}
export function loadN8nModule<T = unknown>(moduleName: string): T | undefined {
try {
return require(moduleName) as T;
} catch {
// Not reachable from our own folder - fall through to the n8n installation.
}
for (const dir of candidateModuleDirs()) {
try {
return require(join(dir, moduleName)) as T;
} catch {
// Keep looking.
}
}
return undefined;
}
export type ChatOpenAiConstructor = new (config: Record<string, unknown>) => unknown;
export function loadChatOpenAi(): ChatOpenAiConstructor {
const loaded = loadN8nModule<{ ChatOpenAI?: ChatOpenAiConstructor }>('@langchain/openai');
if (typeof loaded?.ChatOpenAI !== 'function') {
throw new Error(
'Could not load "@langchain/openai" from the n8n installation. The Chat Model node needs the LangChain packages that ship with n8n (@n8n/n8n-nodes-langchain must be installed).',
);
}
return loaded.ChatOpenAI;
}
/**
* Token/cost accounting in the execution log is provided by n8n's own tracer. It is an internal
* helper, so we use it when it is reachable and stay silent when it is not - the model itself
* works either way.
*/
export function loadN8nLlmTracing(context: unknown): unknown | undefined {
for (const moduleName of ['@n8n/ai-utilities', 'n8n-core']) {
const loaded = loadN8nModule<{ N8nLlmTracing?: new (context: unknown) => unknown }>(moduleName);
if (typeof loaded?.N8nLlmTracing === 'function') {
try {
return new loaded.N8nLlmTracing(context);
} catch {
return undefined;
}
}
}
return undefined;
}