@eeacms/volto-eea-chatbot 3.0.1 → 4.1.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 +53 -4
- package/README.md +115 -12
- package/package.json +1 -1
- package/src/ChatBlock/ChatBlockView.jsx +7 -1
- package/src/ChatBlock/chat/AIMessage.tsx +79 -45
- package/src/ChatBlock/chat/ChatMessage.tsx +17 -10
- package/src/ChatBlock/chat/ChatWindow.tsx +11 -0
- package/src/ChatBlock/chat/MessageContext.ts +12 -0
- package/src/ChatBlock/chat/index.ts +1 -0
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
- package/src/ChatBlock/components/markdown/index.jsx +1 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +21 -4
- package/src/ChatBlock/index.js +9 -1
- package/src/ChatBlock/packets/RendererComponent.tsx +9 -0
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +15 -3
- package/src/ChatBlock/schema.jsx +13 -0
- package/src/ChatBlock/services/messageProcessor.ts +9 -12
- package/src/ChatBlock/types/interfaces.ts +25 -0
- package/src/ChatBlock/utils/index.tsx +10 -0
- package/src/ChatBlock/variations.ts +44 -0
- package/src/halloumi/README.md +27 -1
- package/src/halloumi/middleware.js +41 -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
|
@@ -5,13 +5,18 @@ const Sentry = loadable.lib(
|
|
|
5
5
|
() => import(/* webpackChunkName: "s_entry-browser" */ '@sentry/browser'), // chunk name avoids ad blockers
|
|
6
6
|
);
|
|
7
7
|
|
|
8
|
-
async function fetchHalloumi(answer, sources, maxContextSegments) {
|
|
8
|
+
async function fetchHalloumi(answer, sources, maxContextSegments, batchSize) {
|
|
9
9
|
const halloumiResponse = await fetch('/_ha/generate', {
|
|
10
10
|
method: 'POST',
|
|
11
11
|
headers: {
|
|
12
12
|
'Content-Type': 'application/json',
|
|
13
13
|
},
|
|
14
|
-
body: JSON.stringify({
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
answer,
|
|
16
|
+
sources,
|
|
17
|
+
maxContextSegments,
|
|
18
|
+
...(batchSize != null && { batch_size: batchSize }),
|
|
19
|
+
}),
|
|
15
20
|
});
|
|
16
21
|
return halloumiResponse;
|
|
17
22
|
}
|
|
@@ -38,6 +43,7 @@ export function useQualityMarkers(
|
|
|
38
43
|
message,
|
|
39
44
|
sources,
|
|
40
45
|
maxContextSegments = 0,
|
|
46
|
+
batchSize,
|
|
41
47
|
) {
|
|
42
48
|
const [halloumiResponse, setHalloumiResponse] = React.useState(null);
|
|
43
49
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
@@ -48,7 +54,16 @@ export function useQualityMarkers(
|
|
|
48
54
|
|
|
49
55
|
React.useEffect(() => {
|
|
50
56
|
async function handler() {
|
|
51
|
-
|
|
57
|
+
// Prefer structured sources (with title metadata) over plain text.
|
|
58
|
+
// Fall back to halloumiContext strings for backward compatibility.
|
|
59
|
+
const firstSource = sources[0];
|
|
60
|
+
const hasStructuredSources =
|
|
61
|
+
firstSource && typeof firstSource.halloumiSource === 'object';
|
|
62
|
+
|
|
63
|
+
const halloumiSources = hasStructuredSources
|
|
64
|
+
? sources.map(({ halloumiSource }) => halloumiSource)
|
|
65
|
+
: sources.map(({ halloumiContext }) => halloumiContext);
|
|
66
|
+
|
|
52
67
|
if (sources.length === 0) {
|
|
53
68
|
setHalloumiResponse(empty(message, FAILURE_RATIONALE));
|
|
54
69
|
return;
|
|
@@ -59,8 +74,9 @@ export function useQualityMarkers(
|
|
|
59
74
|
try {
|
|
60
75
|
const feedback = await fetchHalloumi(
|
|
61
76
|
message,
|
|
62
|
-
|
|
77
|
+
halloumiSources,
|
|
63
78
|
maxContextSegments,
|
|
79
|
+
batchSize,
|
|
64
80
|
);
|
|
65
81
|
const body = await feedback.json();
|
|
66
82
|
// console.log({ message, sources, body });
|
|
@@ -88,6 +104,7 @@ export function useQualityMarkers(
|
|
|
88
104
|
message,
|
|
89
105
|
sources,
|
|
90
106
|
maxContextSegments,
|
|
107
|
+
batchSize,
|
|
91
108
|
]);
|
|
92
109
|
|
|
93
110
|
if (halloumiResponse !== null) {
|
package/src/ChatBlock/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import codeSVG from '@plone/volto/icons/code.svg';
|
|
2
2
|
import ChatBlockView from './ChatBlockView';
|
|
3
3
|
import ChatBlockEdit from './ChatBlockEdit';
|
|
4
|
+
import ChatWindow from './chat/ChatWindow';
|
|
4
5
|
import { ChatBlockSchema } from './schema';
|
|
5
6
|
|
|
6
7
|
export default function installChatBlock(config) {
|
|
@@ -26,7 +27,14 @@ export default function installChatBlock(config) {
|
|
|
26
27
|
addPermission: [],
|
|
27
28
|
view: [],
|
|
28
29
|
},
|
|
29
|
-
variations: [
|
|
30
|
+
variations: [
|
|
31
|
+
{
|
|
32
|
+
id: 'classic',
|
|
33
|
+
title: 'Classic',
|
|
34
|
+
isDefault: true,
|
|
35
|
+
view: ChatWindow,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
if (!config.blocks.blocksConfig.danswerChat) {
|
|
@@ -87,6 +87,9 @@ export function RendererComponent({
|
|
|
87
87
|
markers,
|
|
88
88
|
stableContextSources,
|
|
89
89
|
addQualityMarkersPlugin,
|
|
90
|
+
extraMarkdownComponents,
|
|
91
|
+
extraRemarkPlugins,
|
|
92
|
+
extraRehypePlugins,
|
|
90
93
|
}: {
|
|
91
94
|
packets: Packet[];
|
|
92
95
|
onComplete: () => void;
|
|
@@ -98,6 +101,9 @@ export function RendererComponent({
|
|
|
98
101
|
markers?: any;
|
|
99
102
|
stableContextSources?: any;
|
|
100
103
|
addQualityMarkersPlugin?: any;
|
|
104
|
+
extraMarkdownComponents?: Record<string, React.ComponentType<any>>;
|
|
105
|
+
extraRemarkPlugins?: any[];
|
|
106
|
+
extraRehypePlugins?: any[];
|
|
101
107
|
}) {
|
|
102
108
|
const RendererFn = findRenderer({ packets });
|
|
103
109
|
|
|
@@ -116,6 +122,9 @@ export function RendererComponent({
|
|
|
116
122
|
markers={markers}
|
|
117
123
|
stableContextSources={stableContextSources}
|
|
118
124
|
addQualityMarkersPlugin={addQualityMarkersPlugin}
|
|
125
|
+
extraMarkdownComponents={extraMarkdownComponents}
|
|
126
|
+
extraRemarkPlugins={extraRemarkPlugins}
|
|
127
|
+
extraRehypePlugins={extraRehypePlugins}
|
|
119
128
|
>
|
|
120
129
|
{children}
|
|
121
130
|
</RendererFn>
|
|
@@ -30,6 +30,9 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
30
30
|
markers,
|
|
31
31
|
stableContextSources,
|
|
32
32
|
addQualityMarkersPlugin,
|
|
33
|
+
extraMarkdownComponents,
|
|
34
|
+
extraRemarkPlugins = [],
|
|
35
|
+
extraRehypePlugins = [],
|
|
33
36
|
}) => {
|
|
34
37
|
const { remarkGfm } = libs;
|
|
35
38
|
|
|
@@ -163,9 +166,18 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
163
166
|
const renderedContent = (
|
|
164
167
|
<div className="message-text-content">
|
|
165
168
|
<Markdown
|
|
166
|
-
components={
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
components={{
|
|
170
|
+
...components(message, markers, stableContextSources),
|
|
171
|
+
...extraMarkdownComponents,
|
|
172
|
+
}}
|
|
173
|
+
remarkPlugins={[
|
|
174
|
+
...(remarkGfm ? [remarkGfm.default] : []),
|
|
175
|
+
...extraRemarkPlugins,
|
|
176
|
+
]}
|
|
177
|
+
rehypePlugins={[
|
|
178
|
+
...(addQualityMarkersPlugin ? [addQualityMarkersPlugin] : []),
|
|
179
|
+
...extraRehypePlugins,
|
|
180
|
+
]}
|
|
169
181
|
>
|
|
170
182
|
{displayContent}
|
|
171
183
|
</Markdown>
|
package/src/ChatBlock/schema.jsx
CHANGED
|
@@ -92,6 +92,7 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
92
92
|
...(data.qualityCheck && data.qualityCheck !== 'disabled'
|
|
93
93
|
? [
|
|
94
94
|
'maxContextSegments',
|
|
95
|
+
'batchSize',
|
|
95
96
|
'noSupportDocumentsMessage',
|
|
96
97
|
'qualityCheckContext',
|
|
97
98
|
'qualityCheckStages',
|
|
@@ -109,6 +110,11 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
109
110
|
},
|
|
110
111
|
],
|
|
111
112
|
properties: {
|
|
113
|
+
// NOTE: the `variation` field is NOT declared here on purpose. Volto
|
|
114
|
+
// core injects it automatically in the block sidebar
|
|
115
|
+
// (withVariationSchemaEnhancer, wired into BlockDataForm) from
|
|
116
|
+
// blocksConfig.eeaChatbot.variations whenever 2+ variations are
|
|
117
|
+
// registered. See @plone/volto/helpers/Extensions.
|
|
112
118
|
enableShowTotalFailMessage: {
|
|
113
119
|
title: 'Show total failure message',
|
|
114
120
|
type: 'boolean',
|
|
@@ -252,6 +258,13 @@ range is from 0 to 100`,
|
|
|
252
258
|
description:
|
|
253
259
|
'Optimize quality check performance by grouping sentences into a max number of segments. Set to 0 to disable grouping and instead use one sentence per segment.',
|
|
254
260
|
},
|
|
261
|
+
batchSize: {
|
|
262
|
+
title: 'Batch size',
|
|
263
|
+
type: 'number',
|
|
264
|
+
default: 20,
|
|
265
|
+
description:
|
|
266
|
+
'Number of claims to verify per LLM call. Higher values mean fewer LLM calls but larger prompts. Set to 1 for sequential per-claim verification.',
|
|
267
|
+
},
|
|
255
268
|
feedbackReasons: {
|
|
256
269
|
title: 'Feedback reasons',
|
|
257
270
|
description: 'Select the reasons for negative feedback.',
|
|
@@ -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,
|
|
@@ -254,15 +258,8 @@ export class MessageProcessor {
|
|
|
254
258
|
return;
|
|
255
259
|
}
|
|
256
260
|
const citationDelta = packet.obj as CitationDelta;
|
|
257
|
-
|
|
258
|
-
(citation
|
|
259
|
-
citation.citation_num != null && Boolean(citation.document_id),
|
|
260
|
-
);
|
|
261
|
-
if (explicitCitations.length > 0) {
|
|
262
|
-
this.clearFallbackCitations();
|
|
263
|
-
}
|
|
264
|
-
explicitCitations.forEach((citation: StreamingCitation) => {
|
|
265
|
-
if (!this._citations.has(citation.citation_num)) {
|
|
261
|
+
citationDelta.citations?.forEach((citation: StreamingCitation) => {
|
|
262
|
+
if (citation.document_id && !this._citations.has(citation.citation_num)) {
|
|
266
263
|
this._citations.set(citation.citation_num, citation.document_id);
|
|
267
264
|
}
|
|
268
265
|
});
|
|
@@ -82,6 +82,17 @@ export interface MessageRendererProps<T extends Packet = Packet> {
|
|
|
82
82
|
markers?: any;
|
|
83
83
|
stableContextSources?: any;
|
|
84
84
|
addQualityMarkersPlugin?: any;
|
|
85
|
+
/**
|
|
86
|
+
* Extra react-markdown component overrides (e.g. for custom elements
|
|
87
|
+
* produced by remark/rehype plugins), merged on top of the built-in
|
|
88
|
+
* mapping. Allows presentations to plug custom components inline in the
|
|
89
|
+
* message text.
|
|
90
|
+
*/
|
|
91
|
+
extraMarkdownComponents?: Record<string, React.ComponentType<any>>;
|
|
92
|
+
/** Extra remark plugins, appended after the built-in ones. */
|
|
93
|
+
extraRemarkPlugins?: any[];
|
|
94
|
+
/** Extra rehype plugins, appended after the built-in ones. */
|
|
95
|
+
extraRehypePlugins?: any[];
|
|
85
96
|
onComplete: () => void;
|
|
86
97
|
animate: boolean;
|
|
87
98
|
stopPacketSeen: boolean;
|
|
@@ -141,11 +152,25 @@ export interface ChatMessageProps {
|
|
|
141
152
|
enableMatomoTracking?: boolean;
|
|
142
153
|
persona?: number;
|
|
143
154
|
maxContextSegments?: number;
|
|
155
|
+
batchSize?: number;
|
|
144
156
|
isLastMessage?: boolean;
|
|
145
157
|
className?: string;
|
|
146
158
|
chatWindowRef?: React.RefObject<HTMLDivElement>;
|
|
147
159
|
chatWindowEndRef?: React.RefObject<HTMLDivElement>;
|
|
148
160
|
showTools?: PacketType[];
|
|
161
|
+
/**
|
|
162
|
+
* Hide the classic "Sources" tab, sidebar and inline source list for this
|
|
163
|
+
* presentation (the answer text still renders, and any injected markdown
|
|
164
|
+
* components still work). Used by variations that present the cited
|
|
165
|
+
* documents differently (e.g. inline catalogue cards).
|
|
166
|
+
*/
|
|
167
|
+
hideSourcesTab?: boolean;
|
|
168
|
+
/** Extra remark plugins merged after the built-in ones. */
|
|
169
|
+
extraRemarkPlugins?: any[];
|
|
170
|
+
/** Extra react-markdown components merged after the built-in ones. */
|
|
171
|
+
extraMarkdownComponents?: any;
|
|
172
|
+
/** Extra rehype plugins merged after the built-in ones. */
|
|
173
|
+
extraRehypePlugins?: any[];
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
export interface Persona {
|
|
@@ -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,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A chatbot block presentation variation.
|
|
6
|
+
*
|
|
7
|
+
* The `view` component is a complete presentation of the chatbot block.
|
|
8
|
+
* It receives the same props the classic presentation (ChatWindow) receives
|
|
9
|
+
* today: `{ persona, data, block_id, isEditMode, isPlaywrightTest,
|
|
10
|
+
* initialQuery, initialDeepResearch, ... }` plus whatever the HOCs inject
|
|
11
|
+
* (e.g. `rehypePrism`, `remarkGfm` when the view wraps itself with
|
|
12
|
+
* `injectLazyLibs`).
|
|
13
|
+
*/
|
|
14
|
+
export interface ChatBlockVariation {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
isDefault?: boolean;
|
|
18
|
+
view: ComponentType<any>;
|
|
19
|
+
edit?: ComponentType<any>;
|
|
20
|
+
schemaEnhancer?: (args: { schema: any; formData: any }) => any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the active variation from the block data.
|
|
25
|
+
*
|
|
26
|
+
* Semantics follow the established `variations` pattern of the EEA Volto
|
|
27
|
+
* addons (volto-tabs-block, volto-block-data-table, listing block):
|
|
28
|
+
* explicit `data.variation` → `isDefault` → first registered → undefined.
|
|
29
|
+
* Old block content without a `variation` field resolves to the default.
|
|
30
|
+
*/
|
|
31
|
+
export function getChatVariation(
|
|
32
|
+
data: Record<string, any>,
|
|
33
|
+
): ChatBlockVariation | undefined {
|
|
34
|
+
const variations: ChatBlockVariation[] =
|
|
35
|
+
config.blocks?.blocksConfig?.eeaChatbot?.variations ?? [];
|
|
36
|
+
if (variations.length === 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
variations.find((v) => v.id === data?.variation) ??
|
|
41
|
+
variations.find((v) => v.isDefault) ??
|
|
42
|
+
variations[0]
|
|
43
|
+
);
|
|
44
|
+
}
|
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,53 @@ 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;
|
|
36
|
+
const { sources, answer, batch_size } = body;
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
|
|
38
|
+
log('Halloumi request body', {
|
|
39
|
+
answer,
|
|
40
|
+
sources: sources?.length,
|
|
41
|
+
});
|
|
66
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
|
+
...(batch_size != null && { batch_size }),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
log('Halloumi error response', result);
|
|
63
|
+
res.status(response.status).send({
|
|
64
|
+
error: `Fact-checker error: ${result.detail || result.error || 'unknown error'}`,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
log('Halloumi response', result);
|
|
70
|
+
res.send(result);
|
|
79
71
|
} catch (error) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
log('Halloumi fetch error', error);
|
|
73
|
+
res.status(502).send({
|
|
74
|
+
error: `Fact-checker unavailable: ${error.message || 'connection failed'}`,
|
|
83
75
|
});
|
|
84
|
-
// throw error;
|
|
85
76
|
}
|
|
86
77
|
}
|