May 2012

Volume 27 Number 05

働くプログラマ - 相談室 (第 3 回): セラピストに会う

Ted Neward | May 2012

 

Ted Newardこのシリーズの第 1 回 (msdn.microsoft.com/magazine/hh781028、英語) は、クラウドでホストされる、Tropo という音声/SMS システムを使用して、電話による簡単な音声入力システムを作成しました。このシステムはそれほど複雑ではなく、Tropo サーバーでホストされる Tropo Scripting API を使用して、電話を受けたり、メニューを表示したり、応答入力を集めたりする方法を示しました。

第 2 回 (msdn.microsoft.com/magazine/hh852597、英語) は少し話題を変えて Feliza について説明しました。Feliza は、オリジナルの "ELIZA" プログラムの精神を引き継ぐ "チャットボット" で、ユーザーが入力するテキストを受け取り、まるで心理学者のようにそれに応答します。Feliza はそれほど洗練されてはいませんでしたが、"彼女" は要点を伝えるだけでなく、システムをどれほど簡単に拡張できるかを示し、チューリング テストの合格により近づきました。

今回は、これら 2 回のテーマを 1 つにまとめるのが自然の流れのように思えます。Tropo でユーザーからの音声や SMS の入力を集め、それらを Feliza に渡して意味深い、思慮に富んだ答えを導き出し、その答えを Tropo に返信して Tropo からユーザーにその答えを返します。残念ながら、この 2 つが大きくかけ離れているため、それほど簡単なことではありません。Tropo Scripting API を使用しているため、Tropo アプリケーションは Tropo サーバーでホストされます。したがって、Tropo は、ASP.NET アプリケーション、ましてや (前回コラムの段階では、一連の Microsoft .NET Framework DLL にすぎない) Feliza のカスタム バイナリをホストするためにサーバーを開きません。

さいわい、Tropo は、それ自体で音声や SMS を処理できますが、だからと言ってビジネス手腕のある開発者を必要としないわけではないことがわかりました。また、Tropo は、同種の音声/SMS へのアクセスを提供しますが、HTTP/REST のようなチャネル経由です。つまり、Tropo は受け取った音声入力または SMS 入力を、選択した URL に渡してから、応答を受け取り、応答によって Tropo に指示される操作を実行します (図 1 参照)。

Tropo がホストする API の呼び出しフロー
図 1 Tropo がホストする API の呼び出しフロー

これにより、ネットワーク通信の新たな層がシステム全体に追加されます。これには、別のネットワークのラウンドトリップで発生するすべてのフェールオーバーやパフォーマンス上の懸念も伴います。しかし、このことは入力を受け取って、その入力を選択した任意のサーバーに格納できることも意味しますが、セキュリティやデータベース アクセスといった特定のアプリケーションでは重要な懸案事項になる可能性があります。

では、話題を変えて、Tropo が HTTP を使ったこのちょっとしたやり取りをどのように扱うかを見てみましょう。

Tropo とドメイン

まず、"Hello world" スタイルのシンプルなアクセスから始めます。多くのインターネット API と同様、Tropo は HTTP を通信チャネルとして使用し、JSON を送信データのシリアル化形式として使用します。そのため、最も簡単なのは、Tropo 用にシンプルな静的 JSON オブジェクトを作成して、電話番号が呼び出されたときに、電話をかけてきた相手に「Hello」と話しかけるように要求することです。その処理用の JSON は次のようになります。

{
  "tropo": [
    {
      "say": {
        "value":"Hello, Tropo, from my host!"
      }
    }
  ]
}

見た目では、この構造はかなり単純です。JSON オブジェクトは 1 つのフィールドを持つオブジェクトです。このフィールドは、オブジェクトの配列を格納する "tropo" フィールドで、配列の各要素が Tropo に何を行うかを指示します。この例では、"say" が 1 つのコマンドで、Tropo の音声入力エンジンを使用して、「Hello, Tropo, from my host!」と話しかけます。しかし、Tropo はこの JSON オブジェクトを見つける方法を知る必要があります。つまり、新しい Tropo アプリケーションを作成して構成する必要があり、Tropo が検索するサーバー (ファイアウォールの背後にある開発者のノートパソコンではありません) が必要です。2 つ目のポイントは、使い慣れた ASP.NET ホスティング プロバイダーを使用することで簡単に解決できます (今回は WinHost を使用しました。WinHost の基本計画はこれにぴったりです)。1 つ目のポイントでは、Tropo コントロール パネルに戻る必要があります。

今回は、新しいアプリケーションの作成時に、"Tropo Scripting" ではなく "Tropo WebAPI" を選択します (図 2 参照)。次に、特定の JSON ファイルの検索に使用する URL を指定します。ここでは、(この後の手順を予測して) feliza.org を作成し、サイトのルートから削除しました。完全に構成が済むと、図 3 のようになります。

