Работающий программист

Поговори со мной, часть 3: встречайте психотерапевт

Тэд Ньюард

 

Ted NewardВ первой части этой серии (msdn.microsoft.com/magazine/hh781028) я создал простую систему речевого ввода по телефону, используя систему голосовой и SMS-связи — облачный сервис Tropo. Она была не слишком сложна, но демонстрировала, как использовать скриптовый API, размещенный на серверах Tropo, для приема телефонных звонков, предоставления меню, получения команды, выбранной из меню, и т. д.

Во второй части (msdn.microsoft.com/magazine/hh852597) я сделал шаг в сторону и рассказал о Feliza — «чат-боте» в духе оригинальной программы ELIZA, принимающей текстовый ввод от пользователя и реагирующей на него примерно тем, что можно услышать, лежа на кушетке на приеме у психолога. И вновь «она» не была такой уж изощренной, но Feliza ухватывала главное и, что важнее, демонстрировала, насколько легко эту систему можно было бы расширить, чтобы гораздо ближе подобраться к прохождению теста Тьюринга.

Так что вполне естественно сложить вместе эти две части: пусть Tropo принимает голосовой или SMS-ввод от пользователя и передает его в Feliza, а она вычисляет некий глубокомысленный ответ, передает его в Tropo и дает ему возможность переслать его пользователю. Увы, существенная разобщенность не позволяет добиться этого так легко, как кажется поначалу. Поскольку мы используем Tropo Scripting API, наше Tropo-приложение размещается на их серверах, а Tropo не «распахивает двери» своих серверов для хостинга приложений ASP.NET и тем более двоичных файлов Feliza (которые, как было ясно из прошлой статьи, являются просто набором DLL, написанных под Microsoft .NET Framework).

К счастью, в Tropo осознали, что сама по себе возможность голосовой и SMS-связи не представляет для бизнес-разработчиков особого интереса, и теперь предлагает тот же доступ, но по каналам, подобным HTTP/REST. Иначе говоря, Tropo будет принимать входящий голосовой или SMS-ввод, передавать его по заданному вами URL, потом захватывать ответ и… ну, делать то, что указывает ответ (рис. 1).

Tropo-Hosted API Call Flow
Рис. 1. Схема вызовов в Tropo API

Your Web Server Ваш веб-сервер

Верно, это добавляет в систему еще один уровень сетевого взаимодействия со всеми проблемами преодоления сетевых сбоев и производительности, вносимых дополнительным обменом данными через сеть. Но это же означает и то, что мы можем захватывать ввод и хранить его на любых выбранных нами серверах, что крайне важно для определенных приложений в связи с вопросами безопасности, доступа к базам данных и др.

Так что позвольте мне сделать еще один шаг в сторону и пояснить, как Tropo выполняет свой небольшой трюк с HTTP.

«Hello, Tropo»… с моего хоста

Начнем с простых вещей в стиле «Hello world». Tropo, как и многие Internet API, использует HTTP в качестве коммуникационного канала, а JSON — в качестве формата сериализации данных, посылаемых по этому каналу. Поэтому простейшее, что можно сделать, — создать статический JSON-объект для Tropo, чтобы тот выдавал запрос при наборе определенного телефонного номера и произносил «Hello» звонящему. JSON для выполнения этой задачи выглядит так:

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

На первый взгляд структура довольно проста. JSON-объект содержит всего одно поле (tropo), где хранится массив объектов, каждый из которых сообщает Tropo, что делать; в данном случае это единственная команда say, которая использует Tropo-механизм преобразования текста в речь для произнесения фразы «Hello, Tropo, from my host!». Но Tropo нужно знать, как найти этот JSON-объект, а значит, мы должны создать и сконфигурировать новое Tropo-приложение, и еще нам требуется сервер, который Tropo сможет найти (т. е. им не может быть лэптоп разработчика, скрытый за брандмауэром). Вторую задачу легко решить обращением к своему любимому провайдеру хостинга ASP.NET-приложений (я задействовал WinHost — его план Basic идеально подходит для данных целей). А первая задача требует возврата в панель управления Tropo.

На этот раз, создавая новое приложение, выберите Tropo WebAPI вместо Tropo Scripting (рис. 2) и присвойте ему URL, по которому находится ваш JSON-файл; в моем случае я создал feliza.org (предвидя последующие этапы) и оставил его в корне сайта. Полностью сконфигурированное приложение выглядит, как показано на рис. 3.

The Application Wizard
Рис. 2. Application Wizard

The Configured Application
Рис. 3.Сконфигурированное приложение

Хотя Tropo с радостью подключил за нас Skype и SIP-номер (Session Initiation Protocol), нам все равно нужно самостоятельно подключать стандартный телефонный номер. Я сделал это, пока вы смотрели в другую сторону, и этот номер — 425-247-3096 (на случай, если вам захочется позвонить).

И это все! Вроде бы.

