Class: Legion::CLI::Chat::Tools::GenerateInsights

Inherits:
Tools::Base
  • Object
show all
Defined in:
lib/legion/cli/chat/tools/generate_insights.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

.add_anomaly_recs(recs, data) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 198

def self.add_anomaly_recs(recs, data)
  return unless data

  anomalies = (data[:data] || data)[:anomalies] || []
  anomalies.each do |a|
    case a[:metric]
    when /cost/i
      recs << 'Review recent high-cost operations — consider model downgrade for non-critical tasks'
    when /latency/i
      recs << 'Investigate latency spike — check provider health or fleet worker load'
    when /failure/i
      recs << 'Elevated failure rate — check extension health and transport connectivity'
    end
  end
end

.add_trend_recs(recs, data) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 214

def self.add_trend_recs(recs, data)
  return unless data

  buckets = (data[:data] || data)[:buckets] || []
  return if buckets.size < 2

  last = buckets.last
  recs << 'Failure rate above 10% in most recent period — investigate immediately' if last[:failure_rate].to_f > 0.1
  return unless last[:count].to_i.zero? && buckets.size > 2

  recs << 'No recent activity detected — verify daemon extensions are running'
end

.api_get(path) ⇒ Object



243
244
245
246
247
248
249
250
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 243

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 = 5
  response = http.get(uri.request_uri)
  ::JSON.parse(response.body, symbolize_names: true)
end

.api_portObject



252
253
254
255
256
257
258
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 252

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

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

.callObject



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 26

def self.call
  sections = gather_sections
  return 'Legion daemon not running (cannot reach API).' if sections.values.all?(&:nil?)

  format_insights(sections)
rescue Errno::ECONNREFUSED
  'Legion daemon not running (cannot reach API).'
rescue StandardError => e
  Legion::Logging.warn("GenerateInsights#execute failed: #{e.message}") if defined?(Legion::Logging)
  "Error generating insights: #{e.message}"
end

.format_anomaly_section(data) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 78

def self.format_anomaly_section(data)
  return nil unless data

  d = data[:data] || data
  anomalies = d[:anomalies] || []
  if anomalies.empty?
    'Anomalies: None detected (system nominal)'
  else
    items = anomalies.map { |a| "  - [#{(a[:severity] || 'warning').upcase}] #{a[:metric]} (#{a[:ratio]}x)" }
    "Anomalies (#{anomalies.size}):\n#{items.join("\n")}"
  end
end

.format_apollo_section(data) ⇒ Object



106
107
108
109
110
111
112
113
114
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 106

def self.format_apollo_section(data)
  return nil unless data

  d = data[:data] || data
  return nil if d[:error]

  "Knowledge: #{d[:total_entries] || 0} entries | 24h: #{d[:recent_24h] || 0} | " \
    "Confidence: #{d[:avg_confidence] || 0}"
end

.format_graph_section(data) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 127

def self.format_graph_section(data)
  return nil unless data

  d = data[:data] || data
  return nil if d[:error]

  disputed = d[:disputed_entries] || 0
  domains = (d[:domains] || {}).size
  relations = d[:total_relations] || 0

  "Graph: #{domains} domains | #{relations} relations | #{disputed} disputed"
end

.format_health(data) ⇒ Object



71
72
73
74
75
76
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 71

def self.format_health(data)
  return nil unless data

  d = data[:data] || data
  "Health: #{d[:status] || 'unknown'} | Version: #{d[:version] || '?'}"
end

.format_insights(sections) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 57

def self.format_insights(sections)
  lines = ["System Insights Report\n"]
  lines << format_health(sections[:health])
  lines << format_anomaly_section(sections[:anomalies])
  lines << format_trend_section(sections[:trend])
  lines << format_apollo_section(sections[:apollo])
  lines << format_graph_section(sections[:graph])
  lines << format_worker_section(sections[:workers])
  lines << format_scheduling_section(sections[:scheduling])
  lines << format_llm_section(sections[:llm])
  lines << recommendations(sections)
  lines.compact.join("\n\n")
end

.format_llm_section(data) ⇒ Object



149
150
151
152
153
154
155
156
157
158
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 149

