doomiaichat 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +76 -4
- package/dist/index.js +235 -4
- package/package.json +2 -4
- package/src/index.ts +262 -5
- package/tsconfig.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,49 @@ export declare class AIChat extends EventEmitter {
|
|
|
29
29
|
* @param {*} s2
|
|
30
30
|
*/
|
|
31
31
|
getScentenseSimilarity(s1: string, s2: string, axiosOption?: any): Promise<ChatReponse>;
|
|
32
|
+
/**
|
|
33
|
+
* 获得一种内容的相似说法
|
|
34
|
+
* 比如:
|
|
35
|
+
* 你今年多大?
|
|
36
|
+
* 相似问法:您是哪一年出生的
|
|
37
|
+
* 您今年贵庚?
|
|
38
|
+
* @param {*} content
|
|
39
|
+
* @param {需要出来的数量} count
|
|
40
|
+
*/
|
|
41
|
+
getSimilarityContent(content: string, count?: number, axiosOption?: AxiosRequestConfig): Promise<ChatReponse>;
|
|
42
|
+
/**
|
|
43
|
+
* 从指定的文本内容中生成相关的问答
|
|
44
|
+
* @param {*} content
|
|
45
|
+
* @param {*} count
|
|
46
|
+
* @param {*} axiosOption
|
|
47
|
+
* @returns
|
|
48
|
+
*/ generateQuestionsFromContent(content: string, count?: number, axiosOption?: AxiosRequestConfig): Promise<ChatReponse>;
|
|
49
|
+
/**
|
|
50
|
+
* 解析Faq返回的问题
|
|
51
|
+
* @param {*} messages
|
|
52
|
+
* @returns
|
|
53
|
+
*/
|
|
54
|
+
private pickUpFaqContent;
|
|
55
|
+
/**
|
|
56
|
+
* 从指定的文本内容中生成一张试卷
|
|
57
|
+
* @param {*} content
|
|
58
|
+
* @param {试卷的参数} paperOption
|
|
59
|
+
* totalscore: 试卷总分,默认100
|
|
60
|
+
* section: {type:[0,1,2,3]为单选、多选、判断、填空题型 count:生成多少道 score:本段分数}
|
|
61
|
+
* @param {*} axiosOption
|
|
62
|
+
* @returns
|
|
63
|
+
*/ generateExaminationPaperFromContent(content: string, paperOption?: any, axiosOption?: AxiosRequestConfig): Promise<ExaminationPaperResult>;
|
|
64
|
+
/**
|
|
65
|
+
* 从答复中得到题目
|
|
66
|
+
* @param {*} result
|
|
67
|
+
*
|
|
68
|
+
*/
|
|
69
|
+
private pickUpQuestions;
|
|
70
|
+
/**
|
|
71
|
+
* 将一段很长的文本,按1024长度来划分到多个中
|
|
72
|
+
* @param {*} content
|
|
73
|
+
*/
|
|
74
|
+
private splitLongText;
|
|
32
75
|
}
|
|
33
76
|
/**
|
|
34
77
|
* Api封装后的返回结果
|
|
@@ -56,8 +99,37 @@ export interface ChatReponse {
|
|
|
56
99
|
* 调用OpenAI Api的参数约定
|
|
57
100
|
*/
|
|
58
101
|
export interface CallChatApiOption {
|
|
59
|
-
model?: string;
|
|
60
|
-
maxtoken?: number;
|
|
61
|
-
temperature?: number;
|
|
62
|
-
replyCounts?: number;
|
|
102
|
+
'model'?: string;
|
|
103
|
+
'maxtoken'?: number;
|
|
104
|
+
'temperature'?: number;
|
|
105
|
+
'replyCounts'?: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 调用OpenAI Api的参数约定
|
|
109
|
+
*/
|
|
110
|
+
export interface FaqResultResponse {
|
|
111
|
+
'question': string;
|
|
112
|
+
'answer'?: string;
|
|
113
|
+
'keywords'?: Array<string>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 调用OpenAI Api的参数约定
|
|
117
|
+
*/
|
|
118
|
+
export interface ExaminationPaperResult {
|
|
119
|
+
/**
|
|
120
|
+
* return the result of api called
|
|
121
|
+
* @type {boolean}
|
|
122
|
+
*/
|
|
123
|
+
'successed': boolean;
|
|
124
|
+
'score': number;
|
|
125
|
+
'paper': any;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 调用OpenAI Api的参数约定
|
|
129
|
+
*/
|
|
130
|
+
export interface QuestionItemResultResponse {
|
|
131
|
+
'question': string;
|
|
132
|
+
'answer'?: Array<string>;
|
|
133
|
+
'choice'?: Array<string>;
|
|
134
|
+
'score'?: number;
|
|
63
135
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
3
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
4
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -7,9 +8,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
9
|
});
|
|
9
10
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AIChat = void 0;
|
|
13
|
+
const openai_1 = require("openai");
|
|
14
|
+
const events_1 = require("events");
|
|
15
|
+
const SECTION_LENGTH = 256; ///每256个字符分成一组
|
|
16
|
+
const MESSAGE_LENGTH = 8; ///每次送8句话给openai 进行解析,送多了,会报错
|
|
17
|
+
//请将答案放在最后,标记为答案:()
|
|
18
|
+
const QUESTION_TEXT_MAPPING = {
|
|
19
|
+
singlechoice: ',根据以上内容,生成1道单选题,每道题目4个选项,请按照{"question":"","choice":[],"answer":[]}的JSON结构输出,choice中的元素用大写字母ABCD开头,answer数组中包含一个正确答案',
|
|
20
|
+
multiplechoice: ',根据以上内容,请生成1道多选题,提供4个选项,答案至少1个以上选项,请按照{"question":"","choice":[],"answer":[]}的JSON结构输出,choice中的元素用大写字母ABCD开头,answer数组中包含正确答案选项',
|
|
21
|
+
trueorfalse: ',根据以上内容,请生成1道判断题,请按照{"question":"","choice":["A.正确","B.错误"],"answer":[]}的JSON结构输出,answer数组中包含一个元素,"正确"或"错误"',
|
|
22
|
+
completion: ',根据以上内容,请生成1道填空题,每道题目1个填空,请按照{"question":"","answer":[]}的JSON结构输出,answer数组中包含填空答案' //请将答案放在最后,标记为答案:()
|
|
23
|
+
};
|
|
24
|
+
const QUESTION_TYPE = ['singlechoice', 'multiplechoice', 'trueorfalse', 'completion'];
|
|
25
|
+
class AIChat extends events_1.EventEmitter {
|
|
13
26
|
/**
|
|
14
27
|
*
|
|
15
28
|
* @param apiKey 调用OpenAI 的key
|
|
@@ -17,7 +30,7 @@ export class AIChat extends EventEmitter {
|
|
|
17
30
|
*/
|
|
18
31
|
constructor(apiKey, apiOption = {}) {
|
|
19
32
|
super();
|
|
20
|
-
this.chatRobot = new OpenAIApi(new Configuration({ apiKey }));
|
|
33
|
+
this.chatRobot = new openai_1.OpenAIApi(new openai_1.Configuration({ apiKey }));
|
|
21
34
|
this.chatModel = apiOption.model || 'gpt-3.5-turbo';
|
|
22
35
|
this.maxtoken = apiOption.maxtoken || 1024;
|
|
23
36
|
this.temperature = apiOption.temperature || 0.9;
|
|
@@ -83,4 +96,222 @@ export class AIChat extends EventEmitter {
|
|
|
83
96
|
return this.chatRequest(messages, { maxtoken: 32 }, axiosOption);
|
|
84
97
|
});
|
|
85
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* 获得一种内容的相似说法
|
|
101
|
+
* 比如:
|
|
102
|
+
* 你今年多大?
|
|
103
|
+
* 相似问法:您是哪一年出生的
|
|
104
|
+
* 您今年贵庚?
|
|
105
|
+
* @param {*} content
|
|
106
|
+
* @param {需要出来的数量} count
|
|
107
|
+
*/
|
|
108
|
+
getSimilarityContent(content, count = 1, axiosOption = {}) {
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
let chnReg = /([\u4e00-\u9fa5]|[\ufe30-\uffa0])/.test(content); ///检查源话是否含有中文内容
|
|
111
|
+
let engReg = /[a-zA-Z]/.test(content); ///检查源话是否含有英文内容
|
|
112
|
+
///如果源话是全中文,那么结果中不应该出来英文的相似说法,如果源话是全英文,则结果不能出现全中文的说法
|
|
113
|
+
let prefix = (!chnReg && engReg) ? '请用完整的英文表达,' : ((chnReg && !engReg) ? '请用完整的中文表达,' : '');
|
|
114
|
+
const text = `${prefix}生成与下面句子意思相同的内容"${content}"`;
|
|
115
|
+
let result = yield this.chatRequest(this.splitLongText(text), { replyCounts: count }, axiosOption);
|
|
116
|
+
if (!result.successed || !result.message)
|
|
117
|
+
return result;
|
|
118
|
+
let replys = result.message.map(item => { return item.message.content.trim(); });
|
|
119
|
+
return { successed: true, message: replys };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 从指定的文本内容中生成相关的问答
|
|
124
|
+
* @param {*} content
|
|
125
|
+
* @param {*} count
|
|
126
|
+
* @param {*} axiosOption
|
|
127
|
+
* @returns
|
|
128
|
+
*/ //并在答案末尾处必须给出答案内容中的关键词
|
|
129
|
+
generateQuestionsFromContent(content, count = 1, axiosOption = {}) {
|
|
130
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
+
let arrContent = this.splitLongText(content);
|
|
132
|
+
///没20句话分为一组,适应大文件内容多次请求组合结果
|
|
133
|
+
///每一句话需要产生的题目
|
|
134
|
+
let questions4EverySentense = count / arrContent.length; //Math.ceil(arrContent.length / 20);
|
|
135
|
+
let faqs = [], gotted = 0;
|
|
136
|
+
while (arrContent.length > 0 && gotted < count) {
|
|
137
|
+
////每次最多送MESSAGE_LENGTH句话给openai
|
|
138
|
+
let subarray = arrContent.slice(0, MESSAGE_LENGTH);
|
|
139
|
+
let itemCount = Math.min(Math.ceil(subarray.length * questions4EverySentense), count - gotted);
|
|
140
|
+
//subarray.push({ role: 'user', content:'请根据上述内容,给出一道提问与答案以及答案关键词,按照先问题内容,再标准答案,再关键词的顺序输出,关键词之间用、分开'})
|
|
141
|
+
subarray.push({ role: 'user', content: '请根据上述内容,给出一道提问与答案以及答案关键词,按照{"question":"","answer":"","keywords":[]}的JSON结构输出,keywords数组中的关键词必须存在于答案内容中' });
|
|
142
|
+
let result = yield this.chatRequest(subarray, { replyCounts: itemCount }, axiosOption);
|
|
143
|
+
if (result.successed && result.message) {
|
|
144
|
+
let msgs = this.pickUpFaqContent(result.message);
|
|
145
|
+
if (msgs.length) {
|
|
146
|
+
///对外发送检出问答题的信号
|
|
147
|
+
this.emit('parseout', { type: 'qa', items: msgs });
|
|
148
|
+
gotted += msgs.length; //result.message.length;
|
|
149
|
+
// console.log('gotted=', gotted)
|
|
150
|
+
faqs = faqs.concat(msgs);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
////删除已经处理的文本
|
|
154
|
+
arrContent.splice(0, MESSAGE_LENGTH);
|
|
155
|
+
}
|
|
156
|
+
arrContent = []; /// 释放内存
|
|
157
|
+
///发出信号,解析完毕
|
|
158
|
+
this.emit('parseover', { type: 'qa', items: faqs });
|
|
159
|
+
return { successed: true, message: faqs };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 解析Faq返回的问题
|
|
164
|
+
* @param {*} messages
|
|
165
|
+
* @returns
|
|
166
|
+
*/
|
|
167
|
+
pickUpFaqContent(messages) {
|
|
168
|
+
let replys = messages.map(item => {
|
|
169
|
+
let content = item.message.content.trim().replace(/\t|\n|\v|\r|\f/g, '');
|
|
170
|
+
try {
|
|
171
|
+
let jsonObj = JSON.parse(content);
|
|
172
|
+
if (!Array.isArray(jsonObj.keywords)) {
|
|
173
|
+
jsonObj.keywords = (jsonObj.keywords || '').split(',');
|
|
174
|
+
}
|
|
175
|
+
return jsonObj;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.log('JSON error', content, err);
|
|
179
|
+
return { question: null };
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return replys.filter(n => { return n.question != null; });
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 从指定的文本内容中生成一张试卷
|
|
186
|
+
* @param {*} content
|
|
187
|
+
* @param {试卷的参数} paperOption
|
|
188
|
+
* totalscore: 试卷总分,默认100
|
|
189
|
+
* section: {type:[0,1,2,3]为单选、多选、判断、填空题型 count:生成多少道 score:本段分数}
|
|
190
|
+
* @param {*} axiosOption
|
|
191
|
+
* @returns
|
|
192
|
+
*/ //并在答案末尾处必须给出答案内容中的关键词
|
|
193
|
+
generateExaminationPaperFromContent(content, paperOption = {}, axiosOption = {}) {
|
|
194
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
195
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
196
|
+
let arrContent = this.splitLongText(content);
|
|
197
|
+
let sectionCount = {
|
|
198
|
+
singlechoice: (((_a = paperOption.singlechoice) === null || _a === void 0 ? void 0 : _a.count) || 0) / arrContent.length,
|
|
199
|
+
multiplechoice: (((_b = paperOption.multiplechoice) === null || _b === void 0 ? void 0 : _b.count) || 0) / arrContent.length,
|
|
200
|
+
trueorfalse: (((_c = paperOption.trueorfalse) === null || _c === void 0 ? void 0 : _c.count) || 0) / arrContent.length,
|
|
201
|
+
completion: (((_d = paperOption.completion) === null || _d === void 0 ? void 0 : _d.count) || 0) / arrContent.length
|
|
202
|
+
};
|
|
203
|
+
///剩余待生成的题目数量
|
|
204
|
+
let remainCount = {
|
|
205
|
+
singlechoice: ((_e = paperOption.singlechoice) === null || _e === void 0 ? void 0 : _e.count) || 0,
|
|
206
|
+
multiplechoice: ((_f = paperOption.multiplechoice) === null || _f === void 0 ? void 0 : _f.count) || 0,
|
|
207
|
+
trueorfalse: ((_g = paperOption.trueorfalse) === null || _g === void 0 ? void 0 : _g.count) || 0,
|
|
208
|
+
completion: ((_h = paperOption.completion) === null || _h === void 0 ? void 0 : _h.count) || 0
|
|
209
|
+
};
|
|
210
|
+
///每种类型的题目的分数
|
|
211
|
+
let ITEM_SCORE = {
|
|
212
|
+
singlechoice: ((_j = paperOption.singlechoice) === null || _j === void 0 ? void 0 : _j.score) || 0,
|
|
213
|
+
multiplechoice: ((_k = paperOption.multiplechoice) === null || _k === void 0 ? void 0 : _k.score) || 0,
|
|
214
|
+
trueorfalse: ((_l = paperOption.trueorfalse) === null || _l === void 0 ? void 0 : _l.score) || 0,
|
|
215
|
+
completion: ((_m = paperOption.completion) === null || _m === void 0 ? void 0 : _m.score) || 0
|
|
216
|
+
};
|
|
217
|
+
///最后生成出来的结果
|
|
218
|
+
let paperReturned = {
|
|
219
|
+
singlechoice: [], multiplechoice: [], trueorfalse: [], completion: []
|
|
220
|
+
}, noMoreQuestionRetrive = false, totalscore = 0;
|
|
221
|
+
while (arrContent.length > 0 && !noMoreQuestionRetrive) {
|
|
222
|
+
////每次最多送MESSAGE_LENGTH句话给openai
|
|
223
|
+
let subarray = arrContent.slice(0, MESSAGE_LENGTH);
|
|
224
|
+
/**
|
|
225
|
+
* 每种类型的题目进行遍历
|
|
226
|
+
*/
|
|
227
|
+
noMoreQuestionRetrive = true;
|
|
228
|
+
for (const key of QUESTION_TYPE) {
|
|
229
|
+
///还需要抓取题目
|
|
230
|
+
if (remainCount[key] > 0) {
|
|
231
|
+
noMoreQuestionRetrive = false;
|
|
232
|
+
subarray.push({ role: 'user', content: QUESTION_TEXT_MAPPING[key] });
|
|
233
|
+
let itemCount = Math.min(remainCount[key], Math.ceil(subarray.length * sectionCount[key]));
|
|
234
|
+
console.log(QUESTION_TEXT_MAPPING[key], itemCount);
|
|
235
|
+
let result = yield this.chatRequest(subarray, { replyCounts: itemCount }, axiosOption);
|
|
236
|
+
if (result.successed && result.message) {
|
|
237
|
+
//console.log('paper result', key, result.message.length)
|
|
238
|
+
let pickedQuestions = this.pickUpQuestions(result.message, key, ITEM_SCORE[key]);
|
|
239
|
+
if (pickedQuestions.length) {
|
|
240
|
+
///对外发送检出题目的信号
|
|
241
|
+
this.emit('parseout', { type: 'question', name: key, items: pickedQuestions });
|
|
242
|
+
paperReturned[key] = paperReturned[key].concat(pickedQuestions);
|
|
243
|
+
remainCount[key] = remainCount[key] - pickedQuestions.length;
|
|
244
|
+
totalscore = totalscore + pickedQuestions.length * ITEM_SCORE[key];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
subarray.splice(subarray.length - 1, 1); ///把最后的问法删除
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
////删除已经处理的文本
|
|
251
|
+
arrContent.splice(0, MESSAGE_LENGTH);
|
|
252
|
+
}
|
|
253
|
+
///发出信号,解析完毕
|
|
254
|
+
this.emit('parseover', { type: 'question', items: paperReturned });
|
|
255
|
+
return { successed: true, score: totalscore, paper: paperReturned };
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 从答复中得到题目
|
|
260
|
+
* @param {*} result
|
|
261
|
+
*
|
|
262
|
+
*/
|
|
263
|
+
pickUpQuestions(result, questiontype, score = 1) {
|
|
264
|
+
let item = result.map(m => {
|
|
265
|
+
////防止输出的JSON格式不合法
|
|
266
|
+
try {
|
|
267
|
+
let jsonObj = JSON.parse(m.message.content);
|
|
268
|
+
jsonObj.score = score;
|
|
269
|
+
if (jsonObj.choice && Array.isArray(jsonObj.choice) && questiontype != 'completion') {
|
|
270
|
+
jsonObj.fullanswer = (jsonObj.answer + '').replace(/,|[^ABCDE]/g, '');
|
|
271
|
+
jsonObj.choice = jsonObj.choice.map((item, index) => {
|
|
272
|
+
let seqNo = String.fromCharCode(65 + index);
|
|
273
|
+
let correctReg = new RegExp(`${seqNo}.|${seqNo}`, 'ig');
|
|
274
|
+
//let answer = jsonObj.fullanswer
|
|
275
|
+
return {
|
|
276
|
+
id: seqNo,
|
|
277
|
+
content: item.replace(correctReg, '').trim(),
|
|
278
|
+
iscorrect: (jsonObj.fullanswer.indexOf(seqNo) >= 0 || jsonObj.fullanswer.indexOf(m)) >= 0 ? 1 : 0
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
switch (questiontype) {
|
|
283
|
+
case 'singlechoice':
|
|
284
|
+
jsonObj.answer = (jsonObj.answer + '').replace(/,|[^ABCDEFG]/g, '').split('').slice(0, 1);
|
|
285
|
+
break;
|
|
286
|
+
case 'multiplechoice':
|
|
287
|
+
jsonObj.answer = (jsonObj.answer + '').replace(/,|[^ABCDEFG]/g, '').split('');
|
|
288
|
+
break;
|
|
289
|
+
case 'trueorfalse':
|
|
290
|
+
jsonObj.answer = [(jsonObj.answer + '').indexOf('正确') >= 0 ? 'A' : 'B'];
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
return jsonObj;
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
console.log('error happened:', err);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return item.filter(i => { return i != null; });
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 将一段很长的文本,按1024长度来划分到多个中
|
|
304
|
+
* @param {*} content
|
|
305
|
+
*/
|
|
306
|
+
splitLongText(content, len = SECTION_LENGTH) {
|
|
307
|
+
let start = 0, message = [], length = content.length;
|
|
308
|
+
while (start < length) {
|
|
309
|
+
const subtext = content.substr(start, len);
|
|
310
|
+
if (subtext)
|
|
311
|
+
message.push({ role: 'user', content: subtext });
|
|
312
|
+
start += len;
|
|
313
|
+
}
|
|
314
|
+
return message;
|
|
315
|
+
}
|
|
86
316
|
}
|
|
317
|
+
exports.AIChat = AIChat;
|
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doomiaichat",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Doomisoft OpenAI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"ChatGpt"
|
|
11
|
-
],
|
|
9
|
+
"keywords": ["ChatGpt"],
|
|
12
10
|
"author": "Stephen.Shen",
|
|
13
11
|
"license": "ISC",
|
|
14
12
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { Configuration, OpenAIApi, CreateChatCompletionResponse, ChatCompletionRequestMessage } from "openai"
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
3
|
import { AxiosRequestConfig, AxiosResponse } from "axios";
|
|
4
|
+
const SECTION_LENGTH = 256; ///每256个字符分成一组
|
|
5
|
+
const MESSAGE_LENGTH = 8; ///每次送8句话给openai 进行解析,送多了,会报错
|
|
6
|
+
//请将答案放在最后,标记为答案:()
|
|
7
|
+
const QUESTION_TEXT_MAPPING: any = {
|
|
8
|
+
singlechoice: ',根据以上内容,生成1道单选题,每道题目4个选项,请按照{"question":"","choice":[],"answer":[]}的JSON结构输出,choice中的元素用大写字母ABCD开头,answer数组中包含一个正确答案',
|
|
9
|
+
multiplechoice: ',根据以上内容,请生成1道多选题,提供4个选项,答案至少1个以上选项,请按照{"question":"","choice":[],"answer":[]}的JSON结构输出,choice中的元素用大写字母ABCD开头,answer数组中包含正确答案选项', //请将答案放在最后,标记为答案:()
|
|
10
|
+
trueorfalse: ',根据以上内容,请生成1道判断题,请按照{"question":"","choice":["A.正确","B.错误"],"answer":[]}的JSON结构输出,answer数组中包含一个元素,"正确"或"错误"', //标记为答案:(正确或错误)
|
|
11
|
+
completion: ',根据以上内容,请生成1道填空题,每道题目1个填空,请按照{"question":"","answer":[]}的JSON结构输出,answer数组中包含填空答案' //请将答案放在最后,标记为答案:()
|
|
12
|
+
}
|
|
13
|
+
const QUESTION_TYPE:Array<string> = ['singlechoice', 'multiplechoice', 'trueorfalse', 'completion']
|
|
14
|
+
|
|
4
15
|
export class AIChat extends EventEmitter {
|
|
5
16
|
private readonly chatRobot: OpenAIApi;
|
|
6
17
|
private readonly chatModel: string;
|
|
@@ -75,6 +86,220 @@ export class AIChat extends EventEmitter {
|
|
|
75
86
|
]
|
|
76
87
|
return this.chatRequest(messages, {maxtoken:32}, axiosOption);
|
|
77
88
|
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 获得一种内容的相似说法
|
|
93
|
+
* 比如:
|
|
94
|
+
* 你今年多大?
|
|
95
|
+
* 相似问法:您是哪一年出生的
|
|
96
|
+
* 您今年贵庚?
|
|
97
|
+
* @param {*} content
|
|
98
|
+
* @param {需要出来的数量} count
|
|
99
|
+
*/
|
|
100
|
+
async getSimilarityContent(content:string, count:number = 1, axiosOption:AxiosRequestConfig = {}):Promise<ChatReponse> {
|
|
101
|
+
let chnReg:boolean = /([\u4e00-\u9fa5]|[\ufe30-\uffa0])/.test(content) ///检查源话是否含有中文内容
|
|
102
|
+
let engReg: boolean = /[a-zA-Z]/.test(content) ///检查源话是否含有英文内容
|
|
103
|
+
///如果源话是全中文,那么结果中不应该出来英文的相似说法,如果源话是全英文,则结果不能出现全中文的说法
|
|
104
|
+
let prefix = (!chnReg && engReg) ? '请用完整的英文表达,' : ((chnReg && !engReg) ? '请用完整的中文表达,':'')
|
|
105
|
+
const text = `${prefix}生成与下面句子意思相同的内容"${content}"`
|
|
106
|
+
let result = await this.chatRequest(this.splitLongText(text), { replyCounts: count }, axiosOption);
|
|
107
|
+
if (!result.successed || !result.message) return result;
|
|
108
|
+
let replys = result.message.map(item => { return item.message.content.trim(); })
|
|
109
|
+
return { successed: true, message: replys }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 从指定的文本内容中生成相关的问答
|
|
114
|
+
* @param {*} content
|
|
115
|
+
* @param {*} count
|
|
116
|
+
* @param {*} axiosOption
|
|
117
|
+
* @returns
|
|
118
|
+
*///并在答案末尾处必须给出答案内容中的关键词
|
|
119
|
+
async generateQuestionsFromContent(content:string, count:number = 1, axiosOption:AxiosRequestConfig = {}):Promise<ChatReponse> {
|
|
120
|
+
let arrContent = this.splitLongText(content);
|
|
121
|
+
///没20句话分为一组,适应大文件内容多次请求组合结果
|
|
122
|
+
///每一句话需要产生的题目
|
|
123
|
+
let questions4EverySentense:number = count / arrContent.length; //Math.ceil(arrContent.length / 20);
|
|
124
|
+
let faqs:Array<FaqResultResponse> = [], gotted:number = 0;
|
|
125
|
+
while (arrContent.length > 0 && gotted < count) {
|
|
126
|
+
////每次最多送MESSAGE_LENGTH句话给openai
|
|
127
|
+
let subarray = arrContent.slice(0, MESSAGE_LENGTH);
|
|
128
|
+
let itemCount = Math.min(Math.ceil(subarray.length * questions4EverySentense), count - gotted);
|
|
129
|
+
//subarray.push({ role: 'user', content:'请根据上述内容,给出一道提问与答案以及答案关键词,按照先问题内容,再标准答案,再关键词的顺序输出,关键词之间用、分开'})
|
|
130
|
+
subarray.push({ role: 'user', content: '请根据上述内容,给出一道提问与答案以及答案关键词,按照{"question":"","answer":"","keywords":[]}的JSON结构输出,keywords数组中的关键词必须存在于答案内容中' })
|
|
131
|
+
let result = await this.chatRequest(subarray, { replyCounts: itemCount} , axiosOption);
|
|
132
|
+
if (result.successed && result.message) {
|
|
133
|
+
let msgs = this.pickUpFaqContent(result.message);
|
|
134
|
+
if (msgs.length) {
|
|
135
|
+
///对外发送检出问答题的信号
|
|
136
|
+
this.emit('parseout', { type: 'qa', items: msgs })
|
|
137
|
+
gotted += msgs.length; //result.message.length;
|
|
138
|
+
// console.log('gotted=', gotted)
|
|
139
|
+
faqs = faqs.concat(msgs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
////删除已经处理的文本
|
|
144
|
+
arrContent.splice(0, MESSAGE_LENGTH);
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
arrContent = []; /// 释放内存
|
|
148
|
+
///发出信号,解析完毕
|
|
149
|
+
this.emit('parseover', { type: 'qa', items: faqs })
|
|
150
|
+
return { successed: true, message: faqs };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 解析Faq返回的问题
|
|
154
|
+
* @param {*} messages
|
|
155
|
+
* @returns
|
|
156
|
+
*/
|
|
157
|
+
private pickUpFaqContent(messages: Array<any>): Array<FaqResultResponse> {
|
|
158
|
+
let replys = messages.map(item => {
|
|
159
|
+
let content = item.message.content.trim().replace(/\t|\n|\v|\r|\f/g, '');
|
|
160
|
+
try {
|
|
161
|
+
let jsonObj = JSON.parse(content);
|
|
162
|
+
if (!Array.isArray(jsonObj.keywords)) {
|
|
163
|
+
jsonObj.keywords = (jsonObj.keywords || '').split(',')
|
|
164
|
+
}
|
|
165
|
+
return jsonObj ;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.log('JSON error', content, err)
|
|
168
|
+
return {question:null};
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
return replys.filter(n => { return n.question != null });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 从指定的文本内容中生成一张试卷
|
|
176
|
+
* @param {*} content
|
|
177
|
+
* @param {试卷的参数} paperOption
|
|
178
|
+
* totalscore: 试卷总分,默认100
|
|
179
|
+
* section: {type:[0,1,2,3]为单选、多选、判断、填空题型 count:生成多少道 score:本段分数}
|
|
180
|
+
* @param {*} axiosOption
|
|
181
|
+
* @returns
|
|
182
|
+
*///并在答案末尾处必须给出答案内容中的关键词
|
|
183
|
+
async generateExaminationPaperFromContent(content: string, paperOption: any = {}, axiosOption: AxiosRequestConfig = {}): Promise<ExaminationPaperResult> {
|
|
184
|
+
let arrContent = this.splitLongText(content);
|
|
185
|
+
let sectionCount: any = {
|
|
186
|
+
singlechoice: (paperOption.singlechoice?.count || 0) / arrContent.length,
|
|
187
|
+
multiplechoice: (paperOption.multiplechoice?.count || 0) / arrContent.length,
|
|
188
|
+
trueorfalse: (paperOption.trueorfalse?.count || 0) / arrContent.length,
|
|
189
|
+
completion: (paperOption.completion?.count || 0) / arrContent.length
|
|
190
|
+
};
|
|
191
|
+
///剩余待生成的题目数量
|
|
192
|
+
let remainCount:any = {
|
|
193
|
+
singlechoice:paperOption.singlechoice?.count || 0,
|
|
194
|
+
multiplechoice: paperOption.multiplechoice?.count || 0,
|
|
195
|
+
trueorfalse: paperOption.trueorfalse?.count || 0,
|
|
196
|
+
completion: paperOption.completion?.count || 0
|
|
197
|
+
};
|
|
198
|
+
///每种类型的题目的分数
|
|
199
|
+
let ITEM_SCORE: any = {
|
|
200
|
+
singlechoice: paperOption.singlechoice?.score || 0,
|
|
201
|
+
multiplechoice: paperOption.multiplechoice?.score || 0,
|
|
202
|
+
trueorfalse: paperOption.trueorfalse?.score || 0,
|
|
203
|
+
completion: paperOption.completion?.score || 0
|
|
204
|
+
};
|
|
205
|
+
///最后生成出来的结果
|
|
206
|
+
let paperReturned: any = {
|
|
207
|
+
singlechoice: [], multiplechoice: [], trueorfalse: [], completion: []
|
|
208
|
+
|
|
209
|
+
}, noMoreQuestionRetrive:boolean = false, totalscore:number = 0;
|
|
210
|
+
|
|
211
|
+
while (arrContent.length > 0 && !noMoreQuestionRetrive) {
|
|
212
|
+
////每次最多送MESSAGE_LENGTH句话给openai
|
|
213
|
+
let subarray = arrContent.slice(0, MESSAGE_LENGTH);
|
|
214
|
+
/**
|
|
215
|
+
* 每种类型的题目进行遍历
|
|
216
|
+
*/
|
|
217
|
+
noMoreQuestionRetrive = true;
|
|
218
|
+
for (const key of QUESTION_TYPE) {
|
|
219
|
+
///还需要抓取题目
|
|
220
|
+
if (remainCount[key] > 0) {
|
|
221
|
+
noMoreQuestionRetrive = false;
|
|
222
|
+
subarray.push({ role: 'user', content: QUESTION_TEXT_MAPPING[key] })
|
|
223
|
+
let itemCount = Math.min(remainCount[key], Math.ceil(subarray.length * sectionCount[key]));
|
|
224
|
+
console.log(QUESTION_TEXT_MAPPING[key], itemCount);
|
|
225
|
+
let result = await this.chatRequest(subarray, { replyCounts: itemCount } , axiosOption);
|
|
226
|
+
if (result.successed && result.message) {
|
|
227
|
+
//console.log('paper result', key, result.message.length)
|
|
228
|
+
let pickedQuestions = this.pickUpQuestions(result.message, key, ITEM_SCORE[key]);
|
|
229
|
+
if (pickedQuestions.length) {
|
|
230
|
+
///对外发送检出题目的信号
|
|
231
|
+
this.emit('parseout', { type: 'question', name: key, items: pickedQuestions })
|
|
232
|
+
paperReturned[key] = paperReturned[key].concat(pickedQuestions);
|
|
233
|
+
remainCount[key] = remainCount[key] - pickedQuestions.length;
|
|
234
|
+
totalscore = totalscore + pickedQuestions.length * ITEM_SCORE[key];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
subarray.splice(subarray.length - 1, 1); ///把最后的问法删除
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
////删除已经处理的文本
|
|
241
|
+
arrContent.splice(0, MESSAGE_LENGTH);
|
|
242
|
+
}
|
|
243
|
+
///发出信号,解析完毕
|
|
244
|
+
this.emit('parseover', { type: 'question', items: paperReturned })
|
|
245
|
+
return { successed: true, score: totalscore, paper: paperReturned }
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 从答复中得到题目
|
|
249
|
+
* @param {*} result
|
|
250
|
+
*
|
|
251
|
+
*/
|
|
252
|
+
private pickUpQuestions(result: Array<any>, questiontype: string, score: number = 1): Array<QuestionItemResultResponse> {
|
|
253
|
+
let item = result.map(m => {
|
|
254
|
+
////防止输出的JSON格式不合法
|
|
255
|
+
try {
|
|
256
|
+
let jsonObj = JSON.parse(m.message.content)
|
|
257
|
+
jsonObj.score = score;
|
|
258
|
+
if (jsonObj.choice && Array.isArray(jsonObj.choice) && questiontype != 'completion') {
|
|
259
|
+
jsonObj.fullanswer = (jsonObj.answer + '').replace(/,|[^ABCDE]/g, '');
|
|
260
|
+
jsonObj.choice = jsonObj.choice.map((item:string, index:number) => {
|
|
261
|
+
let seqNo = String.fromCharCode(65 + index);
|
|
262
|
+
let correctReg = new RegExp(`${seqNo}.|${seqNo}`, 'ig')
|
|
263
|
+
//let answer = jsonObj.fullanswer
|
|
264
|
+
return {
|
|
265
|
+
id: seqNo,
|
|
266
|
+
content: item.replace(correctReg, '').trim(),
|
|
267
|
+
iscorrect: (jsonObj.fullanswer.indexOf(seqNo) >= 0 || jsonObj.fullanswer.indexOf(m)) >= 0 ? 1 : 0
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
switch (questiontype) {
|
|
272
|
+
case 'singlechoice':
|
|
273
|
+
jsonObj.answer = (jsonObj.answer + '').replace(/,|[^ABCDEFG]/g, '').split('').slice(0, 1);
|
|
274
|
+
break;
|
|
275
|
+
case 'multiplechoice':
|
|
276
|
+
jsonObj.answer = (jsonObj.answer + '').replace(/,|[^ABCDEFG]/g, '').split('');
|
|
277
|
+
break;
|
|
278
|
+
case 'trueorfalse':
|
|
279
|
+
jsonObj.answer = [(jsonObj.answer + '').indexOf('正确') >= 0 ? 'A' : 'B']
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
return jsonObj;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.log('error happened:', err);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
return item.filter(i => { return i != null; });
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* 将一段很长的文本,按1024长度来划分到多个中
|
|
292
|
+
* @param {*} content
|
|
293
|
+
*/
|
|
294
|
+
private splitLongText(content: string, len = SECTION_LENGTH): Array<ChatCompletionRequestMessage> {
|
|
295
|
+
let start = 0, message: Array<ChatCompletionRequestMessage> = [], length = content.length;
|
|
296
|
+
while (start < length) {
|
|
297
|
+
const subtext = content.substr(start, len);
|
|
298
|
+
if (subtext) message.push({ role: 'user', content: subtext })
|
|
299
|
+
start += len;
|
|
300
|
+
}
|
|
301
|
+
return message;
|
|
302
|
+
}
|
|
78
303
|
}
|
|
79
304
|
|
|
80
305
|
/**
|
|
@@ -103,8 +328,40 @@ export interface ChatReponse {
|
|
|
103
328
|
* 调用OpenAI Api的参数约定
|
|
104
329
|
*/
|
|
105
330
|
export interface CallChatApiOption{
|
|
106
|
-
model?:string, ///模型名称
|
|
107
|
-
maxtoken?:number; ///返回的最大token
|
|
108
|
-
temperature?:number;
|
|
109
|
-
replyCounts?:number; ///返回多少答案
|
|
110
|
-
}
|
|
331
|
+
'model'?:string, ///模型名称
|
|
332
|
+
'maxtoken'?:number; ///返回的最大token
|
|
333
|
+
'temperature'?:number;
|
|
334
|
+
'replyCounts'?:number; ///返回多少答案
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 调用OpenAI Api的参数约定
|
|
339
|
+
*/
|
|
340
|
+
export interface FaqResultResponse {
|
|
341
|
+
'question': string, ///模型名称
|
|
342
|
+
'answer'?: string; ///返回的最大token
|
|
343
|
+
'keywords'?: Array<string>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 调用OpenAI Api的参数约定
|
|
348
|
+
*/
|
|
349
|
+
export interface ExaminationPaperResult {
|
|
350
|
+
/**
|
|
351
|
+
* return the result of api called
|
|
352
|
+
* @type {boolean}
|
|
353
|
+
*/
|
|
354
|
+
'successed': boolean,
|
|
355
|
+
'score': number, ///卷面总分
|
|
356
|
+
'paper': any
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 调用OpenAI Api的参数约定
|
|
360
|
+
*/
|
|
361
|
+
export interface QuestionItemResultResponse {
|
|
362
|
+
'question': string, ///模型名称
|
|
363
|
+
'answer'?: Array<string>; ///返回的最大token
|
|
364
|
+
'choice'?: Array<string>;
|
|
365
|
+
'score'?:number;
|
|
366
|
+
}
|
|
367
|
+
|