active_genie 0.30.3 → 0.30.8

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/VERSION +1 -1
  4. data/lib/active_genie/comparator/debate.rb +13 -36
  5. data/lib/active_genie/comparator/fight.rb +3 -3
  6. data/lib/active_genie/comparator.rb +0 -2
  7. data/lib/active_genie/configs/base_config.rb +25 -0
  8. data/lib/active_genie/configs/extractor_config.rb +6 -14
  9. data/lib/active_genie/configs/lister_config.rb +6 -10
  10. data/lib/active_genie/configs/llm_config.rb +12 -26
  11. data/lib/active_genie/configs/log_config.rb +19 -16
  12. data/lib/active_genie/configs/providers/anthropic_config.rb +16 -1
  13. data/lib/active_genie/configs/providers/deepseek_config.rb +8 -2
  14. data/lib/active_genie/configs/providers/google_config.rb +8 -2
  15. data/lib/active_genie/configs/providers/openai_config.rb +8 -2
  16. data/lib/active_genie/configs/providers/provider_base.rb +22 -31
  17. data/lib/active_genie/configs/providers_config.rb +35 -16
  18. data/lib/active_genie/configs/ranker_config.rb +6 -12
  19. data/lib/active_genie/configuration.rb +23 -60
  20. data/lib/active_genie/{ranker/entities → entities}/player.rb +4 -0
  21. data/lib/active_genie/{ranker/entities → entities}/players.rb +1 -1
  22. data/lib/active_genie/entities/result.rb +29 -0
  23. data/lib/active_genie/errors/provider_server_error.rb +8 -5
  24. data/lib/active_genie/errors/without_available_provider_error.rb +39 -0
  25. data/lib/active_genie/extractor/data.json +9 -0
  26. data/lib/active_genie/extractor/data.prompt.md +12 -0
  27. data/lib/active_genie/extractor/data.rb +71 -0
  28. data/lib/active_genie/extractor/explanation.rb +19 -47
  29. data/lib/active_genie/extractor/litote.rb +8 -14
  30. data/lib/active_genie/extractor.rb +5 -0
  31. data/lib/active_genie/lister/feud.json +5 -1
  32. data/lib/active_genie/lister/feud.rb +10 -24
  33. data/lib/active_genie/lister/juries.rb +14 -20
  34. data/lib/active_genie/logger.rb +16 -28
  35. data/lib/active_genie/providers/anthropic_provider.rb +11 -5
  36. data/lib/active_genie/providers/base_provider.rb +15 -17
  37. data/lib/active_genie/providers/deepseek_provider.rb +17 -9
  38. data/lib/active_genie/providers/google_provider.rb +10 -4
  39. data/lib/active_genie/providers/openai_provider.rb +6 -4
  40. data/lib/active_genie/providers/unified_provider.rb +47 -17
  41. data/lib/active_genie/ranker/elo.rb +41 -36
  42. data/lib/active_genie/ranker/free_for_all.rb +45 -28
  43. data/lib/active_genie/ranker/scoring.rb +20 -11
  44. data/lib/active_genie/ranker/tournament.rb +23 -35
  45. data/lib/active_genie/scorer/jury_bench.rb +15 -29
  46. data/lib/active_genie/utils/base_module.rb +34 -0
  47. data/lib/active_genie/utils/call_wrapper.rb +20 -0
  48. data/lib/active_genie/utils/deep_merge.rb +12 -0
  49. data/lib/active_genie/utils/fiber_by_batch.rb +2 -2
  50. data/lib/active_genie/utils/text_case.rb +18 -0
  51. data/lib/active_genie.rb +16 -18
  52. data/lib/tasks/test.rake +61 -3
  53. metadata +19 -8
  54. data/lib/active_genie/configs/comparator_config.rb +0 -10
  55. data/lib/active_genie/configs/scorer_config.rb +0 -10
@@ -5,32 +5,24 @@ require 'fileutils'
5
5
 
6
6
  module ActiveGenie
7
7
  class Logger
