Module: Legion::MCP::UsageFilter

Defined in:
lib/legion/mcp/usage_filter.rb

Constant Summary collapse

ESSENTIAL_TOOLS =
%w[
  legion.do legion.tools legion.run_task legion.get_status legion.describe_runner
].freeze
FREQUENCY_WEIGHT =
0.5
RECENCY_WEIGHT =
0.3
KEYWORD_WEIGHT =
0.2
BASELINE_SCORE =
0.1

Class Method Summary collapse

Class Method Details

.keyword_match(tool_name, keywords) ⇒ Object



78
79
80
81
82
83
# File 'lib/legion/mcp/usage_filter.rb', line 78

def keyword_match(tool_name, keywords)
  return 0.0 if keywords.nil? || keywords.empty?

  hits = keywords.count { |kw| tool_name.include?(kw.to_s) }
  hits.to_f / keywords.size
end

.prune_dead_tools(tool_names, prune_after_seconds: 86_400 * 30) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/legion/mcp/usage_filter.rb', line 52

def prune_dead_tools(tool_names, prune_after_seconds: 86_400 * 30)
  stats = Observer.stats
  window = stats[:since]
  elapsed = window ? (Time.now - window) : 0

  return tool_names if elapsed < prune_after_seconds

  all_stats = Observer.all_tool_stats
  tool_names.reject do |name|
    next false if ESSENTIAL_TOOLS.include?(name)

    calls = all_stats.dig(name, :call_count) || 0
    calls.zero?
  end
end

.ranked_tools(tool_names, limit: nil, keywords: []) ⇒ Object



46
47
48
49
50
# File 'lib/legion/mcp/usage_filter.rb', line 46

def ranked_tools(tool_names, limit: nil, keywords: [])
  scores = score_tools(tool_names, keywords: keywords)
  ranked = tool_names.sort_by { |n| -scores.fetch(n, BASELINE_SCORE) }
  limit ? ranked.first(limit) : ranked
end

.recency_decay(last_used) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/legion/mcp/usage_filter.rb', line 68

def recency_decay(last_used)
  return 0.0 unless last_used

  age_seconds = Time.now - last_used
  return 1.0 if age_seconds <= 0

  decay = 1.0 - (age_seconds / 86_400.0)
  decay.clamp(0.0, 1.0)
end

.score_tools(tool_names, keywords: []) ⇒ Object



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
# File 'lib/legion/mcp/usage_filter.rb', line 17

def score_tools(tool_names, keywords: [])
  all_stats = Observer.all_tool_stats
  call_counts = tool_names.map { |n| all_stats.dig(n, :call_count) || 0 }
  max_calls   = call_counts.max || 0

  tool_names.each_with_object({}) do |name, hash|
    stats = all_stats[name]

    freq_score = if max_calls.positive? && stats
                   (stats[:call_count].to_f / max_calls) * FREQUENCY_WEIGHT
                 else
                   0.0
                 end

    rec_score = if stats&.dig(:last_used)
                  recency_decay(stats[:last_used]) * RECENCY_WEIGHT
                else
                  0.0
                end

    kw_score = keyword_match(name, keywords) * KEYWORD_WEIGHT

    total = freq_score + rec_score + kw_score
    total = BASELINE_SCORE if total.zero?

    hash[name] = total.round(6)
  end
end