@langgraph-js/sdk 1.9.1 → 1.10.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/TestKit.d.ts +204 -0
- package/dist/TestKit.js +226 -0
- package/dist/ToolManager.d.ts +1 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/tool/createTool.d.ts +9 -2
- package/dist/tool/createTool.js +1 -1
- package/package.json +1 -1
- package/src/TestKit.ts +262 -0
- package/src/index.ts +1 -0
- package/src/tool/createTool.ts +3 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { RenderMessage } from "./LangGraphClient.js";
|
|
2
|
+
import { Message } from "@langchain/langgraph-sdk";
|
|
3
|
+
import { CallToolResult, UnionTool } from "./tool/createTool.js";
|
|
4
|
+
import { createChatStore } from "./ui-store/createChatStore.js";
|
|
5
|
+
/**
|
|
6
|
+
* @zh 测试任务接口
|
|
7
|
+
* @en Test task interface
|
|
8
|
+
*/
|
|
9
|
+
interface TestTask {
|
|
10
|
+
/** 任务是否成功完成 */
|
|
11
|
+
success: boolean;
|
|
12
|
+
/** 执行任务的函数 */
|
|
13
|
+
runTask: (messages: readonly RenderMessage[]) => Promise<void>;
|
|
14
|
+
/** 任务失败时的回调函数 */
|
|
15
|
+
fail: () => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @zh LangGraph 测试工具,可以配合 vitest 等常用框架进行测试
|
|
19
|
+
* @en LangGraph test tool, can be used with vitest and other common frameworks for testing
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const testChat = new TestLangGraphChat(createLangGraphClient(), { debug: true });
|
|
24
|
+
* await testChat.humanInput("你好", async () => {
|
|
25
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
26
|
+
* expect(aiMessage.content).toBeDefined();
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class TestLangGraphChat {
|
|
31
|
+
readonly store: ReturnType<typeof createChatStore>;
|
|
32
|
+
/** 是否开启调试模式 */
|
|
33
|
+
private debug;
|
|
34
|
+
/** 上次消息数量,用于检测消息变化 */
|
|
35
|
+
private lastLength;
|
|
36
|
+
/** 待处理的测试任务列表 */
|
|
37
|
+
protected processFunc: TestTask[];
|
|
38
|
+
/**
|
|
39
|
+
* @zh 构造函数,初始化测试环境
|
|
40
|
+
* @en Constructor, initialize test environment
|
|
41
|
+
*/
|
|
42
|
+
constructor(store: ReturnType<typeof createChatStore>, options: {
|
|
43
|
+
debug?: boolean;
|
|
44
|
+
tools?: UnionTool<any, any, any>[];
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* @zh 获取当前所有渲染消息
|
|
48
|
+
* @en Get all current render messages
|
|
49
|
+
*/
|
|
50
|
+
getMessages(): RenderMessage[];
|
|
51
|
+
/**
|
|
52
|
+
* @zh 添加工具到测试环境中,会自动包装工具的 execute 方法
|
|
53
|
+
* @en Add tools to test environment, automatically wraps tool execute methods
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const tools = [createUITool({ name: "test_tool", ... })];
|
|
58
|
+
* testChat.addTools(tools);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
addTools(tools: UnionTool<any, any, any>[]): void;
|
|
62
|
+
/**
|
|
63
|
+
* @zh 检查所有待处理的测试任务,只有在消息数量发生变化时才执行检查
|
|
64
|
+
* @en Check all pending test tasks, only executes when message count changes
|
|
65
|
+
*/
|
|
66
|
+
checkAllTask(messages: readonly RenderMessage[], options?: {
|
|
67
|
+
skipLengthCheck?: boolean;
|
|
68
|
+
}): void;
|
|
69
|
+
/**
|
|
70
|
+
* @zh 准备测试环境,初始化客户端连接
|
|
71
|
+
* @en Prepare test environment, initialize client connection
|
|
72
|
+
*/
|
|
73
|
+
ready(): Promise<import("./LangGraphClient.js").LangGraphClient>;
|
|
74
|
+
/**
|
|
75
|
+
* @zh 模拟人类输入消息并等待测试任务完成,这是测试的核心方法
|
|
76
|
+
* @en Simulate human input and wait for test tasks to complete, this is the core test method
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* await testChat.humanInput("请帮我思考一下", async () => {
|
|
81
|
+
* const toolMessage = await testChat.waitFor("tool", "thinking");
|
|
82
|
+
* expect(toolMessage.tool_input).toBeDefined();
|
|
83
|
+
*
|
|
84
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
85
|
+
* expect(aiMessage.content).toContain("思考");
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
humanInput(text: Message["content"], context: () => Promise<void>): Promise<[void, void]>;
|
|
90
|
+
/**
|
|
91
|
+
* @zh 等待特定类型的消息出现,创建异步等待任务
|
|
92
|
+
* @en Wait for specific type of message to appear, creates async waiting task
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* // 等待 AI 回复
|
|
97
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
98
|
+
*
|
|
99
|
+
* // 等待特定工具调用
|
|
100
|
+
* const toolMessage = await testChat.waitFor("tool", "sequential-thinking");
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
waitFor<D extends "tool" | "ai", T extends RenderMessage, N extends D extends "tool" ? string : undefined>(type: D, name?: N): Promise<T>;
|
|
104
|
+
/**
|
|
105
|
+
* @zh 响应前端工具调用,模拟用户对工具的响应
|
|
106
|
+
* @en Respond to frontend tool calls, simulates user response to tools
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const toolMessage = await testChat.waitFor("tool", "ask_user_for_approve");
|
|
111
|
+
* await testChat.responseFeTool(toolMessage, "approved");
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
responseFeTool(message: RenderMessage, value: CallToolResult): Promise<RenderMessage>;
|
|
115
|
+
/**
|
|
116
|
+
* @zh 查找最后一条指定类型的消息,从消息数组末尾开始向前查找
|
|
117
|
+
* @en Find the last message of specified type, searches backwards from end of messages
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // 查找最后一条 AI 消息
|
|
122
|
+
* const lastAI = testChat.findLast("ai");
|
|
123
|
+
*
|
|
124
|
+
* // 查找最后一条人类消息
|
|
125
|
+
* const lastHuman = testChat.findLast("human");
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
findLast(type: "human" | "ai" | "tool", options?: {
|
|
129
|
+
before?: (item: RenderMessage) => boolean;
|
|
130
|
+
}): (import("@langchain/langgraph-sdk").HumanMessage & {
|
|
131
|
+
name?: string;
|
|
132
|
+
node_name?: string;
|
|
133
|
+
tool_input?: string;
|
|
134
|
+
additional_kwargs?: {
|
|
135
|
+
done?: boolean;
|
|
136
|
+
tool_calls?: {
|
|
137
|
+
function: {
|
|
138
|
+
arguments: string;
|
|
139
|
+
};
|
|
140
|
+
}[];
|
|
141
|
+
};
|
|
142
|
+
usage_metadata?: {
|
|
143
|
+
total_tokens: number;
|
|
144
|
+
input_tokens: number;
|
|
145
|
+
output_tokens: number;
|
|
146
|
+
};
|
|
147
|
+
tool_call_id?: string;
|
|
148
|
+
response_metadata?: {
|
|
149
|
+
create_time: string;
|
|
150
|
+
};
|
|
151
|
+
spend_time?: number;
|
|
152
|
+
unique_id?: string;
|
|
153
|
+
done?: boolean;
|
|
154
|
+
}) | (import("@langchain/langgraph-sdk").AIMessage & {
|
|
155
|
+
name?: string;
|
|
156
|
+
node_name?: string;
|
|
157
|
+
tool_input?: string;
|
|
158
|
+
additional_kwargs?: {
|
|
159
|
+
done?: boolean;
|
|
160
|
+
tool_calls?: {
|
|
161
|
+
function: {
|
|
162
|
+
arguments: string;
|
|
163
|
+
};
|
|
164
|
+
}[];
|
|
165
|
+
};
|
|
166
|
+
usage_metadata?: {
|
|
167
|
+
total_tokens: number;
|
|
168
|
+
input_tokens: number;
|
|
169
|
+
output_tokens: number;
|
|
170
|
+
};
|
|
171
|
+
tool_call_id?: string;
|
|
172
|
+
response_metadata?: {
|
|
173
|
+
create_time: string;
|
|
174
|
+
};
|
|
175
|
+
spend_time?: number;
|
|
176
|
+
unique_id?: string;
|
|
177
|
+
done?: boolean;
|
|
178
|
+
}) | (import("@langchain/langgraph-sdk").ToolMessage & {
|
|
179
|
+
name?: string;
|
|
180
|
+
node_name?: string;
|
|
181
|
+
tool_input?: string;
|
|
182
|
+
additional_kwargs?: {
|
|
183
|
+
done?: boolean;
|
|
184
|
+
tool_calls?: {
|
|
185
|
+
function: {
|
|
186
|
+
arguments: string;
|
|
187
|
+
};
|
|
188
|
+
}[];
|
|
189
|
+
};
|
|
190
|
+
usage_metadata?: {
|
|
191
|
+
total_tokens: number;
|
|
192
|
+
input_tokens: number;
|
|
193
|
+
output_tokens: number;
|
|
194
|
+
};
|
|
195
|
+
tool_call_id?: string;
|
|
196
|
+
response_metadata?: {
|
|
197
|
+
create_time: string;
|
|
198
|
+
};
|
|
199
|
+
spend_time?: number;
|
|
200
|
+
unique_id?: string;
|
|
201
|
+
done?: boolean;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
export {};
|
package/dist/TestKit.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ToolRenderData } from "./tool/ToolUI.js";
|
|
2
|
+
/**
|
|
3
|
+
* @zh LangGraph 测试工具,可以配合 vitest 等常用框架进行测试
|
|
4
|
+
* @en LangGraph test tool, can be used with vitest and other common frameworks for testing
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* const testChat = new TestLangGraphChat(createLangGraphClient(), { debug: true });
|
|
9
|
+
* await testChat.humanInput("你好", async () => {
|
|
10
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
11
|
+
* expect(aiMessage.content).toBeDefined();
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export class TestLangGraphChat {
|
|
16
|
+
/**
|
|
17
|
+
* @zh 构造函数,初始化测试环境
|
|
18
|
+
* @en Constructor, initialize test environment
|
|
19
|
+
*/
|
|
20
|
+
constructor(store, options) {
|
|
21
|
+
var _a;
|
|
22
|
+
this.store = store;
|
|
23
|
+
/** 是否开启调试模式 */
|
|
24
|
+
this.debug = false;
|
|
25
|
+
/** 上次消息数量,用于检测消息变化 */
|
|
26
|
+
this.lastLength = 0;
|
|
27
|
+
/** 待处理的测试任务列表 */
|
|
28
|
+
this.processFunc = [];
|
|
29
|
+
this.debug = (_a = options.debug) !== null && _a !== void 0 ? _a : false;
|
|
30
|
+
options.tools && this.addTools(options.tools);
|
|
31
|
+
const renderMessages = this.store.data.renderMessages;
|
|
32
|
+
// 订阅消息变化,自动检查任务完成状态
|
|
33
|
+
renderMessages.subscribe((messages) => {
|
|
34
|
+
this.checkAllTask(messages);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @zh 获取当前所有渲染消息
|
|
39
|
+
* @en Get all current render messages
|
|
40
|
+
*/
|
|
41
|
+
getMessages() {
|
|
42
|
+
return this.store.data.renderMessages.get();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @zh 添加工具到测试环境中,会自动包装工具的 execute 方法
|
|
46
|
+
* @en Add tools to test environment, automatically wraps tool execute methods
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const tools = [createUITool({ name: "test_tool", ... })];
|
|
51
|
+
* testChat.addTools(tools);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
addTools(tools) {
|
|
55
|
+
tools.forEach((tool) => {
|
|
56
|
+
if (tool.execute) {
|
|
57
|
+
const oldExecute = tool.execute;
|
|
58
|
+
// 包装原始的 execute 方法,在执行后触发任务检查
|
|
59
|
+
tool.execute = (...args) => {
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
this.checkAllTask(this.getMessages(), {
|
|
62
|
+
skipLengthCheck: true,
|
|
63
|
+
});
|
|
64
|
+
}, 100);
|
|
65
|
+
return oldExecute(...args);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
this.store.mutations.setTools(tools);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @zh 检查所有待处理的测试任务,只有在消息数量发生变化时才执行检查
|
|
73
|
+
* @en Check all pending test tasks, only executes when message count changes
|
|
74
|
+
*/
|
|
75
|
+
checkAllTask(messages, options = {}) {
|
|
76
|
+
// 只有 lastLength 发生变化时,才执行检查
|
|
77
|
+
if (!options.skipLengthCheck && this.lastLength === messages.length) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.lastLength = messages.length;
|
|
81
|
+
// 执行所有未完成的任务
|
|
82
|
+
for (const task of this.processFunc) {
|
|
83
|
+
!task.success && task.runTask(options.skipLengthCheck ? messages : messages.slice(0, -1));
|
|
84
|
+
}
|
|
85
|
+
// 调试模式下打印最新消息
|
|
86
|
+
if (this.debug) {
|
|
87
|
+
console.log(messages[messages.length - (options.skipLengthCheck ? 1 : 2)]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* @zh 准备测试环境,初始化客户端连接
|
|
92
|
+
* @en Prepare test environment, initialize client connection
|
|
93
|
+
*/
|
|
94
|
+
ready() {
|
|
95
|
+
return this.store.mutations.initClient();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @zh 模拟人类输入消息并等待测试任务完成,这是测试的核心方法
|
|
99
|
+
* @en Simulate human input and wait for test tasks to complete, this is the core test method
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* await testChat.humanInput("请帮我思考一下", async () => {
|
|
104
|
+
* const toolMessage = await testChat.waitFor("tool", "thinking");
|
|
105
|
+
* expect(toolMessage.tool_input).toBeDefined();
|
|
106
|
+
*
|
|
107
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
108
|
+
* expect(aiMessage.content).toContain("思考");
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
async humanInput(text, context) {
|
|
113
|
+
await this.ready();
|
|
114
|
+
// console.log(text);
|
|
115
|
+
return Promise.all([
|
|
116
|
+
context(),
|
|
117
|
+
this.store.mutations
|
|
118
|
+
.sendMessage([
|
|
119
|
+
{
|
|
120
|
+
type: "human",
|
|
121
|
+
content: text,
|
|
122
|
+
},
|
|
123
|
+
])
|
|
124
|
+
.then(() => {
|
|
125
|
+
this.checkAllTask(this.getMessages(), {
|
|
126
|
+
skipLengthCheck: true,
|
|
127
|
+
});
|
|
128
|
+
})
|
|
129
|
+
.then(async (res) => {
|
|
130
|
+
// 检查是否还有未完成的任务
|
|
131
|
+
const tasks = this.processFunc.filter((i) => {
|
|
132
|
+
return !i.success;
|
|
133
|
+
});
|
|
134
|
+
if (tasks.length) {
|
|
135
|
+
console.warn("still have ", tasks.length, " tasks");
|
|
136
|
+
await Promise.all(tasks.map((i) => i.fail()));
|
|
137
|
+
throw new Error("test task failed");
|
|
138
|
+
}
|
|
139
|
+
this.processFunc = [];
|
|
140
|
+
return res;
|
|
141
|
+
}),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* @zh 等待特定类型的消息出现,创建异步等待任务
|
|
146
|
+
* @en Wait for specific type of message to appear, creates async waiting task
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* // 等待 AI 回复
|
|
151
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
152
|
+
*
|
|
153
|
+
* // 等待特定工具调用
|
|
154
|
+
* const toolMessage = await testChat.waitFor("tool", "sequential-thinking");
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
waitFor(type, name) {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
this.processFunc.push({
|
|
160
|
+
success: false,
|
|
161
|
+
async runTask(messages) {
|
|
162
|
+
const lastMessage = messages[messages.length - 1];
|
|
163
|
+
if (!lastMessage) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// 检查消息类型和名称是否匹配
|
|
167
|
+
if (lastMessage.type === type && (name ? lastMessage.name === name : true)) {
|
|
168
|
+
resolve(lastMessage);
|
|
169
|
+
this.success = true;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
fail() {
|
|
173
|
+
reject(new Error(`wait for ${type} ${name} failed`));
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* @zh 响应前端工具调用,模拟用户对工具的响应
|
|
180
|
+
* @en Respond to frontend tool calls, simulates user response to tools
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const toolMessage = await testChat.waitFor("tool", "ask_user_for_approve");
|
|
185
|
+
* await testChat.responseFeTool(toolMessage, "approved");
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
async responseFeTool(message, value) {
|
|
189
|
+
if (message.content) {
|
|
190
|
+
throw new Error(`message is Done. content: ${message.content}`);
|
|
191
|
+
}
|
|
192
|
+
const tool = new ToolRenderData(message, this.store.data.client.get());
|
|
193
|
+
tool.response(value);
|
|
194
|
+
const messages = await this.waitFor("tool", message.name);
|
|
195
|
+
if (messages.content) {
|
|
196
|
+
return messages;
|
|
197
|
+
}
|
|
198
|
+
throw new Error("tool response failed");
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* @zh 查找最后一条指定类型的消息,从消息数组末尾开始向前查找
|
|
202
|
+
* @en Find the last message of specified type, searches backwards from end of messages
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* // 查找最后一条 AI 消息
|
|
207
|
+
* const lastAI = testChat.findLast("ai");
|
|
208
|
+
*
|
|
209
|
+
* // 查找最后一条人类消息
|
|
210
|
+
* const lastHuman = testChat.findLast("human");
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
findLast(type, options = {}) {
|
|
214
|
+
const messages = this.getMessages();
|
|
215
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
216
|
+
const item = messages[i];
|
|
217
|
+
if (type === item.type) {
|
|
218
|
+
return item;
|
|
219
|
+
}
|
|
220
|
+
if (options.before && options.before(item)) {
|
|
221
|
+
throw new Error(`${type} not found; before specified`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`${type} not found `);
|
|
225
|
+
}
|
|
226
|
+
}
|
package/dist/ToolManager.d.ts
CHANGED
|
@@ -55,12 +55,7 @@ export declare class ToolManager {
|
|
|
55
55
|
toJSON(graphId: string, remote?: boolean): Promise<{
|
|
56
56
|
name: string;
|
|
57
57
|
description: string;
|
|
58
|
-
parameters:
|
|
59
|
-
$schema?: string | undefined;
|
|
60
|
-
definitions?: {
|
|
61
|
-
[key: string]: import("zod-to-json-schema").JsonSchema7Type;
|
|
62
|
-
} | undefined;
|
|
63
|
-
};
|
|
58
|
+
parameters: any;
|
|
64
59
|
}[]>;
|
|
65
60
|
/**
|
|
66
61
|
* @zh 标记指定 ID 的工具等待已完成,并传递结果。
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export interface UnionTool<Args extends ZodRawShape, Child extends Object = Obje
|
|
|
18
18
|
allowAgent?: string[];
|
|
19
19
|
/** 只允许指定的 Graph 使用该工具 */
|
|
20
20
|
allowGraph?: string[];
|
|
21
|
+
/** 是否是纯净的 json schema 参数,而不是 zod 参数 */
|
|
22
|
+
isPureParams?: boolean;
|
|
21
23
|
}
|
|
22
24
|
export type ToolCallback<Args extends ZodRawShape> = (args: z.objectOutputType<Args, ZodTypeAny>, context?: any) => CallToolResult | Promise<CallToolResult>;
|
|
23
25
|
export type CallToolResult = string | {
|
|
@@ -43,12 +45,17 @@ export declare const createFETool: <const T extends Parameter[], Args extends Zo
|
|
|
43
45
|
export declare const createJSONDefineTool: <Args extends ZodRawShape>(tool: UnionTool<Args>) => {
|
|
44
46
|
name: string;
|
|
45
47
|
description: string;
|
|
46
|
-
parameters:
|
|
48
|
+
parameters: Args | ({
|
|
49
|
+
title?: string;
|
|
50
|
+
default?: any;
|
|
51
|
+
description?: string;
|
|
52
|
+
markdownDescription?: string;
|
|
53
|
+
} & {
|
|
47
54
|
$schema?: string | undefined;
|
|
48
55
|
definitions?: {
|
|
49
56
|
[key: string]: import("zod-to-json-schema").JsonSchema7Type;
|
|
50
57
|
} | undefined;
|
|
51
|
-
};
|
|
58
|
+
});
|
|
52
59
|
};
|
|
53
60
|
export declare const createMCPTool: <Args extends ZodRawShape>(tool: UnionTool<Args>) => (string | Args | ((args: z.objectOutputType<Args, ZodTypeAny>) => Promise<{
|
|
54
61
|
content: {
|
package/dist/tool/createTool.js
CHANGED
|
@@ -63,7 +63,7 @@ export const createJSONDefineTool = (tool) => {
|
|
|
63
63
|
return {
|
|
64
64
|
name: tool.name,
|
|
65
65
|
description: tool.description,
|
|
66
|
-
parameters: zodToJsonSchema(z.object(tool.parameters)),
|
|
66
|
+
parameters: tool.isPureParams ? tool.parameters : zodToJsonSchema(z.object(tool.parameters)),
|
|
67
67
|
};
|
|
68
68
|
};
|
|
69
69
|
export const createMCPTool = (tool) => {
|
package/package.json
CHANGED
package/src/TestKit.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { RenderMessage } from "./LangGraphClient.js";
|
|
2
|
+
import { Message } from "@langchain/langgraph-sdk";
|
|
3
|
+
import { CallToolResult, UnionTool } from "./tool/createTool.js";
|
|
4
|
+
import { ToolRenderData } from "./tool/ToolUI.js";
|
|
5
|
+
import { createChatStore } from "./ui-store/createChatStore.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @zh 测试任务接口
|
|
9
|
+
* @en Test task interface
|
|
10
|
+
*/
|
|
11
|
+
interface TestTask {
|
|
12
|
+
/** 任务是否成功完成 */
|
|
13
|
+
success: boolean;
|
|
14
|
+
/** 执行任务的函数 */
|
|
15
|
+
runTask: (messages: readonly RenderMessage[]) => Promise<void>;
|
|
16
|
+
/** 任务失败时的回调函数 */
|
|
17
|
+
fail: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @zh LangGraph 测试工具,可以配合 vitest 等常用框架进行测试
|
|
22
|
+
* @en LangGraph test tool, can be used with vitest and other common frameworks for testing
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const testChat = new TestLangGraphChat(createLangGraphClient(), { debug: true });
|
|
27
|
+
* await testChat.humanInput("你好", async () => {
|
|
28
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
29
|
+
* expect(aiMessage.content).toBeDefined();
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class TestLangGraphChat {
|
|
34
|
+
/** 是否开启调试模式 */
|
|
35
|
+
private debug = false;
|
|
36
|
+
/** 上次消息数量,用于检测消息变化 */
|
|
37
|
+
private lastLength = 0;
|
|
38
|
+
/** 待处理的测试任务列表 */
|
|
39
|
+
protected processFunc: TestTask[] = [];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @zh 构造函数,初始化测试环境
|
|
43
|
+
* @en Constructor, initialize test environment
|
|
44
|
+
*/
|
|
45
|
+
constructor(
|
|
46
|
+
readonly store: ReturnType<typeof createChatStore>,
|
|
47
|
+
options: {
|
|
48
|
+
debug?: boolean;
|
|
49
|
+
tools?: UnionTool<any, any, any>[];
|
|
50
|
+
}
|
|
51
|
+
) {
|
|
52
|
+
this.debug = options.debug ?? false;
|
|
53
|
+
options.tools && this.addTools(options.tools);
|
|
54
|
+
const renderMessages = this.store.data.renderMessages;
|
|
55
|
+
|
|
56
|
+
// 订阅消息变化,自动检查任务完成状态
|
|
57
|
+
renderMessages.subscribe((messages) => {
|
|
58
|
+
this.checkAllTask(messages);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @zh 获取当前所有渲染消息
|
|
64
|
+
* @en Get all current render messages
|
|
65
|
+
*/
|
|
66
|
+
getMessages() {
|
|
67
|
+
return this.store.data.renderMessages.get();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @zh 添加工具到测试环境中,会自动包装工具的 execute 方法
|
|
72
|
+
* @en Add tools to test environment, automatically wraps tool execute methods
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const tools = [createUITool({ name: "test_tool", ... })];
|
|
77
|
+
* testChat.addTools(tools);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
addTools(tools: UnionTool<any, any, any>[]) {
|
|
81
|
+
tools.forEach((tool) => {
|
|
82
|
+
if (tool.execute) {
|
|
83
|
+
const oldExecute = tool.execute;
|
|
84
|
+
// 包装原始的 execute 方法,在执行后触发任务检查
|
|
85
|
+
tool.execute = (...args) => {
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
this.checkAllTask(this.getMessages(), {
|
|
88
|
+
skipLengthCheck: true,
|
|
89
|
+
});
|
|
90
|
+
}, 100);
|
|
91
|
+
return oldExecute!(...args);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
this.store.mutations.setTools(tools);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @zh 检查所有待处理的测试任务,只有在消息数量发生变化时才执行检查
|
|
100
|
+
* @en Check all pending test tasks, only executes when message count changes
|
|
101
|
+
*/
|
|
102
|
+
checkAllTask(messages: readonly RenderMessage[], options: { skipLengthCheck?: boolean } = {}) {
|
|
103
|
+
// 只有 lastLength 发生变化时,才执行检查
|
|
104
|
+
if (!options.skipLengthCheck && this.lastLength === messages.length) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.lastLength = messages.length;
|
|
108
|
+
|
|
109
|
+
// 执行所有未完成的任务
|
|
110
|
+
for (const task of this.processFunc) {
|
|
111
|
+
!task.success && task.runTask(options.skipLengthCheck ? messages : messages.slice(0, -1));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 调试模式下打印最新消息
|
|
115
|
+
if (this.debug) {
|
|
116
|
+
console.log(messages[messages.length - (options.skipLengthCheck ? 1 : 2)]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @zh 准备测试环境,初始化客户端连接
|
|
122
|
+
* @en Prepare test environment, initialize client connection
|
|
123
|
+
*/
|
|
124
|
+
ready() {
|
|
125
|
+
return this.store.mutations.initClient();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @zh 模拟人类输入消息并等待测试任务完成,这是测试的核心方法
|
|
130
|
+
* @en Simulate human input and wait for test tasks to complete, this is the core test method
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* await testChat.humanInput("请帮我思考一下", async () => {
|
|
135
|
+
* const toolMessage = await testChat.waitFor("tool", "thinking");
|
|
136
|
+
* expect(toolMessage.tool_input).toBeDefined();
|
|
137
|
+
*
|
|
138
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
139
|
+
* expect(aiMessage.content).toContain("思考");
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
async humanInput(text: Message["content"], context: () => Promise<void>) {
|
|
144
|
+
await this.ready();
|
|
145
|
+
// console.log(text);
|
|
146
|
+
return Promise.all([
|
|
147
|
+
context(),
|
|
148
|
+
this.store.mutations
|
|
149
|
+
.sendMessage([
|
|
150
|
+
{
|
|
151
|
+
type: "human",
|
|
152
|
+
content: text,
|
|
153
|
+
},
|
|
154
|
+
])
|
|
155
|
+
.then(() => {
|
|
156
|
+
this.checkAllTask(this.getMessages(), {
|
|
157
|
+
skipLengthCheck: true,
|
|
158
|
+
});
|
|
159
|
+
})
|
|
160
|
+
.then(async (res) => {
|
|
161
|
+
// 检查是否还有未完成的任务
|
|
162
|
+
const tasks = this.processFunc.filter((i) => {
|
|
163
|
+
return !i.success;
|
|
164
|
+
});
|
|
165
|
+
if (tasks.length) {
|
|
166
|
+
console.warn("still have ", tasks.length, " tasks");
|
|
167
|
+
await Promise.all(tasks.map((i) => i.fail()));
|
|
168
|
+
throw new Error("test task failed");
|
|
169
|
+
}
|
|
170
|
+
this.processFunc = [];
|
|
171
|
+
return res;
|
|
172
|
+
}),
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @zh 等待特定类型的消息出现,创建异步等待任务
|
|
178
|
+
* @en Wait for specific type of message to appear, creates async waiting task
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* // 等待 AI 回复
|
|
183
|
+
* const aiMessage = await testChat.waitFor("ai");
|
|
184
|
+
*
|
|
185
|
+
* // 等待特定工具调用
|
|
186
|
+
* const toolMessage = await testChat.waitFor("tool", "sequential-thinking");
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
waitFor<D extends "tool" | "ai", T extends RenderMessage, N extends D extends "tool" ? string : undefined>(type: D, name?: N): Promise<T> {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
this.processFunc.push({
|
|
192
|
+
success: false,
|
|
193
|
+
async runTask(messages) {
|
|
194
|
+
const lastMessage = messages[messages.length - 1];
|
|
195
|
+
if (!lastMessage) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// 检查消息类型和名称是否匹配
|
|
199
|
+
if (lastMessage.type === type && (name ? lastMessage.name === name : true)) {
|
|
200
|
+
resolve(lastMessage as T);
|
|
201
|
+
this.success = true;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
fail() {
|
|
205
|
+
reject(new Error(`wait for ${type} ${name} failed`));
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @zh 响应前端工具调用,模拟用户对工具的响应
|
|
213
|
+
* @en Respond to frontend tool calls, simulates user response to tools
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const toolMessage = await testChat.waitFor("tool", "ask_user_for_approve");
|
|
218
|
+
* await testChat.responseFeTool(toolMessage, "approved");
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
async responseFeTool(message: RenderMessage, value: CallToolResult) {
|
|
222
|
+
if (message.content) {
|
|
223
|
+
throw new Error(`message is Done. content: ${message.content}`);
|
|
224
|
+
}
|
|
225
|
+
const tool = new ToolRenderData(message, this.store.data.client.get()!);
|
|
226
|
+
tool.response(value);
|
|
227
|
+
const messages = await this.waitFor("tool", message.name!);
|
|
228
|
+
|
|
229
|
+
if (messages.content) {
|
|
230
|
+
return messages;
|
|
231
|
+
}
|
|
232
|
+
throw new Error("tool response failed");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @zh 查找最后一条指定类型的消息,从消息数组末尾开始向前查找
|
|
237
|
+
* @en Find the last message of specified type, searches backwards from end of messages
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* // 查找最后一条 AI 消息
|
|
242
|
+
* const lastAI = testChat.findLast("ai");
|
|
243
|
+
*
|
|
244
|
+
* // 查找最后一条人类消息
|
|
245
|
+
* const lastHuman = testChat.findLast("human");
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
findLast(type: "human" | "ai" | "tool", options: { before?: (item: RenderMessage) => boolean } = {}) {
|
|
249
|
+
const messages = this.getMessages();
|
|
250
|
+
|
|
251
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
252
|
+
const item = messages[i];
|
|
253
|
+
if (type === item.type) {
|
|
254
|
+
return item;
|
|
255
|
+
}
|
|
256
|
+
if (options.before && options.before(item)) {
|
|
257
|
+
throw new Error(`${type} not found; before specified`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`${type} not found `);
|
|
261
|
+
}
|
|
262
|
+
}
|
package/src/index.ts
CHANGED
package/src/tool/createTool.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface UnionTool<Args extends ZodRawShape, Child extends Object = Obje
|
|
|
21
21
|
allowAgent?: string[];
|
|
22
22
|
/** 只允许指定的 Graph 使用该工具 */
|
|
23
23
|
allowGraph?: string[];
|
|
24
|
+
/** 是否是纯净的 json schema 参数,而不是 zod 参数 */
|
|
25
|
+
isPureParams?: boolean;
|
|
24
26
|
}
|
|
25
27
|
export type ToolCallback<Args extends ZodRawShape> = (args: z.objectOutputType<Args, ZodTypeAny>, context?: any) => CallToolResult | Promise<CallToolResult>;
|
|
26
28
|
|
|
@@ -95,7 +97,7 @@ export const createJSONDefineTool = <Args extends ZodRawShape>(tool: UnionTool<A
|
|
|
95
97
|
return {
|
|
96
98
|
name: tool.name,
|
|
97
99
|
description: tool.description,
|
|
98
|
-
parameters: zodToJsonSchema(z.object(tool.parameters)),
|
|
100
|
+
parameters: tool.isPureParams ? tool.parameters : zodToJsonSchema(z.object(tool.parameters)),
|
|
99
101
|
};
|
|
100
102
|
};
|
|
101
103
|
|