8
- def initialize(log_config: nil)
9
- @log_config = log_config || ActiveGenie.configuration.log
10
- end
11
-
12
- def call(data)
13
- log = data.merge(@log_config.additional_context)
8
+ def call(data, config:)
9
+ log = data.merge(config.log.additional_context || {})
14
10
  .merge(
15
11
  timestamp: Time.now,
16
12
  process_id: Process.pid
17
13
  )
18
14
 
19
- persist!(log)
20
- output_call(log)
21
- call_observers(log)
15
+ persist!(log, config:)
16
+ output_call(log, config:)
17
+ call_observers(log, config:)
22
18
 
23
19
  log
24
20
  end
25
21
 
26
- def merge(log_config = nil)
27
- new(log_config:)
28
- end
29
-
30
22
  private
31
23
 
32
- def call_observers(log)
33
- Array(@log_config.observers).each do |observer|
24
+ def call_observers(log, config:)
25
+ Array(config.log.observers).each do |observer|
34
26
  next unless observer[:scope].all? { |key, value| log[key.to_sym] == value }
35
27
 
36
28
  observer[:observer]&.call(log)
@@ -39,9 +31,9 @@ module ActiveGenie
39
31
  end
40
32
  end
41
33
 
42
- def output_call(log)
43
- if @log_config.output
44
- @log_config.output&.call(log)
34
+ def output_call(log, config:)
35
+ if config.log.output
36
+ config.log.output&.call(log)
45
37
  else
46
38
  $stdout.puts log
47
39
  end
@@ -49,20 +41,16 @@ module ActiveGenie
49
41
  call(code: :output_error, error: e.message)
50
42
  end
51
43
 
52
- def persist!(log)
53
- file_path = log_to_file_path(log)
44
+ def persist!(log, config:)
45
+ file_path = if log.key?(:fine_tune) && log[:fine_tune]
46
+ config.log.fine_tune_file_path
47
+ else
48
+ config.log.file_path
49
+ end
54
50
  folder_path = File.dirname(file_path)
55
51
 
56
52
  FileUtils.mkdir_p(folder_path)
57
53
  File.write(file_path, "#{JSON.generate(log)}\n", mode: 'a')
58
54
  end
59
-
60
- def log_to_file_path(log)
61
- if log.key?(:fine_tune) && log[:fine_tune]
62
- @log_config.fine_tune_file_path
63
- else
64
- @log_config.file_path
65
- end
66
- end
67
55
  end
68
56
  end
@@ -30,7 +30,11 @@ module ActiveGenie
30
30
  temperature: @config.llm.temperature || 0
31
31
  }
32
32
 
33
- request(payload).dig('content', 0, 'input')
33
+ response = retry_with_backoff do
34
+ request(payload)
35
+ end
36
+
37
+ response.dig('content', 0, 'input')
34
38
  end
35
39
 
36
40
  ANTHROPIC_ENDPOINT = '/v1/messages'
@@ -55,7 +59,7 @@ module ActiveGenie
55
59
  def request(payload)
56
60
  response = post(url, payload, headers:)
57
61
 
58
- @config.logger.call(
62
+ ActiveGenie.logger.call(
59
63
  {
60
64
  code: :llm_usage,
61
65
  input_tokens: response.dig('usage', 'input_tokens'),
@@ -65,15 +69,17 @@ module ActiveGenie
65
69
  'output_tokens'),
66
70
  model: payload[:model],
67
71
  usage: response['usage']
68
- }
72
+ },
73
+ config: @config
69
74
  )
70
- @config.logger.call(
75
+ ActiveGenie.logger.call(
71
76
  {
72
77
  code: :function_calling,
73
78
  fine_tune: true,
74
79
  payload:,
75
80
  parsed_response: response.dig('content', 0, 'input')
76
- }
81
+ },
82
+ config: @config
77
83
  )
78
84
 
79
85
  response
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'net/http'
4
+ require_relative '../errors/provider_server_error'
4
5
 
5
6
  module ActiveGenie
6
7
  module Providers
@@ -103,9 +104,7 @@ module ActiveGenie
103
104
  http.read_timeout = @config.llm.read_timeout || DEFAULT_TIMEOUT
104
105
  http.open_timeout = @config.llm.open_timeout || DEFAULT_OPEN_TIMEOUT
