mirror of https://github.com/buster-so/buster.git
Compare commits
3 Commits
c5d5e28cc5
...
7b9afdbb2f
Author | SHA1 | Date |
---|---|---|
|
7b9afdbb2f | |
|
980a786d21 | |
|
455888cdcd |
|
@ -17,6 +17,7 @@ import { type PermissionedDataset, getPermissionedDatasets } from '@buster/acces
|
|||
// AI package imports
|
||||
import { type AnalystWorkflowInput, runAnalystWorkflow } from '@buster/ai';
|
||||
|
||||
import type { ModelMessage } from 'ai';
|
||||
import type { messagePostProcessingTask } from '../message-post-processing/message-post-processing';
|
||||
|
||||
/**
|
||||
|
@ -339,19 +340,20 @@ export const analystAgentTask: ReturnType<
|
|||
logPerformanceMetrics('post-data-load', payload.message_id, taskStartTime, resourceTracker);
|
||||
|
||||
// Task 4: Prepare workflow input with conversation history
|
||||
// Convert conversation history to messages format expected by the workflow
|
||||
const messages =
|
||||
// The conversation history from getChatConversationHistory is already in ModelMessage[] format
|
||||
const modelMessages: ModelMessage[] =
|
||||
conversationHistory.length > 0
|
||||
? conversationHistory
|
||||
: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
role: 'user',
|
||||
// v5 supports string content directly for user messages
|
||||
content: messageContext.requestMessage,
|
||||
},
|
||||
];
|
||||
|
||||
const workflowInput: AnalystWorkflowInput = {
|
||||
messages,
|
||||
messages: modelMessages,
|
||||
messageId: payload.message_id,
|
||||
chatId: messageContext.chatId,
|
||||
userId: messageContext.userId,
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
// Tool repair utilities
|
||||
export * from './tool-call-repair';
|
||||
|
||||
// Message conversion utilities
|
||||
export * from './message-conversion';
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
import type { ModelMessage } from 'ai';
|
||||
|
||||
/**
|
||||
* Converts AI SDK v4 CoreMessage to v5 ModelMessage
|
||||
*
|
||||
* Key differences:
|
||||
* - ImagePart: mimeType → mediaType
|
||||
* - FilePart: mimeType → mediaType, added optional filename
|
||||
* - ToolResultPart: result → output (with structured type)
|
||||
* - ToolCallPart: remains the same (args field)
|
||||
*/
|
||||
export function convertCoreMessageToModelMessage(message: unknown): ModelMessage {
|
||||
// Handle null/undefined
|
||||
if (!message || typeof message !== 'object' || !('role' in message)) {
|
||||
return message as ModelMessage;
|
||||
}
|
||||
|
||||
const msg = message as Record<string, unknown>;
|
||||
const { role, content } = msg;
|
||||
|
||||
switch (role) {
|
||||
case 'system':
|
||||
// System messages remain string-based
|
||||
return {
|
||||
role: 'system',
|
||||
content: typeof content === 'string' ? content : '',
|
||||
};
|
||||
|
||||
case 'user':
|
||||
// User messages can be string or array of parts
|
||||
if (typeof content === 'string') {
|
||||
return {
|
||||
role: 'user',
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return {
|
||||
role: 'user',
|
||||
// biome-ignore lint/suspicious/noExplicitAny: necessary for v4 to v5 conversion
|
||||
content: content.map(convertContentPart) as any,
|
||||
};
|
||||
}
|
||||
|
||||
return { role: 'user', content: '' };
|
||||
|
||||
case 'assistant':
|
||||
// Assistant messages can be string or array of parts
|
||||
if (typeof content === 'string') {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return {
|
||||
role: 'assistant',
|
||||
// biome-ignore lint/suspicious/noExplicitAny: necessary for v4 to v5 conversion
|
||||
content: content.map(convertContentPart) as any,
|
||||
};
|
||||
}
|
||||
|
||||
return { role: 'assistant', content: '' };
|
||||
|
||||
case 'tool':
|
||||
// Tool messages are always arrays of ToolResultPart
|
||||
if (Array.isArray(content)) {
|
||||
return {
|
||||
role: 'tool',
|
||||
// biome-ignore lint/suspicious/noExplicitAny: necessary for v4 to v5 conversion
|
||||
content: content.map(convertToolResultPart) as any,
|
||||
};
|
||||
}
|
||||
|
||||
return { role: 'tool', content: [] };
|
||||
|
||||
default:
|
||||
// Pass through unknown roles
|
||||
return message as ModelMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert content parts from v4 to v5 format
|
||||
*/
|
||||
function convertContentPart(part: unknown): unknown {
|
||||
if (!part || typeof part !== 'object' || !('type' in part)) {
|
||||
return part;
|
||||
}
|
||||
|
||||
const p = part as Record<string, unknown>;
|
||||
switch (p.type) {
|
||||
case 'text':
|
||||
// Text parts remain the same
|
||||
return part;
|
||||
|
||||
case 'image':
|
||||
// Convert mimeType → mediaType
|
||||
if ('mimeType' in p) {
|
||||
const { mimeType, ...rest } = p;
|
||||
return {
|
||||
...rest,
|
||||
mediaType: mimeType,
|
||||
};
|
||||
}
|
||||
return part;
|
||||
|
||||
case 'file':
|
||||
// Convert mimeType → mediaType
|
||||
if ('mimeType' in p) {
|
||||
const { mimeType, ...rest } = p;
|
||||
return {
|
||||
...rest,
|
||||
mediaType: mimeType,
|
||||
};
|
||||
}
|
||||
return part;
|
||||
|
||||
case 'tool-call':
|
||||
// Tool calls: args → input (AI SDK v5 change)
|
||||
if ('args' in p) {
|
||||
const { args, ...rest } = p;
|
||||
return { ...rest, input: args };
|
||||
}
|
||||
return part;
|
||||
|
||||
default:
|
||||
return part;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tool result parts from v4 to v5 format
|
||||
*/
|
||||
function convertToolResultPart(part: unknown): unknown {
|
||||
if (!part || typeof part !== 'object') {
|
||||
return part;
|
||||
}
|
||||
|
||||
const p = part as Record<string, unknown>;
|
||||
|
||||
if (p.type !== 'tool-result') {
|
||||
return part;
|
||||
}
|
||||
|
||||
// Convert result → output with proper structure
|
||||
if ('result' in p && !('output' in p)) {
|
||||
const { result, experimental_content, isError, ...rest } = p;
|
||||
|
||||
// Convert result to structured output format
|
||||
let output: { type: string; value: unknown };
|
||||
if (isError) {
|
||||
// Error results
|
||||
if (typeof result === 'string') {
|
||||
output = { type: 'error-text', value: result };
|
||||
} else {
|
||||
output = { type: 'error-json', value: result };
|
||||
}
|
||||
} else {
|
||||
// Success results
|
||||
if (typeof result === 'string') {
|
||||
output = { type: 'text', value: result };
|
||||
} else {
|
||||
output = { type: 'json', value: result };
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure toolCallId exists and is valid
|
||||
// If it's missing or invalid, preserve the original part
|
||||
if (!rest.toolCallId || typeof rest.toolCallId !== 'string') {
|
||||
return part;
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of v4 CoreMessages to v5 ModelMessages
|
||||
*/
|
||||
export function convertCoreMessagesToModelMessages(messages: unknown[]): ModelMessage[] {
|
||||
if (!Array.isArray(messages)) {
|
||||
return [];
|
||||
}
|
||||
return messages.map(convertCoreMessageToModelMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if content is already in v5 format
|
||||
* Useful when migrating codebases that might have mixed formats
|
||||
*/
|
||||
export function isV5ContentFormat(content: unknown): boolean {
|
||||
// Check for tool result with v5 output field
|
||||
if (Array.isArray(content) && content.length > 0) {
|
||||
const firstItem = content[0];
|
||||
if (firstItem?.type === 'tool-result' && 'output' in firstItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart conversion that handles both v4 and v5 formats
|
||||
* Useful during migration when you might have mixed message formats
|
||||
*/
|
||||
export function ensureModelMessageFormat(message: unknown): ModelMessage {
|
||||
// Already in v5 format if tool messages have 'output' field
|
||||
if (message && typeof message === 'object' && 'role' in message && 'content' in message) {
|
||||
const msg = message as Record<string, unknown>;
|
||||
if (msg.role === 'tool' && isV5ContentFormat(msg.content)) {
|
||||
return message as ModelMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from v4 format
|
||||
return convertCoreMessageToModelMessage(message);
|
||||
}
|
|
@ -6,10 +6,11 @@ import { messages } from '../../schema';
|
|||
|
||||
/**
|
||||
* Convert messages from old CoreMessage format (v4) to ModelMessage format (v5)
|
||||
* Main changes: tool calls 'args' → 'input', tool results 'result' → 'output'
|
||||
*
|
||||
* Since we're dealing with unknown data from the database, we cast to ModelMessage[]
|
||||
* and let the runtime handle any actual format differences.
|
||||
* Key changes:
|
||||
* - Tool calls: 'args' → 'input'
|
||||
* - Tool results: 'result' → structured 'output' object
|
||||
* - Image/File parts: 'mimeType' → 'mediaType'
|
||||
* - User/Assistant string content remains as string (v5 supports both)
|
||||
*/
|
||||
export function convertCoreToModel(messages: unknown): ModelMessage[] {
|
||||
if (!Array.isArray(messages)) {
|
||||
|
@ -23,51 +24,163 @@ export function convertCoreToModel(messages: unknown): ModelMessage[] {
|
|||
}
|
||||
|
||||
const msg = message as Record<string, unknown>;
|
||||
const { role, content } = msg;
|
||||
|
||||
// For assistant messages, update tool call args → input
|
||||
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
||||
return {
|
||||
...msg,
|
||||
content: msg.content.map((part: unknown) => {
|
||||
if (
|
||||
part &&
|
||||
typeof part === 'object' &&
|
||||
'type' in part &&
|
||||
part.type === 'tool-call' &&
|
||||
'args' in part
|
||||
) {
|
||||
const { args, ...rest } = part as Record<string, unknown>;
|
||||
return { ...rest, input: args };
|
||||
}
|
||||
return part;
|
||||
}),
|
||||
} as ModelMessage;
|
||||
switch (role) {
|
||||
case 'system':
|
||||
// System messages remain string-based in both v4 and v5
|
||||
return {
|
||||
...msg,
|
||||
content: typeof content === 'string' ? content : '',
|
||||
} as ModelMessage;
|
||||
|
||||
case 'user':
|
||||
// User messages: handle both string and array content
|
||||
if (typeof content === 'string') {
|
||||
// v5 supports string content directly for user messages
|
||||
return msg as ModelMessage;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
// Convert any image/file parts
|
||||
return {
|
||||
...msg,
|
||||
content: content.map(convertContentPart),
|
||||
} as ModelMessage;
|
||||
}
|
||||
return { ...msg, content: '' } as ModelMessage;
|
||||
|
||||
case 'assistant':
|
||||
// Assistant messages: handle both string and array content
|
||||
if (typeof content === 'string') {
|
||||
// v5 supports string content directly for assistant messages
|
||||
return msg as ModelMessage;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
// Convert tool calls and other parts
|
||||
return {
|
||||
...msg,
|
||||
content: content.map(convertContentPart),
|
||||
} as ModelMessage;
|
||||
}
|
||||
return { ...msg, content: '' } as ModelMessage;
|
||||
|
||||
case 'tool':
|
||||
// Tool messages: convert result to structured output
|
||||
if (Array.isArray(content)) {
|
||||
return {
|
||||
...msg,
|
||||
content: content.map(convertToolResultPart),
|
||||
} as ModelMessage;
|
||||
}
|
||||
return { role: 'tool', content: [] } as ModelMessage;
|
||||
|
||||
default:
|
||||
return msg as ModelMessage;
|
||||
}
|
||||
|
||||
// For tool messages, update result → output
|
||||
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
||||
return {
|
||||
...msg,
|
||||
content: msg.content.map((part: unknown) => {
|
||||
if (
|
||||
part &&
|
||||
typeof part === 'object' &&
|
||||
'type' in part &&
|
||||
part.type === 'tool-result' &&
|
||||
'result' in part
|
||||
) {
|
||||
const { result, ...rest } = part as Record<string, unknown>;
|
||||
return { ...rest, output: result };
|
||||
}
|
||||
return part;
|
||||
}),
|
||||
} as ModelMessage;
|
||||
}
|
||||
|
||||
return message as ModelMessage;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert content parts from v4 to v5 format
|
||||
*/
|
||||
function convertContentPart(part: unknown): unknown {
|
||||
if (!part || typeof part !== 'object') {
|
||||
return part;
|
||||
}
|
||||
|
||||
const p = part as Record<string, unknown>;
|
||||
|
||||
switch (p.type) {
|
||||
case 'text':
|
||||
// Text parts remain the same
|
||||
return part;
|
||||
|
||||
case 'image':
|
||||
// Convert mimeType → mediaType
|
||||
if ('mimeType' in p) {
|
||||
const { mimeType, ...rest } = p;
|
||||
return {
|
||||
...rest,
|
||||
mediaType: mimeType,
|
||||
};
|
||||
}
|
||||
return part;
|
||||
|
||||
case 'file':
|
||||
// Convert mimeType → mediaType
|
||||
if ('mimeType' in p) {
|
||||
const { mimeType, ...rest } = p;
|
||||
return {
|
||||
...rest,
|
||||
mediaType: mimeType,
|
||||
};
|
||||
}
|
||||
return part;
|
||||
|
||||
case 'tool-call':
|
||||
// Tool calls: args → input (AI SDK v5 change)
|
||||
if ('args' in p) {
|
||||
const { args, ...rest } = p;
|
||||
return { ...rest, input: args };
|
||||
}
|
||||
return part;
|
||||
|
||||
default:
|
||||
return part;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tool result parts from v4 to v5 format
|
||||
*/
|
||||
function convertToolResultPart(part: unknown): unknown {
|
||||
if (!part || typeof part !== 'object') {
|
||||
return part;
|
||||
}
|
||||
|
||||
const p = part as Record<string, unknown>;
|
||||
|
||||
if (p.type !== 'tool-result') {
|
||||
return part;
|
||||
}
|
||||
|
||||
// Convert result → structured output
|
||||
if ('result' in p && !('output' in p)) {
|
||||
const { result, experimental_content, isError, ...rest } = p;
|
||||
|
||||
// Convert to v5's structured output format
|
||||
let output: { type: string; value: unknown };
|
||||
if (isError) {
|
||||
// Error results
|
||||
if (typeof result === 'string') {
|
||||
output = { type: 'error-text', value: result };
|
||||
} else {
|
||||
output = { type: 'error-json', value: result };
|
||||
}
|
||||
} else {
|
||||
// Success results
|
||||
if (typeof result === 'string') {
|
||||
output = { type: 'text', value: result };
|
||||
} else {
|
||||
output = { type: 'json', value: result };
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure toolCallId exists and is valid
|
||||
// If it's missing or invalid, preserve the original part
|
||||
if (!rest.toolCallId || typeof rest.toolCallId !== 'string') {
|
||||
return part;
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
// Helper function to get chatId from messageId
|
||||
async function getChatIdFromMessage(messageId: string): Promise<string> {
|
||||
let messageResult: Array<{ chatId: string }>;
|
||||
|
|
Loading…
Reference in New Issue