nodebb-plugin-search-agent 0.0.7 → 0.0.8

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.
@@ -38,8 +38,8 @@ controllers.handleQuery = async function (req, res, helpers) {
38
38
  if (!queryText) {
39
39
  return helpers.formatApiResponse(400, res, new Error('Missing or empty "query" field.'));
40
40
  }
41
- if (queryText.length > 500) {
42
- return helpers.formatApiResponse(400, res, new Error('Query exceeds maximum length of 500 characters.'));
41
+ if (queryText.length > 2000) {
42
+ return helpers.formatApiResponse(400, res, new Error('Query exceeds maximum length of 2000 characters.'));
43
43
  }
44
44
 
45
45
  try {
@@ -84,9 +84,18 @@ async function getIndex(topicLimit) {
84
84
 
85
85
  // ─── OpenAI helper ────────────────────────────────────────────────────────────
86
86
 
87
+ // Reasoning models (o1, o3, o4 series) do not support temperature != default.
88
+ function isReasoningModel(model) {
89
+ return /^o\d/i.test(model);
90
+ }
91
+
87
92
  function callOpenAI(apiKey, model, messages) {
88
93
  return new Promise((resolve, reject) => {
89
- const body = JSON.stringify({ model, messages, temperature: 0 });
94
+ const payload = { model, messages };
95
+ if (!isReasoningModel(model)) {
96
+ payload.temperature = 0;
97
+ }
98
+ const body = JSON.stringify(payload);
90
99
  const options = {
91
100
  hostname: 'api.openai.com',
92
101
  path: '/v1/chat/completions',
@@ -157,8 +166,8 @@ async function reRankWithAI(queryText, candidates, topicMap, apiKey, model, maxR
157
166
  const candidateList = candidates
158
167
  .map((c) => {
159
168
  const title = (topicMap[String(c.tid)] || {}).title || '';
160
- const raw = (snippetByTid[String(c.tid)] || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
161
- const snippet = raw.length > 0 ? `\n תוכן: "${raw.slice(0, 500)}"` : '';
169
+ const raw = (snippetByTid[String(c.tid)] || '').replace(/<[^>]*>/g, ' ').replace(/[ \t]+/g, ' ').trim();
170
+ const snippet = raw.length > 0 ? `\n תוכן: "${raw.slice(0, 1500)}"` : '';
162
171
  return `[tid:${c.tid}] ${title}${snippet}`;
163
172
  })
164
173
  .join('\n\n');
@@ -243,14 +252,17 @@ async function searchTopics(queryText) {
243
252
  topics.filter(t => t && t.tid && !t.deleted).map(t => [String(t.tid), t])
244
253
  );
245
254
 
246
- // Build snippet map: prefer the main post body (which describes what the topic is about).
247
- // Fall back to the best vector-matched post content if no main post is available.
248
- const fallbackSnippetByTid = {};
255
+ // Build snippet map: collect ALL matched post contents per topic so the AI
256
+ // sees full-topic context, not just the first hit.
257
+ const contentsByTid = {}; // tid -> ordered array of matched post contents
249
258
  for (const r of vectorResults) {
250
259
  const key = String(r.topic_id);
251
- if (!fallbackSnippetByTid[key]) fallbackSnippetByTid[key] = r.content;
260
+ if (!contentsByTid[key]) contentsByTid[key] = [];
261
+ if (r.content) contentsByTid[key].push(r.content);
252
262
  }
253
- const snippetByTid = { ...fallbackSnippetByTid };
263
+ // Prepend the main post so the AI sees the topic's opening question first,
264
+ // then the matched reply posts follow (which may contain the answer).
265
+ const snippetByTid = {};
254
266
  const topicsWithMainPid = topics.filter(t => t && t.tid && !t.deleted && t.mainPid);
255
267
  if (topicsWithMainPid.length > 0) {
256
268
  const mainContents = await Posts.getPostsFields(
@@ -258,8 +270,17 @@ async function searchTopics(queryText) {
258
270
  ['pid', 'content']
259
271
  );
260
272
  for (let i = 0; i < topicsWithMainPid.length; i++) {
261
- const content = mainContents[i] && mainContents[i].content;
262
- if (content) snippetByTid[String(topicsWithMainPid[i].tid)] = content;
273
+ const mainContent = mainContents[i] && mainContents[i].content;
274
+ const tid = String(topicsWithMainPid[i].tid);
275
+ const matched = (contentsByTid[tid] || []).filter(s => s !== mainContent);
276
+ const parts = [mainContent, ...matched].filter(Boolean);
277
+ if (parts.length > 0) snippetByTid[tid] = parts.join('\n\n');
278
+ }
279
+ }
280
+ // Topics without a mainPid — use matched posts only
281
+ for (const [tid, contents] of Object.entries(contentsByTid)) {
282
+ if (!snippetByTid[tid] && contents.length > 0) {
283
+ snippetByTid[tid] = contents.join('\n\n');
263
284
  }
264
285
  }
265
286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-search-agent",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "NodeBB plugin that adds a floating chat assistant to help users find relevant forum topics using TF-IDF text similarity",
5
5
  "main": "library.js",
6
6
  "author": "Racheli Bayfus",
@@ -102,14 +102,14 @@ function buildPanelHtml() {
102
102
  </div>
103
103
 
104
104
  <div class="search-agent-panel__footer">
105
- <input
105
+ <textarea
106
106
  id="search-agent-input"
107
107
  class="search-agent-input"
108
- type="text"
108
+ rows="2"
109
109
  placeholder="[[search-agent:input-placeholder]]"
110
- maxlength="500"
110
+ maxlength="2000"
111
111
  autocomplete="off"
112
- />
112
+ ></textarea>
113
113
  <button id="search-agent-send" class="search-agent-send" aria-label="[[search-agent:send-label]]">
114
114
  <i class="fa fa-paper-plane"></i>
115
115
  </button>
@@ -174,7 +174,7 @@ $sa-transition: 0.25s ease;
174
174
 
175
175
  &__footer {
176
176
  display: flex;
177
- align-items: center;
177
+ align-items: flex-end;
178
178
  gap: 8px;
179
179
  padding: 10px 12px;
180
180
  border-top: 1px solid $sa-border;
@@ -187,11 +187,13 @@ $sa-transition: 0.25s ease;
187
187
  .search-agent-input {
188
188
  flex: 1;
189
189
  border: 1px solid $sa-border;
190
- border-radius: 20px;
190
+ border-radius: 12px;
191
191
  padding: 8px 14px;
192
192
  font-size: 14px;
193
193
  color: $sa-text-dark;
194
194
  outline: none;
195
+ resize: none;
196
+ line-height: 1.4;
195
197
  transition: border-color $sa-transition, box-shadow $sa-transition;
196
198
 
197
199
  &:focus {