@langgraph-js/pure-graph 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +11 -0
- package/README.md +104 -10
- package/bun.lock +209 -0
- package/dist/adapter/hono/assistants.js +3 -9
- package/dist/adapter/hono/endpoint.js +1 -2
- package/dist/adapter/hono/runs.js +6 -40
- package/dist/adapter/hono/threads.js +5 -46
- package/dist/adapter/nextjs/endpoint.d.ts +1 -0
- package/dist/adapter/nextjs/endpoint.js +2 -0
- package/dist/adapter/nextjs/index.d.ts +1 -0
- package/dist/adapter/nextjs/index.js +2 -0
- package/dist/adapter/nextjs/router.d.ts +5 -0
- package/dist/adapter/nextjs/router.js +168 -0
- package/dist/adapter/{hono → nextjs}/zod.d.ts +5 -5
- package/dist/adapter/{hono → nextjs}/zod.js +22 -5
- package/dist/adapter/zod.d.ts +577 -0
- package/dist/adapter/zod.js +119 -0
- package/dist/createEndpoint.d.ts +1 -2
- package/dist/createEndpoint.js +5 -5
- package/dist/global.d.ts +6 -4
- package/dist/global.js +10 -5
- package/dist/graph/stream.d.ts +1 -1
- package/dist/graph/stream.js +19 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/queue/event_message.js +1 -1
- package/dist/queue/stream_queue.d.ts +5 -3
- package/dist/queue/stream_queue.js +4 -2
- package/dist/storage/index.d.ts +9 -4
- package/dist/storage/index.js +38 -3
- package/dist/storage/memory/queue.js +13 -4
- package/dist/storage/redis/queue.d.ts +39 -0
- package/dist/storage/redis/queue.js +130 -0
- package/dist/storage/sqlite/DB.d.ts +3 -0
- package/dist/storage/sqlite/DB.js +14 -0
- package/dist/storage/sqlite/checkpoint.d.ts +18 -0
- package/dist/storage/sqlite/checkpoint.js +374 -0
- package/dist/storage/sqlite/threads.d.ts +43 -0
- package/dist/storage/sqlite/threads.js +266 -0
- package/dist/storage/sqlite/type.d.ts +15 -0
- package/dist/storage/sqlite/type.js +1 -0
- package/dist/utils/createEntrypointGraph.d.ts +14 -0
- package/dist/utils/createEntrypointGraph.js +11 -0
- package/dist/utils/getGraph.js +3 -3
- package/examples/nextjs/README.md +36 -0
- package/examples/nextjs/app/api/langgraph/[...path]/route.ts +10 -0
- package/examples/nextjs/app/favicon.ico +0 -0
- package/examples/nextjs/app/globals.css +26 -0
- package/examples/nextjs/app/layout.tsx +34 -0
- package/examples/nextjs/app/page.tsx +211 -0
- package/examples/nextjs/next.config.ts +26 -0
- package/examples/nextjs/package.json +24 -0
- package/examples/nextjs/postcss.config.mjs +5 -0
- package/examples/nextjs/tsconfig.json +27 -0
- package/package.json +10 -4
- package/packages/agent-graph/demo.json +35 -0
- package/packages/agent-graph/package.json +18 -0
- package/packages/agent-graph/src/index.ts +47 -0
- package/packages/agent-graph/src/tools/tavily.ts +9 -0
- package/packages/agent-graph/src/tools.ts +38 -0
- package/packages/agent-graph/src/types.ts +42 -0
- package/pnpm-workspace.yaml +4 -0
- package/src/adapter/hono/assistants.ts +16 -33
- package/src/adapter/hono/endpoint.ts +1 -2
- package/src/adapter/hono/runs.ts +15 -51
- package/src/adapter/hono/threads.ts +15 -70
- package/src/adapter/nextjs/endpoint.ts +2 -0
- package/src/adapter/nextjs/index.ts +2 -0
- package/src/adapter/nextjs/router.ts +193 -0
- package/src/adapter/{hono → nextjs}/zod.ts +22 -5
- package/src/adapter/zod.ts +135 -0
- package/src/createEndpoint.ts +12 -5
- package/src/e.d.ts +3 -0
- package/src/global.ts +11 -6
- package/src/graph/stream.ts +20 -10
- package/src/index.ts +1 -0
- package/src/queue/stream_queue.ts +6 -5
- package/src/storage/index.ts +42 -4
- package/src/storage/redis/queue.ts +148 -0
- package/src/storage/sqlite/DB.ts +16 -0
- package/src/storage/sqlite/checkpoint.ts +503 -0
- package/src/storage/sqlite/threads.ts +366 -0
- package/src/storage/sqlite/type.ts +12 -0
- package/src/utils/createEntrypointGraph.ts +20 -0
- package/src/utils/getGraph.ts +3 -3
- package/test/graph/entrypoint.ts +21 -0
- package/test/graph/index.ts +45 -6
- package/test/hono.ts +5 -0
- package/test/test.ts +0 -10
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** @ts-ignore */
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { client } from './endpoint';
|
|
4
|
+
import {
|
|
5
|
+
AssistantsSearchSchema,
|
|
6
|
+
AssistantGraphQuerySchema,
|
|
7
|
+
RunStreamPayloadSchema,
|
|
8
|
+
RunListQuerySchema,
|
|
9
|
+
RunCancelQuerySchema,
|
|
10
|
+
ThreadCreatePayloadSchema,
|
|
11
|
+
ThreadSearchPayloadSchema,
|
|
12
|
+
} from '../zod';
|
|
13
|
+
import { serialiseAsDict } from '../../graph/stream';
|
|
14
|
+
|
|
15
|
+
// Next.js App Router 的 SSE 响应实现
|
|
16
|
+
async function sseResponse(generator: AsyncGenerator<{ event: string; data: unknown }>): Promise<Response> {
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const stream = new ReadableStream({
|
|
19
|
+
async start(controller) {
|
|
20
|
+
try {
|
|
21
|
+
for await (const { event, data } of generator) {
|
|
22
|
+
const line = `event: ${event}\n` + `data: ${serialiseAsDict(data, 0)}\n\n`;
|
|
23
|
+
controller.enqueue(encoder.encode(line));
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// ignore
|
|
27
|
+
} finally {
|
|
28
|
+
controller.close();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
return new Response(stream, {
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
35
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
36
|
+
Connection: 'keep-alive',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 统一路由处理器
|
|
42
|
+
export async function GET(req: NextRequest) {
|
|
43
|
+
const url = new URL(req.url);
|
|
44
|
+
const pathname = url.pathname;
|
|
45
|
+
|
|
46
|
+
// Assistants routes
|
|
47
|
+
if (pathname.match(/\/assistants\/[^/]+\/graph$/)) {
|
|
48
|
+
const match = pathname.match(/\/assistants\/([^/]+)\/graph$/);
|
|
49
|
+
if (match) {
|
|
50
|
+
const assistant_id = match[1];
|
|
51
|
+
const xrayParam = url.searchParams.get('xray');
|
|
52
|
+
const queryParams = { xray: xrayParam };
|
|
53
|
+
const { xray } = AssistantGraphQuerySchema.parse(queryParams);
|
|
54
|
+
const data = await client.assistants.getGraph(assistant_id, {
|
|
55
|
+
xray: xray !== undefined ? xray === 'true' : undefined,
|
|
56
|
+
});
|
|
57
|
+
return NextResponse.json(data);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Threads routes
|
|
62
|
+
if (pathname.match(/\/threads\/[0-9a-fA-F-]{36}$/)) {
|
|
63
|
+
const match = pathname.match(/\/threads\/([0-9a-fA-F-]{36})$/);
|
|
64
|
+
if (match) {
|
|
65
|
+
const thread_id = match[1];
|
|
66
|
+
const data = await client.threads.get(thread_id);
|
|
67
|
+
return NextResponse.json(data);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Runs routes
|
|
72
|
+
if (pathname.match(/\/threads\/[0-9a-fA-F-]{36}\/runs$/)) {
|
|
73
|
+
const match = pathname.match(/\/threads\/([0-9a-fA-F-]{36})\/runs$/);
|
|
74
|
+
if (match) {
|
|
75
|
+
const thread_id = match[1];
|
|
76
|
+
const limit = url.searchParams.get('limit');
|
|
77
|
+
const offset = url.searchParams.get('offset');
|
|
78
|
+
const status = url.searchParams.get('status');
|
|
79
|
+
const queryParams = { limit, offset, status };
|
|
80
|
+
const {
|
|
81
|
+
limit: parsedLimit,
|
|
82
|
+
offset: parsedOffset,
|
|
83
|
+
status: parsedStatus,
|
|
84
|
+
} = RunListQuerySchema.parse(queryParams);
|
|
85
|
+
const runs = await client.runs.list(thread_id, {
|
|
86
|
+
limit: parsedLimit,
|
|
87
|
+
offset: parsedOffset,
|
|
88
|
+
status: parsedStatus,
|
|
89
|
+
});
|
|
90
|
+
return Response.json(runs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new NextResponse('Not Found', { status: 404 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function POST(req: NextRequest) {
|
|
98
|
+
const url = new URL(req.url);
|
|
99
|
+
const pathname = url.pathname;
|
|
100
|
+
|
|
101
|
+
// Assistants routes
|
|
102
|
+
if (pathname.endsWith('/assistants/search')) {
|
|
103
|
+
const body = await req.json();
|
|
104
|
+
const payload = AssistantsSearchSchema.parse(body);
|
|
105
|
+
const data = await client.assistants.search({
|
|
106
|
+
graphId: payload.graph_id,
|
|
107
|
+
metadata: payload.metadata as any,
|
|
108
|
+
limit: payload.limit,
|
|
109
|
+
offset: payload.offset,
|
|
110
|
+
} as any);
|
|
111
|
+
return NextResponse.json(data, {
|
|
112
|
+
headers: { 'X-Pagination-Total': String(data.length) },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Threads routes
|
|
117
|
+
if (pathname.endsWith('/threads')) {
|
|
118
|
+
const body = await req.json();
|
|
119
|
+
const payload = ThreadCreatePayloadSchema.parse(body);
|
|
120
|
+
const thread = await client.threads.create({
|
|
121
|
+
thread_id: payload.thread_id,
|
|
122
|
+
metadata: payload.metadata as any,
|
|
123
|
+
if_exists: (payload.if_exists as any) ?? undefined,
|
|
124
|
+
});
|
|
125
|
+
return NextResponse.json(thread);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pathname.endsWith('/threads/search')) {
|
|
129
|
+
const body = await req.json();
|
|
130
|
+
const payload = ThreadSearchPayloadSchema.parse(body);
|
|
131
|
+
const result = await client.threads.search({
|
|
132
|
+
metadata: payload.metadata as any,
|
|
133
|
+
status: payload.status as any,
|
|
134
|
+
limit: payload.limit,
|
|
135
|
+
offset: payload.offset,
|
|
136
|
+
sortBy: (payload.sort_by as any) ?? undefined,
|
|
137
|
+
sortOrder: (payload.sort_order as any) ?? undefined,
|
|
138
|
+
});
|
|
139
|
+
return NextResponse.json(result, {
|
|
140
|
+
headers: { 'X-Pagination-Total': String(result.length) },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Runs routes - stream
|
|
145
|
+
if (pathname.match(/\/threads\/[0-9a-fA-F-]{36}\/runs\/stream$/)) {
|
|
146
|
+
const match = pathname.match(/\/threads\/([0-9a-fA-F-]{36})\/runs\/stream$/);
|
|
147
|
+
if (match) {
|
|
148
|
+
const thread_id = match[1];
|
|
149
|
+
const body = await req.json();
|
|
150
|
+
const payload = RunStreamPayloadSchema.parse(body);
|
|
151
|
+
const generator = client.runs.stream(thread_id, payload.assistant_id as string, payload as any);
|
|
152
|
+
return sseResponse(generator as any);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Runs routes - cancel
|
|
157
|
+
if (pathname.match(/\/threads\/[0-9a-fA-F-]{36}\/runs\/[0-9a-fA-F-]{36}\/cancel$/)) {
|
|
158
|
+
const match = pathname.match(/\/threads\/([0-9a-fA-F-]{36})\/runs\/([0-9a-fA-F-]{36})\/cancel$/);
|
|
159
|
+
if (match) {
|
|
160
|
+
const thread_id = match[1];
|
|
161
|
+
const run_id = match[2];
|
|
162
|
+
const waitParam = url.searchParams.get('wait');
|
|
163
|
+
const actionParam = url.searchParams.get('action');
|
|
164
|
+
const queryParams = {
|
|
165
|
+
wait: waitParam ? waitParam === 'true' : false,
|
|
166
|
+
action: actionParam ?? 'interrupt',
|
|
167
|
+
};
|
|
168
|
+
const { wait, action } = RunCancelQuerySchema.parse(queryParams);
|
|
169
|
+
const promise = client.runs.cancel(thread_id, run_id, wait, action);
|
|
170
|
+
if (wait) await promise;
|
|
171
|
+
return new Response(null, { status: wait ? 204 : 202 });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return new NextResponse('Not Found', { status: 404 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function DELETE(req: NextRequest) {
|
|
179
|
+
const url = new URL(req.url);
|
|
180
|
+
const pathname = url.pathname;
|
|
181
|
+
|
|
182
|
+
// Threads routes
|
|
183
|
+
if (pathname.match(/\/threads\/[0-9a-fA-F-]{36}$/)) {
|
|
184
|
+
const match = pathname.match(/\/threads\/([0-9a-fA-F-]{36})$/);
|
|
185
|
+
if (match) {
|
|
186
|
+
const thread_id = match[1];
|
|
187
|
+
await client.threads.delete(thread_id);
|
|
188
|
+
return new NextResponse(null, { status: 204 });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new NextResponse('Not Found', { status: 404 });
|
|
193
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import z from
|
|
1
|
+
import z from "zod";
|
|
2
2
|
|
|
3
3
|
export const AssistantConfigurable = z
|
|
4
4
|
.object({
|
|
@@ -14,7 +14,7 @@ export const AssistantConfig = z
|
|
|
14
14
|
configurable: AssistantConfigurable.optional(),
|
|
15
15
|
})
|
|
16
16
|
.catchall(z.unknown())
|
|
17
|
-
.describe(
|
|
17
|
+
.describe("The configuration of an assistant.");
|
|
18
18
|
|
|
19
19
|
export const Assistant = z.object({
|
|
20
20
|
assistant_id: z.string().uuid(),
|
|
@@ -27,7 +27,14 @@ export const Assistant = z.object({
|
|
|
27
27
|
|
|
28
28
|
export const MetadataSchema = z
|
|
29
29
|
.object({
|
|
30
|
-
source: z
|
|
30
|
+
source: z
|
|
31
|
+
.union([
|
|
32
|
+
z.literal("input"),
|
|
33
|
+
z.literal("loop"),
|
|
34
|
+
z.literal("update"),
|
|
35
|
+
z.string(),
|
|
36
|
+
])
|
|
37
|
+
.optional(),
|
|
31
38
|
step: z.number().optional(),
|
|
32
39
|
writes: z.record(z.unknown()).nullable().optional(),
|
|
33
40
|
parents: z.record(z.string()).optional(),
|
|
@@ -41,9 +48,19 @@ export const SendSchema = z.object({
|
|
|
41
48
|
|
|
42
49
|
export const CommandSchema = z.object({
|
|
43
50
|
update: z
|
|
44
|
-
.union([
|
|
51
|
+
.union([
|
|
52
|
+
z.record(z.unknown()),
|
|
53
|
+
z.array(z.tuple([z.string(), z.unknown()])),
|
|
54
|
+
])
|
|
45
55
|
.nullable()
|
|
46
56
|
.optional(),
|
|
47
57
|
resume: z.unknown().optional(),
|
|
48
|
-
goto: z
|
|
58
|
+
goto: z
|
|
59
|
+
.union([
|
|
60
|
+
SendSchema,
|
|
61
|
+
z.array(SendSchema),
|
|
62
|
+
z.string(),
|
|
63
|
+
z.array(z.string()),
|
|
64
|
+
])
|
|
65
|
+
.optional(),
|
|
49
66
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
|
|
3
|
+
export const AssistantConfigurable = z
|
|
4
|
+
.object({
|
|
5
|
+
thread_id: z.string().optional(),
|
|
6
|
+
thread_ts: z.string().optional(),
|
|
7
|
+
})
|
|
8
|
+
.catchall(z.unknown());
|
|
9
|
+
|
|
10
|
+
export const AssistantConfig = z
|
|
11
|
+
.object({
|
|
12
|
+
tags: z.array(z.string()).optional(),
|
|
13
|
+
recursion_limit: z.number().int().optional(),
|
|
14
|
+
configurable: AssistantConfigurable.optional(),
|
|
15
|
+
})
|
|
16
|
+
.catchall(z.unknown())
|
|
17
|
+
.describe('The configuration of an assistant.');
|
|
18
|
+
|
|
19
|
+
export const Assistant = z.object({
|
|
20
|
+
assistant_id: z.string().uuid(),
|
|
21
|
+
graph_id: z.string(),
|
|
22
|
+
config: AssistantConfig,
|
|
23
|
+
created_at: z.string(),
|
|
24
|
+
updated_at: z.string(),
|
|
25
|
+
metadata: z.object({}).catchall(z.any()),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const MetadataSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
source: z.union([z.literal('input'), z.literal('loop'), z.literal('update'), z.string()]).optional(),
|
|
31
|
+
step: z.number().optional(),
|
|
32
|
+
writes: z.record(z.unknown()).nullable().optional(),
|
|
33
|
+
parents: z.record(z.string()).optional(),
|
|
34
|
+
})
|
|
35
|
+
.catchall(z.unknown());
|
|
36
|
+
|
|
37
|
+
export const SendSchema = z.object({
|
|
38
|
+
node: z.string(),
|
|
39
|
+
input: z.unknown().nullable(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const CommandSchema = z.object({
|
|
43
|
+
update: z
|
|
44
|
+
.union([z.record(z.unknown()), z.array(z.tuple([z.string(), z.unknown()]))])
|
|
45
|
+
.nullable()
|
|
46
|
+
.optional(),
|
|
47
|
+
resume: z.unknown().optional(),
|
|
48
|
+
goto: z.union([SendSchema, z.array(SendSchema), z.string(), z.array(z.string())]).optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 公共的查询参数验证 schema
|
|
52
|
+
export const PaginationQuerySchema = z.object({
|
|
53
|
+
limit: z.number().int().optional(),
|
|
54
|
+
offset: z.number().int().optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const ThreadIdParamSchema = z.object({
|
|
58
|
+
thread_id: z.string().uuid(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const RunIdParamSchema = z.object({
|
|
62
|
+
thread_id: z.string().uuid(),
|
|
63
|
+
run_id: z.string().uuid(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Assistants 相关的 schema
|
|
67
|
+
export const AssistantsSearchSchema = z.object({
|
|
68
|
+
graph_id: z.string().optional(),
|
|
69
|
+
metadata: MetadataSchema.optional(),
|
|
70
|
+
limit: z.number().int().optional(),
|
|
71
|
+
offset: z.number().int().optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const AssistantGraphQuerySchema = z.object({
|
|
75
|
+
xray: z.string().optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Runs 相关的 schema
|
|
79
|
+
export const RunStreamPayloadSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
assistant_id: z.union([z.string().uuid(), z.string()]),
|
|
82
|
+
checkpoint_id: z.string().optional(),
|
|
83
|
+
input: z.any().optional(),
|
|
84
|
+
command: CommandSchema.optional(),
|
|
85
|
+
metadata: MetadataSchema.optional(),
|
|
86
|
+
config: AssistantConfig.optional(),
|
|
87
|
+
webhook: z.string().optional(),
|
|
88
|
+
interrupt_before: z.union([z.literal('*'), z.array(z.string())]).optional(),
|
|
89
|
+
interrupt_after: z.union([z.literal('*'), z.array(z.string())]).optional(),
|
|
90
|
+
on_disconnect: z.enum(['cancel', 'continue']).optional().default('continue'),
|
|
91
|
+
multitask_strategy: z.enum(['reject', 'rollback', 'interrupt', 'enqueue']).optional(),
|
|
92
|
+
stream_mode: z
|
|
93
|
+
.array(z.enum(['values', 'messages', 'messages-tuple', 'updates', 'events', 'debug', 'custom']))
|
|
94
|
+
.optional(),
|
|
95
|
+
stream_subgraphs: z.boolean().optional(),
|
|
96
|
+
stream_resumable: z.boolean().optional(),
|
|
97
|
+
after_seconds: z.number().optional(),
|
|
98
|
+
if_not_exists: z.enum(['create', 'reject']).optional(),
|
|
99
|
+
on_completion: z.enum(['complete', 'continue']).optional(),
|
|
100
|
+
feedback_keys: z.array(z.string()).optional(),
|
|
101
|
+
langsmith_tracer: z.unknown().optional(),
|
|
102
|
+
})
|
|
103
|
+
.describe('Payload for creating a stateful run.');
|
|
104
|
+
|
|
105
|
+
export const RunListQuerySchema = z.object({
|
|
106
|
+
limit: z.coerce.number().int().optional(),
|
|
107
|
+
offset: z.coerce.number().int().optional(),
|
|
108
|
+
status: z.enum(['pending', 'running', 'error', 'success', 'timeout', 'interrupted']).optional(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const RunCancelQuerySchema = z.object({
|
|
112
|
+
wait: z.coerce.boolean().optional().default(false),
|
|
113
|
+
action: z.enum(['interrupt', 'rollback']).optional().default('interrupt'),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Threads 相关的 schema
|
|
117
|
+
export const ThreadCreatePayloadSchema = z
|
|
118
|
+
.object({
|
|
119
|
+
thread_id: z.string().uuid().describe('The ID of the thread. If not provided, an ID is generated.').optional(),
|
|
120
|
+
metadata: MetadataSchema.optional(),
|
|
121
|
+
if_exists: z.union([z.literal('raise'), z.literal('do_nothing')]).optional(),
|
|
122
|
+
})
|
|
123
|
+
.describe('Payload for creating a thread.');
|
|
124
|
+
|
|
125
|
+
export const ThreadSearchPayloadSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
metadata: z.record(z.unknown()).describe('Metadata to search for.').optional(),
|
|
128
|
+
status: z.enum(['idle', 'busy', 'interrupted', 'error']).describe('Filter by thread status.').optional(),
|
|
129
|
+
values: z.record(z.unknown()).describe('Filter by thread values.').optional(),
|
|
130
|
+
limit: z.number().int().gte(1).lte(1000).describe('Maximum number to return.').optional(),
|
|
131
|
+
offset: z.number().int().gte(0).describe('Offset to start from.').optional(),
|
|
132
|
+
sort_by: z.enum(['thread_id', 'status', 'created_at', 'updated_at']).describe('Sort by field.').optional(),
|
|
133
|
+
sort_order: z.enum(['asc', 'desc']).describe('Sort order.').optional(),
|
|
134
|
+
})
|
|
135
|
+
.describe('Payload for listing threads.');
|
package/src/createEndpoint.ts
CHANGED
|
@@ -2,8 +2,7 @@ import { StreamEvent } from '@langchain/core/tracers/log_stream';
|
|
|
2
2
|
import { streamState } from './graph/stream.js';
|
|
3
3
|
import { Assistant, Run, StreamMode, Metadata, AssistantGraph } from '@langchain/langgraph-sdk';
|
|
4
4
|
import { getGraph, GRAPHS } from './utils/getGraph.js';
|
|
5
|
-
import {
|
|
6
|
-
import { globalMessageQueue } from './global.js';
|
|
5
|
+
import { LangGraphGlobal } from './global.js';
|
|
7
6
|
import { AssistantSortBy, CancelAction, ILangGraphClient, RunStatus, SortOrder, StreamInputData } from './types.js';
|
|
8
7
|
export { registerGraph } from './utils/getGraph.js';
|
|
9
8
|
|
|
@@ -57,16 +56,24 @@ export const AssistantEndpoint: ILangGraphClient['assistants'] = {
|
|
|
57
56
|
},
|
|
58
57
|
};
|
|
59
58
|
|
|
60
|
-
export const createEndpoint = (
|
|
59
|
+
export const createEndpoint = (): ILangGraphClient => {
|
|
60
|
+
const threads = LangGraphGlobal.globalThreadsManager;
|
|
61
61
|
return {
|
|
62
62
|
assistants: AssistantEndpoint,
|
|
63
63
|
threads,
|
|
64
64
|
runs: {
|
|
65
|
-
list(
|
|
65
|
+
list(
|
|
66
|
+
threadId: string,
|
|
67
|
+
options?: {
|
|
68
|
+
limit?: number;
|
|
69
|
+
offset?: number;
|
|
70
|
+
status?: RunStatus;
|
|
71
|
+
},
|
|
72
|
+
): Promise<Run[]> {
|
|
66
73
|
return threads.listRuns(threadId, options);
|
|
67
74
|
},
|
|
68
75
|
async cancel(threadId: string, runId: string, wait?: boolean, action?: CancelAction): Promise<void> {
|
|
69
|
-
return globalMessageQueue.cancelQueue(runId);
|
|
76
|
+
return LangGraphGlobal.globalMessageQueue.cancelQueue(runId);
|
|
70
77
|
},
|
|
71
78
|
async *stream(threadId: string, assistantId: string, payload: StreamInputData) {
|
|
72
79
|
if (!payload.config) {
|
package/src/e.d.ts
ADDED
package/src/global.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { createCheckPointer, createMessageQueue } from './storage/index.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { createCheckPointer, createMessageQueue, createThreadManager } from './storage/index.js';
|
|
2
|
+
import type { SqliteSaver } from './storage/sqlite/checkpoint.js';
|
|
3
|
+
const [globalMessageQueue, globalCheckPointer] = await Promise.all([createMessageQueue(), createCheckPointer()]);
|
|
4
|
+
const globalThreadsManager = await createThreadManager({
|
|
5
|
+
checkpointer: globalCheckPointer as SqliteSaver,
|
|
6
|
+
});
|
|
7
|
+
export class LangGraphGlobal {
|
|
8
|
+
static globalMessageQueue = globalMessageQueue;
|
|
9
|
+
static globalCheckPointer = globalCheckPointer;
|
|
10
|
+
static globalThreadsManager = globalThreadsManager;
|
|
11
|
+
}
|
package/src/graph/stream.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Pregel } from '@langchain/langgraph/pregel';
|
|
|
4
4
|
import { getLangGraphCommand } from '../utils/getLangGraphCommand.js';
|
|
5
5
|
import type { BaseStreamQueueInterface } from '../queue/stream_queue.js';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { LangGraphGlobal } from '../global.js';
|
|
8
8
|
import { Run } from '@langgraph-js/sdk';
|
|
9
9
|
import { EventMessage, StreamErrorEventMessage, StreamEndEventMessage } from '../queue/event_message.js';
|
|
10
10
|
|
|
@@ -54,7 +54,13 @@ export async function streamStateWithQueue(
|
|
|
54
54
|
libStreamMode.add('values');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
await queue.push(
|
|
57
|
+
await queue.push(
|
|
58
|
+
new EventMessage('metadata', {
|
|
59
|
+
run_id: run.run_id,
|
|
60
|
+
attempt: options.attempt,
|
|
61
|
+
graph_id: graphId,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
58
64
|
|
|
59
65
|
const metadata = {
|
|
60
66
|
...payload.config?.metadata,
|
|
@@ -107,7 +113,9 @@ export async function streamStateWithQueue(
|
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
if (mode === 'values') {
|
|
110
|
-
await threads.set(run.thread_id, {
|
|
116
|
+
await threads.set(run.thread_id, {
|
|
117
|
+
values: data ? JSON.parse(serialiseAsDict(data)) : '',
|
|
118
|
+
});
|
|
111
119
|
}
|
|
112
120
|
} else if (userStreamMode.includes('events')) {
|
|
113
121
|
await queue.push(new EventMessage('events', event));
|
|
@@ -151,7 +159,9 @@ export async function streamStateWithQueue(
|
|
|
151
159
|
if (messages[message.id] == null) {
|
|
152
160
|
messages[message.id] = message;
|
|
153
161
|
await queue.push(
|
|
154
|
-
new EventMessage('messages/metadata', {
|
|
162
|
+
new EventMessage('messages/metadata', {
|
|
163
|
+
[message.id]: { metadata: event.metadata },
|
|
164
|
+
}),
|
|
155
165
|
);
|
|
156
166
|
} else {
|
|
157
167
|
messages[message.id] = messages[message.id].concat(message);
|
|
@@ -174,11 +184,11 @@ export async function streamStateWithQueue(
|
|
|
174
184
|
* @returns 数据流生成器
|
|
175
185
|
*/
|
|
176
186
|
export async function* createStreamFromQueue(queueId: string): AsyncGenerator<{ event: string; data: unknown }> {
|
|
177
|
-
const queue = globalMessageQueue.getQueue(queueId);
|
|
187
|
+
const queue = LangGraphGlobal.globalMessageQueue.getQueue(queueId);
|
|
178
188
|
return queue.onDataReceive();
|
|
179
189
|
}
|
|
180
190
|
|
|
181
|
-
export const serialiseAsDict = (obj: unknown) => {
|
|
191
|
+
export const serialiseAsDict = (obj: unknown, indent = 2) => {
|
|
182
192
|
return JSON.stringify(
|
|
183
193
|
obj,
|
|
184
194
|
function (key: string | number, value: unknown) {
|
|
@@ -196,7 +206,7 @@ export const serialiseAsDict = (obj: unknown) => {
|
|
|
196
206
|
|
|
197
207
|
return value;
|
|
198
208
|
},
|
|
199
|
-
|
|
209
|
+
indent,
|
|
200
210
|
);
|
|
201
211
|
};
|
|
202
212
|
/**
|
|
@@ -227,12 +237,12 @@ export async function* streamState(
|
|
|
227
237
|
// 启动队列推送任务(在后台异步执行)
|
|
228
238
|
await threads.set(threadId, { status: 'busy' });
|
|
229
239
|
await threads.updateRun(run.run_id, { status: 'running' });
|
|
230
|
-
const queue = globalMessageQueue.createQueue(queueId);
|
|
240
|
+
const queue = LangGraphGlobal.globalMessageQueue.createQueue(queueId);
|
|
231
241
|
const state = queue.onDataReceive();
|
|
232
242
|
streamStateWithQueue(threads, run, queue, payload, options).catch((error) => {
|
|
233
243
|
console.error('Queue task error:', error);
|
|
234
244
|
// 如果生产者出错,向队列推送错误信号
|
|
235
|
-
globalMessageQueue.pushToQueue(queueId, new StreamErrorEventMessage(error));
|
|
245
|
+
LangGraphGlobal.globalMessageQueue.pushToQueue(queueId, new StreamErrorEventMessage(error));
|
|
236
246
|
// TODO 不知道这里需不需要错误处理
|
|
237
247
|
});
|
|
238
248
|
for await (const data of state) {
|
|
@@ -248,6 +258,6 @@ export async function* streamState(
|
|
|
248
258
|
} finally {
|
|
249
259
|
// 在完成后清理队列
|
|
250
260
|
await threads.set(threadId, { status: 'idle' });
|
|
251
|
-
globalMessageQueue.removeQueue(queueId);
|
|
261
|
+
LangGraphGlobal.globalMessageQueue.removeQueue(queueId);
|
|
252
262
|
}
|
|
253
263
|
}
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export class BaseStreamQueue extends EventEmitter<StreamQueueEvents<EventMessage
|
|
|
26
26
|
* Constructor
|
|
27
27
|
* @param compressMessages 是否压缩消息 / Whether to compress messages
|
|
28
28
|
*/
|
|
29
|
-
constructor(readonly compressMessages: boolean = true) {
|
|
29
|
+
constructor(readonly id: string, readonly compressMessages: boolean = true) {
|
|
30
30
|
super();
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -58,6 +58,7 @@ export class BaseStreamQueue extends EventEmitter<StreamQueueEvents<EventMessage
|
|
|
58
58
|
* Base stream queue interface
|
|
59
59
|
*/
|
|
60
60
|
export interface BaseStreamQueueInterface {
|
|
61
|
+
id: string;
|
|
61
62
|
/** 是否压缩消息 / Whether to compress messages */
|
|
62
63
|
compressMessages: boolean;
|
|
63
64
|
/**
|
|
@@ -76,7 +77,7 @@ export interface BaseStreamQueueInterface {
|
|
|
76
77
|
* @param listener 数据变化监听器 / Data change listener
|
|
77
78
|
* @returns 取消监听函数 / Unsubscribe function
|
|
78
79
|
*/
|
|
79
|
-
|
|
80
|
+
onDataReceive(): AsyncGenerator<EventMessage, void, unknown>;
|
|
80
81
|
/** 取消信号控制器 / Cancel signal controller */
|
|
81
82
|
cancelSignal: AbortController;
|
|
82
83
|
/** 取消操作 / Cancel operation */
|
|
@@ -93,7 +94,7 @@ export class StreamQueueManager<Q extends BaseStreamQueueInterface> {
|
|
|
93
94
|
/** 默认是否压缩消息 / Default compress messages setting */
|
|
94
95
|
private defaultCompressMessages: boolean;
|
|
95
96
|
/** 队列构造函数 / Queue constructor */
|
|
96
|
-
private queueConstructor: new (
|
|
97
|
+
private queueConstructor: new (queueId: string) => Q;
|
|
97
98
|
|
|
98
99
|
/**
|
|
99
100
|
* 构造函数
|
|
@@ -102,7 +103,7 @@ export class StreamQueueManager<Q extends BaseStreamQueueInterface> {
|
|
|
102
103
|
* @param options 配置选项 / Configuration options
|
|
103
104
|
*/
|
|
104
105
|
constructor(
|
|
105
|
-
queueConstructor: new (
|
|
106
|
+
queueConstructor: new (id: string) => Q,
|
|
106
107
|
options: {
|
|
107
108
|
/** 默认是否压缩消息 / Default compress messages setting */
|
|
108
109
|
defaultCompressMessages?: boolean;
|
|
@@ -121,7 +122,7 @@ export class StreamQueueManager<Q extends BaseStreamQueueInterface> {
|
|
|
121
122
|
*/
|
|
122
123
|
createQueue(id: string, compressMessages?: boolean): Q {
|
|
123
124
|
const compress = compressMessages ?? this.defaultCompressMessages;
|
|
124
|
-
this.queues.set(id, new this.queueConstructor(
|
|
125
|
+
this.queues.set(id, new this.queueConstructor(id));
|
|
125
126
|
return this.queues.get(id)!;
|
|
126
127
|
}
|
|
127
128
|
|
package/src/storage/index.ts
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
|
-
import { StreamQueueManager } from '../queue/stream_queue';
|
|
1
|
+
import { BaseStreamQueueInterface, StreamQueueManager } from '../queue/stream_queue';
|
|
2
2
|
import { MemorySaver } from './memory/checkpoint';
|
|
3
3
|
import { MemoryStreamQueue } from './memory/queue';
|
|
4
|
+
import { MemoryThreadsManager } from './memory/threads';
|
|
5
|
+
import type { SqliteSaver as SqliteSaverType } from './sqlite/checkpoint';
|
|
6
|
+
import { SQLiteThreadsManager } from './sqlite/threads';
|
|
4
7
|
|
|
5
8
|
// 所有的适配实现,都请写到这里,通过环境变量进行判断使用哪种方式进行适配
|
|
6
9
|
|
|
7
|
-
export const createCheckPointer = () => {
|
|
10
|
+
export const createCheckPointer = async () => {
|
|
11
|
+
if (
|
|
12
|
+
(process.env.REDIS_URL && process.env.CHECKPOINT_TYPE === 'redis') ||
|
|
13
|
+
process.env.CHECKPOINT_TYPE === 'shallow/redis'
|
|
14
|
+
) {
|
|
15
|
+
if (process.env.CHECKPOINT_TYPE === 'redis') {
|
|
16
|
+
const { RedisSaver } = await import('@langchain/langgraph-checkpoint-redis');
|
|
17
|
+
return await RedisSaver.fromUrl(process.env.REDIS_URL!, {
|
|
18
|
+
defaultTTL: 60, // TTL in minutes
|
|
19
|
+
refreshOnRead: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (process.env.CHECKPOINT_TYPE === 'shallow/redis') {
|
|
23
|
+
const { ShallowRedisSaver } = await import('@langchain/langgraph-checkpoint-redis/shallow');
|
|
24
|
+
return await ShallowRedisSaver.fromUrl(process.env.REDIS_URL!);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (process.env.SQLITE_DATABASE_URI) {
|
|
28
|
+
const { SqliteSaver } = await import('./sqlite/checkpoint');
|
|
29
|
+
const db = SqliteSaver.fromConnString(process.env.SQLITE_DATABASE_URI);
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
8
32
|
return new MemorySaver();
|
|
9
33
|
};
|
|
10
34
|
|
|
11
|
-
export const createMessageQueue = () => {
|
|
12
|
-
|
|
35
|
+
export const createMessageQueue = async () => {
|
|
36
|
+
let q: new (id: string) => BaseStreamQueueInterface;
|
|
37
|
+
if (process.env.REDIS_URL) {
|
|
38
|
+
console.log('Using redis as stream queue');
|
|
39
|
+
const { RedisStreamQueue } = await import('./redis/queue');
|
|
40
|
+
q = RedisStreamQueue;
|
|
41
|
+
} else {
|
|
42
|
+
q = MemoryStreamQueue;
|
|
43
|
+
}
|
|
13
44
|
return new StreamQueueManager(q);
|
|
14
45
|
};
|
|
46
|
+
|
|
47
|
+
export const createThreadManager = (config: { checkpointer?: SqliteSaverType }) => {
|
|
48
|
+
if (process.env.SQLITE_DATABASE_URI && config.checkpointer) {
|
|
49
|
+
return new SQLiteThreadsManager(config.checkpointer);
|
|
50
|
+
}
|
|
51
|
+
return new MemoryThreadsManager();
|
|
52
|
+
};
|