Dinaminė konfigūracija kiekvienam skambučiui

Pagal numatytuosius nustatymus kiekvienam telefono numeriui ir publikuojamam raktui priskiriamas statinis agentas. Kai reikia tinkinti pagal skambintoją arba pagal lankytoją — VIP nukreipimui, prisijungusio naudotojo kontekstui, A/B raginimų testams — perjunkite į webhook režimą ir leiskite serveriui nuspręsti.

Kaip tai veikia

  1. Užsiprenumeruokite telephony.incoming (telefonui) arba web.incoming (valdikliui) įvykį. Abu yra blokuojantys webhook pranešimai: ThunderPhone laukia iki 10 sekundžių jūsų atsakymo prieš tęsdamas skambutį.
  2. ThunderPhone siunčia jums {call_id, from_number, to_number} (valdiklio sesijose vietoj numerių pateikiami valdikliui skirti laukai — žr. užklausos schemą).
  3. Jūsų serveris atsako agento konfigūracija (raginimu, balsu, produktu, įrankiais). ThunderPhone šią konfigūraciją naudoja skambučiui.
  4. Jei grąžinate {}, baigiasi laukimo laikas arba įvyksta klaida, kaip atsarginis variantas naudojamas statiškai priskirtas agentas. Saugus numatytasis nustatymas.

1. Sukonfigūruokite webhook paskirties vietą

Telefono skambučiai

Telefono numeriams užsiprenumeruokite savo galinį tašką įvykiui 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"]
  }'

Atsakyme pateikiama vienkartinė secret reikšmė — išsaugokite ją; naudosite ją parašo patikrinimui.

Žiniatinklio valdiklis

Valdiklio sesijoms sukurkite publikuojamą raktą su mode="webhook", į kurį įtrauktas jūsų galinio taško URL:

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"]
  }'

Valdiklis kiekvienos sesijos pradžioje siųs POST užklausą į šį URL.

2. Įgyvendinkite tvarkyklę

Trys pagrindinės taisyklės:

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. Atsakymo schema

Atsakymo turinys tiksliai atitinka gaunamo skambučio atsakymo schemą. Dažniausiai naudojami laukai:

LaukasTipasAprašymas
prompteilutė (būtina)Sistemos raginimas agentui
voiceeilutė (būtina)Balso ID iš GET /v1/voices
producteilutėNumatytoji reikšmė yra spark
background_trackeilutė | nullAplinkos garso ID
acknowledgement_prompt_modeeilutėauto arba manual (tik Storm su patvirtinimu)
acknowledgement_prompteilutėBūtina, kai režimas yra manual
toolsmasyvasĮterptos funkcijų įrankių schemos — žr. Funkcijų įrankiai

Modeliai

Prisijungusio naudotojo kontekstas

Valdikliuose, veikiančiuose webhook režimu, lankytojo puslapis jau žino, kas jis yra. Iškvieskite savo webhook su užklausos eilutės parametru, kurį persiunčia valdiklio SDK (?customer_id=123), ir raskite klientą serverio pusėje.

A/B raginimų diegimas

Prieš kurdami tai patys, atkreipkite dėmesį, kad ThunderPhone turi integruotą eksperimentų funkciją (/dashboard/experiments ir agentų kūrimo priemonės skirtuką A/B), kuri apibrėžia variantus, paskirsto srautą ir lygina rezultatus pagal variantą – webhook nereikalingas.

Jei vis tiek reikia valdymo webhook pusėje: maišykite call_id → segmentas; pateikite A raginimą reikšmėms 0..49 ir B raginimą reikšmėms 50..99. Įrašykite, kurį segmentą pasirinkote, savo DB ir vėliau susiekite jį su užbaigto skambučio įvertinimu.

Nukreipimas pagal laiką

Darbo valandomis → balso agentas „tiesioginė pagalba“; ne darbo valandomis → balso agentas „palikti žinutę“. Tvarkyklėje pakanka paprasto perjungimo pagal new Date().getUTCHours().


Tolesni veiksmai