Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 54 additions & 9 deletions packages/server/src/api/controllers/ai/chatConversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import {
convertToModelMessages,
extractReasoningMiddleware,
generateText,
ModelMessage,
streamText,
wrapLanguageModel,
Expand Down Expand Up @@ -127,6 +128,20 @@ export const findLatestUserQuestion = (chat: ChatConversationRequest) => {
return ""
}

export const getUserMessageTexts = (
messages: ChatConversation["messages"] = []
) =>
messages
.filter(message => message?.role === "user")
.map(message => extractUserText(message))
.filter(text => Boolean(text))

export const shouldRegenerateTitle = (userMessageCount: number, interval = 3) =>
userMessageCount === interval

export const stripThoughtTags = (value: string) =>
value.replace(/<think>[\s\S]*?<\/think>/g, "").trim()

export const truncateTitle = (value: string, maxLength = 120) => {
const trimmed = value.trim()
if (trimmed.length <= maxLength) {
Expand Down Expand Up @@ -268,20 +283,20 @@ export async function agentChatStream(ctx: UserCtx<ChatAgentRequest, void>) {
]
: modelMessages

const result = streamText({
model: wrapLanguageModel({
model,
middleware: extractReasoningMiddleware({
tagName: "think",
}),
const wrappedModel = wrapLanguageModel({
model,
middleware: extractReasoningMiddleware({
tagName: "think",
}),
})

const result = streamText({
model: wrappedModel,
messages: messagesWithContext,
system,
tools,
})

const title = latestQuestion ? truncateTitle(latestQuestion) : chat.title

ctx.respond = false
const messageMetadata =
ragSourcesMetadata && ragSourcesMetadata.length > 0
Expand All @@ -298,12 +313,42 @@ export async function agentChatStream(ctx: UserCtx<ChatAgentRequest, void>) {
const existingChat = chat._id
? await db.tryGet<ChatConversation>(chat._id)
: null
const userMessages = getUserMessageTexts(messages)
const previousTitle = existingChat?.title || chat.title
const firstUserMessage = userMessages[0]
let resolvedTitle = previousTitle

if (userMessages.length > 0 && userMessages.length < 3) {
resolvedTitle = truncateTitle(firstUserMessage)
} else if (shouldRegenerateTitle(userMessages.length)) {
const recentUserMessages = userMessages.slice(-3)
const formattedMessages = recentUserMessages
.map(message => `- ${message}`)
.join("\n")
const titlePrompt = `Previous title: ${previousTitle || "None"}\nRecent user messages:\n${formattedMessages}`
const generatedTitle = await generateText({
model: wrappedModel,
messages: [{ role: "user", content: titlePrompt }],
system:
"Summarize the conversation in 3-6 words. Use title case. Respond with the title only, no quotes, no markdown, and no reasoning.",
})
const sanitizedTitle = truncateTitle(
stripThoughtTags(generatedTitle.text || "")
)
if (sanitizedTitle) {
resolvedTitle = sanitizedTitle
} else if (firstUserMessage) {
resolvedTitle = truncateTitle(firstUserMessage)
}
} else if (!resolvedTitle && firstUserMessage) {
resolvedTitle = truncateTitle(firstUserMessage)
}

const chatToSave = prepareChatConversationForSave({
chatId,
chatAppId,
userId,
title,
title: resolvedTitle,
messages,
chat,
existingChat,
Expand Down
43 changes: 43 additions & 0 deletions packages/server/src/tests/api/chatConversations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type {
import TestConfiguration from "../utilities/TestConfiguration"
import {
findLatestUserQuestion,
getUserMessageTexts,
prepareChatConversationForSave,
shouldRegenerateTitle,
stripThoughtTags,
truncateTitle,
} from "../../api/controllers/ai/chatConversations"

Expand Down Expand Up @@ -237,6 +240,46 @@ describe("chat conversation title helpers", () => {
expect(findLatestUserQuestion(chat)).toBe("latest question")
})

it("collects user message text", () => {
const chat: ChatConversationRequest = {
...baseChat,
messages: [
{
id: "message-1",
role: "assistant",
parts: [{ type: "text", text: "assistant reply" }],
},
{
id: "message-2",
role: "user",
parts: [{ type: "text", text: "first question" }],
},
{
id: "message-3",
role: "user",
parts: [{ type: "text", text: "second question" }],
},
],
}

expect(getUserMessageTexts(chat.messages)).toEqual([
"first question",
"second question",
])
})

it("regenerates titles at the interval", () => {
expect(shouldRegenerateTitle(2)).toBe(false)
expect(shouldRegenerateTitle(3)).toBe(true)
expect(shouldRegenerateTitle(6)).toBe(false)
})

it("strips thought tags", () => {
expect(stripThoughtTags("<think>draft</think>Hello Chat")).toBe(
"Hello Chat"
)
})

it("truncates titles with an ellipsis", () => {
const longMessage = "a".repeat(130)

Expand Down
Loading