@astack-tech/integrations 0.1.1-beta.6 → 0.2.0-beta.7
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/CHANGELOG.md +39 -0
- package/README.md +75 -1
- package/dist/index.cjs +180 -243
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +180 -243
- package/dist/index.js.map +1 -1
- package/dist/model-provider/index.cjs +180 -243
- package/dist/model-provider/index.cjs.map +1 -1
- package/dist/model-provider/index.d.cts +146 -145
- package/dist/model-provider/index.d.ts +146 -145
- package/dist/model-provider/index.js +180 -243
- package/dist/model-provider/index.js.map +1 -1
- package/package.json +45 -26
- package/src/index.ts +1 -5
- package/src/model-provider/deepseek/index.ts +326 -481
- package/src/model-provider/index.ts +9 -2
|
@@ -1,214 +1,247 @@
|
|
|
1
1
|
import { Component } from '@astack-tech/core';
|
|
2
|
+
import type { Port } from '@astack-tech/core';
|
|
2
3
|
import OpenAI from 'openai';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* Options for the DeepSeek model provider.
|
|
6
7
|
*/
|
|
7
8
|
export interface DeepseekConfig {
|
|
8
|
-
/**
|
|
9
|
-
* API 密钥
|
|
10
|
-
*/
|
|
9
|
+
/** DeepSeek API key. */
|
|
11
10
|
apiKey: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 温度参数,控制输出的随机性
|
|
27
|
-
*/
|
|
28
|
-
temperature?: number;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 最大生成的 token 数
|
|
32
|
-
*/
|
|
33
|
-
maxTokens?: number;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 采样概率
|
|
37
|
-
*/
|
|
38
|
-
topP?: number;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 系统提示消息
|
|
42
|
-
*/
|
|
43
|
-
systemPrompt?: string;
|
|
44
|
-
|
|
11
|
+
/** Model name. Defaults to `deepseek-chat`. */
|
|
12
|
+
model?: string | undefined;
|
|
13
|
+
/** API base URL. Defaults to `https://api.deepseek.com/v1`. */
|
|
14
|
+
baseURL?: string | undefined;
|
|
15
|
+
/** Sampling temperature. Defaults to `0.7`. */
|
|
16
|
+
temperature?: number | undefined;
|
|
17
|
+
/** Maximum number of generated tokens. */
|
|
18
|
+
maxTokens?: number | undefined;
|
|
19
|
+
/** Nucleus sampling probability. */
|
|
20
|
+
topP?: number | undefined;
|
|
21
|
+
/** System prompt added before every request. */
|
|
22
|
+
systemPrompt?: string | undefined;
|
|
45
23
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
24
|
+
* Tools given as `{ name, description, parameters }` objects.
|
|
25
|
+
* When this list is not empty, it takes precedence over `tools`.
|
|
48
26
|
*/
|
|
49
|
-
rawTools?:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* 使用 OpenAI 兼容的工具定义格式
|
|
58
|
-
*/
|
|
59
|
-
tools?: unknown[];
|
|
27
|
+
rawTools?: RawTool[] | undefined;
|
|
28
|
+
/** Tools in the OpenAI-compatible `{ type: 'function', function }` format. */
|
|
29
|
+
tools?: unknown[] | undefined;
|
|
30
|
+
/** Allows the OpenAI client to run in a browser. Defaults to `false`. */
|
|
31
|
+
dangerouslyAllowBrowser?: boolean | undefined;
|
|
32
|
+
/** Logs API errors to the console. Defaults to `false`. */
|
|
33
|
+
verbose?: boolean | undefined;
|
|
34
|
+
}
|
|
60
35
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Tool description accepted by `rawTools` and `temporaryTools`.
|
|
38
|
+
*/
|
|
39
|
+
export interface RawTool {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
parameters?: Record<string, unknown> | undefined;
|
|
66
43
|
}
|
|
67
44
|
|
|
68
45
|
/**
|
|
69
|
-
*
|
|
46
|
+
* Role of a chat message.
|
|
70
47
|
*/
|
|
71
48
|
export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
72
49
|
|
|
73
50
|
/**
|
|
74
|
-
*
|
|
51
|
+
* Tool call in the internal format.
|
|
75
52
|
*/
|
|
76
53
|
export interface ToolCall {
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
id?: string;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 工具名称
|
|
84
|
-
*/
|
|
54
|
+
/** Identifier assigned by the API. */
|
|
55
|
+
id?: string | undefined;
|
|
56
|
+
/** Name of the tool. */
|
|
85
57
|
tool_name: string;
|
|
58
|
+
/** Parsed arguments. */
|
|
59
|
+
arguments: Record<string, unknown>;
|
|
60
|
+
}
|
|
86
61
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Tool call returned by `chatCompletion`. It carries both the OpenAI shape
|
|
64
|
+
* (`type`, `function`) and the internal shape (`tool_name`, `arguments`).
|
|
65
|
+
*/
|
|
66
|
+
export interface ResponseToolCall {
|
|
67
|
+
id: string;
|
|
68
|
+
type: 'function';
|
|
69
|
+
function: { name: string; arguments: string };
|
|
70
|
+
tool_name: string;
|
|
90
71
|
arguments: Record<string, unknown>;
|
|
91
72
|
}
|
|
92
73
|
|
|
93
74
|
/**
|
|
94
|
-
*
|
|
75
|
+
* Partial tool call emitted while streaming. `arguments` holds the raw JSON fragment.
|
|
76
|
+
*/
|
|
77
|
+
export interface StreamToolCall {
|
|
78
|
+
id: string;
|
|
79
|
+
type: 'function';
|
|
80
|
+
function: { name: string; arguments: string };
|
|
81
|
+
tool_name: string;
|
|
82
|
+
arguments: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Token usage reported by the API.
|
|
95
87
|
*/
|
|
96
88
|
export interface TokenUsage {
|
|
97
|
-
/**
|
|
98
|
-
* 完成部分的 token 数量
|
|
99
|
-
*/
|
|
100
89
|
completion_tokens: number;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 提示部分的 token 数量
|
|
104
|
-
*/
|
|
105
90
|
prompt_tokens: number;
|
|
91
|
+
prompt_cache_hit_tokens?: number | undefined;
|
|
92
|
+
prompt_cache_miss_tokens?: number | undefined;
|
|
93
|
+
total_tokens: number;
|
|
94
|
+
}
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Chat message accepted as input. It matches the agent message contract in
|
|
98
|
+
* `@astack-tech/components`, so agent histories can be passed directly.
|
|
99
|
+
*/
|
|
100
|
+
export interface Message {
|
|
101
|
+
/** One of `system`, `user`, `assistant` or `tool` (see `MessageRole`). */
|
|
102
|
+
role: string;
|
|
103
|
+
content?: string | undefined;
|
|
104
|
+
/** Accepted for compatibility. Assistant tool calls are not forwarded to the API. */
|
|
105
|
+
tool_calls?: readonly unknown[] | undefined;
|
|
106
|
+
/** Identifier of the call that a `tool` message answers. */
|
|
107
|
+
tool_call_id?: string | undefined;
|
|
108
|
+
usage?: TokenUsage | undefined;
|
|
109
|
+
}
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Assistant message returned by `chatCompletion`.
|
|
113
|
+
*/
|
|
114
|
+
export interface AssistantMessage {
|
|
115
|
+
role: 'assistant';
|
|
116
|
+
content: string;
|
|
117
|
+
tool_calls?: ResponseToolCall[] | undefined;
|
|
118
|
+
usage?: TokenUsage | undefined;
|
|
119
|
+
}
|
|
116
120
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Chunk yielded by `streamChatCompletion`.
|
|
123
|
+
*/
|
|
124
|
+
export interface StreamChunk {
|
|
125
|
+
role: 'assistant';
|
|
126
|
+
content: string;
|
|
127
|
+
tool_calls?: StreamToolCall[] | undefined;
|
|
128
|
+
usage?: TokenUsage | undefined;
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
/**
|
|
124
|
-
*
|
|
132
|
+
* Per-call options.
|
|
125
133
|
*/
|
|
126
|
-
export interface
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
role: MessageRole;
|
|
134
|
+
export interface ChatCompletionOptions {
|
|
135
|
+
/** Tools for this call only. They replace the configured tools. */
|
|
136
|
+
temporaryTools?: unknown[] | undefined;
|
|
137
|
+
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
*/
|
|
135
|
-
content?: string;
|
|
139
|
+
type ChatMessageParam = OpenAI.Chat.ChatCompletionMessageParam;
|
|
140
|
+
type ChatTool = OpenAI.Chat.ChatCompletionTool;
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
interface DeepseekUsage {
|
|
143
|
+
completion_tokens?: number;
|
|
144
|
+
prompt_tokens?: number;
|
|
145
|
+
total_tokens?: number;
|
|
146
|
+
prompt_cache_hit_tokens?: number;
|
|
147
|
+
prompt_cache_miss_tokens?: number;
|
|
148
|
+
}
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
const DEFAULT_BASE_URL = 'https://api.deepseek.com/v1';
|
|
151
|
+
const DEFAULT_MODEL = 'deepseek-chat';
|
|
152
|
+
const DEFAULT_TEMPERATURE = 0.7;
|
|
153
|
+
const FALLBACK_TEXT = 'API call failed';
|
|
146
154
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*/
|
|
150
|
-
usage?: TokenUsage;
|
|
155
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
156
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
* Converts a `{ name, description, parameters }` object to the OpenAI tool format.
|
|
161
|
+
* Objects already in the OpenAI format are returned unchanged.
|
|
162
|
+
*/
|
|
163
|
+
function toApiTool(tool: unknown): ChatTool {
|
|
164
|
+
if (isRecord(tool) && tool['type'] === 'function') {
|
|
165
|
+
return tool as unknown as ChatTool;
|
|
166
|
+
}
|
|
167
|
+
const raw = isRecord(tool) ? tool : {};
|
|
168
|
+
const { name, description, parameters } = raw;
|
|
169
|
+
return {
|
|
170
|
+
type: 'function',
|
|
171
|
+
function: {
|
|
172
|
+
name: typeof name === 'string' ? name : '',
|
|
173
|
+
description: typeof description === 'string' ? description : '',
|
|
174
|
+
parameters: isRecord(parameters) ? parameters : {},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parses tool-call arguments. Anything that is not a JSON object becomes `{}`.
|
|
181
|
+
*/
|
|
182
|
+
function parseArguments(raw: string): Record<string, unknown> {
|
|
183
|
+
if (raw.trim().length === 0) {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const parsed: unknown = JSON.parse(raw);
|
|
188
|
+
return isRecord(parsed) ? parsed : {};
|
|
189
|
+
} catch {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mapUsage(usage: DeepseekUsage): TokenUsage {
|
|
195
|
+
return {
|
|
196
|
+
completion_tokens: usage.completion_tokens ?? 0,
|
|
197
|
+
prompt_tokens: usage.prompt_tokens ?? 0,
|
|
198
|
+
total_tokens: usage.total_tokens ?? 0,
|
|
199
|
+
...(usage.prompt_cache_hit_tokens === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens }),
|
|
202
|
+
...(usage.prompt_cache_miss_tokens === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens }),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* DeepSeek model provider. It calls the DeepSeek API through the OpenAI-compatible client.
|
|
161
210
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
211
|
+
* Ports:
|
|
212
|
+
* - `prompt` (in): a text prompt. The answer is sent on `completion`.
|
|
213
|
+
* - `messages` (in): a list of chat messages. The reply is sent on `message`.
|
|
165
214
|
*/
|
|
166
215
|
class Deepseek extends Component {
|
|
167
|
-
|
|
168
|
-
private
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
private
|
|
172
|
-
private
|
|
173
|
-
private
|
|
174
|
-
private
|
|
175
|
-
private systemPrompt?: string;
|
|
176
|
-
private tools?: unknown[];
|
|
216
|
+
private readonly client: OpenAI;
|
|
217
|
+
private readonly model: string;
|
|
218
|
+
private readonly temperature: number;
|
|
219
|
+
private readonly maxTokens: number | undefined;
|
|
220
|
+
private readonly topP: number | undefined;
|
|
221
|
+
private readonly systemPrompt: string | undefined;
|
|
222
|
+
private readonly tools: unknown[] | undefined;
|
|
223
|
+
private readonly verbose: boolean;
|
|
177
224
|
|
|
178
225
|
constructor(config: DeepseekConfig) {
|
|
179
226
|
super({});
|
|
180
227
|
|
|
181
|
-
// 配置客户端
|
|
182
228
|
this.client = new OpenAI({
|
|
183
229
|
apiKey: config.apiKey,
|
|
184
|
-
baseURL: config.baseURL
|
|
230
|
+
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
|
|
185
231
|
dangerouslyAllowBrowser: config.dangerouslyAllowBrowser ?? false,
|
|
186
232
|
});
|
|
187
233
|
|
|
188
|
-
|
|
189
|
-
this.
|
|
190
|
-
this.temperature = config.temperature ?? 0.7;
|
|
234
|
+
this.model = config.model ?? DEFAULT_MODEL;
|
|
235
|
+
this.temperature = config.temperature ?? DEFAULT_TEMPERATURE;
|
|
191
236
|
this.maxTokens = config.maxTokens;
|
|
192
237
|
this.topP = config.topP;
|
|
193
238
|
this.systemPrompt = config.systemPrompt;
|
|
239
|
+
this.verbose = config.verbose ?? false;
|
|
240
|
+
this.tools =
|
|
241
|
+
config.rawTools !== undefined && config.rawTools.length > 0
|
|
242
|
+
? config.rawTools.map(toApiTool)
|
|
243
|
+
: config.tools;
|
|
194
244
|
|
|
195
|
-
// 处理工具配置,优先使用 rawTools
|
|
196
|
-
if (config.rawTools && config.rawTools.length > 0) {
|
|
197
|
-
// 将工具实例转换为 API 格式
|
|
198
|
-
this.tools = config.rawTools.map(tool => ({
|
|
199
|
-
type: 'function',
|
|
200
|
-
function: {
|
|
201
|
-
name: tool.name,
|
|
202
|
-
description: tool.description,
|
|
203
|
-
parameters: tool.parameters || {},
|
|
204
|
-
},
|
|
205
|
-
}));
|
|
206
|
-
} else {
|
|
207
|
-
// 使用直接提供的 API 格式工具
|
|
208
|
-
this.tools = config.tools;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 重命名端口
|
|
212
245
|
Component.Port.I('prompt').attach(this);
|
|
213
246
|
Component.Port.I('messages').attach(this);
|
|
214
247
|
Component.Port.O('completion').attach(this);
|
|
@@ -216,395 +249,207 @@ class Deepseek extends Component {
|
|
|
216
249
|
}
|
|
217
250
|
|
|
218
251
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* @returns
|
|
252
|
+
* Generates a completion for a single prompt.
|
|
253
|
+
*
|
|
254
|
+
* @returns The text answer, or the JSON-encoded assistant message when it contains tool calls.
|
|
222
255
|
*/
|
|
223
256
|
async generateCompletion(prompt: string): Promise<string> {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
messages.push({
|
|
229
|
-
role: 'system',
|
|
230
|
-
content: this.systemPrompt,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 添加用户提示
|
|
235
|
-
messages.push({
|
|
236
|
-
role: 'user',
|
|
237
|
-
content: prompt,
|
|
257
|
+
const response = await this.client.chat.completions.create({
|
|
258
|
+
...this.baseRequest(),
|
|
259
|
+
messages: [...this.systemMessages(), { role: 'user', content: prompt }],
|
|
260
|
+
...this.toolsParam(this.tools),
|
|
238
261
|
});
|
|
239
262
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
model: this.model,
|
|
244
|
-
messages,
|
|
245
|
-
temperature: this.temperature,
|
|
246
|
-
max_tokens: this.maxTokens,
|
|
247
|
-
top_p: this.topP,
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// 如果有工具定义,添加到请求中
|
|
251
|
-
if (this.tools && this.tools.length > 0) {
|
|
252
|
-
requestParams.tools = this.tools;
|
|
263
|
+
const message = this.firstMessage(response);
|
|
264
|
+
if (message.tool_calls !== undefined && message.tool_calls.length > 0) {
|
|
265
|
+
return JSON.stringify(message);
|
|
253
266
|
}
|
|
254
|
-
|
|
255
|
-
// 调用 API
|
|
256
|
-
const response = await this.client.chat.completions.create(requestParams);
|
|
257
|
-
|
|
258
|
-
// 处理工具调用结果
|
|
259
|
-
if (
|
|
260
|
-
response.choices[0].message.tool_calls &&
|
|
261
|
-
response.choices[0].message.tool_calls.length > 0
|
|
262
|
-
) {
|
|
263
|
-
return JSON.stringify(response.choices[0].message);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return response.choices[0].message.content || '';
|
|
267
|
+
return message.content ?? '';
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
/**
|
|
270
|
-
*
|
|
271
|
-
* @param messages 对话消息数组
|
|
272
|
-
* @param options 可选的调用选项,包含临时工具列表
|
|
273
|
-
* @returns 生成的流式响应消息异步生成器
|
|
271
|
+
* Sends a conversation and returns the assistant reply.
|
|
274
272
|
*/
|
|
275
|
-
async
|
|
273
|
+
async chatCompletion(
|
|
276
274
|
messages: Message[],
|
|
277
|
-
options?:
|
|
278
|
-
):
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
formattedMessages.push({
|
|
285
|
-
role: 'system',
|
|
286
|
-
content: this.systemPrompt,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// 添加用户提供的消息
|
|
291
|
-
formattedMessages.push(
|
|
292
|
-
...messages.map(msg => {
|
|
293
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
294
|
-
const formattedMsg: any = {
|
|
295
|
-
role: msg.role,
|
|
296
|
-
content: msg.content,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
300
|
-
formattedMsg.tool_call_id = msg.tool_call_id;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return formattedMsg;
|
|
304
|
-
})
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
// 创建请求参数
|
|
308
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
309
|
-
const requestParams: any = {
|
|
310
|
-
model: this.model,
|
|
311
|
-
messages: formattedMessages,
|
|
312
|
-
temperature: this.temperature,
|
|
313
|
-
max_tokens: this.maxTokens,
|
|
314
|
-
top_p: this.topP,
|
|
315
|
-
stream: true, // 开启流式模式
|
|
316
|
-
};
|
|
275
|
+
options?: ChatCompletionOptions
|
|
276
|
+
): Promise<AssistantMessage> {
|
|
277
|
+
const response = await this.client.chat.completions.create({
|
|
278
|
+
...this.baseRequest(),
|
|
279
|
+
messages: this.formatMessages(messages),
|
|
280
|
+
...this.toolsParam(options?.temporaryTools ?? this.tools),
|
|
281
|
+
});
|
|
317
282
|
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
// 如果有工具定义,添加到请求中
|
|
322
|
-
if (toolsToUse && toolsToUse.length > 0) {
|
|
323
|
-
const formattedTools = toolsToUse.map(tool => {
|
|
324
|
-
type ToolType = {
|
|
325
|
-
type?: string;
|
|
326
|
-
name?: string;
|
|
327
|
-
description?: string;
|
|
328
|
-
parameters?: Record<string, unknown>;
|
|
329
|
-
};
|
|
283
|
+
const message = this.firstMessage(response);
|
|
284
|
+
const result: AssistantMessage = { role: 'assistant', content: message.content ?? '' };
|
|
330
285
|
|
|
331
|
-
|
|
286
|
+
if (response.usage !== undefined) {
|
|
287
|
+
result.usage = mapUsage(response.usage);
|
|
288
|
+
}
|
|
332
289
|
|
|
333
|
-
|
|
334
|
-
|
|
290
|
+
if (message.tool_calls !== undefined && message.tool_calls.length > 0) {
|
|
291
|
+
result.tool_calls = message.tool_calls.map(toolCall => {
|
|
292
|
+
if (toolCall.type !== 'function') {
|
|
293
|
+
throw new Error(`Unsupported tool call type: ${toolCall.type}`);
|
|
335
294
|
}
|
|
336
|
-
|
|
295
|
+
const rawArguments = toolCall.function.arguments;
|
|
337
296
|
return {
|
|
297
|
+
id: toolCall.id,
|
|
338
298
|
type: 'function',
|
|
339
299
|
function: {
|
|
340
|
-
name:
|
|
341
|
-
|
|
342
|
-
parameters: typedTool.parameters || {},
|
|
300
|
+
name: toolCall.function.name,
|
|
301
|
+
arguments: rawArguments.length > 0 ? rawArguments : '{}',
|
|
343
302
|
},
|
|
303
|
+
tool_name: toolCall.function.name,
|
|
304
|
+
arguments: parseArguments(rawArguments),
|
|
344
305
|
};
|
|
345
306
|
});
|
|
346
|
-
|
|
347
|
-
requestParams.tools = formattedTools;
|
|
348
307
|
}
|
|
349
308
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Streams the assistant reply. Each chunk holds a text delta, tool-call deltas, or usage.
|
|
314
|
+
*/
|
|
315
|
+
async *streamChatCompletion(
|
|
316
|
+
messages: Message[],
|
|
317
|
+
options?: ChatCompletionOptions
|
|
318
|
+
): AsyncGenerator<StreamChunk> {
|
|
319
|
+
const stream = await this.client.chat.completions.create({
|
|
320
|
+
...this.baseRequest(),
|
|
321
|
+
messages: this.formatMessages(messages),
|
|
322
|
+
...this.toolsParam(options?.temporaryTools ?? this.tools),
|
|
323
|
+
stream: true,
|
|
324
|
+
});
|
|
353
325
|
|
|
354
|
-
// 处理流式响应
|
|
355
326
|
for await (const chunk of stream) {
|
|
356
327
|
const delta = chunk.choices[0]?.delta;
|
|
357
328
|
|
|
358
329
|
if (delta?.content) {
|
|
359
|
-
|
|
360
|
-
yield {
|
|
361
|
-
role: 'assistant',
|
|
362
|
-
content: delta.content,
|
|
363
|
-
};
|
|
330
|
+
yield { role: 'assistant', content: delta.content };
|
|
364
331
|
}
|
|
365
332
|
|
|
366
333
|
if (delta?.tool_calls) {
|
|
367
|
-
// 流式工具调用(如果需要)
|
|
368
334
|
yield {
|
|
369
335
|
role: 'assistant',
|
|
370
336
|
content: '',
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
})
|
|
337
|
+
tool_calls: delta.tool_calls.map(toolCall => {
|
|
338
|
+
const name = toolCall.function?.name ?? '';
|
|
339
|
+
const args = toolCall.function?.arguments ?? '';
|
|
340
|
+
return {
|
|
341
|
+
id: toolCall.id ?? '',
|
|
342
|
+
type: 'function',
|
|
343
|
+
function: { name, arguments: args },
|
|
344
|
+
tool_name: name,
|
|
345
|
+
arguments: args,
|
|
346
|
+
};
|
|
347
|
+
}),
|
|
382
348
|
};
|
|
383
349
|
}
|
|
384
350
|
|
|
385
|
-
// 提取并返回 token 使用统计信息
|
|
386
351
|
if (chunk.usage) {
|
|
387
|
-
yield {
|
|
388
|
-
role: 'assistant',
|
|
389
|
-
content: '',
|
|
390
|
-
usage: {
|
|
391
|
-
completion_tokens: chunk.usage.completion_tokens || 0,
|
|
392
|
-
prompt_tokens: chunk.usage.prompt_tokens || 0,
|
|
393
|
-
prompt_cache_hit_tokens: chunk.usage.prompt_cache_hit_tokens,
|
|
394
|
-
prompt_cache_miss_tokens: chunk.usage.prompt_cache_miss_tokens,
|
|
395
|
-
total_tokens: chunk.usage.total_tokens || 0,
|
|
396
|
-
},
|
|
397
|
-
};
|
|
352
|
+
yield { role: 'assistant', content: '', usage: mapUsage(chunk.usage) };
|
|
398
353
|
}
|
|
399
354
|
}
|
|
400
355
|
}
|
|
401
356
|
|
|
402
357
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* @param
|
|
406
|
-
* @returns 生成的响应消息
|
|
358
|
+
* Runs the model outside a pipeline.
|
|
359
|
+
*
|
|
360
|
+
* @param input A prompt string or a list of messages.
|
|
407
361
|
*/
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
): Promise<Message> {
|
|
412
|
-
// 转换消息格式
|
|
413
|
-
const formattedMessages: Array<OpenAI.Chat.ChatCompletionMessageParam> = [];
|
|
414
|
-
|
|
415
|
-
// 添加系统提示
|
|
416
|
-
if (this.systemPrompt) {
|
|
417
|
-
formattedMessages.push({
|
|
418
|
-
role: 'system',
|
|
419
|
-
content: this.systemPrompt,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// 添加用户提供的消息
|
|
424
|
-
formattedMessages.push(
|
|
425
|
-
...messages.map(msg => {
|
|
426
|
-
// 基本消息结构
|
|
427
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
428
|
-
const formattedMsg: any = {
|
|
429
|
-
role: msg.role,
|
|
430
|
-
content: msg.content,
|
|
431
|
-
};
|
|
362
|
+
override run(input: string | Message[]): Promise<string | AssistantMessage> {
|
|
363
|
+
return typeof input === 'string' ? this.generateCompletion(input) : this.chatCompletion(input);
|
|
364
|
+
}
|
|
432
365
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Connects the `prompt` and `messages` input ports to the model.
|
|
368
|
+
* API errors produce a fallback value so the pipeline keeps flowing.
|
|
369
|
+
*/
|
|
370
|
+
override _transform($i: Port, $o: Port): void {
|
|
371
|
+
$i('prompt').receive((prompt: string) => {
|
|
372
|
+
this.generateCompletion(prompt).then(
|
|
373
|
+
completion => {
|
|
374
|
+
$o('completion').send(completion);
|
|
375
|
+
},
|
|
376
|
+
(error: unknown) => {
|
|
377
|
+
this.logError(error);
|
|
378
|
+
$o('completion').send(FALLBACK_TEXT);
|
|
436
379
|
}
|
|
380
|
+
);
|
|
381
|
+
});
|
|
437
382
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
383
|
+
$i('messages').receive((messages: Message[]) => {
|
|
384
|
+
this.chatCompletion(messages).then(
|
|
385
|
+
message => {
|
|
386
|
+
$o('message').send(message);
|
|
387
|
+
},
|
|
388
|
+
(error: unknown) => {
|
|
389
|
+
this.logError(error);
|
|
390
|
+
$o('message').send({ role: 'assistant', content: FALLBACK_TEXT });
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
441
395
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
396
|
+
private baseRequest(): {
|
|
397
|
+
model: string;
|
|
398
|
+
temperature: number;
|
|
399
|
+
max_tokens?: number;
|
|
400
|
+
top_p?: number;
|
|
401
|
+
} {
|
|
402
|
+
return {
|
|
445
403
|
model: this.model,
|
|
446
|
-
messages: formattedMessages,
|
|
447
404
|
temperature: this.temperature,
|
|
448
|
-
max_tokens: this.maxTokens,
|
|
449
|
-
top_p: this.topP,
|
|
405
|
+
...(this.maxTokens === undefined ? {} : { max_tokens: this.maxTokens }),
|
|
406
|
+
...(this.topP === undefined ? {} : { top_p: this.topP }),
|
|
450
407
|
};
|
|
408
|
+
}
|
|
451
409
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// 如果有工具定义,添加到请求中
|
|
457
|
-
if (toolsToUse && toolsToUse.length > 0) {
|
|
458
|
-
// 检查工具格式并转换,确保符合 Deepseek API 要求
|
|
459
|
-
const formattedTools = toolsToUse.map(tool => {
|
|
460
|
-
// 使用类型断言来处理 unknown 类型
|
|
461
|
-
type ToolType = {
|
|
462
|
-
type?: string;
|
|
463
|
-
name?: string;
|
|
464
|
-
description?: string;
|
|
465
|
-
parameters?: Record<string, unknown>;
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
const typedTool = tool as ToolType;
|
|
469
|
-
|
|
470
|
-
// 如果工具已经是 API 格式,直接返回
|
|
471
|
-
if (typedTool.type === 'function') {
|
|
472
|
-
return tool;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// 如果是简单工具实例,转换为 API 格式
|
|
476
|
-
return {
|
|
477
|
-
type: 'function',
|
|
478
|
-
function: {
|
|
479
|
-
name: typedTool.name,
|
|
480
|
-
description: typedTool.description,
|
|
481
|
-
parameters: typedTool.parameters || {},
|
|
482
|
-
},
|
|
483
|
-
};
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
requestParams.tools = formattedTools;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// 调用 API
|
|
490
|
-
const response = await this.client.chat.completions.create(requestParams);
|
|
491
|
-
|
|
492
|
-
// 解析响应
|
|
493
|
-
const responseMessage = response.choices[0].message;
|
|
494
|
-
|
|
495
|
-
// 转换为内部消息格式
|
|
496
|
-
const result: Message = {
|
|
497
|
-
role: 'assistant',
|
|
498
|
-
content: responseMessage.content || '',
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// 提取并添加 token 使用统计信息
|
|
502
|
-
if (response.usage) {
|
|
503
|
-
const usage = response.usage as typeof response.usage & {
|
|
504
|
-
prompt_cache_hit_tokens?: number;
|
|
505
|
-
prompt_cache_miss_tokens?: number;
|
|
506
|
-
};
|
|
507
|
-
result.usage = {
|
|
508
|
-
completion_tokens: usage.completion_tokens || 0,
|
|
509
|
-
prompt_tokens: usage.prompt_tokens || 0,
|
|
510
|
-
prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens,
|
|
511
|
-
prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens,
|
|
512
|
-
total_tokens: usage.total_tokens || 0,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// 如果有工具调用,添加到结果中
|
|
517
|
-
if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
|
|
518
|
-
// 为了兼容 Agent 组件期望的格式,保留 function 和 type 字段
|
|
519
|
-
// 同时为了满足内部 ToolCall 接口,也添加 tool_name 和 arguments 字段
|
|
520
|
-
result.tool_calls = responseMessage.tool_calls.map(toolCall => {
|
|
521
|
-
// Type guard: only handle function tool calls (not custom tool calls)
|
|
522
|
-
if (toolCall.type !== 'function' || !('function' in toolCall)) {
|
|
523
|
-
throw new Error(`Unsupported tool call type: ${toolCall.type}`);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// 解析参数,确保是对象形式
|
|
527
|
-
let args: Record<string, unknown>;
|
|
528
|
-
|
|
529
|
-
const rawArgs = toolCall.function.arguments;
|
|
530
|
-
|
|
531
|
-
if (typeof rawArgs === 'string') {
|
|
532
|
-
const raw = rawArgs;
|
|
533
|
-
try {
|
|
534
|
-
args = raw && raw.trim().length > 0 ? (JSON.parse(raw) as Record<string, unknown>) : {};
|
|
535
|
-
} catch {
|
|
536
|
-
args = {};
|
|
537
|
-
}
|
|
538
|
-
} else if (rawArgs && typeof rawArgs === 'object') {
|
|
539
|
-
args = rawArgs as Record<string, unknown>;
|
|
540
|
-
} else {
|
|
541
|
-
args = {};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return {
|
|
545
|
-
id: toolCall.id,
|
|
546
|
-
// 满足 Agent 组件期望的格式
|
|
547
|
-
function: {
|
|
548
|
-
name: toolCall.function.name,
|
|
549
|
-
arguments: toolCall.function.arguments || '{}',
|
|
550
|
-
},
|
|
551
|
-
type: 'function',
|
|
552
|
-
// 满足内部 ToolCall 接口
|
|
553
|
-
tool_name: toolCall.function.name,
|
|
554
|
-
arguments: args,
|
|
555
|
-
};
|
|
556
|
-
});
|
|
557
|
-
}
|
|
410
|
+
private toolsParam(tools: unknown[] | undefined): { tools?: ChatTool[] } {
|
|
411
|
+
return tools !== undefined && tools.length > 0 ? { tools: tools.map(toApiTool) } : {};
|
|
412
|
+
}
|
|
558
413
|
|
|
559
|
-
|
|
414
|
+
private systemMessages(): ChatMessageParam[] {
|
|
415
|
+
return this.systemPrompt ? [{ role: 'system', content: this.systemPrompt }] : [];
|
|
560
416
|
}
|
|
561
417
|
|
|
562
418
|
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
419
|
+
* Converts input messages to API messages. Assistant `tool_calls` are not forwarded:
|
|
420
|
+
* agents send tool results back as user messages, and the API rejects assistant tool
|
|
421
|
+
* calls that are not followed by tool messages.
|
|
566
422
|
*/
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
423
|
+
private formatMessages(messages: Message[]): ChatMessageParam[] {
|
|
424
|
+
return [
|
|
425
|
+
...this.systemMessages(),
|
|
426
|
+
...messages.map(
|
|
427
|
+
(message): ChatMessageParam =>
|
|
428
|
+
({
|
|
429
|
+
role: message.role,
|
|
430
|
+
content: message.content,
|
|
431
|
+
...(message.role === 'tool' && message.tool_call_id
|
|
432
|
+
? { tool_call_id: message.tool_call_id }
|
|
433
|
+
: {}),
|
|
434
|
+
}) as ChatMessageParam
|
|
435
|
+
),
|
|
436
|
+
];
|
|
573
437
|
}
|
|
574
438
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
// 处理文本提示输入
|
|
583
|
-
$i('prompt').receive(async (prompt: string) => {
|
|
584
|
-
try {
|
|
585
|
-
const completion = await this.generateCompletion(prompt);
|
|
586
|
-
$o('completion').send(completion);
|
|
587
|
-
} catch (error) {
|
|
588
|
-
console.error('Deepseek API 调用错误:', error);
|
|
589
|
-
$o('completion').send('API 调用错误');
|
|
590
|
-
}
|
|
591
|
-
});
|
|
439
|
+
private firstMessage(response: OpenAI.Chat.ChatCompletion): OpenAI.Chat.ChatCompletionMessage {
|
|
440
|
+
const choice = response.choices[0];
|
|
441
|
+
if (choice === undefined) {
|
|
442
|
+
throw new Error('DeepSeek returned no choices.');
|
|
443
|
+
}
|
|
444
|
+
return choice.message;
|
|
445
|
+
}
|
|
592
446
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
} catch (error) {
|
|
599
|
-
console.error('Deepseek API 调用错误:', error);
|
|
600
|
-
$o('message').send({
|
|
601
|
-
role: 'assistant',
|
|
602
|
-
content: 'API 调用错误',
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
});
|
|
447
|
+
private logError(error: unknown): void {
|
|
448
|
+
if (this.verbose) {
|
|
449
|
+
// eslint-disable-next-line no-console -- opt-in diagnostics controlled by `verbose`.
|
|
450
|
+
console.error('[Deepseek] API call failed:', error);
|
|
451
|
+
}
|
|
606
452
|
}
|
|
607
453
|
}
|
|
608
454
|
|
|
609
|
-
// 默认导出组件
|
|
610
455
|
export default Deepseek;
|