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.
- checksums.yaml +4 -4
- data/README.md +63 -57
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +7 -7
- data/lib/active_genie/battle/basic.rb +75 -68
- data/lib/active_genie/battle.rb +4 -0
- data/lib/active_genie/clients/anthropic_client.rb +110 -0
- data/lib/active_genie/clients/google_client.rb +158 -0
- data/lib/active_genie/clients/helpers/retry.rb +29 -0
- data/lib/active_genie/clients/openai_client.rb +58 -38
- data/lib/active_genie/clients/unified_client.rb +5 -5
- data/lib/active_genie/concerns/loggable.rb +44 -0
- data/lib/active_genie/configuration/log_config.rb +1 -1
- data/lib/active_genie/configuration/providers/anthropic_config.rb +54 -0
- data/lib/active_genie/configuration/providers/base_config.rb +85 -0
- data/lib/active_genie/configuration/providers/deepseek_config.rb +54 -0
- data/lib/active_genie/configuration/providers/google_config.rb +56 -0
- data/lib/active_genie/configuration/providers/openai_config.rb +54 -0
- data/lib/active_genie/configuration/providers_config.rb +7 -4
- data/lib/active_genie/configuration/runtime_config.rb +35 -0
- data/lib/active_genie/configuration.rb +18 -4
- data/lib/active_genie/data_extractor/README.md +0 -1
- data/lib/active_genie/data_extractor/basic.rb +22 -19
- data/lib/active_genie/data_extractor/from_informal.rb +4 -15
- data/lib/active_genie/data_extractor.rb +4 -0
- data/lib/active_genie/logger.rb +60 -14
- data/lib/active_genie/{league → ranking}/README.md +7 -7
- data/lib/active_genie/ranking/elo_round.rb +134 -0
- data/lib/active_genie/ranking/free_for_all.rb +93 -0
- data/lib/active_genie/ranking/player.rb +92 -0
- data/lib/active_genie/{league → ranking}/players_collection.rb +19 -12
- data/lib/active_genie/ranking/ranking.rb +153 -0
- data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
- data/lib/active_genie/ranking.rb +12 -0
- data/lib/active_genie/scoring/README.md +1 -1
- data/lib/active_genie/scoring/basic.rb +93 -49
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +18 -7
- data/lib/active_genie/scoring.rb +6 -3
- data/lib/active_genie.rb +1 -1
- data/lib/tasks/benchmark.rake +27 -0
- metadata +100 -100
- data/lib/active_genie/configuration/openai_config.rb +0 -56
- data/lib/active_genie/league/elo_ranking.rb +0 -121
- data/lib/active_genie/league/free_for_all.rb +0 -62
- data/lib/active_genie/league/league.rb +0 -120
- data/lib/active_genie/league/player.rb +0 -59
- 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(
|
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.
|
20
|
+
@default || @all.values.find { |p| p.api_key }.class::NAME
|
20
21
|
end
|
21
22
|
|
22
|
-
def
|
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(
|
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(
|
24
|
-
log: log.to_h(
|
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(
|
7
|
-
new(
|
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
|
48
|
+
properties:,
|
49
|
+
required: properties.keys
|
46
50
|
}
|
47
51
|
}
|
48
52
|
|
49
|
-
::ActiveGenie::Clients::UnifiedClient.function_calling(
|
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(
|
4
|
-
new(
|
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
|
data/lib/active_genie/logger.rb
CHANGED
@@ -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
|
-
|
22
|
+
call(log, level: :info)
|
10
23
|
end
|
11
24
|
|
12
25
|
def error(log)
|
13
|
-
|
26
|
+
call(log, level: :error)
|
14
27
|
end
|
15
28
|
|
16
29
|
def warn(log)
|
17
|
-
|
30
|
+
call(log, level: :warn)
|
18
31
|
end
|
19
32
|
|
20
33
|
def debug(log)
|
21
|
-
|
34
|
+
call(log, level: :debug)
|
22
35
|
end
|
23
36
|
|
24
37
|
def trace(log)
|
25
|
-
|
38
|
+
call(log, level: :trace)
|
26
39
|
end
|
27
40
|
|
28
|
-
|
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
|
-
|
31
|
-
|
54
|
+
log
|
55
|
+
end
|
32
56
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
74
|
+
end
|
41
75
|
|
42
|
-
|
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
|
-
#
|
1
|
+
# Ranking
|
2
2
|
|
3
|
-
The `ActiveGenie::
|
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
|
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
|
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
|
-
- **
|
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
|
27
|
+
Call the ranking using:
|
28
28
|
|
29
29
|
```ruby
|
30
|
-
result = ActiveGenie::
|
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
|