An Ultravox agent for outbound appointment reminders and rescheduling.
This example shows how to use the following:
- Inline instructions
- Specifically uses tool response messages to keep the agent focused
The application uses a state machine and provides a visualizer of the states and transitions.
- Install all dependencies
pnpm install
- Add your Ultravox API Key
- create a file called
.env.local
and add your key as follows
VITE_ULTRAVOX_API_KEY=<your_key_here>
- System Prompt → Defined in
src/config/systemPrompt.ts
- State Machine Definition → Defined in
src/config/states.ts
- Start the app with
pnpm dev
- Start a call by clicking on the "Start Call" button.
- Note: This is creating an outbound call so you need to answer the call before the agent will speak.
- You can provide additional instructions to the agent during the call by clicking the "Wrap Up Call" button or by entering a message in the text box that appears above the conversation transcript.
- The wrap up call button sends a hard coded instruction to the agent to wrap up the call.
- You can also click on the available actions for the various call states and see how those trigger messages to the agent.
Let's talk about how you can use the state machine approach to create your own Ultravox agent.
First, define your states and actions as TypeScript enums for type safety and better code organization:
// src/config/states.ts
export enum StateEnum {
INITIAL = 'initial',
IDENTITY_CHECKING = 'identity_checking',
// Add all your states here...
}
export enum ActionEnum {
START_CALL = 'start_call',
CONFIRM_IDENTITY = 'confirm_identity',
// Add all your actions here...
}
Define all the prompts and actions for each state:
// src/config/states.ts
export const states: Record<StateEnum, StateDefinition> = {
[StateEnum.INITIAL]: {
description: "Preparing to start the call",
template: ({ details }) => `
This template provides instructions for this state.
You can dynamically insert data like: ${details.client_name}
To start the call, select "${ActionEnum.START_CALL}".
`,
actions: {
[ActionEnum.START_CALL]: {
description: "Initiate the outbound call",
nextState: StateEnum.NEXT_STATE
}
}
},
// Define all other states...
};
Create proper TypeScript types for your state machine components:
// Type for template function
export type TemplateFunction = (data: {
details: AppointmentDetails;
callData: CallData;
previousState?: StateEnum | null;
}) => string;
// Type for state action
export type StateAction = {
description: string;
nextState: StateEnum;
condition?: (callData: CallData) => boolean;
updateData?: (callData: CallData) => Partial<CallData>;
};
// Generic state type
export type StateDefinition = {
description: string;
template: TemplateFunction;
actions: Partial<Record<ActionEnum, StateAction>>;
};
In your app, use the state machine:
// App.tsx or another entry point
import { getAvailableActions, initializeStateMachine, transition } from "./stateMachine/stateMachine";
import { StateEnum, ActionEnum } from "./config/states";
// Initialize the state machine
const initialState = initializeStateMachine();
setCallState(initialState);
setAvailableActions(getAvailableActions(StateEnum.INITIAL));
// Handle transitions
const performAction = (action: string) => {
if (availableActions.includes(action)) {
const newCallState = transition(
currentCallState.currentState,
action,
currentCallState.callData
);
setCallState(newCallState);
setAvailableActions(getAvailableActions(newCallState.currentState));
}
};