himanago

Azure・C#などのMS系技術やLINE関連技術など、好きな技術について書くブログ

【GitHub Codespaces + LINE OpenAPI + azd】最新の機能をサポートした C# SDK で LINE Bot 開発が始められるテンプレートを改良しました

昨年末、アドベントカレンダー記事用に実験的に「GitHub Codespaces + LINE OpenAPI による C# での LINE Bot 開発がすぐ始められるテンプレート」を作っていました。 himanago.hatenablog.com

明日このハンズオンイベントをやるので(準備がギリギリすぎる)

linedevelopercommunity.connpass.com

ここで使えるように以前の課題を克服してある程度使えるように改良してみました!

今回作ったテンプレートリポジトリ

こちら。使い方は README に簡単に書きました。

最新の LINE Messaging API の機能をサポートした SDK で開発が始められます。

github.com

改善点

Azure Developer CLI (azd) & Bicep 対応

今回一番やりたかったことですが、最初に azd up すれば一発でオウム返ししてくれる LINE Bot のバックエンド(Azure Functions)が Azure 上にデプロイされます!

LINE のチャネルアクセストークンも azd up 実行時のパラメータとして受け取って、Key Vault に格納するように組んでいるので、使い勝手もよいかと思います。

.NET 8 isolated → .NET 6 in-process への変更

以前作ったものでは .NET 8 isolated になってしまっていました。

Azure Functions の .NET 8 isolated のコードはいままでの書き方とだいぶ違っていたりして作りにくい感じだったのですが、今回は .NET 6 in-process に変更1。だいぶ作りやすくなったかと思います。

もうすぐ .NET 8 in-process が出るとのことなので、そちらが出たらアップグレードしようと思います。

関数コードの改良

DI で MessagingApiApi をもらってくるようにしたので、関数のクラスのコードがちょっとすっきりできたかなと思います。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using LineOpenApi.MessagingApi.Api;
using LineOpenApi.MessagingApi.Model;
using LineOpenApi.Webhook.Model;
using System.Collections.Generic;

namespace LineBotFunctions;
public class WebhookEndpoint
{
    private IMessagingApiApiAsync Api { get; }

    public WebhookEndpoint(IMessagingApiApiAsync api)
    {
        Api = api;
    }

    [FunctionName("WebhookEndpoint")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# webhook endpoint function processed a request.");

        try
        {
            var body = await new StreamReader(req.Body).ReadToEndAsync();
            var callbackRequest = JsonConvert.DeserializeObject<CallbackRequest>(body);

            foreach (var ev in callbackRequest.Events)
            {
                // echo when receive text message
                if (ev is MessageEvent messageEvent && messageEvent.Message is TextMessageContent textMessageContent)
                {
                    var replyMessageRequest = new ReplyMessageRequest(messageEvent.ReplyToken, new List<Message>
                    {
                        new TextMessage(textMessageContent.Text)
                        {
                            Type = "text"
                        }
                    });
                    await Api.ReplyMessageAsync(replyMessageRequest);
                }
            }
        }
        catch (Exception e)
        {
            log.LogError($"Error: {e.Message}");
            log.LogError($"Error: {e.StackTrace}");
        }
        
        return new OkObjectResult("OK");
    }
}

Webhook で受け取る JSON のデシリアライズがうまくいかない件の対応&最新化対応

以前の記事にも書いたのですが、不要な行が OpenAPI Generator で生成した SDK の一部ファイルに含まれています。

そこで、シェルスクリプトでその行を一気に削除するようにしました。

この処理は SDK の生成とあわせてひとつのファイルにまとめ、再実行により常に最新のものにアップデートできるようにしています。

if [ -d "./sdk" ]; then
    rm -rf ./sdk
fi

npx @openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/line/line-openapi/main/messaging-api.yml -o sdk -g csharp --additional-properties=netCoreProjectFile=true,targetFramework=netstandard2.1,library=httpclient,packageName=LineOpenApi.MessagingApi

npx @openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/line/line-openapi/main/webhook.yml -o sdk -g csharp --additional-properties=netCoreProjectFile=true,targetFramework=netstandard2.1,library=httpclient,packageName=LineOpenApi.Webhook

find ./sdk/src/LineOpenApi.Webhook/Model/ -type f | while read file; do
    if ! grep -q ": IEquatable<" "$file"; then
        sed -i '/\[JsonConverter(typeof(JsonSubtypes), "Type")\]/d' "$file"
    fi
done

まとめ

まだオウム返ししか試せてないですが、明日のハンズオンでやりたい機能なども検証しつつハンズオン用のテンプレートも作っていこうと思います。

