Class: DebugAgent::LLMClient
- Inherits:
-
Object
- Object
- DebugAgent::LLMClient
- Defined in:
- lib/debug_agent/llm_client.rb
Instance Method Summary collapse
-
#calculate_delay(attempt) ⇒ Object
Helpers ====================.
-
#chat(messages, tools = nil) ⇒ Object
Non-Streaming ====================.
-
#chat_stream_raw(messages, tools, tool_choice, handler) ⇒ Object
Streaming ====================.
-
#initialize(config) ⇒ LLMClient
constructor
A new instance of LLMClient.
- #post(path, body) ⇒ Object
-
#post_with_retry(path, body) ⇒ Object
Non-Streaming POST with retry ====================.
-
#stream_request(path, body, handler) ⇒ Object
Stream Processing ====================.
Constructor Details
#initialize(config) ⇒ LLMClient
Returns a new instance of LLMClient.
25 26 27 |
# File 'lib/debug_agent/llm_client.rb', line 25 def initialize(config) @cfg = config end |
Instance Method Details
#calculate_delay(attempt) ⇒ Object
Helpers ====================
201 202 203 204 205 206 |
# File 'lib/debug_agent/llm_client.rb', line 201 def calculate_delay(attempt) base = @cfg.retry_base_delay_ms * (2 ** attempt) jitter = rand(base / 2 + 1) delay = base + jitter [delay, @cfg.retry_max_delay_ms].min end |
#chat(messages, tools = nil) ⇒ Object
Non-Streaming ====================
31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/debug_agent/llm_client.rb', line 31 def chat(, tools = nil) body = { 'model' => @cfg.model, 'messages' => , 'temperature' => 0, 'max_tokens' => 1024 } body['tools'] = tools if tools post_with_retry('/chat/completions', body) end |
#chat_stream_raw(messages, tools, tool_choice, handler) ⇒ Object
Streaming ====================
45 46 47 48 49 50 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 80 |
# File 'lib/debug_agent/llm_client.rb', line 45 def chat_stream_raw(, tools, tool_choice, handler) body = { 'model' => @cfg.model, 'messages' => , 'temperature' => @cfg.temperature, 'max_tokens' => @cfg.max_tokens, 'stream' => true, 'stream_options' => { 'include_usage' => true } } body['tools'] = tools if tools && tools.any? body['tool_choice'] = tool_choice if tool_choice max_retries = @cfg.max_retries last_error = nil (0..max_retries).each do |attempt| begin stream_request('/chat/completions', body, handler) return rescue RetriableError => e last_error = e if attempt < max_retries delay = calculate_delay(attempt) sleep(delay / 1000.0) next end handler.on_error(e) return rescue StandardError => e handler.on_error(e) return end end handler.on_error(StandardError.new("Exhausted retries: #{last_error&.}")) end |
#post(path, body) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/debug_agent/llm_client.rb', line 179 def post(path, body) uri = URI(@cfg.base_url + path) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http.read_timeout = @cfg.timeout_seconds request = Net::HTTP::Post.new(uri.path) request['Authorization'] = "Bearer #{@cfg.api_key}" request['Content-Type'] = 'application/json' request.body = JSON.generate(body) response = http.request(request) if response.code.to_i >= 400 raise RetriableError.new(response.code.to_i, "HTTP #{response.code}: #{response.body}") end JSON.parse(response.body) end |
#post_with_retry(path, body) ⇒ Object
Non-Streaming POST with retry ====================
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/debug_agent/llm_client.rb', line 156 def post_with_retry(path, body) max_retries = @cfg.max_retries last_error = nil (0..max_retries).each do |attempt| begin return post(path, body) rescue RetriableError => e last_error = e if attempt < max_retries delay = calculate_delay(attempt) sleep(delay / 1000.0) next end raise rescue StandardError => e raise end end raise last_error end |
#stream_request(path, body, handler) ⇒ Object
Stream Processing ====================
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/debug_agent/llm_client.rb', line 84 def stream_request(path, body, handler) uri = URI(@cfg.base_url + path) Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: @cfg.timeout_seconds) do |http| request = Net::HTTP::Post.new(uri.path) request['Authorization'] = "Bearer #{@cfg.api_key}" request['Content-Type'] = 'application/json' request.body = JSON.generate(body) tool_call_map = {} finish_reason = nil usage = nil http.request(request) do |response| if response.code.to_i >= 400 err_body = response.read_body raise RetriableError.new(response.code.to_i, "HTTP #{response.code}: #{err_body}") end response.read_body do |chunk| chunk.split("\n").each do |line| next unless line.start_with?('data: ') data_str = line[6..] next if data_str.strip == '[DONE]' begin parsed = JSON.parse(data_str) rescue JSON::ParserError next end if parsed['usage'] && parsed['usage']['prompt_tokens'] usage = parsed['usage'] end choices = parsed['choices'] || [] next if choices.empty? choice = choices[0] delta = choice['delta'] || {} if delta['content'] && !delta['content'].empty? handler.on_content(delta['content']) end if delta['tool_calls'] delta['tool_calls'].each do |tc| idx = tc['index'] || 0 tool_call_map[idx] ||= { 'id' => '', 'type' => 'function', 'function' => { 'name' => '', 'arguments' => '' } } entry = tool_call_map[idx] entry['id'] = tc['id'] if tc['id'] entry['type'] = tc['type'] if tc['type'] fn = tc['function'] || {} entry['function']['name'] += fn['name'] if fn['name'] entry['function']['arguments'] += fn['arguments'] if fn['arguments'] end end finish_reason = choice['finish_reason'] if choice['finish_reason'] end end end tool_calls = tool_call_map.keys.sort.map { |k| tool_call_map[k] }.select { |tc| tc['function']['name'] && !tc['function']['name'].empty? } handler.on_complete(tool_calls, finish_reason, usage) end end |