@assistant-ui/react-langchain 0.0.15 → 0.0.18
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/convertMessages.d.ts +37 -3
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +57 -11
- package/dist/convertMessages.js.map +1 -1
- package/dist/findForkCheckpointInHistory.d.ts +26 -0
- package/dist/findForkCheckpointInHistory.d.ts.map +1 -0
- package/dist/findForkCheckpointInHistory.js +34 -0
- package/dist/findForkCheckpointInHistory.js.map +1 -0
- package/dist/hooks.d.ts +88 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +99 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/resolveForkCheckpoint.d.ts +17 -0
- package/dist/resolveForkCheckpoint.d.ts.map +1 -0
- package/dist/resolveForkCheckpoint.js +26 -0
- package/dist/resolveForkCheckpoint.js.map +1 -0
- package/dist/runtimeExtras.d.ts +7 -0
- package/dist/runtimeExtras.d.ts.map +1 -0
- package/dist/runtimeExtras.js +7 -0
- package/dist/runtimeExtras.js.map +1 -0
- package/dist/streamingTiming.d.ts +16 -0
- package/dist/streamingTiming.d.ts.map +1 -0
- package/dist/streamingTiming.js +53 -0
- package/dist/streamingTiming.js.map +1 -0
- package/dist/types.d.ts +105 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/uiMessages.d.ts +24 -0
- package/dist/uiMessages.d.ts.map +1 -0
- package/dist/uiMessages.js +68 -0
- package/dist/uiMessages.js.map +1 -0
- package/dist/useStreamRuntime.d.ts +10 -85
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +186 -135
- package/dist/useStreamRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/__tests__/langChainTestUtils.ts +3 -3
- package/src/convertMessages.test.ts +160 -3
- package/src/convertMessages.ts +104 -11
- package/src/findForkCheckpointInHistory.test.ts +263 -0
- package/src/findForkCheckpointInHistory.ts +68 -0
- package/src/groupUIMessagesByParent.test.ts +61 -0
- package/src/hooks.ts +156 -0
- package/src/index.ts +18 -3
- package/src/resolveForkCheckpoint.test.ts +202 -0
- package/src/resolveForkCheckpoint.ts +49 -0
- package/src/runtimeExtras.ts +5 -0
- package/src/streamingTiming.test.ts +118 -0
- package/src/streamingTiming.ts +85 -0
- package/src/types.ts +140 -3
- package/src/uiMessages.test.ts +190 -0
- package/src/uiMessages.ts +91 -0
- package/src/useLangChainError.test.tsx +1 -1
- package/src/useLangChainInterrupts.test.tsx +48 -0
- package/src/useLangChainRespond.test.tsx +49 -0
- package/src/useLangChainRespondAll.test.tsx +52 -0
- package/src/useLangChainState.test.tsx +1 -1
- package/src/useLangChainStream.test.tsx +42 -0
- package/src/useLangChainSubagents.test.tsx +48 -0
- package/src/useLangChainSubgraphs.test.tsx +48 -0
- package/src/useLangChainToolCalls.test.tsx +1 -1
- package/src/useStreamRuntime.test.tsx +232 -0
- package/src/useStreamRuntime.ts +301 -268
|
@@ -22,7 +22,7 @@ vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
|
22
22
|
};
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
import { useLangChainState } from "./
|
|
25
|
+
import { useLangChainState } from "./hooks";
|
|
26
26
|
|
|
27
27
|
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
28
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { renderHook } from "@testing-library/react";
|
|
5
|
+
import {
|
|
6
|
+
createRunSelectorAgainst,
|
|
7
|
+
makeExtras,
|
|
8
|
+
type Selector,
|
|
9
|
+
} from "./__tests__/langChainTestUtils";
|
|
10
|
+
|
|
11
|
+
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
+
mockUseAuiState: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
useAuiState: ((selector: Selector) =>
|
|
20
|
+
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
+
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
import { useLangChainStream } from "./hooks";
|
|
26
|
+
|
|
27
|
+
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
+
|
|
29
|
+
describe("useLangChainStream", () => {
|
|
30
|
+
it("returns undefined when extras are absent", () => {
|
|
31
|
+
runSelectorAgainst(undefined);
|
|
32
|
+
const { result } = renderHook(() => useLangChainStream());
|
|
33
|
+
expect(result.current).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns the stream handle from extras", () => {
|
|
37
|
+
const stream = { submit: () => {} };
|
|
38
|
+
runSelectorAgainst(makeExtras({ stream }));
|
|
39
|
+
const { result } = renderHook(() => useLangChainStream());
|
|
40
|
+
expect(result.current).toBe(stream);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { renderHook } from "@testing-library/react";
|
|
5
|
+
import {
|
|
6
|
+
createRunSelectorAgainst,
|
|
7
|
+
makeExtras,
|
|
8
|
+
type Selector,
|
|
9
|
+
} from "./__tests__/langChainTestUtils";
|
|
10
|
+
|
|
11
|
+
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
+
mockUseAuiState: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
useAuiState: ((selector: Selector) =>
|
|
20
|
+
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
+
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
import { useLangChainSubagents } from "./hooks";
|
|
26
|
+
|
|
27
|
+
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
+
|
|
29
|
+
describe("useLangChainSubagents", () => {
|
|
30
|
+
it("returns the stable empty map when extras are absent", () => {
|
|
31
|
+
runSelectorAgainst(undefined);
|
|
32
|
+
const { result } = renderHook(() => useLangChainSubagents());
|
|
33
|
+
expect(result.current.size).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns the subagents map from extras", () => {
|
|
37
|
+
const subagents = new Map([["researcher", { namespace: "researcher" }]]);
|
|
38
|
+
runSelectorAgainst(makeExtras({ subagents }));
|
|
39
|
+
const { result } = renderHook(() => useLangChainSubagents());
|
|
40
|
+
expect(result.current).toBe(subagents);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns the stable empty map when extras carry no subagents", () => {
|
|
44
|
+
runSelectorAgainst(makeExtras({ subagents: undefined }));
|
|
45
|
+
const { result } = renderHook(() => useLangChainSubagents());
|
|
46
|
+
expect(result.current.size).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { renderHook } from "@testing-library/react";
|
|
5
|
+
import {
|
|
6
|
+
createRunSelectorAgainst,
|
|
7
|
+
makeExtras,
|
|
8
|
+
type Selector,
|
|
9
|
+
} from "./__tests__/langChainTestUtils";
|
|
10
|
+
|
|
11
|
+
const { mockUseAuiState } = vi.hoisted(() => ({
|
|
12
|
+
mockUseAuiState: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
useAuiState: ((selector: Selector) =>
|
|
20
|
+
mockUseAuiState(selector)) as typeof actual.useAuiState,
|
|
21
|
+
useAui: (() => ({})) as unknown as typeof actual.useAui,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
import { useLangChainSubgraphs } from "./hooks";
|
|
26
|
+
|
|
27
|
+
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
|
+
|
|
29
|
+
describe("useLangChainSubgraphs", () => {
|
|
30
|
+
it("returns the stable empty map when extras are absent", () => {
|
|
31
|
+
runSelectorAgainst(undefined);
|
|
32
|
+
const { result } = renderHook(() => useLangChainSubgraphs());
|
|
33
|
+
expect(result.current.size).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns the subgraphs map from extras", () => {
|
|
37
|
+
const subgraphs = new Map([["planner", { namespace: "planner" }]]);
|
|
38
|
+
runSelectorAgainst(makeExtras({ subgraphs }));
|
|
39
|
+
const { result } = renderHook(() => useLangChainSubgraphs());
|
|
40
|
+
expect(result.current).toBe(subgraphs);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns the stable empty map when extras carry no subgraphs", () => {
|
|
44
|
+
runSelectorAgainst(makeExtras({ subgraphs: undefined }));
|
|
45
|
+
const { result } = renderHook(() => useLangChainSubgraphs());
|
|
46
|
+
expect(result.current.size).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -22,7 +22,7 @@ vi.mock(import("@assistant-ui/store"), async (importOriginal) => {
|
|
|
22
22
|
};
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
import { useLangChainToolCalls } from "./
|
|
25
|
+
import { useLangChainToolCalls } from "./hooks";
|
|
26
26
|
|
|
27
27
|
const runSelectorAgainst = createRunSelectorAgainst(mockUseAuiState);
|
|
28
28
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
6
|
+
import { useAui } from "@assistant-ui/store";
|
|
7
|
+
import type { LangChainBaseMessage } from "./types";
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
const { mockUseChannel, mockUseStream, streamController } = vi.hoisted(() => ({
|
|
11
|
+
mockUseChannel: vi.fn(() => []),
|
|
12
|
+
mockUseStream: vi.fn(),
|
|
13
|
+
streamController: Symbol("STREAM_CONTROLLER"),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@langchain/react", () => ({
|
|
17
|
+
STREAM_CONTROLLER: streamController,
|
|
18
|
+
useChannel: mockUseChannel,
|
|
19
|
+
useStream: mockUseStream,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { useStreamRuntime } from "./useStreamRuntime";
|
|
23
|
+
|
|
24
|
+
type MockStream = {
|
|
25
|
+
messages: LangChainBaseMessage[];
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
isThreadLoading: boolean;
|
|
28
|
+
values: Record<string, unknown>;
|
|
29
|
+
interrupts: unknown[];
|
|
30
|
+
toolCalls: unknown[];
|
|
31
|
+
subagents: unknown[];
|
|
32
|
+
subgraphs: unknown[];
|
|
33
|
+
error: unknown;
|
|
34
|
+
submit: ReturnType<typeof vi.fn>;
|
|
35
|
+
respond: ReturnType<typeof vi.fn>;
|
|
36
|
+
respondAll: ReturnType<typeof vi.fn>;
|
|
37
|
+
interrupt: ReturnType<typeof vi.fn>;
|
|
38
|
+
stop: ReturnType<typeof vi.fn>;
|
|
39
|
+
client: Record<string, unknown>;
|
|
40
|
+
[streamController]: {
|
|
41
|
+
messageMetadataStore: {
|
|
42
|
+
getSnapshot: ReturnType<typeof vi.fn>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const message = (
|
|
48
|
+
id: string,
|
|
49
|
+
type: "human" | "ai",
|
|
50
|
+
content: string,
|
|
51
|
+
): LangChainBaseMessage & { id: string } => ({
|
|
52
|
+
id,
|
|
53
|
+
_getType: () => type,
|
|
54
|
+
content,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const createMockStream = (
|
|
58
|
+
messages: LangChainBaseMessage[] = [],
|
|
59
|
+
): MockStream => ({
|
|
60
|
+
messages,
|
|
61
|
+
isLoading: false,
|
|
62
|
+
isThreadLoading: false,
|
|
63
|
+
values: {},
|
|
64
|
+
interrupts: [],
|
|
65
|
+
toolCalls: [],
|
|
66
|
+
subagents: [],
|
|
67
|
+
subgraphs: [],
|
|
68
|
+
error: undefined,
|
|
69
|
+
submit: vi.fn(async () => {}),
|
|
70
|
+
respond: vi.fn(),
|
|
71
|
+
respondAll: vi.fn(),
|
|
72
|
+
interrupt: vi.fn(),
|
|
73
|
+
stop: vi.fn(),
|
|
74
|
+
client: {},
|
|
75
|
+
[streamController]: {
|
|
76
|
+
messageMetadataStore: {
|
|
77
|
+
getSnapshot: vi.fn(),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const renderRuntime = (stream: MockStream) => {
|
|
83
|
+
mockUseStream.mockReturnValue(stream);
|
|
84
|
+
return renderHook(() => useStreamRuntime({ apiUrl: "/api" } as never));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const renderAui = (stream: MockStream) => {
|
|
88
|
+
const runtimeHook = renderRuntime(stream);
|
|
89
|
+
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
90
|
+
<AssistantRuntimeProvider runtime={runtimeHook.result.current}>
|
|
91
|
+
{children}
|
|
92
|
+
</AssistantRuntimeProvider>
|
|
93
|
+
);
|
|
94
|
+
Wrapper.displayName = "TestWrapper";
|
|
95
|
+
const auiHook = renderHook(() => useAui(), { wrapper: Wrapper });
|
|
96
|
+
return {
|
|
97
|
+
auiResult: auiHook.result,
|
|
98
|
+
rerender: () => {
|
|
99
|
+
runtimeHook.rerender();
|
|
100
|
+
auiHook.rerender();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getText = (aui: ReturnType<typeof useAui>) =>
|
|
106
|
+
aui
|
|
107
|
+
.thread()
|
|
108
|
+
.getState()
|
|
109
|
+
.messages.map((m) =>
|
|
110
|
+
m.content
|
|
111
|
+
.filter((part) => part.type === "text")
|
|
112
|
+
.map((part) => part.text)
|
|
113
|
+
.join(""),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
describe("useStreamRuntime staged messages", () => {
|
|
117
|
+
it("stages a new user message without submitting when startRun is false", async () => {
|
|
118
|
+
const stream = createMockStream([message("u1", "human", "earlier")]);
|
|
119
|
+
const { auiResult } = renderAui(stream);
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
auiResult.current.thread().append({
|
|
123
|
+
role: "user",
|
|
124
|
+
content: [{ type: "text", text: "draft" }],
|
|
125
|
+
startRun: false,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(getText(auiResult.current)).toEqual(["earlier", "draft"]);
|
|
131
|
+
});
|
|
132
|
+
expect(stream.submit).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("keeps a staged edit truncated when stream messages update before promotion", async () => {
|
|
136
|
+
const stream = createMockStream([
|
|
137
|
+
message("u1", "human", "first"),
|
|
138
|
+
message("a1", "ai", "first answer"),
|
|
139
|
+
message("u2", "human", "second"),
|
|
140
|
+
]);
|
|
141
|
+
const { auiResult, rerender } = renderAui(stream);
|
|
142
|
+
|
|
143
|
+
await act(async () => {
|
|
144
|
+
auiResult.current.thread().append({
|
|
145
|
+
role: "user",
|
|
146
|
+
parentId: "u1",
|
|
147
|
+
content: [{ type: "text", text: "edited" }],
|
|
148
|
+
startRun: false,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(getText(auiResult.current)).toEqual(["first", "edited"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
stream.messages = [
|
|
157
|
+
message("u1", "human", "first"),
|
|
158
|
+
message("a1", "ai", "first answer from refresh"),
|
|
159
|
+
message("u2", "human", "second from refresh"),
|
|
160
|
+
];
|
|
161
|
+
rerender();
|
|
162
|
+
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(getText(auiResult.current)).toEqual(["first", "edited"]);
|
|
165
|
+
});
|
|
166
|
+
expect(stream.submit).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("keeps later staged messages visible after promoting one staged parent", async () => {
|
|
170
|
+
const stream = createMockStream([message("u1", "human", "earlier")]);
|
|
171
|
+
const { auiResult, rerender } = renderAui(stream);
|
|
172
|
+
|
|
173
|
+
await act(async () => {
|
|
174
|
+
auiResult.current.thread().append({
|
|
175
|
+
role: "user",
|
|
176
|
+
content: [{ type: "text", text: "first staged" }],
|
|
177
|
+
startRun: false,
|
|
178
|
+
});
|
|
179
|
+
auiResult.current.thread().append({
|
|
180
|
+
role: "user",
|
|
181
|
+
content: [{ type: "text", text: "second staged" }],
|
|
182
|
+
startRun: false,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(getText(auiResult.current)).toEqual([
|
|
188
|
+
"earlier",
|
|
189
|
+
"first staged",
|
|
190
|
+
"second staged",
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const firstStagedId = auiResult.current.thread().getState().messages[1]!.id;
|
|
195
|
+
await act(async () => {
|
|
196
|
+
await auiResult.current.thread().startRun({
|
|
197
|
+
parentId: firstStagedId,
|
|
198
|
+
sourceId: null,
|
|
199
|
+
runConfig: {},
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(stream.submit).toHaveBeenCalledWith(
|
|
204
|
+
{
|
|
205
|
+
messages: [
|
|
206
|
+
expect.objectContaining({
|
|
207
|
+
id: firstStagedId,
|
|
208
|
+
type: "human",
|
|
209
|
+
content: "first staged",
|
|
210
|
+
}),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
undefined,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
stream.messages = [
|
|
217
|
+
message("u1", "human", "earlier"),
|
|
218
|
+
message(firstStagedId, "human", "first staged"),
|
|
219
|
+
message("a1", "ai", "answer"),
|
|
220
|
+
];
|
|
221
|
+
rerender();
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(getText(auiResult.current)).toEqual([
|
|
225
|
+
"earlier",
|
|
226
|
+
"first staged",
|
|
227
|
+
"answer",
|
|
228
|
+
"second staged",
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|