protected override async Task OnPlayFinishedEventAsync(Event ev, Session session, CancellationToken cancellationToken) { // 終わっていなければ無音再生リクエストを繰り返す var status = await DurableClient.GetStatusAsync(session.User.UserId); if (status?.RuntimeStatus == OrchestrationRuntimeStatus.ContinuedAsNew || status?.RuntimeStatus == OrchestrationRuntimeStatus.Pending || status?.RuntimeStatus == OrchestrationRuntimeStatus.Running) { KeepClovaWaiting(); } else if (status.RuntimeStatus == OrchestrationRuntimeStatus.Completed) { // 完了していた場合(=LINEからの外部イベント処理が実行された場合) // 再度セッション継続 KeepClovaWaiting(); // 入力内容をそのまま話させる Response.AddText(status.Output.ToObject <string>()); // オーケストレーターを再実行 await DurableClient.StartNewAsync(nameof(ClovaFunctions.WaitForLineInput), session.User.UserId); } else if (status.RuntimeStatus == OrchestrationRuntimeStatus.Failed) { // 失敗していたら結果をしゃべって終了 Response.AddText("失敗しました。"); } else if (status.RuntimeStatus == OrchestrationRuntimeStatus.Terminated) { // Botからのスキル停止指示 Response.AddText("腹話術を終了します。"); } }
protected override async Task OnIntentRequestAsync(Intent intent, Session session, CancellationToken cancellationToken) { switch (intent.Name) { case "Clova.GuideIntent": Response .AddText("LINEに入力をした内容をしゃべります。準備はいいですか?") .KeepListening(); break; case "Clova.YesIntent": case "ReadyIntent": // 友だち追加チェック try { await LineMessagingClient.GetUserProfileAsync(session.User.UserId); } catch { Response.AddText("連携するLINEアカウントが友だち追加されていません。" + "Clovaアプリの本スキルのページから、連携するLINEアカウントを友だち追加してください。"); break; } await DurableClient.StartNewAsync(nameof(ClovaFunctions.WaitForLineInput), session.User.UserId); Response.AddText("LINEに入力をした内容をしゃべります。好きな内容をLINEから送ってね。"); // 無音無限ループに入る KeepClovaWaiting(); break; case "Clova.PauseIntent": // 無限ループ中の一時停止指示に対し、スキル終了をする await DurableClient.TerminateAsync(session.User.UserId, "intent"); Response.AddText("腹話術を終了します。"); break; case "Clova.NoIntent": case "Clova.CancelIntent": case "NotReadyIntent": // オーケストレーターが起動していないなら終了 var status = await DurableClient.GetStatusAsync(session.User.UserId); if (status?.RuntimeStatus == OrchestrationRuntimeStatus.ContinuedAsNew || status?.RuntimeStatus == OrchestrationRuntimeStatus.Pending || status?.RuntimeStatus == OrchestrationRuntimeStatus.Running) { Response.AddText("腹話術を終了します。"); } else { KeepClovaWaiting(); } break; } }
private async Task StartBatchSubSkills(string replyToken, Context context) { // 子コンテキスト var childQuery = context.UserQuery.SubSkills[0].UserQuery; childQuery.Timestamp = DateTime.UtcNow.Ticks; var childEntityId = new EntityId(nameof(ContextEntity), $"{childQuery.IntentName}-{context.UserId}"); var childContext = new Context { UserId = context.UserId, SkillName = childQuery.IntentName, UserQuery = childQuery }; await DurableClient.SignalEntityAsync <IContextEntity>(childEntityId, proxy => proxy.SetContext(childContext)); // 先頭スキルの実行 var childMessages = await Skills.First(s => s.IntentName == context.UserQuery.SubSkills[0].UserQuery.IntentName).GetReplyMessagesAsync(childContext); await LineMessagingClient.ReplyMessageAsync(replyToken, childMessages); }
/// <summary> /// Gets a <see cref="IDurableClient"/> using configuration from a <see cref="DurableClientOptions"/> instance. /// </summary> /// <param name="durableClientOptions">options containing the client configuration parameters.</param> /// <returns>Returns a <see cref="IDurableClient"/> instance. The returned instance may be a cached instance.</returns> public IDurableClient CreateClient(DurableClientOptions durableClientOptions) { if (durableClientOptions == null) { throw new ArgumentException("Please configure 'DurableClientOptions'"); } if (string.IsNullOrWhiteSpace(durableClientOptions.TaskHub)) { throw new ArgumentException("Please provide value for 'TaskHub'"); } DurableClientAttribute attribute = new DurableClientAttribute(durableClientOptions); DurableClient client = this.cachedClients.GetOrAdd( attribute, attr => { DurabilityProvider innerClient = this.durabilityProviderFactory.GetDurabilityProvider(attribute); return(new DurableClient(innerClient, null, attribute, this.MessageDataConverter, this.TraceHelper, this.durableTaskOptions)); }); return(client); }
protected override async Task OnPlayPausedEventAsync(Event ev, Session session, CancellationToken cancellationToken) { await DurableClient.TerminateAsync(session.User.UserId, "PlayPaused"); Response.AddText("腹話術を終了します。"); }
protected override async Task OnMessageAsync(MessageEvent ev) { await OnBeforeMessageAsync(ev); if (ShouldEnd) { return; } UserQuery query = null; switch (ev.Message.Type) { case EventMessageType.Text: var text = ((TextEventMessage)ev.Message).Text; // テキスト・インテント待ちを確認 var waiterId = new EntityId(nameof(ContextEntity), $"{WaiterEntityPrefix}_{ev.Source.UserId}"); var waiterState = await DurableClient.ReadEntityStateAsync <ContextEntity>(waiterId); var isWaitingForIntents = false; var isWaitingForText = false; Context waitingContext = null; if (waiterState.EntityExists && waiterState.EntityState.Context != null) { waitingContext = waiterState.EntityState.Context; if (waitingContext.ExpectedIntentNames != null) { isWaitingForIntents = true; } else { isWaitingForText = true; } } if (isWaitingForIntents || !isWaitingForText) { // detect intent query = await NluClient.DetectIntent(text, ev.Source.UserId); Logger.LogInformation(query.IntentName); } if (isWaitingForIntents) { Logger.LogInformation(waitingContext.SkillName); } // 実行スキルを選択 var targetSkill = (isWaitingForIntents && waitingContext.ExpectedIntentNames.Contains(query.IntentName)) || isWaitingForText ? waitingContext.SkillName // 待機スキル : query.IntentName; // 解析スキル if (isWaitingForText) { query = new UserQuery { IntentName = targetSkill, }; } // テキストをセット query.Text = text; var skill = Skills.FirstOrDefault(s => s.IntentName == targetSkill); if (skill != null) { // コンテキスト確認を行う var entityId = new EntityId(nameof(ContextEntity), $"{targetSkill}-{ev.Source.UserId}"); var state = await DurableClient.ReadEntityStateAsync <ContextEntity>(entityId); Context context = null; if (state.EntityExists && state.EntityState.Context != null) { context = state.EntityState.Context; var savedTimestamp = context.UserQuery.Timestamp; // Execute incomplete sub-skills when restarting from a pause between sub-skills of a batch execution skill var resume = context.UserQuery.SubSkills?.Where((s, idx) => idx != 0 && !s.IsFinished).FirstOrDefault(); if (resume != null) { context = new Context { UserId = ev.Source.UserId, SkillName = resume.UserQuery.IntentName, UserQuery = resume.UserQuery }; } else { context.UserQuery = query; } } else { context = new Context { UserId = ev.Source.UserId, IsNew = true }; query.Timestamp = DateTime.UtcNow.Ticks; context.UserQuery = query; } context.SkillName = targetSkill; var messages = await skill.GetReplyMessagesAsync(context); // Save context await DurableClient.SignalEntityAsync <IContextEntity>(entityId, proxy => proxy.SetContext(context)); // Waiter if (context.IsWaiting) { await DurableClient.SignalEntityAsync <IContextEntity>(waiterId, proxy => proxy.SetContext(context)); } if (messages != null) { if (!skill.IsContinued) { var quickReply = await FinishAndGetResumeQuickReplyAsync(context); if (messages.Last().QuickReply != null && messages.Last().QuickReply.Items.Count > 0) { messages.Last().QuickReply = MergeQuickReply(messages.Last().QuickReply, quickReply); } else { messages.Last().QuickReply = quickReply; } } await LineMessagingClient.ReplyMessageAsync(ev.ReplyToken, messages); } // Batch execution skill else if (context.UserQuery.SubSkills != null) { await StartBatchSubSkills(ev.ReplyToken, context); } } else if (query.IsFallback) { Logger.LogError("Fallback"); // TODO connect to knowledge base await LineMessagingClient.ReplyMessageAsync(ev.ReplyToken, query.FulfillmentText ?? "すみません、よくわかりませんでした。"); } else { Logger.LogError("Intentに対応するスキル定義がありません。"); await LineMessagingClient.ReplyMessageAsync(ev.ReplyToken, query.FulfillmentText ?? "すみません、よくわかりませんでした。"); } break; case EventMessageType.Sticker: case EventMessageType.Image: case EventMessageType.Video: case EventMessageType.Location: case EventMessageType.Audio: case EventMessageType.File: default: break; } await OnAfterMessageAsync(ev, query); }
/// <summary> /// 会話を終了し、保存されているコンテキスト情報は削除されます。 /// 中断中のスキルがあればひとつ前の中断スキルに戻るためのクイックリプライを返します。 /// </summary> /// <returns></returns> private async Task <QuickReply> FinishAndGetResumeQuickReplyAsync(Context context) { var entityId = new EntityId(nameof(ContextEntity), $"{context.SkillName}-{context.UserId}"); await DurableClient.SignalEntityAsync <IContextEntity>(entityId, proxy => proxy.SetContext(null)); QuickReply ret = null; // 中断スキルへのジャンプおよびバッチ実行の後続スキル呼び出し // ユーザーIDで継続中のコンテキストを検索 var result = await DurableClient.ListEntitiesAsync( new EntityQuery { EntityName = nameof(ContextEntity) }, new System.Threading.CancellationToken()); var entityQuery = result.Entities .Where(e => e.EntityId.EntityKey.EndsWith(context.UserId) && !e.EntityId.EntityKey.StartsWith(context.SkillName) && !e.EntityId.EntityKey.StartsWith(WaiterEntityPrefix)) .OrderByDescending(e => e.LastOperationTime); Context targetContext = null; foreach (var e in entityQuery) { var state = await DurableClient.ReadEntityStateAsync <ContextEntity>(e.EntityId); var target = state.EntityState; if (state.EntityState.Context != null) { targetContext = state.EntityState.Context; break; } } if (targetContext != null) { // 保存されているコンテキストがバッチ実行中のものかを確認 var batchSkills = targetContext.UserQuery.SubSkills; // バッチ実行の場合 if (batchSkills != null && batchSkills.Length != 0 && // 今回完了したスキルがバッチ内のものかを調べる batchSkills.Where((s, idx) => s.UserQuery.IntentName == context.UserQuery.IntentName && // 今回完了したスキルがバッチ実行定義に存在 !s.IsFinished && // 完了していない (idx == 0 || batchSkills[idx - 1].IsFinished)).Any()) // 先頭スキル or 直前が完了 { var skill = batchSkills.First(s => s.UserQuery.IntentName == context.UserQuery.IntentName); var index = Array.IndexOf(batchSkills, skill); var targetEntityId = new EntityId(nameof(ContextEntity), $"{targetContext.UserQuery.IntentName}-{context.UserId}"); if (batchSkills.Length == index + 1) { // 最終スキルなので、バッチ実行コンテキストを削除 await DurableClient.SignalEntityAsync <IContextEntity>(targetEntityId, proxy => proxy.SetContext(null)); } else { // 終了フラグを更新 batchSkills[index].IsFinished = true; await DurableClient.SignalEntityAsync <IContextEntity>(targetEntityId, proxy => proxy.SetContext(targetContext)); // 後続へジャンプ var next = batchSkills[index + 1]; next.UserQuery.IsSubSkill = true; ret = new QuickReply(new List <QuickReplyButtonObject> { new QuickReplyButtonObject(new PostbackTemplateAction( $"続けて{next.DisplayName}に進む", JsonConvert.SerializeObject(next.UserQuery))) }); } } else { var userQuery = targetContext.UserQuery; // 子スキル情報、フルフィルメントテキストは削除(Postbackデータに収まらないため) userQuery.SubSkills = null; userQuery.FulfillmentText = string.Empty; ret = new QuickReply(new List <QuickReplyButtonObject> { new QuickReplyButtonObject(new PostbackTemplateAction( "ひとつ前の対話を再開する", JsonConvert.SerializeObject(userQuery))) }); } } return(ret); }
protected override async Task OnPostbackAsync(PostbackEvent ev) { var query = JsonConvert.DeserializeObject <UserQuery>(ev.Postback.Data); await OnBeforePostbackAsync(ev, query); if (ShouldEnd) { return; } // テキスト・インテント待ちを確認 var waiterId = new EntityId(nameof(ContextEntity), $"{WaiterEntityPrefix}_{ev.Source.UserId}"); var waiterState = await DurableClient.ReadEntityStateAsync <ContextEntity>(waiterId); var isWaitingForIntents = false; var isWaitingForText = false; Context waitingContext = null; if (waiterState.EntityExists && waiterState.EntityState.Context != null) { waitingContext = waiterState.EntityState.Context; if (waitingContext.ExpectedIntentNames != null) { isWaitingForIntents = true; } else { isWaitingForText = true; } } // 実行スキルを選択 var targetSkill = (isWaitingForIntents && waitingContext.ExpectedIntentNames.Contains(query.IntentName)) || isWaitingForText ? waitingContext.SkillName // 待機スキル : query.IntentName; // 解析スキル if (isWaitingForText) { query = new UserQuery { IntentName = targetSkill, }; } var skill = Skills.FirstOrDefault(s => s.IntentName == targetSkill); if (skill != null) { var requestedTimestamp = query.Timestamp; // コンテキスト確認を行う var entityId = new EntityId(nameof(ContextEntity), $"{targetSkill}-{ev.Source.UserId}"); var state = await DurableClient.ReadEntityStateAsync <ContextEntity>(entityId); Context context = null; if (state.EntityExists && state.EntityState.Context != null) { context = state.EntityState.Context; } else { context = new Context { UserId = ev.Source.UserId, IsNew = true }; query.Timestamp = DateTime.UtcNow.Ticks; } context.UserQuery = query; if (!context.UserQuery.IsSubSkill && !context.UserQuery.AllowExternalCalls && (context.IsNew || context.UserQuery.Timestamp > requestedTimestamp)) { await LineMessagingClient.ReplyMessageAsync(ev.ReplyToken, "その操作は現在できません。"); return; } // スキル再確認 var subSkill = Skills.FirstOrDefault(s => s.IntentName == context.UserQuery.IntentName); var execSkill = subSkill ?? skill; var messages = await execSkill.GetReplyMessagesAsync(context); // 状態を保存 await DurableClient.SignalEntityAsync <IContextEntity>(entityId, proxy => proxy.SetContext(context)); // Waiter if (context.IsWaiting) { await DurableClient.SignalEntityAsync <IContextEntity>(waiterId, proxy => proxy.SetContext(context)); } if (messages != null) { if (!execSkill.IsContinued) { var quickReply = await FinishAndGetResumeQuickReplyAsync(context); if (messages.Last().QuickReply != null && messages.Last().QuickReply.Items.Count > 0) { messages.Last().QuickReply = MergeQuickReply(messages.Last().QuickReply, quickReply); } else { messages.Last().QuickReply = quickReply; } } await LineMessagingClient.ReplyMessageAsync(ev.ReplyToken, messages); } // バッチ実行スキル else if (context.UserQuery.SubSkills != null) { await StartBatchSubSkills(ev.ReplyToken, context); } } await OnAfterPostbackAsync(ev, query); }