active_genie 0.0.10 → 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 +17 -54
- data/VERSION +1 -1
- data/lib/active_genie/battle/basic.rb +49 -58
- data/lib/active_genie/clients/openai_client.rb +60 -18
- data/lib/active_genie/clients/unified_client.rb +2 -2
- data/lib/active_genie/configuration/openai_config.rb +9 -9
- data/lib/active_genie/data_extractor/README.md +0 -1
- data/lib/active_genie/data_extractor/basic.rb +8 -18
- data/lib/active_genie/data_extractor/from_informal.rb +4 -15
- data/lib/active_genie/logger.rb +34 -7
- data/lib/active_genie/{league → ranking}/README.md +7 -7
- 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/{league → 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 +1 -1
- data/lib/active_genie/scoring/basic.rb +55 -30
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +18 -7
- data/lib/active_genie/scoring.rb +3 -3
- data/lib/active_genie.rb +1 -1
- metadata +66 -87
- 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
@@ -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,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)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative './players_collection'
|
2
|
+
require_relative './free_for_all'
|
3
|
+
require_relative './elo_round'
|
4
|
+
require_relative './ranking_scoring'
|
5
|
+
|
6
|
+
# This class orchestrates player ranking through multiple evaluation stages
|
7
|
+
# using Elo ranking and free-for-all match simulations.
|
8
|
+
# 1. Sets initial scores
|
9
|
+
# 2. Eliminates low performers
|
10
|
+
# 3. Runs Elo ranking (for large groups)
|
11
|
+
# 4. Conducts free-for-all matches
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# Ranking.call(players, criteria)
|
15
|
+
#
|
16
|
+
# @param param_players [Array<Hash|String>] Collection of player objects to evaluate
|
17
|
+
# Example: ["Circle", "Triangle", "Square"]
|
18
|
+
# or
|
19
|
+
# [
|
20
|
+
# { content: "Circle", score: 10 },
|
21
|
+
# { content: "Triangle", score: 7 },
|
22
|
+
# { content: "Square", score: 5 }
|
23
|
+
# ]
|
24
|
+
# @param criteria [String] Evaluation criteria configuration
|
25
|
+
# Example: "What is more similar to the letter 'O'?"
|
26
|
+
# @param config [Hash] Additional configuration config
|
27
|
+
# Example: { model: "gpt-4o", api_key: ENV['OPENAI_API_KEY'] }
|
28
|
+
# @return [Hash] Final ranked player results
|
29
|
+
module ActiveGenie::Ranking
|
30
|
+
class Ranking
|
31
|
+
def self.call(...)
|
32
|
+
new(...).call
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(param_players, criteria, reviewers: [], config: {})
|
36
|
+
@param_players = param_players
|
37
|
+
@criteria = criteria
|
38
|
+
@reviewers = Array(reviewers).compact.uniq
|
39
|
+
@config = ActiveGenie::Configuration.to_h(config)
|
40
|
+
@players = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def call
|
44
|
+
@players = PlayersCollection.new(@param_players)
|
45
|
+
|
46
|
+
ActiveGenie::Logger.with_context(log_context) do
|
47
|
+
set_initial_player_scores!
|
48
|
+
eliminate_obvious_bad_players!
|
49
|
+
|
50
|
+
while @players.elo_eligible?
|
51
|
+
run_elo_round!
|
52
|
+
eliminate_relegation_players!
|
53
|
+
end
|
54
|
+
|
55
|
+
run_free_for_all!
|
56
|
+
end
|
57
|
+
|
58
|
+
@players.sorted
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
SCORE_VARIATION_THRESHOLD = 10
|
64
|
+
|
65
|
+
def set_initial_player_scores!
|
66
|
+
RankingScoring.call(@players, @criteria, reviewers: @reviewers, config: @config)
|
67
|
+
end
|
68
|
+
|
69
|
+
def eliminate_obvious_bad_players!
|
70
|
+
while @players.coefficient_of_variation >= SCORE_VARIATION_THRESHOLD
|
71
|
+
@players.eligible.last.eliminated = 'variation_too_high'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_elo_round!
|
76
|
+
EloRound.call(@players, @criteria, config: @config)
|
77
|
+
end
|
78
|
+
|
79
|
+
def eliminate_relegation_players!
|
80
|
+
@players.calc_relegation_tier.each { |player| player.eliminated = 'relegation_tier' }
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_free_for_all!
|
84
|
+
FreeForAll.call(@players, @criteria, config: @config)
|
85
|
+
end
|
86
|
+
|
87
|
+
def log_context
|
88
|
+
{ config: @config[:log], ranking_id: }
|
89
|
+
end
|
90
|
+
|
91
|
+
def ranking_id
|
92
|
+
player_ids = @players.map(&:id).join(',')
|
93
|
+
ranking_unique_key = [player_ids, @criteria, @config.to_json].join('-')
|
94
|
+
|
95
|
+
Digest::MD5.hexdigest(ranking_unique_key)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative '../scoring/recommended_reviewers'
|
2
|
+
|
3
|
+
module ActiveGenie::Ranking
|
4
|
+
class RankingScoring
|
5
|
+
def self.call(...)
|
6
|
+
new(...).call
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(players, criteria, reviewers: [], config: {})
|
10
|
+
@players = players
|
11
|
+
@criteria = criteria
|
12
|
+
@config = ActiveGenie::Configuration.to_h(config)
|
13
|
+
@reviewers = Array(reviewers).compact.uniq
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
ActiveGenie::Logger.with_context(log_context) do
|
18
|
+
@reviewers = generate_reviewers
|
19
|
+
|
20
|
+
players_without_score.each do |player|
|
21
|
+
# TODO: This can take a while, can be parallelized
|
22
|
+
player.score = generate_score(player)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def players_without_score
|
30
|
+
@players_without_score ||= @players.select { |player| player.score.nil? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_score(player)
|
34
|
+
score, reasoning = ActiveGenie::Scoring::Basic.call(
|
35
|
+
player.content,
|
36
|
+
@criteria,
|
37
|
+
@reviewers,
|
38
|
+
config: @config
|
39
|
+
).values_at('final_score', 'final_reasoning')
|
40
|
+
|
41
|
+
ActiveGenie::Logger.debug({step: :new_score, player_id: player.id, score:, reasoning: })
|
42
|
+
|
43
|
+
score
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_reviewers
|
47
|
+
return @reviewers if @reviewers.size > 0
|
48
|
+
|
49
|
+
reviewer1, reviewer2, reviewer3 = ActiveGenie::Scoring::RecommendedReviewers.call(
|
50
|
+
[@players.sample.content, @players.sample.content].join("\n\n"),
|
51
|
+
@criteria,
|
52
|
+
config: @config
|
53
|
+
).values_at('reviewer1', 'reviewer2', 'reviewer3')
|
54
|
+
|
55
|
+
ActiveGenie::Logger.debug({step: :new_reviewers, reviewers: [reviewer1, reviewer2, reviewer3] })
|
56
|
+
|
57
|
+
[reviewer1, reviewer2, reviewer3]
|
58
|
+
end
|
59
|
+
|
60
|
+
def log_context
|
61
|
+
{ ranking_scoring_id: }
|
62
|
+
end
|
63
|
+
|
64
|
+
def ranking_scoring_id
|
65
|
+
player_ids = players_without_score.map(&:id).join(',')
|
66
|
+
ranking_unique_key = [player_ids, @criteria, @config.to_json].join('-')
|
67
|
+
|
68
|
+
Digest::MD5.hexdigest(ranking_unique_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -58,7 +58,7 @@ Main interface for scoring text content.
|
|
58
58
|
- `reviewers` [Array<String>] - Optional list of specific reviewers
|
59
59
|
- `config` [Hash] - Additional configuration config
|
60
60
|
|
61
|
-
### `
|
61
|
+
### `RecommendedReviewers.call(text, criteria, config: {})`
|
62
62
|
Recommends appropriate reviewers based on content and criteria.
|
63
63
|
|
64
64
|
#### Parameters
|