diff --git a/.gitignore b/.gitignore index 103d90e7..6148f1e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__ # dependencies node_modules +package-lock.json # IDEs and editors /.idea diff --git a/README.md b/README.md index 4849358f..cc26a12d 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,30 @@ The ElevenLabs Agents Widgets provide an easy way to embed AI agents into any we Learn how to embed the widget into your website [here](https://elevenlabs.io/docs/agents-platform/customization/widget). +#### Dynamic Variables in Widgets + +You can pass dynamic variables to your agent through HTML attributes: + +```html + +``` + +To handle missing required variables, use the `expected-dynamic-variables` and `missing-dynamic-variable-default` attributes: + +```html + +``` + +This automatically fills missing variables (like `previous_orders` and `support_history`) with `null`, preventing conversation failures when your agent's tools require variables you don't have. + ### Agents CLI The ElevenLabs Agents CLI allows you to manage your agents as code, with features like version control, templates, and multi-environment deployments. diff --git a/packages/client/README.md b/packages/client/README.md index 4bfc8fc5..12b8a24b 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -235,6 +235,60 @@ const conversation = await Conversation.startSession({ }); ``` +#### Dynamic Variables + +Dynamic variables allow you to pass runtime data to your agent's tools. If your agent has tools configured with dynamic variable references, you can provide these variables during conversation initiation. + +**Basic usage:** + +```ts +const conversation = await Conversation.startSession({ + agentId: "", + dynamicVariables: { + user_name: "John Doe", + account_type: "premium", + session_id: "abc123", + }, +}); +``` + +**Handling missing required variables:** + +If your agent's tools require dynamic variables that you don't always have values for, you can use `expectedDynamicVariables` and `missingDynamicVariableDefault` to automatically fill missing variables with default values: + +```ts +const conversation = await Conversation.startSession({ + agentId: "", + + // Variables you have available + dynamicVariables: { + user_name: "John Doe", + current_plan: "premium", + }, + + // All variables your agent's tools expect + expectedDynamicVariables: [ + "user_name", + "current_plan", + "previous_orders", // Will be auto-filled + "support_history", // Will be auto-filled + "preferences", // Will be auto-filled + ], + + // Default value for missing variables (defaults to null) + missingDynamicVariableDefault: null, +}); +``` + +This prevents conversation failures when tools require variables you don't have. Missing variables will be automatically filled with the specified default value. + +**Supported default values:** +- `null` (default) +- Empty string: `""` +- Custom string: `"N/A"` +- Number: `0` +- Boolean: `false` + #### User identification You can optionally pass a user ID to identify the user in the conversation. This can be your own customer identifier. This will be included in the conversation initiation data sent to the server: diff --git a/packages/client/src/utils/BaseConnection.ts b/packages/client/src/utils/BaseConnection.ts index 170bc7f8..d9441683 100644 --- a/packages/client/src/utils/BaseConnection.ts +++ b/packages/client/src/utils/BaseConnection.ts @@ -48,6 +48,43 @@ export type BaseSessionConfig = { }; customLlmExtraBody?: unknown; dynamicVariables?: Record; + /** + * List of dynamic variable names that your agent's tools expect. + * Any variables in this list that aren't provided in dynamicVariables will be + * automatically filled with the value specified in missingDynamicVariableDefault. + * + * This is useful when your agent has tools that require dynamic variables, + * but you don't always have values for all of them. + * + * @example + * { + * expectedDynamicVariables: ['clients', 'matters', 'conversation_history'], + * missingDynamicVariableDefault: null, + * dynamicVariables: { clients: 'client data' } + * // Will automatically add: matters: null, conversation_history: null + * } + */ + expectedDynamicVariables?: string[]; + /** + * Default value to use for missing dynamic variables when tools require them. + * This helps avoid errors when tools expect dynamic variables that aren't provided. + * Only used when expectedDynamicVariables is specified. + * + * @default null + * + * @example + * // Fill missing dynamic variables with null + * { missingDynamicVariableDefault: null } + * + * @example + * // Fill missing dynamic variables with empty string + * { missingDynamicVariableDefault: "" } + * + * @example + * // Fill missing dynamic variables with a custom value + * { missingDynamicVariableDefault: "N/A" } + */ + missingDynamicVariableDefault?: string | number | boolean | null; useWakeLock?: boolean; connectionDelay?: DelayConfig; textOnly?: boolean; diff --git a/packages/client/src/utils/overrides.ts b/packages/client/src/utils/overrides.ts index bf1bb188..dabe67df 100644 --- a/packages/client/src/utils/overrides.ts +++ b/packages/client/src/utils/overrides.ts @@ -31,8 +31,33 @@ export function constructOverrides( overridesEvent.custom_llm_extra_body = config.customLlmExtraBody; } - if (config.dynamicVariables) { - overridesEvent.dynamic_variables = config.dynamicVariables; + // Handle dynamic variables with auto-filling of missing required variables + if (config.dynamicVariables || config.expectedDynamicVariables) { + const providedVariables = config.dynamicVariables || {}; + + // If expectedDynamicVariables is specified, fill in missing ones with default value + if ( + config.expectedDynamicVariables && + config.expectedDynamicVariables.length > 0 + ) { + const defaultValue = config.missingDynamicVariableDefault ?? null; + const filledVariables: Record = + { ...providedVariables }; + + // Add missing variables with default value + for (const varName of config.expectedDynamicVariables) { + if (!(varName in filledVariables)) { + filledVariables[varName] = defaultValue; + } + } + + overridesEvent.dynamic_variables = filledVariables as Record< + string, + string | number | boolean + >; + } else { + overridesEvent.dynamic_variables = providedVariables; + } } if (config.userId) { diff --git a/packages/convai-widget-core/src/contexts/session-config.tsx b/packages/convai-widget-core/src/contexts/session-config.tsx index 9dbf911b..ecb0422b 100644 --- a/packages/convai-widget-core/src/contexts/session-config.tsx +++ b/packages/convai-widget-core/src/contexts/session-config.tsx @@ -59,6 +59,36 @@ export function SessionConfigProvider({ return undefined; }); + const expectedDynamicVariablesJSON = useAttribute("expected-dynamic-variables"); + const expectedDynamicVariables = useComputed(() => { + if (expectedDynamicVariablesJSON.value) { + try { + return JSON.parse(expectedDynamicVariablesJSON.value) as string[]; + } catch (e: any) { + console.error( + `[ConversationalAI] Cannot parse expected-dynamic-variables: ${e?.message}` + ); + } + } + + return undefined; + }); + + const missingDynamicVariableDefaultAttr = useAttribute("missing-dynamic-variable-default"); + const missingDynamicVariableDefault = useComputed(() => { + if (missingDynamicVariableDefaultAttr.value) { + try { + // Try to parse as JSON first (for null, numbers, booleans) + return JSON.parse(missingDynamicVariableDefaultAttr.value); + } catch { + // If JSON parse fails, treat as string + return missingDynamicVariableDefaultAttr.value; + } + } + + return null; + }); + const { webSocketUrl } = useServerLocation(); const agentId = useAttribute("agent-id"); const signedUrl = useAttribute("signed-url"); @@ -68,6 +98,8 @@ export function SessionConfigProvider({ const isWebRTC = useWebRTCEnabled.value; const baseConfig = { dynamicVariables: dynamicVariables.value, + expectedDynamicVariables: expectedDynamicVariables.value, + missingDynamicVariableDefault: missingDynamicVariableDefault.value, overrides: overrides.value, connectionDelay: { default: 300 }, textOnly: textOnly.value, diff --git a/packages/convai-widget-core/src/types/attributes.ts b/packages/convai-widget-core/src/types/attributes.ts index cc5bf04a..df2e2c3e 100644 --- a/packages/convai-widget-core/src/types/attributes.ts +++ b/packages/convai-widget-core/src/types/attributes.ts @@ -25,6 +25,8 @@ export const CustomAttributeList = [ "server-location", "language", "dynamic-variables", + "expected-dynamic-variables", + "missing-dynamic-variable-default", "show-avatar-when-collapsed", "override-prompt", "override-first-message", diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 9504b177..0205e055 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -102,6 +102,11 @@ Starts a new conversation session. - `config.agentId`: ElevenLabs agent ID, not needed if you provide a conversationToken. - `config.conversationToken`: Optional pre-generated token, used for private agents that require authentication via your ElevenLabs API key. - `config.userId`: You can optionally pass a user ID to identify the user in the conversation. This can be your own customer identifier. This will be included in the conversation initiation data sent to the server. +- `config.dynamicVariables`: Optional key-value pairs to pass runtime data to your agent's tools. +- `config.expectedDynamicVariables`: Optional array of variable names your agent expects. Any missing variables will be auto-filled. +- `config.missingDynamicVariableDefault`: Default value for missing variables (defaults to `null`). Can be `null`, string, number, or boolean. + +**Basic example:** ```typescript await conversation.startSession({ @@ -109,6 +114,45 @@ await conversation.startSession({ }); ``` +**With dynamic variables:** + +```typescript +await conversation.startSession({ + agentId: "your-agent-id", + dynamicVariables: { + user_name: "John Doe", + account_type: "premium", + }, +}); +``` + +**Auto-filling missing required variables:** + +If your agent's tools require dynamic variables you don't always have, use `expectedDynamicVariables` to prevent errors: + +```typescript +await conversation.startSession({ + agentId: "your-agent-id", + + // Variables you have + dynamicVariables: { + user_name: "John Doe", + current_plan: "premium", + }, + + // All variables your agent expects + expectedDynamicVariables: [ + "user_name", + "current_plan", + "previous_orders", // Will be auto-filled with null + "support_history", // Will be auto-filled with null + ], + + // Optional: customize the default value + missingDynamicVariableDefault: null, // Can also be "", "N/A", 0, false, etc. +}); +``` + #### `endSession(): Promise` Ends the current conversation session. diff --git a/packages/react-native/src/ElevenLabsProvider.tsx b/packages/react-native/src/ElevenLabsProvider.tsx index 9545ac01..b4b3fe8d 100644 --- a/packages/react-native/src/ElevenLabsProvider.tsx +++ b/packages/react-native/src/ElevenLabsProvider.tsx @@ -121,6 +121,8 @@ export const ElevenLabsProvider: React.FC = ({ children overrides, customLlmExtraBody, dynamicVariables, + expectedDynamicVariables, + missingDynamicVariableDefault, userId, } = useConversationSession(callbacksRef, setStatus, setConnect, setToken, setConversationId, tokenFetchUrl); @@ -210,6 +212,8 @@ export const ElevenLabsProvider: React.FC = ({ children overrides, customLlmExtraBody, dynamicVariables, + expectedDynamicVariables, + missingDynamicVariableDefault, userId, }); @@ -223,7 +227,7 @@ export const ElevenLabsProvider: React.FC = ({ children callbacksRef.current.onError?.(error as string); } } - }, [handleParticipantReady, overrides, customLlmExtraBody, dynamicVariables, userId, callbacksRef]); + }, [handleParticipantReady, overrides, customLlmExtraBody, dynamicVariables, expectedDynamicVariables, missingDynamicVariableDefault, userId, callbacksRef]); const conversation: Conversation = { startSession, diff --git a/packages/react-native/src/hooks/useConversationSession.ts b/packages/react-native/src/hooks/useConversationSession.ts index 9cc82a5d..31134736 100644 --- a/packages/react-native/src/hooks/useConversationSession.ts +++ b/packages/react-native/src/hooks/useConversationSession.ts @@ -25,6 +25,10 @@ export const useConversationSession = ( const [dynamicVariables, setDynamicVariables] = useState< ConversationConfig["dynamicVariables"] >({}); + const [expectedDynamicVariables, setExpectedDynamicVariables] = + useState(undefined); + const [missingDynamicVariableDefault, setMissingDynamicVariableDefault] = + useState(null); const [userId, setUserId] = useState(undefined); const startSession = useCallback( @@ -36,6 +40,10 @@ export const useConversationSession = ( setOverrides(config.overrides || {}); setCustomLlmExtraBody(config.customLlmExtraBody || null); setDynamicVariables(config.dynamicVariables || {}); + setExpectedDynamicVariables(config.expectedDynamicVariables); + setMissingDynamicVariableDefault( + config.missingDynamicVariableDefault ?? null + ); setUserId(config.userId); let conversationToken: string; @@ -91,6 +99,8 @@ export const useConversationSession = ( setOverrides({}); setCustomLlmExtraBody(null); setDynamicVariables({}); + setExpectedDynamicVariables(undefined); + setMissingDynamicVariableDefault(null); setUserId(undefined); setConversationId(""); @@ -110,6 +120,8 @@ export const useConversationSession = ( overrides, customLlmExtraBody, dynamicVariables, + expectedDynamicVariables, + missingDynamicVariableDefault, userId, }; }; diff --git a/packages/react-native/src/types.ts b/packages/react-native/src/types.ts index 410d3b0d..eca0ae4c 100644 --- a/packages/react-native/src/types.ts +++ b/packages/react-native/src/types.ts @@ -81,6 +81,43 @@ export type ConversationConfig = { }; customLlmExtraBody?: unknown; dynamicVariables?: Record; + /** + * List of dynamic variable names that your agent's tools expect. + * Any variables in this list that aren't provided in dynamicVariables will be + * automatically filled with the value specified in missingDynamicVariableDefault. + * + * This is useful when your agent has tools that require dynamic variables, + * but you don't always have values for all of them. + * + * @example + * { + * expectedDynamicVariables: ['clients', 'matters', 'conversation_history'], + * missingDynamicVariableDefault: null, + * dynamicVariables: { clients: 'client data' } + * // Will automatically add: matters: null, conversation_history: null + * } + */ + expectedDynamicVariables?: string[]; + /** + * Default value to use for missing dynamic variables when tools require them. + * This helps avoid errors when tools expect dynamic variables that aren't provided. + * Only used when expectedDynamicVariables is specified. + * + * @default null + * + * @example + * // Fill missing dynamic variables with null + * { missingDynamicVariableDefault: null } + * + * @example + * // Fill missing dynamic variables with empty string + * { missingDynamicVariableDefault: "" } + * + * @example + * // Fill missing dynamic variables with a custom value + * { missingDynamicVariableDefault: "N/A" } + */ + missingDynamicVariableDefault?: string | number | boolean | null; userId?: string; }; diff --git a/packages/react-native/src/utils/overrides.ts b/packages/react-native/src/utils/overrides.ts index c1adcce1..d23f0736 100644 --- a/packages/react-native/src/utils/overrides.ts +++ b/packages/react-native/src/utils/overrides.ts @@ -33,8 +33,33 @@ export function constructOverrides( overridesEvent.custom_llm_extra_body = config.customLlmExtraBody; } - if (config.dynamicVariables) { - overridesEvent.dynamic_variables = config.dynamicVariables; + // Handle dynamic variables with auto-filling of missing required variables + if (config.dynamicVariables || config.expectedDynamicVariables) { + const providedVariables = config.dynamicVariables || {}; + + // If expectedDynamicVariables is specified, fill in missing ones with default value + if ( + config.expectedDynamicVariables && + config.expectedDynamicVariables.length > 0 + ) { + const defaultValue = config.missingDynamicVariableDefault ?? null; + const filledVariables: Record = + { ...providedVariables }; + + // Add missing variables with default value + for (const varName of config.expectedDynamicVariables) { + if (!(varName in filledVariables)) { + filledVariables[varName] = defaultValue; + } + } + + overridesEvent.dynamic_variables = filledVariables as Record< + string, + string | number | boolean + >; + } else { + overridesEvent.dynamic_variables = providedVariables; + } } if (config.userId) { diff --git a/packages/react/README.md b/packages/react/README.md index 195cf4e2..d0c7829d 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -128,6 +128,60 @@ const conversation = useConversation({ }); ``` +#### Dynamic Variables + +Dynamic variables allow you to pass runtime data to your agent's tools. If your agent has tools configured with dynamic variable references, you can provide these variables during conversation initiation. + +**Basic usage:** + +```ts +const conversationId = await conversation.startSession({ + agentId: "", + dynamicVariables: { + user_name: "John Doe", + account_type: "premium", + session_id: "abc123", + }, +}); +``` + +**Handling missing required variables:** + +If your agent's tools require dynamic variables that you don't always have values for, you can use `expectedDynamicVariables` and `missingDynamicVariableDefault` to automatically fill missing variables with default values: + +```ts +const conversationId = await conversation.startSession({ + agentId: "", + + // Variables you have available + dynamicVariables: { + user_name: "John Doe", + current_plan: "premium", + }, + + // All variables your agent's tools expect + expectedDynamicVariables: [ + "user_name", + "current_plan", + "previous_orders", // Will be auto-filled + "support_history", // Will be auto-filled + "preferences", // Will be auto-filled + ], + + // Default value for missing variables (defaults to null) + missingDynamicVariableDefault: null, +}); +``` + +This prevents conversation failures when tools require variables you don't have. Missing variables will be automatically filled with the specified default value. + +**Supported default values:** +- `null` (default) +- Empty string: `""` +- Custom string: `"N/A"` +- Number: `0` +- Boolean: `false` + #### User identification You can optionally pass a user ID to identify the user in the conversation. This can be your own customer identifier. This will be included in the conversation initiation data sent to the server: