@eeacms/volto-eea-chatbot 3.0.0 → 4.0.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.
- package/.github/workflows/betterleaks.yml +133 -0
- package/.gitleaks.toml +89 -0
- package/CHANGELOG.md +32 -1
- package/README.md +102 -12
- package/RELEASE.md +1 -1
- package/package.json +1 -1
- package/src/ChatBlock/chat/AIMessage.tsx +60 -41
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
- package/src/ChatBlock/components/markdown/index.jsx +1 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +11 -2
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +20 -11
- package/src/ChatBlock/services/messageProcessor.ts +36 -14
- package/src/ChatBlock/style.less +41 -51
- package/src/ChatBlock/utils/index.tsx +10 -0
- package/src/halloumi/README.md +27 -1
- package/src/halloumi/middleware.js +40 -50
- package/src/halloumi/middleware.test.js +102 -103
- package/src/middleware.js +2 -2
- package/src/middleware.test.js +11 -11
- package/src/halloumi/filtering.js +0 -149
- package/src/halloumi/filtering.test.js +0 -242
- package/src/halloumi/generative.js +0 -324
- package/src/halloumi/generative.test.js +0 -386
- package/src/halloumi/markdown-splitter.js +0 -172
- package/src/halloumi/markdown-splitter.test.js +0 -133
- package/src/halloumi/postprocessing.js +0 -247
- package/src/halloumi/postprocessing.test.js +0 -441
- package/src/halloumi/preprocessing.js +0 -117
- package/src/halloumi/preprocessing.test.js +0 -184
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
2
|
import { isPathAllowed } from '../middleware';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Mock node-fetch
|
|
5
|
+
jest.mock('node-fetch');
|
|
5
6
|
|
|
6
7
|
describe('halloumi middleware', () => {
|
|
7
|
-
let req, res, next;
|
|
8
|
+
let req, res, next, middleware;
|
|
8
9
|
|
|
9
10
|
beforeEach(() => {
|
|
11
|
+
fetch.mockReset();
|
|
12
|
+
// Clear the module cache so middleware is re-imported with current env vars
|
|
13
|
+
jest.resetModules();
|
|
14
|
+
// Re-mock after reset
|
|
15
|
+
jest.mock('node-fetch', () => jest.fn());
|
|
16
|
+
|
|
10
17
|
req = {
|
|
11
18
|
url: '/_ha/generate',
|
|
12
19
|
method: 'POST',
|
|
13
20
|
body: {
|
|
14
21
|
sources: ['source1', 'source2'],
|
|
15
22
|
answer: 'test answer',
|
|
16
|
-
maxContextSegments: 3,
|
|
17
23
|
},
|
|
18
24
|
headers: {},
|
|
19
25
|
ip: '127.0.0.1',
|
|
@@ -30,23 +36,90 @@ describe('halloumi middleware', () => {
|
|
|
30
36
|
jest.restoreAllMocks();
|
|
31
37
|
});
|
|
32
38
|
|
|
33
|
-
it('
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
it('proxies request to rag-fact-checker and returns response', async () => {
|
|
40
|
+
process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
|
|
41
|
+
middleware = require('./middleware').default;
|
|
42
|
+
const mockedFetch = require('node-fetch');
|
|
43
|
+
|
|
44
|
+
const mockResponse = {
|
|
45
|
+
claims: [
|
|
46
|
+
{
|
|
47
|
+
startOffset: 0,
|
|
48
|
+
endOffset: 20,
|
|
49
|
+
segmentIds: ['0'],
|
|
50
|
+
score: 0.95,
|
|
51
|
+
rationale: 'Supported by source.',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
segments: { 0: { startOffset: 0, endOffset: 30 } },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
mockedFetch.mockResolvedValue({
|
|
58
|
+
ok: true,
|
|
59
|
+
status: 200,
|
|
60
|
+
json: () => Promise.resolve(mockResponse),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await middleware(req, res, next);
|
|
64
|
+
|
|
65
|
+
expect(mockedFetch).toHaveBeenCalledWith(
|
|
66
|
+
'http://localhost:8000/halloumi/generate',
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
answer: 'test answer',
|
|
72
|
+
sources: ['source1', 'source2'],
|
|
73
|
+
max_context_segments: 0,
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/json');
|
|
78
|
+
expect(res.send).toHaveBeenCalledWith(mockResponse);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns 502 when rag-fact-checker is unreachable', async () => {
|
|
82
|
+
process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
|
|
83
|
+
middleware = require('./middleware').default;
|
|
84
|
+
const mockedFetch = require('node-fetch');
|
|
85
|
+
|
|
86
|
+
mockedFetch.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
38
87
|
|
|
39
88
|
await middleware(req, res, next);
|
|
40
89
|
|
|
41
|
-
expect(res.
|
|
42
|
-
|
|
90
|
+
expect(res.status).toHaveBeenCalledWith(502);
|
|
91
|
+
expect(res.send).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
error: expect.stringContaining('Fact-checker unavailable'),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns error when rag-fact-checker responds with non-ok status', async () => {
|
|
99
|
+
process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
|
|
100
|
+
middleware = require('./middleware').default;
|
|
101
|
+
const mockedFetch = require('node-fetch');
|
|
102
|
+
|
|
103
|
+
mockedFetch.mockResolvedValue({
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 500,
|
|
106
|
+
json: () => Promise.resolve({ detail: 'Internal server error' }),
|
|
43
107
|
});
|
|
44
108
|
|
|
45
|
-
|
|
46
|
-
|
|
109
|
+
await middleware(req, res, next);
|
|
110
|
+
|
|
111
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
112
|
+
expect(res.send).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
error: expect.stringContaining('Internal server error'),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
47
117
|
});
|
|
48
118
|
|
|
49
119
|
it('rejects disallowed paths with 404', async () => {
|
|
120
|
+
process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
|
|
121
|
+
middleware = require('./middleware').default;
|
|
122
|
+
|
|
50
123
|
req.url = '/_ha/admin/config';
|
|
51
124
|
req.method = 'POST';
|
|
52
125
|
|
|
@@ -57,6 +130,9 @@ describe('halloumi middleware', () => {
|
|
|
57
130
|
});
|
|
58
131
|
|
|
59
132
|
it('rejects allowed path with wrong HTTP method', async () => {
|
|
133
|
+
process.env.RAG_FACT_CHECKER_URL = 'http://localhost:8000';
|
|
134
|
+
middleware = require('./middleware').default;
|
|
135
|
+
|
|
60
136
|
req.url = '/_ha/generate';
|
|
61
137
|
req.method = 'GET';
|
|
62
138
|
|
|
@@ -65,100 +141,29 @@ describe('halloumi middleware', () => {
|
|
|
65
141
|
expect(res.statusCode).toBe(404);
|
|
66
142
|
expect(res.send).toHaveBeenCalledWith({ error: 'Not Found' });
|
|
67
143
|
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// These tests need a fresh module loaded with env vars set so that
|
|
71
|
-
// LLMGW_TOKEN and LLMGW_URL are defined at module load time,
|
|
72
|
-
// allowing the code to reach getVerifyClaimResponse.
|
|
73
|
-
describe('halloumi middleware - dynamic import', () => {
|
|
74
|
-
const buildReq = () => ({
|
|
75
|
-
url: '/_ha/generate',
|
|
76
|
-
method: 'POST',
|
|
77
|
-
body: {
|
|
78
|
-
sources: ['source1', 'source2'],
|
|
79
|
-
answer: 'test answer',
|
|
80
|
-
maxContextSegments: 3,
|
|
81
|
-
},
|
|
82
|
-
headers: {},
|
|
83
|
-
ip: '127.0.0.1',
|
|
84
|
-
});
|
|
85
144
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('sends response on successful getVerifyClaimResponse', async () => {
|
|
93
|
-
const origToken = process.env.LLMGW_TOKEN;
|
|
94
|
-
const origUrl = process.env.LLMGW_URL;
|
|
95
|
-
process.env.LLMGW_TOKEN = 'test-token';
|
|
96
|
-
process.env.LLMGW_URL = 'http://test-url';
|
|
97
|
-
|
|
98
|
-
jest.resetModules();
|
|
99
|
-
|
|
100
|
-
const mockGetVerifyClaimResponse = jest
|
|
101
|
-
.fn()
|
|
102
|
-
.mockResolvedValue({ claims: [], segments: {} });
|
|
103
|
-
|
|
104
|
-
jest.setMock('./generative', {
|
|
105
|
-
getVerifyClaimResponse: mockGetVerifyClaimResponse,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const middlewareMod = require('./middleware').default;
|
|
109
|
-
|
|
110
|
-
const req = buildReq();
|
|
111
|
-
const res = buildRes();
|
|
112
|
-
|
|
113
|
-
await middlewareMod(req, res, jest.fn());
|
|
114
|
-
|
|
115
|
-
expect(mockGetVerifyClaimResponse).toHaveBeenCalled();
|
|
116
|
-
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/json');
|
|
117
|
-
expect(res.send).toHaveBeenCalledWith({ claims: [], segments: {} });
|
|
118
|
-
|
|
119
|
-
process.env.LLMGW_TOKEN = origToken;
|
|
120
|
-
process.env.LLMGW_URL = origUrl;
|
|
121
|
-
});
|
|
145
|
+
it('uses default URL when RAG_FACT_CHECKER_URL is not set', async () => {
|
|
146
|
+
delete process.env.RAG_FACT_CHECKER_URL;
|
|
147
|
+
middleware = require('./middleware').default;
|
|
148
|
+
const mockedFetch = require('node-fetch');
|
|
122
149
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
process.env.LLMGW_URL = 'http://test-url';
|
|
128
|
-
|
|
129
|
-
jest.resetModules();
|
|
130
|
-
|
|
131
|
-
const mockGetVerifyClaimResponse = jest
|
|
132
|
-
.fn()
|
|
133
|
-
.mockRejectedValue(new Error('LLM error'));
|
|
134
|
-
|
|
135
|
-
jest.setMock('./generative', {
|
|
136
|
-
getVerifyClaimResponse: mockGetVerifyClaimResponse,
|
|
150
|
+
mockedFetch.mockResolvedValue({
|
|
151
|
+
ok: true,
|
|
152
|
+
status: 200,
|
|
153
|
+
json: () => Promise.resolve({ claims: [], segments: {} }),
|
|
137
154
|
});
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const req = buildReq();
|
|
142
|
-
const res = buildRes();
|
|
143
|
-
|
|
144
|
-
await middlewareMod(req, res, jest.fn());
|
|
156
|
+
await middleware(req, res, next);
|
|
145
157
|
|
|
146
|
-
expect(
|
|
147
|
-
|
|
148
|
-
expect.
|
|
158
|
+
expect(mockedFetch).toHaveBeenCalledWith(
|
|
159
|
+
'http://localhost:8000/halloumi/generate',
|
|
160
|
+
expect.any(Object),
|
|
149
161
|
);
|
|
150
|
-
|
|
151
|
-
process.env.LLMGW_TOKEN = origToken;
|
|
152
|
-
process.env.LLMGW_URL = origUrl;
|
|
153
162
|
});
|
|
154
163
|
});
|
|
155
164
|
|
|
156
165
|
describe('halloumi path allowlist', () => {
|
|
157
|
-
|
|
158
|
-
const ALLOWED_HALLOUMI_PATHS = [
|
|
159
|
-
{ path: '/generate', methods: ['POST'] },
|
|
160
|
-
{ path: '/classify', methods: ['POST'] },
|
|
161
|
-
];
|
|
166
|
+
const ALLOWED_HALLOUMI_PATHS = [{ path: '/generate', methods: ['POST'] }];
|
|
162
167
|
|
|
163
168
|
it('allows /generate with POST', () => {
|
|
164
169
|
expect(isPathAllowed('/generate', 'POST', ALLOWED_HALLOUMI_PATHS)).toBe(
|
|
@@ -166,12 +171,6 @@ describe('halloumi path allowlist', () => {
|
|
|
166
171
|
);
|
|
167
172
|
});
|
|
168
173
|
|
|
169
|
-
it('allows /classify with POST', () => {
|
|
170
|
-
expect(isPathAllowed('/classify', 'POST', ALLOWED_HALLOUMI_PATHS)).toBe(
|
|
171
|
-
true,
|
|
172
|
-
);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
174
|
it('rejects /generate with wrong method', () => {
|
|
176
175
|
expect(isPathAllowed('/generate', 'GET', ALLOWED_HALLOUMI_PATHS)).toBe(
|
|
177
176
|
false,
|
package/src/middleware.js
CHANGED
|
@@ -289,7 +289,7 @@ export default async function middleware(req, res, next) {
|
|
|
289
289
|
|
|
290
290
|
const reqUrl = `${process.env.ONYX_URL || ''}/api${path}`;
|
|
291
291
|
|
|
292
|
-
const api_key = process.env.ONYX_API_KEY;
|
|
292
|
+
const api_key = process.env.ONYX_API_KEY; //betterleaks:allow
|
|
293
293
|
if (!api_key) {
|
|
294
294
|
res.statusCode = 500;
|
|
295
295
|
res.statusMessage = MSG_INVALID_CONFIGURATION;
|
|
@@ -300,7 +300,7 @@ export default async function middleware(req, res, next) {
|
|
|
300
300
|
try {
|
|
301
301
|
await send_onyx_request(req, res, {
|
|
302
302
|
url: reqUrl,
|
|
303
|
-
api_key: api_key,
|
|
303
|
+
api_key: api_key, //betterleaks:allow
|
|
304
304
|
is_related_question,
|
|
305
305
|
});
|
|
306
306
|
} catch (error) {
|
package/src/middleware.test.js
CHANGED
|
@@ -89,7 +89,7 @@ describe('src/middleware', () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('proxies POST request with api_key and pipes response', async () => {
|
|
92
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
92
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
93
93
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
94
94
|
|
|
95
95
|
await middleware(req, res, next);
|
|
@@ -110,7 +110,7 @@ describe('src/middleware', () => {
|
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
it('handles related question paths', async () => {
|
|
113
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
113
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
114
114
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
115
115
|
req.url = '/_rq/chat/send-message';
|
|
116
116
|
|
|
@@ -124,7 +124,7 @@ describe('src/middleware', () => {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
it('sends error response when fetch throws', async () => {
|
|
127
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
127
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
128
128
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
129
129
|
|
|
130
130
|
nodeFetch.mockRejectedValueOnce(
|
|
@@ -145,7 +145,7 @@ describe('src/middleware', () => {
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
it('handles error without response text', async () => {
|
|
148
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
148
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
149
149
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
150
150
|
|
|
151
151
|
nodeFetch.mockRejectedValueOnce(new Error('No response'));
|
|
@@ -162,7 +162,7 @@ describe('src/middleware', () => {
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
it('handles GET requests without body', async () => {
|
|
165
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
165
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
166
166
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
167
167
|
req.method = 'GET';
|
|
168
168
|
req.url = '/_da/persona/1';
|
|
@@ -175,7 +175,7 @@ describe('src/middleware', () => {
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
it('uses mock create-chat-session when MOCK_LLM_FILE_PATH is set', async () => {
|
|
178
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
178
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
179
179
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
180
180
|
process.env.MOCK_LLM_FILE_PATH = '/tmp/mock.jsonl';
|
|
181
181
|
req.url = '/_da/chat/create-chat-session';
|
|
@@ -189,7 +189,7 @@ describe('src/middleware', () => {
|
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
it('uses mock send-message with stream when MOCK_LLM_FILE_PATH is set', async () => {
|
|
192
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
192
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
193
193
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
194
194
|
process.env.MOCK_LLM_FILE_PATH = '/tmp/mock.jsonl';
|
|
195
195
|
req.url = '/_da/chat/send-message';
|
|
@@ -223,7 +223,7 @@ describe('src/middleware', () => {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
it('dumps LLM response when DUMP_LLM_FILE_PATH is set', async () => {
|
|
226
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
226
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
227
227
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
228
228
|
process.env.DUMP_LLM_FILE_PATH = '/tmp/dumped_response.jsonl';
|
|
229
229
|
|
|
@@ -236,7 +236,7 @@ describe('src/middleware', () => {
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
it('rejects disallowed paths with 404', async () => {
|
|
239
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
239
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
240
240
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
241
241
|
req.url = '/_da/admin/users/delete';
|
|
242
242
|
req.method = 'POST';
|
|
@@ -249,7 +249,7 @@ describe('src/middleware', () => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
it('rejects allowed path with wrong HTTP method', async () => {
|
|
252
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
252
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
253
253
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
254
254
|
req.url = '/_da/persona';
|
|
255
255
|
req.method = 'DELETE';
|
|
@@ -262,7 +262,7 @@ describe('src/middleware', () => {
|
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
it('rejects path traversal attempts', async () => {
|
|
265
|
-
process.env.ONYX_API_KEY = 'test-key';
|
|
265
|
+
process.env.ONYX_API_KEY = 'test-key'; //betterleaks:allow
|
|
266
266
|
process.env.ONYX_URL = 'http://localhost:3000';
|
|
267
267
|
req.url = '/_da/../../etc/passwd';
|
|
268
268
|
req.method = 'GET';
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
|
-
import fetch from 'node-fetch';
|
|
3
|
-
|
|
4
|
-
const log = debug('halloumi');
|
|
5
|
-
|
|
6
|
-
const LLMGW_URL = process.env.LLMGW_URL;
|
|
7
|
-
const LLMGW_API_KEY = process.env.LLMGW_TOKEN;
|
|
8
|
-
const MIN_CONTEXT_SENTENCES_FOR_FILTERING = 75;
|
|
9
|
-
|
|
10
|
-
const filterModel = {
|
|
11
|
-
name: 'Inhouse-LLM/gpt-oss-120b',
|
|
12
|
-
apiUrl: `${LLMGW_URL}/chat/completions`,
|
|
13
|
-
apiKey: LLMGW_API_KEY,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export async function callLLM(apiUrl, apiKey, requestBody, { ip } = {}) {
|
|
17
|
-
const headers = {
|
|
18
|
-
'Content-Type': 'application/json',
|
|
19
|
-
accept: 'application/json',
|
|
20
|
-
};
|
|
21
|
-
if (apiKey) {
|
|
22
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
23
|
-
}
|
|
24
|
-
if (ip) {
|
|
25
|
-
headers['X-Forwarded-For'] = ip;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const response = await fetch(apiUrl, {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers,
|
|
31
|
-
body: JSON.stringify(requestBody),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
return response.json();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function buildClaimFilterPrompt(sentences) {
|
|
38
|
-
const numberedSentences = sentences
|
|
39
|
-
.map((s, i) => `${i + 1}. "${s.trim()}"`)
|
|
40
|
-
.join('\n');
|
|
41
|
-
|
|
42
|
-
return `Given the following numbered sentences, identify which ones are NOT verifiable factual claims.
|
|
43
|
-
|
|
44
|
-
Exclude sentences that are:
|
|
45
|
-
- Greetings, preambles, or transitional phrases
|
|
46
|
-
- Opinions, vague statements, or subjective assessments
|
|
47
|
-
- Introductory sentences that set up a list or table
|
|
48
|
-
- Sentences that merely restate or paraphrase the question
|
|
49
|
-
- Conversational closers (e.g., "let me know if you need more")
|
|
50
|
-
- Broad summaries that don't assert a specific fact
|
|
51
|
-
- Disclaimers or meta-commentary about the response itself
|
|
52
|
-
- Sentences that do not provide enough information to verify
|
|
53
|
-
- Sentences that only mention a concept without providing specific information
|
|
54
|
-
|
|
55
|
-
Respond with ONLY a comma-separated list of the sentence numbers to exclude. If none should be excluded, respond with "NONE".
|
|
56
|
-
|
|
57
|
-
Sentences:
|
|
58
|
-
${numberedSentences}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildContextFilterPrompt(contextSentences, claimSentences) {
|
|
62
|
-
const numberedContext = contextSentences
|
|
63
|
-
.map((s, i) => `${i + 1}. "${s.trim()}"`)
|
|
64
|
-
.join('\n');
|
|
65
|
-
const numberedClaims = claimSentences
|
|
66
|
-
.map((s, i) => `${i + 1}. "${s.trim()}"`)
|
|
67
|
-
.join('\n');
|
|
68
|
-
|
|
69
|
-
return `Given the following context sentences and claims, identify which context sentences are NOT relevant to verifying ANY of the claims.
|
|
70
|
-
|
|
71
|
-
Context sentences:
|
|
72
|
-
${numberedContext}
|
|
73
|
-
|
|
74
|
-
Claims to verify:
|
|
75
|
-
${numberedClaims}
|
|
76
|
-
|
|
77
|
-
Respond with ONLY a comma-separated list of the context sentence numbers that are NOT relevant. If all are relevant, respond with "NONE".`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Parses a comma-separated list of indices from an LLM response.
|
|
82
|
-
*/
|
|
83
|
-
export function parseExcludeIndices(content, maxIndex) {
|
|
84
|
-
const excludeIndices = new Set();
|
|
85
|
-
if (content.trim().toUpperCase() === 'NONE') {
|
|
86
|
-
return excludeIndices;
|
|
87
|
-
}
|
|
88
|
-
const matches = content.match(/\d+/g) || [];
|
|
89
|
-
for (const match of matches) {
|
|
90
|
-
const idx = parseInt(match, 10);
|
|
91
|
-
if (idx >= 1 && idx <= maxIndex) {
|
|
92
|
-
excludeIndices.add(idx);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return excludeIndices;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function callFilterModel(prompt, { ip } = {}) {
|
|
99
|
-
const data = {
|
|
100
|
-
messages: [{ role: 'user', content: prompt }],
|
|
101
|
-
temperature: 0.0,
|
|
102
|
-
model: filterModel.name,
|
|
103
|
-
};
|
|
104
|
-
const jsonData = await callLLM(filterModel.apiUrl, filterModel.apiKey, data, {
|
|
105
|
-
ip,
|
|
106
|
-
});
|
|
107
|
-
return jsonData.choices?.[0]?.message?.content || '';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function excludeClaimSentences(sentences, { ip } = {}) {
|
|
111
|
-
if (sentences.length === 0) {
|
|
112
|
-
return new Set();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const prompt = buildClaimFilterPrompt(sentences);
|
|
117
|
-
const content = await callFilterModel(prompt, { ip });
|
|
118
|
-
const excludedIndices = parseExcludeIndices(content, sentences.length);
|
|
119
|
-
log('Claim filter response', excludedIndices.size);
|
|
120
|
-
return excludedIndices;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
log('Claim filter failed, skipping', error);
|
|
123
|
-
return new Set();
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function excludeContextSentences(
|
|
128
|
-
contextSentences,
|
|
129
|
-
claimSentences,
|
|
130
|
-
{ ip } = {},
|
|
131
|
-
) {
|
|
132
|
-
if (contextSentences.length <= MIN_CONTEXT_SENTENCES_FOR_FILTERING) {
|
|
133
|
-
return new Set();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const prompt = buildContextFilterPrompt(contextSentences, claimSentences);
|
|
138
|
-
const content = await callFilterModel(prompt, { ip });
|
|
139
|
-
const excludedIndices = parseExcludeIndices(
|
|
140
|
-
content,
|
|
141
|
-
contextSentences.length,
|
|
142
|
-
);
|
|
143
|
-
log('Context filter response', excludedIndices.size);
|
|
144
|
-
return excludedIndices;
|
|
145
|
-
} catch (error) {
|
|
146
|
-
log('Context filter failed, skipping', error);
|
|
147
|
-
return new Set();
|
|
148
|
-
}
|
|
149
|
-
}
|