@qafka/react-native 2.3.7 → 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/CHANGELOG.md +6 -0
- package/dist/components/ChatPage.js +5 -5
- package/dist/components/MessageBubble.js +2 -1
- package/dist/components/VoicePage.js +39 -14
- 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/BackendService.js +33 -9
- 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/docs/qafka-tool-authoring-guide.md +328 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.8] — 2026-06-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Ships `docs/qafka-tool-authoring-guide.md` inside the package — a version-matched guide that tells AI coding assistants (and developers) exactly what to configure in the Qafka dashboard and what to implement in code when authoring a tool. The Qafka CLI points a project's `CLAUDE.md` at this file, so the guidance always matches the installed SDK version.
|
|
15
|
+
|
|
10
16
|
## [2.1.1] — 2026-06-03
|
|
11
17
|
|
|
12
18
|
### Fixed
|
|
@@ -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
|
|
@@ -75,7 +75,8 @@ function MessageBubble({ message, role, timestamp, theme, showTimestamp = true,
|
|
|
75
75
|
: dataToRender;
|
|
76
76
|
// Check layout preference (default: vertical)
|
|
77
77
|
const layout = response.layout || 'vertical';
|
|
78
|
-
// If custom layout, pass entire array to component
|
|
78
|
+
// If custom layout, pass entire array to component.
|
|
79
|
+
// VoicePage's tool list branch mirrors this same contract — keep both in sync.
|
|
79
80
|
if (layout === 'custom') {
|
|
80
81
|
return (<react_native_1.View style={{ marginTop: theme.spacing.md }}>
|
|
81
82
|
<CustomComponent data={items} // Full array
|
|
@@ -67,6 +67,18 @@ const BASELINE_PHASE_MS = 800;
|
|
|
67
67
|
const SPIKE_DURATION_MS = 700;
|
|
68
68
|
const SPIKE_THRESHOLD = 0.3;
|
|
69
69
|
const SPIKE_MIN_INTERVAL_MS = 150;
|
|
70
|
+
// Resolve a uiConfig.dataPath ("data" or nested "data.campaigns") against the
|
|
71
|
+
// tool result envelope. Mirrors MessageBubble's text-mode walk byte-for-byte so
|
|
72
|
+
// list/card data is read from the same level in both render paths. No dataPath
|
|
73
|
+
// → the whole envelope (single-card tools that scope nothing).
|
|
74
|
+
function resolveDataPath(envelope, dataPath) {
|
|
75
|
+
if (!dataPath)
|
|
76
|
+
return envelope;
|
|
77
|
+
let cur = envelope;
|
|
78
|
+
for (const part of dataPath.split('.'))
|
|
79
|
+
cur = cur?.[part];
|
|
80
|
+
return cur;
|
|
81
|
+
}
|
|
70
82
|
// --- Default components (used when customer doesn't provide custom ones) ---
|
|
71
83
|
function DefaultVoiceIndicator({ state, amplitude, theme, isMuted }) {
|
|
72
84
|
// Smoothed amplitude (drives ring opacity factor + core scale).
|
|
@@ -532,12 +544,18 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
|
|
|
532
544
|
</react_native_1.View>);
|
|
533
545
|
}
|
|
534
546
|
const rt = rtUnion;
|
|
547
|
+
const ui = rt.tool?.uiConfig;
|
|
548
|
+
// Resolve list/card data through uiConfig.dataPath BEFORE anything reads it,
|
|
549
|
+
// so multi-level paths ("data.campaigns") land on the right level instead of
|
|
550
|
+
// a hardcoded rt.data.data. The empty-data fallback, list slicing and the
|
|
551
|
+
// non-list branch all read this single resolved value (mirror MessageBubble).
|
|
552
|
+
const dataToRender = resolveDataPath(rt.data, ui?.dataPath);
|
|
553
|
+
const isListTool = ui?.responseType === 'list';
|
|
535
554
|
// Empty-data fallback (matches text chat list-item behavior)
|
|
536
|
-
const isListTool = rt.tool?.uiConfig?.responseType === 'list';
|
|
537
555
|
if (isListTool &&
|
|
538
|
-
(
|
|
539
|
-
|
|
540
|
-
(Array.isArray(
|
|
556
|
+
(dataToRender === undefined ||
|
|
557
|
+
dataToRender === null ||
|
|
558
|
+
(Array.isArray(dataToRender) && dataToRender.length === 0))) {
|
|
541
559
|
return (<react_native_1.View key={rt.toolCallId} style={{
|
|
542
560
|
padding: 12,
|
|
543
561
|
backgroundColor: '#f0f0f0',
|
|
@@ -546,7 +564,7 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
|
|
|
546
564
|
<react_native_1.Text>No results.</react_native_1.Text>
|
|
547
565
|
</react_native_1.View>);
|
|
548
566
|
}
|
|
549
|
-
const componentKey =
|
|
567
|
+
const componentKey = ui?.itemComponent || rt.toolKey;
|
|
550
568
|
const Component = registeredComponents?.[componentKey];
|
|
551
569
|
if (!Component) {
|
|
552
570
|
if (__DEV__) {
|
|
@@ -554,18 +572,25 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
|
|
|
554
572
|
}
|
|
555
573
|
return null;
|
|
556
574
|
}
|
|
557
|
-
// List rendering:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
575
|
+
// List rendering: responseType === 'list' over the dataPath-resolved array.
|
|
576
|
+
if (isListTool && Array.isArray(dataToRender)) {
|
|
577
|
+
const max = ui?.maxItems || 10;
|
|
578
|
+
const items = dataToRender.slice(0, max);
|
|
579
|
+
// Mirror MessageBubble's text-mode contract (renderToolResult, list
|
|
580
|
+
// branch): layout === 'custom' hands the whole array to a single
|
|
581
|
+
// component; horizontal/vertical render one component per item. Keep
|
|
582
|
+
// these two render paths in lockstep so list output can't diverge again.
|
|
583
|
+
if (ui?.layout === 'custom') {
|
|
584
|
+
return (<react_native_1.View key={rt.toolCallId}>
|
|
585
|
+
<Component data={items}/>
|
|
586
|
+
</react_native_1.View>);
|
|
587
|
+
}
|
|
563
588
|
return (<react_native_1.View key={rt.toolCallId} style={{ gap: 10 }}>
|
|
564
|
-
{
|
|
589
|
+
{items.map((item, idx) => (<Component key={`${rt.toolCallId}_${idx}`} data={item}/>))}
|
|
565
590
|
</react_native_1.View>);
|
|
566
591
|
}
|
|
567
|
-
// Non-list: render once with
|
|
568
|
-
return <Component key={rt.toolCallId} data={
|
|
592
|
+
// Non-list: render once with the dataPath-resolved data
|
|
593
|
+
return <Component key={rt.toolCallId} data={dataToRender}/>;
|
|
569
594
|
})}
|
|
570
595
|
</react_native_1.ScrollView>) : null}
|
|
571
596
|
</react_native_1.View>
|
|
@@ -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);
|
|
@@ -740,16 +740,40 @@ class BackendService {
|
|
|
740
740
|
headers['Authorization'] = `Bearer ${sessionToken}`;
|
|
741
741
|
}
|
|
742
742
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
743
|
+
// Expo SDK 56+ installs a WinterCG `fetch` that rejects React Native's
|
|
744
|
+
// {uri,name,type} FormData file parts ("Unsupported FormDataPart
|
|
745
|
+
// implementation"). Use XHR (same transport as the chat stream) so the
|
|
746
|
+
// native multipart path handles the file part directly.
|
|
747
|
+
const url = `${this.baseURL}/tools-v2/${toolId}/files/upload`;
|
|
748
|
+
return await new Promise((resolve, reject) => {
|
|
749
|
+
const xhr = new XMLHttpRequest();
|
|
750
|
+
xhr.open('POST', url);
|
|
751
|
+
Object.keys(headers).forEach((k) => xhr.setRequestHeader(k, headers[k]));
|
|
752
|
+
xhr.timeout = this.timeout;
|
|
753
|
+
xhr.onload = () => {
|
|
754
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
755
|
+
try {
|
|
756
|
+
resolve(JSON.parse(xhr.responseText));
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
resolve({});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
let message = `Upload failed: ${xhr.status}`;
|
|
764
|
+
try {
|
|
765
|
+
message = JSON.parse(xhr.responseText).message || message;
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
message = `Upload failed: ${xhr.status}`;
|
|
769
|
+
}
|
|
770
|
+
reject(new Error(message));
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
xhr.onerror = () => reject(new Error('Upload failed'));
|
|
774
|
+
xhr.ontimeout = () => reject(new Error('Upload failed: timeout'));
|
|
775
|
+
xhr.send(formData);
|
|
747
776
|
});
|
|
748
|
-
if (!response.ok) {
|
|
749
|
-
const error = await response.json().catch(() => ({ message: 'Upload failed' }));
|
|
750
|
-
throw new Error(error.message || `Upload failed: ${response.status}`);
|
|
751
|
-
}
|
|
752
|
-
return response.json();
|
|
753
777
|
}
|
|
754
778
|
}
|
|
755
779
|
exports.BackendService = BackendService;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Qafka Tool Authoring Guide
|
|
2
|
+
|
|
3
|
+
**Audience:** AI coding assistants (and developers) working in an app that has the
|
|
4
|
+
`@qafka/react-native` SDK installed. When the developer says *"I need a tool that does X"*,
|
|
5
|
+
this document is the authoritative reference for **exactly what to configure in the Qafka
|
|
6
|
+
dashboard** and **what to implement in code**.
|
|
7
|
+
|
|
8
|
+
> **This guide ships inside the SDK package**, so it always matches the SDK version that is
|
|
9
|
+
> actually installed in this project. If a capability is **not described here, it does not
|
|
10
|
+
> exist in this SDK version** — do not tell the developer to enable a field or pass a prop
|
|
11
|
+
> that this guide does not mention. Inventing tool features that aren't real is the single
|
|
12
|
+
> most common way this goes wrong: the assistant says "turn on feature Y", the developer
|
|
13
|
+
> looks for it, it isn't there, and they're stuck. Don't do that.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Mental model: a tool lives in two places
|
|
18
|
+
|
|
19
|
+
A Qafka tool always has **two halves**, and they are configured in two different places:
|
|
20
|
+
|
|
21
|
+
| Half | Where it lives | Who configures it |
|
|
22
|
+
|------|----------------|-------------------|
|
|
23
|
+
| **Definition** (name, when to use it, parameters, execution behavior, side effects, result UI) | The **Qafka dashboard** (Tool V2) | The **developer**, by hand, in the web dashboard |
|
|
24
|
+
| **Client behavior** (running on-device actions, rendering custom result UI, supplying transient data, handling file uploads) | The app's **code**, on the `<Qafka />` / `<QafkaProvider />` element | The **developer / you, the assistant** |
|
|
25
|
+
|
|
26
|
+
**You (the AI assistant) cannot edit the dashboard.** You have no access to it. So your job
|
|
27
|
+
when asked to build a tool is always **hybrid**:
|
|
28
|
+
|
|
29
|
+
1. Produce **clear, step-by-step dashboard instructions** for the developer ("Create a tool
|
|
30
|
+
named `…`, set its description to `…`, set execution mode to `…`, add a parameter `…`,
|
|
31
|
+
turn on `…`").
|
|
32
|
+
2. **Implement only the code side** yourself (handlers, components, props on `<Qafka />`).
|
|
33
|
+
|
|
34
|
+
Never claim you "created the tool" — you set up the code side and instructed the developer on
|
|
35
|
+
the dashboard side.
|
|
36
|
+
|
|
37
|
+
> **Auth note:** in production the SDK authenticates via device attestation, not an API key in
|
|
38
|
+
> the bundle. You don't need to handle auth when authoring tools; the only required identity
|
|
39
|
+
> props are `projectId` and `endUserId` (see §6).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. The workflow to follow when asked for a tool
|
|
44
|
+
|
|
45
|
+
When the developer describes a tool, resolve these five questions **before** writing anything.
|
|
46
|
+
If the answer to any of them isn't clear from the request, ask:
|
|
47
|
+
|
|
48
|
+
1. **What does it do?** — the parameters it needs, where each value comes from, and whether it
|
|
49
|
+
reads/writes anything (an API call, an email, a navigation).
|
|
50
|
+
2. **Execution mode** — `server`, `custom`, or `custom-with-ai` (§3). This is the most
|
|
51
|
+
important decision; it determines whether you write client code at all.
|
|
52
|
+
3. **Modality** — should it be available in `text`, `voice`, or both (§4)?
|
|
53
|
+
4. **Result UI** — nothing, a built-in inline display, a custom component, or a card (§6).
|
|
54
|
+
5. **Side effects** — does completing it email someone / call a webhook (§5, *actions*)?
|
|
55
|
+
|
|
56
|
+
Then produce the two outputs: **dashboard steps** + **code changes**.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 3. Execution mode (the key decision)
|
|
61
|
+
|
|
62
|
+
A tool's `executionMode` is one of three values. Default is `custom`.
|
|
63
|
+
|
|
64
|
+
### `server`
|
|
65
|
+
The **backend** calls the tool's configured endpoint, then the AI writes the final answer
|
|
66
|
+
narrating the result. **`onToolSuggested` is NOT fired** — your app does not run anything for
|
|
67
|
+
this tool, so **no client handler code is needed**.
|
|
68
|
+
|
|
69
|
+
- Use when the action is a server-side API call and you want the AI to answer using the result.
|
|
70
|
+
- The developer configures the **endpoint** (URL, method, body, headers) in the dashboard (§5).
|
|
71
|
+
- Set a **loading message** in the dashboard so the wait feels natural.
|
|
72
|
+
|
|
73
|
+
### `custom`
|
|
74
|
+
The SDK emits a tool suggestion to your app; **your code runs the action** via the
|
|
75
|
+
`onToolSuggested` prop and reports the result back. There is **no second AI turn** — what your
|
|
76
|
+
handler renders is the final result.
|
|
77
|
+
|
|
78
|
+
- Use when the action must run **in the app / on-device**: navigate a screen, read local
|
|
79
|
+
state, call a device API, or hit an endpoint the AI shouldn't see.
|
|
80
|
+
- You **must implement `onToolSuggested`** (§6).
|
|
81
|
+
|
|
82
|
+
### `custom-with-ai`
|
|
83
|
+
Like `custom` (your code runs the action), **but** after your handler returns data the SDK
|
|
84
|
+
sends it back so the AI produces a follow-up narration.
|
|
85
|
+
|
|
86
|
+
- Use when the action runs in the app **and** you want the AI to summarize/explain the result.
|
|
87
|
+
- You **must implement `onToolSuggested`** (§6). Set a **loading message** in the dashboard.
|
|
88
|
+
|
|
89
|
+
> Decision shortcut:
|
|
90
|
+
> *Backend does the work, AI narrates* → **`server`** (no client code).
|
|
91
|
+
> *App does the work, no narration* → **`custom`**.
|
|
92
|
+
> *App does the work, AI narrates the outcome* → **`custom-with-ai`**.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 4. Modality — `availableIn`
|
|
97
|
+
|
|
98
|
+
In the dashboard, **Available in** controls where the tool can be selected: **Both**,
|
|
99
|
+
**Text only**, or **Voice only** (stored as `availableIn: ["text","voice"]`, `["text"]`, or
|
|
100
|
+
`["voice"]`; default is both). Voice-only requires that voice is enabled for the project — the
|
|
101
|
+
dashboard disables that option otherwise.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 5. Dashboard fields (what you tell the developer to fill in)
|
|
106
|
+
|
|
107
|
+
These are the fields a developer sets on a Tool V2 in the dashboard. Tell them which to fill
|
|
108
|
+
based on the tool you're building. **Only these exist** — don't reference others.
|
|
109
|
+
|
|
110
|
+
### Identity & selection
|
|
111
|
+
- **`name`** *(required)* — the tool's identifier.
|
|
112
|
+
- **`description`** *(required)* — **the full behavior protocol the chat AI reads** once the
|
|
113
|
+
tool is selected. Put the real instructions here: what the tool does, how to use each
|
|
114
|
+
parameter, what to say. This is the substantive field.
|
|
115
|
+
- **`whenToUse`** *(optional)* — a **short, one-line selector hint** used only to decide *when*
|
|
116
|
+
to pick this tool. Keep it brief (~200 chars; longer values are truncated for selection). It
|
|
117
|
+
is **not** a place for behavior detail — that goes in `description`.
|
|
118
|
+
- **`category`**, **`tags`**, **`usageExamples`** — optional metadata to aid organization and
|
|
119
|
+
selection.
|
|
120
|
+
|
|
121
|
+
### Parameters
|
|
122
|
+
Each parameter has:
|
|
123
|
+
- **`name`**, **`type`** (`string` | `number` | `boolean` | `object` | `array`),
|
|
124
|
+
**`required`**, **`description`**.
|
|
125
|
+
- **`source`** — where the value comes from: `ai` (the model fills it), `user_input` (asked
|
|
126
|
+
from the user), or `context` (pulled from the runtime `context` you pass on `<Qafka />`; use
|
|
127
|
+
**`contextKey`** to name the context field).
|
|
128
|
+
- For AI-sourced parameters you can also set **`defaultValue`**, **`label`**, **`enumLabels`**
|
|
129
|
+
(a slug→display-label map so the AI emits a clean slug while the UI/email shows a friendly
|
|
130
|
+
label), and **`hiddenInEmail`** (omit this parameter from action emails).
|
|
131
|
+
|
|
132
|
+
### Execution & UX
|
|
133
|
+
- **`executionMode`** — `custom` | `server` | `custom-with-ai` (§3).
|
|
134
|
+
- **`loadingMessage`** — shown while the tool runs; configure for `server` and `custom-with-ai`.
|
|
135
|
+
- **`availableIn`** — modality (§4).
|
|
136
|
+
- **`riskLevel`** (`low` | `medium` | `high`) and **`requiresConfirmation`** — gate sensitive
|
|
137
|
+
actions behind a confirmation step.
|
|
138
|
+
- **`enabled`** and **`status`** (`DRAFT` / `PUBLISHED`) — a tool must be published & enabled
|
|
139
|
+
to be used.
|
|
140
|
+
|
|
141
|
+
### Calling an endpoint (for `server` / `custom` API tools)
|
|
142
|
+
- **Endpoint config:** `url`, `method` (`GET`/`POST`/`PUT`/`DELETE`/`PATCH`), optional
|
|
143
|
+
`bodyTemplate`, optional `headers`, and `requiredContext` (context keys that must be present).
|
|
144
|
+
- **Token substitution:** in endpoint/webhook/header/description values you can use
|
|
145
|
+
`{{user.*}}`, `{{system.*}}`, and `{{tool.trackingId}}`; they resolve at runtime.
|
|
146
|
+
|
|
147
|
+
### Side effects — actions (email / webhook)
|
|
148
|
+
A tool can run actions when it completes (`trigger` is always *on complete*):
|
|
149
|
+
- **Email:** the recipient `to` can be a **single address or multiple targets**, and each
|
|
150
|
+
target may carry a natural-language **condition** so the AI decides whether to send to it.
|
|
151
|
+
The action itself can also have an overall **condition**. Options include `subjectPrefix`,
|
|
152
|
+
`includeConversation`, `attachFile`, and `includeUserContextKeys` (opt-in: which
|
|
153
|
+
`userContext`/`context` keys to include — nothing about the user is sent unless listed).
|
|
154
|
+
- **Webhook:** `url`, `method` (`POST`/`PUT`), custom `headers` (with token substitution),
|
|
155
|
+
`include*` toggles (extraction, file, conversation, selected context keys), file delivery
|
|
156
|
+
format (`url` | `base64` | `formdata`), and an optional `secret` for an HMAC signature.
|
|
157
|
+
|
|
158
|
+
### Result UI
|
|
159
|
+
- **`uiConfig`** — how structured results render: `responseType` of `list` | `detail` | `card`
|
|
160
|
+
| `table` | `summary`, plus `dataPath`, `maxItems`, layout/position, and `itemComponent` (the
|
|
161
|
+
name of a custom component you register in code — see §6). `list`/`detail`/`card`/`table`
|
|
162
|
+
have built-in default renderers; `summary` has **no** built-in component in this SDK version,
|
|
163
|
+
so only pick it if you supply your own component via `itemComponent` + the `components` prop.
|
|
164
|
+
- **Card template** — for rich cards, the dashboard has an **AI-assisted card designer** (you
|
|
165
|
+
describe the card, it generates a template); the tool stores a `cardTemplateId`. The SDK
|
|
166
|
+
renders the card automatically; card buttons (CTAs) are routed to the `onCard*` props (§6).
|
|
167
|
+
- **`trackingIdFormat`** — optional human-readable tracking id format for the tool run.
|
|
168
|
+
|
|
169
|
+
### File-upload tools
|
|
170
|
+
- **`fileInput`** — accepted types, max size, allowed sources.
|
|
171
|
+
- **`documentExtractor`** — an extraction prompt + the fields to pull out + a model. After the
|
|
172
|
+
user uploads a file, the result arrives via `onExtractionResult` in code (§6).
|
|
173
|
+
|
|
174
|
+
### Response channel (built-ins, automatic)
|
|
175
|
+
- **`responseChannel`** toggle — "send response as a tool action". Turning this on
|
|
176
|
+
**auto-creates** the built-in `qafka_response_status` / `qafka_add_followup` tools. **Never
|
|
177
|
+
tell the developer to create those by hand** — they're server-managed and shown locked in the
|
|
178
|
+
dashboard.
|
|
179
|
+
|
|
180
|
+
### Versioning (optional, advanced)
|
|
181
|
+
A tool can have multiple **versions**, each with its own `minClientVersion` and `status`
|
|
182
|
+
(`draft` / `staging` / `published`). At chat time the highest version compatible with the app's
|
|
183
|
+
version and the request modality is resolved. Most tools don't need this; mention it only if
|
|
184
|
+
the developer asks about rolling out a change without breaking older app builds.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. Code side (what you implement on `<Qafka />`)
|
|
189
|
+
|
|
190
|
+
The SDK exposes two render components — **`QafkaProvider`** (a floating launcher by default,
|
|
191
|
+
or `inline`/`fullscreen`) and **`Qafka`** (the chat/voice surface itself). The Qafka CLI
|
|
192
|
+
scaffolds one of these into a screen; you add tool-related props to that element.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { Qafka } from '@qafka/react-native';
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Always-required props
|
|
199
|
+
- **`projectId`** *(required)* — which Qafka project this app talks to.
|
|
200
|
+
- **`endUserId`** *(required, `string | number`)* — stable id for the end user (≤256 chars; not
|
|
201
|
+
sent to the LLM).
|
|
202
|
+
|
|
203
|
+
### Running tools in your app — `onToolSuggested`
|
|
204
|
+
Implement this for **`custom`** and **`custom-with-ai`** tools (not needed for `server`):
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
<Qafka
|
|
208
|
+
projectId="…"
|
|
209
|
+
endUserId={user.id}
|
|
210
|
+
onToolSuggested={async (tools, addResponse) => {
|
|
211
|
+
for (const tool of tools) {
|
|
212
|
+
if (tool.name === 'go_to_orders') {
|
|
213
|
+
navigation.navigate('Orders');
|
|
214
|
+
// addResponse(data, tool, mode?) mode: 'ui' | 'data' | 'both' (default 'both')
|
|
215
|
+
addResponse({ navigated: true }, tool, 'data');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`addResponse(data, tool, mode?)` reports the result back:
|
|
223
|
+
- `'ui'` — update the rendered UI only (keeps the flow open),
|
|
224
|
+
- `'data'` — commit the result data without touching the UI,
|
|
225
|
+
- `'both'` *(default)* — both.
|
|
226
|
+
|
|
227
|
+
For `custom-with-ai` tools, the data you pass is forwarded so the AI can narrate a follow-up.
|
|
228
|
+
|
|
229
|
+
### Custom result components — the `components` prop
|
|
230
|
+
When a tool returns structured data with a `uiConfig`, the SDK renders it with built-in
|
|
231
|
+
defaults (`DefaultCard`, `DefaultList`, `DefaultDetail`, `DefaultTable`). Override them by
|
|
232
|
+
registering your own by name:
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
<Qafka
|
|
236
|
+
components={{ OrderCard: MyOrderCard }} // matches uiConfig.itemComponent = "OrderCard"
|
|
237
|
+
/>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Each component receives `{ data, tool?, theme?, onAction? }`.
|
|
241
|
+
|
|
242
|
+
### Transient / lazy data — `onToolDataRequested`
|
|
243
|
+
When a tool needs data the AI shouldn't see or that you don't want persisted, supply it
|
|
244
|
+
on-demand. The SDK calls this just before the tool runs and uses the returned bag for that run
|
|
245
|
+
only (it is not written to the conversation or sent to the LLM):
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
<Qafka
|
|
249
|
+
onToolDataRequested={(tool) => { // tool: { key, name }
|
|
250
|
+
if (tool.key === 'current_cart') return { items: cart.items };
|
|
251
|
+
return {};
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### File-upload tools — `onFileUploadRequest` + `onExtractionResult`
|
|
257
|
+
There is **no built-in file picker** in this SDK version. For a tool with `fileInput`, you wire
|
|
258
|
+
your own picker:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
<Qafka
|
|
262
|
+
onFileUploadRequest={({ toolId, fileInput, submit, cancel }) => {
|
|
263
|
+
// open your picker honoring fileInput.accept / maxSize / sources,
|
|
264
|
+
// then call submit({ uri, name, type }) or cancel()
|
|
265
|
+
}}
|
|
266
|
+
onExtractionResult={(result) => { /* { fileId, toolId, data, status, incompleteFields? } */ }}
|
|
267
|
+
/>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Card CTAs
|
|
271
|
+
If a tool uses a card template, the card renders automatically. Its buttons are delivered to
|
|
272
|
+
these props (wire the ones your cards use): `onCardDeepLink`, `onCardSuggestMessage`,
|
|
273
|
+
`onCardExternalNavigation`, `onCardShare`, `onCardCopy`, `onCardToolTrigger`, `onCardCTAClick`.
|
|
274
|
+
|
|
275
|
+
### Inline key/value display — `qafka_display` (built-in)
|
|
276
|
+
`qafka_display` is a **built-in tool**: there is nothing to create in the dashboard and nothing
|
|
277
|
+
to handle in code. It shows a small set of key/value chips (or routes text to the transcript).
|
|
278
|
+
You may optionally restyle the chips via `voiceComponents.dataChipList` / `voiceComponents.dataChip`.
|
|
279
|
+
|
|
280
|
+
### Useful context props
|
|
281
|
+
- **`context`** + **`contextDescription`** — a runtime data bag passed to the AI, with a
|
|
282
|
+
description of what its keys mean. (Tool parameters with `source: 'context'` read from here.)
|
|
283
|
+
- **`endUserData`** — non-LLM metadata about the user (≤8KB).
|
|
284
|
+
- **`subProjectId`** — multi-tenant routing.
|
|
285
|
+
- **`locale`** — BCP-47 locale.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 7. Recipes (common asks → config + code)
|
|
290
|
+
|
|
291
|
+
**A. "Call our backend and let the AI answer."**
|
|
292
|
+
Dashboard: execution mode `server`, fill endpoint config, set a loading message, `availableIn`
|
|
293
|
+
both. Code: nothing — `server` tools don't fire `onToolSuggested`.
|
|
294
|
+
|
|
295
|
+
**B. "Do something in the app (navigate / read local state) and show the result."**
|
|
296
|
+
Dashboard: execution mode `custom` (or `custom-with-ai` if you want the AI to narrate). Add
|
|
297
|
+
parameters. Optionally set `uiConfig`/card for the result. Code: implement `onToolSuggested`;
|
|
298
|
+
register a component via `components` if you used `uiConfig.itemComponent`.
|
|
299
|
+
|
|
300
|
+
**C. "Collect some info and email it to the team."**
|
|
301
|
+
Dashboard: add an **email action**; use multiple targets with conditions if routing depends on
|
|
302
|
+
the input; opt into the context keys you want via `includeUserContextKeys`. Code: usually none
|
|
303
|
+
(the action runs on completion); add a handler only if the tool also does on-device work.
|
|
304
|
+
|
|
305
|
+
**D. "User uploads a document; extract fields from it."**
|
|
306
|
+
Dashboard: configure `fileInput` + `documentExtractor` on the tool. Code: implement
|
|
307
|
+
`onFileUploadRequest` (your picker → `submit(...)`) and read `onExtractionResult`.
|
|
308
|
+
|
|
309
|
+
**E. "Show a couple of key values inline."**
|
|
310
|
+
Use the built-in `qafka_display` — nothing in the dashboard, nothing in code.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 8. Guardrails (read before you answer)
|
|
315
|
+
|
|
316
|
+
- **Don't invent capabilities.** If it isn't in this guide, it isn't in this SDK version.
|
|
317
|
+
Don't tell the developer to toggle a field or pass a prop that doesn't exist here.
|
|
318
|
+
- **You can't touch the dashboard.** Always give explicit dashboard steps for the developer and
|
|
319
|
+
implement only the code side yourself.
|
|
320
|
+
- **`description` carries behavior, `whenToUse` is just a short selector hint.** Don't stuff
|
|
321
|
+
protocol detail into `whenToUse`.
|
|
322
|
+
- **Built-in tools aren't created by hand** — `qafka_display`, and the `responseChannel`
|
|
323
|
+
built-ins (`qafka_response_status` / `qafka_add_followup`), appear automatically.
|
|
324
|
+
- **`server` mode means no client handler.** Only `custom` / `custom-with-ai` use
|
|
325
|
+
`onToolSuggested`.
|
|
326
|
+
- **This guide is version-matched** to the `@qafka/react-native` package it ships inside (see
|
|
327
|
+
its `package.json` `version`). If the developer is on a different version, the dashboard or
|
|
328
|
+
props may differ — defer to the dashboard's own field set when in doubt.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qafka/react-native",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.9",
|
|
4
4
|
"description": "Drop-in AI assistant for React Native: chat, voice, and tool execution with screen-aware navigation.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"react-native.config.js",
|
|
16
16
|
"qafka.config.js",
|
|
17
17
|
"qafka.config.d.ts",
|
|
18
|
+
"docs",
|
|
18
19
|
"README.md",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"CONTRIBUTING.md",
|