Module: SqlChatbot::Grammar::CountRenderer
- Defined in:
- lib/sql_chatbot/grammar/count_renderer.rb
Overview
Programmatic renderer for the grammar’s COUNT primitive. Bypasses the answer-stream LLM entirely — a single number is too easy for the LLM to mis-narrate (e.g., rendering ‘count = 0` as “No matching records found” instead of “There are 0 X”).
Mirrors the architectural shape of ‘ListRenderer`:
- Pure function. No side effects, no LLM calls.
- Returns { ok: true, text: "..." } when conditions match, else { ok: false }.
- Caller (Orchestrator) yields the text as `token` events when ok.
Conditions for programmatic render:
- primitive == :COUNT (or "COUNT")
- exactly one result row
- that row has a numeric (or numeric-stringable) `count` field — the
standard column shape PostgreSQL emits for `SELECT COUNT(*)`.
Anything else (grouped counts, multi-row results) falls through to the answer LLM.
Constant Summary collapse
- IRREGULAR_PLURALS =
{ "people" => "person", "men" => "man", "women" => "woman", "children" => "child", "feet" => "foot", "teeth" => "tooth", "geese" => "goose", "mice" => "mouse", "analyses" => "analysis", "bases" => "basis", "crises" => "crisis", "theses" => "thesis", "data" => "datum", "criteria" => "criterion", "phenomena" => "phenomenon", }.freeze
Class Method Summary collapse
- .already_plural?(word) ⇒ Boolean
- .as_number(v) ⇒ Object
- .match_case(original, transformed) ⇒ Object
- .pluralize_word(word) ⇒ Object
- .singularize_word(word) ⇒ Object
-
.to_plural_label(label) ⇒ Object
Force the LAST word of a (possibly multi-word) label to plural.
-
.to_singular_label(label) ⇒ Object
Force the LAST word to singular.
- .transform_last_word(label) ⇒ Object
- .try_render(primitive, entity_display_label, rows) ⇒ Object
Class Method Details
.already_plural?(word) ⇒ Boolean
62 63 64 65 66 67 68 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 62 def self.already_plural?(word) # A word is already plural if singularize produces a different word. # "credentials" -> "credential" (different) -> plural ✓ # "class" -> "class" (ss-guard, unchanged) -> not plural ✓ # "inbox" -> "inbox" (no s ending) -> not plural ✓ singularize_word(word) != word end |
.as_number(v) ⇒ Object
40 41 42 43 44 45 46 47 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 40 def self.as_number(v) case v when Integer then v when Numeric then v.to_i when String then (v =~ /\A-?\d+\z/) ? v.to_i : nil else nil end end |
.match_case(original, transformed) ⇒ Object
80 81 82 83 84 85 86 87 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 80 def self.match_case(original, transformed) return transformed if original.empty? || transformed.empty? if original[0] == original[0].upcase transformed[0].upcase + transformed[1..] else transformed end end |
.pluralize_word(word) ⇒ Object
89 90 91 92 93 94 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 89 def self.pluralize_word(word) if word =~ /(s|x|z|ch|sh)\z/i then word + "es" elsif word =~ /[^aeiouAEIOU]y\z/ then word[0..-2] + "ies" else word + "s" end end |
.singularize_word(word) ⇒ Object
103 104 105 106 107 108 109 110 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 103 def self.singularize_word(word) return word if word.empty? return IRREGULAR_PLURALS[word] if IRREGULAR_PLURALS.key?(word) return word[0..-4] + "y" if word.length > 3 && word.end_with?("ies") return word[0..-3] if word =~ /(sses|xes|zes|ches|shes)\z/ return word[0..-2] if word.end_with?("s") && !word.end_with?("ss") word end |
.to_plural_label(label) ⇒ Object
Force the LAST word of a (possibly multi-word) label to plural. Detects already-plural inputs (“Credentials” → singularize gives “credential” — different — so it’s already plural) to avoid the “Credentialses” double-pluralization.
53 54 55 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 53 def self.to_plural_label(label) transform_last_word(label) { |w| already_plural?(w) ? w : pluralize_word(w) } end |
.to_singular_label(label) ⇒ Object
Force the LAST word to singular. Mirror of ‘to_plural_label`.
58 59 60 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 58 def self.to_singular_label(label) transform_last_word(label) { |w| already_plural?(w) ? singularize_word(w) : w } end |
.transform_last_word(label) ⇒ Object
70 71 72 73 74 75 76 77 78 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 70 def self.transform_last_word(label) parts = label.split(" ") last = parts.last return label if last.nil? || last.empty? lower = last.downcase transformed = yield(lower) parts[-1] = match_case(last, transformed) parts.join(" ") end |
.try_render(primitive, entity_display_label, rows) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/sql_chatbot/grammar/count_renderer.rb', line 23 def self.try_render(primitive, entity_display_label, rows) return { ok: false } unless primitive.to_s == "COUNT" return { ok: false } unless rows.is_a?(Array) && rows.length == 1 row = rows.first return { ok: false } unless row.is_a?(Hash) v = row["count"] || row[:count] n = as_number(v) return { ok: false } if n.nil? label = entity_display_label.to_s.empty? ? "Item" : entity_display_label.to_s noun = n == 1 ? to_singular_label(label) : to_plural_label(label) verb = n == 1 ? "is" : "are" { ok: true, text: "There #{verb} #{n} #{noun}." } end |