105
106
 
106
- retry_with_backoff do
107
- http.request(request)
108
- end
107
+ http.request(request)
109
108
  end
110
109
 
111
110
  # Apply headers to the request
@@ -153,7 +152,7 @@ module ActiveGenie
153
152
  #
154
153
  # @param details [Hash] Request and response details
155
154
  def log_request_details(uri:, request:, response:, start_time:, parsed_response:)
156
- @config.logger.call(
155
+ ActiveGenie.logger.call(
157
156
  {
158
157
  code: :http_request,
159
158
  uri: uri.to_s,
@@ -161,7 +160,7 @@ module ActiveGenie
161
160
  status: response.code,
162
161
  duration: Time.now - start_time,
163
162
  response_size: parsed_response.to_s.bytesize
164
- }
163
+ }, config: @config
165
164
  )
166
165
  end
167
166
 
@@ -171,23 +170,22 @@ module ActiveGenie
171
170
  retries = 0
172
171
 
173
172
  begin
174
- response = yield
175
-
176
- raise ActiveGenie::ProviderServerError, response if response&.code.to_i >= 500
177
-
178
- response
179
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, ProviderServerError => e
173
+ yield
174
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, ActiveGenie::ProviderServerError,
175
+ JSON::ParserError => e
180
176
  raise if retries > max_retries
181
177
 
182
178
  sleep_time = retry_delay * (2**retries)
183
179
  retries += 1
184
180
 
185
- @config.logger.call(
186
- code: :retry_attempt,
187
- attempt: retries,
188
- max_retries:,
189
- next_retry_in_seconds: sleep_time,
190
- error: e.message
181
+ ActiveGenie.logger.call(
182
+ {
183
+ code: :retry_attempt,
184
+ attempt: retries,
185
+ max_retries:,
186
+ next_retry_in_seconds: sleep_time,
187
+ error: e.message
188
+ }, config: @config
191
189
  )
192
190
 
193
191
  sleep(sleep_time)
@@ -25,14 +25,14 @@ module ActiveGenie
25
25
  model:
26
26
  }
27
27
 
28
- response = request(payload)
29
-
30
- if response.nil? || response.keys.empty?
31
- raise InvalidResponseError,
32
- "Invalid response: #{response}"
28
+ response = retry_with_backoff do
29
+ request(payload)
33
30
  end
34
31
 
35
- @config.logger.call({ code: :function_calling, fine_tune: true, payload:, response: })
32
+ raise InvalidResponseError, "Invalid response: #{response}" if response.keys.empty?
33
+ raise InvalidResponseError, 'Invalid response: empty' if response.nil?
34
+
35
+ ActiveGenie.logger.call({ code: :function_calling, fine_tune: true, payload:, response: }, config: @config)
36
36
 
37
37
  response
38
38
  end
@@ -44,7 +44,7 @@ module ActiveGenie
44
44
 
45
45
  return nil if response.nil?
46
46
 
47
- @config.logger.call(
47
+ ActiveGenie.logger.call(
48
48
  {
49
49
  code: :llm_usage,
50
50
  input_tokens: response.dig('usage', 'prompt_tokens'),
@@ -52,11 +52,19 @@ module ActiveGenie
52
52
  total_tokens: response.dig('usage', 'total_tokens'),
53
53
  model:,
54
54
  usage: response['usage']
55
- }
55
+ }, config: @config
56
56
  )
57
57
 
58
- parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments'))
58
+ parsed_response = JSON.parse(get_response_body(response))
59
59
  parsed_response['message'] || parsed_response
60
+ rescue JSON::ParserError
61
+ raise InvalidResponseError, "Invalid response: #{get_response_body(response)}"
62
+ end
63
+
64
+ def get_response_body(response)
65
+ response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments')
66
+ .gsub(', " "', '')
67
+ .strip
60
68
  end
61
69
 
62
70
  def function_to_tool(function)
@@ -29,12 +29,17 @@ module ActiveGenie
29
29
  }
30
30
  params = { key: provider_config.api_key }
31
31
 
32
- response = request(payload, params)
32
+ response = retry_with_backoff do
33
+ request(payload, params)
34
+ end
33
35
 
