Menerapkan penyimpanan khusus untuk bot Anda
BERLAKU UNTUK: SDK v4
Interaksi bot jatuh ke dalam tiga area: pertukaran aktivitas dengan Azure AI Bot Service, pemuatan dan penyimpanan bot dan status dialog dengan penyimpanan memori, dan integrasi dengan layanan back-end.
Artikel ini mengeksplorasi cara memperluas semantik antara Azure AI Bot Service dan status memori dan penyimpanan bot.
Catatan
Bot Framework JavaScript, C#, dan Python SDK akan terus didukung, namun, Java SDK dihentikan dengan dukungan jangka panjang akhir yang berakhir pada November 2023.
Bot yang ada yang dibangun dengan Java SDK akan terus berfungsi.
Untuk pembuatan bot baru, pertimbangkan untuk menggunakan Microsoft Copilot Studio dan baca tentang memilih solusi salinan yang tepat.
Untuk informasi selengkapnya, lihat Masa depan pembuatan bot.
Prasyarat
- Pengetahuan tentang Dasar-Dasar Microsoft Bot Framework, percakapan berbasis peristiwa menggunakan penanganan aktivitas, dan Mengelola status.
- Salinan sampel peluasan skala di C#, Python, atau Java.
Artikel ini berfokus pada sampel versi C#.
Latar belakang
Bot Framework SDK mencakup implementasi default status bot dan penyimpanan memori. Implementasi ini sesuai dengan kebutuhan aplikasi di mana potongan digunakan bersama dengan beberapa baris kode inisialisasi, seperti yang ditunjukkan dalam banyak sampel.
SDK adalah kerangka kerja dan bukan aplikasi dengan perilaku tetap. Dengan kata lain, implementasi banyak mekanisme dalam kerangka kerja adalah implementasi default dan bukan satu-satunya implementasi yang mungkin. Kerangka kerja tidak menentukan hubungan antara pertukaran aktivitas dengan Azure AI Bot Service dan pemuatan dan penyimpanan status bot apa pun.
Artikel ini menjelaskan salah satu cara untuk memodifikasi semantik status default dan implementasi penyimpanan ketika tidak cukup berfungsi untuk aplikasi Anda. Sampel peluasan skala menyediakan implementasi status dan penyimpanan alternatif yang memiliki semantik yang berbeda dari yang default. Solusi alternatif ini berada sama baiknya dalam kerangka kerja. Tergantung pada skenario Anda, solusi alternatif ini mungkin lebih sesuai untuk aplikasi yang Anda kembangkan.
Perilaku adaptor default dan penyedia penyimpanan
Dengan implementasi default, saat menerima aktivitas, bot memuat status yang sesuai dengan percakapan. Kemudian menjalankan logika dialog dengan status ini dan aktivitas masuk. Dalam proses menjalankan dialog, satu atau beberapa aktivitas keluar dibuat dan segera dikirim. Ketika pemrosesan dialog selesai, bot menyimpan status yang diperbarui, menimpa status lama.
Namun, beberapa hal dapat salah dengan perilaku ini.
Jika operasi penyimpanan gagal karena beberapa alasan, status secara implisit tergelincir tidak sinkron dengan apa yang dilihat pengguna di saluran. Pengguna telah melihat respons dari bot dan percaya bahwa status telah bergerak maju, tetapi belum. Kesalahan ini bisa lebih buruk daripada jika pembaruan status berhasil tetapi pengguna tidak menerima pesan respons.
Kesalahan status tersebut dapat memiliki implikasi untuk desain percakapan Anda. Misalnya, dialog mungkin memerlukan pertukaran konfirmasi tambahan, jika tidak berlebihan dengan pengguna.
Jika implementasi disebarkan diskalakan di beberapa simpul, status dapat secara tidak sengaja ditimpa. Kesalahan ini dapat membingungkan karena dialog kemungkinan akan mengirim aktivitas ke saluran yang membawa pesan konfirmasi.
Pertimbangkan bot pesanan pizza, di mana bot meminta pengguna untuk pilihan topping, dan pengguna mengirim dua pesan cepat: satu untuk menambahkan jamur dan satu untuk menambahkan keju. Dalam skenario peluasan skala, beberapa instans bot mungkin aktif, dan dua pesan pengguna dapat ditangani oleh dua instans terpisah pada komputer terpisah. Konflik semacam itu disebut sebagai kondisi balapan, di mana satu mesin mungkin menimpa status yang ditulis oleh komputer lain. Namun, karena respons sudah dikirim, pengguna menerima konfirmasi bahwa jamur dan keju ditambahkan ke pesanan mereka. Sayangnya, ketika pizza tiba, itu hanya berisi jamur atau keju, tetapi tidak keduanya.
Penguncian optimis
Sampel peluasan skala memperkenalkan beberapa penguncian di sekitar status. Sampel menerapkan penguncian optimis, yang memungkinkan setiap instans berjalan seolah-olah itu adalah satu-satunya yang berjalan dan kemudian memeriksa pelanggaran konkurensi apa pun. Penguncian ini mungkin terdengar rumit, tetapi solusi yang diketahui ada, dan Anda dapat menggunakan teknologi penyimpanan cloud dan titik ekstensi yang tepat dalam Kerangka Kerja Bot.
Sampel menggunakan mekanisme HTTP standar berdasarkan header tag entitas (ETag). Memahami mekanisme ini sangat penting untuk memahami kode yang mengikuti. Diagram berikut mengilustrasikan urutannya.
Diagram memiliki dua klien yang melakukan pembaruan ke beberapa sumber daya.
Saat klien mengeluarkan permintaan GET dan sumber daya dikembalikan dari server, server menyertakan header ETag.
Header ETag adalah nilai buram yang mewakili status sumber daya. Jika sumber daya diubah, server memperbarui ETag-nya untuk sumber daya.
Ketika klien ingin mempertahankan perubahan status, klien mengeluarkan permintaan POST ke server, dengan nilai ETag dalam
If-Match
header prasyarat.Jika nilai ETag permintaan tidak cocok dengan server, maka pemeriksaan prasyarat gagal dengan
412
respons (Prasyarat Gagal).Kegagalan ini menunjukkan bahwa nilai saat ini pada server tidak lagi cocok dengan nilai asli yang dioperasikan klien.
Jika klien menerima respons gagal prasyarat, klien biasanya mendapatkan nilai baru untuk sumber daya, menerapkan pembaruan yang diinginkan, dan mencoba memposting pembaruan sumber daya lagi.
Permintaan POST kedua ini berhasil jika tidak ada klien lain yang memperbarui sumber daya. Jika tidak, klien dapat mencoba lagi.
Proses ini disebut optimis karena klien, setelah memiliki sumber daya, melanjutkan untuk melakukan pemrosesannya—sumber daya itu sendiri tidak dikunci, karena klien lain dapat mengaksesnya tanpa batasan apa pun. Pertikaian apa pun antara klien atas status sumber daya yang seharusnya tidak ditentukan sampai pemrosesan telah dilakukan. Dalam sistem terdistribusi, strategi ini sering lebih optimal daripada pendekatan pesimis yang berlawanan.
Mekanisme penguncian optimis seperti yang dijelaskan mengasumsikan bahwa logika program Anda dapat dicoba kembali dengan aman. Situasi yang ideal adalah di mana permintaan layanan ini idempotensi. Dalam ilmu komputer, operasi idempotensi adalah operasi yang tidak memiliki efek ekstra jika disebut lebih dari sekali dengan parameter input yang sama. Layanan HTTP REST murni yang mengimplementasikan permintaan GET, PUT, dan DELETE seringkali idempotensi. Jika permintaan layanan tidak akan menghasilkan efek tambahan, maka permintaan dapat dijalankan kembali dengan aman sebagai bagian dari strategi coba lagi.
Sampel peluasan skala dan sisa artikel ini mengasumsikan bahwa layanan backend yang digunakan bot Anda adalah semua layanan HTTP REST idempogen.
Buffering aktivitas keluar
Mengirim aktivitas bukanlah operasi yang idempotensi. Aktivitas sering kali merupakan pesan yang menyampaikan informasi kepada pengguna, dan mengulangi pesan yang sama dua kali atau lebih mungkin membingungkan atau menyesatkan.
Penguncian optimis menyiratkan bahwa logika bot Anda mungkin perlu dijalankan ulang beberapa kali. Untuk menghindari pengiriman aktivitas tertentu beberapa kali, tunggu hingga operasi pembaruan status berhasil sebelum mengirim aktivitas kepada pengguna. Logika bot Anda akan terlihat seperti diagram berikut.
Setelah Anda membuat perulangan coba lagi ke dalam eksekusi dialog, Anda memiliki perilaku berikut ketika ada kegagalan prasyarat pada operasi penyimpanan.
Dengan mekanisme ini, bot pizza dari contoh sebelumnya tidak boleh mengirim pengakuan positif yang salah tentang topping pizza yang ditambahkan ke pesanan. Bahkan dengan bot yang disebarkan di beberapa komputer, skema penguncian optimis secara efektif menserialisasikan pembaruan status. Dalam bot pizza, pengakuan dari menambahkan item sekarang bahkan dapat mencerminkan status penuh secara akurat. Misalnya, jika pengguna dengan cepat mengetik "keju" dan kemudian "jamur", dan pesan-pesan ini ditangani oleh dua instans bot yang berbeda, instans terakhir yang harus diselesaikan dapat mencakup "pizza dengan keju dan jamur" sebagai bagian dari responsnya.
Solusi penyimpanan kustom baru ini melakukan tiga hal yang tidak dilakukan implementasi default di SDK:
- Ini menggunakan ETags untuk mendeteksi ketidakcocokan.
- Ini mencoba kembali pemrosesan ketika kegagalan ETag terdeteksi.
- Ini menunggu untuk mengirim aktivitas keluar sampai berhasil menyimpan status.
Sisa artikel ini menjelaskan implementasi ketiga bagian ini.
Menerapkan dukungan ETag
Pertama, tentukan antarmuka untuk penyimpanan baru kami yang mencakup dukungan ETag. Antarmuka membantu menggunakan mekanisme injeksi dependensi dalam ASP.NET. Dimulai dengan antarmuka memungkinkan Anda menerapkan versi terpisah untuk pengujian unit dan untuk produksi. Misalnya, versi pengujian unit mungkin berjalan dalam memori dan tidak memerlukan koneksi jaringan.
Antarmuka terdiri dari metode pemuatan dan penyimpanan . Kedua metode akan menggunakan parameter kunci untuk mengidentifikasi status yang akan dimuat atau disimpan ke penyimpanan.
- Muat akan mengembalikan nilai status dan ETag terkait.
- Simpan akan memiliki parameter untuk nilai status dan ETag terkait dan mengembalikan nilai Boolean yang menunjukkan apakah operasi berhasil. Nilai pengembalian tidak akan berfungsi sebagai indikator kesalahan umum, tetapi sebagai indikator tertentu dari kegagalan prasyarat. Memeriksa kode pengembalian akan menjadi bagian dari logika perulangan coba lagi.
Untuk membuat implementasi penyimpanan diterapkan secara luas, hindari menempatkan persyaratan serialisasi di dalamnya.
Namun, banyak layanan penyimpanan modern mendukung JSON sebagai jenis konten.
Di C#, Anda dapat menggunakan JObject
jenis untuk mewakili objek JSON.
Di JavaScript atau TypeScript, JSON adalah objek asli biasa.
Berikut adalah definisi antarmuka kustom.
IStore.cs
public interface IStore
{
Task<(JObject content, string etag)> LoadAsync(string key);
Task<bool> SaveAsync(string key, JObject content, string etag);
}
Berikut adalah implementasi untuk Azure Blob Storage.
BlobStore.cs
public class BlobStore : IStore
{
private readonly CloudBlobContainer _container;
public BlobStore(string accountName, string accountKey, string containerName)
{
if (string.IsNullOrWhiteSpace(accountName))
{
throw new ArgumentException(nameof(accountName));
}
if (string.IsNullOrWhiteSpace(accountKey))
{
throw new ArgumentException(nameof(accountKey));
}
if (string.IsNullOrWhiteSpace(containerName))
{
throw new ArgumentException(nameof(containerName));
}
var storageCredentials = new StorageCredentials(accountName, accountKey);
var cloudStorageAccount = new CloudStorageAccount(storageCredentials, useHttps: true);
var client = cloudStorageAccount.CreateCloudBlobClient();
_container = client.GetContainerReference(containerName);
}
public async Task<(JObject content, string etag)> LoadAsync(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException(nameof(key));
}
var blob = _container.GetBlockBlobReference(key);
try
{
var content = await blob.DownloadTextAsync();
var obj = JObject.Parse(content);
var etag = blob.Properties.ETag;
return (obj, etag);
}
catch (StorageException e)
when (e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.NotFound)
{
return (new JObject(), null);
}
}
public async Task<bool> SaveAsync(string key, JObject obj, string etag)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException(nameof(key));
}
if (obj == null)
{
throw new ArgumentNullException(nameof(obj));
}
var blob = _container.GetBlockBlobReference(key);
blob.Properties.ContentType = "application/json";
var content = obj.ToString();
if (etag != null)
{
try
{
await blob.UploadTextAsync(content, Encoding.UTF8, new AccessCondition { IfMatchETag = etag }, new BlobRequestOptions(), new OperationContext());
}
catch (StorageException e)
when (e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.PreconditionFailed)
{
return false;
}
}
else
{
await blob.UploadTextAsync(content);
}
return true;
}
}
Azure Blob Storage melakukan banyak pekerjaan. Setiap metode memeriksa pengecualian tertentu untuk memenuhi harapan kode panggilan.
- Metode ini
LoadAsync
, sebagai respons terhadap pengecualian penyimpanan dengan kode status yang tidak ditemukan , mengembalikan nilai null. - Metode ini
SaveAsync
, sebagai respons terhadap pengecualian penyimpanan dengan kode prasyaratfalse
gagal, mengembalikan .
Menerapkan perulangan coba lagi
Desain perulangan coba lagi mengimplementasikan perilaku yang ditunjukkan dalam diagram urutan.
Saat menerima aktivitas, buat kunci untuk status percakapan.
Hubungan antara aktivitas dan status percakapan sama untuk penyimpanan kustom seperti untuk implementasi default. Oleh karena itu, Anda dapat membangun kunci dengan cara yang sama seperti implementasi status default.
Coba muat status percakapan.
Jalankan dialog bot dan ambil aktivitas keluar untuk dikirim.
Coba simpan status percakapan.
Setelah berhasil, kirim aktivitas keluar dan keluar.
Jika gagal, ulangi proses ini dari langkah untuk memuat status percakapan.
Beban baru status percakapan mendapatkan status ETag dan percakapan baru dan saat ini. Dialog dijalankan ulang, dan langkah status simpan memiliki kesempatan untuk berhasil.
Berikut adalah implementasi untuk penanganan aktivitas pesan.
ScaleoutBot.cs
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// Create the storage key for this conversation.
var key = $"{turnContext.Activity.ChannelId}/conversations/{turnContext.Activity.Conversation?.Id}";
// The execution sits in a loop because there might be a retry if the save operation fails.
while (true)
{
// Load any existing state associated with this key
var (oldState, etag) = await _store.LoadAsync(key);
// Run the dialog system with the old state and inbound activity, the result is a new state and outbound activities.
var (activities, newState) = await DialogHost.RunAsync(_dialog, turnContext.Activity, oldState, cancellationToken);
// Save the updated state associated with this key.
var success = await _store.SaveAsync(key, newState, etag);
// Following a successful save, send any outbound Activities, otherwise retry everything.
if (success)
{
if (activities.Any())
{
// This is an actual send on the TurnContext we were given and so will actual do a send this time.
await turnContext.SendActivitiesAsync(activities, cancellationToken);
}
break;
}
}
}
Catatan
Sampel mengimplementasikan eksekusi dialog sebagai panggilan fungsi. Pendekatan yang lebih canggih mungkin adalah menentukan antarmuka dan menggunakan injeksi dependensi. Namun, untuk contoh ini, fungsi statis menekankan sifat fungsi dari pendekatan penguncian optimis ini. Secara umum, ketika Anda mengimplementasikan bagian penting dari kode Anda dengan cara yang fungsional, Anda meningkatkan peluangnya untuk bekerja dengan sukses di jaringan.
Menerapkan buffer aktivitas keluar
Persyaratan berikutnya adalah buffer aktivitas keluar sampai setelah operasi penyimpanan berhasil terjadi, yang memerlukan implementasi adaptor kustom.
Metode kustom SendActivitiesAsync
tidak boleh mengirim aktivitas ke penggunaan, tetapi menambahkan aktivitas ke daftar.
Kode dialog Anda tidak memerlukan modifikasi.
- Dalam skenario khusus ini, aktivitas pembaruan dan operasi aktivitas penghapusan tidak didukung dan metode terkait tidak akan melemparkan pengecualian yang diterapkan.
- Nilai pengembalian dari operasi aktivitas pengiriman digunakan oleh beberapa saluran untuk memungkinkan bot memodifikasi atau menghapus pesan yang dikirim sebelumnya, misalnya, untuk menonaktifkan tombol pada kartu yang ditampilkan di saluran. Pertukaran pesan ini bisa menjadi rumit, terutama ketika status diperlukan, dan berada di luar cakupan artikel ini.
- Dialog Anda membuat dan menggunakan adaptor kustom ini, sehingga dapat menyangga aktivitas.
- Handler giliran bot Anda akan menggunakan standar yang lebih standar
AdapterWithErrorHandler
untuk mengirim aktivitas kepada pengguna.
Berikut adalah implementasi adaptor kustom.
DialogHostAdapter.cs
public class DialogHostAdapter : BotAdapter
{
private List<Activity> _response = new List<Activity>();
public IEnumerable<Activity> Activities => _response;
public override Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext turnContext, Activity[] activities, CancellationToken cancellationToken)
{
foreach (var activity in activities)
{
_response.Add(activity);
}
return Task.FromResult(new ResourceResponse[0]);
}
#region Not Implemented
public override Task DeleteActivityAsync(ITurnContext turnContext, ConversationReference reference, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public override Task<ResourceResponse> UpdateActivityAsync(ITurnContext turnContext, Activity activity, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
#endregion
}
Menggunakan penyimpanan kustom Anda dalam bot
Langkah terakhir adalah menggunakan kelas dan metode kustom ini dengan kelas dan metode kerangka kerja yang ada.
- Perulangan coba lagi utama menjadi bagian dari metode bot
ActivityHandler.OnMessageActivityAsync
Anda dan menyertakan penyimpanan kustom Anda melalui injeksi dependensi. - Kode hosting dialog ditambahkan ke
DialogHost
kelas yang mengekspos metode statisRunAsync
. Host dialog:- Mengambil aktivitas masuk dan status lama lalu mengembalikan aktivitas yang dihasilkan dan status baru.
- Membuat adaptor kustom dan menjalankan dialog dengan cara yang sama seperti yang dilakukan SDK.
- Membuat pengakses properti status kustom, shim yang meneruskan status dialog ke dalam sistem dialog. Aksesor menggunakan semantik referensi untuk meneruskan handel aksesor ke sistem dialog.
Tip
Serialisasi JSON ditambahkan sebaris ke kode hosting untuk menyimpannya di luar lapisan penyimpanan yang dapat dicolokkan, sehingga implementasi yang berbeda dapat diserialisasikan secara berbeda.
Berikut adalah implementasi host dialog.
DialogHost.cs
public static class DialogHost
{
// The serializer to use. Moving the serialization to this layer will make the storage layer more pluggable.
private static readonly JsonSerializer StateJsonSerializer = new JsonSerializer() { TypeNameHandling = TypeNameHandling.All };
/// <summary>
/// A function to run a dialog while buffering the outbound Activities.
/// </summary>
/// <param name="dialog">THe dialog to run.</param>
/// <param name="activity">The inbound Activity to run it with.</param>
/// <param name="oldState">Th eexisting or old state.</param>
/// <returns>An array of Activities 'sent' from the dialog as it executed. And the updated or new state.</returns>
public static async Task<(Activity[], JObject)> RunAsync(Dialog dialog, IMessageActivity activity, JObject oldState, CancellationToken cancellationToken)
{
// A custom adapter and corresponding TurnContext that buffers any messages sent.
var adapter = new DialogHostAdapter();
var turnContext = new TurnContext(adapter, (Activity)activity);
// Run the dialog using this TurnContext with the existing state.
var newState = await RunTurnAsync(dialog, turnContext, oldState, cancellationToken);
// The result is a set of activities to send and a replacement state.
return (adapter.Activities.ToArray(), newState);
}
/// <summary>
/// Execute the turn of the bot. The functionality here closely resembles that which is found in the
/// IBot.OnTurnAsync method in an implementation that is using the regular BotFrameworkAdapter.
/// Also here in this example the focus is explicitly on Dialogs but the pattern could be adapted
/// to other conversation modeling abstractions.
/// </summary>
/// <param name="dialog">The dialog to be run.</param>
/// <param name="turnContext">The ITurnContext instance to use. Note this is not the one passed into the IBot OnTurnAsync.</param>
/// <param name="state">The existing or old state of the dialog.</param>
/// <returns>The updated or new state of the dialog.</returns>
private static async Task<JObject> RunTurnAsync(Dialog dialog, ITurnContext turnContext, JObject state, CancellationToken cancellationToken)
{
// If we have some state, deserialize it. (This mimics the shape produced by BotState.cs.)
var dialogStateProperty = state?[nameof(DialogState)];
var dialogState = dialogStateProperty?.ToObject<DialogState>(StateJsonSerializer);
// A custom accessor is used to pass a handle on the state to the dialog system.
var accessor = new RefAccessor<DialogState>(dialogState);
// Run the dialog.
await dialog.RunAsync(turnContext, accessor, cancellationToken);
// Serialize the result (available as Value on the accessor), and put its value back into a new JObject.
return new JObject { { nameof(DialogState), JObject.FromObject(accessor.Value, StateJsonSerializer) } };
}
}
Dan akhirnya, berikut adalah implementasi dari pengaktif properti status kustom.
RefAccessor.cs
public class RefAccessor<T> : IStatePropertyAccessor<T>
where T : class
{
public RefAccessor(T value)
{
Value = value;
}
public T Value { get; private set; }
public string Name => nameof(T);
public Task<T> GetAsync(ITurnContext turnContext, Func<T> defaultValueFactory = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (Value == null)
{
if (defaultValueFactory == null)
{
throw new KeyNotFoundException();
}
Value = defaultValueFactory();
}
return Task.FromResult(Value);
}
#region Not Implemented
public Task DeleteAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
public Task SetAsync(ITurnContext turnContext, T value, CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
#endregion
}
Informasi Tambahan
Sampel peluasan skala tersedia dari repositori sampel Bot Framework di GitHub di C#, Python, dan Java.