active_genie 0.0.10 → 0.0.18

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +63 -57
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +7 -7
  5. data/lib/active_genie/battle/basic.rb +75 -68
  6. data/lib/active_genie/battle.rb +4 -0
  7. data/lib/active_genie/clients/anthropic_client.rb +110 -0
  8. data/lib/active_genie/clients/google_client.rb +158 -0
  9. data/lib/active_genie/clients/helpers/retry.rb +29 -0
  10. data/lib/active_genie/clients/openai_client.rb +58 -38
  11. data/lib/active_genie/clients/unified_client.rb +5 -5
  12. data/lib/active_genie/concerns/loggable.rb +44 -0
  13. data/lib/active_genie/configuration/log_config.rb +1 -1
  14. data/lib/active_genie/configuration/providers/anthropic_config.rb +54 -0
  15. data/lib/active_genie/configuration/providers/base_config.rb +85 -0
  16. data/lib/active_genie/configuration/providers/deepseek_config.rb +54 -0
  17. data/lib/active_genie/configuration/providers/google_config.rb +56 -0
  18. data/lib/active_genie/configuration/providers/openai_config.rb +54 -0
  19. data/lib/active_genie/configuration/providers_config.rb +7 -4
  20. data/lib/active_genie/configuration/runtime_config.rb +35 -0
  21. data/lib/active_genie/configuration.rb +18 -4
  22. data/lib/active_genie/data_extractor/README.md +0 -1
  23. data/lib/active_genie/data_extractor/basic.rb +22 -19
  24. data/lib/active_genie/data_extractor/from_informal.rb +4 -15
  25. data/lib/active_genie/data_extractor.rb +4 -0
  26. data/lib/active_genie/logger.rb +60 -14
  27. data/lib/active_genie/{league → ranking}/README.md +7 -7
  28. data/lib/active_genie/ranking/elo_round.rb +134 -0
  29. data/lib/active_genie/ranking/free_for_all.rb +93 -0
  30. data/lib/active_genie/ranking/player.rb +92 -0
  31. data/lib/active_genie/{league → ranking}/players_collection.rb +19 -12
  32. data/lib/active_genie/ranking/ranking.rb +153 -0
  33. data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
  34. data/lib/active_genie/ranking.rb +12 -0
  35. data/lib/active_genie/scoring/README.md +1 -1
  36. data/lib/active_genie/scoring/basic.rb +93 -49
  37. data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +18 -7
  38. data/lib/active_genie/scoring.rb +6 -3
  39. data/lib/active_genie.rb +1 -1
  40. data/lib/tasks/benchmark.rake +27 -0
  41. metadata +100 -100
  42. data/lib/active_genie/configuration/openai_config.rb +0 -56
  43. data/lib/active_genie/league/elo_ranking.rb +0 -121
  44. data/lib/active_genie/league/free_for_all.rb +0 -62
  45. data/lib/active_genie/league/league.rb +0 -120
  46. data/lib/active_genie/league/player.rb +0 -59
  47. data/lib/active_genie/league.rb +0 -12
