feat: file upload retry, secure file tokens, UI polish
- Add HMAC-based file token auth for secure AI model file access - Add file upload retry with exponential backoff (max 3 retries) - File endpoint now requires session auth or signed token - Support assistant role messages in chat input - Optimistic UI for attachments on message send - Verify topic ownership before allowing messages - Switch web scraping to Firecrawl API - Agent profile page layout fixes (proper flex overflow) - Add quick switcher (Ctrl+K) to sidenav - Clean up longcat.ts and stale comments
This commit is contained in:
+95
-54
@@ -1,4 +1,4 @@
|
||||
import type { FilePart, ImagePart, ModelMessage } from "ai";
|
||||
import type { AssistantContent, FilePart, ImagePart, JSONValue, ModelMessage, ToolContent } from "ai";
|
||||
import { ToolCallType } from "~~/drizzle/schema";
|
||||
import { Err, Ok, type Result } from "~~/types/result";
|
||||
import type { Message, MessageEntity } from "~/composables/useChat";
|
||||
@@ -37,7 +37,11 @@ export const buildFocusedMessageTree = (messages: Readonly<Message[]>): MessageE
|
||||
return focusedMessageTree;
|
||||
}
|
||||
|
||||
export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[]>): Result<ModelMessage[], string> => {
|
||||
export interface MarshallOptions {
|
||||
signFileUrl?: (fileKey: string) => string;
|
||||
}
|
||||
|
||||
export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[]>, opts?: MarshallOptions): Result<ModelMessage[], string> => {
|
||||
const marshalledMessages: ModelMessage[] = [];
|
||||
|
||||
if (agent && agent.systemPrompt) {
|
||||
@@ -51,47 +55,72 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
|
||||
switch (message.role) {
|
||||
case 'user': {
|
||||
const attachments = message.attachments.map(attachment => {
|
||||
const url = resolveFileUrl(attachment.file.url);
|
||||
let url = resolveFileUrl(attachment.file.url);
|
||||
|
||||
if (opts?.signFileUrl && attachment.file.url.startsWith('/api/files/')) {
|
||||
const fileKey = attachment.file.url.slice('/api/files/'.length);
|
||||
const token = opts.signFileUrl(fileKey);
|
||||
url = `${url}?${token}`;
|
||||
}
|
||||
|
||||
if (attachment.file.mimeType.startsWith('image/')) {
|
||||
return {
|
||||
type: 'image',
|
||||
image: url,
|
||||
image: new URL(url),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'file',
|
||||
data: url,
|
||||
data: new URL(url),
|
||||
filename: attachment.file.name,
|
||||
mediaType: attachment.file.mimeType,
|
||||
};
|
||||
}) as (FilePart | ImagePart)[];
|
||||
|
||||
let messageDate = new Date(message.createdAt);
|
||||
const prompt = `[${messageDate.toDateString()} ${messageDate.toLocaleTimeString()}]: ${message.content!}`;
|
||||
|
||||
marshalledMessages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
content: attachments ? [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[${messageDate.toDateString()} ${messageDate.toLocaleTimeString()}]: ${message.content!}`
|
||||
},
|
||||
...attachments,
|
||||
],
|
||||
] : prompt,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'assistant':
|
||||
let assistantPart: AssistantContent = [];
|
||||
let toolParts: ToolContent = [];
|
||||
|
||||
for (const part of (message.parts || [])) {
|
||||
if (!part) return Err('Part is undefined');
|
||||
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
case 'reasoning': {
|
||||
marshalledMessages.push({
|
||||
role: 'assistant',
|
||||
content: part.content!,
|
||||
});
|
||||
if (toolParts.length > 0) {
|
||||
marshalledMessages.push({
|
||||
role: 'assistant',
|
||||
content: assistantPart,
|
||||
});
|
||||
marshalledMessages.push({
|
||||
role: 'tool',
|
||||
content: toolParts,
|
||||
});
|
||||
toolParts = [];
|
||||
assistantPart = [];
|
||||
}
|
||||
|
||||
if (part.providerOptions || part.content) assistantPart.push({
|
||||
type: part.type,
|
||||
text: part.content || '',
|
||||
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : undefined,
|
||||
})
|
||||
break;
|
||||
}
|
||||
case 'tool-call': {
|
||||
@@ -101,33 +130,32 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
|
||||
return Err('Marshalling tool call that is still pending. This is likely a UI bug if this happens.');
|
||||
}
|
||||
|
||||
let inputValue: string = '';
|
||||
let inputValue: string | object = '';
|
||||
|
||||
switch (part.toolCall.input!.type) {
|
||||
case ToolCallType.Text:
|
||||
inputValue = part.toolCall.input!.value;
|
||||
break;
|
||||
case ToolCallType.Json:
|
||||
inputValue = JSON.stringify(part.toolCall.input!.value);
|
||||
if (typeof part.toolCall.input!.value === 'string') {
|
||||
inputValue = JSON.parse(part.toolCall.input!.value);
|
||||
} else {
|
||||
inputValue = part.toolCall.input!.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
marshalledMessages.push({
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolCallId: part.toolCall.id,
|
||||
toolName: part.toolCall.toolName,
|
||||
input: inputValue,
|
||||
},
|
||||
],
|
||||
assistantPart.push({
|
||||
type: 'tool-call',
|
||||
toolCallId: part.toolCall.id!,
|
||||
toolName: part.toolCall.toolName!,
|
||||
input: inputValue,
|
||||
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : undefined,
|
||||
});
|
||||
})
|
||||
|
||||
if (part.toolCall.status === 'failed') {
|
||||
let failureType: 'error-text' | 'error-json';
|
||||
let failureValue: string;
|
||||
let failureValue: string | JSONValue;
|
||||
|
||||
if (part.toolCall.error === null || part.toolCall.error === undefined) {
|
||||
failureType = 'error-text';
|
||||
@@ -140,27 +168,32 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
|
||||
break;
|
||||
case ToolCallType.Json:
|
||||
failureType = 'error-json';
|
||||
failureValue = JSON.stringify(part.toolCall.error!.value);
|
||||
if (typeof part.toolCall.error!.value === 'string') {
|
||||
failureValue = JSON.parse(part.toolCall.error!.value);
|
||||
} else {
|
||||
failureValue = part.toolCall.error!.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
failureType = 'error-json';
|
||||
failureValue = JSON.stringify(part.toolCall.error!.value);
|
||||
// failureValue = JSON.stringify(part.toolCall.error!.value);
|
||||
if (typeof part.toolCall.error!.value === 'string') {
|
||||
failureValue = JSON.parse(part.toolCall.error!.value);
|
||||
} else {
|
||||
failureValue = part.toolCall.error!.value;
|
||||
}
|
||||
}
|
||||
|
||||
marshalledMessages.push({
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: part.toolCall.id,
|
||||
toolName: part.toolCall.toolName,
|
||||
output: {
|
||||
type: failureType,
|
||||
value: failureValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
toolParts.push({
|
||||
type: 'tool-result',
|
||||
toolCallId: part.toolCall.id!,
|
||||
toolName: part.toolCall.toolName!,
|
||||
// @ts-expect-error - This is a type error, because typescript cant provie that the value must be a string when the type is error-text
|
||||
output: {
|
||||
type: failureType,
|
||||
value: failureValue,
|
||||
},
|
||||
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : undefined,
|
||||
});
|
||||
break;
|
||||
@@ -177,23 +210,22 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
|
||||
break;
|
||||
case ToolCallType.Json:
|
||||
outputType = 'json';
|
||||
outputValue = JSON.stringify(part.toolCall.output!.value);
|
||||
if (typeof part.toolCall.output!.value === 'string') {
|
||||
outputValue = JSON.parse(part.toolCall.output!.value);
|
||||
} else {
|
||||
outputValue = part.toolCall.output!.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
marshalledMessages.push({
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: part.toolCall.id,
|
||||
toolName: part.toolCall.toolName,
|
||||
output: {
|
||||
type: outputType,
|
||||
value: outputValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
toolParts.push({
|
||||
type: 'tool-result',
|
||||
toolCallId: part.toolCall.id!,
|
||||
toolName: part.toolCall.toolName!,
|
||||
output: {
|
||||
type: outputType,
|
||||
value: outputValue,
|
||||
},
|
||||
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : undefined,
|
||||
});
|
||||
break;
|
||||
@@ -203,6 +235,15 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
|
||||
return Err(`Unknown part type: ${part.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (assistantPart.length > 0) marshalledMessages.push({
|
||||
role: 'assistant',
|
||||
content: assistantPart,
|
||||
});
|
||||
if (toolParts.length > 0) marshalledMessages.push({
|
||||
role: 'tool',
|
||||
content: toolParts,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return Err(`Unknown message role: ${message.role}`);
|
||||
|
||||
Reference in New Issue
Block a user