Module: Legion::MCP::Observer
- Extended by:
- Logging::Helper
- Defined in:
- lib/legion/mcp/observer.rb
Constant Summary collapse
- RING_BUFFER_MAX =
500- INTENT_BUFFER_MAX =
200
Class Method Summary collapse
- .all_tool_stats ⇒ Object
- .buffer_mutex ⇒ Object
-
.counters ⇒ Object
Internal state accessors.
- .counters_mutex ⇒ Object
- .intent_buffer ⇒ Object
- .intent_mutex ⇒ Object
- .recent(limit = 10) ⇒ Object
- .recent_intents(limit = 10) ⇒ Object
- .record(tool_name:, duration_ms:, success:, params_keys: [], error: nil) ⇒ Object
- .record_intent(intent, matched_tool_name) ⇒ Object
- .record_intent_with_result(intent:, tool_name:, success:, request_id: nil) ⇒ Object
- .reset! ⇒ Object
- .ring_buffer ⇒ Object
- .started_at ⇒ Object
- .stats ⇒ Object
- .tool_stats(tool_name) ⇒ Object
- .try_embed(text) ⇒ Object
Class Method Details
.all_tool_stats ⇒ Object
98 99 100 101 |
# File 'lib/legion/mcp/observer.rb', line 98 def all_tool_stats names = counters_mutex.synchronize { counters.keys.dup } names.to_h { |name| [name, tool_stats(name)] } end |
.buffer_mutex ⇒ Object
151 152 153 |
# File 'lib/legion/mcp/observer.rb', line 151 def buffer_mutex @buffer_mutex ||= Mutex.new end |
.counters ⇒ Object
Internal state accessors
139 140 141 |
# File 'lib/legion/mcp/observer.rb', line 139 def counters @counters ||= {} end |
.counters_mutex ⇒ Object
143 144 145 |
# File 'lib/legion/mcp/observer.rb', line 143 def counters_mutex @counters_mutex ||= Mutex.new end |
.intent_buffer ⇒ Object
155 156 157 |
# File 'lib/legion/mcp/observer.rb', line 155 def intent_buffer @intent_buffer ||= [] end |
.intent_mutex ⇒ Object
159 160 161 |
# File 'lib/legion/mcp/observer.rb', line 159 def intent_mutex @intent_mutex ||= Mutex.new end |
.recent(limit = 10) ⇒ Object
123 124 125 |
# File 'lib/legion/mcp/observer.rb', line 123 def recent(limit = 10) buffer_mutex.synchronize { ring_buffer.last(limit) } end |
.recent_intents(limit = 10) ⇒ Object
127 128 129 |
# File 'lib/legion/mcp/observer.rb', line 127 def recent_intents(limit = 10) intent_mutex.synchronize { intent_buffer.last(limit) } end |
.record(tool_name:, duration_ms:, success:, params_keys: [], error: nil) ⇒ Object
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 |
# File 'lib/legion/mcp/observer.rb', line 16 def record(tool_name:, duration_ms:, success:, params_keys: [], error: nil) now = Time.now counters_mutex.synchronize do entry = counters[tool_name] || { call_count: 0, total_latency_ms: 0.0, failure_count: 0, last_used: nil, last_error: nil } counters[tool_name] = { call_count: entry[:call_count] + 1, total_latency_ms: entry[:total_latency_ms] + duration_ms.to_f, failure_count: entry[:failure_count] + (success ? 0 : 1), last_used: now, last_error: success ? entry[:last_error] : error } end buffer_mutex.synchronize do ring_buffer << { tool_name: tool_name, duration_ms: duration_ms, success: success, params_keys: params_keys, error: error, recorded_at: now } ring_buffer.shift if ring_buffer.size > RING_BUFFER_MAX end end |
.record_intent(intent, matched_tool_name) ⇒ Object
44 45 46 47 48 49 |
# File 'lib/legion/mcp/observer.rb', line 44 def record_intent(intent, matched_tool_name) intent_mutex.synchronize do intent_buffer << { intent: intent, matched_tool: matched_tool_name, recorded_at: Time.now } intent_buffer.shift if intent_buffer.size > INTENT_BUFFER_MAX end end |
.record_intent_with_result(intent:, tool_name:, success:, request_id: nil) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/legion/mcp/observer.rb', line 51 def record_intent_with_result(intent:, tool_name:, success:, request_id: nil) log.debug("[mcp][observer] action=record_intent_with_result tool=#{tool_name} success=#{success}") record_intent(intent, tool_name) return unless success return unless defined?(Legion::MCP::Patterns::Store) normalized = intent.to_s.strip.downcase.gsub(/\s+/, ' ') intent_hash = Digest::SHA256.hexdigest(normalized) candidate_key = Digest::SHA256.hexdigest("#{normalized}:#{tool_name}") promotion = Legion::MCP::Patterns::Store.record_candidate( intent_hash: intent_hash, candidate_key: candidate_key, tool_chain: [tool_name], intent_text: intent, request_id: request_id ) return unless promotion&.dig(:promote) Legion::MCP::Patterns::Store.promote_candidate( intent_hash: promotion[:intent_hash], candidate_key: candidate_key, tool_chain: promotion[:tool_chain], intent_text: promotion[:intent_text], intent_vector: (normalized), request_id: request_id ) end |
.reset! ⇒ Object
131 132 133 134 135 136 |
# File 'lib/legion/mcp/observer.rb', line 131 def reset! counters_mutex.synchronize { counters.clear } buffer_mutex.synchronize { ring_buffer.clear } intent_mutex.synchronize { intent_buffer.clear } @started_at = Time.now end |
.ring_buffer ⇒ Object
147 148 149 |
# File 'lib/legion/mcp/observer.rb', line 147 def ring_buffer @ring_buffer ||= [] end |
.started_at ⇒ Object
163 164 165 |
# File 'lib/legion/mcp/observer.rb', line 163 def started_at @started_at ||= Time.now end |
.stats ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/legion/mcp/observer.rb', line 103 def stats all_names = counters_mutex.synchronize { counters.keys.dup } total = all_names.sum { |n| counters_mutex.synchronize { counters[n][:call_count] } } failures = all_names.sum { |n| counters_mutex.synchronize { counters[n][:failure_count] } } rate = total.positive? ? (failures.to_f / total).round(4) : 0.0 top = all_names .map { |n| tool_stats(n) } .sort_by { |s| -s[:call_count] } .first(10) { total_calls: total, tool_count: all_names.size, failure_rate: rate, top_tools: top, since: started_at } end |
.tool_stats(tool_name) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/legion/mcp/observer.rb', line 81 def tool_stats(tool_name) entry = counters_mutex.synchronize { counters[tool_name] } return nil unless entry count = entry[:call_count] avg = count.positive? ? (entry[:total_latency_ms] / count).round(2) : 0.0 { name: tool_name, call_count: count, avg_latency_ms: avg, failure_count: entry[:failure_count], last_used: entry[:last_used], last_error: entry[:last_error] } end |
.try_embed(text) ⇒ Object
167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/legion/mcp/observer.rb', line 167 def (text) return nil unless defined?(Legion::MCP::EmbeddingIndex) = Legion::MCP::EmbeddingIndex.instance_variable_get(:@embedder) return nil unless .call(text) rescue StandardError => e handle_exception(e, level: :debug, operation: 'legion.mcp.observer.try_embed') nil end |