aichatmaster
Version:
AI Chat Robot
410 lines (400 loc) • 18.4 kB
JavaScript
/**
* 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;