@astack-tech/integrations 0.1.1-beta.5 → 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.
@@ -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
- * Deepseek 模型提供者配置
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
- * 默认为 'deepseek-chat'
16
- */
17
- model?: string;
18
-
19
- /**
20
- * 自定义 API 基础 URL
21
- * 默认为 Deepseek 官方 API 地址
22
- */
23
- baseURL?: string;
24
-
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;
25
23
  /**
26
- * 温度参数,控制输出的随机性
24
+ * Tools given as `{ name, description, parameters }` objects.
25
+ * When this list is not empty, it takes precedence over `tools`.
27
26
  */
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
-
45
- /**
46
- * 工具实例数组,直接传递工具实例
47
- * 每个工具应具有 name、description 和 parameters 属性
48
- */
49
- rawTools?: Array<{
50
- name: string;
51
- description: string;
52
- parameters?: Record<string, unknown>;
53
- }>;
54
-
55
- /**
56
- * API 格式的工具定义数组,与 rawTools 二选一
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
- * dangerouslyAllowBrowser
63
- * 是否允许在浏览器等非安全环境内运行
64
- */
65
- dangerouslyAllowBrowser?: boolean;
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
- * 工具调用 ID
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
- * Token 使用统计
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
- * 提示缓存命中的 token 数量(可选)
109
- */
110
- prompt_cache_hit_tokens?: number;
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
- * 提示缓存未命中的 token 数量(可选)
114
- */
115
- prompt_cache_miss_tokens?: number;
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
- * 总 token 数量
119
- */
120
- total_tokens: number;
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 Message {
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
- tool_calls?: ToolCall[];
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
- * 工具调用 ID,用于工具响应消息
144
- */
145
- tool_call_id?: string;
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
- * Token 使用统计(可选)
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
- * Deepseek 组件
155
- *
156
- * 使用 OpenAI 兼容接口调用 Deepseek 模型
157
- *
158
- * 输入:
159
- * - prompt: 文本提示
160
- * - messages: 对话消息数组
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
- * - completion: 生成的文本完成
164
- * - message: 生成的响应消息
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
- // OpenAI 客户端实例
168
- private client: OpenAI;
169
-
170
- // 配置选项
171
- private model: string;
172
- private temperature: number;
173
- private maxTokens?: number;
174
- private topP?: number;
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 || 'https://api.deepseek.com/v1',
230
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
185
231
  dangerouslyAllowBrowser: config.dangerouslyAllowBrowser ?? false,
186
232
  });
187
233
 
188
- // 保存基本配置
189
- this.model = config.model || 'deepseek-chat';
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,390 +249,207 @@ class Deepseek extends Component {
216
249
  }
217
250
 
218
251
  /**
219
- * 生成文本完成
220
- * @param prompt 提示文本
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 messages: Array<OpenAI.Chat.ChatCompletionMessageParam> = [];
225
-
226
- // 添加系统提示
227
- if (this.systemPrompt) {
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
242
- const requestParams: any = {
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;
253
- }
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);
263
+ const message = this.firstMessage(response);
264
+ if (message.tool_calls !== undefined && message.tool_calls.length > 0) {
265
+ return JSON.stringify(message);
264
266
  }
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 *streamChatCompletion(
273
+ async chatCompletion(
276
274
  messages: Message[],
277
- options?: { temporaryTools?: unknown[] }
278
- ): AsyncGenerator<Partial<Message>> {
279
- // 转换消息格式 (复用现有逻辑)
280
- const formattedMessages: Array<OpenAI.Chat.ChatCompletionMessageParam> = [];
281
-
282
- // 添加系统提示
283
- if (this.systemPrompt) {
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 toolsToUse = options?.temporaryTools || this.tools;
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
- const typedTool = tool as ToolType;
286
+ if (response.usage !== undefined) {
287
+ result.usage = mapUsage(response.usage);
288
+ }
332
289
 
333
- if (typedTool.type === 'function') {
334
- return tool;
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: typedTool.name,
341
- description: typedTool.description,
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
- // 调用流式 API
351
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
352
- const stream = (await this.client.chat.completions.create(requestParams)) as any;
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
372
- tool_calls: delta.tool_calls.map((toolCall: any) => ({
373
- id: toolCall.id || '',
374
- function: {
375
- name: toolCall.function?.name || '',
376
- arguments: toolCall.function?.arguments || '',
377
- },
378
- type: 'function',
379
- tool_name: toolCall.function?.name || '',
380
- arguments: toolCall.function?.arguments || '',
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
- * @param messages 对话消息数组
405
- * @param options 可选的调用选项,包含临时工具列表
406
- * @returns 生成的响应消息
358
+ * Runs the model outside a pipeline.
359
+ *
360
+ * @param input A prompt string or a list of messages.
407
361
  */
