ボットは、最初から会話を再開する必要がある場合があります。 たとえば、ユーザーが一定期間後に応答しない場合などです。 この記事では、会話の有効期限を切るための 2 つの方法について説明します。
この記事のサンプル コードは、マルチターン ボットの構造で始まり、そのボットの機能を追加コードによって拡張しています (次のセクションで説明します)。 この拡張コードは、一定の期間が経過した後に会話状態をクリアする方法を示しています。
appsettings.json
まず、appsettings.json に ExpireAfterSeconds
設定を追加します。
{
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"ExpireAfterSeconds": 30
}
Bots\DialogBot.cs
次に、ボット クラスにフィールド ExpireAfterSeconds
、LastAccessedTimeProperty
、DialogStateProperty
を追加し、ボットのコンストラクターで初期化します。 また、IConfiguration
値を取得するコンストラクターに ExpireAfterSeconds
パラメーターを 追加します。
OnMessageActivityAsync
メソッドでダイアログ状態プロパティ アクセサーをインラインで作成する代わりに、初期化時に作成して記録します。 ボットには、ダイアログを実行するだけでなく、ダイアログの状態をクリアするためにも、状態プロパティ アクセサーが必要になります。
protected readonly int ExpireAfterSeconds;
protected readonly IStatePropertyAccessor<DateTime> LastAccessedTimeProperty;
protected readonly IStatePropertyAccessor<DialogState> DialogStateProperty;
// Existing fields omitted...
public DialogBot(IConfiguration configuration, ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger)
{
ConversationState = conversationState;
UserState = userState;
Dialog = dialog;
Logger = logger;
ExpireAfterSeconds = configuration.GetValue<int>("ExpireAfterSeconds");
DialogStateProperty = ConversationState.CreateProperty<DialogState>(nameof(DialogState));
LastAccessedTimeProperty = ConversationState.CreateProperty<DateTime>(nameof(LastAccessedTimeProperty));
}
最後に、会話の経過時間が長すぎる場合にダイアログの状態をクリアするコードをボットの OnTurnAsync
メソッドに追加します。
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
// Retrieve the property value, and compare it to the current time.
var lastAccess = await LastAccessedTimeProperty.GetAsync(turnContext, () => DateTime.UtcNow, cancellationToken).ConfigureAwait(false);
if ((DateTime.UtcNow - lastAccess) >= TimeSpan.FromSeconds(ExpireAfterSeconds))
{
// Notify the user that the conversation is being restarted.
await turnContext.SendActivityAsync("Welcome back! Let's start over from the beginning.").ConfigureAwait(false);
// Clear state.
await ConversationState.ClearStateAsync(turnContext, cancellationToken).ConfigureAwait(false);
}
await base.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false);
// Set LastAccessedTime to the current time.
await LastAccessedTimeProperty.SetAsync(turnContext, DateTime.UtcNow, cancellationToken).ConfigureAwait(false);
// Save any state changes that might have occurred during the turn.
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken).ConfigureAwait(false);
await UserState.SaveChangesAsync(turnContext, false, cancellationToken).ConfigureAwait(false);
}
.env
まず、.env に ExpireAfterSeconds
設定を追加します。
MicrosoftAppId=
MicrosoftAppPassword=
ExpireAfterSeconds=30
bots\dialogBot.js
次に、DialogBot
にフィールドを追加してコンストラクターを更新します。 expireAfterSeconds
と lastAccessedTimeProperty
のローカル フィールドを追加します 。
expireAfterSeconds
をコンストラクターにパラメーターとして追加し、必要な StatePropertyAccessor
を作成します。
constructor(expireAfterSeconds, conversationState, userState, dialog) {
// Existing code omitted...
this.lastAccessedTimeProperty = this.conversationState.createProperty('LastAccessedTime');
this.expireAfterSeconds = expireAfterSeconds;
// Existing code omitted...
}
ボットの run
メソッドにコードを追加します。
async run(context) {
// Retrieve the property value, and compare it to the current time.
const now = new Date();
const lastAccess = new Date(await this.lastAccessedTimeProperty.get(context, now.toISOString()));
if (now !== lastAccess && ((now.getTime() - lastAccess.getTime()) / 1000) >= this.expireAfterSeconds) {
// Notify the user that the conversation is being restarted.
await context.sendActivity("Welcome back! Let's start over from the beginning.");
// Clear state.
await this.conversationState.clear(context);
}
await super.run(context);
// Set LastAccessedTime to the current time.
await this.lastAccessedTimeProperty.set(context, now.toISOString());
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
}
index.js
最後に、index.js
パラメーターを ExpireAfterSeconds
に送信するように DialogBot
を更新します。
const bot = new DialogBot(process.env.ExpireAfterSeconds, conversationState, userState, dialog);
application.properties
まず、application.properties に ExpireAfterSeconds
設定を追加します。
MicrosoftAppId=
MicrosoftAppPassword=
server.port=3978
ExpireAfterSeconds=30
DialogBot.java
次に、ボット クラスにフィールド expireAfterSeconds
、lastAccessedTimeProperty
、dialogStateProperty
を追加し、ボットのコンストラクターで初期化します。 ExpireAfterSeconds
値を取得するために、コンストラクターに Configuration
パラメーターパラメーターを追加します。
onMessageActivity
メソッドでダイアログ状態プロパティ アクセサーをインラインで作成する代わりに、初期化時に作成して記録します。 ボットには、ダイアログを実行するだけでなく、ダイアログの状態をクリアするためにも、状態プロパティ アクセサーが必要になります。
protected final int expireAfterSeconds;
protected final StatePropertyAccessor<LocalTime> lastAccessedTimeProperty;
protected final StatePropertyAccessor<DialogState> dialogStateProperty;
// Existing fields omitted...
public DialogBot(
Configuration configuration,
ConversationState withConversationState,
UserState withUserState,
Dialog withDialog
) {
dialog = withDialog;
conversationState = withConversationState;
userState = withUserState;
expireAfterSeconds = configuration.getProperty("ExpireAfterSeconds") != null ?
Integer.parseInt(configuration.getProperty("ExpireAfterSeconds")) :
30;
lastAccessedTimeProperty = conversationState.createProperty("LastAccessedTimeProperty");
dialogStateProperty = conversationState.createProperty("DialogStateProperty");
}
最後に、会話の経過時間が長すぎる場合にダイアログの状態をクリアするコードをボットの onTurn
メソッドに追加します。
@Override
public CompletableFuture<Void> onTurn(
TurnContext turnContext
) {
LocalTime lastAccess = lastAccessedTimeProperty.get(turnContext).join();
if (lastAccess != null
&& (java.time.temporal.ChronoUnit.SECONDS.between(lastAccess, LocalTime.now()) >= expireAfterSeconds)) {
turnContext.sendActivity("Welcome back! Let's start over from the beginning.").join();
conversationState.clearState(turnContext).join();
}
return lastAccessedTimeProperty.set(turnContext, LocalTime.now()).thenCompose(setResult -> {
return super.onTurn(turnContext)
.thenCompose(result -> conversationState.saveChanges(turnContext))
// Save any state changes that might have occurred during the turn.
.thenCompose(result -> userState.saveChanges(turnContext));
});
}
config.py
まず、config.py に ExpireAfterSeconds
設定を追加します。
PORT = 3978
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
EXPIRE_AFTER_SECONDS = os.environ.get("ExpireAfterSeconds", 30)
bots/dialog_bot.py
次に、DialogBot
にフィールドを追加してコンストラクターを更新します。 expire_after_seconds
と last_accessed_time_property
のローカル フィールドを追加します 。
expire_after_seconds
をコンストラクターにパラメーターとして追加し、必要な StatePropertyAccessor
を作成します。
def __init__(
self,
expire_after_seconds: int,
conversation_state: ConversationState,
user_state: UserState,
dialog: Dialog,
):
# Existing code omitted...
self.expire_after_seconds = expire_after_seconds
self.dialog_state_property = conversation_state.create_property("DialogState")
self.last_accessed_time_property = conversation_state.create_property("LastAccessedTime")
self.conversation_state = conversation_state
self.user_state = user_state
self.dialog = dialog
on_message_activity
を変更し、dialog_state_property
を使うようにします。
async def on_message_activity(self, turn_context: TurnContext):
await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.dialog_state_property,
)
ボットの on_turn
メソッドにコードを追加します。
async def on_turn(self, turn_context: TurnContext):
# Retrieve the property value, and compare it to the current time.
now_seconds = int(time.time())
last_access = int(
await self.last_accessed_time_property.get(turn_context, now_seconds)
)
if now_seconds != last_access and (
now_seconds - last_access >= self.expire_after_seconds
):
# Notify the user that the conversation is being restarted.
await turn_context.send_activity(
"Welcome back! Let's start over from the beginning."
)
# Clear state.
await self.conversation_state.clear_state(turn_context)
await self.conversation_state.save_changes(turn_context, True)
await super().on_turn(turn_context)
# Set LastAccessedTime to the current time.
await self.last_accessed_time_property.set(turn_context, now_seconds)
# Save any state changes that might have occurred during the turn.
await self.conversation_state.save_changes(turn_context)
await self.user_state.save_changes(turn_context)
app.py
最後に、app.py
パラメーターを EXPIRE_AFTER_SECONDS
に送信するように DialogBot
を更新します。
BOT = DialogBot(CONFIG.EXPIRE_AFTER_SECONDS, CONVERSATION_STATE, USER_STATE, DIALOG)
Azure Cosmos DB は、Time to Live (TTL) を使用して、一定の期間が経過したらアイテムをコンテナーから自動的に削除する機能を提供します。 これは、Azure portal 内またはコンテナーの作成時 (言語固有の Cosmos DB SDK を使用) から構成できます。
Bot Framework SDK では、TTL 構成設定は公開されません。 ただし、コンテナーの初期化はオーバーライドでき、Cosmos DB SDK を使用して、Bot Framework ストレージの初期化前に TTL を構成できます。
マルチターン プロンプト サンプルの新しいコピーから開始し、Microsoft.Bot.Builder.Azure
NuGet パッケージをプロジェクトに追加します。
appsettings.json
cosmos DB ストレージ オプションを含むように appsettings.json を更新します。
{
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"CosmosDbTimeToLive": 30,
"CosmosDbEndpoint": "<endpoint-for-your-cosmosdb-instance>",
"CosmosDbAuthKey": "<your-cosmosdb-auth-key>",
"CosmosDbDatabaseId": "<your-database-id>",
"CosmosDbUserStateContainerId": "<no-ttl-container-id>",
"CosmosDbConversationStateContainerId": "<ttl-container-id>"
}
2 つの ContainerId に注目してください。1 つは UserState
用で、もう 1 つは ConversationState
用です。 ConversationState
コンテナーには既定の TTL が設定されますが、UserState
には設定されません。
CosmosDbStorageInitializerHostedService.cs
次に、Time To Live が構成されたコンテナーを作成する CosmosDbStorageInitializerHostedService
クラスを作成します。
// Add required using statements...
public class CosmosDbStorageInitializerHostedService : IHostedService
{
readonly CosmosDbPartitionedStorageOptions _storageOptions;
readonly int _cosmosDbTimeToLive;
public CosmosDbStorageInitializerHostedService(IConfiguration config)
{
_storageOptions = new CosmosDbPartitionedStorageOptions()
{
CosmosDbEndpoint = config["CosmosDbEndpoint"],
AuthKey = config["CosmosDbAuthKey"],
DatabaseId = config["CosmosDbDatabaseId"],
ContainerId = config["CosmosDbConversationStateContainerId"]
};
_cosmosDbTimeToLive = config.GetValue<int>("CosmosDbTimeToLive");
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using (var client = new CosmosClient(
_storageOptions.CosmosDbEndpoint,
_storageOptions.AuthKey,
_storageOptions.CosmosClientOptions ?? new CosmosClientOptions()))
{
// Create the contaier with the provided TTL
var containerResponse = await client
.GetDatabase(_storageOptions.DatabaseId)
.DefineContainer(_storageOptions.ContainerId, "/id")
.WithDefaultTimeToLive(_cosmosDbTimeToLive)
.WithIndexingPolicy().WithAutomaticIndexing(false).Attach()
.CreateIfNotExistsAsync(_storageOptions.ContainerThroughput)
.ConfigureAwait(false);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Startup.cs
最後に、Startup.cs
をストレージ初期化子を使用するように更新し、Cosmos DB を次の状態に更新します。
// Existing code omitted...
// commented out MemoryStorage, since we are using CosmosDbPartitionedStorage instead
// services.AddSingleton<IStorage, MemoryStorage>();
// Add the Initializer as a HostedService (so it's called during the app service startup)
services.AddHostedService<CosmosDbStorageInitializerHostedService>();
// Create the storage options for User state
var userStorageOptions = new CosmosDbPartitionedStorageOptions()
{
CosmosDbEndpoint = Configuration["CosmosDbEndpoint"],
AuthKey = Configuration["CosmosDbAuthKey"],
DatabaseId = Configuration["CosmosDbDatabaseId"],
ContainerId = Configuration["CosmosDbUserStateContainerId"]
};
// Create the User state. (Used in this bot's Dialog implementation.)
services.AddSingleton(new UserState(new CosmosDbPartitionedStorage(userStorageOptions)));
// Create the storage options for Conversation state
var conversationStorageOptions = new CosmosDbPartitionedStorageOptions()
{
CosmosDbEndpoint = Configuration["CosmosDbEndpoint"],
AuthKey = Configuration["CosmosDbAuthKey"],
DatabaseId = Configuration["CosmosDbDatabaseId"],
ContainerId = Configuration["CosmosDbConversationStateContainerId"]
};
// Create the Conversation state. (Used by the Dialog system itself.)
services.AddSingleton(new ConversationState(new CosmosDbPartitionedStorage(conversationStorageOptions)));
// Existing code omitted...
マルチターン プロンプト サンプルの新しいコピーから始めます。
.env
Cosmos DB ストレージ オプションを含むように .env を更新します。
MicrosoftAppId=
MicrosoftAppPassword=
CosmosDbTimeToLive=30
CosmosDbEndpoint=<endpoint-for-your-cosmosdb-instance>
CosmosDbAuthKey=<your-cosmosdb-auth-key>
CosmosDbDatabaseId=<your-database-id>
CosmosDbUserStateContainerId=<no-ttl-container-id>
CosmosDbConversationStateContainerId=<ttl-container-id>
2 つの ContainerId に注目してください。1 つは UserState
用で、もう 1 つは ConversationState
用です。 ConversationState
コンテナーには既定の TTL が設定されますが、UserState
には設定されません。
project.json
次に、botbuilder-azure
npm パッケージを project.json に追加します。
"dependencies": {
"botbuilder": "~4.9.2",
"botbuilder-dialogs": "~4.9.2",
"botbuilder-azure": "~4.9.2",
"dotenv": "^8.2.0",
"path": "^0.12.7",
"restify": "~8.5.1"
},
index.js
必要な require ステートメントを index.js に追加します。
const { CosmosDbPartitionedStorage } = require('botbuilder-azure');
const { CosmosClient } = require('@azure/cosmos');
MemoryStorage
、ConversationState
、UserState
の作成を、次で置き換えます。
// const memoryStorage = new MemoryStorage();
// Storage options for Conversation State
const conversationStorageOptions = {
cosmosDbEndpoint: process.env.CosmosDbEndpoint,
authKey: process.env.CosmosDbAuthKey,
databaseId: process.env.CosmosDbDatabaseId,
containerId: process.env.CosmosDbConversationStateContainerId
};
// Create a cosmosClient, and set defaultTtl (with other properties)
var cosmosClient = new CosmosClient({
endpoint: conversationStorageOptions.cosmosDbEndpoint,
key: conversationStorageOptions.authKey,
...conversationStorageOptions.cosmosClientOptions,
});
// Create the container with the provided TTL.
Promise.resolve(cosmosClient
.database(conversationStorageOptions.databaseId)
.containers.createIfNotExists({
id: conversationStorageOptions.containerId,
partitionKey: {
paths: ['/id']
},
defaultTtl: parseInt(process.env.CosmosDbTimeToLive, 10)
}));
// Storage options for User State
const userStorageOptions = {
cosmosDbEndpoint: process.env.CosmosDbEndpoint,
authKey: process.env.CosmosDbAuthKey,
databaseId: process.env.CosmosDbDatabaseId,
containerId: process.env.CosmosDbUserStateContainerId
};
// Create state instances.
const conversationState = new ConversationState(new CosmosDbPartitionedStorage(conversationStorageOptions));
const userState = new UserState(new CosmosDbPartitionedStorage(userStorageOptions));
最後に、ボットを開始する前に npm インストールを実行します。
npm install
マルチターン プロンプト サンプルの新しいコピーから始めて、pom.xml ファイルに次の依存関係を追加します。
<dependency>
<groupId>com.microsoft.bot</groupId>
<artifactId>bot-azure</artifactId>
<version>4.13.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-cosmos</artifactId>
</dependency>
application.properties
application.properties を更新して、Cosmos DB ストレージ オプションを含めます。
MicrosoftAppId=
MicrosoftAppPassword=
server.port=3978
CosmosDbTimeToLive = 30
CosmosDbEndpoint = <endpoint-for-your-cosmosdb-instance>
CosmosDbAuthKey = <your-cosmosdb-auth-key>
CosmosDbDatabaseId = <your-database-id>
CosmosDbUserStateContainerId = <no-ttl-container-id>
CosmosDbConversationStateContainerId = <ttl-container-id>
2 つの ContainerId に注目してください。1 つは UserState
用で、もう 1 つは ConversationState
用です。 ConversationState
コンテナーには既定の TTL が設定されますが、UserState
には設定されません。
CosmosDbStorageInitializer.java
次に、Time To Live が構成されたコンテナーを作成する CosmosDbStorageInitializer
クラスを作成します。
package com.microsoft.bot.sample.multiturnprompt;
import com.azure.cosmos.CosmosAsyncClient;
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.cosmos.models.CosmosContainerProperties;
import com.microsoft.bot.azure.CosmosDbPartitionedStorageOptions;
import com.microsoft.bot.integration.Configuration;
public class CosmosDbStorageInitializer {
final CosmosDbPartitionedStorageOptions storageOptions;
final int cosmosDbTimeToLive;
public CosmosDbStorageInitializer(Configuration configuration) {
storageOptions = new CosmosDbPartitionedStorageOptions();
storageOptions.setCosmosDbEndpoint(configuration.getProperty("CosmosDbEndpoint"));
storageOptions.setAuthKey(configuration.getProperty("CosmosDbAuthKey"));
storageOptions.setDatabaseId(configuration.getProperty("CosmosDbDatabaseId"));
storageOptions.setContainerId(configuration.getProperty("CosmosDbConversationStateContainerId"));
cosmosDbTimeToLive = configuration.getProperty("CosmosDbTimeToLive") != null
? Integer.parseInt(configuration.getProperty("CosmosDbTimeToLive"))
: 30;
}
public void initialize() {
CosmosAsyncClient client = new CosmosClientBuilder().endpoint(storageOptions.getCosmosDbEndpoint())
.key(storageOptions.getAuthKey()).buildAsyncClient();
client.createDatabaseIfNotExists(storageOptions.getDatabaseId()).block();
CosmosContainerProperties cosmosContainerProperties = new CosmosContainerProperties(
storageOptions.getContainerId(), "/id");
cosmosContainerProperties.setDefaultTimeToLiveInSeconds(cosmosDbTimeToLive);
client.getDatabase(storageOptions.getDatabaseId()).createContainerIfNotExists(cosmosContainerProperties)
.block();
client.close();
}
}
Application.java
最後に、Application.java
をストレージ初期化子を使用するように更新し、Cosmos DB を次の状態に更新します。
// Existing code omitted...
@Override
public ConversationState getConversationState(Storage storage) {
Configuration configuration = getConfiguration();
CosmosDbStorageInitializer initializer = new CosmosDbStorageInitializer(configuration);
initializer.initialize();
CosmosDbPartitionedStorageOptions storageOptions = new CosmosDbPartitionedStorageOptions();
storageOptions.setCosmosDbEndpoint(configuration.getProperty("CosmosDbEndpoint"));
storageOptions.setAuthKey(configuration.getProperty("CosmosDbAuthKey"));
storageOptions.setDatabaseId(configuration.getProperty("CosmosDbDatabaseId"));
storageOptions.setContainerId(configuration.getProperty("CosmosDbConversationStateContainerId"));
return new ConversationState(new CosmosDbPartitionedStorage(storageOptions));
}
/**
* Returns a UserState object. Default scope of Singleton.
*
* @param storage The Storage object to use.
* @return A UserState object.
*/
@Override
public UserState getUserState(Storage storage) {
Configuration configuration = getConfiguration();
CosmosDbPartitionedStorageOptions storageOptions = new CosmosDbPartitionedStorageOptions();
storageOptions.setCosmosDbEndpoint(configuration.getProperty("CosmosDbEndpoint"));
storageOptions.setAuthKey(configuration.getProperty("CosmosDbAuthKey"));
storageOptions.setDatabaseId(configuration.getProperty("CosmosDbDatabaseId"));
storageOptions.setContainerId(configuration.getProperty("CosmosDbUserStateContainerId"));
return new UserState(new CosmosDbPartitionedStorage(storageOptions));
}
マルチターン プロンプト サンプルの新しいコピーから始めます。
config.py
Cosmos DB ストレージ オプションを含むように config.py
を更新します。
PORT = 3978
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
COSMOSDB_TTL = os.environ.get("CosmosDbTimeToLive", 30)
COSMOSDB_ENDPOINT = os.environ.get("CosmosDbEndpoint", "<endpoint-for-your-cosmosdb-instance>")
COSMOSDB_AUTH_KEY = os.environ.get("CosmosDbAuthKey", "<your-cosmosdb-auth-key>")
COSMOSDB_DATABASE_ID = os.environ.get("CosmosDbDatabaseId", "<your-database-id>")
COSMOSDB_USER_STATE_CONTAINER_ID = os.environ.get("CosmosDbUserStateContainerId", "<no-ttl-container-id>")
COSMOSDB_CONVERSATION_STATE_CONTAINER_ID = os.environ.get("CosmosDbConversationStateContainerId", "<ttl-container-id>")
2 つの ContainerId に注目してください。1 つは UserState
用で、もう 1 つは ConversationState
用です。 ConversationState
コンテナーには既定の TTL が設定されますが、UserState
には設定されません。
requirements.txt
次に、requirements.txt に botbuilder-azure
パッケージを追加します。
botbuilder-integration-aiohttp>=4.10.0
botbuilder-dialogs>=4.10.0
botbuilder-ai>=4.10.0
botbuilder-azure>=4.10.0
次に、pip インストールを実行します。
pip install -r requirements.txt
app.py
client = cosmos_client.CosmosClient(
CONFIG.COSMOSDB_ENDPOINT, {"masterKey": CONFIG.COSMOSDB_AUTH_KEY},
)
containers = list(client.QueryContainers("dbs/" + CONFIG.COSMOSDB_DATABASE_ID, {
"query": "SELECT * FROM r WHERE r.id=@id",
"parameters": [
{"name": "@id", "value": CONFIG.COSMOSDB_CONVERSATION_STATE_CONTAINER_ID}
],
}))
if len(containers) < 1:
new_container = client.CreateContainer(
"dbs/" + CONFIG.COSMOSDB_DATABASE_ID,
{
"defaultTtl": CONFIG.COSMOSDB_TTL,
"id": CONFIG.COSMOSDB_CONVERSATION_STATE_CONTAINER_ID,
"partitionKey": {"paths": ["/id"], "kind": "Hash",},
},
)