@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 ADDED
@@ -0,0 +1,39 @@
1
+ # @astack-tech/integrations
2
+
3
+ ## 0.2.0-beta.7
4
+
5
+ ### Minor Changes
6
+
7
+ - e71dd3b: Deepseek satisfies the `ModelProvider` interface of `@astack-tech/components` without a cast. Unset request options are omitted, an empty `choices` array fails with a clear error, non-object tool arguments become `{}`, and the pipeline fallback text is `API call failed`. API errors are logged only when `verbose` is set. Node.js 20 or later is required.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [e71dd3b]
12
+ - @astack-tech/core@0.2.0-beta.2
13
+
14
+ ## 0.1.1-beta.6
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+ - @astack-tech/core@0.2.0-beta.1
20
+
21
+ ## 0.1.1-beta.2
22
+
23
+ ### Patch Changes
24
+
25
+ - rebuild package
26
+
27
+ ## 0.1.1-beta.1
28
+
29
+ ### Patch Changes
30
+
31
+ - fix agent tool finder
32
+
33
+ ## 0.1.1-beta.0
34
+
35
+ ### Patch Changes
36
+
37
+ - beta release
38
+ - Updated dependencies
39
+ - @astack-tech/core@0.1.1-beta.0
package/README.md CHANGED
@@ -1 +1,75 @@
1
- > @astack-tech/integrations
1
+ # @astack-tech/integrations
2
+
3
+ <img src="https://raw.githubusercontent.com/astack-tech/astack/master/assets/brand/readme-banner.png" alt="AStack" width="100%">
4
+
5
+ [![npm](https://img.shields.io/npm/v/@astack-tech/integrations?color=111&label=npm)](https://www.npmjs.com/package/@astack-tech/integrations) [![license](https://img.shields.io/npm/l/@astack-tech/integrations?color=111)](https://github.com/astack-tech/astack/blob/master/LICENSE) [![node](https://img.shields.io/node/v/@astack-tech/integrations?color=111)](https://nodejs.org)
6
+
7
+ Model providers for AStack. Each provider is a component: call it directly, use it in a pipeline, or pass it to an agent.
8
+
9
+ [Website](https://astack.tech) · [Docs](https://astack.tech/docs) · [Examples](https://astack.tech/examples) · [GitHub](https://github.com/astack-tech/astack)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @astack-tech/integrations @astack-tech/core
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```typescript
20
+ import { ModelProvider } from '@astack-tech/integrations';
21
+
22
+ const model = new ModelProvider.Deepseek({ apiKey: process.env['DEEPSEEK_API_KEY'] ?? '' });
23
+
24
+ const text = await model.run('What is flow-based programming?');
25
+ const reply = await model.chatCompletion([{ role: 'user', content: 'Hello' }]);
26
+ ```
27
+
28
+ `Deepseek` is also available from the subpath: `import { Deepseek } from '@astack-tech/integrations/model-provider'`.
29
+
30
+ ## Deepseek
31
+
32
+ Uses the OpenAI-compatible DeepSeek API through the official `openai` client.
33
+
34
+ | Option | Default | Description |
35
+ | -------------------- | ----------------------------- | -------------------------------------------------------------------- |
36
+ | `apiKey` | — | DeepSeek API key |
37
+ | `model` | `deepseek-chat` | Model name |
38
+ | `baseURL` | `https://api.deepseek.com/v1` | API base URL |
39
+ | `temperature` | `0.7` | Sampling temperature |
40
+ | `maxTokens`, `topP` | not sent | Optional sampling limits |
41
+ | `systemPrompt` | none | Added before every request |
42
+ | `rawTools` / `tools` | none | Tools as `{ name, description, parameters }` or in the OpenAI format |
43
+ | `verbose` | `false` | Log API errors to the console |
44
+
45
+ | Method | Returns |
46
+ | --------------------------------------------------------------- | -------------------------------------------------------------- |
47
+ | `run(prompt)` | Text |
48
+ | `run(messages)`, `chatCompletion(messages, { temporaryTools })` | Assistant message with `content`, `tool_calls` and `usage` |
49
+ | `streamChatCompletion(messages, options)` | Async generator of text, tool-call and usage chunks |
50
+ | `generateCompletion(prompt)` | Text, or the JSON assistant message when the model calls tools |
51
+
52
+ | Pipeline input port | Output port |
53
+ | ------------------- | ------------ |
54
+ | `prompt` | `completion` |
55
+ | `messages` | `message` |
56
+
57
+ `Deepseek` satisfies the `ModelProvider` interface of `@astack-tech/components`, so it can drive `Agent` and `StreamingAgent` directly.
58
+
59
+ ## Learn more
60
+
61
+ - [Model providers](https://astack.tech/docs/model-providers), including how to write your own
62
+ - Example: [chat](https://astack.tech/examples/chat)
63
+
64
+ ## The AStack packages
65
+
66
+ | Package | Purpose |
67
+ | -------------------------------------------------------------------------------------- | ------------------------------------------ |
68
+ | [`@astack-tech/core`](https://www.npmjs.com/package/@astack-tech/core) | `Component`, `Pipeline` and ports |
69
+ | [`@astack-tech/components`](https://www.npmjs.com/package/@astack-tech/components) | Agents, text splitting and tool invocation |
70
+ | [`@astack-tech/tools`](https://www.npmjs.com/package/@astack-tech/tools) | Tool definitions for agents |
71
+ | [`@astack-tech/integrations`](https://www.npmjs.com/package/@astack-tech/integrations) | Model providers |
72
+
73
+ ## License
74
+
75
+ [MIT](./LICENSE). AStack is in public beta: APIs can change between beta releases. See [CHANGELOG.md](./CHANGELOG.md).
package/dist/index.cjs CHANGED
@@ -43,305 +43,242 @@ __export(model_provider_exports, {
43
43
  // src/model-provider/deepseek/index.ts
44
44
  var import_core = require("@astack-tech/core");
45
45
  var import_openai = __toESM(require("openai"), 1);
46
+ var DEFAULT_BASE_URL = "https://api.deepseek.com/v1";
47
+ var DEFAULT_MODEL = "deepseek-chat";
48
+ var DEFAULT_TEMPERATURE = 0.7;
49
+ var FALLBACK_TEXT = "API call failed";
50
+ function isRecord(value) {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+ function toApiTool(tool) {
54
+ if (isRecord(tool) && tool["type"] === "function") {
55
+ return tool;
56
+ }
57
+ const raw = isRecord(tool) ? tool : {};
58
+ const { name, description, parameters } = raw;
59
+ return {
60
+ type: "function",
61
+ function: {
62
+ name: typeof name === "string" ? name : "",
63
+ description: typeof description === "string" ? description : "",
64
+ parameters: isRecord(parameters) ? parameters : {}
65
+ }
66
+ };
67
+ }
68
+ function parseArguments(raw) {
69
+ if (raw.trim().length === 0) {
70
+ return {};
71
+ }
72
+ try {
73
+ const parsed = JSON.parse(raw);
74
+ return isRecord(parsed) ? parsed : {};
75
+ } catch {
76
+ return {};
77
+ }
78
+ }
79
+ function mapUsage(usage) {
80
+ return {
81
+ completion_tokens: usage.completion_tokens ?? 0,
82
+ prompt_tokens: usage.prompt_tokens ?? 0,
83
+ total_tokens: usage.total_tokens ?? 0,
84
+ ...usage.prompt_cache_hit_tokens === void 0 ? {} : { prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens },
85
+ ...usage.prompt_cache_miss_tokens === void 0 ? {} : { prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens }
86
+ };
87
+ }
46
88
  var Deepseek = class extends import_core.Component {
89
+ client;
90
+ model;
91
+ temperature;
92
+ maxTokens;
93
+ topP;
94
+ systemPrompt;
95
+ tools;
96
+ verbose;
47
97
  constructor(config) {
48
98
  super({});
49
99
  this.client = new import_openai.default({
50
100
  apiKey: config.apiKey,
51
- baseURL: config.baseURL || "https://api.deepseek.com/v1",
101
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
52
102
  dangerouslyAllowBrowser: config.dangerouslyAllowBrowser ?? false
53
103
  });
54
- this.model = config.model || "deepseek-chat";
55
- this.temperature = config.temperature ?? 0.7;
104
+ this.model = config.model ?? DEFAULT_MODEL;
105
+ this.temperature = config.temperature ?? DEFAULT_TEMPERATURE;
56
106
  this.maxTokens = config.maxTokens;
57
107
  this.topP = config.topP;
58
108
  this.systemPrompt = config.systemPrompt;
59
- if (config.rawTools && config.rawTools.length > 0) {
60
- this.tools = config.rawTools.map((tool) => ({
61
- type: "function",
62
- function: {
63
- name: tool.name,
64
- description: tool.description,
65
- parameters: tool.parameters || {}
66
- }
67
- }));
68
- } else {
69
- this.tools = config.tools;
70
- }
109
+ this.verbose = config.verbose ?? false;
110
+ this.tools = config.rawTools !== void 0 && config.rawTools.length > 0 ? config.rawTools.map(toApiTool) : config.tools;
71
111
  import_core.Component.Port.I("prompt").attach(this);
72
112
  import_core.Component.Port.I("messages").attach(this);
73
113
  import_core.Component.Port.O("completion").attach(this);
74
114
  import_core.Component.Port.O("message").attach(this);
75
115
  }
76
116
  /**
77
- * 生成文本完成
78
- * @param prompt 提示文本
79
- * @returns 生成的文本
117
+ * Generates a completion for a single prompt.
118
+ *
119
+ * @returns The text answer, or the JSON-encoded assistant message when it contains tool calls.
80
120
  */
81
121
  async generateCompletion(prompt) {
82
- const messages = [];
83
- if (this.systemPrompt) {
84
- messages.push({
85
- role: "system",
86
- content: this.systemPrompt
87
- });
88
- }
89
- messages.push({
90
- role: "user",
91
- content: prompt
122
+ const response = await this.client.chat.completions.create({
123
+ ...this.baseRequest(),
124
+ messages: [...this.systemMessages(), { role: "user", content: prompt }],
125
+ ...this.toolsParam(this.tools)
92
126
  });
93
- const requestParams = {
94
- model: this.model,
95
- messages,
96
- temperature: this.temperature,
97
- max_tokens: this.maxTokens,
98
- top_p: this.topP
99
- };
100
- if (this.tools && this.tools.length > 0) {
101
- requestParams.tools = this.tools;
127
+ const message = this.firstMessage(response);
128
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
129
+ return JSON.stringify(message);
102
130
  }
103
- const response = await this.client.chat.completions.create(requestParams);
104
- if (response.choices[0].message.tool_calls && response.choices[0].message.tool_calls.length > 0) {
105
- return JSON.stringify(response.choices[0].message);
106
- }
107
- return response.choices[0].message.content || "";
131
+ return message.content ?? "";
108
132
  }
109
133
  /**
110
- * 流式处理对话消息
111
- * @param messages 对话消息数组
112
- * @param options 可选的调用选项,包含临时工具列表
113
- * @returns 生成的流式响应消息异步生成器
134
+ * Sends a conversation and returns the assistant reply.
114
135
  */
115
- async *streamChatCompletion(messages, options) {
116
- const formattedMessages = [];
117
- if (this.systemPrompt) {
118
- formattedMessages.push({
119
- role: "system",
120
- content: this.systemPrompt
121
- });
136
+ async chatCompletion(messages, options) {
137
+ const response = await this.client.chat.completions.create({
138
+ ...this.baseRequest(),
139
+ messages: this.formatMessages(messages),
140
+ ...this.toolsParam(options?.temporaryTools ?? this.tools)
141
+ });
142
+ const message = this.firstMessage(response);
143
+ const result = { role: "assistant", content: message.content ?? "" };
144
+ if (response.usage !== void 0) {
145
+ result.usage = mapUsage(response.usage);
122
146
  }
123
- formattedMessages.push(
124
- ...messages.map((msg) => {
125
- const formattedMsg = {
126
- role: msg.role,
127
- content: msg.content
128
- };
129
- if (msg.role === "tool" && msg.tool_call_id) {
130
- formattedMsg.tool_call_id = msg.tool_call_id;
131
- }
132
- return formattedMsg;
133
- })
134
- );
135
- const requestParams = {
136
- model: this.model,
137
- messages: formattedMessages,
138
- temperature: this.temperature,
139
- max_tokens: this.maxTokens,
140
- top_p: this.topP,
141
- stream: true
142
- // 开启流式模式
143
- };
144
- const toolsToUse = options?.temporaryTools || this.tools;
145
- if (toolsToUse && toolsToUse.length > 0) {
146
- const formattedTools = toolsToUse.map((tool) => {
147
- const typedTool = tool;
148
- if (typedTool.type === "function") {
149
- return tool;
147
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
148
+ result.tool_calls = message.tool_calls.map((toolCall) => {
149
+ if (toolCall.type !== "function") {
150
+ throw new Error(`Unsupported tool call type: ${toolCall.type}`);
150
151
  }
152
+ const rawArguments = toolCall.function.arguments;
151
153
  return {
154
+ id: toolCall.id,
152
155
  type: "function",
153
156
  function: {
154
- name: typedTool.name,
155
- description: typedTool.description,
156
- parameters: typedTool.parameters || {}
157
- }
157
+ name: toolCall.function.name,
158
+ arguments: rawArguments.length > 0 ? rawArguments : "{}"
159
+ },
160
+ tool_name: toolCall.function.name,
161
+ arguments: parseArguments(rawArguments)
158
162
  };
159
163
  });
160
- requestParams.tools = formattedTools;
161
164
  }
162
- const stream = await this.client.chat.completions.create(requestParams);
165
+ return result;
166
+ }
167
+ /**
168
+ * Streams the assistant reply. Each chunk holds a text delta, tool-call deltas, or usage.
169
+ */
170
+ async *streamChatCompletion(messages, options) {
171
+ const stream = await this.client.chat.completions.create({
172
+ ...this.baseRequest(),
173
+ messages: this.formatMessages(messages),
174
+ ...this.toolsParam(options?.temporaryTools ?? this.tools),
175
+ stream: true
176
+ });
163
177
  for await (const chunk of stream) {
164
178
  const delta = chunk.choices[0]?.delta;
165
179
  if (delta?.content) {
166
- yield {
167
- role: "assistant",
168
- content: delta.content
169
- };
180
+ yield { role: "assistant", content: delta.content };
170
181
  }
171
182
  if (delta?.tool_calls) {
172
183
  yield {
173
184
  role: "assistant",
174
185
  content: "",
175
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
- tool_calls: delta.tool_calls.map((toolCall) => ({
177
- id: toolCall.id || "",
178
- function: {
179
- name: toolCall.function?.name || "",
180
- arguments: toolCall.function?.arguments || ""
181
- },
182
- type: "function",
183
- tool_name: toolCall.function?.name || "",
184
- arguments: toolCall.function?.arguments || ""
185
- }))
186
+ tool_calls: delta.tool_calls.map((toolCall) => {
187
+ const name = toolCall.function?.name ?? "";
188
+ const args = toolCall.function?.arguments ?? "";
189
+ return {
190
+ id: toolCall.id ?? "",
191
+ type: "function",
192
+ function: { name, arguments: args },
193
+ tool_name: name,
194
+ arguments: args
195
+ };
196
+ })
186
197
  };
187
198
  }
188
199
  if (chunk.usage) {
189
- yield {
190
- role: "assistant",
191
- content: "",
192
- usage: {
193
- completion_tokens: chunk.usage.completion_tokens || 0,
194
- prompt_tokens: chunk.usage.prompt_tokens || 0,
195
- prompt_cache_hit_tokens: chunk.usage.prompt_cache_hit_tokens,
196
- prompt_cache_miss_tokens: chunk.usage.prompt_cache_miss_tokens,
197
- total_tokens: chunk.usage.total_tokens || 0
198
- }
199
- };
200
+ yield { role: "assistant", content: "", usage: mapUsage(chunk.usage) };
200
201
  }
201
202
  }
202
203
  }
203
204
  /**
204
- * 处理对话消息
205
- * @param messages 对话消息数组
206
- * @param options 可选的调用选项,包含临时工具列表
207
- * @returns 生成的响应消息
205
+ * Runs the model outside a pipeline.
206
+ *
207
+ * @param input A prompt string or a list of messages.
208
208
  */
209
- async chatCompletion(messages, options) {
210
- const formattedMessages = [];
211
- if (this.systemPrompt) {
212
- formattedMessages.push({
213
- role: "system",
214
- content: this.systemPrompt
215
- });
216
- }
217
- formattedMessages.push(
218
- ...messages.map((msg) => {
219
- const formattedMsg = {
220
- role: msg.role,
221
- content: msg.content
222
- };
223
- if (msg.role === "tool" && msg.tool_call_id) {
224
- formattedMsg.tool_call_id = msg.tool_call_id;
209
+ run(input) {
210
+ return typeof input === "string" ? this.generateCompletion(input) : this.chatCompletion(input);
211
+ }
212
+ /**
213
+ * Connects the `prompt` and `messages` input ports to the model.
214
+ * API errors produce a fallback value so the pipeline keeps flowing.
215
+ */
216
+ _transform($i, $o) {
217
+ $i("prompt").receive((prompt) => {
218
+ this.generateCompletion(prompt).then(
219
+ (completion) => {
220
+ $o("completion").send(completion);
221
+ },
222
+ (error) => {
223
+ this.logError(error);
224
+ $o("completion").send(FALLBACK_TEXT);
225
225
  }
226
- return formattedMsg;
227
- })
228
- );
229
- const requestParams = {
226
+ );
227
+ });
228
+ $i("messages").receive((messages) => {
229
+ this.chatCompletion(messages).then(
230
+ (message) => {
231
+ $o("message").send(message);
232
+ },
233
+ (error) => {
234
+ this.logError(error);
235
+ $o("message").send({ role: "assistant", content: FALLBACK_TEXT });
236
+ }
237
+ );
238
+ });
239
+ }
240
+ baseRequest() {
241
+ return {
230
242
  model: this.model,
231
- messages: formattedMessages,
232
243
  temperature: this.temperature,
233
- max_tokens: this.maxTokens,
234
- top_p: this.topP
235
- };
236
- const toolsToUse = options?.temporaryTools || this.tools;
237
- if (toolsToUse && toolsToUse.length > 0) {
238
- const formattedTools = toolsToUse.map((tool) => {
239
- const typedTool = tool;
240
- if (typedTool.type === "function") {
241
- return tool;
242
- }
243
- return {
244
- type: "function",
245
- function: {
246
- name: typedTool.name,
247
- description: typedTool.description,
248
- parameters: typedTool.parameters || {}
249
- }
250
- };
251
- });
252
- requestParams.tools = formattedTools;
253
- }
254
- const response = await this.client.chat.completions.create(requestParams);
255
- const responseMessage = response.choices[0].message;
256
- const result = {
257
- role: "assistant",
258
- content: responseMessage.content || ""
244
+ ...this.maxTokens === void 0 ? {} : { max_tokens: this.maxTokens },
245
+ ...this.topP === void 0 ? {} : { top_p: this.topP }
259
246
  };
260
- if (response.usage) {
261
- const usage = response.usage;
262
- result.usage = {
263
- completion_tokens: usage.completion_tokens || 0,
264
- prompt_tokens: usage.prompt_tokens || 0,
265
- prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens,
266
- prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens,
267
- total_tokens: usage.total_tokens || 0
268
- };
269
- }
270
- if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
271
- result.tool_calls = responseMessage.tool_calls.map((toolCall) => {
272
- if (toolCall.type !== "function" || !("function" in toolCall)) {
273
- throw new Error(`Unsupported tool call type: ${toolCall.type}`);
274
- }
275
- let args;
276
- const rawArgs = toolCall.function.arguments;
277
- if (typeof rawArgs === "string") {
278
- const raw = rawArgs;
279
- try {
280
- args = raw && raw.trim().length > 0 ? JSON.parse(raw) : {};
281
- } catch {
282
- args = {};
283
- }
284
- } else if (rawArgs && typeof rawArgs === "object") {
285
- args = rawArgs;
286
- } else {
287
- args = {};
288
- }
289
- return {
290
- id: toolCall.id,
291
- // 满足 Agent 组件期望的格式
292
- function: {
293
- name: toolCall.function.name,
294
- arguments: toolCall.function.arguments || "{}"
295
- },
296
- type: "function",
297
- // 满足内部 ToolCall 接口
298
- tool_name: toolCall.function.name,
299
- arguments: args
300
- };
301
- });
302
- }
303
- return result;
247
+ }
248
+ toolsParam(tools) {
249
+ return tools !== void 0 && tools.length > 0 ? { tools: tools.map(toApiTool) } : {};
250
+ }
251
+ systemMessages() {
252
+ return this.systemPrompt ? [{ role: "system", content: this.systemPrompt }] : [];
304
253
  }
305
254
  /**
306
- * 在独立模式下运行组件
307
- * @param input 输入参数,可以是提示文本或对话消息数组
308
- * @returns 生成的文本或响应消息
255
+ * Converts input messages to API messages. Assistant `tool_calls` are not forwarded:
256
+ * agents send tool results back as user messages, and the API rejects assistant tool
257
+ * calls that are not followed by tool messages.
309
258
  */
310
- async run(input) {
311
- if (typeof input === "string") {
312
- return this.generateCompletion(input);
313
- } else {
314
- return this.chatCompletion(input);
259
+ formatMessages(messages) {
260
+ return [
261
+ ...this.systemMessages(),
262
+ ...messages.map(
263
+ (message) => ({
264
+ role: message.role,
265
+ content: message.content,
266
+ ...message.role === "tool" && message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}
267
+ })
268
+ )
269
+ ];
270
+ }
271
+ firstMessage(response) {
272
+ const choice = response.choices[0];
273
+ if (choice === void 0) {
274
+ throw new Error("DeepSeek returned no choices.");
315
275
  }
276
+ return choice.message;
316
277
  }
317
- /**
318
- * 在流水线中运行组件
319
- * @param $i 输入端口
320
- * @param $o 输出端口
321
- */
322
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
323
- _transform($i, $o) {
324
- $i("prompt").receive(async (prompt) => {
325
- try {
326
- const completion = await this.generateCompletion(prompt);
327
- $o("completion").send(completion);
328
- } catch (error) {
329
- console.error("Deepseek API \u8C03\u7528\u9519\u8BEF:", error);
330
- $o("completion").send("API \u8C03\u7528\u9519\u8BEF");
331
- }
332
- });
333
- $i("messages").receive(async (messages) => {
334
- try {
335
- const responseMessage = await this.chatCompletion(messages);
336
- $o("message").send(responseMessage);
337
- } catch (error) {
338
- console.error("Deepseek API \u8C03\u7528\u9519\u8BEF:", error);
339
- $o("message").send({
340
- role: "assistant",
341
- content: "API \u8C03\u7528\u9519\u8BEF"
342
- });
343
- }
344
- });
278
+ logError(error) {
279
+ if (this.verbose) {
280
+ console.error("[Deepseek] API call failed:", error);
281
+ }
345
282
  }
346
283
  };
347
284
  var deepseek_default = Deepseek;