@tiledesk/tiledesk-server 2.18.1 → 2.18.4
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/CHANGELOG.md +11 -1
- package/app.js +2 -0
- package/config/kb/situatedContext.js +1 -1
- package/docs/routes-answered.md +153 -0
- package/migrations/1757601159298-project_user_role_type.js +92 -33
- package/models/kb_setting.js +78 -7
- package/package.json +2 -2
- package/routes/answered.js +227 -0
- package/routes/kb.js +50 -20
- package/routes/unanswered.js +31 -18
- package/routes/webhook.js +8 -4
- package/services/aiManager.js +12 -6
- package/test/kbRoute.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
🚀 IN PRODUCTION 🚀
|
|
6
6
|
(https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
|
|
7
7
|
|
|
8
|
-
# 2.18.
|
|
8
|
+
# 2.18.4
|
|
9
|
+
- Added HyDE support for Knowledge Base Q&A
|
|
10
|
+
- Introduced Cache (cRag) functionality in Knowledge Base Q&A
|
|
11
|
+
- Enabled Situated Context for enhanced Knowledge Base content indexing
|
|
12
|
+
- Bumped tybot-connector version to 2.0.48
|
|
13
|
+
|
|
14
|
+
# 2.18.3
|
|
9
15
|
- Added permissions logic
|
|
10
16
|
- Added custom roles support
|
|
17
|
+
- Add answered questions functionality
|
|
18
|
+
- Added AnsweredQuestion schema with TTL index for automatic deletion.
|
|
19
|
+
- Updated UnansweredQuestion schema to include additional fields and improved query handling for searching and sorting.
|
|
20
|
+
- Enhanced the deletion process for unanswered questions.
|
|
11
21
|
|
|
12
22
|
# 2.17.4
|
|
13
23
|
- Refactor error handling and code structure in webhook.js
|
package/app.js
CHANGED
|
@@ -130,6 +130,7 @@ var integration = require('./routes/integration')
|
|
|
130
130
|
var kbsettings = require('./routes/kbsettings');
|
|
131
131
|
var kb = require('./routes/kb');
|
|
132
132
|
var unanswered = require('./routes/unanswered');
|
|
133
|
+
var answered = require('./routes/answered');
|
|
133
134
|
|
|
134
135
|
// var admin = require('./routes/admin');
|
|
135
136
|
var faqpub = require('./routes/faqpub');
|
|
@@ -643,6 +644,7 @@ app.use('/:projectid/mcp', [passport.authenticate(['basic', 'jwt'], { session: f
|
|
|
643
644
|
|
|
644
645
|
app.use('/:projectid/kbsettings', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], kbsettings);
|
|
645
646
|
app.use('/:projectid/kb/unanswered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], unanswered);
|
|
647
|
+
app.use('/:projectid/kb/answered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], answered);
|
|
646
648
|
app.use('/:projectid/kb', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], kb);
|
|
647
649
|
|
|
648
650
|
app.use('/:projectid/logs', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('agent')], logs);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
enable: process.env.SITUATED_CONTEXT_ENABLE === "true",
|
|
3
3
|
provider: process.env.SITUATED_CONTEXT_PROVIDER || "openai",
|
|
4
|
-
model: process.env.SITUATED_CONTEXT_MODEL || "gpt-
|
|
4
|
+
model: process.env.SITUATED_CONTEXT_MODEL || "gpt-5.4-nano",
|
|
5
5
|
api_key: ""
|
|
6
6
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Route `kb/answered` — domande con risposta (Knowledge Base)
|
|
2
|
+
|
|
3
|
+
Documentazione per [`routes/answered.js`](../routes/answered.js).
|
|
4
|
+
|
|
5
|
+
## Scopo
|
|
6
|
+
|
|
7
|
+
API REST per gestire le **domande già risposte** associate a un **namespace** della Knowledge Base del progetto. I documenti sono persistiti nel modello Mongoose `AnsweredQuestion` (vedi [`models/kb_setting.js`](../models/kb_setting.js)).
|
|
8
|
+
|
|
9
|
+
Ogni operazione è vincolata al **progetto corrente** (`req.projectid`, derivato dal segmento `:projectid` nell’URL) e alla validità del **namespace** (deve esistere un `Namespace` con `id` uguale al `namespace` richiesto e `id_project` uguale al progetto).
|
|
10
|
+
|
|
11
|
+
## Montaggio e autenticazione
|
|
12
|
+
|
|
13
|
+
In [`app.js`](../app.js) il router è montato così:
|
|
14
|
+
|
|
15
|
+
- **Path base:** `/:projectid/kb/answered`
|
|
16
|
+
- **Middleware:** `passport.authenticate(['basic', 'jwt'])`, `validtoken`, `roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])`
|
|
17
|
+
|
|
18
|
+
Sono quindi richiesti autenticazione (Basic o JWT), token valido e ruolo **admin** oppure tipi **bot** / **subscription**.
|
|
19
|
+
|
|
20
|
+
Esempio di URL completo (sviluppo locale):
|
|
21
|
+
|
|
22
|
+
```http
|
|
23
|
+
GET http://localhost:3000/{projectId}/kb/answered/{namespace}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sostituire `{projectId}` con l’ID MongoDB del progetto.
|
|
27
|
+
|
|
28
|
+
## Modello dati (`AnsweredQuestion`)
|
|
29
|
+
|
|
30
|
+
| Campo | Tipo | Note |
|
|
31
|
+
|---------------|----------|-------------------------------------------|
|
|
32
|
+
| `id_project` | string | Impostato dal server dal contesto progetto |
|
|
33
|
+
| `namespace` | string | ID del namespace (validato contro `Namespace`) |
|
|
34
|
+
| `question` | string | Obbligatorio |
|
|
35
|
+
| `answer` | string | Obbligatorio |
|
|
36
|
+
| `tokens` | number | Opzionale |
|
|
37
|
+
| `request_id` | string | Opzionale |
|
|
38
|
+
| `created_at` / `updated_at` | date | Da `timestamps: true` |
|
|
39
|
+
|
|
40
|
+
Indici rilevanti: TTL su `created_at`, indice composto `(id_project, namespace, created_at)`, indice **testo** su `question` e `answer` (pesi question 5, answer 1) per la ricerca full-text.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Endpoint
|
|
45
|
+
|
|
46
|
+
Tutti i path qui sotto sono relativi a `/:projectid/kb/answered`.
|
|
47
|
+
|
|
48
|
+
### `POST /`
|
|
49
|
+
|
|
50
|
+
Aggiunge una nuova domanda risposta.
|
|
51
|
+
|
|
52
|
+
**Body JSON**
|
|
53
|
+
|
|
54
|
+
| Campo | Obbligatorio | Descrizione |
|
|
55
|
+
|-------------|--------------|--------------------|
|
|
56
|
+
| `namespace` | sì | ID namespace |
|
|
57
|
+
| `question` | sì | Testo domanda |
|
|
58
|
+
| `answer` | sì | Testo risposta |
|
|
59
|
+
| `tokens` | no | Numero opzionale |
|
|
60
|
+
| `request_id`| no | Riferimento richiesta |
|
|
61
|
+
|
|
62
|
+
**Risposte**
|
|
63
|
+
|
|
64
|
+
| HTTP | Significato |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| 200 | Documento salvato (corpo = documento MongoDB creato) |
|
|
67
|
+
| 400 | Parametri mancanti (`namespace`, `question`, `answer`) |
|
|
68
|
+
| 403 | Namespace non appartenente al progetto |
|
|
69
|
+
| 500 | Errore server |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### `GET /:namespace`
|
|
74
|
+
|
|
75
|
+
Elenco paginato delle domande risposte per il namespace.
|
|
76
|
+
|
|
77
|
+
**Query**
|
|
78
|
+
|
|
79
|
+
| Parametro | Default | Descrizione |
|
|
80
|
+
|--------------|---------|-------------|
|
|
81
|
+
| `page` | `0` | Pagina (0-based) |
|
|
82
|
+
| `limit` | `20` | Elementi per pagina |
|
|
83
|
+
| `sortField` | `created_at` | Campo ordinamento |
|
|
84
|
+
| `direction` | `-1` | `1` crescente, `-1` decrescente |
|
|
85
|
+
| `search` | — | Se presente, attiva ricerca **full-text** (`$text`); l’ordinamento usa lo score di testo |
|
|
86
|
+
|
|
87
|
+
**Risposta 200**
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"count": 0,
|
|
92
|
+
"questions": [],
|
|
93
|
+
"query": {
|
|
94
|
+
"page": 0,
|
|
95
|
+
"limit": 20,
|
|
96
|
+
"sortField": "created_at",
|
|
97
|
+
"direction": -1,
|
|
98
|
+
"search": "..."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Altri codici:** `400` (namespace mancante), `403` (namespace non valido per il progetto), `500`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `DELETE /:id`
|
|
108
|
+
|
|
109
|
+
Elimina una singola domanda per `_id` MongoDB.
|
|
110
|
+
|
|
111
|
+
**Risposte:** `200` con `{ success, message }`, `404` se non trovata (o non del progetto), `500`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### `DELETE /namespace/:namespace`
|
|
116
|
+
|
|
117
|
+
Elimina **tutte** le domande risposte per `namespace` nel progetto corrente.
|
|
118
|
+
|
|
119
|
+
**Risposta 200:** `{ success, count: <deletedCount>, message }`.
|
|
120
|
+
|
|
121
|
+
**403** se il namespace non appartiene al progetto. **500** in errore.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `GET /count/:namespace`
|
|
126
|
+
|
|
127
|
+
Restituisce il conteggio documenti per `id_project` + `namespace`.
|
|
128
|
+
|
|
129
|
+
**Risposta 200:** `{ count: <number> }`.
|
|
130
|
+
|
|
131
|
+
**400** / **403** / **500** come negli altri endpoint con validazione namespace.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Funzione interna `validateNamespace(id_project, namespace_id)`
|
|
136
|
+
|
|
137
|
+
Verifica che esista un documento `Namespace` con `id_project` e `id: namespace_id`. Usata in creazione, lettura elenco, cancellazione bulk e conteggio.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Nota sull’ordine delle route Express
|
|
142
|
+
|
|
143
|
+
Nel file è definito `GET /:namespace` **prima** di `GET /count/:namespace`. In Express la prima route che corrisponde vince: una richiesta del tipo `GET .../kb/answered/count/<namespace>` può essere interpretata come `GET /:namespace` con `namespace` uguale alla stringa letterale `count`, invece che come endpoint di conteggio.
|
|
144
|
+
|
|
145
|
+
Per far funzionare l’URL `GET /count/:namespace` come previsto dal codice, in genere occorre registrare **`GET /count/:namespace` prima di `GET /:namespace`**, oppure usare un prefisso path diverso per uno dei due. Verificare il comportamento in ambiente reale o adeguare l’ordine delle route se il conteggio non risponde come atteso.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Riferimenti
|
|
150
|
+
|
|
151
|
+
- Router: [`routes/answered.js`](../routes/answered.js)
|
|
152
|
+
- Modello e indici: [`models/kb_setting.js`](../models/kb_setting.js) (`AnsweredQuestionSchema`)
|
|
153
|
+
- Montaggio app: [`app.js`](../app.js) (`/:projectid/kb/answered`)
|
|
@@ -1,45 +1,104 @@
|
|
|
1
1
|
var Project_user = require("../models/project_user");
|
|
2
2
|
var winston = require('../config/winston');
|
|
3
3
|
|
|
4
|
+
const BATCH_SIZE = 100;
|
|
5
|
+
/** Log a progress line every this many bulkWrite rounds (plus always after the 1st). */
|
|
6
|
+
const PROGRESS_LOG_EVERY_BATCHES = 10;
|
|
7
|
+
|
|
8
|
+
/** Only documents that still need roleType (idempotent re-runs). */
|
|
9
|
+
const WITHOUT_ROLE_TYPE = {
|
|
10
|
+
$or: [{ roleType: { $exists: false } }, { roleType: null }]
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const AGENT_ROLES_FILTER = {
|
|
14
|
+
$and: [
|
|
15
|
+
{
|
|
16
|
+
$or: [
|
|
17
|
+
{ role: 'agent' },
|
|
18
|
+
{ role: 'supervisor' },
|
|
19
|
+
{ role: 'admin' },
|
|
20
|
+
{ role: 'owner' }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
WITHOUT_ROLE_TYPE
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const USER_ROLES_FILTER = {
|
|
28
|
+
$and: [{ $or: [{ role: 'user' }, { role: 'guest' }] }, WITHOUT_ROLE_TYPE]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function batchSetRoleType(filter, roleType, phaseLabel) {
|
|
32
|
+
const totalMatching = await Project_user.countDocuments(filter);
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
winston.info(
|
|
35
|
+
`[project_user_role_type] ${phaseLabel}: ${totalMatching} documents match filter; streaming _id + bulkWrite in chunks of ${BATCH_SIZE}`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
let modified = 0;
|
|
39
|
+
let scanned = 0;
|
|
40
|
+
let bulkRounds = 0;
|
|
41
|
+
const cursor = Project_user.find(filter).select('_id').lean().cursor();
|
|
42
|
+
let batch = [];
|
|
43
|
+
|
|
44
|
+
function logProgress(reason) {
|
|
45
|
+
const elapsedSec = ((Date.now() - started) / 1000).toFixed(1);
|
|
46
|
+
winston.info(
|
|
47
|
+
`[project_user_role_type] ${phaseLabel} ${reason}: scanned=${scanned}/${totalMatching}, modifiedThisPhase=${modified}, ${elapsedSec}s elapsed`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for await (const doc of cursor) {
|
|
52
|
+
scanned++;
|
|
53
|
+
batch.push({
|
|
54
|
+
updateOne: {
|
|
55
|
+
filter: { _id: doc._id, ...WITHOUT_ROLE_TYPE },
|
|
56
|
+
update: { $set: { roleType } }
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (batch.length >= BATCH_SIZE) {
|
|
60
|
+
const result = await Project_user.bulkWrite(batch);
|
|
61
|
+
modified += result.modifiedCount;
|
|
62
|
+
batch = [];
|
|
63
|
+
bulkRounds++;
|
|
64
|
+
if (bulkRounds === 1 || bulkRounds % PROGRESS_LOG_EVERY_BATCHES === 0) {
|
|
65
|
+
logProgress(`progress (bulk round ${bulkRounds})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (batch.length > 0) {
|
|
70
|
+
const result = await Project_user.bulkWrite(batch);
|
|
71
|
+
modified += result.modifiedCount;
|
|
72
|
+
bulkRounds++;
|
|
73
|
+
logProgress(`final partial batch (bulk round ${bulkRounds})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logProgress('phase complete');
|
|
77
|
+
return modified;
|
|
78
|
+
}
|
|
79
|
+
|
|
4
80
|
/**
|
|
5
81
|
* Make any changes you need to make to the database here
|
|
6
82
|
*/
|
|
7
|
-
async function up
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (err) {
|
|
22
|
-
winston.error("Error appling the migration script", err);
|
|
23
|
-
}
|
|
24
|
-
winston.info("Schema updated for " + updates.nModified + " project_user of type user"),
|
|
25
|
-
winston.debug("updates",updates)
|
|
26
|
-
return resolve('ok');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
83
|
+
async function up() {
|
|
84
|
+
try {
|
|
85
|
+
const agentsModified = await batchSetRoleType(AGENT_ROLES_FILTER, 1, 'agents roleType=1');
|
|
86
|
+
winston.info(`[project_user_role_type] Agents phase done: ${agentsModified} documents modified`);
|
|
87
|
+
|
|
88
|
+
const usersModified = await batchSetRoleType(USER_ROLES_FILTER, 2, 'users/guest roleType=2');
|
|
89
|
+
winston.info(`[project_user_role_type] Users/guest phase done: ${usersModified} documents modified`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
winston.error('[project_user_role_type] Error applying migration:', err);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
36
97
|
* Make any changes that UNDO the up function side effects here (if possible)
|
|
37
98
|
*/
|
|
38
|
-
async function down
|
|
39
|
-
|
|
99
|
+
async function down() {
|
|
40
100
|
// Write migration here
|
|
41
101
|
// console.log("down*********");
|
|
42
102
|
}
|
|
43
103
|
|
|
44
|
-
module.exports = { up, down };
|
|
45
|
-
|
|
104
|
+
module.exports = { up, down };
|
package/models/kb_setting.js
CHANGED
|
@@ -2,10 +2,24 @@ let mongoose = require('mongoose');
|
|
|
2
2
|
let Schema = mongoose.Schema;
|
|
3
3
|
let winston = require('../config/winston');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const DEFAULT_UNANSWERED_TTL_SEC = 7 * 24 * 60 * 60; // 7 days
|
|
6
|
+
const DEFAULT_ANSWERED_TTL_SEC = 7 * 24 * 60 * 60; // 7 days
|
|
7
|
+
|
|
8
|
+
function ttlSecondsFromEnv(raw, fallbackSec) {
|
|
9
|
+
if (raw == null || String(raw).trim() === '') return fallbackSec;
|
|
10
|
+
const n = Number(raw);
|
|
11
|
+
return Number.isFinite(n) && n >= 0 ? n : fallbackSec;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let expireAfterSeconds = ttlSecondsFromEnv(
|
|
15
|
+
process.env.UNANSWERED_QUESTION_EXPIRATION_TIME,
|
|
16
|
+
DEFAULT_UNANSWERED_TTL_SEC
|
|
17
|
+
);
|
|
18
|
+
let expireAnsweredAfterSeconds = ttlSecondsFromEnv(
|
|
19
|
+
process.env.ANSWERED_QUESTION_EXPIRATION_TIME,
|
|
20
|
+
DEFAULT_ANSWERED_TTL_SEC
|
|
21
|
+
);
|
|
22
|
+
|
|
9
23
|
|
|
10
24
|
const EngineSchema = new Schema({
|
|
11
25
|
name: {
|
|
@@ -184,6 +198,11 @@ var KBSchema = new Schema({
|
|
|
184
198
|
type: Array,
|
|
185
199
|
default: undefined,
|
|
186
200
|
required: false
|
|
201
|
+
},
|
|
202
|
+
situated_context: {
|
|
203
|
+
type: Boolean,
|
|
204
|
+
default: false,
|
|
205
|
+
required: false
|
|
187
206
|
}
|
|
188
207
|
}, {
|
|
189
208
|
timestamps: true
|
|
@@ -203,13 +222,63 @@ const UnansweredQuestionSchema = new Schema({
|
|
|
203
222
|
question: {
|
|
204
223
|
type: String,
|
|
205
224
|
required: true
|
|
225
|
+
},
|
|
226
|
+
request_id: {
|
|
227
|
+
type: String,
|
|
228
|
+
required: false,
|
|
229
|
+
},
|
|
230
|
+
sender: {
|
|
231
|
+
type: String,
|
|
232
|
+
required: false,
|
|
206
233
|
}
|
|
207
234
|
},{
|
|
208
235
|
timestamps: true
|
|
209
236
|
});
|
|
210
237
|
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
const AnsweredQuestionSchema = new Schema({
|
|
239
|
+
id_project: {
|
|
240
|
+
type: String,
|
|
241
|
+
required: true,
|
|
242
|
+
index: true
|
|
243
|
+
},
|
|
244
|
+
namespace: {
|
|
245
|
+
type: String,
|
|
246
|
+
required: true,
|
|
247
|
+
index: true
|
|
248
|
+
},
|
|
249
|
+
question: {
|
|
250
|
+
type: String,
|
|
251
|
+
required: true
|
|
252
|
+
},
|
|
253
|
+
answer: {
|
|
254
|
+
type: String,
|
|
255
|
+
required: true
|
|
256
|
+
},
|
|
257
|
+
tokens: {
|
|
258
|
+
type: Number,
|
|
259
|
+
required: false
|
|
260
|
+
},
|
|
261
|
+
request_id: {
|
|
262
|
+
type: String,
|
|
263
|
+
required: false
|
|
264
|
+
}
|
|
265
|
+
}, {
|
|
266
|
+
timestamps: true
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Add TTL index to automatically delete documents
|
|
270
|
+
UnansweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAfterSeconds });
|
|
271
|
+
UnansweredQuestionSchema.index({ id_project: 1, namespace: 1, createdAt: -1 });
|
|
272
|
+
UnansweredQuestionSchema.index({ question: "text" });
|
|
273
|
+
|
|
274
|
+
AnsweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAnsweredAfterSeconds });
|
|
275
|
+
AnsweredQuestionSchema.index({ id_project: 1, namespace: 1, createdAt: -1 });
|
|
276
|
+
AnsweredQuestionSchema.index(
|
|
277
|
+
{ question: "text", answer: "text" },
|
|
278
|
+
{ weights: { question: 3, answer: 1 } }
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
|
|
213
282
|
|
|
214
283
|
// DEPRECATED !! - Start
|
|
215
284
|
const KBSettingSchema = new Schema({
|
|
@@ -246,6 +315,7 @@ const Engine = mongoose.model('Engine', EngineSchema)
|
|
|
246
315
|
const Namespace = mongoose.model('Namespace', NamespaceSchema)
|
|
247
316
|
const KB = mongoose.model('KB', KBSchema)
|
|
248
317
|
const UnansweredQuestion = mongoose.model('UnansweredQuestion', UnansweredQuestionSchema)
|
|
318
|
+
const AnsweredQuestion = mongoose.model('AnsweredQuestion', AnsweredQuestionSchema)
|
|
249
319
|
|
|
250
320
|
|
|
251
321
|
module.exports = {
|
|
@@ -253,5 +323,6 @@ module.exports = {
|
|
|
253
323
|
Namespace: Namespace,
|
|
254
324
|
Engine: Engine,
|
|
255
325
|
KB: KB,
|
|
256
|
-
UnansweredQuestion: UnansweredQuestion
|
|
326
|
+
UnansweredQuestion: UnansweredQuestion,
|
|
327
|
+
AnsweredQuestion: AnsweredQuestion
|
|
257
328
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiledesk/tiledesk-server",
|
|
3
3
|
"description": "The Tiledesk server module",
|
|
4
|
-
"version": "2.18.
|
|
4
|
+
"version": "2.18.4",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "node ./bin/www",
|
|
7
7
|
"pretest": "mongodb-runner start",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@tiledesk/tiledesk-rasa-connector": "^1.0.10",
|
|
50
50
|
"@tiledesk/tiledesk-sms-connector": "^0.1.13",
|
|
51
51
|
"@tiledesk/tiledesk-telegram-connector": "^0.1.14",
|
|
52
|
-
"@tiledesk/tiledesk-tybot-connector": "^2.0.
|
|
52
|
+
"@tiledesk/tiledesk-tybot-connector": "^2.0.48",
|
|
53
53
|
"@tiledesk/tiledesk-voice-twilio-connector": "^0.3.2",
|
|
54
54
|
"@tiledesk/tiledesk-vxml-connector": "^0.1.91",
|
|
55
55
|
"@tiledesk/tiledesk-whatsapp-connector": "1.0.26",
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { Namespace, AnsweredQuestion } = require('../models/kb_setting');
|
|
4
|
+
const winston = require('../config/winston');
|
|
5
|
+
|
|
6
|
+
// Add a new unanswerd question
|
|
7
|
+
router.post('/', async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const { namespace, question, answer, tokens, request_id } = req.body;
|
|
10
|
+
const id_project = req.projectid;
|
|
11
|
+
|
|
12
|
+
if (!namespace || !question || !answer) {
|
|
13
|
+
return res.status(400).json({
|
|
14
|
+
success: false,
|
|
15
|
+
error: "Missing required parameters: namespace, question and answer"
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if namespae belongs to project
|
|
20
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
21
|
+
if (!isValidNamespace) {
|
|
22
|
+
return res.status(403).json({
|
|
23
|
+
success: false,
|
|
24
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const answeredQuestion = new AnsweredQuestion({
|
|
29
|
+
id_project,
|
|
30
|
+
namespace,
|
|
31
|
+
question,
|
|
32
|
+
answer,
|
|
33
|
+
tokens,
|
|
34
|
+
request_id,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const savedQuestion = await answeredQuestion.save();
|
|
38
|
+
res.status(200).json(savedQuestion);
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
winston.error('Error adding answered question:', error);
|
|
42
|
+
res.status(500).json({
|
|
43
|
+
success: false,
|
|
44
|
+
error: "Error adding answered question"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Get all answered questions for a namespace
|
|
50
|
+
router.get('/:namespace', async (req, res) => {
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { namespace } = req.params;
|
|
54
|
+
const id_project = req.projectid;
|
|
55
|
+
|
|
56
|
+
if (!namespace) {
|
|
57
|
+
return res.status(400).json({
|
|
58
|
+
success: false,
|
|
59
|
+
error: "Missing required parameter: namespace"
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if namespace belongs to project
|
|
64
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
65
|
+
if (!isValidNamespace) {
|
|
66
|
+
return res.status(403).json({
|
|
67
|
+
success: false,
|
|
68
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const page = parseInt(req.query.page) || 0;
|
|
73
|
+
const limit = parseInt(req.query.limit) || 20;
|
|
74
|
+
const sortField = req.query.sortField || 'created_at';
|
|
75
|
+
const direction = parseInt(req.query.direction) || -1;
|
|
76
|
+
|
|
77
|
+
const filter = { id_project, namespace };
|
|
78
|
+
|
|
79
|
+
let projection = undefined;
|
|
80
|
+
|
|
81
|
+
if (req.query.search) {
|
|
82
|
+
filter.$text = { $search: req.query.search };
|
|
83
|
+
// Add score to projection if it's a text search
|
|
84
|
+
projection = { score: { $meta: "textScore" } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let sortObj;
|
|
88
|
+
if (projection && projection.score) {
|
|
89
|
+
sortObj = { score: { $meta: "textScore" } };
|
|
90
|
+
} else {
|
|
91
|
+
sortObj = { [sortField]: direction };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const questions = await AnsweredQuestion.find(filter, projection)
|
|
95
|
+
.sort(sortObj)
|
|
96
|
+
.skip(page * limit)
|
|
97
|
+
.limit(limit);
|
|
98
|
+
|
|
99
|
+
const count = await AnsweredQuestion.countDocuments(filter);
|
|
100
|
+
|
|
101
|
+
res.status(200).json({
|
|
102
|
+
count,
|
|
103
|
+
questions,
|
|
104
|
+
query: {
|
|
105
|
+
page,
|
|
106
|
+
limit,
|
|
107
|
+
sortField,
|
|
108
|
+
direction,
|
|
109
|
+
search: req.query.search || undefined
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
winston.error('Error getting answered questions:', error);
|
|
115
|
+
res.status(500).json({
|
|
116
|
+
success: false,
|
|
117
|
+
error: "Error getting answered questions"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
router.delete('/:id', async (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const { id } = req.params;
|
|
126
|
+
const id_project = req.projectid;
|
|
127
|
+
|
|
128
|
+
const deleted = await AnsweredQuestion.findOneAndDelete({ _id: id, id_project });
|
|
129
|
+
if (!deleted) {
|
|
130
|
+
return res.status(404).json({
|
|
131
|
+
success: false,
|
|
132
|
+
error: "Question not found"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.status(200).json({
|
|
137
|
+
success: true,
|
|
138
|
+
message: "Question deleted successfully"
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
winston.error('Error deleting answered question:', error);
|
|
143
|
+
res.status(500).json({
|
|
144
|
+
success: false,
|
|
145
|
+
error: "Error deleting answered question"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
router.delete('/namespace/:namespace', async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const { namespace } = req.params;
|
|
153
|
+
const id_project = req.projectid;
|
|
154
|
+
|
|
155
|
+
// Check if namespace belongs to project
|
|
156
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
157
|
+
if (!isValidNamespace) {
|
|
158
|
+
return res.status(403).json({
|
|
159
|
+
success: false,
|
|
160
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
|
|
165
|
+
res.status(200).json({
|
|
166
|
+
success: true,
|
|
167
|
+
count: result.deletedCount,
|
|
168
|
+
message: "All questions deleted successfully"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
winston.error('Error deleting answered questions:', error);
|
|
173
|
+
res.status(500).json({
|
|
174
|
+
success: false,
|
|
175
|
+
error: "Error deleting answered questions"
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
router.get('/count/:namespace', async (req, res) => {
|
|
181
|
+
try {
|
|
182
|
+
const { namespace } = req.params;
|
|
183
|
+
const id_project = req.projectid;
|
|
184
|
+
|
|
185
|
+
if (!namespace) {
|
|
186
|
+
return res.status(400).json({
|
|
187
|
+
success: false,
|
|
188
|
+
error: "Missing required parameter: namespace"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if namespace belongs to project
|
|
193
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
194
|
+
if (!isValidNamespace) {
|
|
195
|
+
return res.status(403).json({
|
|
196
|
+
success: false,
|
|
197
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
|
|
202
|
+
res.status(200).json({ count });
|
|
203
|
+
|
|
204
|
+
} catch (error) {
|
|
205
|
+
winston.error('Error counting answered questions:', error);
|
|
206
|
+
res.status(500).json({
|
|
207
|
+
success: false,
|
|
208
|
+
error: "Error counting answered questions"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Helper function to validate namespace
|
|
214
|
+
async function validateNamespace(id_project, namespace_id) {
|
|
215
|
+
try {
|
|
216
|
+
const namespace = await Namespace.findOne({
|
|
217
|
+
id_project: id_project,
|
|
218
|
+
id: namespace_id
|
|
219
|
+
});
|
|
220
|
+
return !!namespace;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
winston.error('validate namespace error: ', err);
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = router;
|
package/routes/kb.js
CHANGED
|
@@ -121,7 +121,8 @@ function normalizeEmbedding(embedding) {
|
|
|
121
121
|
return { ...normalizedEmbedding };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function normalizeSituatedContext() {
|
|
124
|
+
function normalizeSituatedContext(enable = false) {
|
|
125
|
+
situatedContext.enable = enable;
|
|
125
126
|
return situatedContext.enable
|
|
126
127
|
? {
|
|
127
128
|
...situatedContext,
|
|
@@ -199,6 +200,11 @@ router.post('/scrape/single', async (req, res) => {
|
|
|
199
200
|
})
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
let situated_context;
|
|
204
|
+
if (sitemapKb.situated_context && sitemapKb.situated_context === true && sitemapKb.scrape_type === 0) {
|
|
205
|
+
situated_context = normalizeSituatedContext(true);
|
|
206
|
+
}
|
|
207
|
+
|
|
202
208
|
if (addedUrls.length > 0) {
|
|
203
209
|
const options = {
|
|
204
210
|
sitemap_origin_id: sitemapKb._id,
|
|
@@ -206,7 +212,8 @@ router.post('/scrape/single', async (req, res) => {
|
|
|
206
212
|
scrape_type: sitemapKb.scrape_type,
|
|
207
213
|
scrape_options: sitemapKb.scrape_options,
|
|
208
214
|
refresh_rate: sitemapKb.refresh_rate,
|
|
209
|
-
tags: sitemapKb.tags
|
|
215
|
+
tags: sitemapKb.tags,
|
|
216
|
+
...(situated_context && { situated_context: situated_context }),
|
|
210
217
|
}
|
|
211
218
|
aiManager.addMultipleUrls(namespace, addedUrls, options).catch((err) => {
|
|
212
219
|
winston.error("(webhook) error adding multiple urls contents: ", err);
|
|
@@ -263,9 +270,8 @@ router.post('/scrape/single', async (req, res) => {
|
|
|
263
270
|
json.hybrid = true;
|
|
264
271
|
}
|
|
265
272
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
json.situated_context = situated_context;
|
|
273
|
+
if (json.situated_context && json.situated_context === true && json.scrape_type === 0) {
|
|
274
|
+
json.situated_context = normalizeSituatedContext(true);
|
|
269
275
|
}
|
|
270
276
|
|
|
271
277
|
winston.verbose("/scrape/single json: ", json);
|
|
@@ -447,12 +453,11 @@ router.post('/qa', async (req, res) => {
|
|
|
447
453
|
}
|
|
448
454
|
|
|
449
455
|
data.stream = data.stream === true;
|
|
456
|
+
data.use_hyde = data.use_hyde === true;
|
|
457
|
+
data.use_cache = data.use_cache === true;
|
|
450
458
|
data.debug = true;
|
|
451
459
|
delete data.advancedPrompt;
|
|
452
460
|
winston.verbose("ask data: ", data);
|
|
453
|
-
|
|
454
|
-
console.log("data: ", data);
|
|
455
|
-
|
|
456
461
|
if (process.env.NODE_ENV === 'test') {
|
|
457
462
|
return res.status(200).send({ success: true, message: "Question skipped in test environment", data: data });
|
|
458
463
|
}
|
|
@@ -1591,7 +1596,7 @@ router.post('/', async (req, res) => {
|
|
|
1591
1596
|
const id_project = req.projectid;
|
|
1592
1597
|
const project = req.project
|
|
1593
1598
|
|
|
1594
|
-
const { name, type, source, content, refresh_rate, scrape_type, scrape_options, tags } = req.body;
|
|
1599
|
+
const { name, type, source, content, refresh_rate, scrape_type, scrape_options, tags, situated_context } = req.body;
|
|
1595
1600
|
const namespace_id = req.body?.namespace;
|
|
1596
1601
|
|
|
1597
1602
|
if (!namespace_id) {
|
|
@@ -1643,6 +1648,10 @@ router.post('/', async (req, res) => {
|
|
|
1643
1648
|
new_kb.tags = tags;
|
|
1644
1649
|
}
|
|
1645
1650
|
|
|
1651
|
+
if (situated_context && situated_context === true && (type !== "url" || scrape_type === 0)) {
|
|
1652
|
+
new_kb.situated_context = situated_context;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1646
1655
|
winston.debug("adding kb: ", new_kb);
|
|
1647
1656
|
|
|
1648
1657
|
KB.findOneAndUpdate({ id_project, type, source }, new_kb, { upsert: true, new: true, rawResult: true }, async (err, raw_content) => {
|
|
@@ -1661,7 +1670,7 @@ router.post('/', async (req, res) => {
|
|
|
1661
1670
|
const embedding = normalizeEmbedding(namespace.embedding);
|
|
1662
1671
|
embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
|
|
1663
1672
|
|
|
1664
|
-
const
|
|
1673
|
+
const situated_context_obj = normalizeSituatedContext(saved_kb.situated_context);
|
|
1665
1674
|
|
|
1666
1675
|
const json = {
|
|
1667
1676
|
id: saved_kb._id,
|
|
@@ -1673,7 +1682,7 @@ router.post('/', async (req, res) => {
|
|
|
1673
1682
|
hybrid: namespace.hybrid,
|
|
1674
1683
|
engine: namespace.engine || default_engine,
|
|
1675
1684
|
embedding: embedding,
|
|
1676
|
-
...(
|
|
1685
|
+
...(situated_context_obj && { situated_context: situated_context_obj }),
|
|
1677
1686
|
...(saved_kb.scrape_type && { scrape_type: saved_kb.scrape_type }),
|
|
1678
1687
|
...(saved_kb.scrape_options && { parameters_scrape_type_4: saved_kb.scrape_options }),
|
|
1679
1688
|
...(saved_kb.tags && { tags: saved_kb.tags }),
|
|
@@ -1705,7 +1714,7 @@ router.post('/multi', upload.single('uploadFile'), async (req, res) => {
|
|
|
1705
1714
|
|
|
1706
1715
|
const id_project = req.projectid;
|
|
1707
1716
|
const project = req.project;
|
|
1708
|
-
let { refresh_rate = 'never', scrape_type = 2, scrape_options } = req.body;
|
|
1717
|
+
let { refresh_rate = 'never', scrape_type = 2, scrape_options, situated_context } = req.body;
|
|
1709
1718
|
let tags = parseStringArrayField(req.body.tags);
|
|
1710
1719
|
|
|
1711
1720
|
|
|
@@ -1748,6 +1757,7 @@ router.post('/multi', upload.single('uploadFile'), async (req, res) => {
|
|
|
1748
1757
|
scrape_type,
|
|
1749
1758
|
scrape_options,
|
|
1750
1759
|
refresh_rate,
|
|
1760
|
+
...(situated_context && situated_context === true && scrape_type === 0 ? { situated_context: true } : {}),
|
|
1751
1761
|
...(Array.isArray(tags) && tags.length > 0 ? { tags } : {})
|
|
1752
1762
|
}
|
|
1753
1763
|
|
|
@@ -1772,6 +1782,11 @@ router.post('/csv', upload.single('uploadFile'), async (req, res) => {
|
|
|
1772
1782
|
const { delimiter = ';' } = req.body;
|
|
1773
1783
|
let tags = parseStringArrayField(req.body.tags);
|
|
1774
1784
|
|
|
1785
|
+
let situated_context = false;
|
|
1786
|
+
if (req.body.situated_context) {
|
|
1787
|
+
situated_context = req.body.situated_context === true || req.body.situated_context === "true";
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1775
1790
|
|
|
1776
1791
|
let namespace;
|
|
1777
1792
|
try {
|
|
@@ -1799,6 +1814,7 @@ router.post('/csv', upload.single('uploadFile'), async (req, res) => {
|
|
|
1799
1814
|
content: question + "\n" + answer,
|
|
1800
1815
|
namespace: namespace_id,
|
|
1801
1816
|
status: -1,
|
|
1817
|
+
...(situated_context && { situated_context: situated_context }),
|
|
1802
1818
|
...(Array.isArray(tags) && tags.length > 0 ? { tags } : {})
|
|
1803
1819
|
})
|
|
1804
1820
|
})
|
|
@@ -1830,16 +1846,20 @@ router.post('/csv', upload.single('uploadFile'), async (req, res) => {
|
|
|
1830
1846
|
let embedding = normalizeEmbedding(namespace.embedding);
|
|
1831
1847
|
embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
|
|
1832
1848
|
let hybrid = namespace.hybrid;
|
|
1833
|
-
const situated_context = normalizeSituatedContext();
|
|
1834
1849
|
|
|
1835
|
-
let
|
|
1850
|
+
let situated_context_obj;
|
|
1851
|
+
if (situated_context) {
|
|
1852
|
+
situated_context_obj = normalizeSituatedContext(situated_context);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
let resources = result.map(({ name, status, __v, createdAt, updatedAt, id_project, situated_context, ...keepAttrs }) => keepAttrs)
|
|
1836
1856
|
resources = resources.map(({ _id, ...rest}) => {
|
|
1837
1857
|
return {
|
|
1838
1858
|
id: _id,
|
|
1839
1859
|
webhook: webhook,
|
|
1840
1860
|
embedding: embedding,
|
|
1841
1861
|
engine: engine,
|
|
1842
|
-
...(
|
|
1862
|
+
...(situated_context_obj && { situated_context: situated_context_obj }),
|
|
1843
1863
|
...rest
|
|
1844
1864
|
};
|
|
1845
1865
|
})
|
|
@@ -1891,7 +1911,7 @@ router.post('/sitemap/import', async (req, res) => {
|
|
|
1891
1911
|
const id_project = req.projectid;
|
|
1892
1912
|
const namespace_id = req.query.namespace;
|
|
1893
1913
|
|
|
1894
|
-
let { type, source, refresh_rate = 'never', scrape_type = 2, scrape_options, tags } = req.body;
|
|
1914
|
+
let { type, source, refresh_rate = 'never', scrape_type = 2, scrape_options, tags, situated_context } = req.body;
|
|
1895
1915
|
if (scrape_type === 2 && !scrape_options) {
|
|
1896
1916
|
scrape_options = aiManager.setDefaultScrapeOptions();
|
|
1897
1917
|
}
|
|
@@ -1957,6 +1977,7 @@ router.post('/sitemap/import', async (req, res) => {
|
|
|
1957
1977
|
scrape_type,
|
|
1958
1978
|
scrape_options,
|
|
1959
1979
|
refresh_rate,
|
|
1980
|
+
...(situated_context && situated_context === true && scrape_type === 0 ? { situated_context: true } : {}),
|
|
1960
1981
|
...(Array.isArray(tags) && tags.length > 0 ? { tags } : {})
|
|
1961
1982
|
}
|
|
1962
1983
|
|
|
@@ -1974,7 +1995,8 @@ router.post('/sitemap/import', async (req, res) => {
|
|
|
1974
1995
|
scrape_type,
|
|
1975
1996
|
scrape_options,
|
|
1976
1997
|
refresh_rate,
|
|
1977
|
-
...(Array.isArray(tags) && tags.length > 0 ? { tags } : {})
|
|
1998
|
+
...(Array.isArray(tags) && tags.length > 0 ? { tags } : {}),
|
|
1999
|
+
...(situated_context && situated_context === true && scrape_type === 0 ? { situated_context: true } : {}),
|
|
1978
2000
|
}
|
|
1979
2001
|
|
|
1980
2002
|
try {
|
|
@@ -1998,7 +2020,7 @@ router.put('/:kb_id', async (req, res) => {
|
|
|
1998
2020
|
const project = req.project;
|
|
1999
2021
|
const kb_id = req.params.kb_id;
|
|
2000
2022
|
|
|
2001
|
-
const { name, type, source, content, refresh_rate, scrape_type, scrape_options, tags } = req.body;
|
|
2023
|
+
const { name, type, source, content, refresh_rate, scrape_type, scrape_options, tags, situated_context } = req.body;
|
|
2002
2024
|
const namespace_id = req.body.namespace;
|
|
2003
2025
|
|
|
2004
2026
|
if (!namespace_id) {
|
|
@@ -2065,6 +2087,10 @@ router.put('/:kb_id', async (req, res) => {
|
|
|
2065
2087
|
return res.status(500).send({ success: false, error: err });
|
|
2066
2088
|
}
|
|
2067
2089
|
|
|
2090
|
+
if (situated_context && situated_context === true && scrape_type === 0) {
|
|
2091
|
+
situated_context = true;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2068
2094
|
let new_content = {
|
|
2069
2095
|
id_project,
|
|
2070
2096
|
name,
|
|
@@ -2096,6 +2122,10 @@ router.put('/:kb_id', async (req, res) => {
|
|
|
2096
2122
|
new_content.tags = tags;
|
|
2097
2123
|
}
|
|
2098
2124
|
|
|
2125
|
+
if (situated_context && situated_context === true && scrape_type === 0) {
|
|
2126
|
+
new_content.situated_context = situated_context;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2099
2129
|
winston.debug("Update content. New content: ", new_content);
|
|
2100
2130
|
|
|
2101
2131
|
let updated_content;
|
|
@@ -2109,7 +2139,7 @@ router.put('/:kb_id', async (req, res) => {
|
|
|
2109
2139
|
const embedding = normalizeEmbedding(namespace.embedding);
|
|
2110
2140
|
embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
|
|
2111
2141
|
let webhook = apiUrl + '/webhook/kb/status?token=' + KB_WEBHOOK_TOKEN;
|
|
2112
|
-
const
|
|
2142
|
+
const situated_context_obj = normalizeSituatedContext(updated_content.situated_context);
|
|
2113
2143
|
|
|
2114
2144
|
const json = {
|
|
2115
2145
|
id: updated_content._id,
|
|
@@ -2121,7 +2151,7 @@ router.put('/:kb_id', async (req, res) => {
|
|
|
2121
2151
|
hybrid: namespace.hybrid,
|
|
2122
2152
|
engine: namespace.engine || default_engine,
|
|
2123
2153
|
embedding: embedding,
|
|
2124
|
-
...(
|
|
2154
|
+
...(situated_context_obj && { situated_context: situated_context_obj }),
|
|
2125
2155
|
...(updated_content.scrape_type && { scrape_type: updated_content.scrape_type }),
|
|
2126
2156
|
...(updated_content.scrape_options && { parameters_scrape_type_4: updated_content.scrape_options }),
|
|
2127
2157
|
...(updated_content.tags && { tags: updated_content.tags }),
|
package/routes/unanswered.js
CHANGED
|
@@ -6,7 +6,7 @@ var winston = require('../config/winston');
|
|
|
6
6
|
// Add a new unanswered question
|
|
7
7
|
router.post('/', async (req, res) => {
|
|
8
8
|
try {
|
|
9
|
-
const { namespace, question } = req.body;
|
|
9
|
+
const { namespace, question, request_id, sender } = req.body;
|
|
10
10
|
const id_project = req.projectid;
|
|
11
11
|
|
|
12
12
|
if (!namespace || !question) {
|
|
@@ -28,7 +28,9 @@ router.post('/', async (req, res) => {
|
|
|
28
28
|
const unansweredQuestion = new UnansweredQuestion({
|
|
29
29
|
id_project,
|
|
30
30
|
namespace,
|
|
31
|
-
question
|
|
31
|
+
question,
|
|
32
|
+
request_id,
|
|
33
|
+
sender
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
const savedQuestion = await unansweredQuestion.save();
|
|
@@ -70,18 +72,29 @@ router.get('/:namespace', async (req, res) => {
|
|
|
70
72
|
const sortField = req.query.sortField || 'createdAt';
|
|
71
73
|
const direction = parseInt(req.query.direction) || -1;
|
|
72
74
|
|
|
73
|
-
const
|
|
74
|
-
id_project,
|
|
75
|
-
namespace
|
|
76
|
-
})
|
|
77
|
-
.sort({ [sortField]: direction })
|
|
78
|
-
.skip(page * limit)
|
|
79
|
-
.limit(limit);
|
|
75
|
+
const filter = { id_project, namespace };
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
let projection = undefined;
|
|
78
|
+
|
|
79
|
+
if (req.query.search) {
|
|
80
|
+
filter.$text = { $search: req.query.search };
|
|
81
|
+
// Add score to projection if it's a text search
|
|
82
|
+
projection = { score: { $meta: "textScore" } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let sortObj;
|
|
86
|
+
if (projection && projection.score) {
|
|
87
|
+
sortObj = { score: { $meta: "textScore" } };
|
|
88
|
+
} else {
|
|
89
|
+
sortObj = { [sortField]: direction };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const questions = await UnansweredQuestion.find(filter, projection)
|
|
93
|
+
.sort(sortObj)
|
|
94
|
+
.skip(page * limit)
|
|
95
|
+
.limit(limit);
|
|
96
|
+
|
|
97
|
+
const count = await UnansweredQuestion.countDocuments(filter);
|
|
85
98
|
|
|
86
99
|
res.status(200).json({
|
|
87
100
|
count,
|
|
@@ -90,7 +103,8 @@ router.get('/:namespace', async (req, res) => {
|
|
|
90
103
|
page,
|
|
91
104
|
limit,
|
|
92
105
|
sortField,
|
|
93
|
-
direction
|
|
106
|
+
direction,
|
|
107
|
+
search: req.query.search || undefined
|
|
94
108
|
}
|
|
95
109
|
});
|
|
96
110
|
|
|
@@ -109,20 +123,19 @@ router.delete('/:id', async (req, res) => {
|
|
|
109
123
|
const { id } = req.params;
|
|
110
124
|
const id_project = req.projectid;
|
|
111
125
|
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
126
|
+
const deleted = await UnansweredQuestion.findOneAndDelete({ _id: id, id_project });
|
|
127
|
+
if (!deleted) {
|
|
114
128
|
return res.status(404).json({
|
|
115
129
|
success: false,
|
|
116
130
|
error: "Question not found"
|
|
117
131
|
});
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
await UnansweredQuestion.deleteOne({ _id: id });
|
|
121
134
|
res.status(200).json({
|
|
122
135
|
success: true,
|
|
123
136
|
message: "Question deleted successfully"
|
|
124
137
|
});
|
|
125
|
-
|
|
138
|
+
|
|
126
139
|
} catch (error) {
|
|
127
140
|
winston.error('Error deleting unanswered question:', error);
|
|
128
141
|
res.status(500).json({
|
package/routes/webhook.js
CHANGED
|
@@ -148,7 +148,11 @@ router.post('/kb/reindex', async (req, res) => {
|
|
|
148
148
|
sitemap_origin: kb.source,
|
|
149
149
|
scrape_type: kb.scrape_type,
|
|
150
150
|
scrape_options: kb.scrape_options,
|
|
151
|
-
refresh_rate: kb.refresh_rate
|
|
151
|
+
refresh_rate: kb.refresh_rate,
|
|
152
|
+
...(kb.tags ? { tags: kb.tags } : {}),
|
|
153
|
+
...(kb.situated_context === true && (kb.scrape_type === 0 || kb.scrape_type === '0')
|
|
154
|
+
? { situated_context: true }
|
|
155
|
+
: {}),
|
|
152
156
|
}
|
|
153
157
|
aiManager.addMultipleUrls(namespace, addedUrls, options).catch((err) => {
|
|
154
158
|
winston.error("(webhook) error adding multiple urls contents: ", err);
|
|
@@ -198,9 +202,9 @@ router.post('/kb/reindex', async (req, res) => {
|
|
|
198
202
|
embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
|
|
199
203
|
json.embedding = embedding;
|
|
200
204
|
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
json.situated_context =
|
|
205
|
+
const situated_context_obj = aiManager.normalizeSituatedContext(kb.situated_context);
|
|
206
|
+
if (situated_context_obj) {
|
|
207
|
+
json.situated_context = situated_context_obj;
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
let resources = [];
|
package/services/aiManager.js
CHANGED
|
@@ -52,6 +52,7 @@ jobManagerHybrid.connectAndStartPublisher((status, error) => {
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
|
|
55
56
|
class AiManager {
|
|
56
57
|
|
|
57
58
|
constructor() { }
|
|
@@ -74,6 +75,7 @@ class AiManager {
|
|
|
74
75
|
...(options.sitemap_origin_id && { sitemap_origin_id: options.sitemap_origin_id }),
|
|
75
76
|
...(options.sitemap_origin && { sitemap_origin: options.sitemap_origin }),
|
|
76
77
|
...(options.tags && { tags: options.tags }),
|
|
78
|
+
...(options.situated_context && { situated_context: options.situated_context }),
|
|
77
79
|
}
|
|
78
80
|
return kb;
|
|
79
81
|
})
|
|
@@ -95,11 +97,14 @@ class AiManager {
|
|
|
95
97
|
let embedding = namespace.embedding || default_embedding;
|
|
96
98
|
embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
|
|
97
99
|
|
|
98
|
-
let situated_context
|
|
100
|
+
let situated_context;
|
|
101
|
+
if (options.situated_context) {
|
|
102
|
+
situated_context = this.normalizeSituatedContext(options.situated_context);
|
|
103
|
+
}
|
|
99
104
|
|
|
100
105
|
let webhook = apiUrl + '/webhook/kb/status?token=' + KB_WEBHOOK_TOKEN;
|
|
101
106
|
|
|
102
|
-
let resources = result.map(({ name, status, __v, createdAt, updatedAt, id_project, ...keepAttrs }) => keepAttrs)
|
|
107
|
+
let resources = result.map(({ name, status, __v, createdAt, updatedAt, id_project, situated_context, ...keepAttrs }) => keepAttrs)
|
|
103
108
|
resources = resources.map(({ _id, scrape_options, ...rest }) => {
|
|
104
109
|
return {
|
|
105
110
|
id: _id,
|
|
@@ -132,8 +137,6 @@ class AiManager {
|
|
|
132
137
|
async scheduleSitemap(namespace, sitemap_content, options) {
|
|
133
138
|
return new Promise((resolve, reject) => {
|
|
134
139
|
|
|
135
|
-
const situated_context = this.normalizeSituatedContext();
|
|
136
|
-
|
|
137
140
|
let kb = {
|
|
138
141
|
id: sitemap_content._id,
|
|
139
142
|
source: sitemap_content.source,
|
|
@@ -144,7 +147,7 @@ class AiManager {
|
|
|
144
147
|
engine: namespace.engine,
|
|
145
148
|
embedding: namespace.embedding,
|
|
146
149
|
hybrid: namespace.hybrid,
|
|
147
|
-
...(situated_context && { situated_context }),
|
|
150
|
+
...(options.situated_context && { situated_context: options.situated_context }),
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
if (process.env.NODE_ENV === 'test') {
|
|
@@ -190,6 +193,8 @@ class AiManager {
|
|
|
190
193
|
throw err;
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
|
|
197
|
+
|
|
193
198
|
// Recreate all url contents with sitemap_origin_id
|
|
194
199
|
let result;
|
|
195
200
|
try {
|
|
@@ -567,7 +572,8 @@ class AiManager {
|
|
|
567
572
|
})
|
|
568
573
|
}
|
|
569
574
|
|
|
570
|
-
normalizeSituatedContext() {
|
|
575
|
+
normalizeSituatedContext(enable = false) {
|
|
576
|
+
situatedContext.enable = enable;
|
|
571
577
|
return situatedContext.enable
|
|
572
578
|
? {
|
|
573
579
|
...situatedContext,
|
package/test/kbRoute.js
CHANGED
|
@@ -1011,6 +1011,7 @@ describe('KbRoute', () => {
|
|
|
1011
1011
|
.auth(email, pwd)
|
|
1012
1012
|
//.set('Content-Type', 'text/csv')
|
|
1013
1013
|
.field('delimiter', ';')
|
|
1014
|
+
.field('situated_context', true)
|
|
1014
1015
|
.field('tags', JSON.stringify(['tag1', 'tag2']))
|
|
1015
1016
|
.attach('uploadFile', fs.readFileSync(path.resolve(__dirname, './fixtures/example-kb-faqs.csv')), 'example-kb-faqs.csv')
|
|
1016
1017
|
.end((err, res) => {
|