Если вы создавали собственный сервис Tropo, следуя за мной, то не получите никакого ответа от набранного номера. На такие случаи Tropo предоставляет Application Debugger, который позволяет просматривать журналы вашего Tropo-приложения. (Ищите его на синей полоске вверху панели управления Tropo.) Открыв журнал, мы видим нечто вроде: «Received non-2XX status code on Tropo-Thread-8d60bf40bc3409843b52f30f929f641c [url=http://www.feliza.org/helloworld.json, code=405]».

Ага, Tropo получил HTTP-ошибку. Точнее, ошибку 405, которая (для тех, кто еще не выучил наизусть спецификацию HTTP) переводится как «Method not supported» («метод не поддерживается»).

Честно говоря, называть Tropo REST-сервисом неправильно, потому что он не следует одному из важнейших правил REST: HTTP-команда описывает операцию над ресурсом. Tropo на самом деле не волнует описание — он просто загоняет все в рамки POST. И вот почему хост корректно реагирует на HTTP-запрос POST: просто потому, что статическая страница не подлежит передаче по POST.

К счастью, нам известна технология, которая позволяет довольно легко устранить эту проблему. На этом этапе мы создаем приложение ASP.NET (подойдет шаблон Empty) и назначаем ему маршрут, который принимает «/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" });
    }
  }
}

…что в свою очередь просто возвращает статический JSON для нашего HelloWorld, как показано здесь (и опять большая часть нерелевантного кода опущена):

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» в JSON о чем-то вам напоминает, то это потому, что мы уже сталкивались с ним при первом исследовании Tropo Scripting API. Тогда это был метод, который мы вызывали, передавая ему серию пар «имя-значение» (в настоящем стиле JavaScript), описывающих, как нужно настроить речевой вывод. В данном случае, поскольку у нас нет возможности вызывать этот API на сервере (вспомните, что JSON-файл размещен на моем сервере, а не в облаке Tropo), мы должны его в структурной форме. Поэтому, если вы хотите, чтобы пользователь слышал некий другой голос, нужно указать его как поле в объекте «say»:

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

Теперь от имени Tropo нас будет приветствовать голос Grace (он описывается как австралийский английский). Все подробности о «say» вы найдете в документации Tropo API на их веб-сайте; там же есть описание всех JSON-объектов, передаваемых в обе стороны.

И вот где для ASP.NET наступает ее звездный час: вместо того чтобы пытаться формировать эти строки JSON в коде, мы можем задействовать неявные привязки Object-to-JSON в ASP.NET (рис. 4).

Рис. 4. Использование привязок Object-to-JSON в .NET Framework

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, где допускаются как одинарные, так и двойные кавычки. Использование привязок Object-to-JSON делает все это совершенно ненужным разработчику приложения. Здорово. (Заметьте: Tropo также предоставляет клиентскую библиотеку для C#, которая абстрагирует большую часть деталей, связанных с JSON; детали см. по ссылке bit.ly/bMMJDv.)

Слушай звук…

Однако смысл Feliza не только в том, чтобы выдавать случайным образом выбранный шаблонный вздор, который несут психологи. Этой программе нужно слушать пользовательский ввод, произносимый голосом, анализировать его, а уж только потом изображать из себя психолога. Для этого мы должны иметь возможность обрабатывать JSON-объект, который отправляет нам Tropo через POST. Делается это сравнительно несложно, учитывая, что это JSON-объект (см. также описание структуры «ask» по ссылке bit.ly/yV5ect) и что в ASP.NET MVC есть некоторые удобные автоматические привязки JSON-to-Object для выполнения этой задачи. Поэтому, чтобы, например, отправить вопрос пользователю и получить другой JSON-результат, нам понадобилась бы «ask», как на рис. 5 (она есть в документации Tropo).

Рис. 5. Пример «ask»

{
  "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 секунд, результаты (это должны быть пять цифр) потом будут связаны в параметр с именем acctNum в последующем JSON-ответе, который передается по POST в конечную точку accountDescribe.json. Если номер учетной записи (account number) не полный, Tropo передаст по POST в accountIncomplete.json и т. д.

В системе в том виде, в каком она написана сейчас, есть лишь одна проблема: если мы изменяем тип ввода (в поле choices) с «[5 DIGITS]» на «[ANY]» (что и нужно Feliza в итоге, ведь пользователи могут говорить ей что угодно), то Tropo сообщает нам в документации на «ask», что попытки захватывать ввод типа «[ANY]» по голосовому каналу запрещены. И это ставит крест на попытках голосового общения с Feliza. Почти во всех остальных сценариях применения это не составило бы проблемы. Обычно голосовой ввод ограничивается малым набором речевых команд, а иначе нам потребовалась бы колоссальная точность в преобразовании речи в текст. Tropo может вести запись по голосовому каналу и хранить ее в виде MP3-файла для автономного анализа, но Tropo предлагает нам другую альтернативу для ничем не ограниченного текстового ввода.

ASP.NET общается с F#

Мы подключили Tropo к нашему веб-сайту, но Feliza по-прежнему скрывается в своих F# DLL. Теперь можно приступить к связыванию двоичных F#-файлов Feliza с поступающим вводом, но это потребует взаимодействия ASP.NET с F# — это упражнение сравнительно простое, но не всегда очевидное. Сайт ASP.NET также должен генерировать JSON-ответы в специальном формате, поэтому мы не станем бросать работу на полпути, а закончим Feliza в следующий раз — и рассмотрим некоторые способы, потенциально позволяющие еще больше расширить эту систему.

Удачи в кодировании!


Тэд Ньюард (Ted Neward) — консультант по архитектуре в компании Neudesic LLC. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области C#. С ним можно связаться по адресу ted@tedneward.com, если вы заинтересованы в сотрудничестве с его группой. Также читайте его блог blogs.tedneward.com.

Выражаю благодарность за рецензирование статьи эксперту Эдаму Келси (Adam Kalsey).