# Sonecentral metin asistanı API'si: dokümanın tamamı > Sonecentral metin asistanını kendi uygulamanızdan ve sitenizden çağırmak için genel API. OpenAI Chat Completions, OpenAI Responses ve Vercel AI SDK biçimleriyle çalışır; yanıtı WhatsApp'taki aynı asistan verir. Kaynak: https://docs.sonecentral.com/. Bu dosya https://docs.sonecentral.com/llms-full.txt adresinde yayınlanır ve her derlemede yeniden üretilir. Sayfalar kenar menüsündeki sırayla gelir; her sayfa kendi başlığı ve adresiyle başlar. --- # Giriş Sayfa: https://docs.sonecentral.com/ Markdown: https://docs.sonecentral.com/index.md > Sonecentral metin asistanınızı kendi uygulamanızdan ve sitenizden, sektörün bildiği OpenAI ve Vercel AI SDK biçimleriyle çağırın. > **Dokümanı ajanınıza verin.** Dokümanın tamamı tek bir Markdown dosyasında: https://docs.sonecentral.com/llms-full.txt . Kodlama ajanınıza bu adresi yapıştırın. Tek bir sayfa için o sayfanın .md adresini verin, örneğin https://docs.sonecentral.com/hizli-baslangic.md . Sayfaların listesi: https://docs.sonecentral.com/llms.txt ## Bu API ne yapar Sonecentral'da kurduğunuz metin asistanı WhatsApp'ta, Instagram'da ve Messenger'da müşterilerinize yanıt veriyor. Bu API aynı asistanı sizin uygulamanıza, sitenize ya da arka uç kodunuza açar. Yanıtı bir dil modeli değil, **sizin asistanınız** verir. Panelde yazdığınız talimat ve çalışma alanınızda tanımlı her şey burada WhatsApp, Instagram ve Messenger'daki gibi aynen geçerlidir: hizmetleriniz, ürünleriniz ve stokunuz, katalog öğeleriniz, ekibiniz, bölümleriniz, çalışma saatleriniz, işletme bilgileriniz, kampanyalarınız, kargo kurallarınız, fiyat listeniz ve iskonto yetkiniz, Bilgi Bankası'ndaki belgeler ve onlardan çıkarılan işletme verileri, sipariş ve randevu talepleri, ekibe devir. Çünkü ortada ikinci bir asistan yok: istek, WhatsApp mesajını yanıtlayan aynı çalışma zamanından geçer. Tek fark ziyaretçinin anonim olmasıdır. Asistan kişinin kendi kayıtlarına bakmaz, randevuyu kendisi kesinleştirmez; her sipariş ve randevu ekibinizin onayını bekleyen bir taleptir. ## Adres ve biçimler Bütün istekler tek adrese gider: ```txt https://pb.sonecentral.com/api/v1 ``` Üç biçimden birini seçin. Üçü de aynı asistanla, aynı sohbet kaydıyla çalışır. | Uç nokta | Biçim ve ne zaman | | --- | --- | | `POST /chat/completions` | OpenAI Chat Completions. OpenAI SDK'sı ya da onunla uyumlu bir istemci zaten elinizdeyse. | | `POST /responses` | OpenAI Responses. Sohbeti `previous_response_id` ile sürdürmek istiyorsanız. | | `POST /chat` | Vercel AI SDK. React ya da Next.js'te `useChat` ile sohbet penceresi yapıyorsanız. | `model` alanına asistanınızın kimliğini yazarsınız. Kimlikleri [Asistanlar ve modeller](https://docs.sonecentral.com/asistanlar-ve-modeller.md) sayfasındaki `GET /models` listeler. Anahtarınız tek bir asistana açıksa `"default"` yazmanız yeterli. ## Bilinmesi gerekenler - **Ziyaretçiler anonimdir.** API'den gelen kişinin kimliği doğrulanmaz. Asistan ona ürün, fiyat, stok ve randevu uygunluğu gibi herkese açık konularda yardım eder; geçmiş siparişler ya da mevcut randevular gibi kişisel kayıtları paylaşmaz. Ayrıntılar: [Siparişler, randevular ve gizlilik](https://docs.sonecentral.com/siparis-randevu-gizlilik.md). - **Yanıt balon balon gelir.** İşletmenin güvenlik kontrolleri yanıtın tamamına bakar, metin bu kontrollerden sonra yola çıkar. Akışta harf harf değil, mesaj mesaj ilerlersiniz. Ayrıntılar: [Akış](https://docs.sonecentral.com/akis.md). - **Zengin mesajlar düz metinle birlikte gelir.** Ürün kartları, düğmeler ve dosyalar yapılandırılmış veri olarak gelir; her birinin düz metin karşılığı da vardır. Ayrıntılar: [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md). - **Sohbetler Mesajlar ekranında görünür.** API'den açılan her sohbet Sonecentral'daki Mesajlar ekranında **API** kaynağıyla, ziyaretçi **API ziyaretçisi** adıyla görünür. Asistan gerektiğinde sohbeti ekibinize devreder; ekip oradan yazar, ziyaretçi bu yanıtları API'den okur. Ayrıntılar: [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md). ## Uç noktalar | Uç nokta | Açıklama | | --- | --- | | `GET /models` | Anahtarın kullanabildiği asistanlar. [Asistanlar ve modeller](https://docs.sonecentral.com/asistanlar-ve-modeller.md) | | `GET /models/{id}` | Tek bir asistan. | | `POST /chat/completions` | OpenAI Chat Completions, akışlı ya da akışsız. [Chat Completions](https://docs.sonecentral.com/chat-completions.md) | | `POST /responses` | OpenAI Responses, akışlı ya da akışsız. [Responses API](https://docs.sonecentral.com/responses.md) | | `GET /responses/{id}` | Saklanan bir yanıt. | | `DELETE /responses/{id}` | Saklanan bir yanıtı siler. | | `GET /responses/{id}/input_items` | Bir yanıtın girdi öğeleri. | | `POST /responses/{id}/cancel` | Yanıt varsa her zaman `400 not_cancellable` döner, yoksa `404 response_not_found`; yanıtlar istek içinde tamamlanır. | | `POST /chat` | Vercel AI SDK UI mesaj akışı, yani `useChat`. [Vercel AI SDK](https://docs.sonecentral.com/vercel-ai-sdk.md) | | `GET /chat/{id}/stream` | `useChat({ resume: true })` için. Her zaman `204` döner. [Bağlantı koparsa](https://docs.sonecentral.com/vercel-ai-sdk.md#baglanti-koparsa) | | `GET /conversations/{id}` | Sohbetin kimde olduğu: asistanda ya da ekipte. [Sohbetin durumu](https://docs.sonecentral.com/insan-devri.md#sohbetin-durumu) | | `GET /conversations/{id}/messages` | Bir sohbetin mesajları; ekip yanıtlarını okumak için. [İnsan devri](https://docs.sonecentral.com/insan-devri.md) | | `GET /files/{id}` | Asistanın paylaştığı bir dosyanın imzalı indirme adresi. Anahtar istemez. [Dosyayı indirmek](https://docs.sonecentral.com/zengin-mesajlar.md#dosya-indirme) | | `GET /key` | Kullandığınız anahtarın özeti. [Kimlik doğrulama](https://docs.sonecentral.com/kimlik-dogrulama.md#anahtari-denetlemek) | Hepsi `OPTIONS` ön isteğine yanıt verir, bu yüzden tarayıcıdan tarayıcı anahtarıyla çağrılabilir. `GET /files/{id}` dışındaki her uç nokta anahtar ister ve anahtarın [sınırlarına](https://docs.sonecentral.com/limitler.md) sayılır. ## Sonraki adım [Hızlı başlangıç](https://docs.sonecentral.com/hizli-baslangic.md) ile anahtarınızı oluşturup ilk isteği gönderin. --- # Hızlı başlangıç Sayfa: https://docs.sonecentral.com/hizli-baslangic Markdown: https://docs.sonecentral.com/hizli-baslangic.md > Anahtarınızı oluşturun, asistanınızın kimliğini bulun ve ilk mesajı gönderin. > **Dokümanı ajanınıza verin.** Dokümanın tamamı tek bir Markdown dosyasında: https://docs.sonecentral.com/llms-full.txt . Kodlama ajanınıza bu adresi yapıştırın. Tek bir sayfa için o sayfanın .md adresini verin, örneğin https://docs.sonecentral.com/hizli-baslangic.md . Sayfaların listesi: https://docs.sonecentral.com/llms.txt Bir kodlama ajanıyla çalışıyorsanız işin en kısa yolu bu: yukarıdaki adresi ajanınıza verin, ne yapmak istediğinizi söyleyin. Kendiniz yazacaksanız aşağıdaki dört adım yeterli. ### Anahtar oluşturun Sonecentral'da **Metin asistanı** sayfasını açın ve **API anahtarları** bölümünden yeni anahtar oluşturun. Bu bölümü yalnız çalışma alanının yöneticileri görür. **Tür** alanında iki seçenek var: - **Sunucu anahtarı** `sk-sone-v1-` ile başlar ve arka uç kodunuz içindir. Tarayıcıya, mobil uygulama paketine ya da herkese açık bir depoya asla koymayın. - **Tarayıcı anahtarı** `pk-sone-v1-` ile başlar ve sitenizdeki sohbet penceresi içindir. Yalnız izin verdiğiniz sitelerden çalışır. Anahtar yalnız oluşturulduğu anda bir kez gösterilir. Hemen ortam değişkeni olarak saklayın: ```bash export SONE_API_KEY="sk-sone-v1-..." ``` Ayrıntılar: [Kimlik doğrulama ve anahtarlar](https://docs.sonecentral.com/kimlik-dogrulama.md). ### Asistanınızın kimliğini bulun ```bash curl https://pb.sonecentral.com/api/v1/models \ -H "Authorization: Bearer $SONE_API_KEY" ``` Listedeki `id`, `model` alanına yazacağınız değerdir. Anahtar tek bir asistana açıksa `"default"` yazmanız da yeter. ### İlk mesajı gönderin **curl** ```bash curl https://pb.sonecentral.com/api/v1/chat/completions \ -H "Authorization: Bearer $SONE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "default", "messages": [ { "role": "user", "content": "Yarın öğleden sonra randevu alabilir miyim?" } ] }' ``` **OpenAI JS** ```ts import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'https://pb.sonecentral.com/api/v1', apiKey: process.env.SONE_API_KEY, }); const completion = await client.chat.completions.create({ model: 'default', messages: [{ role: 'user', content: 'Yarın öğleden sonra randevu alabilir miyim?' }], }); console.log(completion.choices[0].message.content); ``` **OpenAI Python** ```python import os from openai import OpenAI client = OpenAI( base_url="https://pb.sonecentral.com/api/v1", api_key=os.environ["SONE_API_KEY"], ) completion = client.chat.completions.create( model="default", messages=[{"role": "user", "content": "Yarın öğleden sonra randevu alabilir miyim?"}], ) print(completion.choices[0].message.content) ``` **Vercel AI SDK** ```ts import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; const sone = createOpenAI({ baseURL: 'https://pb.sonecentral.com/api/v1', apiKey: process.env.SONE_API_KEY, }); const { text } = await generateText({ model: sone.chat('default'), prompt: 'Yarın öğleden sonra randevu alabilir miyim?', }); console.log(text); ``` Kurulum: `npm i openai`, `pip install openai` ya da `npm i ai @ai-sdk/openai`. Örnekler `openai` 7 (Node), `openai` 2 (Python), `ai` 7 ve `@ai-sdk/openai` 4 ile denendi. ### Sohbeti sürdürün Yanıtta `choices[0].message.sone.conversation_id` gelir. Sonraki mesajda bunu `metadata` içinde geri gönderin; asistan önceki mesajları hatırlar. Yalnız yeni mesajı gönderdiğiniz için her mesaja bir `Idempotency-Key` de ekleyin: ağ hatasından sonra aynı anahtarla yeniden denerseniz mesaj iki kez kaydedilmez. ```ts // `sone` OpenAI tiplerinde yok; tipini Chat Completions sayfasında bulursunuz. const { sone } = completion.choices[0].message as unknown as { sone: { conversation_id: string }; }; await client.chat.completions.create( { model: 'default', metadata: { conversation_id: sone.conversation_id }, messages: [{ role: 'user', content: 'Saat dört uygun mu?' }], }, { headers: { 'Idempotency-Key': crypto.randomUUID() } }, ); ``` Başka yollar, yeniden deneme ve tarayıcıda saklama: [Sohbet sürekliliği](https://docs.sonecentral.com/sohbet-surekliligi.md). ## Buradan nereye - Sitenize sohbet penceresi koymak: [Site içine sohbet penceresi](https://docs.sonecentral.com/sohbet-penceresi.md) - React ve Next.js: [Vercel AI SDK](https://docs.sonecentral.com/vercel-ai-sdk.md) - Ürün kartları ve düğmeler: [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md) --- # Kimlik doğrulama ve anahtarlar Sayfa: https://docs.sonecentral.com/kimlik-dogrulama Markdown: https://docs.sonecentral.com/kimlik-dogrulama.md > Sunucu anahtarı ile tarayıcı anahtarı arasındaki fark, anahtarın nasıl gönderildiği, izin verilen siteler ve anahtarın nasıl iptal edildiği. ## İki anahtar türü | | Sunucu anahtarı | Tarayıcı anahtarı | | --- | --- | --- | | Biçim | `sk-sone-v1-` ve 48 harf ya da rakam | `pk-sone-v1-` ve 48 harf ya da rakam | | Nerede | Arka uç kodu, sunucusuz işlev, betik | Sitenizdeki sohbet penceresi | | Tarayıcıdan | Reddedilir (`403 secret_key_in_browser`) | Yalnız izin verilen sitelerden | | `user` ile sohbet bulma | Var | Yok, gönderilen değer yok sayılır | | Geliştirici bağlamı | Var | Yok, gönderilen bağlam yok sayılır | | Yeni sohbete aktarılan geçmiş | Ziyaretçi ve asistan satırları | Yalnız ziyaretçinin satırları | | İstek sınırı | Anahtar başına dakikada 120 | Ziyaretçi adresi başına dakikada 30; anahtar başına saatte 1.200 istek ve 20.000 okuma | | Otomatik yanıt sınırı | Çalışma alanının sınırı: saatte 60, günde 300 tur | Bu sınırın içinde bütün tarayıcı anahtarlarının ortak payı: saatte 40, günde 200 tur | Anahtarları Sonecentral'da **Metin asistanı** sayfasının **API anahtarları** bölümünde oluşturur, düzenler ve iptal edersiniz. Bu bölümü yalnız çalışma alanının yöneticileri görür. Bir çalışma alanında aynı anda en fazla 50 etkin anahtar olabilir; iptal edilen anahtarlar bu sayıya girmez. ## Anahtarı göndermek Anahtarı `Authorization` başlığında taşıyın: ```http Authorization: Bearer sk-sone-v1-... ``` `x-api-key` başlığı da kabul edilir. İkisi birden gelirse `Authorization` kullanılır. ```http x-api-key: sk-sone-v1-... ``` OpenAI SDK'ları ve Vercel AI SDK, `apiKey` seçeneğine verdiğiniz değeri zaten `Authorization: Bearer` olarak gönderir. ## Anahtar ne zaman gösterilir Anahtarın tamamı yalnız oluşturulduğu anda, bir kez gösterilir. Sonecentral onu saklamaz, yalnız özetini tutar; listede ilk karakterleri ve son dört karakteri görürsünüz. İstek kayıtlarımız da anahtarı tutmaz; `Authorization` ile de, `x-api-key` ile de gönderilse. Anahtarı kaybettiyseniz yenisini oluşturup eskisini iptal edin. ## Anahtarın ayarları Anahtar oluşturulurken şunlar seçilir: - **Ad.** Anahtarı listede tanımanız için, en fazla 80 karakter. `GET /key` yanıtında da görünür; gizli bilgi yazmayın. - **Tür.** Sunucu anahtarı ya da tarayıcı anahtarı. Tür sonradan değiştirilemez; gerekirse yeni anahtar oluşturun. - **Asistanlar.** Anahtarın çağırabileceği asistanlar. Hepsi seçilirse çalışma alanında yayında olan bütün asistanlar, sonradan yayınlananlar da. Henüz yayında olmayan bir asistan da seçilebilir; anahtar onunla asistan yayınlandığında çalışır. - **İzin verilen siteler.** Yalnız tarayıcı anahtarında. En az bir, en fazla 20 site. - **Sipariş ve randevu talebi alabilir.** Açıksa asistan bu sohbetlerde yeni sipariş ya da randevu talebi açabilir; talepler ekibinizin onayını bekler. Kapalıysa asistan isteği ayrıntılarıyla ekibe iletir. Varsayılan olarak açıktır. Ayrıntılar: [Siparişler, randevular ve gizlilik](https://docs.sonecentral.com/siparis-randevu-gizlilik.md). Tür dışındaki ayarlar sonradan düzenlenebilir; değişiklik bir sonraki istekte geçerli olur. ## İzin verilen siteler Tarayıcı, siteler arası her istekte `Origin` başlığını kendisi gönderir. Tarayıcı anahtarı yalnız `Origin` değeri listedeki sitelerden biriyle aynıysa kabul edilir. `Origin` başlığı olmayan bir istekte tarayıcı anahtarı kullanılamaz. Site adresleri şöyle yazılır: - `https://www.ornek.com` gibi, yalnız şema ve alan adı. Alan adında en az bir nokta olmalıdır. 443 dışında bir port gerekiyorsa `https://www.ornek.com:8443` gibi yazılır. - Yol, sorgu, `*` joker karakteri, IP adresi ve `https://localhost` kabul edilmez. - `https://ornek.com` ile `https://www.ornek.com` iki ayrı sitedir; ikisini de kullanıyorsanız ikisini de ekleyin. - Geliştirme için yalnız `http://localhost` ve `http://127.0.0.1` eklenebilir, portlu ya da portsuz: `http://localhost:3000`, `http://127.0.0.1:5173`. Bunun dışında `http` kabul edilmez. Alan adı küçük harfe çevrilerek karşılaştırılır. Listede olmayan bir siteden gelen istek `403 origin_not_allowed` ile reddedilir. ## Sunucu anahtarı tarayıcıda çalışmaz Sunucu anahtarı sitenizin koduna girerse onu herkes okuyabilir ve sizin adınıza istek gönderebilir. Bunun önüne geçmek için Sonecentral, `Origin` başlığında anahtarın listelemediği bir site taşıyan istekte sunucu anahtarını `403 secret_key_in_browser` ile reddeder. Panel sunucu anahtarına site eklemez; bu yüzden tarayıcıdan gelen her istek reddedilir. Tarayıcıda her zaman tarayıcı anahtarını kullanın. Bu bir güvenlik ağıdır, garanti değildir. Bilgisayardaki bir dosyadan ya da korumalı bir çerçeveden açılan sayfalar `Origin: null` gönderir. Sonecentral böyle bir isteği sunucudan gelen bir istekten ayıramaz ve reddetmez. Sunucu anahtarını tarayıcıya, mobil uygulama paketine ya da herkese açık bir depoya hiçbir zaman koymayın. > **Dikkat** > > OpenAI SDK'sının `dangerouslyAllowBrowser` seçeneği bu kuralı aşmaz. Tarayıcıda sunucu anahtarı yerine tarayıcı anahtarı kullanın. Tarayıcıdaki kodunuz yanıtın şu başlıklarını okuyabilir: `x-request-id`, `retry-after`, `x-should-retry` ve `x-sone-conversation-id`. ## Geliştirici bağlamı Sunucu anahtarıyla gönderdiğiniz `system` ve `developer` rolündeki mesajlar, Responses'taki `instructions` alanı ve Vercel AI SDK'daki `context` alanı asistana **yalnız bilgi olarak** iletilir. Örneğin ziyaretçinin hangi ürün sayfasında olduğunu ya da uygulamanızdaki dil tercihini bildirebilirsiniz. Bu metin işletmenin talimatını ve kurallarını değiştiremez; içindeki talimatlara uyulmaz. Birden çok bağlam alt alta birleştirilir ve en fazla 2.000 karakteri dikkate alınır. Responses'taki `instructions` 2.000 karakterden ya da JSON olarak 4 KB'tan uzunsa istek `400 instructions_too_long` alır. Bağlam, gönderildiği tur için geçerlidir: sonraki istekte göndermezseniz kalkar. Tarayıcı anahtarıyla gönderilen geliştirici bağlamı yok sayılır ve saklanmaz, çünkü onu ziyaretçi de değiştirebilir. ## İptal Anahtarı **API anahtarları** bölümünden iptal ettiğiniz anda kullanılamaz olur; sonraki istekler `401 revoked_api_key` alır. İptal geri alınmaz, iptal edilen anahtar düzenlenemez; gerekirse yeni anahtar oluşturun. Anahtarın açtığı sohbetler Mesajlar ekranında kalır. İptal edilen anahtarın sakladığı yanıtlar 30 gün sonra kendiliğinden silinir. Aynı asistanı çağırabilen yeni bir anahtar onları okuyabilir ve `previous_response_id` ile sürdürebilir, ama silemez; `instructions`, `safety_identifier` ve `user` alanlarını da göremez: [Saklanan yanıtlar](https://docs.sonecentral.com/responses.md#saklanan-yanitlar). ## Anahtarı denetlemek `GET /key` kullandığınız anahtarın özetini döner. Anahtarın doğru çalışma alanına ve asistanlara bağlı olduğunu kurulumda bununla doğrulayabilirsiniz. ```bash curl https://pb.sonecentral.com/api/v1/key \ -H "Authorization: Bearer $SONE_API_KEY" ``` ```json { "data": { "label": "pk-sone-v1-Ab3…", "name": "Web sitesi sohbeti", "kind": "publishable", "assistants": ["k3m9x2p7q1w8e4r"], "requests": true, "allowed_origins": ["https://www.ornek.com"], "limits": { "requests_per_minute_per_visitor": 30, "requests_per_hour": 1200, "read_requests_per_hour": 20000, "assistant_turns_per_hour": 60, "assistant_turns_per_day": 300, "browser_assistant_turns_per_hour": 40, "browser_assistant_turns_per_day": 200 } } } ``` | Alan | Tür | Açıklama | | --- | --- | --- | | `label` | string | Anahtarın ilk 14 karakteri ve üç nokta. Anahtarın tamamı hiçbir yanıtta yer almaz. | | `name` | string | Anahtara Sonecentral'da verdiğiniz ad. | | `kind` | "secret" \| "publishable" | secret sunucu anahtarı, publishable tarayıcı anahtarıdır. | | `assistants` | string[] | Anahtarın şu an çağırabildiği, yayında olan asistanların kimlikleri, adlarına göre sıralı. | | `requests` | boolean | Sipariş ve randevu talebi alabilir ayarı açık mı. | | `allowed_origins` | string[] | İzin verilen siteler. Sunucu anahtarında boştur. | | `limits` | object | Sunucu anahtarında requests_per_minute. Tarayıcı anahtarında requests_per_minute_per_visitor, requests_per_hour (okuma dışındaki istekler), read_requests_per_hour (okumalar) ve tarayıcı anahtarlarının otomatik yanıt payı: browser_assistant_turns_per_hour ve browser_assistant_turns_per_day. İkisinde de çalışma alanının API için saatlik ve günlük otomatik yanıt sınırı: assistant_turns_per_hour ve assistant_turns_per_day. | Sunucu anahtarında `limits` şöyledir: ```json { "requests_per_minute": 120, "assistant_turns_per_hour": 60, "assistant_turns_per_day": 300 } ``` `GET /key` isteği de bir okumadır ve anahtarın sınırına sayılır: [Limitler](https://docs.sonecentral.com/limitler.md). ## Hatalar | Durum | `code` | Ne zaman | | --- | --- | --- | | `401` | `missing_api_key` | Anahtar gönderilmedi. | | `401` | `invalid_api_key` | Anahtar tanınmadı, biçimi bozuk ya da öneki türüyle uyuşmuyor. | | `401` | `revoked_api_key` | Anahtar iptal edilmiş. | | `403` | `origin_not_allowed` | Tarayıcı anahtarı listede olmayan bir siteden ya da `Origin` başlığı olmadan kullanıldı. | | `403` | `secret_key_in_browser` | Sunucu anahtarı, anahtarın listelemediği bir sitedeki tarayıcıdan kullanıldı. | | `429` | `rate_limit_exceeded` | Anahtarın istek ya da okuma sınırı aşıldı. `Retry-After` kadar bekleyin. | Bütün hata kodları: [Hatalar](https://docs.sonecentral.com/hatalar.md). --- # Asistanlar ve modeller Sayfa: https://docs.sonecentral.com/asistanlar-ve-modeller Markdown: https://docs.sonecentral.com/asistanlar-ve-modeller.md > model alanına ne yazılır, asistanlar nasıl listelenir ve "default" ne anlama gelir. OpenAI biçiminde `model` bir dil modelinin adıdır. Burada ise **asistanınızın kimliğidir**: hangi asistanın yanıt vereceğini seçer. Dil modelini, talimatı ve araçları asistanın Sonecentral'daki ayarları belirler; istekten değiştirilemez. ## Asistan neleri kullanır API'deki asistan, WhatsApp'ta, Instagram'da ve Messenger'da yanıt veren asistanın kendisidir. Çalışma alanınızda tanımlı her şeyi orada nasıl kullanıyorsa burada da aynen öyle kullanır: - hizmetleriniz; fiyat, süre, görsel ve bağlantılarıyla, - ürünleriniz ve stokunuz; fiyat, görsel ve bağlantılarıyla, - katalog öğeleriniz ve paketleriniz, - ekibiniz, bölümleriniz ve çalışma saatleriniz, - işletme bilgileriniz: ad, adres ve telefon, - bağlı mağazanızın kargo kuralları ve kampanyaları, - fiyat listeniz ve tanımladıysanız iskonto yetkiniz, - Bilgi Bankası'ndaki belgeler, gönderilebilir olarak tanımladığınız belgeler ve belgelerinizden çıkarılan işletme verileri, - anahtarda talep ayarı açıksa sipariş ve randevu talepleri ile uygun randevu saatleri, - ekibe not bırakmak ve sohbeti ekibe devretmek. Ürün ve hizmet kartları, düğmeler ve dosyalar yapılandırılmış veri olarak gelir: [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md). API ziyaretçisinin kimliği doğrulanmadığı için asistan bilerek şunları yapmaz: kişinin kendi kayıtlarını (geçmiş siparişler, mevcut randevular, müşteri kartı, hatırlanan bilgiler, bağlı paneldeki hesap) okumaz ya da değiştirmez ve randevuyu kendisi kesinleştirmez. Ayrıntılar: [Siparişler, randevular ve gizlilik](https://docs.sonecentral.com/siparis-randevu-gizlilik.md). ## Asistanları listelemek ```bash curl https://pb.sonecentral.com/api/v1/models \ -H "Authorization: Bearer $SONE_API_KEY" ``` ```json { "object": "list", "data": [ { "id": "k3m9x2p7q1w8e4r", "object": "model", "created": 1760889600, "owned_by": "sonecentral", "name": "Mağaza asistanı" } ] } ``` Liste yalnız **anahtarın çağırabildiği** ve **yayında olan** asistanları içerir. Taslak ya da kurulumu bitmemiş bir asistan listede görünmez. Sıra, asistanların adına göredir. | Alan | Tür | Açıklama | | --- | --- | --- | | `id` | string | Asistanın kimliği. model alanına bu yazılır. | | `object` | "model" | Her zaman model. | | `created` | integer | Asistanın oluşturulduğu an, Unix saniyesi. | | `owned_by` | "sonecentral" | Her zaman sonecentral. | | `name` | string | Asistanın paneldeki adı. | Tek bir asistan için `GET /models/{id}` aynı nesneyi döner. ```bash curl https://pb.sonecentral.com/api/v1/models/k3m9x2p7q1w8e4r \ -H "Authorization: Bearer $SONE_API_KEY" ``` OpenAI SDK'larında aynı şey `client.models.list()` ve `client.models.retrieve(id)` ile yapılır. `model` değerinde son `/` işaretinden öncesi yok sayılır. Bu yüzden model adlarının önüne sağlayıcı adı koyan araçlarda `sonecentral/k3m9x2p7q1w8e4r` gibi bir değer de aynı asistanı seçer. ## `"default"` `model` boş bırakılırsa ya da `"default"` yazılırsa asistan şu sırayla seçilir: 1. Anahtar yalnız bir asistanı çağırabiliyorsa o asistan. 2. Değilse, anahtar onu çağırabiliyorsa çalışma alanının varsayılan metin asistanı. 3. O da yoksa anahtarın çağırabildiği asistanlardan adı alfabede ilk gelen. Anahtarınız birden çok asistana açıksa ve hangisinin yanıt vereceği önemliyse kimliği açıkça yazın. ## Sohbet ve asistan Bir sohbet açıldığı asistana bağlıdır. Aynı sohbeti başka bir `model` ile sürdürmeye çalışırsanız istek `400 conversation_model_mismatch` alır. Aynı asistanla sürdürün ya da yeni bir sohbet başlatın. ## Hatalar | Durum | `code` | Ne zaman | | --- | --- | --- | | `404` | `model_not_found` | Bu kimlikte yayında bir asistan yok ya da anahtar onu çağıramaz. Asistan taslaktaysa ya da kurulumu bitmemişse de bu kod gelir. | | `400` | `conversation_model_mismatch` | Sohbet başka bir asistanla açılmış. | --- # Chat Completions Sayfa: https://docs.sonecentral.com/chat-completions Markdown: https://docs.sonecentral.com/chat-completions.md > POST /chat/completions ile OpenAI Chat Completions biçiminde mesaj gönderin; yanıtın sone alanı sohbetin kimliğini, durumunu ve zengin mesajları taşır. ```http POST https://pb.sonecentral.com/api/v1/chat/completions ``` OpenAI Chat Completions'ın istek ve yanıt biçimini kullanır. OpenAI SDK'ları, LangChain ve OpenAI uyumlu her istemci `baseURL` değiştirilerek çalışır. ## İstek ```json { "model": "default", "messages": [ { "role": "user", "content": "Bu hafta sonu açık mısınız?" } ], "metadata": { "conversation_id": "conv_x8k2m9p4q7w1r3t" }, "stream": false } ``` | Alan | Tür | Açıklama | | --- | --- | --- | | `model` | string | Asistanın kimliği ya da "default". Asistanlar ve modeller sayfası kimliklerin nasıl bulunduğunu anlatır. Zorunlu. | | `messages` | array | Sohbetin mesajları. Son assistant mesajından sonraki user mesajları yeni mesaj sayılır, en fazla 12 tane. Öncekiler yalnız sohbeti bulmak ve yeni bir sohbeti başlatmak için okunur. Zorunlu. | | `stream` | boolean | true ise yanıt sunucu tarafından gönderilen olaylarla, yani SSE ile akar. Varsayılan: false. | | `stream_options` | object | include_usage: true ise akışın sonunda kullanım bilgisi gelir. | | `conversation_id` | string | Sonecentral uzantısı. Sürdürülecek sohbetin kimliği. | | `metadata` | object | Yalnız metadata.conversation_id okunur ve conversation_id ile aynı işi görür. Geri kalanı kullanılmaz, saklanmaz. | | `user` | string | Yalnız sunucu anahtarıyla dikkate alınır. Kendi sisteminizdeki ziyaretçi kimliği; aynı değer aynı sohbeti bulur. | | `safety_identifier` | string | user ile aynı işi görür. İkisi birden gelirse bu kullanılır. | Sohbetin kimliği birden çok yoldan gelebilir. Sonecentral ilk dolu olanı şu sırayla kullanır: 1. Gövdedeki `conversation_id`. 2. Gövdedeki `sone.conversation_id`. 3. `X-Sone-Conversation-Id` başlığı. 4. `metadata.conversation_id`. 5. Geçmişe olduğu gibi eklediğiniz bir asistan mesajının `sone.conversation_id` alanı. Birden çok varsa en sonuncusu. Kimlik hiç gelmezse sohbet başka yollarla aranır: [Sohbet sürekliliği](https://docs.sonecentral.com/sohbet-surekliligi.md#sohbet-nasil-bulunur). ### Mesaj rolleri | Rol | Ne olur | | --- | --- | | `user` | Ziyaretçinin mesajı. `content` metin ya da `text` parçalarından oluşan bir dizi olabilir. | | `assistant` | Önceki asistan yanıtı. Sohbeti bulmak için okunur. Sunucu anahtarıyla yeni bir sohbet açılırken geçmiş olarak da aktarılır. | | `system`, `developer` | Sunucu anahtarıyla asistana yalnız bilgi olarak iletilir, en fazla 2.000 karakter. Tarayıcı anahtarıyla yok sayılır. Ayrıntılar: [Geliştirici bağlamı](https://docs.sonecentral.com/kimlik-dogrulama.md#gelistirici-baglami). | | `tool`, `function` | Yok sayılır. | Başka bir rol `400 invalid_role` alır. Son mesaj metin taşıyan bir `user` mesajı değilse istek `400 missing_user_message` alır. Yeni bir mesaj 4.000 karakterden uzunsa `400 message_too_long` gelir. Yeni mesajda görsel, ses ya da dosya parçası varsa istek `400 unsupported_content` alır. Önceki mesajlardaki bu tür parçalar sessizce atlanır, yalnız metinleri okunur. ### Yok sayılan ve desteklenmeyen alanlar Asistanın dil modelini, üslubunu ve araçlarını Sonecentral'daki ayarlar belirler. Bu yüzden şu alanlar **yok sayılır**, hata vermez: `temperature`, `top_p`, `max_tokens`, `max_completion_tokens`, `presence_penalty`, `frequency_penalty`, `seed`, `stop`, `logit_bias`, `logprobs`, `top_logprobs`, `reasoning_effort`, `verbosity`, `service_tier`, `store`, `parallel_tool_calls`, `tool_choice`, `function_call`, `audio`, `modalities`. Burada sayılmayan bilinmeyen alanlar da yok sayılır. Şunlar **reddedilir**: | Alan | Hata | | --- | --- | | `tools`, `functions` | `400 tools_not_supported`. Asistan işletmenin kendi araçlarını kullanır. | | `n`, 1 dışında | `400 unsupported_parameter` | | `response_format`, `text` dışında | `400 unsupported_parameter` | ## Yanıt ```json { "id": "chatcmpl-9f2c4a7e1b8d3f6a0c5e2b9d", "object": "chat.completion", "created": 1760866000, "model": "k3m9x2p7q1w8e4r", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Evet, cumartesi 10.00 ile 18.00 arası açığız. Pazar kapalıyız.", "refusal": null, "annotations": [], "sone": { "conversation_id": "conv_x8k2m9p4q7w1r3t", "status": "answered", "messages": [ { "id": "msg_p4q7w1x8k2m9a5z", "role": "assistant", "type": "text", "text": "Evet, cumartesi 10.00 ile 18.00 arası açığız. Pazar kapalıyız.", "created_at": 1760866000 } ] } }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 7, "completion_tokens": 16, "total_tokens": 23 }, "system_fingerprint": null, "sone": { "conversation_id": "conv_x8k2m9p4q7w1r3t", "status": "answered" } } ``` `model` yanıt veren asistanın kimliğidir; `"default"` gönderseniz de seçilen asistanın kimliği gelir. Sohbetin kimliği ayrıca `X-Sone-Conversation-Id` yanıt başlığında gelir. `content`, bu turda gönderilen bütün mesajların düz metnidir; birden çok mesaj araya boş satır, yani `\n\n` konularak birleştirilir. Yalnız `content` gösteren bir istemci de eksiksiz çalışır. `usage` bilgi içindir; gönderilen yeni mesajların ve gelen metnin uzunluğundan, yaklaşık dört karakter bir belirteç sayılarak tahmin edilir. Faturalama bu sayılara göre yapılmaz. ### `sone` alanı OpenAI biçimine eklenen alan budur. Tam hali `choices[0].message.sone` içinde gelir. Aynı özet, `messages` olmadan, yanıtın üst düzeyinde de vardır. | Alan | Tür | Açıklama | | --- | --- | --- | | `conversation_id` | string | Sohbetin kimliği, conv_ ile başlar. Sonraki isteklerde geri gönderin. | | `status` | "answered" \| "handoff" \| "human" \| "silent" | Bu turun sonucu. Değerler aşağıdaki tabloda. | | `messages` | SoneMessage[] | Bu turda gönderilen mesajlar, gönderildikleri sırayla. Biçimi Zengin mesajlar sayfasında. | | `status` | Anlamı | `content` | | --- | --- | --- | | `answered` | Asistan yanıt verdi. | Yanıt metni | | `handoff` | Sohbet bu turda ekibe geçti ve ziyaretçiye bir mesaj yazıldı, genellikle devri söyleyen mesaj. | Devir mesajı | | `human` | Sohbet ekipte. Asistan yanıt vermedi; mesaj ekibe ulaştı. | Boş | | `silent` | Yanıt gerekmedi, örneğin "teşekkürler" gibi bir kapanış. | Boş | `handoff` ve `human` durumunda ekibin yanıtlarını nasıl okuyacağınız [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md) sayfasında. TypeScript'te bu alanları kendiniz tanımlayın: ```ts title="sone.ts" import type { SoneMessage } from './sone-message'; // Zengin mesajlar sayfasındaki tanım export type SoneStatus = 'answered' | 'handoff' | 'human' | 'silent'; // Akışsız yanıtta choices[0].message.sone export type SoneTurn = { conversation_id: string; status: SoneStatus; messages: SoneMessage[]; }; // Akışta bir parçanın üst düzeyindeki sone: her parçada yalnız biri dolu export type SoneChunk = { message?: SoneMessage; // bir mesaj tamamlandı activity?: string; // asistan bir işe bakıyor: "Ürünlere bakılıyor" conversation_id?: string; // son parçada status?: SoneStatus; // son parçada }; export const soneOfMessage = (message: unknown) => (message as { sone?: SoneTurn }).sone; export const soneOfChunk = (chunk: unknown) => (chunk as { sone?: SoneChunk }).sone; ``` Python'da OpenAI modelleri bilinmeyen alanları korur: ```python sone = completion.choices[0].message.to_dict().get("sone", {}) print(sone.get("conversation_id"), sone.get("status")) ``` ## Akış `"stream": true` ile yanıt `text/event-stream` olarak gelir. Parçalar şu sırayla gelir: 1. **İlk parça**, anahtar, sınır ve paket kontrolleri geçip asistan yanıtlamaya başladığında gelir: `delta` rolü taşır. Bu kontrollerden geçmeyen istek akış yerine sıradan bir HTTP hatası alır. 2. Asistan bir araç kullanırken **durum parçası** gelebilir: `delta` boştur, üst düzeydeki `sone.activity` "Ürünlere bakılıyor" gibi kısa bir metindir. 3. Her mesajın metni kelime grupları halinde `delta.content` olarak akar. İkinci ve sonraki mesajlar `\n\n` ile başlar. Bir mesajın **son parçası** ayrıca üst düzeyde `sone.message` taşır: o mesajın tamamı, `SoneMessage` biçiminde. Metni boş bir mesaj, `delta`'sı boş tek bir parçayla gelir. 4. **Son parça**: `finish_reason` `"stop"`, `delta` boş, üst düzeyde `sone` içinde `conversation_id` ve `status`. 5. `stream_options.include_usage` açıksa `choices` dizisi boş, `usage` dolu bir parça daha gelir. Bu durumda önceki bütün parçalarda `"usage": null` vardır. 6. Akış `data: [DONE]` ile biter. `sone` her zaman parçanın üst düzeyindedir, `choices[0].delta` içinde değildir. Sohbetin kimliği akış başlarken `X-Sone-Conversation-Id` yanıt başlığında da gelir. ```txt data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":null}],"sone":{"activity":"Çalışma saatlerine bakılıyor"}} : ping data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"Evet, cumartesi 10.00 ile "},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"18.00 arası açığız. Pazar "},"logprobs":null,"finish_reason":null}]} data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"kapalıyız."},"logprobs":null,"finish_reason":null}],"sone":{"message":{"id":"msg_p4q7w1x8k2m9a5z","role":"assistant","type":"text","text":"Evet, cumartesi 10.00 ile 18.00 arası açığız. Pazar kapalıyız.","created_at":1760866000}}} data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"sone":{"conversation_id":"conv_x8k2m9p4q7w1r3t","status":"answered"}} data: [DONE] ``` Örnekteki kimlikler kısaltıldı. **OpenAI JS** ```ts import type { SoneMessage } from './sone-message'; import { soneOfChunk } from './sone'; const stream = await client.chat.completions.create({ model: 'default', messages: [{ role: 'user', content: 'Bu hafta sonu açık mısınız?' }], stream: true, }); const messages: SoneMessage[] = []; for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta.content ?? ''); const sone = soneOfChunk(chunk); if (sone?.message) messages.push(sone.message); // zengin mesajı burada çizin if (sone?.activity) console.log(`\n${sone.activity}…`); if (sone?.status) console.log('\nsohbet:', sone.conversation_id, sone.status); } ``` **OpenAI Python** ```python stream = client.chat.completions.create( model="default", messages=[{"role": "user", "content": "Bu hafta sonu açık mısınız?"}], stream=True, ) messages = [] for chunk in stream: if chunk.choices: print(chunk.choices[0].delta.content or "", end="", flush=True) sone = chunk.to_dict().get("sone") or {} if "message" in sone: messages.append(sone["message"]) # zengin mesajı burada çizin elif "activity" in sone: print(f"\n{sone['activity']}…") elif "status" in sone: print("\nsohbet:", sone["conversation_id"], sone["status"]) ``` Akış başladıktan sonra bir hata olursa `data: {"error": {…}}` parçası ve ardından `data: [DONE]` gelir. OpenAI SDK'ları bu parçayı hata olarak fırlatır. Ayrıntılar: [Hatalar](https://docs.sonecentral.com/hatalar.md#akis-sirasinda-hata). Metnin neden kelime kelime değil mesaj mesaj ilerlediği ve bağlantının nasıl canlı tutulduğu: [Akış](https://docs.sonecentral.com/akis.md). ## Tekrar denemek Her yeni mesajda yeni bir `Idempotency-Key` gönderin, en fazla 200 karakter. Ağ hatasından sonra aynı mesajı yeniden gönderirken aynı değeri koyun. Sonecentral aynı mesajı ikinci kez kaydetmez: yanıt verilmişse aynı yanıt yeniden döner, asistan iki kez yanıt vermez. Aynı anahtarı başka bir metinle gönderirseniz istek `422 idempotency_key_reused` alır. Tarayıcı anahtarıyla, sohbetin kimliği olmadan başka bir ağ adresinden gelen deneme, örneğin sohbetin ilk mesajı, yeni sohbet açar: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek). OpenAI SDK'ları bu başlığı kendileri eklemez. İsteğin seçeneklerinde verin; SDK'nın kendi yeniden denemeleri de aynı başlığı gönderir: **OpenAI JS** ```ts const key = crypto.randomUUID(); // mesaj başına bir kez üretin, yeniden denerken aynısını kullanın const completion = await client.chat.completions.create( { model: 'default', metadata: { conversation_id: 'conv_x8k2m9p4q7w1r3t' }, messages: [{ role: 'user', content: 'Bu hafta sonu açık mısınız?' }], }, { headers: { 'Idempotency-Key': key } }, ); ``` **OpenAI Python** ```python import uuid key = str(uuid.uuid4()) # mesaj başına bir kez üretin, yeniden denerken aynısını kullanın completion = client.chat.completions.create( model="default", metadata={"conversation_id": "conv_x8k2m9p4q7w1r3t"}, messages=[{"role": "user", "content": "Bu hafta sonu açık mısınız?"}], extra_headers={"Idempotency-Key": key}, ) ``` Bütün geçmişi ve sohbetin kimliğini her istekte gönderiyorsanız Sonecentral, ilk mesaj dışında, yeniden gönderilen mesajı geçmişin sonuna bakarak da tanır. Kimliği olmadan yalnız geçmişle gelen adsız bir deneme sohbeti bulmaz: yeni bir sohbet açar ve asistan yeniden yanıt verir. Sohbetin ilk mesajı, yalnız yeni mesajı gönderen ve kimliği saklamayan istemciler için tek güvenli yol anahtardır; bu yüzden her mesajda gönderin. Ayrıntılar: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek). ## Hatalar | Durum | `code` | Ne zaman | | --- | --- | --- | | `400` | `missing_user_message` | Son mesaj metin taşıyan bir `user` mesajı değil. | | `400` | `invalid_role` | Tanınmayan bir mesaj rolü. | | `400` | `invalid_content` | Bir mesajın `content` alanı metin ya da parça dizisi değil. | | `400` | `unsupported_content` | Yeni mesajda görsel, ses ya da dosya var. | | `400` | `message_too_long` | Yeni bir mesaj 4.000 karakterden uzun. | | `400` | `too_many_new_messages` | Son asistan mesajından sonra 12'den fazla `user` mesajı var. | | `400` | `tools_not_supported` | `tools` ya da `functions` gönderildi. | | `400` | `unsupported_parameter` | `n` 1 dışında ya da `response_format` `text` dışında. | | `400` | `conversation_model_mismatch` | Sohbet başka bir asistanla açılmış. | | `404` | `model_not_found` | Bu kimlikte, anahtarın çağırabildiği yayında bir asistan yok. | | `404` | `conversation_not_found` | Gönderilen sohbet kimliği bu çalışma alanında bir API sohbeti değil. | | `409` | `conversation_busy` | Aynı sohbette önceki mesaj hâlâ yanıtlanıyor. | | `422` | `idempotency_key_reused` | Aynı `Idempotency-Key` başka bir metinle gönderildi. | Bütün kodlar ve ne yapılacağı: [Hatalar](https://docs.sonecentral.com/hatalar.md). --- # Responses API Sayfa: https://docs.sonecentral.com/responses Markdown: https://docs.sonecentral.com/responses.md > POST /responses ile OpenAI Responses biçiminde konuşun; sohbeti previous_response_id ya da conversation ile sürdürün, saklanan yanıtları okuyun ve silin. ```http POST https://pb.sonecentral.com/api/v1/responses ``` OpenAI Responses biçimini kullanır. Sohbeti sürdürmek için önceki yanıtın kimliğini göndermeniz yeter; geçmişi her seferinde yeniden yollamazsınız. ## İlk mesaj ve devamı **OpenAI JS** ```ts const first = await client.responses.create({ model: 'default', input: 'Saç boyası için randevu almak istiyorum.', }); console.log(first.output_text); const second = await client.responses.create({ model: 'default', previous_response_id: first.id, input: 'Perşembe öğleden sonra olur mu?', }); console.log(second.output_text); ``` **OpenAI Python** ```python first = client.responses.create( model="default", input="Saç boyası için randevu almak istiyorum.", ) print(first.output_text) second = client.responses.create( model="default", previous_response_id=first.id, input="Perşembe öğleden sonra olur mu?", ) print(second.output_text) ``` **curl** ```bash curl https://pb.sonecentral.com/api/v1/responses \ -H "Authorization: Bearer $SONE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "default", "input": "Saç boyası için randevu almak istiyorum." }' ``` `output_text` SDK'nın kolaylığıdır: bütün `output_text` parçalarını birleştirir. ## İstek | Alan | Tür | Açıklama | | --- | --- | --- | | `model` | string | Asistanın kimliği ya da "default". Zorunlu. | | `input` | string \| array | Yeni mesaj. Metin ya da OpenAI girdi öğeleri: rolü olan message öğeleri ve item_reference. Mesajlarda input_text ve output_text parçaları okunur. Bir item_reference kimliği en fazla 64 karakterdir. Son öğe ziyaretçinin metin mesajı olmalıdır. Zorunlu. | | `previous_response_id` | string | Önceki yanıtın kimliği, resp_ ve 32 karakter. Aynı sohbet sürer. conversation ile birlikte gönderilemez. Bir yanıt kimliğinden uzun bir değer reddedilir. | | `conversation` | string \| { id: string } | Sohbetin kimliği, yani sone.conversation_id. previous_response_id yerine kullanılabilir. | | `store` | boolean | false ise yanıt saklanmaz; GET /responses/{id} ve previous_response_id onu bulamaz. Varsayılan: true. | | `instructions` | string | Sunucu anahtarıyla asistana yalnız bilgi olarak iletilir. En fazla 2.000 karakter ve JSON olarak 4 KB; daha uzunu reddedilir. Tarayıcı anahtarıyla yok sayılır, saklanmaz ve yanıtta yer almaz. Saklanan yanıtta onu yalnız yanıtı saklayan anahtar görür. | | `stream` | boolean | true ise yanıt olaylarla akar. Varsayılan: false. | | `user` | string | Yalnız sunucu anahtarıyla dikkate alınır. Kendi sisteminizdeki ziyaretçi kimliği; aynı değer aynı sohbeti bulur. En fazla 64 karakter; daha uzunu reddedilir. Yanıtta yer almaz ve saklanmaz. | | `safety_identifier` | string | user ile aynı işi görür. İkisi birden gelirse bu kullanılır. En fazla 64 karakter; daha uzunu reddedilir. Sunucu anahtarıyla yanıtta yer alır ve onunla saklanır; tarayıcı anahtarıyla yok sayılır, saklanmaz ve yanıtta yer almaz. | | `metadata` | object | Yanıtta olduğu gibi geri gelir, sohbeti bulmak için kullanılmaz. Saklanan yanıtta onu yalnız yanıtı saklayan anahtar görür. En fazla 16 anahtar; anahtar en fazla 64 karakter, değer en fazla 512 karakterlik metin; tamamı JSON olarak en fazla 4 KB. | `conversation` yerine `X-Sone-Conversation-Id` başlığını da kullanabilirsiniz. `conversation` ile `previous_response_id` birlikte gelirse istek `400 conflicting_parameters` alır. `input` içindeki `system` ve `developer` mesajları `instructions` gibi [geliştirici bağlamıdır](https://docs.sonecentral.com/kimlik-dogrulama.md#gelistirici-baglami). Geri gönderdiğiniz asistan mesajları ve `item_reference` öğeleri sohbeti bulmak için okunur: [Sohbet sürekliliği](https://docs.sonecentral.com/sohbet-surekliligi.md#sohbet-nasil-bulunur). `temperature`, `top_p`, `max_output_tokens`, `reasoning`, `text.verbosity`, `tool_choice`, `truncation` ve benzeri model ayarları yok sayılır. `max_output_tokens` yanıtta olduğu gibi geri gelir ama yanıtın uzunluğunu değiştirmez. Şunlar reddedilir: | Ne | Hata | | --- | --- | | `tools` ya da `function_call` gibi işlev çağrısı öğeleri | `400 tools_not_supported` | | `background: true` | `400 unsupported_parameter` | | `text.format`, `text` dışında | `400 unsupported_parameter` | | Yeni mesajda görsel ya da dosya parçası | `400 unsupported_content` | | Tanınmayan bir girdi öğesi türü | `400 unsupported_item` | `reasoning` öğeleri sessizce atlanır. ## Yanıt ```json { "id": "resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f", "object": "response", "created_at": 1760866200, "completed_at": 1760866203, "status": "completed", "model": "k3m9x2p7q1w8e4r", "previous_response_id": null, "conversation": { "id": "conv_x8k2m9p4q7w1r3t" }, "output": [ { "type": "message", "id": "msg_w8e4rk3m9x2pq7a", "status": "completed", "role": "assistant", "content": [ { "type": "output_text", "text": "Elbette. Hangi gün size uygun?", "annotations": [], "logprobs": [] } ], "sone": { "id": "msg_w8e4rk3m9x2pq7a", "role": "assistant", "type": "text", "text": "Elbette. Hangi gün size uygun?", "created_at": 1760866203 } } ], "usage": { "input_tokens": 10, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 8, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 18 }, "sone": { "conversation_id": "conv_x8k2m9p4q7w1r3t", "status": "answered" } } ``` Örnek kısaltıldı; yanıt OpenAI'nin tanımladığı diğer alanları da taşır. `usage` [Chat Completions](https://docs.sonecentral.com/chat-completions.md)'taki gibi uzunluktan tahmin edilir. Asistanın gönderdiği her mesaj ayrı bir `message` öğesidir; öğenin `id`'si mesajın Sonecentral'daki kimliğidir. Ürün kartı, düğme ya da dosya içeren bir mesajda da `content` düz metin karşılığını taşır; yapılandırılmış hali öğenin `sone` alanındadır. Biçimi: [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md). Yanıtın üst düzeyindeki `sone` sohbetin kimliğini ve turun durumunu verir. `status` değerleri [Chat Completions](https://docs.sonecentral.com/chat-completions.md) sayfasındakiyle aynıdır. `human` ve `silent` turlarında `output` boş olur. Sohbetin kimliği ayrıca `X-Sone-Conversation-Id` yanıt başlığında gelir. ## Saklanan yanıtlar `store` açıksa yanıt saklanır. Varsayılan budur. Saklanan yanıtlar 30 gün sonra silinir; sohbetin kendisi Mesajlar ekranında kalır. | Uç nokta | Ne yapar | | --- | --- | | `GET /responses/{id}` | Yanıtı aynı biçimde döner. | | `GET /responses/{id}/input_items` | Bu istekte gönderdiğiniz kendi girdi öğelerini liste olarak döner. Varsayılan sıra yeniden eskiye; `?order=asc` eskiden yeniye verir. Liste tek sayfadır, `has_more` her zaman `false` olur. | | `DELETE /responses/{id}` | Saklanan yanıtı siler: `{ "id": "resp_…", "object": "response.deleted", "deleted": true }`. Yalnız yanıtı saklayan anahtarla çalışır. Sohbetin kendisi silinmez. | | `POST /responses/{id}/cancel` | Yanıt varsa her zaman `400 not_cancellable` döner. Yanıtlar istek içinde tamamlanır, iptal edilecek arka plan işi yoktur. | Saklanan yanıt, girdiden yalnız isteğin kendi öğelerini tutar: ziyaretçinin yeni mesajları (son asistan öğesinden sonraki kullanıcı mesajları), `item_reference` öğeleri ve sunucu anahtarıyla `system` ile `developer` mesajları. Tarayıcı anahtarının `system` ve `developer` mesajları, `instructions` gibi, kullanılmaz ve saklanmaz. Yeniden gönderdiğiniz geçmiş saklanmaz; onu sohbetin kendisi tutar. Bu öğelerden en fazla 40'ı ve JSON olarak toplam 8 KB saklanır, en yenileri önce. En yeni öğe her zaman saklanır, 8 KB'a sığmıyorsa metni sonundan kısaltılarak; her öğenin metni en fazla 4.000 karakterdir. Kimliği olmayan öğelere, girdideki yerine göre `resp_…_in_0` gibi bir kimlik verilir. Yanıtla saklanan istek alanlarının hepsi sınırlıdır: `instructions` en fazla 2.000 karakter, `metadata` en fazla 16 anahtar, `safety_identifier` ve `user` en fazla 64 karakter, bir `item_reference` kimliği en fazla 64 karakter. `instructions` ve `metadata` ayrıca JSON olarak en fazla 4 KB olabilir: karakter sınırı içinde kalsa da, örneğin dört baytlık karakterlerle, daha büyük olanı reddedilir. Sınırı aşan istek `400` alır ve hiçbir şey saklanmaz. `previous_response_id` boşlukları atılmış haliyle saklanır; bir yanıt kimliğinden uzunsa istek `400 previous_response_id_too_long` alır. Aynı mesajı yeniden gönderdiğinizde, aynı `Idempotency-Key` ile ya da aynı `previous_response_id` ve aynı `input` ile, asistan yeniden yanıt vermez ve yeni bir yanıt saklanmaz: ilk yanıt aynı kimlikle döner. Sohbet ekipteyken de aynı `Idempotency-Key` ile ya da aynı `previous_response_id` ve aynı `input` ile gelen deneme ilk yanıtı aynı kimlikle alır. Saklanan bir yanıta, yanıtın asistanını çağırabilen her anahtar kimliğiyle ulaşır: yanıtı ve girdi öğelerini okur, `previous_response_id` ile sohbeti sürdürür. Anahtarın çağıramadığı bir asistanın yanıtı ona yokmuş gibi görünür: `404 response_not_found`, `previous_response_id`'de `404 previous_response_not_found`. Geliştirici bağlamınız, `metadata` değerleriniz ve gönderdiğiniz son kullanıcı kimliği yalnız yanıtı saklayan anahtara geri döner. Başka bir anahtar yanıtı `instructions`, `safety_identifier` ve `user` alanları `null`, `metadata` alanı boş (`{}`) olarak okur; girdi öğelerinde `system` ve `developer` mesajlarını görmez. Yanıtı yalnız onu saklayan anahtar silebilir; başka bir anahtarın silme isteği `404 response_not_found` alır. Bir anahtarı yenileyip eskisini iptal ettiğinizde eskisinin sakladığı yanıtlar 30 gün sonra kendiliğinden silinir. Yanıt kimlikleri tahmin edilemeyecek kadar uzundur ama yine de onları sır gibi saklayın. ```ts const stored = await client.responses.retrieve('resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f'); const items = await client.responses.inputItems.list('resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f'); await client.responses.delete('resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f'); ``` ## Akış `"stream": true` ile olaylar OpenAI'nin tanımladığı sırayla gelir. Her olayda 0'dan başlayıp birer artan bir `sequence_number` vardır. Her asistan mesajı kendi öğesini açar ve kapatır: ```txt event: response.created data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_7c1e","status":"in_progress","conversation":{"id":"conv_x8k2m9p4q7w1r3t"},"output":[]}} event: response.in_progress data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_7c1e","status":"in_progress"}} : sone.activity {"label":"Uygun saatlere bakılıyor"} event: response.output_item.added data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"type":"message","id":"msg_w8e4","status":"in_progress","role":"assistant","content":[],"sone":{…}}} event: response.content_part.added data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_w8e4","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[],"logprobs":[]}} event: response.output_text.delta data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_w8e4","output_index":0,"content_index":0,"delta":"Elbette. Hangi gün size ","logprobs":[]} event: response.output_text.delta data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_w8e4","output_index":0,"content_index":0,"delta":"uygun?","logprobs":[]} event: response.output_text.done data: {"type":"response.output_text.done","sequence_number":6,"item_id":"msg_w8e4","output_index":0,"content_index":0,"text":"Elbette. Hangi gün size uygun?","logprobs":[]} event: response.content_part.done data: {"type":"response.content_part.done","sequence_number":7,"item_id":"msg_w8e4","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Elbette. Hangi gün size uygun?","annotations":[],"logprobs":[]}} event: response.output_item.done data: {"type":"response.output_item.done","sequence_number":8,"output_index":0,"item":{"type":"message","id":"msg_w8e4","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Elbette. Hangi gün size uygun?","annotations":[],"logprobs":[]}],"sone":{…}}} event: response.completed data: {"type":"response.completed","sequence_number":9,"response":{"id":"resp_7c1e","status":"completed","sone":{"conversation_id":"conv_x8k2m9p4q7w1r3t","status":"answered"}}} ``` Örnekteki nesneler kısaltıldı; gerçek olaylar alanların tamamını taşır. `: sone.activity` satırı bir olay değil, bir SSE yorumudur: asistan bir araç kullanırken "Uygun saatlere bakılıyor" gibi kısa bir durumu `label` alanında verir. `sequence_number` taşımaz. OpenAI SDK'ları yorum satırlarını atlar; bu durumu göstermek istiyorsanız akışı kendiniz ayrıştırın ve `: sone.activity ` ile başlayan satırın geri kalanını JSON olarak okuyun. `: ping` satırları da yorumdur. **OpenAI JS** ```ts import OpenAI from 'openai'; const stream = await client.responses.create({ model: 'default', input: 'Saç boyası için randevu almak istiyorum.', stream: true, }); try { for await (const event of stream) { if (event.type === 'response.output_text.delta') process.stdout.write(event.delta); if (event.type === 'response.output_item.done') process.stdout.write('\n'); } } catch (error) { // Akış başladıktan sonraki bir hata: SDK error olayını APIError olarak fırlatır. if (error instanceof OpenAI.APIError) console.error(error.code, error.message); else throw error; } ``` **OpenAI Python** ```python stream = client.responses.create( model="default", input="Saç boyası için randevu almak istiyorum.", stream=True, ) for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) elif event.type == "response.output_item.done": print() elif event.type == "error": print(event.code, event.message) ``` Akış başladıktan sonra bir hata olursa önce `code` ve `message` taşıyan bir `error` olayı, ardından `response.failed` gelir ve akış kapanır. Responses akışında `data: [DONE]` yoktur; akış `response.completed` ya da `response.failed` ile biter. OpenAI'nin Node SDK'sı `error` olayında hata fırlatır; Python SDK'sı onu `type` değeri `"error"` olan bir olay olarak verir. Ayrıntılar: [Hatalar](https://docs.sonecentral.com/hatalar.md#akis-sirasinda-hata). ## Hatalar | Durum | `code` | Ne zaman | | --- | --- | --- | | `400` | `missing_input` | `input` yok. | | `400` | `invalid_input` | `input` bir metin ya da öğe dizisi değil. | | `400` | `invalid_role` | Bir mesaj öğesinin rolü yok ya da tanınmıyor. | | `400` | `missing_user_message` | Son öğe ziyaretçinin metin mesajı değil. | | `400` | `unsupported_item` | Tanınmayan bir girdi öğesi türü var. | | `400` | `conflicting_parameters` | `previous_response_id` ile `conversation` birlikte gönderildi. | | `400` | `instructions_too_long` | `instructions` 2.000 karakterden ya da JSON olarak 4 KB'tan uzun. | | `400` | `invalid_metadata` | `metadata` sınırı aşıyor (16 anahtar, 64 ve 512 karakter, JSON olarak 4 KB) ya da bir değeri metin değil. | | `400` | `safety_identifier_too_long` | `safety_identifier` 64 karakterden uzun. | | `400` | `user_too_long` | `user` 64 karakterden uzun. | | `400` | `previous_response_id_too_long` | `previous_response_id`, boşlukları atıldıktan sonra bir yanıt kimliğinden uzun. | | `400` | `item_id_too_long` | Bir `item_reference` kimliği 64 karakterden uzun. | | `400` | `not_cancellable` | `cancel` çağrıldı. | | `400` | `conversation_model_mismatch` | Sohbet başka bir asistanla açılmış. | | `404` | `response_not_found` | Yanıt yok, silinmiş, 30 günü geçmiş, `store: false` ile üretilmiş, başka bir çalışma alanına ait ya da anahtarınızın çağıramadığı bir asistanın sohbetinde. Silmede ayrıca: yanıtı başka bir anahtar saklamış. | | `404` | `previous_response_not_found` | `previous_response_id` ile gösterilen yanıt bulunamadı ya da anahtarınızın çağıramadığı bir asistanın sohbetinde. | | `404` | `conversation_not_found` | `conversation` bu çalışma alanında bir API sohbeti değil. | | `409` | `conversation_busy` | Aynı sohbette önceki mesaj hâlâ yanıtlanıyor. | Bütün kodlar: [Hatalar](https://docs.sonecentral.com/hatalar.md). --- # Vercel AI SDK Sayfa: https://docs.sonecentral.com/vercel-ai-sdk Markdown: https://docs.sonecentral.com/vercel-ai-sdk.md > useChat ile tarayıcıdan doğrudan /chat uç noktasına bağlanın, zengin mesajları tipli veri parçaları olarak çizin; ya da sunucuda OpenAI sağlayıcısıyla generateText ve streamText kullanın. Vercel AI SDK ile iki yol var: - **`useChat` ile doğrudan.** Sitenizdeki sohbet penceresi tarayıcı anahtarıyla `POST /chat` uç noktasına bağlanır. Arada sizin sunucunuz yoktur. Ürün kartları, düğmeler ve dosyalar tipli veri parçaları olarak gelir. - **Sağlayıcı olarak.** Sunucu kodunuzda `@ai-sdk/openai` ya da `@ai-sdk/openai-compatible` ile `generateText` ve `streamText` kullanırsınız. Örnekler `ai` 7, `@ai-sdk/react` 4, `@ai-sdk/openai` 4 ve `@ai-sdk/openai-compatible` 3 ile denendi. `/chat` akışı AI SDK 5 ve sonrasının UI mesaj akışı biçimindedir. ## `useChat` ile doğrudan ```npm npm i ai @ai-sdk/react ``` ```tsx title="components/chat.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; const transport = new DefaultChatTransport({ api: 'https://pb.sonecentral.com/api/v1/chat', headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` }, body: { model: 'default' }, }); export function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, status, error } = useChat({ transport }); const busy = status === 'submitted' || status === 'streaming'; return (
{messages.map((message) => (
{message.parts.map((part, index) => part.type === 'text' ?

{part.text}

: null, )}
))} {error ?

{error.message}

: null}
{ event.preventDefault(); if (!input.trim() || busy) return; sendMessage({ text: input }); setInput(''); }} > setInput(event.target.value)} disabled={busy} />
); } ``` `NEXT_PUBLIC_SONE_PUBLISHABLE_KEY` bir **tarayıcı anahtarı** olmalı, yani `pk-sone-v1-…` ile başlamalı. Sitenizin adresi de anahtarın izin verilen siteleri arasında bulunmalı. Sunucu anahtarı burada `403 secret_key_in_browser` alır. Bu en kısa örnek yalnız metin parçalarını çizer. Ürün kartları, düğmeler ve dosyalar `data-sone-*` parçaları olarak gelir; onları çizen örnek aşağıda: [Tipli mesajlar](#tipli-mesajlar). Girdi yalnız bir mesaj yanıtlanırken kapalıdır. Bir hatadan sonra `status` `error` olur, `error.message` Türkçe açıklamayı taşır ve ziyaretçi yeniden yazabilir. ### İstek gövdesi `DefaultChatTransport` her mesajda şunu gönderir; `body` içine koyduklarınız da eklenir: ```json { "id": "chat_l7k2…", "messages": [{ "id": "…", "role": "user", "parts": [{ "type": "text", "text": "Merhaba" }] }], "trigger": "submit-message", "model": "default" } ``` | Alan | Tür | Açıklama | | --- | --- | --- | | `id` | string | useChat'in sohbet kimliği. Tarayıcının seçtiği bir değerdir; sohbeti yalnız gönderdiğiniz mesajlar kayıttaki son ziyaretçi mesajı ve yanıtıyla bitiyorsa sürdürür. | | `messages` | UIMessage[] | Sohbetin mesajları. text parçaları, asistan mesajlarında data-sone-* parçalarının data.text alanı da okunur. Son asistan mesajından sonraki user mesajları yeni mesajdır; yeni mesajda dosya parçası varsa istek reddedilir. | | `model` | string | Asistanın kimliği. X-Sone-Model başlığıyla da verilebilir. Varsayılan: "default". | | `conversationId` | string | Sürdürülecek sohbetin kimliği. X-Sone-Conversation-Id başlığıyla da verilebilir. | | `user` | string | Yalnız sunucu anahtarıyla dikkate alınır. Kendi sisteminizdeki ziyaretçi kimliği. | | `context` | string | Yalnız sunucu anahtarıyla dikkate alınır. Asistana yalnız bilgi olarak iletilen geliştirici bağlamı, en fazla 2.000 karakter. | `trigger` ve `messageId` alanları okunmaz. ### Sohbetin sürmesi Her asistan mesajının `metadata.conversationId` alanında sohbetin kimliği vardır. Mesajlar her istekte geri gittiği için sohbet onunla bulunur. Sohbetin kimliği şu sırayla okunur: gövdedeki `conversationId`, `X-Sone-Conversation-Id` başlığı, son asistan mesajının `metadata.conversationId` alanı. Sayfa yenilendiğinde sohbeti sürdürmek için `useChat({ id, messages })` ile hem kimliği hem mesajları, asistan mesajlarının `metadata` alanıyla birlikte geri verin. Yalnız `id` yetmez: tarayıcının seçtiği kimlik sohbeti yalnız sohbet asistandayken, son yanıtı asistan kendisi yazmışken ve geri verdiğiniz mesajlar sohbetin ilk mesajından başlıyorsa sürdürür; sohbet ekipteyken, son yanıt hazır bir cümle, karşılama mesajı veya ekibinizin bir mesajıyken ya da mesajlar kısaltılmışken yeni bir sohbet açılır. Saklama örneği: [Sohbet sürekliliği](https://docs.sonecentral.com/sohbet-surekliligi.md#vercel-ai-sdkda). Bir mesaj yeniden gönderildiğinde mesajın kendi `id`'si tekrar denemeyi tanır: aynı mesaj ikinci kez kaydedilmez, asistan iki kez yanıt vermez. Kimliği aynı kalıp metni değişen, yani düzenlenen bir mesaj yeni bir mesaj sayılır. Tarayıcı anahtarıyla, sohbetin kimliği olmadan başka bir ağ adresinden gelen deneme yeni sohbet açar. Bu ilk mesajda olur, bir de asistan mesajlarının `metadata.conversationId` alanını sakladığınız geçmişte tutmuyorsanız: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek). ### Uzun sohbetlerde yalnız son mesajı göndermek İstek gövdesi en fazla 1 MB olabilir ve gerçek kayıt Sonecentral'dadır. Sohbetin kimliği elinizdeyse yalnız son mesajı gönderebilirsiniz: ```ts title="lib/transport.ts" import { DefaultChatTransport } from 'ai'; import type { SoneUIMessage } from '@/lib/sone'; export const transport = new DefaultChatTransport({ api: 'https://pb.sonecentral.com/api/v1/chat', headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` }, prepareSendMessagesRequest: ({ id, messages }) => { const conversationId = messages.findLast((m) => m.metadata?.conversationId)?.metadata?.conversationId; return { body: { id, model: 'default', conversationId, // Kimlik varsa yalnız ziyaretçinin yeni mesajı yeter; yoksa bütün geçmiş gider. messages: conversationId ? messages.slice(-1) : messages, }, }; }, }); ``` `prepareSendMessagesRequest` verildiğinde `body` seçeneği kullanılmaz; `model`'i dönen gövdeye kendiniz yazın. Mesajın kendi `id`'si gittiği için yeniden deneme bu yolda da güvenlidir. ### Akışta neler gelir Yanıt, AI SDK'nın UI mesaj akışı biçimindedir ve `x-vercel-ai-ui-message-stream: v1` başlığını taşır: | Parça | Ne zaman | | --- | --- | | `start` | Kontroller geçip asistan yanıtlamaya başladığında. `messageMetadata` içinde `conversationId` ve `model` gelir. | | `start-step` | `start`'ın hemen ardından. | | `text-start`, `text-delta`, `text-end` | Asistanın her düz metin mesajı için bir kez. `id` mesajın Sonecentral'daki kimliğidir. | | `data-sone-catalog` | Ürün ya da hizmet kartları. | | `data-sone-actions` | Düğmeler ya da hızlı yanıtlar. | | `data-sone-file` | Paylaşılan bir dosya. | | `data-sone-staff` | Ekibinizden birinin bu tur sırasında yazdığı düz metin mesajı. | | `data-sone-status` | Asistanın o an ne yaptığı, örneğin "Ürünlere bakılıyor". `transient: true` taşır: mesaja eklenmez, yalnız `onData`'ya gelir. | | `finish-step`, `finish` | Tur bitti. `finish` parçasının `messageMetadata`'sında `conversationId`, `model` ve `status` vardır. | | `error` | Akış başladıktan sonra bir hata oldu. `errorText` Türkçe açıklamadır. | Akış `data: [DONE]` ile biter. Bekleme sırasında `: ping` yorum satırları gelir; AI SDK onları atlar. Zengin parçaların `data` alanı, çizmek için gerekeni taşır: ```json { "type": "data-sone-catalog", "id": "msg_c4t2l9g7x1p3h8q", "data": { "text": "Keten gömlek, taş rengi\n1.290,00 TL\nM ve L stokta\nhttps://www.ornek.com/urun/keten-gomlek-tas", "role": "assistant", "catalog": { "format": "card", "items": [ { "id": "r8k2m9p4q7w1x3z", "kind": "inventory", "title": "Keten gömlek, taş rengi", "price": "1.290,00 TL", "detail": "M ve L stokta", "url": "https://www.ornek.com/urun/keten-gomlek-tas", "button_title": "Ürünü incele" } ] } } } ``` Parçanın `id`'si mesajın Sonecentral'daki kimliğidir; sohbetin mesaj listesindeki `id` ile aynıdır. `data.text` düz metin karşılığıdır. `data.role` `assistant` ya da `staff` olur: ekibiniz de kart, düğme ya da dosya gönderebilir. Türüne göre `catalog`, `actions` ya da `file` alanlarından biri doludur; biçimleri [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md) sayfasındadır. ### Tipli mesajlar ```ts title="lib/sone.ts" import type { UIMessage } from 'ai'; // SoneMessage, SoneCatalog, SoneActions ve SoneFile: Zengin mesajlar sayfasındaki tanımları bu dosyaya ekleyin. export type SoneStatus = 'answered' | 'handoff' | 'human' | 'silent'; // /chat akışındaki zengin parçaların data alanı export type SoneDataPart = { text: string; // düz metin karşılığı role: 'assistant' | 'staff'; catalog?: SoneCatalog; actions?: SoneActions; file?: SoneFile; }; export type SoneUIMessage = UIMessage< { conversationId?: string; model?: string; status?: SoneStatus; }, { 'sone-catalog': SoneDataPart; 'sone-actions': SoneDataPart; 'sone-file': SoneDataPart; 'sone-staff': { text: string; role: 'staff' }; 'sone-status': { label: string }; } >; ``` ```tsx title="components/chat.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; import type { SoneUIMessage } from '@/lib/sone'; const transport = new DefaultChatTransport({ api: 'https://pb.sonecentral.com/api/v1/chat', headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` }, body: { model: 'default' }, }); export function Chat() { const [activity, setActivity] = useState(''); const { messages, sendMessage } = useChat({ transport, onData: (part) => { if (part.type === 'data-sone-status') setActivity(part.data.label); }, onError: () => setActivity(''), onFinish: () => setActivity(''), }); return (
{messages.map((message) => message.parts.map((part, index) => { const key = `${message.id}-${index}`; switch (part.type) { case 'text': return

{part.text}

; case 'data-sone-staff': return (

Ekip: {part.data.text}

); case 'data-sone-catalog': return ( ); case 'data-sone-actions': { const actions = part.data.actions; return (
{actions?.text ?

{actions.text}

: null} {actions?.buttons?.map((button) => ( {button.title} ))} {actions?.choices?.map((choice) => ( ))}
); } case 'data-sone-file': return part.data.file?.url ? ( {part.data.file.name} ) : (

{part.data.text}

); default: return null; } }), )} {activity ?

{activity}…

: null}
); } ``` URL'si olmayan bir düğmeye ya da hızlı yanıta tıklanınca, başlığını ziyaretçinin yeni mesajı olarak gönderin. Asistan onu ziyaretçinin yazdığı bir mesaj gibi okur. ### Sohbetin kimliği ve durumu ```ts const last = messages.at(-1); const conversationId = last?.metadata?.conversationId; const status = last?.metadata?.status; // 'human' ise ekip yazıyor ``` `status` `handoff` ya da `human` olduğunda ekibin sonraki yanıtları akışa gelmez; onları [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md) sayfasındaki gibi okuyun. Sohbetin şu an kimde olduğunu `GET /conversations/{id}` söyler. Tam bir pencere örneği: [Site içine sohbet penceresi](https://docs.sonecentral.com/sohbet-penceresi.md#react-ve-nextjs). ### Hatalar `/chat` akış başlamadan bir isteği reddederse gövde JSON değil, düz Türkçe metindir. AI SDK bu metni olduğu gibi `useChat`'in `error.message` değerine koyar. Hatanın kodu `X-Sone-Error-Code` başlığında gelir; bu başlık tarayıcıdaki koda açılmaz, yalnız sunucudan yapılan isteklerde okunur. Akış başladıktan sonraki bir hata `error` parçasıyla gelir; `error.message` yine Türkçe açıklamadır. Kodların listesi: [Hatalar](https://docs.sonecentral.com/hatalar.md). ### Bağlantı koparsa `useChat({ resume: true })` açılışta `GET /chat/{id}/stream` ister. Sonecentral bu isteğe anahtarı denetledikten sonra her zaman `204` döner: sürdürülecek bir akış yoktur, çünkü bir turun mesajları gönderildikleri anda sohbete kaydedilir. Kopan bir bağlantıdan sonra kaçırdığınız mesajları `GET /conversations/{id}/messages` ile okuyun. ## Sunucuda sağlayıcı olarak Sunucu kodunuzda asistanı herhangi bir AI SDK modeli gibi kullanın. Sunucu anahtarı burada güvendedir. ```npm npm i ai @ai-sdk/openai ``` **@ai-sdk/openai** ```ts import { createOpenAI } from '@ai-sdk/openai'; import { generateText, streamText } from 'ai'; const sone = createOpenAI({ baseURL: 'https://pb.sonecentral.com/api/v1', apiKey: process.env.SONE_API_KEY, }); // Chat Completions uç noktası const { text } = await generateText({ model: sone.chat('default'), prompt: 'Kargo kaç günde gelir?', }); // Responses uç noktası const result = streamText({ model: sone.responses('default'), prompt: 'İade nasıl yapılıyor?', }); for await (const delta of result.textStream) { process.stdout.write(delta); } ``` **@ai-sdk/openai-compatible** ```ts import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { generateText } from 'ai'; const sone = createOpenAICompatible({ name: 'sone', baseURL: 'https://pb.sonecentral.com/api/v1', apiKey: process.env.SONE_API_KEY, }); const { text } = await generateText({ model: sone.chatModel('default'), prompt: 'Kargo kaç günde gelir?', }); ``` Sohbetin kimliği her yanıtın `X-Sone-Conversation-Id` başlığında gelir. Sonraki istekte aynı başlıkla geri gönderin: ```ts const first = await generateText({ model: sone.chat('default'), prompt: 'Kargo kaç günde gelir?', }); const conversationId = first.response.headers?.['x-sone-conversation-id']; const second = await generateText({ model: sone.chat('default'), headers: conversationId ? { 'X-Sone-Conversation-Id': conversationId } : undefined, prompt: 'Peki iade süresi?', }); ``` `@ai-sdk/openai`'nin Responses modeliyle önceki turun mesajlarını geri verirseniz sağlayıcı önceki çıktıları `item_reference` olarak gönderir ve sohbet onlarla da bulunur: ```ts const turn1 = await generateText({ model: sone.responses('default'), prompt: 'İade koşullarınız nedir?' }); const turn2 = await generateText({ model: sone.responses('default'), messages: [ { role: 'user', content: 'İade koşullarınız nedir?' }, ...turn1.response.messages, { role: 'user', content: 'Kaç gün içinde?' }, ], }); ``` Sağlayıcı yolu yalnız metni verir. Ürün kartlarını ve düğmeleri okumak istiyorsanız OpenAI SDK'sını ya da yukarıdaki `useChat` yolunu kullanın. AI SDK'nın araç çağırma özelliği, yani `tools`, desteklenmez: asistanın araçlarını Sonecentral yönetir. --- # Akış Sayfa: https://docs.sonecentral.com/akis Markdown: https://docs.sonecentral.com/akis.md > Akışlı yanıtlar nasıl gelir, bağlantı nasıl canlı tutulur ve metin neden kelime kelime değil mesaj mesaj ilerler. Üç biçim de akışı destekler: Chat Completions ve Responses'ta `"stream": true`, Vercel AI SDK'da `/chat` her zaman akar. Hepsi **sunucu tarafından gönderilen olaylar**, yani SSE kullanır. ## Bağlantı Akışlı yanıtın başlıkları: ```http Content-Type: text/event-stream; charset=utf-8 Cache-Control: no-cache, no-transform Connection: keep-alive X-Accel-Buffering: no X-Sone-Conversation-Id: conv_x8k2m9p4q7w1r3t ``` Vercel AI SDK akışında ayrıca `x-vercel-ai-ui-message-stream: v1` gelir. - **Önce kontroller.** Anahtar, istek sınırı, asistan, sohbet ve paket kontrolleri akış başlamadan yapılır. Geçmeyen istek akış yerine sıradan bir HTTP hatası alır: [Hatalar](https://docs.sonecentral.com/hatalar.md). - **İlk olay asistan başlarken gelir.** Kontroller geçince Chat Completions'ta rolü taşıyan ilk parça, Responses'ta `response.created`, AI SDK'da `start` hemen gelir. Asistan daha düşünürken bağlantının kurulduğunu ve sohbetin kimliğini bilirsiniz. - **On saniyede bir kalp atışı.** Uzun süren turlarda bağlantı `: ping` satırlarıyla canlı tutulur. İki nokta üst üste ile başlayan satırlar SSE yorumudur; SDK'lar onları zaten atlar, kendi ayrıştırıcınızı yazıyorsanız siz de atlayın. - **Bitiş.** Chat Completions ve AI SDK `data: [DONE]` ile biter. Responses `response.completed` ya da, bir hatada, `response.failed` olayıyla biter; orada `[DONE]` yoktur. ## Metin neden mesaj mesaj gelir Genel bir dil modeli API'sinde metin model ürettikçe, kelime kelime gelir. Burada öyle değil; bu bilerek böyle. Asistanın yazdığı yanıt ziyaretçiye gitmeden önce işletmenin güvenlik kontrollerinden geçer: fiyat ve stok doğru mu, söz verilmemesi gereken bir şey var mı, mesaj gerçekten gönderilmeli mi. Bu kontroller yanıtın **tamamına** bakar. Bu yüzden bir mesaj ancak bitip kontrolden geçtikten sonra yola çıkar. Sonuçta akış şöyle ilerler: 1. Kontroller geçer, bağlantı açılır, ilk olay gelir. 2. Asistan düşünür ve araçlarını kullanır. Bu birkaç saniye sürebilir. Bu sırada "Ürünlere bakılıyor" gibi kısa durumlar gelir. 3. İlk mesaj kontrolden geçer ve gelir. Daktilo etkisi bozulmasın diye kelime gruplarına bölünerek hızla akar. 4. Varsa sonraki mesajlar, ürün kartları ve düğmeler aynı şekilde sırayla gelir. Kısa durumlar her biçimde başka bir yerde gelir: | Biçim | Durum | | --- | --- | | Chat Completions | `delta`'sı boş bir parçanın üst düzeyindeki `sone.activity` | | Responses | Bir olay değil, SSE yorum satırı: `: sone.activity {"label":"Ürünlere bakılıyor"}`. `sequence_number` taşımaz; SDK'lar onu atlar, okumak için akışı kendiniz ayrıştırın. | | Vercel AI SDK | Geçici (`transient: true`) `data-sone-status` parçasının `data.label` alanı; yalnız `onData`'ya gelir | Durum metni bir araç adı değil, ziyaretçinin anlayacağı Türkçe bir cümledir. İki mesaj arasında kısa bir bekleme olabilir. Arayüzünüzde bir "yazıyor" göstergesi tutmak iyi olur. Aynı mesajı aynı `Idempotency-Key` ile, ya da sohbetin kimliği ve aynı geçmişle yeniden gönderdiğinizde asistan yeniden yanıt vermez; kayıtlı yanıt akışın sonunda bir kerede gelir. Ayrıntılar: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek). ## Bağlantı koparsa Asistanın gönderdiği her mesaj, gönderildiği anda sohbete kaydedilir. Akış yarıda koparsa kaçırdığınız mesajları `GET /conversations/{id}/messages` ile okuyabilirsiniz. Sohbetin kimliği akışın başında `X-Sone-Conversation-Id` başlığında gelir. Ayrıntılar: [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md). Aynı mesajı yeniden göndermeniz gerekirse ilk gönderimdeki `Idempotency-Key` ile gönderin; asistan iki kez yanıt vermez. Önceki tur hâlâ yanıtlanıyorsa istek `409 conversation_busy` alır. ## Proxy arkasında Akışı kendi sunucunuzdan geçiriyorsanız ara katmanın yanıtı tamponlamadığından emin olun. Nginx'te `proxy_buffering off;` yeterlidir. Next.js route handler'larında yanıt gövdesini olduğu gibi döndürün: ```ts title="app/api/sohbet/route.ts" export async function POST(request: Request) { const upstream = await fetch('https://pb.sonecentral.com/api/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.SONE_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': request.headers.get('Idempotency-Key') ?? crypto.randomUUID(), }, body: await request.text(), }); return new Response(upstream.body, { status: upstream.status, headers: { 'Content-Type': upstream.headers.get('Content-Type') ?? 'text/event-stream', 'Cache-Control': 'no-cache', 'X-Sone-Conversation-Id': upstream.headers.get('X-Sone-Conversation-Id') ?? '', }, }); } ``` Bu şekilde sunucu anahtarını kullanırsanız, isteği gönderen ziyaretçiyi kendi tarafınızda doğrulamak ve sınırlamak sizin işinizdir. Sitenizdeki sohbet penceresi için tarayıcı anahtarı daha basittir. --- # Zengin mesajlar Sayfa: https://docs.sonecentral.com/zengin-mesajlar Markdown: https://docs.sonecentral.com/zengin-mesajlar.md > Ürün ve hizmet kartları, düğmeler, hızlı yanıtlar ve dosyalar SoneMessage nesnesi olarak gelir; her birinin düz metin karşılığı da vardır. Asistan WhatsApp'ta ürün kartı, düğme ya da belge gönderebiliyorsa API'de de gönderir. API onları belirli bir uygulamanın biçiminde değil, sizin istediğiniz gibi çizebileceğiniz yapılandırılmış veri olarak verir. Bu verinin adı `SoneMessage`. Ekibiniz Mesajlar ekranından bir ürün, hizmet ya da belge paylaştığında da aynı yapı gelir, `role: "staff"` ile: [Ekibin paylaştıkları](https://docs.sonecentral.com/insan-devri.md#ekibin-paylastiklari). ## Nerede gelir | Biçim | Yer | | --- | --- | | Chat Completions | Akışsız yanıtta `choices[0].message.sone.messages`. Akışta her mesaj, kendi son parçasının üst düzeyindeki `sone.message` alanında gelir. | | Responses | Her `message` çıktı öğesinin `sone` alanı. | | Vercel AI SDK | `data-sone-catalog`, `data-sone-actions`, `data-sone-file` ve `data-sone-staff` parçaları. Asistanın düz metin mesajları `text` parçası olarak gelir. | | Sohbetin mesajları | `GET /conversations/{id}/messages` yanıtının `data` dizisi. | Vercel AI SDK parçalarının `data` alanı `SoneMessage`'ın tamamını değil, çizmek için gerekeni taşır: `text`, `role` ve mesajın türüne göre `catalog`, `actions` ya da `file`. `data-sone-staff` yalnız ekibinizin düz metin mesajları içindir; ekip kart, düğme ya da dosya gönderirse ilgili parça `data.role: "staff"` ile gelir. Ayrıntılar: [Vercel AI SDK](https://docs.sonecentral.com/vercel-ai-sdk.md#tipli-mesajlar). ## Düz metin her zaman vardır Her `SoneMessage`'ın `text` alanı doludur. Ürün kartı içeren bir mesajda bu, ürünlerin adını, fiyatını ve adresini sayan düz bir metindir. Zengin mesajları çizmeyen bir istemci yalnız `text`'i gösterebilir. Çok uzun bir katalog paylaşımında `text` kısalır, kayıtların tamamı yine `catalog.items`'tadır: [Katalog](#katalog). OpenAI biçimlerinde aynı metin `content` alanında da vardır. ## `SoneMessage` ```ts title="sone-message.ts" export type SoneMessage = { id: string; // "msg_…" role: 'assistant' | 'staff' | 'user'; // staff: ekibinizden biri yazdı type: 'text' | 'catalog' | 'actions' | 'file'; text: string; // düz metin karşılığı created_at: number; // Unix saniyesi catalog?: SoneCatalog; actions?: SoneActions; file?: SoneFile; }; export type SoneCatalog = { // İşletmenin seçtiği sunum: "card", "card_button", "image_text", "link_button", "text", "carousel" ya da "list" format: string; items: SoneCatalogItem[]; }; export type SoneCatalogItem = { id: string; // kaydın Sonecentral'daki kimliği kind: 'inventory' | 'offering'; // ürün ya da hizmet title: string; description?: string; price?: string; // biçimlendirilmiş fiyat: "1.290,00 TL" detail?: string; // stok, süre gibi kısa bilgi attributes?: string; // renk, beden gibi özellikler, düz metin image_url?: string; url?: string; // ürünün ya da hizmetin sayfası button_title?: string; // url düğmesinin yazısı; url varsa gelir }; export type SoneActions = { format: 'buttons' | 'quick_replies' | 'card' | 'link_button'; title?: string; text?: string; image_url?: string; buttons?: { title: string; url: string }[]; choices?: string[]; }; export type SoneFile = { id: string; // "file_…" name: string; mime_type: string; size: number; // bayt url: string; // imzalı indirme bağlantısı }; ``` `role: "user"` yalnız sohbetin mesaj listesinde görünür ve ziyaretçinin kendi satırlarıdır; bu satırların `type` değeri her zaman `text` olur. Bir turun yanıtındaki mesajlar her zaman işletme tarafındandır: `assistant` ya da `staff`. Boş alanlar gönderilmez: `description`, `price`, `image_url` gibi bir alan yoksa nesnede de yoktur. ### Katalog Bir paylaşım 1 ile 10 kayıt taşır, asistanın seçtiği sırayla. ```json { "id": "msg_c4t2l9g7x1p3h8q", "role": "assistant", "type": "catalog", "text": "Keten gömlek, taş rengi\n1.290,00 TL\nM ve L stokta\nhttps://www.ornek.com/urun/keten-gomlek-tas\n\nKeten gömlek, lacivert\n1.290,00 TL\nYalnız M kaldı\nhttps://www.ornek.com/urun/keten-gomlek-lacivert", "created_at": 1760866872, "catalog": { "format": "carousel", "items": [ { "id": "r8k2m9p4q7w1x3z", "kind": "inventory", "title": "Keten gömlek, taş rengi", "price": "1.290,00 TL", "detail": "M ve L stokta", "image_url": "https://cdn.ornek.com/urun/keten-tas.jpg", "url": "https://www.ornek.com/urun/keten-gomlek-tas", "button_title": "Ürünü incele" }, { "id": "r4q7w1x8k2m9a5z", "kind": "inventory", "title": "Keten gömlek, lacivert", "price": "1.290,00 TL", "detail": "Yalnız M kaldı", "url": "https://www.ornek.com/urun/keten-gomlek-lacivert", "button_title": "Ürünü incele" } ] } } ``` `format` işletmenin bu kayıtlar için seçtiği sunumdur. Tek kayıtta kaydın kendi sunumu gelir: `card`, `card_button`, `image_text`, `link_button` ya da `text`. Birden çok kayıt `carousel` olur; hepsi liste olarak seçilmişse `list`. Size yol gösterir ama bağlamaz: bir listeyi kart olarak çizmek de doğrudur. Hizmetler `kind: "offering"` ile aynı alanlarla gelir; `detail` genellikle süreyi taşır. `text`, her kayıt için ad, açıklama, fiyat, kısa bilgi, özellikler ve adresi alt alta yazar; kayıtlar arasında boş bir satır olur. `text` en fazla 4.000 karakterdir. Örneğin uzun açıklamalı on ürün buna sığmazsa yer kayıtlar arasında paylaştırılır. Payına sığan kayıt tam yazılır. Sığmayan kayıtta önce ad, fiyat ve kısa bilgi gelir, adres sığıyorsa en sona yazılır; özellikler ve açıklamanın tam cümleleri kalan yere eklenir. Bir alan ya tam yazılır ya hiç yazılmaz, ortadan kesilmez. `catalog.items` her kaydı her zaman eksiksiz taşır; kartları oradan çizin, `text`'i yedek olarak kullanın. ### Düğmeler ve hızlı yanıtlar ```json { "id": "msg_a7c1t9n2k4q8z3w", "role": "assistant", "type": "actions", "text": "Randevunuz için hangi gün uygun?\nPerşembe\nCuma\nCumartesi", "created_at": 1760866923, "actions": { "format": "quick_replies", "text": "Randevunuz için hangi gün uygun?", "choices": ["Perşembe", "Cuma", "Cumartesi"] } } ``` | `format` | Ne taşır | | --- | --- | | `buttons` | `text` ve 1 ile 3 arası seçenek, `choices` içinde. | | `quick_replies` | `text` ve 1 ile 13 arası seçenek, `choices` içinde. | | `link_button` | `text` ve bir bağlantı düğmesi, `buttons` içinde. | | `card` | `title`, kısa bir `text`, isteğe bağlı `image_url` ve en fazla 3 bağlantı düğmesi, `buttons` içinde. | Bir seçenek ve bir düğme başlığı en fazla 20 karakterdir. `text` alanı başlığı, metni, her bağlantı düğmesini `Başlık: adres` biçiminde ve seçenekleri alt alta yazar. - Her düğme bir `url` taşır ve o adresi yeni sekmede açar. - Bir seçeneğe tıklanınca başlığını ziyaretçinin yeni mesajı olarak gönderin. Asistan onu ziyaretçinin yazdığı bir mesaj gibi okur. - Seçenekler yalnız ziyaretçinin bir sonraki mesajına kadar anlamlıdır. Asistan seçeneklerin ardından bir metin mesajı da yazabilir; seçenekleri ziyaretçi yeniden yazana kadar gösterin, sonra gizleyin. ### Dosya ```json { "id": "msg_f1l3d0c5x9w2m6r", "role": "assistant", "type": "file", "text": "Fiyat listesi", "created_at": 1760867010, "file": { "id": "file_p9x2k4m7q1w8e3t", "name": "fiyat-listesi-2026.pdf", "mime_type": "application/pdf", "size": 188416, "url": "https://pb.sonecentral.com/api/v1/files/file_p9x2k4m7q1w8e3t?exp=1761471810&sig=…" } } ``` `text` dosyanın açıklamasıdır; açıklama yoksa dosyanın adıdır. Bağlantıyı bir `` ile verin. Bir dosya en fazla 32 MB olabilir. Aynı dosya aynı sohbette yeniden paylaşılırsa `file.id` aynı kalır. Başka bir sohbette dosya kendi kimliğiyle ve kendi bağlantısıyla gelir. Kaç sohbette paylaşılırsa paylaşılsın dosya çalışma alanınızın depolamasında bir kez yer tutar. ## Dosyayı indirmek `file.url`, `GET /files/{id}` uç noktasının imzalı adresidir: ```http GET https://pb.sonecentral.com/api/v1/files/{id}?exp={son_geçerlilik}&sig={imza} ``` - Anahtar gerekmez, adresteki imza yeter. Adresi olduğu gibi kullanın. - Bağlantı **yedi gün** geçerlidir. Sohbetin mesajlarını her okuduğunuzda yedi gün geçerli yeni bir bağlantı gelir. - Yanıt her zaman indirme olarak gelir (`Content-Disposition: attachment`) ve dosyanın adını taşır; Türkçe harfli bir ad `filename*` ile gelir. Tarayıcı dosyayı sayfanızın içinde açmaz. - Süresi dolmuş ya da bozuk bir bağlantı `404 file_not_found` alır. Dosya o an okunamıyorsa `503 file_unavailable` döner. - Aynı dosya kimliği saatte en fazla 10 kez indirilir; fazlası `429 file_download_limit` alır. Çalışma alanının günlük indirme sınırı (varsayılan 2 GiB, UTC gününe göre) dolunca `429 file_egress_limit` gelir. İkisi de `Retry-After` taşır ve isteğin hangi adresten geldiğine bakmaz. - Aynı adresten dakikada 120'den fazla indirme isteği de `429` alır; bu yanıtın gövdesi OpenAI hata biçiminde değildir. ## Çizmek Aşağıdaki işlev bir `SoneMessage`'ı DOM'a çevirir. Bütün metinleri `textContent` ile yazar, `innerHTML` kullanmaz; böylece hiçbir alan sayfanıza kod sokamaz. ```js title="render-sone-message.js" function el(tag, props = {}, ...children) { const node = document.createElement(tag); for (const [key, value] of Object.entries(props)) { if (value == null) continue; if (key === 'class') node.className = value; else if (key.startsWith('on')) node.addEventListener(key.slice(2), value); else node.setAttribute(key, value); } for (const child of children.flat()) { if (child == null || child === false) continue; node.append(typeof child === 'string' ? document.createTextNode(child) : child); } return node; } export function renderSoneMessage(message, { onChoose }) { if (message.type === 'catalog' && message.catalog) { return el( 'div', { class: 'sone-catalog' }, message.catalog.items.map((item) => el( 'article', { class: 'sone-item' }, item.image_url ? el('img', { src: item.image_url, alt: '', loading: 'lazy' }) : null, el('strong', {}, item.title), item.detail ? el('span', {}, item.detail) : null, item.price ? el('span', { class: 'sone-price' }, item.price) : null, item.url ? el('a', { href: item.url, target: '_blank', rel: 'noopener' }, item.button_title || 'İncele') : null, ), ), ); } if (message.type === 'actions' && message.actions) { const { title, text, buttons = [], choices = [] } = message.actions; return el( 'div', { class: 'sone-actions' }, title ? el('strong', {}, title) : null, text ? el('p', {}, text) : null, buttons.map((button) => el('a', { href: button.url, target: '_blank', rel: 'noopener' }, button.title)), choices.map((choice) => el('button', { type: 'button', onclick: () => onChoose(choice) }, choice)), ); } if (message.type === 'file' && message.file?.url) { return el('a', { class: 'sone-file', href: message.file.url, download: message.file.name }, message.file.name); } return el('p', { class: message.role === 'staff' ? 'sone-text sone-staff' : 'sone-text' }, message.text); } ``` Tam bir pencere içinde kullanımı: [Site içine sohbet penceresi](https://docs.sonecentral.com/sohbet-penceresi.md). --- # Sohbet sürekliliği Sayfa: https://docs.sonecentral.com/sohbet-surekliligi Markdown: https://docs.sonecentral.com/sohbet-surekliligi.md > Anonim bir ziyaretçinin sohbeti sayfa yenilense de nasıl sürer, Sonecentral bir isteği hangi sohbete nasıl bağlar ve yeniden gönderilen bir mesajı nasıl tanır. API'den gelen ziyaretçinin hesabı ya da kimliği yoktur. Sohbetin sürmesi için iki şey yeterli: **sohbetin kimliği** ve **gösterdiğiniz mesajlar**. İkisini de ziyaretçinin tarayıcısında saklarsınız. ## Gerçek kayıt sunucudadır Her sohbet Sonecentral'da tutulur; asistan geçmişi oradan okur. Sizin gönderdiğiniz geçmiş yalnız şu işler için kullanılır: - yeni mesajı ya da mesajları ayırmak, - kimlik gelmediğinde sohbeti bulmak, - yeniden gönderilen bir mesajı tanımak, - hiç sohbet yoksa yenisini bu geçmişle başlatmak. Yani geçmişi değiştirerek asistanın hatırladığını değiştiremezsiniz. Ekibinizin Mesajlar ekranında gördüğü konuşma, asistanın gördüğü konuşmadır. ## Sohbet nasıl bulunur Sonecentral her istekte sohbeti şu sırayla arar ve ilk bulduğunu kullanır: 1. **Sohbetin kimliği**, `conv_` ile başlayan değer. - Chat Completions: gövdedeki `conversation_id`, gövdedeki `sone.conversation_id`, `X-Sone-Conversation-Id` başlığı, `metadata.conversation_id`, son olarak geçmişe olduğu gibi eklediğiniz asistan mesajının `sone.conversation_id` alanı. İlk dolu olan kullanılır. - Responses: `conversation`, yoksa `X-Sone-Conversation-Id` başlığı. - Vercel AI SDK: gövdedeki `conversationId`, `X-Sone-Conversation-Id` başlığı, son olarak geri gönderilen asistan mesajının `metadata.conversationId` alanı. 2. **Responses'ta `previous_response_id`.** 3. **Responses'ta önceki çıktıların kimlikleri:** `item_reference` öğeleri ya da `msg_…` kimliğiyle geri gönderilen asistan mesajları. 4. **Vercel AI SDK'da `useChat`'in `id` değeri,** aşağıdaki dört koşul tutarsa. 5. **`user` ya da `safety_identifier`.** Yalnız sunucu anahtarıyla; Vercel AI SDK'da gövdedeki `user`. Aynı değer, aynı asistanla hep aynı sohbeti bulur. 6. **Önceki mesajların parmak izi.** İsteğin geçmişi sohbetin ilk mesajından başlıyorsa Sonecentral turdan sonra o ana kadarki mesajların özetini kaydeder; aynı geçmişle gelen istek, aşağıdaki dört koşul tutarsa o sohbete bağlanır. Sohbete kimlikle ulaşıp yalnız yeni mesajını gönderen bir istek parmak izi kaydetmez. 7. **İlk mesajın yeniden denemesi.** Sohbetin ilk mesajı aynı `Idempotency-Key` ile ya da Vercel AI SDK'da aynı mesaj kimliğiyle 10 dakika içinde yeniden gelirse, mesaj o sohbetin hâlâ son ziyaretçi mesajıysa, açtığı sohbete döner. Tarayıcı anahtarıyla deneme ayrıca aynı ağ adresinden, Vercel AI SDK'da aynı `useChat` kimliğiyle gelmelidir: [Tekrar denemek](#tekrar-denemek). 8. Hiçbiri tutmazsa **yeni sohbet** açılır. Gönderdiğiniz önceki mesajların son 40'ı yeni sohbete geçmiş olarak aktarılır, her biri en fazla 4.000 karakter. Tarayıcı anahtarıyla yalnız ziyaretçinin kendi satırları aktarılır; asistan satırları alınmaz, çünkü tarayıcıda onları herkes değiştirebilir. İlk üç yoldaki kimlikleri Sonecentral üretir; beşinci yoldaki değeri yalnız sunucunuz bilir. Bunlar olduğu gibi kabul edilir: - Bir sohbet kimliği ya da `previous_response_id` bu çalışma alanında bir API sohbeti göstermiyorsa istek yeni sohbet açmaz; `404 conversation_not_found` ya da `404 previous_response_not_found` alır. - Sohbet başka bir asistanla açılmışsa istek `400 conversation_model_mismatch` alır. - `previous_response_id` ile `conversation` birlikte gönderilemez: `400 conflicting_parameters`. İlk iki durumda o kimliği bırakıp yeni sohbet başlatın. Dördüncü ve altıncı yol, yani `useChat`'in `id` değeri ve parmak izi, tarayıcının seçtiği değerlere dayanır. Bu yüzden sohbeti yalnız şu dört koşul birlikte tutarsa sürdürür: 1. **Sohbet asistandadır.** Ekibinizin devraldığı bir sohbete bu iki yolla hiç ulaşılmaz. 2. **Gönderdiğiniz geçmiş kayıttaki en yeni karşılıklaşmayı kelimesi kelimesine taşır:** ziyaretçinin son mesajı ve ona verilen yanıtın tamamı. Yanıt, ekibinizin o arada yazdıkları da dahil, kayıtta nasılsa öyle karşılaştırılır. 3. **O yanıtın en az bir mesajını asistan bu sohbette kendisi yazmıştır.** Platformun her sohbette aynı olan hazır cümleleri (örneğin asistan yanıt üretemediğinde giden "Kusura bakmayın, bunu ekip arkadaşıma soruyorum. Size buradan dönüş yapacağız."), ekibinizin mesajları ve paylaşılan ürün, hizmet ya da dosya kartları tek başına yetmez, çünkü başka biri de onları bilebilir. Asistanın ayarlarındaki karşılama mesajı da yetmez: asistan onu her ziyaretçiye aynı yazar. 4. **Gönderdiğiniz geçmiş sohbetin ilk mesajından başlar:** sohbette kayıtlı bütün ziyaretçi mesajlarını sırasıyla taşır. Yalnız son karşılıklaşma yetmez, çünkü o çoğu zaman tahmin edilebilir: "Teşekkürler" ve asistanın her seferinde yazdığı aynı nezaket cümlesi gibi. Yeni sohbet geçmişinizin yalnız son 40 mesajını aktardıysa, daha eski mesajlarınız geçmişin başında kalabilir. Karşılaştırma boşlukları ve satır sonlarını yok sayar, art arda gelen aynı roldeki mesajları tek mesaj sayar. Tarayıcı anahtarıyla yeni sohbete aktarılan önceki satırlar bu karşılaştırmayı bozmaz. Tek istisna, aynı ziyaretçinin sohbetin son mesajını kendi adıyla, yani aynı `Idempotency-Key` ya da Vercel AI SDK'da aynı mesaj kimliğiyle yeniden denemesidir: [Tekrar denemek](#tekrar-denemek). Bir koşul tutmazsa istek yeni bir sohbet açar ve asistan orada yanıt verir. Şu durumlar da buna girer: - Sohbet ekipte. - Son yanıt yalnız hazır bir cümleden, ekibinizin bir mesajından ya da bir paylaşımdan oluşuyor. - Ziyaretçinin son mesajlarına henüz yanıt verilmedi, örneğin tur sessiz kaldı veya reddedildi. Geçmişiniz o mesajları ister arkalarında boş bir asistan mesajıyla (Chat Completions'ta `content` değeri `""` ya da `null`, Vercel AI SDK'da metni olmayan bir asistan mesajı), ister yeni mesajlar olarak taşısın. - Ekibiniz son mesaja, sizin geçmişe eklemediğiniz bir yanıt yazdı. - Son yanıt yalnız asistanın karşılama mesajı. - Geçmişiniz sohbetin ilk mesajından başlamıyor, örneğin sohbete bir süre kimlikle ulaşıp yalnız yeni mesajları gönderdiniz, sonra kimliği bırakıp kısaltılmış bir geçmiş gönderiyorsunuz. - Yanıtını alamadığınız bir isteği adı olmadan, aynen yeniden gönderiyorsunuz. Bunların hepsi başka biri tarafından tahmin edilebilir. Sohbeti yalnız onlarla bulmak başka bir ziyaretçiyi bu sohbete sokabilir ve ekibinizin ona yazdıklarını gösterebilirdi. İki sohbetin paylaştığı bir `useChat` kimliği ya da parmak izi bırakılır ve bir daha kullanılmaz. Bir belirsizlikte sohbet ikiye bölünür. Kalan tek sınır: başka biri bir sohbetin bütün geçmişini kelimesi kelimesine yazabilirse bu iki yol onu o sohbete bağlayabilir. Bu pratikte henüz tek karşılıklaşması olan bir sohbette olur: tahmin edilebilir bir ilk mesaj, örneğin "Merhaba", ve asistanın ona her ziyaretçiye kelimesi kelimesine aynı yazdığı bir yanıt. Ziyaretçi ikinci mesajını gönderdiğinde bu yol kapanır. Tarayıcı anahtarında geçmişin parmak izi ziyaretçinin ağ adresine de bağlıdır: bunu ancak aynı ağdaki biri yapabilir, adresi değişen ziyaretçi ise yeni bir sohbet açar. Bu yüzden her istekte `conversation_id` gönderin. Bir sohbette kalmak için her istekte sohbetin kimliğini gönderin, her zaman. Her mesaja da bir `Idempotency-Key` ekleyin. Kimlik her yanıtın `X-Sone-Conversation-Id` başlığında ve gövdesinde gelir: Chat Completions'ta `sone.conversation_id`, Responses'ta `conversation.id`, Vercel AI SDK'da asistan mesajının `metadata.conversationId` alanı. ## Tekrar denemek Ağ hatasından, zaman aşımından ya da bir `5xx` yanıtından sonra aynı mesajı yeniden göndermek güvenli olmalı. Yeniden deneme sohbete ulaştığında Sonecentral bir ziyaretçi mesajını iki kez kaydetmez ve asistan aynı mesaja iki kez yanıt vermez. Deneme sohbete sohbetin kimliğiyle, gösterilen yanıtla (Responses'ta `previous_response_id` ya da `item_reference`) ya da mesajın adıyla ulaşır; ne kimlik ne ad taşıyan bir deneme ulaşmaz (aşağıda). Mesajı nasıl tanıdığı, isteğin ne taşıdığına bağlıdır: - **`Idempotency-Key` başlığı** isteğin en yeni ziyaretçi mesajına bir ad verir, en fazla 200 karakter. Aynı anahtar aynı metinle gelirse mesaj yeniden kaydedilmez: yanıt verilmişse aynı yanıt yeniden döner, verilmemişse, örneğin önceki deneme reddedildiyse, şimdi yanıtlanır. Aynı anahtar başka bir metinle gelirse istek `422 idempotency_key_reused` alır ve `x-should-retry: false` taşır. Her yeni mesajda yeni bir anahtar üretin. - **Vercel AI SDK'da** her ziyaretçi mesajı kendi `id`'si ve metniyle adlandırılır; ayrıca bir şey göndermeniz gerekmez. Kimliği aynı kalıp metni değişen, yani düzenlenen bir mesaj yeni bir mesajdır. - **Adı olmayan mesajlar** için, sohbete kimlikle ulaşan istek gönderdiğiniz geçmişe göre sohbetin sonuna yerleştirilir. Yanıtı verilmiş bir mesajın yeniden denemesi kayıtlı yanıtı yeniden alır. Reddedilen bir önceki isteğin kaydettiği mesajlar ikinci kez kaydedilmez. Ziyaretçi bir mesajı yanıtını aldıktan sonra yeniden yazarsa, örneğin yine "evet" derse, bu yeni bir mesajdır. Sohbetin kimliğiyle gelen ve henüz yanıtlanmamış mesajları taşıyan bir isteğin yeniden denemesi de onları ikinci kez kaydetmez; o arada yanıtlandılarsa yanıtları yeniden gelir. Tek istisna: siz son mesajı gönderdikten sonra ekibinizden biri yazdıysa, adı olmayan bir yeniden deneme mesajı ikinci kez kaydeder. Bunun için her mesaja bir `Idempotency-Key` ekleyin. Kimlik ve ad olmadan, yalnız geçmişle gelen bir yeniden deneme sohbeti bulmaz: yeni bir sohbet açar ve asistan mesaja yeniden yanıt verir. Reddedilen ya da ekipte bekleyen bir mesajı yeni mesajlarla birlikte yeniden gönderen istek de öyle. Responses'ta yalnız yeni mesajı `previous_response_id` ya da `item_reference` ile gönderen istemci için, gösterilen yanıtın karşılıklaşması geçmişin yerine geçer. - **Ne geçmiş ne anahtar** gönderen bir istemcinin, yani yalnız sohbet kimliğini ve yeni mesajı gönderenin yeniden denemesi yeni bir mesajdan ayırt edilemez. Böyle çalışıyorsanız her mesajda `Idempotency-Key` gönderin. - **Tarayıcı anahtarıyla**, istek sohbetin kimliğini ya da gösterilen yanıtı taşımıyorsa, örneğin ilk mesajda, mesajın adı ziyaretçinin ağ adresine ve Vercel AI SDK'da `useChat` kimliğine de bağlıdır. Başka bir adresten gelen deneme, örneğin Wi-Fi'dan mobil veriye geçince, yeni bir sohbet açar ve yeniden yanıtlanır. Aynı adresi paylaşan iki ziyaretçi, örneğin aynı ofis ağındakiler, aynı `Idempotency-Key`'i ya da aynı `useChat` kimliğiyle aynı mesaj kimliğini aynı metinle gönderirse ilk mesajları aynı sohbete düşebilir. Bu değerleri her ziyaretçi için rastgele üretin; Vercel AI SDK'nın varsayılanı budur. Sohbetin kimliğiyle ya da gösterilen yanıtla gelen deneme her adresten tanınır. Ayrıca: - Reddedilen bir ilk mesaj, örneğin `ai_turn_ceiling`, `plan_required` ya da `assistant_paused` alan, sohbet açmaz ve hiçbir şey kaydetmez. Biraz sonra yeniden gönderebilirsiniz. - Önceki deneme hâlâ yanıtlanıyorsa istek `409 conversation_busy` alır. `Retry-After` kadar bekleyip aynı anahtarla yeniden gönderin. - OpenAI SDK'ları `Idempotency-Key` başlığını kendileri eklemez. Node'da isteğin ikinci argümanında `headers`, Python'da `extra_headers` ile verin: [Chat Completions](https://docs.sonecentral.com/chat-completions.md#tekrar-denemek). SDK'nın kendi yeniden denemeleri aynı başlığı gönderir. `fetch` ile çalışıyorsanız: ```js title="gonder.js" // Aynı mesajı en fazla dört kez dener; her denemede aynı Idempotency-Key gider. export async function sendOnce(url, key, body) { const name = crypto.randomUUID(); // bu mesajın adı let response; for (let attempt = 0; attempt < 4; attempt += 1) { if (attempt > 0) await new Promise((done) => setTimeout(done, waitSeconds(response, attempt) * 1000)); try { response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'Idempotency-Key': name }, body: JSON.stringify(body), }); } catch { response = undefined; // ağ hatası: yeniden dene continue; } const retry = response.headers.get('x-should-retry') !== 'false' && (response.status === 409 || response.status === 429 || response.status >= 500); if (!retry) return response; } if (response) return response; throw new Error('Mesaj gönderilemedi.'); } function waitSeconds(response, attempt) { const after = Number(response?.headers.get('retry-after')); return after > 0 && after <= 60 ? after : 2 ** (attempt - 1); } ``` ## Tarayıcıda saklamak Sitenizin köşesinde açılan bir sohbet penceresinde sohbetin kimliğini ve gösterdiğiniz mesajları `localStorage`'da tutun. Ziyaretçi sayfayı yenilese ya da başka bir sayfaya geçse de sohbet kaldığı yerden sürer. ```js title="sohbet-kaydi.js" const STORE = 'sone-chat-v1'; export function loadChat() { try { return JSON.parse(localStorage.getItem(STORE)) ?? { conversationId: null, messages: [] }; } catch { return { conversationId: null, messages: [] }; } } export function saveChat(chat) { try { localStorage.setItem(STORE, JSON.stringify(chat)); } catch { // Gizli pencere ya da dolu depo: sohbet bu sekmede sürer, yenilenince baştan başlar. } } export function forgetChat() { try { localStorage.removeItem(STORE); } catch {} } ``` Her turdan sonra: ```js const chat = loadChat(); chat.conversationId = sone.conversation_id; // yanıttan chat.messages.push({ role: 'user', content: text }, ...sone.messages.map((m) => ({ role: 'assistant', message: m }))); saveChat(chat); ``` Sonraki istekte kimliği gönderin: ```js headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), // yeniden denerken aynısını kullanın }, body: JSON.stringify({ model: 'default', conversation_id: chat.conversationId ?? undefined, messages: [{ role: 'user', content: text }], }), ``` Kimlik gönderdiğinizde yalnız yeni mesajı yollamanız yeterli. Yalnız yeni mesajı gönderdiğiniz için `Idempotency-Key` burada gereklidir. "Yeni sohbet" düğmesi `forgetChat()` çağırır; sonraki mesaj yeni bir sohbet açar. İstek `404 conversation_not_found` ya da `400 conversation_model_mismatch` alırsa da saklanan kimliği silin. Bu tarifin tamamı: [Site içine sohbet penceresi](https://docs.sonecentral.com/sohbet-penceresi.md). ## Vercel AI SDK'da `useChat`'te sohbetin kimliği asistan mesajlarının `metadata.conversationId` alanındadır ve mesajlarla birlikte geri gider. Bu yüzden hem `useChat`'in `id`'sini hem mesajları, asistan mesajlarının `metadata` alanıyla birlikte saklayıp geri verin. Yalnız `id`'yi saklamak yetmez: tarayıcının seçtiği kimlik sohbeti yalnız [dört koşul](#sohbet-nasil-bulunur) tutarsa sürdürür, sohbet ekipteyken hiç sürdürmez. ```tsx 'use client'; import { useChat } from '@ai-sdk/react'; import type { ChatTransport } from 'ai'; import { useEffect, useState } from 'react'; import type { SoneUIMessage } from '@/lib/sone'; function storedChat(): { id: string; messages: SoneUIMessage[] } { try { const saved = JSON.parse(localStorage.getItem('sone-chat-v1') ?? 'null'); if (saved?.id) return saved; } catch {} return { id: crypto.randomUUID(), messages: [] }; } export function useStoredChat(transport: ChatTransport) { const [initial] = useState(storedChat); const chat = useChat({ id: initial.id, messages: initial.messages, transport }); useEffect(() => { if (chat.status === 'submitted' || chat.status === 'streaming') return; try { localStorage.setItem('sone-chat-v1', JSON.stringify({ id: initial.id, messages: chat.messages })); } catch {} }, [chat.status, chat.messages, initial.id]); return chat; } ``` Bu kanca yalnız tarayıcıda çalışmalı. Next.js'te onu kullanan bileşeni bir `'use client'` dosyasında `dynamic(() => import(...), { ssr: false })` ile yükleyin. Uzun sohbetlerde yalnız son mesajı göndermek için: [Vercel AI SDK](https://docs.sonecentral.com/vercel-ai-sdk.md#uzun-sohbetlerde-yalniz-son-mesaji-gondermek). Örneğin tamamı: [Site içine sohbet penceresi](https://docs.sonecentral.com/sohbet-penceresi.md#react-ve-nextjs). ## Sunucu anahtarı ve `user` Kendi sisteminizde oturum açmış kullanıcılar için `user` alanına kendi kullanıcı kimliğinizi koyabilirsiniz. Aynı değer aynı asistanla aynı sohbeti bulur, böylece kullanıcı farklı cihazlardan aynı sohbeti sürdürür. `user` yalnız sohbeti bulmak içindir, kimlik kanıtı değildir: asistan bu kişiyi yine anonim ziyaretçi sayar ve kişisel kayıtlarını paylaşmaz. Ayrıntılar: [Siparişler, randevular ve gizlilik](https://docs.sonecentral.com/siparis-randevu-gizlilik.md). Tarayıcı anahtarıyla gönderilen `user` yok sayılır, çünkü ziyaretçi onu değiştirebilir. E-posta ya da telefon gibi kişisel bir veriyi `user` olarak göndermeyin. Kendi sisteminizdeki anlamsız bir kimlik yeterli. ## Sınırlar - Bir sohbette en fazla 200 ziyaretçi mesajı olur. Sonrası `400 conversation_full` alır; yeni sohbet başlatın. - Tek istekte en fazla 12 yeni ziyaretçi mesajı gönderilir. - Yeni bir mesaj en fazla 4.000 karakterdir. - Yeni bir sohbete en fazla son 40 önceki mesaj aktarılır. - İstek gövdesi en fazla 1 MB'dir. Uzun geçmişleri her istekte göndermek yerine sohbetin kimliğini gönderin. --- # Site içine sohbet penceresi Sayfa: https://docs.sonecentral.com/sohbet-penceresi Markdown: https://docs.sonecentral.com/sohbet-penceresi.md > Sitenizin köşesinde açılan, asistanınızla konuşan bir sohbet penceresi. Kopyalayıp yapıştırabileceğiniz düz HTML ve JavaScript ile React ve Next.js sürümü. Bu sayfadaki iki tarif de eksiksizdir: akışı okur, sohbeti tarayıcıda saklar, ürün kartlarını ve düğmeleri çizer, sohbet ekibe geçince ekibin yanıtlarını gösterir. Başlamadan önce Sonecentral'da bir **tarayıcı anahtarı** oluşturun ve sitenizin adresini anahtarın **İzin verilen siteler** listesine ekleyin. Geliştirme için `http://localhost:3000` gibi bir adres de eklenebilir. Ayrıntılar: [Kimlik doğrulama ve anahtarlar](https://docs.sonecentral.com/kimlik-dogrulama.md). ## Düz HTML ve JavaScript Hiçbir kütüphane gerekmez. Aşağıdaki üç parçayı sayfanızın `` etiketinden hemen önce yapıştırın ve `KEY` değerini kendi tarayıcı anahtarınızla değiştirin. ```html title="sohbet.html" ``` ```css title="sohbet.css" #sone-open { position: fixed; right: 20px; bottom: 20px; z-index: 50; padding: 12px 18px; border: 0; border-radius: 999px; background: #17123f; color: #fff; font: 600 15px/1 system-ui, sans-serif; cursor: pointer; } #sone-panel { position: fixed; right: 20px; bottom: 76px; z-index: 50; width: min(380px, calc(100vw - 40px)); height: min(580px, calc(100dvh - 110px)); display: flex; flex-direction: column; overflow: hidden; background: #fff; color: #17123f; border-radius: 16px; box-shadow: 0 2px 6px rgba(23, 18, 63, 0.08), 0 12px 32px rgba(23, 18, 63, 0.12); font: 15px/1.45 system-ui, sans-serif; } #sone-panel[hidden] { display: none; } .sone-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; } .sone-head button { border: 0; background: none; color: #524d7a; font: inherit; font-size: 13px; cursor: pointer; } #sone-log { flex: 1; overflow-y: auto; margin: 0; padding: 4px 16px 12px; list-style: none; display: flex; flex-direction: column; gap: 8px; } .sone-row { display: flex; flex-direction: column; align-items: flex-start; } .sone-row.sone-user { align-items: flex-end; } .sone-text { margin: 0; max-width: 85%; padding: 8px 12px; border-radius: 14px; background: #f1eefb; white-space: pre-wrap; } .sone-user .sone-text { background: #17123f; color: #fff; } .sone-staff .sone-text::before { content: 'Ekip'; display: block; font-size: 12px; font-weight: 600; color: #524d7a; } .sone-catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; width: 100%; } .sone-item { display: flex; flex-direction: column; gap: 2px; padding: 10px; border-radius: 12px; background: #f7f5fd; } .sone-item img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; margin-bottom: 4px; } .sone-item span { font-size: 13px; color: #524d7a; } .sone-item .sone-price { color: #17123f; font-weight: 600; } .sone-item a, .sone-file { color: #4f31d6; font-size: 14px; font-weight: 600; } .sone-actions { display: flex; flex-wrap: wrap; gap: 6px; } .sone-actions p { width: 100%; margin: 0 0 2px; } .sone-actions button, .sone-actions a { padding: 6px 12px; border: 1px solid #cfc5f7; border-radius: 999px; background: #fff; color: #4f31d6; font: inherit; font-size: 14px; text-decoration: none; cursor: pointer; } #sone-status { margin: 0; padding: 0 16px 8px; font-size: 13px; color: #524d7a; } #sone-form { display: flex; gap: 8px; padding: 12px; background: #faf9fe; } #sone-input { flex: 1; resize: none; border: 1px solid #ddd6f3; border-radius: 10px; padding: 8px 10px; font: inherit; } #sone-form button { border: 0; border-radius: 10px; padding: 0 14px; background: #17123f; color: #fff; font: inherit; font-weight: 600; cursor: pointer; } ``` ```html title="sohbet.js" ``` Pencereyi kendi tasarımınıza uydurmak için yalnız CSS'i ve düğme yazılarını değiştirin. Asistanın paneldeki adını göstermek isterseniz `GET /models` yanıtındaki `name` alanını kullanabilirsiniz. ### Bu tarif ne yapar - Mesajı `POST /chat/completions` ile akışlı gönderir ve metni geldikçe gösterir. Her mesaj tamamlanınca, parçanın üst düzeyindeki `sone.message` ile o mesajı ürün kartı, düğme ya da dosya olarak yeniden çizer. - Sohbetin kimliğini gönderdiği için yalnız yeni mesajı yollar ve her mesajda yeni bir `Idempotency-Key` gönderir. - Asistan bir araç kullanırken `sone.activity` metnini durum satırında gösterir. - Hızlı yanıtlara ve adresi olmayan düğmelere tıklanınca başlıklarını ziyaretçinin yeni mesajı olarak gönderir. - Sohbetin kimliğini, gösterilen mesajları ve okunan son satırı `localStorage`'da saklar. Sayfa yenilense de sohbet sürer; depolama kapalıysa sohbet o sekmede sürer. - Sohbet ekibe geçerse, yani `status` `handoff` ya da `human` gelirse, beş saniyede bir `GET /conversations/{id}/messages?after=` ile ekibin yanıtlarını okur. İmleç olarak hep yanıttaki `last_id`'yi gönderir, ziyaretçinin kendi satırlarını çizmez ve `mode` `"assistant"` olunca sormayı bırakır. Pencere kapalıyken ya da sekme arka plandayken sormaz. Aynı anda çok ziyaretçi ekip yanıtı bekleyebiliyorsa aralığı uzatın: [Ne sıklıkla sormalı](https://docs.sonecentral.com/insan-devri.md#ne-siklikla-sormali). - Açık bir sohbette asistan yanıt veremezse (`ai_turn_ceiling`, `assistant_quota_exhausted`, `plan_required`, `assistant_paused`) mesajın ekibe ulaştığını söyler ve ekibin yanıtını bekler. - Saklanan sohbet artık bulunamazsa (`conversation_not_found`) sonraki mesajda yeni bir sohbet açar. - Akış içinde gelen bir hata parçasını yakalar ve durum satırında gösterir. ## React ve Next.js Vercel AI SDK ile aynı pencere. Önce `lib/sone.ts` dosyasına [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md) sayfasındaki `SoneMessage`, `SoneCatalog`, `SoneActions` ve `SoneFile` tiplerini ve [Vercel AI SDK](https://docs.sonecentral.com/vercel-ai-sdk.md#tipli-mesajlar) sayfasındaki `SoneUIMessage` tipini ekleyin. ```npm npm i ai @ai-sdk/react ``` ```tsx title="components/sone-chat.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useEffect, useState } from 'react'; import type { SoneMessage, SoneUIMessage } from '@/lib/sone'; const API = 'https://pb.sonecentral.com/api/v1'; const KEY = process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY!; const STORE = 'sone-chat-v1'; const conversationOf = (messages: SoneUIMessage[]) => messages.findLast((m) => m.metadata?.conversationId)?.metadata?.conversationId; const transport = new DefaultChatTransport({ api: `${API}/chat`, headers: { Authorization: `Bearer ${KEY}` }, // Sohbetin kimliği varsa yalnız yeni mesaj gider; gerçek kayıt Sonecentral'dadır. prepareSendMessagesRequest: ({ id, messages }) => { const conversationId = conversationOf(messages); return { body: { id, model: 'default', conversationId, messages: conversationId ? messages.slice(-1) : messages }, }; }, }); // team: sohbet ekipte. cursor: sohbette okunan son satırın kimliği. type Stored = { id: string; messages: SoneUIMessage[]; team: boolean; cursor: string | null }; type ThreadPage = { data: SoneMessage[]; has_more: boolean; mode: 'assistant' | 'team'; last_id?: string; }; const fresh = (): Stored => ({ id: crypto.randomUUID(), messages: [], team: false, cursor: null }); function readStored(): Stored { try { const saved = JSON.parse(localStorage.getItem(STORE) ?? 'null'); if (saved?.id) return { team: false, cursor: null, ...saved }; } catch {} return fresh(); } // Ekipten birinin yazdığı mesajı, akışta gelecek veri parçasıyla aynı biçime çevirir. function fromTeam(message: SoneMessage): SoneUIMessage { const data = { text: message.text, role: 'staff' as const }; const part = message.type === 'catalog' ? { type: 'data-sone-catalog' as const, id: message.id, data: { ...data, catalog: message.catalog } } : message.type === 'actions' ? { type: 'data-sone-actions' as const, id: message.id, data: { ...data, actions: message.actions } } : message.type === 'file' ? { type: 'data-sone-file' as const, id: message.id, data: { ...data, file: message.file } } : { type: 'data-sone-staff' as const, id: message.id, data }; return { id: message.id, role: 'assistant', parts: [part] }; } // Bir mesajın ekipten gelip gelmediği: veri parçalarının role alanına bakar. function byTeam(message: SoneUIMessage) { return message.parts.some((part) => 'data' in part && 'role' in part.data && part.data.role === 'staff'); } export function SoneChat() { const [stored, setStored] = useState(readStored); const [open, setOpen] = useState(false); const [input, setInput] = useState(''); const [activity, setActivity] = useState(''); const [team, setTeam] = useState(stored.team); const [cursor, setCursor] = useState(stored.cursor); // id değişince useChat yeni bir sohbet başlatır. const { messages, setMessages, sendMessage, status, error } = useChat({ id: stored.id, messages: stored.messages, transport, onData: (part) => { if (part.type === 'data-sone-status') setActivity(part.data.label); }, onError: () => setActivity(''), onFinish: ({ message }) => { setActivity(''); const turn = message.metadata?.status; if (turn) setTeam(turn === 'handoff' || turn === 'human'); }, }); const busy = status === 'submitted' || status === 'streaming'; useEffect(() => { if (busy) return; try { localStorage.setItem(STORE, JSON.stringify({ id: stored.id, messages, team, cursor })); } catch {} }, [busy, messages, team, cursor, stored.id]); // Sohbet ekipteyken ekibin yazdıklarını okur. Asistanın satırları hep akıştan // gelir; listeden yalnız ekibin satırları, kimliğe göre bir kez eklenir. // İlk okuma sohbetin başından sayfa sayfa sona gider, sonrakiler imleçten sonrasını okur. const conversationId = conversationOf(messages); useEffect(() => { if (!open || !team || busy || !conversationId) return; const read = async () => { if (document.hidden) return; let next = cursor; let mode: ThreadPage['mode'] = 'team'; const fromStaff: SoneMessage[] = []; for (;;) { const url = new URL(`${API}/conversations/${encodeURIComponent(conversationId)}/messages`); if (next) url.searchParams.set('after', next); const response = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } }).catch(() => null); if (response?.status === 400) setCursor(null); // invalid_cursor: sonraki okuma baştan başlar if (!response?.ok) return; const page = (await response.json()) as ThreadPage; fromStaff.push(...page.data.filter((m) => m.role === 'staff')); next = page.last_id ?? next; mode = page.mode; if (!page.has_more) break; } setMessages((current) => { const known = new Set(current.flatMap((m) => [m.id, ...m.parts.map((p) => ('id' in p ? p.id : undefined))])); const added = fromStaff.filter((m) => !known.has(m.id)).map(fromTeam); return added.length > 0 ? [...current, ...added] : current; }); setCursor(next); if (mode === 'assistant') setTeam(false); }; const timer = setInterval(read, 5000); return () => clearInterval(timer); }, [open, team, busy, conversationId, cursor, setMessages]); const send = (text: string) => { if (!text.trim() || busy) return; sendMessage({ text }); }; const reset = () => { const next = fresh(); try { localStorage.removeItem(STORE); } catch {} setStored(next); setTeam(false); setCursor(null); setActivity(''); }; // Sohbet ekipteyken bir tur yanıtsız biter; içi boş asistan mesajı çizilmez. const shown = messages.filter((m) => m.parts.some((p) => p.type === 'text' || p.type.startsWith('data-'))); // Seçenekler, ziyaretçi yeniden yazana kadar gösterilir. const lastUser = shown.map((m) => m.role).lastIndexOf('user'); return ( <> {open ? (
Bize yazın
    {shown.map((message, messageIndex) => (
  1. {message.parts.map((part, index) => { const key = `${message.id}-${index}`; if (part.type === 'text') return

    {part.text}

    ; if (part.type === 'data-sone-staff') return

    {part.data.text}

    ; if (part.type === 'data-sone-catalog') return (
    ); if (part.type === 'data-sone-actions') { const current = messageIndex > lastUser; const actions = part.data.actions; return (
    {actions?.title ? {actions.title} : null} {actions?.text ?

    {actions.text}

    : null} {actions?.buttons?.map((button) => ( {button.title} ))} {current ? actions?.choices?.map((choice) => ( )) : null}
    ); } if (part.type === 'data-sone-file') return part.data.file?.url ? ( {part.data.file.name} ) : (

    {part.data.text}

    ); return null; })}
  2. ))}
{activity ?

{activity}…

: null} {team ?

Ekibimiz en kısa sürede burada yanıt verecek.

: null} {error ?

{error.message || 'Mesaj gönderilemedi. Biraz sonra yeniden deneyin.'}

: null}
{ event.preventDefault(); send(input); setInput(''); }} >