debug-agent 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +46 -5
- data/lib/debug_agent/inspectors/active_record_stats.rb +131 -0
- data/lib/debug_agent/inspectors/cache.rb +194 -0
- data/lib/debug_agent/inspectors/concurrent.rb +78 -0
- data/lib/debug_agent/inspectors/error_tracking.rb +120 -0
- data/lib/debug_agent/inspectors/faraday.rb +79 -0
- data/lib/debug_agent/inspectors/gc.rb +53 -0
- data/lib/debug_agent/inspectors/health.rb +110 -0
- data/lib/debug_agent/inspectors/http_client.rb +145 -0
- data/lib/debug_agent/inspectors/logging.rb +163 -0
- data/lib/debug_agent/inspectors/metrics.rb +71 -0
- data/lib/debug_agent/inspectors/scheduler.rb +154 -0
- data/lib/debug_agent/inspectors/security.rb +201 -0
- data/lib/debug_agent/inspectors/websocket.rb +188 -0
- data/lib/debug_agent/llm_client.rb +56 -56
- data/lib/debug_agent/version.rb +1 -1
- data/lib/debug_agent.rb +12 -0
- metadata +13 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
module DebugAgent
|
|
2
|
+
# Registry of auth configurations (Devise, Warden, OmniAuth, Rack auth).
|
|
3
|
+
#
|
|
4
|
+
# DebugAgent.register_auth_config(:api_key, { strategy: 'X-API-Key', secret_present: true })
|
|
5
|
+
@auth_configs = {}
|
|
6
|
+
|
|
7
|
+
# Registry of session stores (Rack::Session, ActiveRecord::Session, custom).
|
|
8
|
+
#
|
|
9
|
+
# DebugAgent.register_session_store(:rack_session, env['rack.session'])
|
|
10
|
+
@session_stores = {}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :auth_configs, :session_stores
|
|
14
|
+
|
|
15
|
+
def register_auth_config(name, config)
|
|
16
|
+
@auth_configs[name.to_s] = config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register_session_store(name, store)
|
|
20
|
+
@session_stores[name.to_s] = store
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
register_tool('get_auth_config',
|
|
25
|
+
'List registered auth configurations (Devise, Warden, OmniAuth, Rack auth). ' \
|
|
26
|
+
'Shows strategy, whether a secret is present, and token expiry') do |name: nil|
|
|
27
|
+
# Auto-detect from loaded gems when nothing is registered
|
|
28
|
+
if auth_configs.empty?
|
|
29
|
+
auto = {}
|
|
30
|
+
if defined?(::Devise)
|
|
31
|
+
auto['Devise'] = {
|
|
32
|
+
strategy: 'Devise',
|
|
33
|
+
secret_present: defined?(Devise.secret_key) && !Devise.secret_key.to_s.empty?,
|
|
34
|
+
models: (Devise.mappings.keys.map(&:to_s) rescue []),
|
|
35
|
+
token_expiry: nil
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
if defined?(::Warden)
|
|
39
|
+
auto['Warden'] = {
|
|
40
|
+
strategy: 'Warden',
|
|
41
|
+
secret_present: !!(Warden::Manager.respond_to?(:secret) rescue false),
|
|
42
|
+
default_strategies: (Warden::Config.new.default_strategies rescue []),
|
|
43
|
+
token_expiry: nil
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
if defined?(::OmniAuth)
|
|
47
|
+
auto['OmniAuth'] = {
|
|
48
|
+
strategy: 'OmniAuth',
|
|
49
|
+
providers: (OmniAuth.strategies.map(&:to_s) rescue []),
|
|
50
|
+
secret_present: !!(OmniAuth.configuration rescue nil),
|
|
51
|
+
token_expiry: nil
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
if auto.any?
|
|
55
|
+
next { auth_configs: auto, source: 'auto-detected' }
|
|
56
|
+
end
|
|
57
|
+
next { error: 'No auth configs registered. Call DebugAgent.register_auth_config(:name, config).' }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
targets = name ? { name.to_s => auth_configs[name.to_s] } : auth_configs
|
|
61
|
+
targets = targets.reject { |_, c| c.nil? }
|
|
62
|
+
next { error: "No auth config registered under '#{name}'" } if targets.empty?
|
|
63
|
+
|
|
64
|
+
results = targets.map do |cfg_name, cfg|
|
|
65
|
+
begin
|
|
66
|
+
if cfg.is_a?(Hash)
|
|
67
|
+
normalized = {
|
|
68
|
+
name: cfg_name,
|
|
69
|
+
strategy: cfg[:strategy] || cfg['strategy'],
|
|
70
|
+
secret_present: cfg.key?(:secret_present) ? cfg[:secret_present] : cfg['secret_present'],
|
|
71
|
+
token_expiry: cfg[:token_expiry] || cfg['token_expiry']
|
|
72
|
+
}.merge(cfg.reject { |k, _| %i[strategy secret_present token_expiry].include?(k.to_sym) })
|
|
73
|
+
normalized
|
|
74
|
+
else
|
|
75
|
+
{ name: cfg_name, type: cfg.class.name, raw: cfg.inspect }
|
|
76
|
+
end
|
|
77
|
+
rescue => e
|
|
78
|
+
{ name: cfg_name, error: e.message }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
{ auth_configs: results }
|
|
83
|
+
rescue => e
|
|
84
|
+
{ error: e.message }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
register_tool('get_active_sessions',
|
|
88
|
+
'List active sessions from registered session stores ' \
|
|
89
|
+
'(Rack::Session, ActiveRecord::Session). ' \
|
|
90
|
+
'Shows session ID, user, creation, and expiry') do |name: nil|
|
|
91
|
+
if session_stores.empty?
|
|
92
|
+
next { error: 'No session stores registered. Call DebugAgent.register_session_store(:name, store).' }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
targets = name ? { name.to_s => session_stores[name.to_s] } : session_stores
|
|
96
|
+
targets = targets.reject { |_, s| s.nil? }
|
|
97
|
+
next { error: "No session store registered under '#{name}'" } if targets.empty?
|
|
98
|
+
|
|
99
|
+
results = targets.map do |store_name, store|
|
|
100
|
+
begin
|
|
101
|
+
introspect_session_store(store_name, store)
|
|
102
|
+
rescue => e
|
|
103
|
+
{ name: store_name, error: e.message }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
{ session_stores: results }
|
|
108
|
+
rescue => e
|
|
109
|
+
{ error: e.message }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
register_tool('get_cors_config',
|
|
113
|
+
'Show CORS settings (Rack::Cors config: allowed origins, methods, headers)') do
|
|
114
|
+
# Try to find Rack::Cors middleware in the app middleware stack
|
|
115
|
+
cors_rules = []
|
|
116
|
+
|
|
117
|
+
if app && app.respond_to?(:middleware)
|
|
118
|
+
app.middleware.each do |middleware_entry|
|
|
119
|
+
middleware_class = middleware_entry.shift if middleware_entry.is_a?(Array)
|
|
120
|
+
next unless middleware_class.to_s.include?('Cors')
|
|
121
|
+
|
|
122
|
+
args = middleware_entry || []
|
|
123
|
+
cors_rules << { middleware: middleware_class.to_s, args: args.inspect }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Try Rack::Cors introspection
|
|
128
|
+
if defined?(::Rack::Cors)
|
|
129
|
+
begin
|
|
130
|
+
rack_cors = ::Rack::Cors
|
|
131
|
+
cors_rules << { middleware: 'Rack::Cors', version: rack_cors.respond_to?(:VERSION) ? rack_cors::VERSION : 'unknown' }
|
|
132
|
+
rescue => e
|
|
133
|
+
cors_rules << { middleware: 'Rack::Cors', error: e.message }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if cors_rules.empty?
|
|
138
|
+
next {
|
|
139
|
+
message: 'No CORS configuration detected. Install rack-cors and configure Rack::Cors middleware, ' \
|
|
140
|
+
'or the inspector could not find CORS rules in the middleware stack.',
|
|
141
|
+
cors_enabled: false
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
{ cors_enabled: true, rules: cors_rules }
|
|
146
|
+
rescue => e
|
|
147
|
+
{ error: e.message }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# --- Helpers ---
|
|
151
|
+
|
|
152
|
+
def self.introspect_session_store(store_name, store)
|
|
153
|
+
info = { name: store_name, type: store.class.name }
|
|
154
|
+
|
|
155
|
+
# Rack::Session::Abstract::SessionHash or similar
|
|
156
|
+
if store.respond_to?(:to_hash)
|
|
157
|
+
begin
|
|
158
|
+
data = store.to_hash
|
|
159
|
+
info[:session_count] = data.size
|
|
160
|
+
info[:keys] = data.keys.first(50)
|
|
161
|
+
info[:has_user] = data.key?('user_id') || data.key?(:user_id) || data.key?('user')
|
|
162
|
+
rescue
|
|
163
|
+
info[:session_count] = 'unable to read'
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# ActiveRecord::SessionStore
|
|
168
|
+
if store.respond_to?(:all)
|
|
169
|
+
begin
|
|
170
|
+
sessions = store.all.to_a
|
|
171
|
+
info[:session_count] = sessions.size
|
|
172
|
+
info[:sessions] = sessions.first(20).map do |sess|
|
|
173
|
+
sess_data = begin
|
|
174
|
+
sess.respond_to?(:data) ? sess.data.keys : []
|
|
175
|
+
rescue
|
|
176
|
+
[]
|
|
177
|
+
end
|
|
178
|
+
{
|
|
179
|
+
session_id: sess.respond_to?(:session_id) ? sess.session_id : sess.id,
|
|
180
|
+
data: sess_data,
|
|
181
|
+
created_at: sess.respond_to?(:created_at) ? sess.created_at : nil,
|
|
182
|
+
updated_at: sess.respond_to?(:updated_at) ? sess.updated_at : nil
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
rescue
|
|
186
|
+
info[:session_count] = 'unable to read'
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Generic session store with session_id method
|
|
191
|
+
if store.respond_to?(:session_id)
|
|
192
|
+
begin
|
|
193
|
+
info[:session_id] = store.session_id
|
|
194
|
+
rescue
|
|
195
|
+
# ignore
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
info
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'thread'
|
|
3
|
+
|
|
4
|
+
module DebugAgent
|
|
5
|
+
# Registry of WebSocket servers (faye-websocket, websocket-driver, ActionCable).
|
|
6
|
+
# Applications register WS server objects so the inspector can introspect them.
|
|
7
|
+
#
|
|
8
|
+
# DebugAgent.register_ws_server(:faye, ws_server)
|
|
9
|
+
@ws_servers = {}
|
|
10
|
+
@ws_connections = []
|
|
11
|
+
@ws_lock = Mutex.new
|
|
12
|
+
@ws_message_log = []
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_reader :ws_servers, :ws_connections, :ws_message_log
|
|
16
|
+
|
|
17
|
+
def register_ws_server(name, server)
|
|
18
|
+
@ws_servers[name.to_s] = server
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Track a WebSocket connection
|
|
22
|
+
def track_ws_connection(conn_id, remote_addr, channel = nil)
|
|
23
|
+
@ws_lock.synchronize do
|
|
24
|
+
@ws_connections << {
|
|
25
|
+
id: conn_id,
|
|
26
|
+
remote_addr: remote_addr,
|
|
27
|
+
channel: channel,
|
|
28
|
+
connected_since: Time.now.iso8601,
|
|
29
|
+
messages_sent: 0,
|
|
30
|
+
messages_received: 0
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
conn_id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Remove a tracked WebSocket connection
|
|
37
|
+
def untrack_ws_connection(conn_id)
|
|
38
|
+
@ws_lock.synchronize do
|
|
39
|
+
@ws_connections.reject! { |c| c[:id] == conn_id }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Log a WebSocket message
|
|
44
|
+
def log_ws_message(conn_id, direction, data, size = nil)
|
|
45
|
+
@ws_lock.synchronize do
|
|
46
|
+
@ws_message_log << {
|
|
47
|
+
timestamp: Time.now.iso8601,
|
|
48
|
+
connection_id: conn_id,
|
|
49
|
+
direction: direction, # 'sent' or 'received'
|
|
50
|
+
size: size || data.to_s.bytesize,
|
|
51
|
+
preview: data.to_s[0...200]
|
|
52
|
+
}
|
|
53
|
+
@ws_message_log.shift if @ws_message_log.size > 200
|
|
54
|
+
|
|
55
|
+
conn = @ws_connections.find { |c| c[:id] == conn_id }
|
|
56
|
+
if conn
|
|
57
|
+
if direction == 'sent'
|
|
58
|
+
conn[:messages_sent] += 1
|
|
59
|
+
else
|
|
60
|
+
conn[:messages_received] += 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
register_tool('get_ws_connections',
|
|
68
|
+
'Get active WebSocket connections (faye-websocket, websocket-driver, ActionCable). ' \
|
|
69
|
+
'Shows connection ID, remote address, connected since, and message counts') do
|
|
70
|
+
conns = @ws_lock.synchronize { @ws_connections.dup }
|
|
71
|
+
|
|
72
|
+
# Also try to auto-detect ActionCable connections
|
|
73
|
+
if defined?(::ActionCable)
|
|
74
|
+
begin
|
|
75
|
+
server = ::ActionCable.server
|
|
76
|
+
if server && server.respond_to?(:connections)
|
|
77
|
+
server.connections.each do |conn|
|
|
78
|
+
next if conns.any? { |c| c[:id] == conn.object_id }
|
|
79
|
+
conns << {
|
|
80
|
+
id: conn.object_id,
|
|
81
|
+
remote_addr: conn.respond_to?(:env) ? (conn.env['REMOTE_ADDR'] rescue 'unknown') : 'unknown',
|
|
82
|
+
channel: 'ActionCable',
|
|
83
|
+
connected_since: nil,
|
|
84
|
+
messages_sent: conn.respond_to?(:transmissions) ? conn.transmissions : nil,
|
|
85
|
+
messages_received: nil,
|
|
86
|
+
source: 'actioncable'
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
rescue => e
|
|
91
|
+
conns << { source: 'actioncable', error: e.message }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if conns.empty?
|
|
96
|
+
next {
|
|
97
|
+
message: 'No WebSocket connections tracked. Register with DebugAgent.register_ws_server ' \
|
|
98
|
+
'or DebugAgent.track_ws_connection. Auto-detects ActionCable if loaded.',
|
|
99
|
+
total: 0
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
{ total: conns.size, connections: conns }
|
|
104
|
+
rescue => e
|
|
105
|
+
{ error: e.message }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
register_tool('get_ws_stats',
|
|
109
|
+
'Get WebSocket statistics: total connections, total messages, ' \
|
|
110
|
+
'messages per connection, uptime') do
|
|
111
|
+
conns = @ws_lock.synchronize { @ws_connections.dup }
|
|
112
|
+
msgs = @ws_lock.synchronize { @ws_message_log.dup }
|
|
113
|
+
|
|
114
|
+
if conns.empty? && msgs.empty?
|
|
115
|
+
next { total_connections: 0, total_messages: 0, message: 'No WebSocket activity recorded.' }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sent = conns.sum { |c| c[:messages_sent] || 0 }
|
|
119
|
+
received = conns.sum { |c| c[:messages_received] || 0 }
|
|
120
|
+
total_bytes = msgs.sum { |m| m[:size] || 0 }
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
total_connections: conns.size,
|
|
124
|
+
total_messages_sent: sent,
|
|
125
|
+
total_messages_received: received,
|
|
126
|
+
total_messages: sent + received,
|
|
127
|
+
total_bytes: total_bytes,
|
|
128
|
+
avg_messages_per_connection: conns.empty? ? 0 : ((sent + received).to_f / conns.size).round(2),
|
|
129
|
+
registered_servers: ws_servers.keys,
|
|
130
|
+
recent_messages: msgs.reverse.first(20)
|
|
131
|
+
}
|
|
132
|
+
rescue => e
|
|
133
|
+
{ error: e.message }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
register_tool('get_ws_channels',
|
|
137
|
+
'Get WebSocket channels with subscriber counts (ActionCable channels or custom pub/sub)') do
|
|
138
|
+
channels = []
|
|
139
|
+
|
|
140
|
+
# Auto-detect ActionCable channels
|
|
141
|
+
if defined?(::ActionCable::Channel::Base)
|
|
142
|
+
begin
|
|
143
|
+
# Look for channel subclasses
|
|
144
|
+
channel_classes = []
|
|
145
|
+
ObjectSpace.each_object(Class) do |klass|
|
|
146
|
+
if klass < ::ActionCable::Channel::Base && klass != ::ActionCable::Channel::Base
|
|
147
|
+
channel_classes << klass
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
channel_classes.uniq.each do |klass|
|
|
152
|
+
channels << {
|
|
153
|
+
name: klass.name,
|
|
154
|
+
source: 'actioncable',
|
|
155
|
+
subscribers: 0 # ActionCable doesn't expose per-channel counts easily
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
rescue => e
|
|
159
|
+
channels << { source: 'actioncable', error: e.message }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Track connections grouped by channel from our own registry
|
|
164
|
+
@ws_lock.synchronize do
|
|
165
|
+
by_channel = @ws_connections.group_by { |c| c[:channel] || 'default' }
|
|
166
|
+
by_channel.each do |channel, conns|
|
|
167
|
+
existing = channels.find { |ch| ch[:name] == channel }
|
|
168
|
+
if existing
|
|
169
|
+
existing[:subscribers] = conns.size
|
|
170
|
+
else
|
|
171
|
+
channels << { name: channel, source: 'tracked', subscribers: conns.size }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if channels.empty?
|
|
177
|
+
next {
|
|
178
|
+
message: 'No WebSocket channels found. Define ActionCable channels or use ' \
|
|
179
|
+
'DebugAgent.track_ws_connection with a channel name.',
|
|
180
|
+
total: 0
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
{ total: channels.size, channels: channels }
|
|
185
|
+
rescue => e
|
|
186
|
+
{ error: e.message }
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -83,72 +83,72 @@ module DebugAgent
|
|
|
83
83
|
|
|
84
84
|
def stream_request(path, body, handler)
|
|
85
85
|
uri = URI(@cfg.base_url + path)
|
|
86
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
87
|
-
http.use_ssl = uri.scheme == 'https'
|
|
88
|
-
http.read_timeout = @cfg.timeout_seconds
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
response = http.request(request)
|
|
96
|
-
|
|
97
|
-
if response.code.to_i >= 400
|
|
98
|
-
raise RetriableError.new(response.code.to_i, "HTTP #{response.code}: #{response.body}")
|
|
99
|
-
end
|
|
87
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: @cfg.timeout_seconds) do |http|
|
|
88
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
89
|
+
request['Authorization'] = "Bearer #{@cfg.api_key}"
|
|
90
|
+
request['Content-Type'] = 'application/json'
|
|
91
|
+
request.body = JSON.generate(body)
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
tool_call_map = {}
|
|
94
|
+
finish_reason = nil
|
|
95
|
+
usage = nil
|
|
104
96
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
data_str = line[6..]
|
|
110
|
-
next if data_str.strip == '[DONE]'
|
|
111
|
-
|
|
112
|
-
begin
|
|
113
|
-
parsed = JSON.parse(data_str)
|
|
114
|
-
rescue JSON::ParserError
|
|
115
|
-
next
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
if parsed['usage'] && parsed['usage']['prompt_tokens']
|
|
119
|
-
usage = parsed['usage']
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
choices = parsed['choices'] || []
|
|
123
|
-
next if choices.empty?
|
|
124
|
-
|
|
125
|
-
choice = choices[0]
|
|
126
|
-
delta = choice['delta'] || {}
|
|
127
|
-
|
|
128
|
-
if delta['content'] && !delta['content'].empty?
|
|
129
|
-
handler.on_content(delta['content'])
|
|
97
|
+
http.request(request) do |response|
|
|
98
|
+
if response.code.to_i >= 400
|
|
99
|
+
err_body = response.read_body
|
|
100
|
+
raise RetriableError.new(response.code.to_i, "HTTP #{response.code}: #{err_body}")
|
|
130
101
|
end
|
|
131
102
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
103
|
+
response.read_body do |chunk|
|
|
104
|
+
chunk.split("\n").each do |line|
|
|
105
|
+
next unless line.start_with?('data: ')
|
|
106
|
+
|
|
107
|
+
data_str = line[6..]
|
|
108
|
+
next if data_str.strip == '[DONE]'
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
parsed = JSON.parse(data_str)
|
|
112
|
+
rescue JSON::ParserError
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if parsed['usage'] && parsed['usage']['prompt_tokens']
|
|
117
|
+
usage = parsed['usage']
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
choices = parsed['choices'] || []
|
|
121
|
+
next if choices.empty?
|
|
122
|
+
|
|
123
|
+
choice = choices[0]
|
|
124
|
+
delta = choice['delta'] || {}
|
|
125
|
+
|
|
126
|
+
if delta['content'] && !delta['content'].empty?
|
|
127
|
+
handler.on_content(delta['content'])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if delta['tool_calls']
|
|
131
|
+
delta['tool_calls'].each do |tc|
|
|
132
|
+
idx = tc['index'] || 0
|
|
133
|
+
tool_call_map[idx] ||= { 'id' => '', 'type' => 'function', 'function' => { 'name' => '', 'arguments' => '' } }
|
|
134
|
+
entry = tool_call_map[idx]
|
|
135
|
+
entry['id'] = tc['id'] if tc['id']
|
|
136
|
+
entry['type'] = tc['type'] if tc['type']
|
|
137
|
+
fn = tc['function'] || {}
|
|
138
|
+
entry['function']['name'] += fn['name'] if fn['name']
|
|
139
|
+
entry['function']['arguments'] += fn['arguments'] if fn['arguments']
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
finish_reason = choice['finish_reason'] if choice['finish_reason']
|
|
142
144
|
end
|
|
143
145
|
end
|
|
144
|
-
|
|
145
|
-
finish_reason = choice['finish_reason'] if choice['finish_reason']
|
|
146
146
|
end
|
|
147
|
-
end
|
|
148
147
|
|
|
149
|
-
|
|
148
|
+
tool_calls = tool_call_map.keys.sort.map { |k| tool_call_map[k] }.select { |tc| tc['function']['name'] && !tc['function']['name'].empty? }
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
handler.on_complete(tool_calls, finish_reason, usage)
|
|
151
|
+
end
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# ==================== Non-Streaming POST with retry ====================
|
data/lib/debug_agent/version.rb
CHANGED
data/lib/debug_agent.rb
CHANGED
|
@@ -23,6 +23,18 @@ require_relative 'debug_agent/inspectors/redis'
|
|
|
23
23
|
require_relative 'debug_agent/inspectors/rails'
|
|
24
24
|
require_relative 'debug_agent/inspectors/sidekiq'
|
|
25
25
|
require_relative 'debug_agent/inspectors/puma'
|
|
26
|
+
require_relative 'debug_agent/inspectors/logging'
|
|
27
|
+
require_relative 'debug_agent/inspectors/cache'
|
|
28
|
+
require_relative 'debug_agent/inspectors/http_client'
|
|
29
|
+
require_relative 'debug_agent/inspectors/metrics'
|
|
30
|
+
require_relative 'debug_agent/inspectors/active_record_stats'
|
|
31
|
+
require_relative 'debug_agent/inspectors/faraday'
|
|
32
|
+
require_relative 'debug_agent/inspectors/concurrent'
|
|
33
|
+
require_relative 'debug_agent/inspectors/security'
|
|
34
|
+
require_relative 'debug_agent/inspectors/health'
|
|
35
|
+
require_relative 'debug_agent/inspectors/scheduler'
|
|
36
|
+
require_relative 'debug_agent/inspectors/error_tracking'
|
|
37
|
+
require_relative 'debug_agent/inspectors/websocket'
|
|
26
38
|
|
|
27
39
|
module DebugAgent
|
|
28
40
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: debug-agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ggcode
|
|
@@ -164,9 +164,18 @@ files:
|
|
|
164
164
|
- lib/debug_agent/config.rb
|
|
165
165
|
- lib/debug_agent/context_compressor.rb
|
|
166
166
|
- lib/debug_agent/engine.rb
|
|
167
|
+
- lib/debug_agent/inspectors/active_record_stats.rb
|
|
168
|
+
- lib/debug_agent/inspectors/cache.rb
|
|
169
|
+
- lib/debug_agent/inspectors/concurrent.rb
|
|
167
170
|
- lib/debug_agent/inspectors/core_ext.rb
|
|
171
|
+
- lib/debug_agent/inspectors/error_tracking.rb
|
|
172
|
+
- lib/debug_agent/inspectors/faraday.rb
|
|
168
173
|
- lib/debug_agent/inspectors/gc.rb
|
|
174
|
+
- lib/debug_agent/inspectors/health.rb
|
|
175
|
+
- lib/debug_agent/inspectors/http_client.rb
|
|
169
176
|
- lib/debug_agent/inspectors/http_tracker.rb
|
|
177
|
+
- lib/debug_agent/inspectors/logging.rb
|
|
178
|
+
- lib/debug_agent/inspectors/metrics.rb
|
|
170
179
|
- lib/debug_agent/inspectors/object_space.rb
|
|
171
180
|
- lib/debug_agent/inspectors/process_info.rb
|
|
172
181
|
- lib/debug_agent/inspectors/puma.rb
|
|
@@ -174,9 +183,12 @@ files:
|
|
|
174
183
|
- lib/debug_agent/inspectors/redis.rb
|
|
175
184
|
- lib/debug_agent/inspectors/routes.rb
|
|
176
185
|
- lib/debug_agent/inspectors/runtime.rb
|
|
186
|
+
- lib/debug_agent/inspectors/scheduler.rb
|
|
187
|
+
- lib/debug_agent/inspectors/security.rb
|
|
177
188
|
- lib/debug_agent/inspectors/sidekiq.rb
|
|
178
189
|
- lib/debug_agent/inspectors/system.rb
|
|
179
190
|
- lib/debug_agent/inspectors/threads.rb
|
|
191
|
+
- lib/debug_agent/inspectors/websocket.rb
|
|
180
192
|
- lib/debug_agent/llm_client.rb
|
|
181
193
|
- lib/debug_agent/middleware.rb
|
|
182
194
|
- lib/debug_agent/system_prompt_builder.rb
|