UNPKG

tmaiplugin

Version:

TrainingMaster AIGC Component

270 lines (262 loc) 16.6 kB
import { QuestionItem } from "./declare"; import { fixedJsonString,splitLongText } from "./util/stringutil"; import PluginBase from './aipluginbase' const ROLE_DEFINE:any = [ '你是一位精通各行业的技能考试专家,擅长根据文档内容提取重点要点并形成考核问题、相关选项及正确答案', '你是一位程序设计专家,特别擅长数据分析,内容组织并进行结构化设计,形成Json格式的数据' ] const QUESTION_TYPE: string[] = ['singlechoice', 'multiplechoice', 'trueorfalse', 'completion'] const PROMPT_LINK: any = { singlechoice:[ `根据以下内容提炼{{COUNT}}道单选题。要求如下: 1、问题需要与所给的资料相关,绝不能问超出所给资料的范围及自己捏造;所提问题需要准确、完整、清晰,绝不能有歧义;意思相近的问题不要重复给出; 2、每道题目4个选项,选项中有且仅有一个正确选项; 3、生成问题的时候,请出具有代表性的问题,对于一些无关紧要的问题可以忽略。特别注意资料中关于数字、参数、特点等关键信息的提取,在给出的问题中尽可能覆盖; 4、输出结果包括:题目内容、考题选项、答案; 5、如果无法从内容中提炼任何问题,仅需返回"NO"。 内容如下:""" {{CONTENT}} """`, `请将以下内容去除题目序号,按照[{"question":"","choice":[],"answer":[]}]的标准Json数组结构输出。 内容如下:""" {{CONTENT}} """`], multiplechoice: [ `根据以下内容提炼{{COUNT}}道多选题。要求如下: 1、问题需要与所给的资料相关,绝不能问超出所给资料的范围及自己捏造;所提问题需要准确、完整、清晰,绝不能有歧义;意思相近的问题不要重复给出; 2、每道题目4个选项,选项中至少两个或以上正确选项; 3、生成问题的时候,请出具有代表性的问题,对于一些无关紧要的问题可以忽略。特别注意资料中关于数字、参数、特点等关键信息的提取,在给出的问题中尽可能覆盖; 4、输出结果包括:题目内容、考题选项、答案; 5、如果无法从内容中提炼任何问题,仅需返回"NO"。 内容如下:""" {{CONTENT}} """`, `请将以下内容去除题目序号,按照[{"question":"","choice":[],"answer":[]}]的标准Json数组结构输出。 内容如下:""" {{CONTENT}} """`], // trueorfalse: [ `根据以下内容提炼{{COUNT}}道判断题。要求如下: 1、问题需要与所给的资料相关,绝不能问超出所给资料的范围及自己捏造;所提问题需要准确、完整、清晰,绝不能有歧义;意思相近的问题不要重复给出; 2、请出具有代表性的问题,对于一些无关紧要的问题可以忽略。特别注意资料中关于数字、参数、特点等关键信息的提取,在给出的问题中尽可能覆盖; 3、绝对不能在题目中包含答案,判断题答案选项固定为:"正确"、"错误"。 4、输出结果包括:题目内容、选项、答案; 5、如果无法从内容中提炼任何问题,仅需返回"NO"。 内容如下:""" {{CONTENT}} """`, `请将以下内容去除题目序号,按照[{"question":"","choice":["A.正确","B.错误"],"answer":[]}]的标准Json数组结构输出。 内容如下:""" {{CONTENT}} """`], completion: [ `根据以下内容提炼{{COUNT}}道填空题。要求如下: 1、问题需要与所给的资料相关,绝不能问超出所给资料的范围及自己捏造;所提问题需要准确、完整、清晰,绝不能有歧义;意思相近的问题不要重复给出; 2、每题至少一个或以上的填空,尽量将填空的内容放置于题目中间,并在内容中用“____”为每个填空预留标记; 3、如填空项不能预留在题目内容中,则在题目内容结尾处添加对应填空数量的“____”; 4、填空务必简短,每个空的内容长度绝对不超过10个中文字或3个英文单词; 5、生成问题的时候,请出具有代表性的问题,对于一些无关紧要的问题可以忽略。特别注意资料中关于数字、参数、特点等关键信息的提取,在给出的问题中尽可能覆盖; 6、输出结果包括:题目内容、填空答案; 7、如果无法从内容中提炼任何问题,仅需返回"NO"。 内容如下:""" {{CONTENT}} """`, `请将以下内容去除题目序号,按照[{"question":"","answer":["填空1","填空2"]}]的标准Json数组结构输出。 内容如下:""" {{CONTENT}} """`] }; /** * Faq问题的提取器插件 */ export class QuestionPlugin extends PluginBase { /** * 从指定的文本内容中生成相关的问答 * @param {*} content * @param {*} paper * @param {*} axios * @returns *///并在答案末尾处必须给出答案内容中的关键词 async execute(params: any):Promise<any> { let { content, paper: paperOption, sectionlength, axios} = params; sectionlength = sectionlength || 1024; if (!this.gptInstance) return {successed:false}; let arrContent = splitLongText(content, sectionlength); if (!arrContent.length) return { successed: false }; let sectionCount: any = { 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: any = { singlechoice: paperOption.singlechoice?.count || 0, multiplechoice: paperOption.multiplechoice?.count || 0, trueorfalse: paperOption.trueorfalse?.count || 0, completion: paperOption.completion?.count || 0 }; ///每种类型的题目的分数 let ITEM_SCORE: any = { singlechoice: paperOption.singlechoice?.score || 0, multiplechoice: paperOption.multiplechoice?.score || 0, trueorfalse: paperOption.trueorfalse?.score || 0, completion: paperOption.completion?.score || 0 }; ///最后生成出来的结果 let paperReturned: any = { singlechoice: [], multiplechoice: [], trueorfalse: [], completion: [] }, noMoreQuestionRetrive: boolean = false, totalscore: number = 0; while (arrContent.length > 0 && !noMoreQuestionRetrive) { /** * 每种类型的题目进行遍历 */ noMoreQuestionRetrive = true; for (const key of QUESTION_TYPE) { console.log('key', key) ///还需要抓取题目 if (remainCount[key] > 0) { noMoreQuestionRetrive = false; //let itemCount = Math.min(remainCount[key], Math.ceil(subarray.length * sectionCount[key])); let itemCount = Math.min(remainCount[key], Math.ceil(sectionCount[key])); let subarray: any[] = [ { role: 'system', content: ROLE_DEFINE[0] }, { role: 'user', content: PROMPT_LINK[key][0].replace('{{COUNT}}', itemCount).replace('{{CONTENT}}', arrContent.slice(0, 1)[0]) }, // { role: 'user', content:`出题材料:${arrContent.slice(0, 1)[0]}`} ] // subarray.unshift() console.log('subarray', subarray) let result:any = await this.gptInstance.chatRequest(subarray, { replyCounts: 1 }, axios); ///如果请求发生了网络错误(不是内容合规问题),则再重试一次,如果任然有错则放弃 if (!result.successed && result.error != 'content_filter') { console.log('network error,retry onemore time') result = await this.gptInstance.chatRequest(subarray, { replyCounts: 1 }, axios); } console.log('subarray returned', result.successed) if (result.successed && result.message) { let pickedQuestions = await this.pickUpQuestions(result.message, itemCount, 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]; } } } } ////删除已经处理的文本 arrContent.splice(0, 1); } console.log('parseover') ///发出信号,解析完毕 this.emit('parseover', { type: 'question', items: paperReturned }) return { successed: true, score: totalscore, paper: paperReturned } } /** * 从答复中得到题目 * @param {*} result * */ protected async pickUpQuestions(result: Array<any>, count: number, questiontype: string, score: number = 1): Promise<Array<QuestionItem>> { if (!this.gptInstance || !result[0]?.message?.content) return []; let answerString = result[0].message.content.trim().replace(/\t|\n|\v|\r|\f/g, ''); if (answerString.includes('NO')) return []; let orgJsonPrompt = [ { role: 'system', content: ROLE_DEFINE[1] }, { role: 'user', content: PROMPT_LINK[questiontype][1].replace('{{CONTENT}}', answerString) } ] console.log('orgJsonPrompt', orgJsonPrompt) let fixedJsonResult: any = await this.gptInstance.chatRequest(orgJsonPrompt, { replyCounts: 1 }, {}) if (fixedJsonResult.successed) { answerString = fixedJsonResult.message[0].message.content.trim().replace(/\t|\n|\v|\r|\f/g, ''); } let jsonObj = fixedJsonResult.successed ? fixedJsonString(answerString) : []; let returnItems: QuestionItem[] = []; const chineseReg = new RegExp("[\\u4E00-\\u9FFF]+", "g") try { returnItems = jsonObj.map((questionitem: any) => { console.log('answer item from jsonObj', questionitem); if (!questionitem.question) return null; ///避免内容最前面出现 题目1: questionitem.question = questionitem.question.trim().replace(/^题目\s*\d*\s*(:|:)*/, '').replace(/\s*(\(|()*(单选|多选|判断|填空)(\)|))*\s*$/, ''); if (questionitem.choice && Array.isArray(questionitem.choice) && questiontype != 'completion') { questionitem.fullanswer = (questionitem.answer + '').replace(/,|[^ABCDE]/g, ''); questionitem.score = score; if (questionitem.choice) { questionitem.choice = questionitem.choice.map((item: string, index: number) => { let seqNo = 'ABCDEFG'[index]; //String.fromCharCode(65 + index); let correctReg = new RegExp(`${seqNo}.|${seqNo}`, 'ig') return { id: seqNo, content: (item + '').replace(correctReg, '').trim().replace(/^选项\s*\d*\s*(:|:)*/, ''), iscorrect: (questionitem.fullanswer || '').indexOf(seqNo) >= 0 ? 1 : 0 } }) } ///如果是非判断题,题目的选项数量小于2 ,则无效 ///如果是判断题,题目的选项必须=2 if (!questionitem.choice || (questiontype != 'trueorfalse' && questionitem.choice.length < 3) || (questiontype == 'trueorfalse' && questionitem.choice.length != 2)) { return null; } } switch (questiontype) { case 'singlechoice': questionitem.answer = (questionitem.answer + '').replace(/,|[^ABCDEFG]/g, '').split('').slice(0, 1); break; case 'multiplechoice': questionitem.answer = Array.from(new Set((questionitem.answer + '').replace(/,|[^ABCDEFG]/g, '').split(''))).sort(); ////多选题选项少于2个的,也不要了 if (questionitem.answer.length<2) return null; break; case 'trueorfalse': if (questionitem.choice.length!=2) return null; ////选项必须是正确或错误,否者这道题不要了 if (!['正确', '错误'].includes(questionitem.choice[0].content)) return null; let rightItem = questionitem.choice.find((x: any) => { return x.iscorrect == 1 }); questionitem.answer = [rightItem?.id || 'Z']; //[(questionitem.answer + '').indexOf('正确') >= 0 ? 'A' : 'B'] ///防止题目内容中直接出现了答案内容 questionitem.question = questionitem.question.replace(/(答案){0,1}(:|:)(-){0,5}\s?(\(|(){0,1}\s{0,5}(正确|错误|正确\/错误|错误\/正确){0,1}(\)|)){0,1}(\/){0,1}(.|。|;|;){0,1}$/,'').trim().replace(/(,|,)$/,'。'); break; case 'completion': let verylong = questionitem.answer.filter((item:string)=>{ ///如果回答内容不包括英文,则填空题的长度不能超过15 if (chineseReg.test(item)) return item.length>=15 ////如果包含了英文,则不超过40个长度 return item.length >= 30 }); ////如果填空题的内容超长,这道题也不要 if (verylong.length>0) return null; ///如果填空题没有提空答案,不要 if (!questionitem.answer || questionitem.answer.length==0) return null; questionitem.answer = questionitem.answer.map((item:string)=>{ return item.replace(/([-"“”’',,。??、!!~*; ])/g,'') }) ////去除填空题尾部可能出来的杂项 questionitem.question = questionitem.question.replace(/(\(|().{0,1}(个){0,1}(\)|)){0,1}(.|。){0,1}$/, ''); break; } ///单选题验证 if (questiontype == 'singlechoice') { let rightAnswer = questionitem.choice ? questionitem.choice.filter((item: { iscorrect: number; }) => { return item.iscorrect === 1 }) : []; ///单选题的正确选项大于了1个 if (rightAnswer.length != 1 || !questionitem.answer || questionitem.answer.length !== 1) return null; ///正确选项和答案不一致 if (rightAnswer[0].id.toUpperCase() != (questionitem.answer[0] || '').toUpperCase()) return null; } ///多选题验证 if (questiontype == 'multiplechoice') { let rightAnswer = questionitem.choice ? questionitem.choice.filter((item: { iscorrect: number; }) => { return item.iscorrect === 1 }) : []; ///单选题的正确选项大于了1个 if (rightAnswer.length === 0 || !questionitem.answer || questionitem.answer.length === 0) return null; } ///判断题验证:防止没有答案的 if (questiontype == 'trueorfalse' && !questionitem.answer.length) return null; return questionitem; }) } catch (err) { console.log('error happened:', err); } return returnItems.filter(i => { return i != null; }).slice(0, count); } }