@@ -0,0 +1,54 @@
1
+ require_relative '../../clients/openai_client'
2
+ require_relative './base_config'
3
+
4
+ module ActiveGenie
5
+ module Configuration::Providers
6
+ # Configuration class for the OpenAI API client.
7
+ # Manages API keys, organization IDs, URLs, model selections, and client instantiation.
8
+ class OpenaiConfig < BaseConfig
9
+ NAME = :openai
10
+
11
+ # Retrieves the API key.
12
+ # Falls back to the OPENAI_API_KEY environment variable if not set.
13
+ # @return [String, nil] The API key.
14
+ def api_key
15
+ @api_key || ENV['OPENAI_API_KEY']
16
+ end
17
+
18
+ # Retrieves the base API URL for OpenAI API.
19
+ # Defaults to 'https://api.openai.com/v1'.
20
+ # @return [String] The API base URL.
21
+ def api_url
22
+ @api_url || 'https://api.openai.com/v1'
23
+ end
24
+
25
+ # Lazily initializes and returns an instance of the OpenaiClient.
26
+ # Passes itself (the config object) to the client's constructor.
27
+ # @return [ActiveGenie::Clients::OpenaiClient] The client instance.
28
+ def client
29
+ @client ||= ::ActiveGenie::Clients::OpenaiClient.new(self)
30
+ end
31
+
32
+ # Retrieves the model name designated for the lower tier (e.g., cost-effective, faster).
33
+ # Defaults to 'gpt-4o-mini'.
34
+ # @return [String] The lower tier model name.
35
+ def lower_tier_model
36
+ @lower_tier_model || 'gpt-4o-mini'
37
+ end
38
+
39
+ # Retrieves the model name designated for the middle tier (e.g., balanced performance).
40
+ # Defaults to 'gpt-4o'.
41
+ # @return [String] The middle tier model name.
42
+ def middle_tier_model
43
+ @middle_tier_model || 'gpt-4o'
44
+ end
45
+
46
+ # Retrieves the model name designated for the upper tier (e.g., most capable).
47
+ # Defaults to 'o1-preview'.
48
+ # @return [String] The upper tier model name.
49
+ def upper_tier_model
50
+ @upper_tier_model || 'o1-preview'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -5,8 +5,9 @@ module ActiveGenie::Configuration
5
5
  @default = nil
6
6
  end
7
7
 
8
- def register(name, provider_class)
8
+ def register(provider_class)
9
9
  @all ||= {}
10
+ name = provider_class::NAME
10
11
  @all[name] = provider_class.new
11
12
  define_singleton_method(name) do
12
13
  instance_variable_get("@#{name}") || instance_variable_set("@#{name}", @all[name])
@@ -16,11 +17,12 @@ module ActiveGenie::Configuration
16
17
  end
17
18
 
18
19
  def default
19
- @default || @all.values.first
20
+ @default || @all.values.find { |p| p.api_key }.class::NAME
20
21
  end
21
22
 
22
- def all
23
- @all
23
+ def valid
24
+ valid_provider_keys = @all.keys.select { |k| @all[k].valid? }
25
+ @all.slice(*valid_provider_keys)
24
26
  end
25
27
 
26
28
  def to_h(config = {})
@@ -32,6 +34,7 @@ module ActiveGenie::Configuration
32
34
  end
33
35
 
34
36
  private
37
+
35
38
  attr_writer :default
36
39
  end
37
40
  end
@@ -0,0 +1,35 @@
1
+ module ActiveGenie::Configuration
2
+ class RuntimeConfig
3
+ attr_writer :max_tokens, :temperature, :model, :provider, :api_key, :max_retries
4
+
5
+ def max_tokens
6
+ @max_tokens ||= 4096
7
+ end
8
+
9
+ def temperature
10
+ @temperature ||= 0.1
11
+ end
12
+
13
+ def model
14
+ @model
15
+ end
16
+
17
+ def provider
18
+ @provider ||= ActiveGenie.configuration.providers.default
19
+ end
20
+
21
+ def api_key
22
+ @api_key
23
+ end
24
+
25
+ def max_retries
26
+ @max_retries ||= 3
27
+ end
28
+
29
+ def to_h(config = {})
30
+ {
31
+ max_tokens:, temperature:, model:, provider:, api_key:, max_retries:,
32
+ }.merge(config)
33
+ end
34
+ end
35
+ end
@@ -1,6 +1,10 @@
1
1
  require_relative 'configuration/providers_config'
2
- require_relative 'configuration/openai_config'
2
+ require_relative 'configuration/providers/openai_config'
3
+ require_relative 'configuration/providers/google_config'
4
+ require_relative 'configuration/providers/anthropic_config'
5
+ require_relative 'configuration/providers/deepseek_config'
3
6
  require_relative 'configuration/log_config'
7
+ require_relative 'configuration/runtime_config'
4
8
 
5
9
  module ActiveGenie
6
10
  module Configuration
@@ -9,7 +13,10 @@ module ActiveGenie
9
13
  def providers
10
14
  @providers ||= begin
11
15
  p = ProvidersConfig.new