今回のテンプレートはハンズオンでもわりと使いやすいと思うので、今後ハンズオンする際に活用していきたいなと思います。


  1. .NET 6 に落とした際、OpenAPI Generator で生成したコードで使用している RestSharp でビルドエラーになったので、生成コマンドのオプションでこのライブラリを使わないように指定して回避しました。

GitHub Codespaces + LINE OpenAPI による C# での LINE Bot 開発がすぐ始められるテンプレートを作ってみた

はじめに

この記事は 「LINE DC Advent Calendar 2023」 19日目の記事です。

Azure には Azure OpenAI Service があり、LINE Pay Bot からも使いたい、そして Azure では C# での開発が何かと便利。

Azure と C# で LINE Bot 開発したくなるのは必然です。

そこで今回は LINE OpenAPI を使って、C# による開発を便利にできないか?ということをちょっと趣向を変えつつ考えてみました。

LINE OpenAPI は、LINE DC のアドカレ4日目の記事でも取り上げられてます。

qiita.com

以前は C# で LINE Bot を開発するときは、コミュニティ SDK を使っていましたが、現在は最新化されておらず…

LINE OpenAPI を使って SDK を最新化するのもよいのですが、最近 GitHub Codespaces を使っていてすごく便利だなと感じているので、今回はそれと絡めて、Codespace の開発環境を立ち上げたときに最新の SDK がその場で自動生成され、すぐに Bot 開発が始められるテンプレートを作る、ということをやってみました。

やりたいこと

GitHub Codespaces にはテンプレート機能というのがあります。

DevContainer に使いたい環境を定義し、あらかじめ用意しておきたいファイルとセットで GitHub にテンプレートリポジトリとして公開しておけば、それを Codespace から開くことができます。

好きな開発環境を展開できるのでとても便利です。

docs.github.com

これを使って、

  • Azure Functions の開発環境(CLIVS Code 拡張機能など)
  • LINE OpenAPI 定義をもとに生成された C# SDK
  • オウム返しをする LINE Bot のサンプル関数クラス

がセットになった環境をすぐ開けるようにおぜん立てします。

C# SDK の生成は、openapi-generator-cli を使用します。

github.com

作ったもの

ということで作ってみたのですが、後述する理由から正直そこまでの実用性のあるものにはなってません。。

github.com

このテンプレートから Codespace を起動すると、起動後に openapi-generator-cli が走り、最新の LINE OpenAPI 定義に基づいて LINE Bot 開発に必要な C# のプロジェクト群が sdk フォルダに作られます。

必要な拡張機能や Azure Functions Core Tools なども最初から入るようにしています。

DevContainer の定義

postCreateCommand のところで、openapi-generator-cli を使って LINE OpenAPI の定義を基にした C# コードを生成するコマンドを書いています。

生成している定義はとりあえず最低限、messaging-api.ymlwebhook.yml です。

{
    "name": "LINE OpenAPI C# Azure Functions Starter",
    "image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye",
    "features": {
        "ghcr.io/devcontainers/features/dotnet:2": {
            "version": "6.0"
        },
        "ghcr.io/devcontainers/features/java:1": {
            "installMaven": true,
            "version": "latest",
            "jdkDistro": "ms",
            "gradleVersion": "latest",
            "mavenVersion": "latest",
            "antVersion": "latest"
        }
    },

    "postCreateCommand": "npm install @openapitools/openapi-generator-cli -g && npm install -g azure-functions-core-tools@4 && npx @openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/line/line-openapi/main/messaging-api.yml -o sdk -g csharp --additional-properties=netCoreProjectFile=true,targetFramework=netstandard2.1,packageName=LineOpenApi.MessagingApi && npx @openapitools/openapi-generator-cli generate -i https://raw.githubusercontent.com/line/line-openapi/main/webhook.yml -o sdk -g csharp --additional-properties=netCoreProjectFile=true,targetFramework=netstandard2.1,packageName=LineOpenApi.Webhook",
    "customizations": {
        "codespaces": {
            "openFiles": ["LineBotFunctions/ReplyFunction.cs"]
        },
        "vscode": {
            "extensions": [
                "ms-azuretools.vscode-azurefunctions",
                "ms-dotnettools.csdevkit"
            ]
        }
    }
}

生成される LINE OpenAPI のコード

自動生成で下記のようなものが作られています。

それっぽいクラス群がコマンド一発で生成されるのはとても気持ちいいですね。

オウム返し用サンプル関数

生成されるコードにあわせて、こんなかんじになりました。

Event オブジェクトを取りまわして分岐させていくところは自分で書かないといけないので、ここはちゃんと作られた SDK には当然劣りますね。

あと、これは仕方ないのですが MessagingApiApi というのがなんともいえない感じです。

[Function("ReplyFunction")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
    try
    {
        var body = await new StreamReader(req.Body).ReadToEndAsync();
        var callbackRequest = JsonConvert.DeserializeObject<CallbackRequest>(body);

        var api = new MessagingApiApi(new Configuration
        {
            AccessToken = Environment.GetEnvironmentVariable("ChannelAccessToken"),
            BasePath = "https://api.line.me"
        });

        foreach (var ev in callbackRequest.Events)
        {
            if (ev is MessageEvent messageEvent && messageEvent.Message is TextMessageContent textMessageContent)
            {
                var replyMessageRequest = new ReplyMessageRequest(messageEvent.ReplyToken, new List<Message>
                {
                    new TextMessage(textMessageContent.Text)
                    {
                        Type = "text"
                    }
                });
                await api.ReplyMessageAsync(replyMessageRequest);
            }
        }
    }
    catch (Exception e)
    {
        _logger.LogError($"Error: {e.Message}");
        _logger.LogError($"Error: {e.StackTrace}");
    }
    
    return req.CreateResponse(HttpStatusCode.OK);
}

困りごと

Webhook で受け取る JSON のデシリアライズC# オブジェクトへの変換)がうまくいかない

今回最もハマったところなのですが、

if (ev is MessageEvent messageEvent && messageEvent.Message is TextMessageContent textMessageContent)

でやっている分岐がうまく動かない、というものでした。

実は生成されたコードでは、MessageEventEvent を継承し、TextMessageContentMessageContent を継承しています。

LINE Messaging API ではさまざまなイベントやメッセージタイプがありますが、それをポリモーフィズムで表現しています。

それぞれのクラス定義では、以下のように継承の親子関係を JSON からデシリアライズする際にわかるように指定がされています。

