Class: Legion::CLI::Chat::Tools::ViewTrends

Inherits:
Tools::Base
  • Object
show all
Defined in:
lib/legion/cli/chat/tools/view_trends.rb

Constant Summary collapse

DEFAULT_PORT =
4567
DEFAULT_HOST =
'127.0.0.1'

Class Method Summary collapse

Methods inherited from Tools::Base

deferred, deferred?, description, error_response, extension, handle_exception, input_schema, log, mcp_category, mcp_tier, runner, sticky, tags, text_response, tool_name, trigger_words

Class Method Details

.api_get(path) ⇒ Object



116
117
118
119
120
121
122
123
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 116

def self.api_get(path)
  uri = URI("http://#{DEFAULT_HOST}:#{api_port}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = 2
  http.read_timeout = 10
  response = http.get(uri.request_uri)
  ::JSON.parse(response.body, symbolize_names: true)
end

.api_portObject



125
126
127
128
129
130
131
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 125

def self.api_port
  return DEFAULT_PORT unless defined?(Legion::Settings)

  Legion::Settings[:api]&.dig(:port) || DEFAULT_PORT
rescue StandardError
  DEFAULT_PORT
end

.avg_metric(buckets, key) ⇒ Object



94
95
96
97
98
99
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 94

def self.avg_metric(buckets, key)
  values = buckets.map { |b| (b[key] || 0).to_f }
  return 0.0 if values.empty?

  values.sum / values.size
end

.call(hours: 24, buckets: 12) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 33

def self.call(hours: 24, buckets: 12)
  hours = hours.to_i.clamp(1, 168)
  buckets = buckets.to_i.clamp(2, 48)

  data = api_get("/api/traces/trend?hours=#{hours}&buckets=#{buckets}")
  return "API error: #{data[:error][:message]}" if data[:error]

  format_trend(data[:data] || data)
rescue Errno::ECONNREFUSED
  'Legion daemon not running (cannot reach trend API).'
rescue StandardError => e
  Legion::Logging.warn("ViewTrends#execute failed: #{e.message}") if defined?(Legion::Logging)
  "Error fetching trends: #{e.message}"
end

.direction_label(name, first_avg, second_avg) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 101

def self.direction_label(name, first_avg, second_avg)
  return "#{name}: stable" if first_avg.zero? && second_avg.zero?
  return "#{name}: rising" if first_avg.zero?

  change = ((second_avg - first_avg) / first_avg * 100).round(0)
  arrow = if change > 10
            'rising'
          elsif change < -10
            'falling'
          else
            'stable'
          end
  "#{name}: #{arrow} (#{'+' if change.positive?}#{change}%)"
end

.format_time(iso_str) ⇒ Object



72
73
74
75
76
77
78
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 72

def self.format_time(iso_str)
  return iso_str unless iso_str.is_a?(String)

  Time.parse(iso_str).strftime('%m/%d %H:%M')
rescue ArgumentError
  iso_str
end

.format_trend(data) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 48

def self.format_trend(data)
  trend_buckets = data[:buckets] || []
  return 'No trend data available.' if trend_buckets.empty?

  mins = data[:bucket_minutes] || 120
  lines = ["Trend (last #{data[:hours]}h, #{mins}min buckets):\n"]
  lines << '  Time                  Count   Avg Cost    Avg Lat  Fail%'
  lines << "  #{'' * 56}"

  trend_buckets.each do |b|
    time = format_time(b[:time])
    count = b[:count] || 0
    cost = format('$%.4f', (b[:avg_cost] || 0).to_f)
    latency = format('%.0fms', (b[:avg_latency] || 0).to_f)
    fail_pct = format('%.1f%%', (b[:failure_rate] || 0).to_f * 100)
    lines << format('  %-20<time>s %6<count>d %10<cost>s %10<latency>s %6<fail>s',
                    time: time, count: count, cost: cost, latency: latency, fail: fail_pct)
  end

  lines << ''
  lines << summarize_direction(trend_buckets)
  lines.join("\n")
end

.summarize_direction(trend_buckets) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/legion/cli/chat/tools/view_trends.rb', line 80

def self.summarize_direction(trend_buckets)
  return '' if trend_buckets.size < 2

  first_half = trend_buckets[0...(trend_buckets.size / 2)]
  second_half = trend_buckets[(trend_buckets.size / 2)..]

  directions = []
  directions << direction_label('Volume', avg_metric(first_half, :count), avg_metric(second_half, :count))
  directions << direction_label('Cost', avg_metric(first_half, :avg_cost), avg_metric(second_half, :avg_cost))
  directions << direction_label('Latency', avg_metric(first_half, :avg_latency),
                                avg_metric(second_half, :avg_latency))
  "  Direction: #{directions.join(' | ')}"
end