408
- async chatCompletion(
409
- messages: Message[],
410
- options?: { temporaryTools?: unknown[] }
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
- // 如果有工具调用 ID,添加到消息中
434
- if (msg.role === 'tool' && msg.tool_call_id) {
435
- formattedMsg.tool_call_id = msg.tool_call_id;
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
- return formattedMsg;
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
444
- const requestParams: any = {
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
- const toolsToUse = options?.temporaryTools || this.tools;
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
- // 解析参数,确保是对象形式
522
- let args: Record<string, unknown>;
523
-
524
- const rawArgs = toolCall.function.arguments;
525
-
526
- if (typeof rawArgs === 'string') {
527
- const raw = rawArgs;
528
- try {
529
- args = raw && raw.trim().length > 0 ? (JSON.parse(raw) as Record<string, unknown>) : {};
530
- } catch {
531
- args = {};
532
- }
533
- } else if (rawArgs && typeof rawArgs === 'object') {
534
- args = rawArgs as Record<string, unknown>;
535
- } else {
536
- args = {};
537
- }
538
-
539
- return {
540
- id: toolCall.id,
541
- // 满足 Agent 组件期望的格式
542
- function: {
543
- name: toolCall.function.name,
544
- arguments: toolCall.function.arguments || '{}',
545
- },
546
- type: 'function',
547
- // 满足内部 ToolCall 接口
548
- tool_name: toolCall.function.name,
549
- arguments: args,
550
- };
551
- });
552
- }
410
+ private toolsParam(tools: unknown[] | undefined): { tools?: ChatTool[] } {
411
+ return tools !== undefined && tools.length > 0 ? { tools: tools.map(toApiTool) } : {};
412
+ }
553
413
 
554
- return result;
414
+ private systemMessages(): ChatMessageParam[] {
415
+ return this.systemPrompt ? [{ role: 'system', content: this.systemPrompt }] : [];
555
416
  }
556
417
 
557
418
  /**
558
- * 在独立模式下运行组件
559
- * @param input 输入参数,可以是提示文本或对话消息数组
560
- * @returns 生成的文本或响应消息
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.
561
422
  */
562
- async run(input: string | Message[]): Promise<string | Message> {
563
- if (typeof input === 'string') {
564
- return this.generateCompletion(input);
565
- } else {
566
- return this.chatCompletion(input);
567
- }
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
+ ];
568
437
  }
569
438
 
570
- /**
571
- * 在流水线中运行组件
572
- * @param $i 输入端口
573
- * @param $o 输出端口
574
- */
575
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
576
- _transform($i: any, $o: any) {
577
- // 处理文本提示输入
578
- $i('prompt').receive(async (prompt: string) => {
579
- try {
580
- const completion = await this.generateCompletion(prompt);
581
- $o('completion').send(completion);
582
- } catch (error) {
583
- console.error('Deepseek API 调用错误:', error);
584
- $o('completion').send('API 调用错误');
585
- }
586
- });
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
+ }
587
446
 
588
- // 处理对话消息输入
589
- $i('messages').receive(async (messages: Message[]) => {
590
- try {
591
- const responseMessage = await this.chatCompletion(messages);
592
- $o('message').send(responseMessage);
593
- } catch (error) {
594
- console.error('Deepseek API 调用错误:', error);
595
- $o('message').send({
596
- role: 'assistant',
597
- content: 'API 调用错误',
598
- });
599
- }
600
- });
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
+ }
601
452
  }
602
453
  }
603
454
 
604
- // 默认导出组件
605
455
  export default Deepseek;