nodebb-plugin-openai 1.2.0 → 1.3.1
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/languages/en-GB/openai.json +3 -1
- package/languages/en-US/openai.json +3 -1
- package/lib/summary.js +123 -0
- package/library.js +73 -7
- package/package.json +1 -1
- package/plugin.json +13 -1
- package/public/lib/main.js +41 -0
- package/screenshots/screenshot1.png +0 -0
- package/templates/admin/plugins/openai.tpl +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"error.need-x-reputation-to-mention": "You need at least %1 reputation for mentioning chatgpt"
|
|
2
|
+
"error.need-x-reputation-to-mention": "You need at least %1 reputation for mentioning chatgpt",
|
|
3
|
+
"summarize-topic": "Summarize Topic",
|
|
4
|
+
"topic-summary": "Topic Summary"
|
|
3
5
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"error.need-x-reputation-to-mention": "You need at least %1 reputation for mentioning chatgpt"
|
|
2
|
+
"error.need-x-reputation-to-mention": "You need at least %1 reputation for mentioning chatgpt",
|
|
3
|
+
"summarize-topic": "Summarize Topic",
|
|
4
|
+
"topic-summary": "Topic Summary"
|
|
3
5
|
}
|
package/lib/summary.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require.main.require('./src/database');
|
|
4
|
+
const batch = require.main.require('./src/batch');
|
|
5
|
+
const topics = require.main.require('./src/topics');
|
|
6
|
+
const posts = require.main.require('./src/posts');
|
|
7
|
+
const user = require.main.require('./src/user');
|
|
8
|
+
|
|
9
|
+
function formatPosts(posts) {
|
|
10
|
+
return posts
|
|
11
|
+
.map((post) => `User ${post.username}:\n${post.content}`)
|
|
12
|
+
.join('\n---\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function chunkPosts(posts, maxTokensPerChunk = 3000) {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let currentChunk = [];
|
|
18
|
+
let currentTokens = 0;
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
21
|
+
for (const post of posts) {
|
|
22
|
+
const estimatedTokens = Math.ceil(post.content.length / 4) + 10;
|
|
23
|
+
if (currentTokens + estimatedTokens > maxTokensPerChunk) {
|
|
24
|
+
chunks.push(currentChunk);
|
|
25
|
+
currentChunk = [];
|
|
26
|
+
currentTokens = 0;
|
|
27
|
+
}
|
|
28
|
+
currentChunk.push(post);
|
|
29
|
+
currentTokens += estimatedTokens;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (currentChunk.length > 0) {
|
|
33
|
+
chunks.push(currentChunk);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return chunks;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function summarizeChunk(chunk, openai, settings) {
|
|
40
|
+
const threadText = formatPosts(chunk);
|
|
41
|
+
if (!threadText) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
const response = await openai.chat.completions.create({
|
|
45
|
+
model: settings.model || 'gpt-3.5-turbo',
|
|
46
|
+
messages: [
|
|
47
|
+
{
|
|
48
|
+
role: 'system',
|
|
49
|
+
content: 'You summarize discussion forum threads into concise summaries.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
role: 'user',
|
|
53
|
+
content: `Summarize the following discussion thread:\n\n${threadText}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
temperature: 0.5,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return response.choices[0].message.content.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
exports.summarizeTopic = async function(tid, openai, settings) {
|
|
63
|
+
const allPids = await topics.getPids(tid);
|
|
64
|
+
|
|
65
|
+
const userMap = {};
|
|
66
|
+
|
|
67
|
+
const chunkSummaries = [];
|
|
68
|
+
|
|
69
|
+
await batch.processArray(allPids, async function (pids) {
|
|
70
|
+
let postData = await posts.getPostsFields(pids, ['uid', 'content', 'deleted']);
|
|
71
|
+
postData = postData.filter((p) => !p.deleted);
|
|
72
|
+
const missingUids = [];
|
|
73
|
+
postData.forEach((p) => {
|
|
74
|
+
if (!userMap.hasOwnProperty(p.uid)) {
|
|
75
|
+
missingUids.push(p.uid)
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const userData = await user.getUsersFields(missingUids, ['username']);
|
|
79
|
+
userData.forEach((u) => {
|
|
80
|
+
userMap[u.uid] = u.displayname;
|
|
81
|
+
});
|
|
82
|
+
postData.forEach((p) => {
|
|
83
|
+
p.username = userMap[p.uid];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const chunks = chunkPosts(postData);
|
|
87
|
+
|
|
88
|
+
chunkSummaries.push(...await Promise.all(chunks.map(async (chunk) => {
|
|
89
|
+
return summarizeChunk(chunk, openai, settings)
|
|
90
|
+
})));
|
|
91
|
+
}, {
|
|
92
|
+
batch: 500,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (chunkSummaries.length === 1) {
|
|
96
|
+
return chunkSummaries[0];
|
|
97
|
+
}
|
|
98
|
+
// Final summary from all summaries
|
|
99
|
+
const finalInput = chunkSummaries.join("\n\n");
|
|
100
|
+
if (!finalInput) {
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
const finalResponse = await openai.chat.completions.create({
|
|
104
|
+
model: settings.model || 'gpt-3.5-turbo',
|
|
105
|
+
messages: [
|
|
106
|
+
{
|
|
107
|
+
role: 'system',
|
|
108
|
+
content: 'You are an assistant that summarizes forum thread summaries into a single cohesive summary.',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
role: 'user',
|
|
112
|
+
content: `Here are summaries of parts of a forum discussion:\n\n${finalInput}\n\nPlease write a final, concise summary of the full discussion.`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
temperature: 0.5,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return finalResponse.choices[0].message.content.trim();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
exports.clearTopicSummary = async function(tids) {
|
|
122
|
+
await db.deleteObjectFields(tids.map((tid) => `topic:${tid}`), ['openai:summary']);
|
|
123
|
+
};
|
package/library.js
CHANGED
|
@@ -16,6 +16,8 @@ const api = require.main.require('./src/api');
|
|
|
16
16
|
const privileges = require.main.require('./src/privileges');
|
|
17
17
|
const groups = require.main.require('./src/groups');
|
|
18
18
|
const sockets = require.main.require('./src/socket.io');
|
|
19
|
+
const socketPlugins = require.main.require('./src/socket.io/plugins');
|
|
20
|
+
const summary = require('./lib/summary');
|
|
19
21
|
|
|
20
22
|
const plugin = module.exports;
|
|
21
23
|
|
|
@@ -166,21 +168,21 @@ plugin.actionMessagingSave = async function (hookData) {
|
|
|
166
168
|
}
|
|
167
169
|
};
|
|
168
170
|
|
|
169
|
-
async function canUseOpenAI(uid, settings) {
|
|
170
|
-
if (!await checkReputation(uid, settings)) {
|
|
171
|
+
async function canUseOpenAI(uid, settings, silent = false) {
|
|
172
|
+
if (!await checkReputation(uid, settings, silent)) {
|
|
171
173
|
return false;
|
|
172
174
|
}
|
|
173
|
-
if (!await checkGroupMembership(uid, settings)) {
|
|
175
|
+
if (!await checkGroupMembership(uid, settings, silent)) {
|
|
174
176
|
return false;
|
|
175
177
|
}
|
|
176
178
|
return true;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
async function checkReputation(uid, settings) {
|
|
181
|
+
async function checkReputation(uid, settings, silent) {
|
|
180
182
|
const reputation = await user.getUserField(uid, 'reputation');
|
|
181
183
|
const hasEnoughRep = parseInt(settings.minimumReputation, 10) === 0 || parseInt(reputation, 10) >= parseInt(settings.minimumReputation, 10);
|
|
182
184
|
|
|
183
|
-
if (!hasEnoughRep) {
|
|
185
|
+
if (!hasEnoughRep && !silent) {
|
|
184
186
|
sockets.server.in(`uid_${uid}`).emit('event:alert', {
|
|
185
187
|
type: 'danger',
|
|
186
188
|
title: '[[global:alert.error]]',
|
|
@@ -190,7 +192,7 @@ async function checkReputation(uid, settings) {
|
|
|
190
192
|
return hasEnoughRep;
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
async function checkGroupMembership(uid, settings) {
|
|
195
|
+
async function checkGroupMembership(uid, settings, silent) {
|
|
194
196
|
let allowedGroups = [];
|
|
195
197
|
try {
|
|
196
198
|
allowedGroups = JSON.parse(settings.allowedGroups) || [];
|
|
@@ -205,7 +207,7 @@ async function checkGroupMembership(uid, settings) {
|
|
|
205
207
|
|
|
206
208
|
const isMembers = await groups.isMemberOfGroups(uid, allowedGroups);
|
|
207
209
|
const memberOfAny = isMembers.includes(true);
|
|
208
|
-
if (!memberOfAny) {
|
|
210
|
+
if (!memberOfAny && !silent) {
|
|
209
211
|
sockets.server.in(`uid_${uid}`).emit('event:alert', {
|
|
210
212
|
type: 'danger',
|
|
211
213
|
title: '[[global:alert.error]]',
|
|
@@ -262,3 +264,67 @@ plugin.addAdminNavigation = (header) => {
|
|
|
262
264
|
return header;
|
|
263
265
|
};
|
|
264
266
|
|
|
267
|
+
plugin.filterTopicThreadTools = async (hookData) => {
|
|
268
|
+
if (!await canUseOpenAI(hookData.uid, await getSettings(), true)) {
|
|
269
|
+
return hookData;
|
|
270
|
+
}
|
|
271
|
+
hookData.tools.push({
|
|
272
|
+
class: 'openai-summarize-topic',
|
|
273
|
+
icon: 'fa-wand-magic-sparkles',
|
|
274
|
+
title: '[[openai:summarize-topic]]',
|
|
275
|
+
});
|
|
276
|
+
return hookData;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
plugin.actionTopicReply = async (hookData) => {
|
|
280
|
+
await summary.clearTopicSummary([hookData.post.tid]);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
plugin.actionPostEdit = async (hookData) => {
|
|
284
|
+
await summary.clearTopicSummary([hookData.post.tid]);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
plugin.actionPostsPurge = async (hookData) => {
|
|
288
|
+
const { posts } = hookData;
|
|
289
|
+
const uniqTids = [...new Set(posts.map(p => p.tid))];
|
|
290
|
+
await summary.clearTopicSummary(uniqTids);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
plugin.actionPostRestore = async (hookData) => {
|
|
294
|
+
await summary.clearTopicSummary([hookData.post.tid]);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
plugin.actionPostDelete = async (hookData) => {
|
|
298
|
+
await summary.clearTopicSummary([hookData.post.tid]);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
plugin.actionPostMove = async (hookData) => {
|
|
302
|
+
await summary.clearTopicSummary([hookData.post.tid, hookData.tid]);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
plugin.actionPostChangeOwner = async (hookData) => {
|
|
306
|
+
const { posts } = hookData;
|
|
307
|
+
const uniqTids = [...new Set(posts.map(p => p.tid))];
|
|
308
|
+
await summary.clearTopicSummary(uniqTids);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
socketPlugins.openai = {};
|
|
312
|
+
|
|
313
|
+
socketPlugins.openai.summarizeTopic = async function (socket, data) {
|
|
314
|
+
const { tid } = data;
|
|
315
|
+
if (!await privileges.topics.can('topics:read', tid, socket.uid)) {
|
|
316
|
+
throw new Error('[[error:no-privileges]]');
|
|
317
|
+
}
|
|
318
|
+
const settings = await getSettings();
|
|
319
|
+
if (!await canUseOpenAI(socket.uid, settings)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let openaiSummary = await topics.getTopicField(tid, 'openai:summary');
|
|
324
|
+
if (openaiSummary) {
|
|
325
|
+
return openaiSummary;
|
|
326
|
+
}
|
|
327
|
+
openaiSummary = await summary.summarizeTopic(tid, openai, settings);
|
|
328
|
+
await topics.setTopicField(tid, 'openai:summary', openaiSummary);
|
|
329
|
+
return openaiSummary;
|
|
330
|
+
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
{ "hook": "static:app.load", "method": "init" },
|
|
7
7
|
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
|
8
8
|
{ "hook": "action:mentions.notify", "method": "actionMentionsNotify" },
|
|
9
|
-
{ "hook": "action:messaging.save", "method": "actionMessagingSave" }
|
|
9
|
+
{ "hook": "action:messaging.save", "method": "actionMessagingSave" },
|
|
10
|
+
{ "hook": "filter:topic.thread_tools", "method": "filterTopicThreadTools" },
|
|
11
|
+
|
|
12
|
+
{ "hook": "action:topic.reply", "method": "actionTopicReply" },
|
|
13
|
+
{ "hook": "action:post.edit", "method": "actionPostEdit" },
|
|
14
|
+
{ "hook": "action:posts.purge", "method": "actionPostsPurge" },
|
|
15
|
+
{ "hook": "action:post.restore", "method": "actionPostRestore" },
|
|
16
|
+
{ "hook": "action:post.delete", "method": "actionPostDelete" },
|
|
17
|
+
{ "hook": "action:post.move", "method": "actionPostMove" },
|
|
18
|
+
{ "hook": "action:post.changeOwner", "method": "actionPostChangeOwner" }
|
|
19
|
+
],
|
|
20
|
+
"scripts": [
|
|
21
|
+
"./public/lib/main.js"
|
|
10
22
|
],
|
|
11
23
|
"modules": {
|
|
12
24
|
"../admin/plugins/openai.js": "./public/lib/admin.js"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
$('document').ready(function () {
|
|
4
|
+
|
|
5
|
+
function alertType(type, message) {
|
|
6
|
+
require(['alerts'], function (alerts) {
|
|
7
|
+
alerts[type](message);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
$(window).on('action:topic.tools.load', function () {
|
|
12
|
+
$('.openai-summarize-topic').on('click', summarizeTopic);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function summarizeTopic() {
|
|
16
|
+
const tid = ajaxify.data.tid;
|
|
17
|
+
require(['bootbox'], function (bootbox) {
|
|
18
|
+
const modal = bootbox.dialog({
|
|
19
|
+
title: '[[openai:topic-summary]]',
|
|
20
|
+
message: `<div class="openai-summarize-topic"><div class="loading text-center"><i class="fa-solid fa-spinner fa-spin fa-2x"></i></div></div>`,
|
|
21
|
+
size: 'large',
|
|
22
|
+
buttons: {
|
|
23
|
+
ok: {
|
|
24
|
+
label: 'OK',
|
|
25
|
+
className: 'btn-primary',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
socket.emit('plugins.openai.summarizeTopic', { tid }, function (err, data) {
|
|
30
|
+
if (err) {
|
|
31
|
+
return alertType('error', err.message);
|
|
32
|
+
}
|
|
33
|
+
if (data) {
|
|
34
|
+
modal.find('.openai-summarize-topic').text(data);
|
|
35
|
+
} else {
|
|
36
|
+
modal.modal('hide');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
Binary file
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
</div>
|
|
43
43
|
<div class="">
|
|
44
44
|
<label class="form-label" for="systemPrompt">System Prompt</label>
|
|
45
|
-
<textarea class="form-control" id="systemPrompt" name="systemPrompt" title="System prompt" placeholder="You are a helpful assistant"></textarea>
|
|
45
|
+
<textarea class="form-control" id="systemPrompt" name="systemPrompt" title="System prompt" placeholder="You are a helpful assistant" rows="8"></textarea>
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
48
|
|