@qafka/react-native 2.3.8 → 2.3.9
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/dist/components/ChatPage.js +5 -5
- package/dist/decide-interrupt.d.ts +9 -0
- package/dist/decide-interrupt.js +12 -0
- package/dist/hooks/useChatMessages.js +73 -22
- package/dist/hooks/useVoiceChat.js +16 -0
- package/dist/services/RealtimeService.d.ts +2 -0
- package/dist/services/RealtimeService.js +7 -0
- package/dist/utils/extractionContinuation.d.ts +32 -0
- package/dist/utils/extractionContinuation.js +48 -0
- package/dist/utils/joinNarration.d.ts +15 -0
- package/dist/utils/joinNarration.js +25 -0
- package/dist/utils/toolNarration.d.ts +16 -0
- package/dist/utils/toolNarration.js +22 -0
- package/package.json +1 -1
|
@@ -60,7 +60,9 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
60
60
|
</react_native_1.View>)}
|
|
61
61
|
|
|
62
62
|
{/* Message List */}
|
|
63
|
-
<react_native_1.FlatList ref={flatListRef} data={displayMessages} renderItem={renderMessage} keyExtractor={(item) => item.id} style={{ flex: 1, flexGrow: 1, flexShrink: 1 }} contentContainerStyle={styles.messageListStyle} ListEmptyComponent={renderEmpty} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled"
|
|
63
|
+
<react_native_1.FlatList ref={flatListRef} data={displayMessages} renderItem={renderMessage} keyExtractor={(item) => item.id} style={{ flex: 1, flexGrow: 1, flexShrink: 1 }} contentContainerStyle={styles.messageListStyle} ListEmptyComponent={renderEmpty} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" ListFooterComponent={toolStatus ? (<react_native_1.View style={{ paddingHorizontal: 8, paddingTop: 4, alignItems: 'flex-start' }}>
|
|
64
|
+
<ToolStatusPill_1.ToolStatusPill message={toolStatus.message} theme={theme}/>
|
|
65
|
+
</react_native_1.View>) : null} onContentSizeChange={() => {
|
|
64
66
|
if (displayMessages.length > 0) {
|
|
65
67
|
flatListRef.current?.scrollToEnd({ animated: true });
|
|
66
68
|
}
|
|
@@ -71,10 +73,8 @@ function ChatPage({ displayMessages, isTyping, isSending, streamingMessage, tool
|
|
|
71
73
|
<TypingIndicator_1.TypingIndicator theme={theme}/>
|
|
72
74
|
</react_native_1.View>)}
|
|
73
75
|
|
|
74
|
-
{/* Tool Status Pill (
|
|
75
|
-
|
|
76
|
-
<ToolStatusPill_1.ToolStatusPill message={toolStatus.message} theme={theme}/>
|
|
77
|
-
</react_native_1.View>)}
|
|
76
|
+
{/* Tool Status Pill now renders inside the list (ListFooterComponent)
|
|
77
|
+
so it sits directly under the last message and scrolls with it. */}
|
|
78
78
|
|
|
79
79
|
{/* Input Area — hidden until SDK is ready so users can't type a message
|
|
80
80
|
that would be rejected by an un-attested send. TypingIndicator in the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when a realtime event signals a server-side interruption
|
|
3
|
+
* (user barge-in), so the caller can stop audio playback and let the user's
|
|
4
|
+
* new turn take over. Kept as a pure function so the decision is unit-testable
|
|
5
|
+
* without loading the native-bound RealtimeService.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shouldStopPlayback(event: {
|
|
8
|
+
type?: string;
|
|
9
|
+
} | null | undefined): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldStopPlayback = shouldStopPlayback;
|
|
4
|
+
/**
|
|
5
|
+
* Returns true when a realtime event signals a server-side interruption
|
|
6
|
+
* (user barge-in), so the caller can stop audio playback and let the user's
|
|
7
|
+
* new turn take over. Kept as a pure function so the decision is unit-testable
|
|
8
|
+
* without loading the native-bound RealtimeService.
|
|
9
|
+
*/
|
|
10
|
+
function shouldStopPlayback(event) {
|
|
11
|
+
return event?.type === 'response.interrupted';
|
|
12
|
+
}
|
|
@@ -4,6 +4,9 @@ exports.useChatMessages = void 0;
|
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const QafkaSDK_1 = require("../QafkaSDK");
|
|
6
6
|
const Qafka_utils_1 = require("../components/Qafka.utils");
|
|
7
|
+
const joinNarration_1 = require("../utils/joinNarration");
|
|
8
|
+
const extractionContinuation_1 = require("../utils/extractionContinuation");
|
|
9
|
+
const toolNarration_1 = require("../utils/toolNarration");
|
|
7
10
|
/**
|
|
8
11
|
* Custom hook for managing chat messages and sending
|
|
9
12
|
*/
|
|
@@ -32,29 +35,47 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
32
35
|
const lastConversationIdRef = (0, react_1.useRef)(null);
|
|
33
36
|
// guard against double-fire of postToolResult per toolKey
|
|
34
37
|
const inFlightPostToolResultRef = (0, react_1.useRef)(new Set());
|
|
38
|
+
// The most recent suggested tool that is BOTH multi-step and file-input.
|
|
39
|
+
// Uploading + extracting a file does not start a new model turn on its own,
|
|
40
|
+
// so when extraction completes we fire a hidden continuation turn (tools
|
|
41
|
+
// enabled) to let the model advance to the next step. Captured here at
|
|
42
|
+
// suggestion time because the extraction callback only carries the result,
|
|
43
|
+
// not the tool definition.
|
|
44
|
+
const activeMultiStepFileToolRef = (0, react_1.useRef)(null);
|
|
45
|
+
// Always-current `context` so a deferred send (the post-extraction
|
|
46
|
+
// continuation) reads the consumer's freshly-updated context (e.g. a CV
|
|
47
|
+
// summary it set in onExtractionResult) rather than a stale closure value.
|
|
48
|
+
const contextRef = (0, react_1.useRef)(context);
|
|
49
|
+
contextRef.current = context;
|
|
35
50
|
const scrollToBottom = (animated = true) => {
|
|
36
51
|
setTimeout(() => {
|
|
37
52
|
flatListRef.current?.scrollToEnd({ animated });
|
|
38
53
|
}, 100);
|
|
39
54
|
};
|
|
40
|
-
|
|
55
|
+
// `hidden` turns send a message to the backend WITHOUT rendering a user
|
|
56
|
+
// bubble — used to auto-advance a tool flow (e.g. after a file upload) where
|
|
57
|
+
// the "message" is a system continuation the end user never typed.
|
|
58
|
+
const handleSend = async (message, options) => {
|
|
41
59
|
if (isSending)
|
|
42
60
|
return;
|
|
61
|
+
const hidden = options?.hidden === true;
|
|
43
62
|
setIsSending(true);
|
|
44
63
|
setIsTyping(true);
|
|
45
64
|
setStreamingMessage('');
|
|
46
65
|
finalChunkBufferRef.current = '';
|
|
47
|
-
// Add user message
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
// Add user message (skipped for hidden continuation turns)
|
|
67
|
+
if (!hidden) {
|
|
68
|
+
const userMessage = {
|
|
69
|
+
id: Date.now().toString(),
|
|
70
|
+
role: 'user',
|
|
71
|
+
text: message,
|
|
72
|
+
content: message,
|
|
73
|
+
timestamp: new Date(),
|
|
74
|
+
};
|
|
75
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
76
|
+
onMessageSent?.(message);
|
|
77
|
+
scrollToBottom();
|
|
78
|
+
}
|
|
58
79
|
const messageToSendAI = contextDescription
|
|
59
80
|
? `${contextDescription}\n\n${message}`
|
|
60
81
|
: message;
|
|
@@ -179,7 +200,7 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
179
200
|
// onError
|
|
180
201
|
(err) => {
|
|
181
202
|
handleError(err);
|
|
182
|
-
},
|
|
203
|
+
}, contextRef.current, contextDescription,
|
|
183
204
|
// onToolSuggested
|
|
184
205
|
async (tools, conversationId, messageId) => {
|
|
185
206
|
if (conversationId) {
|
|
@@ -188,6 +209,12 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
188
209
|
if (messageId) {
|
|
189
210
|
lastMessageIdRef.current = messageId;
|
|
190
211
|
}
|
|
212
|
+
// Remember a multi-step file tool so a completed extraction can
|
|
213
|
+
// auto-advance the flow (see the onExtractionResult handler below).
|
|
214
|
+
const multiStepFileTool = (tools || []).find(extractionContinuation_1.isMultiStepFileTool);
|
|
215
|
+
if (multiStepFileTool) {
|
|
216
|
+
activeMultiStepFileToolRef.current = multiStepFileTool;
|
|
217
|
+
}
|
|
191
218
|
if (onToolSuggested) {
|
|
192
219
|
// Tracks tools the consumer's `onToolSuggested` handler explicitly
|
|
193
220
|
// called `addResponse` for. The auto-fire loop below uses this — NOT
|
|
@@ -251,7 +278,13 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
251
278
|
// In managed follow-up mode the SDK should POST the
|
|
252
279
|
// tool result back to the backend, which runs a second LLM turn
|
|
253
280
|
// and streams a `final_chunk` reply.
|
|
254
|
-
|
|
281
|
+
//
|
|
282
|
+
// Skipped for card-rendering tools: there the model's first reply
|
|
283
|
+
// (bridge) already carries the conversation and the card shows the
|
|
284
|
+
// data, so the turn-2 narration only repeats it in a second message.
|
|
285
|
+
if (commits &&
|
|
286
|
+
tool?.executionMode === 'custom-with-ai' &&
|
|
287
|
+
!(0, toolNarration_1.shouldSkipTurn2Narration)(tool)) {
|
|
255
288
|
const conversationId = lastConversationIdRef.current;
|
|
256
289
|
const messageId = lastMessageIdRef.current;
|
|
257
290
|
if (conversationId && messageId) {
|
|
@@ -288,7 +321,10 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
288
321
|
setMessages((prev) => {
|
|
289
322
|
const lastMsg = prev[prev.length - 1];
|
|
290
323
|
if (lastMsg && lastMsg.role === 'assistant') {
|
|
291
|
-
|
|
324
|
+
// Turn-2 (post file-upload) narration appends to the same bubble; keep a
|
|
325
|
+
// paragraph break so it doesn't glue onto turn-1 ("..yeterli!Harika!").
|
|
326
|
+
// Matches the streaming path's separator and the backend persisted join.
|
|
327
|
+
const nextText = (0, joinNarration_1.joinNarration)(lastMsg.text || '', content);
|
|
292
328
|
return [
|
|
293
329
|
...prev.slice(0, -1),
|
|
294
330
|
{
|
|
@@ -401,8 +437,26 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
401
437
|
}
|
|
402
438
|
onFileUploadRequest?.(request);
|
|
403
439
|
},
|
|
404
|
-
// onExtractionResult
|
|
405
|
-
|
|
440
|
+
// onExtractionResult — forward to consumer, then auto-advance multi-step
|
|
441
|
+
// file flows. A completed extraction does not start a new model turn by
|
|
442
|
+
// itself, so we fire a hidden continuation turn (tools enabled) carrying
|
|
443
|
+
// the extracted fields, letting the model proceed to the next step.
|
|
444
|
+
(result) => {
|
|
445
|
+
onExtractionResult?.(result);
|
|
446
|
+
const tool = activeMultiStepFileToolRef.current;
|
|
447
|
+
if ((0, extractionContinuation_1.shouldAutoContinueAfterExtraction)({
|
|
448
|
+
isMultiStepFileTool: !!tool,
|
|
449
|
+
extractionStatus: result?.status,
|
|
450
|
+
})) {
|
|
451
|
+
activeMultiStepFileToolRef.current = null;
|
|
452
|
+
// A completed extraction does not start a new model turn by itself; fire a
|
|
453
|
+
// hidden continuation (tools enabled) carrying the extracted fields inline
|
|
454
|
+
// so the model reliably advances to the next step.
|
|
455
|
+
void handleSend((0, extractionContinuation_1.buildExtractionContinuationMessage)(result?.data), {
|
|
456
|
+
hidden: true,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
},
|
|
406
460
|
// onToolStatus: show pill
|
|
407
461
|
(status) => {
|
|
408
462
|
setToolStatus(status);
|
|
@@ -439,15 +493,12 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
439
493
|
setToolStatus(null);
|
|
440
494
|
finalChunkBufferRef.current +=
|
|
441
495
|
(finalChunkBufferRef.current ? '\n\n' : '') + content;
|
|
442
|
-
setStreamingMessage((prev) =>
|
|
443
|
-
const sep = prev && !prev.endsWith('\n') ? '\n\n' : '';
|
|
444
|
-
return prev + sep + content;
|
|
445
|
-
});
|
|
496
|
+
setStreamingMessage((prev) => (0, joinNarration_1.joinNarration)(prev, content));
|
|
446
497
|
});
|
|
447
498
|
}
|
|
448
499
|
else {
|
|
449
500
|
// Non-streaming
|
|
450
|
-
const response = await sdk.sendMessage(message,
|
|
501
|
+
const response = await sdk.sendMessage(message, contextRef.current, contextDescription);
|
|
451
502
|
const aiMessage = {
|
|
452
503
|
id: response.id,
|
|
453
504
|
role: 'assistant',
|
|
@@ -131,6 +131,22 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
131
131
|
}
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
|
+
case 'response.interrupted': {
|
|
135
|
+
// Barge-in: the user cut in. RealtimeService already stopped native
|
|
136
|
+
// playback; finalize the partial AI turn (matching the server, which
|
|
137
|
+
// persists what was already streamed) and return to listening.
|
|
138
|
+
const partialAi = transcriptRef.current;
|
|
139
|
+
if (partialAi && partialAi.trim().length > 0) {
|
|
140
|
+
setTranscriptHistory((h) => [
|
|
141
|
+
...h,
|
|
142
|
+
{ id: `a_${Date.now()}_${h.length}`, role: 'assistant', text: partialAi },
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
transcriptRef.current = '';
|
|
146
|
+
setTranscript('');
|
|
147
|
+
setState('listening');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
134
150
|
case 'session.closed':
|
|
135
151
|
setState('idle');
|
|
136
152
|
setNavigationSuggestion(null);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RealtimeService = void 0;
|
|
4
4
|
const QafkaAudio_1 = require("../native/QafkaAudio");
|
|
5
5
|
const QafkaAttestation_1 = require("../native/QafkaAttestation");
|
|
6
|
+
const decide_interrupt_1 = require("../decide-interrupt");
|
|
6
7
|
class RealtimeService {
|
|
7
8
|
apiUrl;
|
|
8
9
|
apiKey;
|
|
@@ -121,6 +122,12 @@ class RealtimeService {
|
|
|
121
122
|
// playback errors are surfaced to the consumer via eventHandler
|
|
122
123
|
});
|
|
123
124
|
}
|
|
125
|
+
else if ((0, decide_interrupt_1.shouldStopPlayback)(event)) {
|
|
126
|
+
// Barge-in: the server signalled the user interrupted. Purge native
|
|
127
|
+
// playback so queued audio stops; the WS is ordered, so no further
|
|
128
|
+
// audio delta arrives after this for the cancelled turn.
|
|
129
|
+
QafkaAudio_1.QafkaAudio.stopPlayback().catch(() => { });
|
|
130
|
+
}
|
|
124
131
|
this.eventHandler?.(event);
|
|
125
132
|
}
|
|
126
133
|
attachAudioListener() {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for auto-continuing a multi-step file-input tool flow after a
|
|
3
|
+
* document is uploaded and its fields are extracted.
|
|
4
|
+
*
|
|
5
|
+
* Background: uploading a file + extracting its fields does NOT, on its own,
|
|
6
|
+
* start a new model turn. Only a fresh `/chat` turn re-enables tools, which is
|
|
7
|
+
* what lets the model advance to the next step (e.g. ask the first question).
|
|
8
|
+
* For multi-step file tools we therefore fire a hidden continuation turn that
|
|
9
|
+
* carries the extracted fields so the model can proceed.
|
|
10
|
+
*
|
|
11
|
+
* These helpers are intentionally generic — they know nothing about any
|
|
12
|
+
* specific tool, field name, or domain.
|
|
13
|
+
*/
|
|
14
|
+
/** A suggested tool spans multiple steps AND collects a file when it exposes
|
|
15
|
+
* both a `step` parameter and a file-input module. Single-shot file tools
|
|
16
|
+
* (extract → render → done) are excluded. */
|
|
17
|
+
export declare function isMultiStepFileTool(tool: any): boolean;
|
|
18
|
+
/** Auto-advance only once a multi-step file tool's extraction has completed.
|
|
19
|
+
* Incomplete extractions and single-shot tools stay on the normal path. */
|
|
20
|
+
export declare function shouldAutoContinueAfterExtraction(args: {
|
|
21
|
+
isMultiStepFileTool: boolean;
|
|
22
|
+
extractionStatus?: string;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/** Build the hidden continuation message. The extracted fields are embedded
|
|
25
|
+
* inline as JSON: in practice the model only reliably uses the values (and
|
|
26
|
+
* stays on-script / in-language) when they are present in the message itself —
|
|
27
|
+
* a generic "read it from context" trigger derails it. The data IS also in the
|
|
28
|
+
* `context` prop, but this inline copy is what keeps the continuation reliable.
|
|
29
|
+
* Keeping it out of the persisted transcript is handled separately, server side,
|
|
30
|
+
* not by stripping it here.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildExtractionContinuationMessage(data: Record<string, any> | undefined | null): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers for auto-continuing a multi-step file-input tool flow after a
|
|
4
|
+
* document is uploaded and its fields are extracted.
|
|
5
|
+
*
|
|
6
|
+
* Background: uploading a file + extracting its fields does NOT, on its own,
|
|
7
|
+
* start a new model turn. Only a fresh `/chat` turn re-enables tools, which is
|
|
8
|
+
* what lets the model advance to the next step (e.g. ask the first question).
|
|
9
|
+
* For multi-step file tools we therefore fire a hidden continuation turn that
|
|
10
|
+
* carries the extracted fields so the model can proceed.
|
|
11
|
+
*
|
|
12
|
+
* These helpers are intentionally generic — they know nothing about any
|
|
13
|
+
* specific tool, field name, or domain.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.isMultiStepFileTool = isMultiStepFileTool;
|
|
17
|
+
exports.shouldAutoContinueAfterExtraction = shouldAutoContinueAfterExtraction;
|
|
18
|
+
exports.buildExtractionContinuationMessage = buildExtractionContinuationMessage;
|
|
19
|
+
/** A suggested tool spans multiple steps AND collects a file when it exposes
|
|
20
|
+
* both a `step` parameter and a file-input module. Single-shot file tools
|
|
21
|
+
* (extract → render → done) are excluded. */
|
|
22
|
+
function isMultiStepFileTool(tool) {
|
|
23
|
+
const params = tool?.parameters;
|
|
24
|
+
const hasStep = !!params && typeof params === 'object' && 'step' in params;
|
|
25
|
+
return Boolean(tool?.modules?.hasFileInput) && hasStep;
|
|
26
|
+
}
|
|
27
|
+
/** Auto-advance only once a multi-step file tool's extraction has completed.
|
|
28
|
+
* Incomplete extractions and single-shot tools stay on the normal path. */
|
|
29
|
+
function shouldAutoContinueAfterExtraction(args) {
|
|
30
|
+
return args.isMultiStepFileTool && args.extractionStatus === 'complete';
|
|
31
|
+
}
|
|
32
|
+
/** Build the hidden continuation message. The extracted fields are embedded
|
|
33
|
+
* inline as JSON: in practice the model only reliably uses the values (and
|
|
34
|
+
* stays on-script / in-language) when they are present in the message itself —
|
|
35
|
+
* a generic "read it from context" trigger derails it. The data IS also in the
|
|
36
|
+
* `context` prop, but this inline copy is what keeps the continuation reliable.
|
|
37
|
+
* Keeping it out of the persisted transcript is handled separately, server side,
|
|
38
|
+
* not by stripping it here.
|
|
39
|
+
*/
|
|
40
|
+
function buildExtractionContinuationMessage(data) {
|
|
41
|
+
const fields = data && typeof data === 'object' ? JSON.stringify(data) : '{}';
|
|
42
|
+
return [
|
|
43
|
+
'[Uploaded document — extracted fields]',
|
|
44
|
+
fields,
|
|
45
|
+
'',
|
|
46
|
+
'(The user just uploaded a document and it was processed. Use these fields to continue the tool flow — proceed to the next step. Do not restart or repeat the greeting.)',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins two pieces of assistant narration into a single text body with a
|
|
3
|
+
* consistent paragraph break.
|
|
4
|
+
*
|
|
5
|
+
* A custom-with-ai tool produces a reply in two streamed parts (e.g. a first
|
|
6
|
+
* reply, then a follow-up after a file upload). Those parts land in the same
|
|
7
|
+
* assistant bubble, so they must be separated the same way everywhere — both in
|
|
8
|
+
* the live render and in what the backend persists — otherwise the live text
|
|
9
|
+
* comes out glued together with no break (".. yeterli!Harika! ..").
|
|
10
|
+
*
|
|
11
|
+
* Rules: empty parts pass through untouched; if `prev` already ends with a
|
|
12
|
+
* newline, no extra break is added; otherwise the two parts are joined with a
|
|
13
|
+
* blank line.
|
|
14
|
+
*/
|
|
15
|
+
export declare function joinNarration(prev: string, next: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.joinNarration = joinNarration;
|
|
4
|
+
/**
|
|
5
|
+
* Joins two pieces of assistant narration into a single text body with a
|
|
6
|
+
* consistent paragraph break.
|
|
7
|
+
*
|
|
8
|
+
* A custom-with-ai tool produces a reply in two streamed parts (e.g. a first
|
|
9
|
+
* reply, then a follow-up after a file upload). Those parts land in the same
|
|
10
|
+
* assistant bubble, so they must be separated the same way everywhere — both in
|
|
11
|
+
* the live render and in what the backend persists — otherwise the live text
|
|
12
|
+
* comes out glued together with no break (".. yeterli!Harika! ..").
|
|
13
|
+
*
|
|
14
|
+
* Rules: empty parts pass through untouched; if `prev` already ends with a
|
|
15
|
+
* newline, no extra break is added; otherwise the two parts are joined with a
|
|
16
|
+
* blank line.
|
|
17
|
+
*/
|
|
18
|
+
function joinNarration(prev, next) {
|
|
19
|
+
if (!prev)
|
|
20
|
+
return next || '';
|
|
21
|
+
if (!next)
|
|
22
|
+
return prev;
|
|
23
|
+
const sep = prev.endsWith('\n') ? '' : '\n\n';
|
|
24
|
+
return prev + sep + next;
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether to skip the turn-2 narration (the post-tool-result round-trip) for a
|
|
3
|
+
* suggested tool.
|
|
4
|
+
*
|
|
5
|
+
* custom-with-ai tools answer in two streamed parts: the model's first reply
|
|
6
|
+
* (the "bridge", produced alongside the tool call) and a turn-2 narration of
|
|
7
|
+
* the tool result. When the tool renders its result as a CARD, the bridge
|
|
8
|
+
* already carries the conversational text and the card shows the data, so the
|
|
9
|
+
* turn-2 text narration is redundant — it just repeats the bridge in a second
|
|
10
|
+
* message. We skip it for those tools.
|
|
11
|
+
*
|
|
12
|
+
* Tools WITHOUT a card are left untouched: there the substantive answer often
|
|
13
|
+
* lives only in the turn-2 narration (e.g. a lookup whose bridge is a short
|
|
14
|
+
* "one moment" filler), so it must still run.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shouldSkipTurn2Narration(tool: any): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldSkipTurn2Narration = shouldSkipTurn2Narration;
|
|
4
|
+
/**
|
|
5
|
+
* Whether to skip the turn-2 narration (the post-tool-result round-trip) for a
|
|
6
|
+
* suggested tool.
|
|
7
|
+
*
|
|
8
|
+
* custom-with-ai tools answer in two streamed parts: the model's first reply
|
|
9
|
+
* (the "bridge", produced alongside the tool call) and a turn-2 narration of
|
|
10
|
+
* the tool result. When the tool renders its result as a CARD, the bridge
|
|
11
|
+
* already carries the conversational text and the card shows the data, so the
|
|
12
|
+
* turn-2 text narration is redundant — it just repeats the bridge in a second
|
|
13
|
+
* message. We skip it for those tools.
|
|
14
|
+
*
|
|
15
|
+
* Tools WITHOUT a card are left untouched: there the substantive answer often
|
|
16
|
+
* lives only in the turn-2 narration (e.g. a lookup whose bridge is a short
|
|
17
|
+
* "one moment" filler), so it must still run.
|
|
18
|
+
*/
|
|
19
|
+
function shouldSkipTurn2Narration(tool) {
|
|
20
|
+
return (tool?.executionMode === 'custom-with-ai' &&
|
|
21
|
+
tool?.uiConfig?.responseType === 'card');
|
|
22
|
+
}
|
package/package.json
CHANGED