UNPKG

aichatmaster

Version:

AI Chat Robot

410 lines (400 loc) 18.4 kB
/** * ChatGpt 封装 */ const { Configuration, OpenAIApi } =require("openai"); const Event = require('events'); const SECTION_LENGTH = 256; ///每256个字符分成一组 const MESSAGE_LENGTH = 8; ///每次送8句话给openai 进行解析,送多了,会报错 //请将答案放在最后,标记为答案:() const QUESTION_TEXT_MAPPING = { singlechoice:',根据以上内容,生成1道单选题,每道题目4个选项,请按照{"question":"","choice":[],"answer":[]}的JSON结构输出,choice中的元素用大写字母ABCD开头,answer数组中包含一个正确答案', multiplechoice: ',根据以上内容,请生成1道多选题,提供4个选项,答案至少1个以上选项,请按照{"question":"","choice":[],"answer":[]}的JSON结构输出,choice中的元素用大写字母ABCD开头,answer数组中包含正确答案选项', //请将答案放在最后,标记为答案:() trueorfalse: ',根据以上内容,请生成1道判断题,请按照{"question":"","choice":["A.正确","B.错误"],"answer":[]}的JSON结构输出,answer数组中包含一个元素,"正确"或"错误"', //标记为答案:(正确或错误) completion: ',根据以上内容,请生成1道填空题,每道题目1个填空,请按照{"question":"","answer":[]}的JSON结构输出,answer数组中包含填空答案' //请将答案放在最后,标记为答案:() } const QUESTION_TYPE = ['singlechoice', 'multiplechoice', 'trueorfalse','completion'] class AIChat extends Event.EventEmitter { /** * * @param {*} apiKey 调用ChatGpt的key */ constructor(apiKey) { super(); // this.configuration = ; ///初始化好聊天实例 this.chatRobot = new OpenAIApi(new Configuration({ apiKey })); } /** * 对话聊天的文本模型 */ get chatModel(){ return this._chatmodel || 'gpt-3.5-turbo';////'text-davinci-003' } set chatModel(modelname) { return this._chatmodel = modelname; } /** * 对话返回的最大令牌数 */ get maxToken() { return this._maxToken || 150; } set maxToken(token) { return this._maxToken = token; } /** * 回复的温度精准度 */ get temperature() { return this._temperature || 0.9; } set temperature(tmp) { if (isNaN(tmp)) this._temperature = 0.9; else if (tmp>1 || tmp<0) this._temperature = 0.9; else this._temperature = tmp; } /** * 机器回复的答案数量 */ get replyCounts(){ return this._replycount || 1; } set replyCounts(cnt) { if (isNaN(tmp)) this._replycount=1; else this._replycount = cnt; } /** * 获得一个文字聊天的回复 * @param {*} chatText */ async chatTextResponse(chatText,axiosOption){ if (!chatText) return {successed:false,errcode:2,errmsg:'缺失聊天的内容'} if (!this.chatRobot) return { successed: false, errcode: 1, errmsg: '聊天机器人无效' } const response = await this.chatRobot.createChatCompletion({ model: this.chatModel, messages: chatText, temperature: this.temperature, max_tokens: this.maxToken // n: this.replyCounts }, axiosOption); //console.log('response', { model: this.chatModel, prompt: chatText, temperature: this.temperature, max_tokens: this.maxToken }, response) if (response.data.error){ console.log('response.data.error', response.data) return { successed: false, error: response.data.error }; } return { successed: true, message: response.data.choices[0].message } } /** * 判断一句话的表达情绪 * @param {*} s1 * @param {*} axiosOption */ async getScentenseEmotional(s1, axiosOption = { timeout: 30000 }){ if (!s1 ) return { successed: false, errcode: 2, errmsg: '缺失参数' } try { const emotion = ['愤怒', '威胁', '讽刺', '愧疚', '兴奋', '友好', '友好', '消极', '生气', '正常']; const messages = [ { role: 'user', content: s1 }, { role: 'user', content: `请分析上述内容的语言情绪,请从"${emotion.join(',')}"这些情绪中对应一个输出` }, ] const param = { model: this.chatModel, messages, temperature: this.temperature, max_tokens: 100, n: 1 } const response = await this.chatRobot.createChatCompletion(param, axiosOption); if (response.data.error) { return { successed: false, error: response.data.error }; } let value = response.data.choices[0].message.content.trim(); for (const word of emotion){ if (value.indexOf(word) >= 0) return { successed: true, value: word }; } return { successed: true, value:'不知道' }; } catch (err) { return { successed: false, error: err }; } } /** * 获取两句话的相似度取值 * @param {*} s1 * @param {*} s2 */ async getScentenseSimilarity(s1, s2, axiosOption= {timeout:30000}){ if (!s1 || !s2) return { successed: false, errcode: 2, errmsg: '缺失参数' } try { const messages = [ { role: 'user', content: s1 }, { role: 'user', content: s2 }, { role: 'user', content: '请从语义上对比以上两句话的相似度,请仅输出0至100之间的整数对比结果即可' }, ] const param = { model: this.chatModel, messages, temperature: this.temperature, max_tokens:255, n: 1 } const response = await this.chatRobot.createChatCompletion(param, axiosOption); if (response.data.error) { return { successed: false, error: response.data.error }; } let value = response.data.choices[0].message.content.replace(/[^\d]/g, "") if (value > 100) value = Math.floor(value / 10); return { successed: true, value: value }; } catch (err) { return { successed: false, error: err }; } } /** * 获得一种内容的相似说法 * 比如: * 你今年多大? * 相似问法:您是哪一年出生的 * 您今年贵庚? * @param {*} content * @param {需要出来的数量} count */ async getSimilarityContent(content, count = 1, axiosOption={}){ const text = `请提供一句${content}的相似说法` let result = await this.generateChatContent(text, count, axiosOption); if (!result.successed) return result; let replys = result.message.map(item => { return item.message.content.trim(); }) return { successed: true, message: replys } } /** * 从指定的文本内容中生成一张试卷 * @param {*} content * @param {试卷的参数} paperOption * totalscore: 试卷总分,默认100 * section: {type:[0,1,2,3]为单选、多选、判断、填空题型 count:生成多少道 score:本段分数} * @param {*} axiosOption * @returns *///并在答案末尾处必须给出答案内容中的关键词 async generateExaminationPaperFromContent(content, paperOption, axiosOption = {}){ let arrContent = this.splitLongText(content); let sectionCount ={ singlechoice : (paperOption.singlechoice?.count || 0) / arrContent.length, multiplechoice: (paperOption.multiplechoice?.count || 0) / arrContent.length, trueorfalse: (paperOption.trueorfalse?.count || 0) / arrContent.length, completion: (paperOption.completion?.count || 0) / arrContent.length } ; ///剩余待生成的题目数量 let remainCount = { singlechoice: paperOption.singlechoice?.count || 0, multiplechoice: paperOption.multiplechoice?.count || 0, trueorfalse: paperOption.trueorfalse?.count || 0, completion: paperOption.completion?.count || 0 }; ///每种类型的题目的分数 let ITEM_SCORE = { singlechoice: paperOption.singlechoice?.score || 0, multiplechoice: paperOption.multiplechoice?.score || 0, trueorfalse: paperOption.trueorfalse?.score || 0, completion: paperOption.completion?.score || 0 }; ///最后生成出来的结果 let paperReturned = { singlechoice: [], multiplechoice: [], trueorfalse: [], completion:[] }, noMoreQuestionRetrive = false,totalscore=0; while (arrContent.length > 0 && !noMoreQuestionRetrive) { ////每次最多送MESSAGE_LENGTH句话给openai let subarray = arrContent.slice(0, MESSAGE_LENGTH); /** * 每种类型的题目进行遍历 */ noMoreQuestionRetrive = true; for (const key of QUESTION_TYPE){ ///还需要抓取题目 if (remainCount[key]>0){ noMoreQuestionRetrive = false; subarray.push({ role: 'user', content: QUESTION_TEXT_MAPPING[key] }) let itemCount = Math.min(remainCount[key],Math.ceil(subarray.length * sectionCount[key])); console.log(QUESTION_TEXT_MAPPING[key], itemCount); let result = await this.generateChatContent(subarray, itemCount, axiosOption); if (result.successed){ //console.log('paper result', key, result.message.length) let pickedQuestions = this.pickUpQuestions(result.message, key, ITEM_SCORE[key]) ; if (pickedQuestions.length){ ///对外发送检出题目的信号 this.emit('parseout', { type: 'question', name: key, items:pickedQuestions}) paperReturned[key] = paperReturned[key].concat(pickedQuestions); remainCount[key] = remainCount[key] - pickedQuestions.length; totalscore = totalscore + pickedQuestions.length * ITEM_SCORE[key]; } } subarray.splice(subarray.length-1,1 ); ///把最后的问法删除 } } ////删除已经处理的文本 arrContent.splice(0, MESSAGE_LENGTH); } ///发出信号,解析完毕 this.emit('parseover', { type: 'question', items: paperReturned }) return { successed: true, message: { score: parseInt(totalscore),paper:paperReturned }} } /** * 从答复中得到题目 * @param {*} message */ pickUpQuestions(result,questiontype,score=1){ let item =result.map(m=>{ ////防止输出的JSON格式不合法 try{ let jsonObj = JSON.parse(m.message.content) jsonObj.score = score; if (jsonObj.choice && Array.isArray(jsonObj.choice) && questiontype != 'completion') { jsonObj.fullanswer = (jsonObj.answer + '').replace(/,|[^ABCDE]/g, ''); jsonObj.choice = jsonObj.choice.map((item, index) => { let seqNo = String.fromCharCode(65 + index); let correctReg = new RegExp(`${seqNo}.|${seqNo}`,'ig') //let answer = jsonObj.fullanswer return { id: seqNo, content: item.replace(correctReg, '').trim(), iscorrect: (jsonObj.fullanswer.indexOf(seqNo) >= 0 || jsonObj.fullanswer.indexOf(m)) >= 0 ? 1 : 0 } }) } switch (questiontype) { case 'singlechoice': jsonObj.answer = (jsonObj.answer + '').replace(/,|[^ABCDEFG]/g, '').split('').slice(0,1); break; case 'multiplechoice': jsonObj.answer = (jsonObj.answer + '').replace(/,|[^ABCDEFG]/g, '').split(''); break; case 'trueorfalse': jsonObj.answer = [(jsonObj.answer + '').indexOf('正确') >= 0 ? 'A' : 'B'] break; } return jsonObj; }catch(err){ console.log('error happened:', err); return null; } }) return item.filter(i => { return i !=null;}); } /** * 从指定的文本内容中生成相关的问答 * @param {*} content * @param {*} count * @param {*} axiosOption * @returns *///并在答案末尾处必须给出答案内容中的关键词 async generateQuestionsFromContent(content, count = 1, axiosOption = {}) { let arrContent = this.splitLongText(content); ///没20句话分为一组,适应大文件内容多次请求组合结果 ///每一句话需要产生的题目 let questions4EverySentense = count / arrContent.length; //Math.ceil(arrContent.length / 20); let faqs = [],gotted=0; while (arrContent.length > 0 && gotted < count){ ////每次最多送MESSAGE_LENGTH句话给openai let subarray = arrContent.slice(0, MESSAGE_LENGTH); let itemCount = Math.min(Math.ceil(subarray.length * questions4EverySentense), count - gotted); //subarray.push({ role: 'user', content:'请根据上述内容,给出一道提问与答案以及答案关键词,按照先问题内容,再标准答案,再关键词的顺序输出,关键词之间用、分开'}) subarray.push({ role: 'user', content:'请根据上述内容,给出一道提问与答案以及答案关键词,按照{"question":"","answer":"","keywords":[]}的JSON结构输出,keywords数组中的关键词必须存在于答案内容中'}) // console.log('itemCount', subarray.length, itemCount) let result = await this.generateChatContent(subarray, itemCount, axiosOption); if (result.successed){ let msgs = this.pickUpFaqContent(result.message); if (msgs.length){ ///对外发送检出问答题的信号 this.emit('parseout', { type: 'qa', items: msgs }) gotted += msgs.length; //result.message.length; // console.log('gotted=', gotted) faqs = faqs.concat(msgs); } } ////删除已经处理的文本 arrContent.splice(0, MESSAGE_LENGTH); } arrContent = null; /// 释放内存 ///发出信号,解析完毕 this.emit('parseover', { type: 'qa', items: faqs }) return { successed: true, message: faqs }; } /** * 解析Faq返回的问题 * @param {*} messages * @returns */ pickUpFaqContent(messages){ let replys = messages.map(item => { let content = item.message.content.trim().replace(/\t|\n|\v|\r|\f/g, ''); try { let jsonObj = JSON.parse(content); if (!Array.isArray(jsonObj.keywords)) { jsonObj.keywords = (jsonObj.keywords || '').split(',') } return jsonObj; } catch (err) { console.log('JSON error', content, err) return null; } }) return replys.filter(n => { return n != null }) ; } /** * 通过ChatGPT生成内容 * @param {*} content * @param {*} count * @param {*} axiosOption * @returns */ async generateChatContent(content, count = 1, axiosOption = {}) { const messages = Array.isArray(content) ? content : this.splitLongText(content);// [{ role: 'user', content }]; try { const param = { model: this.chatModel, messages, temperature: this.temperature, max_tokens: this.maxToken, n: Number(count) } const response = await this.chatRobot.createChatCompletion(param,axiosOption); if (response.data.error) { return { successed: false, error: response.data.error }; } return { successed: true, message: response.data.choices }; } catch (err) { return { successed: false, error: err }; } } /** * 将一段很长的文本,按1024长度来划分到多个中 * @param {*} content */ splitLongText(content, len = SECTION_LENGTH ){ let start = 0, message = [], length = content.length ; while (start < length){ const subtext = content.substr(start, len); if (subtext) message.push({ role: 'user', content: subtext }) start += len; } return message; } /** * 根据聊天的提示文字返回图片信息 * @param {*} chatText 询问的文本 * @param {counts:返回的图片数量,size:图片大小} imgOption 图片参数 * @returns */ async chatImageRepsonse(chatText,imgOption={}){ if (!chatText) return { successed: false, errcode: 2, errmsg: '缺失聊天的内容' } if (!this.chatRobot) return { successed: false, errcode: 1, errmsg: '聊天机器人无效' } const response = await this.chatRobot.createImage({ prompt: chatText, n: imgOption.counts || this.replyCounts, size: imgOption.size || "1024x1024", }); if (response.data.error) { return { successed: false, error: response.data.error, json: response.toJSON() }; } return { successed: true, choices: response.data } } } exports = module.exports = AIChat;