@cloudbase/agent-adapter-yuanqi 0.0.16 → 0.0.18

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.mjs CHANGED
@@ -3,6 +3,8 @@ import {
3
3
  AbstractAgent,
4
4
  EventType as EventType2
5
5
  } from "@ag-ui/client";
6
+ import tcb from "@cloudbase/node-sdk";
7
+ import managedTcb from "@cloudbase/manager-node";
6
8
  import OpenAI from "openai";
7
9
  import { randomUUID } from "crypto";
8
10
 
@@ -27,6 +29,14 @@ function camelToSnakeKeys(obj) {
27
29
  }
28
30
  return obj;
29
31
  }
32
+ function genRandomStr(length) {
33
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
34
+ let result = "";
35
+ for (let i = 0; i < length; i++) {
36
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
37
+ }
38
+ return result;
39
+ }
30
40
 
31
41
  // src/stream.ts
32
42
  import { EventType } from "@ag-ui/client";
@@ -45,6 +55,28 @@ async function* processYuanqiStream(stream, context) {
45
55
  for await (const chunk of stream) {
46
56
  const delta = chunk.choices[0]?.delta;
47
57
  if (!delta) continue;
58
+ if (delta.role === "tool") {
59
+ const toolCallId = delta.tool_call_id;
60
+ if (toolCallId) {
61
+ if (state.toolCallsMap.has(toolCallId)) {
62
+ yield {
63
+ type: EventType.TOOL_CALL_END,
64
+ threadId,
65
+ runId,
66
+ toolCallId
67
+ };
68
+ state.toolCallsMap.delete(toolCallId);
69
+ }
70
+ yield {
71
+ type: EventType.TOOL_CALL_RESULT,
72
+ threadId,
73
+ runId,
74
+ toolCallId,
75
+ content: delta.content || ""
76
+ };
77
+ }
78
+ continue;
79
+ }
48
80
  if (delta.content) {
49
81
  if (reasoningState.hasStarted) {
50
82
  reasoningState.hasStarted = false;
@@ -117,6 +149,15 @@ async function* processYuanqiStream(stream, context) {
117
149
  toolCallId,
118
150
  toolCallName: toolCall.function.name
119
151
  };
152
+ if (toolCall.function.arguments) {
153
+ yield {
154
+ type: EventType.TOOL_CALL_ARGS,
155
+ threadId,
156
+ runId,
157
+ toolCallId,
158
+ delta: toolCall.function.arguments
159
+ };
160
+ }
120
161
  state.toolCallsMap.set(toolCallId, {
121
162
  name: toolCall.function.name,
122
163
  args: toolCall.function.arguments || ""
@@ -145,6 +186,20 @@ async function* processYuanqiStream(stream, context) {
145
186
  messageId
146
187
  };
147
188
  }
189
+ if (reasoningState.hasStarted) {
190
+ yield {
191
+ type: EventType.THINKING_TEXT_MESSAGE_END,
192
+ threadId,
193
+ runId,
194
+ messageId
195
+ };
196
+ yield {
197
+ type: EventType.THINKING_END,
198
+ threadId,
199
+ runId,
200
+ messageId
201
+ };
202
+ }
148
203
  for (const [toolCallId] of state.toolCallsMap) {
149
204
  yield {
150
205
  type: EventType.TOOL_CALL_END,
@@ -153,24 +208,217 @@ async function* processYuanqiStream(stream, context) {
153
208
  toolCallId
154
209
  };
155
210
  }
156
- yield {
157
- type: EventType.RUN_FINISHED,
158
- threadId,
159
- runId
160
- };
161
211
  }
162
212
 
163
213
  // src/agent.ts
164
214
  import { Observable } from "rxjs";
215
+
216
+ // src/constant.ts
217
+ var CHAT_HISTORY_DATA_SOURCE = "ai_bot_chat_history_5hobd2b";
218
+
219
+ // src/chat_history.ts
220
+ function genRecordId() {
221
+ return "record-" + genRandomStr(8);
222
+ }
223
+ async function createChatHistory({
224
+ tcbClient,
225
+ chatHistoryEntity
226
+ }) {
227
+ try {
228
+ const recordId = chatHistoryEntity.recordId || genRecordId();
229
+ const data = {
230
+ record_id: recordId,
231
+ bot_id: chatHistoryEntity.botId,
232
+ role: chatHistoryEntity.role,
233
+ content: chatHistoryEntity.content,
234
+ sender: chatHistoryEntity.sender,
235
+ conversation: chatHistoryEntity.conversation,
236
+ type: chatHistoryEntity.type,
237
+ image: chatHistoryEntity.image,
238
+ trigger_src: chatHistoryEntity.triggerSrc,
239
+ origin_msg: chatHistoryEntity.originMsg,
240
+ reply_to: chatHistoryEntity.replyTo,
241
+ reply: chatHistoryEntity.reply,
242
+ trace_id: chatHistoryEntity.traceId,
243
+ need_async_reply: chatHistoryEntity.needAsyncReply,
244
+ async_reply: chatHistoryEntity.asyncReply,
245
+ createdAt: Date.now(),
246
+ updatedAt: Date.now()
247
+ };
248
+ const db = tcbClient.database();
249
+ const collection = db.collection(CHAT_HISTORY_DATA_SOURCE);
250
+ const result = await collection.add(data);
251
+ return recordId;
252
+ } catch (error) {
253
+ console.error("Failed to create chat history record, error:", error);
254
+ return void 0;
255
+ }
256
+ }
257
+ async function updateChatHistoryByRecordId({
258
+ tcbClient,
259
+ recordId,
260
+ chatHistoryEntity
261
+ }) {
262
+ try {
263
+ const db = tcbClient.database();
264
+ const _ = db.command;
265
+ const collection = db.collection(CHAT_HISTORY_DATA_SOURCE);
266
+ const result = await collection.where({ record_id: _.eq(recordId) }).update({
267
+ content: chatHistoryEntity.content,
268
+ image: chatHistoryEntity.image,
269
+ async_reply: chatHistoryEntity.asyncReply,
270
+ recommend_questions: chatHistoryEntity.recommendQuestions,
271
+ status: chatHistoryEntity.status,
272
+ origin_msg: chatHistoryEntity.originMsg,
273
+ updatedAt: Date.now()
274
+ });
275
+ return chatHistoryEntity.recordId;
276
+ } catch (error) {
277
+ console.error("Failed to update chat history, error:", error);
278
+ return void 0;
279
+ }
280
+ }
281
+ async function describeChatHistory({
282
+ tcbClient,
283
+ botId,
284
+ sort,
285
+ pageSize = 10,
286
+ pageNumber = 1,
287
+ conversation,
288
+ startCreatedAt,
289
+ triggerSrc
290
+ }) {
291
+ if (!sort || sort.length === 0) {
292
+ sort = "desc";
293
+ }
294
+ try {
295
+ const db = tcbClient.database();
296
+ const _ = db.command;
297
+ const collection = db.collection(CHAT_HISTORY_DATA_SOURCE);
298
+ const whereConditions = {
299
+ bot_id: _.eq(botId)
300
+ };
301
+ if (conversation) {
302
+ whereConditions.conversation = _.eq(conversation);
303
+ }
304
+ if (startCreatedAt !== void 0) {
305
+ whereConditions.createdAt = _.gt(startCreatedAt);
306
+ }
307
+ if (triggerSrc) {
308
+ whereConditions.trigger_src = _.eq(triggerSrc);
309
+ }
310
+ const skip = (pageNumber - 1) * pageSize;
311
+ const result = await collection.where(whereConditions).orderBy("createdAt", sort).skip(skip).limit(pageSize).get();
312
+ const countResult = await collection.where(whereConditions).count();
313
+ const total = countResult.total || 0;
314
+ const records = result?.data || [];
315
+ const entityList = records.map(
316
+ (item) => transDataToChatEntity(item)
317
+ );
318
+ return [entityList, total];
319
+ } catch (error) {
320
+ console.error("Failed to query chat history, error:", error);
321
+ return [[], 0];
322
+ }
323
+ }
324
+ function transDataToChatEntity(item) {
325
+ if (!item) {
326
+ return new ChatHistoryEntity();
327
+ }
328
+ const chatEntity = new ChatHistoryEntity();
329
+ chatEntity.botId = item.bot_id;
330
+ chatEntity.recordId = item.record_id;
331
+ chatEntity.role = item.role;
332
+ chatEntity.status = item.status;
333
+ chatEntity.content = item.content;
334
+ chatEntity.sender = item.sender;
335
+ chatEntity.conversation = item.conversation;
336
+ chatEntity.type = item.type;
337
+ chatEntity.triggerSrc = item.trigger_src;
338
+ chatEntity.originMsg = item.origin_msg;
339
+ chatEntity.replyTo = item.reply_to;
340
+ chatEntity.reply = item.reply;
341
+ chatEntity.traceId = item.trace_id;
342
+ chatEntity.needAsyncReply = item.need_async_reply;
343
+ chatEntity.asyncReply = item.async_reply;
344
+ chatEntity.createdAt = item.createdAt;
345
+ chatEntity.updatedAt = item.updatedAt;
346
+ return chatEntity;
347
+ }
348
+ async function queryForLLM({
349
+ tcbClient,
350
+ botId,
351
+ pageSize = 10,
352
+ startCreatedAt,
353
+ triggerSrc
354
+ }) {
355
+ if (startCreatedAt === void 0) {
356
+ startCreatedAt = Date.now() - 24 * 60 * 60 * 1e3;
357
+ }
358
+ const recordEntityList = [];
359
+ const [recordList] = await describeChatHistory({
360
+ tcbClient,
361
+ botId,
362
+ sort: "desc",
363
+ pageSize,
364
+ startCreatedAt,
365
+ triggerSrc
366
+ });
367
+ recordEntityList.push(...recordList.reverse());
368
+ const entityMap = /* @__PURE__ */ new Map();
369
+ recordEntityList.filter((item) => {
370
+ if (item.needAsyncReply === true) {
371
+ return !!item.asyncReply;
372
+ } else {
373
+ return !!item.content;
374
+ }
375
+ }).forEach((item) => {
376
+ entityMap.set(item.recordId, item);
377
+ });
378
+ const result = [];
379
+ recordEntityList.forEach((item) => {
380
+ const { role, content, reply } = item;
381
+ if (role === "user" && content?.length !== 0) {
382
+ if (entityMap.has(reply)) {
383
+ result.push({ role, content });
384
+ result.push({
385
+ role: entityMap.get(reply).role,
386
+ content: entityMap.get(reply).content
387
+ });
388
+ }
389
+ }
390
+ });
391
+ if (result.length % 2 === 1) {
392
+ result.splice(-1, 1);
393
+ }
394
+ return result;
395
+ }
396
+ var ChatHistoryEntity = class {
397
+ };
398
+
399
+ // src/agent.ts
400
+ var YuanqiAgentError = class extends Error {
401
+ constructor(message, code) {
402
+ super(message);
403
+ this.name = "YuanqiAgentError";
404
+ if (code) this.code = code;
405
+ }
406
+ };
165
407
  var YuanqiAgent = class extends AbstractAgent {
166
408
  constructor(config) {
167
409
  super(config);
410
+ this.finalCloudCredential = {};
168
411
  this.yuanqiConfig = config.yuanqiConfig;
169
412
  this.model = new OpenAI({
170
413
  apiKey: "",
171
414
  baseURL: this.yuanqiConfig.request?.baseUrl || "https://yuanqi.tencent.com/openapi/v1/agent"
172
415
  });
173
416
  this.finalAppId = this.yuanqiConfig.appId || this.yuanqiConfig.request?.body?.assistantId || process.env.YUANQI_APP_ID || "";
417
+ this.finalCloudCredential = {
418
+ secretId: this.yuanqiConfig.credential?.secretId || process.env.TENCENTCLOUD_SECRETID,
419
+ secretKey: this.yuanqiConfig.credential?.secretKey || process.env.TENCENTCLOUD_SECRETKEY,
420
+ token: this.yuanqiConfig.credential?.token || process.env.TENCENTCLOUD_SESSIONTOKEN
421
+ };
174
422
  }
175
423
  generateRequestBody({
176
424
  messages,
@@ -194,27 +442,49 @@ var YuanqiAgent = class extends AbstractAgent {
194
442
  }
195
443
  async _run(subscriber, input) {
196
444
  try {
197
- const { messages, runId, threadId: _threadId, forwardedProps } = input;
198
- const threadId = _threadId || randomUUID();
445
+ const { messages, runId } = input;
446
+ const openai = this.model;
447
+ const threadId = input.threadId || randomUUID();
199
448
  subscriber.next({
200
449
  type: EventType2.RUN_STARTED,
201
450
  threadId,
202
451
  runId
203
452
  });
204
453
  if (!this.finalAppId) {
205
- throw new Error(
206
- "YUANQI_APP_ID is required, check your env variables or config passed with the adapter"
454
+ throw new YuanqiAgentError(
455
+ "YUANQI_APP_ID is required, check your env variables or config passed with the adapter",
456
+ "MISSING_YUANQI_APP_ID"
207
457
  );
208
458
  }
209
459
  if (!this.yuanqiConfig.appKey && !process.env.YUANQI_APP_KEY) {
210
- throw new Error(
211
- "YUANQI_APP_KEY is required, check your env variables or config passed with the adapter"
460
+ throw new YuanqiAgentError(
461
+ "YUANQI_APP_KEY is required, check your env variables or config passed with the adapter",
462
+ "MISSING_YUANQI_APP_KEY"
212
463
  );
213
464
  }
214
- const openai = this.model;
215
- const openaiMessages = convertMessagesToOpenAI(messages);
465
+ const trimmedCount = messages.length - 1;
466
+ if (trimmedCount > 0) {
467
+ subscriber.next({
468
+ type: EventType2.RAW,
469
+ rawEvent: {
470
+ message: `Yuanqi handles message history itself, so that a total of ${trimmedCount} messages before the last user message will be trimmed.`,
471
+ type: "warn"
472
+ }
473
+ });
474
+ }
475
+ const latestUserMessage = messages.filter((m) => m.role === "user").pop();
476
+ if (!latestUserMessage) {
477
+ throw new YuanqiAgentError(
478
+ "No user message found, please send a message first.",
479
+ "MESSAGE_FORMAT_ERROR"
480
+ );
481
+ }
482
+ const allMessages = await this.getChatHistory(
483
+ subscriber,
484
+ latestUserMessage
485
+ );
216
486
  const body = this.generateRequestBody({
217
- messages: openaiMessages,
487
+ messages: allMessages,
218
488
  input
219
489
  });
220
490
  const stream = await openai.chat.completions.create(
@@ -231,27 +501,192 @@ var YuanqiAgent = class extends AbstractAgent {
231
501
  }
232
502
  }
233
503
  );
234
- const messageId = `msg_${Date.now()}`;
235
- const context = { threadId, runId, messageId };
504
+ const userRecordId = `record-${randomUUID().slice(0, 8)}`;
505
+ const assistantRecordId = `record-${randomUUID().slice(0, 8)}`;
506
+ const context = { threadId, runId, messageId: userRecordId };
507
+ let fullAssistantContent = "";
236
508
  for await (const event of processYuanqiStream(stream, context)) {
237
509
  subscriber.next(event);
510
+ if (event.type === EventType2.TEXT_MESSAGE_CONTENT && event.delta) {
511
+ fullAssistantContent += event.delta;
512
+ }
238
513
  }
239
- } catch (error) {
240
- if (error instanceof Error) {
241
- console.error(`Error ${error.name}: ${error.message}`);
242
- } else {
243
- console.error(JSON.stringify(error));
514
+ const userContent = typeof latestUserMessage?.content === "string" ? latestUserMessage.content : latestUserMessage?.content?.filter((c) => c.type === "text").map((c) => c.text).join("") || "";
515
+ await this.saveChatHistory(
516
+ subscriber,
517
+ input,
518
+ userRecordId,
519
+ assistantRecordId,
520
+ userContent,
521
+ fullAssistantContent
522
+ );
523
+ subscriber.next({
524
+ type: EventType2.RUN_FINISHED,
525
+ threadId,
526
+ runId
527
+ });
528
+ } catch (e) {
529
+ console.error("[ERROR] Uncaught error: ", JSON.stringify(e));
530
+ let code = "UNKNOWN_ERROR";
531
+ let message = JSON.stringify(e);
532
+ if (e instanceof YuanqiAgentError) {
533
+ code = e.code || "AGENT_ERROR";
534
+ message = e.message;
535
+ } else if (e instanceof Error) {
536
+ code = e.name || "ERROR";
537
+ message = e.message;
244
538
  }
245
539
  subscriber.next({
246
540
  type: EventType2.RUN_ERROR,
247
- message: error instanceof Error ? error.message : String(error),
248
- code: error instanceof Error ? error.name : "UNKNOWN_ERROR"
541
+ code,
542
+ message: `Sorry, an error occurred while running the agent: Error code ${code}, ${message}`
249
543
  });
250
544
  } finally {
251
545
  subscriber.complete();
252
546
  }
253
547
  }
548
+ // Can be override by subclasses
549
+ async getChatHistory(subscriber, latestUserMessage) {
550
+ const botId = `bot-yuanqi-${this.finalAppId}`;
551
+ const tcbClient = this.getTcbClient();
552
+ const isDBReady = await this.checkIsDatabaseReady();
553
+ if (!isDBReady) {
554
+ subscriber.next({
555
+ type: EventType2.RAW,
556
+ rawEvent: {
557
+ message: `Chat history database is not ready, skip history loading.`,
558
+ type: "warn"
559
+ }
560
+ });
561
+ return convertMessagesToOpenAI([latestUserMessage]);
562
+ }
563
+ let historyMessages = [];
564
+ const historyCount = this.yuanqiConfig.historyCount ?? 10;
565
+ const historyRecords = await queryForLLM({
566
+ tcbClient,
567
+ botId,
568
+ pageSize: historyCount
569
+ });
570
+ historyMessages = historyRecords.map((record) => ({
571
+ role: record.role,
572
+ content: [{ type: "text", text: record.content }]
573
+ }));
574
+ const allMessages = historyMessages.concat(
575
+ convertMessagesToOpenAI([latestUserMessage])
576
+ );
577
+ return allMessages;
578
+ }
579
+ // Can be override by subclasses
580
+ async saveChatHistory(subscriber, input, userRecordId, assistantRecordId, userContent, assistantContent) {
581
+ const botId = `bot-yuanqi-${this.finalAppId}`;
582
+ const { threadId, runId } = input;
583
+ const tcbClient = this.getTcbClient();
584
+ const isDBReady = await this.checkIsDatabaseReady();
585
+ if (!isDBReady) {
586
+ subscriber.next({
587
+ type: EventType2.RAW,
588
+ rawEvent: {
589
+ message: `Chat history database is not ready, skip history saving.`,
590
+ type: "warn"
591
+ }
592
+ });
593
+ return;
594
+ }
595
+ const userEntity = new ChatHistoryEntity();
596
+ userEntity.recordId = userRecordId;
597
+ userEntity.botId = botId;
598
+ userEntity.role = "user";
599
+ userEntity.content = userContent;
600
+ userEntity.conversation = threadId;
601
+ userEntity.reply = assistantRecordId;
602
+ userEntity.triggerSrc = "";
603
+ userEntity.traceId = randomUUID();
604
+ await createChatHistory({ tcbClient, chatHistoryEntity: userEntity });
605
+ const assistantEntity = new ChatHistoryEntity();
606
+ assistantEntity.recordId = assistantRecordId;
607
+ assistantEntity.botId = botId;
608
+ assistantEntity.role = "assistant";
609
+ assistantEntity.content = assistantContent;
610
+ assistantEntity.conversation = threadId;
611
+ assistantEntity.replyTo = userRecordId;
612
+ assistantEntity.triggerSrc = "";
613
+ assistantEntity.traceId = runId;
614
+ assistantEntity.status = "done";
615
+ await createChatHistory({
616
+ tcbClient,
617
+ chatHistoryEntity: assistantEntity
618
+ });
619
+ }
620
+ getTcbClient() {
621
+ const envId = this.yuanqiConfig.envId || getCloudbaseEnvId();
622
+ const tcbClient = tcb.init({
623
+ env: envId,
624
+ secretId: this.finalCloudCredential.secretId,
625
+ secretKey: this.finalCloudCredential.secretKey,
626
+ sessionToken: this.finalCloudCredential.token
627
+ });
628
+ return tcbClient;
629
+ }
630
+ async checkIsDatabaseReady() {
631
+ try {
632
+ const envId = this.yuanqiConfig.envId || getCloudbaseEnvId();
633
+ if (!envId) {
634
+ throw new YuanqiAgentError(
635
+ "When saving chat history to CloudBase, CLOUDBASE_ENV_ID is required, check your env variables or config passed with the adapter",
636
+ "MISSING_CLOUDBASE_ENV_ID"
637
+ );
638
+ }
639
+ if (!this.finalCloudCredential.token) {
640
+ if (!this.finalCloudCredential.secretId) {
641
+ throw new YuanqiAgentError(
642
+ "When saving chat history to CloudBase, TENCENTCLOUD_SECRETID is required, check your env variables or config passed with the adapter",
643
+ "MISSING_SECRET_ID"
644
+ );
645
+ }
646
+ if (!this.finalCloudCredential.secretKey) {
647
+ throw new YuanqiAgentError(
648
+ "When saving chat history to CloudBase, TENCENTCLOUD_SECRETKEY is required, check your env variables or config passed with the adapter",
649
+ "MISSING_SECRET_KEY"
650
+ );
651
+ }
652
+ }
653
+ const managedTcbClient = managedTcb.init({
654
+ envId,
655
+ secretId: this.finalCloudCredential.secretId,
656
+ secretKey: this.finalCloudCredential.secretKey,
657
+ token: this.finalCloudCredential.token
658
+ });
659
+ const checkDBRes = await managedTcbClient.database.checkCollectionExists(
660
+ CHAT_HISTORY_DATA_SOURCE
661
+ );
662
+ if (checkDBRes && checkDBRes.Exists) {
663
+ return true;
664
+ } else if (checkDBRes && !checkDBRes.Exists) {
665
+ await managedTcbClient.database.createCollection(
666
+ CHAT_HISTORY_DATA_SOURCE
667
+ );
668
+ return true;
669
+ } else {
670
+ throw new Error("Check database exists failed");
671
+ }
672
+ } catch (dbError) {
673
+ console.error(
674
+ "[ERROR] Failed to check/create chat history collection:",
675
+ JSON.stringify(dbError)
676
+ );
677
+ return false;
678
+ }
679
+ }
254
680
  };
681
+ function getCloudbaseEnvId() {
682
+ if (process.env.CBR_ENV_ID) {
683
+ return process.env.CBR_ENV_ID;
684
+ } else if (process.env.SCF_NAMESPACE) {
685
+ return process.env.SCF_NAMESPACE;
686
+ } else {
687
+ return process.env.CLOUDBASE_ENV_ID || "";
688
+ }
689
+ }
255
690
  function convertMessagesToOpenAI(messages, systemPrompt) {
256
691
  const openaiMessages = [];
257
692
  if (systemPrompt) {
@@ -299,8 +734,15 @@ function convertMessagesToOpenAI(messages, systemPrompt) {
299
734
  return openaiMessages;
300
735
  }
301
736
  export {
737
+ ChatHistoryEntity,
302
738
  YuanqiAgent,
303
- camelToSnakeKeys,
304
- convertMessagesToOpenAI
739
+ YuanqiAgentError,
740
+ convertMessagesToOpenAI,
741
+ createChatHistory,
742
+ describeChatHistory,
743
+ processYuanqiStream,
744
+ queryForLLM,
745
+ transDataToChatEntity,
746
+ updateChatHistoryByRecordId
305
747
  };
306
748
  //# sourceMappingURL=index.mjs.map