@devicai/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +363 -0
- package/dist/cjs/api/client.js +118 -0
- package/dist/cjs/api/client.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +122 -0
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/ChatInput.js +128 -0
- package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +56 -0
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/ToolTimeline.js +26 -0
- package/dist/cjs/components/ChatDrawer/ToolTimeline.js.map +1 -0
- package/dist/cjs/hooks/useDevicChat.js +258 -0
- package/dist/cjs/hooks/useDevicChat.js.map +1 -0
- package/dist/cjs/hooks/useModelInterface.js +130 -0
- package/dist/cjs/hooks/useModelInterface.js.map +1 -0
- package/dist/cjs/hooks/usePolling.js +109 -0
- package/dist/cjs/hooks/usePolling.js.map +1 -0
- package/dist/cjs/index.js +36 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/provider/DevicContext.js +32 -0
- package/dist/cjs/provider/DevicContext.js.map +1 -0
- package/dist/cjs/provider/DevicProvider.js +43 -0
- package/dist/cjs/provider/DevicProvider.js.map +1 -0
- package/dist/cjs/styles.css +1 -0
- package/dist/cjs/utils/index.js +117 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/esm/api/client.d.ts +56 -0
- package/dist/esm/api/client.js +115 -0
- package/dist/esm/api/client.js.map +1 -0
- package/dist/esm/api/types.d.ts +184 -0
- package/dist/esm/components/ChatDrawer/ChatDrawer.d.ts +27 -0
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +120 -0
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -0
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +197 -0
- package/dist/esm/components/ChatDrawer/ChatInput.d.ts +5 -0
- package/dist/esm/components/ChatDrawer/ChatInput.js +126 -0
- package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +5 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.js +54 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -0
- package/dist/esm/components/ChatDrawer/ToolTimeline.d.ts +5 -0
- package/dist/esm/components/ChatDrawer/ToolTimeline.js +24 -0
- package/dist/esm/components/ChatDrawer/ToolTimeline.js.map +1 -0
- package/dist/esm/components/ChatDrawer/index.d.ts +5 -0
- package/dist/esm/hooks/index.d.ts +6 -0
- package/dist/esm/hooks/useDevicChat.d.ts +120 -0
- package/dist/esm/hooks/useDevicChat.js +256 -0
- package/dist/esm/hooks/useDevicChat.js.map +1 -0
- package/dist/esm/hooks/useModelInterface.d.ts +68 -0
- package/dist/esm/hooks/useModelInterface.js +128 -0
- package/dist/esm/hooks/useModelInterface.js.map +1 -0
- package/dist/esm/hooks/usePolling.d.ts +64 -0
- package/dist/esm/hooks/usePolling.js +107 -0
- package/dist/esm/hooks/usePolling.js.map +1 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/provider/DevicContext.d.ts +15 -0
- package/dist/esm/provider/DevicContext.js +28 -0
- package/dist/esm/provider/DevicContext.js.map +1 -0
- package/dist/esm/provider/DevicProvider.d.ts +17 -0
- package/dist/esm/provider/DevicProvider.js +41 -0
- package/dist/esm/provider/DevicProvider.js.map +1 -0
- package/dist/esm/provider/index.d.ts +3 -0
- package/dist/esm/provider/types.d.ts +58 -0
- package/dist/esm/styles.css +1 -0
- package/dist/esm/utils/index.d.ts +32 -0
- package/dist/esm/utils/index.js +109 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import 'react/jsx-runtime';
|
|
3
|
+
import { useOptionalDevicContext } from '../provider/DevicContext.js';
|
|
4
|
+
import { DevicApiClient } from '../api/client.js';
|
|
5
|
+
import { usePolling } from './usePolling.js';
|
|
6
|
+
import { useModelInterface } from './useModelInterface.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Main hook for managing chat with a Devic assistant
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const {
|
|
14
|
+
* messages,
|
|
15
|
+
* isLoading,
|
|
16
|
+
* sendMessage,
|
|
17
|
+
* } = useDevicChat({
|
|
18
|
+
* assistantId: 'my-assistant',
|
|
19
|
+
* modelInterfaceTools: [
|
|
20
|
+
* {
|
|
21
|
+
* toolName: 'get_user_location',
|
|
22
|
+
* schema: { ... },
|
|
23
|
+
* callback: async () => ({ lat: 40.7, lng: -74.0 })
|
|
24
|
+
* }
|
|
25
|
+
* ],
|
|
26
|
+
* onMessageReceived: (msg) => console.log('Received:', msg),
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function useDevicChat(options) {
|
|
31
|
+
const { assistantId, chatUid: initialChatUid, apiKey: propsApiKey, baseUrl: propsBaseUrl, tenantId, tenantMetadata, enabledTools, modelInterfaceTools = [], pollingInterval = 1000, onMessageSent, onMessageReceived, onToolCall, onError, onChatCreated, } = options;
|
|
32
|
+
// Get context (may be null if not wrapped in provider)
|
|
33
|
+
const context = useOptionalDevicContext();
|
|
34
|
+
// Resolve configuration
|
|
35
|
+
const apiKey = propsApiKey || context?.apiKey;
|
|
36
|
+
const baseUrl = propsBaseUrl || context?.baseUrl || 'https://api.devic.ai';
|
|
37
|
+
const resolvedTenantId = tenantId || context?.tenantId;
|
|
38
|
+
const resolvedTenantMetadata = { ...context?.tenantMetadata, ...tenantMetadata };
|
|
39
|
+
// State
|
|
40
|
+
const [messages, setMessages] = useState([]);
|
|
41
|
+
const [chatUid, setChatUid] = useState(initialChatUid || null);
|
|
42
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
43
|
+
const [status, setStatus] = useState('idle');
|
|
44
|
+
const [error, setError] = useState(null);
|
|
45
|
+
// Polling state
|
|
46
|
+
const [shouldPoll, setShouldPoll] = useState(false);
|
|
47
|
+
// Refs for callbacks
|
|
48
|
+
const onMessageReceivedRef = useRef(onMessageReceived);
|
|
49
|
+
const onErrorRef = useRef(onError);
|
|
50
|
+
const onChatCreatedRef = useRef(onChatCreated);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onMessageReceivedRef.current = onMessageReceived;
|
|
53
|
+
onErrorRef.current = onError;
|
|
54
|
+
onChatCreatedRef.current = onChatCreated;
|
|
55
|
+
});
|
|
56
|
+
// Create API client
|
|
57
|
+
const clientRef = useRef(null);
|
|
58
|
+
if (!clientRef.current && apiKey) {
|
|
59
|
+
clientRef.current = new DevicApiClient({ apiKey, baseUrl });
|
|
60
|
+
}
|
|
61
|
+
// Update client config if it changes
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (clientRef.current && apiKey) {
|
|
64
|
+
clientRef.current.setConfig({ apiKey, baseUrl });
|
|
65
|
+
}
|
|
66
|
+
}, [apiKey, baseUrl]);
|
|
67
|
+
// Model interface hook
|
|
68
|
+
const { toolSchemas, handleToolCalls, extractPendingToolCalls, } = useModelInterface({
|
|
69
|
+
tools: modelInterfaceTools,
|
|
70
|
+
onToolExecute: onToolCall,
|
|
71
|
+
});
|
|
72
|
+
// Polling hook
|
|
73
|
+
usePolling(shouldPoll ? chatUid : null, async () => {
|
|
74
|
+
if (!clientRef.current || !chatUid) {
|
|
75
|
+
throw new Error('Cannot poll without client or chatUid');
|
|
76
|
+
}
|
|
77
|
+
return clientRef.current.getRealtimeHistory(assistantId, chatUid);
|
|
78
|
+
}, {
|
|
79
|
+
interval: pollingInterval,
|
|
80
|
+
enabled: shouldPoll,
|
|
81
|
+
stopStatuses: ['completed', 'error', 'waiting_for_tool_response'],
|
|
82
|
+
onUpdate: async (data) => {
|
|
83
|
+
setMessages(data.chatHistory);
|
|
84
|
+
setStatus(data.status);
|
|
85
|
+
// Notify about new messages
|
|
86
|
+
const lastMessage = data.chatHistory[data.chatHistory.length - 1];
|
|
87
|
+
if (lastMessage && lastMessage.role === 'assistant') {
|
|
88
|
+
onMessageReceivedRef.current?.(lastMessage);
|
|
89
|
+
}
|
|
90
|
+
// Handle model interface - check for pending tool calls
|
|
91
|
+
if (data.status === 'waiting_for_tool_response' || data.pendingToolCalls?.length) {
|
|
92
|
+
await handlePendingToolCalls(data);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onStop: async (data) => {
|
|
96
|
+
setIsLoading(false);
|
|
97
|
+
setShouldPoll(false);
|
|
98
|
+
if (data?.status === 'error') {
|
|
99
|
+
const err = new Error('Chat processing failed');
|
|
100
|
+
setError(err);
|
|
101
|
+
onErrorRef.current?.(err);
|
|
102
|
+
}
|
|
103
|
+
else if (data?.status === 'waiting_for_tool_response') {
|
|
104
|
+
// Handle tool response
|
|
105
|
+
await handlePendingToolCalls(data);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
onError: (err) => {
|
|
109
|
+
setError(err);
|
|
110
|
+
setIsLoading(false);
|
|
111
|
+
setShouldPoll(false);
|
|
112
|
+
onErrorRef.current?.(err);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
// Handle pending tool calls from model interface
|
|
116
|
+
const handlePendingToolCalls = useCallback(async (data) => {
|
|
117
|
+
if (!clientRef.current || !chatUid)
|
|
118
|
+
return;
|
|
119
|
+
// Get pending tool calls
|
|
120
|
+
const pendingCalls = data.pendingToolCalls || extractPendingToolCalls(data.chatHistory);
|
|
121
|
+
if (pendingCalls.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
// Execute client-side tools
|
|
125
|
+
const responses = await handleToolCalls(pendingCalls);
|
|
126
|
+
if (responses.length > 0) {
|
|
127
|
+
// Send tool responses back to the API
|
|
128
|
+
await clientRef.current.sendToolResponses(assistantId, chatUid, responses);
|
|
129
|
+
// Resume polling
|
|
130
|
+
setShouldPoll(true);
|
|
131
|
+
setIsLoading(true);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
136
|
+
setError(error);
|
|
137
|
+
onErrorRef.current?.(error);
|
|
138
|
+
}
|
|
139
|
+
}, [chatUid, assistantId, handleToolCalls, extractPendingToolCalls]);
|
|
140
|
+
// Send a message
|
|
141
|
+
const sendMessage = useCallback(async (message, sendOptions) => {
|
|
142
|
+
if (!clientRef.current) {
|
|
143
|
+
const err = new Error('API client not configured. Please provide an API key.');
|
|
144
|
+
setError(err);
|
|
145
|
+
onErrorRef.current?.(err);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setIsLoading(true);
|
|
149
|
+
setError(null);
|
|
150
|
+
setStatus('processing');
|
|
151
|
+
// Add user message optimistically
|
|
152
|
+
const userMessage = {
|
|
153
|
+
uid: `temp-${Date.now()}`,
|
|
154
|
+
role: 'user',
|
|
155
|
+
content: {
|
|
156
|
+
message,
|
|
157
|
+
files: sendOptions?.files?.map((f) => ({
|
|
158
|
+
name: f.name,
|
|
159
|
+
url: f.downloadUrl || '',
|
|
160
|
+
type: f.fileType || 'other',
|
|
161
|
+
})),
|
|
162
|
+
},
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
166
|
+
onMessageSent?.(userMessage);
|
|
167
|
+
try {
|
|
168
|
+
// Build request DTO
|
|
169
|
+
const dto = {
|
|
170
|
+
message,
|
|
171
|
+
chatUid: chatUid || undefined,
|
|
172
|
+
files: sendOptions?.files,
|
|
173
|
+
metadata: {
|
|
174
|
+
...resolvedTenantMetadata,
|
|
175
|
+
...sendOptions?.metadata,
|
|
176
|
+
},
|
|
177
|
+
tenantId: resolvedTenantId,
|
|
178
|
+
enabledTools,
|
|
179
|
+
// Include model interface tools if any
|
|
180
|
+
...(toolSchemas.length > 0 && { tools: toolSchemas }),
|
|
181
|
+
};
|
|
182
|
+
// Send message in async mode
|
|
183
|
+
const response = await clientRef.current.sendMessageAsync(assistantId, dto);
|
|
184
|
+
// Update chat UID if this is a new chat
|
|
185
|
+
if (response.chatUid && response.chatUid !== chatUid) {
|
|
186
|
+
setChatUid(response.chatUid);
|
|
187
|
+
onChatCreatedRef.current?.(response.chatUid);
|
|
188
|
+
}
|
|
189
|
+
// Start polling for results
|
|
190
|
+
setShouldPoll(true);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
194
|
+
setError(error);
|
|
195
|
+
setIsLoading(false);
|
|
196
|
+
setStatus('error');
|
|
197
|
+
onErrorRef.current?.(error);
|
|
198
|
+
// Remove optimistic user message on error
|
|
199
|
+
setMessages((prev) => prev.filter((m) => m.uid !== userMessage.uid));
|
|
200
|
+
}
|
|
201
|
+
}, [
|
|
202
|
+
chatUid,
|
|
203
|
+
assistantId,
|
|
204
|
+
enabledTools,
|
|
205
|
+
resolvedTenantId,
|
|
206
|
+
resolvedTenantMetadata,
|
|
207
|
+
toolSchemas,
|
|
208
|
+
onMessageSent,
|
|
209
|
+
]);
|
|
210
|
+
// Clear chat
|
|
211
|
+
const clearChat = useCallback(() => {
|
|
212
|
+
setMessages([]);
|
|
213
|
+
setChatUid(null);
|
|
214
|
+
setStatus('idle');
|
|
215
|
+
setError(null);
|
|
216
|
+
setShouldPoll(false);
|
|
217
|
+
}, []);
|
|
218
|
+
// Load existing chat
|
|
219
|
+
const loadChat = useCallback(async (loadChatUid) => {
|
|
220
|
+
if (!clientRef.current) {
|
|
221
|
+
const err = new Error('API client not configured');
|
|
222
|
+
setError(err);
|
|
223
|
+
onErrorRef.current?.(err);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
setIsLoading(true);
|
|
227
|
+
setError(null);
|
|
228
|
+
try {
|
|
229
|
+
const history = await clientRef.current.getChatHistory(assistantId, loadChatUid);
|
|
230
|
+
setMessages(history.chatContent);
|
|
231
|
+
setChatUid(loadChatUid);
|
|
232
|
+
setStatus('completed');
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
236
|
+
setError(error);
|
|
237
|
+
onErrorRef.current?.(error);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
setIsLoading(false);
|
|
241
|
+
}
|
|
242
|
+
}, [assistantId]);
|
|
243
|
+
return {
|
|
244
|
+
messages,
|
|
245
|
+
chatUid,
|
|
246
|
+
isLoading,
|
|
247
|
+
status,
|
|
248
|
+
error,
|
|
249
|
+
sendMessage,
|
|
250
|
+
clearChat,
|
|
251
|
+
loadChat,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export { useDevicChat };
|
|
256
|
+
//# sourceMappingURL=useDevicChat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDevicChat.js","sources":["../../../src/hooks/useDevicChat.ts"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { useOptionalDevicContext } from '../provider';\nimport { DevicApiClient } from '../api/client';\nimport { usePolling } from './usePolling';\nimport { useModelInterface } from './useModelInterface';\nimport type {\n ChatMessage,\n ChatFile,\n ModelInterfaceTool,\n RealtimeChatHistory,\n RealtimeStatus,\n} from '../api/types';\n\nexport interface UseDevicChatOptions {\n /**\n * Assistant identifier\n */\n assistantId: string;\n\n /**\n * Existing chat UID to continue a conversation\n */\n chatUid?: string;\n\n /**\n * API key (overrides provider context)\n */\n apiKey?: string;\n\n /**\n * Base URL (overrides provider context)\n */\n baseUrl?: string;\n\n /**\n * Tenant ID for multi-tenant environments\n */\n tenantId?: string;\n\n /**\n * Tenant metadata\n */\n tenantMetadata?: Record<string, any>;\n\n /**\n * Tools enabled from the assistant's configured tool groups\n */\n enabledTools?: string[];\n\n /**\n * Client-side tools for model interface protocol\n */\n modelInterfaceTools?: ModelInterfaceTool[];\n\n /**\n * Polling interval for async mode (ms)\n * @default 1000\n */\n pollingInterval?: number;\n\n /**\n * Callback when a message is sent\n */\n onMessageSent?: (message: ChatMessage) => void;\n\n /**\n * Callback when a message is received\n */\n onMessageReceived?: (message: ChatMessage) => void;\n\n /**\n * Callback when a tool is called\n */\n onToolCall?: (toolName: string, params: any) => void;\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void;\n\n /**\n * Callback when a new chat is created\n */\n onChatCreated?: (chatUid: string) => void;\n}\n\nexport interface UseDevicChatResult {\n /**\n * Current chat messages\n */\n messages: ChatMessage[];\n\n /**\n * Current chat UID\n */\n chatUid: string | null;\n\n /**\n * Whether a message is being processed\n */\n isLoading: boolean;\n\n /**\n * Current status\n */\n status: RealtimeStatus | 'idle';\n\n /**\n * Last error\n */\n error: Error | null;\n\n /**\n * Send a message\n */\n sendMessage: (\n message: string,\n options?: {\n files?: ChatFile[];\n metadata?: Record<string, any>;\n }\n ) => Promise<void>;\n\n /**\n * Clear the chat and start a new conversation\n */\n clearChat: () => void;\n\n /**\n * Load an existing chat\n */\n loadChat: (chatUid: string) => Promise<void>;\n}\n\n/**\n * Main hook for managing chat with a Devic assistant\n *\n * @example\n * ```tsx\n * const {\n * messages,\n * isLoading,\n * sendMessage,\n * } = useDevicChat({\n * assistantId: 'my-assistant',\n * modelInterfaceTools: [\n * {\n * toolName: 'get_user_location',\n * schema: { ... },\n * callback: async () => ({ lat: 40.7, lng: -74.0 })\n * }\n * ],\n * onMessageReceived: (msg) => console.log('Received:', msg),\n * });\n * ```\n */\nexport function useDevicChat(options: UseDevicChatOptions): UseDevicChatResult {\n const {\n assistantId,\n chatUid: initialChatUid,\n apiKey: propsApiKey,\n baseUrl: propsBaseUrl,\n tenantId,\n tenantMetadata,\n enabledTools,\n modelInterfaceTools = [],\n pollingInterval = 1000,\n onMessageSent,\n onMessageReceived,\n onToolCall,\n onError,\n onChatCreated,\n } = options;\n\n // Get context (may be null if not wrapped in provider)\n const context = useOptionalDevicContext();\n\n // Resolve configuration\n const apiKey = propsApiKey || context?.apiKey;\n const baseUrl = propsBaseUrl || context?.baseUrl || 'https://api.devic.ai';\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedTenantMetadata = { ...context?.tenantMetadata, ...tenantMetadata };\n\n // State\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [chatUid, setChatUid] = useState<string | null>(initialChatUid || null);\n const [isLoading, setIsLoading] = useState(false);\n const [status, setStatus] = useState<RealtimeStatus | 'idle'>('idle');\n const [error, setError] = useState<Error | null>(null);\n\n // Polling state\n const [shouldPoll, setShouldPoll] = useState(false);\n\n // Refs for callbacks\n const onMessageReceivedRef = useRef(onMessageReceived);\n const onErrorRef = useRef(onError);\n const onChatCreatedRef = useRef(onChatCreated);\n\n useEffect(() => {\n onMessageReceivedRef.current = onMessageReceived;\n onErrorRef.current = onError;\n onChatCreatedRef.current = onChatCreated;\n });\n\n // Create API client\n const clientRef = useRef<DevicApiClient | null>(null);\n if (!clientRef.current && apiKey) {\n clientRef.current = new DevicApiClient({ apiKey, baseUrl });\n }\n\n // Update client config if it changes\n useEffect(() => {\n if (clientRef.current && apiKey) {\n clientRef.current.setConfig({ apiKey, baseUrl });\n }\n }, [apiKey, baseUrl]);\n\n // Model interface hook\n const {\n toolSchemas,\n handleToolCalls,\n extractPendingToolCalls,\n } = useModelInterface({\n tools: modelInterfaceTools,\n onToolExecute: onToolCall,\n });\n\n // Polling hook\n const polling = usePolling(\n shouldPoll ? chatUid : null,\n async () => {\n if (!clientRef.current || !chatUid) {\n throw new Error('Cannot poll without client or chatUid');\n }\n return clientRef.current.getRealtimeHistory(assistantId, chatUid);\n },\n {\n interval: pollingInterval,\n enabled: shouldPoll,\n stopStatuses: ['completed', 'error', 'waiting_for_tool_response'],\n onUpdate: async (data: RealtimeChatHistory) => {\n setMessages(data.chatHistory);\n setStatus(data.status);\n\n // Notify about new messages\n const lastMessage = data.chatHistory[data.chatHistory.length - 1];\n if (lastMessage && lastMessage.role === 'assistant') {\n onMessageReceivedRef.current?.(lastMessage);\n }\n\n // Handle model interface - check for pending tool calls\n if (data.status === 'waiting_for_tool_response' || data.pendingToolCalls?.length) {\n await handlePendingToolCalls(data);\n }\n },\n onStop: async (data) => {\n setIsLoading(false);\n setShouldPoll(false);\n\n if (data?.status === 'error') {\n const err = new Error('Chat processing failed');\n setError(err);\n onErrorRef.current?.(err);\n } else if (data?.status === 'waiting_for_tool_response') {\n // Handle tool response\n await handlePendingToolCalls(data);\n }\n },\n onError: (err) => {\n setError(err);\n setIsLoading(false);\n setShouldPoll(false);\n onErrorRef.current?.(err);\n },\n }\n );\n\n // Handle pending tool calls from model interface\n const handlePendingToolCalls = useCallback(\n async (data: RealtimeChatHistory) => {\n if (!clientRef.current || !chatUid) return;\n\n // Get pending tool calls\n const pendingCalls = data.pendingToolCalls || extractPendingToolCalls(data.chatHistory);\n\n if (pendingCalls.length === 0) return;\n\n try {\n // Execute client-side tools\n const responses = await handleToolCalls(pendingCalls);\n\n if (responses.length > 0) {\n // Send tool responses back to the API\n await clientRef.current.sendToolResponses(assistantId, chatUid, responses);\n\n // Resume polling\n setShouldPoll(true);\n setIsLoading(true);\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n onErrorRef.current?.(error);\n }\n },\n [chatUid, assistantId, handleToolCalls, extractPendingToolCalls]\n );\n\n // Send a message\n const sendMessage = useCallback(\n async (\n message: string,\n sendOptions?: {\n files?: ChatFile[];\n metadata?: Record<string, any>;\n }\n ) => {\n if (!clientRef.current) {\n const err = new Error(\n 'API client not configured. Please provide an API key.'\n );\n setError(err);\n onErrorRef.current?.(err);\n return;\n }\n\n setIsLoading(true);\n setError(null);\n setStatus('processing');\n\n // Add user message optimistically\n const userMessage: ChatMessage = {\n uid: `temp-${Date.now()}`,\n role: 'user',\n content: {\n message,\n files: sendOptions?.files?.map((f) => ({\n name: f.name,\n url: f.downloadUrl || '',\n type: f.fileType || 'other',\n })),\n },\n timestamp: Date.now(),\n };\n\n setMessages((prev) => [...prev, userMessage]);\n onMessageSent?.(userMessage);\n\n try {\n // Build request DTO\n const dto = {\n message,\n chatUid: chatUid || undefined,\n files: sendOptions?.files,\n metadata: {\n ...resolvedTenantMetadata,\n ...sendOptions?.metadata,\n },\n tenantId: resolvedTenantId,\n enabledTools,\n // Include model interface tools if any\n ...(toolSchemas.length > 0 && { tools: toolSchemas }),\n };\n\n // Send message in async mode\n const response = await clientRef.current.sendMessageAsync(assistantId, dto);\n\n // Update chat UID if this is a new chat\n if (response.chatUid && response.chatUid !== chatUid) {\n setChatUid(response.chatUid);\n onChatCreatedRef.current?.(response.chatUid);\n }\n\n // Start polling for results\n setShouldPoll(true);\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n setIsLoading(false);\n setStatus('error');\n onErrorRef.current?.(error);\n\n // Remove optimistic user message on error\n setMessages((prev) => prev.filter((m) => m.uid !== userMessage.uid));\n }\n },\n [\n chatUid,\n assistantId,\n enabledTools,\n resolvedTenantId,\n resolvedTenantMetadata,\n toolSchemas,\n onMessageSent,\n ]\n );\n\n // Clear chat\n const clearChat = useCallback(() => {\n setMessages([]);\n setChatUid(null);\n setStatus('idle');\n setError(null);\n setShouldPoll(false);\n }, []);\n\n // Load existing chat\n const loadChat = useCallback(\n async (loadChatUid: string) => {\n if (!clientRef.current) {\n const err = new Error('API client not configured');\n setError(err);\n onErrorRef.current?.(err);\n return;\n }\n\n setIsLoading(true);\n setError(null);\n\n try {\n const history = await clientRef.current.getChatHistory(\n assistantId,\n loadChatUid\n );\n\n setMessages(history.chatContent);\n setChatUid(loadChatUid);\n setStatus('completed');\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n onErrorRef.current?.(error);\n } finally {\n setIsLoading(false);\n }\n },\n [assistantId]\n );\n\n return {\n messages,\n chatUid,\n isLoading,\n status,\n error,\n sendMessage,\n clearChat,\n loadChat,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ModelInterfaceTool, ModelInterfaceToolSchema, ToolCall, ToolCallResponse, ChatMessage } from '../api/types';
|
|
2
|
+
export interface UseModelInterfaceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Client-side tools available for model interface protocol
|
|
5
|
+
*/
|
|
6
|
+
tools: ModelInterfaceTool[];
|
|
7
|
+
/**
|
|
8
|
+
* Callback when a tool is being executed
|
|
9
|
+
*/
|
|
10
|
+
onToolExecute?: (toolName: string, params: any) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Callback when a tool execution completes
|
|
13
|
+
*/
|
|
14
|
+
onToolComplete?: (toolName: string, result: any) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Callback when a tool execution fails
|
|
17
|
+
*/
|
|
18
|
+
onToolError?: (toolName: string, error: Error) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface UseModelInterfaceResult {
|
|
21
|
+
/**
|
|
22
|
+
* Tool schemas to send to the API
|
|
23
|
+
*/
|
|
24
|
+
toolSchemas: ModelInterfaceToolSchema[];
|
|
25
|
+
/**
|
|
26
|
+
* Check if a tool call should be handled client-side
|
|
27
|
+
*/
|
|
28
|
+
isClientTool: (toolName: string) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Handle tool calls from the model, returns tool responses
|
|
31
|
+
*/
|
|
32
|
+
handleToolCalls: (toolCalls: ToolCall[]) => Promise<ToolCallResponse[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Process messages and extract pending tool calls that need client handling
|
|
35
|
+
*/
|
|
36
|
+
extractPendingToolCalls: (messages: ChatMessage[]) => ToolCall[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hook for implementing the Model Interface Protocol
|
|
40
|
+
*
|
|
41
|
+
* The Model Interface Protocol allows client-side tools to be executed
|
|
42
|
+
* during an assistant conversation. When the model calls a client-side tool,
|
|
43
|
+
* this hook handles executing the tool and preparing the response.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const { toolSchemas, handleToolCalls } = useModelInterface({
|
|
48
|
+
* tools: [
|
|
49
|
+
* {
|
|
50
|
+
* toolName: 'get_user_location',
|
|
51
|
+
* schema: {
|
|
52
|
+
* type: 'function',
|
|
53
|
+
* function: {
|
|
54
|
+
* name: 'get_user_location',
|
|
55
|
+
* description: 'Get user current location',
|
|
56
|
+
* parameters: { type: 'object', properties: {} }
|
|
57
|
+
* }
|
|
58
|
+
* },
|
|
59
|
+
* callback: async () => {
|
|
60
|
+
* const pos = await getCurrentPosition();
|
|
61
|
+
* return { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* ]
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function useModelInterface(options: UseModelInterfaceOptions): UseModelInterfaceResult;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useMemo, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for implementing the Model Interface Protocol
|
|
5
|
+
*
|
|
6
|
+
* The Model Interface Protocol allows client-side tools to be executed
|
|
7
|
+
* during an assistant conversation. When the model calls a client-side tool,
|
|
8
|
+
* this hook handles executing the tool and preparing the response.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { toolSchemas, handleToolCalls } = useModelInterface({
|
|
13
|
+
* tools: [
|
|
14
|
+
* {
|
|
15
|
+
* toolName: 'get_user_location',
|
|
16
|
+
* schema: {
|
|
17
|
+
* type: 'function',
|
|
18
|
+
* function: {
|
|
19
|
+
* name: 'get_user_location',
|
|
20
|
+
* description: 'Get user current location',
|
|
21
|
+
* parameters: { type: 'object', properties: {} }
|
|
22
|
+
* }
|
|
23
|
+
* },
|
|
24
|
+
* callback: async () => {
|
|
25
|
+
* const pos = await getCurrentPosition();
|
|
26
|
+
* return { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ]
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function useModelInterface(options) {
|
|
34
|
+
const { tools, onToolExecute, onToolComplete, onToolError } = options;
|
|
35
|
+
// Extract tool schemas for API
|
|
36
|
+
const toolSchemas = useMemo(() => {
|
|
37
|
+
return tools.map((tool) => tool.schema);
|
|
38
|
+
}, [tools]);
|
|
39
|
+
// Map of tool name to tool definition
|
|
40
|
+
const toolMap = useMemo(() => {
|
|
41
|
+
return new Map(tools.map((tool) => [tool.toolName, tool]));
|
|
42
|
+
}, [tools]);
|
|
43
|
+
// Check if a tool is a client-side tool
|
|
44
|
+
const isClientTool = useCallback((toolName) => {
|
|
45
|
+
return toolMap.has(toolName);
|
|
46
|
+
}, [toolMap]);
|
|
47
|
+
// Handle tool calls and execute client-side tools
|
|
48
|
+
const handleToolCalls = useCallback(async (toolCalls) => {
|
|
49
|
+
const responses = [];
|
|
50
|
+
for (const toolCall of toolCalls) {
|
|
51
|
+
const toolName = toolCall.function.name;
|
|
52
|
+
const tool = toolMap.get(toolName);
|
|
53
|
+
if (!tool) {
|
|
54
|
+
// Not a client-side tool, skip
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
// Parse arguments
|
|
59
|
+
let params = {};
|
|
60
|
+
try {
|
|
61
|
+
params = JSON.parse(toolCall.function.arguments || '{}');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Keep empty params if parsing fails
|
|
65
|
+
}
|
|
66
|
+
// Notify about execution
|
|
67
|
+
onToolExecute?.(toolName, params);
|
|
68
|
+
// Execute the tool callback
|
|
69
|
+
const result = await tool.callback(params);
|
|
70
|
+
// Notify about completion
|
|
71
|
+
onToolComplete?.(toolName, result);
|
|
72
|
+
// Add response
|
|
73
|
+
responses.push({
|
|
74
|
+
tool_call_id: toolCall.id,
|
|
75
|
+
content: result,
|
|
76
|
+
role: 'tool',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
// Notify about error
|
|
82
|
+
onToolError?.(toolName, error);
|
|
83
|
+
// Add error response
|
|
84
|
+
responses.push({
|
|
85
|
+
tool_call_id: toolCall.id,
|
|
86
|
+
content: { error: error.message },
|
|
87
|
+
role: 'tool',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return responses;
|
|
92
|
+
}, [toolMap, onToolExecute, onToolComplete, onToolError]);
|
|
93
|
+
// Extract pending tool calls from messages that need client handling
|
|
94
|
+
const extractPendingToolCalls = useCallback((messages) => {
|
|
95
|
+
const pendingCalls = [];
|
|
96
|
+
// Look at the last assistant message
|
|
97
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
98
|
+
const message = messages[i];
|
|
99
|
+
if (message.role === 'assistant' && message.tool_calls?.length) {
|
|
100
|
+
// Filter for client-side tools only
|
|
101
|
+
const clientToolCalls = message.tool_calls.filter((tc) => isClientTool(tc.function.name));
|
|
102
|
+
// Check if these tool calls have been responded to
|
|
103
|
+
const respondedToolIds = new Set(messages
|
|
104
|
+
.slice(i + 1)
|
|
105
|
+
.filter((m) => m.role === 'tool')
|
|
106
|
+
.map((m) => m.tool_call_id));
|
|
107
|
+
// Get unresponded tool calls
|
|
108
|
+
for (const tc of clientToolCalls) {
|
|
109
|
+
if (!respondedToolIds.has(tc.id)) {
|
|
110
|
+
pendingCalls.push(tc);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Only check the most recent assistant message with tool calls
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return pendingCalls;
|
|
118
|
+
}, [isClientTool]);
|
|
119
|
+
return {
|
|
120
|
+
toolSchemas,
|
|
121
|
+
isClientTool,
|
|
122
|
+
handleToolCalls,
|
|
123
|
+
extractPendingToolCalls,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { useModelInterface };
|
|
128
|
+
//# sourceMappingURL=useModelInterface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useModelInterface.js","sources":["../../../src/hooks/useModelInterface.ts"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport type {\n ModelInterfaceTool,\n ModelInterfaceToolSchema,\n ToolCall,\n ToolCallResponse,\n ChatMessage,\n} from '../api/types';\n\nexport interface UseModelInterfaceOptions {\n /**\n * Client-side tools available for model interface protocol\n */\n tools: ModelInterfaceTool[];\n\n /**\n * Callback when a tool is being executed\n */\n onToolExecute?: (toolName: string, params: any) => void;\n\n /**\n * Callback when a tool execution completes\n */\n onToolComplete?: (toolName: string, result: any) => void;\n\n /**\n * Callback when a tool execution fails\n */\n onToolError?: (toolName: string, error: Error) => void;\n}\n\nexport interface UseModelInterfaceResult {\n /**\n * Tool schemas to send to the API\n */\n toolSchemas: ModelInterfaceToolSchema[];\n\n /**\n * Check if a tool call should be handled client-side\n */\n isClientTool: (toolName: string) => boolean;\n\n /**\n * Handle tool calls from the model, returns tool responses\n */\n handleToolCalls: (toolCalls: ToolCall[]) => Promise<ToolCallResponse[]>;\n\n /**\n * Process messages and extract pending tool calls that need client handling\n */\n extractPendingToolCalls: (messages: ChatMessage[]) => ToolCall[];\n}\n\n/**\n * Hook for implementing the Model Interface Protocol\n *\n * The Model Interface Protocol allows client-side tools to be executed\n * during an assistant conversation. When the model calls a client-side tool,\n * this hook handles executing the tool and preparing the response.\n *\n * @example\n * ```tsx\n * const { toolSchemas, handleToolCalls } = useModelInterface({\n * tools: [\n * {\n * toolName: 'get_user_location',\n * schema: {\n * type: 'function',\n * function: {\n * name: 'get_user_location',\n * description: 'Get user current location',\n * parameters: { type: 'object', properties: {} }\n * }\n * },\n * callback: async () => {\n * const pos = await getCurrentPosition();\n * return { lat: pos.coords.latitude, lng: pos.coords.longitude };\n * }\n * }\n * ]\n * });\n * ```\n */\nexport function useModelInterface(\n options: UseModelInterfaceOptions\n): UseModelInterfaceResult {\n const { tools, onToolExecute, onToolComplete, onToolError } = options;\n\n // Extract tool schemas for API\n const toolSchemas = useMemo(() => {\n return tools.map((tool) => tool.schema);\n }, [tools]);\n\n // Map of tool name to tool definition\n const toolMap = useMemo(() => {\n return new Map(tools.map((tool) => [tool.toolName, tool]));\n }, [tools]);\n\n // Check if a tool is a client-side tool\n const isClientTool = useCallback(\n (toolName: string): boolean => {\n return toolMap.has(toolName);\n },\n [toolMap]\n );\n\n // Handle tool calls and execute client-side tools\n const handleToolCalls = useCallback(\n async (toolCalls: ToolCall[]): Promise<ToolCallResponse[]> => {\n const responses: ToolCallResponse[] = [];\n\n for (const toolCall of toolCalls) {\n const toolName = toolCall.function.name;\n const tool = toolMap.get(toolName);\n\n if (!tool) {\n // Not a client-side tool, skip\n continue;\n }\n\n try {\n // Parse arguments\n let params: any = {};\n try {\n params = JSON.parse(toolCall.function.arguments || '{}');\n } catch {\n // Keep empty params if parsing fails\n }\n\n // Notify about execution\n onToolExecute?.(toolName, params);\n\n // Execute the tool callback\n const result = await tool.callback(params);\n\n // Notify about completion\n onToolComplete?.(toolName, result);\n\n // Add response\n responses.push({\n tool_call_id: toolCall.id,\n content: result,\n role: 'tool',\n });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n\n // Notify about error\n onToolError?.(toolName, error);\n\n // Add error response\n responses.push({\n tool_call_id: toolCall.id,\n content: { error: error.message },\n role: 'tool',\n });\n }\n }\n\n return responses;\n },\n [toolMap, onToolExecute, onToolComplete, onToolError]\n );\n\n // Extract pending tool calls from messages that need client handling\n const extractPendingToolCalls = useCallback(\n (messages: ChatMessage[]): ToolCall[] => {\n const pendingCalls: ToolCall[] = [];\n\n // Look at the last assistant message\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages[i];\n\n if (message.role === 'assistant' && message.tool_calls?.length) {\n // Filter for client-side tools only\n const clientToolCalls = message.tool_calls.filter((tc) =>\n isClientTool(tc.function.name)\n );\n\n // Check if these tool calls have been responded to\n const respondedToolIds = new Set(\n messages\n .slice(i + 1)\n .filter((m) => m.role === 'tool')\n .map((m) => m.tool_call_id)\n );\n\n // Get unresponded tool calls\n for (const tc of clientToolCalls) {\n if (!respondedToolIds.has(tc.id)) {\n pendingCalls.push(tc);\n }\n }\n\n // Only check the most recent assistant message with tool calls\n break;\n }\n }\n\n return pendingCalls;\n },\n [isClientTool]\n );\n\n return {\n toolSchemas,\n isClientTool,\n handleToolCalls,\n extractPendingToolCalls,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RealtimeChatHistory, RealtimeStatus } from '../api/types';
|
|
2
|
+
export interface UsePollingOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Polling interval in milliseconds
|
|
5
|
+
* @default 1000
|
|
6
|
+
*/
|
|
7
|
+
interval?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Whether polling is enabled
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Statuses that should stop polling
|
|
15
|
+
* @default ['completed', 'error']
|
|
16
|
+
*/
|
|
17
|
+
stopStatuses?: RealtimeStatus[];
|
|
18
|
+
/**
|
|
19
|
+
* Callback when polling stops
|
|
20
|
+
*/
|
|
21
|
+
onStop?: (data: RealtimeChatHistory | null) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Callback on each poll update
|
|
24
|
+
*/
|
|
25
|
+
onUpdate?: (data: RealtimeChatHistory) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Callback on poll error
|
|
28
|
+
*/
|
|
29
|
+
onError?: (error: Error) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface UsePollingResult {
|
|
32
|
+
/**
|
|
33
|
+
* Current polling data
|
|
34
|
+
*/
|
|
35
|
+
data: RealtimeChatHistory | null;
|
|
36
|
+
/**
|
|
37
|
+
* Whether polling is currently active
|
|
38
|
+
*/
|
|
39
|
+
isPolling: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Last error that occurred
|
|
42
|
+
*/
|
|
43
|
+
error: Error | null;
|
|
44
|
+
/**
|
|
45
|
+
* Start polling
|
|
46
|
+
*/
|
|
47
|
+
start: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Stop polling
|
|
50
|
+
*/
|
|
51
|
+
stop: () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Manually trigger a fetch
|
|
54
|
+
*/
|
|
55
|
+
refetch: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Hook for polling real-time chat history
|
|
59
|
+
*
|
|
60
|
+
* @param chatUid - The chat UID to poll for
|
|
61
|
+
* @param fetchFn - Function that fetches the realtime history
|
|
62
|
+
* @param options - Polling options
|
|
63
|
+
*/
|
|
64
|
+
export declare function usePolling(chatUid: string | null, fetchFn: () => Promise<RealtimeChatHistory>, options?: UsePollingOptions): UsePollingResult;
|