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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/assistants.py +4 -4
- langgraph_api/api/store.py +10 -6
- langgraph_api/config.py +1 -0
- langgraph_api/graph.py +28 -5
- langgraph_api/js/remote.py +16 -11
- langgraph_api/metadata.py +28 -16
- langgraph_api/store.py +127 -0
- langgraph_api/stream.py +17 -7
- langgraph_api/worker.py +1 -1
- {langgraph_api-0.2.27.dist-info → langgraph_api-0.2.28.dist-info}/METADATA +24 -30
- {langgraph_api-0.2.27.dist-info → langgraph_api-0.2.28.dist-info}/RECORD +42 -65
- {langgraph_api-0.2.27.dist-info → langgraph_api-0.2.28.dist-info}/WHEEL +1 -1
- langgraph_api-0.2.28.dist-info/entry_points.txt +2 -0
- langgraph_api/js/tests/api.test.mts +0 -2194
- langgraph_api/js/tests/auth.test.mts +0 -648
- langgraph_api/js/tests/compose-postgres.auth.yml +0 -59
- langgraph_api/js/tests/compose-postgres.yml +0 -59
- langgraph_api/js/tests/graphs/.gitignore +0 -1
- langgraph_api/js/tests/graphs/agent.css +0 -1
- langgraph_api/js/tests/graphs/agent.mts +0 -187
- langgraph_api/js/tests/graphs/agent.ui.tsx +0 -10
- langgraph_api/js/tests/graphs/agent_simple.mts +0 -105
- langgraph_api/js/tests/graphs/auth.mts +0 -106
- langgraph_api/js/tests/graphs/command.mts +0 -48
- langgraph_api/js/tests/graphs/delay.mts +0 -30
- langgraph_api/js/tests/graphs/dynamic.mts +0 -24
- langgraph_api/js/tests/graphs/error.mts +0 -17
- langgraph_api/js/tests/graphs/http.mts +0 -76
- langgraph_api/js/tests/graphs/langgraph.json +0 -11
- langgraph_api/js/tests/graphs/nested.mts +0 -44
- langgraph_api/js/tests/graphs/package.json +0 -13
- langgraph_api/js/tests/graphs/weather.mts +0 -57
- langgraph_api/js/tests/graphs/yarn.lock +0 -242
- langgraph_api/js/tests/utils.mts +0 -17
- langgraph_api-0.2.27.dist-info/LICENSE +0 -93
- langgraph_api-0.2.27.dist-info/entry_points.txt +0 -3
- logging.json +0 -22
- openapi.json +0 -4562
- /LICENSE → /langgraph_api-0.2.28.dist-info/licenses/LICENSE +0 -0
|
@@ -1,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}
|