115 lines
4.4 KiB
TypeScript
115 lines
4.4 KiB
TypeScript
import type { StatelessChatRequest } from '@/lib/types/chat';
|
|
|
|
// ==================== Message Conversion ====================
|
|
|
|
/**
|
|
* Convert UI messages to OpenAI format
|
|
* Includes tool call information so the model knows what actions were taken
|
|
*/
|
|
export function convertMessagesToOpenAI(
|
|
messages: StatelessChatRequest['messages'],
|
|
currentAgentId?: string,
|
|
): Array<{ role: 'system' | 'user' | 'assistant'; content: string }> {
|
|
return messages
|
|
.filter((msg) => msg.role === 'user' || msg.role === 'assistant')
|
|
.map((msg) => {
|
|
if (msg.role === 'assistant') {
|
|
// Assistant messages use JSON array format to serve as few-shot examples
|
|
// that match the expected output format from the system prompt
|
|
const items: Array<{ type: string; [key: string]: string }> = [];
|
|
|
|
if (msg.parts) {
|
|
for (const part of msg.parts) {
|
|
const p = part as Record<string, unknown>;
|
|
|
|
if (p.type === 'text' && p.text) {
|
|
items.push({ type: 'text', content: p.text as string });
|
|
} else if ((p.type as string)?.startsWith('action-') && p.state === 'result') {
|
|
const actionName = (p.actionName ||
|
|
(p.type as string).replace('action-', '')) as string;
|
|
const output = p.output as Record<string, unknown> | undefined;
|
|
const isSuccess = output?.success === true;
|
|
const resultSummary = isSuccess
|
|
? output?.data
|
|
? `result: ${JSON.stringify(output.data).slice(0, 100)}`
|
|
: 'success'
|
|
: (output?.error as string) || 'failed';
|
|
items.push({
|
|
type: 'action',
|
|
name: actionName,
|
|
result: resultSummary,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const content = items.length > 0 ? JSON.stringify(items) : '';
|
|
const msgAgentId = msg.metadata?.agentId;
|
|
|
|
// When currentAgentId is provided and this message is from a DIFFERENT agent,
|
|
// convert to user role with agent name attribution
|
|
if (currentAgentId && msgAgentId && msgAgentId !== currentAgentId) {
|
|
const agentName = msg.metadata?.senderName || msgAgentId;
|
|
return {
|
|
role: 'user' as const,
|
|
content: content ? `[${agentName}]: ${content}` : '',
|
|
};
|
|
}
|
|
|
|
return {
|
|
role: 'assistant' as const,
|
|
content,
|
|
};
|
|
}
|
|
|
|
// User messages: keep plain text concatenation
|
|
const contentParts: string[] = [];
|
|
|
|
if (msg.parts) {
|
|
for (const part of msg.parts) {
|
|
const p = part as Record<string, unknown>;
|
|
|
|
if (p.type === 'text' && p.text) {
|
|
contentParts.push(p.text as string);
|
|
} else if ((p.type as string)?.startsWith('action-') && p.state === 'result') {
|
|
const actionName = (p.actionName ||
|
|
(p.type as string).replace('action-', '')) as string;
|
|
const output = p.output as Record<string, unknown> | undefined;
|
|
const isSuccess = output?.success === true;
|
|
const resultSummary = isSuccess
|
|
? output?.data
|
|
? `result: ${JSON.stringify(output.data).slice(0, 100)}`
|
|
: 'success'
|
|
: (output?.error as string) || 'failed';
|
|
contentParts.push(`[Action ${actionName}: ${resultSummary}]`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract speaker name from metadata (e.g. other agents' messages in discussion)
|
|
const senderName = msg.metadata?.senderName;
|
|
let content = contentParts.join('\n');
|
|
if (senderName) {
|
|
content = `[${senderName}]: ${content}`;
|
|
}
|
|
|
|
// Annotate interrupted messages so the LLM knows context was cut short
|
|
const isInterrupted =
|
|
(msg as unknown as Record<string, unknown>).metadata &&
|
|
((msg as unknown as Record<string, unknown>).metadata as Record<string, unknown>)
|
|
?.interrupted;
|
|
return {
|
|
role: 'user' as const,
|
|
content: isInterrupted
|
|
? `${content}\n[This response was interrupted — do NOT continue it. Start a new JSON array response.]`
|
|
: content,
|
|
};
|
|
})
|
|
.filter((msg) => {
|
|
// Drop empty messages and messages with only dots/ellipsis/whitespace
|
|
// (produced by failed agent streams)
|
|
const stripped = msg.content.replace(/[.\s…]+/g, '');
|
|
return stripped.length > 0;
|
|
});
|
|
}
|