アプリケーション ウィザード
図 2 アプリケーション ウィザード

構成後のアプリケーション
図 3 構成後のアプリケーション

Tropo は Skype およびセッション開始プロトコル (SIP) の番号をフックしましたが、標準の電話番号は手動でフックする必要があります。この作業はこちらで行いました。皆さんが時間を取ってサーバーに電話をかけるのであれば、その番号は 425-247-3096 です。

これだけです、まあ、どちらかと言えば。

ここでの説明とは別に独自の Tropo サービスを作成している場合、電話をかけても何の応答もありません。このような場合は Tropo アプリケーションからログを表示できるように、Tropo はアプリケーション デバッガーを提供します (ページ上部の青色のバーにあります)。ログを見ると、次のようなメッセージが表示されます。"Received non-2XX status code on Tropo-Thread-8d60bf40bc3409843b52f30f929f641c [url=http://www.feliza.org/helloworld.json, code=405]." (Tropo スレッド -8d60bf40bc3409843b52f30f929f641c [url=http://www.feliza.org/helloworld.json, code=405] で -2XX 以外のステータス コードを受け取りました。)

そう、Tropo で HTTP エラーが発生しています。具体的には "405" エラーです。これは、(HTTP の仕様を覚えていない方のために説明すると) 「メソッドがサポートされていません」と言い換えてもかまいません。

正直なところ、Tropo を REST サービスと呼ぶのは間違いです。というのも、Tropo は REST の主要ルールの 1 つに従わないためです。それは、HTTP 動詞がリソースでのアクションを表すというルールです。Tropo は、実際には動詞には頓着せず、単純にすべてをポストします。静的ページはポストできないので、ホストが HTTP POST 要求に (正しく) 応答するのはこのためです。ちょっと待ってください。

さいわい、この問題をかなり簡単に修正するテクノロジを知っています。この時点で、以下のコードに示すように (関連のない多くのコードを省略しています)、ASP.NET アプリケーション (空のアプリケーションが適しています) を作成して、"/helloworld.json" を受け取り、それをシンプルなコントローラーにマップするルーティングを指定します。

namespace TropoApp
{
  public class MvcApplication : System.Web.HttpApplication
  {
    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.MapRoute("HelloWorld", "helloworld.json",
        new { controller = "HelloWorld", action = "Index" });
    }
  }
}

これは最終的に、以下に示すように (関連のない多くのコードを省略しています)、今回の HelloWorld 用に静的 JSON を返します。

namespace TropoApp.Controllers
  {
    public class HelloWorldController : Controller
    {
      public const string helloworldJSON =
        "{ \"tropo\":[{\"say\":{\"value\":\"Hello, Tropo," +
        " from my host!\"}}]}";
      [AcceptVerbs("GET", "POST")]
      public string Index() {
        return helloworldJSON;
      }
    }
  }

これをサーバーに送れば、それで終わりです。

Say, Say, Say …

JSON の "say" が皆さんの記憶を少しくすぐるのは、以前に Tropo Scripting API について説明したときにそれを目にしているためです。"say" はそのとき呼び出したメソッドで、パラメーターの (真の JavaScriptの方式で) 一連の名前と値のペアに渡され、話された出力をカスタマイズする方法を表します。ここで、サーバーから API を呼び出す機能がないため (この JSON ファイルは Tropo クラウドではなくサーバーでホストされています)、代わりに構造的な方式で表す必要があります。したがって、異なる音声でユーザーに話しかけたいのであれば、その内容を "say" オブジェクトのフィールドに指定する必要があります。

{
  "tropo":[
    {
      "say":
      {
        "value":"Hello, Tropo, from my host!",
        "voice":"Grace"
      }
    }
  ]
}

すると、Grace ("オーストラリア英語" として表されます) が Tropo の代理であいさつします。"say" の完全な詳細は、受け渡されるすべての JSON オブジェクトとして、Web サイトの Tropo API のドキュメントで説明されています。

ここで ASP.NET を活用します。コード内で JSON のこれらの文字列を作成するのではなく、オブジェクトから JSON への ASP.NET の暗黙のバインドを使用すれば、このような JSON オブジェクトを簡単に切り離すことができます (図 4 参照)。

図 4 .NET Framework のオブジェクトから JSON へのバインドの使用

public static object helloworld =
  new { tropo =
    new[] {
      new {
        say = new {
          value = "Hello, Tropo, from my host!",
          voice = "Grace"
        }
      }
    }
  };
[AcceptVerbs("POST")]
public JsonResult Index()
{
  return Json(helloworld);
}

JSON の送信では、単一引用符または二重引用符の "いずれでもかまわない" 通常の JavaScript とは異なり、フィールドと値を二重引用符で囲む必要があります。オブジェクトから JSON へのバインドを使用することにより、アプリケーション開発者にとってはそのすべてが完全に無関係になります (注: Tropo では、C# のクライアント ライブラリも提供されます。これは、JSON のほとんどの構成要素を抽象化します。しかし、ここでは "手動による" REST 呼び出しに注目します。というのも、これは、一般に、ASP.NET MVC と同じ種類の処理を行う方法を示すのにも役立つからです。詳細については、bit.ly/bMMJDv (英語) を参照してください)。

音声に耳を傾ける

Feliza のポイントは、型にはまった心理学的なつまらない話を無作為に返すだけではない点です。Feliza は、ユーザーが話した言葉を聞いて、分析してから、型にはまった心理学的なつまらない話を無作為に返します。これを行うには、Tropo が送信し、着信するポストされた JSON オブジェクトを処理できる必要があります。これを行うのは比較的簡単です。1 つはこれを行うための JSON オブジェクトがあるためです。もう 1 つは、ASP.NET MVC にはこれを行うためのすばらしい JSON からオブジェクトへの自動バインドがあるためです。これを行う JSON オブジェクトの "ask" 構造 (bit.ly/yV5ect、英語) は、何かを話してから、一時停止して、入力を待機します。たとえば、ユーザーに質問を投げかけて、さまざまな JSON 結果を出力するには、図 5 のような "ask" 構造が必要です (これについては Tropo のドキュメントに示されています)。

図 5 "ask" の例

public static object helloworld =   new { tropo =
    new[] {
      new {
        say = new {
          value = "Hello, Tropo, from my host!",
          voice = "Grace"
        }
      }
    }
};
[AcceptVerbs("POST")]
public JsonResult Index()
{
  return Json(helloworld);
}
{
  "tropo": [
    {
      "ask": {
        "say": [
          {
            "value": "Please say your account number"  
          }
        ],
        "required": true,
        "timeout": 30,
        "name": "acctNum",
        "choices": {
          "value": "[5 DIGITS]"
        } 
      } 
    },
    {
      "on":{
        "next":"/accountDescribe.json",
        "event":"continue"
      }
    },
    {
      "on":{
        "next":"/accountIncomplete.json",
        "event":"incomplete"
      }
    }
  ] 
}

上記のパラメーターが示すように、この "ask" は 30 秒でタイムアウトしてから、結果 (これは 5 桁にする必要があります) を、ポスト バックされる後続の JSON 応答にある "acctNum" というパラメーターにバインドします。これは "accountDescribe.json" エンドポイントに送信されます。アカウント番号が不完全な場合、Tropo は "accountIncomplete.json" などにポストします。

現時点で残る問題は 1 つだけです。"choices" フィールドの入力の種類を "[5 DIGITS]" から "[ANY]" に変更すると (これは Feliza が望むことです。Feliza はユーザーが話したいことを話すことを望みます)、Tropo は "ask" に関するドキュメントの中で、音声チャネルを介して "[ANY] (あらゆる)" 種類の入力を取得することは許可されていないことを示します。これにより、音声を使用して Feliza に話し掛けることができなくなります。ほぼすべてのアプリケーション シナリオでは、これは問題になりません。通常、音声入力は少ない入力セットに制限する必要があります。さもないと、音声をテキストに変換する際に膨大な精度が求められます。Tropo は音声チャネルを録音して、オフライン分析のために MP3 ファイルとして格納できますが、制限のないテキスト入力のために別の代替策も提供します。

ASP.NET と F# との対話

Tropo を Web サイトに接続しましたが、Feliza はまだ、未接続の F# DLL に依存しています。ここで、着信入力に対して Feliza の F# バイナリの接続を開始できますが、その際、ASP.NET は F# と対話する必要があります。これは比較的簡単な作業ですが、必ずしも明確というわけではありません。また、ASP.NET サイトでは、JSON のカスタム応答を返す必要があるので、作業を中途半端にしておくのではなく、次回のコラムで Feliza を完成させ、システムをさらに拡張するいくつかの方法について説明します。

コーディングを楽しんでください。

Ted Neward は、Neudesic LLC のアーキテクチャ コンサルタントです。これまでに 100 本を超える記事を執筆している Ted は、C# MVP であり、INETA の講演者でもあります。さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox 2010 年、英語) もそのうちの 1 冊です。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。彼がチームの作業に加わることに興味を持ったり、ブログをご覧になったりする場合は、blogs.tedneward.com (英語) にアクセスしてください。

この記事のレビューに協力してくれた技術スタッフの Adam Kalsey に心より感謝いたします。