Class: RubyLabs::ElizaLab::Eliza

Inherits:
Object
  • Object
show all
Defined in:
lib/elizalab.rb

Overview

Eliza

This top-level class of the Eliza module defines a singleton object that has methods for managing a chat with Eliza.

Class Method Summary collapse

Class Method Details

.apply(line, rule) ⇒ Object

The apply method implements the second step in the “Eliza algorithm” to determine the response to an input sentence. It is called from the top level method (Eliza.transform) to see if a rule applies to an input sentence. If so, return the string generated by the rule object, otherwise return nil.

This is the method that handles indirection in scripts. If a rule body has a line of the form “@x” it means sentences containing the rule for this word should be handle by the rule for x. For example, suppose a script has this rule:

duck
   /football/
     "I love my Ducks"
   /.*/
     @bird

If an input sentence contains the word “duck”, this rule will be added to the queue. If Eliza applies the rule (after first trying higher priority rules) it will

see if the sentence matches the pattern /football/, i.e. if the word “football” appears anywhere else in the sentence, and if so respond with the string “I love my Ducks”. If not, the next pattern succeeds (every input matches .*) and the response is generated by the rules for “bird”.



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/elizalab.rb', line 491

def Eliza.apply(line, rule)
  puts "applying rule: key = '#{rule.key}'" if @@verbose
  if res = rule.apply(line, :no_preprocess)   
    if res[0] == ?@
      rulename = res.slice(1..-1)
      if @@rules[rulename]
        return Eliza.apply( line, @@rules[rulename] )
      else
        warn "Eliza.apply: no rule for #{rulename}"
        return nil
      end
    else
      return res
    end
  else
    return nil
  end
end

.checkout(script, filename = nil) ⇒ Object

Save a copy of a script that is distributed with RubyLabs; if no output file name specified make a file name from the program name.



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/elizalab.rb', line 419

def Eliza.checkout(script, filename = nil)
  scriptfilename = script.to_s + ".txt"
  scriptfilename = File.join(@@elizaDirectory, scriptfilename)
  if !File.exists?(scriptfilename)
    puts "Script not found: #{scriptfilename}"
    return nil
  end
  outfilename = filename.nil? ? (script.to_s + ".txt") : filename
  dest = File.open(outfilename, "w")
  File.open(scriptfilename).each do |line|
    dest.puts line.chomp
  end
  dest.close
  puts "Copy of #{script} saved in #{outfilename}"
end

.clearObject

Initialize (or reinitialize) the module – clear out any rules that have been loaded from a script, and install the default script that simply echoes the user intput.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/elizalab.rb', line 354

def Eliza.clear
  @@script = nil
  @@aliases = Hash.new
  @@vars = Hash.new
  @@starts = Array.new
  @@stops = Array.new
  @@queue = PriorityQueue.new
  
  @@verbose = false
  @@pre.clear
  @@post.clear
  @@rules.clear

  @@default = Rule.new(:default)
  @@default.addPattern(/(.*)/)
  @@default.addReassembly("$1")
  
  return true
end

.compileRulesObject

Helper method called by Eliza.load. Check each pattern’s regular expression and replace var names by alternation constructs. If the script specified a default rule name look up that rule and save it as the default.



594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/elizalab.rb', line 594

def Eliza.compileRules
  @@rules.each do |key,val|
    a = val.patterns()
    a.each do |p|
      expr = p.regexp.inspect
      expr.gsub!(/\$\w+/) { |x| @@vars[x].join("|") }
      p.regexp = eval(expr)
    end
  end
  if @@default.class == String
    @@default = @@rules[@@default]
  end
end

.detachWord(line) ⇒ Object

Helper method called by methods that read scripts – remove a word from the front of a line



578
579
580
581
582
583
584
585
586
587
# File 'lib/elizalab.rb', line 578

def Eliza.detachWord(line)
  word = line[@@word]                 # pattern matches the first word
  if line.index(" ")
    line.slice!(0..line.index(" "))   # delete up to end of the word
    line.lstrip!                      # in case there are extra spaces after word
  else
    line.slice!(0..-1)                # line just had the one word
  end
  return word
end

.dumpObject

Print a complete description of all the rules from the current script.



669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
# File 'lib/elizalab.rb', line 669

def Eliza.dump
  Eliza.clear unless defined? @@default
  puts "Script: #{@@script}"
  print "Starts:\n  "; p @@starts
  print "Stops:\n  "; p @@stops
  print "Vars:\n  "; p @@vars
  print "Aliases:\n  "; p @@aliases
  print "Pre:\n  "; p @@pre
  print "Post:\n  "; p @@post
  print "Default:\n  "; p @@default
  print "Queue:\n  "; p @@queue.collect { |r| r.key }
  puts
  @@rules.each { |key,val| puts val }
  return nil
