langgraph-api 0.2.27__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 (40) 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/config.py +1 -0
  5. langgraph_api/graph.py +28 -5
  6. langgraph_api/js/remote.py +16 -11
  7. langgraph_api/metadata.py +28 -16
  8. langgraph_api/store.py +127 -0
  9. langgraph_api/stream.py +17 -7
  10. langgraph_api/worker.py +1 -1
  11. {langgraph_api-0.2.27.dist-info → langgraph_api-0.2.28.dist-info}/METADATA +24 -30
  12. {langgraph_api-0.2.27.dist-info → langgraph_api-0.2.28.dist-info}/RECORD +42 -65
  13. {langgraph_api-0.2.27.dist-info → langgraph_api-0.2.28.dist-info}/WHEEL +1 -1
  14. langgraph_api-0.2.28.dist-info/entry_points.txt +2 -0
  15. langgraph_api/js/tests/api.test.mts +0 -2194
  16. langgraph_api/js/tests/auth.test.mts +0 -648
  17. langgraph_api/js/tests/compose-postgres.auth.yml +0 -59
  18. langgraph_api/js/tests/compose-postgres.yml +0 -59
  19. langgraph_api/js/tests/graphs/.gitignore +0 -1
  20. langgraph_api/js/tests/graphs/agent.css +0 -1
  21. langgraph_api/js/tests/graphs/agent.mts +0 -187
  22. langgraph_api/js/tests/graphs/agent.ui.tsx +0 -10
  23. langgraph_api/js/tests/graphs/agent_simple.mts +0 -105
  24. langgraph_api/js/tests/graphs/auth.mts +0 -106
  25. langgraph_api/js/tests/graphs/command.mts +0 -48
  26. langgraph_api/js/tests/graphs/delay.mts +0 -30
  27. langgraph_api/js/tests/graphs/dynamic.mts +0 -24
  28. langgraph_api/js/tests/graphs/error.mts +0 -17
  29. langgraph_api/js/tests/graphs/http.mts +0 -76
  30. langgraph_api/js/tests/graphs/langgraph.json +0 -11
  31. langgraph_api/js/tests/graphs/nested.mts +0 -44
  32. langgraph_api/js/tests/graphs/package.json +0 -13
  33. langgraph_api/js/tests/graphs/weather.mts +0 -57
  34. langgraph_api/js/tests/graphs/yarn.lock +0 -242
  35. langgraph_api/js/tests/utils.mts +0 -17
  36. langgraph_api-0.2.27.dist-info/LICENSE +0 -93
  37. langgraph_api-0.2.27.dist-info/entry_points.txt +0 -3
  38. logging.json +0 -22
  39. openapi.json +0 -4562
  40. /LICENSE → /langgraph_api-0.2.28.dist-info/licenses/LICENSE +0 -0
