add chat flow for search results
This commit is contained in:
@@ -108,6 +108,13 @@ function mapSearchResultPreview(result: any, index: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function truncateContextPart(value: string | null | undefined, maxLength: number) {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.length <= maxLength) return trimmed;
|
||||
return `${trimmed.slice(0, maxLength - 1).trimEnd()}...`;
|
||||
}
|
||||
|
||||
function parseAnswerText(answerResponse: any) {
|
||||
if (typeof answerResponse?.answer === "string") return answerResponse.answer;
|
||||
if (answerResponse?.answer) return JSON.stringify(answerResponse.answer, null, 2);
|
||||
@@ -153,6 +160,57 @@ function normalizeUrlForMatch(input: string | null | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildSearchChatContext(search: any) {
|
||||
const query = truncateContextPart(search.query, 500) ?? truncateContextPart(search.title, 500) ?? "Untitled search";
|
||||
const lines: string[] = [
|
||||
"You are Sybil. The user started this chat from a saved web search. Use the search answer and result context below when answering follow-up questions. If the context is insufficient, say so and use available tools when appropriate.",
|
||||
"",
|
||||
`Search query: ${query}`,
|
||||
];
|
||||
|
||||
const answer = truncateContextPart(search.answerText, 6000);
|
||||
if (answer) {
|
||||
lines.push("", "Search answer:", answer);
|
||||
}
|
||||
|
||||
if (Array.isArray(search.answerCitations) && search.answerCitations.length) {
|
||||
lines.push("", "Answer citations:");
|
||||
for (const [index, citation] of search.answerCitations.slice(0, 8).entries()) {
|
||||
const title = truncateContextPart(citation?.title, 160);
|
||||
const url = truncateContextPart(citation?.url ?? citation?.id, 400);
|
||||
if (title || url) {
|
||||
lines.push(`${index + 1}. ${[title, url].filter(Boolean).join(" - ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(search.results) && search.results.length) {
|
||||
lines.push("", "Search results:");
|
||||
for (const result of search.results.slice(0, 10)) {
|
||||
const title = truncateContextPart(result.title, 180) ?? result.url;
|
||||
const url = truncateContextPart(result.url, 500);
|
||||
const published = truncateContextPart(result.publishedDate, 80);
|
||||
const author = truncateContextPart(result.author, 120);
|
||||
const text = truncateContextPart(result.text, 1000);
|
||||
const highlights = Array.isArray(result.highlights)
|
||||
? result.highlights
|
||||
.map((highlight: unknown) => truncateContextPart(typeof highlight === "string" ? highlight : null, 360))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
lines.push(`${result.rank + 1}. ${title}`);
|
||||
if (url) lines.push(` URL: ${url}`);
|
||||
if (published || author) lines.push(` Source detail: ${[published, author].filter(Boolean).join(" - ")}`);
|
||||
if (text) lines.push(` Text: ${text}`);
|
||||
for (const highlight of highlights.slice(0, 2)) {
|
||||
lines.push(` Highlight: ${highlight}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSseHeaders(originHeader: string | undefined) {
|
||||
const origin = originHeader && originHeader !== "null" ? originHeader : "*";
|
||||
const headers: Record<string, string> = {
|
||||
@@ -370,6 +428,54 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
return { search };
|
||||
});
|
||||
|
||||
app.post("/v1/searches/:searchId/chat", async (req) => {
|
||||
requireAdmin(req);
|
||||
const Params = z.object({ searchId: z.string() });
|
||||
const Body = z.object({ title: z.string().optional() });
|
||||
const { searchId } = Params.parse(req.params);
|
||||
const body = Body.parse(req.body ?? {});
|
||||
|
||||
const search = await prisma.search.findUnique({
|
||||
where: { id: searchId },
|
||||
include: { results: { orderBy: { rank: "asc" } } },
|
||||
});
|
||||
if (!search) return app.httpErrors.notFound("search not found");
|
||||
|
||||
const fallbackTitle = search.query?.trim() || search.title?.trim() || "Search results";
|
||||
const title = body.title?.trim() || `Search: ${fallbackTitle.slice(0, 72)}`;
|
||||
const context = buildSearchChatContext(search);
|
||||
|
||||
const chat = await prisma.chat.create({
|
||||
data: {
|
||||
title,
|
||||
messages: {
|
||||
create: {
|
||||
role: "system" as any,
|
||||
content: context,
|
||||
metadata: {
|
||||
kind: "search_context",
|
||||
searchId: search.id,
|
||||
query: search.query,
|
||||
resultCount: search.results.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
initiatedProvider: true,
|
||||
initiatedModel: true,
|
||||
lastUsedProvider: true,
|
||||
lastUsedModel: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { chat };
|
||||
});
|
||||
|
||||
app.post("/v1/searches/:searchId/run", async (req) => {
|
||||
requireAdmin(req);
|
||||
const Params = z.object({ searchId: z.string() });
|
||||
|
||||
Reference in New Issue
Block a user