Line.OpenApi.* を使ってボットを作る — LINE × Hugging Face の実装例

前回、公式の OpenAPI 仕様から Kiota で Line.OpenApi.* を生成する話をしました。今回はこのライブラリを使ったアプリの実装例を紹介します。

line-hf-bot とは

題材は、私が公開している line-hf-bot です。LINE のトーク画面から Hugging Face の各種モデルを使える AI ボットで、次のことができます。

  • 💬 チャット — 会話の履歴を覚えたまま、AI と自然にやり取りできます
  • 🎨 画像生成 — 文章を送ると、その内容の画像を作ります
  • ✏️ 画像編集 — 送った写真や生成した画像に「夜にして」などと指示して編集できます
  • 🔍 画像への質問 — 写真の内容を尋ねられます。続けて追加の質問をしても、同じ画像の文脈のまま答えます
  • 🎬 動画生成 — 文章から、あるいは画像を動かして短い動画を作ります(負荷が高いため既定はオフ)
  • 🎛️ モード切替メニュー — 画面下のリッチメニューでチャット / 画像 / 動画を切り替えられます

日本語・英語の切り替えに対応し、Docker イメージとして配布しています。手元の PC で Docker を起動し、トンネルで公開して LINE につなぐ、という手軽さが特徴です。個人・小規模での利用を想定しています。

ソースコードと Docker イメージはこちらで公開しています。

システム構成

実装は ASP.NET(.NET 10 / Minimal API)の単一プロジェクトです。データベースは使わず、会話履歴も生成した画像・動画もすべてメモリ上に持ちます。手軽さを優先した構成で、動かすのは 1 インスタンスだけを前提にしています。

処理の流れはこうです。

LINE → POST /webhook(署名を検証して 200 を返す) → メモリ内キュー → バックグラウンドワーカー → Hugging Face → reply / push で LINE へ返信(画像は /media/{id} で配信)

Webhook を受けたらすぐ 200 を返し、実際の生成はバックグラウンドで進めます。これは、生成に時間がかかっても LINE 側をブロックしないための構成です。順を追うと次のようになります。

  1. LINE から /webhook に届いたリクエストの署名を検証する
  2. 何をするか(チャット / 画像 / 動画など)を判断して、メモリ内キューに積む
  3. すぐ 200 を返す(ここまでが Webhook の担当)
  4. バックグラウンドのワーカーがキューから取り出し、Hugging Face を呼ぶ
  5. できあがった結果を Push API で LINE に送る(画像・動画は /media/{id} で配信)

このうち LINE との連携部分で Line.OpenApi.Bot が登場するのは、主に次の 4 か所です。

  1. Webhook を受け取る(署名の検証とイベントの読み取り)
  2. メッセージを送る(reply / push)
  3. ユーザーが送った画像を取り込む(Content API)
  4. リッチメニューを設定する

順に見ていきます。

1. Webhook を受け取る — WebhookRequestParser

受信側は、DI 登録とパーサ呼び出しの 2 ステップです。まず AddLineWebhook にチャネルシークレットを渡して登録します。

// Program.cs builder.Services.AddLineWebhook(o => o.ChannelSecret = configuration["Line:ChannelSecret"] ?? "");

エンドポイントでは、注入された WebhookRequestParser にリクエストボディと署名ヘッダを渡します。ParseAsync が署名の検証とイベントへの変換をまとめて行ってくれます。

// Program.cs — /webhook app.MapPost("/webhook", async ( HttpRequest request, WebhookRequestParser parser, MessageDispatcher dispatcher) => { using var ms = new MemoryStream(); await request.Body.CopyToAsync(ms); var body = ms.ToArray(); var signature = request.Headers["x-line-signature"].ToString(); var callback = await parser.ParseAsync(body, signature); await dispatcher.DispatchAsync(callback, request.HttpContext.RequestAborted); return Results.Ok(); });

HMAC-SHA256 による署名の検証を自分で書かずに済むのが、このライブラリを使う大きな利点です。パース結果の callbackCallbackRequest)は仕様から生成された型なので、イベントの振り分けは C# のパターンマッチで素直に書けます。

// MessageDispatcher.cs foreach (var ev in callback.Events ?? []) { switch (ev) { case MessageEvent { Message: TextMessageContent text } me: await HandleTextAsync(me, text, cancellationToken); // テキスト break; case MessageEvent { Message: ImageMessageContent img } ime: await HandleImageReceiveAsync(ime, img, cancellationToken); // 画像 break; case PostbackEvent pe: await HandlePostbackAsync(pe, cancellationToken); // ボタン/メニュー操作 break; } }

MessageEventTextMessageContentPostbackEvent はどれも生成された型です。プロパティの補完が効くので、LINE のイベント JSON の形を手元で確認しながら書けます。

2. メッセージを送る — MessagingClient

送信には MessagingClient を使います。登録は AddLineMessaging にアクセストークンを渡すだけです。

builder.Services.AddLineMessaging(o => o.ChannelAccessToken = configuration["Line:ChannelAccessToken"] ?? "");

このボットでは、生成が終わったタイミングで結果を送るため、主に Push API を使います。テキストの送信はこう書けます。

// LineMessenger.cs await client.Api.V2.Bot.Message.Push.PostAsync(new PushMessageRequest { To = userId, Messages = [new TextMessage { Type = "text", Text = text, QuickReply = quickReply }], }, cancellationToken: ct);

client.Api.V2.Bot.Message.Push.PostAsync(...) のように、API のパスをそのままメソッドチェーンでたどって呼び出せます。これが Kiota で生成したクライアントの書き味です。

画像・動画のメッセージは、LINE の仕様上、公開された HTTPS の URL を渡す必要があります。そこでこのボットは、生成した画像・動画を自前で /media/{id} から配信し、その URL を渡しています。

