Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/genie-core/src/tools/number_words.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ enum CardinalWord {

pub(crate) fn parse_spoken_number(tokens: &[&str], start: usize) -> Option<(u64, usize)> {
if let Some(Ok(value)) = tokens.get(start).map(|token| token.parse::<u64>()) {
return Some((value, start + 1));
// Compose a digit token with an immediately following magnitude word so
// "100 thousand" / "5 hundred" scale correctly. Without this the digit
// returned alone and the magnitude word was later mis-parsed as its own
// number ("100 thousand" -> "100 1000"). A digit with no magnitude word
// still returns as-is.
return Some(match tokens.get(start + 1) {
Some(&"hundred") => (value.saturating_mul(100), start + 2),
Some(&"thousand") => (value.saturating_mul(1000), start + 2),
_ => (value, start + 1),
});
Comment on lines +16 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The early return truncates mixed cardinal numbers.

Returning after digit + magnitude leaves suffixes such as twenty, 300, or and 5 unconsumed, producing malformed expressions like 500 20 and 2000 300. Initialize the existing parser state from the scaled value and continue parsing from start + 2; retain the immediate return only for bare digits.

🤖 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 `@crates/genie-core/src/tools/number_words.rs` around lines 16 - 20, Update the
number parsing logic around the match on tokens.get(start + 1) so hundred and
thousand values initialize the existing parser state with the scaled value, then
continue parsing from start + 2 to consume suffixes such as twenty, 300, or and
5. Retain the immediate return path only for bare digits.

}

let mut total: u64 = 0;
Expand Down
22 changes: 22 additions & 0 deletions crates/genie-core/src/tools/quick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5790,6 +5790,28 @@ mod tests {
assert_eq!(call.arguments["expression"], "100 / 5");
}

#[test]
fn routes_digit_with_magnitude_word_to_calculate() {
// Mixed digit + magnitude word ("100 thousand", "5 hundred") is how STT
// commonly renders round numbers. parse_spoken_number's digit fast-path
// returned on the digit and left the magnitude word behind, so "100
// thousand / 4" became the garbled "100 1000 / 4" instead of "100000 / 4"
// — the same class as the all-word garble fixed above.
for (utterance, expression) in [
("what is 100 thousand divided by 4", "100000 / 4"),
("what is 20 thousand times 2", "20000 * 2"),
("what is 5 hundred plus 10", "500 + 10"),
] {
let call = route(utterance).unwrap_or_else(|| panic!("no route for {utterance:?}"));
assert_eq!(call.name, "calculate", "{utterance:?}");
assert_eq!(call.arguments["expression"], expression, "{utterance:?}");
}

// A bare digit not followed by a magnitude word is unchanged.
let call = route("what is 1000 divided by 4").unwrap();
assert_eq!(call.arguments["expression"], "1000 / 4");
}

#[test]
fn routes_weather_and_home_status_before_memory_recall() {
let call = route("Jared: Is it raining for school pickup?").unwrap();
Expand Down
Loading