active_genie 0.0.12 → 0.0.19

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -22
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +7 -7
  5. data/lib/active_genie/battle/basic.rb +48 -32
  6. data/lib/active_genie/battle.rb +4 -0
  7. data/lib/active_genie/clients/anthropic_client.rb +84 -0
  8. data/lib/active_genie/clients/base_client.rb +241 -0
  9. data/lib/active_genie/clients/google_client.rb +135 -0
  10. data/lib/active_genie/clients/helpers/retry.rb +29 -0
  11. data/lib/active_genie/clients/openai_client.rb +70 -91
  12. data/lib/active_genie/clients/unified_client.rb +4 -4
  13. data/lib/active_genie/concerns/loggable.rb +44 -0
  14. data/lib/active_genie/configuration/log_config.rb +1 -1
  15. data/lib/active_genie/configuration/providers/anthropic_config.rb +54 -0
  16. data/lib/active_genie/configuration/providers/base_config.rb +85 -0
  17. data/lib/active_genie/configuration/providers/deepseek_config.rb +54 -0
  18. data/lib/active_genie/configuration/providers/google_config.rb +56 -0
  19. data/lib/active_genie/configuration/providers/openai_config.rb +54 -0
  20. data/lib/active_genie/configuration/providers_config.rb +7 -4
  21. data/lib/active_genie/configuration/runtime_config.rb +35 -0
  22. data/lib/active_genie/configuration.rb +18 -4
  23. data/lib/active_genie/data_extractor/basic.rb +16 -3
  24. data/lib/active_genie/data_extractor.rb +4 -0
  25. data/lib/active_genie/logger.rb +40 -21
  26. data/lib/active_genie/ranking/elo_round.rb +71 -50
  27. data/lib/active_genie/ranking/free_for_all.rb +31 -14
  28. data/lib/active_genie/ranking/player.rb +11 -16
  29. data/lib/active_genie/ranking/players_collection.rb +4 -4
  30. data/lib/active_genie/ranking/ranking.rb +74 -19
  31. data/lib/active_genie/ranking/ranking_scoring.rb +3 -3
  32. data/lib/active_genie/scoring/basic.rb +44 -25
  33. data/lib/active_genie/scoring/recommended_reviewers.rb +1 -1
  34. data/lib/active_genie/scoring.rb +3 -0
  35. data/lib/tasks/benchmark.rake +27 -0
  36. metadata +92 -70
  37. data/lib/active_genie/configuration/openai_config.rb +0 -56
@@ -0,0 +1,241 @@
1
+ module ActiveGenie
2
+ module Clients
3
+ class BaseClient
4
+ class ClientError < StandardError; end
5
+ class RateLimitError < ClientError; end
6
+ class TimeoutError < ClientError; end
7
+ class NetworkError < ClientError; end
8
+
9
+ DEFAULT_HEADERS = {
10
+ 'Content-Type': 'application/json',
11
+ 'Accept': 'application/json',
12
+ 'User-Agent': 'ActiveGenie/1.0',
13
+ }.freeze
14
+
15
+ DEFAULT_TIMEOUT = 60 # seconds
16
+ DEFAULT_OPEN_TIMEOUT = 10 # seconds
17
+ DEFAULT_MAX_RETRIES = 3
18
+ DEFAULT_RETRY_DELAY = 1 # seconds
19
+
20
+ attr_reader :app_config
21
+
22
+ def initialize(config)
23
+ @app_config = config
24
+ end
25
+
26
+ # Make a GET request to the specified endpoint
27
+ #
28
+ # @param endpoint [String] The API endpoint to call
29
+ # @param headers [Hash] Additional headers to include in the request
30
+ # @param params [Hash] Query parameters for the request
31
+ # @param config [Hash] Configuration options including timeout, retries, etc.
32
+ # @return [Hash, nil] The parsed JSON response or nil if empty
33
+ def get(endpoint, params: {}, headers: {}, config: {})
34
+ uri = build_uri(endpoint, params)
35
+ request = Net::HTTP::Get.new(uri)
36
+ execute_request(uri, request, headers, config)
37
+ end
38
+
39
+ # Make a POST request to the specified endpoint
40
+ #
41
+ # @param endpoint [String] The API endpoint to call
42
+ # @param payload [Hash] The request body to send
43
+ # @param headers [Hash] Additional headers to include in the request
44
+ # @param config [Hash] Configuration options including timeout, retries, etc.
45
+ # @return [Hash, nil] The parsed JSON response or nil if empty
46
+ def post(endpoint, payload, params: {}, headers: {}, config: {})
47
+ uri = build_uri(endpoint, params)
48
+ request = Net::HTTP::Post.new(uri)
49
+ request.body = payload.to_json
50
+ execute_request(uri, request, headers, config)
51
+ end
52
+
53
+ # Make a PUT request to the specified endpoint
54
+ #
55
+ # @param endpoint [String] The API endpoint to call
56
+ # @param payload [Hash] The request body to send
57
+ # @param headers [Hash] Additional headers to include in the request
58
+ # @param config [Hash] Configuration options including timeout, retries, etc.
59
+ # @return [Hash, nil] The parsed JSON response or nil if empty
60
+ def put(endpoint, payload, headers: {}, config: {})
61
+ uri = build_uri(endpoint)
62
+ request = Net::HTTP::Put.new(uri)
63
+ request.body = payload.to_json
64
+ execute_request(uri, request, headers, config)
65
+ end
66
+
67
+ # Make a DELETE request to the specified endpoint
68
+ #
69
+ # @param endpoint [String] The API endpoint to call
70
+ # @param headers [Hash] Additional headers to include in the request
71
+ # @param params [Hash] Query parameters for the request
72
+ # @param config [Hash] Configuration options including timeout, retries, etc.
73
+ # @return [Hash, nil] The parsed JSON response or nil if empty
74
+ def delete(endpoint, headers: {}, params: {}, config: {})
75
+ uri = build_uri(endpoint, params)
76
+ request = Net::HTTP::Delete.new(uri)
77
+ execute_request(uri, request, headers, config)
78
+ end
79
+
80
+ protected
81
+
82
+ # Execute a request with retry logic and proper error handling
83
+ #
84
+ # @param uri [URI] The URI for the request
85
+ # @param request [Net::HTTP::Request] The request object
86
+ # @param headers [Hash] Additional headers to include
87
+ # @param config [Hash] Configuration options
88
+ # @return [Hash, nil] The parsed JSON response or nil if empty
89
+ def execute_request(uri, request, headers, config)
90
+ start_time = Time.now
91
+
92
+ # Apply headers
93
+ apply_headers(request, headers)
94
+
95
+ # Apply retry logic
96
+ retry_with_backoff(config) do
97
+ http = create_http_client(uri, config)
98
+
99
+ begin
100
+ response = http.request(request)
101
+
102
+ # Handle common HTTP errors
103
+ case response
104
+ when Net::HTTPSuccess
105
+ parsed_response = parse_response(response)
106
+
107
+ # Log request details if logging is enabled
108
+ log_request_details(
109
+ uri: uri,
110
+ method: request.method,
111
+ status: response.code,
112
+ duration: Time.now - start_time,
113
+ response: parsed_response
114
+ )
115
+
116
+ parsed_response
117
+ when Net::HTTPTooManyRequests
118
+ raise RateLimitError, "Rate limit exceeded: #{response.body}"
119
+ when Net::HTTPClientError, Net::HTTPServerError
120
+ raise ClientError, "HTTP Error #{response.code}: #{response.body}"
121
+ else
122
+ raise ClientError, "Unexpected response: #{response.code} - #{response.body}"
123
+ end
124
+ rescue Timeout::Error, Errno::ETIMEDOUT
125
+ raise TimeoutError, "Request to #{uri} timed out"
126
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, SocketError => e
127
+ raise NetworkError, "Network error: #{e.message}"
128
+ end
129
+ end
130
+ end
131
+
132
+ # Create and configure an HTTP client
133
+ #
134
+ # @param uri [URI] The URI for the request
135
+ # @param config [Hash] Configuration options
136
+ # @return [Net::HTTP] Configured HTTP client
137
+ def create_http_client(uri, config)
138
+ http = Net::HTTP.new(uri.host, uri.port)
139
+ http.use_ssl = (uri.scheme == 'https')
140
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
141
+ http.read_timeout = config.dig(:runtime, :timeout) || DEFAULT_TIMEOUT
142
+ http.open_timeout = config.dig(:runtime, :open_timeout) || DEFAULT_OPEN_TIMEOUT
143
+ http
144
+ end
145
+
146
+ # Apply headers to the request
147
+ #
148
+ # @param request [Net::HTTP::Request] The request object
149
+ # @param headers [Hash] Additional headers to include
150
+ def apply_headers(request, headers)
151
+ DEFAULT_HEADERS.each do |key, value|
152
+ request[key] = value
153
+ end
154
+
155
+ headers.each do |key, value|
156
+ request[key.to_s] = value
157
+ end
158
+ end
159
+
160
+ # Build a URI for the request
161
+ #
162
+ # @param endpoint [String] The API endpoint
163
+ # @param params [Hash] Query parameters
164
+ # @return [URI] The constructed URI
165
+ def build_uri(endpoint, params = {})
166
+ base_url = @app_config.api_url
167
+ uri = URI("#{base_url}#{endpoint}")
168
+
169
+ unless params.empty?
170
+ uri.query = URI.encode_www_form(params)
171
+ end
172
+
173
+ uri
174
+ end
175
+
176
+ # Parse the response body
177
+ #
178
+ # @param response [Net::HTTPResponse] The HTTP response
179
+ # @return [Hash, nil] Parsed JSON or nil if empty
180
+ def parse_response(response)
181
+ return nil if response.body.nil? || response.body.empty?
182
+
183
+ begin
184
+ JSON.parse(response.body)
185
+ rescue JSON::ParserError => e
186
+ raise ClientError, "Failed to parse JSON response: #{e.message}"
187
+ end
188
+ end
189
+
190
+ # Log request details if logging is enabled
191
+ #
192
+ # @param details [Hash] Request and response details
193
+ def log_request_details(details)
194
+ return unless defined?(ActiveGenie::Logger)
195
+
196
+ ActiveGenie::Logger.trace({
197
+ code: :http_request,
198
+ uri: details[:uri].to_s,
199
+ method: details[:method],
200
+ status: details[:status],
201
+ duration: details[:duration],
202
+ response_size: details[:response].to_s.bytesize
203
+ })
204
+ end
205
+
206
+ # Retry a block with exponential backoff
207
+ #
208
+ # @param config [Hash] Configuration options
209
+ # @yield The block to retry
210
+ # @return [Object] The result of the block
211
+ def retry_with_backoff(config = {})
212
+ max_retries = config.dig(:runtime, :max_retries) || DEFAULT_MAX_RETRIES
213
+ retry_delay = config.dig(:runtime, :retry_delay) || DEFAULT_RETRY_DELAY
214
+
215
+ retries = 0
216
+
217
+ begin
218
+ yield
219
+ rescue RateLimitError, NetworkError => e
220
+ if retries < max_retries
221
+ sleep_time = retry_delay * (2 ** retries)
222
+ retries += 1
223
+
224
+ ActiveGenie::Logger.trace({
225
+ code: :retry_attempt,
226
+ attempt: retries,
227
+ max_retries: max_retries,
228
+ delay: sleep_time,
229
+ error: e.message
230
+ }) if defined?(ActiveGenie::Logger)
231
+
232
+ sleep(sleep_time)
233
+ retry
234
+ else
235
+ raise
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,135 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require_relative './helpers/retry'
5
+ require_relative './base_client'
6
+
7
+ module ActiveGenie::Clients
8
+ # Client for interacting with the Google Generative Language API.
9
+ class GoogleClient < BaseClient
10
+ class GoogleError < ClientError; end
11
+ class RateLimitError < GoogleError; end
12
+
13
+ API_VERSION_PATH = '/v1beta/models'.freeze
14
+
15
+ def initialize(config)
16
+ super(config)
17
+ end
18
+
19
+ # Requests structured JSON output from the Google Generative Language model based on a schema.
20
+ #
21
+ # @param messages [Array<Hash>] A list of messages representing the conversation history.
22
+ # Each hash should have :role ('user' or 'model') and :content (String).
23
+ # Google Generative Language uses 'user' and 'model' roles.
24
+ # @param function [Hash] A JSON schema definition describing the desired output format.
25
+ # @param model_tier [Symbol, nil] A symbolic representation of the model quality/size tier.
26
+ # @param config [Hash] Optional configuration overrides:
27
+ # - :api_key [String] Override the default API key.
28
+ # - :model [String] Override the model name directly.
29
+ # - :max_retries [Integer] Max retries for the request.
30
+ # - :retry_delay [Integer] Initial delay for retries.
31
+ # @return [Hash, nil] The parsed JSON object matching the schema, or nil if parsing fails or content is empty.
32
+ def function_calling(messages, function, model_tier: nil, config: {})
33
+ model = config[:runtime][:model] || @app_config.tier_to_model(model_tier)
34
+ api_key = config[:runtime][:api_key] || @app_config.api_key
35
+
36
+ contents = convert_messages_to_contents(messages, function)
37
+ contents << output_as_json_schema(function)
38
+
39
+ payload = {
40
+ contents: contents,
41
+ generationConfig: {
42
+ response_mime_type: "application/json",
43
+ temperature: 0.1
44
+ }
45
+ }
46
+
47
+ endpoint = "#{API_VERSION_PATH}/#{model}:generateContent"
48
+ params = { key: api_key }
49
+ headers = DEFAULT_HEADERS
50
+
51
+ retry_with_backoff(config:) do
52
+ start_time = Time.now
53
+
54
+ response = post(endpoint, payload, headers:, params:, config: config)
55
+
56
+ json_string = response&.dig('candidates', 0, 'content', 'parts', 0, 'text')
57
+ return nil if json_string.nil? || json_string.empty?
58
+
59
+ begin
60
+ parsed_response = JSON.parse(json_string)
61
+
62
+ # Log usage metrics
63
+ usage_metadata = response['usageMetadata'] || {}
64
+ prompt_tokens = usage_metadata['promptTokenCount'] || 0
65
+ candidates_tokens = usage_metadata['candidatesTokenCount'] || 0
66
+ total_tokens = usage_metadata['totalTokenCount'] || (prompt_tokens + candidates_tokens)
67
+
68
+ ActiveGenie::Logger.trace({
69
+ code: :llm_usage,
70
+ input_tokens: prompt_tokens,
71
+ output_tokens: candidates_tokens,
72
+ total_tokens: total_tokens,
73
+ model: model,
74
+ duration: Time.now - start_time,
75
+ usage: usage_metadata
76
+ })
77
+
78
+ ActiveGenie::Logger.trace({ code: :function_calling, payload:, parsed_response: })
79
+
80
+ normalize_function_output(parsed_response)
81
+ rescue JSON::ParserError => e
82
+ raise GoogleError, "Failed to parse Google API response: #{e.message} - Content: #{json_string}"
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def normalize_function_output(output)
90
+ output = if output.is_a?(Array)
91
+ output.dig(0, 'properties') || output.dig(0)
92
+ else
93
+ output
94
+ end
95
+
96
+ output.dig('input_schema', 'properties') || output
97
+ end
98
+
99
+ ROLE_TO_GOOGLE_ROLE = {
100
+ user: 'user',
101
+ assistant: 'model',
102
+ }.freeze
103
+
104
+ # Converts standard message format to Google's 'contents' format
105
+ # and injects JSON schema instructions.
106
+ # @param messages [Array<Hash>] Array of { role: 'user'/'assistant'/'system', content: '...' }
107
+ # @param function_schema [Hash] The JSON schema for the desired output.
108
+ # @return [Array<Hash>] Array formatted for Google's 'contents' field.
109
+ def convert_messages_to_contents(messages, function_schema)
110
+ messages.map do |message|
111
+ {
112
+ role: ROLE_TO_GOOGLE_ROLE[message[:role].to_sym] || 'user',
113
+ parts: [{ text: message[:content] }]
114
+ }
115
+ end
116
+ end
117
+
118
+ def output_as_json_schema(function_schema)
119
+ json_instruction = <<~PROMPT
120
+ Generate a JSON object that strictly adheres to the following JSON schema:
121
+
122
+ ```json
123
+ #{JSON.pretty_generate(function_schema[:parameters])}
124
+ ```
125
+
126
+ IMPORTANT: Only output the raw JSON object. Do not include any other text, explanations, or markdown formatting like ```json ... ``` wrappers around the final output.
127
+ PROMPT
128
+
129
+ {
130
+ role: 'user',
131
+ parts: [{ text: json_instruction }]
132
+ }
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,29 @@
1
+ MAX_RETRIES = 3
2
+ BASE_DELAY = 0.5
3
+
4
+ def retry_with_backoff(config: {})
5
+ retries = config[:runtime][:max_retries] || MAX_RETRIES
6
+
7
+ begin
8
+ yield
9
+ rescue => e
10
+ if retries > 0
11
+ ActiveGenie::Logger.warn({ code: :retry_with_backoff, message: "Retrying request after error: #{e.message}. Attempts remaining: #{retries}" })
12
+
13
+ retries -= 1
14
+ backoff_time = calculate_backoff(MAX_RETRIES - retries)
15
+ sleep(backoff_time)
16
+ retry
17
+ else
18
+ raise
19
+ end
20
+ end
21
+ end
22
+
23
+ def calculate_backoff(retry_count)
24
+ # Exponential backoff with jitter: 2^retry_count + random jitter
25
+ # Base delay is 0.5 seconds, doubles each retry, plus up to 0.5 seconds of random jitter
26
+ # Simplified example: 0.5, 1, 2, 4, 8, 12, 16, 20, 24, 28, 30 seconds
27
+ jitter = rand * BASE_DELAY
28
+ [BASE_DELAY * (2 ** retry_count) + jitter, 30].min # Cap at 30 seconds
29
+ end
@@ -1,119 +1,98 @@
1
1
  require 'json'
