UNPKG

@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
'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,