tmaiplugin
Version:
TrainingMaster AIGC Component
270 lines (262 loc) • 16.6 kB
text/typescript
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);
}
}