2
2
  require 'net/http'
3
3
 
4
+ require_relative './helpers/retry'
5
+ require_relative './base_client'
6
+
4
7
  module ActiveGenie::Clients
5
- class OpenaiClient
6
- MAX_RETRIES = 3
7
-
8
- class OpenaiError < StandardError; end
8
+ class OpenaiClient < BaseClient
9
+ class OpenaiError < ClientError; end
9
10
  class RateLimitError < OpenaiError; end
11
+ class InvalidResponseError < StandardError; end
10
12
 
11
13
  def initialize(config)
12
- @app_config = config
14
+ super(config)
13
15
  end
14
16
 
17
+ # Requests structured JSON output from the OpenAI model based on a schema.
18
+ #
19
+ # @param messages [Array<Hash>] A list of messages representing the conversation history.
20
+ # Each hash should have :role ('user', 'assistant', or 'system') and :content (String).
21
+ # @param function [Hash] A JSON schema definition describing the desired output format.
22
+ # @param model_tier [Symbol, nil] A symbolic representation of the model quality/size tier.
23
+ # @param config [Hash] Optional configuration overrides:
24
+ # - :api_key [String] Override the default API key.
25
+ # - :model [String] Override the model name directly.
26
+ # - :max_retries [Integer] Max retries for the request.
27
+ # - :retry_delay [Integer] Initial delay for retries.
28
+ # @return [Hash, nil] The parsed JSON object matching the schema, or nil if parsing fails or content is empty.
15
29
  def function_calling(messages, function, model_tier: nil, config: {})