Event クラスでは、「Type"message" だったら MessageEvent として扱う」などの指定が存在するサブタイプの数だけ指定されています。

    [DataContract(Name = "Event")]
    [JsonConverter(typeof(JsonSubtypes), "Type")]
    [JsonSubtypes.KnownSubType(typeof(AccountLinkEvent), "accountLink")]
    [JsonSubtypes.KnownSubType(typeof(ActivatedEvent), "activated")]
    [JsonSubtypes.KnownSubType(typeof(BeaconEvent), "beacon")]
    [JsonSubtypes.KnownSubType(typeof(BotResumedEvent), "botResumed")]
    [JsonSubtypes.KnownSubType(typeof(BotSuspendedEvent), "botSuspended")]
    [JsonSubtypes.KnownSubType(typeof(DeactivatedEvent), "deactivated")]
    [JsonSubtypes.KnownSubType(typeof(FollowEvent), "follow")]
    [JsonSubtypes.KnownSubType(typeof(JoinEvent), "join")]
    [JsonSubtypes.KnownSubType(typeof(LeaveEvent), "leave")]
    [JsonSubtypes.KnownSubType(typeof(MemberJoinedEvent), "memberJoined")]
    [JsonSubtypes.KnownSubType(typeof(MemberLeftEvent), "memberLeft")]
    [JsonSubtypes.KnownSubType(typeof(MessageEvent), "message")]
    [JsonSubtypes.KnownSubType(typeof(ModuleEvent), "module")]
    [JsonSubtypes.KnownSubType(typeof(PostbackEvent), "postback")]
    [JsonSubtypes.KnownSubType(typeof(ThingsEvent), "things")]
    [JsonSubtypes.KnownSubType(typeof(UnfollowEvent), "unfollow")]
    [JsonSubtypes.KnownSubType(typeof(UnsendEvent), "unsend")]
    [JsonSubtypes.KnownSubType(typeof(VideoPlayCompleteEvent), "videoPlayComplete")]
    public partial class Event : IEquatable<Event>, IValidatableObject
    {

サブタイプである MessageEvent クラスはこのように定義されています。

    [DataContract(Name = "MessageEvent")]
    [JsonConverter(typeof(JsonSubtypes), "Type")]
    public partial class MessageEvent : Event, IEquatable<MessageEvent>, IValidatableObject
    {

これらの設定により、JSON データの中身を見てそれがどのサブタイプにデシリアライズされるべきか見てくれる動きは、「JsonSubTypes」というライブラリによって実現する機能です。

しかし、どういうわけかこれが効かず、MessageEvent になってほしいのに基底クラスの Event としてデシリアライズされてしまうという現象が起きてしまいました。

なんやかんや試行錯誤したり ChatGPT に聞いたりしていたら、解決。

どうやらサブタイプ側の記述が不要だったようです。

解決策

サブタイプのクラスについている [JsonConverter(typeof(JsonSubtypes), "Type")] を消す or コメントアウトします。

    [DataContract(Name = "MessageEvent")]
    // ↓↓↓これを消す↓↓↓
    // [JsonConverter(typeof(JsonSubtypes), "Type")]
    public partial class MessageEvent : Event, IEquatable<MessageEvent>, IValidatableObject
    {

この記述が悪さをして、うまくサブタイプでデシリアライズしてくれなかったようです。(OpenAPI Generator の不具合なのか…??)

これを、Webhook プロジェクト内の Model 内のすべてのサブタイプに対して行う必要がありますが、サンプルのオウム返しを動かすだけなら

  • sdk/src/LineOpenApi.Webhook/Model/MessageEvent.cs
  • sdk/src/LineOpenApi.Webhook/Model/TextMessageContent.cs

だけで大丈夫です。

コメントアウトしたら、無事に動くようになりました。

まとめ

今回は実験的試みでしたが、OpenAPI Generator の癖なども実感することができたので得たものはあったかなというところです。

公式の OpenAPIの活用によるSDK開発の進化:LINE APIの進化の軌跡 の記事でも紹介されていますが、公式の SDK は、GitHub Actions を用いて自動生成やスクリプト実行によって最新状態を保っているようです。公式 SDK の各リポジトリを見るとその仕組みが垣間見れます。

今回はまったく異なるアプローチでの実験でしたが、やはり実用性を考えれば、公式 SDK 同様のアプローチで SDK を作ってあげるのがスマートですね。

Azure のハンズオン記事紹介(サーバーレス / ChatGPT)&今年度振り返り

はじめに

今年度もそろそろ終わりですが、今年ハンズオンを作成してあまり宣伝していなかったので、こちらに書いておきます。

Azure サーバーレスをササっと体験するハンズオン

github.com

Gitpod 上でサクッとサーバーレスなアプリを構築して Azure の便利さを体験するハンズオンです。

Bicep と GitHub Actions を使って Azure サーバーレステクノロジーを組み合わせた開発を高速で体験します。

README 記載の手順に沿って作業すると、以下のテクノロジーを使用した簡易掲示板アプリがデプロイされます。

  • Azure Static Web Apps
  • Azure Functions
  • Azure Cosmos DB
  • Azure SignalR Service

環境も Gitpod 対応なので、ローカルの開発環境の準備は不要です。

ChatGPT と話せる LINE Bot を Logic Apps テンプレートで超爆速開発

zenn.dev

話題の ChatGPT API と話せる LINE Bot を爆速で開発するハンズオンです。

以前公開した Azure Logic Apps 用の LINE Bot テンプレートを利用して、Logic App ・LINE 間の接続部分の手順を省略しています。

ささっと LINE から ChatGPT が使えるようになるので ChatGPT に興味がある方も、ノーコード・ローコードでの LINE Bot 開発に興味がある方にもおすすめです。

もっと便利にする続編も書いていますので、こちらも近日公開予定です。

今年度の振り返り

今年度は Azure Container Apps について大きなカンファレンスで登壇することができました。

LINE Developers Community REV UP 2022」という LINE 関連開発者向けカンファレンス、「ServerlessDays Tokyo 2022 Virtual」というサーバレス開発者向けのカンファレンスです。

REV UP 2022 のほうは実行委員までやらせていただき、とてもよい経験になりました。来年度(今年)も楽しみです。

資料・動画

Container Apps と LINE API の連携・活用アイディアについて解説しています。

資料

REV UP版 www.docswell.com

ServerlessDays版 www.docswell.com

動画

REV UP版 www.youtube.com

ServerlessDays版 www.youtube.com

おわりに

MVP の審査の活動報告のタイミングだったので、年度末ということもあり振り返ってみました。

来年度は少し活動の幅を広げてみたいなと思ってます。引き続き、よろしくお願いいたします。

【小ネタ】Azure Cognitive Service for Language の質問応答の回答を "ねこ" っぽくする

はじめに

2022年2月22日、にゃんにゃんにゃんで 2 が 6 個ということで、今日はかなりの「ねこの日」でした。

ちょうど自社内でチャットボットの話をする機会もあったので、Azure のナレッジベースのサービスを使って "ねこ" っぽいチャットボットを作りました。

技術的にはだいぶしょーもないやり方ですが、備忘的に記録しておきます。

手順

ナレッジベースを作る

ふつうに URL などからナレッジベースを作ります。

「Manage sources」から、適当な FAQ ページの URL を追加します。

f:id:himanago:20220222233324p:plain

一般的な日本語の FAQ ページからとれば、だいたい「ですます調」で回答が作成されるかと思います。

f:id:himanago:20220222230818p:plain

次に、雑談対応のためにおしゃべり(Chitchat)を追加します。

あとからねこっぽくするわけですが、その変換がしやすいので、ここであえて Professional を選択します。

f:id:himanago:20220222230734p:plain

猫語に変換

このやり方が、まじでしょーもないです。

ナレッジベースを Excel にエクスポートします。

f:id:himanago:20220222230840p:plain

エクスポートした Excel ファイルを開き、で回答の列を選択肢、以下の順で文字列置換します。

  1. 「よ。」→「。」
  2. 「ね。」→「。」
  3. 「。」→「にゃ。」

※ 1 と 2 は主におしゃべりの「~ですよ。」「~ですね。」をいったん「~です。」に統一する目的

f:id:himanago:20220222231024p:plain

こうすると、おしゃべり部分含めて語尾が違和感なく「にゃ。」になり、ちょっと丁寧なねこの口調(?)になります。

f:id:himanago:20220222231157p:plain

この Excel ファイルを上書き保存して、Language Studio にインポート。

f:id:himanago:20220222231215p:plain

ナレッジベースが、語尾が「にゃ。」になった回答で上書きされます。

動かしてみる

ちゃんとねこっぽいですね。

f:id:himanago:20220222231626p:plain

おしゃべりの部分も「早くお休みになれるといいですね。」→「早くお休みになれるといいですにゃ。」になっており、違和感なくねこです。

f:id:himanago:20220222233846p:plain

Azure Bot Service を使って Teams / LINE / Alexa 等に公開したい場合は以下の記事などを参照ください。

Azure Cognitive Service for Language + Azure Bot Service で超簡単にチャットボット作成①~ナレッジベースの作成&Webチャットの公開 - himanago

Azure Cognitive Service for Language + Azure Bot Service で超簡単にチャットボット作成②~Microsoft Teamsへの簡易接続 - himanago

Azure Cognitive Service for Language + Azure Bot Service で超簡単にチャットボット作成③~LINE Botとしての公開 - himanago

Azure Cognitive Service for Language + Azure Bot Service で超簡単にチャットボット作成④~Alexa スキルとしての公開 - himanago

まとめ

語尾を変えるくらいなら Excel でばばっとやってしまうのが早いこともありますね。

Language Studio の質問応答、Excel 形式でエクスポート・インポートできるのは地味に便利でした。

【Update】GitHub のボタンから 1 クリックでデプロイできる LINE Bot のオウム返しバックエンド用 Azure Logic Apps フロー

以前公開した、1 クリックで LINE Bot 用の Logic Apps フローをデプロイできる ARM テンプレートですが、オウム返しのフローを更新しました。

himanago.hatenablog.com

テンプレートはこちらのリポジトリから。

github.com

README のところにボタンがあるので、ここからデプロイして即オウム返しの LINE Bot が完成します。

f:id:himanago:20220222221712p:plain

「Deploy to Azure」ボタンを押して Azure にサインイン後、

  • サブスクリプション
  • リソースグループ
  • リージョン
  • Logic App 名
  • LINE Messaging API のチャネル ID
  • LINE Messaging API のチャネルシークレット

を入力すると選んだサブスクリプション、リソースグループにデプロイされます。

今回のアップデート内容

200 の応答を即返すようにしつつ、全体的にフローをみなしました。

f:id:himanago:20220222222353p:plain

これまではユーザーから送られたテキスト内容をそのまま返信するオウム返しとして、テキストメッセージのみに対応していましたが、今回からメッセージ種別の分岐をスイッチで追加し、スタンプが送られた場合は固定のスタンプを返すようにしています。

f:id:himanago:20220222223709p:plain

他のメッセージ種別の分岐を追加するのも容易になったと思います。

また、メッセージイベントのみに反応させるための制御(条件)も追加しているので、必要に応じてその他のイベント(フォローイベントなど)の処理も追加しやすくなったと思います。

こういった分岐が最初からあると、本格的な LINE Bot を作る足掛かりになると思うので、ぜひ使ってみてほしいと思います。

Azure Functions に Django で作った Web アプリをホストする

はじめに

ちょっとおもしろい記事を見つけました。

Pythonフルスタックな Web アプリケーション開発フレームワークである「Django」で作ったアプリケーションを、まるごと Azure Functions に乗せてしまうというものです。

szwarc.ai

フルスタックな Web アプリケーションフレームワークで作った重厚なアプリが、サーバーレスな関数上で動くということで、なかなかおもしろそうです。

実際にやってみた

この記事をベースに、実際に Azure 上で動くか試してみます。

Azure SQL Database の作成

まず DB を Azure 上に作ります。元記事と同じように、サーバーレスで作っていきます。

f:id:himanago:20220203002826p:plain

サーバーは新規作成で、構成は以下の通り。

f:id:himanago:20220203002711p:plain

データベースの構成は下記のような感じで。

f:id:himanago:20220203002507p:plain

f:id:himanago:20220203002557p:plain

作成されたら、ファイアウォール設定を変えておきます。

これから作る Django のアプリケーションをローカルで動かす際もこの Azure 上の DB を見に行くので、ローカルからも接続できるようにしておきます。

f:id:himanago:20220203004410p:plain

ローカルで Azure Functions のプロジェクト作成

VS Code を開き、Azure Functions 拡張機能から Python の Function App を作っていきます。

f:id:himanago:20220203003825p:plain

関数の名前は DjangoTrigger で。

f:id:himanago:20220203003838p:plain

承認レベルは必ず Anonymous にします。

f:id:himanago:20220203003920p:plain

Functions プロジェクト内に Django アプリ作成

つづいて Django アプリを作っていきます。

venv を作ってから、

python -m venv venv

requirements.txt を編集します。下3行を追記。

azure-functions
django==4.0.2
mssql-django
whitenoise

まとめて pip install します。

pip install -r requirements.txt

Django プロジェクトをつくります。

. を末尾につければ直下に直接作ってくれます。

django-admin startproject config .

ここまででこのようになっていれば OK です。

f:id:himanago:20220203005929p:plain

Function → Django をつなぐ設定

Functions から Django が呼ばれるように設定していきます。このあたりは元記事にかいてあるとおりです。

DjangoTrigger/function.json を以下のようにします("route": の行を追加)。

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ],
      "route": "DjangoTrigger/{*path_info}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ]
}

DjangoTrigger/__init__.py を丸ごと書き換えます。

import azure.functions as func
from config.wsgi import application

def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return func.WsgiMiddleware(application).handle(req, context)

ここでいったん動作確認してみます。

func host start

ブラウザで開いてみても、まだ動きません。

f:id:himanago:20220203010658p:plain

より細かい設定をしていきます。Azure の SQL DB への接続もやってしまいます。

config/settings.py の内容を編集していきます。

# (略)

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

FUNCTION_APP_PATH = 'api/DjangoTrigger'   # 追加

# (略)

ALLOWED_HOSTS = ['*']  # 変更

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'whitenoise.runserver_nostatic',  # 追加
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # 追加
]

