Google HomeにRSSを読み上げさせよう:完成編:特集:Google Homeプログラミングを始めよう(2/3 ページ)
Dialogflowの会話機能とAzure Functionsを組み合わせて、Azure Table Storageに保存したRSSフィードのデータを実際に読み上げさせてみよう。
Functionsアプリのコード
実際に記述したFunctionsアプリのコードを以下に示す(ここではVisual Studioは使わずに、AzureでFunctionsアプリを作成して、そこに以下のコードを記述している)。
#r "Microsoft.WindowsAzure.Storage"
#r "Newtonsoft.Json"
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Net;
using Newtonsoft.Json;
static Dictionary<string, string> partitionKey = new Dictionary<string, string>
{
  { "insider.net", "insider.net rss feed item" },
  { "windows server insider", "windows server insider rss feed item" }
};
static Dictionary<string, int> index = new Dictionary<string, int>
{
  { "first",   0 }, { "second", 1 },
  { "third",   2 }, { "fourth", 3 },
  { "fifth",   4 }, { "sixth",  5 },
  { "seventh", 6 }, { "eighth", 7 },
  { "ninth",   8 }, { "tenth",  9 }
};
public static async Task<HttpResponseMessage> Run(
  HttpRequestMessage req,
  IQueryable<TableItem> inputTable,
  TraceWriter log)
{
  log.Info("C# HTTP trigger function processed a request.");
  string data = await req.Content.ReadAsStringAsync();
  var jsondata = JsonConvert.DeserializeObject<ReqBody>(data);
  string forum = jsondata.result.parameters["forum"];
  string pkey = partitionKey[forum];
  int idx = index[jsondata.result.parameters["index"]];
  string text = $"{forum}の{idx+1}番目の記事ですね。";
  log.Info(text);
  var items = inputTable.ToList().
    Where(x => x.PartitionKey == pkey).
    OrderByDescending(x => x.publishDate);
  var item = items.ElementAt(idx);
  text += $"タイトルは『{item.title}』です。" + 
    $"フィードの内容は次の通りです。『{item.summary}』";
  var result = req.CreateResponse(HttpStatusCode.OK, new
  {
    speech = text,
    displayText = text
  });
  result.Headers.Add("ContentType", "application/json");
  return result;
}
public class TableItem : TableEntity
{
  public string id { get; set; }
  public string title { get; set; }
  public string primaryLink { get; set; }
  public string updateOn { get; set; }
  public string publishDate { get; set; }
  public string summary { get; set; }
}
public class ReqBody
{
  public Result result { get; set; }
}
public class Result
{
  public string resolvedQuery { get; set; }
  public Dictionary<string, string> parameters { get; set; }
}
public class Response
{
  public string speech { get; set; }
  public string displayText { get; set; }
  public string source { get; set; }
}
最初にある2つのDictionaryクラスのインスタンスは、Run関数のreqパラメーターに含まれているフォーラムおよびインデックスを示す文字列と、実際のフォーラムおよびインデックスとをマッピングさせたものだ。フォーラムを示すデータとしては「insider.net」か「windows server insider」が渡される。インデックスを示すデータとしては「first」「second」……「tenth」が渡される。そこで、これらの値とパーティションキーとなる文字列(フォーラムの場合)、あるいは実際のインデックスを示す整数値(インデックスの場合)とをマッピングしているということだ。
reqパラメーター
第1パラメーターの「req」にはDialogflowがWeb hookを介して送信してきたリクエストが格納されている。このデータとDialogflowに返信するレスポンスをモデル化したのが、上掲のリストの最後で定義している3つのクラスだ。
public class ReqBody
{
  public Result result { get; set; }
}
public class Result
{
  public string resolvedQuery { get; set; }
  public Dictionary<string, string> parameters { get; set; }
}
public class Response
{
  public string speech { get; set; }
  public string displayText { get; set; }
  public string source { get; set; }
}
実際にはリクエストボディーにはもっと大量のデータが含まれているが、ここで必要なのはフォーラムとインデックスを示すデータだけなので、ここでは簡略化している。送られてきたデータはJSON形式なので、Run関数ではこれをデシリアライズして、フォーラムとインデックスを表すデータを取り出している。
string data = await req.Content.ReadAsStringAsync();
var jsondata = JsonConvert.DeserializeObject<ReqBody>(data);
string forum = jsondata.result.parameters["forum"];
string pkey = partitionKey[forum];
int idx = index[jsondata.result.parameters["index"]];
これら2つのデータは(既に述べた通り)Table Storageに格納されているデータから該当するフィードを取得するために使用する。そのTable Storageに格納されているデータを参照しているのが第2パラメーターのinputTableだ。
inputTableパラメーター
Run関数の第2パラメーターである「inputTable」は「バインディング」と呼ばれる機構を利用して、Table Storageに保存されているデータが渡される。バインディングについては前回の記事を参考にしてほしい。ここでは、inputTableパラメーターに2つのフォーラムが保存されていることと、それらがTableItemクラスで定義されているメンバに相当するフィールドを持っていることが重要だ(パーティションキーを表すメンバであるPartitionKeyプロパティはTableItemクラスの基底クラスであるTableEntityクラスで定義されている)。
public class TableItem : TableEntity
{
  public string id { get; set; }
  public string title { get; set; }
  public string primaryLink { get; set; }
  public string updateOn { get; set; }
  public string publishDate { get; set; }
  public string summary { get; set; }
}
Run関数では上で見たようにreqパラメーターからフォーラムとインデックスを示すデータを取り出してから、それらを利用して、inputTableパラメーターに保存されているデータを取り出す。
ただし、Table Storageに対して呼び出せるLINQクエリには制限があるので、ここでは一度全てのデータをリスト化している。その後、Where句でフォーラムを限定し(パーティションキーの値を使用)、それをOrderByDescending句で最新の日付順に並べ替えている。最後にインデックスを指定して、フィードアイテムを得る。
var items = inputTable.ToList().
  Where(x => x.PartitionKey == pkey).
  OrderByDescending(x => x.publishDate);
var item = items.ElementAt(idx);
返送するメッセージの組み立て
フィードを得たら、後はそれを基に返送するメッセージを組み立てるだけだ。
text += $"タイトルは『{item.title}』です。" + 
  $"フィードの内容は次の通りです。『{item.summary}』";
var result = req.CreateResponse(HttpStatusCode.OK, new
{
  speech = text,
  displayText = text
});
result.Headers.Add("ContentType", "application/json");
return result;
コードの説明は以上だ。Dialogflow側の設定とコードの記述ができたので、まずはこれを実際に試してみた。それが以下の動画だ。
Google Home miniにRSSを読み上げさせてみた
筆者の滑舌がよくないため、「3番目」をGoogle Home miniが聞き取れずに言い直すことになったが、RSSの読み上げには成功したといってよいだろう。
最後に「Windowsは?」「2番目」などと、フォーラムとインデックスのうちのどちらかの情報を抜かして問い掛けたときの対処について考えてみよう。
Copyright© Digital Advantage Corp. All Rights Reserved.