34
36
  json_string = response&.dig('candidates', 0, 'content', 'parts', 0, 'text')
35
37
  return nil if json_string.nil? || json_string.empty?
36
38
 
37
- @config.logger.call({ code: :function_calling, fine_tune: true, payload:, parsed_response: json_string })
39
+ ActiveGenie.logger.call(
40
+ { code: :function_calling, fine_tune: true, payload:, parsed_response: json_string },
41
+ config: @config
42
+ )
38
43
 
39
44
  normalize_response(json_string)
40
45
  end
@@ -50,7 +55,7 @@ module ActiveGenie
50
55
  def request(payload, params)
51
56
  response = post(url, payload, headers: DEFAULT_HEADERS, params:)
52
57
 
53
- @config.logger.call(
58
+ ActiveGenie.logger.call(
54
59
  {
55
60
  code: :llm_usage,
56
61
  input_tokens: response['usageMetadata']['promptTokenCount'] || 0,
@@ -58,7 +63,8 @@ module ActiveGenie
58
63
  total_tokens: response['usageMetadata']['totalTokenCount'] || (prompt_tokens + candidates_tokens),
59
64
  model:,
60
65
  usage: response['usageMetadata'] || {}
61
- }
66
+ },
67
+ config: @config
62
68
  )
63
69
 
64
70
  response
@@ -25,11 +25,13 @@ module ActiveGenie
25
25
  model:
26
26
  }
27
27
 
28
- response = request(payload)
28
+ response = retry_with_backoff do
29
+ request(payload)
30
+ end
29
31
 
30
32
  raise InvalidResponseError, "Invalid response: #{response}" if response.nil? || response.keys.empty?
31
33
 
32
- @config.logger.call({ code: :function_calling, fine_tune: true, payload:, response: })
34
+ ActiveGenie.logger.call({ code: :function_calling, fine_tune: true, payload:, response: }, config: @config)
33
35
 
34
36
  response
35
37
  end
@@ -41,7 +43,7 @@ module ActiveGenie
41
43
 
42
44
  return nil if response.nil?
43
45
 
44
- @config.logger.call(
46
+ ActiveGenie.logger.call(
45
47
  {
46
48
  code: :llm_usage,
47
49
  input_tokens: response.dig('usage', 'prompt_tokens'),
@@ -49,7 +51,7 @@ module ActiveGenie
49
51
  total_tokens: response.dig('usage', 'total_tokens'),
50
52
  model:,
51
53
  usage: response['usage']
52
- }
54
+ }, config: @config
53
55
  )
54
56
 
55
57
  parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments'))
@@ -6,6 +6,7 @@ require_relative 'google_provider'
6
6
  require_relative 'deepseek_provider'
7
7
  require_relative '../errors/invalid_provider_error'
8
8
  require_relative '../errors/invalid_model_error'
9
+ require_relative '../errors/without_available_provider_error'
9
10
 
10
11
  module ActiveGenie
11
12
  module Providers
@@ -19,8 +20,13 @@ module ActiveGenie
19
20
  }.freeze
20
21
 
21
22
  def function_calling(messages, function, config: {})
22
- provider = provider(config)
23
- define_llm_model(config)
23
+ model, provider_name = model_and_provider_by(config)
24
+
25
+ provider = PROVIDER_NAME_TO_PROVIDER[provider_name&.to_sym]
26
+
27
+ raise ActiveGenie::WithoutAvailableProviderError if provider.nil?
28
+
29
+ config.llm.model = model
24
30
 
25
31
  response = provider.new(config).function_calling(messages, function)
26
32
 
@@ -29,30 +35,54 @@ module ActiveGenie
29
35
 
30
36
  private
31
37
 
32
- def provider(config)
33
- provider_name = config.llm.provider_name || config.providers.default
38
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
39
+ def model_and_provider_by(config)
40
+ model, provider_name = explicit_choice(config)
41
+ model, provider_name = global_default(config) if model.nil? && provider_name.nil?
42
+ model, provider_name = module_recommendation(config) if model.nil? && provider_name.nil?
34
43
 
