active_genie 0.0.8 → 0.0.12
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 +36 -72
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +2 -2
- data/lib/active_genie/battle/basic.rb +52 -56
- data/lib/active_genie/battle.rb +1 -1
- data/lib/active_genie/clients/openai_client.rb +119 -0
- data/lib/active_genie/clients/unified_client.rb +19 -0
- data/lib/active_genie/configuration/log_config.rb +14 -0
- data/lib/active_genie/configuration/openai_config.rb +56 -0
- data/lib/active_genie/configuration/providers_config.rb +37 -0
- data/lib/active_genie/configuration.rb +18 -23
- data/lib/active_genie/data_extractor/README.md +4 -5
- data/lib/active_genie/data_extractor/basic.rb +13 -13
- data/lib/active_genie/data_extractor/from_informal.rb +7 -7
- data/lib/active_genie/data_extractor.rb +1 -1
- data/lib/active_genie/logger.rb +72 -0
- data/lib/active_genie/ranking/README.md +43 -0
- data/lib/active_genie/ranking/elo_round.rb +113 -0
- data/lib/active_genie/ranking/free_for_all.rb +76 -0
- data/lib/active_genie/ranking/player.rb +97 -0
- data/lib/active_genie/{leaderboard → ranking}/players_collection.rb +18 -11
- data/lib/active_genie/ranking/ranking.rb +98 -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 +4 -8
- data/lib/active_genie/scoring/basic.rb +58 -24
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +21 -12
- data/lib/active_genie/scoring.rb +4 -4
- data/lib/active_genie.rb +10 -18
- data/lib/tasks/install.rake +3 -3
- data/lib/tasks/templates/active_genie.rb +17 -0
- metadata +74 -90
- data/lib/active_genie/clients/openai.rb +0 -61
- data/lib/active_genie/clients/router.rb +0 -41
- data/lib/active_genie/leaderboard/elo_ranking.rb +0 -88
- data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
- data/lib/active_genie/leaderboard/league.rb +0 -48
- data/lib/active_genie/leaderboard/player.rb +0 -52
- data/lib/active_genie/leaderboard.rb +0 -11
- data/lib/active_genie/utils/math.rb +0 -15
- data/lib/tasks/templates/active_genie.yml +0 -7
@@ -1,33 +1,28 @@
|
|
1
|
-
|
1
|
+
require_relative 'configuration/providers_config'
|
2
|
+
require_relative 'configuration/openai_config'
|
3
|
+
require_relative 'configuration/log_config'
|
2
4
|
|
3
5
|
module ActiveGenie
|
4
|
-
|
5
|
-
|
6
|
+
module Configuration
|
7
|
+
module_function
|
6
8
|
|
7
|
-
def
|
8
|
-
@
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
return @values if @values
|
13
|
-
|
14
|
-
@values = load_values.transform_keys(&:to_sym)
|
15
|
-
@values.each do |key, _value|
|
16
|
-
@values[key][:model] = key
|
17
|
-
@values[key] = @values[key].transform_keys(&:to_sym)
|
9
|
+
def providers
|
10
|
+
@providers ||= begin
|
11
|
+
p = ProvidersConfig.new
|
12
|
+
p.register(:openai, ActiveGenie::Configuration::OpenaiConfig)
|
13
|
+
p
|
18
14
|
end
|
19
15
|
end
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
return {} unless File.exist?(@path_to_config)
|
17
|
+
def log
|
18
|
+
@log ||= LogConfig.new
|
19
|
+
end
|
25
20
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
21
|
+
def to_h(configs = {})
|
22
|
+
{
|
23
|
+
providers: providers.to_h(configs.dig(:providers) || {}),
|
24
|
+
log: log.to_h(configs.dig(:log) || {})
|
25
|
+
}
|
31
26
|
end
|
32
27
|
end
|
33
28
|
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
|
|
@@ -95,7 +94,7 @@ result = ActiveGenie::DataExtractor.from_informal(text, schema)
|
|
95
94
|
|
96
95
|
## Interface
|
97
96
|
|
98
|
-
### `.call(text, data_to_extract,
|
97
|
+
### `.call(text, data_to_extract, config = {})`
|
99
98
|
Extracts structured data from text based on a predefined schema.
|
100
99
|
|
101
100
|
#### Parameters
|
@@ -103,9 +102,9 @@ Extracts structured data from text based on a predefined schema.
|
|
103
102
|
| --- | --- | --- | --- | --- |
|
104
103
|
| `text` | `String` | The text to analyze and extract data from | Yes | "John Doe is 25 years old" |
|
105
104
|
| `data_to_extract` | `Hash` | Schema defining the data structure to extract | Yes | `{ name: { type: 'string' } }` |
|
106
|
-
| `
|
105
|
+
| `config` | `Hash` | Additional extraction configuration | No | `{ model: "gpt-4" }` |
|
107
106
|
|
108
|
-
####
|
107
|
+
#### config
|
109
108
|
| Name | Type | Description |
|
110
109
|
| --- | --- | --- |
|
111
110
|
| `model` | `String` | The model to use for the extraction |
|
@@ -117,7 +116,7 @@ Extracts structured data from text based on a predefined schema.
|
|
117
116
|
- Explanation field for each extracted value
|
118
117
|
- Additional analysis fields when using `from_informal`
|
119
118
|
|
120
|
-
### `.from_informal(text, data_to_extract,
|
119
|
+
### `.from_informal(text, data_to_extract, config = {})`
|
121
120
|
Extends basic extraction with rhetorical analysis, particularly for litotes.
|
122
121
|
|
123
122
|
#### Additional Return Fields
|
@@ -1,9 +1,10 @@
|
|
1
|
-
|
1
|
+
|
2
|
+
require_relative '../clients/unified_client'
|
2
3
|
|
3
4
|
module ActiveGenie::DataExtractor
|
4
5
|
class Basic
|
5
|
-
def self.call(
|
6
|
-
new(
|
6
|
+
def self.call(...)
|
7
|
+
new(...).call
|
7
8
|
end
|
8
9
|
|
9
10
|
# Extracts structured data from text based on a predefined schema.
|
@@ -11,9 +12,7 @@ module ActiveGenie::DataExtractor
|
|
11
12
|
# @param text [String] The input text to analyze and extract data from
|
12
13
|
# @param data_to_extract [Hash] Schema defining the data structure to extract.
|
13
14
|
# Each key in the hash represents a field to extract, and its value defines the expected type and constraints.
|
14
|
-
# @param
|
15
|
-
# @option options [String] :model The model to use for the extraction
|
16
|
-
# @option options [String] :api_key The API key to use for the extraction
|
15
|
+
# @param config [Hash] Additional config for the extraction process
|
17
16
|
#
|
18
17
|
# @return [Hash] The extracted data matching the schema structure. Each field will include
|
19
18
|
# both the extracted value and an explanation of how it was derived.
|
@@ -27,10 +26,10 @@ module ActiveGenie::DataExtractor
|
|
27
26
|
# DataExtractor.call(text, schema)
|
28
27
|
# # => { name: "John Doe", name_explanation: "Found directly in text",
|
29
28
|
# # age: 25, age_explanation: "Explicitly stated as 25 years old" }
|
30
|
-
def initialize(text, data_to_extract,
|
29
|
+
def initialize(text, data_to_extract, config: {})
|
31
30
|
@text = text
|
32
31
|
@data_to_extract = data_to_extract
|
33
|
-
@
|
32
|
+
@config = ActiveGenie::Configuration.to_h(config)
|
34
33
|
end
|
35
34
|
|
36
35
|
def call
|
@@ -47,7 +46,12 @@ module ActiveGenie::DataExtractor
|
|
47
46
|
}
|
48
47
|
}
|
49
48
|
|
50
|
-
::ActiveGenie::Clients::
|
49
|
+
::ActiveGenie::Clients::UnifiedClient.function_calling(
|
50
|
+
messages,
|
51
|
+
function,
|
52
|
+
model_tier: 'lower_tier',
|
53
|
+
config: @config
|
54
|
+
)
|
51
55
|
end
|
52
56
|
|
53
57
|
private
|
@@ -60,10 +64,6 @@ module ActiveGenie::DataExtractor
|
|
60
64
|
1. **Identify Data Types**: Determine the types of data to collect, such as names, dates, email addresses, phone numbers, etc.
|
61
65
|
2. **Extract Information**: Use pattern recognition and language understanding to identify and extract the relevant pieces of data from the user message.
|
62
66
|
3. **Categorize Data**: Assign the extracted data to the appropriate predefined fields.
|
63
|
-
4. **Structure Data**: Format the extracted and categorized data in a structured format, such as JSON.
|
64
|
-
|
65
|
-
# Output Format
|
66
|
-
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.
|
67
67
|
|
68
68
|
# Notes
|
69
69
|
- Handle missing or partial information gracefully.
|
@@ -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.
|
@@ -9,7 +9,7 @@ module ActiveGenie::DataExtractor
|
|
9
9
|
#
|
10
10
|
# @param text [String] The informal text to analyze
|
11
11
|
# @param data_to_extract [Hash] Schema defining the data structure to extract
|
12
|
-
# @param
|
12
|
+
# @param config [Hash] Additional config for the extraction process
|
13
13
|
#
|
14
14
|
# @return [Hash] The extracted data including litote analysis. In addition to the
|
15
15
|
# schema-defined fields, includes:
|
@@ -23,17 +23,17 @@ module ActiveGenie::DataExtractor
|
|
23
23
|
# # => { mood: "positive", mood_explanation: "Speaker views weather favorably",
|
24
24
|
# # message_litote: true,
|
25
25
|
# # litote_rephrased: "The weather is good today" }
|
26
|
-
def initialize(text, data_to_extract,
|
26
|
+
def initialize(text, data_to_extract, config: {})
|
27
27
|
@text = text
|
28
28
|
@data_to_extract = data_to_extract
|
29
|
-
@
|
29
|
+
@config = ActiveGenie::Configuration.to_h(config)
|
30
30
|
end
|
31
31
|
|
32
32
|
def call
|
33
|
-
response = Basic.call(@text, data_to_extract_with_litote,
|
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,
|
36
|
+
response = Basic.call(response['litote_rephrased'], @data_to_extract, config: @config)
|
37
37
|
end
|
38
38
|
|
39
39
|
response
|
@@ -2,7 +2,7 @@ require_relative 'data_extractor/basic'
|
|
2
2
|
require_relative 'data_extractor/from_informal'
|
3
3
|
|
4
4
|
module ActiveGenie
|
5
|
-
#
|
5
|
+
# See the [DataExtractor README](lib/active_genie/data_extractor/README.md) for more information.
|
6
6
|
module DataExtractor
|
7
7
|
module_function
|
8
8
|
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module ActiveGenie
|
5
|
+
module Logger
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def with_context(context)
|
9
|
+
@context ||= {}
|
10
|
+
begin
|
11
|
+
@context = @context.merge(context)
|
12
|
+
yield if block_given?
|
13
|
+
ensure
|
14
|
+
@context.delete_if { |key, _| context.key?(key) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def info(log)
|
19
|
+
save(log, level: :info)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(log)
|
23
|
+
save(log, level: :error)
|
24
|
+
end
|
25
|
+
|
26
|
+
def warn(log)
|
27
|
+
save(log, level: :warn)
|
28
|
+
end
|
29
|
+
|
30
|
+
def debug(log)
|
31
|
+
save(log, level: :debug)
|
32
|
+
end
|
33
|
+
|
34
|
+
def trace(log)
|
35
|
+
save(log, level: :trace)
|
36
|
+
end
|
37
|
+
|
38
|
+
def save(data, level: :info)
|
39
|
+
log = @context.merge(data || {})
|
40
|
+
log[:timestamp] = Time.now
|
41
|
+
log[:level] = level.to_s.upcase
|
42
|
+
log[:process_id] = Process.pid
|
43
|
+
config_log_level = LOG_LEVELS[log.dig(:config, :log_level)] || LOG_LEVELS[:info]
|
44
|
+
|
45
|
+
FileUtils.mkdir_p('logs')
|
46
|
+
File.write('logs/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
|
47
|
+
if config_log_level >= LOG_LEVELS[level]
|
48
|
+
$stdout.puts log
|
49
|
+
else
|
50
|
+
$stdout.print '.'
|
51
|
+
end
|
52
|
+
|
53
|
+
log
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Log Levels
|
59
|
+
#
|
60
|
+
# LOG_LEVELS defines different levels of logging within the application.
|
61
|
+
# Each level serves a specific purpose, balancing verbosity and relevance.
|
62
|
+
#
|
63
|
+
# - :info -> General log messages providing an overview of application behavior, ensuring readability without excessive detail.
|
64
|
+
# - :warn -> Indicates unexpected behaviors that do not halt execution but require attention, such as retries, timeouts, or necessary conversions.
|
65
|
+
# - :error -> Represents critical errors that prevent the application from functioning correctly.
|
66
|
+
# - :debug -> Provides detailed logs for debugging, offering the necessary context for audits but with slightly less detail than trace logs.
|
67
|
+
# - :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.
|
68
|
+
LOG_LEVELS = { info: 0, error: 0, warn: 1, debug: 2, trace: 3 }.freeze
|
69
|
+
|
70
|
+
attr_accessor :context
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Ranking
|
2
|
+
|
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
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The ranking system performs the following steps:
|
8
|
+
|
9
|
+
1. **Initial Scoring**: Each player’s textual content is evaluated using `ActiveGenie::Scoring`. This produces a `score` based on multiple expert reviews.
|
10
|
+
|
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
|
+
|
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
|
+
|
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
|
+
|
17
|
+
## Components
|
18
|
+
|
19
|
+
- **ranking**: Orchestrates the entire ranking process. Initializes player scores, eliminates outliers, and coordinates ranking rounds.
|
20
|
+
|
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
|
+
|
23
|
+
- **Free for all**: Conducts complete pairwise matches among eligible players to record win/loss/draw statistics and refine the final standings.
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Call the ranking using:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
result = ActiveGenie::Ranking.call(players, criteria, config: {})
|
31
|
+
```
|
32
|
+
|
33
|
+
- `players`: A collection of player instances, each containing textual content to be scored.
|
34
|
+
- `criteria`: A string defining the evaluation criteria used by the scoring system.
|
35
|
+
- `config`: A hash of additional parameters for customization (e.g., model, api_key).
|
36
|
+
|
37
|
+
The method processes the players through scoring, elimination, and ranking phases, then returns a hash containing the player statistics and rankings.
|
38
|
+
|
39
|
+
## Possible improvements
|
40
|
+
- Adjust initial criteria to ensure consistency
|
41
|
+
- Adjust each player's content to ensure consistency
|
42
|
+
- Support players with images or audio
|
43
|
+
- Parallelize processing battles and scoring
|
@@ -0,0 +1,113 @@
|
|
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
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
ActiveGenie::Logger.with_context(log_context) do
|
20
|
+
matches.each do |player_a, player_b|
|
21
|
+
# TODO: battle can take a while, can be parallelized
|
22
|
+
winner, loser = battle(player_a, player_b)
|
23
|
+
|
24
|
+
next if winner.nil? || loser.nil?
|
25
|
+
|
26
|
+
new_winner_elo, new_loser_elo = calculate_new_elo(winner.elo, loser.elo)
|
27
|
+
|
28
|
+
winner.elo = new_winner_elo
|
29
|
+
loser.elo = new_loser_elo
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO: add a round report. Duration, Elo changes, etc.
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
BATTLE_PER_PLAYER = 3
|
39
|
+
LOSE_PENALTY = 15
|
40
|
+
K = 32
|
41
|
+
|
42
|
+
def matches
|
43
|
+
@relegation_tier.reduce([]) do |matches, attack_player|
|
44
|
+
BATTLE_PER_PLAYER.times do
|
45
|
+
matches << [attack_player, next_defense_player].shuffle
|
46
|
+
end
|
47
|
+
matches
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def next_defense_player
|
52
|
+
@tmp_defenders = @defender_tier if @tmp_defenders.size.zero?
|
53
|
+
|
54
|
+
@tmp_defenders.shuffle.pop!
|
55
|
+
end
|
56
|
+
|
57
|
+
def battle(player_a, player_b)
|
58
|
+
result = ActiveGenie::Battle.basic(
|
59
|
+
player_a,
|
60
|
+
player_b,
|
61
|
+
@criteria,
|
62
|
+
config: @config
|
63
|
+
)
|
64
|
+
|
65
|
+
winner, loser = case result['winner']
|
66
|
+
when 'player_a' then [player_a, player_b]
|
67
|
+
when 'player_b' then [player_b, player_a]
|
68
|
+
when 'draw' then [nil, nil]
|
69
|
+
end
|
70
|
+
|
71
|
+
ActiveGenie::Logger.debug({
|
72
|
+
step: :elo_round_battle,
|
73
|
+
player_ids: [player_a.id, player_b.id],
|
74
|
+
winner_id: winner&.id,
|
75
|
+
loser_id: loser&.id,
|
76
|
+
reasoning: result['reasoning']
|
77
|
+
})
|
78
|
+
|
79
|
+
[winner, loser]
|
80
|
+
end
|
81
|
+
|
82
|
+
# INFO: Read more about the Elo rating system on https://en.wikipedia.org/wiki/Elo_rating_system
|
83
|
+
def calculate_new_elo(winner_elo, loser_elo)
|
84
|
+
expected_score_a = 1 / (1 + 10**((loser_elo - winner_elo) / 400))
|
85
|
+
expected_score_b = 1 - expected_score_a
|
86
|
+
|
87
|
+
new_winner_elo = [winner_elo + K * (1 - expected_score_a), max_defense_elo].min
|
88
|
+
new_loser_elo = [loser_elo + K * (1 - expected_score_b) - LOSE_PENALTY, min_relegation_elo].max
|
89
|
+
|
90
|
+
[new_winner_elo, new_loser_elo]
|
91
|
+
end
|
92
|
+
|
93
|
+
def max_defense_elo
|
94
|
+
@defender_tier.max_by(&:elo).elo
|
95
|
+
end
|
96
|
+
|
97
|
+
def min_relegation_elo
|
98
|
+
@relegation_tier.min_by(&:elo).elo
|
99
|
+
end
|
100
|
+
|
101
|
+
def log_context
|
102
|
+
{ elo_round_id: }
|
103
|
+
end
|
104
|
+
|
105
|
+
def elo_round_id
|
106
|
+
relegation_tier_ids = @relegation_tier.map(&:id).join(',')
|
107
|
+
defender_tier_ids = @defender_tier.map(&:id).join(',')
|
108
|
+
|
109
|
+
ranking_unique_key = [relegation_tier_ids, defender_tier_ids, @criteria, @config.to_json].join('-')
|
110
|
+
Digest::MD5.hexdigest(ranking_unique_key)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
ActiveGenie::Logger.with_context(log_context) do
|
17
|
+
matches.each do |player_a, player_b|
|
18
|
+
winner, loser = battle(player_a, player_b)
|
19
|
+
|
20
|
+
if winner.nil? || loser.nil?
|
21
|
+
player_a.draw!
|
22
|
+
player_b.draw!
|
23
|
+
else
|
24
|
+
winner.win!
|
25
|
+
loser.lose!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: add a freeForAll report. Duration, Elo changes, etc.
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# TODO: reduce the number of matches based on transitivity.
|
36
|
+
# 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
|
37
|
+
def matches
|
38
|
+
@players.eligible.combination(2).to_a
|
39
|
+
end
|
40
|
+
|
41
|
+
def battle(player_a, player_b)
|
42
|
+
result = ActiveGenie::Battle.basic(
|
43
|
+
player_a,
|
44
|
+
player_b,
|
45
|
+
@criteria,
|
46
|
+
config: @config
|
47
|
+
)
|
48
|
+
|
49
|
+
winner, loser = case result['winner']
|
50
|
+
when 'player_a' then [player_a, player_b, result['reasoning']]
|
51
|
+
when 'player_b' then [player_b, player_a, result['reasoning']]
|
52
|
+
when 'draw' then [nil, nil, result['reasoning']]
|
53
|
+
end
|
54
|
+
|
55
|
+
ActiveGenie::Logger.debug({
|
56
|
+
step: :free_for_all_battle,
|
57
|
+
player_ids: [player_a.id, player_b.id],
|
58
|
+
winner_id: winner&.id,
|
59
|
+
loser_id: loser&.id,
|
60
|
+
reasoning: result['reasoning']
|
61
|
+
})
|
62
|
+
|
63
|
+
[winner, loser]
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_context
|
67
|
+
{ free_for_all_id: }
|
68
|
+
end
|
69
|
+
|
70
|
+
def free_for_all_id
|
71
|
+
eligible_ids = @players.eligible.map(&:id).join(',')
|
72
|
+
ranking_unique_key = [eligible_ids, @criteria, @config.to_json].join('-')
|
73
|
+
Digest::MD5.hexdigest(ranking_unique_key)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module ActiveGenie::Ranking
|
4
|
+
class Player
|
5
|
+
def initialize(params)
|
6
|
+
params = { content: params } if params.is_a?(String)
|
7
|
+
|
8
|
+
@content = params.dig(:content) || params
|
9
|
+
@id = params.dig(:id) || Digest::MD5.hexdigest(@content)
|
10
|
+
@score = params.dig(:score) || nil
|
11
|
+
@elo = params.dig(:elo) || nil
|
12
|
+
@ffa_win_count = params.dig(:ffa_win_count) || 0
|
13
|
+
@ffa_lose_count = params.dig(:ffa_lose_count) || 0
|
14
|
+
@ffa_draw_count = params.dig(:ffa_draw_count) || 0
|
15
|
+
@eliminated = params.dig(:eliminated) || nil
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :id, :content, :score, :elo,
|
19
|
+
:ffa_win_count, :ffa_lose_count, :ffa_draw_count,
|
20
|
+
:eliminated
|
21
|
+
attr_accessor :rank
|
22
|
+
|
23
|
+
def score=(value)
|
24
|
+
@score = value
|
25
|
+
ActiveGenie::Logger.debug({ step: :new_score, player_id: id, score: value })
|
26
|
+
end
|
27
|
+
|
28
|
+
def elo
|
29
|
+
generate_elo_by_score if @elo.nil?
|
30
|
+
|
31
|
+
@elo
|
32
|
+
end
|
33
|
+
|
34
|
+
def elo=(value)
|
35
|
+
@elo = value
|
36
|
+
ActiveGenie::Logger.debug({ step: :new_elo, player_id: id, elo: value })
|
37
|
+
end
|
38
|
+
|
39
|
+
def eliminated=(value)
|
40
|
+
@eliminated = value
|
41
|
+
ActiveGenie::Logger.debug({ step: :new_eliminated, player_id: id, eliminated: value })
|
42
|
+
end
|
43
|
+
|
44
|
+
def draw!
|
45
|
+
@ffa_draw_count += 1
|
46
|
+
ActiveGenie::Logger.debug({ step: :new_ffa_score, player_id: id, result: 'draw', ffa_score: })
|
47
|
+
end
|
48
|
+
|
49
|
+
def win!
|
50
|
+
@ffa_win_count += 1
|
51
|
+
ActiveGenie::Logger.debug({ step: :new_ffa_score, player_id: id, result: 'win', ffa_score: })
|
52
|
+
end
|
53
|
+
|
54
|
+
def lose!
|
55
|
+
@ffa_lose_count += 1
|
56
|
+
ActiveGenie::Logger.debug({ step: :new_ffa_score, player_id: id, result: 'lose', ffa_score: })
|
57
|
+
end
|
58
|
+
|
59
|
+
def ffa_score
|
60
|
+
@ffa_win_count * 3 + @ffa_draw_count
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_h
|
64
|
+
{
|
65
|
+
id:, content:, score:, elo:,
|
66
|
+
ffa_win_count:, ffa_lose_count:, ffa_draw_count:,
|
67
|
+
eliminated:, ffa_score:
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(method_name, *args, &block)
|
72
|
+
if method_name == :[] && args.size == 1
|
73
|
+
attr_name = args.first.to_sym
|
74
|
+
|
75
|
+
if respond_to?(attr_name)
|
76
|
+
return send(attr_name)
|
77
|
+
else
|
78
|
+
return nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
super
|
83
|
+
end
|
84
|
+
|
85
|
+
def respond_to_missing?(method_name, include_private = false)
|
86
|
+
method_name == :[] || super
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
BASE_ELO = 1000
|
92
|
+
|
93
|
+
def generate_elo_by_score
|
94
|
+
@elo = BASE_ELO + ((@score || 0) - 50)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
require_relative '../utils/math'
|
2
1
|
require_relative './player'
|
3
2
|
|
4
|
-
module ActiveGenie::
|
3
|
+
module ActiveGenie::Ranking
|
5
4
|
class PlayersCollection
|
6
5
|
def initialize(param_players)
|
7
6
|
@players = build(param_players)
|
@@ -9,9 +8,11 @@ module ActiveGenie::Leaderboard
|
|
9
8
|
attr_reader :players
|
10
9
|
|
11
10
|
def coefficient_of_variation
|
12
|
-
score_list = eligible.map(&:score)
|
11
|
+
score_list = eligible.map(&:score).compact
|
12
|
+
return nil if score_list.empty?
|
13
|
+
|
13
14
|
mean = score_list.sum.to_f / score_list.size
|
14
|
-
return nil if mean == 0
|
15
|
+
return nil if mean == 0
|
15
16
|
|
16
17
|
variance = score_list.map { |num| (num - mean) ** 2 }.sum / score_list.size
|
17
18
|
standard_deviation = Math.sqrt(variance)
|
@@ -19,11 +20,11 @@ module ActiveGenie::Leaderboard
|
|
19
20
|
(standard_deviation / mean) * 100
|
20
21
|
end
|
21
22
|
|
22
|
-
def
|
23
|
+
def calc_relegation_tier
|
23
24
|
eligible[(tier_size*-1)..-1]
|
24
25
|
end
|
25
26
|
|
26
|
-
def
|
27
|
+
def calc_defender_tier
|
27
28
|
eligible[(tier_size*-2)...(tier_size*-1)]
|
28
29
|
end
|
29
30
|
|
@@ -35,18 +36,24 @@ module ActiveGenie::Leaderboard
|
|
35
36
|
@players.reject(&:eliminated).size
|
36
37
|
end
|
37
38
|
|
39
|
+
def elo_eligible?
|
40
|
+
eligible.size > 15
|
41
|
+
end
|
42
|
+
|
43
|
+
def sorted
|
44
|
+
@players.sort_by { |p| [-p.ffa_score, -(p.elo || 0), -(p.score || 0)] }
|
45
|
+
@players.each_with_index { |p, i| p.rank = i + 1 }
|
46
|
+
@players
|
47
|
+
end
|
48
|
+
|
38
49
|
def to_h
|
39
|
-
sorted.map
|
50
|
+
sorted.map { |p| p.to_h }
|
40
51
|
end
|
41
52
|
|
42
53
|
def method_missing(...)
|
43
54
|
@players.send(...)
|
44
55
|
end
|
45
56
|
|
46
|
-
def sorted
|
47
|
-
@players.sort_by { |p| [-p.league_score, -(p.elo || 0), -p.score] }
|
48
|
-
end
|
49
|
-
|
50
57
|
private
|
51
58
|
|
52
59
|
def build(param_players)
|