প্রতি কলে ডায়নামিক কনফিগারেশন

ডিফল্টভাবে প্রতিটি ফোন নম্বর এবং publishable key-এর সঙ্গে একটি স্ট্যাটিক এজেন্ট নির্ধারিত থাকে। যখন আপনার প্রতি-কলার বা প্রতি-ভিজিটর কাস্টমাইজেশন প্রয়োজন — VIP রাউটিং, লগ-ইন করা ব্যবহারকারীর প্রসঙ্গ, A/B prompt পরীক্ষা — তখন webhook মোডে স্যুইচ করুন এবং আপনার সার্ভারকে সিদ্ধান্ত নিতে দিন।

এটি কীভাবে কাজ করে

  1. আপনি telephony.incoming (ফোন) অথবা web.incoming (উইজেট) ইভেন্টে সাবস্ক্রাইব করেন। উভয়ই ব্লকিং webhook: ThunderPhone কল চালিয়ে যাওয়ার আগে আপনার প্রতিক্রিয়ার জন্য সর্বোচ্চ 10 সেকেন্ড অপেক্ষা করে।
  2. ThunderPhone আপনাকে {call_id, from_number, to_number} পাঠায় (উইজেট সেশনে নম্বরের পরিবর্তে উইজেট-নির্দিষ্ট ফিল্ড থাকে — request schema দেখুন)।
  3. আপনার সার্ভার একটি এজেন্ট কনফিগারেশন (prompt, voice, product, tools) দিয়ে প্রতিক্রিয়া জানায়। ThunderPhone কলের জন্য সেই কনফিগারেশন ব্যবহার করে।
  4. আপনি {} ফেরত দিলে, সময় শেষ হলে বা ত্রুটি হলে, স্ট্যাটিকভাবে নির্ধারিত এজেন্টটি fallback হিসেবে ব্যবহৃত হয়। এটি একটি নিরাপদ ডিফল্ট।

1. webhook গন্তব্য কনফিগার করুন

ফোন কল

ফোন নম্বরের জন্য, আপনার endpoint-কে telephony.incoming-এ সাবস্ক্রাইব করুন:

curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label":  "Prod call-incoming",
    "url":    "https://example.com/thunderphone/incoming",
    "events": ["telephony.incoming"]
  }'

প্রতিক্রিয়ায় একবার ব্যবহারযোগ্য একটি secret থাকে — এটি সংরক্ষণ করুন; স্বাক্ষর যাচাইয়ের জন্য আপনি এটি ব্যবহার করবেন।

ওয়েব উইজেট

উইজেট সেশনের জন্য, আপনার endpoint URL অন্তর্ভুক্ত করে mode="webhook"-এ একটি publishable key তৈরি করুন:

curl -X POST https://api.thunderphone.com/v1/publishable-key \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":            "Dynamic widget",
    "mode":            "webhook",
    "webhook_url":     "https://example.com/thunderphone/widget-incoming",
    "allowed_domains": ["example.com"]
  }'

প্রতিটি সেশন শুরুতে উইজেটটি এই URL-এ POST করবে।

2. হ্যান্ডলার বাস্তবায়ন করুন

তিনটি ব্যবহারিক নিয়ম:

import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

def verify(body: bytes, sig: str) -> bool:
    expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig or "")

@app.post("/thunderphone/incoming")
async def incoming(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(401)

    event = json.loads(body)
    if event["type"] not in ("telephony.incoming", "web.incoming"):
        return {}  # fall back to default

    caller = event["data"]["from_number"]
    # Cheap DB lookup: is this a known VIP?
    customer = lookup_customer(caller)
    if customer and customer.tier == "vip":
        return {
            "prompt":  f"You are a VIP concierge for {customer.name}. Be proactive…",
            "voice":   "john",
            "product": "storm-base",
        }
    return {}  # default agent handles non-VIPs

def lookup_customer(phone: str):
    # ... your CRM integration ...
    pass
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;

function verify(body, sig) {
  const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
  return sig &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

app.post(
  "/thunderphone/incoming",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));

    const IMPORTANT_TYPES = new Set([
      "telephony.incoming",
      "web.incoming",
    ]);
    if (!IMPORTANT_TYPES.has(event.type)) return res.json({});

    const customer = await lookupCustomer(event.data.from_number);
    if (customer?.tier === "vip") {
      return res.json({
        prompt:  `You are a VIP concierge for ${customer.name}. Be proactive…`,
        voice:   "john",
        product: "storm-base",
      });
    }
    res.json({}); // fall back to default agent
  },
);

3. রেসপন্স স্কিমা

রেসপন্স বডি ইনকামিং-কল রেসপন্স স্কিমা-র সঙ্গে হুবহু মেলে। সাধারণভাবে ব্যবহৃত ফিল্ডগুলো:

ফিল্ডধরনবিবরণ
promptস্ট্রিং (প্রয়োজনীয়)এজেন্টের জন্য সিস্টেম prompt
voiceস্ট্রিং (প্রয়োজনীয়)GET /v1/voices থেকে ভয়েস আইডি
productস্ট্রিংডিফল্ট হলো spark
background_trackস্ট্রিং | nullপরিবেশের অডিও আইডি
acknowledgement_prompt_modeস্ট্রিংauto অথবা manual (শুধু স্বীকৃতিসহ Storm)
acknowledgement_promptস্ট্রিংমোড manual হলে প্রয়োজনীয়
toolsঅ্যারেইনলাইন ফাংশন-টুল স্কিমা — দেখুন ফাংশন টুল

প্যাটার্ন

লগইন করা ব্যবহারকারীর প্রসঙ্গ

webhook-mode উইজেটে, ভিজিটরের পেজ আগে থেকেই জানে তিনি কে। একটি query string parameter সহ আপনার webhook কল করুন, যা widget SDK ফরওয়ার্ড করে (?customer_id=123), এবং server-side-এ কাস্টমার খুঁজে নিন।

A/B prompt রোলআউট

নিজে এটি তৈরি করার আগে মনে রাখুন, ThunderPhone-এ একটি নেটিভ পরীক্ষা ফিচার রয়েছে (/dashboard/experiments এবং এজেন্ট বিল্ডারের A/B ট্যাব), যা ভ্যারিয়েন্ট নির্ধারণ করে, ট্রাফিক ভাগ করে এবং প্রতিটি ভ্যারিয়েন্টের ফলাফল তুলনা করে — কোনো webhook প্রয়োজন নেই।

তবুও যদি আপনার webhook-side নিয়ন্ত্রণ প্রয়োজন হয়: call_id হ্যাশ করুন → bucket; 0..49-এর জন্য prompt A এবং 50..99-এর জন্য prompt B প্রদান করুন। আপনি যে bucket বেছে নিয়েছেন তা নিজের DB-তে রেকর্ড করুন এবং পরে সম্পন্ন কলের grade-এর সঙ্গে সম্পর্ক স্থাপন করুন।

সময়ভিত্তিক রাউটিং

ব্যবসায়িক সময় → "সরাসরি সহায়তা" এজেন্ট; ব্যবসায়িক সময়ের বাইরে → "বার্তা নিন" এজেন্ট। আপনার handler-এ new Date().getUTCHours()-এর ওপর সাধারণ switch।


পরবর্তী ধাপ