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.
@@ -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(message) {
234
+ async function chatComplete(messages) {
231
235
  if (!openai) {
232
236
  throw new Error('API not created!');
233
237
  }
234
- const isConversation = Array.isArray(message);
235
- const { model } = await meta.settings.get('openai');
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: isConversation ? message : [{ role: 'user', content: message }],
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-openai",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "A NodeBB OpenAI plugin",
5
5
  "main": "library.js",
6
6
  "repository": {
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