UNPKG

@rexdug7005/nvidia-llama4

Version:

Integración de NVIDIA Llama4 con LangChain.js, con soporte para Tools Agent de n8n

368 lines (364 loc) 13.9 kB
'use strict'; var messages = require('@langchain/core/messages'); var zod = require('zod'); var zodToJsonSchema = require('zod-to-json-schema'); /** * 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 */ const NvidiaMessageSchema = 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; } exports.NvidiaMessageSchema = NvidiaMessageSchema; exports.convertOptionsToNvidiaParams = convertOptionsToNvidiaParams; exports.convertResponseToLangChainMessage = convertResponseToLangChainMessage; exports.convertToOpenAITool = convertToOpenAITool; exports.formatMessagesForNvidia = formatMessagesForNvidia; exports.processToolCallsFromContent = processToolCallsFromContent; exports.safeJsonParse = safeJsonParse; exports.toCamelCaseOptions = toCamelCaseOptions; exports.toSnakeCaseOptions = toSnakeCaseOptions;