# (略)

# 変更
DATABASES = {
    'default': {
        'ENGINE': 'mssql',
        'NAME': 'xxxx-xxxx-xxxx',       # SQL DB の名称
        'USER': 'xxxxxxxx',                   # サーバー管理者ログイン
        'PASSWORD': '************',    # サーバー管理者パスワード
        'HOST': 'xxxxxxx.database.windows.net',  # ホスト名
        'PORT': '',
        'OPTIONS': {
            'driver': 'ODBC Driver 17 for SQL Server',
        },
    }
}

# (略)

STATIC_URL = '/' + FUNCTION_APP_PATH + '/static/'   # 変更

つづいて config/urls.py を書き換えます。

from django.contrib import admin
from django.urls import path
from config.settings import FUNCTION_APP_PATH   # <- import our FUNCTION_APP_PATH constant from settings.py file

urlpatterns = [
    path(FUNCTION_APP_PATH + '/admin/', admin.site.urls),  # <- change "path" to include FUNCITION_APP_PATH
]

ここまでできたら、もう一度起動してみます。

func host start

このタイミングで DB につなぐので、ODBC Driver がインストールされていないと起動できません。

インストールされていなければ、入れておきましょう。

docs.microsoft.com

自分が Mac でやったときは Xcode を最新化する必要があったりで結構面倒でした。

