From d65d88b0d11c4b268c3440c149dac4a0698f171b Mon Sep 17 00:00:00 2001 From: Mike Lowis Date: Thu, 1 Oct 2026 14:41:44 -0400 Subject: [PATCH] added minor typo correction --- nlshell.rb | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/nlshell.rb b/nlshell.rb index e0c5330..172dd7f 100755 --- a/nlshell.rb +++ b/nlshell.rb @@ -7,7 +7,6 @@ require "strscan" # Scan the environment for possible shell commands ShellCommands = Set.new ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).reverse.each do |dir| - pp dir next unless File.directory?(dir) Dir.each_child(dir) do |name| path = File.join(dir, name) @@ -161,6 +160,34 @@ module Lexer end end +# Shared fuzzy-equality helper: lets a comparison accept a small typo instead +# of requiring an exact string match. Only ever used to compare a word against +# ONE specific expected word (a pattern/phrase word), never to search a whole +# vocabulary - so there's no ambiguity to resolve, and slot values (which +# never go through this) can't be touched. +module Fuzzy + MAX_DISTANCE = 2 + + def self.close?(a, b) + a == b || distance(a, b) <= MAX_DISTANCE + end + + # Levenshtein edit distance. + def self.distance(a, b) + costs = (0..b.length).to_a + a.each_char.with_index do |ca, i| + prev = costs[0] + costs[0] = i + 1 + b.each_char.with_index do |cb, j| + tmp = costs[j + 1] + costs[j + 1] = [costs[j + 1] + 1, costs[j] + 1, prev + (ca == cb ? 0 : 1)].min + prev = tmp + end + end + costs[b.length] + end +end + # Stage: CLS. Tags a clause as teach/conditional/loop/query/action by # matching its leading words against configured phrases, checked in the # configured order (first match wins). @@ -185,7 +212,7 @@ module Classifier def self.leading_match?(clause, phrase) return false if clause.length < phrase.length - clause.first(phrase.length).map { |t| t.word? ? t.text : nil } == phrase + clause.first(phrase.length).each_with_index.all? { |t, i| t.word? && Fuzzy.close?(t.text, phrase[i]) } end end @@ -224,13 +251,14 @@ module Tier1 args[word.delete_prefix(":").to_sym] = token else - return nil unless token.word? && token.text == word + return nil unless token.word? && Fuzzy.close?(token.text, word) end end args 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 @@ -258,6 +286,9 @@ module Utterance 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 + # Classification/Tier 1 tolerate small typos on their own (Fuzzy.close? + # inside Classifier.leading_match? and Tier1.match), so no separate + # correction pass is needed here. Result.new(raw, clauses.map { |tokens| Clause.new(tokens, Classifier.classify(tokens)) }) end end -- 2.55.0