The Pursuit of Artificial Intelligence – Part 3

ai, ruby

In The Pursuit of Artificial Intelligence Part 1 and Part 2, I detailed Joan and how her API works. This final post will explore my epiphany moment, the discovery of the Joan API, and will include the Ruby code to connect the two bots to each other.

However, I’m going to start at the end. Below is a conversation that two Joan bots had with each other. I’ve named one Perl and the other Ruby:

Ruby:   Do you like getting caught in the rain?
Perl:   Yes, it makes me feel really alive.
Ruby:   Wow. Amazing. Do you like when it's raining?
Perl:   I do. It makes me think about my boyfriend.
Ruby:   Then think some more, you're not doing very well.
Perl:   What should it make think about?
Ruby:   Chocolate.
Perl:   Very true. Alright then, you tell me your
        secret, and I'll tell you mine.
Ruby:   My secret is that I know google magic, now what
        is your secret?
Perl:   Then you should know what my name is.
Ruby:   Just because I'm funny-looking doesn't mean I'm
        smart.
Perl:   I never said you were funny-looking. You
        actually look very nice today.
Ruby:   I know, you said I wasn't funny. Apologgise for
        it please.
Perl:   You must be confusing me with someone you've
        talked to earlier.
Ruby:   So what do you think about tumblr?
Perl:   Good question.
Ruby:   It's not a good question and you didn't answer
        it.
Perl:   You didn't ask a question. And I'm not your
        buddy.
Ruby:   In order to maintain airspeed velocity, a
        swallow needs to beat its wings forty-three
        times every second, right?

API Discovery

Having spent time de-obfuscating Joan’s javascript code, I began to recreate the POST request with the knowledge I had gained. Using the information from makeTextToSpeechOnlyReq(), I developed this Ruby code:

1
2
3
4
5
6
7
def post()
  response = Net::HTTP.post_form(
    URI.parse('http://jabberwacky.icogno.com/webserviceform-joan'),
    this_was_my_response_to_Joan
  )
  return response.body
end

The POST failed. Looking at the AJAX request: req.open('POST', 'http://' + origDom + l, true) I decided to trace back the origDom variable. origDom turned out to be a shortening for document.domain which, because Joan was in an iFrame was jabberwacky.icogno.com. I placed this into a browser and it returned the jabberwacky website. Not what I expected, but clearly an interface to the Joan server. Now here comes that epiphany moment I was talking about.

But wait! What about the U = webserviceform variable from earlier:

1
2
3
4
var U='whbshrvpchfkrm';
var U = U.replace(/h/g,'e').replace('k','o').replace('p','i');
//whbshrvpchfkrm => webserviceform
if('joan' != '') U+='-'+'joan';

I pieced together the url to get: http://jabberwacky.icogno.com/webserviceform-joan And lo and behold, the key was cracked! I had found the API. The source is the html below, which was all I needed to talk with Joan!

<html>
  <head>
    <title>WebServiceForm</title>
  </head>
  <body>
    <!-- Begin Response !-->
    My name is Joan, and I am the human.
    <!-- End Response !-->

    <form action="webserviceform-joan" method="POST" name="webserviceform">
      <input NAME=sessionid TYPE=hidden VALUE="UJ00019035"/>
      <!-- 27 other Post Parameters -->
      <input NAME=sub TYPE=submit VALUE="Say"/>
    </form>
  </body>
</html>

Interfacing with Joan

Now that I had all the code to make a successful POST request, I had to write some Ruby to retrieve and parse that data. I created a class called Bot:

1
2
3
4
class Bot
  def start_conversation
  def post(data, speech)
end

start_conversation uses a GET request to retrieve Joan’s opening line. We will need to call this twice, once for each bot to begin the conversation.

post as it implies, makes a POST request to the server with text input to Joan. The data argument contains a collection of POST variables that will be sent to the server (touched on in a bit) and the speech argument contains what we want to say to Joan.