16
- model = config[:model] || @app_config.tier_to_model(model_tier)
30
+ model = config[:runtime][:model] || @app_config.tier_to_model(model_tier)
17
31
 
18
32
  payload = {
19
33
  messages:,
20
- response_format: {
21
- type: 'json_schema',
22
- json_schema: function
23
- },
34
+ tools: [{
35
+ type: 'function',
36
+ function: {
37
+ **function,
38
+ parameters: {
39
+ **function[:parameters],
40
+ additionalProperties: false
41
+ },
42
+ strict: true
43
+ }.compact
44
+ }],
45
+ tool_choice: { type: 'function', function: { name: function[:name] } },
46
+ stream: false,
24
47
  model:,
25
48
  }
26
49
 
27
- api_key = config[:api_key] || @app_config.api_key
28
- headers = DEFAULT_HEADERS.merge(
50
+ api_key = config[:runtime][:api_key] || @app_config.api_key
51
+ headers = {
29
52
  'Authorization': "Bearer #{api_key}"
30
- ).compact
53
+ }.compact
54
+
55
+ retry_with_backoff(config:) do
56
+ response = request_openai(payload, headers, config:)
57
+
58
+ parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments'))
59
+ parsed_response = parsed_response.dig('message') || parsed_response
31
60
 