@@ -1,648 +0,0 @@
1
- import { Client } from "@langchain/langgraph-sdk";
2
- import { beforeAll, expect, it } from "vitest";
3
- import { gatherIterator } from "./utils.mjs";
4
- import postgres from "postgres";
5
- import { SignJWT } from "jose";
6
-
7
- const sql = postgres(
8
- process.env.POSTGRES_URI ??
9
- "postgres://postgres:postgres@127.0.0.1:5433/postgres?sslmode=disable",
10
- );
11
-
12
- const config = { configurable: { user_id: "123" } };
13
-
14
- const SECRET_KEY = new TextEncoder().encode(
15
- "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7",
16
- );
17
- const ALGORITHM = "HS256";
18
-
19
- const createJwtClient = async (sub: string, scopes: string[] = []) => {
20
- const accessToken = await new SignJWT({ sub, scopes })
21
- .setProtectedHeader({ alg: ALGORITHM })
22
- .setIssuedAt()
23
- .setExpirationTime("10s")
24
- .sign(SECRET_KEY);
25
- return new Client({
26
- apiUrl: "http://localhost:9123",
27
- defaultHeaders: { Authorization: `Bearer ${accessToken}` },
28
- });
29
- };
30
-
31
- beforeAll(async () => {
32
- await sql`DELETE FROM thread`;
33
- await sql`DELETE FROM store`;
34
- await sql`DELETE FROM assistant WHERE metadata->>'created_by' is null OR metadata->>'created_by' != 'system'`;
35
- });
36
-
37
- it("unauthenticated user", async () => {
38
- const client = await createJwtClient("wfh", ["me"]);
39
- await expect(client.assistants.create({ graphId: "agent" })).rejects.toThrow(
40
- "HTTP 401",
41
- );
42
- });
43
-
44
- it("create assistant with forbidden scopes", async () => {
45
- let user = await createJwtClient("johndoe");
46
- await expect(user.assistants.create({ graphId: "agent" })).rejects.toThrow(
47
- "HTTP 403",
48
- );
49
-
50
- user = await createJwtClient("johndoe", ["foo"]);
51
- await expect(user.assistants.create({ graphId: "agent" })).rejects.toThrow(
52
- "HTTP 403",
53
- );
54
-
55
- user = await createJwtClient("johndoe", ["assistants:write"]);
56
- await user.assistants.create({ graphId: "agent" });
57
-
58
- const fetched = await user.assistants.search({ graphId: "agent" });
59
- expect(fetched).toHaveLength(1);
60
- expect(fetched).toMatchObject([{ metadata: { owner: "johndoe" } }]);
61
- });
62
-
63
- it("get thread history from unauthorized user", async () => {
64
- const input = { messages: [{ role: "human", content: "foo" }] };
65
- const user1 = await createJwtClient("johndoe", ["me", "assistants:write"]);
66
-
67
- await user1.assistants.create({ graphId: "agent" });
68
- let thread = await user1.threads.create();
69
- let history = await user1.threads.getHistory(thread.thread_id);
70
- expect(history).toEqual([]);
71
-
72
- await user1.runs.wait(thread.thread_id, "agent", { input, config });
73
- history = await user1.threads.getHistory(thread.thread_id);
74
- expect(history).toHaveLength(5);
75
-
76
- const user2 = await createJwtClient("alice", ["me"]);
77
- await expect(
78
- user2.runs.wait(thread.thread_id, "agent", { input, config }),
79
- ).rejects.toThrow("HTTP 404");
80
- });
81
-
82
- it("add run to unauthorized thread", async () => {
83
- const user1 = await createJwtClient("johndoe", ["me"]);
84
- const thread = await user1.threads.create();
85
-
86
- const input = { messages: [{ role: "human", content: "foo" }] };
87
- const history = await user1.threads.getHistory(thread.thread_id);
88
- expect(history).toEqual([]);
89
-
90
- const user2 = await createJwtClient("alice", ["me"]);
91
- await expect(
92
- user2.runs.wait(thread.thread_id, "agent", { input, config }),
93
- ).rejects.toThrow("HTTP 404");
94
- });
95
-
96
- it("asssistant access control", async () => {
97
- const owner = await createJwtClient("johndoe", ["assistants:write"]);
98
- const otherUser = await createJwtClient("alice", ["assistants:write"]);
99
-
100
- const assistant = await owner.assistants.create({ graphId: "agent" });
101
-
102
- // Other user can't update the assistant
103
- await expect(
104
- otherUser.assistants.update(assistant.assistant_id, {
105
- metadata: { foo: "bar" },
106
- }),
107
- ).rejects.toThrow("HTTP 404");
108
-
109
- // Other user can't delete the assistant
110
- await expect(
111
- otherUser.assistants.delete(assistant.assistant_id),
112
- ).rejects.toThrow("HTTP 404");
113
- });
114
-
115
- it("thread operations auth", async () => {
116
- const owner = await createJwtClient("johndoe", ["me"]);
117
- const otherUser = await createJwtClient("alice", ["me"]);
118
-
119
- const thread = await owner.threads.create();
120
-
121
- // Other user can't update thread
122
- await expect(
123
- otherUser.threads.update(thread.thread_id, { metadata: { foo: "bar" } }),
124
- ).rejects.toThrow("HTTP 404");
125
-
126
- // Other user can't delete thread
127
- await expect(otherUser.threads.delete(thread.thread_id)).rejects.toThrow(
128
- "HTTP 404",
129
- );
130
- });
131
-
132
- it("run streaming auth", async () => {
133
- const owner = await createJwtClient("johndoe", ["me"]);
134
- const otherUser = await createJwtClient("alice", ["me"]);
135
-
136
- const thread = await owner.threads.create();
137
- const input = { messages: [{ role: "human", content: "foo" }] };
138
-
139
- const run = await owner.runs.create(thread.thread_id, "agent", {
140
- input,
141
- config,
142
- });
143
-
144
- const chunks = await gatherIterator(
145
- otherUser.runs.joinStream(thread.thread_id, run.run_id),
146
- );
147
-
148
- expect(chunks).toMatchObject([
149
- { event: "error", data: { message: expect.stringContaining("404") } },
150
- ]);
151
- });
152
-
153
- it("store auth", async () => {
154
- const userA = await createJwtClient("johndoe", ["me", "assistants:write"]);
155
- const userB = await createJwtClient("alice", ["me", "assistants:write"]);
156
-
157
- await userA.store.deleteItem(["johndoe"], "key_one");
158
- await userB.store.deleteItem(["alice"], "key_one");
159
-
160
- const threadA = await userA.threads.create();
161
- const threadB = await userB.threads.create();
162
-
163
- const input1 = { messages: [{ role: "human", content: "test user A" }] };
164
- const input2 = { messages: [{ role: "human", content: "test user B" }] };
165
-
166
- await Promise.all([
167
- userA.runs.wait(threadA.thread_id, "agent_simple", {
168
- input: input1,
169
- config,
170
- }),
171
- userB.runs.wait(threadB.thread_id, "agent_simple", {
172
- input: input2,
173
- config,
174
- }),
175
- ]);
176
-
177
- // Test store access control
178
- await expect(userA.store.getItem(["ALL"], "key_one")).rejects.toThrow(
179
- "HTTP 403",
180
- );
181
- await expect(
182
- userA.store.putItem(["ALL"], "key_one", { foo: "bar" }),
183
- ).rejects.toThrow("HTTP 403");
184
- await expect(userA.store.deleteItem(["ALL"], "key_one")).rejects.toThrow(
185
- "HTTP 403",
186
- );
187
- await expect(userA.store.searchItems(["ALL"])).rejects.toThrow("HTTP 403");
188
- await expect(userA.store.listNamespaces({ prefix: ["ALL"] })).rejects.toThrow(
189
- "HTTP 403",
190
- );
191
-
192
- // Test owner can access their own store
193
- expect(await userA.store.getItem(["johndoe"], "key_one")).toMatchObject({
194
- value: { text: "test user A" },
195
- });
196
-
197
- expect(await userA.store.searchItems(["johndoe"])).toMatchObject({
198
- items: [{ key: "key_one", value: { text: "test user A" } }],
199
- });
200
-
201
- expect(
202
- await userA.store.listNamespaces({ prefix: ["johndoe"] }),
203
- ).toMatchObject({ namespaces: [["johndoe"]] });
204
-
205
- // Test other user can access their own store
206
- expect(await userB.store.getItem(["alice"], "key_one")).toMatchObject({
207
- value: { text: "test user B" },
208
- });
209
- expect(await userB.store.searchItems(["alice"])).toMatchObject({
210
- items: [{ key: "key_one", value: { text: "test user B" } }],
211
- });
212
- expect(await userB.store.listNamespaces({ prefix: ["alice"] })).toMatchObject(
213
- { namespaces: [["alice"]] },
214
- );
215
- });
216
-
217
- it("run cancellation", { retry: 3 }, async () => {
218
- const owner = await createJwtClient("johndoe", ["me"]);
219
- const otherUser = await createJwtClient("alice", ["me"]);
220
-
221
- const thread = await owner.threads.create();
222
- const input = { messages: [{ role: "human", content: "test" }] };
223
- const run = await owner.runs.create(thread.thread_id, "agent", {
224
- input,
225
- config,
226
- });
227
-
228
- // Other user can't cancel the run
229
- await expect(
230
- otherUser.runs.cancel(thread.thread_id, run.run_id),
231
- ).rejects.toThrow("HTTP 404");
232
-
233
- // Owner can cancel their own run
234
- await owner.runs.cancel(thread.thread_id, run.run_id);
235
- });
236
-
237
- it("get assistant ownership", async () => {
238
- const owner = await createJwtClient("johndoe", ["assistants:write"]);
239
- const otherUser = await createJwtClient("alice", ["assistants:write"]);
240
-
241
- const assistant = await owner.assistants.create({ graphId: "agent" });
242
-
243
- // Owner can get the assistant
244
- const fetched = await owner.assistants.get(assistant.assistant_id);
245
- expect(fetched.assistant_id).toBe(assistant.assistant_id);
246
-
247
- // Another user cannot get this assistant
248
- await expect(
249
- otherUser.assistants.get(assistant.assistant_id),
250
- ).rejects.toThrow("HTTP 404");
251
-
252
- // Test invalid assistant IDs
253
- const nonexistantUuid = crypto.randomUUID();
254
- await expect(owner.assistants.get(nonexistantUuid)).rejects.toThrow(
255
- "HTTP 404",
256
- );
257
- });
258
-
259
- it("get assistant graph", async () => {
260
- const owner = await createJwtClient("johndoe", ["assistants:write"]);
261
- const otherUser = await createJwtClient("alice", ["assistants:write"]);
262
-
263
- const assistant = await owner.assistants.create({ graphId: "agent" });
264
-
265
- // Owner can get the graph
266
- const graph = await owner.assistants.getGraph(assistant.assistant_id);
267
- expect(graph).toBeInstanceOf(Object);
268
- expect(graph).toHaveProperty("nodes");
269
- expect(graph).toHaveProperty("edges");
270
-
271
- // Another user can't access the graph
272
- await expect(
273
- otherUser.assistants.getGraph(assistant.assistant_id),
274
- ).rejects.toThrow("HTTP 404");
275
- });
276
-
277
- it("thread state operations", async () => {
278
- const owner = await createJwtClient("johndoe", ["me"]);
279
- const otherUser = await createJwtClient("alice", ["me"]);
280
-
281
- const thread = await owner.threads.create();
282
- const input = { messages: [{ type: "human", content: "test" }] };
283
- const run = await owner.runs.create(thread.thread_id, "agent_simple", {
284
- input,
285
- config,
286
- });
287
- expect(run.run_id).toBeDefined();
288
- await owner.runs.join(thread.thread_id, run.run_id);
289
-
290
- // Owner can get and update state
291
- const state = await owner.threads.getState(thread.thread_id);
292
- expect(state.values).toMatchObject({
293
- messages: expect.arrayContaining([
294
- expect.objectContaining({ type: "human", content: "test" }),
295
- ]),
296
- });
297
-
298
- await owner.threads.updateState(thread.thread_id, { values: { sleep: 432 } });
299
- const updatedState = await owner.threads.getState(thread.thread_id);
300
- expect(updatedState.values).toMatchObject({ sleep: 432 });
301
-
302
- // Another user cannot access or modify state
303
- await expect(otherUser.threads.getState(thread.thread_id)).rejects.toThrow(
304
- "HTTP 404",
305
- );
306
- await expect(
307
- otherUser.threads.updateState(thread.thread_id, { values: { sleep: 432 } }),
308
- ).rejects.toThrow("HTTP 404");
309
- });
310
-
311
- it("run operations", async () => {
312
- const owner = await createJwtClient("johndoe", ["me"]);
313
- const otherUser = await createJwtClient("alice", ["me"]);
314
-
315
- const thread = await owner.threads.create();
316
- const input = { messages: [{ role: "human", content: "test" }] };
317
- const run = await owner.runs.create(thread.thread_id, "agent", {
318
- input,
319
- config,
320
- afterSeconds: 100,
321
- });
322
- expect(run.run_id).toBeDefined();
323
-
324
- // Owner can list runs
325
- const runs = await owner.runs.list(thread.thread_id);
326
- expect(runs).toMatchObject(
327
- expect.arrayContaining([expect.objectContaining({ run_id: run.run_id })]),
328
- );
329
-
330
- // Owner can get specific run
331
- const runInfo = await owner.runs.get(thread.thread_id, run.run_id);
332
- expect(runInfo).toMatchObject({ run_id: run.run_id });
333
-
334
- // Another user cannot access runs, cancel or delete a run not owned by them
335
- await expect(otherUser.runs.list(thread.thread_id)).rejects.toThrow(
336
- "HTTP 404",
337
- );
338
- await expect(
339
- otherUser.runs.get(thread.thread_id, run.run_id),
340
- ).rejects.toThrow("HTTP 404");
341
-
342
- await expect(
343
- otherUser.runs.cancel(thread.thread_id, run.run_id, true),
344
- ).rejects.toThrow("HTTP 404");
345
-
346
- await expect(
347
- otherUser.runs.delete(thread.thread_id, run.run_id),
348
- ).rejects.toThrow("HTTP 404");
349
-
350
- // Owner can cancel run
351
- await owner.runs.cancel(thread.thread_id, run.run_id, true);
352
-
353
- // Owner can delete run
354
- await owner.runs.delete(thread.thread_id, run.run_id);
355
- await expect(owner.runs.get(thread.thread_id, run.run_id)).rejects.toThrow(
356
- "HTTP 404",
357
- );
358
- });
359
-
360
- it("create run in other user thread", async () => {
361
- const owner = await createJwtClient("johndoe", ["me"]);
362
- const otherUser = await createJwtClient("alice", ["me"]);
363
-
364
- const thread = await owner.threads.create();
365
- const input = {
366
- messages: [{ role: "human", content: "Unauthorized attempt" }],
367
- };
368
-
369
- await expect(
370
- otherUser.runs.create(thread.thread_id, "agent", { input, config }),
371
- ).rejects.toThrow("HTTP 404");
372
- });
373
-
374
- it("list runs other user thread", async () => {
375
- const owner = await createJwtClient("johndoe", ["me"]);
376
- const otherUser = await createJwtClient("alice", ["me"]);
377
-
378
- const thread = await owner.threads.create();
379
- const input = { messages: [{ role: "human", content: "Hello" }] };
380
- const run = await owner.runs.create(thread.thread_id, "agent", {
381
- input,
382
- config,
383
- });
384
-
385
- // Owner can list runs
386
- const ownerRuns = await owner.runs.list(thread.thread_id);
387
- expect(ownerRuns.some((r) => r.run_id === run.run_id)).toBe(true);
388
-
389
- // Other user cannot list runs
390
- await expect(otherUser.runs.list(thread.thread_id)).rejects.toThrow(
391
- "HTTP 404",
392
- );
393
- });
394
-
395
- it("get run other user thread", async () => {
396
- const owner = await createJwtClient("johndoe", ["me"]);
397
- const otherUser = await createJwtClient("alice", ["me"]);
398
-
399
- const thread = await owner.threads.create();
400
- const run = await owner.runs.create(thread.thread_id, "agent", {
401
- input: { messages: [{ role: "human", content: "Check run" }] },
402
- config,
403
- });
404
-
405
- // Other user attempts to get the run
406
- await expect(
407
- otherUser.runs.get(thread.thread_id, run.run_id),
408
- ).rejects.toThrow("HTTP 404");
409
- });
410
-
411
- it("join run other user thread", async () => {
412
- const owner = await createJwtClient("johndoe", ["me"]);
413
- const otherUser = await createJwtClient("alice", ["me"]);
414
-
415
- const thread = await owner.threads.create();
416
- const run = await owner.runs.create(thread.thread_id, "agent", {
417
- input: { messages: [{ role: "human", content: "Join?" }] },
418
- config,
419
- });
420
-
421
- // Other user tries to join the run
422
- await expect(
423
- otherUser.runs.join(thread.thread_id, run.run_id),
424
- ).rejects.toThrow("HTTP 404");
425
- });
426
-
427
- it("wait run other user thread", async () => {
428
- const owner = await createJwtClient("johndoe", ["me"]);
429
- const otherUser = await createJwtClient("alice", ["me"]);
430
-
431
- const thread = await owner.threads.create();
432
- const input = { messages: [{ role: "human", content: "Waiting test" }] };
433
- await owner.runs.create(thread.thread_id, "agent", { input, config });
434
-
435
- // Other user tries to wait on run result
436
- await expect(
437
- otherUser.runs.wait(thread.thread_id, "agent", { input, config }),
438
- ).rejects.toThrow("HTTP 404");
439
- });
440
-
441
- it("stream run other user thread", async () => {
442
- const owner = await createJwtClient("johndoe", ["me"]);
443
- const otherUser = await createJwtClient("alice", ["me"]);
444
-
445
- const thread = await owner.threads.create();
446
- const run = await owner.runs.create(thread.thread_id, "agent", {
447
- input: { messages: [{ role: "human", content: "Stream me" }] },
448
- config,
449
- });
450
-
451
- // Other user tries to join_stream
452
- const chunks = await gatherIterator(
453
- otherUser.runs.joinStream(thread.thread_id, run.run_id),
454
- );
455
- expect(chunks).toHaveLength(1);
456
- expect(chunks).toMatchObject([
457
- { event: "error", data: { message: expect.stringContaining("404") } },
458
- ]);
459
- });
460
-
461
- it("cancel run other user thread", { retry: 3 }, async () => {
462
- const owner = await createJwtClient("johndoe", ["me"]);
463
- const otherUser = await createJwtClient("alice", ["me"]);
464
-
465
- const thread = await owner.threads.create();
466
- const run = await owner.runs.create(thread.thread_id, "agent", {
467
- input: { messages: [{ role: "human", content: "Cancel test" }] },
468
- config,
469
- afterSeconds: 100,
470
- });
471
-
472
- await expect(
473
- otherUser.runs.cancel(thread.thread_id, run.run_id),
474
- ).rejects.toThrow("HTTP 404");
475
-
476
- await owner.runs.cancel(thread.thread_id, run.run_id);
477
- });
478
-
479
- it("delete run other user thread", async () => {
480
- const owner = await createJwtClient("johndoe", ["me"]);
481
- const otherUser = await createJwtClient("alice", ["me"]);
482
-
483
- const thread = await owner.threads.create();
484
- const run = await owner.runs.create(thread.thread_id, "agent", {
485
- input: { messages: [{ role: "human", content: "Delete me" }] },
486
- config,
487
- afterSeconds: 100,
488
- });
489
-
490
- await expect(
491
- otherUser.runs.delete(thread.thread_id, run.run_id),
492
- ).rejects.toThrow("HTTP 404");
493
-
494
- await owner.runs.cancel(thread.thread_id, run.run_id);
495
- });
496
-
497
- it("update thread state other user", async () => {
498
- const owner = await createJwtClient("johndoe", ["me"]);
499
- const otherUser = await createJwtClient("alice", ["me"]);
500
-
501
- const thread = await owner.threads.create();
502
- const newState = { values: { some: "value" } };
503
-
504
- // Other user tries to update state
505
- await expect(
506
- otherUser.threads.updateState(thread.thread_id, newState),
507
- ).rejects.toThrow("HTTP 404");
508
- });
509
-
510
- it("get checkpoint other user", async () => {
511
- const owner = await createJwtClient("johndoe", ["me", "assistants:write"]);
512
- const otherUser = await createJwtClient("alice", ["me"]);
513
-
514
- await owner.assistants.create({ graphId: "agent" });
515
- const thread = await owner.threads.create();
516
- const input = { messages: [{ role: "human", content: "Checkpoint test" }] };
517
- await owner.runs.wait(thread.thread_id, "agent", { input, config });
518
-
519
- // Get history to find a checkpoint
520
- const history = await owner.threads.getHistory(thread.thread_id);
521
- if (history.length === 0) {
522
- return; // Skip if no checkpoints
523
- }
524
-
525
- const checkpointId = history[history.length - 1].checkpoint?.checkpoint_id;
526
- if (!checkpointId) {
527
- return; // Skip if no checkpoint ID
528
- }
529
-
530
- await expect(
531
- otherUser.threads.getState(thread.thread_id, checkpointId),
532
- ).rejects.toThrow("HTTP 404");
533
- });
534
-
535
- it("assistant version leakage", async () => {
536
- const owner = await createJwtClient("johndoe", ["assistants:write"]);
537
- const otherUser = await createJwtClient("alice", ["assistants:write"]);
538
-
539
- const assistant = await owner.assistants.create({ graphId: "agent" });
540
- const someId = crypto.randomUUID();
541
- const result = await owner.assistants.update(assistant.assistant_id, {
542
- metadata: { foo: someId },
543
- });
544
- expect(result.metadata?.foo).toBe(someId);
545
-
546
- await expect(
547
- otherUser.assistants.getVersions(assistant.assistant_id),
548
- ).rejects.toThrow("HTTP 404");
549
- await expect(
550
- otherUser.assistants.setLatest(assistant.assistant_id, 1),
551
- ).rejects.toThrow("HTTP 404");
552
- });
553
-
554
- it("assistant set latest", async () => {
555
- const owner = await createJwtClient("johndoe", ["assistants:write"]);
556
- const otherUser = await createJwtClient("alice", ["assistants:write"]);
557
-
558
- const assistant = await owner.assistants.create({ graphId: "agent" });
559
- const updated = await owner.assistants.update(assistant.assistant_id, {
560
- metadata: { foo: "bar" },
561
- });
562
- expect(updated.metadata?.foo).toBe("bar");
563
-
564
- await expect(
565
- otherUser.assistants.setLatest(assistant.assistant_id, 1),
566
- ).rejects.toThrow("HTTP 404");
567
-
568
- const result = await owner.assistants.setLatest(assistant.assistant_id, 1);
569
- expect(result.assistant_id).toBe(assistant.assistant_id);
570
- expect(result.version).toBe(1);
571
- });
572
-
573
- it("assistant search filtering", async () => {
574
- const user1 = await createJwtClient("johndoe", ["assistants:write"]);
575
- const user2 = await createJwtClient("alice", ["assistants:write"]);
576
-
577
- const assistant1 = await user1.assistants.create({ graphId: "agent" });
578
- const assistant2 = await user2.assistants.create({ graphId: "agent" });
579
-
580
- // each user should only see their own assistants
581
- const results1 = await user1.assistants.search();
582
- expect(results1).toContainEqual(
583
- expect.objectContaining({ assistant_id: assistant1.assistant_id }),
584
- );
585
- expect(results1).not.toContainEqual(
586
- expect.objectContaining({ assistant_id: assistant2.assistant_id }),
587
- );
588
-
589
- const results2 = await user2.assistants.search();
590
- expect(results2).toContainEqual(
591
- expect.objectContaining({ assistant_id: assistant2.assistant_id }),
592
- );
593
- expect(results2).not.toContainEqual(
594
- expect.objectContaining({ assistant_id: assistant1.assistant_id }),
595
- );
596
- });
597
-
598
- it("thread copy authorization", async () => {
599
- const owner = await createJwtClient("johndoe", ["me"]);
600
- const otherUser = await createJwtClient("alice", ["me"]);
601
-
602
- const thread = await owner.threads.create();
603
-
604
- // Other user can't copy the thread
605
- await expect(otherUser.threads.copy(thread.thread_id)).rejects.toThrow(
606
- "HTTP 409",
607
- );
608
-
609
- // Owner can copy the thread
610
- const copiedThread = await owner.threads.copy(thread.thread_id);
611
- expect(copiedThread).not.toBeNull();
612
- });
613
-
614
- it("thread history authorization", async () => {
615
- const owner = await createJwtClient("johndoe", ["me"]);
616
- const otherUser = await createJwtClient("alice", ["me"]);
617
-
618
- const thread = await owner.threads.create();
619
- const input = { messages: [{ role: "human", content: "foo" }] };
620
-
621
- await owner.runs.wait(thread.thread_id, "agent", { input, config });
622
- const history = await owner.threads.getHistory(thread.thread_id);
623
- expect(history).toHaveLength(5);
624
-
625
- await expect(otherUser.threads.getHistory(thread.thread_id)).rejects.toThrow(
626
- "HTTP 404",
627
- );
628
- });
629
-
630
- it("test stateless runs", async () => {
631
- const owner = await createJwtClient("johndoe", ["me", "assistants:write"]);
632
- const assistant = await owner.assistants.create({ graphId: "agent" });
633
- const input = {
634
- messages: [{ role: "human", content: "stateless run test" }],
635
- };
636
-
637
- const values = await owner.runs.wait(null, assistant.assistant_id, {
638
- input,
639
- config,
640
- });
641
-
642
- expect(values).not.toBeNull();
643
- const chunks = await gatherIterator(
644
- owner.runs.stream(null, assistant.assistant_id, { input, config }),
645
- );
646
-
647
- expect(chunks.find((i) => i.event === "error")).not.toBeDefined();
648
- });
@@ -1,59 +0,0 @@
1
- services:
2
- langgraph-postgres:
3
- image: postgres:16.3
4
- ports:
5
- - "5433:5432"
6
- environment:
7
- POSTGRES_DB: postgres
8
- POSTGRES_USER: postgres
9
- POSTGRES_PASSWORD: postgres
10
- healthcheck:
11
- test: pg_isready -U postgres
12
- start_period: 10s
13
- timeout: 1s
14
- retries: 5
15
- interval: 60s
16
- start_interval: 1s
17
- langgraph-redis:
18
- image: redis:6
19
- restart: on-failure
20
- ports:
21
- - "6381:6379"
22
- healthcheck:
23
- test: redis-cli ping
24
- start_period: 10s
25
- timeout: 1s
26
- retries: 5
27
- interval: 60s
28
- start_interval: 1s
29
- api:
30
- build:
31
- context: graphs
32
- dockerfile_inline: |
33
- FROM langchain/langgraphjs-api:${NODE_VERSION:-20}
34
- ADD . /deps/graphs
35
- WORKDIR /deps/graphs
36
- RUN yarn install --frozen-lockfile
37
- ENV LANGSERVE_GRAPHS='{"agent":"./agent.mts:graph", "agent_simple": "./agent_simple.mts:graph"}'
38
- ENV LANGGRAPH_CONFIG='{"agent": {"configurable": {"model_name": "openai"}}}'
39
- ENV LANGGRAPH_AUTH='{"path": "./auth.mts:auth"}'
40
- ENV LANGGRAPH_UI='{"agent": "./agent.ui.tsx"}'
41
- RUN tsx /api/langgraph_api/js/build.mts
42
- depends_on:
43
- langgraph-postgres:
44
- condition: service_healthy
45
- langgraph-redis:
46
- condition: service_healthy
47
- ports:
48
- - "9123:8000"
49
- healthcheck:
50
- test: python /api/healthcheck.py
51
- interval: 60s
52
- start_interval: 1s
53
- start_period: 10s
54
- environment:
55
- REDIS_URI: redis://langgraph-redis:6379
56
- DATABASE_URI: postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable
57
- N_JOBS_PER_WORKER: "5"
58
- LANGGRAPH_CLOUD_LICENSE_KEY: ${LANGGRAPH_CLOUD_LICENSE_KEY}
59
- FF_JS_ZEROMQ_ENABLED: ${FF_JS_ZEROMQ_ENABLED}