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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-openai",
3
- "version": "1.2.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
@@ -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