エラーなく起動したら、http://localhost:7071/api/DjangoTrigger/admin/ をブラウザで開いてみます。

以下のようなログイン画面が表示されれば、OK です(Django が動いてますね!)。

f:id:himanago:20220203092807p:plain

データベースへのテーブル作成

より本格的に使えるか試すため、SQL DB にテーブルを作っていきます。

python manage.py migrate

Mac でやったときはここで

[08001] [Microsoft][ODBC Driver 17 for SQL Server]Client unable to establish connection (0) (SQLDriverConnect)

というエラーが出てしまいました。どうも Mac に入っている OpenSSL が悪いらしく、LibreSSL から OpenSSL@1.1 にしたり

github.com

を参考にいろいろやったりしてなんとか解消しました(ちなみに Windows の環境でやったときはすんなりでした)。

ログインユーザー作成

うまく動いたら、続いてアプリの管理者ユーザーを作ります。

コマンドで作成します。

python manage.py createsuperuser

指示に従って、ユーザー名、メールアドレス、パスワードを設定します。

モデルクラス&テーブル作成

元記事にはないですが、新しいモデルクラスとテーブルも作ってみます。

作るテーブルは公式チュートリアルのものを使います。

docs.djangoproject.com

python manage.py startapp polls

ファイルが作られるので、その中の polls/models.py を以下のようにします。

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

