diff --git a/recipes/javascript/voice-agents/v1/parallel-tool-calling/README.md b/recipes/javascript/voice-agents/v1/parallel-tool-calling/README.md new file mode 100644 index 00000000..b3234ccd --- /dev/null +++ b/recipes/javascript/voice-agents/v1/parallel-tool-calling/README.md @@ -0,0 +1,39 @@ +# Parallel Tool Calling (Voice Agents v1) + +Voice agents often need to gather data from multiple sources in a single turn — checking weather while looking up time zones, or querying a database and an external API simultaneously. Parallel tool calling lets the LLM request multiple functions at once, and your client executes them concurrently with `Promise.all`, reducing perceived latency compared to sequential calls. + +When the agent decides it needs multiple tools, Deepgram sends a single `FunctionCallRequest` with an array of functions. Your client runs them all in parallel and sends back individual `FunctionCallResponse` messages. The agent then composes all results into one natural response. + +## Key parameters + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `think.functions` | `[{name, description, parameters}]` | Array of tool definitions the LLM can invoke | +| `think.functions[].parameters` | JSON Schema object | Describes the expected arguments for each tool | +| `FunctionCallRequest.functions` | Array | Contains one or more function calls — multiple means parallel execution | +| `FunctionCallResponse.id` | String | Must match the `id` from the corresponding request | +| `FunctionCallResponse.content` | String | The tool's result, sent back to the agent | + +## Example output + +``` +Settings applied with 3 tools registered +User: What's the weather, time, and population in New York? +Parallel call: 3 function(s) requested + get_weather({"city":"New York"}) → New York: 72°F, sunny + get_time({"timezone":"America/New_York"}) → Current time in America/New_York: 2:30 PM + get_population({"city":"New York"}) → New York population: 8.3 million +Agent: In New York, it's currently 72°F and sunny. The time is 2:30 PM, and the city has a population of about 8.3 million. +``` + +## Prerequisites + +- Node.js 20+ +- Set `DEEPGRAM_API_KEY` environment variable +- Install dependencies: `npm install` (from `recipes/javascript/`) + +## Run + +```bash +node example.js +``` diff --git a/recipes/javascript/voice-agents/v1/parallel-tool-calling/example.js b/recipes/javascript/voice-agents/v1/parallel-tool-calling/example.js new file mode 100644 index 00000000..f86ba9d9 --- /dev/null +++ b/recipes/javascript/voice-agents/v1/parallel-tool-calling/example.js @@ -0,0 +1,50 @@ +import { DeepgramClient } from "@deepgram/sdk"; + +const tools = [ + { name: "get_weather", description: "Get weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } }, + { name: "get_time", description: "Get current time in a timezone", parameters: { type: "object", properties: { timezone: { type: "string" } }, required: ["timezone"] } }, + { name: "get_population", description: "Get population of a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } }, +]; +const handlers = { + get_weather: ({ city }) => `${city}: 72°F, sunny`, + get_time: ({ timezone }) => `Current time in ${timezone}: 2:30 PM`, + get_population: ({ city }) => `${city} population: 8.3 million`, +}; + +async function main() { + const client = new DeepgramClient(); + const connection = await client.agent.v1.createConnection(); + connection.on("message", async (data) => { + if (data.type === "SettingsApplied") { + console.log("Settings applied with 3 tools registered"); + connection.sendInjectUserMessage({ type: "InjectUserMessage", content: "What's the weather, time, and population in New York?" }); + } else if (data.type === "FunctionCallRequest") { + console.log(`Parallel call: ${data.functions.length} function(s) requested`); + const results = await Promise.all(data.functions.map(async (fn) => { + const args = JSON.parse(fn.arguments); + const result = handlers[fn.name]?.(args) ?? "Unknown tool"; + console.log(` ${fn.name}(${JSON.stringify(args)}) -> ${result}`); + return { id: fn.id, name: fn.name, content: result }; + })); + results.forEach((r) => connection.sendFunctionCallResponse({ type: "FunctionCallResponse", ...r })); + } else if (data.type === "ConversationText") { + console.log(`${data.role === "assistant" ? "Agent" : "User"}: ${data.content}`); + } + }); + connection.on("error", (err) => console.error("Error:", err)); + connection.connect(); + await connection.waitForOpen(); + connection.sendSettings({ + type: "Settings", + audio: { input: { encoding: "linear16", sample_rate: 24000 }, output: { encoding: "linear16", sample_rate: 16000, container: "wav" } }, + agent: { + language: "en", + listen: { provider: { type: "deepgram", model: "nova-3" } }, + think: { provider: { type: "open_ai", model: "gpt-4o-mini" }, prompt: "You are a helpful assistant. Use all available tools in parallel when the user asks about multiple things.", functions: tools }, + speak: { provider: { type: "deepgram", model: "aura-2-thalia-en" } }, + }, + }); + await new Promise((resolve) => setTimeout(resolve, 20000)); + connection.close(); +} +main().catch(console.error); diff --git a/recipes/javascript/voice-agents/v1/parallel-tool-calling/example.test.js b/recipes/javascript/voice-agents/v1/parallel-tool-calling/example.test.js new file mode 100644 index 00000000..caf57645 --- /dev/null +++ b/recipes/javascript/voice-agents/v1/parallel-tool-calling/example.test.js @@ -0,0 +1,19 @@ +import { describe, it } from "node:test"; +import { ok } from "node:assert"; +import { execSync } from "node:child_process"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +describe("voice-agent-parallel-tool-calling", () => { + it("runs without error and produces output", () => { + const output = execSync("node example.js", { + cwd: __dirname, + timeout: 60000, + env: process.env, + encoding: "utf8", + }); + ok(output.trim().length > 0, "Expected non-empty output"); + }); +});