nodebb-plugin-openai 1.1.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 +51 -11
- 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 +5 -0
|
@@ -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
|
|
|
@@ -26,6 +29,7 @@ const defaults = {
|
|
|
26
29
|
model: 'gpt-3.5-turbo',
|
|
27
30
|
minimumReputation: 0,
|
|
28
31
|
allowedGroups: '[]',
|
|
32
|
+
systemPrompt: 'You are a helpful assistant',
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
|
|
@@ -165,21 +169,21 @@ plugin.actionMessagingSave = async function (hookData) {
|
|
|
165
169
|
}
|
|
166
170
|
};
|
|
167
171
|
|
|
168
|
-
async function canUseOpenAI(uid, settings) {
|
|
169
|
-
if (!await checkReputation(uid, settings)) {
|
|
172
|
+
async function canUseOpenAI(uid, settings, silent = false) {
|
|
173
|
+
if (!await checkReputation(uid, settings, silent)) {
|
|
170
174
|
return false;
|
|
171
175
|
}
|
|
172
|
-
if (!await checkGroupMembership(uid, settings)) {
|
|
176
|
+
if (!await checkGroupMembership(uid, settings, silent)) {
|
|
173
177
|
return false;
|
|
174
178
|
}
|
|
175
179
|
return true;
|
|
176
180
|
}
|
|
177
181
|
|
|
178
|
-
async function checkReputation(uid, settings) {
|
|
182
|
+
async function checkReputation(uid, settings, silent) {
|
|
179
183
|
const reputation = await user.getUserField(uid, 'reputation');
|
|
180
184
|
const hasEnoughRep = parseInt(settings.minimumReputation, 10) === 0 || parseInt(reputation, 10) >= parseInt(settings.minimumReputation, 10);
|
|
181
185
|
|
|
182
|
-
if (!hasEnoughRep) {
|
|
186
|
+
if (!hasEnoughRep && !silent) {
|
|
183
187
|
sockets.server.in(`uid_${uid}`).emit('event:alert', {
|
|
184
188
|
type: 'danger',
|
|
185
189
|
title: '[[global:alert.error]]',
|
|
@@ -189,7 +193,7 @@ async function checkReputation(uid, settings) {
|
|
|
189
193
|
return hasEnoughRep;
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
async function checkGroupMembership(uid, settings) {
|
|
196
|
+
async function checkGroupMembership(uid, settings, silent) {
|
|
193
197
|
let allowedGroups = [];
|
|
194
198
|
try {
|
|
195
199
|
allowedGroups = JSON.parse(settings.allowedGroups) || [];
|
|
@@ -204,7 +208,7 @@ async function checkGroupMembership(uid, settings) {
|
|
|
204
208
|
|
|
205
209
|
const isMembers = await groups.isMemberOfGroups(uid, allowedGroups);
|
|
206
210
|
const memberOfAny = isMembers.includes(true);
|
|
207
|
-
if (!memberOfAny) {
|
|
211
|
+
if (!memberOfAny && !silent) {
|
|
208
212
|
sockets.server.in(`uid_${uid}`).emit('event:alert', {
|
|
209
213
|
type: 'danger',
|
|
210
214
|
title: '[[global:alert.error]]',
|
|
@@ -227,15 +231,25 @@ async function getMessageIds(roomId, uid, start, stop) {
|
|
|
227
231
|
);
|
|
228
232
|
}
|
|
229
233
|
|
|
230
|
-
async function chatComplete(
|
|
234
|
+
async function chatComplete(messages) {
|
|
231
235
|
if (!openai) {
|
|
232
236
|
throw new Error('API not created!');
|
|
233
237
|
}
|
|
234
|
-
const isConversation = Array.isArray(
|
|
235
|
-
const { model } = await
|
|
238
|
+
const isConversation = Array.isArray(messages);
|
|
239
|
+
const { model, systemPrompt } = await getSettings();
|
|
240
|
+
const conversation = [];
|
|
241
|
+
if (systemPrompt) {
|
|
242
|
+
conversation.push({ role: 'system', content: systemPrompt });
|
|
243
|
+
}
|
|
244
|
+
if (isConversation) {
|
|
245
|
+
conversation.push(...messages);
|
|
246
|
+
} else {
|
|
247
|
+
conversation.push({ role: 'user', content: messages });
|
|
248
|
+
}
|
|
249
|
+
|
|
236
250
|
const chatCompletion = await openai.chat.completions.create({
|
|
237
251
|
model: model || 'gpt-3.5-turbo',
|
|
238
|
-
messages:
|
|
252
|
+
messages: conversation,
|
|
239
253
|
});
|
|
240
254
|
|
|
241
255
|
return chatCompletion.choices[0]?.message?.content;
|
|
@@ -251,3 +265,29 @@ plugin.addAdminNavigation = (header) => {
|
|
|
251
265
|
return header;
|
|
252
266
|
};
|
|
253
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
|
|
@@ -40,7 +40,12 @@
|
|
|
40
40
|
<option value="gpt-4">gpt-4</option>
|
|
41
41
|
</select>
|
|
42
42
|
</div>
|
|
43
|
+
<div class="">
|
|
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" rows="8"></textarea>
|
|
46
|
+
</div>
|
|
43
47
|
</div>
|
|
48
|
+
|
|
44
49
|
<div class="">
|
|
45
50
|
<h5 class="fw-bold tracking-tight settings-header">Restrictions</h5>
|
|
46
51
|
|