@assistant-ui/react-langchain 0.0.34 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LangChainThreadController.d.ts +9 -0
- package/dist/LangChainThreadController.d.ts.map +1 -0
- package/dist/LangChainThreadController.js +21 -0
- package/dist/LangChainThreadController.js.map +1 -0
- package/dist/convertMessages.d.ts +2 -1
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +12 -12
- package/dist/convertMessages.js.map +1 -1
- package/dist/converter.d.ts +24 -1
- package/dist/converter.d.ts.map +1 -1
- package/dist/converter.js +84 -18
- package/dist/converter.js.map +1 -1
- package/dist/langChainThreadState.d.ts +54 -0
- package/dist/langChainThreadState.d.ts.map +1 -0
- package/dist/langChainThreadState.js +135 -0
- package/dist/langChainThreadState.js.map +1 -0
- package/dist/sdkIdentity.js +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/useStreamRuntime.d.ts +3 -2
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +159 -144
- package/dist/useStreamRuntime.js.map +1 -1
- package/dist/useSubagentTranscripts.js +1 -1
- package/package.json +9 -8
- package/src/LangChainThreadController.ts +24 -0
- package/src/convertMessages.test.ts +23 -0
- package/src/convertMessages.ts +43 -17
- package/src/converter.test.ts +322 -0
- package/src/converter.ts +138 -27
- package/src/getPendingToolCalls.test.ts +35 -0
- package/src/langChainThreadState.test.ts +203 -0
- package/src/langChainThreadState.ts +239 -0
- package/src/types.ts +9 -0
- package/src/useStreamRuntime.cloud-options.test.tsx +61 -0
- package/src/useStreamRuntime.test.tsx +296 -8
- package/src/useStreamRuntime.ts +221 -249
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { act, render, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
6
6
|
import type {
|
|
7
7
|
AssistantRuntime,
|
|
@@ -9,7 +9,11 @@ import type {
|
|
|
9
9
|
RemoteThreadListAdapter,
|
|
10
10
|
} from "@assistant-ui/core";
|
|
11
11
|
import { useAui } from "@assistant-ui/store";
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
LangChainBaseMessage,
|
|
14
|
+
LangChainToolCall,
|
|
15
|
+
UIMessage,
|
|
16
|
+
} from "./types";
|
|
13
17
|
import { startTransition, Suspense, type ReactNode } from "react";
|
|
14
18
|
import {
|
|
15
19
|
useLangChainRespond,
|
|
@@ -19,21 +23,41 @@ import {
|
|
|
19
23
|
useLangChainSubmit,
|
|
20
24
|
} from "./hooks";
|
|
21
25
|
|
|
22
|
-
const { mockUseChannel, mockUseStream, streamController } =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const { conversionSpy, mockUseChannel, mockUseStream, streamController } =
|
|
27
|
+
vi.hoisted(() => ({
|
|
28
|
+
conversionSpy: vi.fn(),
|
|
29
|
+
mockUseChannel: vi.fn((): unknown[] => []),
|
|
30
|
+
mockUseStream: vi.fn(),
|
|
31
|
+
streamController: Symbol("STREAM_CONTROLLER"),
|
|
32
|
+
}));
|
|
27
33
|
|
|
28
|
-
vi.mock("@langchain/react", () => ({
|
|
34
|
+
vi.mock("@langchain/react", async (importOriginal) => ({
|
|
35
|
+
...(await importOriginal<typeof import("@langchain/react")>()),
|
|
29
36
|
STREAM_CONTROLLER: streamController,
|
|
30
37
|
useChannel: mockUseChannel,
|
|
31
38
|
useStream: mockUseStream,
|
|
32
39
|
}));
|
|
33
40
|
|
|
41
|
+
vi.mock("./convertMessages", async (importOriginal) => {
|
|
42
|
+
const original = await importOriginal<typeof import("./convertMessages")>();
|
|
43
|
+
return {
|
|
44
|
+
...original,
|
|
45
|
+
convertLangChainBaseMessage: (
|
|
46
|
+
...args: Parameters<typeof original.convertLangChainBaseMessage>
|
|
47
|
+
) => {
|
|
48
|
+
conversionSpy(args[0].id);
|
|
49
|
+
return original.convertLangChainBaseMessage(...args);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
34
54
|
import { useStreamRuntime } from "./useStreamRuntime";
|
|
35
55
|
import { settleOutsideAct } from "./tests/settleOutsideAct";
|
|
36
56
|
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
mockUseChannel.mockReturnValue([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
37
61
|
type MockStream = {
|
|
38
62
|
messages: LangChainBaseMessage[];
|
|
39
63
|
isLoading: boolean;
|
|
@@ -131,6 +155,45 @@ const getText = (aui: ReturnType<typeof useAui>) =>
|
|
|
131
155
|
.join(""),
|
|
132
156
|
);
|
|
133
157
|
|
|
158
|
+
describe("useStreamRuntime metadata cache", () => {
|
|
159
|
+
it("re-converts only the parent when a UI event changes", () => {
|
|
160
|
+
const messageCount = 1_000;
|
|
161
|
+
const messages = Array.from({ length: messageCount }, (_, index) =>
|
|
162
|
+
message(
|
|
163
|
+
`message-${index}`,
|
|
164
|
+
index % 2 === 0 ? "human" : "ai",
|
|
165
|
+
`Message ${index}`,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const parentId = `message-${messageCount - 1}`;
|
|
169
|
+
const parentUI: UIMessage = {
|
|
170
|
+
type: "ui",
|
|
171
|
+
id: "ui-1",
|
|
172
|
+
name: "chart",
|
|
173
|
+
props: { value: 1 },
|
|
174
|
+
metadata: { message_id: parentId },
|
|
175
|
+
};
|
|
176
|
+
const stream = createMockStream(messages);
|
|
177
|
+
const { auiResult, rerender } = renderAui(stream);
|
|
178
|
+
|
|
179
|
+
expect(conversionSpy).toHaveBeenCalledTimes(messageCount);
|
|
180
|
+
conversionSpy.mockClear();
|
|
181
|
+
|
|
182
|
+
mockUseChannel.mockReturnValue([{ params: { data: parentUI } }] as never);
|
|
183
|
+
rerender();
|
|
184
|
+
|
|
185
|
+
expect(conversionSpy).toHaveBeenCalledOnce();
|
|
186
|
+
expect(conversionSpy).toHaveBeenCalledWith(parentId);
|
|
187
|
+
expect(
|
|
188
|
+
auiResult.current.thread.getState().messages.at(-1)?.content.at(-1),
|
|
189
|
+
).toMatchObject({
|
|
190
|
+
type: "data",
|
|
191
|
+
name: "chart",
|
|
192
|
+
data: { value: 1 },
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
134
197
|
const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
|
|
135
198
|
list: vi.fn(async () => ({
|
|
136
199
|
threads: [
|
|
@@ -918,6 +981,231 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
918
981
|
});
|
|
919
982
|
});
|
|
920
983
|
|
|
984
|
+
describe("useStreamRuntime fork ownership", () => {
|
|
985
|
+
type ForkHistory = {
|
|
986
|
+
values: { messages: LangChainBaseMessage[] };
|
|
987
|
+
checkpoint: { checkpoint_id: string };
|
|
988
|
+
}[];
|
|
989
|
+
|
|
990
|
+
const renderFork = async (stream: MockStream) => {
|
|
991
|
+
mockUseStream.mockReturnValue(stream);
|
|
992
|
+
const runtime = renderHook(() =>
|
|
993
|
+
useStreamRuntime({
|
|
994
|
+
apiUrl: "/api",
|
|
995
|
+
unstable_threadListAdapter: makeThreadListAdapter(),
|
|
996
|
+
} as never),
|
|
997
|
+
);
|
|
998
|
+
const view = render(
|
|
999
|
+
<AssistantRuntimeProvider runtime={runtime.result.current}>
|
|
1000
|
+
{null}
|
|
1001
|
+
</AssistantRuntimeProvider>,
|
|
1002
|
+
);
|
|
1003
|
+
await settleOutsideAct(() =>
|
|
1004
|
+
runtime.result.current.threads.switchToThread("thread-a"),
|
|
1005
|
+
);
|
|
1006
|
+
return { runtime, view };
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
it("stops the streaming reply before an edit looks up its checkpoint and submits once", async () => {
|
|
1010
|
+
const events: string[] = [];
|
|
1011
|
+
const lookup = deferred<ForkHistory>();
|
|
1012
|
+
const stream = createMockStream([
|
|
1013
|
+
message("u1", "human", "original"),
|
|
1014
|
+
message("a1", "ai", "partial reply"),
|
|
1015
|
+
]);
|
|
1016
|
+
stream.isLoading = true;
|
|
1017
|
+
stream.stop.mockImplementation(() => events.push("stop"));
|
|
1018
|
+
stream.client = {
|
|
1019
|
+
threads: {
|
|
1020
|
+
getHistory: vi.fn(() => {
|
|
1021
|
+
events.push("lookup");
|
|
1022
|
+
return lookup.promise;
|
|
1023
|
+
}),
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
stream.submit.mockImplementation(async () => {
|
|
1027
|
+
events.push("submit");
|
|
1028
|
+
});
|
|
1029
|
+
const { runtime, view } = await renderFork(stream);
|
|
1030
|
+
|
|
1031
|
+
await act(async () => {
|
|
1032
|
+
runtime.result.current.thread.append({
|
|
1033
|
+
role: "user",
|
|
1034
|
+
parentId: null,
|
|
1035
|
+
sourceId: "u1",
|
|
1036
|
+
content: [{ type: "text", text: "edited" }],
|
|
1037
|
+
});
|
|
1038
|
+
await Promise.resolve();
|
|
1039
|
+
});
|
|
1040
|
+
expect(events).toEqual(["stop", "lookup"]);
|
|
1041
|
+
expect(stream.submit).not.toHaveBeenCalled();
|
|
1042
|
+
|
|
1043
|
+
await act(async () => {
|
|
1044
|
+
lookup.resolve([
|
|
1045
|
+
{ values: { messages: [] }, checkpoint: { checkpoint_id: "cp-1" } },
|
|
1046
|
+
]);
|
|
1047
|
+
await Promise.resolve();
|
|
1048
|
+
});
|
|
1049
|
+
expect(events).toEqual(["stop", "lookup", "submit"]);
|
|
1050
|
+
expect(stream.submit).toHaveBeenCalledExactlyOnceWith(
|
|
1051
|
+
{ messages: [{ type: "human", content: "edited" }] },
|
|
1052
|
+
{ forkFrom: "cp-1" },
|
|
1053
|
+
);
|
|
1054
|
+
view.unmount();
|
|
1055
|
+
runtime.unmount();
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it("drops the first edit when a second edit supersedes its checkpoint lookup", async () => {
|
|
1059
|
+
const firstLookup = deferred<ForkHistory>();
|
|
1060
|
+
const secondLookup = deferred<ForkHistory>();
|
|
1061
|
+
const stream = createMockStream([
|
|
1062
|
+
message("u1", "human", "original"),
|
|
1063
|
+
message("a1", "ai", "partial reply"),
|
|
1064
|
+
]);
|
|
1065
|
+
stream.isLoading = true;
|
|
1066
|
+
const getHistory = vi
|
|
1067
|
+
.fn()
|
|
1068
|
+
.mockImplementationOnce(() => firstLookup.promise)
|
|
1069
|
+
.mockImplementationOnce(() => secondLookup.promise);
|
|
1070
|
+
stream.client = { threads: { getHistory } };
|
|
1071
|
+
const { runtime, view } = await renderFork(stream);
|
|
1072
|
+
|
|
1073
|
+
await act(async () => {
|
|
1074
|
+
runtime.result.current.thread.append({
|
|
1075
|
+
role: "user",
|
|
1076
|
+
parentId: null,
|
|
1077
|
+
sourceId: "u1",
|
|
1078
|
+
content: [{ type: "text", text: "first edit" }],
|
|
1079
|
+
});
|
|
1080
|
+
await Promise.resolve();
|
|
1081
|
+
});
|
|
1082
|
+
await waitFor(() => expect(getHistory).toHaveBeenCalledTimes(1));
|
|
1083
|
+
await act(async () => {
|
|
1084
|
+
runtime.result.current.thread.append({
|
|
1085
|
+
role: "user",
|
|
1086
|
+
parentId: null,
|
|
1087
|
+
sourceId: "u1",
|
|
1088
|
+
content: [{ type: "text", text: "second edit" }],
|
|
1089
|
+
});
|
|
1090
|
+
await Promise.resolve();
|
|
1091
|
+
});
|
|
1092
|
+
await waitFor(() => expect(getHistory).toHaveBeenCalledTimes(2));
|
|
1093
|
+
|
|
1094
|
+
await act(async () => {
|
|
1095
|
+
firstLookup.resolve([
|
|
1096
|
+
{ values: { messages: [] }, checkpoint: { checkpoint_id: "cp-1" } },
|
|
1097
|
+
]);
|
|
1098
|
+
await Promise.resolve();
|
|
1099
|
+
});
|
|
1100
|
+
expect(stream.submit).not.toHaveBeenCalled();
|
|
1101
|
+
|
|
1102
|
+
await act(async () => {
|
|
1103
|
+
secondLookup.resolve([
|
|
1104
|
+
{ values: { messages: [] }, checkpoint: { checkpoint_id: "cp-2" } },
|
|
1105
|
+
]);
|
|
1106
|
+
await Promise.resolve();
|
|
1107
|
+
});
|
|
1108
|
+
expect(stream.submit).toHaveBeenCalledExactlyOnceWith(
|
|
1109
|
+
{ messages: [{ type: "human", content: "second edit" }] },
|
|
1110
|
+
{ forkFrom: "cp-2" },
|
|
1111
|
+
);
|
|
1112
|
+
view.unmount();
|
|
1113
|
+
runtime.unmount();
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it("stops the streaming reply before reload looks up its checkpoint", async () => {
|
|
1117
|
+
const events: string[] = [];
|
|
1118
|
+
const lookup = deferred<ForkHistory>();
|
|
1119
|
+
const stream = createMockStream([
|
|
1120
|
+
message("u1", "human", "question"),
|
|
1121
|
+
message("a1", "ai", "partial reply"),
|
|
1122
|
+
]);
|
|
1123
|
+
stream.isLoading = true;
|
|
1124
|
+
stream.stop.mockImplementation(() => events.push("stop"));
|
|
1125
|
+
stream.client = {
|
|
1126
|
+
threads: {
|
|
1127
|
+
getHistory: vi.fn(() => {
|
|
1128
|
+
events.push("lookup");
|
|
1129
|
+
return lookup.promise;
|
|
1130
|
+
}),
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
stream.submit.mockImplementation(async () => {
|
|
1134
|
+
events.push("submit");
|
|
1135
|
+
});
|
|
1136
|
+
const { runtime, view } = await renderFork(stream);
|
|
1137
|
+
|
|
1138
|
+
await act(async () => {
|
|
1139
|
+
runtime.result.current.thread.getMessageById("a1").reload();
|
|
1140
|
+
await Promise.resolve();
|
|
1141
|
+
});
|
|
1142
|
+
expect(events).toEqual(["stop", "lookup"]);
|
|
1143
|
+
expect(stream.submit).not.toHaveBeenCalled();
|
|
1144
|
+
|
|
1145
|
+
await act(async () => {
|
|
1146
|
+
lookup.resolve([
|
|
1147
|
+
{
|
|
1148
|
+
values: { messages: [message("u1", "human", "question")] },
|
|
1149
|
+
checkpoint: { checkpoint_id: "cp-1" },
|
|
1150
|
+
},
|
|
1151
|
+
]);
|
|
1152
|
+
await Promise.resolve();
|
|
1153
|
+
});
|
|
1154
|
+
expect(events).toEqual(["stop", "lookup", "submit"]);
|
|
1155
|
+
expect(stream.submit).toHaveBeenCalledExactlyOnceWith(null, {
|
|
1156
|
+
forkFrom: "cp-1",
|
|
1157
|
+
});
|
|
1158
|
+
view.unmount();
|
|
1159
|
+
runtime.unmount();
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("drops an edit lookup after Stop", async () => {
|
|
1163
|
+
const lookup = deferred<ForkHistory>();
|
|
1164
|
+
const stream = createMockStream([
|
|
1165
|
+
message("u1", "human", "original"),
|
|
1166
|
+
message("a1", "ai", "partial reply"),
|
|
1167
|
+
]);
|
|
1168
|
+
stream.stop.mockImplementation(() => {
|
|
1169
|
+
stream.isLoading = false;
|
|
1170
|
+
});
|
|
1171
|
+
const getHistory = vi.fn(() => lookup.promise);
|
|
1172
|
+
stream.client = { threads: { getHistory } };
|
|
1173
|
+
const { runtime, view } = await renderFork(stream);
|
|
1174
|
+
|
|
1175
|
+
await act(async () => {
|
|
1176
|
+
runtime.result.current.thread.append({
|
|
1177
|
+
role: "user",
|
|
1178
|
+
parentId: null,
|
|
1179
|
+
sourceId: "u1",
|
|
1180
|
+
content: [{ type: "text", text: "edited" }],
|
|
1181
|
+
});
|
|
1182
|
+
await Promise.resolve();
|
|
1183
|
+
});
|
|
1184
|
+
await waitFor(() => expect(getHistory).toHaveBeenCalledOnce());
|
|
1185
|
+
runtime.rerender();
|
|
1186
|
+
expect(runtime.result.current.thread.getState().isRunning).toBe(true);
|
|
1187
|
+
await act(async () => {
|
|
1188
|
+
await runtime.result.current.thread.cancelRun();
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
await act(async () => {
|
|
1192
|
+
lookup.resolve([
|
|
1193
|
+
{ values: { messages: [] }, checkpoint: { checkpoint_id: "cp-1" } },
|
|
1194
|
+
]);
|
|
1195
|
+
await Promise.resolve();
|
|
1196
|
+
});
|
|
1197
|
+
expect(stream.submit).not.toHaveBeenCalled();
|
|
1198
|
+
expect(runtime.result.current.thread.getState().isRunning).toBe(false);
|
|
1199
|
+
expect(
|
|
1200
|
+
runtime.result.current.thread
|
|
1201
|
+
.getState()
|
|
1202
|
+
.messages.find((m) => m.id === "a1")?.metadata,
|
|
1203
|
+
).not.toHaveProperty("timing");
|
|
1204
|
+
view.unmount();
|
|
1205
|
+
runtime.unmount();
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
|
|
921
1209
|
describe("useStreamRuntime pending tool call cancellation", () => {
|
|
922
1210
|
it("cancels only the pending tool calls that carry an id", async () => {
|
|
923
1211
|
const stream = createMockStream([
|