32
- response = request(payload, headers, config:)
61
+ raise InvalidResponseError, "Invalid response: #{parsed_response}" if parsed_response.nil? || parsed_response.keys.size.zero?
33
62
 
34
- parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'content'))
35
- parsed_response.dig('properties') || parsed_response
36
- rescue JSON::ParserError
37
- nil
63
+ ActiveGenie::Logger.trace({code: :function_calling, payload:, parsed_response: })
64
+
65
+ parsed_response
66
+ end
38
67
  end
39
68
 
69
+
40
70
  private
41
71
 
42
- def request(payload, headers, config:)
43
- retries = config[:max_retries] || MAX_RETRIES
72
+ # Make a request to the OpenAI API
73
+ #
74
+ # @param payload [Hash] The request payload
75
+ # @param headers [Hash] Additional headers
76
+ # @param config [Hash] Configuration options
77
+ # @return [Hash] The parsed response
78
+ def request_openai(payload, headers, config:)
44
79
  start_time = Time.now
45
-
46
- begin
47
- response = Net::HTTP.post(
48
- URI("#{@app_config.api_url}/chat/completions"),
49
- payload.to_json,
50
- headers
51
- )
52
-
53
- if response.is_a?(Net::HTTPTooManyRequests)
54
- raise RateLimitError, "OpenAI API rate limit exceeded: #{response.body}"
55
- end
56
-
57
- raise OpenaiError, response.body unless response.is_a?(Net::HTTPSuccess)
58
-
59
- return nil if response.body.empty?
60
-
61
- parsed_body = JSON.parse(response.body)
62
- # log_response(start_time, parsed_body, config:)
63
-
64
- parsed_body
65
- rescue OpenaiError, Net::HTTPError, JSON::ParserError, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout => e
66
- if retries > 0
67
- retries -= 1
68
- backoff_time = calculate_backoff(MAX_RETRIES - retries)
69
- ActiveGenie::Logger.trace(
70
- {
71
- category: :llm,
72
- trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
73
- message: "Retrying request after error: #{e.message}. Attempts remaining: #{retries}",
74
- backoff_time: backoff_time
75
- }
76
- )
77
- sleep(backoff_time)
78
- retry
79
- else
80
- ActiveGenie::Logger.trace(
81
- {
82
- category: :llm,
83
- trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
84
- message: "Max retries reached. Failing with error: #{e.message}"
85
- }
86
- )
87
- raise
88
- end
89
- end
90
- end
91
80
 
