fix: router keyword matching now uses whole words, not substrings - #36
Conversation
CODER_KEYWORDS.any? { |k| normalized.include?(k) } matched substrings,
so "classical"/"classic" matched "class" and "errors" matched "error",
misrouting research questions to :coder. Verified empirically before
the fix (all 4 examples below misrouted):
"What is the classical explanation for gravity?"
"Can you summarize this classic novel?"
"How does a democracy function in practice?"
"What errors did historians identify in this account?"
Switched to splitting the message into words and checking array
intersection, which also makes Array#intersect? the actually-correct
call (previously disabled here because the receiver was a String).
The "function" case remains ambiguous even as a whole word ("a
function" vs. "how it functions") — that's a real limitation of
keyword matching, not something this fix can resolve, and is now a
documented regression spec rather than a silent gap. Tracked
separately as a follow-up to replace routing with an LLM classifier.
Closes #3
📝 WalkthroughWalkthrough
ChangesRouter matching
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/chorus/router.rb`:
- Around line 34-35: Update the tokenization in the router logic around
CODER_KEYWORDS to use a Unicode-aware word pattern such as \p{Word}+ so accented
words remain single tokens, and add a regression case covering Unicode input
like “classé” to verify coder keyword matching is not triggered by partial ASCII
tokenization.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2a0fbb52-58ec-427d-8b41-ea9204cf1f42
📒 Files selected for processing (2)
lib/chorus/router.rbspec/chorus/router_spec.rb
| words = message.downcase.scan(/\w+/) | ||
| CODER_KEYWORDS.intersect?(words) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Ruby version:"
ruby -v
echo
echo "Locate router.rb:"
fd -a 'router\.rb$' . | sed 's#^\./##'
echo
echo "Relevant file excerpt:"
sed -n '1,120p' lib/chorus/router.rb 2>/dev/null || true
echo
echo "Unicode tokenization behavior:"
ruby - <<'RB'
messages = [
"What does classé mean?",
"Check the function.",
"classé",
"café résumé naïve",
]
messages.each do |message|
puts message.inspect
puts " \\w+ => #{message.downcase.scan(/\w+/).inspect}"
puts " Unicode \\p{Word}+ => #{message.downcase.scan(/\p{Word}+/).inspect}"
end
RBRepository: Amayyas/Chorus
Length of output: 2335
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ruby -E utf-8 -v - <<'RB'
messages = [
"What does classé mean?",
"Check the function.",
"classé",
"café résumé naïve",
"I use the classical approach.",
]
messages.each do |message|
puts message.inspect
puts " UTF8\\w+ => #{message.downcase.scan(/\w+/).inspect}"
puts " UTF8\\p{Word}+ => #{message.downcase.scan(/\p{Word}+/).inspect}"
end
RB
python3 - <<'PY'
import unicodedata
samples = ["what does classé mean?", "café résumé naïve", "I use the classical approach."]
for s in samples:
words_w = []
cur = []
for ch in s:
cat = unicodedata.category(ch)
if cat.startswith('L') or cat.startswith('N') or ch in '_':
cur.append(ch)
else:
if cur:
words_w.append("".join(cur))
cur = []
if cur:
words_w.append("".join(cur))
print(s)
print(" Unicode word segments:", words_w)
PYRepository: Amayyas/Chorus
Length of output: 710
🌐 Web query:
Ruby Regexp /w/ Unicode property \p{Word} documentation
💡 Result:
In Ruby regular expressions, \p{Word} is a Unicode property that matches characters defined as word characters according to Unicode standards [1][2]. As of recent Ruby versions, \p{Word} (and the equivalent POSIX-style bracket expression [[:word:]]) matches a character if it belongs to one of the following Unicode categories or has one of the specified Unicode properties [1][2]: Unicode Categories: - Mark (M) [2] - Decimal Number (Nd) [2] - Connector Punctuation (Pc) [2] Unicode Properties: - Alpha (Alphabetic characters) [2] - Join_Control (specifically U+200C and U+200D) [2] Documentation Note: Historically, documentation for \p{Word} was less precise or incomplete regarding the specific Unicode categories and properties included [3][2][4]. Recent updates (e.g., in Ruby 3.3+) have aligned the documentation with current implementations and Unicode Technical Standard #18, which defines word characters to include alphabetic values, decimal numbers, and join control characters [2][5]. Be aware that bugs or inconsistencies between the documented behavior and the actual implementation of the underlying Unicode character database can occasionally occur across different Ruby versions [3][4]. Comparison with \w: - \w: In Ruby, \w is typically equivalent to [a-zA-Z0-9_] (when not using the /u flag for full Unicode support or when relying on standard ASCII behavior), though its exact behavior can vary depending on the regex engine's configuration and the version of Ruby [6]. - \p{Word}: Unlike \w, \p{Word} is specifically designed for Unicode-aware matching, providing a much broader set of characters that conform to global linguistic standards for what constitutes a word character [1][6].
Citations:
- 1: https://docs.ruby-lang.org/en/master/Regexp.html
- 2: ruby/ruby@060f14b
- 3: https://bugs.ruby-lang.org/issues/19417
- 4: https://bugs.ruby-lang.org/issues/21503
- 5: http://www.unicode.org/reports/tr18/tr18-18.html
- 6: https://idiosyncratic-ruby.com/30-regex-with-class.html
🌐 Web query:
site:ruby-doc.org/core Regexp \w Unicode \p{Word}
💡 Result:
In regular expression engines, the \w token and \p{Word} property serve different purposes regarding character matching, particularly concerning Unicode support [1][2]. \w (Word Character) The \w shorthand character class typically matches a limited set of ASCII characters. In most standard regex implementations—including Ruby's—it is equivalent to the character set [a-zA-Z0-9_] [1][3]. While some modern regex engines (such as those in PCRE or certain language-specific implementations) may be configured to treat \w as Unicode-aware, it is historically and primarily defined as matching ASCII letters, digits, and underscores [1][4]. \p{Word} (Unicode Property) The \p{Word} construct is a Unicode property escape [1][2]. It matches any character that is considered a "word" character according to the Unicode standard [1][4]. This is significantly broader than \w because it includes characters from various scripts, symbols, and marks [1][3]. Specifically, it matches any character belonging to the following Unicode General Categories [1][2]: - Letter (L) - Mark (M) - Number (N) - Connector_Punctuation (Pc) While \w is useful for matching technical identifiers constrained to ASCII, \p{Word} is the correct choice for internationalized applications requiring support for a wide range of global languages and character sets [1][4].
Citations:
- 1: http://www.ruby-doc.org/core/Regexp.html
- 2: http://ruby-doc.org/core/doc/regexp_rdoc.html
- 3: https://ruby-doc.org/core-2.5.8/Regexp.html
- 4: https://ruby-doc.org/core-2.2.10/Regexp.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmp="$(mktemp)"
cat > "$tmp" <<'RB'
messages = [
"What does classé mean?",
"Check the function.",
"classé",
"café résumé naïve",
"I use the classical approach.",
]
messages.each do |message|
puts message.inspect
puts " UTF8\\w+ => #{message.downcase.scan(/\w+/).inspect}"
puts " UTF8\\p{Word}+ => #{message.downcase.scan(/\p{Word}+/).inspect}"
end
RB
ruby -E utf-8 -v "$tmp"Repository: Amayyas/Chorus
Length of output: 777
Make tokenization Unicode-aware.
Ruby’s \w tokenizes classé as ["class", "mean"], causing accented words to still match :coder keywords. Use a Unicode word property such as /\p{Word}+/ and add a regression case for Unicode input.
Proposed fix
- words = message.downcase.scan(/\w+/)
+ words = message.downcase.scan(/\p{Word}+/)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| words = message.downcase.scan(/\w+/) | |
| CODER_KEYWORDS.intersect?(words) | |
| words = message.downcase.scan(/\p{Word}+/) | |
| CODER_KEYWORDS.intersect?(words) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/chorus/router.rb` around lines 34 - 35, Update the tokenization in the
router logic around CODER_KEYWORDS to use a Unicode-aware word pattern such as
\p{Word}+ so accented words remain single tokens, and add a regression case
covering Unicode input like “classé” to verify coder keyword matching is not
triggered by partial ASCII tokenization.
Summary
Router#coding_task?matched keywords as substrings of the raw message, so "classical"/"classic" matched "class" and "errors" matched "error" — misrouting research-flavored questions to:coder. Verified empirically before fixing (see linked issue).Array#intersect?, which is also the semantically correct call now that both sides are genuinely arrays (previously it had to be disabled via rubocop comment because the receiver was a String).Closes #3
Test plan
bundle exec rspec— 34/34 passing (30 previous + 4 new regression cases)gem build chorus-llm.gemspec— builds cleanlySummary by CodeRabbit
Bug Fixes
Tests