config/settings.py にさらに追記します。

INSTALLED_APPS = [
    'polls.apps.PollsConfig',  # これを追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'whitenoise.runserver_nostatic',
]

コマンドを実行します。

python manage.py makemigrations polls
python manage.py sqlmigrate polls 0001
python manage.py migrate

Azure 上の SQL DB に、作ったモデルクラスにあったテーブルが作成されます。

最後に polls/admin.py を以下のようにします。

from django.contrib import admin
from .models import Question
from .models import Choice

admin.site.register(Question)
admin.site.register(Choice)

ローカルでの動作確認

func host start でローカルで実行して、以下の http://localhost:7071/api/DjangoTrigger/admin/ にアクセスしてみます。

createsuperuser で作った ユーザー名とパスワードでログインすると、以下のようになります。

f:id:himanago:20220203181751p:plain

ちゃんと動いてるっぽいです!

Function App のデプロイ

ではいよいよ Azure に Function App としてデプロイしてみます。

VS Code拡張機能を使っていきます。

Azure にサインインします。

f:id:himanago:20220204142809p:plain

「Deploy to Function App...」をクリック。

f:id:himanago:20220204142957p:plain

あとはプロジェクト、サブスクリプション、リージョンなどを指示通りに指定していけば作成・デプロイされます。

Azure 上での動作確認

デプロイ後は、Azure ポータルにいかずとも VS Code 上で関数 URL を得られます。

f:id:himanago:20220204143546p:plain

コピーした URL をもとにして、https://xxxxxxxxxx.azurewebsites.net/api/DjangoTrigger/admin/ となるように編集(大文字小文字に注意!)してアクセスしてみます。

ログイン画面 f:id:himanago:20220204144007p:plain

管理画面 f:id:himanago:20220204144300p:plain

データ追加 f:id:himanago:20220204144723p:plain

f:id:himanago:20220204144733p:plain

f:id:himanago:20220204144743p:plain

f:id:himanago:20220204144753p:plain

ちゃんと動いてます!

おわりに

このままでは URL に api などが入っていていまいちなのですが、Web アプリがまるごと Function App に乗るのはなかなかおもしろいですね。

Consumption プランで乗せてしまえば、コールドスタートの問題はあるものの低価格&スケーラブルに Django アプリが運用できます。

ちなみにログイン機能で使われるセッションキーについてですが、サーバー側はデータベースに保存されるようになっているので、サーバーレスでも運用できそうです。

Azure Cognitive Service for Language + Azure Bot Service で超簡単にチャットボット作成④~Alexa スキルとしての公開

以下のつづきです。たぶん今回が最終回。

① ナレッジベースの作成&Webチャットの公開 himanago.hatenablog.com

Microsoft Teamsへの簡易接続 himanago.hatenablog.com

③ LINE Botとしての公開 himanago.hatenablog.com

最後は Alexa

Azure Cognitive Service for Language + Azure Bot Service で作ったチャットボットを、最後は Alexa スキルとしてスマートスピーカーから使えるようにしてみます。

ドキュメントの手順どおりやればたしかに簡単に公開できるのですが…ちょっといままでと違ってそのままで OK!とはならなそうです。

さっそく手順をみていきましょう。

手順

接続方法はこちらにまとまっています。

docs.microsoft.com

このとおりにやってみます。

Alexa Developer Console でのスキル作成

Alexa Developer Console にログインし、「スキルの作成」ボタンをクリックします。

f:id:himanago:20220131230021p:plain

新しいスキルの名前を入力し、「日本語」「カスタム」「ユーザー定義のプロビジョニング」を選択して、最後に上部の「スキルを作成」をクリックします。

f:id:himanago:20220131230615p:plain

「スクラッチで作成」が選択された状態で「選択」をクリックします。