92
- BASE_DELAY = 0.5
93
- def calculate_backoff(retry_count)
94
- # Exponential backoff with jitter: 2^retry_count + random jitter
95
- # Base delay is 0.5 seconds, doubles each retry, plus up to 0.5 seconds of random jitter
96
- # Simplified example: 0.5, 1, 2, 4, 8, 12, 16, 20, 24, 28, 30 seconds
97
- jitter = rand * BASE_DELAY
98
- [BASE_DELAY * (2 ** retry_count) + jitter, 30].min # Cap at 30 seconds
99
- end
81
+ response = post("/chat/completions", payload, headers: headers, config: config)
82
+
83
+ return nil if response.nil?
84
+
85
+ ActiveGenie::Logger.trace({
86
+ code: :llm_usage,
87
+ input_tokens: response.dig('usage', 'prompt_tokens'),
88
+ output_tokens: response.dig('usage', 'completion_tokens'),
89
+ total_tokens: response.dig('usage', 'total_tokens'),
90
+ model: payload[:model],
91
+ duration: Time.now - start_time,
92
+ usage: response.dig('usage')
93
+ })
100
94
 
101
- DEFAULT_HEADERS = {
102
- 'Content-Type': 'application/json',
103
- }
104
-
105
- def log_response(start_time, response, config: {})
106
- ActiveGenie::Logger.trace(
107
- {
108
- **config.dig(:log),
109
- category: :llm,
110
- trace: "#{config.dig(:log, :trace)}/#{self.class.name}",
111
- total_tokens: response.dig('usage', 'total_tokens'),
112
- model: response.dig('model'),
113
- request_duration: Time.now - start_time,
114
- openai: response
115
- }
116
- )
95
+ response
117
96
  end
