Skip to content

Commit 3bfbda6

Browse files
committed
feat: implement rolling request summary persistence in chat route
1 parent 638456a commit 3bfbda6

2 files changed

Lines changed: 102 additions & 4 deletions

File tree

app/api/chat/route.test.mts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ test("chat route uses persisted session summary state before runtime compaction"
5252
);
5353
});
5454

55+
test("chat route persists a rolling request summary before runtime compaction", () => {
56+
const routeSource = readFileSync(
57+
fileURLToPath(new URL("./route.ts", import.meta.url)),
58+
"utf8"
59+
);
60+
61+
assert.match(routeSource, /selectMessagesForPersistentSummary/);
62+
assert.match(routeSource, /buildRollingConversationSummaryPrompt/);
63+
assert.match(
64+
routeSource,
65+
/persistRequestRollingConversationSummaryIfNeeded\([\s\S]*contextMessages[\s\S]*\)/
66+
);
67+
assert.match(routeSource, /context_summary:\s*summary/);
68+
assert.match(routeSource, /\.in\("ui_message_id", summarizedUiMessageIds\)/);
69+
});
70+
5571
test("chat route appends expert-requested skill instructions", () => {
5672
const routeSource = readFileSync(
5773
fileURLToPath(new URL("./route.ts", import.meta.url)),

app/api/chat/route.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import {
1919
import {
2020
appendSavedConversationSummaryContext,
2121
buildConversationSummaryPrompt,
22+
buildRollingConversationSummaryPrompt,
2223
filterSummarizedMessages,
2324
prepareChatModelContext,
25+
selectMessagesForPersistentSummary,
2426
} from "@/lib/botchat/chat-context";
2527
import {
2628
appendChatSkillInstructions,
@@ -29,6 +31,10 @@ import {
2931

3032
export const maxDuration = 30;
3133

34+
type SupabaseServerClient = Awaited<
35+
ReturnType<typeof createSupabaseServerClient>
36+
>;
37+
3238
function isRecord(value: unknown): value is Record<string, unknown> {
3339
return typeof value === "object" && value !== null;
3440
}
@@ -105,6 +111,57 @@ function buildCurrentSystemDateTimeContext() {
105111
Use this as the authoritative current date/time if the user asks about "now", "today", the current date, the current time, or the current time zone.`;
106112
}
107113

114+
async function persistRequestRollingConversationSummaryIfNeeded(
115+
supabase: SupabaseServerClient,
116+
sessionId: string,
117+
previousSummary: string | null,
118+
messages: UIMessage[]
119+
) {
120+
const rowsToSummarize = selectMessagesForPersistentSummary(messages);
121+
if (rowsToSummarize.length === 0) {
122+
return { summary: previousSummary, summarizedUiMessageIds: [] };
123+
}
124+
125+
const { text } = await generateText({
126+
model: openai(getConversationSummaryModelId()),
127+
providerOptions: {
128+
openai: {
129+
reasoningEffort: "none",
130+
},
131+
},
132+
instructions:
133+
"You update a rolling compressed summary of earlier chat history. Preserve facts, decisions, constraints, and unresolved user intent. Do not answer the user.",
134+
prompt: buildRollingConversationSummaryPrompt(
135+
previousSummary,
136+
rowsToSummarize
137+
),
138+
});
139+
140+
const summary = text.trim();
141+
const summarizedAt = new Date().toISOString();
142+
const summarizedUiMessageIds = rowsToSummarize.map((message) => message.id);
143+
144+
const { error: summaryError } = await supabase
145+
.from("chat_sessions")
146+
.update({
147+
context_summary: summary,
148+
context_summary_updated_at: summarizedAt,
149+
})
150+
.eq("id", sessionId);
151+
152+
if (summaryError) throw new Error(summaryError.message);
153+
154+
const { error: markerError } = await supabase
155+
.from("chat_messages")
156+
.update({ summarized_at: summarizedAt })
157+
.eq("session_id", sessionId)
158+
.in("ui_message_id", summarizedUiMessageIds);
159+
160+
if (markerError) throw new Error(markerError.message);
161+
162+
return { summary, summarizedUiMessageIds };
163+
}
164+
108165
export async function POST(request: Request) {
109166
const body = await request.json();
110167
const messages = await Promise.resolve(body.messages);
@@ -124,9 +181,10 @@ export async function POST(request: Request) {
124181
"You are a premium luggage brand assistant. Be concise, confident, and proactive with tasteful product suggestions.";
125182
let contextSummary: string | null = null;
126183
let summarizedUiMessageIds = new Set<string>();
184+
let supabase: SupabaseServerClient | null = null;
127185

128186
try {
129-
const supabase = await createSupabaseServerClient();
187+
supabase = await createSupabaseServerClient();
130188
if (sessionId) {
131189
const [sessionResult, summarizedMessagesResult] = await Promise.all([
132190
supabase
@@ -186,13 +244,37 @@ export async function POST(request: Request) {
186244
system = `${system}\n\n${buildCurrentSystemDateTimeContext()}`;
187245
}
188246

189-
system = appendSavedConversationSummaryContext(system, contextSummary);
190-
191-
const contextMessages = filterSummarizedMessages(
247+
let contextMessages = filterSummarizedMessages(
192248
messages as UIMessage[],
193249
summarizedUiMessageIds
194250
);
195251

252+
if (sessionId && supabase) {
253+
try {
254+
const persistedSummary =
255+
await persistRequestRollingConversationSummaryIfNeeded(
256+
supabase,
257+
sessionId,
258+
contextSummary,
259+
contextMessages
260+
);
261+
262+
contextSummary = persistedSummary.summary;
263+
summarizedUiMessageIds = new Set([
264+
...summarizedUiMessageIds,
265+
...persistedSummary.summarizedUiMessageIds,
266+
]);
267+
contextMessages = filterSummarizedMessages(
268+
messages as UIMessage[],
269+
summarizedUiMessageIds
270+
);
271+
} catch (error) {
272+
console.error("Failed to persist request conversation summary", error);
273+
}
274+
}
275+
276+
system = appendSavedConversationSummaryContext(system, contextSummary);
277+
196278
const preparedContext = await prepareChatModelContext(contextMessages, {
197279
summarizeMessages: async (messagesToSummarize) => {
198280
const { text } = await generateText({

0 commit comments

Comments
 (0)