From e8c65d028d9cd87b74e03737a507a3b85391e8ac Mon Sep 17 00:00:00 2001 From: recipes-bot Date: Sun, 14 Jun 2026 21:56:17 +0000 Subject: [PATCH] =?UTF-8?q?feat(python):=20voice-agents=20v1=20=E2=80=94?= =?UTF-8?q?=20dynamic-tool-availability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/dynamic-tool-availability/README.md | 59 +++++++++++++ .../v1/dynamic-tool-availability/example.py | 83 +++++++++++++++++++ .../dynamic-tool-availability/example_test.py | 16 ++++ 3 files changed, 158 insertions(+) create mode 100644 recipes/python/voice-agents/v1/dynamic-tool-availability/README.md create mode 100644 recipes/python/voice-agents/v1/dynamic-tool-availability/example.py create mode 100644 recipes/python/voice-agents/v1/dynamic-tool-availability/example_test.py diff --git a/recipes/python/voice-agents/v1/dynamic-tool-availability/README.md b/recipes/python/voice-agents/v1/dynamic-tool-availability/README.md new file mode 100644 index 00000000..ac043612 --- /dev/null +++ b/recipes/python/voice-agents/v1/dynamic-tool-availability/README.md @@ -0,0 +1,59 @@ +# Dynamic Tool Availability (Voice Agents v1) + +Control which tools the LLM can call on each conversation turn, enabling phased workflows where tool access is granted or revoked as the conversation progresses. + +## What it does + +Starts a voice agent session with a single tool (`verify_identity`). Once the user's identity is verified via a function call, the agent dynamically swaps in a new tool set (`get_balance`, `transfer_funds`) by calling `send_update_think()` on the live WebSocket — no reconnection needed. The system prompt is updated in tandem so the LLM knows which tools are now available. + +This pattern is essential for production agents with multi-phase conversations: a banking agent should only expose transfer tools after identity verification, a support agent should restrict escalation tools until troubleshooting is exhausted, and so on. + +## Key parameters + +| Parameter | Description | +|-----------|-------------| +| `think.functions` | List of tool/function definitions the LLM may call | +| `send_update_think(AgentV1UpdateThink)` | Replace the think config (including functions) mid-session | +| `send_update_prompt(AgentV1UpdatePrompt)` | Update the system prompt to reflect new tool availability | +| `AgentV1FunctionCallRequest` | Event fired when the LLM invokes a tool | +| `send_function_call_response()` | Return the tool result to the agent | + +## Conversation phases + +| Phase | Available tools | Trigger to advance | +|-------|-----------------|--------------------| +| 1 — Greeting | `verify_identity` | User provides verification code | +| 2 — Authorized | `get_balance`, `transfer_funds` | (end of demo) | + +## Example output + +``` +Connection opened +[Phase 1] Tools: verify_identity +[user] Hi, my code is ABC123 +[Phase 1] Tool called: verify_identity +[Phase 2] Tools updated → get_balance, transfer_funds +[assistant] Your identity has been verified. How can I help you today? +[user] What is my balance? +[Phase 2] Tool called: get_balance +[assistant] Your current balance is $4,210.50. +Connection closed +``` + +## Prerequisites + +- Python 3.10+ +- Set `DEEPGRAM_API_KEY` environment variable +- Install: `pip install -r recipes/python/requirements.txt` + +## Run + +```bash +python example.py +``` + +## Test + +```bash +pytest example_test.py -v +``` diff --git a/recipes/python/voice-agents/v1/dynamic-tool-availability/example.py b/recipes/python/voice-agents/v1/dynamic-tool-availability/example.py new file mode 100644 index 00000000..c9057d9c --- /dev/null +++ b/recipes/python/voice-agents/v1/dynamic-tool-availability/example.py @@ -0,0 +1,83 @@ +""" +Recipe: Dynamic Tool Availability (Voice Agents v1) +===================================================== +Demonstrates changing which tools the LLM can call per conversation +phase. Phase 1 offers only verify_identity; after verification +succeeds, send_update_think() swaps in get_balance and transfer_funds +— all on the same live WebSocket session. +""" + +import threading, time + +from deepgram import DeepgramClient +from deepgram.agent.v1.types import ( + AgentV1FunctionCallRequest, AgentV1InjectUserMessage, + AgentV1SendFunctionCallResponse, AgentV1Settings, AgentV1SettingsAgent, + AgentV1SettingsAgentListen, AgentV1SettingsAgentListenProvider_V1, + AgentV1SettingsAudio, AgentV1SettingsAudioInput, AgentV1UpdateThink, +) +from deepgram.core.events import EventType +from deepgram.types.speak_settings_v1 import SpeakSettingsV1 +from deepgram.types.speak_settings_v1provider import SpeakSettingsV1Provider_Deepgram +from deepgram.types.think_settings_v1 import ThinkSettingsV1 +from deepgram.types.think_settings_v1provider import ThinkSettingsV1Provider_OpenAi + +def _fn(name, desc, props, req): + return {"name": name, "description": desc, "parameters": {"type": "object", "properties": props, "required": req}} + +VERIFY = _fn("verify_identity", "Verify customer", {"code": {"type": "string"}}, ["code"]) +BALANCE = _fn("get_balance", "Check balance", {}, []) +TRANSFER = _fn("transfer_funds", "Transfer money", {"amount": {"type": "number"}, "to": {"type": "string"}}, ["amount", "to"]) + +RESULTS = {"verify_identity": '{"verified":true}', "get_balance": '{"balance":"$4,210.50"}', "transfer_funds": '{"confirmation":"TXN-98201"}'} +P1 = "You are a bank assistant. Verify the user with verify_identity before anything else." +P2 = "Identity verified. You may now use get_balance and transfer_funds." + +def main(): + client = DeepgramClient() + phase, ready, done = {"n": 1}, threading.Event(), threading.Event() + + with client.agent.v1.connect() as agent: + def on_msg(msg): + if isinstance(msg, AgentV1FunctionCallRequest): + print(f"[Phase {phase['n']}] Tool called: {msg.name}") + agent.send_function_call_response(AgentV1SendFunctionCallResponse( + type="FunctionCallResponse", id=msg.id, name=msg.name, content=RESULTS.get(msg.name, "{}"))) + if msg.name == "verify_identity" and phase["n"] == 1: + phase["n"] = 2 + agent.send_update_think(AgentV1UpdateThink(think=ThinkSettingsV1( + provider=ThinkSettingsV1Provider_OpenAi(type="open_ai", model="gpt-4o-mini"), + prompt=P2, functions=[BALANCE, TRANSFER]))) + print("[Phase 2] Tools updated → get_balance, transfer_funds") + elif phase["n"] == 2: + done.set() + elif not isinstance(msg, bytes): + mt = getattr(msg, "type", "") + if mt == "SettingsApplied": + ready.set() + elif mt == "ConversationText": + print(f"[{getattr(msg, 'role', '?')}] {getattr(msg, 'content', '')}") + + agent.on(EventType.OPEN, lambda _: print("Connection opened")) + agent.on(EventType.MESSAGE, on_msg) + agent.on(EventType.CLOSE, lambda _: print("Connection closed")) + threading.Thread(target=agent.start_listening, daemon=True).start() + + agent.send_settings(AgentV1Settings( + audio=AgentV1SettingsAudio(input=AgentV1SettingsAudioInput(encoding="linear16", sample_rate=24000)), + agent=AgentV1SettingsAgent( + listen=AgentV1SettingsAgentListen(provider=AgentV1SettingsAgentListenProvider_V1(type="deepgram", model="nova-3")), + think=ThinkSettingsV1(provider=ThinkSettingsV1Provider_OpenAi(type="open_ai", model="gpt-4o-mini"), prompt=P1, functions=[VERIFY]), + speak=SpeakSettingsV1(provider=SpeakSettingsV1Provider_Deepgram(type="deepgram", model="aura-2-thalia-en"))))) + print("[Phase 1] Tools: verify_identity") + if not ready.wait(10): + raise TimeoutError("Settings not applied") + + agent.send_inject_user_message(AgentV1InjectUserMessage(content="Hi, my code is ABC123")) + if not done.wait(30): + agent.send_inject_user_message(AgentV1InjectUserMessage(content="What is my balance?")) + done.wait(15) + time.sleep(2) + +if __name__ == "__main__": + main() diff --git a/recipes/python/voice-agents/v1/dynamic-tool-availability/example_test.py b/recipes/python/voice-agents/v1/dynamic-tool-availability/example_test.py new file mode 100644 index 00000000..848cfd5d --- /dev/null +++ b/recipes/python/voice-agents/v1/dynamic-tool-availability/example_test.py @@ -0,0 +1,16 @@ +import subprocess +from pathlib import Path + +def test_example_runs(): + """Runs the dynamic-tool-availability example and verifies it produces output.""" + example = Path(__file__).parent / "example.py" + result = subprocess.run( + ["python", str(example)], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, ( + f"Example failed\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}" + ) + assert result.stdout.strip(), "Example produced no output"