@langgraph-js/sdk 3.8.4 → 4.0.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/dist/LangGraphClient.d.ts +9 -15
- package/dist/LangGraphClient.js +49 -4
- package/dist/ToolManager.d.ts +3 -2
- package/dist/ToolManager.js +6 -3
- package/dist/humanInTheLoop.d.ts +38 -0
- package/dist/humanInTheLoop.js +4 -0
- package/dist/tool/ToolUI.d.ts +8 -3
- package/dist/tool/ToolUI.js +25 -6
- package/dist/tool/createTool.d.ts +3 -19
- package/package.json +1 -1
- package/src/LangGraphClient.ts +52 -19
- package/src/ToolManager.ts +6 -3
- package/src/humanInTheLoop.ts +40 -0
- package/src/tool/ToolUI.ts +27 -8
- package/src/tool/createTool.ts +4 -18
|
@@ -2,6 +2,7 @@ import type { Thread, Message, Assistant, Command } from "@langchain/langgraph-s
|
|
|
2
2
|
import { EventEmitter } from "eventemitter3";
|
|
3
3
|
import { ToolManager } from "./ToolManager.js";
|
|
4
4
|
import { CallToolResult } from "./tool/createTool.js";
|
|
5
|
+
import { HumanInTheLoopDecision, HumanInTheLoopState } from "./humanInTheLoop.js";
|
|
5
6
|
import { type ILangGraphClient } from "@langgraph-js/pure-graph/dist/types.js";
|
|
6
7
|
import { RevertChatToOptions } from "./time-travel/index.js";
|
|
7
8
|
export type RenderMessage = Message & {
|
|
@@ -44,20 +45,6 @@ export type SendMessageOptions = {
|
|
|
44
45
|
command?: Command;
|
|
45
46
|
joinRunId?: string;
|
|
46
47
|
};
|
|
47
|
-
export type InterruptData = {
|
|
48
|
-
id: string;
|
|
49
|
-
value: {
|
|
50
|
-
actionRequests: {
|
|
51
|
-
name: string;
|
|
52
|
-
description: string;
|
|
53
|
-
args: any;
|
|
54
|
-
}[];
|
|
55
|
-
reviewConfigs: {
|
|
56
|
-
actionName: string;
|
|
57
|
-
allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
|
|
58
|
-
}[];
|
|
59
|
-
};
|
|
60
|
-
}[];
|
|
61
48
|
export interface LangGraphClientConfig {
|
|
62
49
|
apiUrl?: string;
|
|
63
50
|
apiKey?: string;
|
|
@@ -244,7 +231,7 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
|
|
|
244
231
|
messages: Message[];
|
|
245
232
|
}>;
|
|
246
233
|
messagesMetadata: {};
|
|
247
|
-
humanInTheLoop:
|
|
234
|
+
humanInTheLoop: HumanInTheLoopState | null;
|
|
248
235
|
/**
|
|
249
236
|
* @zh 发送消息到 LangGraph 后端。
|
|
250
237
|
* @en Sends a message to the LangGraph backend.
|
|
@@ -267,6 +254,7 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
|
|
|
267
254
|
*/
|
|
268
255
|
private processStreamChunk;
|
|
269
256
|
private runFETool;
|
|
257
|
+
private responseHumanInTheLoop;
|
|
270
258
|
private callFETool;
|
|
271
259
|
extraParams: Record<string, any>;
|
|
272
260
|
/**
|
|
@@ -277,8 +265,14 @@ export declare class LangGraphClient<TStateType = unknown> extends EventEmitter<
|
|
|
277
265
|
/**
|
|
278
266
|
* @zh 标记前端工具等待已完成。
|
|
279
267
|
* @en Marks the frontend tool waiting as completed.
|
|
268
|
+
* @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
|
|
280
269
|
*/
|
|
281
270
|
doneFEToolWaiting(id: string, result: CallToolResult): void;
|
|
271
|
+
/**
|
|
272
|
+
* @zh 标记人机交互等待已完成。
|
|
273
|
+
* @en Marks the human in the loop waiting as completed.
|
|
274
|
+
*/
|
|
275
|
+
doneHumanInTheLoopWaiting(tool_id: string, action_request_id: string, result: HumanInTheLoopDecision): void;
|
|
282
276
|
/**
|
|
283
277
|
* @zh 获取当前的 Thread。
|
|
284
278
|
* @en Gets the current Thread.
|
package/dist/LangGraphClient.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "eventemitter3";
|
|
2
2
|
import { ToolManager } from "./ToolManager.js";
|
|
3
|
+
import { createActionRequestID } from "./humanInTheLoop.js";
|
|
3
4
|
import { MessageProcessor } from "./MessageProcessor.js";
|
|
4
5
|
import { revertChatTo } from "./time-travel/index.js";
|
|
5
6
|
import camelcaseKeys from "camelcase-keys";
|
|
@@ -281,9 +282,9 @@ export class LangGraphClient extends EventEmitter {
|
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
const data = await this.runFETool();
|
|
285
|
+
await this.responseHumanInTheLoop();
|
|
284
286
|
if (data)
|
|
285
287
|
streamRecord.push(...data);
|
|
286
|
-
this.humanInTheLoop = null;
|
|
287
288
|
this._status = "idle";
|
|
288
289
|
this.emit("done", {
|
|
289
290
|
event: "done",
|
|
@@ -334,9 +335,30 @@ export class LangGraphClient extends EventEmitter {
|
|
|
334
335
|
const data = chunk.data;
|
|
335
336
|
if (data?.__interrupt__) {
|
|
336
337
|
this._status = "interrupted";
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
const humanInTheLoop = data?.__interrupt__.map((i) => {
|
|
339
|
+
// 修复 python 版本是以下划线命名的,而 js 版本是以驼峰命名的
|
|
340
|
+
if (i && "review_configs" in i.value) {
|
|
341
|
+
return {
|
|
342
|
+
id: i.id,
|
|
343
|
+
value: {
|
|
344
|
+
/** @ts-ignore */
|
|
345
|
+
actionRequests: i.value.action_requests,
|
|
346
|
+
reviewConfigs: camelcaseKeys(i.value.review_configs, { deep: true }),
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return i;
|
|
351
|
+
});
|
|
352
|
+
humanInTheLoop.forEach((i) => {
|
|
353
|
+
i.value.actionRequests.forEach((j) => {
|
|
354
|
+
j.id = j.id || createActionRequestID(j);
|
|
355
|
+
});
|
|
356
|
+
return i;
|
|
339
357
|
});
|
|
358
|
+
this.humanInTheLoop = {
|
|
359
|
+
interruptData: humanInTheLoop,
|
|
360
|
+
result: {},
|
|
361
|
+
};
|
|
340
362
|
}
|
|
341
363
|
else if (data?.messages) {
|
|
342
364
|
const isResume = !!command?.resume;
|
|
@@ -375,16 +397,27 @@ export class LangGraphClient extends EventEmitter {
|
|
|
375
397
|
});
|
|
376
398
|
this._status = "interrupted";
|
|
377
399
|
this.currentThread.status = "interrupted"; // 修复某些机制下,状态不为 interrupted 与后端有差异
|
|
400
|
+
console.log("batch call tools", result.length);
|
|
378
401
|
return Promise.all(result);
|
|
379
402
|
}
|
|
380
403
|
}
|
|
404
|
+
async responseHumanInTheLoop() {
|
|
405
|
+
if (this.humanInTheLoop) {
|
|
406
|
+
const humanInTheLoop = this.humanInTheLoop;
|
|
407
|
+
this.humanInTheLoop = null;
|
|
408
|
+
return this.resume({
|
|
409
|
+
decisions: humanInTheLoop.interruptData[0].value.actionRequests.map((i) => {
|
|
410
|
+
return humanInTheLoop.result[i.id];
|
|
411
|
+
}),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
381
415
|
async callFETool(message, args) {
|
|
382
416
|
const that = this; // 防止 this 被错误解析
|
|
383
417
|
const result = await this.tools.callTool(message.name, args, { client: that, message });
|
|
384
418
|
if (!result) {
|
|
385
419
|
return;
|
|
386
420
|
}
|
|
387
|
-
return this.resume(result);
|
|
388
421
|
}
|
|
389
422
|
extraParams = {};
|
|
390
423
|
/**
|
|
@@ -401,6 +434,7 @@ export class LangGraphClient extends EventEmitter {
|
|
|
401
434
|
/**
|
|
402
435
|
* @zh 标记前端工具等待已完成。
|
|
403
436
|
* @en Marks the frontend tool waiting as completed.
|
|
437
|
+
* @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
|
|
404
438
|
*/
|
|
405
439
|
doneFEToolWaiting(id, result) {
|
|
406
440
|
const done = this.tools.doneWaiting(id, result);
|
|
@@ -408,6 +442,17 @@ export class LangGraphClient extends EventEmitter {
|
|
|
408
442
|
this.resume(result);
|
|
409
443
|
}
|
|
410
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* @zh 标记人机交互等待已完成。
|
|
447
|
+
* @en Marks the human in the loop waiting as completed.
|
|
448
|
+
*/
|
|
449
|
+
doneHumanInTheLoopWaiting(tool_id, action_request_id, result) {
|
|
450
|
+
// 移除等待状态
|
|
451
|
+
this.tools.doneWaiting(tool_id, result);
|
|
452
|
+
if (this.humanInTheLoop) {
|
|
453
|
+
this.humanInTheLoop.result[action_request_id] = result;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
411
456
|
/**
|
|
412
457
|
* @zh 获取当前的 Thread。
|
|
413
458
|
* @en Gets the current Thread.
|
package/dist/ToolManager.d.ts
CHANGED
|
@@ -62,11 +62,12 @@ export declare class ToolManager {
|
|
|
62
62
|
* @en Marks the tool waiting with the specified ID as completed and passes the result.
|
|
63
63
|
*/
|
|
64
64
|
doneWaiting(id: string, value: CallToolResult): boolean;
|
|
65
|
+
isAllToolCompleted(): number;
|
|
65
66
|
/**
|
|
66
67
|
* @zh 等待指定 ID 的工具完成。
|
|
67
68
|
* @en Waits for the tool with the specified ID to complete.
|
|
68
69
|
*/
|
|
69
|
-
waitForDone(id: string): Promise<unknown
|
|
70
|
+
waitForDone(id: string): Promise<unknown>;
|
|
70
71
|
/**
|
|
71
72
|
* @zh 一个静态方法,用于在前端等待用户界面操作完成。
|
|
72
73
|
* @en A static method used in the frontend to wait for user interface operations to complete.
|
|
@@ -74,5 +75,5 @@ export declare class ToolManager {
|
|
|
74
75
|
static waitForUIDone<T>(_: T, context: {
|
|
75
76
|
client: LangGraphClient;
|
|
76
77
|
message: ToolMessage;
|
|
77
|
-
}): Promise<unknown
|
|
78
|
+
}): Promise<unknown>;
|
|
78
79
|
}
|
package/dist/ToolManager.js
CHANGED
|
@@ -92,14 +92,17 @@ export class ToolManager {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
+
isAllToolCompleted() {
|
|
96
|
+
return this.waitingMap.size;
|
|
97
|
+
}
|
|
95
98
|
/**
|
|
96
99
|
* @zh 等待指定 ID 的工具完成。
|
|
97
100
|
* @en Waits for the tool with the specified ID to complete.
|
|
98
101
|
*/
|
|
99
102
|
waitForDone(id) {
|
|
100
|
-
if (this.waitingMap.has(id)) {
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
+
// if (this.waitingMap.has(id)) {
|
|
104
|
+
// return this.waitingMap.get(id);
|
|
105
|
+
// }
|
|
103
106
|
const promise = new Promise((resolve, reject) => {
|
|
104
107
|
this.waitingMap.set(id, resolve);
|
|
105
108
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type HumanInTheLoopDecision = {
|
|
2
|
+
type: "approve" | "edit" | "reject" | "respond";
|
|
3
|
+
edited_action?: {
|
|
4
|
+
name: string;
|
|
5
|
+
args: Record<string, any>;
|
|
6
|
+
};
|
|
7
|
+
message?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* HumanInTheLoop 的标准回复格式
|
|
11
|
+
*/
|
|
12
|
+
export type InterruptResponse = {
|
|
13
|
+
decisions: HumanInTheLoopDecision[];
|
|
14
|
+
};
|
|
15
|
+
/** 由于 langchain human in the loop 没有设计调用 id,所以我们需要给一个 id */
|
|
16
|
+
export declare const createActionRequestID: (j: {
|
|
17
|
+
name: string;
|
|
18
|
+
args: any;
|
|
19
|
+
}) => string;
|
|
20
|
+
export type HumanInTheLoopState = {
|
|
21
|
+
interruptData: InterruptData;
|
|
22
|
+
result: Record<string, HumanInTheLoopDecision>;
|
|
23
|
+
};
|
|
24
|
+
export type InterruptData = {
|
|
25
|
+
id: string;
|
|
26
|
+
value: {
|
|
27
|
+
actionRequests: {
|
|
28
|
+
id?: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
args: any;
|
|
32
|
+
}[];
|
|
33
|
+
reviewConfigs: {
|
|
34
|
+
actionName: string;
|
|
35
|
+
allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
}[];
|
package/dist/tool/ToolUI.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RenderMessage } from "../LangGraphClient.js";
|
|
2
2
|
import { LangGraphClient } from "../LangGraphClient.js";
|
|
3
|
-
import {
|
|
3
|
+
import { HumanInTheLoopDecision } from "../humanInTheLoop.js";
|
|
4
4
|
export type DeepPartial<T> = {
|
|
5
5
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
6
6
|
};
|
|
@@ -8,16 +8,19 @@ export declare class ToolRenderData<I, D> {
|
|
|
8
8
|
message: RenderMessage;
|
|
9
9
|
client: LangGraphClient;
|
|
10
10
|
constructor(message: RenderMessage, client: LangGraphClient);
|
|
11
|
+
private getToolActionRequestID;
|
|
11
12
|
/**
|
|
12
13
|
* 获取人机交互数据
|
|
13
14
|
* 直接使用 reviewConfig 获取可以显示的按钮
|
|
14
15
|
* actionRequest 获取当前工具的入参
|
|
15
16
|
*/
|
|
16
17
|
getHumanInTheLoopData(): {
|
|
18
|
+
actionRequestIndex: number;
|
|
17
19
|
config: {
|
|
18
20
|
id: string;
|
|
19
21
|
value: {
|
|
20
22
|
actionRequests: {
|
|
23
|
+
id?: string;
|
|
21
24
|
name: string;
|
|
22
25
|
description: string;
|
|
23
26
|
args: any;
|
|
@@ -33,14 +36,16 @@ export declare class ToolRenderData<I, D> {
|
|
|
33
36
|
allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
|
|
34
37
|
};
|
|
35
38
|
actionRequest: {
|
|
39
|
+
id?: string;
|
|
36
40
|
name: string;
|
|
37
41
|
description: string;
|
|
38
42
|
args: any;
|
|
39
43
|
};
|
|
44
|
+
result: HumanInTheLoopDecision;
|
|
40
45
|
} | null;
|
|
41
46
|
/** 发送恢复状态的数据 */
|
|
42
|
-
sendResumeData(response:
|
|
43
|
-
get state(): "idle" | "
|
|
47
|
+
sendResumeData(response: HumanInTheLoopDecision): void;
|
|
48
|
+
get state(): "idle" | "done" | "loading";
|
|
44
49
|
get input(): I | null;
|
|
45
50
|
get output(): string;
|
|
46
51
|
getJSONOutput(): D;
|
package/dist/tool/ToolUI.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getMessageContent } from "../ui-store/createChatStore.js";
|
|
2
2
|
import { jsonrepair } from "jsonrepair";
|
|
3
|
+
import { createActionRequestID } from "../humanInTheLoop.js";
|
|
3
4
|
export class ToolRenderData {
|
|
4
5
|
message;
|
|
5
6
|
client;
|
|
@@ -7,19 +8,33 @@ export class ToolRenderData {
|
|
|
7
8
|
this.message = message;
|
|
8
9
|
this.client = client;
|
|
9
10
|
}
|
|
11
|
+
getToolActionRequestID() {
|
|
12
|
+
return createActionRequestID({
|
|
13
|
+
name: this.message.name,
|
|
14
|
+
args: this.getInputRepaired(),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
18
|
* 获取人机交互数据
|
|
12
19
|
* 直接使用 reviewConfig 获取可以显示的按钮
|
|
13
20
|
* actionRequest 获取当前工具的入参
|
|
14
21
|
*/
|
|
15
22
|
getHumanInTheLoopData() {
|
|
16
|
-
const
|
|
23
|
+
const toolActionRequestID = this.getToolActionRequestID();
|
|
24
|
+
if (!this.client.humanInTheLoop)
|
|
25
|
+
return null;
|
|
26
|
+
const configOfHumanInTheLoop = this.client.humanInTheLoop.interruptData.find((i) => i.value.actionRequests.some((j) => j.id === toolActionRequestID));
|
|
17
27
|
if (!configOfHumanInTheLoop)
|
|
18
28
|
return null;
|
|
29
|
+
const actionRequestIndex = configOfHumanInTheLoop.value.actionRequests.findIndex((j) => j.id === toolActionRequestID);
|
|
30
|
+
if (actionRequestIndex === -1)
|
|
31
|
+
return null;
|
|
19
32
|
return {
|
|
33
|
+
actionRequestIndex: actionRequestIndex,
|
|
20
34
|
config: configOfHumanInTheLoop,
|
|
21
|
-
reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName ===
|
|
22
|
-
actionRequest: configOfHumanInTheLoop.value.actionRequests
|
|
35
|
+
reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName === configOfHumanInTheLoop.value.actionRequests[actionRequestIndex].name),
|
|
36
|
+
actionRequest: configOfHumanInTheLoop.value.actionRequests[actionRequestIndex],
|
|
37
|
+
result: this.client.humanInTheLoop.result[toolActionRequestID],
|
|
23
38
|
};
|
|
24
39
|
}
|
|
25
40
|
/** 发送恢复状态的数据 */
|
|
@@ -28,14 +43,18 @@ export class ToolRenderData {
|
|
|
28
43
|
/**@ts-ignore 修复 sb 的 langchain 官方的命名不统一,我们一致采用下划线版本,而非驼峰版本 */
|
|
29
44
|
response.editedAction = response.edited_action;
|
|
30
45
|
}
|
|
31
|
-
return this.client.
|
|
46
|
+
return this.client.doneHumanInTheLoopWaiting(this.message.id, this.getToolActionRequestID(), response);
|
|
32
47
|
}
|
|
33
48
|
get state() {
|
|
34
49
|
if (this.message.type === "tool" && this.message?.additional_kwargs?.done) {
|
|
35
50
|
return "done";
|
|
36
51
|
}
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
const humanInTheLoopData = this.getHumanInTheLoopData();
|
|
53
|
+
if (humanInTheLoopData?.result) {
|
|
54
|
+
return "done";
|
|
55
|
+
}
|
|
56
|
+
if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
|
|
57
|
+
return "done";
|
|
39
58
|
}
|
|
40
59
|
if (this.message.tool_input) {
|
|
41
60
|
return "loading";
|
|
@@ -2,6 +2,7 @@ import { z, ZodRawShape } from "zod";
|
|
|
2
2
|
import { Action, Parameter } from "./copilotkit-actions.js";
|
|
3
3
|
import { Message } from "@langchain/langgraph-sdk";
|
|
4
4
|
import { ToolRenderData } from "./ToolUI.js";
|
|
5
|
+
import { HumanInTheLoopDecision, InterruptResponse as HumanInTheLoopResponse } from "../humanInTheLoop.js";
|
|
5
6
|
export interface UnionTool<Args extends ZodRawShape, Child extends Object = Object, ResponseType = any> {
|
|
6
7
|
name: string;
|
|
7
8
|
description: string;
|
|
@@ -22,27 +23,10 @@ export interface UnionTool<Args extends ZodRawShape, Child extends Object = Obje
|
|
|
22
23
|
isPureParams?: boolean;
|
|
23
24
|
}
|
|
24
25
|
export type ToolCallback<Args extends ZodRawShape> = (args: z.infer<z.ZodObject<Args>>, context?: any) => CallToolResult | Promise<CallToolResult>;
|
|
25
|
-
/**
|
|
26
|
-
* HumanInTheLoop 的标准回复格式
|
|
27
|
-
*/
|
|
28
|
-
export type InterruptResponse = {
|
|
29
|
-
decisions: ({
|
|
30
|
-
type: "approve";
|
|
31
|
-
} | {
|
|
32
|
-
type: "edit";
|
|
33
|
-
edited_action: {
|
|
34
|
-
name: string;
|
|
35
|
-
args: Record<string, any>;
|
|
36
|
-
};
|
|
37
|
-
} | {
|
|
38
|
-
type: "reject";
|
|
39
|
-
message?: string;
|
|
40
|
-
})[];
|
|
41
|
-
};
|
|
42
26
|
export type CallToolResult = string | {
|
|
43
27
|
type: "text";
|
|
44
28
|
text: string;
|
|
45
|
-
}[] |
|
|
29
|
+
}[] | HumanInTheLoopDecision | HumanInTheLoopResponse;
|
|
46
30
|
/** 用于格式校验 */
|
|
47
31
|
export declare const createTool: <Args extends ZodRawShape>(tool: UnionTool<Args>) => UnionTool<Args, Object, any>;
|
|
48
32
|
/**
|
|
@@ -173,7 +157,7 @@ export declare const createMCPTool: <Args extends ZodRawShape>(tool: UnionTool<A
|
|
|
173
157
|
}[];
|
|
174
158
|
isError?: undefined;
|
|
175
159
|
} | {
|
|
176
|
-
content:
|
|
160
|
+
content: HumanInTheLoopDecision | HumanInTheLoopResponse | {
|
|
177
161
|
type: "text";
|
|
178
162
|
text: string;
|
|
179
163
|
}[] | undefined;
|
package/package.json
CHANGED
package/src/LangGraphClient.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Thread, Message, Assistant, HumanMessage, AIMessage, ToolMessage,
|
|
|
2
2
|
import { EventEmitter } from "eventemitter3";
|
|
3
3
|
import { ToolManager } from "./ToolManager.js";
|
|
4
4
|
import { CallToolResult } from "./tool/createTool.js";
|
|
5
|
+
import { createActionRequestID, HumanInTheLoopDecision, HumanInTheLoopState, InterruptData } from "./humanInTheLoop.js";
|
|
5
6
|
import { type ILangGraphClient } from "@langgraph-js/pure-graph/dist/types.js";
|
|
6
7
|
import { MessageProcessor } from "./MessageProcessor.js";
|
|
7
8
|
import { revertChatTo, RevertChatToOptions } from "./time-travel/index.js";
|
|
@@ -45,20 +46,7 @@ export type SendMessageOptions = {
|
|
|
45
46
|
command?: Command;
|
|
46
47
|
joinRunId?: string;
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
-
id: string;
|
|
50
|
-
value: {
|
|
51
|
-
actionRequests: {
|
|
52
|
-
name: string;
|
|
53
|
-
description: string;
|
|
54
|
-
args: any;
|
|
55
|
-
}[];
|
|
56
|
-
reviewConfigs: {
|
|
57
|
-
actionName: string;
|
|
58
|
-
allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
|
|
59
|
-
}[];
|
|
60
|
-
};
|
|
61
|
-
}[];
|
|
49
|
+
|
|
62
50
|
export interface LangGraphClientConfig {
|
|
63
51
|
apiUrl?: string;
|
|
64
52
|
apiKey?: string;
|
|
@@ -337,7 +325,7 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
|
|
|
337
325
|
return state;
|
|
338
326
|
}
|
|
339
327
|
public messagesMetadata = {};
|
|
340
|
-
public humanInTheLoop:
|
|
328
|
+
public humanInTheLoop: HumanInTheLoopState | null = null;
|
|
341
329
|
/**
|
|
342
330
|
* @zh 发送消息到 LangGraph 后端。
|
|
343
331
|
* @en Sends a message to the LangGraph backend.
|
|
@@ -411,8 +399,8 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
|
|
|
411
399
|
}
|
|
412
400
|
}
|
|
413
401
|
const data = await this.runFETool();
|
|
402
|
+
await this.responseHumanInTheLoop();
|
|
414
403
|
if (data) streamRecord.push(...data);
|
|
415
|
-
this.humanInTheLoop = null;
|
|
416
404
|
this._status = "idle";
|
|
417
405
|
this.emit("done", {
|
|
418
406
|
event: "done",
|
|
@@ -466,9 +454,30 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
|
|
|
466
454
|
|
|
467
455
|
if (data?.__interrupt__) {
|
|
468
456
|
this._status = "interrupted";
|
|
469
|
-
|
|
470
|
-
|
|
457
|
+
const humanInTheLoop = data?.__interrupt__.map((i) => {
|
|
458
|
+
// 修复 python 版本是以下划线命名的,而 js 版本是以驼峰命名的
|
|
459
|
+
if (i && "review_configs" in i.value) {
|
|
460
|
+
return {
|
|
461
|
+
id: i.id,
|
|
462
|
+
value: {
|
|
463
|
+
/** @ts-ignore */
|
|
464
|
+
actionRequests: i.value.action_requests,
|
|
465
|
+
reviewConfigs: camelcaseKeys(i.value.review_configs as any[], { deep: true }),
|
|
466
|
+
},
|
|
467
|
+
} as InterruptData[number];
|
|
468
|
+
}
|
|
469
|
+
return i;
|
|
470
|
+
});
|
|
471
|
+
humanInTheLoop.forEach((i) => {
|
|
472
|
+
i.value.actionRequests.forEach((j) => {
|
|
473
|
+
j.id = j.id || createActionRequestID(j);
|
|
474
|
+
});
|
|
475
|
+
return i;
|
|
471
476
|
});
|
|
477
|
+
this.humanInTheLoop = {
|
|
478
|
+
interruptData: humanInTheLoop,
|
|
479
|
+
result: {},
|
|
480
|
+
};
|
|
472
481
|
} else if (data?.messages) {
|
|
473
482
|
const isResume = !!command?.resume;
|
|
474
483
|
const isLongerThanLocal = data.messages.length >= this.messageProcessor.getGraphMessages().length;
|
|
@@ -505,16 +514,27 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
|
|
|
505
514
|
});
|
|
506
515
|
this._status = "interrupted";
|
|
507
516
|
this.currentThread!.status = "interrupted"; // 修复某些机制下,状态不为 interrupted 与后端有差异
|
|
517
|
+
console.log("batch call tools", result.length);
|
|
508
518
|
return Promise.all(result);
|
|
509
519
|
}
|
|
510
520
|
}
|
|
521
|
+
private async responseHumanInTheLoop() {
|
|
522
|
+
if (this.humanInTheLoop) {
|
|
523
|
+
const humanInTheLoop = this.humanInTheLoop;
|
|
524
|
+
this.humanInTheLoop = null;
|
|
525
|
+
return this.resume({
|
|
526
|
+
decisions: humanInTheLoop.interruptData[0].value.actionRequests.map((i) => {
|
|
527
|
+
return humanInTheLoop.result[i.id!] as HumanInTheLoopDecision;
|
|
528
|
+
}),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
511
532
|
private async callFETool(message: ToolMessage, args: any) {
|
|
512
533
|
const that = this; // 防止 this 被错误解析
|
|
513
534
|
const result = await this.tools.callTool(message.name!, args, { client: that, message });
|
|
514
535
|
if (!result) {
|
|
515
536
|
return;
|
|
516
537
|
}
|
|
517
|
-
return this.resume(result);
|
|
518
538
|
}
|
|
519
539
|
extraParams: Record<string, any> = {};
|
|
520
540
|
|
|
@@ -532,6 +552,7 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
|
|
|
532
552
|
/**
|
|
533
553
|
* @zh 标记前端工具等待已完成。
|
|
534
554
|
* @en Marks the frontend tool waiting as completed.
|
|
555
|
+
* @deprecated 请使用 doneHumanInTheLoopWaiting 和规范的 humanInTheLoop 协议定义状态
|
|
535
556
|
*/
|
|
536
557
|
doneFEToolWaiting(id: string, result: CallToolResult) {
|
|
537
558
|
const done = this.tools.doneWaiting(id, result);
|
|
@@ -540,6 +561,18 @@ export class LangGraphClient<TStateType = unknown> extends EventEmitter<LangGrap
|
|
|
540
561
|
}
|
|
541
562
|
}
|
|
542
563
|
|
|
564
|
+
/**
|
|
565
|
+
* @zh 标记人机交互等待已完成。
|
|
566
|
+
* @en Marks the human in the loop waiting as completed.
|
|
567
|
+
*/
|
|
568
|
+
doneHumanInTheLoopWaiting(tool_id: string, action_request_id: string, result: HumanInTheLoopDecision) {
|
|
569
|
+
// 移除等待状态
|
|
570
|
+
this.tools.doneWaiting(tool_id, result);
|
|
571
|
+
if (this.humanInTheLoop) {
|
|
572
|
+
this.humanInTheLoop.result[action_request_id] = result;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
543
576
|
/**
|
|
544
577
|
* @zh 获取当前的 Thread。
|
|
545
578
|
* @en Gets the current Thread.
|
package/src/ToolManager.ts
CHANGED
|
@@ -102,15 +102,18 @@ export class ToolManager {
|
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
isAllToolCompleted() {
|
|
106
|
+
return this.waitingMap.size;
|
|
107
|
+
}
|
|
105
108
|
|
|
106
109
|
/**
|
|
107
110
|
* @zh 等待指定 ID 的工具完成。
|
|
108
111
|
* @en Waits for the tool with the specified ID to complete.
|
|
109
112
|
*/
|
|
110
113
|
waitForDone(id: string) {
|
|
111
|
-
if (this.waitingMap.has(id)) {
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
+
// if (this.waitingMap.has(id)) {
|
|
115
|
+
// return this.waitingMap.get(id);
|
|
116
|
+
// }
|
|
114
117
|
const promise = new Promise((resolve, reject) => {
|
|
115
118
|
this.waitingMap.set(id, resolve);
|
|
116
119
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type HumanInTheLoopDecision = {
|
|
2
|
+
type: "approve" | "edit" | "reject" | "respond";
|
|
3
|
+
edited_action?: {
|
|
4
|
+
name: string;
|
|
5
|
+
args: Record<string, any>;
|
|
6
|
+
};
|
|
7
|
+
message?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HumanInTheLoop 的标准回复格式
|
|
12
|
+
*/
|
|
13
|
+
export type InterruptResponse = {
|
|
14
|
+
decisions: HumanInTheLoopDecision[];
|
|
15
|
+
};
|
|
16
|
+
/** 由于 langchain human in the loop 没有设计调用 id,所以我们需要给一个 id */
|
|
17
|
+
export const createActionRequestID = (j: { name: string; args: any }) => {
|
|
18
|
+
return j.name + JSON.stringify(j.args);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type HumanInTheLoopState = {
|
|
22
|
+
interruptData: InterruptData;
|
|
23
|
+
result: Record<string, HumanInTheLoopDecision>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type InterruptData = {
|
|
27
|
+
id: string;
|
|
28
|
+
value: {
|
|
29
|
+
actionRequests: {
|
|
30
|
+
id?: string;
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
args: any;
|
|
34
|
+
}[];
|
|
35
|
+
reviewConfigs: {
|
|
36
|
+
actionName: string;
|
|
37
|
+
allowedDecisions: ("approve" | "edit" | "reject" | "respond")[];
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
}[];
|
package/src/tool/ToolUI.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { RenderMessage } from "../LangGraphClient.js";
|
|
|
3
3
|
import { LangGraphClient } from "../LangGraphClient.js";
|
|
4
4
|
import { getMessageContent } from "../ui-store/createChatStore.js";
|
|
5
5
|
import { jsonrepair } from "jsonrepair";
|
|
6
|
-
import { InterruptResponse } from "
|
|
6
|
+
import { createActionRequestID, HumanInTheLoopDecision, InterruptResponse } from "../humanInTheLoop.js";
|
|
7
7
|
|
|
8
8
|
export type DeepPartial<T> = {
|
|
9
9
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
@@ -13,34 +13,53 @@ export class ToolRenderData<I, D> {
|
|
|
13
13
|
public message: RenderMessage,
|
|
14
14
|
public client: LangGraphClient
|
|
15
15
|
) {}
|
|
16
|
+
private getToolActionRequestID() {
|
|
17
|
+
return createActionRequestID({
|
|
18
|
+
name: this.message.name!,
|
|
19
|
+
args: this.getInputRepaired(),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
16
22
|
/**
|
|
17
23
|
* 获取人机交互数据
|
|
18
24
|
* 直接使用 reviewConfig 获取可以显示的按钮
|
|
19
25
|
* actionRequest 获取当前工具的入参
|
|
20
26
|
*/
|
|
21
27
|
getHumanInTheLoopData() {
|
|
22
|
-
const
|
|
28
|
+
const toolActionRequestID = this.getToolActionRequestID();
|
|
29
|
+
if (!this.client.humanInTheLoop) return null;
|
|
30
|
+
const configOfHumanInTheLoop = this.client.humanInTheLoop.interruptData.find((i) => i.value.actionRequests.some((j) => j.id === toolActionRequestID));
|
|
23
31
|
if (!configOfHumanInTheLoop) return null;
|
|
32
|
+
|
|
33
|
+
const actionRequestIndex = configOfHumanInTheLoop.value.actionRequests.findIndex((j) => j.id === toolActionRequestID);
|
|
34
|
+
if (actionRequestIndex === -1) return null;
|
|
35
|
+
|
|
24
36
|
return {
|
|
37
|
+
actionRequestIndex: actionRequestIndex,
|
|
25
38
|
config: configOfHumanInTheLoop,
|
|
26
|
-
reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName ===
|
|
27
|
-
actionRequest: configOfHumanInTheLoop.value.actionRequests
|
|
39
|
+
reviewConfig: configOfHumanInTheLoop.value.reviewConfigs.find((j) => j.actionName === configOfHumanInTheLoop.value.actionRequests[actionRequestIndex].name)!,
|
|
40
|
+
actionRequest: configOfHumanInTheLoop.value.actionRequests[actionRequestIndex],
|
|
41
|
+
result: this.client.humanInTheLoop.result[toolActionRequestID],
|
|
28
42
|
};
|
|
29
43
|
}
|
|
30
44
|
/** 发送恢复状态的数据 */
|
|
31
|
-
sendResumeData(response:
|
|
45
|
+
sendResumeData(response: HumanInTheLoopDecision) {
|
|
32
46
|
if (response.type === "edit") {
|
|
33
47
|
/**@ts-ignore 修复 sb 的 langchain 官方的命名不统一,我们一致采用下划线版本,而非驼峰版本 */
|
|
34
48
|
response.editedAction = response.edited_action;
|
|
35
49
|
}
|
|
36
|
-
|
|
50
|
+
|
|
51
|
+
return this.client.doneHumanInTheLoopWaiting(this.message.id!, this.getToolActionRequestID(), response);
|
|
37
52
|
}
|
|
38
53
|
get state() {
|
|
39
54
|
if (this.message.type === "tool" && this.message?.additional_kwargs?.done) {
|
|
40
55
|
return "done";
|
|
41
56
|
}
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
const humanInTheLoopData = this.getHumanInTheLoopData();
|
|
58
|
+
if (humanInTheLoopData?.result) {
|
|
59
|
+
return "done";
|
|
60
|
+
}
|
|
61
|
+
if (this.client.status === "interrupted" && humanInTheLoopData?.actionRequest) {
|
|
62
|
+
return "done";
|
|
44
63
|
}
|
|
45
64
|
if (this.message.tool_input) {
|
|
46
65
|
return "loading";
|
package/src/tool/createTool.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { actionParametersToJsonSchema, convertJsonSchemaToZodRawShape } from "./utils.js";
|
|
2
|
-
import { z, ZodRawShape } from "zod";
|
|
2
|
+
import { uuidv4, z, ZodRawShape } from "zod";
|
|
3
3
|
import { Action, Parameter } from "./copilotkit-actions.js";
|
|
4
4
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
5
5
|
import { Message } from "@langchain/langgraph-sdk";
|
|
6
6
|
import { ToolRenderData } from "./ToolUI.js";
|
|
7
|
+
import { HumanInTheLoopDecision, InterruptResponse as HumanInTheLoopResponse } from "../humanInTheLoop.js";
|
|
7
8
|
|
|
8
9
|
export interface UnionTool<Args extends ZodRawShape, Child extends Object = Object, ResponseType = any> {
|
|
9
10
|
name: string;
|
|
@@ -25,23 +26,8 @@ export interface UnionTool<Args extends ZodRawShape, Child extends Object = Obje
|
|
|
25
26
|
isPureParams?: boolean;
|
|
26
27
|
}
|
|
27
28
|
export type ToolCallback<Args extends ZodRawShape> = (args: z.infer<z.ZodObject<Args>>, context?: any) => CallToolResult | Promise<CallToolResult>;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
export type InterruptResponse = {
|
|
32
|
-
decisions: (
|
|
33
|
-
| { type: "approve" }
|
|
34
|
-
| {
|
|
35
|
-
type: "edit";
|
|
36
|
-
edited_action: {
|
|
37
|
-
name: string;
|
|
38
|
-
args: Record<string, any>;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
| { type: "reject"; message?: string }
|
|
42
|
-
)[];
|
|
43
|
-
};
|
|
44
|
-
export type CallToolResult = string | { type: "text"; text: string }[] | InterruptResponse;
|
|
29
|
+
|
|
30
|
+
export type CallToolResult = string | { type: "text"; text: string }[] | HumanInTheLoopDecision | HumanInTheLoopResponse;
|
|
45
31
|
|
|
46
32
|
/** 用于格式校验 */
|
|
47
33
|
export const createTool = <Args extends ZodRawShape>(tool: UnionTool<Args>) => {
|