langgraph-api 0.2.26__py3-none-any.whl → 0.2.28__py3-none-any.whl
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.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/assistants.py +4 -4
- langgraph_api/api/store.py +10 -6
- langgraph_api/asgi_transport.py +171 -0
- langgraph_api/asyncio.py +17 -0
- langgraph_api/config.py +1 -0
- langgraph_api/graph.py +28 -5
- langgraph_api/js/remote.py +16 -11
- langgraph_api/metadata.py +28 -16
- langgraph_api/store.py +127 -0
- langgraph_api/stream.py +17 -7
- langgraph_api/worker.py +1 -1
- {langgraph_api-0.2.26.dist-info → langgraph_api-0.2.28.dist-info}/METADATA +24 -30
- {langgraph_api-0.2.26.dist-info → langgraph_api-0.2.28.dist-info}/RECORD +42 -64
- {langgraph_api-0.2.26.dist-info → langgraph_api-0.2.28.dist-info}/WHEEL +1 -1
- langgraph_api-0.2.28.dist-info/entry_points.txt +2 -0
- langgraph_api/js/tests/api.test.mts +0 -2194
- langgraph_api/js/tests/auth.test.mts +0 -648
- langgraph_api/js/tests/compose-postgres.auth.yml +0 -59
- langgraph_api/js/tests/compose-postgres.yml +0 -59
- langgraph_api/js/tests/graphs/.gitignore +0 -1
- langgraph_api/js/tests/graphs/agent.css +0 -1
- langgraph_api/js/tests/graphs/agent.mts +0 -187
- langgraph_api/js/tests/graphs/agent.ui.tsx +0 -10
- langgraph_api/js/tests/graphs/agent_simple.mts +0 -105
- langgraph_api/js/tests/graphs/auth.mts +0 -106
- langgraph_api/js/tests/graphs/command.mts +0 -48
- langgraph_api/js/tests/graphs/delay.mts +0 -30
- langgraph_api/js/tests/graphs/dynamic.mts +0 -24
- langgraph_api/js/tests/graphs/error.mts +0 -17
- langgraph_api/js/tests/graphs/http.mts +0 -76
- langgraph_api/js/tests/graphs/langgraph.json +0 -11
- langgraph_api/js/tests/graphs/nested.mts +0 -44
- langgraph_api/js/tests/graphs/package.json +0 -13
- langgraph_api/js/tests/graphs/weather.mts +0 -57
- langgraph_api/js/tests/graphs/yarn.lock +0 -242
- langgraph_api/js/tests/utils.mts +0 -17
- langgraph_api-0.2.26.dist-info/LICENSE +0 -93
- langgraph_api-0.2.26.dist-info/entry_points.txt +0 -3
- logging.json +0 -22
- openapi.json +0 -4562
- /LICENSE → /langgraph_api-0.2.28.dist-info/licenses/LICENSE +0 -0
|
@@ -1,2194 +0,0 @@
|
|
|
1
|
-
import { Client, FeedbackStreamEvent } from "@langchain/langgraph-sdk";
|
|
2
|
-
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
BaseMessageFields,
|
|
6
|
-
BaseMessageLike,
|
|
7
|
-
MessageType,
|
|
8
|
-
} from "@langchain/core/messages";
|
|
9
|
-
import { randomUUID } from "crypto";
|
|
10
|
-
import postgres from "postgres";
|
|
11
|
-
import { findLast, gatherIterator } from "./utils.mts";
|
|
12
|
-
|
|
13
|
-
const sql = postgres(
|
|
14
|
-
process.env.POSTGRES_URI ??
|
|
15
|
-
"postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable",
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const API_URL = "http://localhost:9123";
|
|
19
|
-
const client = new Client<any>({ apiUrl: API_URL });
|
|
20
|
-
|
|
21
|
-
// Passed to all invocation requests as the graph now requires this field to be present
|
|
22
|
-
// in `configurable` due to a new `SharedValue` field requiring it.
|
|
23
|
-
const globalConfig = {
|
|
24
|
-
configurable: {
|
|
25
|
-
user_id: "123",
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// TODO: this is not exported anywhere in JS
|
|
30
|
-
// we should support only the flattened one
|
|
31
|
-
type BaseMessage = {
|
|
32
|
-
type: MessageType | "user" | "assistant" | "placeholder";
|
|
33
|
-
} & BaseMessageFields;
|
|
34
|
-
|
|
35
|
-
interface AgentState {
|
|
36
|
-
messages: Array<BaseMessage>;
|
|
37
|
-
sharedStateValue?: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// the way this test is set up, it instantiates the client
|
|
41
|
-
// with 6 graphs.
|
|
42
|
-
beforeAll(async () => {
|
|
43
|
-
await sql`DELETE FROM thread`;
|
|
44
|
-
await sql`DELETE FROM store`;
|
|
45
|
-
await sql`DELETE FROM assistant WHERE metadata->>'created_by' is null OR metadata->>'created_by' != 'system'`;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("assistants", () => {
|
|
49
|
-
beforeEach(async () => {
|
|
50
|
-
await sql`DELETE FROM assistant WHERE metadata->>'created_by' is null OR metadata->>'created_by' != 'system'`;
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("create read update delete", async () => {
|
|
54
|
-
const graphId = "agent";
|
|
55
|
-
const config = { configurable: { model_name: "gpt" } };
|
|
56
|
-
|
|
57
|
-
let res = await client.assistants.create({
|
|
58
|
-
graphId,
|
|
59
|
-
config,
|
|
60
|
-
name: "assistant1",
|
|
61
|
-
});
|
|
62
|
-
expect(res).toMatchObject({
|
|
63
|
-
graph_id: graphId,
|
|
64
|
-
config,
|
|
65
|
-
name: "assistant1",
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const metadata = { name: "woof" };
|
|
69
|
-
await client.assistants.update(res.assistant_id, { graphId, metadata });
|
|
70
|
-
|
|
71
|
-
res = await client.assistants.get(res.assistant_id);
|
|
72
|
-
expect(res).toMatchObject({ graph_id: graphId, config, metadata });
|
|
73
|
-
|
|
74
|
-
const secondAssistant = await client.assistants.create({
|
|
75
|
-
graphId,
|
|
76
|
-
config,
|
|
77
|
-
name: "assistant2",
|
|
78
|
-
});
|
|
79
|
-
expect(secondAssistant).toMatchObject({
|
|
80
|
-
graph_id: graphId,
|
|
81
|
-
config,
|
|
82
|
-
name: "assistant2",
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const search = await client.assistants.search();
|
|
86
|
-
const customAssistants = search.filter(
|
|
87
|
-
(a) => a.metadata?.created_by !== "system",
|
|
88
|
-
);
|
|
89
|
-
expect(customAssistants.length).toBe(2);
|
|
90
|
-
expect(customAssistants[0].name).toBe("assistant2");
|
|
91
|
-
expect(customAssistants[1].name).toBe("assistant1");
|
|
92
|
-
|
|
93
|
-
const search2 = await client.assistants.search({
|
|
94
|
-
sortBy: "name",
|
|
95
|
-
sortOrder: "desc",
|
|
96
|
-
});
|
|
97
|
-
const customAssistants2 = search2.filter(
|
|
98
|
-
(a) => a.metadata?.created_by !== "system",
|
|
99
|
-
);
|
|
100
|
-
expect(customAssistants2.length).toBe(2);
|
|
101
|
-
expect(customAssistants2[0].name).toBe("assistant2");
|
|
102
|
-
expect(customAssistants2[1].name).toBe("assistant1");
|
|
103
|
-
|
|
104
|
-
await client.assistants.delete(res.assistant_id);
|
|
105
|
-
await expect(() => client.assistants.get(res.assistant_id)).rejects.toThrow(
|
|
106
|
-
"HTTP 404: Not Found",
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("schemas", async () => {
|
|
111
|
-
const graphId = "agent";
|
|
112
|
-
const config = { configurable: { model: "openai" } };
|
|
113
|
-
|
|
114
|
-
let res = await client.assistants.create({ graphId, config });
|
|
115
|
-
expect(res).toMatchObject({ graph_id: graphId, config });
|
|
116
|
-
|
|
117
|
-
res = await client.assistants.get(res.assistant_id);
|
|
118
|
-
expect(res).toMatchObject({ graph_id: graphId, config });
|
|
119
|
-
|
|
120
|
-
const graph = await client.assistants.getGraph(res.assistant_id);
|
|
121
|
-
expect(graph).toMatchObject({
|
|
122
|
-
nodes: expect.arrayContaining([
|
|
123
|
-
{ id: "__start__", type: "unknown", data: "__start__" },
|
|
124
|
-
{ id: "__end__", type: "unknown", data: "__end__" },
|
|
125
|
-
{ id: "agent", type: "unknown", data: "agent" },
|
|
126
|
-
{ id: "tool", type: "unknown", data: "tool" },
|
|
127
|
-
]),
|
|
128
|
-
edges: expect.arrayContaining([
|
|
129
|
-
{ source: "tool", target: "agent" },
|
|
130
|
-
{ source: "agent", target: "tool", conditional: true },
|
|
131
|
-
{ source: "__start__", target: "agent" },
|
|
132
|
-
{ source: "agent", target: "__end__", conditional: true },
|
|
133
|
-
]),
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const schemas = await client.assistants.getSchemas(res.assistant_id);
|
|
137
|
-
|
|
138
|
-
expect(schemas.input_schema).not.toBe(null);
|
|
139
|
-
expect(schemas.output_schema).not.toBe(null);
|
|
140
|
-
expect(schemas.config_schema).toMatchObject({
|
|
141
|
-
type: "object",
|
|
142
|
-
properties: { model_name: { type: "string" } },
|
|
143
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(schemas.state_schema).toMatchObject({
|
|
147
|
-
type: "object",
|
|
148
|
-
properties: {
|
|
149
|
-
messages: {
|
|
150
|
-
type: "array",
|
|
151
|
-
items: {
|
|
152
|
-
$ref: "#/definitions/BaseMessage",
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
definitions: {
|
|
157
|
-
BaseMessage: {
|
|
158
|
-
oneOf: [
|
|
159
|
-
{ $ref: "#/definitions/BaseMessageChunk" },
|
|
160
|
-
{ $ref: "#/definitions/ToolMessage" },
|
|
161
|
-
{ $ref: "#/definitions/AIMessage" },
|
|
162
|
-
{ $ref: "#/definitions/ChatMessage" },
|
|
163
|
-
{ $ref: "#/definitions/FunctionMessage" },
|
|
164
|
-
{ $ref: "#/definitions/HumanMessage" },
|
|
165
|
-
{ $ref: "#/definitions/SystemMessage" },
|
|
166
|
-
{ $ref: "#/definitions/RemoveMessage" },
|
|
167
|
-
],
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
await client.assistants.delete(res.assistant_id);
|
|
174
|
-
await expect(() => client.assistants.get(res.assistant_id)).rejects.toThrow(
|
|
175
|
-
"HTTP 404: Not Found",
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("list assistants", async () => {
|
|
180
|
-
let search = await client.assistants.search();
|
|
181
|
-
const numAssistants = search.length;
|
|
182
|
-
|
|
183
|
-
const customAssistants = search.filter(
|
|
184
|
-
(a) => a.metadata?.created_by !== "system",
|
|
185
|
-
);
|
|
186
|
-
expect(customAssistants.length).toBe(0);
|
|
187
|
-
|
|
188
|
-
const graphid = "agent";
|
|
189
|
-
const create = await client.assistants.create({ graphId: "agent" });
|
|
190
|
-
|
|
191
|
-
search = await client.assistants.search();
|
|
192
|
-
expect(search.length).toBe(numAssistants + 1);
|
|
193
|
-
|
|
194
|
-
search = await client.assistants.search({ graphId: graphid });
|
|
195
|
-
expect(search.length).toBe(2);
|
|
196
|
-
expect(search.every((i) => i.graph_id === graphid)).toBe(true);
|
|
197
|
-
|
|
198
|
-
search = await client.assistants.search({
|
|
199
|
-
metadata: { created_by: "system" },
|
|
200
|
-
});
|
|
201
|
-
expect(search.length).toBe(numAssistants);
|
|
202
|
-
expect(search.every((i) => i.assistant_id !== create.assistant_id)).toBe(
|
|
203
|
-
true,
|
|
204
|
-
);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("config from env", async () => {
|
|
208
|
-
let search = await client.assistants.search({
|
|
209
|
-
graphId: "agent",
|
|
210
|
-
metadata: { created_by: "system" },
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
expect(search.length).toBe(1);
|
|
214
|
-
expect(search[0].config).toMatchObject({
|
|
215
|
-
configurable: { model_name: "openai" },
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe("threads crud", () => {
|
|
221
|
-
beforeEach(async () => {
|
|
222
|
-
await sql`DELETE FROM thread`;
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("create, read, update, delete thread", async () => {
|
|
226
|
-
const metadata = { name: "test_thread" };
|
|
227
|
-
|
|
228
|
-
const threadOne = await client.threads.create({ metadata });
|
|
229
|
-
expect(threadOne.metadata).toEqual(metadata);
|
|
230
|
-
|
|
231
|
-
let get = await client.threads.get(threadOne.thread_id);
|
|
232
|
-
expect(get.thread_id).toBe(threadOne.thread_id);
|
|
233
|
-
expect(get.metadata).toEqual(metadata);
|
|
234
|
-
|
|
235
|
-
await client.threads.update(threadOne.thread_id, {
|
|
236
|
-
metadata: { modified: true },
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
get = await client.threads.get(threadOne.thread_id);
|
|
240
|
-
expect(get.metadata).toEqual({ ...metadata, modified: true });
|
|
241
|
-
|
|
242
|
-
const threadTwo = await client.threads.create({
|
|
243
|
-
metadata: { name: "another_thread" },
|
|
244
|
-
});
|
|
245
|
-
let search = await client.threads.search();
|
|
246
|
-
expect(search.length).toBe(2);
|
|
247
|
-
expect(search[0].thread_id).toBe(threadTwo.thread_id);
|
|
248
|
-
expect(search[1].thread_id).toBe(threadOne.thread_id);
|
|
249
|
-
|
|
250
|
-
search = await client.threads.search({ metadata: { modified: true } });
|
|
251
|
-
expect(search.length).toBe(1);
|
|
252
|
-
expect(search[0].thread_id).toBe(threadOne.thread_id);
|
|
253
|
-
|
|
254
|
-
await client.threads.delete(threadOne.thread_id);
|
|
255
|
-
search = await client.threads.search();
|
|
256
|
-
|
|
257
|
-
expect(search.length).toBe(1);
|
|
258
|
-
expect(search[0].thread_id).toBe(threadTwo.thread_id);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("list threads", async () => {
|
|
262
|
-
let search = await client.threads.search();
|
|
263
|
-
expect(search.length).toBe(0);
|
|
264
|
-
|
|
265
|
-
// test adding a single thread w/o metadata
|
|
266
|
-
const createThreadResponse = await client.threads.create();
|
|
267
|
-
search = await client.threads.search();
|
|
268
|
-
|
|
269
|
-
expect(search.length).toBe(1);
|
|
270
|
-
expect(createThreadResponse.thread_id).toBe(search[0].thread_id);
|
|
271
|
-
|
|
272
|
-
// test adding a thread w/ metadata
|
|
273
|
-
const metadata = { name: "test_thread" };
|
|
274
|
-
const create = await client.threads.create({ metadata });
|
|
275
|
-
|
|
276
|
-
search = await client.threads.search();
|
|
277
|
-
expect(search.length).toBe(2);
|
|
278
|
-
expect(create.thread_id).toBe(search[0].thread_id);
|
|
279
|
-
|
|
280
|
-
// test filtering on metadata
|
|
281
|
-
search = await client.threads.search({ metadata });
|
|
282
|
-
expect(search.length).toBe(1);
|
|
283
|
-
expect(create.thread_id).toBe(search[0].thread_id);
|
|
284
|
-
|
|
285
|
-
// test pagination
|
|
286
|
-
search = await client.threads.search({ offset: 1, limit: 1 });
|
|
287
|
-
expect(search.length).toBe(1);
|
|
288
|
-
expect(createThreadResponse.thread_id).toBe(search[0].thread_id);
|
|
289
|
-
|
|
290
|
-
// test sorting
|
|
291
|
-
search = await client.threads.search({
|
|
292
|
-
sortBy: "created_at",
|
|
293
|
-
sortOrder: "asc",
|
|
294
|
-
});
|
|
295
|
-
expect(search[0].thread_id).toBe(createThreadResponse.thread_id);
|
|
296
|
-
|
|
297
|
-
search = await client.threads.search({
|
|
298
|
-
sortBy: "created_at",
|
|
299
|
-
sortOrder: "desc",
|
|
300
|
-
});
|
|
301
|
-
expect(search[1].thread_id).toBe(createThreadResponse.thread_id);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
describe("threads copy", () => {
|
|
306
|
-
it.concurrent("copy", { retry: 3 }, async () => {
|
|
307
|
-
const assistantId = "agent";
|
|
308
|
-
const thread = await client.threads.create();
|
|
309
|
-
const input = { messages: [{ type: "human", content: "foo" }] };
|
|
310
|
-
await client.runs.wait(thread.thread_id, assistantId, {
|
|
311
|
-
input,
|
|
312
|
-
config: globalConfig,
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
const threadState = await client.threads.getState(thread.thread_id);
|
|
316
|
-
|
|
317
|
-
const copiedThread = await client.threads.copy(thread.thread_id);
|
|
318
|
-
const copiedThreadState = await client.threads.getState(
|
|
319
|
-
copiedThread.thread_id,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
// check copied thread state matches expected output
|
|
323
|
-
const expectedThreadMetadata = {
|
|
324
|
-
...threadState.metadata,
|
|
325
|
-
thread_id: copiedThread.thread_id,
|
|
326
|
-
};
|
|
327
|
-
const expectedThreadState = {
|
|
328
|
-
...threadState,
|
|
329
|
-
checkpoint: {
|
|
330
|
-
...threadState.checkpoint,
|
|
331
|
-
thread_id: copiedThread.thread_id,
|
|
332
|
-
},
|
|
333
|
-
parent_checkpoint: {
|
|
334
|
-
...threadState.parent_checkpoint,
|
|
335
|
-
thread_id: copiedThread.thread_id,
|
|
336
|
-
},
|
|
337
|
-
metadata: expectedThreadMetadata,
|
|
338
|
-
checkpoint_id: copiedThreadState.checkpoint.checkpoint_id,
|
|
339
|
-
parent_checkpoint_id: copiedThreadState.parent_checkpoint?.checkpoint_id,
|
|
340
|
-
};
|
|
341
|
-
expect(copiedThreadState).toEqual(expectedThreadState);
|
|
342
|
-
|
|
343
|
-
// check checkpoints in DB
|
|
344
|
-
const existingCheckpoints = await sql`
|
|
345
|
-
SELECT * FROM checkpoints WHERE thread_id = ${thread.thread_id}
|
|
346
|
-
`;
|
|
347
|
-
const copiedCheckpoints = await sql`
|
|
348
|
-
SELECT * FROM checkpoints WHERE thread_id = ${copiedThread.thread_id}
|
|
349
|
-
`;
|
|
350
|
-
|
|
351
|
-
expect(existingCheckpoints.length).toBe(copiedCheckpoints.length);
|
|
352
|
-
for (let i = 0; i < existingCheckpoints.length; i++) {
|
|
353
|
-
const existing = existingCheckpoints[i];
|
|
354
|
-
const copied = copiedCheckpoints[i];
|
|
355
|
-
delete existing.thread_id;
|
|
356
|
-
delete existing.metadata.thread_id;
|
|
357
|
-
delete copied.thread_id;
|
|
358
|
-
delete copied.metadata.thread_id;
|
|
359
|
-
expect(existing).toEqual(copied);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// check checkpoint blobs in DB
|
|
363
|
-
const existingCheckpointBlobs = await sql`
|
|
364
|
-
SELECT * FROM checkpoint_blobs WHERE thread_id = ${thread.thread_id} ORDER BY channel, version
|
|
365
|
-
`;
|
|
366
|
-
const copiedCheckpointBlobs = await sql`
|
|
367
|
-
SELECT * FROM checkpoint_blobs WHERE thread_id = ${copiedThread.thread_id} ORDER BY channel, version
|
|
368
|
-
`;
|
|
369
|
-
|
|
370
|
-
expect(existingCheckpointBlobs.length).toBe(copiedCheckpointBlobs.length);
|
|
371
|
-
for (let i = 0; i < existingCheckpointBlobs.length; i++) {
|
|
372
|
-
const existing = existingCheckpointBlobs[i];
|
|
373
|
-
const copied = copiedCheckpointBlobs[i];
|
|
374
|
-
delete existing.thread_id;
|
|
375
|
-
delete copied.thread_id;
|
|
376
|
-
expect(existing).toEqual(copied);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it.concurrent("copy runs", { retry: 3 }, async () => {
|
|
381
|
-
const assistantId = "agent";
|
|
382
|
-
const thread = await client.threads.create();
|
|
383
|
-
|
|
384
|
-
const input = { messages: [{ type: "human", content: "foo" }] };
|
|
385
|
-
await client.runs.wait(thread.thread_id, assistantId, {
|
|
386
|
-
input,
|
|
387
|
-
config: globalConfig,
|
|
388
|
-
});
|
|
389
|
-
const originalThreadState = await client.threads.getState(thread.thread_id);
|
|
390
|
-
|
|
391
|
-
const copiedThread = await client.threads.copy(thread.thread_id);
|
|
392
|
-
const newInput = { messages: [{ type: "human", content: "bar" }] };
|
|
393
|
-
await client.runs.wait(copiedThread.thread_id, assistantId, {
|
|
394
|
-
input: newInput,
|
|
395
|
-
config: globalConfig,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// test that copied thread has original as well as new values
|
|
399
|
-
const copiedThreadState = await client.threads.getState<AgentState>(
|
|
400
|
-
copiedThread.thread_id,
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const copiedThreadStateMessages = copiedThreadState.values.messages.map(
|
|
404
|
-
(m) => m.content,
|
|
405
|
-
);
|
|
406
|
-
expect(copiedThreadStateMessages).toEqual([
|
|
407
|
-
// original messages
|
|
408
|
-
"foo",
|
|
409
|
-
"begin",
|
|
410
|
-
"tool_call__begin",
|
|
411
|
-
"end",
|
|
412
|
-
// new messages
|
|
413
|
-
"bar",
|
|
414
|
-
"begin",
|
|
415
|
-
"tool_call__begin",
|
|
416
|
-
"end",
|
|
417
|
-
]);
|
|
418
|
-
|
|
419
|
-
// test that the new run on the copied thread doesn't affect the original one
|
|
420
|
-
const currentOriginalThreadState = await client.threads.getState(
|
|
421
|
-
thread.thread_id,
|
|
422
|
-
);
|
|
423
|
-
expect(currentOriginalThreadState).toEqual(originalThreadState);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it.concurrent("get thread history", { retry: 3 }, async () => {
|
|
427
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
428
|
-
const thread = await client.threads.create();
|
|
429
|
-
const input = { messages: [{ type: "human", content: "foo" }] };
|
|
430
|
-
|
|
431
|
-
const emptyHistory = await client.threads.getHistory(thread.thread_id);
|
|
432
|
-
expect(emptyHistory.length).toBe(0);
|
|
433
|
-
|
|
434
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
435
|
-
input,
|
|
436
|
-
config: globalConfig,
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
const history = await client.threads.getHistory<AgentState>(
|
|
440
|
-
thread.thread_id,
|
|
441
|
-
);
|
|
442
|
-
expect(history.length).toBe(5);
|
|
443
|
-
expect(history[0].values.messages.length).toBe(4);
|
|
444
|
-
expect(history[0].next.length).toBe(0);
|
|
445
|
-
expect(history.at(-1)?.next).toEqual(["__start__"]);
|
|
446
|
-
|
|
447
|
-
const runMetadata = { run_metadata: "run_metadata" };
|
|
448
|
-
const inputBar = { messages: [{ type: "human", content: "bar" }] };
|
|
449
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
450
|
-
input: inputBar,
|
|
451
|
-
metadata: runMetadata,
|
|
452
|
-
config: globalConfig,
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
const fullHistory = await client.threads.getHistory<AgentState>(
|
|
456
|
-
thread.thread_id,
|
|
457
|
-
);
|
|
458
|
-
const filteredHistory = await client.threads.getHistory<AgentState>(
|
|
459
|
-
thread.thread_id,
|
|
460
|
-
{ metadata: runMetadata },
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
expect(fullHistory.length).toBe(10);
|
|
464
|
-
expect(fullHistory.at(-1)?.values.messages.length).toBe(0);
|
|
465
|
-
|
|
466
|
-
expect(filteredHistory.length).toBe(5);
|
|
467
|
-
expect(filteredHistory.at(-1)?.values.messages.length).toBe(4);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
it.concurrent("copy update", { retry: 3 }, async () => {
|
|
471
|
-
const assistantId = "agent";
|
|
472
|
-
const thread = await client.threads.create();
|
|
473
|
-
const input = {
|
|
474
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
475
|
-
};
|
|
476
|
-
await client.runs.wait(thread.thread_id, assistantId, {
|
|
477
|
-
input,
|
|
478
|
-
config: globalConfig,
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
const originalState = await client.threads.getState(thread.thread_id);
|
|
482
|
-
const copyThread = await client.threads.copy(thread.thread_id);
|
|
483
|
-
|
|
484
|
-
// update state on a copied thread
|
|
485
|
-
const update = { type: "human", content: "bar", id: "initial-message" };
|
|
486
|
-
await client.threads.updateState(copyThread.thread_id, {
|
|
487
|
-
values: { messages: [update] },
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
const copiedThreadState = await client.threads.getState<AgentState>(
|
|
491
|
-
copyThread.thread_id,
|
|
492
|
-
);
|
|
493
|
-
expect(copiedThreadState.values.messages[0].content).toBe("bar");
|
|
494
|
-
|
|
495
|
-
// test that updating the copied thread doesn't affect the original one
|
|
496
|
-
const currentOriginalThreadState = await client.threads.getState(
|
|
497
|
-
thread.thread_id,
|
|
498
|
-
);
|
|
499
|
-
expect(currentOriginalThreadState).toEqual(originalState);
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
describe("runs", () => {
|
|
504
|
-
beforeAll(async () => {
|
|
505
|
-
await sql`DELETE FROM thread`;
|
|
506
|
-
await sql`DELETE FROM store`;
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it.concurrent("list runs", async () => {
|
|
510
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
511
|
-
const thread = await client.threads.create();
|
|
512
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
513
|
-
input: { messages: [{ type: "human", content: "foo" }] },
|
|
514
|
-
config: globalConfig,
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
const pendingRun = await client.runs.create(
|
|
518
|
-
thread.thread_id,
|
|
519
|
-
assistant.assistant_id,
|
|
520
|
-
{
|
|
521
|
-
input: { messages: [{ type: "human", content: "bar" }] },
|
|
522
|
-
config: globalConfig,
|
|
523
|
-
afterSeconds: 10,
|
|
524
|
-
},
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
let runs = await client.runs.list(thread.thread_id);
|
|
528
|
-
expect(runs.length).toBe(2);
|
|
529
|
-
|
|
530
|
-
runs = await client.runs.list(thread.thread_id, { status: "pending" });
|
|
531
|
-
expect(runs.length).toBe(1);
|
|
532
|
-
|
|
533
|
-
await client.runs.cancel(thread.thread_id, pendingRun.run_id);
|
|
534
|
-
|
|
535
|
-
runs = await client.runs.list(thread.thread_id, { status: "interrupted" });
|
|
536
|
-
expect(runs.length).toBe(1);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it.concurrent("stream values", { retry: 3 }, async () => {
|
|
540
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
541
|
-
const thread = await client.threads.create();
|
|
542
|
-
const input = {
|
|
543
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
544
|
-
};
|
|
545
|
-
const stream = client.runs.stream(
|
|
546
|
-
thread.thread_id,
|
|
547
|
-
assistant.assistant_id,
|
|
548
|
-
{ input, streamMode: "values", config: globalConfig },
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
let runId: string | null = null;
|
|
552
|
-
let previousMessageIds = [];
|
|
553
|
-
const seenEventTypes = new Set();
|
|
554
|
-
|
|
555
|
-
let chunk: any;
|
|
556
|
-
for await (chunk of stream) {
|
|
557
|
-
seenEventTypes.add(chunk.event);
|
|
558
|
-
|
|
559
|
-
if (chunk.event === "metadata") {
|
|
560
|
-
runId = chunk.data.run_id;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (chunk.event === "values") {
|
|
564
|
-
const messageIds = chunk.data.messages.map(
|
|
565
|
-
(message: any) => message.id,
|
|
566
|
-
);
|
|
567
|
-
expect(messageIds.slice(0, -1)).toEqual(previousMessageIds);
|
|
568
|
-
previousMessageIds = messageIds;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
expect(chunk.event).toBe("values");
|
|
573
|
-
expect(seenEventTypes).toEqual(new Set(["metadata", "values"]));
|
|
574
|
-
|
|
575
|
-
expect(runId).not.toBeNull();
|
|
576
|
-
const run = await client.runs.get(thread.thread_id, runId as string);
|
|
577
|
-
expect(run.status).toBe("success");
|
|
578
|
-
|
|
579
|
-
let cur = await sql`SELECT * FROM checkpoints WHERE run_id is null`;
|
|
580
|
-
|
|
581
|
-
expect(cur).toHaveLength(0);
|
|
582
|
-
|
|
583
|
-
cur = await sql`SELECT * FROM checkpoints WHERE run_id = ${run.run_id}`;
|
|
584
|
-
expect(cur.length).toBeGreaterThan(1);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it.concurrent("wait error", { retry: 3 }, async () => {
|
|
588
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
589
|
-
const thread = await client.threads.create();
|
|
590
|
-
const input = {
|
|
591
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
await expect(
|
|
595
|
-
client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
596
|
-
input,
|
|
597
|
-
config: { ...globalConfig, recursion_limit: 1 },
|
|
598
|
-
}),
|
|
599
|
-
).rejects.toThrowError(/GraphRecursionError/);
|
|
600
|
-
const threadUpdated = await client.threads.get(thread.thread_id);
|
|
601
|
-
expect(threadUpdated.status).toBe("error");
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it.concurrent("wait", { retry: 3 }, async () => {
|
|
605
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
606
|
-
const thread = await client.threads.create();
|
|
607
|
-
const input = {
|
|
608
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
609
|
-
};
|
|
610
|
-
const values = await client.runs.wait(
|
|
611
|
-
thread.thread_id,
|
|
612
|
-
assistant.assistant_id,
|
|
613
|
-
{ input, config: globalConfig },
|
|
614
|
-
);
|
|
615
|
-
|
|
616
|
-
expect(Array.isArray((values as any).messages)).toBe(true);
|
|
617
|
-
const threadUpdated = await client.threads.get(thread.thread_id);
|
|
618
|
-
expect(threadUpdated.status).toBe("idle");
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it.concurrent("stream updates", async () => {
|
|
622
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
623
|
-
const thread = await client.threads.create();
|
|
624
|
-
const input = {
|
|
625
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
626
|
-
};
|
|
627
|
-
const stream = client.runs.stream(
|
|
628
|
-
thread.thread_id,
|
|
629
|
-
assistant.assistant_id,
|
|
630
|
-
{ input, streamMode: "updates", config: globalConfig },
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
let runId: string | null = null;
|
|
634
|
-
const seenEventTypes = new Set();
|
|
635
|
-
const seenNodes: string[] = [];
|
|
636
|
-
|
|
637
|
-
let chunk: any;
|
|
638
|
-
for await (chunk of stream) {
|
|
639
|
-
seenEventTypes.add(chunk.event);
|
|
640
|
-
|
|
641
|
-
if (chunk.event === "metadata") {
|
|
642
|
-
runId = chunk.data.run_id;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (chunk.event === "updates") {
|
|
646
|
-
const node = Object.keys(chunk.data)[0];
|
|
647
|
-
seenNodes.push(node);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
expect(seenNodes).toEqual(["agent", "tool", "agent"]);
|
|
652
|
-
|
|
653
|
-
expect(chunk.event).toBe("updates");
|
|
654
|
-
expect(seenEventTypes).toEqual(new Set(["metadata", "updates"]));
|
|
655
|
-
|
|
656
|
-
expect(runId).not.toBeNull();
|
|
657
|
-
const run = await client.runs.get(thread.thread_id, runId as string);
|
|
658
|
-
expect(run.status).toBe("success");
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it.concurrent("stream events", async () => {
|
|
662
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
663
|
-
const thread = await client.threads.create();
|
|
664
|
-
const input = {
|
|
665
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
666
|
-
};
|
|
667
|
-
const stream = client.runs.stream(
|
|
668
|
-
thread.thread_id,
|
|
669
|
-
assistant.assistant_id,
|
|
670
|
-
{ input, streamMode: "events", config: globalConfig },
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
const events = await gatherIterator(stream);
|
|
674
|
-
expect(new Set(events.map((i) => i.event))).toEqual(
|
|
675
|
-
new Set(["metadata", "events"]),
|
|
676
|
-
);
|
|
677
|
-
|
|
678
|
-
expect(
|
|
679
|
-
new Set(
|
|
680
|
-
events
|
|
681
|
-
.filter((i) => i.event === "events")
|
|
682
|
-
.map((i) => (i.data as any).event),
|
|
683
|
-
),
|
|
684
|
-
).toEqual(
|
|
685
|
-
new Set([
|
|
686
|
-
"on_chain_start",
|
|
687
|
-
"on_chain_end",
|
|
688
|
-
"on_chat_model_end",
|
|
689
|
-
"on_chat_model_start",
|
|
690
|
-
"on_chat_model_stream",
|
|
691
|
-
]),
|
|
692
|
-
);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it.concurrent("stream messages", { retry: 3 }, async () => {
|
|
696
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
697
|
-
const thread = await client.threads.create();
|
|
698
|
-
const input = {
|
|
699
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
700
|
-
};
|
|
701
|
-
const stream = client.runs.stream(
|
|
702
|
-
thread.thread_id,
|
|
703
|
-
assistant.assistant_id,
|
|
704
|
-
{ input, streamMode: "messages", config: globalConfig },
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
let runId: string | null = null;
|
|
708
|
-
const seenEventTypes = new Set();
|
|
709
|
-
const messageIdToContent: Record<string, string> = {};
|
|
710
|
-
let lastMessage: any = null;
|
|
711
|
-
|
|
712
|
-
let chunk: any;
|
|
713
|
-
for await (chunk of stream) {
|
|
714
|
-
seenEventTypes.add(chunk.event);
|
|
715
|
-
|
|
716
|
-
if (chunk.event === "metadata") {
|
|
717
|
-
runId = chunk.data.run_id;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (chunk.event === "messages/partial") {
|
|
721
|
-
const message = chunk.data[0];
|
|
722
|
-
messageIdToContent[message.id] = message.content;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (chunk.event === "messages/complete") {
|
|
726
|
-
const message = chunk.data[0];
|
|
727
|
-
expect(message.content).not.toBeNull();
|
|
728
|
-
if (message.type === "ai") {
|
|
729
|
-
expect(message.content).toBe(messageIdToContent[message.id]);
|
|
730
|
-
}
|
|
731
|
-
lastMessage = message;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
expect(lastMessage).not.toBeNull();
|
|
736
|
-
expect(lastMessage.content).toBe("end");
|
|
737
|
-
|
|
738
|
-
expect(chunk.event).toBe("messages/complete");
|
|
739
|
-
expect(seenEventTypes).toEqual(
|
|
740
|
-
new Set([
|
|
741
|
-
"metadata",
|
|
742
|
-
"messages/metadata",
|
|
743
|
-
"messages/partial",
|
|
744
|
-
"messages/complete",
|
|
745
|
-
]),
|
|
746
|
-
);
|
|
747
|
-
|
|
748
|
-
expect(runId).not.toBeNull();
|
|
749
|
-
const run = await client.runs.get(thread.thread_id, runId as string);
|
|
750
|
-
expect(run.status).toBe("success");
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
it.concurrent("stream messages tuple", { retry: 3 }, async () => {
|
|
754
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
755
|
-
const thread = await client.threads.create();
|
|
756
|
-
const input = {
|
|
757
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
758
|
-
};
|
|
759
|
-
const stream = await client.runs.stream(
|
|
760
|
-
thread.thread_id,
|
|
761
|
-
assistant.assistant_id,
|
|
762
|
-
{ input, streamMode: "messages-tuple", config: globalConfig },
|
|
763
|
-
);
|
|
764
|
-
|
|
765
|
-
const chunks = await gatherIterator(stream);
|
|
766
|
-
const runId = findLast(
|
|
767
|
-
chunks,
|
|
768
|
-
(i): i is FeedbackStreamEvent => i.event === "metadata",
|
|
769
|
-
)?.data.run_id;
|
|
770
|
-
expect(runId).not.toBeNull();
|
|
771
|
-
|
|
772
|
-
const messages = chunks
|
|
773
|
-
.filter((i) => i.event === "messages")
|
|
774
|
-
.map((i) => i.data[0]);
|
|
775
|
-
|
|
776
|
-
expect(messages).toHaveLength("begin".length + "end".length + 1);
|
|
777
|
-
expect(messages).toMatchObject([
|
|
778
|
-
..."begin".split("").map((c) => ({ content: c })),
|
|
779
|
-
{ content: "tool_call__begin" },
|
|
780
|
-
..."end".split("").map((c) => ({ content: c })),
|
|
781
|
-
]);
|
|
782
|
-
|
|
783
|
-
const seenEventTypes = new Set(chunks.map((i) => i.event));
|
|
784
|
-
expect(seenEventTypes).toEqual(new Set(["metadata", "messages"]));
|
|
785
|
-
|
|
786
|
-
const run = await client.runs.get(thread.thread_id, runId as string);
|
|
787
|
-
expect(run.status).toBe("success");
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
it.concurrent("stream mixed modes", async () => {
|
|
791
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
792
|
-
const thread = await client.threads.create();
|
|
793
|
-
const input = {
|
|
794
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
795
|
-
};
|
|
796
|
-
const stream = await client.runs.stream(
|
|
797
|
-
thread.thread_id,
|
|
798
|
-
assistant.assistant_id,
|
|
799
|
-
{ input, streamMode: ["messages", "values"], config: globalConfig },
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
const chunks = await gatherIterator(stream);
|
|
803
|
-
expect(chunks.at(-1)?.event).toBe("messages/complete");
|
|
804
|
-
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
|
|
805
|
-
|
|
806
|
-
const messages: BaseMessage[] = findLast(
|
|
807
|
-
chunks,
|
|
808
|
-
(i) => i.event === "values",
|
|
809
|
-
)?.data.messages;
|
|
810
|
-
|
|
811
|
-
expect(messages.length).toBe(4);
|
|
812
|
-
expect(messages.at(-1)?.content).toBe("end");
|
|
813
|
-
|
|
814
|
-
const runId = findLast(chunks, (i) => i.event === "metadata")?.data.run_id;
|
|
815
|
-
expect(runId).not.toBeNull();
|
|
816
|
-
|
|
817
|
-
const seenEventTypes = new Set(chunks.map((i) => i.event));
|
|
818
|
-
expect(seenEventTypes).toEqual(
|
|
819
|
-
new Set([
|
|
820
|
-
"metadata",
|
|
821
|
-
"messages/metadata",
|
|
822
|
-
"messages/partial",
|
|
823
|
-
"messages/complete",
|
|
824
|
-
"values",
|
|
825
|
-
]),
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
const run = await client.runs.get(thread.thread_id, runId!);
|
|
829
|
-
expect(run.status).toBe("success");
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
it.concurrent(
|
|
833
|
-
"human in the loop - no modification",
|
|
834
|
-
{ retry: 3 },
|
|
835
|
-
async () => {
|
|
836
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
837
|
-
const thread = await client.threads.create();
|
|
838
|
-
const input = {
|
|
839
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
840
|
-
};
|
|
841
|
-
let messages: BaseMessage[] = [];
|
|
842
|
-
|
|
843
|
-
// (1) interrupt and then continue running, no modification
|
|
844
|
-
// run until the interrupt
|
|
845
|
-
let chunks = await gatherIterator(
|
|
846
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
847
|
-
input,
|
|
848
|
-
interruptBefore: ["tool"],
|
|
849
|
-
config: globalConfig,
|
|
850
|
-
}),
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
|
|
854
|
-
messages = findLast(chunks, (i) => i.event === "values")?.data.messages;
|
|
855
|
-
|
|
856
|
-
const threadAfterInterrupt = await client.threads.get(thread.thread_id);
|
|
857
|
-
expect(threadAfterInterrupt.status).toBe("interrupted");
|
|
858
|
-
|
|
859
|
-
expect(messages.at(-1)).not.toBeNull();
|
|
860
|
-
expect(messages.at(-1)?.content).toBe("begin");
|
|
861
|
-
|
|
862
|
-
const state = await client.threads.getState(thread.thread_id);
|
|
863
|
-
expect(state.next).toEqual(["tool"]);
|
|
864
|
-
|
|
865
|
-
// continue after interrupt
|
|
866
|
-
chunks = await gatherIterator(
|
|
867
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
868
|
-
input: null,
|
|
869
|
-
config: globalConfig,
|
|
870
|
-
}),
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
|
|
874
|
-
messages = findLast(chunks, (i) => i.event === "values")?.data.messages;
|
|
875
|
-
|
|
876
|
-
expect(messages.length).toBe(4);
|
|
877
|
-
expect(messages[2].content).toBe("tool_call__begin");
|
|
878
|
-
expect(messages.at(-1)?.content).toBe("end");
|
|
879
|
-
|
|
880
|
-
const threadAfterContinue = await client.threads.get(thread.thread_id);
|
|
881
|
-
expect(threadAfterContinue.status).toBe("idle");
|
|
882
|
-
},
|
|
883
|
-
);
|
|
884
|
-
|
|
885
|
-
it.concurrent("human in the loop - modification", { retry: 3 }, async () => {
|
|
886
|
-
// (2) interrupt, modify the message and then continue running
|
|
887
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
888
|
-
const thread = await client.threads.create();
|
|
889
|
-
const input = {
|
|
890
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
891
|
-
};
|
|
892
|
-
let messages: BaseMessage[] = [];
|
|
893
|
-
|
|
894
|
-
// run until the interrupt
|
|
895
|
-
let chunks = await gatherIterator(
|
|
896
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
897
|
-
input,
|
|
898
|
-
interruptBefore: ["tool"],
|
|
899
|
-
config: globalConfig,
|
|
900
|
-
}),
|
|
901
|
-
);
|
|
902
|
-
|
|
903
|
-
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
|
|
904
|
-
|
|
905
|
-
// edit the last message
|
|
906
|
-
const lastMessage = findLast(
|
|
907
|
-
chunks,
|
|
908
|
-
(i) => i.event === "values",
|
|
909
|
-
)?.data.messages.at(-1);
|
|
910
|
-
lastMessage.content = "modified";
|
|
911
|
-
|
|
912
|
-
// update state
|
|
913
|
-
await client.threads.updateState<AgentState>(thread.thread_id, {
|
|
914
|
-
values: { messages: [lastMessage] },
|
|
915
|
-
});
|
|
916
|
-
await client.threads.update(thread.thread_id, {
|
|
917
|
-
metadata: { modified: true },
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
const modifiedThread = await client.threads.get(thread.thread_id);
|
|
921
|
-
expect(modifiedThread.status).toBe("interrupted");
|
|
922
|
-
expect(modifiedThread.metadata?.modified).toBe(true);
|
|
923
|
-
|
|
924
|
-
const stateAfterModify = await client.threads.getState<AgentState>(
|
|
925
|
-
thread.thread_id,
|
|
926
|
-
);
|
|
927
|
-
expect(stateAfterModify.values.messages.at(-1)?.content).toBe("modified");
|
|
928
|
-
expect(stateAfterModify.next).toEqual(["tool"]);
|
|
929
|
-
expect(stateAfterModify.tasks).toMatchObject([
|
|
930
|
-
{ id: expect.any(String), name: "tool", error: null, interrupts: [] },
|
|
931
|
-
]);
|
|
932
|
-
|
|
933
|
-
// continue after interrupt
|
|
934
|
-
chunks = await gatherIterator(
|
|
935
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
936
|
-
input: null,
|
|
937
|
-
config: globalConfig,
|
|
938
|
-
}),
|
|
939
|
-
);
|
|
940
|
-
|
|
941
|
-
const threadAfterContinue = await client.threads.get(thread.thread_id);
|
|
942
|
-
expect(threadAfterContinue.status).toBe("idle");
|
|
943
|
-
|
|
944
|
-
expect(chunks.filter((i) => i.event === "error").length).toBe(0);
|
|
945
|
-
messages = findLast(chunks, (i) => i.event === "values")?.data.messages;
|
|
946
|
-
|
|
947
|
-
expect(messages.length).toBe(4);
|
|
948
|
-
expect(messages[2].content).toBe(`tool_call__modified`);
|
|
949
|
-
expect(messages.at(-1)?.content).toBe("end");
|
|
950
|
-
|
|
951
|
-
// get the history
|
|
952
|
-
const history = await client.threads.getHistory<AgentState>(
|
|
953
|
-
thread.thread_id,
|
|
954
|
-
);
|
|
955
|
-
expect(history.length).toBe(6);
|
|
956
|
-
expect(history[0].next.length).toBe(0);
|
|
957
|
-
expect(history[0].values.messages.length).toBe(4);
|
|
958
|
-
expect(history.at(-1)?.next).toEqual(["__start__"]);
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
it.concurrent("interrupt before", { retry: 3 }, async () => {
|
|
962
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
963
|
-
let thread = await client.threads.create();
|
|
964
|
-
const input = {
|
|
965
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
969
|
-
input,
|
|
970
|
-
interruptBefore: ["agent"],
|
|
971
|
-
config: globalConfig,
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
thread = await client.threads.get(thread.thread_id);
|
|
975
|
-
expect(thread.status).toBe("interrupted");
|
|
976
|
-
});
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
describe("shared state", () => {
|
|
980
|
-
beforeEach(async () => {
|
|
981
|
-
await sql`DELETE FROM store`;
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
it("should share state between runs with the same thread ID", async () => {
|
|
985
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
986
|
-
const thread = await client.threads.create();
|
|
987
|
-
|
|
988
|
-
const input = {
|
|
989
|
-
messages: [
|
|
990
|
-
{ type: "human", content: "should_end", id: "initial-message" },
|
|
991
|
-
],
|
|
992
|
-
};
|
|
993
|
-
const config = { configurable: { user_id: "start_user_id" } };
|
|
994
|
-
|
|
995
|
-
// First run
|
|
996
|
-
const res1 = (await client.runs.wait(
|
|
997
|
-
thread.thread_id,
|
|
998
|
-
assistant.assistant_id,
|
|
999
|
-
{ input, config },
|
|
1000
|
-
)) as Awaited<Record<string, any>>;
|
|
1001
|
-
expect(res1.sharedStateValue).toBe(null);
|
|
1002
|
-
|
|
1003
|
-
// Second run with the same thread ID & config
|
|
1004
|
-
const res2 = (await client.runs.wait(
|
|
1005
|
-
thread.thread_id,
|
|
1006
|
-
assistant.assistant_id,
|
|
1007
|
-
{ input, config },
|
|
1008
|
-
)) as Awaited<Record<string, any>>;
|
|
1009
|
-
expect(res2.sharedStateValue).toBe(config.configurable.user_id);
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
it("should not share state between runs with different thread IDs", async () => {
|
|
1013
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
1014
|
-
const thread = await client.threads.create();
|
|
1015
|
-
|
|
1016
|
-
const input = {
|
|
1017
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
// Run with the default `globalConfig`
|
|
1021
|
-
const config1 = { configurable: { user_id: "start_user_id" } };
|
|
1022
|
-
const res1 = (await client.runs.wait(
|
|
1023
|
-
thread.thread_id,
|
|
1024
|
-
assistant.assistant_id,
|
|
1025
|
-
{ input, config: config1 },
|
|
1026
|
-
)) as Awaited<Record<string, any>>;
|
|
1027
|
-
|
|
1028
|
-
// Run with the same thread id but a new config
|
|
1029
|
-
const config2 = { configurable: { user_id: "new_user_id" } };
|
|
1030
|
-
const res2 = (await client.runs.wait(
|
|
1031
|
-
thread.thread_id,
|
|
1032
|
-
assistant.assistant_id,
|
|
1033
|
-
{ input, config: config2 },
|
|
1034
|
-
)) as Awaited<Record<string, any>>;
|
|
1035
|
-
|
|
1036
|
-
expect(res1.sharedStateValue).toBe(config1.configurable.user_id);
|
|
1037
|
-
// Null on first iteration since the shared value is set in the second iteration
|
|
1038
|
-
expect(res2.sharedStateValue).toBe(config2.configurable.user_id);
|
|
1039
|
-
expect(res1.sharedStateValue).not.toBe(res2.sharedStateValue);
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
it("should be able to set and return data from store in config", async () => {
|
|
1043
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
1044
|
-
const thread = await client.threads.create();
|
|
1045
|
-
|
|
1046
|
-
const input = {
|
|
1047
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
1048
|
-
};
|
|
1049
|
-
const config = {
|
|
1050
|
-
configurable: {
|
|
1051
|
-
user_id: "start_user_id",
|
|
1052
|
-
},
|
|
1053
|
-
};
|
|
1054
|
-
|
|
1055
|
-
// Run with the default `globalConfig`
|
|
1056
|
-
const res1 = (await client.runs.wait(
|
|
1057
|
-
thread.thread_id,
|
|
1058
|
-
assistant.assistant_id,
|
|
1059
|
-
{ input, config },
|
|
1060
|
-
)) as Awaited<Record<string, any>>;
|
|
1061
|
-
expect(res1.sharedStateFromStoreConfig).toBeDefined();
|
|
1062
|
-
expect(res1.sharedStateFromStoreConfig.id).toBeDefined();
|
|
1063
|
-
expect(res1.sharedStateFromStoreConfig.id).toBe(
|
|
1064
|
-
config.configurable.user_id,
|
|
1065
|
-
);
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
it("Should be able to use the store client to fetch values", async () => {
|
|
1069
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
1070
|
-
const thread = await client.threads.create();
|
|
1071
|
-
|
|
1072
|
-
const input = {
|
|
1073
|
-
messages: [{ type: "human", content: "foo", id: "initial-message" }],
|
|
1074
|
-
};
|
|
1075
|
-
const config = {
|
|
1076
|
-
configurable: {
|
|
1077
|
-
user_id: "start_user_id",
|
|
1078
|
-
},
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
// For shared state
|
|
1082
|
-
const namespace = ["sharedState", "data"];
|
|
1083
|
-
const key = "user_id";
|
|
1084
|
-
|
|
1085
|
-
// Run with the default `globalConfig`
|
|
1086
|
-
const res1 = (await client.runs.wait(
|
|
1087
|
-
thread.thread_id,
|
|
1088
|
-
assistant.assistant_id,
|
|
1089
|
-
{ input, config },
|
|
1090
|
-
)) as Awaited<Record<string, any>>;
|
|
1091
|
-
expect(res1.sharedStateFromStoreConfig).toBeDefined();
|
|
1092
|
-
expect(res1.sharedStateFromStoreConfig.id).toBeDefined();
|
|
1093
|
-
expect(res1.sharedStateFromStoreConfig.id).toBe(
|
|
1094
|
-
config.configurable.user_id,
|
|
1095
|
-
);
|
|
1096
|
-
|
|
1097
|
-
// Fetch data from store client
|
|
1098
|
-
const storeRes = await client.store.getItem(namespace, key);
|
|
1099
|
-
expect(storeRes).toBeDefined();
|
|
1100
|
-
expect(storeRes?.value).toBeDefined();
|
|
1101
|
-
expect(storeRes?.value).toEqual({ id: config.configurable.user_id });
|
|
1102
|
-
});
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
describe("StoreClient", () => {
|
|
1106
|
-
beforeEach(async () => {
|
|
1107
|
-
await sql`DELETE FROM store`;
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
it("Should be able to use the store client methods", async () => {
|
|
1111
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
1112
|
-
const thread = await client.threads.create();
|
|
1113
|
-
|
|
1114
|
-
const input = {
|
|
1115
|
-
messages: [
|
|
1116
|
-
{
|
|
1117
|
-
type: "human",
|
|
1118
|
-
content: "___check_state_value",
|
|
1119
|
-
id: "initial-message",
|
|
1120
|
-
},
|
|
1121
|
-
],
|
|
1122
|
-
};
|
|
1123
|
-
const config = {
|
|
1124
|
-
configurable: {
|
|
1125
|
-
user_id: "start_user_id",
|
|
1126
|
-
},
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
// For shared state
|
|
1130
|
-
const namespace = ["inputtedState", "data"];
|
|
1131
|
-
const key = "my_key";
|
|
1132
|
-
|
|
1133
|
-
// Set the value
|
|
1134
|
-
await client.store.putItem(namespace, key, { isTrue: true });
|
|
1135
|
-
|
|
1136
|
-
// Invoke the graph and ensure the value is set
|
|
1137
|
-
// When the graph is invoked with this input, it will route to
|
|
1138
|
-
// a special node that throws an error if the value is not set.
|
|
1139
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
1140
|
-
input,
|
|
1141
|
-
config,
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
// Verify it can be fetched
|
|
1145
|
-
const storeRes = await client.store.getItem(namespace, key);
|
|
1146
|
-
expect(storeRes).toBeDefined();
|
|
1147
|
-
expect(storeRes?.value).toBeDefined();
|
|
1148
|
-
expect(storeRes?.value).toEqual({ isTrue: true });
|
|
1149
|
-
|
|
1150
|
-
await client.store.deleteItem(namespace, key);
|
|
1151
|
-
const storeResAfterDelete = await client.store.getItem(namespace, key);
|
|
1152
|
-
expect(storeResAfterDelete).toBe(null);
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
it("Can put, search, list, get and delete", async () => {
|
|
1156
|
-
const namespace = ["allMethods", "data"];
|
|
1157
|
-
const key = randomUUID();
|
|
1158
|
-
const value = { foo: "bar" };
|
|
1159
|
-
|
|
1160
|
-
// Try searching when no values are present.
|
|
1161
|
-
const searchRes = await client.store.searchItems(namespace);
|
|
1162
|
-
expect(searchRes.items).toBeDefined();
|
|
1163
|
-
expect(searchRes.items.length).toBe(0);
|
|
1164
|
-
|
|
1165
|
-
// Try listing when no values are present.
|
|
1166
|
-
const listRes = await client.store.listNamespaces();
|
|
1167
|
-
expect(listRes.namespaces).toBeDefined();
|
|
1168
|
-
expect(listRes.namespaces.length).toBe(0);
|
|
1169
|
-
|
|
1170
|
-
// Put an item
|
|
1171
|
-
await client.store.putItem(namespace, key, value);
|
|
1172
|
-
|
|
1173
|
-
// Get the item
|
|
1174
|
-
const getRes = await client.store.getItem(namespace, key);
|
|
1175
|
-
expect(getRes).toBeDefined();
|
|
1176
|
-
expect(getRes?.value).toEqual(value);
|
|
1177
|
-
|
|
1178
|
-
const searchResAfterPut = await client.store.searchItems(namespace);
|
|
1179
|
-
expect(searchResAfterPut.items).toBeDefined();
|
|
1180
|
-
expect(searchResAfterPut.items.length).toBe(1);
|
|
1181
|
-
expect(searchResAfterPut.items[0].key).toBe(key);
|
|
1182
|
-
expect(searchResAfterPut.items[0].value).toEqual(value);
|
|
1183
|
-
expect(searchResAfterPut.items[0].createdAt).toBeDefined();
|
|
1184
|
-
expect(searchResAfterPut.items[0].updatedAt).toBeDefined();
|
|
1185
|
-
expect(
|
|
1186
|
-
new Date(searchResAfterPut.items[0].createdAt).getTime(),
|
|
1187
|
-
).toBeLessThanOrEqual(Date.now());
|
|
1188
|
-
expect(
|
|
1189
|
-
new Date(searchResAfterPut.items[0].updatedAt).getTime(),
|
|
1190
|
-
).toBeLessThanOrEqual(Date.now());
|
|
1191
|
-
|
|
1192
|
-
const updatedValue = { foo: "baz" };
|
|
1193
|
-
await client.store.putItem(namespace, key, updatedValue);
|
|
1194
|
-
|
|
1195
|
-
const getResAfterUpdate = await client.store.getItem(namespace, key);
|
|
1196
|
-
expect(getResAfterUpdate).toBeDefined();
|
|
1197
|
-
expect(getResAfterUpdate?.value).toEqual(updatedValue);
|
|
1198
|
-
|
|
1199
|
-
const searchResAfterUpdate = await client.store.searchItems(namespace);
|
|
1200
|
-
expect(searchResAfterUpdate.items).toBeDefined();
|
|
1201
|
-
expect(searchResAfterUpdate.items.length).toBe(1);
|
|
1202
|
-
expect(searchResAfterUpdate.items[0].key).toBe(key);
|
|
1203
|
-
expect(searchResAfterUpdate.items[0].value).toEqual(updatedValue);
|
|
1204
|
-
|
|
1205
|
-
expect(
|
|
1206
|
-
new Date(searchResAfterUpdate.items[0].updatedAt).getTime(),
|
|
1207
|
-
).toBeGreaterThan(new Date(searchResAfterPut.items[0].updatedAt).getTime());
|
|
1208
|
-
|
|
1209
|
-
const listResAfterPut = await client.store.listNamespaces();
|
|
1210
|
-
expect(listResAfterPut.namespaces).toBeDefined();
|
|
1211
|
-
expect(listResAfterPut.namespaces.length).toBe(1);
|
|
1212
|
-
expect(listResAfterPut.namespaces[0]).toEqual(namespace);
|
|
1213
|
-
|
|
1214
|
-
await client.store.deleteItem(namespace, key);
|
|
1215
|
-
|
|
1216
|
-
const getResAfterDelete = await client.store.getItem(namespace, key);
|
|
1217
|
-
expect(getResAfterDelete).toBeNull();
|
|
1218
|
-
|
|
1219
|
-
const searchResAfterDelete = await client.store.searchItems(namespace);
|
|
1220
|
-
expect(searchResAfterDelete.items).toBeDefined();
|
|
1221
|
-
expect(searchResAfterDelete.items.length).toBe(0);
|
|
1222
|
-
});
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
describe("subgraphs", () => {
|
|
1226
|
-
it.concurrent("get subgraphs", async () => {
|
|
1227
|
-
const assistant = await client.assistants.create({ graphId: "nested" });
|
|
1228
|
-
|
|
1229
|
-
expect(
|
|
1230
|
-
Object.keys(await client.assistants.getSubgraphs(assistant.assistant_id)),
|
|
1231
|
-
).toEqual(["gp_two"]);
|
|
1232
|
-
|
|
1233
|
-
const subgraphs = await client.assistants.getSubgraphs(
|
|
1234
|
-
assistant.assistant_id,
|
|
1235
|
-
{ recurse: true },
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1238
|
-
expect(Object.keys(subgraphs)).toEqual(["gp_two", "gp_two|p_two"]);
|
|
1239
|
-
expect(subgraphs).toMatchObject({
|
|
1240
|
-
gp_two: {
|
|
1241
|
-
state: {
|
|
1242
|
-
type: "object",
|
|
1243
|
-
properties: {
|
|
1244
|
-
parent: {
|
|
1245
|
-
type: "string",
|
|
1246
|
-
enum: ["parent_one", "parent_two"],
|
|
1247
|
-
},
|
|
1248
|
-
messages: { type: "array" },
|
|
1249
|
-
},
|
|
1250
|
-
},
|
|
1251
|
-
},
|
|
1252
|
-
"gp_two|p_two": {
|
|
1253
|
-
state: {
|
|
1254
|
-
type: "object",
|
|
1255
|
-
properties: {
|
|
1256
|
-
child: {
|
|
1257
|
-
type: "string",
|
|
1258
|
-
enum: ["child_one", "child_two"],
|
|
1259
|
-
},
|
|
1260
|
-
messages: { type: "array" },
|
|
1261
|
-
},
|
|
1262
|
-
},
|
|
1263
|
-
},
|
|
1264
|
-
});
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
// (1) interrupt and then continue running, no modification
|
|
1268
|
-
it.concurrent(
|
|
1269
|
-
"human in the loop - no modification",
|
|
1270
|
-
{ retry: 3 },
|
|
1271
|
-
async () => {
|
|
1272
|
-
const assistant = await client.assistants.create({ graphId: "weather" });
|
|
1273
|
-
const thread = await client.threads.create();
|
|
1274
|
-
|
|
1275
|
-
// run until the interrupt
|
|
1276
|
-
let lastMessageBeforeInterrupt: { content?: string } | null = null;
|
|
1277
|
-
let chunks = await gatherIterator(
|
|
1278
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1279
|
-
input: {
|
|
1280
|
-
messages: [{ role: "human", content: "SF", id: "initial-message" }],
|
|
1281
|
-
},
|
|
1282
|
-
interruptBefore: ["tool"],
|
|
1283
|
-
}),
|
|
1284
|
-
);
|
|
1285
|
-
|
|
1286
|
-
for (const chunk of chunks) {
|
|
1287
|
-
if (chunk.event === "values") {
|
|
1288
|
-
lastMessageBeforeInterrupt =
|
|
1289
|
-
chunk.data.messages[chunk.data.messages.length - 1];
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
if (chunk.event === "error") {
|
|
1293
|
-
throw new Error(chunk.data.error);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
expect(lastMessageBeforeInterrupt?.content).toBe("SF");
|
|
1298
|
-
expect(chunks).toEqual([
|
|
1299
|
-
{ event: "metadata", data: { run_id: expect.any(String), attempt: 1 } },
|
|
1300
|
-
{
|
|
1301
|
-
event: "values",
|
|
1302
|
-
data: {
|
|
1303
|
-
messages: [
|
|
1304
|
-
{
|
|
1305
|
-
content: "SF",
|
|
1306
|
-
additional_kwargs: {},
|
|
1307
|
-
response_metadata: {},
|
|
1308
|
-
type: "human",
|
|
1309
|
-
id: "initial-message",
|
|
1310
|
-
},
|
|
1311
|
-
],
|
|
1312
|
-
},
|
|
1313
|
-
},
|
|
1314
|
-
{
|
|
1315
|
-
event: "values",
|
|
1316
|
-
data: {
|
|
1317
|
-
messages: [
|
|
1318
|
-
{
|
|
1319
|
-
content: "SF",
|
|
1320
|
-
additional_kwargs: {},
|
|
1321
|
-
response_metadata: {},
|
|
1322
|
-
type: "human",
|
|
1323
|
-
id: "initial-message",
|
|
1324
|
-
},
|
|
1325
|
-
],
|
|
1326
|
-
route: "weather",
|
|
1327
|
-
},
|
|
1328
|
-
},
|
|
1329
|
-
]);
|
|
1330
|
-
|
|
1331
|
-
let state = await client.threads.getState(thread.thread_id);
|
|
1332
|
-
expect(state.next).toEqual(["weather_graph"]);
|
|
1333
|
-
expect(state.tasks).toEqual([
|
|
1334
|
-
{
|
|
1335
|
-
id: expect.any(String),
|
|
1336
|
-
name: "weather_graph",
|
|
1337
|
-
path: ["__pregel_pull", "weather_graph"],
|
|
1338
|
-
error: null,
|
|
1339
|
-
interrupts: [],
|
|
1340
|
-
checkpoint: {
|
|
1341
|
-
checkpoint_ns: expect.stringMatching(/^weather_graph:/),
|
|
1342
|
-
thread_id: expect.any(String),
|
|
1343
|
-
},
|
|
1344
|
-
state: null,
|
|
1345
|
-
result: null,
|
|
1346
|
-
},
|
|
1347
|
-
]);
|
|
1348
|
-
|
|
1349
|
-
const stateRecursive = await client.threads.getState(
|
|
1350
|
-
thread.thread_id,
|
|
1351
|
-
undefined,
|
|
1352
|
-
{ subgraphs: true },
|
|
1353
|
-
);
|
|
1354
|
-
|
|
1355
|
-
expect(stateRecursive.next).toEqual(["weather_graph"]);
|
|
1356
|
-
expect(stateRecursive.tasks).toEqual([
|
|
1357
|
-
{
|
|
1358
|
-
id: expect.any(String),
|
|
1359
|
-
name: "weather_graph",
|
|
1360
|
-
path: ["__pregel_pull", "weather_graph"],
|
|
1361
|
-
error: null,
|
|
1362
|
-
interrupts: [],
|
|
1363
|
-
checkpoint: null,
|
|
1364
|
-
result: null,
|
|
1365
|
-
state: {
|
|
1366
|
-
values: {
|
|
1367
|
-
city: "San Francisco",
|
|
1368
|
-
messages: [
|
|
1369
|
-
{
|
|
1370
|
-
content: "SF",
|
|
1371
|
-
additional_kwargs: {},
|
|
1372
|
-
response_metadata: {},
|
|
1373
|
-
type: "human",
|
|
1374
|
-
id: "initial-message",
|
|
1375
|
-
},
|
|
1376
|
-
],
|
|
1377
|
-
},
|
|
1378
|
-
next: ["weather_node"],
|
|
1379
|
-
tasks: [
|
|
1380
|
-
{
|
|
1381
|
-
id: expect.any(String),
|
|
1382
|
-
name: "weather_node",
|
|
1383
|
-
path: ["__pregel_pull", "weather_node"],
|
|
1384
|
-
error: null,
|
|
1385
|
-
interrupts: [],
|
|
1386
|
-
checkpoint: null,
|
|
1387
|
-
state: null,
|
|
1388
|
-
result: null,
|
|
1389
|
-
},
|
|
1390
|
-
],
|
|
1391
|
-
metadata: expect.any(Object),
|
|
1392
|
-
created_at: expect.any(String),
|
|
1393
|
-
checkpoint: expect.any(Object),
|
|
1394
|
-
parent_checkpoint: expect.any(Object),
|
|
1395
|
-
checkpoint_id: expect.any(String),
|
|
1396
|
-
parent_checkpoint_id: expect.any(String),
|
|
1397
|
-
},
|
|
1398
|
-
},
|
|
1399
|
-
]);
|
|
1400
|
-
|
|
1401
|
-
const threadAfterInterrupt = await client.threads.get(thread.thread_id);
|
|
1402
|
-
expect(threadAfterInterrupt.status).toBe("interrupted");
|
|
1403
|
-
|
|
1404
|
-
// continue after interrupt
|
|
1405
|
-
const chunksSubgraph = await gatherIterator(
|
|
1406
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1407
|
-
input: null,
|
|
1408
|
-
streamMode: ["values", "updates"],
|
|
1409
|
-
streamSubgraphs: true,
|
|
1410
|
-
}),
|
|
1411
|
-
);
|
|
1412
|
-
|
|
1413
|
-
expect(chunksSubgraph.filter((i) => i.event === "error")).toEqual([]);
|
|
1414
|
-
expect(chunksSubgraph.at(-1)?.event).toBe("values");
|
|
1415
|
-
|
|
1416
|
-
type ChunkType = (typeof chunksSubgraph)[number];
|
|
1417
|
-
const continueMessages = findLast(
|
|
1418
|
-
chunksSubgraph,
|
|
1419
|
-
(i): i is ChunkType & { event: "values" } => i.event === "values",
|
|
1420
|
-
)?.data.messages;
|
|
1421
|
-
|
|
1422
|
-
expect(continueMessages.length).toBe(2);
|
|
1423
|
-
expect(continueMessages[0].content).toBe("SF");
|
|
1424
|
-
expect(continueMessages[1].content).toBe("It's sunny in San Francisco!");
|
|
1425
|
-
expect(chunksSubgraph).toEqual([
|
|
1426
|
-
{
|
|
1427
|
-
event: "metadata",
|
|
1428
|
-
data: { run_id: expect.any(String), attempt: 1 },
|
|
1429
|
-
},
|
|
1430
|
-
{
|
|
1431
|
-
event: "values",
|
|
1432
|
-
data: {
|
|
1433
|
-
messages: [
|
|
1434
|
-
{
|
|
1435
|
-
content: "SF",
|
|
1436
|
-
additional_kwargs: {},
|
|
1437
|
-
response_metadata: {},
|
|
1438
|
-
type: "human",
|
|
1439
|
-
id: "initial-message",
|
|
1440
|
-
},
|
|
1441
|
-
],
|
|
1442
|
-
route: "weather",
|
|
1443
|
-
},
|
|
1444
|
-
},
|
|
1445
|
-
{
|
|
1446
|
-
event: expect.stringMatching(/^values\|weather_graph:/),
|
|
1447
|
-
data: {
|
|
1448
|
-
messages: [
|
|
1449
|
-
{
|
|
1450
|
-
content: "SF",
|
|
1451
|
-
additional_kwargs: {},
|
|
1452
|
-
response_metadata: {},
|
|
1453
|
-
type: "human",
|
|
1454
|
-
id: "initial-message",
|
|
1455
|
-
},
|
|
1456
|
-
],
|
|
1457
|
-
city: "San Francisco",
|
|
1458
|
-
},
|
|
1459
|
-
},
|
|
1460
|
-
{
|
|
1461
|
-
event: expect.stringMatching(/^updates\|weather_graph:/),
|
|
1462
|
-
data: {
|
|
1463
|
-
weather_node: {
|
|
1464
|
-
messages: [
|
|
1465
|
-
{
|
|
1466
|
-
content: "It's sunny in San Francisco!",
|
|
1467
|
-
additional_kwargs: {},
|
|
1468
|
-
response_metadata: {},
|
|
1469
|
-
type: "ai",
|
|
1470
|
-
id: expect.any(String),
|
|
1471
|
-
tool_calls: [],
|
|
1472
|
-
invalid_tool_calls: [],
|
|
1473
|
-
},
|
|
1474
|
-
],
|
|
1475
|
-
},
|
|
1476
|
-
},
|
|
1477
|
-
},
|
|
1478
|
-
{
|
|
1479
|
-
event: expect.stringMatching(/^values\|weather_graph:/),
|
|
1480
|
-
data: {
|
|
1481
|
-
messages: [
|
|
1482
|
-
{
|
|
1483
|
-
content: "SF",
|
|
1484
|
-
additional_kwargs: {},
|
|
1485
|
-
response_metadata: {},
|
|
1486
|
-
type: "human",
|
|
1487
|
-
id: "initial-message",
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
content: "It's sunny in San Francisco!",
|
|
1491
|
-
additional_kwargs: {},
|
|
1492
|
-
response_metadata: {},
|
|
1493
|
-
type: "ai",
|
|
1494
|
-
id: expect.any(String),
|
|
1495
|
-
tool_calls: [],
|
|
1496
|
-
invalid_tool_calls: [],
|
|
1497
|
-
},
|
|
1498
|
-
],
|
|
1499
|
-
city: "San Francisco",
|
|
1500
|
-
},
|
|
1501
|
-
},
|
|
1502
|
-
{
|
|
1503
|
-
event: "updates",
|
|
1504
|
-
data: {
|
|
1505
|
-
weather_graph: {
|
|
1506
|
-
messages: [
|
|
1507
|
-
{
|
|
1508
|
-
content: "SF",
|
|
1509
|
-
additional_kwargs: {},
|
|
1510
|
-
response_metadata: {},
|
|
1511
|
-
type: "human",
|
|
1512
|
-
id: "initial-message",
|
|
1513
|
-
},
|
|
1514
|
-
{
|
|
1515
|
-
content: "It's sunny in San Francisco!",
|
|
1516
|
-
additional_kwargs: {},
|
|
1517
|
-
response_metadata: {},
|
|
1518
|
-
type: "ai",
|
|
1519
|
-
id: expect.any(String),
|
|
1520
|
-
tool_calls: [],
|
|
1521
|
-
invalid_tool_calls: [],
|
|
1522
|
-
},
|
|
1523
|
-
],
|
|
1524
|
-
},
|
|
1525
|
-
},
|
|
1526
|
-
},
|
|
1527
|
-
{
|
|
1528
|
-
event: "values",
|
|
1529
|
-
data: {
|
|
1530
|
-
messages: [
|
|
1531
|
-
{
|
|
1532
|
-
content: "SF",
|
|
1533
|
-
additional_kwargs: {},
|
|
1534
|
-
response_metadata: {},
|
|
1535
|
-
type: "human",
|
|
1536
|
-
id: "initial-message",
|
|
1537
|
-
},
|
|
1538
|
-
{
|
|
1539
|
-
content: "It's sunny in San Francisco!",
|
|
1540
|
-
additional_kwargs: {},
|
|
1541
|
-
response_metadata: {},
|
|
1542
|
-
type: "ai",
|
|
1543
|
-
id: expect.any(String),
|
|
1544
|
-
tool_calls: [],
|
|
1545
|
-
invalid_tool_calls: [],
|
|
1546
|
-
},
|
|
1547
|
-
],
|
|
1548
|
-
route: "weather",
|
|
1549
|
-
},
|
|
1550
|
-
},
|
|
1551
|
-
]);
|
|
1552
|
-
|
|
1553
|
-
const threadAfterContinue = await client.threads.get(thread.thread_id);
|
|
1554
|
-
expect(threadAfterContinue.status).toBe("idle");
|
|
1555
|
-
},
|
|
1556
|
-
);
|
|
1557
|
-
|
|
1558
|
-
// (2) interrupt, modify the message and then continue running
|
|
1559
|
-
it.concurrent("human in the loop - modification", { retry: 3 }, async () => {
|
|
1560
|
-
const assistant = await client.assistants.create({ graphId: "weather" });
|
|
1561
|
-
const thread = await client.threads.create();
|
|
1562
|
-
const input = {
|
|
1563
|
-
messages: [{ role: "human", content: "SF", id: "initial-message" }],
|
|
1564
|
-
};
|
|
1565
|
-
|
|
1566
|
-
// run until the interrupt (same as before)
|
|
1567
|
-
let chunks = await gatherIterator(
|
|
1568
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, { input }),
|
|
1569
|
-
);
|
|
1570
|
-
expect(chunks.filter((i) => i.event === "error")).toEqual([]);
|
|
1571
|
-
|
|
1572
|
-
// get state after interrupt
|
|
1573
|
-
const state = await client.threads.getState(thread.thread_id);
|
|
1574
|
-
expect(state.next).toEqual(["weather_graph"]);
|
|
1575
|
-
expect(state.tasks).toEqual([
|
|
1576
|
-
{
|
|
1577
|
-
id: expect.any(String),
|
|
1578
|
-
name: "weather_graph",
|
|
1579
|
-
path: ["__pregel_pull", "weather_graph"],
|
|
1580
|
-
error: null,
|
|
1581
|
-
interrupts: [],
|
|
1582
|
-
checkpoint: {
|
|
1583
|
-
checkpoint_ns: expect.stringMatching(/^weather_graph:/),
|
|
1584
|
-
thread_id: expect.any(String),
|
|
1585
|
-
},
|
|
1586
|
-
state: null,
|
|
1587
|
-
result: null,
|
|
1588
|
-
},
|
|
1589
|
-
]);
|
|
1590
|
-
|
|
1591
|
-
// edit the city in the subgraph state
|
|
1592
|
-
await client.threads.updateState(thread.thread_id, {
|
|
1593
|
-
values: { city: "LA" },
|
|
1594
|
-
checkpoint: state.tasks[0].checkpoint ?? undefined,
|
|
1595
|
-
});
|
|
1596
|
-
|
|
1597
|
-
// get inner state after update
|
|
1598
|
-
const innerState = await client.threads.getState<{ city: string }>(
|
|
1599
|
-
thread.thread_id,
|
|
1600
|
-
state.tasks[0].checkpoint ?? undefined,
|
|
1601
|
-
);
|
|
1602
|
-
|
|
1603
|
-
expect(innerState.values.city).toBe("LA");
|
|
1604
|
-
expect(innerState.next).toEqual(["weather_node"]);
|
|
1605
|
-
expect(innerState.tasks).toEqual([
|
|
1606
|
-
{
|
|
1607
|
-
id: expect.any(String),
|
|
1608
|
-
name: "weather_node",
|
|
1609
|
-
path: ["__pregel_pull", "weather_node"],
|
|
1610
|
-
error: null,
|
|
1611
|
-
interrupts: [],
|
|
1612
|
-
checkpoint: null,
|
|
1613
|
-
state: null,
|
|
1614
|
-
result: null,
|
|
1615
|
-
},
|
|
1616
|
-
]);
|
|
1617
|
-
|
|
1618
|
-
// continue after interrupt
|
|
1619
|
-
chunks = await gatherIterator(
|
|
1620
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1621
|
-
input: null,
|
|
1622
|
-
}),
|
|
1623
|
-
);
|
|
1624
|
-
|
|
1625
|
-
expect(chunks.filter((i) => i.event === "error")).toEqual([]);
|
|
1626
|
-
expect(chunks.at(-1)?.event).toBe("values");
|
|
1627
|
-
|
|
1628
|
-
const continueMessages = findLast(chunks, (i) => i.event === "values")?.data
|
|
1629
|
-
.messages;
|
|
1630
|
-
|
|
1631
|
-
expect(continueMessages.length).toBe(2);
|
|
1632
|
-
expect(continueMessages[0].content).toBe("SF");
|
|
1633
|
-
expect(continueMessages[1].content).toBe("It's sunny in LA!");
|
|
1634
|
-
|
|
1635
|
-
// get the history for the root graph
|
|
1636
|
-
const history = await client.threads.getHistory<{
|
|
1637
|
-
messages: BaseMessageLike[];
|
|
1638
|
-
}>(thread.thread_id);
|
|
1639
|
-
expect(history.length).toBe(4);
|
|
1640
|
-
expect(history[0].next.length).toBe(0);
|
|
1641
|
-
expect(history[0].values.messages.length).toBe(2);
|
|
1642
|
-
expect(history[history.length - 1].next).toEqual(["__start__"]);
|
|
1643
|
-
|
|
1644
|
-
// get inner history
|
|
1645
|
-
const innerHistory = await client.threads.getHistory<{
|
|
1646
|
-
messages: BaseMessageLike[];
|
|
1647
|
-
city: string;
|
|
1648
|
-
}>(thread.thread_id, {
|
|
1649
|
-
checkpoint: state.tasks[0].checkpoint ?? undefined,
|
|
1650
|
-
});
|
|
1651
|
-
expect(innerHistory.length).toBe(5);
|
|
1652
|
-
expect(innerHistory[0].next.length).toBe(0);
|
|
1653
|
-
expect(innerHistory[0].values.messages.length).toBe(2);
|
|
1654
|
-
expect(innerHistory[innerHistory.length - 1].next).toEqual(["__start__"]);
|
|
1655
|
-
});
|
|
1656
|
-
|
|
1657
|
-
it.concurrent("interrupt inside node", { retry: 3 }, async () => {
|
|
1658
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
1659
|
-
|
|
1660
|
-
let thread = await client.threads.create();
|
|
1661
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
1662
|
-
input: {
|
|
1663
|
-
messages: [{ role: "human", content: "SF", id: "initial-message" }],
|
|
1664
|
-
interrupt: true,
|
|
1665
|
-
},
|
|
1666
|
-
config: globalConfig,
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
const state = await client.threads.getState(thread.thread_id);
|
|
1670
|
-
expect(state.next).toEqual(["agent"]);
|
|
1671
|
-
expect(state.tasks).toMatchObject([
|
|
1672
|
-
{
|
|
1673
|
-
id: expect.any(String),
|
|
1674
|
-
name: "agent",
|
|
1675
|
-
path: ["__pregel_pull", "agent"],
|
|
1676
|
-
error: null,
|
|
1677
|
-
interrupts: [
|
|
1678
|
-
{
|
|
1679
|
-
value: "i want to interrupt",
|
|
1680
|
-
when: "during",
|
|
1681
|
-
resumable: true,
|
|
1682
|
-
ns: [expect.stringMatching(/^agent:/)],
|
|
1683
|
-
},
|
|
1684
|
-
],
|
|
1685
|
-
checkpoint: null,
|
|
1686
|
-
state: null,
|
|
1687
|
-
result: null,
|
|
1688
|
-
},
|
|
1689
|
-
]);
|
|
1690
|
-
|
|
1691
|
-
thread = await client.threads.get(thread.thread_id);
|
|
1692
|
-
expect(thread.status).toBe("interrupted");
|
|
1693
|
-
expect(thread.interrupts).toMatchObject({
|
|
1694
|
-
[state.tasks[0].id]: [
|
|
1695
|
-
{
|
|
1696
|
-
value: "i want to interrupt",
|
|
1697
|
-
when: "during",
|
|
1698
|
-
resumable: true,
|
|
1699
|
-
ns: [expect.stringMatching(/^agent:/)],
|
|
1700
|
-
},
|
|
1701
|
-
],
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
const stream = await gatherIterator(
|
|
1705
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1706
|
-
command: { resume: "i want to resume" },
|
|
1707
|
-
}),
|
|
1708
|
-
);
|
|
1709
|
-
|
|
1710
|
-
expect(stream.at(-1)?.event).toBe("values");
|
|
1711
|
-
expect(stream.at(-1)?.data.messages.length).toBe(4);
|
|
1712
|
-
});
|
|
1713
|
-
});
|
|
1714
|
-
|
|
1715
|
-
describe("errors", () => {
|
|
1716
|
-
it.concurrent("stream", async () => {
|
|
1717
|
-
const assistant = await client.assistants.create({ graphId: "error" });
|
|
1718
|
-
const thread = await client.threads.create();
|
|
1719
|
-
|
|
1720
|
-
const stream = await gatherIterator(
|
|
1721
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1722
|
-
input: { messages: [] },
|
|
1723
|
-
streamMode: ["debug", "events"],
|
|
1724
|
-
}),
|
|
1725
|
-
);
|
|
1726
|
-
|
|
1727
|
-
expect(stream.at(-1)).toMatchObject({
|
|
1728
|
-
event: "error",
|
|
1729
|
-
data: {
|
|
1730
|
-
error: "CustomError",
|
|
1731
|
-
message: "Boo!",
|
|
1732
|
-
},
|
|
1733
|
-
});
|
|
1734
|
-
});
|
|
1735
|
-
|
|
1736
|
-
it.concurrent("create + join", async () => {
|
|
1737
|
-
const assistant = await client.assistants.create({ graphId: "error" });
|
|
1738
|
-
const thread = await client.threads.create();
|
|
1739
|
-
|
|
1740
|
-
const run = await client.runs.create(
|
|
1741
|
-
thread.thread_id,
|
|
1742
|
-
assistant.assistant_id,
|
|
1743
|
-
{ input: { messages: [] } },
|
|
1744
|
-
);
|
|
1745
|
-
|
|
1746
|
-
await client.runs.join(thread.thread_id, run.run_id);
|
|
1747
|
-
const runState = await client.runs.get(thread.thread_id, run.run_id);
|
|
1748
|
-
expect(runState.status).toEqual("error");
|
|
1749
|
-
});
|
|
1750
|
-
|
|
1751
|
-
it.concurrent("create + stream join", async () => {
|
|
1752
|
-
const assistant = await client.assistants.create({ graphId: "error" });
|
|
1753
|
-
const thread = await client.threads.create();
|
|
1754
|
-
|
|
1755
|
-
const run = await client.runs.create(
|
|
1756
|
-
thread.thread_id,
|
|
1757
|
-
assistant.assistant_id,
|
|
1758
|
-
{ input: { messages: [] } },
|
|
1759
|
-
);
|
|
1760
|
-
|
|
1761
|
-
const stream = await gatherIterator(
|
|
1762
|
-
client.runs.joinStream(thread.thread_id, run.run_id),
|
|
1763
|
-
);
|
|
1764
|
-
|
|
1765
|
-
expect(stream.at(-1)).toMatchObject({
|
|
1766
|
-
event: "error",
|
|
1767
|
-
data: {
|
|
1768
|
-
error: "CustomError",
|
|
1769
|
-
message: "Boo!",
|
|
1770
|
-
},
|
|
1771
|
-
});
|
|
1772
|
-
|
|
1773
|
-
const runState = await client.runs.get(thread.thread_id, run.run_id);
|
|
1774
|
-
expect(runState.status).toEqual("error");
|
|
1775
|
-
});
|
|
1776
|
-
});
|
|
1777
|
-
|
|
1778
|
-
describe("long running tasks", () => {
|
|
1779
|
-
it.concurrent.for([1000, 8000, 12000])(
|
|
1780
|
-
"long running task with %dms delay",
|
|
1781
|
-
{ timeout: 15_000 },
|
|
1782
|
-
async (delay) => {
|
|
1783
|
-
const assistant = await client.assistants.create({ graphId: "delay" });
|
|
1784
|
-
const thread = await client.threads.create();
|
|
1785
|
-
|
|
1786
|
-
const run = await client.runs.create(
|
|
1787
|
-
thread.thread_id,
|
|
1788
|
-
assistant.assistant_id,
|
|
1789
|
-
{
|
|
1790
|
-
input: { messages: [], delay },
|
|
1791
|
-
config: globalConfig,
|
|
1792
|
-
},
|
|
1793
|
-
);
|
|
1794
|
-
|
|
1795
|
-
await client.runs.join(thread.thread_id, run.run_id);
|
|
1796
|
-
|
|
1797
|
-
const runState = await client.runs.get(thread.thread_id, run.run_id);
|
|
1798
|
-
expect(runState.status).toEqual("success");
|
|
1799
|
-
|
|
1800
|
-
const runResult = await client.threads.getState<{
|
|
1801
|
-
messages: BaseMessageLike[];
|
|
1802
|
-
delay: number;
|
|
1803
|
-
}>(thread.thread_id);
|
|
1804
|
-
|
|
1805
|
-
expect(runResult.values.messages).toMatchObject([
|
|
1806
|
-
{ content: `finished after ${delay}ms` },
|
|
1807
|
-
]);
|
|
1808
|
-
},
|
|
1809
|
-
);
|
|
1810
|
-
});
|
|
1811
|
-
|
|
1812
|
-
it("unusual newline termination characters", async () => {
|
|
1813
|
-
const thread = await client.threads.create({ graphId: "agent" });
|
|
1814
|
-
|
|
1815
|
-
await client.threads.updateState(thread.thread_id, {
|
|
1816
|
-
values: {
|
|
1817
|
-
messages: [
|
|
1818
|
-
{
|
|
1819
|
-
type: "human",
|
|
1820
|
-
content:
|
|
1821
|
-
"Page break characters: \n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029",
|
|
1822
|
-
},
|
|
1823
|
-
],
|
|
1824
|
-
},
|
|
1825
|
-
});
|
|
1826
|
-
|
|
1827
|
-
const history = await client.threads.getHistory<{
|
|
1828
|
-
messages: { type: string; content: string }[];
|
|
1829
|
-
}>(thread.thread_id);
|
|
1830
|
-
expect(history.length).toBe(1);
|
|
1831
|
-
expect(history[0].values.messages.length).toBe(1);
|
|
1832
|
-
expect(history[0].values.messages[0].content).toBe(
|
|
1833
|
-
"Page break characters: \n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029",
|
|
1834
|
-
);
|
|
1835
|
-
});
|
|
1836
|
-
|
|
1837
|
-
describe("command update state", () => {
|
|
1838
|
-
it("updates state via commands", async () => {
|
|
1839
|
-
const assistant = await client.assistants.create({ graphId: "agent" });
|
|
1840
|
-
const thread = await client.threads.create();
|
|
1841
|
-
|
|
1842
|
-
const input = { messages: [{ role: "human", content: "foo" }] };
|
|
1843
|
-
|
|
1844
|
-
// dict-based updates
|
|
1845
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
1846
|
-
input,
|
|
1847
|
-
config: globalConfig,
|
|
1848
|
-
});
|
|
1849
|
-
let stream = await gatherIterator(
|
|
1850
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1851
|
-
command: { update: { keyOne: "value3", keyTwo: "value4" } },
|
|
1852
|
-
config: globalConfig,
|
|
1853
|
-
}),
|
|
1854
|
-
);
|
|
1855
|
-
expect(stream.filter((chunk) => chunk.event === "error")).toEqual([]);
|
|
1856
|
-
|
|
1857
|
-
let state = await client.threads.getState<{
|
|
1858
|
-
keyOne: string;
|
|
1859
|
-
keyTwo: string;
|
|
1860
|
-
}>(thread.thread_id);
|
|
1861
|
-
|
|
1862
|
-
expect(state.values).toMatchObject({
|
|
1863
|
-
keyOne: "value3",
|
|
1864
|
-
keyTwo: "value4",
|
|
1865
|
-
});
|
|
1866
|
-
|
|
1867
|
-
// list-based updates
|
|
1868
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
1869
|
-
input,
|
|
1870
|
-
config: globalConfig,
|
|
1871
|
-
});
|
|
1872
|
-
stream = await gatherIterator(
|
|
1873
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1874
|
-
command: {
|
|
1875
|
-
update: [
|
|
1876
|
-
["keyOne", "value1"],
|
|
1877
|
-
["keyTwo", "value2"],
|
|
1878
|
-
],
|
|
1879
|
-
},
|
|
1880
|
-
config: globalConfig,
|
|
1881
|
-
}),
|
|
1882
|
-
);
|
|
1883
|
-
|
|
1884
|
-
expect(stream.filter((chunk) => chunk.event === "error")).toEqual([]);
|
|
1885
|
-
|
|
1886
|
-
state = await client.threads.getState<{
|
|
1887
|
-
keyOne: string;
|
|
1888
|
-
keyTwo: string;
|
|
1889
|
-
}>(thread.thread_id);
|
|
1890
|
-
|
|
1891
|
-
expect(state.values).toMatchObject({
|
|
1892
|
-
keyOne: "value1",
|
|
1893
|
-
keyTwo: "value2",
|
|
1894
|
-
});
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
it("goto skip start + map-reduce", async () => {
|
|
1898
|
-
const assistant = await client.assistants.create({ graphId: "command" });
|
|
1899
|
-
let thread = await client.threads.create();
|
|
1900
|
-
|
|
1901
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
1902
|
-
command: { goto: "map" },
|
|
1903
|
-
config: globalConfig,
|
|
1904
|
-
});
|
|
1905
|
-
|
|
1906
|
-
let state = await client.threads.getState(thread.thread_id);
|
|
1907
|
-
expect(state.values.messages).toEqual([
|
|
1908
|
-
"map",
|
|
1909
|
-
"task: 1",
|
|
1910
|
-
"task: 2",
|
|
1911
|
-
"task: 3",
|
|
1912
|
-
]);
|
|
1913
|
-
|
|
1914
|
-
await client.runs.wait(thread.thread_id, assistant.assistant_id, {
|
|
1915
|
-
command: {
|
|
1916
|
-
goto: [
|
|
1917
|
-
{ node: "task", input: { value: 4 } },
|
|
1918
|
-
{ node: "task", input: { value: 5 } },
|
|
1919
|
-
{ node: "task", input: { value: 6 } },
|
|
1920
|
-
],
|
|
1921
|
-
},
|
|
1922
|
-
config: globalConfig,
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
state = await client.threads.getState(thread.thread_id);
|
|
1926
|
-
expect(state.values.messages).toEqual([
|
|
1927
|
-
"map",
|
|
1928
|
-
"task: 1",
|
|
1929
|
-
"task: 2",
|
|
1930
|
-
"task: 3",
|
|
1931
|
-
"task: 4",
|
|
1932
|
-
"task: 5",
|
|
1933
|
-
"task: 6",
|
|
1934
|
-
]);
|
|
1935
|
-
});
|
|
1936
|
-
|
|
1937
|
-
it("goto interrupt", async () => {
|
|
1938
|
-
const assistant = await client.assistants.create({ graphId: "command" });
|
|
1939
|
-
let thread = await client.threads.create({ graphId: "command" });
|
|
1940
|
-
|
|
1941
|
-
let stream = await gatherIterator(
|
|
1942
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1943
|
-
// TODO: figure out why we cannot go to the interrupt node directly
|
|
1944
|
-
command: { goto: "before_interrupt" },
|
|
1945
|
-
}),
|
|
1946
|
-
);
|
|
1947
|
-
|
|
1948
|
-
let state = await client.threads.getState(thread.thread_id);
|
|
1949
|
-
expect(state.next).toEqual(["interrupt"]);
|
|
1950
|
-
expect(state.tasks).toMatchObject([
|
|
1951
|
-
{
|
|
1952
|
-
name: "interrupt",
|
|
1953
|
-
interrupts: [{ value: "interrupt", resumable: true, when: "during" }],
|
|
1954
|
-
},
|
|
1955
|
-
]);
|
|
1956
|
-
|
|
1957
|
-
stream = await gatherIterator(
|
|
1958
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
1959
|
-
command: { resume: "resume" },
|
|
1960
|
-
streamMode: ["updates"],
|
|
1961
|
-
}),
|
|
1962
|
-
);
|
|
1963
|
-
|
|
1964
|
-
state = await client.threads.getState(thread.thread_id);
|
|
1965
|
-
expect(state.values.messages).toEqual([
|
|
1966
|
-
"before_interrupt",
|
|
1967
|
-
"interrupt: resume",
|
|
1968
|
-
]);
|
|
1969
|
-
});
|
|
1970
|
-
});
|
|
1971
|
-
|
|
1972
|
-
it("dynamic graph", async () => {
|
|
1973
|
-
const defaultAssistant = await client.assistants.create({
|
|
1974
|
-
graphId: "dynamic",
|
|
1975
|
-
});
|
|
1976
|
-
|
|
1977
|
-
let updates = await gatherIterator(
|
|
1978
|
-
client.runs.stream(null, defaultAssistant.assistant_id, {
|
|
1979
|
-
input: { messages: ["input"] },
|
|
1980
|
-
streamMode: ["updates"],
|
|
1981
|
-
}),
|
|
1982
|
-
);
|
|
1983
|
-
|
|
1984
|
-
expect
|
|
1985
|
-
.soft(
|
|
1986
|
-
updates
|
|
1987
|
-
.filter((i) => i.event === "updates")
|
|
1988
|
-
.flatMap((i) => Object.keys(i.data)),
|
|
1989
|
-
)
|
|
1990
|
-
.toEqual(expect.arrayContaining(["default"]));
|
|
1991
|
-
|
|
1992
|
-
updates = await gatherIterator(
|
|
1993
|
-
client.runs.stream(null, defaultAssistant.assistant_id, {
|
|
1994
|
-
input: { messages: ["input"] },
|
|
1995
|
-
config: { configurable: { nodeName: "runtime" } },
|
|
1996
|
-
streamMode: ["updates"],
|
|
1997
|
-
}),
|
|
1998
|
-
);
|
|
1999
|
-
|
|
2000
|
-
expect
|
|
2001
|
-
.soft(
|
|
2002
|
-
updates
|
|
2003
|
-
.filter((i) => i.event === "updates")
|
|
2004
|
-
.flatMap((i) => Object.keys(i.data)),
|
|
2005
|
-
)
|
|
2006
|
-
.toEqual(expect.arrayContaining(["runtime"]));
|
|
2007
|
-
|
|
2008
|
-
const configAssistant = await client.assistants.create({
|
|
2009
|
-
graphId: "dynamic",
|
|
2010
|
-
config: { configurable: { nodeName: "assistant" } },
|
|
2011
|
-
});
|
|
2012
|
-
|
|
2013
|
-
let thread = await client.threads.create({ graphId: "dynamic" });
|
|
2014
|
-
updates = await gatherIterator(
|
|
2015
|
-
client.runs.stream(thread.thread_id, configAssistant.assistant_id, {
|
|
2016
|
-
input: { messages: ["input"], configurable: { nodeName: "assistant" } },
|
|
2017
|
-
streamMode: ["updates"],
|
|
2018
|
-
}),
|
|
2019
|
-
);
|
|
2020
|
-
|
|
2021
|
-
expect
|
|
2022
|
-
.soft(
|
|
2023
|
-
updates
|
|
2024
|
-
.filter((i) => i.event === "updates")
|
|
2025
|
-
.flatMap((i) => Object.keys(i.data)),
|
|
2026
|
-
)
|
|
2027
|
-
.toEqual(expect.arrayContaining(["assistant"]));
|
|
2028
|
-
|
|
2029
|
-
thread = await client.threads.get(thread.thread_id);
|
|
2030
|
-
|
|
2031
|
-
// check if we are properly recreating the graph with the
|
|
2032
|
-
// stored configuration inside a thread
|
|
2033
|
-
await client.threads.updateState(thread.thread_id, {
|
|
2034
|
-
values: { messages: "update" },
|
|
2035
|
-
asNode: "assistant",
|
|
2036
|
-
});
|
|
2037
|
-
|
|
2038
|
-
const state = await client.threads.getState(thread.thread_id);
|
|
2039
|
-
expect(state.values.messages).toEqual(["input", "assistant", "update"]);
|
|
2040
|
-
});
|
|
2041
|
-
|
|
2042
|
-
it("generative ui", async () => {
|
|
2043
|
-
const ui = await client["~ui"].getComponent("agent", "weather-component");
|
|
2044
|
-
expect(ui).toContain(
|
|
2045
|
-
`<link rel="stylesheet" href="http://localhost:9123/ui/agent/entrypoint.css" />`,
|
|
2046
|
-
);
|
|
2047
|
-
expect(ui).toContain(
|
|
2048
|
-
`<script src="http://localhost:9123/ui/agent/entrypoint.js" onload='__LGUI_agent.render("weather-component", "{{shadowRootId}}")'></script>`,
|
|
2049
|
-
);
|
|
2050
|
-
|
|
2051
|
-
const match = /src="(?<src>[^"]+)"/.exec(ui);
|
|
2052
|
-
let jsFile = match?.groups?.src;
|
|
2053
|
-
if (!jsFile) throw new Error("No JS file found");
|
|
2054
|
-
if (jsFile.startsWith("//")) jsFile = "http:" + jsFile;
|
|
2055
|
-
|
|
2056
|
-
// Used to manually pass runtime dependencies
|
|
2057
|
-
const js = await fetch(jsFile).then((a) => a.text());
|
|
2058
|
-
expect(js).contains(`globalThis[Symbol.for("LGUI_REQUIRE")]`);
|
|
2059
|
-
|
|
2060
|
-
await expect(() =>
|
|
2061
|
-
client["~ui"].getComponent("non-existent", "none"),
|
|
2062
|
-
).rejects.toThrow();
|
|
2063
|
-
});
|
|
2064
|
-
|
|
2065
|
-
it("custom routes", async () => {
|
|
2066
|
-
const fetcher = async (...args: Parameters<typeof fetch>) => {
|
|
2067
|
-
const res = await fetch(...args);
|
|
2068
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
2069
|
-
return { json: await res.json(), headers: res.headers };
|
|
2070
|
-
};
|
|
2071
|
-
|
|
2072
|
-
let res = await fetcher(new URL("/custom/my-route?aCoolParam=13", API_URL), {
|
|
2073
|
-
headers: { "x-custom-input": "hey" },
|
|
2074
|
-
});
|
|
2075
|
-
expect(res.json).toEqual({ foo: "bar" });
|
|
2076
|
-
expect(res.headers.get("x-custom-output")).toEqual("hey");
|
|
2077
|
-
expect(res.headers.get("x-js-middleware")).toEqual("true");
|
|
2078
|
-
|
|
2079
|
-
res = await fetcher(new URL("/runs/afakeroute", API_URL));
|
|
2080
|
-
expect(res.json).toEqual({ foo: "afakeroute" });
|
|
2081
|
-
|
|
2082
|
-
await expect(() =>
|
|
2083
|
-
fetcher(new URL("/does/not/exist", API_URL)),
|
|
2084
|
-
).rejects.toThrow("404");
|
|
2085
|
-
|
|
2086
|
-
await expect(() =>
|
|
2087
|
-
fetcher(new URL("/custom/error", API_URL)),
|
|
2088
|
-
).rejects.toThrow("400");
|
|
2089
|
-
|
|
2090
|
-
await expect(() =>
|
|
2091
|
-
fetcher(new URL("/__langgraph_check", API_URL), { method: "OPTIONS" }),
|
|
2092
|
-
).rejects.toThrow("404");
|
|
2093
|
-
|
|
2094
|
-
const stream = await fetch(new URL("/custom/streaming", API_URL));
|
|
2095
|
-
const reader = stream.body?.getReader();
|
|
2096
|
-
if (!reader) throw new Error("No reader");
|
|
2097
|
-
|
|
2098
|
-
const chunks: string[] = [];
|
|
2099
|
-
while (true) {
|
|
2100
|
-
const { done, value } = await reader.read();
|
|
2101
|
-
if (done) break;
|
|
2102
|
-
chunks.push(new TextDecoder().decode(value));
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
expect(chunks.length).toBeGreaterThanOrEqual(4); // Must actually stream
|
|
2106
|
-
expect(chunks.join("")).toEqual("Count: 0\nCount: 1\nCount: 2\nCount: 3\n");
|
|
2107
|
-
|
|
2108
|
-
const thread = await client.threads.create();
|
|
2109
|
-
await client.runs.wait(thread.thread_id, "agent_simple", {
|
|
2110
|
-
input: { messages: [{ role: "human", content: "foo" }] },
|
|
2111
|
-
webhook: "/custom/webhook",
|
|
2112
|
-
});
|
|
2113
|
-
|
|
2114
|
-
await expect
|
|
2115
|
-
.poll(() => fetcher(new URL("/custom/webhook-payload", API_URL)), {
|
|
2116
|
-
interval: 500,
|
|
2117
|
-
timeout: 3000,
|
|
2118
|
-
})
|
|
2119
|
-
.toMatchObject({ json: { status: "success" } });
|
|
2120
|
-
|
|
2121
|
-
// check if custom middleware is applied even for python routes
|
|
2122
|
-
res = await fetcher(new URL("/info", API_URL));
|
|
2123
|
-
expect(res.headers.get("x-js-middleware")).toEqual("true");
|
|
2124
|
-
|
|
2125
|
-
// ... and if we can intercept a request targeted for Python API
|
|
2126
|
-
res = await fetcher(new URL("/info?interrupt", API_URL));
|
|
2127
|
-
expect(res.json).toEqual({ status: "interrupted" });
|
|
2128
|
-
});
|
|
2129
|
-
|
|
2130
|
-
it("custom routes - mutate request body", async () => {
|
|
2131
|
-
const client = new Client<any>({
|
|
2132
|
-
apiUrl: API_URL,
|
|
2133
|
-
defaultHeaders: {
|
|
2134
|
-
"x-configurable-header": "extra-client",
|
|
2135
|
-
},
|
|
2136
|
-
});
|
|
2137
|
-
|
|
2138
|
-
const thread = await client.threads.create();
|
|
2139
|
-
const res = await client.runs.wait(thread.thread_id, "agent_simple", {
|
|
2140
|
-
input: { messages: [{ role: "human", content: "input" }] },
|
|
2141
|
-
});
|
|
2142
|
-
|
|
2143
|
-
expect(res).toEqual({
|
|
2144
|
-
messages: expect.arrayContaining([
|
|
2145
|
-
expect.objectContaining({ content: "end: extra-client" }),
|
|
2146
|
-
]),
|
|
2147
|
-
prompts: [],
|
|
2148
|
-
});
|
|
2149
|
-
});
|
|
2150
|
-
|
|
2151
|
-
it("custom routes - langgraph", async () => {
|
|
2152
|
-
const fetcher = async (...args: Parameters<typeof fetch>) => {
|
|
2153
|
-
const res = await fetch(...args);
|
|
2154
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
2155
|
-
return { json: await res.json(), headers: res.headers };
|
|
2156
|
-
};
|
|
2157
|
-
|
|
2158
|
-
const res = await fetcher(new URL("/custom/client", API_URL));
|
|
2159
|
-
expect(res.json).toEqual({
|
|
2160
|
-
result: {
|
|
2161
|
-
messages: expect.arrayContaining([
|
|
2162
|
-
expect.objectContaining({ content: "input" }),
|
|
2163
|
-
]),
|
|
2164
|
-
prompts: [],
|
|
2165
|
-
},
|
|
2166
|
-
});
|
|
2167
|
-
});
|
|
2168
|
-
|
|
2169
|
-
it("send map-reduce", async () => {
|
|
2170
|
-
const assistant = await client.assistants.create({ graphId: "agent_simple" });
|
|
2171
|
-
const thread = await client.threads.create();
|
|
2172
|
-
|
|
2173
|
-
const chunks = await gatherIterator(
|
|
2174
|
-
client.runs.stream(thread.thread_id, assistant.assistant_id, {
|
|
2175
|
-
input: { messages: [{ role: "human", content: "input" }] },
|
|
2176
|
-
config: { configurable: { "map-reduce": true } },
|
|
2177
|
-
}),
|
|
2178
|
-
);
|
|
2179
|
-
|
|
2180
|
-
expect(chunks.filter((i) => i.event === "error")).toEqual([]);
|
|
2181
|
-
expect(findLast(chunks, (i) => i.event === "values")).toMatchObject({
|
|
2182
|
-
data: {
|
|
2183
|
-
messages: [
|
|
2184
|
-
{ type: "human", content: "input" },
|
|
2185
|
-
{ type: "human", content: "map-reduce" },
|
|
2186
|
-
],
|
|
2187
|
-
prompts: [
|
|
2188
|
-
{ type: "ai", content: "first" },
|
|
2189
|
-
{ type: "ai", content: "second" },
|
|
2190
|
-
{ type: "ai", content: "third" },
|
|
2191
|
-
],
|
|
2192
|
-
},
|
|
2193
|
-
});
|
|
2194
|
-
});
|