Skip to content

Commit 4201eb2

Browse files
authored
fix: Only create thrad id at request time (#419)
1 parent d0079cb commit 4201eb2

File tree

9 files changed

+194
-193
lines changed

9 files changed

+194
-193
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"python.languageServer": "None"
3+
}

frontend/app/components/ChatLangChain.tsx

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useState } from "react";
3+
import React, { useEffect, useRef, useState } from "react";
44
import {
55
AppendMessage,
66
AssistantRuntimeProvider,
@@ -19,37 +19,74 @@ import { SelectModel } from "./SelectModel";
1919
import { ThreadHistory } from "./thread-history";
2020
import { Toaster } from "./ui/toaster";
2121
import { useGraphContext } from "../contexts/GraphContext";
22+
import { useQueryState } from "nuqs";
2223

2324
function ChatLangChainComponent(): React.ReactElement {
2425
const { toast } = useToast();
2526
const { threadsData, userData, graphData } = useGraphContext();
2627
const { userId } = userData;
27-
const { getUserThreads, threadId: currentThread } = threadsData;
28-
const { messages, setMessages, streamMessage } = graphData;
28+
const { getUserThreads, createThread, getThreadById } = threadsData;
29+
const { messages, setMessages, streamMessage, switchSelectedThread } =
30+
graphData;
2931
const [isRunning, setIsRunning] = useState(false);
32+
const [threadId, setThreadId] = useQueryState("threadId");
3033

31-
const isSubmitDisabled = !userId || !currentThread;
34+
const hasCheckedThreadIdParam = useRef(false);
35+
useEffect(() => {
36+
if (typeof window === "undefined" || hasCheckedThreadIdParam.current)
37+
return;
38+
if (!threadId) {
39+
hasCheckedThreadIdParam.current = true;
40+
return;
41+
}
42+
43+
hasCheckedThreadIdParam.current = true;
44+
45+
try {
46+
getThreadById(threadId).then((thread) => {
47+
if (!thread) {
48+
setThreadId(null);
49+
return;
50+
}
51+
52+
switchSelectedThread(thread);
53+
});
54+
} catch (e) {
55+
console.error("Failed to fetch thread in query param", e);
56+
setThreadId(null);
57+
}
58+
}, [threadId]);
59+
60+
const isSubmitDisabled = !userId;
3261

3362
async function onNew(message: AppendMessage): Promise<void> {
3463
if (isSubmitDisabled) {
35-
let description = "";
36-
if (!userId) {
37-
description = "Unable to find user ID. Please try again later.";
38-
} else if (!currentThread) {
39-
description =
40-
"Unable to find current thread ID. Please try again later.";
41-
}
4264
toast({
4365
title: "Failed to send message",
44-
description,
66+
description: "Unable to find user ID. Please try again later.",
4567
});
4668
return;
4769
}
4870
if (message.content[0]?.type !== "text") {
4971
throw new Error("Only text messages are supported");
5072
}
73+
5174
setIsRunning(true);
5275

76+
let currentThreadId = threadId;
77+
if (!currentThreadId) {
78+
const thread = await createThread(userId);
79+
if (!thread) {
80+
toast({
81+
title: "Error",
82+
description: "Thread creation failed.",
83+
});
84+
return;
85+
}
86+
setThreadId(thread.thread_id);
87+
currentThreadId = thread.thread_id;
88+
}
89+
5390
try {
5491
const humanMessage = new HumanMessage({
5592
content: message.content[0].text,
@@ -58,7 +95,7 @@ function ChatLangChainComponent(): React.ReactElement {
5895

5996
setMessages((prevMessages) => [...prevMessages, humanMessage]);
6097

61-
await streamMessage({
98+
await streamMessage(currentThreadId, {
6299
messages: [convertToOpenAIFormat(humanMessage)],
63100
});
64101
} finally {

frontend/app/components/thread-history/index.tsx

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
11
import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button";
22
import { SquarePen, History } from "lucide-react";
33
import { Sheet, SheetContent, SheetTrigger } from "../ui/sheet";
4-
import { useToast } from "../../hooks/use-toast";
54
import { Skeleton } from "../ui/skeleton";
65
import React from "react";
76
import { useGraphContext } from "../../contexts/GraphContext";
87
import { groupThreads } from "./utils";
98
import { ThreadsList } from "./thread-list";
9+
import { useQueryState } from "nuqs";
1010

1111
const LoadingThread = () => <Skeleton className="w-full h-8 bg-[#373737]" />;
1212

1313
function ThreadHistoryComponent() {
14-
const { toast } = useToast();
1514
const { threadsData, userData, graphData } = useGraphContext();
16-
const {
17-
userThreads,
18-
isUserThreadsLoading,
19-
threadId,
20-
deleteThread,
21-
createThread,
22-
getUserThreads,
23-
} = threadsData;
15+
const { userThreads, isUserThreadsLoading, deleteThread } = threadsData;
2416
const { userId } = userData;
25-
const { messages, switchSelectedThread, setMessages } = graphData;
26-
const isEmpty = messages.length === 0;
17+
const { switchSelectedThread, setMessages } = graphData;
18+
const [_threadId, setThreadId] = useQueryState("threadId");
2719

2820
const clearMessages = () => {
2921
setMessages([]);
@@ -40,25 +32,9 @@ function ThreadHistoryComponent() {
4032
deleteThreadAndClearMessages,
4133
);
4234

43-
const createAndSetupNewThread = async () => {
44-
if (!userId) {
45-
toast({
46-
title: "Error creating thread",
47-
description: "Your user ID was not found. Please try again later.",
48-
});
49-
return;
50-
}
51-
const currentThread = userThreads.find(
52-
(thread) => thread.thread_id === threadId,
53-
);
54-
if (currentThread && !currentThread.values && isEmpty) {
55-
return;
56-
}
57-
35+
const createNewSession = async () => {
36+
setThreadId(null);
5837
clearMessages();
59-
await createThread(userId);
60-
// Re-fetch threads so that the new thread shows up.
61-
await getUserThreads(userId);
6238
};
6339

6440
return (
@@ -73,7 +49,7 @@ function ThreadHistoryComponent() {
7349
tooltip="New chat"
7450
variant="ghost"
7551
className="w-fit p-2"
76-
onClick={createAndSetupNewThread}
52+
onClick={createNewSession}
7753
>
7854
<SquarePen className="w-5 h-5" />
7955
</TooltipIconButton>
@@ -121,7 +97,7 @@ function ThreadHistoryComponent() {
12197
tooltip="New chat"
12298
variant="ghost"
12399
className="w-fit h-fit p-2"
124-
onClick={createAndSetupNewThread}
100+
onClick={createNewSession}
125101
>
126102
<SquarePen className="w-6 h-6" />
127103
</TooltipIconButton>

frontend/app/contexts/GraphContext.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
"use client";
2+
13
import { parsePartialJson } from "@langchain/core/output_parsers";
24
import {
35
createContext,
46
Dispatch,
57
ReactNode,
68
SetStateAction,
79
useContext,
8-
useEffect,
910
useState,
1011
} from "react";
1112
import { AIMessage, BaseMessage, HumanMessage } from "@langchain/core/messages";
@@ -18,14 +19,15 @@ import { useRuns } from "../hooks/useRuns";
1819
import { useUser } from "../hooks/useUser";
1920
import { addDocumentLinks, createClient, nodeToStep } from "./utils";
2021
import { Thread } from "@langchain/langgraph-sdk";
22+
import { useQueryState } from "nuqs";
2123

2224
interface GraphData {
2325
messages: BaseMessage[];
2426
selectedModel: ModelOptions;
2527
setSelectedModel: Dispatch<SetStateAction<ModelOptions>>;
2628
setMessages: Dispatch<SetStateAction<BaseMessage[]>>;
27-
streamMessage: (params: GraphInput) => Promise<void>;
28-
switchSelectedThread: (thread: Thread) => Promise<void>;
29+
streamMessage: (currentThreadId: string, params: GraphInput) => Promise<void>;
30+
switchSelectedThread: (thread: Thread) => void;
2931
}
3032

3133
type UserDataContextType = ReturnType<typeof useUser>;
@@ -49,30 +51,31 @@ export function GraphProvider({ children }: { children: ReactNode }) {
4951
const {
5052
isUserThreadsLoading,
5153
userThreads,
52-
setUserThreads,
5354
getThreadById,
55+
setUserThreads,
5456
getUserThreads,
5557
createThread,
56-
searchOrCreateThread,
57-
threadId,
58-
setThreadId,
5958
deleteThread,
6059
} = useThreads(userId);
6160
const { toast } = useToast();
6261
const { shareRun } = useRuns();
6362
const [messages, setMessages] = useState<BaseMessage[]>([]);
64-
const [selectedModel, setSelectedModel] =
65-
useState<ModelOptions>("anthropic/claude-3-5-haiku-20241022");
63+
const [selectedModel, setSelectedModel] = useState<ModelOptions>(
64+
"anthropic/claude-3-5-haiku-20241022",
65+
);
66+
const [_threadId, setThreadId] = useQueryState("threadId");
6667

67-
const streamMessage = async (params: GraphInput): Promise<void> => {
68-
if (!threadId) {
68+
const streamMessage = async (
69+
currentThreadId: string,
70+
params: GraphInput,
71+
): Promise<void> => {
72+
if (!userId) {
6973
toast({
7074
title: "Error",
71-
description: "Thread ID not found",
75+
description: "User ID not found",
7276
});
7377
return;
7478
}
75-
7679
const client = createClient();
7780

7881
const input = {
@@ -92,7 +95,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
9295
}),
9396
};
9497

95-
const stream = client.runs.stream(threadId, "chat", {
98+
const stream = client.runs.stream(currentThreadId, "chat", {
9699
input,
97100
streamMode: "events",
98101
config: {
@@ -556,7 +559,7 @@ export function GraphProvider({ children }: { children: ReactNode }) {
556559
}
557560
};
558561

559-
const switchSelectedThread = async (thread: Thread) => {
562+
const switchSelectedThread = (thread: Thread) => {
560563
setThreadId(thread.thread_id);
561564
if (!thread.values) {
562565
setMessages([]);
@@ -658,13 +661,10 @@ export function GraphProvider({ children }: { children: ReactNode }) {
658661
threadsData: {
659662
isUserThreadsLoading,
660663
userThreads,
661-
setUserThreads,
662664
getThreadById,
665+
setUserThreads,
663666
getUserThreads,
664667
createThread,
665-
searchOrCreateThread,
666-
threadId,
667-
setThreadId,
668668
deleteThread,
669669
},
670670
graphData: {

frontend/app/hooks/useThreads.tsx

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
"use client";
2+
13
import { useEffect, useState } from "react";
24

35
import { Client, Thread } from "@langchain/langgraph-sdk";
4-
import { getCookie, setCookie } from "../utils/cookies";
56
import { useToast } from "./use-toast";
6-
import { THREAD_ID_COOKIE_NAME } from "../utils/constants";
7+
import { useQueryState } from "nuqs";
78

89
export const createClient = () => {
910
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api";
@@ -16,38 +17,13 @@ export function useThreads(userId: string | undefined) {
1617
const { toast } = useToast();
1718
const [isUserThreadsLoading, setIsUserThreadsLoading] = useState(false);
1819
const [userThreads, setUserThreads] = useState<Thread[]>([]);
19-
const [threadId, setThreadId] = useState<string>();
20+
const [threadId, setThreadId] = useQueryState("threadId");
2021

2122
useEffect(() => {
2223
if (typeof window == "undefined" || !userId) return;
2324
getUserThreads(userId);
2425
}, [userId]);
2526

26-
useEffect(() => {
27-
if (threadId || typeof window === "undefined" || !userId) return;
28-
searchOrCreateThread(userId);
29-
}, [userId]);
30-
31-
const searchOrCreateThread = async (id: string) => {
32-
const threadIdCookie = getCookie(THREAD_ID_COOKIE_NAME);
33-
if (!threadIdCookie) {
34-
await createThread(id);
35-
return;
36-
}
37-
// Thread ID is in cookies.
38-
39-
const thread = await getThreadById(threadIdCookie);
40-
if (!thread.values) {
41-
// No values = no activity. Can keep.
42-
setThreadId(threadIdCookie);
43-
return;
44-
} else {
45-
// Current thread has activity. Create a new thread.
46-
await createThread(id);
47-
return;
48-
}
49-
};
50-
5127
const createThread = async (id: string) => {
5228
const client = createClient();
5329
let thread;
@@ -61,7 +37,6 @@ export function useThreads(userId: string | undefined) {
6137
throw new Error("Thread creation failed.");
6238
}
6339
setThreadId(thread.thread_id);
64-
setCookie(THREAD_ID_COOKIE_NAME, thread.thread_id);
6540
} catch (e) {
6641
console.error("Error creating thread", e);
6742
toast({
@@ -111,28 +86,24 @@ export function useThreads(userId: string | undefined) {
11186
);
11287
return newThreads;
11388
});
89+
const client = createClient();
90+
await client.threads.delete(id);
11491
if (id === threadId) {
92+
// Remove the threadID from query params, and refetch threads to
93+
// update the sidebar UI.
11594
clearMessages();
116-
// Create a new thread. Use .then to avoid blocking the UI.
117-
// Once completed re-fetch threads to update UI.
118-
createThread(userId).then(async () => {
119-
await getUserThreads(userId);
120-
});
95+
getUserThreads(userId);
96+
setThreadId(null);
12197
}
122-
const client = createClient();
123-
await client.threads.delete(id);
12498
};
12599

126100
return {
127101
isUserThreadsLoading,
128102
userThreads,
129-
setUserThreads,
130103
getThreadById,
104+
setUserThreads,
131105
getUserThreads,
132106
createThread,
133-
searchOrCreateThread,
134-
threadId,
135-
setThreadId,
136107
deleteThread,
137108
};
138109
}

0 commit comments

Comments
 (0)