12
- p.register(:openai, ActiveGenie::Configuration::OpenaiConfig)
16
+ p.register(ActiveGenie::Configuration::Providers::OpenaiConfig)
17
+ p.register(ActiveGenie::Configuration::Providers::GoogleConfig)
18
+ p.register(ActiveGenie::Configuration::Providers::AnthropicConfig)
19
+ p.register(ActiveGenie::Configuration::Providers::DeepseekConfig)
13
20
  p
14
21
  end
15
22
  end
@@ -18,10 +25,17 @@ module ActiveGenie
18
25
  @log ||= LogConfig.new
19
26
  end
20
27
 
28
+ def runtime
29
+ @runtime ||= RuntimeConfig.new
30
+ end
31
+
21
32
  def to_h(configs = {})
33
+ normalized_configs = configs.dig(:runtime) ? configs : { runtime: configs }
34
+
22
35
  {
23
- providers: providers.to_h(configs.dig(:providers) || {}),
24
- log: log.to_h(configs.dig(:log) || {})
36
+ providers: providers.to_h(normalized_configs.dig(:providers) || {}),
37
+ log: log.to_h(normalized_configs.dig(:log) || {}),
38
+ runtime: runtime.to_h(normalized_configs.dig(:runtime) || {})
25
39
  }
26
40
  end
27
41
  end
@@ -65,7 +65,6 @@ The `from_informal` method extends the basic extraction by analyzing rhetorical
65
65
  - Litotes ("not bad", "isn't terrible")
66
66
  - Affirmative expressions ("sure", "no problem")
67
67
  - Negative expressions ("nah", "not really")
68
- - Hedging ("maybe", "I guess")
69
68
 
70
69
  ### Example
71
70
 
@@ -3,8 +3,8 @@ require_relative '../clients/unified_client'
3
3
 
4
4
  module ActiveGenie::DataExtractor
5
5
  class Basic
6
- def self.call(text, data_to_extract, config: {})
7
- new(text, data_to_extract, config:).call
6
+ def self.call(...)
7
+ new(...).call
8
8
  end
9
9
 
10
10
  # Extracts structured data from text based on a predefined schema.
@@ -37,16 +37,34 @@ module ActiveGenie::DataExtractor
37
37
  { role: 'system', content: PROMPT },
38
38
  { role: 'user', content: @text }
39
39
  ]
40
+
41
+ properties = data_to_extract_with_explaination
42
+
40
43
  function = {
41
44
  name: 'data_extractor',
42
45
  description: 'Extract structured and typed data from user messages.',
43
46
  schema: {
44
47
  type: "object",
45
- properties: data_to_extract_with_explaination
48
+ properties:,
49
+ required: properties.keys
46
50
  }
47
51
  }
48
52
 
49
- ::ActiveGenie::Clients::UnifiedClient.function_calling(messages, function, config:)
53
+ result = ::ActiveGenie::Clients::UnifiedClient.function_calling(
54
+ messages,
55
+ function,
56
+ model_tier: 'lower_tier',
57
+ config: @config
58
+ )
59
+
60
+ ActiveGenie::Logger.debug({
61
+ code: :data_extractor,
62
+ text: @text[0..30],
63
+ data_to_extract: @data_to_extract,
64
+ extracted_data: result
65
+ })
66
+
67
+ result
50
68
  end
51
69
 
52
70
  private
@@ -59,10 +77,6 @@ module ActiveGenie::DataExtractor
59
77
  1. **Identify Data Types**: Determine the types of data to collect, such as names, dates, email addresses, phone numbers, etc.
60
78
  2. **Extract Information**: Use pattern recognition and language understanding to identify and extract the relevant pieces of data from the user message.
61
79
  3. **Categorize Data**: Assign the extracted data to the appropriate predefined fields.
62
- 4. **Structure Data**: Format the extracted and categorized data in a structured format, such as JSON.
63
-
64
- # Output Format
65
- The output should be a JSON object containing fields with their corresponding extracted values. If a value is not found, the field should still be included with a null value.
66
80
 
67
81
  # Notes
68
82
  - Handle missing or partial information gracefully.
