]> git.mdlowis.com Git - proto/nlshell.git/commitdiff
added utterances
authorMike Lowis <mike.lowis@gentex.com>
Thu, 1 Oct 2026 01:30:39 +0000 (21:30 -0400)
committerMike Lowis <mike.lowis@gentex.com>
Thu, 1 Oct 2026 01:30:39 +0000 (21:30 -0400)
nlshell.rb

index d6c720c7c650277c0b4be78979e526fdf79bd4bb..8cf64f256996240d709512bb3644f89a1e350cc4 100755 (executable)
@@ -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