@eeacms/volto-eea-chatbot 2.0.2 → 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/CHANGELOG.md +19 -1
- package/artifacts/ONYX_V3_INTEGRATION.md +34 -0
- package/jest-addon.config.js +1 -1
- package/package.json +1 -1
- package/src/ChatBlock/chat/AIMessage.tsx +19 -3
- package/src/ChatBlock/chat/ChatWindow.tsx +3 -0
- package/src/ChatBlock/hooks/useChatController.ts +60 -10
- package/src/ChatBlock/hooks/useToolDisplayTiming.ts +1 -0
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +79 -48
- package/src/ChatBlock/packets/RendererComponent.tsx +7 -2
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +6 -1
- package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +1 -0
- package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +20 -11
- package/src/ChatBlock/schema.jsx +13 -0
- package/src/ChatBlock/services/messageProcessor.ts +66 -14
- package/src/ChatBlock/services/packetUtils.ts +11 -1
- package/src/ChatBlock/services/streamingService.ts +147 -66
- package/src/ChatBlock/types/streamingModels.ts +47 -2
- package/src/middleware.js +18 -1
- package/src/middleware.test.js +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,25 @@ 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
|
-
### [2.0.
|
|
7
|
+
### [2.0.3](https://github.com/eea/volto-eea-chatbot/compare/2.0.2...2.0.3) - 18 May 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat: integrate Onyx 3.x support [Zoltan Szabo - [`6b0d9d5`](https://github.com/eea/volto-eea-chatbot/commit/6b0d9d5a847995953680a5a9a46281bfc3997b70)]
|
|
12
|
+
|
|
13
|
+
#### :house: Internal changes
|
|
14
|
+
|
|
15
|
+
- style: Automated code fix [eea-jenkins - [`ba195ce`](https://github.com/eea/volto-eea-chatbot/commit/ba195cec917b06faa70211b05c7def1d025d5356)]
|
|
16
|
+
|
|
17
|
+
#### :hammer_and_wrench: Others
|
|
18
|
+
|
|
19
|
+
- fixes for eslint [Zoltan Szabo - [`ec926dd`](https://github.com/eea/volto-eea-chatbot/commit/ec926dd32106105fb87cff226b5f92a495a1eadf)]
|
|
20
|
+
- increase test coverage [Zoltan Szabo - [`d4d1178`](https://github.com/eea/volto-eea-chatbot/commit/d4d1178fd52d215a755480c710b300f71f674f3f)]
|
|
21
|
+
- Rather than dealing with complex and fragile Webpack/Babel config transpilation rules for ES Modules, the cleanest solution is to remove the dependency entirely [Zoltan Szabo - [`3cdedee`](https://github.com/eea/volto-eea-chatbot/commit/3cdedee16ecddf695779b179eb6beeecde16295d)]
|
|
22
|
+
- updated snapshots [Zoltan Szabo - [`58dd3b8`](https://github.com/eea/volto-eea-chatbot/commit/58dd3b8e7d795032338b3580e33cd10c24576911)]
|
|
23
|
+
- fixed jest-addon.config.js [Zoltan Szabo - [`95c4c6f`](https://github.com/eea/volto-eea-chatbot/commit/95c4c6f494c71a9d5e9171b7d56bb1609dccc9f3)]
|
|
24
|
+
- updated dependencies [Zoltan Szabo - [`44e174e`](https://github.com/eea/volto-eea-chatbot/commit/44e174e4c3634d11241f78b46d86deefe0ed6191)]
|
|
25
|
+
### [2.0.2](https://github.com/eea/volto-eea-chatbot/compare/2.0.1...2.0.2) - 9 April 2026
|
|
8
26
|
|
|
9
27
|
### [2.0.1](https://github.com/eea/volto-eea-chatbot/compare/2.0.0...2.0.1) - 2 April 2026
|
|
10
28
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Onyx v3 Integration for Volto EEA Chatbot
|
|
2
|
+
|
|
3
|
+
This document summarizes the major architectural changes and features implemented to support **Onyx 3.x** while maintaining full backward compatibility with **Onyx 2.x**.
|
|
4
|
+
|
|
5
|
+
## 1. Version Switching & Configuration
|
|
6
|
+
- **Onyx Version Toggle**: Added a configuration setting in the Chat Block schema to switch between Onyx 2 and 3.
|
|
7
|
+
- **Dynamic Routing**: The `streamingService` now branches logic based on the selected version, targeting the appropriate API endpoints:
|
|
8
|
+
- **v2**: `/send-message`
|
|
9
|
+
- **v3**: `/send-chat-message`
|
|
10
|
+
|
|
11
|
+
## 2. Packet-Based Architecture (v3)
|
|
12
|
+
Implemented support for the new Onyx 3.x packet schema, which utilizes turn indices (`ind`) for better synchronization:
|
|
13
|
+
- **New Packet Types**: Integrated support for `search_tool_start`, `search_tool_queries_delta`, `search_tool_documents_delta`, `citation_info`, and `reasoning_done`.
|
|
14
|
+
- **Packet Normalization**: Developed a normalization layer in `streamingService.ts` to map v3 packets into the chatbot's internal `Packet` structure.
|
|
15
|
+
- **Turn Index Mapping**: Updated `MessageProcessor` to use explicit `ind` values from the backend for reliable turn completion detection, replacing the synthetic index fallback used in v2.
|
|
16
|
+
|
|
17
|
+
## 3. Streaming & UI Visibility
|
|
18
|
+
- **Eager Rendering**: Updated `MultiToolRenderer` and `SearchToolRenderer` to display search queries and documents as they arrive via delta packets, rather than waiting for tool completion.
|
|
19
|
+
- **Improved Context**: Added distinct labels for "Web Queries" and "Internal Search Queries" in the Search Tool UI.
|
|
20
|
+
- **Reasoning Support**: Enhanced `ReasoningRenderer` to handle v3 reasoning packets and provide a smoother transition between "thinking" and "answering" phases.
|
|
21
|
+
|
|
22
|
+
## 4. Related Questions (RQ) Restoration
|
|
23
|
+
- **Middleware Routing**: Updated the RQ workflow to route through the specialized `/_rq/` middleware proxy for both session creation and message sending.
|
|
24
|
+
- **Dedicated Sessions**: Ensured that Related Questions use a fresh chat session correctly targeting the QGen assistant on the appropriate version backend.
|
|
25
|
+
- **Completion Resiliency**: Patched a race condition where missing `section_end` packets for the main message turn would block RQ interrogation. The system now automatically synthesizes turn completion when the stream ends.
|
|
26
|
+
|
|
27
|
+
## 5. Middleware & Proxying
|
|
28
|
+
- **Proxy Expansion**: Synchronized `middleware.js` to handle both standard (`/_da/`) and related questions (`/_rq/`) paths for all v3 endpoints.
|
|
29
|
+
- **Payload Alignment**: Updated the payload builders to support v3-specific fields like `alternate_assistant_id`, `file_descriptors`, and `internal_search_filters`.
|
|
30
|
+
|
|
31
|
+
## 6. Bug Fixes & Stability
|
|
32
|
+
- **Citation Fallback**: Added a fallback for citations in v3 if explicit `citation_info` packets are missing, leveraging the `final_documents` array in `message_start`.
|
|
33
|
+
- **Typewriter Synchronization**: Adjusted `MessageTextRenderer` to coordinate with the new turn-based completion logic, ensuring the UI remains interactive.
|
|
34
|
+
- **Logging & Observability**: Injected comprehensive tracing logs (`[RQ]`, `[sendMessage]`, `[MessageProcessor]`) to monitor the end-to-end flow of packets.
|
package/jest-addon.config.js
CHANGED
|
@@ -377,7 +377,7 @@ const getCoveragePatterns = () => {
|
|
|
377
377
|
arg === reserved || arg.startsWith(reserved.split('=')[0] + '='),
|
|
378
378
|
) &&
|
|
379
379
|
process.argv.indexOf(arg) >
|
|
380
|
-
|
|
380
|
+
process.argv.findIndex((item) => item === 'test'),
|
|
381
381
|
);
|
|
382
382
|
|
|
383
383
|
if (directoryArg) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatMessageProps } from '@eeacms/volto-eea-chatbot/ChatBlock/types/interfaces';
|
|
2
2
|
import { useState, useMemo, useEffect } from 'react';
|
|
3
3
|
import cx from 'classnames';
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import loadable from '@loadable/component';
|
|
6
6
|
import {
|
|
7
7
|
Tab,
|
|
@@ -41,6 +41,17 @@ const HalloumiFeedback: any = loadable(
|
|
|
41
41
|
import('@eeacms/volto-eea-chatbot/ChatBlock/components/HalloumiFeedback'),
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
+
function visit(node: any, type: string, visitor: (node: any, idx?: number, parent?: any) => void, idx?: number, parent?: any) {
|
|
45
|
+
if (node.type === type) {
|
|
46
|
+
visitor(node, idx, parent);
|
|
47
|
+
}
|
|
48
|
+
if (node.children && Array.isArray(node.children)) {
|
|
49
|
+
node.children.forEach((child: any, cidx: number) => {
|
|
50
|
+
visit(child, type, visitor, cidx, node);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
function addQualityMarkersPlugin() {
|
|
45
56
|
return function (tree: any) {
|
|
46
57
|
visit(tree, 'element', function (node: any) {
|
|
@@ -320,8 +331,11 @@ export function AIMessage({
|
|
|
320
331
|
if (isFetchingRelatedQuestions || typeof relatedQuestions !== 'undefined') {
|
|
321
332
|
return;
|
|
322
333
|
}
|
|
323
|
-
if (
|
|
324
|
-
|
|
334
|
+
if (isLastMessage && isComplete && onFetchRelatedQuestions) {
|
|
335
|
+
console.log(`[AIMessage] Triggering RQ: messageDisplayed=${messageDisplayed}, isComplete=${isComplete}, hasContent=${!!message.message}`);
|
|
336
|
+
if (messageDisplayed) {
|
|
337
|
+
onFetchRelatedQuestions();
|
|
338
|
+
}
|
|
325
339
|
}
|
|
326
340
|
}, [
|
|
327
341
|
messageDisplayed,
|
|
@@ -329,6 +343,8 @@ export function AIMessage({
|
|
|
329
343
|
isComplete,
|
|
330
344
|
onFetchRelatedQuestions,
|
|
331
345
|
isFetchingRelatedQuestions,
|
|
346
|
+
isLastMessage,
|
|
347
|
+
message.message,
|
|
332
348
|
]);
|
|
333
349
|
|
|
334
350
|
useEffect(() => {
|
|
@@ -48,6 +48,7 @@ interface ChatWindowProps {
|
|
|
48
48
|
enableMatomoTracking?: boolean;
|
|
49
49
|
onDemandInputToggle?: boolean;
|
|
50
50
|
maxContextSegments?: number;
|
|
51
|
+
onyxVersion?: '2' | '3';
|
|
51
52
|
isPlaywrightTest?: boolean;
|
|
52
53
|
[key: string]: any;
|
|
53
54
|
}
|
|
@@ -83,6 +84,7 @@ function ChatWindow({
|
|
|
83
84
|
enableMatomoTracking = true,
|
|
84
85
|
onDemandInputToggle = true,
|
|
85
86
|
maxContextSegments = 0,
|
|
87
|
+
onyxVersion = '2',
|
|
86
88
|
} = data;
|
|
87
89
|
const [qualityCheckEnabled, setQualityCheckEnabled] = useState(
|
|
88
90
|
onDemandInputToggle ?? true,
|
|
@@ -117,6 +119,7 @@ function ChatWindow({
|
|
|
117
119
|
qgenAsistantId,
|
|
118
120
|
enableQgen,
|
|
119
121
|
deepResearch,
|
|
122
|
+
onyxVersion,
|
|
120
123
|
});
|
|
121
124
|
|
|
122
125
|
const [showLandingPage, setShowLandingPage] = useState(true);
|
|
@@ -13,6 +13,7 @@ interface UseChatControllerProps {
|
|
|
13
13
|
enableQgen?: boolean;
|
|
14
14
|
qgenAsistantId?: number;
|
|
15
15
|
deepResearch?: string;
|
|
16
|
+
onyxVersion?: '2' | '3';
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
interface RelatedQuestion {
|
|
@@ -21,14 +22,42 @@ interface RelatedQuestion {
|
|
|
21
22
|
|
|
22
23
|
// Extract JSON array from related questions response
|
|
23
24
|
function extractRelatedQuestions(str: string): RelatedQuestion[] {
|
|
24
|
-
if (str.toLowerCase().includes('no_response')) {
|
|
25
|
-
|
|
25
|
+
if (!str || str.toLowerCase().includes('no_response')) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Try to parse as JSON first if it looks like JSON
|
|
30
|
+
const trimmed = str.trim();
|
|
31
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(trimmed);
|
|
34
|
+
const items = Array.isArray(parsed) ? parsed : parsed.questions || [];
|
|
35
|
+
if (Array.isArray(items)) {
|
|
36
|
+
return items
|
|
37
|
+
.map((item) => {
|
|
38
|
+
if (typeof item === 'string') return { question: item };
|
|
39
|
+
if (item && typeof item === 'object' && item.question)
|
|
40
|
+
return { question: item.question };
|
|
41
|
+
return null;
|
|
42
|
+
})
|
|
43
|
+
.filter((i): i is RelatedQuestion => i !== null);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Fallback to line parsing
|
|
47
|
+
}
|
|
26
48
|
}
|
|
27
49
|
|
|
50
|
+
// Fallback: split by lines and clean up common list formats
|
|
28
51
|
return str
|
|
29
52
|
.split('\n')
|
|
30
|
-
.
|
|
31
|
-
.
|
|
53
|
+
.map((line) => line.trim())
|
|
54
|
+
.filter((line) => line.length > 0)
|
|
55
|
+
.map((line) => {
|
|
56
|
+
// Remove leading numbers or bullets like "1. ", "- ", "* ", etc.
|
|
57
|
+
const cleaned = line.replace(/^[\d\.\-\*\s]+/, '').trim();
|
|
58
|
+
return cleaned ? { question: cleaned } : null;
|
|
59
|
+
})
|
|
60
|
+
.filter((i): i is RelatedQuestion => i !== null);
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
// Fetch related questions using the qgen assistant
|
|
@@ -36,12 +65,16 @@ async function fetchRelatedQuestions(
|
|
|
36
65
|
query: string,
|
|
37
66
|
answer: string,
|
|
38
67
|
qgenAsistantId: number,
|
|
68
|
+
onyxVersion: '2' | '3' = '2',
|
|
39
69
|
): Promise<RelatedQuestion[]> {
|
|
40
70
|
try {
|
|
71
|
+
console.log(`[RQ] Creating session for assistant ${qgenAsistantId} (Onyx v${onyxVersion})`);
|
|
41
72
|
const chatSessionId = await createChatSession(
|
|
42
73
|
qgenAsistantId,
|
|
43
74
|
`Q: ${query}`,
|
|
75
|
+
true,
|
|
44
76
|
);
|
|
77
|
+
console.log(`[RQ] Session created: ${chatSessionId}`);
|
|
45
78
|
|
|
46
79
|
const params = {
|
|
47
80
|
message: `Question: ${query}\nAnswer:\n${answer}`,
|
|
@@ -49,23 +82,35 @@ async function fetchRelatedQuestions(
|
|
|
49
82
|
fileDescriptors: [],
|
|
50
83
|
parentMessageId: null,
|
|
51
84
|
chatSessionId,
|
|
52
|
-
promptId: 0,
|
|
53
85
|
filters: null,
|
|
54
86
|
selectedDocumentIds: [],
|
|
55
87
|
use_agentic_search: false,
|
|
56
88
|
regenerate: false,
|
|
89
|
+
onyxVersion,
|
|
57
90
|
};
|
|
58
91
|
|
|
92
|
+
if (onyxVersion === '3') {
|
|
93
|
+
console.log('[Onyx v3] Sending RQ prompt:', params.message);
|
|
94
|
+
}
|
|
95
|
+
|
|
59
96
|
let result = '';
|
|
60
97
|
for await (const packets of sendMessage(params, true)) {
|
|
61
98
|
for (const packet of packets) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
99
|
+
if (onyxVersion === '3') {
|
|
100
|
+
// console.log('[Onyx v3] RQ Packet:', packet);
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
packet.obj.type === PacketType.MESSAGE_DELTA ||
|
|
104
|
+
packet.obj.type === PacketType.MESSAGE_START
|
|
105
|
+
) {
|
|
106
|
+
result += (packet.obj as any).content || '';
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
}
|
|
67
|
-
|
|
68
|
-
|
|
110
|
+
console.log(`[RQ] Final response string: "${result}"`);
|
|
111
|
+
const extracted = extractRelatedQuestions(result);
|
|
112
|
+
console.log(`[RQ] Extracted ${extracted.length} questions`);
|
|
113
|
+
return extracted;
|
|
69
114
|
} catch (error) {
|
|
70
115
|
console.error('Error fetching related questions:', error);
|
|
71
116
|
return [];
|
|
@@ -77,6 +122,7 @@ export function useChatController({
|
|
|
77
122
|
enableQgen = false,
|
|
78
123
|
qgenAsistantId,
|
|
79
124
|
deepResearch,
|
|
125
|
+
onyxVersion = '2',
|
|
80
126
|
}: UseChatControllerProps) {
|
|
81
127
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
82
128
|
const [chatSessionId, setChatSessionId] = useState<string | null>(null);
|
|
@@ -229,6 +275,7 @@ export function useChatController({
|
|
|
229
275
|
regenerate: false,
|
|
230
276
|
filters: null,
|
|
231
277
|
selectedDocumentIds: [],
|
|
278
|
+
onyxVersion,
|
|
232
279
|
},
|
|
233
280
|
assistantNodeId,
|
|
234
281
|
userNodeId,
|
|
@@ -250,6 +297,7 @@ export function useChatController({
|
|
|
250
297
|
);
|
|
251
298
|
|
|
252
299
|
const onFetchRelatedQuestions = useCallback(async () => {
|
|
300
|
+
console.log('[RQ] onFetchRelatedQuestions triggered');
|
|
253
301
|
const latestAssistantMessage = messages
|
|
254
302
|
.filter((m) => m.type === 'assistant')
|
|
255
303
|
.pop();
|
|
@@ -259,6 +307,7 @@ export function useChatController({
|
|
|
259
307
|
qgenAsistantId &&
|
|
260
308
|
latestAssistantMessage?.type === 'assistant'
|
|
261
309
|
) {
|
|
310
|
+
console.log(`[RQ] Criteria met: assistantNodeId=${latestAssistantMessage.nodeId}, qgenAssistant=${qgenAsistantId}`);
|
|
262
311
|
if (isDeepResearchEnabled) {
|
|
263
312
|
setMessages((prev) => {
|
|
264
313
|
return prev.map((m) =>
|
|
@@ -284,6 +333,7 @@ export function useChatController({
|
|
|
284
333
|
userMessage.message,
|
|
285
334
|
latestAssistantMessage.message,
|
|
286
335
|
qgenAsistantId,
|
|
336
|
+
onyxVersion,
|
|
287
337
|
);
|
|
288
338
|
}
|
|
289
339
|
} catch (error) {
|
|
@@ -299,7 +349,7 @@ export function useChatController({
|
|
|
299
349
|
setIsFetchingRelatedQuestions(false);
|
|
300
350
|
}
|
|
301
351
|
}
|
|
302
|
-
}, [messages, enableQgen, qgenAsistantId, isDeepResearchEnabled]);
|
|
352
|
+
}, [messages, enableQgen, qgenAsistantId, isDeepResearchEnabled, onyxVersion]);
|
|
303
353
|
|
|
304
354
|
const clearChat = useCallback(() => {
|
|
305
355
|
setMessages([]);
|
|
@@ -19,7 +19,7 @@ interface MultiToolRendererProps {
|
|
|
19
19
|
|
|
20
20
|
export function MultiToolRenderer({
|
|
21
21
|
toolGroups,
|
|
22
|
-
showTools = [PacketType.SEARCH_TOOL_START],
|
|
22
|
+
showTools = [PacketType.SEARCH_TOOL_START, PacketType.REASONING_START],
|
|
23
23
|
message,
|
|
24
24
|
libs,
|
|
25
25
|
onAllToolsDisplayed,
|
|
@@ -28,22 +28,33 @@ export function MultiToolRenderer({
|
|
|
28
28
|
const { isFinalMessageComing = false, isComplete = false } = message;
|
|
29
29
|
|
|
30
30
|
// Filter tool groups based on allowed tool types
|
|
31
|
-
const filteredToolGroups = useMemo(
|
|
32
|
-
()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
const filteredToolGroups = useMemo(() => {
|
|
32
|
+
const expandedShowTools = [...(showTools || [])];
|
|
33
|
+
if (showTools?.includes(PacketType.SEARCH_TOOL_START)) {
|
|
34
|
+
expandedShowTools.push(
|
|
35
|
+
PacketType.SEARCH_TOOL_START_V3,
|
|
36
|
+
PacketType.SEARCH_TOOL_QUERIES_DELTA,
|
|
37
|
+
PacketType.SEARCH_TOOL_DOCUMENTS_DELTA,
|
|
38
|
+
PacketType.SEARCH_TOOL_DELTA,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (showTools?.includes(PacketType.REASONING_START)) {
|
|
42
|
+
expandedShowTools.push(
|
|
43
|
+
PacketType.REASONING_DELTA,
|
|
44
|
+
PacketType.REASONING_DONE,
|
|
45
|
+
PacketType.REASONING_END as any,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return toolGroups.filter((group) =>
|
|
49
|
+
group.packets?.some((packet) =>
|
|
50
|
+
expandedShowTools.includes(packet.obj.type as PacketType),
|
|
37
51
|
),
|
|
38
|
-
|
|
39
|
-
);
|
|
52
|
+
);
|
|
53
|
+
}, [toolGroups, showTools]);
|
|
40
54
|
|
|
41
55
|
// Manage tool display timing
|
|
42
|
-
const { allToolsDisplayed, handleToolComplete } =
|
|
43
|
-
filteredToolGroups,
|
|
44
|
-
isFinalMessageComing,
|
|
45
|
-
isComplete,
|
|
46
|
-
);
|
|
56
|
+
const { allToolsDisplayed, handleToolComplete, toolStates } =
|
|
57
|
+
useToolDisplayTiming(filteredToolGroups, isFinalMessageComing, isComplete);
|
|
47
58
|
|
|
48
59
|
// Notify parent when all tools are displayed
|
|
49
60
|
useEffect(() => {
|
|
@@ -64,27 +75,29 @@ export function MultiToolRenderer({
|
|
|
64
75
|
|
|
65
76
|
if (filteredToolGroups.length === 0) return null;
|
|
66
77
|
|
|
67
|
-
const
|
|
78
|
+
const isOverallStreaming = !allToolsDisplayed;
|
|
68
79
|
|
|
69
80
|
const count = filteredToolGroups.length;
|
|
70
81
|
|
|
71
|
-
const ariaLabel = `${count} ${
|
|
72
|
-
|
|
73
|
-
}, ${isExpanded ? 'expanded' : 'collapsed'}`;
|
|
82
|
+
const ariaLabel = `${count} ${
|
|
83
|
+
isOverallStreaming ? 'processing' : 'completed'
|
|
84
|
+
} ${count === 1 ? 'step' : 'steps'}, ${isExpanded ? 'expanded' : 'collapsed'}`;
|
|
74
85
|
|
|
75
86
|
return (
|
|
76
87
|
<div
|
|
77
88
|
className={cx('multi-tool-renderer', {
|
|
78
|
-
streaming:
|
|
79
|
-
complete: !
|
|
89
|
+
streaming: isOverallStreaming,
|
|
90
|
+
complete: !isOverallStreaming,
|
|
80
91
|
})}
|
|
81
92
|
>
|
|
82
93
|
{/* Header */}
|
|
83
|
-
<div
|
|
94
|
+
<div
|
|
95
|
+
className={cx({ 'tools-container collapsed-view': isOverallStreaming })}
|
|
96
|
+
>
|
|
84
97
|
<div
|
|
85
98
|
className={cx({
|
|
86
|
-
'tools-collapsed-header':
|
|
87
|
-
'tools-summary-header': !
|
|
99
|
+
'tools-collapsed-header': isOverallStreaming,
|
|
100
|
+
'tools-summary-header': !isOverallStreaming,
|
|
88
101
|
})}
|
|
89
102
|
onClick={toggleExpanded}
|
|
90
103
|
role="button"
|
|
@@ -109,21 +122,25 @@ export function MultiToolRenderer({
|
|
|
109
122
|
{/* Tools List */}
|
|
110
123
|
<div
|
|
111
124
|
className={cx({
|
|
112
|
-
'tools-collapsed-list':
|
|
113
|
-
'tools-expanded-content': !
|
|
114
|
-
expanded: isExpanded &&
|
|
115
|
-
visible: isExpanded && !
|
|
125
|
+
'tools-collapsed-list': isOverallStreaming,
|
|
126
|
+
'tools-expanded-content': !isOverallStreaming,
|
|
127
|
+
expanded: isExpanded && isOverallStreaming,
|
|
128
|
+
visible: isExpanded && !isOverallStreaming,
|
|
116
129
|
})}
|
|
117
130
|
>
|
|
118
|
-
<div className={cx({ 'tools-list':
|
|
131
|
+
<div className={cx({ 'tools-list': isOverallStreaming })}>
|
|
119
132
|
<div>
|
|
120
133
|
{filteredToolGroups.map((toolGroup, index) => {
|
|
121
134
|
const isLastItem = index === filteredToolGroups.length - 1;
|
|
135
|
+
const toolState = toolStates.get(toolGroup.ind);
|
|
136
|
+
const isToolCompleted = toolState?.isCompleted;
|
|
122
137
|
|
|
123
138
|
return (
|
|
124
139
|
<div
|
|
125
140
|
key={toolGroup.ind}
|
|
126
|
-
className={cx({
|
|
141
|
+
className={cx({
|
|
142
|
+
'tool-collapsed-wrapper': isOverallStreaming,
|
|
143
|
+
})}
|
|
127
144
|
>
|
|
128
145
|
<RendererComponent
|
|
129
146
|
packets={toolGroup.packets}
|
|
@@ -143,32 +160,46 @@ export function MultiToolRenderer({
|
|
|
143
160
|
) : (
|
|
144
161
|
<span
|
|
145
162
|
className={cx({
|
|
146
|
-
'tool-icon-dot':
|
|
147
|
-
'tool-icon-default': !
|
|
163
|
+
'tool-icon-dot': isOverallStreaming,
|
|
164
|
+
'tool-icon-default': !isOverallStreaming,
|
|
148
165
|
})}
|
|
149
166
|
/>
|
|
150
167
|
);
|
|
151
168
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<span className="tool-collapsed-status">
|
|
165
|
-
{status}
|
|
166
|
-
</span>
|
|
167
|
-
</div>
|
|
169
|
+
// If tool is not completed and we are overall streaming, show collapsed view
|
|
170
|
+
// EXCEPT for reasoning and search which we want to see while they stream/progress
|
|
171
|
+
if (isOverallStreaming && !isToolCompleted) {
|
|
172
|
+
const isDetailedTool = toolGroup.packets.some(
|
|
173
|
+
(p) =>
|
|
174
|
+
p.obj.type === PacketType.REASONING_START ||
|
|
175
|
+
p.obj.type === PacketType.REASONING_DELTA ||
|
|
176
|
+
p.obj.type === PacketType.SEARCH_TOOL_START ||
|
|
177
|
+
p.obj.type === PacketType.SEARCH_TOOL_START_V3 ||
|
|
178
|
+
p.obj.type === PacketType.SEARCH_TOOL_DELTA ||
|
|
179
|
+
p.obj.type === PacketType.SEARCH_TOOL_QUERIES_DELTA ||
|
|
180
|
+
p.obj.type === PacketType.SEARCH_TOOL_DOCUMENTS_DELTA,
|
|
168
181
|
);
|
|
182
|
+
|
|
183
|
+
if (!isDetailedTool || !content) {
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
className={cx('tool-item-collapsed', {
|
|
187
|
+
active: isLastItem,
|
|
188
|
+
completed: isToolCompleted,
|
|
189
|
+
})}
|
|
190
|
+
>
|
|
191
|
+
<div className="tool-collapsed-icon">
|
|
192
|
+
{finalIcon}
|
|
193
|
+
</div>
|
|
194
|
+
<span className="tool-collapsed-status">
|
|
195
|
+
{status}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
169
200
|
}
|
|
170
201
|
|
|
171
|
-
//
|
|
202
|
+
// Expanded view (full content) - used for completed tools or when overall complete
|
|
172
203
|
return (
|
|
173
204
|
<div className="tool-item-expanded">
|
|
174
205
|
<div className="tool-connector-line" />
|
|
@@ -26,7 +26,10 @@ function isChatPacket(packet: Packet): boolean {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function isSearchToolPacket(packet: Packet): boolean {
|
|
29
|
-
return
|
|
29
|
+
return (
|
|
30
|
+
packet.obj.type === PacketType.SEARCH_TOOL_START ||
|
|
31
|
+
packet.obj.type === PacketType.SEARCH_TOOL_START_V3
|
|
32
|
+
);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
function isImageToolPacket(packet: Packet): boolean {
|
|
@@ -44,7 +47,9 @@ function isFetchToolPacket(packet: Packet): boolean {
|
|
|
44
47
|
function isReasoningPacket(packet: Packet): boolean {
|
|
45
48
|
return (
|
|
46
49
|
packet.obj.type === PacketType.REASONING_START ||
|
|
47
|
-
packet.obj.type === PacketType.REASONING_DELTA
|
|
50
|
+
packet.obj.type === PacketType.REASONING_DELTA ||
|
|
51
|
+
packet.obj.type === PacketType.REASONING_DONE ||
|
|
52
|
+
packet.obj.type === PacketType.REASONING_END
|
|
48
53
|
);
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -90,7 +90,11 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
90
90
|
// If we're far behind, catch up faster
|
|
91
91
|
const increment =
|
|
92
92
|
remaining > CATCH_UP_THRESHOLD ? PACKETS_PER_TICK : 1;
|
|
93
|
-
|
|
93
|
+
const next = Math.min(prev + increment, packets.length);
|
|
94
|
+
if (isStreamFinished && next === packets.length) {
|
|
95
|
+
console.log(`[MessageTextRenderer] Animation finished: ${next}/${packets.length}`);
|
|
96
|
+
}
|
|
97
|
+
return next;
|
|
94
98
|
});
|
|
95
99
|
}, PACKET_DELAY_MS);
|
|
96
100
|
|
|
@@ -121,6 +125,7 @@ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
|
|
|
121
125
|
) {
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
128
|
+
console.log(`[MessageTextRenderer] Calling onComplete: packets=${packets.length}, finished=${isStreamFinished}`);
|
|
124
129
|
onComplete();
|
|
125
130
|
}
|
|
126
131
|
}, [
|
|
@@ -21,6 +21,7 @@ function constructCurrentReasoningState(packets: ReasoningPacket[]) {
|
|
|
21
21
|
const hasEnd = packets.some(
|
|
22
22
|
(p) =>
|
|
23
23
|
p.obj.type === PacketType.SECTION_END ||
|
|
24
|
+
p.obj.type === PacketType.REASONING_DONE ||
|
|
24
25
|
// Support either convention for reasoning completion
|
|
25
26
|
(p.obj as any).type === PacketType.REASONING_END,
|
|
26
27
|
);
|
|
@@ -55,26 +55,33 @@ const constructCurrentSearchState = (
|
|
|
55
55
|
isInternetSearch: boolean;
|
|
56
56
|
} => {
|
|
57
57
|
const searchStart = packets.find(
|
|
58
|
-
(packet) =>
|
|
58
|
+
(packet) =>
|
|
59
|
+
packet.obj.type === PacketType.SEARCH_TOOL_START ||
|
|
60
|
+
packet.obj.type === PacketType.SEARCH_TOOL_START_V3,
|
|
59
61
|
)?.obj as SearchToolStart | null;
|
|
60
62
|
|
|
61
63
|
const searchDeltas = packets
|
|
62
|
-
.filter(
|
|
63
|
-
|
|
64
|
+
.filter(
|
|
65
|
+
(packet) =>
|
|
66
|
+
packet.obj.type === PacketType.SEARCH_TOOL_DELTA ||
|
|
67
|
+
packet.obj.type === PacketType.SEARCH_TOOL_QUERIES_DELTA ||
|
|
68
|
+
packet.obj.type === PacketType.SEARCH_TOOL_DOCUMENTS_DELTA,
|
|
69
|
+
)
|
|
70
|
+
.map((packet) => packet.obj);
|
|
64
71
|
|
|
65
72
|
const searchEnd = packets.find(
|
|
66
73
|
(packet) => packet.obj.type === PacketType.SECTION_END,
|
|
67
74
|
)?.obj as SectionEnd | null;
|
|
68
75
|
|
|
69
|
-
// Extract queries from
|
|
76
|
+
// Extract queries from various delta packets
|
|
70
77
|
const queries = searchDeltas
|
|
71
|
-
.flatMap((delta) => delta?.queries || [])
|
|
78
|
+
.flatMap((delta: any) => delta?.queries || [])
|
|
72
79
|
.filter((query, index, arr) => arr.indexOf(query) === index); // Remove duplicates
|
|
73
80
|
|
|
74
81
|
const seenDocIds = new Set<string>();
|
|
75
82
|
const results = searchDeltas
|
|
76
|
-
.flatMap((delta) => delta?.documents || [])
|
|
77
|
-
.filter((doc) => {
|
|
83
|
+
.flatMap((delta: any) => delta?.documents || [])
|
|
84
|
+
.filter((doc: OnyxDocument) => {
|
|
78
85
|
if (!doc || !doc.document_id) return false;
|
|
79
86
|
if (seenDocIds.has(doc.document_id)) return false;
|
|
80
87
|
seenDocIds.add(doc.document_id);
|
|
@@ -202,8 +209,8 @@ export const SearchToolRenderer: MessageRenderer<SearchToolPacket> = ({
|
|
|
202
209
|
<SVGIcon name={isInternetSearch ? GlobeIcon : SearchIcon} size={size} />
|
|
203
210
|
);
|
|
204
211
|
|
|
205
|
-
// Don't render anything if search hasn't started
|
|
206
|
-
if (queries.length === 0) {
|
|
212
|
+
// Don't render anything if search hasn't started or has no data yet
|
|
213
|
+
if (queries.length === 0 && results.length === 0) {
|
|
207
214
|
return children({
|
|
208
215
|
icon: IconComponent,
|
|
209
216
|
status: status,
|
|
@@ -218,7 +225,9 @@ export const SearchToolRenderer: MessageRenderer<SearchToolPacket> = ({
|
|
|
218
225
|
<div className="search-tool-renderer">
|
|
219
226
|
<div className="queries-section">
|
|
220
227
|
<div className="queries-header">
|
|
221
|
-
<strong>
|
|
228
|
+
<strong>
|
|
229
|
+
{isInternetSearch ? 'Web Queries' : 'Internal Search Queries'}
|
|
230
|
+
</strong>
|
|
222
231
|
</div>
|
|
223
232
|
<div className="queries-list">
|
|
224
233
|
{queries.slice(0, queriesToShow).map((query, index) => (
|
|
@@ -262,7 +271,7 @@ export const SearchToolRenderer: MessageRenderer<SearchToolPacket> = ({
|
|
|
262
271
|
|
|
263
272
|
<div className="results-section">
|
|
264
273
|
<div className="results-header">
|
|
265
|
-
<strong>{isInternetSearch ? 'Results' : 'Documents'}</strong>
|
|
274
|
+
<strong>{isInternetSearch ? 'Web Results' : 'Documents'}</strong>
|
|
266
275
|
</div>
|
|
267
276
|
|
|
268
277
|
<div className="results-list">
|
package/src/ChatBlock/schema.jsx
CHANGED
|
@@ -104,6 +104,7 @@ export function ChatBlockSchema({ assistants, data }) {
|
|
|
104
104
|
'scrollToInput',
|
|
105
105
|
'showAssistantTitle',
|
|
106
106
|
'showAssistantDescription',
|
|
107
|
+
'onyxVersion',
|
|
107
108
|
],
|
|
108
109
|
},
|
|
109
110
|
],
|
|
@@ -368,6 +369,18 @@ range is from 0 to 100`,
|
|
|
368
369
|
type: 'boolean',
|
|
369
370
|
default: true,
|
|
370
371
|
},
|
|
372
|
+
onyxVersion: {
|
|
373
|
+
title: 'Onyx API version',
|
|
374
|
+
choices: [
|
|
375
|
+
['2', 'Onyx 2.x'],
|
|
376
|
+
['3', 'Onyx 3.x'],
|
|
377
|
+
],
|
|
378
|
+
default: '2',
|
|
379
|
+
description:
|
|
380
|
+
'Select which Onyx API version the backend is running. ' +
|
|
381
|
+
'Onyx 2.x uses the legacy send-message payload; ' +
|
|
382
|
+
'Onyx 3.x uses the new send-message payload with placement-based streaming.',
|
|
383
|
+
},
|
|
371
384
|
showAssistantPrompts: {
|
|
372
385
|
title: 'Show predefined prompts',
|
|
373
386
|
type: 'boolean',
|
|
@@ -111,17 +111,26 @@ export class MessageProcessor {
|
|
|
111
111
|
this.indicesStarted.push(packet.ind);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.indicesStarted.shift()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
// Handle SECTION_END (v2/v3 compatible)
|
|
115
|
+
if (packet.obj.type === PacketType.SECTION_END) {
|
|
116
|
+
let targetInd = packet.ind;
|
|
117
|
+
|
|
118
|
+
// If ind is -1 (common in v2), use synthetic logic
|
|
119
|
+
if (targetInd === -1 && this.indicesStarted.length > 0) {
|
|
120
|
+
targetInd = this.indicesStarted.shift()!;
|
|
121
|
+
} else if (targetInd !== -1) {
|
|
122
|
+
// If ind is provided (v3), remove it from indicesStarted if present
|
|
123
|
+
const startIdx = this.indicesStarted.indexOf(targetInd);
|
|
124
|
+
if (startIdx > -1) {
|
|
125
|
+
this.indicesStarted.splice(startIdx, 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (targetInd === -1) {
|
|
130
|
+
return; // Skip if we can't map it to a turn
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
processedPacket = getSynteticPacket(targetInd, PacketType.SECTION_END);
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
const { ind } = processedPacket;
|
|
@@ -129,6 +138,10 @@ export class MessageProcessor {
|
|
|
129
138
|
// Store processed packet for later aggregation
|
|
130
139
|
this.packets.push(processedPacket);
|
|
131
140
|
|
|
141
|
+
if (processedPacket.obj.type === PacketType.MESSAGE_START || processedPacket.obj.type === PacketType.SECTION_END) {
|
|
142
|
+
console.log(`[MessageProcessor] Processed ${processedPacket.obj.type} for ind=${processedPacket.ind}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
// Group packets by index for later processing
|
|
133
146
|
if (!this.groupedPackets.has(ind)) {
|
|
134
147
|
this.groupedPackets.set(ind, []);
|
|
@@ -174,6 +187,7 @@ export class MessageProcessor {
|
|
|
174
187
|
PacketType.MESSAGE_START,
|
|
175
188
|
PacketType.SEARCH_TOOL_DELTA,
|
|
176
189
|
PacketType.FETCH_TOOL_START,
|
|
190
|
+
PacketType.SEARCH_TOOL_DOCUMENTS_DELTA,
|
|
177
191
|
].includes(packet.obj.type)
|
|
178
192
|
) {
|
|
179
193
|
return;
|
|
@@ -181,7 +195,8 @@ export class MessageProcessor {
|
|
|
181
195
|
let newDocuments = false;
|
|
182
196
|
const data = packet.obj as any;
|
|
183
197
|
const documents = data.final_documents || data.documents;
|
|
184
|
-
|
|
198
|
+
|
|
199
|
+
if (documents && Array.isArray(documents)) {
|
|
185
200
|
documents.forEach((doc: OnyxDocument) => {
|
|
186
201
|
const docId = doc.document_id;
|
|
187
202
|
if (docId && !this.documentMap.has(docId)) {
|
|
@@ -190,8 +205,23 @@ export class MessageProcessor {
|
|
|
190
205
|
}
|
|
191
206
|
});
|
|
192
207
|
}
|
|
208
|
+
|
|
193
209
|
if (newDocuments) {
|
|
194
210
|
this._documents = Array.from(this.documentMap.values());
|
|
211
|
+
|
|
212
|
+
// If we have final_documents and no citations yet, create a fallback mapping
|
|
213
|
+
// This ensures the Sources tab shows up in v3 when citation_info is missing
|
|
214
|
+
if (
|
|
215
|
+
packet.obj.type === PacketType.MESSAGE_START &&
|
|
216
|
+
data.final_documents &&
|
|
217
|
+
this._citations.size === 0
|
|
218
|
+
) {
|
|
219
|
+
data.final_documents.forEach((doc: OnyxDocument, index: number) => {
|
|
220
|
+
if (doc.document_id) {
|
|
221
|
+
this._citations.set(index + 1, doc.document_id);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
195
225
|
}
|
|
196
226
|
}
|
|
197
227
|
|
|
@@ -200,6 +230,16 @@ export class MessageProcessor {
|
|
|
200
230
|
* Updates the internal citation collection and notifies when new citations are added
|
|
201
231
|
*/
|
|
202
232
|
private processCitations(packet: Packet) {
|
|
233
|
+
if (packet.obj.type === PacketType.CITATION_INFO) {
|
|
234
|
+
const citationInfo = packet.obj as any;
|
|
235
|
+
if (citationInfo.citation_number && citationInfo.document_id) {
|
|
236
|
+
this._citations.set(
|
|
237
|
+
citationInfo.citation_number,
|
|
238
|
+
citationInfo.document_id,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
203
243
|
if (packet.obj.type !== PacketType.CITATION_DELTA) {
|
|
204
244
|
return;
|
|
205
245
|
}
|
|
@@ -237,6 +277,13 @@ export class MessageProcessor {
|
|
|
237
277
|
*/
|
|
238
278
|
private processStreamEnd(packet: Packet) {
|
|
239
279
|
if ([PacketType.STOP, PacketType.ERROR].includes(packet.obj.type)) {
|
|
280
|
+
// Close any remaining open sections (especially the last one)
|
|
281
|
+
while (this.indicesStarted.length > 0) {
|
|
282
|
+
const ind = this.indicesStarted.shift()!;
|
|
283
|
+
console.log(`[MessageProcessor] Stream ended. Synthesizing section_end for ind=${ind}`);
|
|
284
|
+
const synthetic = getSynteticPacket(ind, PacketType.SECTION_END);
|
|
285
|
+
this.processPacket(synthetic);
|
|
286
|
+
}
|
|
240
287
|
this._isComplete = true;
|
|
241
288
|
}
|
|
242
289
|
}
|
|
@@ -248,7 +295,9 @@ export class MessageProcessor {
|
|
|
248
295
|
private extractToolCall(packets: Packet[]): ToolCallMetadata | null {
|
|
249
296
|
// Look for search tool packets
|
|
250
297
|
const searchToolStart = packets.find(
|
|
251
|
-
(p) =>
|
|
298
|
+
(p) =>
|
|
299
|
+
p.obj.type === PacketType.SEARCH_TOOL_START ||
|
|
300
|
+
p.obj.type === PacketType.SEARCH_TOOL_START_V3,
|
|
252
301
|
);
|
|
253
302
|
|
|
254
303
|
if (!searchToolStart) {
|
|
@@ -260,7 +309,10 @@ export class MessageProcessor {
|
|
|
260
309
|
const processedDocs: Record<string, any> = {};
|
|
261
310
|
|
|
262
311
|
for (const packet of packets) {
|
|
263
|
-
if (
|
|
312
|
+
if (
|
|
313
|
+
packet.obj.type === PacketType.SEARCH_TOOL_DELTA ||
|
|
314
|
+
packet.obj.type === PacketType.SEARCH_TOOL_DOCUMENTS_DELTA
|
|
315
|
+
) {
|
|
264
316
|
const delta = packet.obj as any;
|
|
265
317
|
if (delta.documents && Array.isArray(delta.documents)) {
|
|
266
318
|
delta.documents.forEach((doc: any) => {
|
|
@@ -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
|
}
|
|
@@ -30,6 +30,7 @@ export interface SendMessageParams {
|
|
|
30
30
|
enabledToolIds?: number[];
|
|
31
31
|
forcedToolIds?: number[];
|
|
32
32
|
retrieval_options?: any;
|
|
33
|
+
onyxVersion?: '2' | '3';
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export interface StreamResponse {
|
|
@@ -90,11 +91,131 @@ export const processRawChunkString = (
|
|
|
90
91
|
return [parsedChunkSections, currPartialChunk];
|
|
91
92
|
};
|
|
92
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
|
+
|
|
93
213
|
/**
|
|
94
214
|
* Handle streaming response from the backend
|
|
95
215
|
*/
|
|
96
216
|
export async function* handleStream(
|
|
97
217
|
streamingResponse: Response,
|
|
218
|
+
onyxVersion: '2' | '3' = '2',
|
|
98
219
|
): AsyncGenerator<Packet[], void, unknown> {
|
|
99
220
|
const reader = streamingResponse.body?.getReader();
|
|
100
221
|
if (!reader) {
|
|
@@ -120,6 +241,10 @@ export async function* handleStream(
|
|
|
120
241
|
previousPartialChunk,
|
|
121
242
|
);
|
|
122
243
|
|
|
244
|
+
if (onyxVersion === '3' && completedChunks.length > 0) {
|
|
245
|
+
// console.log('[Onyx v3] Raw completed chunks:', completedChunks);
|
|
246
|
+
}
|
|
247
|
+
|
|
123
248
|
if (!completedChunks.length && !partialChunk) {
|
|
124
249
|
break;
|
|
125
250
|
}
|
|
@@ -130,6 +255,10 @@ export async function* handleStream(
|
|
|
130
255
|
const packets: Packet[] = completedChunks
|
|
131
256
|
.filter((chunk) => chunk && typeof chunk === 'object')
|
|
132
257
|
.map((chunk) => {
|
|
258
|
+
if (onyxVersion === '3') {
|
|
259
|
+
return normaliseV3Chunk(chunk);
|
|
260
|
+
}
|
|
261
|
+
|
|
133
262
|
// Onyx v2 format: { ind: number, obj: { type: string, ... } }
|
|
134
263
|
if ('ind' in chunk && 'obj' in chunk) {
|
|
135
264
|
return chunk as Packet;
|
|
@@ -173,77 +302,24 @@ export async function* handleStream(
|
|
|
173
302
|
* Send a message and stream the response
|
|
174
303
|
*/
|
|
175
304
|
export async function* sendMessage(
|
|
176
|
-
|
|
177
|
-
regenerate,
|
|
178
|
-
retrieval_options,
|
|
179
|
-
message,
|
|
180
|
-
fileDescriptors,
|
|
181
|
-
currentMessageFiles,
|
|
182
|
-
parentMessageId,
|
|
183
|
-
chatSessionId,
|
|
184
|
-
filters,
|
|
185
|
-
selectedDocumentIds,
|
|
186
|
-
queryOverride,
|
|
187
|
-
forceSearch,
|
|
188
|
-
modelProvider,
|
|
189
|
-
modelVersion,
|
|
190
|
-
temperature,
|
|
191
|
-
systemPromptOverride,
|
|
192
|
-
taskPromptOverride,
|
|
193
|
-
useExistingUserMessage,
|
|
194
|
-
alternateAssistantId,
|
|
195
|
-
signal,
|
|
196
|
-
useAgentSearch,
|
|
197
|
-
enabledToolIds,
|
|
198
|
-
forcedToolIds,
|
|
199
|
-
}: SendMessageParams,
|
|
305
|
+
params: SendMessageParams,
|
|
200
306
|
isRelatedQuestion: boolean = false,
|
|
201
307
|
): AsyncGenerator<Packet[], void, unknown> {
|
|
202
|
-
const
|
|
203
|
-
selectedDocumentIds && selectedDocumentIds.length > 0;
|
|
308
|
+
const { onyxVersion = '2', signal } = params;
|
|
204
309
|
|
|
205
|
-
const payload =
|
|
206
|
-
|
|
207
|
-
chat_session_id: chatSessionId,
|
|
208
|
-
parent_message_id: parentMessageId,
|
|
209
|
-
message,
|
|
210
|
-
prompt_id: null,
|
|
211
|
-
search_doc_ids: documentsAreSelected ? selectedDocumentIds : null,
|
|
212
|
-
file_descriptors: fileDescriptors,
|
|
213
|
-
current_message_files: currentMessageFiles,
|
|
214
|
-
regenerate,
|
|
215
|
-
retrieval_options:
|
|
216
|
-
retrieval_options ??
|
|
217
|
-
(!documentsAreSelected
|
|
218
|
-
? {
|
|
219
|
-
run_search: queryOverride || forceSearch ? 'always' : 'auto',
|
|
220
|
-
real_time: true,
|
|
221
|
-
filters: filters,
|
|
222
|
-
}
|
|
223
|
-
: null),
|
|
224
|
-
query_override: queryOverride,
|
|
225
|
-
prompt_override: {
|
|
226
|
-
...(systemPromptOverride ? { system_prompt: systemPromptOverride } : {}),
|
|
227
|
-
...(taskPromptOverride ? { task_prompt: taskPromptOverride } : {}),
|
|
228
|
-
},
|
|
229
|
-
llm_override:
|
|
230
|
-
temperature || modelVersion
|
|
231
|
-
? {
|
|
232
|
-
temperature,
|
|
233
|
-
model_provider: modelProvider,
|
|
234
|
-
model_version: modelVersion,
|
|
235
|
-
}
|
|
236
|
-
: null,
|
|
237
|
-
use_existing_user_message: useExistingUserMessage,
|
|
238
|
-
use_agentic_search: useAgentSearch ?? false,
|
|
239
|
-
allowed_tool_ids: enabledToolIds,
|
|
240
|
-
forced_tool_ids: forcedToolIds,
|
|
241
|
-
};
|
|
310
|
+
const payload =
|
|
311
|
+
onyxVersion === '3' ? buildPayloadV3(params) : buildPayloadV2(params);
|
|
242
312
|
|
|
243
313
|
const body = JSON.stringify(payload);
|
|
244
314
|
|
|
245
315
|
const middleware = isRelatedQuestion ? '_rq' : '_da';
|
|
246
|
-
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}`, {
|
|
247
323
|
method: 'POST',
|
|
248
324
|
headers: {
|
|
249
325
|
'Content-Type': 'application/json',
|
|
@@ -258,7 +334,7 @@ export async function* sendMessage(
|
|
|
258
334
|
throw new Error(`Failed to send message - ${errorMsg}`);
|
|
259
335
|
}
|
|
260
336
|
|
|
261
|
-
yield* handleStream(sendMessageResponse);
|
|
337
|
+
yield* handleStream(sendMessageResponse, onyxVersion);
|
|
262
338
|
}
|
|
263
339
|
|
|
264
340
|
/**
|
|
@@ -267,8 +343,13 @@ export async function* sendMessage(
|
|
|
267
343
|
export async function createChatSession(
|
|
268
344
|
personaId: number,
|
|
269
345
|
description?: string,
|
|
346
|
+
isRelatedQuestion: boolean = false,
|
|
270
347
|
): Promise<string> {
|
|
271
|
-
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, {
|
|
272
353
|
method: 'POST',
|
|
273
354
|
headers: {
|
|
274
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 {
|
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
|
});
|