Module: AIGit::AIClient
- Defined in:
- lib/ai_git/ai_client.rb
Constant Summary collapse
- READ_TIMEOUT_SECONDS =
120- OPEN_TIMEOUT_SECONDS =
10- MAX_ATTEMPTS =
Transient failures worth retrying with backoff.
3- RETRY_BASE_DELAY =
0.5- TRANSIENT_STATUSES =
[408, 425, 429, 500, 502, 503, 504].freeze
- RETRYABLE_ERRORS =
[ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, Net::OpenTimeout, Net::ReadTimeout, SocketError, EOFError ].freeze
- OUTPUT_NOISE_PREFIXES =
/^(Here|Output|Generated|Based on|The changes)/i.freeze
- OUTPUT_NOISE_HEADERS =
/^(Here is|The (commit message|review) is|```|json|markdown)/i.freeze
Class Method Summary collapse
-
.complete(prompt:, model_name:, temperature:) ⇒ Object
Sends
promptto the local llama.cpp server and returns the cleaned text body. - .connection_error_message(error) ⇒ Object
- .http_error_message(uri, response) ⇒ Object
- .openai_complete(prompt, model_name, temperature) ⇒ Object
- .perform_request(uri, body) ⇒ Object
- .post_json(body) ⇒ Object
- .retry_delay(attempt) ⇒ Object
- .sanitize(text) ⇒ Object
- .transient_status?(code) ⇒ Boolean
Class Method Details
.complete(prompt:, model_name:, temperature:) ⇒ Object
Sends prompt to the local llama.cpp server and returns the cleaned text body.
29 30 31 |
# File 'lib/ai_git/ai_client.rb', line 29 def complete(prompt:, model_name:, temperature:) sanitize(openai_complete(prompt, model_name, temperature)) end |
.connection_error_message(error) ⇒ Object
105 106 107 108 109 110 111 |
# File 'lib/ai_git/ai_client.rb', line 105 def (error) provider = AIGit::Config.provider base_url = AIGit::Config.base_url hint = "Is the local server running? See `ai_git config`." "Cannot reach #{provider} at #{base_url} after #{MAX_ATTEMPTS} attempts: #{error.}. #{hint}" end |
.http_error_message(uri, response) ⇒ Object
94 95 96 97 98 99 100 101 102 103 |
# File 'lib/ai_git/ai_client.rb', line 94 def (uri, response) provider = AIGit::Config.provider body = response.body.to_s.strip body = "#{body[0, 500]}…" if body.length > 500 hint = response.code.to_i == 404 ? " Check the model name and base URL (see `ai_git config`)." : "" "#{provider} returned HTTP #{response.code} at #{uri}.#{hint}" \ "#{body.empty? ? '' : "\n#{body}"}" end |
.openai_complete(prompt, model_name, temperature) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/ai_git/ai_client.rb', line 33 def openai_complete(prompt, model_name, temperature) body = { model: model_name, messages: [{ role: "user", content: prompt }], stream: false, temperature: temperature } data = post_json(body) data.dig("choices", 0, "message", "content").to_s end |
.perform_request(uri, body) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/ai_git/ai_client.rb', line 80 def perform_request(uri, body) request = Net::HTTP::Post.new(uri) request["Content-Type"] = "application/json" request.body = body.to_json Net::HTTP.start( uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: OPEN_TIMEOUT_SECONDS, read_timeout: READ_TIMEOUT_SECONDS ) { |http| http.request(request) } end |
.post_json(body) ⇒ Object
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 |
# File 'lib/ai_git/ai_client.rb', line 53 def post_json(body) uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}") attempt = 0 loop do attempt += 1 begin response = perform_request(uri, body) rescue *RETRYABLE_ERRORS => e raise (e) if attempt >= MAX_ATTEMPTS sleep retry_delay(attempt) next end return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess) if transient_status?(response.code) && attempt < MAX_ATTEMPTS sleep retry_delay(attempt) next end raise (uri, response) end end |
.retry_delay(attempt) ⇒ Object
49 50 51 |
# File 'lib/ai_git/ai_client.rb', line 49 def retry_delay(attempt) RETRY_BASE_DELAY * (2**(attempt - 1)) end |
.sanitize(text) ⇒ Object
113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/ai_git/ai_client.rb', line 113 def sanitize(text) cleaned = text.to_s .gsub(OUTPUT_NOISE_HEADERS, "") .gsub(/^>\s*/, "") .gsub(/\\n/, "\n") .strip cleaned.lines .map(&:strip) .reject { |line| line.match?(OUTPUT_NOISE_PREFIXES) } .join("\n") .strip end |
.transient_status?(code) ⇒ Boolean
45 46 47 |
# File 'lib/ai_git/ai_client.rb', line 45 def transient_status?(code) TRANSIENT_STATUSES.include?(code.to_i) end |