Class: RubyLabs::ElizaLab::Eliza
- Inherits:
-
Object
- Object
- RubyLabs::ElizaLab::Eliza
- 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
-
.apply(line, rule) ⇒ Object
The apply method implements the second step in the “Eliza algorithm” to determine the response to an input sentence.
-
.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.
-
.clear ⇒ Object
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.
-
.compileRules ⇒ Object
Helper method called by Eliza.load.
-
.detachWord(line) ⇒ Object
Helper method called by methods that read scripts – remove a word from the front of a line.
-
.dump ⇒ Object
Print a complete description of all the rules from the current script.
-
.info ⇒ Object
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.
-
.load(filename) ⇒ Object
Parse rules in
filename
, store them in global arrays. -
.parseDirective(line) ⇒ Object
Helper method – Eliza.load calls this method to deal with directives (lines where the first word begins with a colon).
-
.post ⇒ Object
:nodoc:.
-
.pre ⇒ Object
These methods are useful for debugging Eliza, but not for end users…
-
.preprocess(s) ⇒ Object
Apply preprocessing rules to an input
s
. -
.quiet ⇒ Object
Turn off “verbose mode” to return to normal processing.
-
.reset ⇒ Object
Delete the current script, reset Eliza back to its initial state.
-
.rule_for(w) ⇒ Object
See if Eliza has a rule associated with keyword
w
. -
.rules ⇒ Object
:nodoc:.
-
.run ⇒ Object
Top level method to carry on a conversation.
-
.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.
-
.scan(line, queue) ⇒ Object
The scan method implements the first step in the “Eliza algorithm” to determine the response to an input sentence.
-
.transform(s) ⇒ Object
The transform method is called by the top level Eliza.run method to process each sentence typed by the user.
-
.verbose ⇒ Object
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.
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 |
.clear ⇒ Object
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 |
.compileRules ⇒ Object
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 |
.dump ⇒ Object
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 |
.info ⇒ Object
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 |
.post ⇒ Object
:nodoc:
394 395 396 |
# File 'lib/elizalab.rb', line 394 def Eliza.post # :nodoc: return @@post end |
.pre ⇒ Object
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 |
.quiet ⇒ Object
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 |
.reset ⇒ Object
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 |
.rules ⇒ Object
:nodoc:
398 399 400 |
# File 'lib/elizalab.rb', line 398 def Eliza.rules # :nodoc: return @@rules end |
.run ⇒ Object
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 |
.verbose ⇒ Object
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 |