35
- unless config.providers.valid.keys.include?(provider_name.to_sym)
36
- raise ActiveGenie::InvalidProviderError,
37
- provider_name
38
- end
44
+ model, provider_name = infer_from_partial(config, model, provider_name) if model.nil? || provider_name.nil?
45
+ model, provider_name = any_available(config) if model.nil? || provider_name.nil?
39
46
 
40
- provider = PROVIDER_NAME_TO_PROVIDER[provider_name.to_sym]
47
+ [model, provider_name]
48
+ end
49
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
41
50
 
42
- raise ActiveGenie::InvalidProviderError, provider_name if provider.nil?
51
+ def explicit_choice(config)
52
+ model = config.llm.model
53
+ provider_name = config.providers.default
43
54
 
44
- provider
55
+ [model, provider_name]
45
56
  end
46
57
 
47
- def define_llm_model(config)
48
- if config.llm.model.nil?
49
- raise ActiveGenie::InvalidModelError, 'nil' unless config.llm.recommended_model
58
+ def global_default(config)
59
+ provider_name = config.providers.default
50
60
 
51
- config.llm.model = config.llm.recommended_model
61
+ [nil, provider_name]
62
+ end
52
63
 
53
- end
64
+ def module_recommendation(config)
65
+ model = config.llm.recommended_model
66
+ provider_name = config.providers.provider_name_by_model(model) if model
67
+
68
+ return nil if model.nil? || provider_name.nil?
69
+
70
+ [model, provider_name]
71
+ end
72
+
73
+ def infer_from_partial(config, model, provider_name)
74
+ provider_name ||= config.providers.provider_name_by_model(model) if model
75
+ model ||= config.providers.valid[provider_name.to_sym]&.default_model if provider_name
76
+
77
+ [model, provider_name]
78
+ end
79
+
80
+ def any_available(config)
81
+ provider = config.providers.valid.first
82
+ provider_name = provider&.first
83
+ model = provider&.last&.default_model
54
84
 
55
- config.llm.model
85
+ [model, provider_name]
56
86
  end
57
87
 
58
88
  def normalize_response(response)
@@ -12,7 +12,7 @@ module ActiveGenie
12
12
  @higher_tier = players.calc_higher_tier
13
13
  @lower_tier = players.calc_lower_tier
14
14
  @criteria = criteria
15
- @config = ActiveGenie.configuration.merge(config)
15
+ @initial_config = config
16
16
  @tmp_highers = []
17
17
  @total_tokens = 0
18
18
  @previous_elo = @players.to_h { |player| [player.id, player.elo] }
@@ -20,16 +20,16 @@ module ActiveGenie
20
20
  end
21
21
 
22
22
  def call
23
- @config.log.add_observer(observers: ->(log) { log_observer(log) })
24
- @config.log.additional_context = { elo_id: }
23
+ config.log.add_observer(observers: ->(log) { log_observer(log) })
24
+ config.log.additional_context = { elo_id: }
25
25
 
26
- ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
26
+ ActiveGenie::FiberByBatch.call(matches, config:) do |player_a, player_b|
27
27
  winner, loser = debate(player_a, player_b)
28
28
 
29
29
  update_players_elo(winner, loser)
30
30
  end
31
31
 
32
- build_report
32
+ elo_result
33
33
  end
34
34
 
35
35
  DEBATE_PER_PLAYER = 3
@@ -38,39 +38,37 @@ module ActiveGenie
38
38
  private
39
39
 
40
40
  def matches
41
- match_keys = {}
42
-
43
- @higher_tier.each_with_object([]) do |attack_player, matches|
41
+ @matches ||= @lower_tier.each_with_object([]) do |lower_player, matches|
44
42
  DEBATE_PER_PLAYER.times do
45
43
  higher_player = next_higher_player
46
44
 
47
- next if match_keys["#{attack_player.id}_#{higher_player.id}"]
45
+ next if matches.include?([lower_player, higher_player])
48
46
 
49
- match_keys["#{attack_player.id}_#{higher_player.id}"] = true
50
- matches << [attack_player, higher_player]
47
+ matches << [lower_player, higher_player]
51
48
  end
52
49
  end
53
50
  end
