doomiaichat
Version:
Doomisoft OpenAI
358 lines (353 loc) • 17.4 kB
text/typescript
/**
* 扣子智能体
*/
import {
CozeAPI,
COZE_CN_BASE_URL,
ChatEventType,
RoleType,
StreamChatReq,
EnterMessage,
// ChatWorkflowReq,
// CreateChatReq,
// ChatStatus,
VariableUpdateReq,
VariableRetrieveReq,
CreateChatReq,
RunWorkflowReq,
RunWorkflowData,
CreateChatData,
ChatStatus,
WorkflowEventType,
ChatWorkflowReq
} from '@coze/api';
import GptBase from "./gptbase"
import { CorzAuthorization } from './corzauthorization';
import { parse } from 'querystring'
import { ApiResult } from './declare';
// 定义深度思考的动作标签
// 这是小鹭里面专用的几个思考动作标签
const DeepThinkingAction: any = {
thinking: { start: '<xiaolu-think>', end: '</xiaolu-think>', tag: 'xiaolu-think' },
reasoning: { start: '<xiaolu-reason>', end: '</xiaolu-reason>', tag: 'xiaolu-reason' },
card: { start: '<xiaolu-card>', end: '</xiaolu-card>', tag: 'xiaolu-card' },
}
// 定义深度思考的状态
enum DeepThinkingStatus {
None,
Thinking,
// ContentOutput,
ThinkingOver
}
// 智能体思考时输出的动作
type CardType = { type: number, tag: string }
// type TThinkingMessage = { action: string, textposition: number }
export default class CorzBot extends GptBase {
private botid: string | null = null; // 智能体id
private workflowid: string | null = null; // 工作流id
private talkflowid: string | null = null; // 对话流id
private apiKey: string;
/**
*
* @param apikey 调用AI中台 的key
* @param botid 智能体信息
*/
constructor(private authorizationProvider: CorzAuthorization, private setting: any = {}) {
super();
////初始化扣子客户端
if (this.setting.workflowid)
this.workflowid = this.setting.workflowid;
else if (this.setting.talkflowid)
this.talkflowid = this.setting.talkflowid;
else if (this.setting.botid || this.setting.botID)
this.botid = this.setting.botid || this.setting.botID;
else
throw new Error('no botid or talkflowid or workflowid setting for coze');
this.apiKey = this.setting['apiKey'];
}
private async createClient(): Promise<CozeAPI> {
// 存在AppKey则使用AppKey进行认证
if (this.apiKey) return new CozeAPI({ baseURL: COZE_CN_BASE_URL, token: this.apiKey });
const accessToken = await this.authorizationProvider.getAccessToken();
if (!accessToken) throw new Error('get access token failed');
return new CozeAPI({ baseURL: COZE_CN_BASE_URL, token: accessToken });
}
/**
* 发起一次会话
*/
override async createCoversation(client?: CozeAPI): Promise<string | null> {
try {
const czApi = client ?? await this.createClient();
const params = this.botid ? { bot_id: this.botid } : {};
const result = await czApi.conversations.create(params)
return result.id;
} catch (error) {
console.error('createCoversation error in coze api');
return null;
}
}
/**
* 设置Coze的变量
* @param params
* @returns
*/
override async setVariables(params: any): Promise<ApiResult> {
const client = await this.createClient();
const cozeParams: VariableUpdateReq = Object.assign({}, this.botid ? { bot_id: this.botid } : {}, params);
client.variables.update(cozeParams)
return { successed: true };
}
/**
* 获取设置的变量
* @returns
*/
override async getVariables(params: any): Promise<any> {
const client = await this.createClient();
const cozeParams: VariableRetrieveReq = Object.assign({}, { bot_id: this.botid }, params);
const data = await client.variables.retrieve(cozeParams);
if (data) return { successed: true, ...data }
return { successed: false, error: 'get variables failed' };
}
/**
* 组装请求参数
* @param message
* @param callChatOption
* @param attach
* @param _axiosOption
*/
async getRequestStream<T>(client: CozeAPI, message: EnterMessage[], callChatOption: any = {}): Promise<T> {
//简单的对话请求
const conversation_id = callChatOption.session_id ?? await this.createCoversation(client);
if (this.botid) {
const req: StreamChatReq = {
bot_id: this.botid,
additional_messages: message,
user_id: callChatOption.userid || callChatOption.cozeUserID,
conversation_id,
parameters: Object.assign({ request_src:1 }, callChatOption.parameters || {}),
}
req.custom_variables = Object.assign({}, this.setting.customVariables || {}, callChatOption.customVariables || {});
return req as T;
}
if (this.workflowid){
const worflowreq: RunWorkflowReq = {
ext: callChatOption.ext,
workflow_id: this.workflowid,//callChatOption.workflowid,
is_async: false,
// parameters: Object.assign({ input: message[0]?.content }, this.setting.customVariables || {}, callChatOption.customVariables || {}),
parameters: Object.assign({ request_src :1},callChatOption.parameters || {}, { input: message[0]?.content})
}
return worflowreq as T;
}
const worflowreq: ChatWorkflowReq = {
additional_messages: message,
ext: callChatOption.ext,
workflow_id: this.talkflowid!,//callChatOption.workflowid,
conversation_id,
parameters: Object.assign({request_src:1},callChatOption.parameters || {}) //Object.assign({}, this.setting.customVariables || {}, callChatOption.customVariables || {}),
//parameters: { input: message[0]?.content }
}
return worflowreq as T;
}
/**
* 同步调用的问答请求需要获取会话详情
* @param chatid
* @param conversation_id
*/
private async getChatDetail(client: CozeAPI, conversation_id: string, chatid: string) {
const maxWaitTime = 120000; // 最大等待120秒
const startTime = Date.now();
while ((Date.now() - startTime) < maxWaitTime) {
const chatinfo = await client.chat.retrieve(conversation_id, chatid);
// 状态已经是完成了,可以去获取对话的内容了
if (chatinfo.status === ChatStatus.COMPLETED) return { usage: chatinfo.usage, messages: await client.chat.messages.list(conversation_id, chatid) };
await new Promise(resolve => setTimeout(resolve, 1500)); // 等待1500ms
}
return null;
}
/**
* 非流式传输聊天请求
* @param chatText
* @param callChatOption
* @param _axiosOption
*/
public async chatRequest(message: any[] | string, callChatOption: any = {}, _axiosOption: any = {}): Promise<any> {
if (!message) this.emit('chaterror', { successed: false, error: 'no message in chat' });
///如果是字符,则组装成API需要的消息
if (typeof message === 'string') message = [
{
role: RoleType.User,
content_type: 'text',
content: message
}
];
const client = await this.createClient();
////如果参数中用的是Workflow,则调用对话流来输出结果
////否则用智能体的对话来输出结果
const params = await this.getRequestStream(client, message, callChatOption);
const response = this.botid ? await client.chat.create(params as CreateChatReq) :await client.workflows.runs.create(params as RunWorkflowReq);
if (this.workflowid && (response as RunWorkflowData).msg === 'Success') {
const resp = (response as RunWorkflowData).data;
return { successed: true, message: [{ role: 'assistant', type: 'answer', content: resp }] };
// try {
// return { successed: true, message: [{ role: 'assistant', type: 'answer', content: JSON.parse(resp).data }] };
// } catch (error) {
// return { successed: true, message: [{ role: 'assistant', type: 'answer', content: resp }] };
// }
}
if (!this.workflowid && (response as CreateChatData).conversation_id && (response as CreateChatData).id) {
const ccd = response as CreateChatData;
const chatData = await this.getChatDetail(client, ccd.conversation_id, ccd.id);
if (chatData) {
const message = chatData.messages.filter(x => x.type === 'answer').map(item => ({
role: item.role,
type: item.type,
content: item.content,
}));
return { successed: true, message, usage: chatData.usage, session_id: ccd.conversation_id };
}
}
return { successed: false, error: { message: '聊天未完成' } };
}
/**
* 提取XML标签中间的内容,
* @param xmlStr
* @param tagName
* @returns
*/
extractXmlContent(xmlStr: string, tagName: string): string {
// 构建匹配标签的正则(支持任意空白字符和大小写)
const regex = new RegExp(`<\\s*${tagName}\\s*>([\\s\\S]*?)<\\s*\\/\\s*${tagName}\\s*>`, 'i');
const match = xmlStr.match(regex);
if (match && match[1]) return match[1].trim();
return ''; // 未找到标签或内容
}
/**
* 解析深度思考的JSON内容
* @param content
* @param status
* @returns
*/
private parseDeepThinkingJson(content: string) {
// const thinkingStartIndex = status === DeepThinkingStatus.Thinking ? content.indexOf("{\"process_msg") :
// (status===DeepThinkingStatus.ReasonOutput ? content.indexOf("{\"reasoning_content") : content.indexOf("{\"card_resource")) ;
const xmlTagLocation = [content.indexOf(DeepThinkingAction.thinking.start),
content.indexOf(DeepThinkingAction.reasoning.start),
content.indexOf(DeepThinkingAction.card.start)];
let minLocation = 10000, minIndex = -1;
// 找到最小的并且大于0 的位置的下标
xmlTagLocation.forEach((x, index) => {
if (x >= 0 && x < minLocation) {
minLocation = x;
minIndex = index;
}
});
// const tagLocation = xmlTagLocation.filter(x => x >= 0);
const thinkingStartIndex = minIndex >= 0 ? minLocation : -1; //tagLocation.length > 0 ? Math.min(...tagLocation) : -1;
if (thinkingStartIndex < 0) return { content, status: DeepThinkingStatus.ThinkingOver };
const currentAction = [DeepThinkingAction.thinking, DeepThinkingAction.reasoning, DeepThinkingAction.card][minIndex];
const currentActionIsOver = content.indexOf(currentAction.end, thinkingStartIndex);
const thinkingEndIndex = currentActionIsOver >= 0 ? currentActionIsOver: content.indexOf('</', thinkingStartIndex);
const thinkingContent = this.extractXmlContent(content.substring(thinkingStartIndex, thinkingEndIndex >= 0 ? thinkingEndIndex : undefined) + currentAction.end, currentAction.tag); //"\"}";
if (currentActionIsOver >= 0) content = content.substring(currentActionIsOver + currentAction.end.length);
return {
thinking: {
action: currentAction,
text: thinkingContent,//Object.values(thinkingObject)[0]
completed: currentActionIsOver >= 0
}, content};
}
/**
* 流式传输聊天请求
* @param chatText
* @param callChatOption
* @param attach
* @param _axiosOption
* @returns
*/
override async chatRequestInStream(message: any[] | string, callChatOption: any = {}, attach?: any, _axiosOption?: any): Promise<any> {
if (!message) this.emit('chaterror', { successed: false, error: 'no message in chat' });
///如果是字符,则组装成API需要的消息
if (typeof message === 'string') message = [
{
role: RoleType.User,
content_type: 'text',
content: message
}
];
const client = await this.createClient();
////如果参数中用的是Workflow,则调用对话流来输出结果
////否则用智能体的对话来输出结果
let requestid = Math.ceil(Math.random() * (new Date().getTime() * Math.random()) / 1000), index = 0;
const params = await this.getRequestStream(client, message, callChatOption);
const stream = this.botid ? client.chat.stream(params as StreamChatReq) :
(this.workflowid ? client.workflows.runs.stream(params as RunWorkflowReq) :
client.workflows.chat.stream(params as ChatWorkflowReq)) ;
let deltaindex = 0, fullanswer: string[] = [], followup: string[] = [], cardData: CardType[] = [];
let deepThinking = '', thinkingStatus = DeepThinkingStatus.None, cardResource = ''; // 是否在深度思考中
for await (const part of stream) {
if (part.event === ChatEventType.ERROR) return this.emit('chaterror', { successed: false, error: 'call failed' });
if (part.event === ChatEventType.CONVERSATION_MESSAGE_DELTA ||
part.event === WorkflowEventType.MESSAGE
) {
let { conversation_id, content_type, type = 'answer', role = 'assistant', content, reasoning_content: reasoning } = part.data as any;
if (!content&& reasoning) {
this.emit('chatthinking', { text: reasoning, completed: false, action: 'deep-thinking' });
continue;
}
// 如果存在深度思考,则一开始就会带有相关的关键信息
if (deltaindex === 0 && content.startsWith("<xiaolu-")) {
thinkingStatus = DeepThinkingStatus.Thinking;
deltaindex++;
}
// 如果在深度思考中,则不输出消息
if (thinkingStatus === DeepThinkingStatus.Thinking) {
deepThinking += content;
const result = this.parseDeepThinkingJson(deepThinking)
const thinking = result.thinking;
if (thinking) {
deepThinking = result.content;
if (thinking.action === DeepThinkingAction.card) {
cardResource += result.thinking.text;
// 卡片流结束,解析卡片资源数据
if (result.thinking.completed) {
const allCards = cardResource.replace(/[\x00-\x1F\x7F]/g, '').split('|')
for (const item of allCards) {
const cardinfo: any = parse(item);
if (cardinfo.type && cardinfo.tag) cardData.push({ ...cardinfo })
}
// 将卡片资源返回给客户端
this.emit('chatcard', cardData);
}
} else {
this.emit('chatthinking', { text: result.thinking.text, completed: result.thinking.completed, action: thinking.action?.tag });
}
}
if (result.status != DeepThinkingStatus.ThinkingOver) continue;
thinkingStatus = DeepThinkingStatus.ThinkingOver;
// 将排除了thinking之后的消息内容要带下去成为正式的输出内容
content = deepThinking;
}
fullanswer.push(content);
let output = { successed: true, type, content_type, role, requestid, segment: content, text: fullanswer.join(''), index: index++, session_id: conversation_id };
if (attach) output = Object.assign({}, output, attach);
this.emit('chattext', output)
}
////在流式传输中,提取相关推荐问题
if (part.event === ChatEventType.CONVERSATION_MESSAGE_COMPLETED) {
const { type, content } = part.data ?? {};
if (type === 'follow_up') followup.push(content);
}
///整个对话结束
if (part.event === ChatEventType.CONVERSATION_CHAT_COMPLETED ||
part.event === WorkflowEventType.DONE
) {
const { conversation_id, content } =( part.data )?? {} as any;
let output = { successed: true, cards: cardData.length ? cardData : null, followup, type: 'answer', content_type: 'text', role: RoleType.Assistant, requestid, segment: null, text: content??fullanswer.join(''), index: index++, session_id: conversation_id };
if (attach) output = Object.assign({}, output, attach);
this.emit('chatdone', output)
}
}
return { successed: true, requestid }
}
}