Class: MegaHAL

Inherits:
Object
  • Object
show all
Defined in:
lib/megahal/keyword.rb,
lib/megahal/megahal.rb

Constant Summary collapse

GREETING =
["G'DAY", "GREETINGS", "HELLO", "HULLO", "HI", "HOWDY", "WELCOME"]
ANTONYMS =
[
  ["DISLIKE", "LIKE"],
  ["HATE", "LOVE"],
  ["I", "YOU"],
  ["I'D", "YOU'D"],
  ["I'LL", "YOU'LL"],
  ["I'M", "YOU'RE"],
  ["I'VE", "YOU'VE"],
  ["LIKE", "DISLIKE"],
  ["LOVE", "HATE"],
  ["ME", "YOU"],
  ["MINE", "YOURS"],
  ["MY", "YOUR"],
  ["MYSELF", "YOURSELF"],
  ["NO", "YES"],
  ["WHY", "BECAUSE"],
  ["YES", "NO"],
  ["YOU", "I"],
  ["YOU", "ME"],
  ["YOU'D", "I'D"],
  ["YOU'LL", "I'LL"],
  ["YOU'RE", "I'M"],
  ["YOU'VE", "I'VE"],
  ["YOUR", "MY"],
  ["YOURS", "MINE"],
  ["YOURSELF", "MYSELF"],
  ["HOLMES", "WATSON"],
  ["FRIEND", "ENEMY"],
  ["ALIVE", "DEAD"],
  ["LIFE", "DEATH"],
  ["QUESTION", "ANSWER"],
  ["BLACK", "WHITE"],
  ["COLD", "HOT"],
  ["HAPPY", "SAD"],
  ["FALSE", "TRUE"],
  ["HEAVEN", "HELL"],
  ["GOD", "DEVIL"],
  ["NOISY", "QUIET"],
  ["WAR", "PEACE"],
  ["SORRY", "APOLOGY"]
]
SWAP =
AUXILIARY =
<<-EOS.each_line.to_a.map(&:strip)
  DISLIKE
  HE
  HER
  HERS
  HIM
  HIS
  I
  I'D
  I'LL
  I'M
  I'VE
  LIKE
  ME
  MINE
  MY
  MYSELF
  ONE
  SHE
  THREE
  TWO
  YOU
  YOU'D
  YOU'LL
  YOU'RE
  YOU'VE
  YOUR
  YOURS
  YOURSELF
