############################################################################### # Stores and retrieves FAQ questions. # # > add faq: What is an FAQ? answer: FAQ means Frequently Asked Questions. # Okay, I have added FAQ means Frequently Asked Questions to faq What is an FAQ # # > find faq # What is an faq? # FAQ means Frequently Asked Questions. # # > what is a FAQ = what is an FAQ # Okay, I have added what is a FAQ = what is an FAQ # # > What is a FAQ? # What is an FAQ? # FAQ means Frequently Asked Questions. # # > ################################################################################ require 'yaml' require 'graph' $Quitwords = [':q', 'quit', 'exit', 'bye'] class MyAgent attr_accessor :DEBUG def initialize(args=Array.new) @workingDir = File.expand_path(File.dirname(__FILE__)) @filename = @workingDir + "/faqagent-api.yaml" unless load_message = load_synonyms @patterns = [ [/^(.*?) is a synonym for (.*)/, :add_synonym], [/^save syn/, :save_synonyms], [/^load syn/, :load_synonyms], [/^add pattern (.*), (.*)/, :add_pattern], [/^>(.*)/, :defining_method], [/^restart/, :restart], [/^show api/, :show_api], [/^show (.*)'s api/, :show_one_method_api] ] save_synonyms # creates patterns.yaml on first use. else puts load_message end @method_def = '' @DEBUG = 0 @FAQFILE = "graph.yaml" @graph = Graph.new @question_words = ['what', 'why', 'who', 'when', 'how', 'can', 'may', 'did', 'do', 'does', 'will', 'is', 'are'] end def def_method method_def file = IO.read(__FILE__) return "Doesn't have 'end # class'" unless file =~ /(.*)end # class(.*)if \$0 == __FILE__(.*)/m grp1 = $1.to_s; grp2 = $2.to_s; grp3 = $3.to_s new_filename = get_new_filename File.open(new_filename, "w+") { |f| f.puts grp1 + method_def + "\n\nend # class\n\nif $0 == __FILE__" + grp3 } #@patterns.push( [@new_regexp, @new_symbol] ) # replace above line with 'smart push' @patterns.each_with_index { |e,i| found = false puts "[def_method]: e.inspect==#{e.inspect}" unless @DEBUG == 0 pattern = e[0].to_s puts "[def_method]: pattern==#{pattern}" unless @DEBUG == 0 if pattern =~ Regexp.new(Regexp.escape("(?i-mx:(.*)")) puts "[def_method]: pattern matched!" @patterns.insert(i, [@new_regexp, @new_symbol]) found = true end if found then break end } save_synonyms puts "#{method_def}" MyAgent.class_eval(method_def) end # If this file has a '.' + a number before the '.rb' extension # (i.e. foo.0.rb), increment the number (i.e. return foo.1.rb). # Otherwise return the name of this file unchanged. def get_new_filename old_filename = __FILE__ if old_filename =~ /(.*)\.(\d+)\.rb$/ incr = $2.to_i + 1 return new_filename = $1.to_s + "." + incr.to_s + ".rb" end return old_filename # Dangerous! end # Match input against each regexp pattern in @patterns; # if there's a match, call the method associated with the pattern. def getResponse input r = "Default response." #puts "input==#{input}" @score = 2 @patterns.each { |pattern| if input =~ pattern[0] return [self.send(pattern[1], pattern[0], input), @score] rescue return r end } return r end def load_synonyms(pattern=nil, input=nil) if File.exist?(@filename) @patterns = YAML.load_file(@filename) return "I have loaded #{@filename}." end return false end def save_synonyms(pattern=nil, input=nil) r = "Eep! error saving synonyms! " begin File.open(@filename, 'w') do |out| YAML.dump(@patterns, out) end return "Okay I have saved the synonyms to disk." rescue => error return r << error end end # a is a synonym for b, or /a/ is a synonym for /b/ def add_synonym(pattern, input) return "Problem matching pattern." unless input =~ pattern g1 = $1; g2 = $2; #puts "[add_synonym] g1==#{g1}; g2=#{g2}" # Remove enclosing "/", if present. if g1 =~ /^\// then g1.sub!(/^\//, '') end if g1 =~ /\/$/ then g1.sub!(/\/$/, '') end if g2 =~ /^\// then g2.sub!(/^\//, '') end if g2 =~ /\/$/ then g2.sub!(/\/$/, '') end #puts "[add_synonym] g1==#{g1}; g2=#{g2}" grp1 = %r{(?i-mx:^#{g1})} grp2 = %r{(?i-mx:^#{g2})} #puts "[add_synonym] grp1==#{grp1}; grp2=#{grp2}" method ='' index = -1 @patterns.each_with_index { |p,i| #puts "p[0]==#{p[0]}; p[0].class==#{p[0].class}; grp2==#{grp2}; grp2.class==#{grp2.class}" if p[0].to_s == grp2.to_s method = p[1] index = i break end } # If still no match, try again with slightly different regexps. if method == '' then grp1 = %r{(?i-mx:#{g1})}; grp2 = %r{(?i-mx:#{g2})}; end @patterns.each_with_index {|p,i| if p[0].to_s == grp2.to_s; method = p[1]; index = i; break; end} if method == '' then grp1 = %r{#{g1}}; grp2 = %r{#{g2}}; end @patterns.each_with_index {|p,i| if p[0].to_s == grp2.to_s; method = p[1]; index = i; break; end} if method == '' then grp1 = %r{^#{g1}}; grp2 = %r{^#{g2}}; end @patterns.each_with_index {|p,i| if p[0].to_s == grp2.to_s; method = p[1]; index = i; break; end} return unless method.to_s != nil puts "add_synonym: index==#{index}" unless @DEBUG == 0 @patterns.insert(index, [grp1, method] ) save_synonyms return "Okay, #{grp1} has been added, and will now call #{method}." end def add_pattern(pattern=nil, input=nil) if input =~ pattern @new_regexp = $1 @new_symbol = $2 @new_regexp.gsub!(/\//, '') new_re = %r{#{@new_regexp}} # Make the regexp case insensitive. new_re_s = new_re.to_s if new_re_s =~ /(-mix)/ new_re_s = $~.pre_match << "i-mx" << $~.post_match end @new_regexp = %r{#{new_re_s}} #puts "@new_regexp==#{@new_regexp}" method_def = " def #{@new_symbol}(pattern=nil, input=nil)\n return \"#\{input} doesn't match #\{pattern}\" unless input =~ pattern\n return 'Default response from " + @new_symbol + "'\n end # method_def" return def_method(method_def) else return "#{input} doesn't match #{pattern}." end end def defining_method(pattern=nil, input=nil) if input =~ pattern @method_def << "\n " << $1.to_s if @method_def =~ /end # method_def/ return def_method(@method_def) end return '' else return "#{input} doesn't match #{pattern}." end end def method_missing input, *rest return getResponse(input.to_s) end # Strips delimiters. def process str str.strip! str.sub!(/\?$/, '') str.sub!(/^\[/, '') str.sub!(/\]$/, '') str.sub!(/^(?:"|')/, '') str.sub!(/(?:"|')$/, '') str.sub!(/\.$/, '') str.sub!(/;$/, '') str.sub!(/,$/, '') str.sub!(/!$/, '') str.sub!(/\?$/, '') str.sub!(/^\//, '') str.sub!(/\/$/, '') return str end def restart(pattern=nil,input=nil) exec "ruby #{__FILE__}" end # method_def def show_api(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern apiarr= Array.new # Contains methods and patterns from MyAgent-api.yaml. @patterns.each { |p| regexp = p[0].to_s; method = p[1].to_s; found = false puts "regexp=#{regexp}; method=#{method}" unless @DEBUG == 0 apiarr.each { |arr| if arr.include? method then found = true end } next unless not found apiarr.push [regexp, method] } # If more than one pattern maps to a method, only the first is stored. # Try to pretty-print. toReturn = '' puts "apiarr.inspect=#{apiarr.inspect}" unless @DEBUG == 0 apiarr.each { |arr| puts "1. arr=#{arr}" unless @DEBUG == 0 regex = arr[0]; method = arr[1] if regex =~ /\(\?(?:[i\-mx]*)(?:\:)?(?:\^)?(.*)\)/ # try to clean regex regex = $1.to_s end if method == nil or method == '' then method = "Default response" end toReturn << method.to_s << " | " << regex.to_s << "\n" } return toReturn end # method_def def show_one_method_api(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern grp1 = process $1 # Supplied method name. apiarr= Array.new # To hold methods and patterns from MyAgent-api.yaml. @patterns.each { |p| # Collect all the regexps for the supplied method. regexp = p[0].to_s; method = p[1].to_s puts "regexp=#{regexp}; method=#{method}." unless @DEBUG == 0 if method == grp1 then apiarr.push [regexp, method] end } # Try to pretty-print. toReturn = '' puts "apiarr.inspect=#{apiarr.inspect}" unless @DEBUG == 0 apiarr.each { |arr| puts "1. arr=#{arr}" unless @DEBUG == 0 regex = arr[0]; method = arr[1] if regex =~ /\(\?(?:[i\-mx]*)(?:\:)?(?:\^)?(.*)\)/ # try to clean regex regex = $1.to_s end if method == nil or method == '' then method = "Default response" end toReturn << method.to_s << " | " << regex.to_s << "\n" } return toReturn end # method_def # Start faqagent-specific methods. # Note: If questions do not have a final '?', it is added before storing def find_faq(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern grp1 = process $1 #puts "[find_faq] grp1==#{grp1}" #unless @DEBUG == 0 q = grp1; if q !~ /\?$/ then q << '?' end returnArr = @graph.a_r_?(q.downcase, ["is answered by"]) puts "returnArr==#{returnArr}" unless @DEBUG == 0 # if returnArr is blank, look for the = relation and ask those questions questionArr = Array.new if returnArr.empty? or returnArr == nil questionArr = @graph.a_r_?(q.downcase, "=") end # flatten questionArray flatArray = Array.new questionArr.each { |elem| elem.each{ |e| flatArray.push(e) } } questionArr = flatArray #puts "questionArr==#{questionArr}" #unless @DEBUG == 0 tempArr = Array.new if questionArr.size > 0 and questionArr != nil questionArr.each { |question| #puts "1. tempArr==#{tempArr}" q2 = question; if q2 !~ /\?$/ then q2 << '?' end tempArr.push @graph.a_r_?(q2, "is answered by") #puts "2. tempArr==#{tempArr}" tempArr.each { |elem| returnArr.push elem } } end returnArr.uniq! #puts "returnArr (2) == #{returnArr}" # Pretty-print response TODO: put into separate method grp1 << "?" unless grp1.split.size < 3 grp1.capitalize! unless grp1.split.size < 3 if grp1 =~ /\?\?$/ then grp1.sub!(/\?\?$/, '?') end period = "." unless grp1 =~ /\?$/ if returnArr.size == 0 then return "I can't find #{grp1}#{period}" end @score = 4 # Get the main faq answered by the response, so both can be returned. questions = Array.new returnArr.each { |elem| #puts "elem==#{elem}" if elem == nil then next end if elem.size == 0 then next end if elem.class == String then elem.chomp! end if elem.class == Array then elem = elem[0] end q = @graph.what_r_b?("is answered by", elem) #puts "[find_faq] q==#{q}" #unless @DEBUG == 0 if q.class == Array then q.each { |e| e.capitalize!; e.strip! } end if q.class == String then q.capitalize!; q.strip! end questions.push(q) } answers = returnArr.join("; "); if answers =~ /; $/ then answers.sub!(/; $/, '') end if answers =~ /^; / then answers.sub!(/^; /, '') end return questions.uniq.join(" ") << "\n" << answers << "." end # method_def def show_faq_questions(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern arr = @graph.vertex_hash.keys returnArr = Array.new arr.each { |elem| if elem =~ /\?$/ then returnArr.push(elem) end } return returnArr.join(" ") unless returnArr.size == 0 return "There are no faq questions." end # method_def def set_faq_file(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern grp1 = process $1 @FAQFILE = grp1 if @FAQFILE !~ /\.yaml$/ then @FAQFILE << ".yaml" end load_graph(@FAQFILE) @score = 4 return "Okay, I have set the faq file to #{@FAQFILE}." end # method_def def what_is_the_faq_file(pattern=nil, input=nil) @score = 4 return "The faq file is currently #{@FAQFILE}." end # method_def def add_faq_and_answer(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern grp1 = process $1; #puts "grp1==#{grp1}" grp1 << "?" grp2 = process $2; #puts "grp2=#{grp2}" @graph.a_r_b!(grp1.downcase, "is answered by", grp2) return "Okay I have added answer #{grp2} to faq #{grp1}" end # method_def def print_graph(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern s = @graph.to_s arr = s.split("\n") @score = 5 return arr.join("\n") unless arr.empty? return "The graph is empty." end # method_def def save_graph(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern begin; file = process $1; rescue; file = @FILENAME; end r = @graph.save(file) @score = 4 return "#{r}" end # method_def def reset_graph(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern @graph = Graph.new(false) @save = false return "Okay, I have reset the graph." end # method_def def load_graph(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern file = process $1 @score = 5 return @graph.load(file) end # method_def def reset_graph(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern @graph = Graph.new(false) @score = 5 return "Okay, I have reset the graph." end # method_def def a_equals_b(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern grp1 = process $1; grp2 = process $2 @graph.a_r_b!(grp1.downcase, "=", grp2.downcase) @graph.a_r_b!(grp2.downcase, "=", grp1.downcase) return "Okay, I have added #{grp1} = #{grp2}." end # method_def def a_is_answered_by_b(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern grp1 = process $1; grp2 = process $2 @graph.a_r_b!(grp1.downcase, "is answered by", grp2.downcase) return "Okay, I have added #{$1} is answered by #{$2}." end # method_def def isQuestion?(s = '') if s =~ /^(#{@question_words.join('|')})/ then return true end if s =~ /\?$/ then return true end return false end # method_def def default_handler(pattern=nil, input=nil) return "#{input} doesn't match #{pattern}" unless input =~ pattern if not isQuestion?($1) then return 'Default response'; @score -= 2 end #return self.send("find faq: #{$1}") str, sc = self.send("find faq: #{$1}") @score = sc return str end # method_def end # class if $0 == __FILE__ bot = MyAgent.new print "\n> "; $stdout.flush while (line = gets) !~ /^#{$Quitwords.join('|')}$/i and line !~ /^$/ response = bot.send(line.to_s).to_s if response.class == "Array" then response = response[0] end response = response.chop # remove the score puts response + "\n\n" print "> "; $stdout.flush end puts "Bye" end # of file