@eeacms/volto-eea-chatbot 2.0.1 → 2.0.3
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/.eslintrc.js +6 -6
- package/CHANGELOG.md +20 -0
- package/artifacts/ONYX_V3_INTEGRATION.md +34 -0
- package/jest-addon.config.js +2 -1
- package/package.json +1 -1
- package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
- package/src/ChatBlock/chat/AIMessage.tsx +36 -16
- package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
- package/src/ChatBlock/chat/ChatWindow.tsx +13 -11
- package/src/ChatBlock/chat/UserMessage.tsx +4 -4
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
- package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
- package/src/ChatBlock/components/EmptyState.jsx +1 -1
- package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
- package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
- package/src/ChatBlock/components/Source.jsx +2 -2
- package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
- package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
- package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
- package/src/ChatBlock/hooks/useChatController.ts +67 -14
- package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
- package/src/ChatBlock/hooks/useToolDisplayTiming.ts +2 -1
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +86 -56
- package/src/ChatBlock/packets/RendererComponent.tsx +13 -5
- package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
- package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
- package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +14 -9
- package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +6 -5
- package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +30 -21
- package/src/ChatBlock/{schema.js → schema.jsx} +13 -0
- package/src/ChatBlock/services/messageProcessor.ts +72 -17
- package/src/ChatBlock/services/packetUtils.ts +13 -3
- package/src/ChatBlock/services/streamingService.ts +155 -68
- package/src/ChatBlock/types/streamingModels.ts +47 -2
- package/src/ChatBlock/utils/citations.ts +1 -1
- package/src/halloumi/filtering.test.js +199 -1
- package/src/middleware.js +18 -1
- package/src/middleware.test.js +14 -0
- package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
- package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
- package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
- package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
- package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
- package/src/ChatBlock/tests/Citation.test.jsx +0 -107
- package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
- package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
- package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
- package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
- package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
- package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
- package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
- package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
- package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
- package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
- package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
- package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
- package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
- package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
- package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
- package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
- package/src/ChatBlock/tests/Source.test.jsx +0 -79
- package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
- package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
- package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
- package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
- package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
- package/src/ChatBlock/tests/citations.test.js +0 -114
- package/src/ChatBlock/tests/index.test.js +0 -51
- package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
- package/src/ChatBlock/tests/packetUtils.test.js +0 -158
- package/src/ChatBlock/tests/schema.test.js +0 -166
- package/src/ChatBlock/tests/streamingService.test.js +0 -467
- package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
- package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
- package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
- package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
- package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
- package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
- package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
- package/src/ChatBlock/tests/utils.test.jsx +0 -241
- package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Packet } from '
|
|
2
|
-
import { PacketType } from '
|
|
1
|
+
import type { Packet } from '@eeacms/volto-eea-chatbot/ChatBlock/types/streamingModels';
|
|
2
|
+
import { PacketType } from '@eeacms/volto-eea-chatbot/ChatBlock/types/streamingModels';
|
|
3
3
|
|
|
4
4
|
export function getSynteticPacket(ind: number, type: PacketType): Packet {
|
|
5
5
|
return {
|
|
@@ -11,11 +11,15 @@ export function getSynteticPacket(ind: number, type: PacketType): Packet {
|
|
|
11
11
|
export function isToolPacket(packet: Packet): boolean {
|
|
12
12
|
const toolPacketTypes = [
|
|
13
13
|
PacketType.SEARCH_TOOL_START,
|
|
14
|
+
PacketType.SEARCH_TOOL_START_V3,
|
|
15
|
+
PacketType.SEARCH_TOOL_QUERIES_DELTA,
|
|
16
|
+
PacketType.SEARCH_TOOL_DOCUMENTS_DELTA,
|
|
14
17
|
PacketType.SEARCH_TOOL_DELTA,
|
|
15
18
|
PacketType.CUSTOM_TOOL_START,
|
|
16
19
|
PacketType.CUSTOM_TOOL_DELTA,
|
|
17
20
|
PacketType.REASONING_START,
|
|
18
21
|
PacketType.REASONING_DELTA,
|
|
22
|
+
PacketType.REASONING_DONE,
|
|
19
23
|
PacketType.FETCH_TOOL_START,
|
|
20
24
|
];
|
|
21
25
|
|
|
@@ -40,9 +44,15 @@ export function isFinalAnswerComplete(packets: Packet[]): boolean {
|
|
|
40
44
|
return false;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
const hasSectionEnd = packets.some(
|
|
44
48
|
(packet) =>
|
|
45
49
|
packet.obj.type === PacketType.SECTION_END &&
|
|
46
50
|
packet.ind === messageStartPacket.ind,
|
|
47
51
|
);
|
|
52
|
+
|
|
53
|
+
if (hasSectionEnd) {
|
|
54
|
+
console.log(`[isFinalAnswerComplete] Complete! ind=${messageStartPacket.ind}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return hasSectionEnd;
|
|
48
58
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
PacketType,
|
|
3
|
+
type Packet,
|
|
4
|
+
} from '@eeacms/volto-eea-chatbot/ChatBlock/types/streamingModels';
|
|
5
|
+
import type {
|
|
6
|
+
FileDescriptor,
|
|
7
|
+
Filters,
|
|
8
|
+
} from '@eeacms/volto-eea-chatbot/ChatBlock/types/interfaces';
|
|
3
9
|
|
|
4
10
|
export interface SendMessageParams {
|
|
5
11
|
regenerate: boolean;
|
|
@@ -24,6 +30,7 @@ export interface SendMessageParams {
|
|
|
24
30
|
enabledToolIds?: number[];
|
|
25
31
|
forcedToolIds?: number[];
|
|
26
32
|
retrieval_options?: any;
|
|
33
|
+
onyxVersion?: '2' | '3';
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export interface StreamResponse {
|
|
@@ -84,11 +91,131 @@ export const processRawChunkString = (
|
|
|
84
91
|
return [parsedChunkSections, currPartialChunk];
|
|
85
92
|
};
|
|
86
93
|
|
|
94
|
+
// ─── Payload Templates ─────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the Onyx 2.x send-message payload
|
|
98
|
+
*/
|
|
99
|
+
function buildPayloadV2(params: SendMessageParams): Record<string, unknown> {
|
|
100
|
+
const documentsAreSelected =
|
|
101
|
+
params.selectedDocumentIds && params.selectedDocumentIds.length > 0;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
alternate_assistant_id: params.alternateAssistantId,
|
|
105
|
+
chat_session_id: params.chatSessionId,
|
|
106
|
+
parent_message_id: params.parentMessageId,
|
|
107
|
+
message: params.message,
|
|
108
|
+
prompt_id: null,
|
|
109
|
+
search_doc_ids: documentsAreSelected ? params.selectedDocumentIds : null,
|
|
110
|
+
file_descriptors: params.fileDescriptors,
|
|
111
|
+
current_message_files: params.currentMessageFiles,
|
|
112
|
+
regenerate: params.regenerate,
|
|
113
|
+
retrieval_options:
|
|
114
|
+
params.retrieval_options ??
|
|
115
|
+
(!documentsAreSelected
|
|
116
|
+
? {
|
|
117
|
+
run_search:
|
|
118
|
+
params.queryOverride || params.forceSearch ? 'always' : 'auto',
|
|
119
|
+
real_time: true,
|
|
120
|
+
filters: params.filters,
|
|
121
|
+
}
|
|
122
|
+
: null),
|
|
123
|
+
query_override: params.queryOverride,
|
|
124
|
+
prompt_override: {
|
|
125
|
+
...(params.systemPromptOverride
|
|
126
|
+
? { system_prompt: params.systemPromptOverride }
|
|
127
|
+
: {}),
|
|
128
|
+
...(params.taskPromptOverride
|
|
129
|
+
? { task_prompt: params.taskPromptOverride }
|
|
130
|
+
: {}),
|
|
131
|
+
},
|
|
132
|
+
llm_override:
|
|
133
|
+
params.temperature || params.modelVersion
|
|
134
|
+
? {
|
|
135
|
+
temperature: params.temperature,
|
|
136
|
+
model_provider: params.modelProvider,
|
|
137
|
+
model_version: params.modelVersion,
|
|
138
|
+
}
|
|
139
|
+
: null,
|
|
140
|
+
use_existing_user_message: params.useExistingUserMessage,
|
|
141
|
+
use_agentic_search: params.useAgentSearch ?? false,
|
|
142
|
+
allowed_tool_ids: params.enabledToolIds,
|
|
143
|
+
forced_tool_ids: params.forcedToolIds,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build the Onyx 3.x send-message payload
|
|
149
|
+
*/
|
|
150
|
+
function buildPayloadV3(params: SendMessageParams): Record<string, unknown> {
|
|
151
|
+
const payload = {
|
|
152
|
+
message: params.message,
|
|
153
|
+
chat_session_id: params.chatSessionId,
|
|
154
|
+
parent_message_id: params.parentMessageId,
|
|
155
|
+
file_descriptors: params.fileDescriptors ?? [],
|
|
156
|
+
internal_search_filters: {
|
|
157
|
+
source_type: params.filters?.source_type ?? ['web', 'github'],
|
|
158
|
+
document_set: params.filters?.document_set ?? null,
|
|
159
|
+
time_cutoff: params.filters?.time_cutoff ?? null,
|
|
160
|
+
tags: params.filters?.tags ?? [],
|
|
161
|
+
},
|
|
162
|
+
deep_research: params.useAgentSearch ?? false,
|
|
163
|
+
allowed_tool_ids: params.enabledToolIds?.length
|
|
164
|
+
? params.enabledToolIds
|
|
165
|
+
: [1],
|
|
166
|
+
forced_tool_id: params.forcedToolIds?.[0] ?? null,
|
|
167
|
+
llm_override: {
|
|
168
|
+
temperature: params.temperature ?? 0.5,
|
|
169
|
+
model_provider:
|
|
170
|
+
params.modelProvider || 'Inhouse LiteLLM provider oss 120b',
|
|
171
|
+
model_version: params.modelVersion || 'Inhouse-LLM/gpt-oss-120b',
|
|
172
|
+
},
|
|
173
|
+
llm_overrides: null,
|
|
174
|
+
origin: 'webapp',
|
|
175
|
+
additional_context: null,
|
|
176
|
+
alternate_assistant_id: params.alternateAssistantId ?? null,
|
|
177
|
+
stream: true,
|
|
178
|
+
};
|
|
179
|
+
return payload;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Normalise a raw Onyx 3.x stream object into the canonical { ind, obj } Packet shape.
|
|
184
|
+
*/
|
|
185
|
+
function normaliseV3Chunk(raw: any): Packet | null {
|
|
186
|
+
// Bare identity packet (user/assistant message IDs – no placement wrapper)
|
|
187
|
+
if ('user_message_id' in raw && 'reserved_assistant_message_id' in raw) {
|
|
188
|
+
return {
|
|
189
|
+
ind: -1,
|
|
190
|
+
obj: {
|
|
191
|
+
type: PacketType.MESSAGE_END_ID_INFO,
|
|
192
|
+
user_message_id: raw.user_message_id,
|
|
193
|
+
reserved_assistant_message_id: raw.reserved_assistant_message_id,
|
|
194
|
+
},
|
|
195
|
+
} as Packet;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!raw.placement || typeof raw.obj !== 'object') return null;
|
|
199
|
+
|
|
200
|
+
const ind: number = raw.placement.turn_index ?? 0;
|
|
201
|
+
const obj = raw.obj;
|
|
202
|
+
|
|
203
|
+
// Map citation_number to citation_num for compatibility with CitationDelta consumers
|
|
204
|
+
if (obj.type === PacketType.CITATION_INFO && 'citation_number' in obj) {
|
|
205
|
+
obj.citation_num = obj.citation_number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const normalised = { ind, obj } as Packet;
|
|
209
|
+
// console.log('[Onyx v3] Normalised packet:', normalised);
|
|
210
|
+
return normalised;
|
|
211
|
+
}
|
|
212
|
+
|
|
87
213
|
/**
|
|
88
214
|
* Handle streaming response from the backend
|
|
89
215
|
*/
|
|
90
216
|
export async function* handleStream(
|
|
91
217
|
streamingResponse: Response,
|
|
218
|
+
onyxVersion: '2' | '3' = '2',
|
|
92
219
|
): AsyncGenerator<Packet[], void, unknown> {
|
|
93
220
|
const reader = streamingResponse.body?.getReader();
|
|
94
221
|
if (!reader) {
|
|
@@ -114,6 +241,10 @@ export async function* handleStream(
|
|
|
114
241
|
previousPartialChunk,
|
|
115
242
|
);
|
|
116
243
|
|
|
244
|
+
if (onyxVersion === '3' && completedChunks.length > 0) {
|
|
245
|
+
// console.log('[Onyx v3] Raw completed chunks:', completedChunks);
|
|
246
|
+
}
|
|
247
|
+
|
|
117
248
|
if (!completedChunks.length && !partialChunk) {
|
|
118
249
|
break;
|
|
119
250
|
}
|
|
@@ -124,6 +255,10 @@ export async function* handleStream(
|
|
|
124
255
|
const packets: Packet[] = completedChunks
|
|
125
256
|
.filter((chunk) => chunk && typeof chunk === 'object')
|
|
126
257
|
.map((chunk) => {
|
|
258
|
+
if (onyxVersion === '3') {
|
|
259
|
+
return normaliseV3Chunk(chunk);
|
|
260
|
+
}
|
|
261
|
+
|
|
127
262
|
// Onyx v2 format: { ind: number, obj: { type: string, ... } }
|
|
128
263
|
if ('ind' in chunk && 'obj' in chunk) {
|
|
129
264
|
return chunk as Packet;
|
|
@@ -167,77 +302,24 @@ export async function* handleStream(
|
|
|
167
302
|
* Send a message and stream the response
|
|
168
303
|
*/
|
|
169
304
|
export async function* sendMessage(
|
|
170
|
-
|
|
171
|
-
regenerate,
|
|
172
|
-
retrieval_options,
|
|
173
|
-
message,
|
|
174
|
-
fileDescriptors,
|
|
175
|
-
currentMessageFiles,
|
|
176
|
-
parentMessageId,
|
|
177
|
-
chatSessionId,
|
|
178
|
-
filters,
|
|
179
|
-
selectedDocumentIds,
|
|
180
|
-
queryOverride,
|
|
181
|
-
forceSearch,
|
|
182
|
-
modelProvider,
|
|
183
|
-
modelVersion,
|
|
184
|
-
temperature,
|
|
185
|
-
systemPromptOverride,
|
|
186
|
-
taskPromptOverride,
|
|
187
|
-
useExistingUserMessage,
|
|
188
|
-
alternateAssistantId,
|
|
189
|
-
signal,
|
|
190
|
-
useAgentSearch,
|
|
191
|
-
enabledToolIds,
|
|
192
|
-
forcedToolIds,
|
|
193
|
-
}: SendMessageParams,
|
|
305
|
+
params: SendMessageParams,
|
|
194
306
|
isRelatedQuestion: boolean = false,
|
|
195
307
|
): AsyncGenerator<Packet[], void, unknown> {
|
|
196
|
-
const
|
|
197
|
-
selectedDocumentIds && selectedDocumentIds.length > 0;
|
|
308
|
+
const { onyxVersion = '2', signal } = params;
|
|
198
309
|
|
|
199
|
-
const payload =
|
|
200
|
-
|
|
201
|
-
chat_session_id: chatSessionId,
|
|
202
|
-
parent_message_id: parentMessageId,
|
|
203
|
-
message,
|
|
204
|
-
prompt_id: null,
|
|
205
|
-
search_doc_ids: documentsAreSelected ? selectedDocumentIds : null,
|
|
206
|
-
file_descriptors: fileDescriptors,
|
|
207
|
-
current_message_files: currentMessageFiles,
|
|
208
|
-
regenerate,
|
|
209
|
-
retrieval_options:
|
|
210
|
-
retrieval_options ??
|
|
211
|
-
(!documentsAreSelected
|
|
212
|
-
? {
|
|
213
|
-
run_search: queryOverride || forceSearch ? 'always' : 'auto',
|
|
214
|
-
real_time: true,
|
|
215
|
-
filters: filters,
|
|
216
|
-
}
|
|
217
|
-
: null),
|
|
218
|
-
query_override: queryOverride,
|
|
219
|
-
prompt_override: {
|
|
220
|
-
...(systemPromptOverride ? { system_prompt: systemPromptOverride } : {}),
|
|
221
|
-
...(taskPromptOverride ? { task_prompt: taskPromptOverride } : {}),
|
|
222
|
-
},
|
|
223
|
-
llm_override:
|
|
224
|
-
temperature || modelVersion
|
|
225
|
-
? {
|
|
226
|
-
temperature,
|
|
227
|
-
model_provider: modelProvider,
|
|
228
|
-
model_version: modelVersion,
|
|
229
|
-
}
|
|
230
|
-
: null,
|
|
231
|
-
use_existing_user_message: useExistingUserMessage,
|
|
232
|
-
use_agentic_search: useAgentSearch ?? false,
|
|
233
|
-
allowed_tool_ids: enabledToolIds,
|
|
234
|
-
forced_tool_ids: forcedToolIds,
|
|
235
|
-
};
|
|
310
|
+
const payload =
|
|
311
|
+
onyxVersion === '3' ? buildPayloadV3(params) : buildPayloadV2(params);
|
|
236
312
|
|
|
237
313
|
const body = JSON.stringify(payload);
|
|
238
314
|
|
|
239
315
|
const middleware = isRelatedQuestion ? '_rq' : '_da';
|
|
240
|
-
const
|
|
316
|
+
const endpoint =
|
|
317
|
+
onyxVersion === '3' ? 'send-chat-message' : 'send-message';
|
|
318
|
+
|
|
319
|
+
console.log(`[sendMessage] Target URL: /${middleware}/chat/${endpoint} (v${onyxVersion})`);
|
|
320
|
+
console.log(`[sendMessage] Payload:`, payload);
|
|
321
|
+
|
|
322
|
+
const sendMessageResponse = await fetch(`/${middleware}/chat/${endpoint}`, {
|
|
241
323
|
method: 'POST',
|
|
242
324
|
headers: {
|
|
243
325
|
'Content-Type': 'application/json',
|
|
@@ -252,7 +334,7 @@ export async function* sendMessage(
|
|
|
252
334
|
throw new Error(`Failed to send message - ${errorMsg}`);
|
|
253
335
|
}
|
|
254
336
|
|
|
255
|
-
yield* handleStream(sendMessageResponse);
|
|
337
|
+
yield* handleStream(sendMessageResponse, onyxVersion);
|
|
256
338
|
}
|
|
257
339
|
|
|
258
340
|
/**
|
|
@@ -261,8 +343,13 @@ export async function* sendMessage(
|
|
|
261
343
|
export async function createChatSession(
|
|
262
344
|
personaId: number,
|
|
263
345
|
description?: string,
|
|
346
|
+
isRelatedQuestion: boolean = false,
|
|
264
347
|
): Promise<string> {
|
|
265
|
-
const
|
|
348
|
+
const middleware = isRelatedQuestion ? '_rq' : '_da';
|
|
349
|
+
const url = `/${middleware}/chat/create-chat-session`;
|
|
350
|
+
console.log(`[createChatSession] URL: ${url}`);
|
|
351
|
+
|
|
352
|
+
const response = await fetch(url, {
|
|
266
353
|
method: 'POST',
|
|
267
354
|
headers: {
|
|
268
355
|
'Content-Type': 'application/json',
|
|
@@ -51,6 +51,13 @@ export enum PacketType {
|
|
|
51
51
|
MESSAGE_END_ID_INFO = 'message_end_id_info',
|
|
52
52
|
|
|
53
53
|
ERROR = 'error',
|
|
54
|
+
|
|
55
|
+
// Onyx v3 replacements
|
|
56
|
+
SEARCH_TOOL_START_V3 = 'search_tool_start',
|
|
57
|
+
SEARCH_TOOL_QUERIES_DELTA = 'search_tool_queries_delta',
|
|
58
|
+
SEARCH_TOOL_DOCUMENTS_DELTA = 'search_tool_documents_delta',
|
|
59
|
+
CITATION_INFO = 'citation_info',
|
|
60
|
+
REASONING_DONE = 'reasoning_done',
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// Basic Message Packets
|
|
@@ -174,12 +181,44 @@ export interface ErrorObj extends BaseObj {
|
|
|
174
181
|
error: string;
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
// Onyx v3 Specific Packets
|
|
185
|
+
export interface SearchToolStartV3 extends BaseObj {
|
|
186
|
+
type: PacketType.SEARCH_TOOL_START_V3;
|
|
187
|
+
is_internet_search?: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface SearchToolQueriesDelta extends BaseObj {
|
|
191
|
+
type: PacketType.SEARCH_TOOL_QUERIES_DELTA;
|
|
192
|
+
queries: string[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface SearchToolDocumentsDelta extends BaseObj {
|
|
196
|
+
type: PacketType.SEARCH_TOOL_DOCUMENTS_DELTA;
|
|
197
|
+
documents: OnyxDocument[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface CitationInfo extends BaseObj {
|
|
201
|
+
type: PacketType.CITATION_INFO;
|
|
202
|
+
citation_number: number;
|
|
203
|
+
document_id: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ReasoningDone extends BaseObj {
|
|
207
|
+
type: PacketType.REASONING_DONE;
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
export type ChatObj = MessageStart | MessageDelta | MessageEnd;
|
|
178
211
|
export type StopObj = Stop;
|
|
179
212
|
export type SectionEndObj = SectionEnd;
|
|
180
213
|
|
|
181
214
|
// Specific tool objects
|
|
182
|
-
export type SearchToolObj =
|
|
215
|
+
export type SearchToolObj =
|
|
216
|
+
| SearchToolStart
|
|
217
|
+
| SearchToolDelta
|
|
218
|
+
| SearchToolStartV3
|
|
219
|
+
| SearchToolQueriesDelta
|
|
220
|
+
| SearchToolDocumentsDelta
|
|
221
|
+
| SectionEnd;
|
|
183
222
|
export type ImageGenerationToolObj =
|
|
184
223
|
| ImageGenerationToolStart
|
|
185
224
|
| ImageGenerationToolDelta
|
|
@@ -196,6 +235,7 @@ export type ReasoningObj =
|
|
|
196
235
|
| ReasoningStart
|
|
197
236
|
| ReasoningDelta
|
|
198
237
|
| ReasoningEnd
|
|
238
|
+
| ReasoningDone
|
|
199
239
|
| SectionEnd;
|
|
200
240
|
export type CitationObj =
|
|
201
241
|
| CitationStart
|
|
@@ -212,7 +252,12 @@ export type ObjTypes =
|
|
|
212
252
|
| SectionEndObj
|
|
213
253
|
| CitationObj
|
|
214
254
|
| ErrorObj
|
|
215
|
-
| MessageEndIdInfo
|
|
255
|
+
| MessageEndIdInfo
|
|
256
|
+
| SearchToolStartV3
|
|
257
|
+
| SearchToolQueriesDelta
|
|
258
|
+
| SearchToolDocumentsDelta
|
|
259
|
+
| CitationInfo
|
|
260
|
+
| ReasoningDone;
|
|
216
261
|
|
|
217
262
|
// Packet wrapper for streaming objects
|
|
218
263
|
export interface Packet {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseExcludeIndices,
|
|
5
|
+
callLLM,
|
|
6
|
+
excludeClaimSentences,
|
|
7
|
+
excludeContextSentences,
|
|
8
|
+
} from './filtering';
|
|
9
|
+
jest.mock('node-fetch');
|
|
2
10
|
|
|
3
11
|
describe('parseExcludeIndices', () => {
|
|
4
12
|
it('parses single indices', () => {
|
|
@@ -42,3 +50,193 @@ describe('parseExcludeIndices', () => {
|
|
|
42
50
|
expect(result).toEqual(new Set());
|
|
43
51
|
});
|
|
44
52
|
});
|
|
53
|
+
|
|
54
|
+
describe('callLLM', () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
fetch.mockReset();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
fetch.mockReset();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('makes POST request with correct headers', async () => {
|
|
64
|
+
const mockResponse = { choices: [{ message: { content: 'result' } }] };
|
|
65
|
+
fetch.mockResolvedValue({
|
|
66
|
+
json: () => Promise.resolve(mockResponse),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await callLLM('https://api.example.com', 'my-api-key', { model: 'gpt-4' });
|
|
70
|
+
|
|
71
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
72
|
+
'https://api.example.com',
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: expect.objectContaining({
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
Authorization: 'Bearer my-api-key',
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('omits Authorization header when no apiKey', async () => {
|
|
84
|
+
fetch.mockResolvedValue({
|
|
85
|
+
json: () => Promise.resolve({}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await callLLM('https://api.example.com', null, { model: 'gpt-4' });
|
|
89
|
+
|
|
90
|
+
const headers = fetch.mock.calls[0][1].headers;
|
|
91
|
+
expect(headers['Authorization']).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('adds X-Forwarded-For header when ip is provided', async () => {
|
|
95
|
+
fetch.mockResolvedValue({
|
|
96
|
+
json: () => Promise.resolve({}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await callLLM('https://api.example.com', null, {}, { ip: '1.2.3.4' });
|
|
100
|
+
|
|
101
|
+
const headers = fetch.mock.calls[0][1].headers;
|
|
102
|
+
expect(headers['X-Forwarded-For']).toBe('1.2.3.4');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns parsed JSON response', async () => {
|
|
106
|
+
const mockResponse = { choices: [{ message: { content: 'hello' } }] };
|
|
107
|
+
fetch.mockResolvedValue({
|
|
108
|
+
json: () => Promise.resolve(mockResponse),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const result = await callLLM('https://api.example.com', null, {});
|
|
112
|
+
expect(result).toEqual(mockResponse);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('excludeClaimSentences', () => {
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
fetch.mockReset();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
fetch.mockReset();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns empty set for empty sentences array', async () => {
|
|
126
|
+
const result = await excludeClaimSentences([]);
|
|
127
|
+
expect(result).toEqual(new Set());
|
|
128
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('calls LLM and returns excluded sentence indices', async () => {
|
|
132
|
+
fetch.mockResolvedValue({
|
|
133
|
+
json: () =>
|
|
134
|
+
Promise.resolve({
|
|
135
|
+
choices: [{ message: { content: '1,3' } }],
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const sentences = ['Sentence one.', 'Sentence two.', 'Sentence three.'];
|
|
140
|
+
const result = await excludeClaimSentences(sentences);
|
|
141
|
+
|
|
142
|
+
expect(result).toEqual(new Set([1, 3]));
|
|
143
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns empty set when LLM returns NONE', async () => {
|
|
147
|
+
fetch.mockResolvedValue({
|
|
148
|
+
json: () =>
|
|
149
|
+
Promise.resolve({
|
|
150
|
+
choices: [{ message: { content: 'NONE' } }],
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const sentences = ['Some factual sentence.', 'Another factual sentence.'];
|
|
155
|
+
const result = await excludeClaimSentences(sentences);
|
|
156
|
+
|
|
157
|
+
expect(result).toEqual(new Set());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns empty set on LLM error', async () => {
|
|
161
|
+
fetch.mockRejectedValue(new Error('Network error'));
|
|
162
|
+
|
|
163
|
+
const sentences = ['Sentence one.'];
|
|
164
|
+
const result = await excludeClaimSentences(sentences);
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual(new Set());
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('passes ip option to LLM call', async () => {
|
|
170
|
+
fetch.mockResolvedValue({
|
|
171
|
+
json: () =>
|
|
172
|
+
Promise.resolve({ choices: [{ message: { content: 'NONE' } }] }),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await excludeClaimSentences(['test sentence'], { ip: '10.0.0.1' });
|
|
176
|
+
|
|
177
|
+
const headers = fetch.mock.calls[0][1].headers;
|
|
178
|
+
expect(headers['X-Forwarded-For']).toBe('10.0.0.1');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('excludeContextSentences', () => {
|
|
183
|
+
const MIN_SENTENCES = 75;
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
fetch.mockReset();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
fetch.mockReset();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns empty set when context sentences <= minimum threshold', async () => {
|
|
194
|
+
const shortContext = Array(10).fill('Short sentence.');
|
|
195
|
+
const claims = ['A claim.'];
|
|
196
|
+
const result = await excludeContextSentences(shortContext, claims);
|
|
197
|
+
|
|
198
|
+
expect(result).toEqual(new Set());
|
|
199
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('calls LLM when context exceeds minimum threshold', async () => {
|
|
203
|
+
fetch.mockResolvedValue({
|
|
204
|
+
json: () =>
|
|
205
|
+
Promise.resolve({
|
|
206
|
+
choices: [{ message: { content: '1,2' } }],
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
|
|
211
|
+
const claims = ['A claim.'];
|
|
212
|
+
const result = await excludeContextSentences(longContext, claims);
|
|
213
|
+
|
|
214
|
+
expect(result).toEqual(new Set([1, 2]));
|
|
215
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns empty set on LLM error', async () => {
|
|
219
|
+
fetch.mockRejectedValue(new Error('timeout'));
|
|
220
|
+
|
|
221
|
+
const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
|
|
222
|
+
const claims = ['A claim.'];
|
|
223
|
+
const result = await excludeContextSentences(longContext, claims);
|
|
224
|
+
|
|
225
|
+
expect(result).toEqual(new Set());
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns empty set when LLM returns NONE for context', async () => {
|
|
229
|
+
fetch.mockResolvedValue({
|
|
230
|
+
json: () =>
|
|
231
|
+
Promise.resolve({
|
|
232
|
+
choices: [{ message: { content: 'NONE' } }],
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
|
|
237
|
+
const claims = ['A claim.'];
|
|
238
|
+
const result = await excludeContextSentences(longContext, claims);
|
|
239
|
+
|
|
240
|
+
expect(result).toEqual(new Set());
|
|
241
|
+
});
|
|
242
|
+
});
|
package/src/middleware.js
CHANGED
|
@@ -186,11 +186,21 @@ async function send_onyx_request(
|
|
|
186
186
|
options.body = JSON.stringify(req.body);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
console.log('[Middleware] Sending request to Onyx:', {
|
|
190
|
+
url,
|
|
191
|
+
method: req.method,
|
|
192
|
+
hasBody: !!req.body,
|
|
193
|
+
body: JSON.stringify(req.body, null, 2),
|
|
194
|
+
});
|
|
195
|
+
|
|
189
196
|
const mock_file = is_related_question
|
|
190
197
|
? process.env.MOCK_LLM_FILE_PATH_RQ
|
|
191
198
|
: process.env.MOCK_LLM_FILE_PATH;
|
|
192
199
|
|
|
193
|
-
if (
|
|
200
|
+
if (
|
|
201
|
+
mock_file &&
|
|
202
|
+
(req.url.endsWith('send-message') || req.url.endsWith('send-chat-message'))
|
|
203
|
+
) {
|
|
194
204
|
try {
|
|
195
205
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
196
206
|
mock_send_message(res, is_related_question);
|
|
@@ -210,6 +220,13 @@ async function send_onyx_request(
|
|
|
210
220
|
log(`Fetching ${url}`);
|
|
211
221
|
const response = await fetch(url, options, req.body);
|
|
212
222
|
|
|
223
|
+
console.log('[Middleware] Received response from Onyx:', {
|
|
224
|
+
url,
|
|
225
|
+
status: response.status,
|
|
226
|
+
statusText: response.statusText,
|
|
227
|
+
headers: response.headers.raw(),
|
|
228
|
+
});
|
|
229
|
+
|
|
213
230
|
if (process.env.DUMP_LLM_FILE_PATH && !is_related_question) {
|
|
214
231
|
const filePath = process.env.DUMP_LLM_FILE_PATH;
|
|
215
232
|
const writer = fs.createWriteStream(filePath);
|
package/src/middleware.test.js
CHANGED
|
@@ -18,6 +18,7 @@ jest.mock('node-fetch', () => {
|
|
|
18
18
|
status: 200,
|
|
19
19
|
headers: {
|
|
20
20
|
get: jest.fn().mockReturnValue('application/json'),
|
|
21
|
+
raw: jest.fn().mockReturnValue({}),
|
|
21
22
|
},
|
|
22
23
|
body: { pipe: mockPipe },
|
|
23
24
|
});
|
|
@@ -220,4 +221,17 @@ describe('src/middleware', () => {
|
|
|
220
221
|
|
|
221
222
|
expect(res.write).toHaveBeenCalled();
|
|
222
223
|
});
|
|
224
|
+
|
|
225
|
+
it('dumps LLM response when DUMP_LLM_FILE_PATH is set', async () => {
|
|
226
|
+
process.env.ONYX_API_KEY = 'test-key';
|
|
227
|
+
process.env.ONYX_URL = 'http://localhost:3000';
|
|
228
|
+
process.env.DUMP_LLM_FILE_PATH = '/tmp/dumped_response.jsonl';
|
|
229
|
+
|
|
230
|
+
await middleware(req, res, next);
|
|
231
|
+
|
|
232
|
+
const fs = require('fs');
|
|
233
|
+
expect(fs.createWriteStream).toHaveBeenCalledWith(
|
|
234
|
+
'/tmp/dumped_response.jsonl',
|
|
235
|
+
);
|
|
236
|
+
});
|
|
223
237
|
});
|