@eeacms/volto-eea-chatbot 3.0.1 → 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/CHANGELOG.md +26 -4
- package/README.md +43 -12
- 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/services/messageProcessor.ts +7 -3
- 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/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
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,32 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
-
### [
|
|
7
|
+
### [4.0.0](https://github.com/eea/volto-eea-chatbot/compare/3.0.1...4.0.0) - 13 August 2026
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: correct 100.00% → High in ClaimModal snapshot [Tiberiu Ichim - [`2f3e620`](https://github.com/eea/volto-eea-chatbot/commit/2f3e620627faae3b050f0cb6ba1e3db4015dc5db)]
|
|
12
|
+
- fix: avoid Betterleaks false positive on LLM_API_KEY example [Tiberiu Ichim - [`44e05bc`](https://github.com/eea/volto-eea-chatbot/commit/44e05bc74a042e7f08cc0e6878930f9acc1964b9)]
|
|
13
|
+
- fix: filter skipped claims in span processor to prevent text duplication [Tiberiu Ichim - [`9477a42`](https://github.com/eea/volto-eea-chatbot/commit/9477a42d363d6961f4def628ed567a2409b316ea)]
|
|
14
|
+
|
|
15
|
+
#### :house: Internal changes
|
|
16
|
+
|
|
17
|
+
- chore: bump version to 4.0.0 (breaking change) [Tiberiu Ichim - [`e1038b6`](https://github.com/eea/volto-eea-chatbot/commit/e1038b6db64612f7c772a898b99ff802b3c133e9)]
|
|
18
|
+
- style: Automated code fix [eea-jenkins - [`03891e2`](https://github.com/eea/volto-eea-chatbot/commit/03891e24e48ccd0eccb55d4ed42aabbcdf34251f)]
|
|
19
|
+
|
|
20
|
+
#### :house: Documentation changes
|
|
21
|
+
|
|
22
|
+
- docs: document rag-facts-check backend dependency for quality checks [Tiberiu Ichim - [`123c7ea`](https://github.com/eea/volto-eea-chatbot/commit/123c7ea5d56ee48e6d5c787049a010b6bebd5aef)]
|
|
23
|
+
- docs: update README for rag-fact-checker integration [Tiberiu Ichim - [`4ae32b4`](https://github.com/eea/volto-eea-chatbot/commit/4ae32b41e838176d4a3ba1ea55367fc21e1a8cae)]
|
|
24
|
+
|
|
25
|
+
#### :hammer_and_wrench: Others
|
|
26
|
+
|
|
27
|
+
- test: update ClaimModal snapshots for categorical score labels [Tiberiu Ichim - [`3794be1`](https://github.com/eea/volto-eea-chatbot/commit/3794be1fc112b9f209a58c6577f99649350b1d59)]
|
|
28
|
+
- Deduplicate sources by text before sending to fact-checker [Tiberiu Ichim - [`67dc793`](https://github.com/eea/volto-eea-chatbot/commit/67dc793f955058d9ba5dbc92ea23a652e4586172)]
|
|
29
|
+
- Fix: use doc.content from final_documents when tool packets lack content [Tiberiu Ichim - [`4174212`](https://github.com/eea/volto-eea-chatbot/commit/4174212a90742c139ad2399f7a07feb99d619fbc)]
|
|
30
|
+
- Replace numeric claim scores with categorical labels [Tiberiu Ichim - [`d0aaf77`](https://github.com/eea/volto-eea-chatbot/commit/d0aaf779a69340386af81e9af4aa5147d93be388)]
|
|
31
|
+
- Send structured sources with document titles to fact-checker [Tiberiu Ichim - [`190c79b`](https://github.com/eea/volto-eea-chatbot/commit/190c79b2a888a7d655157e683633fd9901b8871c)]
|
|
32
|
+
### [3.0.1](https://github.com/eea/volto-eea-chatbot/compare/3.0.0...3.0.1) - 7 August 2026
|
|
8
33
|
|
|
9
34
|
#### :bug: Bug Fixes
|
|
10
35
|
|
|
@@ -35,7 +60,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
35
60
|
#### :house: Internal changes
|
|
36
61
|
|
|
37
62
|
- chore: bump version to 3.0.0 (EEA design system dependency) [skip ci] [Tiberiu Ichim - [`bc5a8c1`](https://github.com/eea/volto-eea-chatbot/commit/bc5a8c1f72d4e706b096cb96d0d24c2c3f5ac0f8)]
|
|
38
|
-
- chore: [JENKINSFILE] sanitize sonarqube command Refs #305084 [valentinab25 - [`83bf08b`](https://github.com/eea/volto-eea-chatbot/commit/83bf08b7188548a50801b8e464a4d113f0cbbac4)]
|
|
39
63
|
|
|
40
64
|
#### :house: Documentation changes
|
|
41
65
|
|
|
@@ -113,7 +137,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
113
137
|
#### :house: Internal changes
|
|
114
138
|
|
|
115
139
|
- style: Automated code fix [eea-jenkins - [`8a8c3c4`](https://github.com/eea/volto-eea-chatbot/commit/8a8c3c4172a4f669661378cf1b5a3569d85609e6)]
|
|
116
|
-
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`535d986`](https://github.com/eea/volto-eea-chatbot/commit/535d986b7adc77743a668bc4ac63f835eef58df3)]
|
|
117
140
|
|
|
118
141
|
#### :hammer_and_wrench: Others
|
|
119
142
|
|
|
@@ -124,7 +147,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
124
147
|
#### :house: Internal changes
|
|
125
148
|
|
|
126
149
|
- style: Automated code fix [eea-jenkins - [`fdcd884`](https://github.com/eea/volto-eea-chatbot/commit/fdcd8848fd4c3f990ca5ea021f407487aebd6010)]
|
|
127
|
-
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`3d428d7`](https://github.com/eea/volto-eea-chatbot/commit/3d428d72f32c3d05452b0961c76f5db1c416e05c)]
|
|
128
150
|
|
|
129
151
|
#### :hammer_and_wrench: Others
|
|
130
152
|
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ The **Volto Chatbot** block allows the integration of an AI-powered chatbot into
|
|
|
40
40
|
| `enableShowTotalFailMessage` | Show total failure message. | Boolean | `false` |
|
|
41
41
|
| `showAssistantTitle` | Display or hide the assistant's title in the chat interface. | Boolean | `true` |
|
|
42
42
|
| `showAssistantDescription` | Display or hide the assistant's description in the chat interface. | Boolean | `true` |
|
|
43
|
-
| `qualityCheck` | Show
|
|
43
|
+
| `qualityCheck` | Show automated fact-checking of AI answers against source documents. | Dropdown | `Disabled` |
|
|
44
44
|
| `onDemandInputToggle` | Sets the default state of the fact-check AI toggle. | Boolean | `true` |
|
|
45
45
|
| `showTools` | Show or hide tools in the chat interface. | Array | `["internal_search_tool_start"]` |
|
|
46
46
|
| `scrollToInput` | Automatically scroll the page to focus on the chat input when interacting. | Boolean | `false` |
|
|
@@ -116,6 +116,44 @@ Go to http://localhost:3000
|
|
|
116
116
|
|
|
117
117
|
For a legacy Volto 17 project, install the package with `yarn` and restart the frontend as usual.
|
|
118
118
|
|
|
119
|
+
## Quality Checks (Fact-Checking)
|
|
120
|
+
|
|
121
|
+
When `qualityCheck` is enabled, the chatbot sends AI answers and their source
|
|
122
|
+
documents to a fact-checking backend that extracts claims, verifies each
|
|
123
|
+
against the sources, and returns per-claim verdicts with evidence.
|
|
124
|
+
|
|
125
|
+
### Backend dependency
|
|
126
|
+
|
|
127
|
+
The fact-checking feature requires the
|
|
128
|
+
[eea/rag-facts-check](https://github.com/eea/rag-facts-check) service running
|
|
129
|
+
and reachable at the URL configured in `RAG_FACT_CHECKER_URL`.
|
|
130
|
+
|
|
131
|
+
**Without this backend, quality checks will fail with a connection error.**
|
|
132
|
+
|
|
133
|
+
The backend exposes a halloumi-compatible endpoint (`POST /halloumi/generate`)
|
|
134
|
+
so the frontend can call it without code changes. It also provides a native
|
|
135
|
+
`POST /check` endpoint with a richer response schema.
|
|
136
|
+
|
|
137
|
+
### Deploying the backend
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Clone and build
|
|
141
|
+
git clone https://github.com/eea/rag-facts-check.git
|
|
142
|
+
cd rag-facts-check
|
|
143
|
+
docker build -t rag-fact-check .
|
|
144
|
+
|
|
145
|
+
# Run (requires an LLM endpoint)
|
|
146
|
+
# Set LLM_API_KEY to your actual API key before running
|
|
147
|
+
docker run -p 8000:8000 \
|
|
148
|
+
-e LLM_API_BASE=http://your-llm:4002/v1 \
|
|
149
|
+
-e LLM_API_KEY \
|
|
150
|
+
-e LLM_MODEL=gemma \
|
|
151
|
+
rag-fact-check
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
See the [backend README](https://github.com/eea/rag-facts-check#rag-facts-check)
|
|
155
|
+
for full configuration options.
|
|
156
|
+
|
|
119
157
|
## Environment Configuration
|
|
120
158
|
|
|
121
159
|
To properly configure the middleware and authenticate with the Onyx service, ensure that the following environment variables are set:
|
|
@@ -131,20 +169,13 @@ This document lists the environment variables used in the Volto Chatbot project.
|
|
|
131
169
|
- `JEST_USE_SETUP`
|
|
132
170
|
Used in Jest configuration. When set to 'ON', it enables a specific Jest setup.
|
|
133
171
|
|
|
134
|
-
- `
|
|
135
|
-
The URL for the
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
The token for authenticating with the LLM Gateway service.
|
|
172
|
+
- `RAG_FACT_CHECKER_URL`
|
|
173
|
+
The base URL for the [rag-facts-check](https://github.com/eea/rag-facts-check)
|
|
174
|
+
fact-checking backend. Required when `qualityCheck` is enabled.
|
|
175
|
+
Default: `http://localhost:8000`.
|
|
139
176
|
|
|
140
177
|
### Development-specific environment variables
|
|
141
178
|
|
|
142
|
-
- `MOCK_HALLOUMI_FILE_PATH`
|
|
143
|
-
When set, this specifies the absolute path to the JSON file containing the mocked Halloumi response. Setting this variable enables mocking of Halloumi API calls.
|
|
144
|
-
|
|
145
|
-
- `DUMP_HALLOUMI_FILE_PATH`
|
|
146
|
-
When set, the Halloumi response will be dumped to the specified absolute file path for debugging or to create new mock files.
|
|
147
|
-
|
|
148
179
|
- `MOCK_LLM_FILE_PATH`
|
|
149
180
|
When set, this specifies the absolute path to the JSONL file containing the mocked Onyx stream response. Setting this variable enables mocking of Onyx LLM calls.
|
|
150
181
|
|
package/package.json
CHANGED
|
@@ -81,31 +81,21 @@ function addQualityMarkersPlugin() {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// const sourceType = doc.source_type
|
|
100
|
-
// ? sources[doc.source_type] || capitalize(doc.source_type)
|
|
101
|
-
// : '';
|
|
102
|
-
|
|
103
|
-
// const header = `${docIndex}${doc.semantic_identifier}${
|
|
104
|
-
// sourceType ? `\nSource: ${sourceType}` : ''
|
|
105
|
-
// }${updatedDate ? `\nUpdated: ${updatedDate}` : ''}`;
|
|
106
|
-
|
|
107
|
-
// return `${header}\n${text}`;
|
|
108
|
-
return text.replace(/\u00A0/g, ' ');
|
|
84
|
+
/**
|
|
85
|
+
* Build a structured source object for the fact-checker backend.
|
|
86
|
+
*
|
|
87
|
+
* The backend accepts {text, title, source_type} dicts so it can
|
|
88
|
+
* include document titles in LLM prompts without polluting the raw
|
|
89
|
+
* text (which would break span-offset matching).
|
|
90
|
+
*/
|
|
91
|
+
export function buildHalloumiSource(doc: any, text: string) {
|
|
92
|
+
const cleanedText = text.replace(/\u00A0/g, ' ');
|
|
93
|
+
return {
|
|
94
|
+
text: cleanedText,
|
|
95
|
+
title: doc.semantic_identifier || null,
|
|
96
|
+
source_type: doc.source_type || null,
|
|
97
|
+
link: doc.link || null,
|
|
98
|
+
};
|
|
109
99
|
}
|
|
110
100
|
|
|
111
101
|
function mapToolDocumentsToText(message: any) {
|
|
@@ -135,24 +125,36 @@ function getContextSources(
|
|
|
135
125
|
const documentIdToText = mapToolDocumentsToText(message);
|
|
136
126
|
|
|
137
127
|
return qualityCheckContext === 'citations'
|
|
138
|
-
? sources.map((doc: any) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
? sources.map((doc: any) => {
|
|
129
|
+
// Prefer content from tool packets (may have enriched text),
|
|
130
|
+
// but fall back to doc.content from final_documents.
|
|
131
|
+
// Without this fallback, sources sent to the fact-checker are
|
|
132
|
+
// empty strings when tool packets don't carry full content.
|
|
133
|
+
const text = documentIdToText[doc.document_id] || doc.content || '';
|
|
134
|
+
const cleanedText = text.replace(/\u00A0/g, ' ');
|
|
135
|
+
return {
|
|
136
|
+
...doc,
|
|
137
|
+
id: doc.document_id,
|
|
138
|
+
text,
|
|
139
|
+
// Keep for ClaimSegments span highlighting (plain text)
|
|
140
|
+
halloumiContext: cleanedText,
|
|
141
|
+
// Structured source for the fact-checker backend
|
|
142
|
+
halloumiSource: buildHalloumiSource(doc, text),
|
|
143
|
+
};
|
|
144
|
+
})
|
|
147
145
|
: (message.toolCalls || []).reduce(
|
|
148
146
|
(acc: any, cur: any) => [
|
|
149
147
|
...acc,
|
|
150
|
-
...(cur.tool_result || []).map((doc: any) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
...(cur.tool_result || []).map((doc: any) => {
|
|
149
|
+
const cleanedText = (doc.content || '').replace(/\u00A0/g, ' ');
|
|
150
|
+
return {
|
|
151
|
+
...doc,
|
|
152
|
+
id: doc.document_id,
|
|
153
|
+
text: doc.content,
|
|
154
|
+
halloumiContext: cleanedText,
|
|
155
|
+
halloumiSource: buildHalloumiSource(doc, doc.content || ''),
|
|
156
|
+
};
|
|
157
|
+
}),
|
|
156
158
|
], // TODO: make sure we don't add multiple times the same doc
|
|
157
159
|
// TODO: this doesn't have the index for source
|
|
158
160
|
[],
|
|
@@ -281,7 +283,24 @@ export function AIMessage({
|
|
|
281
283
|
qualityCheckContext,
|
|
282
284
|
);
|
|
283
285
|
|
|
284
|
-
|
|
286
|
+
// Deduplicate sources by text content. The streaming backend often sends
|
|
287
|
+
// the same document in multiple packets (MESSAGE_START, SEARCH_TOOL_DELTA,
|
|
288
|
+
// etc.), resulting in 40+ copies of the same text. Dedup keeps the first
|
|
289
|
+
// occurrence, preserving order. Critical: texts must remain identical
|
|
290
|
+
// between frontend (span highlighting) and backend (evidence spans).
|
|
291
|
+
const dedupedSources = useMemo(() => {
|
|
292
|
+
const seen = new Set<string>();
|
|
293
|
+
return contextSources.filter((src: any) => {
|
|
294
|
+
const key = src.halloumiContext || src.halloumiSource?.text || '';
|
|
295
|
+
if (key && !seen.has(key)) {
|
|
296
|
+
seen.add(key);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
});
|
|
301
|
+
}, [contextSources]);
|
|
302
|
+
|
|
303
|
+
const stableContextSources = useDeepCompareMemoize(dedupedSources);
|
|
285
304
|
|
|
286
305
|
const doQualityControl =
|
|
287
306
|
messageDisplayed &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Modal, ModalContent, ModalHeader } from 'semantic-ui-react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import {
|
|
3
|
+
import { scoreToLabel } from '@eeacms/volto-eea-chatbot/ChatBlock/utils';
|
|
4
4
|
import SVGIcon from '@eeacms/volto-eea-chatbot/ChatBlock/components/Icon';
|
|
5
5
|
import { getSupportedBgColor } from './colors';
|
|
6
6
|
import { ClaimSegments } from './ClaimSegments';
|
|
@@ -70,9 +70,9 @@ export function ClaimModal({ claim, markers, text, citedSources }) {
|
|
|
70
70
|
<div className="score-badge-section">
|
|
71
71
|
<div className="score-badge">
|
|
72
72
|
<span className="score-percentage">
|
|
73
|
-
{
|
|
73
|
+
{scoreToLabel(claim.score)}
|
|
74
74
|
</span>
|
|
75
|
-
<span className="score-label">
|
|
75
|
+
<span className="score-label">Verification</span>
|
|
76
76
|
</div>
|
|
77
77
|
<div className="score-progress-bar">
|
|
78
78
|
<div
|
|
@@ -43,6 +43,7 @@ export function components(message, markers, citedSources) {
|
|
|
43
43
|
const end = child.position.end.offset;
|
|
44
44
|
const claims = markers.claims?.filter(
|
|
45
45
|
(claim) =>
|
|
46
|
+
!claim.skipped &&
|
|
46
47
|
claim.score !== null &&
|
|
47
48
|
((start >= claim.startOffset && end <= claim.endOffset) ||
|
|
48
49
|
(start <= claim.endOffset && end >= claim.endOffset) ||
|
|
@@ -48,7 +48,16 @@ export function useQualityMarkers(
|
|
|
48
48
|
|
|
49
49
|
React.useEffect(() => {
|
|
50
50
|
async function handler() {
|
|
51
|
-
|
|
51
|
+
// Prefer structured sources (with title metadata) over plain text.
|
|
52
|
+
// Fall back to halloumiContext strings for backward compatibility.
|
|
53
|
+
const firstSource = sources[0];
|
|
54
|
+
const hasStructuredSources =
|
|
55
|
+
firstSource && typeof firstSource.halloumiSource === 'object';
|
|
56
|
+
|
|
57
|
+
const halloumiSources = hasStructuredSources
|
|
58
|
+
? sources.map(({ halloumiSource }) => halloumiSource)
|
|
59
|
+
: sources.map(({ halloumiContext }) => halloumiContext);
|
|
60
|
+
|
|
52
61
|
if (sources.length === 0) {
|
|
53
62
|
setHalloumiResponse(empty(message, FAILURE_RATIONALE));
|
|
54
63
|
return;
|
|
@@ -59,7 +68,7 @@ export function useQualityMarkers(
|
|
|
59
68
|
try {
|
|
60
69
|
const feedback = await fetchHalloumi(
|
|
61
70
|
message,
|
|
62
|
-
|
|
71
|
+
halloumiSources,
|
|
63
72
|
maxContextSegments,
|
|
64
73
|
);
|
|
65
74
|
const body = await feedback.json();
|
|
@@ -211,8 +211,9 @@ export class MessageProcessor {
|
|
|
211
211
|
this._documents = Array.from(this.documentMap.values());
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// If we have final_documents and no citations yet, create a fallback mapping
|
|
215
|
-
// This ensures the Sources tab shows up in v3 when citation_info is missing
|
|
214
|
+
// If we have final_documents and no citations yet, create a fallback mapping.
|
|
215
|
+
// This ensures the Sources tab shows up in v3 when citation_info is missing.
|
|
216
|
+
// The fallback is cleared in processCitations() when real citation_info arrives.
|
|
216
217
|
if (
|
|
217
218
|
packet.obj.type === PacketType.MESSAGE_START &&
|
|
218
219
|
data.final_documents &&
|
|
@@ -240,9 +241,12 @@ export class MessageProcessor {
|
|
|
240
241
|
*/
|
|
241
242
|
private processCitations(packet: Packet) {
|
|
242
243
|
if (packet.obj.type === PacketType.CITATION_INFO) {
|
|
244
|
+
// First real citation_info arrived — discard the fallback mapping
|
|
245
|
+
// (which mapped ALL final_documents) and start fresh with actual citations.
|
|
246
|
+
this.clearFallbackCitations();
|
|
247
|
+
|
|
243
248
|
const citationInfo = packet.obj as any;
|
|
244
249
|
if (citationInfo.citation_number && citationInfo.document_id) {
|
|
245
|
-
this.clearFallbackCitations();
|
|
246
250
|
this._citations.set(
|
|
247
251
|
citationInfo.citation_number,
|
|
248
252
|
citationInfo.document_id,
|
|
@@ -58,6 +58,16 @@ export const useCopyToClipboard = (text: string): [boolean, () => void] => {
|
|
|
58
58
|
return [copied, copy];
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Map a 0-1 claim score to a categorical label.
|
|
63
|
+
* Scores: 1.0 = supported, 0.4 = not_enough_info, 0.0 = contradicted.
|
|
64
|
+
*/
|
|
65
|
+
export function scoreToLabel(score: number): string {
|
|
66
|
+
if (score >= 0.8) return 'High';
|
|
67
|
+
if (score >= 0.1) return 'Low';
|
|
68
|
+
return 'Failed';
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
export function convertToPercentage(
|
|
62
72
|
floatValue: number,
|
|
63
73
|
digits: number = 2,
|
package/src/halloumi/README.md
CHANGED
|
@@ -1 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
# Halloumi Fact-Checking Middleware
|
|
2
|
+
|
|
3
|
+
This middleware proxies fact-checking requests to the **rag-fact-checker** service (https://github.com/eea/rag-fact-check), which replaces the original Halloumi LLM-based integration.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Frontend (useQualityMarkers.js)
|
|
9
|
+
→ POST /_ha/generate
|
|
10
|
+
→ Express middleware (middleware.js)
|
|
11
|
+
→ POST {RAG_FACT_CHECKER_URL}/halloumi/generate
|
|
12
|
+
→ rag-fact-checker FastAPI service
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The rag-fact-checker's `/halloumi/generate` endpoint is a drop-in replacement for the original Halloumi API — it accepts the same request format (`{answer, sources}`) and returns a compatible response (`{claims, segments}`), so all existing frontend components (`HalloumiFeedback`, `useQualityMarkers`, `ClaimSegments`) work without changes.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
| Environment Variable | Default | Description |
|
|
20
|
+
| ---------------------- | ----------------------- | ---------------------------------------- |
|
|
21
|
+
| `RAG_FACT_CHECKER_URL` | `http://localhost:8000` | Base URL of the rag-fact-checker service |
|
|
22
|
+
|
|
23
|
+
## Original Halloumi
|
|
24
|
+
|
|
25
|
+
The original implementation was based on Apache2 licensed https://github.com/oumi-ai/halloumi-demo/tree/d088e1f25e7785326a53bc120113e226ee2f54b7
|
|
26
|
+
|
|
27
|
+
It used a custom LLM model (HallOumi-8B) accessed via an LLM gateway, with complex prompt preprocessing, chunking, logprob-based postprocessing, and claim filtering. This has been replaced by the rag-fact-checker service which uses a claim extraction + per-claim verification pipeline.
|
|
@@ -1,41 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import debug from 'debug';
|
|
3
3
|
import { isPathAllowed } from '../middleware';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const log = debug('halloumi');
|
|
6
6
|
|
|
7
7
|
const MSG_INVALID_CONFIGURATION =
|
|
8
|
-
'Invalid configuration: missing
|
|
8
|
+
'Invalid configuration: missing RAG_FACT_CHECKER_URL';
|
|
9
9
|
|
|
10
10
|
// Allowed paths for _ha (Halloumi) proxy.
|
|
11
11
|
// When adding new endpoints, update this list.
|
|
12
|
-
const ALLOWED_HALLOUMI_PATHS = [
|
|
13
|
-
{ path: '/generate', methods: ['POST'] },
|
|
14
|
-
{ path: '/classify', methods: ['POST'] },
|
|
15
|
-
];
|
|
12
|
+
const ALLOWED_HALLOUMI_PATHS = [{ path: '/generate', methods: ['POST'] }];
|
|
16
13
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const generativeModel = {
|
|
21
|
-
name: 'Inhouse-LLM/HallOumi-8B',
|
|
22
|
-
apiUrl: `${LLMGW_URL}/chat/completions`,
|
|
23
|
-
plattScaling: {
|
|
24
|
-
a: -0.5764390035379638,
|
|
25
|
-
b: 0.16648741572432335,
|
|
26
|
-
},
|
|
27
|
-
isEmbeddingModel: false,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const classifyModel = {
|
|
31
|
-
name: 'Inhouse-LLM/HallOumi-8B-classifier',
|
|
32
|
-
apiUrl: `${LLMGW_URL}/v1/embeddings`,
|
|
33
|
-
isEmbeddingModel: true,
|
|
34
|
-
plattScaling: {
|
|
35
|
-
a: -0.9468640744087437,
|
|
36
|
-
b: -0.07379217647931409,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
14
|
+
const RAG_FACT_CHECKER_URL =
|
|
15
|
+
process.env.RAG_FACT_CHECKER_URL || 'http://localhost:8000';
|
|
39
16
|
|
|
40
17
|
export default async function middleware(req, res, next) {
|
|
41
18
|
const path = req.url.replace('/_ha/', '/');
|
|
@@ -48,39 +25,52 @@ export default async function middleware(req, res, next) {
|
|
|
48
25
|
return;
|
|
49
26
|
}
|
|
50
27
|
|
|
51
|
-
if (!
|
|
28
|
+
if (!RAG_FACT_CHECKER_URL) {
|
|
52
29
|
res.send({
|
|
53
30
|
error: MSG_INVALID_CONFIGURATION,
|
|
54
31
|
});
|
|
55
32
|
return;
|
|
56
33
|
}
|
|
57
34
|
|
|
58
|
-
const model = {
|
|
59
|
-
...(path === '/classify' ? classifyModel : generativeModel),
|
|
60
|
-
apiKey: LLMGW_TOKEN, //betterleaks:allow
|
|
61
|
-
};
|
|
62
35
|
const body = req.body;
|
|
63
|
-
|
|
64
|
-
// log('Halloumi body', body);
|
|
65
36
|
const { sources, answer } = body;
|
|
66
37
|
|
|
38
|
+
log('Halloumi request body', {
|
|
39
|
+
answer,
|
|
40
|
+
sources: sources?.length,
|
|
41
|
+
});
|
|
42
|
+
|
|
67
43
|
res.set('Content-Type', 'application/json');
|
|
68
44
|
|
|
69
45
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
46
|
+
const response = await fetch(`${RAG_FACT_CHECKER_URL}/halloumi/generate`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
answer,
|
|
53
|
+
sources,
|
|
54
|
+
max_context_segments: 0,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await response.json();
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
log('Halloumi error response', result);
|
|
62
|
+
res.status(response.status).send({
|
|
63
|
+
error: `Fact-checker error: ${result.detail || result.error || 'unknown error'}`,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log('Halloumi response', result);
|
|
69
|
+
res.send(result);
|
|
79
70
|
} catch (error) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
log('Halloumi fetch error', error);
|
|
72
|
+
res.status(502).send({
|
|
73
|
+
error: `Fact-checker unavailable: ${error.message || 'connection failed'}`,
|
|
83
74
|
});
|
|
84
|
-
// throw error;
|
|
85
75
|
}
|
|
86
76
|
}
|