graphlit-client
Version:
Graphlit API Client for TypeScript
545 lines (544 loc) • 22 kB
JavaScript
import { ConversationRoleTypes, } from "../generated/graphql-types.js";
/**
* Format GraphQL conversation messages for OpenAI SDK
*/
export function formatMessagesForOpenAI(messages) {
const formattedMessages = [];
for (const message of messages) {
if (!message.role) {
continue;
}
// Allow messages with tool calls even if they have no text content
const hasContent = message.message?.trim();
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
if (!hasContent && !hasToolCalls) {
continue;
}
const trimmedMessage = message.message?.trim() || "";
switch (message.role) {
case ConversationRoleTypes.System:
formattedMessages.push({
role: "system",
content: trimmedMessage,
});
break;
case ConversationRoleTypes.Assistant:
const assistantMessage = {
role: "assistant",
};
// Only add content if there's actual text
if (trimmedMessage) {
assistantMessage.content = trimmedMessage;
}
// Add tool calls if present
if (message.toolCalls && message.toolCalls.length > 0) {
assistantMessage.tool_calls = message.toolCalls
.filter((tc) => tc !== null)
.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.name,
arguments: toolCall.arguments,
},
}));
}
formattedMessages.push(assistantMessage);
break;
case ConversationRoleTypes.Tool:
formattedMessages.push({
role: "tool",
content: trimmedMessage,
tool_call_id: message.toolCallId || "",
});
break;
default: // User messages
// Check if this message has image data
if (message.mimeType && message.data) {
// Multi-modal message with image
const contentParts = [];
// Add text content if present
if (trimmedMessage) {
contentParts.push({
type: "text",
text: trimmedMessage,
});
}
// Add image content
contentParts.push({
type: "image_url",
image_url: {
url: `data:${message.mimeType};base64,${message.data}`,
},
});
formattedMessages.push({
role: "user",
content: contentParts,
});
}
else {
// Text-only message
formattedMessages.push({
role: "user",
content: trimmedMessage,
});
}
break;
}
}
return formattedMessages;
}
/**
* Format GraphQL conversation messages for Anthropic SDK
*/
export function formatMessagesForAnthropic(messages) {
let systemPrompt;
const formattedMessages = [];
for (const message of messages) {
if (!message.role)
continue;
// Allow messages with tool calls even if they have no text content
const hasContent = message.message?.trim();
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
if (!hasContent && !hasToolCalls)
continue;
const trimmedMessage = message.message?.trim() || "";
switch (message.role) {
case ConversationRoleTypes.System:
systemPrompt = trimmedMessage;
break;
case ConversationRoleTypes.Assistant:
const content = []; // Use any[] to allow thinking blocks
// Handle thinking blocks for extended thinking preservation
if (trimmedMessage && trimmedMessage.includes("<thinking")) {
// Extract thinking content and signature if present
const thinkingMatch = trimmedMessage.match(/<thinking(?:\s+signature="([^"]*)")?>([\s\S]*?)<\/thinking>/);
if (thinkingMatch) {
const signature = thinkingMatch[1]; // Optional signature
const thinkingContent = thinkingMatch[2].trim();
// Add thinking block for conversation history preservation
if (thinkingContent) {
const thinkingBlock = {
type: "thinking",
thinking: thinkingContent,
};
// Add signature if present (required by Anthropic API)
if (signature) {
thinkingBlock.signature = signature;
}
else {
// Provide a default signature if none captured
thinkingBlock.signature = "";
}
content.push(thinkingBlock);
}
// Remove thinking tags from the main text and add remaining content
const textWithoutThinking = trimmedMessage
.replace(/<thinking(?:\s+[^>]*)?>[\s\S]*?<\/thinking>/g, "")
.trim();
if (textWithoutThinking) {
content.push({
type: "text",
text: textWithoutThinking,
});
}
}
else {
// No valid thinking blocks found, add as regular text
content.push({
type: "text",
text: trimmedMessage,
});
}
}
else if (trimmedMessage) {
// Add regular text content
content.push({
type: "text",
text: trimmedMessage,
});
}
// Add tool uses if present
if (message.toolCalls && message.toolCalls.length > 0) {
for (const toolCall of message.toolCalls) {
if (toolCall) {
content.push({
type: "tool_use",
id: toolCall.id,
name: toolCall.name,
input: toolCall.arguments ? JSON.parse(toolCall.arguments) : {},
});
}
}
}
formattedMessages.push({
role: "assistant",
content,
});
break;
case ConversationRoleTypes.Tool:
// Anthropic expects tool responses as user messages with tool_result content blocks
formattedMessages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: message.toolCallId || "",
content: trimmedMessage,
},
],
});
break;
default: // User messages
// Check if this message has image data
if (message.mimeType && message.data) {
// Multi-modal message with image
const contentParts = [];
// Add text content if present
if (trimmedMessage) {
contentParts.push({
type: "text",
text: trimmedMessage,
});
}
// Add image content
contentParts.push({
type: "image",
source: {
type: "base64",
media_type: message.mimeType,
data: message.data,
},
});
formattedMessages.push({
role: "user",
content: contentParts,
});
}
else {
// Text-only message
formattedMessages.push({
role: "user",
content: trimmedMessage,
});
}
break;
}
}
const result = { system: systemPrompt, messages: formattedMessages };
return result;
}
/**
* Format GraphQL conversation messages for Google SDK
*/
export function formatMessagesForGoogle(messages) {
const formattedMessages = [];
for (const message of messages) {
if (!message.role)
continue;
// Allow messages with image data even if they have no text content
const hasContent = message.message?.trim();
const hasImageData = message.mimeType && message.data;
if (!hasContent && !hasImageData)
continue;
const trimmedMessage = message.message?.trim() || "";
switch (message.role) {
case ConversationRoleTypes.System:
// Google handles system prompts differently, usually as part of the first user message
formattedMessages.push({
role: "user",
parts: [{ text: trimmedMessage }],
});
break;
case ConversationRoleTypes.Assistant:
const parts = [];
// Add text content
if (trimmedMessage) {
parts.push({ text: trimmedMessage });
}
// Add function calls if present
if (message.toolCalls && message.toolCalls.length > 0) {
for (const toolCall of message.toolCalls) {
if (toolCall) {
parts.push({
functionCall: {
name: toolCall.name,
args: toolCall.arguments
? JSON.parse(toolCall.arguments)
: {},
},
});
}
}
}
formattedMessages.push({
role: "model",
parts,
});
break;
default: // User messages
// Check if this message has image data
if (message.mimeType && message.data) {
// Multi-modal message with image
const parts = [];
// Add text content if present
if (trimmedMessage) {
parts.push({ text: trimmedMessage });
}
// Add image content
parts.push({
inlineData: {
mimeType: message.mimeType,
data: message.data,
},
});
formattedMessages.push({
role: "user",
parts,
});
}
else {
// Text-only message
formattedMessages.push({
role: "user",
parts: [{ text: trimmedMessage }],
});
}
break;
}
}
return formattedMessages;
}
/**
* Format GraphQL conversation messages for Cohere SDK
*/
export function formatMessagesForCohere(messages) {
const formattedMessages = [];
for (const message of messages) {
if (!message.role)
continue;
// Allow messages with tool calls even if they have no text content
const hasContent = message.message?.trim();
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
if (!hasContent && !hasToolCalls)
continue;
const trimmedMessage = message.message?.trim() || "";
switch (message.role) {
case ConversationRoleTypes.System:
formattedMessages.push({
role: "SYSTEM",
message: trimmedMessage,
});
break;
case ConversationRoleTypes.Assistant:
const assistantMessage = {
role: "CHATBOT",
message: trimmedMessage,
};
// Add tool calls if present
if (message.toolCalls && message.toolCalls.length > 0) {
assistantMessage.tool_calls = message.toolCalls
.filter((tc) => tc !== null)
.map((toolCall) => ({
id: toolCall.id,
name: toolCall.name,
parameters: toolCall.arguments
? JSON.parse(toolCall.arguments)
: {},
}));
}
formattedMessages.push(assistantMessage);
break;
case ConversationRoleTypes.Tool:
// Cohere expects tool results as TOOL messages
formattedMessages.push({
role: "TOOL",
message: trimmedMessage,
tool_results: [
{
call: {
name: "", // Would need to be tracked from the tool call
parameters: {},
},
outputs: [
{
output: trimmedMessage, // Changed from 'text' to 'output'
},
],
},
],
});
break;
default: // User messages
formattedMessages.push({
role: "USER",
message: trimmedMessage,
});
break;
}
}
return formattedMessages;
}
/**
* Format GraphQL conversation messages for Mistral SDK
*/
export function formatMessagesForMistral(messages) {
if (process.env.DEBUG_GRAPHLIT_SDK_STREAMING) {
console.log(`[Mistral Formatter] Input: ${messages.length} messages`);
messages.forEach((msg, idx) => {
console.log(` Input ${idx}: role=${msg.role}, hasToolCalls=${!!msg.toolCalls}, toolCallId=${msg.toolCallId}`);
});
}
const formattedMessages = [];
for (const message of messages) {
if (!message.role)
continue;
const hasContent = message.message?.trim();
const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
if (!hasContent && !hasToolCalls)
continue;
const trimmedMessage = message.message?.trim() || "";
switch (message.role) {
case ConversationRoleTypes.System:
formattedMessages.push({
role: "system",
content: trimmedMessage,
});
break;
case ConversationRoleTypes.Assistant:
const assistantMessage = {
role: "assistant",
content: trimmedMessage || "", // Mistral expects string, not null
};
// Add tool calls if present
if (message.toolCalls && message.toolCalls.length > 0) {
assistantMessage.tool_calls = message.toolCalls
.filter((tc) => tc !== null)
.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.name,
arguments: toolCall.arguments,
},
}));
}
formattedMessages.push(assistantMessage);
break;
case ConversationRoleTypes.Tool:
if (!message.toolCallId) {
console.warn(`[Mistral] Tool message missing toolCallId, skipping`);
break;
}
// Mistral expects tool_call_id (snake_case) and a name field
formattedMessages.push({
role: "tool",
name: message.toolName || "unknown", // Access toolName from extended message
content: trimmedMessage,
tool_call_id: message.toolCallId, // Mistral uses snake_case!
});
break;
default: // User messages
// Check if this message has image data
if (message.mimeType && message.data) {
// Multi-modal message with image
const contentParts = [];
// Add text content if present
if (trimmedMessage) {
contentParts.push({
type: "text",
text: trimmedMessage,
});
}
// Add image content
contentParts.push({
type: "image_url",
image_url: `data:${message.mimeType};base64,${message.data}`,
});
formattedMessages.push({
role: "user",
content: contentParts,
});
}
else {
// Text-only message
formattedMessages.push({
role: "user",
content: trimmedMessage,
});
}
break;
}
}
if (process.env.DEBUG_GRAPHLIT_SDK_STREAMING) {
console.log(`[Mistral Formatter] Output: ${formattedMessages.length} messages`);
formattedMessages.forEach((msg, idx) => {
const msgWithTools = msg;
console.log(` Output ${idx}: role=${msg.role}, hasToolCalls=${!!msgWithTools.tool_calls}, toolCallId=${msgWithTools.tool_call_id}`);
if (msgWithTools.tool_calls) {
console.log(` Tool calls: ${JSON.stringify(msgWithTools.tool_calls)}`);
}
});
}
return formattedMessages;
}
/**
* Format GraphQL conversation messages for Bedrock SDK (Claude models)
*/
export function formatMessagesForBedrock(messages) {
let systemPrompt;
const formattedMessages = [];
for (const message of messages) {
if (!message.role || !message.message?.trim())
continue;
const trimmedMessage = message.message.trim();
switch (message.role) {
case ConversationRoleTypes.System:
systemPrompt = trimmedMessage;
break;
case ConversationRoleTypes.Assistant:
formattedMessages.push({
role: "assistant",
content: trimmedMessage,
});
break;
default: // User messages
// Check if this message has image data
if (message.mimeType && message.data) {
// Multi-modal message with image
const contentParts = [];
// Add text content if present
if (trimmedMessage) {
contentParts.push({
type: "text",
text: trimmedMessage,
});
}
// Add image content
const format = message.mimeType.split("/")[1];
contentParts.push({
type: "image",
image: {
format,
source: {
bytes: message.data,
},
},
});
formattedMessages.push({
role: "user",
content: contentParts,
});
}
else {
// Text-only message
formattedMessages.push({
role: "user",
content: trimmedMessage,
});
}
break;
}
}
return { system: systemPrompt, messages: formattedMessages };
}