@rexdug7005/nvidia-llama4
Version:
Integración de NVIDIA Llama4 con LangChain.js, con soporte para Tools Agent de n8n
1,260 lines (1,253 loc) • 56.4 kB
JavaScript
'use strict';
var llms = require('@langchain/core/language_models/llms');
var outputs = require('@langchain/core/outputs');
var axios = require('axios');
var messages = require('@langchain/core/messages');
var zod = require('zod');
var zodToJsonSchema = require('zod-to-json-schema');
var chat_models = require('@langchain/core/language_models/chat_models');
var embeddings = require('@langchain/core/embeddings');
/**
* Intenta analizar una cadena JSON y la devuelve como objeto.
* Si falla, devuelve un objeto con la propiedad raw conteniendo la cadena original.
*
* @param jsonString - Cadena JSON a analizar
* @returns Objeto analizado o {raw: cadenaOriginal}
*/
function safeJsonParse(jsonString) {
try {
return JSON.parse(jsonString);
}
catch (e) {
// Si es un string que parece contener llamadas a herramientas en formato de array JSON
if (jsonString.startsWith("[") && jsonString.endsWith("]")) {
try {
// A veces el modelo devuelve un array de strings JSON
const toolCalls = JSON.parse(jsonString);
if (Array.isArray(toolCalls) &&
toolCalls.every((item) => typeof item === "string")) {
const parsedCalls = toolCalls.map((call) => {
try {
return JSON.parse(call);
}
catch {
return { raw: call };
}
});
return { tool_calls_array: parsedCalls };
}
}
catch {
// Continuar con el manejo por defecto
}
}
return { raw: jsonString };
}
}
/**
* Convierte una herramienta de LangChain al formato de OpenAI
*/
function convertToOpenAITool(tool) {
if ("type" in tool && "function" in tool) {
// Ya está en formato OpenAI
return tool;
}
// Convertir desde formato LangChain
let schema = {};
if ("schema" in tool && tool.schema) {
if (typeof tool.schema === "object") {
if ("schema" in tool.schema && typeof tool.schema.schema === "function") {
schema = tool.schema.schema();
}
else {
schema = zodToJsonSchema.zodToJsonSchema(tool.schema);
}
}
}
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: schema,
},
};
}
/**
* Convierte opciones en formato camelCase a los parámetros esperados por la API de NVIDIA
*/
function convertOptionsToNvidiaParams(options) {
const result = {};
// Mapeo de nombres camelCase a los nombres de la API
if (options.model !== undefined)
result.model = options.model;
if (options.maxTokens !== undefined)
result.max_tokens = options.maxTokens;
if (options.temperature !== undefined)
result.temperature = options.temperature;
if (options.topP !== undefined)
result.top_p = options.topP;
if (options.topK !== undefined)
result.top_k = options.topK;
if (options.presencePenalty !== undefined)
result.presence_penalty = options.presencePenalty;
if (options.frequencyPenalty !== undefined)
result.frequency_penalty = options.frequencyPenalty;
if (options.stop !== undefined)
result.stop = options.stop;
if (options.images !== undefined)
result.images = options.images;
return result;
}
/**
* Definición del tipo para los mensajes en formato NVIDIA
*/
zod.z.object({
role: zod.z.enum(["system", "user", "assistant"]),
content: zod.z.string().or(zod.z.array(zod.z.union([
zod.z.string(),
zod.z.object({
type: zod.z.literal("image"),
image_url: zod.z.object({
url: zod.z.string(),
}),
}),
]))),
});
/**
* Formatea los mensajes de LangChain para la API de NVIDIA
*/
function formatMessagesForNvidia(messages) {
return messages.map((message) => {
// Convertir de mensajes de LangChain a formato NVIDIA
const messageType = message.constructor.name;
if (messageType === "SystemMessage") {
return {
role: "system",
content: message.content,
};
}
else if (messageType === "HumanMessage") {
// Manejar contenido multimodal para HumanMessage
if (typeof message.content === "string") {
return {
role: "user",
content: message.content,
};
}
else {
// Procesar contenido multimodal (texto + imagen)
const content = [];
const parts = message.content;
for (const part of parts) {
if (part.type === "text") {
content.push(part.text);
}
else if (part.type === "image_url") {
content.push({
type: "image",
image_url: {
url: part.image_url.url,
},
});
}
}
return {
role: "user",
content,
};
}
}
else if (messageType === "AIMessage") {
return {
role: "assistant",
content: message.content.toString(),
};
}
else if (messageType === "ChatMessage") {
// Mapear los roles de ChatMessage a los roles de NVIDIA
let role = "user";
const chatMessage = message;
if (chatMessage.role === "system") {
role = "system";
}
else if (chatMessage.role === "assistant") {
role = "assistant";
}
else {
// Por defecto, asignar cualquier otro rol como "user"
role = "user";
}
return {
role,
content: message.content,
};
}
else {
// Para cualquier otro tipo de mensaje, usar el rol de usuario
return {
role: "user",
content: message.content.toString(),
};
}
});
}
/**
* Procesa las llamadas a herramientas desde el contenido de un mensaje
* Esta función maneja tanto el formato oficial de tool_calls como contenido
* que podría contener llamadas a herramientas en formato de texto
*/
function processToolCallsFromContent(content) {
// Si el contenido está vacío, no hay nada que procesar
if (!content) {
return { processedContent: content, extractedToolCalls: null };
}
// Verificar si el contenido parece ser JSON
if ((content.startsWith("{") && content.endsWith("}")) ||
(content.startsWith("[") && content.endsWith("]"))) {
try {
// Intentar analizar como JSON
const parsed = safeJsonParse(content);
// Si encontramos un array de llamadas a herramientas
if ("tool_calls_array" in parsed &&
Array.isArray(parsed.tool_calls_array)) {
const toolCalls = parsed.tool_calls_array
.map((call, index) => {
if (typeof call === "object" && call !== null) {
return {
id: `generated-${index}`,
type: "function",
name: call.name ||
call.function?.name ||
"unknown_function",
args: call.parameters ||
call.args ||
call.function?.parameters ||
call.function?.args ||
{},
};
}
return null;
})
.filter(Boolean);
if (toolCalls.length > 0) {
return {
// Devolver una cadena vacía ya que todo el contenido eran llamadas a herramientas
processedContent: "",
extractedToolCalls: toolCalls,
};
}
}
// Si no encontramos un formato reconocible, devolver el contenido original
return { processedContent: content, extractedToolCalls: null };
}
catch (e) {
// Si falla el análisis, devolver el contenido original
return { processedContent: content, extractedToolCalls: null };
}
}
// Si no parece JSON, devolver el contenido original
return { processedContent: content, extractedToolCalls: null };
}
/**
* Convierte la respuesta de NVIDIA a un mensaje de LangChain
*/
function convertResponseToLangChainMessage(response) {
// Extraer el contenido del mensaje de la respuesta
const responseObj = response;
const messageResponse = responseObj.choices?.[0]?.message;
const content = messageResponse?.content || "";
// Procesar posibles llamadas a herramientas en el contenido
const { processedContent, extractedToolCalls } = processToolCallsFromContent(content);
// Procesar llamadas a herramientas explícitas de la API si existen
const toolCalls = messageResponse?.tool_calls;
if ((toolCalls && toolCalls.length > 0) || extractedToolCalls) {
// Combinar las llamadas explícitas y las extraídas del contenido
const allToolCalls = [];
// Procesar llamadas explícitas
if (toolCalls && toolCalls.length > 0) {
allToolCalls.push(...toolCalls.map((toolCall) => {
try {
const args = safeJsonParse(toolCall.function.arguments);
return {
id: toolCall.id,
type: "function",
name: toolCall.function.name,
args,
};
}
catch (e) {
return {
id: toolCall.id,
type: "function",
name: toolCall.function.name,
args: { raw: toolCall.function.arguments },
};
}
}));
}
// Añadir las llamadas extraídas del contenido si existen
if (extractedToolCalls) {
allToolCalls.push(...extractedToolCalls);
}
// Crear un mensaje de IA con las llamadas a herramientas
return new messages.AIMessage({
content: processedContent, // Usar el contenido procesado
tool_calls: allToolCalls,
additional_kwargs: {
finish_reason: responseObj.choices?.[0]?.finish_reason,
token_usage: responseObj.usage,
},
});
}
// Si no hay llamadas a herramientas, crear un mensaje de IA normal
return new messages.AIMessage({
content,
additional_kwargs: {
finish_reason: responseObj.choices?.[0]?.finish_reason,
token_usage: responseObj.usage,
},
});
}
/**
* Convierte opciones de snake_case a camelCase para uso interno
*/
function toCamelCaseOptions(options) {
const { max_tokens, top_p, top_k, frequency_penalty, presence_penalty, stream, tool_choice, ...rest } = options;
return {
...rest,
...(max_tokens !== undefined && { maxTokens: max_tokens }),
...(top_p !== undefined && { topP: top_p }),
...(top_k !== undefined && { topK: top_k }),
...(frequency_penalty !== undefined && {
frequencyPenalty: frequency_penalty,
}),
...(presence_penalty !== undefined && {
presencePenalty: presence_penalty,
}),
...(stream !== undefined && { streaming: stream }),
...(tool_choice !== undefined && { toolChoice: tool_choice }),
};
}
/**
* Convierte opciones de camelCase a snake_case para la API de NVIDIA
*/
function toSnakeCaseOptions(options) {
const { maxTokens, topP, topK, frequencyPenalty, presencePenalty, streaming, toolChoice, tools, ...rest } = options;
// Crear un objeto para las opciones formateadas
const formattedOptions = {
...rest,
...(maxTokens !== undefined && { max_tokens: maxTokens }),
...(topP !== undefined && { top_p: topP }),
...(topK !== undefined && { top_k: topK }),
...(frequencyPenalty !== undefined && {
frequency_penalty: frequencyPenalty,
}),
...(presencePenalty !== undefined && { presence_penalty: presencePenalty }),
...(streaming !== undefined && { stream: streaming }),
...(toolChoice !== undefined && { tool_choice: toolChoice }),
};
// Procesar las herramientas: convertir array a formato OpenAI o mantener booleano
if (tools !== undefined) {
if (Array.isArray(tools)) {
formattedOptions.tools = tools.map(convertToOpenAITool);
}
else if (typeof tools === "boolean") {
formattedOptions.tools = tools;
}
}
return formattedOptions;
}
/**
* Implementación del modelo de lenguaje NVIDIA Llama4 para LangChain
*/
class NvidiaLlama4 extends llms.BaseLLM {
static lc_name() {
return "NvidiaLlama4";
}
constructor(fields) {
super(fields);
Object.defineProperty(this, "apiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "baseUrl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "modelName", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "defaultOptions", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "streaming", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.apiKey = fields.apiKey;
this.baseUrl =
fields.baseUrl || "https://integrate.api.nvidia.com/v1/chat/completions";
this.modelName = fields.model || "meta/llama-4-maverick-17b-128e-instruct";
this.streaming = fields.streaming ?? false;
// Extraer opciones predeterminadas eliminando las propiedades que no son opciones del modelo
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { apiKey, baseUrl, model, streaming, ...rest } = fields;
this.defaultOptions = rest;
}
_llmType() {
return "nvidia-llama4";
}
/**
* Obtiene los parámetros para la llamada a la API
*/
getParams(prompt, options, streaming = false) {
// Convertir las opciones a formato NVIDIA
const baseOptions = convertOptionsToNvidiaParams({
...this.defaultOptions,
...options,
model: this.modelName,
});
// Construir el payload para la API (formato de chat)
const payload = {
...baseOptions,
messages: [{ role: "user", content: prompt }],
stream: streaming,
};
// Agregar imágenes si existen (para capacidades multimodales)
if (options.images && options.images.length > 0) {
payload.images = options.images;
}
return payload;
}
/**
* Genera una respuesta sincrónica (no streaming)
*/
async _generate(prompts, options) {
const requestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
Accept: "application/json",
},
};
const generations = await Promise.all(prompts.map(async (prompt) => {
const params = this.getParams(prompt, options, false);
try {
const response = await axios.post(this.baseUrl, params, requestOptions);
const responseData = response.data;
// En el formato de chat/completions, el texto está en choices[0].message.content
const text = responseData.choices?.[0]?.message?.content || "";
return [
{
text,
generationInfo: {
finishReason: responseData.choices?.[0]?.finish_reason,
tokenUsage: responseData.usage,
},
},
];
}
catch (error) {
throw new Error(`Error al llamar a la API de NVIDIA Llama4: ${String(error)}`);
}
}));
return {
generations,
};
}
/**
* Procesa la respuesta de streaming de la API
*/
async *_streamResponseChunks(prompt, options, runManager) {
const requestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
Accept: "text/event-stream",
},
responseType: "stream",
};
const params = this.getParams(prompt, options, true);
try {
const response = await axios.post(this.baseUrl, params, requestOptions);
const stream = response.data;
// Un buffer para acumular los datos del stream
let buffer = "";
for await (const chunk of stream) {
const chunkText = Buffer.from(chunk).toString("utf-8");
buffer += chunkText;
// Procesar líneas completas
while (buffer.includes("\n")) {
const newlineIndex = buffer.indexOf("\n");
const line = buffer.substring(0, newlineIndex).trim();
buffer = buffer.substring(newlineIndex + 1);
if (line.startsWith("data: ")) {
const data = line.substring(6).trim();
// Fin del stream
if (data === "[DONE]") {
return;
}
try {
const parsedData = JSON.parse(data);
// En el formato de chat/completions, el contenido está en choices[0].delta.content
const text = parsedData.choices?.[0]?.delta?.content || "";
if (text) {
const chunk = new outputs.GenerationChunk({
text,
generationInfo: {
finishReason: parsedData.choices?.[0]?.finish_reason,
},
});
yield chunk;
// Notificar al manager de callbacks si existe
if (runManager) {
await runManager.handleLLMNewToken(text);
}
}
}
catch (error) {
// Ignorar líneas no válidas
continue;
}
}
}
}
}
catch (error) {
throw new Error(`Error al procesar el stream de NVIDIA Llama4: ${String(error)}`);
}
}
/**
* Implementación del método _call requerido para LLMs
*/
async _call(prompt, options) {
if (this.streaming) {
let responseText = "";
for await (const chunk of this._streamResponseChunks(prompt, options)) {
if (chunk && chunk.text) {
responseText += chunk.text;
}
}
return responseText;
}
const result = await this._generate([prompt], options);
return result.generations[0]?.[0]?.text || "";
}
}
/**
* Implementación del modelo de chat NVIDIA Llama4 para LangChain
*/
class ChatNvidiaLlama4 extends chat_models.BaseChatModel {
static lc_name() {
return "ChatNvidiaLlama4";
}
constructor(fields) {
super(fields);
Object.defineProperty(this, "apiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "baseUrl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "modelName", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "defaultOptions", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "streaming", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.apiKey = fields.apiKey;
this.baseUrl =
fields.baseUrl || "https://integrate.api.nvidia.com/v1/chat/completions";
this.modelName = fields.model || "meta/llama-4-maverick-17b-128e-instruct";
this.streaming = fields.streaming ?? false;
// Extraer opciones predeterminadas eliminando las propiedades que no son opciones del modelo
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { apiKey, baseUrl, model, streaming, ...rest } = fields;
this.defaultOptions = rest;
}
_llmType() {
return "nvidia-llama4";
}
/**
* Vincula herramientas al modelo para habilitar la funcionalidad de agente
* @param tools Lista de herramientas para vincular al modelo
* @param kwargs Opciones adicionales para la llamada
*/
bindTools(tools, kwargs) {
return this.bind({
tools: tools.map((tool) => convertToOpenAITool(tool)),
...kwargs,
});
}
/**
* Obtiene los parámetros para la llamada a la API
*/
getParams(messages, options, streaming = false) {
// Convertir las opciones a formato NVIDIA
const baseOptions = convertOptionsToNvidiaParams({
...this.defaultOptions,
...options,
model: this.modelName,
});
// Formatear los mensajes para la API de NVIDIA
const formattedMessages = formatMessagesForNvidia(messages);
// Manejar herramientas si están presentes
let toolsParam;
if (options.tools && options.tools.length > 0) {
toolsParam = options.tools.map((tool) => convertToOpenAITool(tool));
}
// Manejar tool_choice si está presente
const toolChoice = options.tool_choice;
// Construir el payload final
return {
...baseOptions,
messages: formattedMessages,
stream: streaming,
...(toolsParam && { tools: toolsParam }),
...(toolChoice && { tool_choice: toolChoice }),
};
}
/**
* Genera una respuesta sincrónica (no streaming)
*/
async _generate(messages, options) {
const requestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
Accept: "application/json",
},
};
const params = this.getParams(messages, options, false);
try {
const response = await axios.post(this.baseUrl, params, requestOptions);
const responseData = response.data;
const message = convertResponseToLangChainMessage(responseData);
const generation = {
text: message.content.toString(),
message,
generationInfo: {
finishReason: responseData.choices?.[0]?.finish_reason,
tokenUsage: responseData.usage,
},
};
return {
generations: [generation],
};
}
catch (error) {
throw new Error(`Error al llamar a la API de NVIDIA Llama4: ${String(error)}`);
}
}
/**
* Procesa la respuesta de streaming de la API
*/
async *_streamResponseChunks(messages$1, options, runManager) {
const requestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
Accept: "text/event-stream",
},
responseType: "stream",
};
const params = this.getParams(messages$1, options, true);
try {
const response = await axios.post(this.baseUrl, params, requestOptions);
const stream = response.data;
// Buffer para acumular datos del stream
let buffer = "";
// Acumulador para contenido completo para detectar posibles herramientas
let completeContent = "";
// Estado para seguimiento de herramientas
let toolCallsDetected = false;
// Buffer para herramientas usando un objeto en lugar de array
const toolsBuffer = {};
for await (const chunk of stream) {
const chunkText = Buffer.from(chunk).toString("utf-8");
buffer += chunkText;
// Buscar eventos completos en el buffer
let eventIndex;
while ((eventIndex = buffer.indexOf("\n\n")) !== -1) {
const eventText = buffer.substring(0, eventIndex);
buffer = buffer.substring(eventIndex + 2); // +2 para saltar los dos saltos de línea
if (eventText.startsWith("data: ")) {
const data = eventText.substring(6); // saltar "data: "
if (data === "[DONE]") {
// Fin del stream
// Si acumulamos contenido que podría contener herramientas, procesarlo
if (completeContent && !toolCallsDetected) {
const { processedContent, extractedToolCalls } = processToolCallsFromContent(completeContent);
if (extractedToolCalls && extractedToolCalls.length > 0) {
// Crear un nuevo chunk con las herramientas extraídas
const toolChunk = new outputs.ChatGenerationChunk({
text: processedContent,
message: new messages.AIMessageChunk({
content: processedContent,
tool_calls: extractedToolCalls,
}),
});
yield toolChunk;
if (runManager) {
await runManager.handleLLMNewToken("Tool calls processed");
}
}
}
break;
}
try {
const parsed = JSON.parse(data);
const deltaContent = parsed.choices?.[0]?.delta?.content || "";
const deltaToolCalls = parsed.choices?.[0]?.delta?.tool_calls;
// Manejar llamadas a herramientas explícitas
if (deltaToolCalls && deltaToolCalls.length > 0) {
toolCallsDetected = true;
// Acumular llamadas a herramientas
for (const toolCall of deltaToolCalls) {
// Si es una nueva llamada a herramienta, inicializar en el buffer
if (toolCall.index !== undefined) {
const toolIndex = toolCall.index;
if (!toolsBuffer[toolIndex]) {
toolsBuffer[toolIndex] = {
id: toolCall.id || `call-${toolIndex}`,
type: "function",
function: {
name: toolCall.function?.name || "",
arguments: toolCall.function?.arguments || "",
},
};
}
else {
// Actualizar llamada existente
if (toolCall.function?.name) {
toolsBuffer[toolIndex].function.name =
toolCall.function.name;
}
if (toolCall.function?.arguments) {
toolsBuffer[toolIndex].function.arguments +=
toolCall.function.arguments;
}
}
}
}
// Convertir las herramientas acumuladas al formato de LangChain
const toolCalls = Object.values(toolsBuffer).map((tool) => ({
id: tool.id,
type: "function",
function: {
name: tool.function.name,
arguments: tool.function.arguments,
},
}));
const toolChunk = new outputs.ChatGenerationChunk({
text: completeContent,
message: new messages.AIMessageChunk({
content: completeContent,
tool_calls: toolCalls,
}),
});
yield toolChunk;
if (runManager) {
await runManager.handleLLMNewToken("Tool call received");
}
}
// Contenido de texto normal
else if (deltaContent) {
completeContent += deltaContent;
// Verificar si el contenido parece contener herramientas
// Solo si no hemos detectado herramientas explícitas
if (!toolCallsDetected &&
(completeContent.includes('{"function":') ||
completeContent.includes('"tool_calls":') ||
(completeContent.startsWith("[") &&
completeContent.includes('"name":')))) {
// Esperar a acumular más contenido antes de intentar procesarlo
// Solo emitimos chunk si parece un texto normal
if (!completeContent.startsWith("{") &&
!completeContent.startsWith("[") &&
!completeContent.includes("```json")) {
const chunkGen = new outputs.ChatGenerationChunk({
text: deltaContent,
message: new messages.AIMessageChunk({ content: deltaContent }),
});
yield chunkGen;
if (runManager) {
await runManager.handleLLMNewToken(deltaContent);
}
}
}
else {
// Contenido normal, emitir directamente
const chunkGen = new outputs.ChatGenerationChunk({
text: deltaContent,
message: new messages.AIMessageChunk({ content: deltaContent }),
});
yield chunkGen;
if (runManager) {
await runManager.handleLLMNewToken(deltaContent);
}
}
}
}
catch (err) {
console.error("Error al analizar chunk de evento:", err);
}
}
}
}
}
catch (error) {
throw new Error(`Error al procesar stream de NVIDIA Llama4: ${String(error)}`);
}
}
async _call(messages, options) {
if (this.streaming) {
const stream = await this._streamResponseChunks(messages, options);
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk.text);
}
return chunks.join("");
}
else {
const result = await this._generate(messages, options);
return result.generations[0].text;
}
}
}
/**
* Implementación de Embeddings de NVIDIA para LangChain
*/
class NvidiaEmbeddings extends embeddings.Embeddings {
constructor(fields) {
super(fields);
Object.defineProperty(this, "apiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "baseUrl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "modelName", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "inputType", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "encodingFormat", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "truncate", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "maxRetries", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "defaultOptions", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.apiKey = fields.apiKey;
this.baseUrl =
fields.baseUrl || "https://integrate.api.nvidia.com/v1/embeddings";
this.modelName = fields.model || "nvidia/nv-embedcode-7b-v1";
this.inputType = fields.inputType || "query";
this.encodingFormat = fields.encodingFormat || "float";
this.truncate = fields.truncate || "NONE";
this.maxRetries = fields.maxRetries ?? 3;
// Extraer las opciones que no son parte de la configuración principal
const keysToExclude = [
"apiKey",
"baseUrl",
"model",
"inputType",
"encodingFormat",
"truncate",
"maxRetries",
];
// Creamos un objeto con todas las propiedades que no son de configuración principal
this.defaultOptions = Object.fromEntries(Object.entries(fields).filter(([key]) => !keysToExclude.includes(key)));
}
/**
* Método para realizar la llamada a la API con reintentos
*/
async embeddingWithRetry(text) {
const texts = Array.isArray(text) ? text : [text];
// Preparar payload para la API
const payload = {
model: this.modelName,
input: texts,
input_type: this.inputType,
encoding_format: this.encodingFormat,
truncate: this.truncate,
...this.defaultOptions,
};
// Opciones para la petición
const requestOptions = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
};
// Implementación de backoff exponencial para reintentos
let error = "";
for (let i = 0; i < this.maxRetries; i += 1) {
try {
const response = await axios.post(this.baseUrl, payload, requestOptions);
return response.data.data.map((item) => item.embedding);
}
catch (err) {
error = String(err);
// Esperar antes de reintentar (backoff exponencial)
const waitTime = 2 ** i * 1000 + Math.random() * 100;
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, waitTime);
});
}
}
// Si llegamos aquí, todos los reintentos fallaron
throw new Error(`Error al generar embeddings después de ${this.maxRetries} intentos: ${error}`);
}
/**
* Generar embedding para un solo texto
*/
async embedQuery(text) {
const embeddings = await this.embeddingWithRetry(text);
return embeddings[0];
}
/**
* Generar embeddings para múltiples textos
*/
async embedDocuments(documents) {
return this.embeddingWithRetry(documents);
}
}
/**
* Implementación mejorada del modelo de chat NVIDIA Llama4 para n8n
* Optimizada para trabajar con Tools Agent
*/
class ChatNvidiaLlama4Tools extends chat_models.BaseChatModel {
static lc_name() {
return "ChatNvidiaLlama4Tools";
}
constructor(fields) {
super(fields);
Object.defineProperty(this, "apiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "baseUrl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "modelName", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "streaming", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "defaultOptions", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// Guardar herramientas vinculadas para usarlas en cada llamada
Object.defineProperty(this, "linkedTools", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
// Flag para compatibilidad con n8n Tools Agent
Object.defineProperty(this, "toolCallModel", {
enumerable: true,
configurable: true,
writable: true,
value: true
});
this.apiKey = fields.apiKey;
this.baseUrl =
fields.baseUrl || "https://integrate.api.nvidia.com/v1/chat/completions";
this.modelName = fields.model || "meta/llama-4-maverick-17b-128e-instruct";
this.streaming = fields.streaming ?? false;
// Si se proporcionan herramientas iniciales, guardarlas
if (Array.isArray(fields.tools)) {
this.linkedTools = fields.tools;
}
// Extraer opciones predeterminadas eliminando las propiedades que no son opciones del modelo
const { apiKey, baseUrl, model, streaming, tools, toolChoice, ...rest } = fields;
// Incluir herramientas en las opciones predeterminadas
this.defaultOptions = {
...rest,
tools: true,
toolChoice: "auto",
};
}
_llmType() {
return "nvidia-llama4-n8n-tools";
}
/**
* Método requerido por n8n Tools Agent para vincular herramientas
*/
bindTools(tools) {
if (tools && tools.length > 0) {
this.linkedTools = tools;
}
// Devolver this mantiene compatibilidad con n8n
return this;
}
/**
* Convierte una herramienta al formato compatible con OpenAI/NVIDIA
*/
convertToOpenAITool(tool) {
// Si ya está en formato OpenAI
if ("type" in tool && "function" in tool) {
return tool;
}
// Convertir desde formato LangChain
let schema = {};
if ("schema" in tool && tool.schema) {
if (typeof tool.schema === "object") {
try {
if ("schema" in tool.schema &&
typeof tool.schema.schema === "function") {
schema = tool.schema.schema();
}
else {
schema = zodToJsonSchema.zodToJsonSchema(tool.schema);
}
}
catch (e) {
console.error("Error al convertir schema de herramienta:", e);
schema = {};
}
}
}
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: schema,
},
};
}
/**
* Prepara los mensajes para la API de NVIDIA
*/
formatMessagesForNvidia(messages) {
return messages.map((message) => {
const messageType = message._getType();
if (messageType === "system") {
return {
role: "system",
content: message.content,
};
}
else if (messageType === "human") {
// Manejar contenido multimodal para HumanMessage
if (typeof message.content === "string") {
return {
role: "user",
content: message.content,
};
}
else {
// Procesar contenido multimodal (texto + imagen)
const content = [];
// Manejo de contenido multimodal
const parts = message.content;
for (const part of parts) {
if (part.type === "text") {
content.push(part.text);
}
else if (part.type === "image_url") {
content.push({
type: "image",
image_url: {
url: part.image_url.url,
},
});
}
}
return {
role: "user",
content,
};
}
}
else if (messageType === "ai") {
return {
role: "assistant",
content: message.content.toString(),
};
}
else if (message.role === "chat") {
// Manejo de ChatMessage con tipo personalizado
const role = message.role;
let nvidiaRole = "user";
if (role === "system") {
nvidiaRole = "system";
}
else if (role === "assistant") {
nvidiaRole = "assistant";
}
return {
role: nvidiaRole,
content: message.content,
};
}
else {
return {
role: "user",
content: message.content.toString(),
};
}
});
}
/**
* Obtiene los parámetros para la llamada a la API
*/
getParams(messages, options, streaming = false) {
// Extraer opciones para adaptar al formato de NVIDIA
const { temperature, maxTokens, topP, topK, frequencyPenalty, presencePenalty, stop, images, tools: callTools, tool_choice: callToolChoice, ...restOptions } = { ...this.defaultOptions, ...options };
// Opciones base para la API
const baseOptions = {
model: this.modelName,
stream: streaming,
};
// Añadir parámetros opcionales solo si están definidos
if (temperature !== undefined)
baseOptions.temperature = temperature;
if (maxTokens !== undefined)
baseOptions.max_tokens = maxTokens;
if (topP !== undefined)
baseOptions.top_p = topP;
if (topK !== undefined)
baseOptions.top_k = topK;
if (frequencyPenalty !== undefined)
baseOptions.frequency_penalty = frequencyPenalty;
if (presencePenalty !== undefined)
baseOptions.presence_penalty = presencePenalty;
if (stop !== undefined)
baseOptions.stop = stop;
if (images !== undefined)
baseOptions.images = images;
// Formatear los mensajes para la API de NVIDIA
const formattedMessages = this.formatMessagesForNvidia(messages);
// Determinar qué herramientas usar:
// 1. Priorizar las herramientas pasadas en la llamada
// 2. Usar las herramientas vinculadas previamente
// 3. Si hay herramientas, configurar el modelo para usarlas
const toolsToUse = callTools || this.linkedTools;
let toolsParam;
let toolChoice = callToolChoice;
if (toolsToUse && toolsToUse.length > 0) {
toolsParam = toolsToUse.map((tool) => this.convertToOpenAITool(tool));
// Si no se especificó toolChoice, usar "auto" por defecto cuando hay herramientas
if (toolChoice === undefined) {
toolChoice = "auto";
}
}
// Construir el payload final con todas las opciones
return {
...baseOptions,
...restOptions,
messages: formattedMessages,
...(toolsParam && { tools: toolsParam }),
...(toolChoice && { tool_choice: toolChoice }),
};
}
/**
* Procesa una respuesta de la API a formato LangChain
*/
convertResponseToMessage(responseData) {
// Implementación simplificada - devuelve el contenido directamente
const content = responseData.choices?.[0]?.message?.content || "";
const toolCalls = responseData.choices?.[0]?.message?.tool_calls;
// Si hay tool_calls, convertirlos al formato esperado por LangChain
if (toolCalls && toolCalls.length > 0) {
return {
content,
tool_calls: toolCalls.map((tc) => {
let args;
try {
args = JSON.parse(tc.function.arguments);
}
catch (e) {
args = { raw: tc.function.arguments };
}
return {
id: tc.id,
type: "function",
name: tc.function.name,
args,
};
}),
additional_kwargs: {
finish_reason: responseData.choices?.[0]?.finish_reason,