end

.infoObject

Print a summary description of the current script, with the number of rules and sentence patterns and a list of key words from all the rules.



688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'lib/elizalab.rb', line 688

def Eliza.info
  Eliza.clear unless defined? @@default
  
  words = Hash.new
  npatterns = 0
  
  @@rules.each do |k,r| 
    words[k] = 1 unless k[0] == ?$
    r.patterns.each do |p|
      npatterns += 1
      p.cleanRegexp.split.each do |w|
        Eliza.saveWords(w, words)
      end
    end
  end
  
  @@aliases.keys.each do |k|
    Eliza.saveWords(k, words)
  end
  
  puts "Script: #{@@script}"
  puts "  #{@@rules.size} rules with #{npatterns} sentence patterns"
  puts "  #{words.length} key words: #{words.keys.sort.join(', ')}"
end

.load(filename) ⇒ Object

Parse rules in filename, store them in global arrays. If filename is a symbol it refers to a script file in the ElizaLab data directory; if it’s a string it should be the name of a file in the current directory. – Strategy: use a local var named ‘rule’, initially set to nil. New rules start with a single word at the start of a line. When such a line is found in the input file, create a new Rule object and store it in ‘rule’. Subsequent lines that are part of the current rule (lines that contain regular expressions or strings) are added to current Rule object. Directives indicate the end of a rule, so ‘rule’ is reset to nil when a directive is seen.



619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# File 'lib/elizalab.rb', line 619

def Eliza.load(filename)
  begin
    Eliza.clear
    rule = nil
    if filename.class == Symbol
      filename = File.join(@@elizaDirectory, filename.to_s + ".txt")
    end
    File.open(filename).each do |line|
      line.strip!
      next if line.empty? || line[0] == ?#
      if line[0] == ?:
        Eliza.parseDirective(line)
        rule = nil
      else
        if line =~ @@iword
          rulename, priority = line.split
          rule = priority ? Rule.new(rulename, priority.to_i) : Rule.new(rulename)
          @@rules[rule.key] = rule
        elsif rule.nil?
          warn "missing rule name? unexpected input '#{line}'"
        elsif line[0] == ?/
          if line[-1] == ?/
            rule.addPattern(line)
          else
            warn "badly formed expression (missing /): '#{line}'"
          end
        elsif line[0] == ?"
          if line[-1] == ?"
            rule.addReassembly(line.unquote)
          else
            warn "badly formed string (missing \"): '#{line}'"
          end
        elsif line[0] == ?@
          rule.addReassembly(line)
        else
          warn "unexpected line in rule for #{rulename}: '#{line}'"
        end
      end
    end
    Eliza.compileRules
    @@script = filename
  rescue
    puts "Eliza: Error processing #{filename}: #{$!}"
    return false
  end
  return true
end

.parseDirective(line) ⇒ Object

Helper method – Eliza.load calls this method to deal with directives (lines where the first word begins with a colon)



544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'lib/elizalab.rb', line 544

def Eliza.parseDirective(line)    # :nodoc:
  word = Eliza.detachWord(line)
  case word
  when "alias"
    if line.empty? || line[0] != ?$
      warn "symbol after :alias must be a variable name; ignoring '#{word} #{line}'"
      return
    else
      sym = Eliza.detachWord(line)
      @@vars[sym] = Array.new
      line.split.each do |s| 
        @@aliases[s] = sym
        @@vars[sym] << s
      end
    end
  when "start"
    @@starts << line.unquote
  when "stop"
    @@stops << line.unquote
  when "pre"
    sym = Eliza.detachWord(line)
    @@pre[sym] = line.unquote
  when "post"
    sym = Eliza.detachWord(line)
    @@post[sym] = line.unquote
  when "default"
    @@default = line[@@word]
  else
    warn "unknown directive: :#{word} (ignored)"
  end
end

.postObject

:nodoc:



394
395
396
# File 'lib/elizalab.rb', line 394

def Eliza.post   # :nodoc:
  return @@post
end

.preObject

These methods are useful for debugging Eliza, but not for end users…



390
391
392
# File 'lib/elizalab.rb', line 390

def Eliza.pre   # :nodoc:
  return @@pre
end

.preprocess(s) ⇒ Object

Apply preprocessing rules to an input s. Makes sure the entire input is a single line and words are separated by single space, then applies pre-processing substitution rules. The string is modified in place, so after this call the string s has all of the preprocessing substitutions.



447
448
449
450
451
# File 'lib/elizalab.rb', line 447

