Chat title generation
This commit is contained in:
@@ -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() });
|
||||
|
||||
Reference in New Issue
Block a user