feat: custom tool renderers
This commit is contained in:
@@ -13,6 +13,8 @@ import { buildFocusedMessageTree, buildMessageTree, marshallMessages } from "~~/
|
||||
import path from "path";
|
||||
import { isRerankingProvider } from "~~/server/utils/ai-provider";
|
||||
import { generateFileToken } from "~~/server/utils/file-token";
|
||||
import { serializeToolError } from "~~/utils/tool-errors";
|
||||
import { readFirecrawlResponse } from "~~/server/utils/firecrawl";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await protectRoute(event);
|
||||
@@ -490,11 +492,16 @@ const fetchUrlTool = tool({
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
formats: ["markdown"],
|
||||
}),
|
||||
});
|
||||
const content = await response.json();
|
||||
const result = await readFirecrawlResponse(response);
|
||||
if (!result.ok) {
|
||||
// The SDK needs a rejected execution to emit a tool-error, not a successful result.
|
||||
return Promise.reject(new Error(result.error));
|
||||
}
|
||||
return {
|
||||
content: content.data.markdown,
|
||||
content: result.data,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -608,6 +615,7 @@ async function generateResponse(
|
||||
|
||||
// Mark failed tool calls
|
||||
await Promise.all([...activeToolCalls].map(async id => {
|
||||
const lastUpdatedAt = new Date();
|
||||
await db.update(toolCalls).set({
|
||||
status: 'failed',
|
||||
error: { type: ToolCallType.Text, value: 'Generation failed' }
|
||||
@@ -619,10 +627,12 @@ async function generateResponse(
|
||||
messageId: message.id,
|
||||
toolCallId: id,
|
||||
toolName: null,
|
||||
status: 'failed',
|
||||
error: {
|
||||
type: ToolCallType.Text,
|
||||
value: 'Generation failed'
|
||||
}
|
||||
},
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
}));
|
||||
@@ -891,8 +901,6 @@ async function generateResponse(
|
||||
})
|
||||
|
||||
activeToolCalls.add(dbToolCallId);
|
||||
|
||||
activeParts.set(key, { id: part.id, accumulatedContent: '' });
|
||||
} break;
|
||||
case 'tool-call': {
|
||||
let inputType: ToolCallType = ToolCallType.Text;
|
||||
@@ -997,8 +1005,6 @@ async function generateResponse(
|
||||
})
|
||||
|
||||
activeToolCalls.add(dbToolCallId);
|
||||
|
||||
activeParts.set(key, { id: part.id, accumulatedContent: '' });
|
||||
}
|
||||
} break;
|
||||
case 'tool-result': {
|
||||
@@ -1028,6 +1034,7 @@ async function generateResponse(
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, dbToolCallId));
|
||||
|
||||
const lastUpdatedAt = new Date();
|
||||
await topicEvents.emit(topicId, {
|
||||
type: 'tool-call-delta',
|
||||
payload: {
|
||||
@@ -1039,6 +1046,7 @@ async function generateResponse(
|
||||
value: outputValue,
|
||||
},
|
||||
status: 'failed',
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1048,6 +1056,7 @@ async function generateResponse(
|
||||
}
|
||||
|
||||
if (dbToolCallId) {
|
||||
const lastUpdatedAt = new Date();
|
||||
await db.update(toolCalls).set({
|
||||
status: 'completed',
|
||||
output: {
|
||||
@@ -1055,9 +1064,11 @@ async function generateResponse(
|
||||
value: outputValue,
|
||||
},
|
||||
}).where(eq(toolCalls.id, dbToolCallId));
|
||||
if (token.providerMetadata) await db.update(messageParts)
|
||||
await db.update(messageParts)
|
||||
.set({
|
||||
providerOptions: token.providerMetadata,
|
||||
finished: true,
|
||||
lastUpdatedAt,
|
||||
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, dbToolCallId));
|
||||
|
||||
@@ -1072,6 +1083,7 @@ async function generateResponse(
|
||||
value: outputValue,
|
||||
},
|
||||
status: 'completed',
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1092,8 +1104,8 @@ async function generateResponse(
|
||||
outputValue = token.error;
|
||||
break;
|
||||
case 'object':
|
||||
outputType = ToolCallType.Json;
|
||||
outputValue = JSON.stringify(token.error);
|
||||
outputType = token.error instanceof Error ? ToolCallType.Text : ToolCallType.Json;
|
||||
outputValue = serializeToolError(token.error);
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown error type', token.error);
|
||||
@@ -1104,6 +1116,7 @@ async function generateResponse(
|
||||
|
||||
const existingDbToolCallId = nativeToDbToolCallId.get(token.toolCallId);
|
||||
if (existingDbToolCallId && activeToolCalls.has(existingDbToolCallId)) {
|
||||
const lastUpdatedAt = new Date();
|
||||
await db.update(toolCalls).set({
|
||||
status: 'failed',
|
||||
error: {
|
||||
@@ -1111,9 +1124,11 @@ async function generateResponse(
|
||||
value: outputValue as string,
|
||||
}
|
||||
}).where(eq(toolCalls.id, existingDbToolCallId));
|
||||
if (token.providerMetadata) await db.update(messageParts)
|
||||
await db.update(messageParts)
|
||||
.set({
|
||||
providerOptions: token.providerMetadata,
|
||||
finished: true,
|
||||
lastUpdatedAt,
|
||||
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
|
||||
})
|
||||
.where(eq(messageParts.toolCallId, existingDbToolCallId));
|
||||
|
||||
@@ -1123,10 +1138,12 @@ async function generateResponse(
|
||||
messageId: message.id,
|
||||
toolCallId: existingDbToolCallId,
|
||||
toolName: token.toolName,
|
||||
status: 'failed',
|
||||
error: {
|
||||
type: outputType as ToolCallType,
|
||||
value: outputValue as string,
|
||||
},
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -1176,11 +1193,9 @@ async function generateResponse(
|
||||
type: 'tool-call-start',
|
||||
payload: {
|
||||
messageId: message.id,
|
||||
part,
|
||||
part: { ...part, toolCall },
|
||||
}
|
||||
})
|
||||
|
||||
activeParts.set(key, { id: part.id, accumulatedContent: '' });
|
||||
}
|
||||
|
||||
if (existingDbToolCallId) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Result } from "../../types/result";
|
||||
import { z } from "zod";
|
||||
import { Err, Ok } from "../../types/result";
|
||||
|
||||
const scrapeResponseSchema = z.object({
|
||||
success: z.boolean().optional(),
|
||||
error: z.union([z.string(), z.object({ message: z.string() })]).optional(),
|
||||
message: z.string().optional(),
|
||||
data: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export const readFirecrawlResponse = async (response: Response): Promise<Result<string, string>> => {
|
||||
const context = `Firecrawl scrape (HTTP ${response.status})`;
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
return Err(`${context}: received a non-JSON or unreadable response.`);
|
||||
}
|
||||
|
||||
const parsed = scrapeResponseSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return Err(`${context}: received an unexpected response format.`);
|
||||
}
|
||||
|
||||
const payload = parsed.data;
|
||||
const providerError = typeof payload.error === "string" ? payload.error : payload.error?.message;
|
||||
if (!response.ok || payload.success === false || providerError) {
|
||||
return Err(`${context}: ${providerError || payload.message || response.statusText || "Scrape failed without an error message."}`);
|
||||
}
|
||||
const content = z.object({ markdown: z.string() }).safeParse(payload.data);
|
||||
if (!content.success || content.data.markdown.trim() === "") {
|
||||
return Err(`${context}: no Markdown content was returned for this URL.`);
|
||||
}
|
||||
return Ok(content.data.markdown);
|
||||
};
|
||||
Reference in New Issue
Block a user