Class: RiveScript::Brain
- Inherits:
-
Object
- Object
- RiveScript::Brain
- Defined in:
- lib/rivescript/brain.rb
Constant Summary collapse
- MAX_REPLY_WEIGHT =
Reply weights above this are clamped to prevent trivially-crafted weight=N tags from ballooning the random-choice bucket.
10_000- BEGIN_OK_TOKEN =
Private-use-area token used to shield a fully-processed inner reply from being tag-processed a second time when substituted into a BEGIN block's ok placeholder.
"\uE000RIVE_OK\uE000"- BEGIN_OK_FORMAT_TAGS =
Simple text-transform tags that are allowed to wrap ok in a BEGIN block (e.g. "uppercaseok/uppercase"). These are resolved against the already-processed inner reply directly (bypassing the tag engine) so the wrapping author's intent is preserved without re-running the full tag processor (with its side-effecting tags) over user-influenced reply text a second time.
%w[person formal sentence uppercase lowercase].freeze
- TAGS =
{ "bot" => { "self_closing" => true, "handle" => lambda { |rive, data, _user, _scope| vars = rive._var split = Brain.split_assignment(data) if split.length > 1 vars[split[0].strip] = split[1] "" elsif split.length == 1 val = vars[split[0].strip] val = "undefined" if val.nil? val else "undefined" end } }, "env" => { "self_closing" => true, "handle" => lambda { |rive, data, _user, _scope| globals = rive._global split = Brain.split_assignment(data) if split.length > 1 globals[split[0].strip] = split[1] "" elsif split.length == 1 val = globals[split[0].strip] val = "undefined" if val.nil? val else "undefined" end } }, "set" => { "self_closing" => true, "handle" => lambda { |rive, data, user, _scope| split = Brain.split_assignment(data) rive.set_uservar(user, split[0].strip, split[1]) if split.length > 1 "" } }, "get" => { "self_closing" => true, "handle" => lambda { |rive, data, user, _scope| rive.get_uservar(user, data.strip) } }, "add" => { "self_closing" => true, "handle" => lambda { |rive, data, user, _scope| split = Brain.split_assignment(data) name = split[0].strip raw_value = split[1] existing_value = rive.get_uservar(user, name) || 0 existing_value = 0 if existing_value == "undefined" value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip) existing_number = Brain.parse_int_js(existing_value.to_s) if value.nil? return "[ERR: Math can't 'add' non-numeric value '#{raw_value}']" elsif existing_number.nil? return "[ERR: Math can't 'add' non-numeric user variable '#{name}']" else result = existing_number + value rive.set_uservar(user, name, result) end "" } }, "sub" => { "self_closing" => true, "handle" => lambda { |rive, data, user, _scope| split = Brain.split_assignment(data) name = split[0].strip raw_value = split[1] existing_value = rive.get_uservar(user, name) || 0 value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip) existing_value = 0 if existing_value == "undefined" existing_number = Brain.parse_int_js(existing_value.to_s) if value.nil? return "[ERR: Math can't 'sub' non-numeric value '#{raw_value}']" elsif existing_number.nil? return "[ERR: Math can't 'sub' non-numeric user variable '#{name}']" else result = existing_number - value rive.set_uservar(user, name, result) end "" } }, "mult" => { "self_closing" => true, "handle" => lambda { |rive, data, user, _scope| split = Brain.split_assignment(data) name = split[0].strip raw_value = split[1] existing_value = rive.get_uservar(user, name) || 0 value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip) existing_value = 0 if existing_value == "undefined" existing_number = Brain.parse_int_js(existing_value.to_s) if value.nil? return "[ERR: Math can't 'mult' non-numeric value '#{raw_value}']" elsif existing_number.nil? return "[ERR: Math can't 'mult' non-numeric user variable '#{name}']" else result = existing_number * value rive.set_uservar(user, name, result) end "" } }, "div" => { "self_closing" => true, "handle" => lambda { |rive, data, user, _scope| split = Brain.split_assignment(data) name = split[0].strip raw_value = split[1] existing_value = rive.get_uservar(user, name) || 0 value = raw_value.nil? ? nil : Brain.parse_int_js(raw_value.strip) existing_value = 0 if existing_value == "undefined" existing_number = Brain.parse_int_js(existing_value.to_s) if value.nil? return "[ERR: Math can't 'div' non-numeric value '#{raw_value}']" elsif existing_number.nil? return "[ERR: Math can't 'div' non-numeric user variable '#{name}']" elsif value == 0 return "[ERR: Can't Divide By Zero]" else result = existing_number.fdiv(value) result = result.to_i if result == result.to_i rive.set_uservar(user, name, result) end "" } }, "call" => { "self_closing" => false, "handle" => lambda { |rive, data, _user, scope| trimmed = Utils.trim(data) m = trimmed.match(/\A(\S+)(?:\s+(.*))?\z/m) output = rive.errors["objectNotFound"] return output unless m obj = m[1] args = m[2] ? Utils.parse_call_args(m[2]) : [] objlangs = rive._objlangs handlers = rive._handlers if objlangs.key?(obj) lang = objlangs[obj] if handlers[lang] begin output = handlers[lang].call(rive, obj, args, scope) rescue StandardError => e rive.brain.warn(e.) unless e.nil? output = "[ERR: Error raised by object macro: #{e.}]" end else output = "[ERR: No Object Handler]" end end output } } }.freeze
Class Method Summary collapse
- .parse_int_js(str) ⇒ Object
-
.split_assignment(data) ⇒ Object
Splits "name=value" assignment data on the first "=" only, so that values which themselves contain "=" are preserved intact.
Instance Method Summary collapse
-
#current_user ⇒ Object
The user ID currently being processed by #reply (only meaningful from within object macros invoked during a reply).
- #format_message(msg, botreply = nil) ⇒ Object
- #handle_tag(rive, user, content, scope, depth) ⇒ Object
-
#initialize(master) ⇒ Brain
constructor
A new instance of Brain.
- #parse_complex_tags(rive, user, content, scope, depth, end_tag = "") ⇒ Object
- #process_tags(user, msg, reply, st, bst, step, scope) ⇒ Object
- #reply(user, msg, scope = nil) ⇒ Object
- #say(message) ⇒ Object
- #substitute(msg, type) ⇒ Object
- #trigger_regexp(user, regexp) ⇒ Object
- #warn(message, filename = nil, lineno = nil) ⇒ Object
Constructor Details
#initialize(master) ⇒ Brain
Returns a new instance of Brain.
205 206 207 208 209 210 |
# File 'lib/rivescript/brain.rb', line 205 def initialize(master) @master = master @strict = master._strict @utf8 = master._utf8 @mutex = Mutex.new end |
Class Method Details
.parse_int_js(str) ⇒ Object
27 28 29 30 |
# File 'lib/rivescript/brain.rb', line 27 def self.parse_int_js(str) m = str.to_s.strip.match(/\A[-+]?\d+/) m ? m[0].to_i : nil end |
.split_assignment(data) ⇒ Object
Splits "name=value" assignment data on the first "=" only, so that values which themselves contain "=" are preserved intact.
34 35 36 |
# File 'lib/rivescript/brain.rb', line 34 def self.split_assignment(data) data.to_s.split("=", 2) end |
Instance Method Details
#current_user ⇒ Object
The user ID currently being processed by #reply (only meaningful from within object macros invoked during a reply). Stored per-thread so concurrent calls to #reply from different threads don't clobber each other's notion of "the current user".
216 217 218 |
# File 'lib/rivescript/brain.rb', line 216 def current_user Thread.current[:rivescript_current_user] end |
#format_message(msg, botreply = nil) ⇒ Object
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
# File 'lib/rivescript/brain.rb', line 286 def (msg, botreply = nil) msg = msg.to_s msg = msg.downcase unless case_sensitive? msg = substitute(msg, "sub") if @utf8 msg = msg.gsub(/[\\<>]+/, "") if !@master.unicode_punctuation.nil? msg = msg.gsub(@master.unicode_punctuation, "") end if !botreply.nil? msg = msg.gsub(/[.?,!;:@#$%^&*()]/, "") end else msg = Utils.strip_nasties(msg, @utf8) end msg.strip.gsub(/\s+/, " ") end |
#handle_tag(rive, user, content, scope, depth) ⇒ Object
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 |
# File 'lib/rivescript/brain.rb', line 417 def handle_tag(rive, user, content, scope, depth) tag = "" reminder = "" i = 0 while i < content.length if TAGS.key?(tag) reminder = content[(i + 1)..] break elsif content[i] == " " reminder = content[(i + 1)..] break elsif content[i] == ">" reminder = content[(i + 1)..] return { "response" => "<#{tag}>", "reminder" => reminder } end tag += content[i] i += 1 end tag_def = TAGS[tag] self_closing = tag_def ? tag_def["self_closing"] : true end_tag = self_closing ? ">" : "</#{tag}>" result = (rive, user, reminder, scope, depth, end_tag) reminder = result["reminder"] response = if tag_def && tag_def["handle"] tag_def["handle"].call(rive, result["response"], user, scope) else "<#{tag} #{result["response"]}>" end { "response" => response, "reminder" => reminder } end |
#parse_complex_tags(rive, user, content, scope, depth, end_tag = "") ⇒ Object
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 |
# File 'lib/rivescript/brain.rb', line 450 def (rive, user, content, scope, depth, end_tag = "") return { "response" => content, "reminder" => "" } if depth > 50 response = "" reminder = content next_tag = reminder.index("<") next_end = end_tag.empty? ? reminder.length : (reminder.index(end_tag) || reminder.length) while !reminder.empty? && next_tag && next_tag < next_end response += reminder[0...next_tag] reminder = reminder[(next_tag + 1)..] result = handle_tag(rive, user, reminder, scope, depth + 1) response += result["response"].to_s reminder = result["reminder"].to_s next_tag = reminder.index("<") next_end = end_tag.empty? ? reminder.length : (reminder.index(end_tag) || reminder.length) end response += reminder[0...next_end].to_s reminder = reminder[(next_end + end_tag.length)..] || "" { "response" => response, "reminder" => reminder } end |
#process_tags(user, msg, reply, st, bst, step, scope) ⇒ Object
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 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 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 |
# File 'lib/rivescript/brain.rb', line 474 def (user, msg, reply, st, bst, step, scope) stars = [""] stars.concat(st) botstars = [""] botstars.concat(bst) stars.push("undefined") if stars.length == 1 botstars.push("undefined") if botstars.length == 1 giveup = 0 while (match = reply.match(/\(@([A-Za-z0-9_]+)\)/i)) if (giveup += 1) > bot_depth warn("Infinite loop looking for arrays in reply!") break end name = match[1] arrays = bot_array result = if arrays[name] "{random}#{arrays[name].join("|")}{/random}" else "\x00@#{name}\x00" end reply = reply.sub(/\(@#{Regexp.escape(name)}\)/i, result) end reply = reply.gsub(/\x00@([A-Za-z0-9_]+)\x00/, '(@\1)') reply = reply.gsub(/<person>/i, "{person}<star>{/person}") reply = reply.gsub(/<@>/i, "{@<star>}") reply = reply.gsub(/<formal>/i, "{formal}<star>{/formal}") reply = reply.gsub(/<sentence>/i, "{sentence}<star>{/sentence}") reply = reply.gsub(/<uppercase>/i, "{uppercase}<star>{/uppercase}") reply = reply.gsub(/<lowercase>/i, "{lowercase}<star>{/lowercase}") reply = reply.gsub(/\{weight=\d+\}/i, "") reply = reply.gsub(/<star>/i, stars[1].to_s) reply = reply.gsub(/<botstar>/i, botstars[1].to_s) (1...stars.length).each do |i| reply = reply.gsub(/<star#{i}>/i, stars[i].to_s) end (1...botstars.length).each do |i| reply = reply.gsub(/<botstar#{i}>/i, botstars[i].to_s) end history = bot_session.get(user, "__history__") history = new_history if history == "undefined" reply = reply.gsub(/<input>/i, history["input"] ? history["input"][0] : "undefined") reply = reply.gsub(/<reply>/i, history["reply"] ? history["reply"][0] : "undefined") (1..9).each do |i| reply = reply.gsub(/<input#{i}>/i, history["input"][i - 1]) if reply.include?("<input#{i}>") reply = reply.gsub(/<reply#{i}>/i, history["reply"][i - 1]) if reply.include?("<reply#{i}>") end reply = reply.gsub(/<id>/i, user) reply = reply.gsub(/\\s/i, " ") reply = reply.gsub(/\\n/i, "\n") reply = reply.gsub(/\\#/i, "#") giveup = 0 while (match = reply.match(/\{random\}(.+?)\{\/random\}/i)) if (giveup += 1) > bot_depth warn("Infinite loop looking for random tag!") break end text = match[1] random = text.include?("|") ? text.split("|") : text.split(" ") output = random[(rand * random.length).floor] reply = reply.sub(/\{random\}#{Regexp.escape(text)}\{\/random\}/i, output) end %w[person formal sentence uppercase lowercase].each do |type| giveup = 0 while (match = reply.match(/\{#{type}\}(.+?)\{\/#{type}\}/i)) giveup += 1 if giveup >= 50 warn("Infinite loop looking for #{type} tag!") break end content = match[1] replace = if type == "person" substitute(content, "person") else Utils.string_format(type, content) end reply = reply.sub(/\{#{type}\}#{Regexp.escape(content)}\{\/#{type}\}/i, replace) end end reply = (@master, user, reply, scope, 0)["response"] giveup = 0 while (match = reply.match(/\{topic=(.+?)\}/i)) giveup += 1 if giveup >= 50 warn("Infinite loop looking for topic tag!") break end name = match[1] @master.set_uservar(user, "topic", name) reply = reply.sub(/\{topic=#{Regexp.escape(name)}\}/i, "") end giveup = 0 while (match = reply.match(/\{@([^\}]*?)\}/)) giveup += 1 if giveup >= 50 warn("Infinite loop looking for redirect tag!") break end target = (Utils.strip(match[1])) say("Inline redirection to: #{target}") subreply = get_reply(user, target, "normal", step + 1, scope) reply = reply.sub(/\{@#{Regexp.escape(match[1])}\}/i, subreply) end reply end |
#reply(user, msg, scope = nil) ⇒ Object
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/rivescript/brain.rb', line 228 def reply(user, msg, scope = nil) @mutex.synchronize do say("Asked to reply to [#{user}] #{msg}") Thread.current[:rivescript_current_user] = user msg = (msg) reply = "" bot_session.set(user, { "__initialmatch__" => nil }) if bot_topics["__begin__"] begin_reply = get_reply(user, "request", "begin", 0, scope) if begin_reply.include?("{ok}") inner_reply = get_reply(user, msg, "normal", 0, scope) ok_replacement = inner_reply BEGIN_OK_FORMAT_TAGS.each do |type| wrap_pattern = /\{#{type}\}\{ok\}\{\/#{type}\}/i next unless begin_reply.match?(wrap_pattern) ok_replacement = type == "person" ? substitute(inner_reply, "person") : Utils.string_format(type, inner_reply) begin_reply = begin_reply.gsub(wrap_pattern, BEGIN_OK_TOKEN) end begin_reply = begin_reply.gsub("{ok}", BEGIN_OK_TOKEN) reply = (user, msg, begin_reply, [], [], 0, scope) reply = reply.gsub(BEGIN_OK_TOKEN, ok_replacement) else reply = (user, msg, begin_reply, [], [], 0, scope) end else reply = get_reply(user, msg, "normal", 0, scope) end history = bot_session.get(user, "__history__") history = new_history if history == "undefined" begin history["input"].pop history["input"].unshift(msg) # Keep %previous intact when nothing matched / no reply was found. # Otherwise ERR strings overwrite history.reply[0] and short # conversations cannot continue (aichaos/rivescript-js#411). unless error_reply?(reply) history["reply"].pop history["reply"].unshift(reply) end rescue StandardError history = new_history end bot_session.set(user, { "__history__" => history }) reply end ensure Thread.current[:rivescript_current_user] = nil end |
#say(message) ⇒ Object
220 221 222 |
# File 'lib/rivescript/brain.rb', line 220 def say() @master.send(:say, ) end |
#substitute(msg, type) ⇒ Object
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 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 |
# File 'lib/rivescript/brain.rb', line 599 def substitute(msg, type) sort_key = type == "sub" ? "sub" : "person" unless bot_sorted && bot_sorted[sort_key] @master.warn("You forgot to call sortReplies()!") return msg end subs = type == "sub" ? bot_sub : bot_person maxwords = type == "sub" ? bot_submax : bot_personmax result = "" pattern = if !@master.unicode_punctuation.nil? msg.gsub(@master.unicode_punctuation, "") else msg.gsub(/[.,!?;:]/, "") end giveup = 0 subgiveup = 0 while pattern.include?(" ") giveup += 1 if giveup >= 1000 warn("Too many loops when handling substitutions!") break end li = Utils.n_index_of(pattern, " ", maxwords) subpattern = pattern[0...li] result = subs[subpattern] if !result.nil? msg = msg.sub(subpattern, result) else while subpattern.include?(" ") subgiveup += 1 if subgiveup >= 1000 warn("Too many loops when handling substitutions!") break end li = subpattern.rindex(" ") subpattern = subpattern[0...li] result = subs[subpattern] if !result.nil? msg = msg.sub(subpattern, result) break end end end fi = pattern.index(" ") pattern = pattern[(fi + 1)..] end result = subs[pattern] msg = msg.sub(pattern, result) if !result.nil? msg end |
#trigger_regexp(user, regexp) ⇒ Object
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/rivescript/brain.rb', line 309 def trigger_regexp(user, regexp) regexp = regexp.gsub(/^\*$/, "<zerowidthstar>") regexp = regexp.gsub("*", "(.+?)") regexp = regexp.gsub("#", "(\\d+?)") regexp = regexp.gsub("_", "(\\w+?)") regexp = regexp.gsub(/\s*\{weight=\d+\}\s*/i, "") regexp = regexp.gsub("<zerowidthstar>", "(.*?)") regexp = regexp.gsub(/\|{2,}/, "|") regexp = regexp.gsub(/(\(|\[)\|/, '\1') regexp = regexp.gsub(/\|(\)|\])/, '\1') if @utf8 regexp = regexp.gsub("\\@", "\\u0040") end giveup = 0 while (match = regexp.match(/\[(.+?)\]/)) if (giveup += 1) > 50 warn("Infinite loop when trying to process optionals in a trigger!") return "" end parts = match[1].split("|") opts = parts.map { |p| "(?:\\s|\\b)+#{p}(?:\\s|\\b)+" } pipes = opts.join("|") pipes = pipes.gsub(Regexp.new(Regexp.escape("(.+?)")), "(?:.+?)") pipes = pipes.gsub(Regexp.new(Regexp.escape("(\\d+?)")), "(?:\\d+?)") pipes = pipes.gsub(Regexp.new(Regexp.escape("(\\w+?)")), "(?:\\w+?)") pipes = pipes.gsub("[", "__lb__").gsub("]", "__rb__") regexp = regexp.sub( Regexp.new("\\s*\\[#{Regexp.escape(match[1])}\\]\\s*"), "(?:#{pipes}|(?:\\b|\\s)+)" ) end regexp = regexp.gsub("__lb__", "[").gsub("__rb__", "]") regexp = regexp.gsub("\\w", "[^\\s\\d]") giveup = 0 while regexp.include?("@") if (giveup += 1) > 50 break end if (match = regexp.match(/@(.+?)\b/)) name = match[1] rep = "" arrays = bot_array if arrays[name] && !arrays[name].empty? rep = "(?:" + arrays[name].map { |item| Utils.(item) }.join("|") + ")" end regexp = regexp.sub(/@#{Regexp.escape(name)}\b/, rep) end end giveup = 0 while regexp.include?("<bot") if (giveup += 1) > 50 break end if (match = regexp.match(/<bot (.+?)>/i)) name = match[1] rep = "" vars = bot_var rep = Utils.(Utils.strip_nasties(vars[name], @utf8).downcase) if vars[name] regexp = regexp.sub(/<bot #{Regexp.escape(name)}>/i, rep) end end giveup = 0 while regexp.include?("<get") if (giveup += 1) > 50 break end if (match = regexp.match(/<get (.+?)>/i)) name = match[1] rep = @master.get_uservar(user, name) regexp = regexp.sub(/<get #{Regexp.escape(name)}>/i, Utils.(rep.to_s.downcase)) end end giveup = 0 regexp = regexp.gsub(/<input>/i, "<input1>") regexp = regexp.gsub(/<reply>/i, "<reply1>") history = bot_session.get(user, "__history__") history = new_history if history == "undefined" while regexp.include?("<input") || regexp.include?("<reply") if (giveup += 1) > 50 break end %w[input reply].each do |type| (1..9).each do |i| tag = "<#{type}#{i}>" next unless regexp.include?(tag) value = Utils.((history[type][i - 1], type == "reply")) regexp = regexp.gsub(tag, value) end end end if @utf8 && regexp.include?("\\u") regexp = regexp.gsub(/\\u([A-Fa-f0-9]{4})/) { Regexp.last_match(1).to_i(16).chr(Encoding::UTF_8) } end regexp.gsub(/\|{2,}/m, "|") end |
#warn(message, filename = nil, lineno = nil) ⇒ Object
224 225 226 |
# File 'lib/rivescript/brain.rb', line 224 def warn(, filename = nil, lineno = nil) @master.warn(, filename, lineno) end |