Module: Legion::MCP::GapDetector
- Defined in:
- lib/legion/mcp/gap_detector.rb
Constant Summary collapse
- GAP_INTENT_THRESHOLD =
5- FAILURE_RATE_THRESHOLD =
0.4- STALE_CANDIDATE_HOURS =
24- MAX_GAPS =
20
Class Method Summary collapse
- .calculate_priority(count, type) ⇒ Object
- .detect_gaps ⇒ Object
- .detect_high_failure_tools ⇒ Object
- .detect_stale_candidates ⇒ Object
- .detect_unmatched_intents ⇒ Object
- .normalize_intent(text) ⇒ Object
Class Method Details
.calculate_priority(count, type) ⇒ Object
95 96 97 98 99 100 101 102 103 |
# File 'lib/legion/mcp/gap_detector.rb', line 95 def calculate_priority(count, type) base = case type when :unmatched then 0.8 when :failure then 0.6 when :stale then 0.4 else 0.3 end (base + [count * 0.02, 0.2].min).clamp(0.0, 1.0).round(4) end |
.detect_gaps ⇒ Object
15 16 17 18 19 20 21 22 |
# File 'lib/legion/mcp/gap_detector.rb', line 15 def detect_gaps gaps = [] gaps.concat(detect_unmatched_intents) gaps.concat(detect_high_failure_tools) gaps.concat(detect_stale_candidates) gaps.uniq { |g| g[:id] }.first(MAX_GAPS) end |
.detect_high_failure_tools ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/legion/mcp/gap_detector.rb', line 47 def detect_high_failure_tools return [] unless defined?(Observer) stats = Observer.all_tool_stats stats.filter_map do |tool_name, tool_stat| next unless tool_stat next if tool_stat[:call_count] < 5 failure_rate = tool_stat[:failure_count].to_f / tool_stat[:call_count] next if failure_rate < FAILURE_RATE_THRESHOLD { id: "failing:#{tool_name}", type: :high_failure_tool, tool_name: tool_name, failure_rate: failure_rate.round(4), call_count: tool_stat[:call_count], failure_count: tool_stat[:failure_count], last_error: tool_stat[:last_error], priority: calculate_priority(tool_stat[:failure_count], :failure) } end end |
.detect_stale_candidates ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/legion/mcp/gap_detector.rb', line 71 def detect_stale_candidates return [] unless defined?(PatternStore) candidates = PatternStore.candidates candidates.filter_map do |intent_hash, entry| next if entry[:count] < 2 { id: "stale:#{intent_hash[0, 12]}", type: :stale_candidate, intent_hash: intent_hash, intent_text: entry[:intent_text], observation_count: entry[:count], tool_chain: entry[:tool_chain], priority: calculate_priority(entry[:count], :stale) } end end |
.detect_unmatched_intents ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/legion/mcp/gap_detector.rb', line 24 def detect_unmatched_intents return [] unless defined?(Observer) recent = Observer.recent_intents(200) unmatched = recent.select { |r| r[:matched_tool].nil? || r[:matched_tool] == 'none' } grouped = unmatched.group_by { |r| normalize_intent(r[:intent]) } grouped.filter_map do |intent_text, occurrences| next if occurrences.size < GAP_INTENT_THRESHOLD { id: "unmatched:#{Digest::SHA256.hexdigest(intent_text)[0, 12]}", type: :unmatched_intent, intent: intent_text, occurrences: occurrences.size, first_seen: occurrences.first[:recorded_at], last_seen: occurrences.last[:recorded_at], priority: calculate_priority(occurrences.size, :unmatched) } end end |
.normalize_intent(text) ⇒ Object
91 92 93 |
# File 'lib/legion/mcp/gap_detector.rb', line 91 def normalize_intent(text) text.to_s.strip.downcase.gsub(/\s+/, ' ') end |