nodebb-plugin-openai 1.2.0 → 1.3.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/languages/en-GB/openai.json +3 -1
- package/languages/en-US/openai.json +3 -1
- package/lib/summary.js +112 -0
- package/library.js +36 -7
- package/package.json +1 -1
- package/plugin.json +5 -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,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const batch = require.main.require('./src/batch');
|
|
4
|
+
const topics = require.main.require('./src/topics');
|
|
5
|
+
const posts = require.main.require('./src/posts');
|
|
6
|
+
const user = require.main.require('./src/user');
|
|
7
|
+
|
|
8
|
+
function formatPosts(posts) {
|
|
9
|
+
return posts
|
|
10
|
+
.map((post) => `User ${post.username}:\n${post.content}`)
|
|
11
|
+
.join('\n---\n');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function chunkPosts(posts, maxTokensPerChunk = 3000) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let currentChunk = [];
|
|
17
|
+
let currentTokens = 0;
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
20
|
+
for (const post of posts) {
|
|
21
|
+
const estimatedTokens = Math.ceil(post.content.length / 4) + 10;
|
|
22
|
+
if (currentTokens + estimatedTokens > maxTokensPerChunk) {
|
|
23
|
+
chunks.push(currentChunk);
|
|
24
|
+
currentChunk = [];
|
|
25
|
+
currentTokens = 0;
|
|
26
|
+
}
|
|
27
|
+
currentChunk.push(post);
|
|
28
|
+
currentTokens += estimatedTokens;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (currentChunk.length > 0) {
|
|
32
|
+
chunks.push(currentChunk);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return chunks;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function summarizeChunk(chunk, openai, settings) {
|
|
39
|
+
const threadText = formatPosts(chunk);
|
|
40
|
+
const response = await openai.chat.completions.create({
|
|
41
|
+
model: settings.model || 'gpt-3.5-turbo',
|
|
42
|
+
messages: [
|
|
43
|
+
{
|
|
44
|
+
role: 'system',
|
|
45
|
+
content: 'You summarize discussion forum threads into concise summaries.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
role: 'user',
|
|
49
|
+
content: `Summarize the following discussion thread:\n\n${threadText}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
temperature: 0.5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return response.choices[0].message.content.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.summarizeTopic = async function(tid, openai, settings) {
|
|
59
|
+
const allPids = await topics.getPids(tid);
|
|
60
|
+
|
|
61
|
+
const userMap = {};
|
|
62
|
+
|
|
63
|
+
const chunkSummaries = [];
|
|
64
|
+
|
|
65
|
+
await batch.processArray(allPids, async function (pids) {
|
|
66
|
+
const postData = await posts.getPostsFields(pids, ['uid', 'content']);
|
|
67
|
+
|
|
68
|
+
const missingUids = [];
|
|
69
|
+
postData.forEach((p) => {
|
|
70
|
+
if (!userMap.hasOwnProperty(p.uid)) {
|
|
71
|
+
missingUids.push(p.uid)
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const userData = await user.getUsersFields(missingUids, ['username']);
|
|
75
|
+
userData.forEach((u) => {
|
|
76
|
+
userMap[u.uid] = u.displayname;
|
|
77
|
+
});
|
|
78
|
+
postData.forEach((p) => {
|
|
79
|
+
p.username = userMap[p.uid];
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const chunks = chunkPosts(postData);
|
|
83
|
+
|
|
84
|
+
chunkSummaries.push(...await Promise.all(chunks.map(async (chunk) => {
|
|
85
|
+
return summarizeChunk(chunk, openai, settings)
|
|
86
|
+
})));
|
|
87
|
+
}, {
|
|
88
|
+
batch: 500,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (chunkSummaries.length === 1) {
|
|
92
|
+
return chunkSummaries[0];
|
|
93
|
+
}
|
|
94
|
+
// Final summary from all summaries
|
|
95
|
+
const finalInput = chunkSummaries.join("\n\n");
|
|
96
|
+
const finalResponse = await openai.chat.completions.create({
|
|
97
|
+
model: settings.model || 'gpt-3.5-turbo',
|
|
98
|
+
messages: [
|
|
99
|
+
{
|
|
100
|
+
role: 'system',
|
|
101
|
+
content: 'You are an assistant that summarizes forum thread summaries into a single cohesive summary.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
role: 'user',
|
|
105
|
+
content: `Here are summaries of parts of a forum discussion:\n\n${finalInput}\n\nPlease write a final, concise summary of the full discussion.`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
temperature: 0.5,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return finalResponse.choices[0].message.content.trim();
|
|
112
|
+
};
|
package/library.js
CHANGED
|
@@ -10,12 +10,15 @@ const controllers = require('./lib/controllers');
|
|
|
10
10
|
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
11
11
|
const socketHelpers = require.main.require('./src/socket.io/helpers');
|
|
12
12
|
const topics = require.main.require('./src/topics');
|
|
13
|
+
const posts = require.main.require('./src/posts');
|
|
13
14
|
const user = require.main.require('./src/user');
|
|
14
15
|
const messaging = require.main.require('./src/messaging');
|
|
15
16
|
const api = require.main.require('./src/api');
|
|
16
17
|
const privileges = require.main.require('./src/privileges');
|
|
17
18
|
const groups = require.main.require('./src/groups');
|
|
18
19
|
const sockets = require.main.require('./src/socket.io');
|
|
20
|
+
const socketPlugins = require.main.require('./src/socket.io/plugins');
|
|
21
|
+
const summary = require('./lib/summary');
|
|
19
22
|
|
|
20
23
|
const plugin = module.exports;
|
|
21
24
|
|
|
@@ -166,21 +169,21 @@ plugin.actionMessagingSave = async function (hookData) {
|
|
|
166
169
|
}
|
|
167
170
|
};
|
|
168
171
|
|
|
169
|
-
async function canUseOpenAI(uid, settings) {
|
|
170
|
-
if (!await checkReputation(uid, settings)) {
|
|
172
|
+
async function canUseOpenAI(uid, settings, silent = false) {
|
|
173
|
+
if (!await checkReputation(uid, settings, silent)) {
|
|
171
174
|
return false;
|
|
172
175
|
}
|
|
173
|
-
if (!await checkGroupMembership(uid, settings)) {
|
|
176
|
+
if (!await checkGroupMembership(uid, settings, silent)) {
|
|
174
177
|
return false;
|
|
175
178
|
}
|
|
176
179
|
return true;
|
|
177
180
|
}
|
|
178
181
|
|
|
179
|
-
async function checkReputation(uid, settings) {
|
|
182
|
+
async function checkReputation(uid, settings, silent) {
|
|
180
183
|
const reputation = await user.getUserField(uid, 'reputation');
|
|
181
184
|
const hasEnoughRep = parseInt(settings.minimumReputation, 10) === 0 || parseInt(reputation, 10) >= parseInt(settings.minimumReputation, 10);
|
|
182
185
|
|
|
183
|
-
if (!hasEnoughRep) {
|
|
186
|
+
if (!hasEnoughRep && !silent) {
|
|
184
187
|
sockets.server.in(`uid_${uid}`).emit('event:alert', {
|
|
185
188
|
type: 'danger',
|
|
186
189
|
title: '[[global:alert.error]]',
|
|
@@ -190,7 +193,7 @@ async function checkReputation(uid, settings) {
|
|
|
190
193
|
return hasEnoughRep;
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
async function checkGroupMembership(uid, settings) {
|
|
196
|
+
async function checkGroupMembership(uid, settings, silent) {
|
|
194
197
|
let allowedGroups = [];
|
|
195
198
|
try {
|
|
196
199
|
allowedGroups = JSON.parse(settings.allowedGroups) || [];
|
|
@@ -205,7 +208,7 @@ async function checkGroupMembership(uid, settings) {
|
|
|
205
208
|
|
|
206
209
|
const isMembers = await groups.isMemberOfGroups(uid, allowedGroups);
|
|
207
210
|
const memberOfAny = isMembers.includes(true);
|
|
208
|
-
if (!memberOfAny) {
|
|
211
|
+
if (!memberOfAny && !silent) {
|
|
209
212
|
sockets.server.in(`uid_${uid}`).emit('event:alert', {
|
|
210
213
|
type: 'danger',
|
|
211
214
|
title: '[[global:alert.error]]',
|
|
@@ -262,3 +265,29 @@ plugin.addAdminNavigation = (header) => {
|
|
|
262
265
|
return header;
|
|
263
266
|
};
|
|
264
267
|
|
|
268
|
+
plugin.filterTopicThreadTools = async (hookData) => {
|
|
269
|
+
if (!await canUseOpenAI(hookData.uid, await getSettings(), true)) {
|
|
270
|
+
return hookData;
|
|
271
|
+
}
|
|
272
|
+
hookData.tools.push({
|
|
273
|
+
class: 'openai-summarize-topic',
|
|
274
|
+
icon: 'fa-wand-magic-sparkles',
|
|
275
|
+
title: '[[openai:summarize-topic]]',
|
|
276
|
+
});
|
|
277
|
+
return hookData;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
socketPlugins.openai = {};
|
|
281
|
+
|
|
282
|
+
socketPlugins.openai.summarizeTopic = async function (socket, data) {
|
|
283
|
+
const { tid } = data;
|
|
284
|
+
if (!await privileges.topics.can('topics:read', tid, socket.uid)) {
|
|
285
|
+
throw new Error('[[error:no-privileges]]');
|
|
286
|
+
}
|
|
287
|
+
const settings = await getSettings();
|
|
288
|
+
if (!await canUseOpenAI(socket.uid, settings)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return summary.summarizeTopic(tid, openai, settings);
|
|
293
|
+
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
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
|
+
"scripts": [
|
|
13
|
+
"./public/lib/main.js"
|
|
10
14
|
],
|
|
11
15
|
"modules": {
|
|
12
16
|
"../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
|
|