EOS
BANNED =
<<-EOS.each_line.to_a.map(&:strip)
  A
  ABILITY
  ABLE
  ABOUT
  ABSOLUTE
  ABSOLUTELY
  ACROSS
  ACTUAL
  ACTUALLY
  AFTER
  AFTERNOON
  AGAIN
  AGAINST
  AGO
  AGREE
  ALL
  ALMOST
  ALONG
  ALREADY
  ALTHOUGH
  ALWAYS
  AM
  AN
  AND
  ANOTHER
  ANY
  ANYHOW
  ANYTHING
  ANYWAY
  ARE
  AREN'T
  AROUND
  AS
  AT
  AWAY
  BACK
  BAD
  BE
  BEEN
  BEFORE
  BEHIND
  BEING
  BELIEVE
  BELONG
  BEST
  BETTER
  BETWEEN
  BIG
  BIGGER
  BIGGEST
  BIT
  BOTH
  BUDDY
  BUT
  BY
  CALL
  CALLED
  CALLING
  CAME
  CAN
  CAN'T
  CANNOT
  CARE
  CARING
  CASE
  CATCH
  CAUGHT
  CERTAIN
  CERTAINLY
  CHANGE
  CLOSE
  CLOSER
  COME
  COMING
  COMMON
  CONSTANT
  CONSTANTLY
  COULD
  CURRENT
  DAY
  DAYS
  DERIVED
  DESCRIBE
  DESCRIBES
  DETERMINE
  DETERMINES
  DID
  DIDN'T
  DO
  DOES
  DOESN'T
  DOING
  DON'T
  DONE
  DOUBT
  DOWN
  EACH
  EARLIER
  EARLY
  ELSE
  ENJOY
  ESPECIALLY
  EVEN
  EVER
  EVERY
  EVERYBODY
  EVERYONE
  EVERYTHING
  FACT
  FAIR
  FAIRLY
  FAR
  FELLOW
  FEW
  FIND
  FINE
  FOR
  FORM
  FOUND
  FROM
  FULL
  FURTHER
  GAVE
  GET
  GETTING
  GIVE
  GIVEN
  GIVING
  GO
  GOING
  GONE
  GOOD
  GOT
  GOTTEN
  GREAT
  HAD
  HAS
  HASN'T
  HAVE
  HAVEN'T
  HAVING
  HELD
  HERE
  HIGH
  HOLD
  HOLDING
  HOW
  IF
  IN
  INDEED
  INSIDE
  INSTEAD
  INTO
  IS
  ISN'T
  IT
  IT'S
  ITS
  JUST
  KEEP
  KIND
  KNEW
  KNOW
  KNOWN
  LARGE
  LARGER
  LARGETS
  LAST
  LATE
  LATER
  LEAST
  LESS
  LET
  LET'S
  LEVEL
  LIKES
  LITTLE
  LONG
  LONGER
  LOOK
  LOOKED
  LOOKING
  LOOKS
  LOW
  MADE
  MAKE
  MAKING
  MANY
  MATE
  MAY
  MAYBE
  MEAN
  MEET
  MENTION
  MERE
  MIGHT
  MOMENT
  MORE
  MORNING
  MOST
  MOVE
  MUCH
  MUST
  NEAR
  NEARER
  NEVER
  NEXT
  NICE
  NOBODY
  NONE
  NOON
  NOONE
  NOT
  NOTE
  NOTHING
  NOW
  OBVIOUS
  OF
  OFF
  ON
  ONCE
  ONLY
  ONTO
  OPINION
  OR
  OTHER
  OUR
  OUT
  OVER
  OWN
  PART
  PARTICULAR
  PARTICULARLY
  PERHAPS
  PERSON
  PIECE
  PLACE
  PLEASANT
  PLEASE
  POPULAR
  PREFER
  PRETTY
  PUT
  QUITE
  REAL
  REALLY
  RECEIVE
  RECEIVED
  RECENT
  RECENTLY
  RELATED
  RESULT
  RESULTING
  RESULTS
  SAID
  SAME
  SAW
  SAY
  SAYING
  SEE
  SEEM
  SEEMED
  SEEMS
  SEEN
  SELDOM
  SENSE
  SET
  SEVERAL
  SHALL
  SHORT
  SHORTER
  SHOULD
  SHOW
  SHOWS
  SIMPLE
  SIMPLY
  SMALL
  SO
  SOME
  SOMEONE
  SOMETHING
  SOMETIME
  SOMETIMES
  SOMEWHERE
  SORT
  SORTS
  SPEND
  SPENT
  STILL
  STUFF
  SUCH
  SUGGEST
  SUGGESTION
  SUPPOSE
  SURE
  SURELY
  SURROUND
  SURROUNDS
  TAKE
  TAKEN
  TAKING
  TELL
  THAN
  THANK
  THANKS
  THAT
  THAT'S
  THATS
  THE
  THEIR
  THEM
  THEN
  THERE
  THEREFORE
  THESE
  THEY
  THING
  THINGS
  THIS
  THOSE
  THOUGH
  THOUGHTS
  THOUROUGHLY
  THROUGH
  TINY
  TO
  TODAY
  TOGETHER
  TOLD
  TOMORROW
  TOO
  TOTAL
  TOTALLY
  TOUCH
  TRY
  TWICE
  UNDER
  UNDERSTAND
  UNDERSTOOD
  UNTIL
  UP
  US
  USED
  USING
  USUALLY
  VARIOUS
  VERY
  WANT
  WANTED
  WANTS
  WAS
  WATCH
  WAY
  WAYS
  WE
  WE'RE
  WELL
  WENT
  WERE
  WHAT
  WHAT'S
  WHATEVER
  WHATS
  WHEN
  WHERE
  WHERE'S
  WHICH
  WHILE
  WHILST
  WHO
  WHO'S
  WHOM
  WILL
  WISH
  WITH
  WITHIN
  WONDER
  WONDERFUL
  WORSE
  WORST
  WOULD
  WRONG
  YESTERDAY
  YET