def Eliza.preprocess(s)
  s.gsub!( /\s+/, " " )
  s.gsub!(@@word) { |w| @@pre.has_key?(w) ? @@pre[w] : w }
  puts "preprocess: line = '#{s}'" if @@verbose    
end

.quietObject

Turn off “verbose mode” to return to normal processing. See Eliza.verbose.



412
413
414
# File 'lib/elizalab.rb', line 412

def Eliza.quiet
  @@verbose = false
end

.resetObject

Delete the current script, reset Eliza back to its initial state.



729
730
731
732
733
734
# File 'lib/elizalab.rb', line 729

def Eliza.reset
  @@rules.each do |k, r|
    r.patterns.each { |p| p.reset }
  end
  return true
end

.rule_for(w) ⇒ Object

See if Eliza has a rule associated with keyword w. If so, return a reference to that Rule object, otherwise return nil.



438
439
440
# File 'lib/elizalab.rb', line 438

def Eliza.rule_for(w)
  @@rules[w] || ((x = @@aliases[w]) && (r = @@rules[x]))
end

.rulesObject

:nodoc:



398
399
400
# File 'lib/elizalab.rb', line 398

def Eliza.rules   # :nodoc:
  return @@rules
end

.runObject

Top level method to carry on a conversation. Starts a read-eval-print loop, stopping when the user types “bye” or “quit”. For each sentence, call Eliza.transform to find a rule that applies to the sentence and print the response.



741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
# File 'lib/elizalab.rb', line 741

def Eliza.run
  Eliza.clear unless defined? @@default
  puts @@starts[rand(@@starts.length)] if ! @@starts.empty?
  loop do 
    s = readline("  H: ", true)
    return if s.nil?
    s.chomp!
    next if s.empty?
    if s == "bye" || s == "quit"
      puts @@stops[rand(@@stops.length)] if ! @@stops.empty?
      return
    end
    puts "  C: " + Eliza.transform(s)
  end
end

.saveWords(s, hash) ⇒ Object

Helper method called by Eliza.info – don’t include common words like “the” or “a” in list of key words, and clean up regular expression symbols. Put the remaining items in the hash.



717
718
719
720
721
722
723
724
725
# File 'lib/elizalab.rb', line 717

def Eliza.saveWords(s, hash)  # :nodoc:
  return if ["a","an","in","of","the"].include?(s)
  s.gsub! "(", ""
  s.gsub! ")", ""
  s.gsub! ".*", ""
  s.gsub! "?", ""
  return if s.length == 0
  s.split(/\|/).each { |w| hash[w.downcase] = 1 }    
end

.scan(line, queue) ⇒ Object

The scan method implements the first step in the “Eliza algorithm” to determine the response to an input sentence. Apply preprocessing substitutions, then break the line into individual words, and for each word that is associated with a Rule object, add the rule to the priority queue. –

NOTE: this method does a destructive update to the input line.…



460
461
462
463
464
465
466
467
468
469
# File 'lib/elizalab.rb', line 460

def Eliza.scan(line, queue)
  Eliza.preprocess(line)
  line.scan(@@word) do |w|
    w.downcase!
    if r = Eliza.rule_for(w)
      queue << r 
      puts "add rule for '#{w}' to queue" if @@verbose
    end
  end
end

.transform(s) ⇒ Object

The transform method is called by the top level Eliza.run method to process each sentence typed by the user. Initialize a priority queue, apply preprocessing transformations, and add rules for each word to the queue. Then apply the rules, in order, until a call to r.apply for some rule r returns a non-nil response. Note that the default rule should apply to any input string, so it should never be the case that the queue empties out before some rule can apply.



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/elizalab.rb', line 517

def Eliza.transform(s)
  s.sub!(/[\n\.\?!\-]*$/,"")        # strip trailing punctuation
  # s.downcase!

  @@queue = PriorityQueue.new
  @@queue << @@default              # initialize queue with default rule

  Eliza.scan(s, @@queue)            # add rules for recognized key words

  while @@queue.length > 0          # apply rules in order of priority
    if @@verbose
      print "queue: " 
      p @@queue.collect { |r| r.key }
    end
    rule = @@queue.shift
    if result = Eliza.apply(s, rule)
      return result
    end
  end

  warn "No rules applied" if @@queue.empty?
  return nil
end

.verboseObject

Turn on “verbose mode” to see a detailed trace of which rules and sentence patterns are being applied as Eliza responds to an input sentence. Call Eliza.quiet to return to normal mode.



406
407
408
# File 'lib/elizalab.rb', line 406

def Eliza.verbose
  @@verbose = true
end