Open in
Cấu hình động cho từng cuộc gọi
Chọn tác nhân AI trả lời — hoặc viết lại prompt và cài đặt của tác nhân — riêng cho từng cuộc gọi đến, dựa trên logic tùy chỉnh trong webhook do bạn kiểm soát.
Theo mặc định, mọi số điện thoại và khóa có thể công khai đều được gán một tác nhân AI tĩnh. Khi bạn cần tùy chỉnh theo từng người gọi hoặc theo từng khách truy cập — định tuyến VIP, ngữ cảnh người dùng đã đăng nhập, kiểm thử prompt A/B — hãy chuyển sang chế độ webhook và để máy chủ của bạn quyết định.
Cách hoạt động
- Bạn đăng ký sự kiện
telephony.incoming(điện thoại) hoặcweb.incoming(widget). Cả hai đều là webhook chặn: ThunderPhone chờ phản hồi của bạn trước khi tiếp tục cuộc gọi, tối đa 13 giây đối với cuộc gọi điện thoại (bao gồm một lần thử lại) và 10 giây đối với phiên widget. - ThunderPhone gửi cho bạn
{call_id, from_number, to_number}(phiên widget chứa các trường dành riêng cho widget thay vì số điện thoại — xem schema yêu cầu). - Máy chủ của bạn phản hồi bằng cấu hình tác nhân AI (prompt, giọng nói, sản phẩm, công cụ). ThunderPhone sử dụng cấu hình đó cho cuộc gọi.
- Không có tác nhân AI dự phòng. Nếu bạn trả về
{}hoặc một cấu hình không hợp lệ, hoặc yêu cầu vẫn thất bại sau lần thử lại, cuộc gọi điện thoại sẽ bị kết thúc ngay và được ghi lại vớiend_reasoninbound_config_webhook_failed; yêu cầu phiên widget sẽ thất bại.
1. Cấu hình đích webhook
Đối với số điện thoại, đăng ký điểm cuối của bạn với 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"]
}'Phản hồi bao gồm secret dùng một lần — hãy lưu lại; bạn sẽ dùng nó
để xác minh chữ ký.
Đối với phiên widget, tạo khóa có thể công khai ở mode="webhook"
với URL điểm cuối của bạn được nhúng sẵn:
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"]
}'Widget sẽ POST đến URL này mỗi khi bắt đầu phiên.
2. Triển khai handler
Ba quy tắc thực hành:
- Xác minh chữ ký trên mọi yêu cầu (xem Xác minh chữ ký webhook). Đừng bỏ qua bước này trong môi trường dev — thực hiện đúng một lần rồi tái sử dụng.
- Phản hồi nhanh. Lần thử đầu tiên của một cuộc gọi sẽ bị ngắt sau 10 giây và toàn bộ trao đổi sau 13 giây, và mỗi giây đều là khoảng lặng với người gọi. Hãy tra cứu cơ sở dữ liệu nếu cần, nhưng đừng gọi đồng bộ các LLM ở hạ nguồn — nếu muốn tạo prompt động, hãy tính trước và lưu vào bộ nhớ đệm.
- Dự phòng gọn gàng. Với mọi trạng thái không mong đợi, hãy phản hồi bằng
promptvàvoicemặc định (hoặcagent_idđã lưu). Trả về{}hoặc trạng thái lỗi sẽ kết thúc cuộc gọi.
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 ...
passimport 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. Lược đồ phản hồi
Nội dung phản hồi khớp chính xác với lược đồ phản hồi cuộc gọi đến. Các trường thường dùng:
| Trường | Loại | Mô tả |
|---|---|---|
prompt | chuỗi (bắt buộc) | System prompt cho tác nhân AI |
voice | chuỗi (bắt buộc) | ID giọng nói từ GET /v1/voices |
product | chuỗi | Mặc định là spark. Sản phẩm thử nghiệm chỉ dành cho tổ chức trong danh sách cho phép. |
background_track | chuỗi | null | ID âm thanh nền |
acknowledgement_prompt_mode | chuỗi | auto hoặc manual (chỉ Storm-with-ack) |
acknowledgement_prompt | chuỗi | Bắt buộc khi chế độ là manual |
tools | mảng | Schema công cụ hàm nội tuyến — xem Công cụ hàm |
Giữ một tác nhân AI đã lưu và cung cấp biến
Trả về {"agent_id": 12, "variables": {"name": "Ada"}} để sử dụng tác nhân AI
đã lưu của tổ chức đó với dữ liệu theo từng cuộc gọi. Prompt của tác nhân có thể chứa {{name}} hoặc
{{name|Friend}}. Biến webhook được hợp nhất đè lên biến ở cấp yêu cầu; giá trị null
sử dụng giá trị mặc định của chỗ giữ chỗ hoặc văn bản trống nếu không có giá trị mặc định. Các giá trị cuối cùng
và tên chưa được phân giải xuất hiện trong chi tiết cuộc gọi và webhook hoàn tất.
Phản hồi tác nhân đã lưu chỉ chấp nhận agent_id, variables và timezone. Nếu có prompt,
phản hồi sẽ dùng cấu hình nội tuyến và bỏ qua agent_id (bao gồm
siêu dữ liệu null hoặc không phải số nguyên); prompt nội tuyến vẫn phải hợp lệ. Phản hồi cấu hình
nội tuyến cũng có thể bao gồm variables. Phản hồi tác nhân đã lưu sử dụng phân chia A/B đã triển khai của
tác nhân trên cả cuộc gọi điện thoại và cuộc gọi widget, sau đó hiển thị biến. Xem biến cuộc gọi
để biết giới hạn và khả năng hỗ trợ API phiên. Cấu hình chặn đến từ URL
số điện thoại/tổ chức cũ hoặc khóa widget ở chế độ webhook; các sự kiện đến của hệ thống điểm cuối chỉ là thông báo.
Đặt múi giờ cho từng cuộc gọi
Một tác nhân AI có thể trả lời cho liên hệ ở các múi giờ khác nhau. Thêm timezone,
một tên IANA, vào phản hồi tác nhân đã lưu hoặc phản hồi nội tuyến và tác nhân sẽ nêu ngày
và giờ cũng như đề xuất thời gian theo múi giờ đó cho cuộc gọi này, thay vì múi giờ
của chính tác nhân:
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Gửi múi giờ thay vì dấu thời gian trong variables: đồng hồ của tác nhân vẫn tiếp tục
chạy trong cuộc gọi và thời gian bạn truyền vào sẽ trở nên lỗi thời. Giá trị không phải
là múi giờ IANA sẽ bị bỏ qua và cuộc gọi kết nối theo múi giờ của tác nhân.
Mẫu
Ngữ cảnh người dùng đã đăng nhập
Trong widget ở chế độ webhook, trang của khách truy cập đã biết họ
là ai. Gọi webhook của bạn bằng tham số chuỗi truy vấn mà SDK widget
chuyển tiếp (?customer_id=123) và tra cứu khách hàng ở phía máy chủ.
Triển khai prompt A/B
Trước khi tự xây dựng cách này, hãy lưu ý rằng ThunderPhone có tính năng
Thử nghiệm gốc
(/dashboard/experiments và tab A/B của trình tạo tác nhân AI) để
xác định biến thể, phân chia lưu lượng và so sánh kết quả theo từng biến thể —
không cần webhook.
Nếu bạn vẫn cần quyền kiểm soát ở phía webhook: băm call_id → nhóm;
phục vụ prompt A cho 0..49 và prompt B cho 50..99. Ghi lại nhóm
bạn đã chọn trong DB riêng và sau đó đối chiếu với điểm của cuộc gọi
đã hoàn tất.
Định tuyến theo thời gian
Trong giờ làm việc → tác nhân AI "hỗ trợ trực tiếp"; ngoài giờ → tác nhân AI "nhận lời nhắn".
Chỉ cần chuyển nhánh theo new Date().getUTCHours() trong trình xử lý của bạn.
Bước tiếp theo
Lược đồ yêu cầu + phản hồi chính xác, bao gồm mọi khóa cấu hình.
Thiết lập HMAC đúng một lần; tái sử dụng ở mọi nơi.
Kết hợp định tuyến động với công cụ theo từng tác nhân AI.
Thử lại, thứ tự, thời gian chờ.