nodebb-plugin-search-agent 0.0.6 → 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.
- package/lib/controllers.js +2 -2
- package/lib/searchHandler.js +33 -11
- package/package.json +1 -1
- package/public/lib/main.js +4 -4
- package/scss/search-agent.scss +4 -2
package/lib/controllers.js
CHANGED
|
@@ -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 >
|
|
42
|
-
return helpers.formatApiResponse(400, res, new Error('Query exceeds maximum length of
|
|
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 {
|
package/lib/searchHandler.js
CHANGED
|
@@ -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
|
|
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(
|
|
161
|
-
const snippet = raw.length > 0 ? `\n תוכן: "${raw.slice(0,
|
|
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');
|
|
@@ -168,7 +177,8 @@ async function reRankWithAI(queryText, candidates, topicMap, apiKey, model, maxR
|
|
|
168
177
|
'לכל נושא ברשימה, דרג את הרלוונטיות שלו לשאלת המשתמש בסקלה 0-10: ' +
|
|
169
178
|
'10 = עונה ישירות ובאופן מלא. 7-9 = עונה על חלק משמעותי. 0-6 = לא רלוונטי. ' +
|
|
170
179
|
'החזר אך ורק JSON תקני במבנה: {"tid": ציון, ...} — לדוגמה: {"42": 9, "15": 3}. ' +
|
|
171
|
-
'אין להוסיף הסברים, טקסט נוסף, או עיצוב מחוץ ל-JSON.'
|
|
180
|
+
'אין להוסיף הסברים, טקסט נוסף, או עיצוב מחוץ ל-JSON.'+
|
|
181
|
+
'הוסף שדה נוסף "scoreExplanation" עם משפט קצר שמסביר למה נושא עם ציון נמוך לא רלוונטי, כדי שנוכל להבין את שיקול הדעת של המודל.';
|
|
172
182
|
|
|
173
183
|
const userMessage =
|
|
174
184
|
`שאלת המשתמש: "${queryText}"\n\nנושאים:\n${candidateList}`;
|
|
@@ -242,14 +252,17 @@ async function searchTopics(queryText) {
|
|
|
242
252
|
topics.filter(t => t && t.tid && !t.deleted).map(t => [String(t.tid), t])
|
|
243
253
|
);
|
|
244
254
|
|
|
245
|
-
// Build snippet map:
|
|
246
|
-
//
|
|
247
|
-
const
|
|
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
|
|
248
258
|
for (const r of vectorResults) {
|
|
249
259
|
const key = String(r.topic_id);
|
|
250
|
-
if (!
|
|
260
|
+
if (!contentsByTid[key]) contentsByTid[key] = [];
|
|
261
|
+
if (r.content) contentsByTid[key].push(r.content);
|
|
251
262
|
}
|
|
252
|
-
|
|
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 = {};
|
|
253
266
|
const topicsWithMainPid = topics.filter(t => t && t.tid && !t.deleted && t.mainPid);
|
|
254
267
|
if (topicsWithMainPid.length > 0) {
|
|
255
268
|
const mainContents = await Posts.getPostsFields(
|
|
@@ -257,8 +270,17 @@ async function searchTopics(queryText) {
|
|
|
257
270
|
['pid', 'content']
|
|
258
271
|
);
|
|
259
272
|
for (let i = 0; i < topicsWithMainPid.length; i++) {
|
|
260
|
-
const
|
|
261
|
-
|
|
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');
|
|
262
284
|
}
|
|
263
285
|
}
|
|
264
286
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-search-agent",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
package/public/lib/main.js
CHANGED
|
@@ -102,14 +102,14 @@ function buildPanelHtml() {
|
|
|
102
102
|
</div>
|
|
103
103
|
|
|
104
104
|
<div class="search-agent-panel__footer">
|
|
105
|
-
<
|
|
105
|
+
<textarea
|
|
106
106
|
id="search-agent-input"
|
|
107
107
|
class="search-agent-input"
|
|
108
|
-
|
|
108
|
+
rows="2"
|
|
109
109
|
placeholder="[[search-agent:input-placeholder]]"
|
|
110
|
-
maxlength="
|
|
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>
|
package/scss/search-agent.scss
CHANGED
|
@@ -174,7 +174,7 @@ $sa-transition: 0.25s ease;
|
|
|
174
174
|
|
|
175
175
|
&__footer {
|
|
176
176
|
display: flex;
|
|
177
|
-
align-items:
|
|
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:
|
|
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 {
|