how can i get tool name invoked and post to webhoo...
# ask-ai
r
how can i get tool name invoked and post to webhook ?
t
Thanks for reaching out, Saurabh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you want to capture which tool the agent invoked and post that information to a webhook. Below are simple, practical approaches you can use depending on where you want to send the webhook (from the tool implementation server-side, or from a client/frontend tool). I’ll assume you already have a webhook URL to receive the POST. If you want the tool itself to send the webhook (server-side Python example) 1. In your function tool, include the tool name in the payload and POST to the webhook using an HTTP client. 2. Return whatever result the agent needs. Example (Python, using aiohttp):
Copy code
import aiohttp
from livekit.agents import function_tool, Agent

class MyAgent(Agent):
    @function_tool()
    async def lookup_weather(self, location: str) -> dict:
        # tool result
        result = {"weather": "sunny", "temperature_f": 70}

        # send invoked tool name + data to webhook
        async with aiohttp.ClientSession() as session:
            await <http://session.post|session.post>(
                "<https://your-webhook.url>",
                json={
                    "tool": "lookup_weather",
                    "args": {"location": location},
                    "result": result,
                },
                timeout=10,
            )

        return result
If you prefer Node.js (server-side) use a similar pattern with fetch/axios: • Include
"tool": "yourToolName"
and the arguments/result in the POST body. If the tool is a client/front-end tool (forward call to frontend and let client post) 1. The tool should send an RPC to the frontend to run the client-only action (e.g., access geolocation). 2. When the frontend responds with data, the tool implementation can then POST that response to your webhook, or the frontend can POST directly. Example outline (Python agent forwarding to frontend via RPC): • Tool triggers an RPC (e.g.,
getUserLocation
) to a participant identity. • RPC returns the client’s response. • Tool posts that response to your webhook (include
"tool"
and args). Error handling & security notes • Always catch and log network errors; decide whether failures to post should fail the tool or be non-blocking. • Add retries or queued delivery for reliability if webhooks are critical. • Secure webhook endpoints (HMAC, secret token) and include an auth header in the POST. Questions to help me tailor this • Do you want the webhook POST to come from your server-side agent, or directly from the user’s browser/client? • Which SDK/language are you using (Python, Node.js, or something else)? • Do you need an example that includes authentication headers or retry logic? Tell me which option you prefer and I’ll provide a tailored code sample with error handling and auth. Sources: Tool definition and use | LiveKit Documentation | Best practices for managing webhook event streams | LiveKit
r
dynamically
t
Thanks — I hear you want a dynamic, non‑hardcoded way to capture whichever tool the agent invokes and post that info to a webhook. I can provide a ready solution, but first a couple quick clarifying questions so I give code that fits your setup: 1. Do you want the webhook POST to originate from the server-side agent process or from the client/browser that runs a client tool? 2. Which SDK/language are you using for your agent code: Python or Node.js? 3. Do you want to include the full tool arguments and result in the webhook, or only the tool name (and maybe an ID/timestamp)? 4. Do you need authentication on the webhook (e.g., add an Authorization header or HMAC signature)? If you prefer, tell me “server / Python / include args+result / HMAC” (or similar) and I’ll return a complete, dynamic example that: • Detects the invoked tool name automatically (no hardcoding), • Builds a payload with args/result/timestamp, • Sends the webhook with optional auth and retry handling.
r
server-side agent
Python
full tool arguments
t
Great — thanks for confirming. Below is a server-side Python pattern that dynamically captures whichever
function_tool
is invoked, includes the full tool arguments and result, and posts that payload to your webhook. It adds optional HMAC signing, retry logic, and non-blocking behavior so webhook failures won’t crash the tool unless you want them to. If you’d like any adjustments (no HMAC, synchronous blocking post, different retry policy), tell me and I’ll adapt it. 1) Core idea • Use a decorator wrapper around your tool functions to intercept the call, capture: tool name, args, kwargs, result (or exception), timestamp, and an execution id. • Post that payload to your webhook asynchronously with retries and optional HMAC auth. • Return the original result (or re-raise) so agent logic is unchanged. 2) Example implementation
Copy code
import asyncio
import aiohttp
import functools
import json
import time
import uuid
import hmac
import hashlib
from typing import Any, Callable

WEBHOOK_URL = "<https://your-webhook.url/path>"
WEBHOOK_SECRET = b"your_secret_here"  # set to None to disable HMAC
MAX_RETRIES = 3
RETRY_BACKOFF = 1.5  # multiplier

def sign_payload(secret: bytes, payload_bytes: bytes) -> str:
    mac = hmac.new(secret, payload_bytes, digestmod=hashlib.sha256)
    return mac.hexdigest()

async def post_with_retries(url: str, json_payload: dict, headers: dict = None):
    headers = headers or {}
    attempt = 0
    delay = 1.0
    async with aiohttp.ClientSession() as session:
        while attempt < MAX_RETRIES:
            try:
                async with <http://session.post|session.post>(url, json=json_payload, headers=headers, timeout=10) as resp:
                    # consider 2xx as success
                    if 200 <= resp.status < 300:
                        return await resp.text()
                    # treat non-2xx as retryable (customize if needed)
                    text = await resp.text()
                    attempt += 1
                    await asyncio.sleep(delay)
                    delay *= RETRY_BACKOFF
            except asyncio.CancelledError:
                raise
            except Exception:
                attempt += 1
                await asyncio.sleep(delay)
                delay *= RETRY_BACKOFF
    # final failure; return None or raise depending on how you want to handle it
    return None

