@nickyzj2023/ai 1.5.0 → 1.6.1

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/README.md CHANGED
@@ -1,64 +1,83 @@
1
- # ai
2
-
3
- 男生自用Coding Agent,参考了[nanopi教学](https://pi-from-scratch.vercel.app/)
4
-
5
- 不断学习+开发中,当前项目仍属于比nano还简陋的“bare-pi”,but one day...
6
-
7
- ![demo](demo.gif)
8
-
9
- ## 安装
10
-
11
- ```bash
12
- # npm
13
- npm install @nickyzj2023/ai
14
-
15
- # yarn
16
- yarn add @nickyzj2023/ai
17
-
18
- # pnpm
19
- pnpm add @nickyzj2023/ai
20
- ```
21
-
22
- ## 使用方式
23
-
24
- ### 在项目里使用
25
-
26
- ```typescript
27
- import { defineModel, runAgent, getWeather } from "@nickyzj2023/ai";
28
-
29
- const model = defineModel({
30
- baseUrl: "https://api.deepseek.com/v1",
31
- apiKey: process.env.APIKEY,
32
- model: "deepseek-v4-flash",
33
- });
34
-
35
- const tools = [
36
- // 内置工具
37
- getWeather,
38
- getTime,
39
- // 自定义工具
40
- defineTool("getRandomNumber", "生成随机数字", {min: {type: "number", description: "最小区间"}, max: {type:"number", description:"最大区间"}}, () => 33550336),
41
- ];
42
-
43
- const messages = [{ role: "user", content: "随机一个负无穷到正无穷的整数" }];
44
-
45
- for await (const e of runAgent(model, messages, tools)) {
46
- console.log(e);
47
- }
48
- ```
49
-
50
- ### 在终端里使用
51
-
52
- ```bash
53
- # 首次使用:交互式配置BASE_URL / APIKEY / MODEL / ...
54
- ai setup
55
-
56
- # 启动对话
57
- ai
58
- ```
59
-
60
- 配置保存在 `~/.@nickyzj2023/ai/config.json`,任意目录下执行`ai`都能读取到
61
-
62
- ## License
63
-
64
- ISC
1
+ # ai
2
+
3
+ 男生自用Coding Agent,参考了[nanopi教学](https://pi-from-scratch.vercel.app/)
4
+
5
+ 不断学习+开发中,当前项目仍属于比nano还简陋的“bare-pi”,but one day...
6
+
7
+ ![demo](demo.gif)
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ # npm
13
+ npm install @nickyzj2023/ai
14
+
15
+ # yarn
16
+ yarn add @nickyzj2023/ai
17
+
18
+ # pnpm
19
+ pnpm add @nickyzj2023/ai
20
+ ```
21
+
22
+ ## 使用方式
23
+
24
+ ### 在项目里使用
25
+
26
+ ```typescript
27
+ import { defineModel, runAgent, getWeather } from "@nickyzj2023/ai";
28
+ // MCP体积较大(200kb),有需要时单独引入
29
+ import { loadMCPTools } from "@nickyzj2023/ai/mcp";
30
+
31
+ const model = defineModel({
32
+ baseUrl: "https://api.deepseek.com/v1",
33
+ apiKey: process.env.APIKEY,
34
+ model: "deepseek-v4-flash",
35
+ });
36
+
37
+ const tools = [
38
+ // 内置工具
39
+ getWeather,
40
+ getTime,
41
+ // MCP工具
42
+ ..(await loadMCPTools({
43
+ exa: {
44
+ type: "streamable_http",
45
+ url: "https://mcp.exa.ai/mcp",
46
+ headers: {
47
+ "x-api-key": "xxxxx",
48
+ },
49
+ }
50
+ })),
51
+ // 自定义工具
52
+ defineTool("skipReply", "跳过本轮回复", {reason: {type: "string", description: "不回复的理由"}}, () => {
53
+ const error = new Error(`模型保持沉默,理由:${reason}`);
54
+ error.name = "skipReply";
55
+ throw error;
56
+ }),
57
+ ];
58
+
59
+ const messages = [{ role: "user", content: "随机一个负无穷到正无穷的整数" }];
60
+
61
+ for await (const e of runAgent(model, messages, tools)) {
62
+ console.log(e);
63
+ }
64
+ ```
65
+
66
+ ### 在终端里使用
67
+
68
+ ```bash
69
+ # 首次使用:交互式配置BASE_URL / APIKEY / MODEL / ...
70
+ ai setup
71
+
72
+ # 启动对话
73
+ ai
74
+
75
+ # 直接开聊,本质是“ai->输入提示词->回车”的语法糖
76
+ ai 你好
77
+ ```
78
+
79
+ 配置保存在 `~/.@nickyzj2023/ai/config.json`,任意目录下执行`ai`都能读取到
80
+
81
+ ## License
82
+
83
+ ISC
package/dist/cli.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { a as get_time_default, i as get_weather_default, o as runAgent, r as loadMCPTools, s as defineModel } from "./src-C1Zeveeb.mjs";
1
+ import { i as runAgent, n as get_weather_default, r as get_time_default } from "./src-BzQy5h8z.mjs";
2
+ import { t as defineModel } from "./helper-juLVGxlQ.mjs";
3
+ import { n as loadMCPTools } from "./mcp-DcntQg-a.mjs";
2
4
  import { compactStr, extractErrorMessage, isObject } from "@nickyzj2023/utils";
3
5
  import readline from "node:readline";
4
6
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -306,6 +308,8 @@ var TUI = class {
306
308
  md = null;
307
309
  /** 是否允许输入 */
308
310
  isBusy = false;
311
+ /** 是否正停在提示符上等输入(决定printNotice要不要重绘提示符) */
312
+ isPrompting = false;
309
313
  /** 上次打印内容所属的状态(reasoning/content/tool)
310
314
  * 用于在新的状态开始时改变样式、打印前缀
311
315
  */
@@ -317,19 +321,28 @@ var TUI = class {
317
321
  constructor(onPrompt) {
318
322
  this.onPrompt = onPrompt;
319
323
  }
320
- /** 启动TUI */
321
- start() {
324
+ /**
325
+ * 启动TUI
326
+ * @param initialPrompt 传入时先替用户发出这一轮(ai “提示词”的快捷方式),之后照常等输入
327
+ */
328
+ start(initialPrompt) {
322
329
  this.rl = readline.createInterface({
323
330
  input: process.stdin,
324
331
  output: process.stdout
325
332
  });
326
333
  process.stdin.on("keypress", this.onKeypress);
327
- this.prompting();
328
334
  this.rl.on("close", () => {
329
335
  process.stdin.off("keypress", this.onKeypress);
330
336
  this.rl?.close();
331
337
  this.rl = null;
332
338
  });
339
+ if (initialPrompt) {
340
+ process.stdout.write(`> ${initialPrompt}\n`);
341
+ this.submit({
342
+ text: initialPrompt,
343
+ images: []
344
+ });
345
+ } else this.prompting();
333
346
  }
334
347
  /** 监听用户的特定按键 */
335
348
  onKeypress = (_str, key) => {
@@ -339,7 +352,9 @@ var TUI = class {
339
352
  /** 监听用户输入 */
340
353
  prompting() {
341
354
  if (this.isBusy) return;
355
+ this.isPrompting = true;
342
356
  this.rl?.question("> ", async (answer) => {
357
+ this.isPrompting = false;
343
358
  await this.clipboardTask;
344
359
  const text = answer.trim();
345
360
  const images = this.pendingImages;
@@ -348,15 +363,22 @@ var TUI = class {
348
363
  this.prompting();
349
364
  return;
350
365
  }
351
- this.isBusy = true;
352
- await this.onPrompt?.({
366
+ await this.submit({
353
367
  text,
354
368
  images
355
369
  });
356
- this.isBusy = false;
357
- this.prompting();
358
370
  });
359
371
  }
372
+ /**
373
+ * 走完整的一轮:把输入交给调用方,结束后继续等用户输入
374
+ * @remarks 提示词来源可以是用户敲的,也可以是start()传进来的initialPrompt
375
+ */
376
+ async submit(input) {
377
+ this.isBusy = true;
378
+ await this.onPrompt?.(input);
379
+ this.isBusy = false;
380
+ this.prompting();
381
+ }
360
382
  /** 读取剪贴板图片,推入待发送列表 */
361
383
  async attachImage() {
362
384
  const image = await readClipboardImage();
@@ -399,6 +421,27 @@ var TUI = class {
399
421
  if (!process.stdout.isTTY) return text;
400
422
  return `\x1b[${ansiCode}m${text}\x1b[0m`;
401
423
  }
424
+ /** 把一个Agent事件分发到对应的print方法上 */
425
+ render(event) {
426
+ switch (event.type) {
427
+ case "reasoning_delta":
428
+ this.printReasoning(event.delta);
429
+ break;
430
+ case "content_delta":
431
+ this.printContent(event.delta);
432
+ break;
433
+ case "tool_call":
434
+ this.printToolCall(event.name, event.args);
435
+ break;
436
+ case "tool_result":
437
+ this.printToolResult(event.name, event.result);
438
+ break;
439
+ case "error":
440
+ this.printContent(event.message);
441
+ break;
442
+ case "done": this.printFinish(event.finishReason, event.usage);
443
+ }
444
+ }
402
445
  /** 流式打印AI思考内容(灰色) */
403
446
  printReasoning(delta) {
404
447
  if (this.preparePrint("reasoning_delta")) delta = `[思考内容] ${delta.replaceAll("\n", "")}`;
@@ -430,10 +473,10 @@ var TUI = class {
430
473
  this.preparePrint("done");
431
474
  process.stdout.write(this.colorize(`[本轮结束:${finishReason}] ${usage ? `输入${humanizeNumber(usage.prompt_tokens)},输出${humanizeNumber(usage.completion_tokens)},总共${humanizeNumber(usage.total_tokens)}` : ""}${usage?.speed ? `,速度${usage.speed}/秒` : ""}${finishReason === "stop" ? "\n\n" : "\n"}`, "90"));
432
475
  }
433
- /** 打印一行黄字提示,并重绘当前输入行 */
476
+ /** 打印一行黄字提示,正停在提示符上时顺便重绘输入行 */
434
477
  printNotice(message) {
435
478
  process.stdout.write(`${this.colorize(message, "93")}\n`);
436
- this.rl?.prompt(true);
479
+ if (this.isPrompting) this.rl?.prompt(true);
437
480
  }
438
481
  };
439
482
  //#endregion
@@ -452,8 +495,11 @@ const buildUserContent = (text, images) => {
452
495
  content.push(...images);
453
496
  return content;
454
497
  };
455
- /** 启动交互对话:配置来自全局配置文件,环境变量可临时覆盖 */
456
- const startChat = async () => {
498
+ /**
499
+ * 启动交互对话:配置来自全局配置文件,环境变量可临时覆盖
500
+ * @param prompt 带上提示词时,先自动发出这一轮,之后照常继续对话
501
+ */
502
+ const startChat = async (prompt) => {
457
503
  const config = loadConfig();
458
504
  if (!config) {
459
505
  console.error("请先运行 `ai setup` 配置一个模型");
@@ -481,38 +527,23 @@ const startChat = async () => {
481
527
  get_time_default,
482
528
  ...await mcpLoading
483
529
  ];
484
- for await (const e of runAgent(model, messages, tools)) switch (e.type) {
485
- case "reasoning_delta":
486
- tui.printReasoning(e.delta);
487
- break;
488
- case "content_delta":
489
- tui.printContent(e.delta);
490
- break;
491
- case "tool_call":
492
- tui.printToolCall(e.name, e.args);
493
- break;
494
- case "tool_result":
495
- tui.printToolResult(e.name, e.result);
496
- break;
497
- case "error":
498
- tui.printContent(e.message);
499
- break;
500
- case "done": tui.printFinish(e.finishReason, e.usage);
501
- }
530
+ for await (const e of runAgent(model, messages, tools)) tui.render(e);
502
531
  });
503
- tui.start();
532
+ tui.start(prompt);
504
533
  };
505
534
  /** 打印命令用法 */
506
535
  const printHelp = () => {
507
- console.log(`用法: ai [命令]
536
+ console.log(`用法: ai [命令/提示词]
508
537
 
509
538
  命令:
510
539
  --help 显示帮助
511
540
  setup 交互式配置模型APIKEY / BASE_URL / MODEL(保存到 ~/.@nickyzj2023/ai/config.json)
512
541
 
513
- 不带命令则启动对话`);
542
+ 带上提示词时,启动对话的同时先把这段提示词发出去(如\`ai 明天适合洗车吗\`)
543
+ 不带参数则直接进入对话`);
514
544
  };
515
- const command = process.argv[2];
545
+ const args = process.argv.slice(2);
546
+ const command = args[0];
516
547
  switch (command) {
517
548
  case void 0:
518
549
  startChat();
@@ -525,8 +556,11 @@ switch (command) {
525
556
  printHelp();
526
557
  break;
527
558
  default:
528
- console.error(`未知命令:${command}(可以运行ai --help查看用法)`);
529
- process.exit(1);
559
+ if (command.startsWith("-")) {
560
+ console.error(`未知命令:${command}(可以运行ai --help查看用法)`);
561
+ process.exit(1);
562
+ }
563
+ startChat(args.join(" ").trim() || void 0);
530
564
  }
531
565
  //#endregion
532
566
  export {};
@@ -0,0 +1,65 @@
1
+ //#region src/utils/helper.ts
2
+ /**
3
+ * 辅助定义一个POST /chat/completions支持的model参数
4
+ * @remarks 只有baseUrl字段是必须的
5
+ */
6
+ const defineModel = (config) => ({
7
+ modalities: ["text"],
8
+ context: 131072,
9
+ ...config
10
+ });
11
+ /**
12
+ * 辅助定义一个POST /chat/completions支持的tool对象
13
+ * @param execute 实际执行工具的函数
14
+ */
15
+ const defineTool = (name, description, properties, execute) => {
16
+ const _required = [];
17
+ return {
18
+ type: "function",
19
+ function: {
20
+ name,
21
+ description,
22
+ parameters: {
23
+ type: "object",
24
+ properties: Object.entries(properties).reduce((result, [key, property]) => {
25
+ if ("required" in property) {
26
+ _required.push(key);
27
+ delete property.required;
28
+ }
29
+ result[key] = property;
30
+ return result;
31
+ }, {}),
32
+ required: _required
33
+ }
34
+ },
35
+ execute
36
+ };
37
+ };
38
+ const segmenter = new Intl.Segmenter([], { granularity: "word" });
39
+ const estimateTextTokens = (text) => {
40
+ let words = 0;
41
+ let others = 0;
42
+ for (const seg of segmenter.segment(text)) if (seg.isWordLike) words++;
43
+ else others++;
44
+ return Math.ceil(words * 1.5 + others / 4);
45
+ };
46
+ /**
47
+ * 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
48
+ * - 单词 ≈ 1.5token
49
+ * - 标点/空白等非词字符每4个 ≈ 1token
50
+ * - 图片/音频/视频/文件 ≈ 4096token(不好估算,取个较大的值)
51
+ */
52
+ const estimateTokens = (messages) => {
53
+ if (!messages?.length) return 0;
54
+ return messages.reduce((acc, message) => {
55
+ const { content, tool_calls, ...metadata } = message;
56
+ if (typeof content === "string") acc += estimateTextTokens(content);
57
+ else for (const part of content) if (part.type === "text") acc += estimateTextTokens(part.text);
58
+ else acc += 4096;
59
+ if (tool_calls) acc += estimateTextTokens(JSON.stringify(tool_calls));
60
+ acc += estimateTextTokens(JSON.stringify(metadata));
61
+ return acc;
62
+ }, 0);
63
+ };
64
+ //#endregion
65
+ export { estimateTokens as i, defineTool as n, estimateTextTokens as r, defineModel as t };
package/dist/index.d.mts CHANGED
@@ -1,208 +1,21 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client";
2
- //#region src/types.d.ts
3
- type Model = {
4
- baseUrl: string;
5
- /** 如果不传,会尝试使用GET {baseUrl}/models获取到的第一个模型 */
6
- model?: string;
7
- /** 使用本地llama.cpp等服务时可以不填 */
8
- apiKey?: string;
9
- /**
10
- * 模型支持的消息输入类型
11
- * @default ["text"]
12
- * @remarks 会在每轮对话前校验消息类型,存在不支持的输入时报错
13
- */
14
- modalities?: Modality[];
15
- /**
16
- * 模型的最大上下文
17
- * @default 131072
18
- * @remarks 会在每轮对话结束后检查上下文阈值,自动压缩消息
19
- */
20
- context?: number;
21
- /**
22
- * 自定义请求体,会在每轮请求时带上
23
- * @example chat_template_kwargs: { enable_thinking: false }
24
- */
25
- [key: string]: unknown;
26
- };
27
- /** 文/图/音/视频,暂未实现文件输入 */
28
- type Modality = "text" | "image" | "audio" | "video";
29
- type Message = {
30
- role: "system" | "user" | "assistant" | "tool";
31
- reasoning?: string | null;
32
- content: string | ContentPart[];
33
- tool_calls?: ToolCall[];
34
- tool_call_id?: string;
35
- [key: string]: unknown;
36
- };
37
- type ContentPart = TextContent | ImageContent | AudioContent | VideoContent;
38
- type TextContent = {
39
- type: "text";
40
- text: string;
41
- };
42
- type ImageContent = {
43
- type: "image_url";
44
- image_url: {
45
- url: string;
46
- };
47
- };
48
- type AudioContent = {
49
- type: "input_audio";
50
- input_audio: {
51
- /** 使用公网可访问的音频链接 */
52
- url?: string;
53
- /** 使用base64 */
54
- data?: string;
55
- format: string;
56
- };
57
- };
58
- type VideoContent = {
59
- type: "video_url";
60
- video_url: {
61
- url: string;
62
- };
63
- };
64
- type ToolDefinition = {
65
- type: "function";
66
- function: {
67
- name: string;
68
- description: string;
69
- parameters: {
70
- type: "object";
71
- properties: Record<string, {
72
- type: string;
73
- description?: string;
74
- /** defineTool()提供的语法糖,等价于在parameters.required.push(当前property key) */
75
- required?: boolean;
76
- }>;
77
- required?: string[];
78
- };
79
- };
80
- /** 工具的实际执行函数 */
81
- execute: (...args: any) => any;
82
- };
83
- type ToolCall = {
84
- id: string;
85
- type: "function";
86
- function: {
87
- name: string;
88
- arguments: string;
89
- };
90
- };
91
- /** stream()对外输出的事件 */
92
- type LLMEvent = {
93
- type: "reasoning_delta";
94
- delta: string;
95
- } | {
96
- type: "content_delta";
97
- delta: string;
98
- } | {
99
- type: "tool_call";
100
- id: string;
101
- name: string;
102
- args: any;
103
- } | {
104
- type: "done";
105
- finishReason: FinishReason;
106
- usage?: Usage;
107
- } | {
108
- type: "error";
109
- message: string;
110
- };
111
- /** stream()内部要处理的事件流 */
112
- type ChatCompletionsChunk = {
113
- id: string;
114
- object: "chat.completion.chunk";
115
- created: number;
116
- model: string;
117
- choices: {
118
- index: number;
119
- delta: Pick<Message, "reasoning" | "content"> & {
120
- tool_calls?: ({
121
- index: number;
122
- } & Partial<ToolCall>)[];
123
- };
124
- finish_reason: FinishReason;
125
- }[];
126
- usage?: Usage;
127
- } | "[DONE]";
128
- /** 模型可能返回的finish_reason值:停止/工具调用/上下文超限 */
129
- type FinishReason = "stop" | "tool_calls" | "length" | null;
130
- type Usage = {
131
- prompt_tokens: number;
132
- completion_tokens: number;
133
- total_tokens: number;
134
- /** 每秒生成多少个token */
135
- speed?: number;
136
- [key: string]: any;
137
- };
138
- type AgentEvent = LLMEvent | {
139
- type: "tool_result";
140
- id: string;
141
- name: string;
142
- result: string;
143
- };
144
- //#endregion
1
+ import { a as FinishReason, c as Message, d as TextContent, f as ToolCall, h as VideoContent, i as ContentPart, l as Modality, m as Usage, n as AudioContent, o as ImageContent, p as ToolDefinition, r as ChatCompletionsChunk, s as LLMEvent, t as AgentEvent, u as Model } from "./types--dXFoUow.mjs";
145
2
  //#region src/agent.d.ts
146
3
  declare function runAgent(model: Model, messages: Message[], tools: ToolDefinition[]): AsyncGenerator<AgentEvent>;
147
4
  //#endregion
5
+ //#region src/llm.d.ts
6
+ /**
7
+ * 流式请求模型,转发
8
+ */
9
+ declare function stream(model: Model, messages: Message[], tools?: ToolDefinition[], options?: Omit<RequestInit, "body"> & {
10
+ body?: Record<string, unknown>;
11
+ }): AsyncGenerator<LLMEvent>;
12
+ //#endregion
148
13
  //#region src/tools/get-time.d.ts
149
14
  declare const _default: ToolDefinition;
150
15
  //#endregion
151
16
  //#region src/tools/get-weather.d.ts
152
17
  declare const _default$1: ToolDefinition;
153
18
  //#endregion
154
- //#region src/tools/mcp.d.ts
155
- type McpServer = {
156
- type: "streamable_http" | "sse";
157
- url: string;
158
- headers?: Record<string, any>;
159
- ignoredToolNames?: string[];
160
- };
161
- declare class MCPRouter {
162
- private entries;
163
- /** 注册一个新的MCP客户端 */
164
- addClient(name: string, url: string, options?: Omit<McpServer, "type" | "url">): Promise<Client<{
165
- method: string;
166
- params?: {
167
- [x: string]: unknown;
168
- _meta?: {
169
- [x: string]: unknown;
170
- progressToken?: string | number | undefined;
171
- "io.modelcontextprotocol/related-task"?: {
172
- taskId: string;
173
- } | undefined;
174
- } | undefined;
175
- } | undefined;
176
- }, {
177
- method: string;
178
- params?: {
179
- [x: string]: unknown;
180
- _meta?: {
181
- [x: string]: unknown;
182
- progressToken?: string | number | undefined;
183
- "io.modelcontextprotocol/related-task"?: {
184
- taskId: string;
185
- } | undefined;
186
- } | undefined;
187
- } | undefined;
188
- }, {
189
- [x: string]: unknown;
190
- _meta?: {
191
- [x: string]: unknown;
192
- progressToken?: string | number | undefined;
193
- "io.modelcontextprotocol/related-task"?: {
194
- taskId: string;
195
- } | undefined;
196
- } | undefined;
197
- }> | undefined>;
198
- /** 返回OpenAI API兼容的tools数组 */
199
- getTools(): Promise<ToolDefinition[]>;
200
- }
201
- /**
202
- * 把传入的MCPServer列表转换成OpenAI API兼容的tools数组
203
- */
204
- declare const loadMCPTools: (mcpServers?: Record<string, McpServer>) => Promise<ToolDefinition[]>;
205
- //#endregion
206
19
  //#region src/utils/compact/types.d.ts
207
20
  declare namespace Compact {
208
21
  type Options = {
@@ -264,6 +77,8 @@ declare namespace Compact {
264
77
  /** 指导大模型如何总结消息 */
265
78
  systemPrompt: string;
266
79
  model: Model;
80
+ /** 触发总结前的钩子 */
81
+ beforeSummarize?: (summarizable: Message[]) => void | Promise<void>;
267
82
  };
268
83
  type Result = {
269
84
  /** 是否压缩了工具调用结果 */
@@ -321,5 +136,13 @@ declare const defineModel: (config: Model) => Model;
321
136
  * @param execute 实际执行工具的函数
322
137
  */
323
138
  declare const defineTool: (name: ToolDefinition["function"]["name"], description: ToolDefinition["function"]["description"], properties: ToolDefinition["function"]["parameters"]["properties"], execute: ToolDefinition["execute"]) => ToolDefinition;
139
+ declare const estimateTextTokens: (text: string) => number;
140
+ /**
141
+ * 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
142
+ * - 单词 ≈ 1.5token
143
+ * - 标点/空白等非词字符每4个 ≈ 1token
144
+ * - 图片/音频/视频/文件 ≈ 4096token(不好估算,取个较大的值)
145
+ */
146
+ declare const estimateTokens: (messages?: Message[]) => number;
324
147
  //#endregion
325
- export { type AgentEvent, type AudioContent, type ChatCompletionsChunk, type ContentPart, type FinishReason, type ImageContent, type LLMEvent, MCPRouter, McpServer, type Message, type Modality, type Model, type TextContent, type ToolCall, type ToolDefinition, type Usage, type VideoContent, compact, defineModel, defineTool, _default as getTime, _default$1 as getWeather, loadMCPTools, runAgent };
148
+ export { type AgentEvent, type AudioContent, type ChatCompletionsChunk, type ContentPart, type FinishReason, type ImageContent, type LLMEvent, type Message, type Modality, type Model, type TextContent, type ToolCall, type ToolDefinition, type Usage, type VideoContent, compact, defineModel, defineTool, estimateTextTokens, estimateTokens, _default as getTime, _default$1 as getWeather, runAgent, stream };
package/dist/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
- import { a as get_time_default, c as defineTool, i as get_weather_default, n as MCPRouter, o as runAgent, r as loadMCPTools, s as defineModel, t as compact } from "./src-C1Zeveeb.mjs";
2
- export { MCPRouter, compact, defineModel, defineTool, get_time_default as getTime, get_weather_default as getWeather, loadMCPTools, runAgent };
1
+ import { a as stream, i as runAgent, n as get_weather_default, r as get_time_default, t as compact } from "./src-BzQy5h8z.mjs";
2
+ import { i as estimateTokens, n as defineTool, r as estimateTextTokens, t as defineModel } from "./helper-juLVGxlQ.mjs";
3
+ export { compact, defineModel, defineTool, estimateTextTokens, estimateTokens, get_time_default as getTime, get_weather_default as getWeather, runAgent, stream };
@@ -0,0 +1,58 @@
1
+ import { n as defineTool } from "./helper-juLVGxlQ.mjs";
2
+ import { extractErrorMessage, isObject, logger, omit } from "@nickyzj2023/utils";
3
+ //#region src/tools/mcp.ts
4
+ /** 全局单例MCP加载器 */
5
+ let router = null;
6
+ var MCPRouter = class {
7
+ entries = /* @__PURE__ */ new Map();
8
+ /** 注册一个新的MCP客户端 */
9
+ async addClient(name, url, options) {
10
+ if (this.entries.has(name)) return;
11
+ const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([import("@modelcontextprotocol/sdk/client"), import("@modelcontextprotocol/sdk/client/streamableHttp.js")]);
12
+ const transport = new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: options?.headers } });
13
+ const client = new Client({
14
+ name,
15
+ version: "1.0.0"
16
+ });
17
+ await client.connect(transport);
18
+ const { tools } = await client.listTools();
19
+ const normalizedTools = tools.filter((tool) => !options?.ignoredToolNames?.includes(tool.name)).map((tool) => {
20
+ const _properties = { ...tool.inputSchema.properties ?? {} };
21
+ tool.inputSchema.required?.forEach((key) => {
22
+ if (isObject(_properties[key])) _properties[key] = {
23
+ ..._properties[key],
24
+ required: true
25
+ };
26
+ });
27
+ return defineTool(tool.name, tool.description ?? "", _properties, (args) => client.callTool({
28
+ name: tool.name,
29
+ arguments: args
30
+ }));
31
+ });
32
+ this.entries.set(name, {
33
+ client,
34
+ tools: normalizedTools
35
+ });
36
+ return client;
37
+ }
38
+ /** 返回OpenAI API兼容的tools数组 */
39
+ async getTools() {
40
+ return [...this.entries.values()].flatMap((e) => e.tools);
41
+ }
42
+ };
43
+ /**
44
+ * 把传入的MCPServer列表转换成OpenAI API兼容的tools数组
45
+ */
46
+ const loadMCPTools = async (mcpServers = {}) => {
47
+ router ||= new MCPRouter();
48
+ await Promise.allSettled(Object.entries(mcpServers).map(async ([name, server]) => {
49
+ try {
50
+ await router?.addClient(name, server.url, omit(server, ["type", "url"]));
51
+ } catch (e) {
52
+ logger(`MCP服务器${name}加载失败:${extractErrorMessage(e)}`);
53
+ }
54
+ }));
55
+ return router.getTools();
56
+ };
57
+ //#endregion
58
+ export { loadMCPTools as n, MCPRouter as t };
@@ -1,6 +1,5 @@
1
- import { createXMLText, extractErrorMessage, fetcher, isObject, logger, omit, parseSSE, pick, to } from "@nickyzj2023/utils";
2
- import { Client } from "@modelcontextprotocol/sdk/client";
3
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1
+ import { i as estimateTokens, n as defineTool, r as estimateTextTokens } from "./helper-juLVGxlQ.mjs";
2
+ import { createXMLText, extractErrorMessage, fetcher, logger, parseSSE, pick, to } from "@nickyzj2023/utils";
4
3
  //#region src/llm.ts
5
4
  /**
6
5
  * 分离ToolDefinition中的valid/invalid字段,前者可以传给模型,后者用于本地运算
@@ -22,7 +21,7 @@ const extractReasoning = (msgLike) => {
22
21
  /**
23
22
  * 流式请求模型,转发
24
23
  */
25
- async function* stream(model, messages, tools = []) {
24
+ async function* stream(model, messages, tools = [], options) {
26
25
  const validTools = tools.map((tool) => detachToolArguments(tool)[0]);
27
26
  const api = fetcher(model.baseUrl, {
28
27
  headers: { Authorization: `Bearer ${model.apiKey}` },
@@ -32,9 +31,11 @@ async function* stream(model, messages, tools = []) {
32
31
  stream: true,
33
32
  model: model.model,
34
33
  messages,
35
- tools: validTools
36
- }));
34
+ tools: validTools,
35
+ ...options?.body
36
+ }, options));
37
37
  if (error) {
38
+ if (options?.signal?.aborted) return;
38
39
  yield {
39
40
  type: "error",
40
41
  message: error.message
@@ -44,39 +45,48 @@ async function* stream(model, messages, tools = []) {
44
45
  const toolCallBuffers = /* @__PURE__ */ new Map();
45
46
  let usage;
46
47
  let finishReason = null;
47
- for await (const chunk of parseSSE(response)) {
48
- if (typeof chunk === "string") continue;
49
- if (chunk.usage) usage = chunk.usage;
50
- const choice = chunk.choices?.[0];
51
- if (!choice) continue;
52
- const { delta } = choice;
53
- const { content: contentDelta, tool_calls: toolCalls } = delta;
54
- const reasoning = extractReasoning(delta);
55
- if (reasoning) yield {
56
- type: "reasoning_delta",
57
- delta: reasoning
58
- };
59
- if (contentDelta) yield {
60
- type: "content_delta",
61
- delta: contentDelta.toString()
62
- };
63
- if (toolCalls) for (const call of toolCalls) {
64
- const { index = 0, type = "function", id, function: fn, ...extra } = call;
65
- const existing = toolCallBuffers.getOrInsert(index, {
66
- id: "",
67
- type,
68
- function: {
69
- name: "",
70
- arguments: ""
71
- }
72
- });
73
- if (id) existing.id = id;
74
- if (fn?.name) existing.function.name += fn.name;
75
- if (fn?.arguments) existing.function.arguments += fn.arguments;
76
- if (extra) Object.assign(existing, extra);
77
- toolCallBuffers.set(index, existing);
48
+ try {
49
+ for await (const chunk of parseSSE(response)) {
50
+ if (typeof chunk === "string") continue;
51
+ if (chunk.usage) usage = chunk.usage;
52
+ const choice = chunk.choices?.[0];
53
+ if (!choice) continue;
54
+ const { delta } = choice;
55
+ const { content: contentDelta, tool_calls: toolCalls } = delta;
56
+ const reasoning = extractReasoning(delta);
57
+ if (reasoning) yield {
58
+ type: "reasoning_delta",
59
+ delta: reasoning
60
+ };
61
+ if (contentDelta) yield {
62
+ type: "content_delta",
63
+ delta: contentDelta.toString()
64
+ };
65
+ if (toolCalls) for (const call of toolCalls) {
66
+ const { index = 0, type = "function", id, function: fn, ...extra } = call;
67
+ const existing = toolCallBuffers.getOrInsert(index, {
68
+ id: "",
69
+ type,
70
+ function: {
71
+ name: "",
72
+ arguments: ""
73
+ }
74
+ });
75
+ if (id) existing.id = id;
76
+ if (fn?.name) existing.function.name += fn.name;
77
+ if (fn?.arguments) existing.function.arguments += fn.arguments;
78
+ if (extra) Object.assign(existing, extra);
79
+ toolCallBuffers.set(index, existing);
80
+ }
81
+ if (choice.finish_reason) finishReason = choice.finish_reason;
78
82
  }
79
- if (choice.finish_reason) finishReason = choice.finish_reason;
83
+ } catch (e) {
84
+ if (options?.signal?.aborted) return;
85
+ yield {
86
+ type: "error",
87
+ message: extractErrorMessage(e)
88
+ };
89
+ return;
80
90
  }
81
91
  for (const [, call] of toolCallBuffers) yield {
82
92
  type: "tool_call",
@@ -91,70 +101,6 @@ async function* stream(model, messages, tools = []) {
91
101
  };
92
102
  }
93
103
  //#endregion
94
- //#region src/utils/helper.ts
95
- /**
96
- * 辅助定义一个POST /chat/completions支持的model参数
97
- * @remarks 只有baseUrl字段是必须的
98
- */
99
- const defineModel = (config) => ({
100
- modalities: ["text"],
101
- context: 131072,
102
- ...config
103
- });
104
- /**
105
- * 辅助定义一个POST /chat/completions支持的tool对象
106
- * @param execute 实际执行工具的函数
107
- */
108
- const defineTool = (name, description, properties, execute) => {
109
- const _required = [];
110
- return {
111
- type: "function",
112
- function: {
113
- name,
114
- description,
115
- parameters: {
116
- type: "object",
117
- properties: Object.entries(properties).reduce((result, [key, property]) => {
118
- if ("required" in property) {
119
- _required.push(key);
120
- delete property.required;
121
- }
122
- result[key] = property;
123
- return result;
124
- }, {}),
125
- required: _required
126
- }
127
- },
128
- execute
129
- };
130
- };
131
- const segmenter = new Intl.Segmenter([], { granularity: "word" });
132
- const estimateTextTokens = (text) => {
133
- let words = 0;
134
- let others = 0;
135
- for (const seg of segmenter.segment(text)) if (seg.isWordLike) words++;
136
- else others++;
137
- return Math.ceil(words * 1.5 + others / 4);
138
- };
139
- /**
140
- * 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
141
- * - 单词 ≈ 1.5token
142
- * - 标点/空白等非词字符每4个 ≈ 1token
143
- * - 图片/音频/视频/文件 ≈ 4096token(不好估算,取个较大的值)
144
- */
145
- const estimateTokens = (messages) => {
146
- if (!messages?.length) return 0;
147
- return messages.reduce((acc, message) => {
148
- const { content, tool_calls, ...metadata } = message;
149
- if (typeof content === "string") acc += estimateTextTokens(content);
150
- else for (const part of content) if (part.type === "text") acc += estimateTextTokens(part.text);
151
- else acc += 4096;
152
- if (tool_calls) acc += estimateTextTokens(JSON.stringify(tool_calls));
153
- acc += estimateTextTokens(JSON.stringify(metadata));
154
- return acc;
155
- }, 0);
156
- };
157
- //#endregion
158
104
  //#region src/agent.ts
159
105
  async function* runAgent(model, messages, tools) {
160
106
  const toolMap = new Map(tools.map((tool) => [tool.function.name, tool]));
@@ -252,60 +198,6 @@ var get_weather_default = defineTool("get_weather", "查询指定城市的天气
252
198
  return fetcher("https://wttr.in", { params: { format: "j1" } }).get(`/${city}`);
253
199
  });
254
200
  //#endregion
255
- //#region src/tools/mcp.ts
256
- /** 全局单例MCP加载器 */
257
- let router = null;
258
- var MCPRouter = class {
259
- entries = /* @__PURE__ */ new Map();
260
- /** 注册一个新的MCP客户端 */
261
- async addClient(name, url, options) {
262
- if (this.entries.has(name)) return;
263
- const transport = new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: options?.headers } });
264
- const client = new Client({
265
- name,
266
- version: "1.0.0"
267
- });
268
- await client.connect(transport);
269
- const { tools } = await client.listTools();
270
- const normalizedTools = tools.filter((tool) => !options?.ignoredToolNames?.includes(tool.name)).map((tool) => {
271
- const _properties = { ...tool.inputSchema.properties ?? {} };
272
- tool.inputSchema.required?.forEach((key) => {
273
- if (isObject(_properties[key])) _properties[key] = {
274
- ..._properties[key],
275
- required: true
276
- };
277
- });
278
- return defineTool(tool.name, tool.description ?? "", _properties, (args) => client.callTool({
279
- name: tool.name,
280
- arguments: args
281
- }));
282
- });
283
- this.entries.set(name, {
284
- client,
285
- tools: normalizedTools
286
- });
287
- return client;
288
- }
289
- /** 返回OpenAI API兼容的tools数组 */
290
- async getTools() {
291
- return [...this.entries.values()].flatMap((e) => e.tools);
292
- }
293
- };
294
- /**
295
- * 把传入的MCPServer列表转换成OpenAI API兼容的tools数组
296
- */
297
- const loadMCPTools = async (mcpServers = {}) => {
298
- router ||= new MCPRouter();
299
- await Promise.allSettled(Object.entries(mcpServers).map(async ([name, server]) => {
300
- try {
301
- await router?.addClient(name, server.url, omit(server, ["type", "url"]));
302
- } catch (e) {
303
- logger(`MCP服务器${name}加载失败:${extractErrorMessage(e)}`);
304
- }
305
- }));
306
- return router.getTools();
307
- };
308
- //#endregion
309
201
  //#region src/utils/compact/helper.ts
310
202
  /**
311
203
  * 校验assistant(tool_calls)消息
@@ -410,12 +302,13 @@ const compactMediaMessages = async (compressible, options) => {
410
302
  return count;
411
303
  };
412
304
  const summarizeMessages = async (compressible, options) => {
413
- const { model, systemPrompt } = options ?? {};
305
+ const { model, systemPrompt, beforeSummarize } = options ?? {};
414
306
  const summarizable = compressible.slice(1);
415
307
  if (summarizable.length === 0) {
416
308
  logger("消息太少,无需总结");
417
309
  return 0;
418
310
  }
311
+ await beforeSummarize?.(summarizable);
419
312
  const count = summarizable.length;
420
313
  summarizable.push({
421
314
  role: "system",
@@ -487,10 +380,11 @@ const compact = Object.assign(async (messages, model, options) => {
487
380
  model
488
381
  }) > 0;
489
382
  if (tokens > context * ratioToSummarize) {
490
- const { systemPrompt = "你现在的任务是总结历史消息" } = summarizeOptions ?? {};
383
+ const { systemPrompt = "你现在的任务是总结历史消息", beforeSummarize } = summarizeOptions ?? {};
491
384
  const [error, count] = await to(summarizeMessages(compressible, {
492
385
  model,
493
- systemPrompt
386
+ systemPrompt,
387
+ beforeSummarize
494
388
  }));
495
389
  if (!error) result.hasSummarized = count > 0;
496
390
  else result.hasDiscardMessages = discardMessagesUntil(compressible, (compressible) => {
@@ -512,4 +406,4 @@ const compact = Object.assign(async (messages, model, options) => {
512
406
  discardMessagesUntil
513
407
  });
514
408
  //#endregion
515
- export { get_time_default as a, defineTool as c, get_weather_default as i, MCPRouter as n, runAgent as o, loadMCPTools as r, defineModel as s, compact as t };
409
+ export { stream as a, runAgent as i, get_weather_default as n, get_time_default as r, compact as t };
@@ -0,0 +1,55 @@
1
+ import { p as ToolDefinition } from "../types--dXFoUow.mjs";
2
+ import { Client } from "@modelcontextprotocol/sdk/client";
3
+ //#region src/tools/mcp.d.ts
4
+ type McpServer = {
5
+ type: "streamable_http" | "sse";
6
+ url: string;
7
+ headers?: Record<string, any>;
8
+ ignoredToolNames?: string[];
9
+ };
10
+ declare class MCPRouter {
11
+ private entries;
12
+ /** 注册一个新的MCP客户端 */
13
+ addClient(name: string, url: string, options?: Omit<McpServer, "type" | "url">): Promise<Client<{
14
+ method: string;
15
+ params?: {
16
+ [x: string]: unknown;
17
+ _meta?: {
18
+ [x: string]: unknown;
19
+ progressToken?: string | number | undefined;
20
+ "io.modelcontextprotocol/related-task"?: {
21
+ taskId: string;
22
+ } | undefined;
23
+ } | undefined;
24
+ } | undefined;
25
+ }, {
26
+ method: string;
27
+ params?: {
28
+ [x: string]: unknown;
29
+ _meta?: {
30
+ [x: string]: unknown;
31
+ progressToken?: string | number | undefined;
32
+ "io.modelcontextprotocol/related-task"?: {
33
+ taskId: string;
34
+ } | undefined;
35
+ } | undefined;
36
+ } | undefined;
37
+ }, {
38
+ [x: string]: unknown;
39
+ _meta?: {
40
+ [x: string]: unknown;
41
+ progressToken?: string | number | undefined;
42
+ "io.modelcontextprotocol/related-task"?: {
43
+ taskId: string;
44
+ } | undefined;
45
+ } | undefined;
46
+ }> | undefined>;
47
+ /** 返回OpenAI API兼容的tools数组 */
48
+ getTools(): Promise<ToolDefinition[]>;
49
+ }
50
+ /**
51
+ * 把传入的MCPServer列表转换成OpenAI API兼容的tools数组
52
+ */
53
+ declare const loadMCPTools: (mcpServers?: Record<string, McpServer>) => Promise<ToolDefinition[]>;
54
+ //#endregion
55
+ export { MCPRouter, McpServer, loadMCPTools };
@@ -0,0 +1,2 @@
1
+ import { n as loadMCPTools, t as MCPRouter } from "../mcp-DcntQg-a.mjs";
2
+ export { MCPRouter, loadMCPTools };
@@ -0,0 +1,144 @@
1
+ //#region src/types.d.ts
2
+ type Model = {
3
+ baseUrl: string;
4
+ /** 如果不传,会尝试使用GET {baseUrl}/models获取到的第一个模型 */
5
+ model?: string;
6
+ /** 使用本地llama.cpp等服务时可以不填 */
7
+ apiKey?: string;
8
+ /**
9
+ * 模型支持的消息输入类型
10
+ * @default ["text"]
11
+ * @remarks 会在每轮对话前校验消息类型,存在不支持的输入时报错
12
+ */
13
+ modalities?: Modality[];
14
+ /**
15
+ * 模型的最大上下文
16
+ * @default 131072
17
+ * @remarks 会在每轮对话结束后检查上下文阈值,自动压缩消息
18
+ */
19
+ context?: number;
20
+ /**
21
+ * 自定义请求体,会在每轮请求时带上
22
+ * @example chat_template_kwargs: { enable_thinking: false }
23
+ */
24
+ [key: string]: unknown;
25
+ };
26
+ /** 文/图/音/视频,暂未实现文件输入 */
27
+ type Modality = "text" | "image" | "audio" | "video";
28
+ type Message = {
29
+ role: "system" | "user" | "assistant" | "tool";
30
+ reasoning?: string | null;
31
+ content: string | ContentPart[];
32
+ tool_calls?: ToolCall[];
33
+ tool_call_id?: string;
34
+ [key: string]: unknown;
35
+ };
36
+ type ContentPart = TextContent | ImageContent | AudioContent | VideoContent;
37
+ type TextContent = {
38
+ type: "text";
39
+ text: string;
40
+ };
41
+ type ImageContent = {
42
+ type: "image_url";
43
+ image_url: {
44
+ url: string;
45
+ };
46
+ };
47
+ type AudioContent = {
48
+ type: "input_audio";
49
+ input_audio: {
50
+ /** 使用公网可访问的音频链接 */
51
+ url?: string;
52
+ /** 使用base64 */
53
+ data?: string;
54
+ format: string;
55
+ };
56
+ };
57
+ type VideoContent = {
58
+ type: "video_url";
59
+ video_url: {
60
+ url: string;
61
+ };
62
+ };
63
+ type ToolDefinition = {
64
+ type: "function";
65
+ function: {
66
+ name: string;
67
+ description: string;
68
+ parameters: {
69
+ type: "object";
70
+ properties: Record<string, {
71
+ type: string;
72
+ description?: string;
73
+ /** defineTool()提供的语法糖,等价于在parameters.required.push(当前property key) */
74
+ required?: boolean;
75
+ }>;
76
+ required?: string[];
77
+ };
78
+ };
79
+ /** 工具的实际执行函数 */
80
+ execute: (...args: any) => any;
81
+ };
82
+ type ToolCall = {
83
+ id: string;
84
+ type: "function";
85
+ function: {
86
+ name: string;
87
+ arguments: string;
88
+ };
89
+ };
90
+ /** stream()对外输出的事件 */
91
+ type LLMEvent = {
92
+ type: "reasoning_delta";
93
+ delta: string;
94
+ } | {
95
+ type: "content_delta";
96
+ delta: string;
97
+ } | {
98
+ type: "tool_call";
99
+ id: string;
100
+ name: string;
101
+ args: any;
102
+ } | {
103
+ type: "done";
104
+ finishReason: FinishReason;
105
+ usage?: Usage;
106
+ } | {
107
+ type: "error";
108
+ message: string;
109
+ };
110
+ /** stream()内部要处理的事件流 */
111
+ type ChatCompletionsChunk = {
112
+ id: string;
113
+ object: "chat.completion.chunk";
114
+ created: number;
115
+ model: string;
116
+ choices: {
117
+ index: number;
118
+ delta: Pick<Message, "reasoning" | "content"> & {
119
+ tool_calls?: ({
120
+ index: number;
121
+ } & Partial<ToolCall>)[];
122
+ };
123
+ finish_reason: FinishReason;
124
+ }[];
125
+ usage?: Usage;
126
+ } | "[DONE]";
127
+ /** 模型可能返回的finish_reason值:停止/工具调用/上下文超限 */
128
+ type FinishReason = "stop" | "tool_calls" | "length" | null;
129
+ type Usage = {
130
+ prompt_tokens: number;
131
+ completion_tokens: number;
132
+ total_tokens: number;
133
+ /** 每秒生成多少个token */
134
+ speed?: number;
135
+ [key: string]: any;
136
+ };
137
+ type AgentEvent = LLMEvent | {
138
+ type: "tool_result";
139
+ id: string;
140
+ name: string;
141
+ result: string;
142
+ };
143
+ //#endregion
144
+ export { FinishReason as a, Message as c, TextContent as d, ToolCall as f, VideoContent as h, ContentPart as i, Modality as l, Usage as m, AudioContent as n, ImageContent as o, ToolDefinition as p, ChatCompletionsChunk as r, LLMEvent as s, AgentEvent as t, Model as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nickyzj2023/ai",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "我的“pi”,参考了pi-from-scratch",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.mts",
11
11
  "import": "./dist/index.mjs"
12
+ },
13
+ "./mcp": {
14
+ "types": "./dist/tools/mcp.d.mts",
15
+ "import": "./dist/tools/mcp.mjs"
12
16
  }
13
17
  },
14
18
  "files": [