If you’re building a bot that’s primarily designed to be used with incoming or outgoing phone calls, you won’t have an RTVI front-end to handle things like function calls. To give you more control over what happens in those sessions, Daily Bots allows you to specify webhook URLs for handling LLM function calls.

To learn more about how function calling works in Daily Bots, see this tutorial page.

Function call webhooks work in two different ways. If you want the same result functionality you get from handleFunctionCall, you can return a JSON response from your webhook. If you want more control over the execution of your bot, you can provide RTVI actions in a streaming response.

Configuring function call webhooks

You can configure a function call webhook in your /bots/start request using the webhook_tools like this:

const payload = {
  bot_profile: "voice_2024_10",
  max_duration: 600,
  services,
  api_keys: {
    openai: process.env.OPENAI_API_KEY,
  },
  config: [...config],
  webhook_tools: {
    get_weather: {
      url: "http://127.0.0.1:8000/weather",
      streaming: true,
    },
    get_local_time: {
      url: "http://127.0.0.1:8000/localtime",
      method: "GET",
    },
  },
};

You can set the following properties in each webhook_tools object:

  • url: The URL of your webhook.
  • method: Defaults to POST, but you can set it to GET or other methods if you want.
  • custom_headers: An object with key-value pairs for any custom headers to add to a webhook request (such as authentication).
  • streaming: Determines whether the bot should expect a simple JSON response (false) or a streaming response with RTVI action data (true). Defaults to false. More on this below.

JSON responses

By default (with streaming set to false), function call webhooks act just like handleFunctionCall callbacks. When the LLM decides to call a function, the bot will make a request to your webhook URL with the function arguments from the LLM. Your webhook returns a JSON object, and that object will be used as the function call result data.

Here’s an example of what a non-streaming webhook handler looks like in FastAPI:

@app.get("/localtime")
async def population(req: FunctionCallRequest):
    tz = pytz.timezone("America/Los_Angeles")
    time = datetime.now(tz)
    return {"time": time}

Streaming responses

If you want more flexibility in how your function calls are handled, your webhook can stream server-sent events (SSE) containing RTVI messages (actions and config updates) back to the bot. Your SSE text stream should look like this:

event: action
data:{RTVI action data as JSON}

event: action
data:{RTVI action data as JSON}

event: update-config
data:{RTVI updateConfig() data as JSON}

event: close

Each event needs to begin with the event: property, followed by the type of message you’re sending, such as action, update-config, or close. Then on the next line, send data: followed by JSON data. Finally,end with two newline characters (\n\n) to create a blank line. The last message should be event: close to ensure that the bot closes the connection.

You can also get the JSON response behavior by sending an llm:function_result action, as shown below.

Here’s an example FastAPI streaming response. This code sends a config update to change the TTS language, followed by an llm:function_result action.

async def language_changer(function_name, tool_call_id, arguments):
lang = languages[arguments["language"]]
events = [
    {
        "update-config": {
            "config": [
                {
                    "service": "tts",
                    "options": [
                        {"name": "voice", "value": lang["default_voice"]},
                        {"name": "model", "value": lang["tts_model"]},
                        {"name": "language", "value": lang["value"]},
                    ],
                },
            ]
        }
    },
    {
        "action": {
            "service": "llm",
            "action": "function_result",
            "arguments": [
                {"name": "function_name", "value": function_name},
                {"name": "tool_call_id", "value": tool_call_id},
                {"name": "arguments", "value": arguments},
                {
                    "name": "result",
                    "value": {"language": arguments["language"]},
                },
            ],
        }
    },
]
for e in events:
    for k, v in e.items():
        yield f"event: {k}\ndata: {json.dumps(v)}\n\n"
yield "data:close\n\n"

@app.post("/language")
async def set_language(req: FunctionCallRequest):
    print("Language request received: req")
    return StreamingResponse(
        language_changer(req.function_name, req.tool_call_id, req.arguments),
        media_type="text/event-stream",
    )

Unfortunately, there isn’t a great way to get information back from your bot when you send these messages. For example, if you send an invalid object for an update-config message, the config change will just silently fail. You’ll need to look at your Daily Bots dashboard log for debugging.

You can use any of the documented RTVI actions in this way, but be careful if you’re modifying the bot context while also using function results!