@assistant-ui/react-langchain 0.0.35 → 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.map +1 -1
- package/dist/convertMessages.js +6 -11
- 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 +152 -139
- package/dist/useStreamRuntime.js.map +1 -1
- package/dist/useSubagentTranscripts.js +1 -1
- package/package.json +8 -8
- package/src/LangChainThreadController.ts +24 -0
- package/src/convertMessages.test.ts +23 -0
- package/src/convertMessages.ts +9 -16
- 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 +227 -1
- package/src/useStreamRuntime.ts +210 -239
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { AssistantCloud } from "assistant-cloud";
|
|
6
|
+
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
7
|
+
import type { UseStreamRuntimeOptions } from "./types";
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
cloudAdapter: {},
|
|
11
|
+
runtime: {},
|
|
12
|
+
useCloudThreadListAdapter: vi.fn(() => ({})),
|
|
13
|
+
useRemoteThreadListRuntime: vi.fn(() => ({})),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
|
|
18
|
+
useCloudThreadListAdapter: mocks.useCloudThreadListAdapter,
|
|
19
|
+
useRemoteThreadListRuntime: mocks.useRemoteThreadListRuntime,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
23
|
+
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
24
|
+
useAui: () => ({
|
|
25
|
+
threadListItem: {
|
|
26
|
+
source: null,
|
|
27
|
+
getState: () => ({ externalId: undefined }),
|
|
28
|
+
initialize: vi.fn(),
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import { LANGCHAIN_SDK } from "./sdkIdentity";
|
|
34
|
+
import { useStreamRuntime } from "./useStreamRuntime";
|
|
35
|
+
|
|
36
|
+
describe("useStreamRuntime Cloud options", () => {
|
|
37
|
+
it("forwards the Cloud scope to the thread-list adapter", () => {
|
|
38
|
+
const cloud = {} as AssistantCloud;
|
|
39
|
+
mocks.useCloudThreadListAdapter.mockReturnValue(mocks.cloudAdapter);
|
|
40
|
+
mocks.useRemoteThreadListRuntime.mockReturnValue(
|
|
41
|
+
mocks.runtime as AssistantRuntime,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const options = {
|
|
45
|
+
apiUrl: "/api",
|
|
46
|
+
assistantId: "assistant-1",
|
|
47
|
+
cloud,
|
|
48
|
+
scopeId: "workspace-1",
|
|
49
|
+
} satisfies UseStreamRuntimeOptions;
|
|
50
|
+
|
|
51
|
+
renderHook(() => useStreamRuntime(options));
|
|
52
|
+
|
|
53
|
+
expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith(
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
cloud,
|
|
56
|
+
scopeId: "workspace-1",
|
|
57
|
+
sdk: LANGCHAIN_SDK,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -31,7 +31,8 @@ const { conversionSpy, mockUseChannel, mockUseStream, streamController } =
|
|
|
31
31
|
streamController: Symbol("STREAM_CONTROLLER"),
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
|
-
vi.mock("@langchain/react", () => ({
|
|
34
|
+
vi.mock("@langchain/react", async (importOriginal) => ({
|
|
35
|
+
...(await importOriginal<typeof import("@langchain/react")>()),
|
|
35
36
|
STREAM_CONTROLLER: streamController,
|
|
36
37
|
useChannel: mockUseChannel,
|
|
37
38
|
useStream: mockUseStream,
|
|
@@ -980,6 +981,231 @@ describe("useStreamRuntime staged messages", () => {
|
|
|
980
981
|
});
|
|
981
982
|
});
|
|
982
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
|
+
|
|
983
1209
|
describe("useStreamRuntime pending tool call cancellation", () => {
|
|
984
1210
|
it("cancels only the pending tool calls that carry an id", async () => {
|
|
985
1211
|
const stream = createMockStream([
|