@@ -83,16 +97,5 @@ module ActiveGenie::DataExtractor
83
97
 
84
98
  with_explaination
85
99
  end
86
-
87
- def config
88
- {
89
- all_providers: { model_tier: 'lower_tier' },
90
- log: {
91
- **(@config.dig(:log) || {}),
92
- trace: self.class.name,
93
- },
94
- **@config
95
- }
96
- end
97
100
  end
98
101
  end
@@ -1,7 +1,7 @@
1
1
  module ActiveGenie::DataExtractor
2
2
  class FromInformal
3
- def self.call(text, data_to_extract, config: {})
4
- new(text, data_to_extract, config:).call()
3
+ def self.call(...)
4
+ new(...).call()
5
5
  end
6
6
 
7
7
  # Extracts data from informal text while also detecting litotes and their meanings.
@@ -30,10 +30,10 @@ module ActiveGenie::DataExtractor
30
30
  end
31
31
 
32
32
  def call
33
- response = Basic.call(@text, data_to_extract_with_litote, config:)
33
+ response = Basic.call(@text, data_to_extract_with_litote, config: @config)
34
34
 
35
35
  if response['message_litote']
36
- response = Basic.call(response['litote_rephrased'], @data_to_extract, config:)
36
+ response = Basic.call(response['litote_rephrased'], @data_to_extract, config: @config)
37
37
  end
38
38
 
39
39
  response
@@ -54,16 +54,5 @@ module ActiveGenie::DataExtractor
54
54
  }
55
55
  }
56
56
  end
57
-
58
- def config
59
- {
60
- all_providers: { model_tier: 'lower_tier' },
61
- **@config,
62
- log: {
63
- **@config.dig(:log),
64
- trace: self.class.name,
65
- },
66
- }
67
- end
68
57
  end
69
58
  end
@@ -10,6 +10,10 @@ module ActiveGenie
10
10
  Basic.call(...)
11
11
  end
12
12
 
13
+ def call(...)
14
+ Basic.call(...)
15
+ end
16
+
13
17
  def from_informal(...)
14
18
  FromInformal.call(...)
15
19
  end
@@ -5,41 +5,87 @@ module ActiveGenie
5
5
  module Logger
6
6
  module_function
7
7
 
8
+ def with_context(context, observer: nil)
9
+ @context ||= {}
10
+ @observers ||= []
11
+ begin
12
+ @context = @context.merge(context)
13
+ @observers << observer if observer
14
+ yield if block_given?
15
+ ensure
16
+ @context.delete_if { |key, _| context.key?(key) }
17
+ @observers.delete(observer)
18
+ end
19
+ end
20
+
8
21
  def info(log)
9
- save(log, level: :info)
22
+ call(log, level: :info)
10
23
  end
11
24
 
12
25
  def error(log)
13
- save(log, level: :error)
26
+ call(log, level: :error)
14
27
  end
15
28
 
16
29
  def warn(log)
17
- save(log, level: :warn)
30
+ call(log, level: :warn)
18
31
  end
19
32
 
20
33
  def debug(log)
21
- save(log, level: :debug)
34
+ call(log, level: :debug)
22
35
  end
23
36
 
24
37
  def trace(log)
25
- save(log, level: :trace)
38
+ call(log, level: :trace)
26
39
  end
27
40
 
28
- LOG_LEVELS = { info: 0, error: 1, warn: 2, debug: 3, trace: 4 }.freeze
41
+ def call(data, level: :info)
42
+ log = {
43
+ **(@context || {}),
44
+ **(data || {}),
45
+ timestamp: Time.now,
46
+ level: level.to_s.upcase,
47
+ process_id: Process.pid
48
+ }
49
+
50
+ append_to_file(log)
51
+ output(log, level)
52
+ call_observers(log)
29
53
 
30
- def save(log, level: :info)
31
- return if LOG_LEVELS[log.dig(:log, :log_level)] || -1 < LOG_LEVELS[level]
54
+ log
55
+ end
32
56
 
