@astack-tech/integrations 0.1.1-beta.6 → 0.2.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -37,310 +37,295 @@ module.exports = __toCommonJS(src_exports);
37
37
  // src/model-provider/index.ts
38
38
  var model_provider_exports = {};
39
39
  __export(model_provider_exports, {
40
- Deepseek: () => deepseek_default
40
+ Deepseek: () => deepseek_default,
41
+ OpenAICompatible: () => openai_compatible_default
41
42
  });
42
43
 
43
- // src/model-provider/deepseek/index.ts
44
+ // src/model-provider/openai-compatible/index.ts
44
45
  var import_core = require("@astack-tech/core");
45
46
  var import_openai = __toESM(require("openai"), 1);
46
- var Deepseek = class extends import_core.Component {
47
+ var DEFAULT_BASE_URL = "https://api.openai.com/v1";
48
+ var DEFAULT_NAME = "The model API";
49
+ var DEFAULT_TEMPERATURE = 0.7;
50
+ var FALLBACK_TEXT = "API call failed";
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function toApiTool(tool) {
55
+ const raw = isRecord(tool) ? tool : {};
56
+ const definition = raw["function"];
57
+ if (raw["type"] === "function" && isRecord(definition)) {
58
+ const { name: name2, description: description2, parameters: parameters2, strict } = definition;
59
+ return {
60
+ type: "function",
61
+ function: {
62
+ name: typeof name2 === "string" ? name2 : "",
63
+ ...typeof description2 === "string" ? { description: description2 } : {},
64
+ ...isRecord(parameters2) ? { parameters: parameters2 } : {},
65
+ ...typeof strict === "boolean" ? { strict } : {}
66
+ }
67
+ };
68
+ }
69
+ const { name, description, parameters } = raw;
70
+ return {
71
+ type: "function",
72
+ function: {
73
+ name: typeof name === "string" ? name : "",
74
+ description: typeof description === "string" ? description : "",
75
+ parameters: isRecord(parameters) ? parameters : {}
76
+ }
77
+ };
78
+ }
79
+ function parseArguments(raw) {
80
+ if (raw.trim().length === 0) {
81
+ return {};
82
+ }
83
+ try {
84
+ const parsed = JSON.parse(raw);
85
+ return isRecord(parsed) ? parsed : {};
86
+ } catch {
87
+ return {};
88
+ }
89
+ }
90
+ function mapUsage(usage) {
91
+ return {
92
+ completion_tokens: usage.completion_tokens ?? 0,
93
+ prompt_tokens: usage.prompt_tokens ?? 0,
94
+ total_tokens: usage.total_tokens ?? 0,
95
+ ...usage.prompt_cache_hit_tokens === void 0 ? {} : { prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens },
96
+ ...usage.prompt_cache_miss_tokens === void 0 ? {} : { prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens }
97
+ };
98
+ }
99
+ var OpenAICompatible = class extends import_core.Component {
100
+ client;
101
+ model;
102
+ temperature;
103
+ maxTokens;
104
+ topP;
105
+ systemPrompt;
106
+ tools;
107
+ verbose;
108
+ name;
47
109
  constructor(config) {
48
110
  super({});
49
111
  this.client = new import_openai.default({
50
112
  apiKey: config.apiKey,
51
- baseURL: config.baseURL || "https://api.deepseek.com/v1",
113
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
52
114
  dangerouslyAllowBrowser: config.dangerouslyAllowBrowser ?? false
53
115
  });
54
- this.model = config.model || "deepseek-chat";
55
- this.temperature = config.temperature ?? 0.7;
116
+ this.model = config.model;
117
+ this.name = config.name ?? DEFAULT_NAME;
118
+ this.temperature = config.temperature ?? DEFAULT_TEMPERATURE;
56
119
  this.maxTokens = config.maxTokens;
57
120
  this.topP = config.topP;
58
121
  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
- }
122
+ this.verbose = config.verbose ?? false;
123
+ this.tools = config.rawTools !== void 0 && config.rawTools.length > 0 ? config.rawTools.map(toApiTool) : config.tools;
71
124
  import_core.Component.Port.I("prompt").attach(this);
72
125
  import_core.Component.Port.I("messages").attach(this);
73
126
  import_core.Component.Port.O("completion").attach(this);
74
127
  import_core.Component.Port.O("message").attach(this);
75
128
  }
76
129
  /**
77
- * 生成文本完成
78
- * @param prompt 提示文本
79
- * @returns 生成的文本
130
+ * Generates a completion for a single prompt.
131
+ *
132
+ * @returns The text answer, or the JSON-encoded assistant message when it contains tool calls.
80
133
  */
81
134
  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
135
+ const response = await this.client.chat.completions.create({
136
+ ...this.baseRequest(),
137
+ messages: [...this.systemMessages(), { role: "user", content: prompt }],
138
+ ...this.toolsParam(this.tools)
92
139
  });
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;
102
- }
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);
140
+ const message = this.firstMessage(response);
141
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
142
+ return JSON.stringify(message);
106
143
  }
