mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-12 10:04:45 +08:00
feat(threads): paginate full history via summaries endpoint (#1022)
* feat(threads): add paginated summaries API and load full history * fix(threads): address summaries review feedback - validate summaries sort params and log gateway failures - page frontend thread summaries without stale query keys or silent truncation - export router modules and tighten thread list typing Refs: 2901804166, 2901804176, 2901804179, 2901804180, 2901804183, 2901804187, 2901804191 --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
|
||||
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
@@ -8,6 +7,7 @@ import { toast } from "sonner";
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
|
||||
import { getAPIClient } from "../api";
|
||||
import { getBackendBaseURL } from "../config";
|
||||
import { useI18n } from "../i18n/hooks";
|
||||
import type { FileInMessage } from "../messages/utils";
|
||||
import type { LocalSettings } from "../settings";
|
||||
@@ -15,7 +15,9 @@ import { useUpdateSubtask } from "../tasks/context";
|
||||
import type { UploadedFileInfo } from "../uploads";
|
||||
import { uploadFiles } from "../uploads";
|
||||
|
||||
import type { AgentThread, AgentThreadState } from "./types";
|
||||
import type { AgentThreadState, ThreadListItem } from "./types";
|
||||
|
||||
const THREADS_LIST_QUERY_KEY = ["threads", "search"] as const;
|
||||
|
||||
export type ToolEndEvent = {
|
||||
name: string;
|
||||
@@ -110,10 +112,10 @@ export function useThreadStream({
|
||||
if (update && "title" in update && update.title) {
|
||||
void queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: ["threads", "search"],
|
||||
queryKey: THREADS_LIST_QUERY_KEY,
|
||||
exact: false,
|
||||
},
|
||||
(oldData: Array<AgentThread> | undefined) => {
|
||||
(oldData: Array<ThreadListItem> | undefined) => {
|
||||
return oldData?.map((t) => {
|
||||
if (t.thread_id === threadIdRef.current) {
|
||||
return {
|
||||
@@ -148,7 +150,7 @@ export function useThreadStream({
|
||||
},
|
||||
onFinish(state) {
|
||||
listeners.current.onFinish?.(state.values);
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
void queryClient.invalidateQueries({ queryKey: THREADS_LIST_QUERY_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -338,7 +340,7 @@ export function useThreadStream({
|
||||
},
|
||||
},
|
||||
);
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
void queryClient.invalidateQueries({ queryKey: THREADS_LIST_QUERY_KEY });
|
||||
} catch (error) {
|
||||
setOptimisticMessages([]);
|
||||
throw error;
|
||||
@@ -359,20 +361,90 @@ export function useThreadStream({
|
||||
return [mergedThread, sendMessage] as const;
|
||||
}
|
||||
|
||||
|
||||
type ThreadSummaryApiResponse = {
|
||||
threads: ThreadListItem[];
|
||||
next_offset: number | null;
|
||||
};
|
||||
|
||||
type ThreadListQueryParams = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: "updated_at" | "created_at";
|
||||
sortOrder?: "asc" | "desc";
|
||||
};
|
||||
|
||||
async function fetchThreadSummariesPage(
|
||||
params: Required<ThreadListQueryParams>,
|
||||
): Promise<ThreadSummaryApiResponse> {
|
||||
const baseURL = getBackendBaseURL();
|
||||
const url = new URL(`${baseURL}/api/threads/summaries`,
|
||||
typeof window !== "undefined" ? window.location.origin : "http://localhost:2026",
|
||||
);
|
||||
url.searchParams.set("limit", String(params.limit));
|
||||
url.searchParams.set("offset", String(params.offset));
|
||||
url.searchParams.set("sort_by", params.sortBy);
|
||||
url.searchParams.set("sort_order", params.sortOrder);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch thread summaries: ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as ThreadSummaryApiResponse;
|
||||
}
|
||||
|
||||
export function useThreads(
|
||||
params: Parameters<ThreadsClient["search"]>[0] = {
|
||||
params: ThreadListQueryParams = {
|
||||
limit: 50,
|
||||
sortBy: "updated_at",
|
||||
sortOrder: "desc",
|
||||
select: ["thread_id", "updated_at", "values"],
|
||||
},
|
||||
) {
|
||||
const apiClient = getAPIClient();
|
||||
return useQuery<AgentThread[]>({
|
||||
queryKey: ["threads", "search", params],
|
||||
const pageSize = params.limit ?? 50;
|
||||
const initialOffset = params.offset ?? 0;
|
||||
const sortBy = params.sortBy ?? "updated_at";
|
||||
const sortOrder = params.sortOrder ?? "desc";
|
||||
|
||||
return useQuery<ThreadListItem[]>({
|
||||
queryKey: [...THREADS_LIST_QUERY_KEY, pageSize, initialOffset, sortBy, sortOrder],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.threads.search<AgentThreadState>(params);
|
||||
return response as AgentThread[];
|
||||
const allThreads: ThreadListItem[] = [];
|
||||
let offset = initialOffset;
|
||||
const seenOffsets = new Set<number>();
|
||||
|
||||
while (true) {
|
||||
if (seenOffsets.has(offset)) {
|
||||
throw new Error(`Detected repeated thread summaries offset: ${offset}`);
|
||||
}
|
||||
seenOffsets.add(offset);
|
||||
|
||||
const page = await fetchThreadSummariesPage({
|
||||
limit: pageSize,
|
||||
offset,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
if (!Array.isArray(page.threads) || page.threads.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allThreads.push(...page.threads);
|
||||
|
||||
if (page.next_offset == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (page.next_offset <= offset) {
|
||||
throw new Error(
|
||||
`Thread summaries pagination did not advance: ${page.next_offset}`,
|
||||
);
|
||||
}
|
||||
|
||||
offset = page.next_offset;
|
||||
}
|
||||
|
||||
return allThreads;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
@@ -388,11 +460,11 @@ export function useDeleteThread() {
|
||||
onSuccess(_, { threadId }) {
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: ["threads", "search"],
|
||||
queryKey: THREADS_LIST_QUERY_KEY,
|
||||
exact: false,
|
||||
},
|
||||
(oldData: Array<AgentThread>) => {
|
||||
return oldData.filter((t) => t.thread_id !== threadId);
|
||||
(oldData: Array<ThreadListItem> | undefined) => {
|
||||
return oldData?.filter((t) => t.thread_id !== threadId) ?? oldData;
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -417,11 +489,11 @@ export function useRenameThread() {
|
||||
onSuccess(_, { threadId, title }) {
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: ["threads", "search"],
|
||||
queryKey: THREADS_LIST_QUERY_KEY,
|
||||
exact: false,
|
||||
},
|
||||
(oldData: Array<AgentThread>) => {
|
||||
return oldData.map((t) => {
|
||||
(oldData: Array<ThreadListItem> | undefined) => {
|
||||
return oldData?.map((t) => {
|
||||
if (t.thread_id === threadId) {
|
||||
return {
|
||||
...t,
|
||||
@@ -432,7 +504,7 @@ export function useRenameThread() {
|
||||
};
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}) ?? oldData;
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface AgentThreadState extends Record<string, unknown> {
|
||||
|
||||
export interface AgentThread extends Thread<AgentThreadState> {}
|
||||
|
||||
export type ThreadListItem = Pick<AgentThread, "thread_id" | "updated_at"> & {
|
||||
values: Pick<AgentThreadState, "title">;
|
||||
};
|
||||
|
||||
export interface AgentThreadContext extends Record<string, unknown> {
|
||||
thread_id: string;
|
||||
model_name: string | undefined;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
import type { AgentThread } from "./types";
|
||||
|
||||
export function pathOfThread(threadId: string) {
|
||||
return `/workspace/chats/${threadId}`;
|
||||
@@ -19,6 +18,6 @@ export function textOfMessage(message: Message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function titleOfThread(thread: AgentThread) {
|
||||
export function titleOfThread(thread: { values?: { title?: string | null } | null }) {
|
||||
return thread.values?.title ?? "Untitled";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user