33
- log[:trace] = log.dig(:trace)&.to_s&.gsub('ActiveGenie::', '')
34
- log[:timestamp] = Time.now
35
- log[:level] = level.to_s.upcase
36
- log[:process_id] = Process.pid
57
+ # Log Levels
58
+ #
59
+ # LOG_LEVELS defines different levels of logging within the application.
60
+ # Each level serves a specific purpose, balancing verbosity and relevance.
61
+ #
62
+ # - :info -> General log messages providing an overview of application behavior, ensuring readability without excessive detail.
63
+ # - :warn -> Indicates unexpected behaviors that do not halt execution but require attention, such as retries, timeouts, or necessary conversions.
64
+ # - :error -> Represents critical errors that prevent the application from functioning correctly.
65
+ # - :debug -> Provides detailed logs for debugging, offering the necessary context for audits but with slightly less detail than trace logs.
66
+ # - :trace -> Logs every external call with the highest level of detail, primarily for auditing or state-saving purposes. These logs do not provide context regarding triggers or reasons.
67
+ LOG_LEVELS = { info: 0, error: 0, warn: 1, debug: 2, trace: 3 }.freeze
37
68
 
69
+ attr_accessor :context
70
+
71
+ def append_to_file(log)
38
72
  FileUtils.mkdir_p('logs')