107
- return response.choices[0].message.content || "";
144
+ return message.content ?? "";
108
145
  }
109
146
  /**
110
- * 流式处理对话消息
111
- * @param messages 对话消息数组
112
- * @param options 可选的调用选项,包含临时工具列表
113
- * @returns 生成的流式响应消息异步生成器
147
+ * Sends a conversation and returns the assistant reply.
114
148
  */
115
- async *streamChatCompletion(messages, options) {
116
- const formattedMessages = [];
117
- if (this.systemPrompt) {
118
- formattedMessages.push({
119
- role: "system",
120
- content: this.systemPrompt
121
- });
149
+ async chatCompletion(messages, options) {
150
+ const response = await this.client.chat.completions.create({
151
+ ...this.baseRequest(),
152
+ messages: this.formatMessages(messages),
153
+ ...this.toolsParam(options?.temporaryTools ?? this.tools)
154
+ });
155
+ const message = this.firstMessage(response);
156
+ const result = { role: "assistant", content: message.content ?? "" };
157
+ if (response.usage !== void 0) {
158
+ result.usage = mapUsage(response.usage);
122
159
  }
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;
160
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
161
+ result.tool_calls = message.tool_calls.map((toolCall) => {
162
+ if (toolCall.type !== "function") {
163
+ throw new Error(`Unsupported tool call type: ${toolCall.type}`);
150
164
  }
165
+ const rawArguments = toolCall.function.arguments;
151
166
  return {
167
+ id: toolCall.id,
152
168
  type: "function",
153
169
  function: {
154
- name: typedTool.name,
155
- description: typedTool.description,
156
- parameters: typedTool.parameters || {}
157
- }
170
+ name: toolCall.function.name,
171
+ arguments: rawArguments.length > 0 ? rawArguments : "{}"
172
+ },
173
+ tool_name: toolCall.function.name,
174
+ arguments: parseArguments(rawArguments)
158
175
  };
159
176
  });
160
- requestParams.tools = formattedTools;
161
177
  }
162
- const stream = await this.client.chat.completions.create(requestParams);
178
+ return result;
179
+ }
180
+ /**
181
+ * Streams the assistant reply. Each chunk holds a text delta, tool-call deltas, or usage.
182
+ */
183
+ async *streamChatCompletion(messages, options) {
184
+ const stream = await this.client.chat.completions.create({
185
+ ...this.baseRequest(),
186
+ messages: this.formatMessages(messages),
187
+ ...this.toolsParam(options?.temporaryTools ?? this.tools),
188
+ stream: true
189
+ });
163
190
  for await (const chunk of stream) {
164
191
  const delta = chunk.choices[0]?.delta;
165
- if (delta?.content) {
166
- yield {
167
- role: "assistant",
168
- content: delta.content
169
- };
192
+ const text = delta?.content ?? "";
193
+ if (text !== "") {
194
+ yield { role: "assistant", content: text };
170
195
  }
171
196
  if (delta?.tool_calls) {
172
197
  yield {
173
198
  role: "assistant",
174
199
  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
- }))
200
+ tool_calls: delta.tool_calls.map((toolCall) => {
201
+ const name = toolCall.function?.name ?? "";
202
+ const args = toolCall.function?.arguments ?? "";
203
+ return {
204
+ id: toolCall.id ?? "",
205
+ type: "function",
206
+ function: { name, arguments: args },
207
+ tool_name: name,
208
+ arguments: args
209
+ };
210
+ })
186
211
  };
187
212
  }
188
213
  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
- };
214
+ yield { role: "assistant", content: "", usage: mapUsage(chunk.usage) };
200
215
  }
201
216
  }
202
217
  }