118
97
  end
119
- end
98
+ end
@@ -2,12 +2,12 @@ module ActiveGenie::Clients
2
2
  class UnifiedClient
3
3
  class << self
4
4
  def function_calling(messages, function, model_tier: nil, config: {})
5
- provider_name = config[:provider]&.downcase&.strip&.to_sym
6
- provider = ActiveGenie.configuration.providers.all[provider_name] || ActiveGenie.configuration.providers.default
5
+ provider_name = config[:runtime][:provider]&.to_s&.downcase&.strip&.to_sym || ActiveGenie.configuration.providers.default
6
+ provider_instance = ActiveGenie.configuration.providers.valid[provider_name]
7
7
 
8
- raise InvalidProviderError if provider.nil? || provider.client.nil?
8
+ raise InvalidProviderError if provider_instance.nil? || provider_instance.client.nil?
9
9
 
10
- provider.client.function_calling(messages, function, model_tier:, config:)
10
+ provider_instance.client.function_calling(messages, function, model_tier:, config:)
11
11
  end
12
12
 
13
13
  private
@@ -0,0 +1,44 @@
1
+ module ActiveGenie
2
+ module Concerns
3
+ module Loggable
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def with_logging_context(context_method, observer_proc = nil)
10
+ original_method = instance_method(:call)
11
+
12
+ define_method(:call) do |*args, **kwargs, &block|
13
+ context = send(context_method, *args, **kwargs)
14
+ bound_observer = observer_proc ? ->(log) { instance_exec(log, &observer_proc) } : nil
15
+
16
+ ActiveGenie::Logger.with_context(context, observer: bound_observer) do
17
+ original_method.bind(self).call(*args, **kwargs, &block)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def info(log)
24
+ ::ActiveGenie::Logger.info(log)
25
+ end
26
+
27
+ def error(log)
28
+ ::ActiveGenie::Logger.error(log)
29
+ end
30
+
31
+ def warn(log)
32
+ ::ActiveGenie::Logger.warn(log)
33
+ end
34
+
35
+ def debug(log)
36
+ ::ActiveGenie::Logger.debug(log)
37
+ end
38
+
39
+ def trace(log)
40
+ ::ActiveGenie::Logger.trace(log)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -8,7 +8,7 @@ module ActiveGenie::Configuration
8
8
  end
9
9
 
10
10
  def to_h(config = {})
11
- { log_level:, **config }
11
+ { log_level: }.merge(config)
12
12
  end
13
13
  end
14
14
  end