EOS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeMegaHAL

Create a new MegaHAL instance, loading the :default personality.



11
12
13
14
15
16
17
18
19
# File 'lib/megahal/megahal.rb', line 11

def initialize
  @learning = true
  @seed = Sooth::Predictor.new(0)
  @fore = Sooth::Predictor.new(0)
  @back = Sooth::Predictor.new(0)
  @case = Sooth::Predictor.new(0)
  @punc = Sooth::Predictor.new(0)
  become(:default)
end

Instance Attribute Details

#learningObject

Returns the value of attribute learning.



8
9
10
# File 'lib/megahal/megahal.rb', line 8

def learning
  @learning
end

Class Method Details

.add_personality(name, data) ⇒ Object



38
39
40
41
42
# File 'lib/megahal/megahal.rb', line 38

def self.add_personality(name, data)
  @@personalities ||= {}
  @@personalities[name.to_sym] = data.each_line.to_a
  nil
end

.extract(words) ⇒ Object

This takes an array of capitalised (normalised) words, and returns an array of keywords (which simply remove banned words, and switch some words with their antonyms). This exists purely to emulate the original MegaHAL. It would be better if keywords were learned by observing question-answer pairs.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/megahal/keyword.rb', line 6

def self.extract(words)
  return GREETING if words.nil?
  words
    .map do |word|
      if word =~ /^[0-9]/
        nil
      elsif BANNED.include?(word)
        nil
      elsif SWAP.key?(word)
         SWAP[word]
      else
        word
      end
    end
    .compact
    .uniq
end

.listArray

Returns an array of MegaHAL personalities.

Returns:

  • (Array)

    A list of symbols representing the available personalities.



47
48
49
50
# File 'lib/megahal/megahal.rb', line 47

def self.list
  @@personalities ||= {}
  @@personalities.keys
end

Instance Method Details

#become(name = :default, bar = nil) ⇒ Object

Loads the specified personality. Will raise an exception if the personality parameter isn’t one of those returned by #list. Note that this will clear MegaHAL’s brain first.

Parameters:

  • name (Symbol) (defaults to: :default)

    The personality to be loaded.

  • bar (ProgressBar) (defaults to: nil)

    An optional progress bar instance.

Raises:

  • (ArgumentError)


58
59
60
61
62
63
# File 'lib/megahal/megahal.rb', line 58

def become(name=:default, bar = nil)
  raise ArgumentError, "no such personality" unless @@personalities.key?(name)
  clear
  bar.total = @@personalities[name].length unless bar.nil?
  _train(@@personalities[name], bar)
end

#clearObject

Wipe MegaHAL’s brain. Note that this wipes the personality too, allowing you to begin from a truly blank slate.



27
28
29
30
31
32
33
34
35
36
# File 'lib/megahal/megahal.rb', line 27

def clear
  @seed.clear    
  @fore.clear    
  @back.clear    
  @case.clear    
  @punc.clear    
  @dictionary = { "<error>" => 0, "<fence>" => 1, "<blank>" => 2 }
  @brain = {}
  nil
end

#inspectObject



21
22
23
# File 'lib/megahal/megahal.rb', line 21

def inspect
  to_s
end

#load(filename, bar = nil) ⇒ Object

Load a brain that has previously been saved.

Parameters:

  • filename (String)

    The brain file to be loaded.

  • bar (ProgressBar) (defaults to: nil)

    An optional progress bar instance.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/megahal/megahal.rb', line 138

