From 871e5262c3aecadf838453c1701fe9750201f540 Mon Sep 17 00:00:00 2001 From: Mike Lowis Date: Wed, 30 Sep 2026 21:30:39 -0400 Subject: [PATCH] added utterances --- nlshell.rb | 71 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/nlshell.rb b/nlshell.rb index d6c720c..8cf64f2 100755 --- a/nlshell.rb +++ b/nlshell.rb @@ -231,6 +231,36 @@ module Tier1 end end +# Stage 0 result (spec SS5.7). Everything downstream (Tier 1/2/3, executor) +# operates on this, never on raw text or loose token/clause arrays. +module Utterance + Clause = Struct.new(:tokens, :kind) + Result = Struct.new(:raw, :clauses) + + # SS5.8 limits. + MAX_CHARS = 500 + MAX_CLAUSES = 10 + + # Raised with a user-facing message; caller should print it and move on. + Rejected = Class.new(StandardError) + + # Returns a Result, or nil for empty/punctuation-only input (a no-op). + def self.build(raw) + raise Rejected, "that's too long, please split it into smaller requests" if raw.length > MAX_CHARS + + tokens = Lexer.tokenize(raw) + raise Rejected, "unmatched quote, please close it and try again" if Lexer.unbalanced_quote?(tokens) + + tokens = Lexer.downcase_words(tokens) + sentences = Lexer.sentences(tokens) + return nil if sentences.empty? + + clauses = sentences.flat_map { |sentence| Lexer.clauses(sentence) } + raise Rejected, "that's too many clauses in one request, please split it up" if clauses.length > MAX_CLAUSES + + Result.new(raw, clauses.map { |tokens| Clause.new(tokens, Classifier.classify(tokens)) }) + end +end module Ruleset class RulesModule @@ -330,35 +360,28 @@ loop do text = gets exit 0 if text.nil? - # Tokenize the request - tokens = Lexer.tokenize(text.chomp) - if Lexer.unbalanced_quote?(tokens) - puts "unmatched quote, please close it and try again" + begin + utterance = Utterance.build(text.chomp) + rescue Utterance::Rejected => e + puts e.message next end - tokens = Lexer.downcase_words(tokens) + next if utterance.nil? # empty/punctuation-only input: no-op - # Debug only: print the sentences found - Lexer.sentences(tokens).each_with_index do |sentence, i| - puts "#{i}: #{sentence.map(&:to_s).join(' ')}" + if utterance.clauses.any? { |clause| clause.kind.nil? } + puts "couldn't tell what kind of request this is, please rephrase" + next + end - clauses = Lexer.clauses(sentence) - kinds = clauses.map { |clause| Classifier.classify(clause) } - if kinds.any?(&:nil?) - puts " couldn't tell what kind of request this is, please rephrase" - next - end + utterance.clauses.each_with_index do |clause, j| + puts "clause #{j} [#{clause.kind}]: #{clause.tokens.map(&:to_s).join(' ')}" + next unless clause.kind == :action - clauses.each_with_index do |clause, j| - puts " clause #{j} [#{kinds[j]}]: #{clause.map(&:to_s).join(' ')}" - if kinds[j] == :action - node = Tier1.lookup(clause) - puts " #{node ? "tier1 hit: #{node}" : 'tier1 miss'}" - end - end - puts " match: #{Lexer.match_text(sentence)}" + node = Tier1.lookup(clause.tokens) + puts " #{node ? "tier1 hit: #{node}" : 'tier1 miss'}" end + puts "match: #{Lexer.match_text(utterance.clauses.flat_map(&:tokens))}" - # Later stages (clause splitting, classification, tiers) take a sentence's - # token array. The executor reads literal.text directly; no restore step. + # Later stages (Tier 2/3, executor) take a clause's token array. The + # executor reads literal.text directly; no restore step. end -- 2.55.0