203
218
  /**
204
- * 处理对话消息
205
- * @param messages 对话消息数组
206
- * @param options 可选的调用选项,包含临时工具列表
207
- * @returns 生成的响应消息
219
+ * Runs the model outside a pipeline.
220
+ *
221
+ * @param input - A prompt string or a list of messages.
208
222
  */
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;
223
+ run(input) {
224
+ return typeof input === "string" ? this.generateCompletion(input) : this.chatCompletion(input);
225
+ }
226
+ /**
227
+ * Connects the `prompt` and `messages` input ports to the model.
228
+ * API errors produce a fallback value so the pipeline keeps flowing.
229
+ */
230
+ _transform($i, $o) {
231
+ $i("prompt").receive((prompt) => {
232
+ this.generateCompletion(prompt).then(
233
+ (completion) => {
234
+ $o("completion").send(completion);
235
+ },
236
+ (error) => {
237
+ this.logError(error);
238
+ $o("completion").send(FALLBACK_TEXT);
225
239
  }
226
- return formattedMsg;
227
- })
228
- );
229
- const requestParams = {
240
+ );
241
+ });
242
+ $i("messages").receive((messages) => {
243
+ this.chatCompletion(messages).then(
244
+ (message) => {
245
+ $o("message").send(message);
246
+ },
247
+ (error) => {
248
+ this.logError(error);
249
+ $o("message").send({ role: "assistant", content: FALLBACK_TEXT });
250
+ }
251
+ );
252
+ });
253
+ }
254
+ baseRequest() {
255
+ return {
230
256
  model: this.model,
231
- messages: formattedMessages,
232
257
  temperature: this.temperature,
233
- max_tokens: this.maxTokens,
234
- top_p: this.topP
258
+ ...this.maxTokens === void 0 ? {} : { max_tokens: this.maxTokens },
259
+ ...this.topP === void 0 ? {} : { top_p: this.topP }
235
260
  };
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 || ""
259
- };
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;
261
+ }
262
+ toolsParam(tools) {
263
+ return tools !== void 0 && tools.length > 0 ? { tools: tools.map(toApiTool) } : {};
264
+ }
265
+ systemMessages() {
266
+ return this.systemPrompt === void 0 || this.systemPrompt === "" ? [] : [{ role: "system", content: this.systemPrompt }];
304
267
  }
305
268
  /**
306
- * 在独立模式下运行组件
307
- * @param input 输入参数,可以是提示文本或对话消息数组
308
- * @returns 生成的文本或响应消息
269
+ * Converts input messages to API messages. Assistant `tool_calls` are not forwarded:
270
+ * agents send tool results back as user messages, and the API rejects assistant tool
271
+ * calls that are not followed by tool messages.
309
272
  */
310
- async run(input) {
311
- if (typeof input === "string") {
312
- return this.generateCompletion(input);
313
- } else {
314
- return this.chatCompletion(input);
315
- }
273
+ formatMessages(messages) {
274
+ return [...this.systemMessages(), ...messages.map((message) => this.toApiMessage(message))];
316
275
  }
317
276
  /**
318
- * 在流水线中运行组件
319
- * @param $i 输入端口
320
- * @param $o 输出端口
277
+ * Converts one input message to its API shape.
278
+ *
279
+ * @throws Error when the role is not one of `MessageRole`, or a `tool` message has no
280
+ * `tool_call_id`. The API would reject both.
321
281
  */
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
- }
282
+ toApiMessage(message) {
283
+ const content = message.content ?? "";
284
+ switch (message.role) {
285
+ case "system":
286
+ return { role: "system", content };
287
+ case "user":
288
+ return { role: "user", content };
289
+ case "assistant":
290
+ return message.content === void 0 ? { role: "assistant" } : { role: "assistant", content: message.content };
291
+ case "tool":
292
+ if (message.tool_call_id === void 0 || message.tool_call_id === "") {
293
+ throw new Error(
294
+ `${this.name}: a tool message needs the tool_call_id of the call it answers.`
295
+ );
296
+ }
297
+ return { role: "tool", content, tool_call_id: message.tool_call_id };
298
+ default:
299
+ throw new Error(
300
+ `${this.name}: unsupported message role '${message.role}'. Use system, user, assistant or tool.`
301
+ );
302
+ }
303
+ }
304
+ firstMessage(response) {
305
+ const choice = response.choices[0];
306
+ if (choice === void 0) {
307
+ throw new Error(`${this.name} returned no choices.`);
308
+ }
309
+ return choice.message;
310
+ }
311
+ logError(error) {
312
+ if (this.verbose) {
313
+ console.error(`[${this.name}] API call failed:`, error);
314
+ }
315
+ }
316
+ };
317
+ var openai_compatible_default = OpenAICompatible;
318
+
319
+ // src/model-provider/deepseek/index.ts
320
+ var DEFAULT_BASE_URL2 = "https://api.deepseek.com/v1";
321
+ var DEFAULT_MODEL = "deepseek-chat";
322
+ var Deepseek = class extends openai_compatible_default {
323
+ constructor(config) {
324
+ super({
325
+ ...config,
326
+ model: config.model ?? DEFAULT_MODEL,
327
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL2,
328
+ name: "DeepSeek"
344
329
  });
345
330
  }
346
331
  };