@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
JavaScript
;
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;