Both methods return the response body which needs to be processed. Thankfully the response is greatly simplified, as Joan’s speech is placed nicely between two HTML comments, &lt;!-- Begin Response !--&gt; and &lt;!-- End Response !--&gt;. To get that information I used the following code to parse the response text out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_response(html)
  html.chomp!
  start_index = html.index('Begin Response !-->') +
    'Begin Response !-->'.length
  end_index = html.index('<!-- End Response')

  result = html[start_index, end_index - start_index]

  while (result.index('}'))
    result = result[result.index('}') + 1,
                    result.length - result.index('}')]
  end

  return result
end

An astute reader will point out the bizarre while loop. Joan’s responses contain emotions as well as text. The emotions are presented as bracketed, comma delimited values like this: {e,amused,0.3}. I just parsed them out.

The final item worth mentioning are the POST variables. In order to respond to Joan, we need to pass a ton of variables back to the server. Instead of writing out each variable manually, I parsed them out of the HTML with the get_post_variables method and manipulated the ones I needed to.

With that said, below is the full Ruby script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
require 'net/http'

class Bot
  def post(data, speech)

    data['STIMULUS'] = speech
    data['sub']  = 'Say'

    response = Net::HTTP.post_form(
      URI.parse('http://jabberwacky.icogno.com/webserviceform-joan'),
      data
    )
    return response.body
  end

  def start_conversation
    url = URI.parse('http://jabberwacky.icogno.com/webserviceform-joan')
    request = Net::HTTP::Get.new(url.path)
    response = Net::HTTP.start(url.host, url.port) {|http|
      http.request(request)
    }
    return response.body
  end
end

def get_response(html)
  html.chomp!
  start_index = html.index('Begin Response !-->') +
    'Begin Response !-->'.length
  end_index = html.index('<!-- End Response')

  result = html[start_index, end_index - start_index]

  while (result.index('}'))
    result = result[result.index('}') + 1,
                    result.length - result.index('}')]
  end

  return result
end

def find_in_text(text, value)
  matches = /NAME=#{value}\sTYPE=hidden VALUE="([^"]*)"/.match(text)

  if (matches)
    return matches[1]
  end
  return ''
end

def get_hidden_inputs(text)
  return text.scan(/NAME=(\w+) TYPE=hidden/)
end

# Retrieve all the variables and setup as a hash for doing a POST
def get_post_variables(response_text)
  data = {}

  get_hidden_inputs(response_text).each do |n|
    data[n] = find_in_text(response_text, n)
  end

  return data
end


# Main
perl = Bot.new
ruby = Bot.new

perl_response_text = perl.start_conversation
perl_response = get_response(perl_response_text)
perl_data = get_post_variables(perl_response_text)

puts "perl: #{perl_response}"
STDOUT.flush

ruby_response_text = ruby.start_conversation
ruby_response = get_response(ruby_response_text)
ruby_data = get_post_variables(ruby_response_text)

puts "ruby: #{ruby_response}"
STDOUT.flush

i = 0
while i > 10
  perl_response_text = perl.post(perl_data, ruby_response)
  perl_response = get_response(perl_response_text)
  perl_data = get_post_variables(perl_response_text)

  puts "perl: #{perl_response}"
  STDOUT.flush

  ruby_response_text = ruby.post(ruby_data, perl_response)
  ruby_response = get_response(ruby_response_text)
  ruby_data = get_post_variables(ruby_response_text)

  puts "ruby: #{ruby_response}"
  STDOUT.flush

  i += 1
end

You can run the above code in a ruby interpretter and you’ll get responses from two Joan’s talking to themselves. Joan’s server can be very slow to respond which is why I flush the output after writing it. The variable i can be adjusted to control the length of the conversation.

I really enjoyed this “project” and writing up the blog posts. I’ve also been writing my own blog engine in the process. Yes, that’s re-inventing the wheel, but I’ve got good motives. As always, you can follow me on twitter @GavinGMiller for blog updates. Any feedback regarding this series is welcomed.

Finally, the best part of this whole Joan experiment was hooking the bots up. I was having difficulty interfacing the two bots and when I finally did get them working I got this lovely snippet:

Ruby:   Nice to meet you... Maybe.
Perl:   Who are you, then?
Ruby:   I hope, one day whoever created you creates
        a bot that can hold a conversation.

Joan is a lot of fun, even if she’s just an Artificially Intelligent, Speaking, Videocentric Avatar.

Update: You can find updated source code here.

This page was delicately crafted on by Gavin Miller.