def self.format_llm_section(data)
  return nil unless data

  parts = []
  parts << "Escalations: #{data[:escalations]}" if data[:escalations]
  parts << "Shadow evals: #{data[:shadow_evals]}" if data[:shadow_evals]
  return nil if parts.empty?

  "LLM: #{parts.join(' | ')}"
end

.format_scheduling_section(data) ⇒ Object



140
141
142
143
144
145
146
147
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 140

def self.format_scheduling_section(data)
  return nil unless data

  peak = data[:peak_hours] ? 'PEAK' : 'off-peak'
  batch_size = data.dig(:batch, :queue_size) || 0

  "Scheduling: #{peak} | Batch queue: #{batch_size}"
end

.format_trend_section(data) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 91

def self.format_trend_section(data)
  return nil unless data

  d = data[:data] || data
  buckets = d[:buckets] || []
  return nil if buckets.empty?

  first = buckets.first
  last = buckets.last
  vol_change = percent_change(first[:count], last[:count])
  cost_change = percent_change(first[:avg_cost], last[:avg_cost])

  "Trend (24h): Volume #{vol_change} | Cost #{cost_change}"
end

.format_worker_section(data) ⇒ Object



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

def self.format_worker_section(data)
  return nil unless data

  workers = data[:data] || []
  workers = Array(workers)
  return nil if workers.empty?

  active = workers.count { |w| w[:lifecycle_state] == 'active' }
  "Workers: #{active}/#{workers.size} active"
end

.gather_sectionsObject



38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 38

def self.gather_sections
  {
    health:     safe_fetch('/api/health'),
    anomalies:  safe_fetch('/api/traces/anomalies'),
    trend:      safe_fetch('/api/traces/trend?hours=24&buckets=6'),
    apollo:     safe_fetch('/api/apollo/stats'),
    graph:      safe_fetch('/api/apollo/graph'),
    workers:    safe_fetch('/api/workers'),
    scheduling: scheduling_status,
    llm:        llm_status
  }
end

.llm_statusObject



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 173

def self.llm_status
  result = {}
  if defined?(Legion::LLM::EscalationTracker)
    s = Legion::LLM::EscalationTracker.summary
    result[:escalations] = s[:total_escalations]
  end
  if defined?(Legion::LLM::ShadowEval)
    s = Legion::LLM::ShadowEval.summary
    result[:shadow_evals] = s[:total_evaluations]
  end
  result.empty? ? nil : result
rescue StandardError => e
  Legion::Logging.debug("GenerateInsights#llm_status failed: #{e.message}") if defined?(Legion::Logging)
  nil
end

.percent_change(first_val, last_val) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 227

def self.percent_change(first_val, last_val)
  f = (first_val || 0).to_f
  l = (last_val || 0).to_f
  return 'stable' if f.zero? && l.zero?
  return 'rising' if f.zero?

  pct = ((l - f) / f * 100).round(0)
  if pct > 10
    "rising (+#{pct}%)"
  elsif pct < -10
    "falling (#{pct}%)"
  else
    "stable (#{pct}%)"
  end
end

.recommendations(sections) ⇒ Object



189
190
191
192
193
194
195
196
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 189

def self.recommendations(sections)
  recs = []
  add_anomaly_recs(recs, sections[:anomalies])
  add_trend_recs(recs, sections[:trend])
  return nil if recs.empty?

  "Recommendations:\n#{recs.map { |r| "  * #{r}" }.join("\n")}"
end

.safe_fetch(path) ⇒ Object



51
52
53
54
55
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 51

def self.safe_fetch(path)
  api_get(path)
rescue StandardError
  nil
end

.scheduling_statusObject



160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/legion/cli/chat/tools/generate_insights.rb', line 160

def self.scheduling_status
  result = {}
  if defined?(Legion::LLM::Scheduling)
    s = Legion::LLM::Scheduling.status
    result.merge!(s)
  end
  result[:batch] = Legion::LLM::Batch.status if defined?(Legion::LLM::Batch)
  result.empty? ? nil : result
rescue StandardError => e
  Legion::Logging.debug("GenerateInsights#scheduling_status failed: #{e.message}") if defined?(Legion::Logging)
  nil
end