def webhook_notify_decorator(send_to_webhook: bool = True, fail_on_webhook_error: bool = False):
    """
    Decorator to wrap function tools. Captures tool name, args, kwargs, result/exception,
    and posts to webhook. If fail_on_webhook_error is True, exceptions in posting will be raised.
    """
    def decorator(fn: Callable):
        @functools.wraps(fn)
        async def wrapper(*args, **kwargs):
            invocation_id = str(uuid.uuid4())
            tool_name = fn.__name__
            timestamp = time.time()

            payload = {
                "invocation_id": invocation_id,
                "tool": tool_name,
                "args": args,        # may need serialization
                "kwargs": kwargs,    # may need serialization
                "timestamp": timestamp,
            }

            try:
                # execute the original tool
                result = await fn(*args, **kwargs)
                payload["result"] = result
                payload["status"] = "success"
            except Exception as exc:
                payload["result"] = {"error": str(exc)}
                payload["status"] = "error"
                # post the webhook about the failure, then re-raise
                if send_to_webhook:
                    payload_bytes = json.dumps(payload, default=str).encode("utf-8")
                    headers = {"Content-Type": "application/json"}
                    if WEBHOOK_SECRET:
                        headers["X-Signature"] = sign_payload(WEBHOOK_SECRET, payload_bytes)
                    # run post async and optionally wait/raise
                    coro = post_with_retries(WEBHOOK_URL, json.loads(payload_bytes), headers=headers)
                    task = asyncio.create_task(coro)
                    if fail_on_webhook_error:
                        resp = await task
                        if resp is None:
                            raise RuntimeError("Webhook POST failed after retries")
                raise

            # success: send webhook in background unless configured to wait
            if send_to_webhook:
                # serialize args/kwargs/result safely
                try:
                    payload_serializable = {
                        "invocation_id": invocation_id,
                        "tool": tool_name,
                        "args": json.loads(json.dumps(payload["args"], default=str)),
                        "kwargs": json.loads(json.dumps(payload["kwargs"], default=str)),
                        "result": json.loads(json.dumps(payload["result"], default=str)),
                        "timestamp": timestamp,
                        "status": payload["status"],
                    }
                except Exception:
                    # fallback: stringify everything
                    payload_serializable = {
                        "invocation_id": invocation_id,
                        "tool": tool_name,
                        "args": str(payload["args"]),
                        "kwargs": str(payload["kwargs"]),
                        "result": str(payload["result"]),
                        "timestamp": timestamp,
                        "status": payload["status"],
                    }

                payload_bytes = json.dumps(payload_serializable).encode("utf-8")
                headers = {"Content-Type": "application/json"}
                if WEBHOOK_SECRET:
                    headers["X-Signature"] = sign_payload(WEBHOOK_SECRET, payload_bytes)

                # post in background; do not block agent response
                task = asyncio.create_task(post_with_retries(WEBHOOK_URL, payload_serializable, headers=headers))
                # optionally await task here if you want to fail tool on webhook error:
                # resp = await task
            return result
        return wrapper
    return decorator
3) How to use it with your LiveKit function tools • Wrap your function tools with both the agent
@function_tool()
and the webhook decorator. Order matters: decorate with the webhook wrapper outermost so the wrapper observes the actual invocation and result. Example:
Copy code
from livekit.agents import function_tool, Agent

class MyAgent(Agent):

    @webhook_notify_decorator(send_to_webhook=True, fail_on_webhook_error=False)
    @function_tool()
    async def lookup_weather(self, location: str) -> dict:
        # realistic implementation...
        result = {"weather": "sunny", "temperature_f": 70}
        return result

    @webhook_notify_decorator(send_to_webhook=True)
    @function_tool()
    async def compute_sum(self, a: int, b: int) -> int:
        return a + b
4) Notes and recommendations • Serialization: Arguments or results that include complex objects may not be directly JSON serializable. The example uses json.dumps(..., default=str) as a safe fallback. If you have structured types, add custom serializers. • Security: The example shows adding an
X-Signature
HMAC header. Verify this on the webhook receiver side with the same secret. • Reliability: Background posting (create_task) prevents slowing agent responses. If webhook delivery is critical, set
fail_on_webhook_error=True
and await the post task (or implement durable queuing). • Logging: Log webhook failures and responses for debugging. Consider exposing metrics on success/failure rates. • Concurrency: aiohttp ClientSession is created per call in this example; for high throughput, consider a long-lived session shared across calls. If you want, I can: • Provide a version that awaits the webhook post and raises on failure (blocking behavior). • Show a Node.js equivalent. • Add an example that includes the agent/room/job context (e.g., add room id / participant id) into the payload if you store that in the agent context. Which of those would help next?