f:id:himanago:20220131230823p:plain

するとスキルが作成されます。

対話モデル JSON の登録

通常はここからスキルとの対話をどのように行うかを設計し、対話モデルを作っていくのですが、Azure Bot Service とつなぐ場合はそこはいりません。対話は Azure Bot Service 側ですべて処理するので、Alexa スキルはユーザーからの入力をそのまま伝える役割しか担わないのです。

Alexa スキルの場合、phrase スロットを使うと入力そのままを処理できます。

とてもシンプルなモデルになるので、ドキュメントにはあらかじめ JSON で定義された対話モデルが用意されています。

スキルダッシュボードの左側のメニューの「対話モデル」の「JSON エディター」に、以下の JSON を設定します。

一番上の呼び出し名と、一番下発話例3つが自分で考える部分です。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "アジュールサポート使い方ボット",
            "intents": [
                {
                    "name": "GetUserIntent",
                    "slots": [
                        {
                            "name": "phrase",
                            "type": "phrase"
                        }
                    ],
                    "samples": [
                        "{phrase}"
                    ]
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                }
            ],
            "types": [
                {
                    "name": "phrase",
                    "values": [
                        {
                            "name": {
                                "value": "サポートの使い方を教えて"
                            }
                        },
                        {
                            "name": {
                                "value": "サポートの対応言語を教えて"
                            }
                        },
                        {
                            "name": {
                                "value": "こんにちは"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

起動名は、音声入力できちんと一致するような表現にしてあげないといけないので、実機テストをしながら誤動作しないような名前にしてあげましょう。

また、JSON エディター上では IME がオンだとうまく入力ができないので、JSON を完成させてからコピペするようにしたほうがいいです。

「モデルの保存」をクリックし、その後活性化される「モデルのビルド」をクリックします。これで対話モデルがスキルに反映されます。

f:id:himanago:20220131231851p:plain

Azure Bot Service との接続

Alexa 側のスキル ID を取得して Bot Service へ設定し、Bot Service の URL を Alexa スキルのエンドポイントに設定します。

ドキュメントでは Alexa Developer Console の最初のページを開けと書いてありますが、「エンドポイント」セクションを開くのが早いです。

スキル ID が表示されているので、これをコピーします。

f:id:himanago:20220131232413p:plain

Azure ポータルを別タブで開き、Azure Bot Service のチャンネルを有効化します。

f:id:himanago:20220126232903p:plain

スキル ID を貼り付け、エンドポイント URI をコピーします(まだ保存は押さない)。

f:id:himanago:20220131232931p:plain

Alexa Developer Console に戻り、サービス エンドポイントの種類を「HTTPS」に変更し、「デフォルトの地域」にコピーしたエンドポイント URI の値に設定します。

その下にの選択は「開発用のエンドポイントは、証明機関が発行したワイルドカード証明書をもつドメインサブドメインです」にします。

f:id:himanago:20220131233007p:plain

設定したら、上部の「エンドポイントを保存」をクリックします。

これで完成です!

動作確認

Developer Console の「テスト」から、「スキルテストが有効になっているステージ」を「開発中」にするとシミュレーターや実機で動作確認できます。

f:id:himanago:20220131235512p:plain

スキル起動後にあいさつが返ってきて、その後リスニング状態になります。

質問をすると回答が返ってきて再びリスニング状態に…という繰り返しで動きます。

おしゃべりもちゃんと対応してくれますし、音声認識による表現のぶれなども吸収してくれます。

ただ、回答が長いので VUI 向きではないかなと思うものも多いです。画面付きでなければ正直きついです。

やはりスマートスピーカーで使う場合は、音声になじむように回答を作らないといけないですね。

おわりに

クロスプラットフォームにチャットボットをまったくの実装なしに公開できる、というのは非常に優れたサービスだと思います。

とはいえ、チャットボット(スマートスピーカースキル)はそれぞれのプラットフォームや形式ごとに適した UI/UX が存在します。

質問・回答の1対1の対話だとしても、それぞれのチャンネル(プラットフォーム)に適した回答のしかたをするべきで、安易にそのまま公開すればよいというものではなさそうです(特に Alexa)。

実際複数のチャンネルに公開したい場合は単純に Language の質問回答をつなげただけで要件を満たすことはないと思うので、素直に Bot Framework で各チャンネル固有の実装をしてカスタマイズすべきでしょう。

限られた用途なら、今回紹介した手順でささっと公開することはありだと思います。

公開したいチャンネルの特性を踏まえて質問・回答をチューニングして、用途を絞って使っていきましょう。