Botunuza özel depolama uygulama
ŞUNLAR IÇIN GEÇERLIDIR: SDK v4
Botun etkileşimleri üç alana ayrılır: Azure AI Bot Hizmeti ile etkinliklerin değişimi, bot ve iletişim durumunun bellek deposuyla yüklenip kaydedilmesi ve arka uç hizmetleriyle tümleştirme.
Bu makalede, Azure AI Bot Hizmeti ile bot'un bellek durumu ve depolama alanı arasındaki semantiğin nasıl genişletilebileceğiniz açıklanır.
Not
Bot Framework JavaScript, C# ve Python SDK'ları desteklenmeye devam edecektir, ancak Java SDK'sı son uzun vadeli destek Kasım 2023'te sona erecek şekilde kullanımdan kaldırılacaktır.
Java SDK ile oluşturulan mevcut botlar çalışmaya devam edecektir.
Yeni bot derlemesi için Microsoft Copilot Studio'yu kullanmayı göz önünde bulundurun ve doğru copilot çözümünü seçme hakkında bilgi edinin.
Daha fazla bilgi için bkz . Bot oluşturmanın geleceği.
Önkoşullar
- Microsoft Bot Framework'ün Temel bilgileri, etkinlik işleyicisi kullanan Olay temelli konuşmalar ve Yönetim durumu.
- C#, Python veya Java'da ölçeği genişletme örneğinin bir kopyası.
Bu makale, örneğin C# sürümüne odaklanır.
Background
Bot Framework SDK'sı, bot durumunun ve bellek depolamanın varsayılan bir uygulamasını içerir. Bu uygulama, örneklerin çoğunda gösterildiği gibi parçaların birkaç başlatma kodu satırıyla birlikte kullanıldığı uygulamaların gereksinimlerine uyar.
SDK bir çerçevedir ve sabit davranışa sahip bir uygulama değildir. Başka bir deyişle, çerçevedeki mekanizmaların birçoğunun uygulanması varsayılan bir uygulamadır ve tek olası uygulama değildir. Çerçeve, Azure AI Bot Hizmeti ile etkinliklerin değişimi ile herhangi bir bot durumunun yüklenmesi ve kaydedilmesi arasındaki ilişkiyi belirlemez.
Bu makalede, uygulamanız için tam olarak işe yaramadığında varsayılan durum ve depolama uygulamasının semantiğini değiştirmenin bir yolu açıklanmaktadır. Ölçeği genişletme örneği, durum ve depolamanın varsayılanlardan farklı semantiği olan alternatif bir uygulamasını sağlar. Bu alternatif çözüm, çerçevede eşit derecede iyi durur. Senaryonuza bağlı olarak, bu alternatif çözüm geliştirmekte olduğunuz uygulama için daha uygun olabilir.
Varsayılan bağdaştırıcının ve depolama sağlayıcısının davranışı
Varsayılan uygulamayla, bir etkinliği alırken bot konuşmaya karşılık gelen durumu yükler. Ardından bu durum ve gelen etkinlikle iletişim mantığı çalıştırır. İletişim kutusunu çalıştırma işleminde, bir veya daha fazla giden etkinlik oluşturulur ve hemen gönderilir. İletişim kutusunun işlenmesi tamamlandığında, bot güncelleştirilmiş durumu kaydeder ve eski durumun üzerine yazar.
Ancak, bu davranışla ilgili birkaç sorun olabilir.
Kaydetme işlemi bir nedenden dolayı başarısız olursa durum, kullanıcının kanalda gördükleriyle örtülü olarak eşitlenmemiştir. Kullanıcı bottan gelen yanıtları gördü ve durumun ilerlediğini ama ilerlemediğini düşünüyor. Bu hata, durum güncelleştirmesi başarılı olsa da kullanıcının yanıt iletilerini almamış olması durumundan daha kötü olabilir.
Bu tür durum hataları konuşma tasarımınızı etkileyebilir. Örneğin, iletişim kutusu kullanıcıyla fazladan, aksi halde yedekli onay değişimi gerektirebilir.
Uygulamanın ölçeği birden çok düğümde genişletilirse, durum yanlışlıkla üzerine yazılabilir. İletişim kutusu büyük olasılıkla onay iletilerini taşıyan kanala etkinlik gönderdiğinden bu hata kafa karıştırıcı olabilir.
Bir pizza sipariş botu düşünün, burada bot kullanıcıdan topping seçenekleri ister ve kullanıcı iki hızlı mesaj gönderir: biri mantar eklemek ve diğeri peynir eklemek için. Ölçeği genişletilmiş bir senaryoda, botun birden çok örneği etkin olabilir ve iki kullanıcı iletisi ayrı makinelerde iki ayrı örnek tarafından işlenebilir. Böyle bir çakışma, bir makinenin başka bir makine tarafından yazılan durumun üzerine yazabileceği bir yarış durumu olarak adlandırılır. Ancak, yanıtlar zaten gönderildiğinden, kullanıcı hem mantar hem de peynirin siparişine eklendiğine dair onay aldı. Ne yazık ki, pizza geldiğinde, sadece mantar veya peynir içerir, ancak ikisini birden içermez.
İyimser kilitleme
Ölçeği genişletme örneği, durumu biraz kilitlemeye neden olur. Örnek, her örneğin çalışan tek örnekmiş gibi çalışmasını ve ardından eşzamanlılık ihlallerini denetlemesini sağlayan iyimser kilitleme uygular. Bu kilitleme karmaşık gelebilir, ancak bilinen çözümler mevcuttur ve Bot Framework'te bulut depolama teknolojilerini ve doğru uzantı noktalarını kullanabilirsiniz.
Örnek, varlık etiketi üst bilgisini (ETag) temel alan standart bir HTTP mekanizması kullanır. Bu mekanizmayı anlamak, aşağıdaki kodu anlamak için çok önemlidir. Aşağıdaki diyagramda sıra gösterilmektedir.
Diyagramda bazı kaynaklarda güncelleştirme gerçekleştiren iki istemci vardır.
İstemci bir GET isteği gönderirse ve sunucudan bir kaynak döndürülürse, sunucu bir ETag üst bilgisi içerir.
ETag üst bilgisi, kaynağın durumunu temsil eden opak bir değerdir. Bir kaynak değiştirilirse, sunucu kaynak için ETag'ini güncelleştirir.
İstemci bir durum değişikliğini kalıcı hale getirmek istediğinde, ön koşul üst bilgisinde ETag değeriyle sunucuya bir
If-Match
POST isteği gönderir.İsteğin ETag değeri sunucununkiyle eşleşmiyorsa önkoşul denetimi (
412
Önkoşul Başarısız) yanıtıyla başarısız olur.Bu hata, sunucudaki geçerli değerin artık istemcinin üzerinde çalıştırıldığı özgün değerle eşleşmediğini gösterir.
İstemci önkoşul başarısız yanıtı alırsa, istemci genellikle kaynak için yeni bir değer alır, istediği güncelleştirmeyi uygular ve kaynak güncelleştirmesini yeniden göndermeyi dener.
Kaynağı başka bir istemci güncelleştirmezse bu ikinci POST isteği başarılı olur. Aksi takdirde istemci yeniden deneyebilir.
Bu işleme iyimser denir çünkü bir kaynağı olduğunda istemci işlemeye devam eder; diğer istemciler herhangi bir kısıtlama olmadan erişebildiğinden kaynağın kendisi kilitlenmez. İstemciler arasında kaynağın durumunun ne olması gerektiğiyle ilgili herhangi bir çekişme, işleme yapılana kadar belirlenemez. Dağıtılmış bir sistemde bu strateji genellikle karşıt kötümser yaklaşımdan daha optimaldir.
Açıklandığı gibi iyimser kilitleme mekanizması, program mantığınızın güvenli bir şekilde yeniden denenebileceğini varsayar. İdeal durum, bu hizmet isteklerinin bir kez etkili olmasıdır. Bilgisayar biliminde bir kez etkili bir işlem, aynı giriş parametreleriyle birden çok kez çağrılırsa fazladan etkisi olmayan bir işlemdir. GET, PUT ve DELETE isteklerini uygulayan saf HTTP REST hizmetleri genellikle bir kez etkili olur. Bir hizmet isteği ek etkiler oluşturmazsa, istekler yeniden deneme stratejisinin bir parçası olarak güvenli bir şekilde yeniden yürütülebilir.
Ölçeği genişletme örneği ve bu makalenin geri kalanında botunuzun kullandığı arka uç hizmetlerinin tümünün etkili HTTP REST hizmetleri olduğu varsayılır.
Giden etkinlikleri arabelleğe alma
Etkinlik göndermek bir kez etkili bir işlem değildir. Etkinlik genellikle bilgileri kullanıcıya aktaran bir iletidir ve aynı iletiyi iki veya daha fazla kez yinelemek kafa karıştırıcı veya yanıltıcı olabilir.
İyimser kilitleme, bot mantığınızın birden çok kez yeniden çalıştırılması gerekebileceğini gösterir. Belirli bir etkinliğin birden çok kez gönderilmesini önlemek için, kullanıcıya etkinlik göndermeden önce durum güncelleştirme işleminin başarılı olmasını bekleyin. Bot mantığınız aşağıdaki diyagrama benzer olmalıdır.
İletişim kutusu yürütmenize bir yeniden deneme döngüsü derledikten sonra, kaydetme işleminde önkoşul hatası olduğunda aşağıdaki davranışa sahip olursunuz.
Bu mekanizma uygulandığında, önceki örnekteki pizza botu hiçbir zaman siparişe eklenen pizza toppingine ilişkin hatalı pozitif bir onay göndermemelidir. Bot birden çok makineye dağıtıldığında bile iyimser kilitleme düzeni durum güncelleştirmelerini etkili bir şekilde seri hale getirmektedir. Pizza botunda, bir öğe ekleme onayı artık tam durumu bile doğru yansıtabilir. Örneğin, kullanıcı hızlı bir şekilde "peynir" ve sonra "mantar" yazarken bu iletiler botun iki farklı örneği tarafından işleniyorsa, tamamlanması gereken son örnek yanıtın bir parçası olarak "peynirli ve mantarlı bir pizza" içerebilir.
Bu yeni özel depolama çözümü, SDK'daki varsayılan uygulamanın gerçekleştirmediği üç şey yapar:
- Çekişme algılamak için ETag'leri kullanır.
- ETag hatası algılandığında işlemi yeniden denenir.
- Durumu başarıyla kaydedilene kadar giden etkinlikleri göndermeyi bekler.
Bu makalenin geri kalanında bu üç bölümün uygulanması açıklanmaktadır.
ETag desteği uygulama
İlk olarak, yeni mağazamız için ETag desteği içeren bir arabirim tanımlayın. Arabirim, ASP.NET bağımlılık ekleme mekanizmalarının kullanılmasına yardımcı olur. Arabiriminden başlayarak, birim testleri ve üretim için ayrı sürümler uygulamanıza olanak tanır. Örneğin, birim testi sürümü bellekte çalışabilir ve ağ bağlantısı gerektirmez.
Arabirim, yük ve kaydetme yöntemlerinden oluşur. Her iki yöntem de depolama alanından yüklenecek veya depolama alanına kaydedilecek durumu belirlemek için bir anahtar parametresi kullanır.
- Yük , durum değerini ve ilişkili ETag'i döndürür.
- Kaydet,durum değeri ve ilişkili ETag parametrelerine sahip olur ve işlemin başarılı olup olmadığını gösteren bir Boole değeri döndürür. Dönüş değeri genel bir hata göstergesi olarak değil, önkoşul hatasının belirli bir göstergesi olarak hizmet verir. Dönüş kodunun denetlenmesi, yeniden deneme döngüsünün mantığının bir parçası olacaktır.
Depolama uygulamasını yaygın olarak uygulanabilir hale getirmek için üzerine serileştirme gereksinimleri koymaktan kaçının.
Ancak, birçok modern depolama hizmeti içerik türü olarak JSON'ı destekler.
C# dilinde, JSON nesnesini temsil etmek için türünü kullanabilirsiniz JObject
.
JavaScript veya TypeScript'te JSON normal bir yerel nesnedir.
Özel arabirimin tanımı aşağıdadır.
IStore.cs
public interface IStore
{
Task<(JObject content, string etag)> LoadAsync(string key);
Task<bool> SaveAsync(string key, JObject content, string etag);
}
İşte Azure Blob Depolama için bir uygulama.
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 Depolama işin çoğunu yapıyor. Her yöntem, çağıran kodun beklentilerini karşılamak için belirli bir özel durumu denetler.
LoadAsync
yöntemi, bulunamadı durum koduna sahip bir depolama özel durumuna yanıt olarak null değer döndürür.SaveAsync
yöntemi, önkoşul başarısız kod içeren bir depolama özel durumuyla yanıt olarak döndürürfalse
.
Yeniden deneme döngüsü uygulama
Yeniden deneme döngüsünün tasarımı, sıralı diyagramlarda gösterilen davranışı uygular.
Etkinlik aldıktan sonra konuşma durumu için bir anahtar oluşturun.
Bir etkinlik ile konuşma durumu arasındaki ilişki, özel depolama için varsayılan uygulamayla aynıdır. Bu nedenle, anahtarı varsayılan durum uygulamasının yaptığı gibi oluşturabilirsiniz.
Konuşma durumunu yüklemeyi deneme.
Bot iletişim kutularını çalıştırın ve gönderilecek giden etkinlikleri yakalayın.
Konuşma durumunu kaydetmeyi deneme.
Başarılı olduğunda giden etkinlikleri gönderin ve çıkın.
Hata durumunda, konuşma durumunu yüklemek için bu işlemi adımdan tekrarlayın.
Konuşma durumunun yeni yükü yeni ve geçerli bir ETag ile konuşma durumunu alır. İletişim kutusu yeniden çalıştırılır ve kaydetme durumu adımının başarılı olma şansı vardır.
İleti etkinliği işleyicisi için bir uygulama aşağıdadır.
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;
}
}
}
Not
Örnek, iletişim kutusu yürütmesini işlev çağrısı olarak uygular. Bir arabirim tanımlamak ve bağımlılık ekleme özelliğini kullanmak daha karmaşık bir yaklaşım olabilir. Ancak bu örnekte statik işlev, bu iyimser kilitleme yaklaşımının işlevsel niteliğini vurgular. Genel olarak, kodunuzun önemli bölümlerini işlevsel bir şekilde uyguladığınızda, ağlarda başarılı bir şekilde çalışma şansını artırırsınız.
Giden etkinlik arabelleği uygulama
Sonraki gereksinim, özel bir bağdaştırıcı uygulaması gerektiren başarılı bir kaydetme işlemi gerçekleşene kadar giden etkinlikleri arabelleğe almaktır.
Özel SendActivitiesAsync
yöntem, etkinlikleri kullanıma göndermemeli, ancak etkinlikleri bir listeye eklemelidir.
İletişim kutunuzda değişiklik yapılması gerekmez.
- Bu özel senaryoda, güncelleştirme etkinliği ve silme etkinliği işlemleri desteklenmez ve ilişkili yöntemler uygulanmamış özel durumlar oluşturur.
- Gönderme etkinlikleri işlemindeki dönüş değeri, bazı kanallar tarafından botların daha önce gönderilmiş bir iletiyi değiştirmesine veya silmesine izin vermek için kullanılır. Örneğin, kanalda görüntülenen kartlardaki düğmeleri devre dışı bırakmak için. Bu ileti alışverişleri, özellikle durum gerekli olduğunda ve bu makalenin kapsamı dışında olduğunda karmaşık hale gelebilir.
- İletişim kutunuz bu özel bağdaştırıcıyı oluşturup kullandığı için etkinlikleri arabelleğe alabilir.
- Botunuzun dönüş işleyicisi, etkinlikleri kullanıcıya göndermek için daha fazla standart
AdapterWithErrorHandler
kullanır.
Burada özel bağdaştırıcının bir uygulaması yer alır.
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
}
Botta özel depolama alanınızı kullanma
Son adım, bu özel sınıfları ve yöntemleri mevcut çerçeve sınıfları ve yöntemleriyle kullanmaktır.
- Ana yeniden deneme döngüsü botunuzun
ActivityHandler.OnMessageActivityAsync
yönteminin bir parçası olur ve bağımlılık ekleme yoluyla özel depolama alanınızı içerir. - İletişim kutusu barındırma kodu, statik
RunAsync
bir yöntemi kullanıma sunan sınıfaDialogHost
eklenir. İletişim kutusu konağı:- Gelen etkinliği ve eski durumu alır ve ardından sonuçta elde edilen etkinlikleri ve yeni durumu döndürür.
- Özel bağdaştırıcıyı oluşturur ve aksi takdirde iletişim kutusunu SDK ile aynı şekilde çalıştırır.
- İletişim durumunu iletişim kutusu sistemine geçiren bir dolgu olan özel durum özellik erişimcisi oluşturur. Erişimci, iletişim kutusu sistemine bir erişimci tutamacını geçirmek için başvuru semantiğini kullanır.
İpucu
JSON serileştirmesi, farklı uygulamaların farklı şekilde seri hale getirebilmesi için eklenebilir depolama katmanının dışında tutmak için barındırma koduna satır içi olarak eklenir.
İletişim kutusu ana bilgisayarının bir uygulaması aşağıdadır.
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) } };
}
}
Ve son olarak, özel durum özellik erişimcisinin bir uygulaması aşağıdadır.
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
}
Ek bilgi
Ölçeği genişletme örneği C#, Python ve Java'daki GitHub'daki Bot Framework örnekleri deposundan edinilebilir.