54
51
 
55
52
  def next_higher_player
56
- @tmp_highers = @higher_tier.shuffle if @tmp_highers.empty?
53
+ @tmp_highers = @higher_tier.dup if @tmp_highers.empty?
57
54
 
58
- @tmp_highers.pop
55
+ @tmp_highers.count % 2 ? @tmp_highers.shift : @tmp_highers.pop
59
56
  end
60
57
 
61
58
  def debate(player_a, player_b)
59
+ debate_config = ActiveGenie::DeepMerge.call(
60
+ config.to_h,
61
+ { log: { additional_context: { player_a_id: player_a.id, player_b_id: player_b.id } } }
62
+ )
63
+
62
64
  result = ActiveGenie::Comparator.by_debate(
63
65
  player_a.content,
64
66
  player_b.content,
65
67
  @criteria,
66
- config: @config.merge(additional_context: { player_a_id: player_a.id, player_b_id: player_b.id })
68
+ config: ActiveGenie.new_configuration(debate_config)
67
69
  )
68
70
 
69
- winner, loser = case result['winner']
70
- when 'player_a' then [player_a, player_b]
71
- when 'player_b' then [player_b, player_a]
72
- when 'draw' then [nil, nil]
73
- end
71
+ winner, loser = result.data == player_a.content ? [player_a, player_b] : [player_b, player_a]
74
72
 
75
73
  [winner, loser]
76
74
  end
@@ -94,27 +92,30 @@ module ActiveGenie
94
92
  higher_tier_ids = @higher_tier.map(&:id).join(',')
95
93
  lower_tier_ids = @lower_tier.map(&:id).join(',')
96
94
 
97
- ranker_unique_key = [higher_tier_ids, lower_tier_ids, @criteria, @config.to_json].join('-')
95
+ ranker_unique_key = [higher_tier_ids, lower_tier_ids, @criteria].join('-')
98
96
  Digest::MD5.hexdigest(ranker_unique_key)
99
97
  end
100
98
  end
101
99
 
102
- def build_report
103
- report = {
104
- elo_id:,
105
- players_in: players_in.map(&:id),
106
- debates_count: matches.size,
107
- total_tokens: @total_tokens,
108
- players_in_round: players_in.map(&:id),
109
- previous_highest_elo: @previous_highest_elo,
110
- highest_elo:,
111
- highest_elo_diff: highest_elo - @previous_highest_elo,
112
- players_elo_diff:
113
- }
114
-
115
- @config.logger.call({ elo_id:, code: :elo_report, **report })
116
-
117
- report
100
+ def elo_result
101
+ result = ActiveGenie::Result.new(
102
+ data: @players.sorted.map(&:content),
103
+ metadata: {
104
+ elo_id:,
105
+ players: @players,
106
+ players_in_round: players_in.map(&:id),
107
+ debates_count: matches.size,
108
+ total_tokens: @total_tokens,
109
+ previous_highest_elo: @previous_highest_elo,
110
+ highest_elo:,
111
+ highest_elo_diff: highest_elo - @previous_highest_elo,
112
+ players_elo_diff:
113
+ }
114
+ )
115
+
116
+ ActiveGenie.logger.call({ elo_id:, code: :elo_report, **result.metadata }, config:)
117
+
118
+ result
118
119
  end
119
120
 
120
121
  def players_in
@@ -135,6 +136,10 @@ module ActiveGenie
135
136
  def log_observer(log)
136
137
  @total_tokens += log[:total_tokens] if log[:code] == :llm_usage
137
138
  end
139
+
140
+ def config
141
+ @config ||= ActiveGenie.new_configuration(@initial_config)
142
+ end
138
143
  end
139
144
  end
140
145
  end
@@ -10,22 +10,19 @@ module ActiveGenie
10
10
  def initialize(players, criteria, config: nil)
11
11
  @players = Entities::Players.new(players)
12
12
  @criteria = criteria
13
- @config = config || ActiveGenie.configuration
13
+ @initial_config = config
14
14
  @start_time = Time.now
15
15
  @total_tokens = 0
16
16
  end
17
17
 
18
18
  def call