39
73
  File.write('logs/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
40
- puts log
74
+ end
41
75
 
42
- log
76
+ def output(log, level)
77
+ config_log_level = LOG_LEVELS[log.dig(:config, :log_level)] || LOG_LEVELS[:info]
78
+ if config_log_level >= LOG_LEVELS[level]
79
+ $stdout.puts log
80
+ else
81
+ $stdout.print '.'
82
+ end
83
+ end
84
+
85
+ def call_observers(log)
86
+ return if @observers.nil? || @observers.size.zero?
87
+
88
+ @observers.each { |observer| observer.call(log) }
43
89
  end
44
90
  end
45
91
  end
@@ -1,22 +1,22 @@
1
- # league
1
+ # Ranking
2
2
 
3
- The `ActiveGenie::League` module organizes players based on scores derived from textual evaluations and then ranks them using a multi-stage process. It leverages the scoring system from the `ActiveGenie::Scoring` module to assign initial scores, and then applies advanced ranking methods to produce a competitive league.
3
+ The `ActiveGenie::Ranking` module organizes players based on scores derived from textual evaluations and then ranks them using a multi-stage process. It leverages the scoring system from the `ActiveGenie::Scoring` module to assign initial scores, and then applies advanced ranking methods to produce a competitive ranking.
4
4
 
5
5
  ## Overview
6
6
 
7
- The league system performs the following steps:
7
+ The ranking system performs the following steps:
8
8
 
9
9
  1. **Initial Scoring**: Each player’s textual content is evaluated using `ActiveGenie::Scoring`. This produces a `score` based on multiple expert reviews.
10
10
 
11
11
  2. **Elimination of Poor Performers**: Players whose scores show a high coefficient of variation (indicating inconsistency) are progressively eliminated. This ensures that only competitive candidates continue in the ranking process.
12
12
 
13
- 3. **ELO Ranking**: If there are more than 10 eligible players, an ELO-based ranking is applied. Battles between players are simulated via `ActiveGenie::Battle`, and scores are updated using an ELO algorithm tailored to the league.
13
+ 3. **ELO Ranking**: If there are more than 10 eligible players, an ELO-based ranking is applied. Battles between players are simulated via `ActiveGenie::Battle`, and scores are updated using an ELO algorithm tailored to the ranking.
14
14
 
15
15
  4. **Free for all Matches**: Finally, the remaining players engage in head-to-head matches where each unique pair competes. Match outcomes (wins, losses, draws) are recorded to finalize the rankings.
16
16
 
17
17
  ## Components
18
18
 
19
- - **league**: Orchestrates the entire ranking process. Initializes player scores, eliminates outliers, and coordinates ranking rounds.
19
+ - **ranking**: Orchestrates the entire ranking process. Initializes player scores, eliminates outliers, and coordinates ranking rounds.
20
20
 
21
21
  - **EloRanking**: Applies an ELO-based system to rank players through simulated battles. It updates players’ ELO scores based on match outcomes and predefined rules (including penalties for losses).
22
22
 
@@ -24,10 +24,10 @@ The league system performs the following steps:
24
24
 
25
25
  ## Usage
26
26
 
27
- Call the league using:
27
+ Call the ranking using:
28
28
 
29
29
  ```ruby
30
- result = ActiveGenie::League::league.call(players, criteria, config: {})
30
+ result = ActiveGenie::Ranking.call(players, criteria, config: {})
31
31
  ```
32
32
 
33
33
  - `players`: A collection of player instances, each containing textual content to be scored.
@@ -0,0 +1,134 @@
1
+ require_relative '../battle/basic'
2
+
3
+ module ActiveGenie::Ranking
4
+ class EloRound
5
+ def self.call(...)
6
+ new(...).call
7
+ end
8
+
9
+ def initialize(players, criteria, config: {})
10
+ @players = players
11
+ @relegation_tier = players.calc_relegation_tier
12
+ @defender_tier = players.calc_defender_tier
13
+ @criteria = criteria
14
+ @config = config
15
+ @tmp_defenders = []
16
+ @start_time = Time.now
17
+ @total_tokens = 0
18
+ @previous_elo = {}
19
+ @previous_highest_elo = @defender_tier.max_by(&:elo).elo
20
+ end
21
+
22
+ def call
23
+ ActiveGenie::Logger.with_context(log_context) do
24
+ save_previous_elo
25
+ matches.each do |player_1, player_2|
26
+ # TODO: battle can take a while, can be parallelized
27
+ winner, loser = battle(player_1, player_2)
28
+ next if winner.nil? || loser.nil?
29
+
30
+ winner.elo = calculate_new_elo(winner.elo, loser.elo, 1)
31
+ loser.elo = calculate_new_elo(loser.elo, winner.elo, 0)
32
+ end
33
+ end
34
+
35
+ ActiveGenie::Logger.info({ code: :elo_round_report, **report })
36
+
37
+ report
38
+ end
39
+
40
+ private
41
+
42
+ BATTLE_PER_PLAYER = 3
43
+ K = 32
44
+
45
+ def save_previous_elo
46
+ @previous_elo = @players.map { |player| [player.id, player.elo] }.to_h
47
+ end
48
+
49
+ def matches
50
+ @relegation_tier.reduce([]) do |matches, attack_player|
51
+ BATTLE_PER_PLAYER.times do
52
+ matches << [attack_player, next_defense_player].shuffle
53
+ end
54
+ matches
55
+ end
56
+ end
57
+
58
+ def next_defense_player
59
+ @tmp_defenders = @defender_tier.shuffle if @tmp_defenders.size.zero?
60
+
61
+ @tmp_defenders.pop
62
+ end
63
+
64
+ def battle(player_1, player_2)
65
+ ActiveGenie::Logger.with_context({ player_1_id: player_1.id, player_2_id: player_2.id }) do
66
+ result = ActiveGenie::Battle.basic(
67
+ player_1.content,
68
+ player_2.content,
69
+ @criteria,
70
+ config: @config
71
+ )
72
+
73
+ winner, loser = case result['winner']
74
+ when 'player_1' then [player_1, player_2]
75
+ when 'player_2' then [player_2, player_1]
76
+ when 'draw' then [nil, nil]
77
+ end
78
+
79
+ [winner, loser]
80
+ end
81
+ end
82
+
83
+ # INFO: Read more about the Elo rating system on https://en.wikipedia.org/wiki/Elo_rating_system
84
+ def calculate_new_elo(player_rating, opponent_rating, score)
85
+ expected_score = 1.0 / (1.0 + 10.0 ** ((opponent_rating - player_rating) / 400.0))
86
+
87
+ player_rating + (K * (score - expected_score)).round
88
+ end
89
+
90
+ def log_context
91
+ { elo_round_id: }
92
+ end
93
+
94
+ def elo_round_id
95
+ relegation_tier_ids = @relegation_tier.map(&:id).join(',')
96
+ defender_tier_ids = @defender_tier.map(&:id).join(',')
97
+
98
+ ranking_unique_key = [relegation_tier_ids, defender_tier_ids, @criteria, @config.to_json].join('-')
99
+ Digest::MD5.hexdigest(ranking_unique_key)
100
+ end
101
+
102
+ def report
103
+ {
104
+ elo_round_id:,
105
+ players_in_round: players_in_round.map(&:id),
106
+ battles_count: matches.size,
107
+ duration_seconds: Time.now - @start_time,
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
+ end
115
+
116
+ def players_in_round
117
+ @defender_tier + @relegation_tier
118
+ end
119
+
120
+ def highest_elo
121
+ players_in_round.max_by(&:elo).elo
122
+ end
123
+
124
+ def players_elo_diff
125
+ players_in_round.map do |player|
126
+ [player.id, player.elo - @previous_elo[player.id]]
127
+ end.sort_by { |_, diff| -diff }.to_h
128
+ end
129
+
130
+ def log_observer(log)
131
+ @total_tokens += log[:total_tokens] if log[:code] == :llm_usage
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,93 @@
1
+ require_relative '../battle/basic'
2
+
3
+ module ActiveGenie::Ranking
4
+ class FreeForAll
5
+ def self.call(...)
6
+ new(...).call
7
+ end
8
+
9
+ def initialize(players, criteria, config: {})
10
+ @players = players
11
+ @criteria = criteria
12
+ @config = config
13
+ @start_time = Time.now
14
+ @total_tokens = 0
15
+ end
16
+
17
+ def call
18
+ ActiveGenie::Logger.with_context(log_context, observer: method(:log_observer)) do
19
+ matches.each do |player_1, player_2|
20
+ winner, loser = battle(player_1, player_2)
21
+
22
+ if winner.nil? || loser.nil?
23
+ player_1.draw!
24
+ player_2.draw!
25
+ else
26
+ winner.win!
27
+ loser.lose!
28
+ end
29
+ end
30
+ end
31
+
32
+ ActiveGenie::Logger.info({ code: :free_for_all_report, **report })
33
+
34
+ report
35
+ end
36
+
37
+ private
38
+
39
+ # TODO: reduce the number of matches based on transitivity.
40
+ # For example, if A is better than B, and B is better than C, then battle between A and C should be auto win A
41
+ def matches
42
+ @players.eligible.combination(2).to_a
43
+ end
44
+
45
+ def battle(player_1, player_2)
46
+ result = ActiveGenie::Battle.basic(
47
+ player_1.content,
48
+ player_2.content,
49
+ @criteria,
50
+ config: @config
51
+ )
52
+
53
+ winner, loser = case result['winner']
54
+ when 'player_1' then [player_1, player_2, result['reasoning']]
55
+ when 'player_2' then [player_2, player_1, result['reasoning']]
56
+ when 'draw' then [nil, nil, result['reasoning']]
57
+ end
58
+
59
+ ActiveGenie::Logger.debug({
60
+ code: :free_for_all_battle,
61
+ player_ids: [player_1.id, player_2.id],
62
+ winner_id: winner&.id,
63
+ loser_id: loser&.id,
64
+ reasoning: result['reasoning']
65
+ })
66
+
67
+ [winner, loser]
68
+ end
69
+
70
+ def log_context
71
+ { free_for_all_id: }
72
+ end
73
+
74
+ def free_for_all_id
75
+ eligible_ids = @players.eligible.map(&:id).join(',')
76
+ ranking_unique_key = [eligible_ids, @criteria, @config.to_json].join('-')
77
+ Digest::MD5.hexdigest(ranking_unique_key)
78
+ end
79
+
80
+ def report
81
+ {
82
+ free_for_all_id:,
83
+ battles_count: matches.size,
84
+ duration_seconds: Time.now - @start_time,
85
+ total_tokens: @total_tokens,
86
+ }
87
+ end
88
+
89
+ def log_observer(log)
90
+ @total_tokens += log[:total_tokens] if log[:code] == :llm_usage
91
+ end
92
+ end
93
+ end