Messages = [new ImageMessage { Type = "image", OriginalContentUrl = originalContentUrl, // 例: https://.../media/abc PreviewImageUrl = previewImageUrl, QuickReply = quickReply, }]

QuickReply を添えると、返信に「🔄 再生成」「✏️ 編集」「💬 質問」といったボタンを付けられます。ボタンが押されると PostbackEvent として届くので、Dispatcher で受けて次の処理につなげます。

3. ユーザーが送った画像を取り込む — MessagingClient.Blob

ユーザーが写真を送ってきたときは、その画像データを LINE の Content API から取得します。この API は通常のメッセージ API とはホストが異なりますが、生成されたクライアントでは MessagingClient.Blob からたどれます。

// LineContentService.cs await using var stream = await client.Blob.V2.Bot.Message[messageId].Content .GetAsync(cancellationToken: ct);

Blob.V2.Bot.Message[messageId].Content.GetAsync(...) で画像データのストリームが得られます。メッセージ送信と同じ書き方で、データ取得用のホストにもアクセスできるのが分かりやすいところです。

取得したストリームは、上限バイト数を決めて読み込みます(大きすぎるアップロードでメモリを使い切らないようにするためです)。読み込んだ画像は、そのあと編集や質問の入力として Hugging Face に渡します。

4. リッチメニューを設定する — RichMenuClient

モード(チャット / 画像 / 動画)を切り替えるボトムメニューは、起動時に一度だけ登録します。ここでは RichMenuClient を使います。

// RichMenuManager.cs var client = RichMenuClient.CreateWithStaticToken(channelAccessToken); var menuId = await client.CreateAsync(BuildRequest(mode), ct); // メニュー定義を作成 await client.SetImageFromFileAsync(menuId, imagePath, ct); // 画像をアップロード await client.Messaging.Api.V2.Bot.Richmenu.Alias.PostAsync( // 別名を割り当て new CreateRichMenuAliasRequest { RichMenuAliasId = aliasId, RichMenuId = menuId }, ct);

メニューの作成・画像のアップロード・別名(alias)の割り当てといった一連の操作が、それぞれメソッド 1 つで呼べます。メニューのタブには richmenuswitch アクションを割り当てておき、タップされると別名を通じて対応するメニューへ切り替わる仕組みです。

チャット部分 — Semantic Kernel × Hugging Face

LINE 連携から離れて、チャットの実装も見ておきます。ここは Semantic Kernel を使っています。

Semantic Kernel は、マイクロソフトが提供する AI 向けの開発ライブラリです。強みのひとつは、AI モデルとのやり取りを共通のインターフェースで扱えることです。モデルの提供元(OpenAI、Azure OpenAI、Hugging Face など)ごとに「コネクタ」が用意されていて、コネクタを差し替えるだけで、アプリ側のコードはほとんど変えずに接続先を切り替えられます。このボットでは Hugging Face コネクタを使っています。

登録は 1 行です。使うモデル・接続先・アクセストークンを渡します。

// Program.cs builder.Services.AddHuggingFaceChatCompletion( model: hf.ChatModel, endpoint: new Uri(hf.ChatEndpoint), // 既定 https://router.huggingface.co apiKey: hf.ApiKey);

こう登録しておくと、IChatCompletionService(チャット応答を得るための共通インターフェース)を DI で注入できるようになります。あとは会話の履歴を組み立てて渡すだけで応答が返ります。

会話履歴は ChatHistory という型で表します。これは、AI とのやり取りを 1 通ずつ順番に並べたものです。先頭に AI の振る舞いを決める「システムプロンプト」を置き、続けてユーザーの発言・AI の応答を交互に積んでいきます。このボットでは、この履歴を LINE のユーザーごとにメモリで持ち、会話の文脈を保っています。

// ChatService.cs public async Task<string> CompleteAsync(string userId, string userText, CancellationToken ct) { // ユーザーごとの履歴に、システムプロンプトと今回の発言を載せた ChatHistory を組み立てる var history = store.Build(userId, systemPrompt, userText); // 共通インターフェース経由で Hugging Face のモデルに問い合わせる var result = await chat.GetChatMessageContentAsync(history, cancellationToken: ct); var answer = result.Content ?? ""; store.Append(userId, userText, answer); // 今回のやり取りを履歴に追記して、次回の文脈にする return answer; }

GetChatMessageContentAsync に履歴を渡すと、モデルの応答が返ってきます。裏側で Hugging Face の API を呼ぶ処理はコネクタが引き受けてくれるので、アプリ側はこの共通インターフェースだけを見ていれば済みます。モデル名は環境変数で差し替えられるので、好みのチャットモデルに切り替えられます。

なお、画像生成・動画生成・画像への質問は、Semantic Kernel を通さず Hugging Face の推論 API を HttpClient で直接呼んでいます。これらは応答が画像や動画のバイナリだったり、モデルの提供元ごとに呼び出し方が異なったりするため、チャットのような共通インターフェースには乗せず、用途ごとに直接実装しています。呼び出しには同じアクセストークンを使います。

まとめ

Line.OpenApi.Bot を実際のアプリに組み込むと、LINE 連携のコードは次の 4 点に集約されました。

  • 受信: AddLineWebhook + WebhookRequestParser.ParseAsync(署名の検証込み)
  • 送信: MessagingClient(reply / push)
  • 画像取得: MessagingClient.Blob(Content API)
  • リッチメニュー: RichMenuClient

署名の検証や API 呼び出しといった定型的な部分をライブラリに任せられるので、アプリ本来のロジック(バックグラウンド処理、モードの管理、メディアの配信、Hugging Face との連携)に集中できました。LINE ボットを .NET で作るときの一例として参考になればうれしいです。

Comments

0
0
0