وكيل صُرَد عبر خادمك
لواجهتك الخاصة: تطبيق جوال، أو موقع مخصص، أو نظام داخلي. يدير صُرَد عقل الوكيل وصوته ومعرفته، وتتحكم أنت في التجربة كاملة.
قبل البدء
- وكيل منشور في Studio، ومعرّفه (
agent_id) ومعرّف مساحة العمل (workspace_id): تجدهما في Studio ← الدمج. - مفتاح API من Studio ← الدمج. يُحفظ في خادمك فقط.
- نطاق واجهتك مسجل في «النطاقات المسموح لها» (مثل
https://app.example.com). كل طلب من الواجهة يحمل هذا النطاق في ترويسةOrigin، ويرفض صُرَد ما يخالفه.
1. رمز الجلسة (من خادمك)
POST/api/v1/workspaces/{workspace_id}/widget/session-tokens
الترويسة: Authorization: Bearer sk_…. يعيد token تمرره إلى واجهتك.
| الحقل | النوع | الوصف |
|---|---|---|
agent_id* | uuid | الوكيل الذي ستحادثه الجلسة. |
origin* | url | نطاق واجهتك كما سجّلته. الرمز مربوط به ولا يعمل من غيره. |
ttl_seconds | int | مدة صلاحية الرمز: من 60 إلى 3600 ثانية. الافتراضي 900. |
agent_version_id | uuid | لتثبيت مراجعة بعينها. إن أُغفل تعمل الجلسة على المراجعة المنشورة. |
2. فتح جلسة
POST/api/v1/widget/sessions
الترويستان: Authorization: Bearer <token> وOrigin. يعيد conversation.conversation_id الذي تستخدمه في بقية الطلبات.
| الحقل | النوع | الوصف |
|---|---|---|
mode | text | voice | طبيعة المحادثة. الافتراضي text. |
conversation_id | string | لاستئناف محادثة سابقة بعد انتهاء رمزها: افتح جلسة برمز جديد ومرّر المعرّف. |
external_visitor_id | string | معرّفك أنت للزائر، يُحفظ مع المحادثة للربط مع نظامك. |
409 TOKEN_REPLAYED)، ثم يبقى صالحاً لأدوار هذه الجلسة حتى تنتهي مدته. وعند انتهائها (401) اطلب رمزاً جديداً وافتح جلسة بنفس conversation_id.3. الأدوار النصية
POST/api/v1/widget/conversations/{conversation_id}/turns
| الحقل | النوع | الوصف |
|---|---|---|
text* | string | رسالة المستخدم. |
mode | text | voice | voice إن كانت واجهتك ستنطق الرد. |
idempotency_key | string | مفتاح فريد للدور. إعادة الطلب بنفس المفتاح لا تكرر التنفيذ ولا تدفع صوتاً مرتين. |
أهم حقول الرد
| الحقل | النوع | الوصف |
|---|---|---|
reply.display | string | نص الرد للعرض. |
reply.spoken | string | النص كما نُطق (قد يختلف في النطق المضبوط للأسماء والأرقام). |
audio.content_base64 | base64 WAV | صوت الرد كاملاً، إن كان «شكل الرد» يشمل الصوت. |
audio.voice | string | الصوت الذي نطق فعلاً. |
identity.voice_profile_id | string | ملف الصوت المطبق، للتحقق من ثبات صوت وكيلك. |
turn_id | string | معرّف الدور، للربط مع سجلاتك. |
4. الرسائل الصوتية
POST/api/v1/widget/conversations/{conversation_id}/voice/utterance
ترسل صوت المستخدم ملفاً (multipart/form-data، الحقل file)، فيحوله صُرَد إلى نص، ويرد الوكيل، ويعود الرد نصاً وصوتاً في طلب واحد. الأنسب: WAV أحادي 16kHz بترميز PCM16، وبحد أقصى 4MB.
الرد كالدور النصي، ومعه transcript.text: ما فهمه صُرَد من كلام المستخدم.
5. الأحداث الحية (اختياري)
GET/api/v1/widget/conversations/{conversation_id}/events
بث SSE لما يجري في الجلسة: session.ready، transcript.final، agent.responding، audio.first، audio.chunk، audio.completed، turn.completed، session.error. كل حدث بالشكل event: <type> ثم data: <json>، والتفاصيل في payload.
// قراءة الأحداث الحية بـ fetch (EventSource لا يرسل ترويسة Authorization)
const res = await fetch(API + '/api/v1/widget/conversations/' + conversationId + '/events', {
headers: { Authorization: 'Bearer ' + token, Accept: 'text/event-stream' },
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
let end
while ((end = buffer.indexOf('\n\n')) >= 0) {
const frame = buffer.slice(0, end)
buffer = buffer.slice(end + 2)
const type = /^event: (.*)$/m.exec(frame)?.[1]
const data = /^data: (.*)$/m.exec(frame)?.[1]
if (type && data) onEvent(type, JSON.parse(data)) // data.payload يحمل التفاصيل
}
}6. الإيقاف والإنهاء
POST/api/v1/widget/conversations/{conversation_id}/cancel
يوقف الرد الجاري (مثلاً حين يقاطع المستخدم الوكيل)، ولا يُسلَّم بعده صوت قديم.
POST/api/v1/widget/conversations/{conversation_id}/close
ينهي الجلسة. تبقى المحادثة محفوظة في السجل.
7. سجل المحادثات
GET/api/v1/workspaces/{workspace_id}/conversations
GET/api/v1/workspaces/{workspace_id}/conversations/{conversation_id}
بمفتاح API يحمل نطاق agents.read. القائمة من الأحدث، وتقبل agent_id وbefore وlimit (حتى 100)، وتعيد next_before للصفحة التالية. وتُعرض المحادثات نفسها في Studio ← المحادثات.
أمثلة كاملة
# 1) خادمك: رمز جلسة قصير العمر (المفتاح لا يغادر الخادم)
curl -X POST https://app.surad-ai.com/api/v1/workspaces/$WORKSPACE_ID/widget/session-tokens \
-H "Authorization: Bearer $SURAD_API_KEY" -H "Content-Type: application/json" \
-d '{"agent_id":"'$AGENT_ID'","origin":"https://app.example.com","ttl_seconds":900}'
# 2) واجهتك: افتح جلسة بالرمز ونطاقك
curl -X POST https://app.surad-ai.com/api/v1/widget/sessions \
-H "Authorization: Bearer $TOKEN" -H "Origin: https://app.example.com" \
-H "Content-Type: application/json" -d '{"mode":"text"}'
# 3) دور نصي
curl -X POST https://app.surad-ai.com/api/v1/widget/conversations/$CONVERSATION_ID/turns \
-H "Authorization: Bearer $TOKEN" -H "Origin: https://app.example.com" \
-H "Content-Type: application/json" -d '{"text":"هل عندكم توصيل اليوم؟","mode":"text"}'
# 4) رسالة صوتية (WAV)
curl -X POST https://app.surad-ai.com/api/v1/widget/conversations/$CONVERSATION_ID/voice/utterance \
-H "Authorization: Bearer $TOKEN" -H "Origin: https://app.example.com" \
-F "file=@question.wav;type=audio/wav"import base64, httpx
API = "https://app.surad-ai.com"
ORIGIN = "https://app.example.com"
def new_session(api_key: str, workspace_id: str, agent_id: str) -> tuple[str, str]:
minted = httpx.post(
f"{API}/api/v1/workspaces/{workspace_id}/widget/session-tokens",
headers={"Authorization": f"Bearer {api_key}"},
json={"agent_id": agent_id, "origin": ORIGIN, "ttl_seconds": 900},
).raise_for_status().json()
token = minted["token"]
opened = httpx.post(
f"{API}/api/v1/widget/sessions",
headers={"Authorization": f"Bearer {token}", "Origin": ORIGIN},
json={"mode": "voice"},
).raise_for_status().json()
return token, opened["conversation"]["conversation_id"]
def ask(token: str, conversation_id: str, text: str) -> dict:
turn = httpx.post(
f"{API}/api/v1/widget/conversations/{conversation_id}/turns",
headers={"Authorization": f"Bearer {token}", "Origin": ORIGIN},
json={"text": text, "mode": "voice"},
timeout=60,
).raise_for_status().json()
if turn.get("audio"):
with open("reply.wav", "wb") as f:
f.write(base64.b64decode(turn["audio"]["content_base64"]))
return turn["reply"] # {"display": "...", "spoken": "..."}الأخطاء وحدود الاستخدام وصيغ الصوت في المرجع.
