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.

Files changed (42) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/assistants.py +4 -4
  3. langgraph_api/api/store.py +10 -6
  4. langgraph_api/asgi_transport.py +171 -0
  5. langgraph_api/asyncio.py +17 -0
  6. langgraph_api/config.py +1 -0
  7. langgraph_api/graph.py +28 -5
  8. langgraph_api/js/remote.py +16 -11
  9. langgraph_api/metadata.py +28 -16
  10. langgraph_api/store.py +127 -0
  11. langgraph_api/stream.py +17 -7
  12. langgraph_api/worker.py +1 -1
  13. {langgraph_api-0.2.26.dist-info → langgraph_api-0.2.28.dist-info}/METADATA +24 -30
  14. {langgraph_api-0.2.26.dist-info → langgraph_api-0.2.28.dist-info}/RECORD +42 -64
  15. {langgraph_api-0.2.26.dist-info → langgraph_api-0.2.28.dist-info}/WHEEL +1 -1
  16. langgraph_api-0.2.28.dist-info/entry_points.txt +2 -0
  17. langgraph_api/js/tests/api.test.mts +0 -2194
  18. langgraph_api/js/tests/auth.test.mts +0 -648
  19. langgraph_api/js/tests/compose-postgres.auth.yml +0 -59
  20. langgraph_api/js/tests/compose-postgres.yml +0 -59
  21. langgraph_api/js/tests/graphs/.gitignore +0 -1
  22. langgraph_api/js/tests/graphs/agent.css +0 -1
  23. langgraph_api/js/tests/graphs/agent.mts +0 -187
  24. langgraph_api/js/tests/graphs/agent.ui.tsx +0 -10
  25. langgraph_api/js/tests/graphs/agent_simple.mts +0 -105
  26. langgraph_api/js/tests/graphs/auth.mts +0 -106
  27. langgraph_api/js/tests/graphs/command.mts +0 -48
  28. langgraph_api/js/tests/graphs/delay.mts +0 -30
  29. langgraph_api/js/tests/graphs/dynamic.mts +0 -24
  30. langgraph_api/js/tests/graphs/error.mts +0 -17
  31. langgraph_api/js/tests/graphs/http.mts +0 -76
  32. langgraph_api/js/tests/graphs/langgraph.json +0 -11
  33. langgraph_api/js/tests/graphs/nested.mts +0 -44
  34. langgraph_api/js/tests/graphs/package.json +0 -13
  35. langgraph_api/js/tests/graphs/weather.mts +0 -57
  36. langgraph_api/js/tests/graphs/yarn.lock +0 -242
  37. langgraph_api/js/tests/utils.mts +0 -17
  38. langgraph_api-0.2.26.dist-info/LICENSE +0 -93
  39. langgraph_api-0.2.26.dist-info/entry_points.txt +0 -3
  40. logging.json +0 -22
  41. openapi.json +0 -4562
  42. /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
- });