Module: Legion::LLM::Inference::Steps::TriggerMatch
- Includes:
- Logging, Legion::Logging::Helper
- Included in:
- Executor
- Defined in:
- lib/legion/llm/inference/steps/trigger_match.rb
Constant Summary collapse
- HARNESS_PREFIXES =
[ '[SUGGESTION MODE', '[REQUEST INTERRUPTED', 'Write the title in the predominant language' ].freeze
Instance Method Summary collapse
- #extract_recent_text ⇒ Object
- #extract_session_text(text) ⇒ Object
- #format_trigger_log_value(value) ⇒ Object
- #harness_message?(text) ⇒ Boolean
- #log_trigger_match(action, **fields) ⇒ Object
- #normalize_message_words(text) ⇒ Object
- #rank_and_cap(matched, per_word, limit) ⇒ Object
- #record_trigger_match_timeline(count, start_time = nil) ⇒ Object
- #sanitize_trigger_text(text) ⇒ Object
- #settings_extension_tool_matches(word_set, extensions = settings_extensions) ⇒ Object
- #settings_extensions ⇒ Object
- #step_trigger_match ⇒ Object
- #strip_tagged_blocks(text) ⇒ Object
- #subtract_always_loaded(matched, extensions = settings_extensions) ⇒ Object
- #trigger_scan_depth ⇒ Object
- #trigger_tool_limit ⇒ Object
- #trigger_tool_name(tool) ⇒ Object
- #trigger_tool_name_for_word_match(name) ⇒ Object
- #trigger_words_for_entry(entry) ⇒ Object
Instance Method Details
#extract_recent_text ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 79 def extract_recent_text depth = trigger_scan_depth = @request..last(depth) # G3: pipeline messages are Canonical::Message — role/text are # member reads (no dual-shape accessor). text = .filter_map do |msg| next unless msg.role.to_s == 'user' raw = msg.text.to_s next if (raw) raw end.join(' ') sanitize_trigger_text(text) end |
#extract_session_text(text) ⇒ Object
115 116 117 118 119 120 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 115 def extract_session_text(text) matches = text.to_s.scan(%r{<session\b[^>]*>(.*?)</session>}mi).flatten return nil if matches.empty? matches.join(' ') end |
#format_trigger_log_value(value) ⇒ Object
215 216 217 218 219 220 221 222 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 215 def format_trigger_log_value(value) case value when Array value.map(&:to_s).first(Legion::Settings[:llm][:tools][:trigger][:log_name_limit]).join(',') else value end end |
#harness_message?(text) ⇒ Boolean
108 109 110 111 112 113 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 108 def (text) return false if text.nil? || text.empty? trimmed = text.lstrip HARNESS_PREFIXES.any? { |prefix| trimmed.start_with?(prefix) } end |
#log_trigger_match(action, **fields) ⇒ Object
205 206 207 208 209 210 211 212 213 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 205 def log_trigger_match(action, **fields) payload = fields.map { |key, value| "#{key}=#{format_trigger_log_value(value)}" }.join(' ') log.info( "[llm][tools][trigger] action=#{action} request_id=#{@request.id} " \ "conversation_id=#{@request.conversation_id || 'none'} #{payload}".strip ) rescue StandardError => e handle_exception(e, level: :warn, handled: true, operation: 'llm.pipeline.steps.trigger_match.log') end |
#normalize_message_words(text) ⇒ Object
122 123 124 125 126 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 122 def (text) return Set.new if text.nil? || text.empty? text.downcase.gsub(/[^a-z ]/, ' ').split.to_set end |
#rank_and_cap(matched, per_word, limit) ⇒ Object
166 167 168 169 170 171 172 173 174 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 166 def rank_and_cap(matched, per_word, limit) scores = Hash.new(0) per_word.each_value do |tools| tools.each { |tool| scores[tool] += 1 } end matched.to_a .sort_by { |tool| [-scores[tool], trigger_tool_name(tool)] } .first(limit) end |
#record_trigger_match_timeline(count, start_time = nil) ⇒ Object
224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 224 def record_trigger_match_timeline(count, start_time = nil) return unless @timeline.respond_to?(:record) duration = start_time ? ((::Time.now - start_time) * 1000).to_i : 0 @timeline.record( category: :enrichment, key: 'tool:trigger_match', direction: :inbound, detail: "#{count} tools matched via trigger words", from: 'settings_extensions', to: 'pipeline', duration_ms: duration ) end |
#sanitize_trigger_text(text) ⇒ Object
97 98 99 100 101 102 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 97 def sanitize_trigger_text(text) session_text = extract_session_text(text) return session_text if session_text strip_tagged_blocks(text) end |
#settings_extension_tool_matches(word_set, extensions = settings_extensions) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 134 def settings_extension_tool_matches(word_set, extensions = settings_extensions) matched = Set.new per_word = Hash.new { |hash, word| hash[word] = Set.new } return [matched, per_word] unless extensions.respond_to?(:tools) Array(extensions.tools).each do |entry| next unless entry.is_a?(Hash) tool_words = trigger_words_for_entry(entry) matching_words = word_set & tool_words next if matching_words.empty? matched << entry matching_words.each { |word| per_word[word] << entry } end [matched, per_word] end |
#settings_extensions ⇒ Object
128 129 130 131 132 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 128 def settings_extensions return unless defined?(Legion::Settings::Extensions) Legion::Settings::Extensions end |
#step_trigger_match ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 14 def step_trigger_match start_time = nil extensions = settings_extensions unless extensions.respond_to?(:tools) log_step_debug(:trigger_match, :skipped, reason: :settings_extensions_unavailable) log_trigger_match(:skipped, reason: :settings_extensions_unavailable) return end start_time = ::Time.now text = extract_recent_text word_set = (text) if word_set.empty? log_step_debug(:trigger_match, :skipped, reason: :no_words) return end log_step_debug(:trigger_match, :scanning, word_count: word_set.size) matched, per_word = settings_extension_tool_matches(word_set, extensions) pre_filter_count = matched.size subtract_always_loaded(matched, extensions) if matched.empty? log_step_debug(:trigger_match, :no_matches) log_trigger_match(:no_matches, word_count: word_set.size, pre_filter_count: pre_filter_count) return end limit = trigger_tool_limit @triggered_tools = if matched.size <= limit matched.to_a else log.warn( "[llm][steps][trigger_match] action=tools_capped request_id=#{@request.id} " \ "matched=#{matched.size} limit=#{limit} dropped=#{matched.size - limit}" ) rank_and_cap(matched, per_word, limit) end names = @triggered_tools.map { |tool| trigger_tool_name(tool) } if @triggered_tools.any? @enrichments['tool:trigger_match'] = { content: "#{@triggered_tools.size} tools matched via trigger words", data: { tool_count: @triggered_tools.size, tool_names: names }, timestamp: ::Time.now } end record_trigger_match_timeline(@triggered_tools.size, start_time) log_step_debug(:trigger_match, :matched, matched_count: matched.size, injected_count: @triggered_tools.size, limit: limit) log_trigger_match(:matched, word_count: word_set.size, matched_count: matched.size, injected_count: @triggered_tools.size, limit: limit, names: names) rescue StandardError => e @warnings << "Trigger match error: #{e.}" handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.trigger_match') record_trigger_match_timeline(0, start_time) end |
#strip_tagged_blocks(text) ⇒ Object
104 105 106 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 104 def strip_tagged_blocks(text) text.to_s.gsub(%r{<[a-z][\w-]*\b[^>]*>.*?</[a-z][\w-]*>}mi, ' ') end |
#subtract_always_loaded(matched, extensions = settings_extensions) ⇒ Object
176 177 178 179 180 181 182 183 184 185 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 176 def subtract_always_loaded(matched, extensions = settings_extensions) unless extensions.respond_to?(:filter_tools) log_step_debug(:trigger_match, :always_loaded_filter_skipped, reason: :settings_extensions_unavailable) return end always = extensions.filter_tools(deferred: false).map { |t| t[:name] } matched.reject! { |tool| always.include?(trigger_tool_name(tool)) } log_step_debug(:trigger_match, :always_loaded_filtered, always_loaded_count: always.size, remaining_count: matched.size) end |
#trigger_scan_depth ⇒ Object
197 198 199 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 197 def trigger_scan_depth Legion::Settings[:llm][:tools][:trigger][:scan_depth] end |
#trigger_tool_limit ⇒ Object
201 202 203 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 201 def trigger_tool_limit Legion::Settings[:llm][:tools][:trigger][:tool_limit] end |
#trigger_tool_name(tool) ⇒ Object
187 188 189 190 191 192 193 194 195 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 187 def trigger_tool_name(tool) if tool.is_a?(Hash) (tool[:name] || tool['name']).to_s elsif tool.respond_to?(:tool_name) tool.tool_name.to_s else tool.to_s end end |
#trigger_tool_name_for_word_match(name) ⇒ Object
162 163 164 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 162 def trigger_tool_name_for_word_match(name) name.to_s.sub(/\Alegion[-_]/, '') end |
#trigger_words_for_entry(entry) ⇒ Object
153 154 155 156 157 158 159 160 |
# File 'lib/legion/llm/inference/steps/trigger_match.rb', line 153 def trigger_words_for_entry(entry) values = Array(entry[:trigger_words]) values << trigger_tool_name_for_word_match(entry[:name]) values << entry[:extension] values << entry[:runner] values << entry[:function] (values.compact.join(' ')) end |