def load(filename, bar = nil)
  bar.total = 6 unless bar.nil?
  Zip::File.open(filename) do |zipfile|
    data = Marshal::load(zipfile.find_entry("dictionary").get_input_stream.read)
    raise "bad version" unless data[:version] == "MH11"
    @learning = data[:learning]
    @brain = data[:brain]
    @dictionary = data[:dictionary]
    bar.increment unless bar.nil?
    [:seed, :fore, :back, :case, :punc].each do |name|
      tmp = _get_tmp_filename(name)
      zipfile.find_entry(name.to_s).extract(tmp)      
      instance_variable_get("@#{name}").load(tmp)
      bar.increment unless bar.nil?
    end
  end
end

#reply(input, error = "...") ⇒ String

Generate a reply to the user’s input. If the learning attribute is set to true, MegaHAL will also learn from what the user said. Note that it takes MegaHAL about one second to generate about 500 replies.

Parameters:

  • input (String)

    A string that represents the user’s input. If this is nil, MegaHAL will attempt to reply with a greeting, suitable for beginning a conversation.

  • error (String) (defaults to: "...")

    The default reply, which will be used when no suitable reply can be formed.

Returns:

  • (String)

    MegaHAL’s reply to the user’s input, or the error string if no reply could be formed.



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
103
104
105
106
# File 'lib/megahal/megahal.rb', line 77

def reply(input, error="...")
  puncs, norms, words = _decompose(input ? input.strip : nil)

  keyword_symbols =
    MegaHAL.extract(norms)
        .map { |keyword| @dictionary[keyword] }
        .compact

  input_symbols = (norms || []).map { |norm| @dictionary[norm] }

  # create candidate utterances
  utterances = []
  9.times { utterances << _generate(keyword_symbols) }
  utterances << _generate([])
  utterances.delete_if { |utterance| utterance == input_symbols }
  utterances.compact!

  # select the best utterance, and handle _rewrite failure
  reply = nil
  while reply.nil? && utterances.length > 0
    break unless utterance = _select_utterance(utterances, keyword_symbols)
    reply = _rewrite(utterance)
    utterances.delete(utterance)
  end

  # learn from what the user said _after_ generating the reply
  _learn(puncs, norms, words) if @learning && norms

  return reply || error
end

#save(filename, bar = nil) ⇒ Object

Save MegaHAL’s brain to the specified binary file.

Parameters:

  • filename (String)

    The brain file to be saved.

  • bar (ProgressBar) (defaults to: nil)

    An optional progress bar instance.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/megahal/megahal.rb', line 112

def save(filename, bar = nil)
  bar.total = 6 unless bar.nil?
  Zip::File.open(filename, Zip::File::CREATE) do |zipfile|
    zipfile.get_output_stream("dictionary") do |file|
      data = {
        version: 'MH11',
        learning: @learning,
        brain: @brain,
        dictionary: @dictionary
      }
      file.write(Marshal::dump(data))
    end
    bar.increment unless bar.nil?
    [:seed, :fore, :back, :case, :punc].each do |name|
      tmp = _get_tmp_filename(name)
      instance_variable_get("@#{name}").save(tmp)
      zipfile.add(name, tmp)
      bar.increment unless bar.nil?
    end
  end
end

#train(filename, bar = nil) ⇒ Object

Train MegaHAL with the contents of the specified file, which should be plain text with one sentence per line. Note that it takes MegaHAL about one second to process about 500 lines, so large files may cause the process to block for a while. Lines that are too long will be skipped.

Parameters:

  • filename (String)

    The text file to be used for training.

  • bar (ProgressBar) (defaults to: nil)

    An optional progress bar instance.



163
164
165
166
167
# File 'lib/megahal/megahal.rb', line 163

def train(filename, bar = nil)
  lines = File.read(filename).each_line.to_a
  bar.total = lines.length unless bar.nil?
  _train(lines, bar)
end