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
|
// AI package imports
|
||||||
import { type AnalystWorkflowInput, runAnalystWorkflow } from '@buster/ai';
|
import { type AnalystWorkflowInput, runAnalystWorkflow } from '@buster/ai';
|
||||||
|
|
||||||
|
import type { ModelMessage } from 'ai';
|
||||||
import type { messagePostProcessingTask } from '../message-post-processing/message-post-processing';
|
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);
|
logPerformanceMetrics('post-data-load', payload.message_id, taskStartTime, resourceTracker);
|
||||||
|
|
||||||
// Task 4: Prepare workflow input with conversation history
|
// Task 4: Prepare workflow input with conversation history
|
||||||
// Convert conversation history to messages format expected by the workflow
|
// The conversation history from getChatConversationHistory is already in ModelMessage[] format
|
||||||
const messages =
|
const modelMessages: ModelMessage[] =
|
||||||
conversationHistory.length > 0
|
conversationHistory.length > 0
|
||||||
? conversationHistory
|
? conversationHistory
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
role: 'user' as const,
|
role: 'user',
|
||||||
|
// v5 supports string content directly for user messages
|
||||||
content: messageContext.requestMessage,
|
content: messageContext.requestMessage,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const workflowInput: AnalystWorkflowInput = {
|
const workflowInput: AnalystWorkflowInput = {
|
||||||
messages,
|
messages: modelMessages,
|
||||||
messageId: payload.message_id,
|
messageId: payload.message_id,
|
||||||
chatId: messageContext.chatId,
|
chatId: messageContext.chatId,
|
||||||
userId: messageContext.userId,
|
userId: messageContext.userId,
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
// Tool repair utilities
|
// Tool repair utilities
|
||||||
export * from './tool-call-repair';
|
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)
|
* Convert messages from old CoreMessage format (v4) to ModelMessage format (v5)
|
||||||
* Main changes: tool calls 'args' → 'input', tool results 'result' → 'output'
|
* Key changes:
|
||||||
*
|
* - Tool calls: 'args' → 'input'
|
||||||
* Since we're dealing with unknown data from the database, we cast to ModelMessage[]
|
* - Tool results: 'result' → structured 'output' object
|
||||||
* and let the runtime handle any actual format differences.
|
* - Image/File parts: 'mimeType' → 'mediaType'
|
||||||
|
* - User/Assistant string content remains as string (v5 supports both)
|
||||||
*/
|
*/
|
||||||
export function convertCoreToModel(messages: unknown): ModelMessage[] {
|
export function convertCoreToModel(messages: unknown): ModelMessage[] {
|
||||||
if (!Array.isArray(messages)) {
|
if (!Array.isArray(messages)) {
|
||||||
|
@ -23,51 +24,163 @@ export function convertCoreToModel(messages: unknown): ModelMessage[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = message as Record<string, unknown>;
|
const msg = message as Record<string, unknown>;
|
||||||
|
const { role, content } = msg;
|
||||||
|
|
||||||
// For assistant messages, update tool call args → input
|
switch (role) {
|
||||||
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
case 'system':
|
||||||
return {
|
// System messages remain string-based in both v4 and v5
|
||||||
...msg,
|
return {
|
||||||
content: msg.content.map((part: unknown) => {
|
...msg,
|
||||||
if (
|
content: typeof content === 'string' ? content : '',
|
||||||
part &&
|
} as ModelMessage;
|
||||||
typeof part === 'object' &&
|
|
||||||
'type' in part &&
|
case 'user':
|
||||||
part.type === 'tool-call' &&
|
// User messages: handle both string and array content
|
||||||
'args' in part
|
if (typeof content === 'string') {
|
||||||
) {
|
// v5 supports string content directly for user messages
|
||||||
const { args, ...rest } = part as Record<string, unknown>;
|
return msg as ModelMessage;
|
||||||
return { ...rest, input: args };
|
}
|
||||||
}
|
if (Array.isArray(content)) {
|
||||||
return part;
|
// Convert any image/file parts
|
||||||
}),
|
return {
|
||||||
} as ModelMessage;
|
...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
|
// Helper function to get chatId from messageId
|
||||||
async function getChatIdFromMessage(messageId: string): Promise<string> {
|
async function getChatIdFromMessage(messageId: string): Promise<string> {
|
||||||
let messageResult: Array<{ chatId: string }>;
|
let messageResult: Array<{ chatId: string }>;
|
||||||
|
|
Loading…
Reference in New Issue