# 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)
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).
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
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
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