Chat title generation

This commit is contained in:
2026-02-14 21:27:44 -08:00
parent 7ef2825c16
commit 684d441763
4 changed files with 144 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import { env } from "./env.js";
import { runMultiplex } from "./llm/multiplexer.js";
import { runMultiplexStream } from "./llm/streaming.js";
import { getModelCatalogSnapshot } from "./llm/model-catalog.js";
import { openaiClient } from "./llm/providers.js";
import { exaClient } from "./search/exa.js";
type IncomingChatMessage = {
@@ -100,6 +101,33 @@ function parseAnswerText(answerResponse: any) {
return null;
}
function normalizeSuggestedTitle(raw: string, fallback: string) {
const oneLine = raw
.replace(/\r?\n+/g, " ")
.replace(/^['"`\s]+|['"`\s]+$/g, "")
.replace(/\s+/g, " ")
.trim();
const fromRaw = oneLine || fallback;
const words = fromRaw.split(/\s+/).filter(Boolean);
return words.slice(0, 4).join(" ").slice(0, 64).trim() || fallback;
}
async function generateChatTitle(content: string) {
const systemPrompt =
"You create short chat titles. Return exactly one line, maximum 4 words, no quotes, no trailing punctuation.";
const userPrompt = `User request:\n${content}\n\nTitle:`;
const response = await openaiClient().chat.completions.create({
model: "gpt-4.1-mini",
temperature: 0,
max_completion_tokens: 20,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
});
return response.choices?.[0]?.message?.content ?? "";
}
function normalizeUrlForMatch(input: string | null | undefined) {
if (!input) return "";
try {
@@ -159,6 +187,56 @@ export async function registerRoutes(app: FastifyInstance) {
return { chat };
});
app.patch("/v1/chats/:chatId", async (req) => {
requireAdmin(req);
const Params = z.object({ chatId: z.string() });
const Body = z.object({ title: z.string().trim().min(1) });
const { chatId } = Params.parse(req.params);
const body = Body.parse(req.body ?? {});
const updated = await prisma.chat.updateMany({
where: { id: chatId },
data: { title: body.title },
});
if (updated.count === 0) return app.httpErrors.notFound("chat not found");
const chat = await prisma.chat.findUnique({
where: { id: chatId },
select: { id: true, title: true, createdAt: true, updatedAt: true },
});
if (!chat) return app.httpErrors.notFound("chat not found");
return { chat };
});
app.post("/v1/chats/title/suggest", async (req) => {
requireAdmin(req);
const Body = z.object({
chatId: z.string(),
content: z.string().trim().min(1),
});
const body = Body.parse(req.body ?? {});
const existing = await prisma.chat.findUnique({
where: { id: body.chatId },
select: { id: true, title: true, createdAt: true, updatedAt: true },
});
if (!existing) return app.httpErrors.notFound("chat not found");
if (existing.title?.trim()) return { chat: existing };
const fallback = body.content.split(/\r?\n/)[0]?.trim().slice(0, 48) || "New chat";
const suggestedRaw = await generateChatTitle(body.content);
const title = normalizeSuggestedTitle(suggestedRaw, fallback);
const chat = await prisma.chat.update({
where: { id: body.chatId },
data: { title },
select: { id: true, title: true, createdAt: true, updatedAt: true },
});
return { chat };
});
app.delete("/v1/chats/:chatId", async (req) => {
requireAdmin(req);
const Params = z.object({ chatId: z.string() });