19
- @config.log.add_observer(observers: ->(log) { log_observer(log) })
20
- @config.log.additional_context = { free_for_all_id: }
21
-
22
- ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
19
+ ActiveGenie::FiberByBatch.call(matches, config:) do |player_a, player_b|
23
20
  winner, loser = debate(player_a, player_b)
24
21
 
25
22
  update_players_score(winner, loser)
26
23
  end
27
24
 
28
- build_report
25
+ build_result
29
26
  end
30
27
 
31
28
  private
@@ -37,29 +34,32 @@ module ActiveGenie
37
34
  end
38
35
 
39
36
  def debate(player_a, player_b)
40
- log_context = { player_a_id: player_a.id, player_b_id: player_b.id }
37
+ debate_config = ActiveGenie::DeepMerge.call(
38
+ config.to_h,
39
+ { log: { additional_context: { player_a_id: player_a.id, player_b_id: player_b.id } } }
40
+ )
41
41
 
42
42
  result = ActiveGenie::Comparator.by_debate(
43
43
  player_a.content,
44
44
  player_b.content,
45
45
  @criteria,
46
- config: @config.merge(additional_context: log_context)
46
+ config: ActiveGenie.new_configuration(debate_config)
47
47
  )
48
48
 
49
- winner, loser = case result['winner']
50
- when 'player_a' then [player_a, player_b]
51
- when 'player_b' then [player_b, player_a]
52
- when 'draw' then [nil, nil]
49
+ winner, loser = case result.data
50
+ when player_a.to_s then [player_a, player_b]
51
+ else [player_b, player_a]
53
52
  end
54
53
 
55
- @config.logger.call(
54
+ ActiveGenie.logger.call(
56
55
  {
57
- **log_context,
56
+ player_a_id: player_a.id,
57
+ player_b_id: player_b.id,
58
58
  code: :free_for_all,
59
- winner_id: winner&.id,
60
- loser_id: loser&.id,
61
- reasoning: result['reasoning']
62
- }
59
+ winner: winner.to_s[0..20],
60
+ loser: loser.to_s[0..20],
61
+ reasoning: result.reasoning
62
+ }, config:
63
63
  )
64
64
 
65
65
  [winner, loser]
@@ -80,27 +80,44 @@ module ActiveGenie
80
80
  def free_for_all_id
81
81
  @free_for_all_id ||= begin
82
82
  eligible_ids = @players.eligible.map(&:id).join(',')
83
- ranking_unique_key = [eligible_ids, @criteria, @config.to_json].join('-')
83
+ ranking_unique_key = [eligible_ids, @criteria].join('-')
84
84
  Digest::MD5.hexdigest(ranking_unique_key)
85
85
  end
86
86
  end
87
87
 
88
- def build_report
89
- report = {
90
- free_for_all_id:,
91
- debates_count: matches.size,
92
- duration_seconds: Time.now - @start_time,
93
- total_tokens: @total_tokens
94
- }
88
+ def build_result
89
+ result = ActiveGenie::Result.new(
90
+ data: @players.sorted.map(&:content),
91
+ metadata: {
92
+ free_for_all_id:,
93
+ players: @players,
94
+ debates_count: matches.size,
95
+ duration_seconds: Time.now - @start_time,
96
+ total_tokens: @total_tokens
97
+ }
98
+ )
95
99
 
96
- @config.logger.call({ code: :free_for_all_report, **report })
100
+ ActiveGenie.logger.call({ code: :free_for_all_report, **result.metadata }, config:)
97
101
 
98
- report
102
+ result
99
103
  end
100
104
 
101
105
  def log_observer(log)
102
106
  @total_tokens += log[:total_tokens] if log[:code] == :llm_usage
103
107
  end
108
+
109
+ def config
110
+ @config ||= begin
111
+ c = ActiveGenie.new_configuration(
112
+ ActiveGenie::DeepMerge.call(
113
+ @initial_config.to_h,
114
+ { log: { context: { free_for_all_id: } } }
115
+ )
116
+ )
117
+ c.log.add_observer(observers: ->(log) { log_observer(log) })
118
+ c
119
+ end
120
+ end
104
121
  end
105
122
  end
106
123
  end