active_genie 0.0.8 → 0.0.10
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 +34 -33
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +2 -2
- data/lib/active_genie/battle/basic.rb +24 -19
- data/lib/active_genie/battle.rb +1 -1
- data/lib/active_genie/clients/openai_client.rb +77 -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 -4
- data/lib/active_genie/data_extractor/basic.rb +19 -9
- data/lib/active_genie/data_extractor/from_informal.rb +18 -7
- data/lib/active_genie/data_extractor.rb +1 -1
- data/lib/active_genie/league/README.md +43 -0
- data/lib/active_genie/{leaderboard → league}/elo_ranking.rb +41 -8
- data/lib/active_genie/league/free_for_all.rb +62 -0
- data/lib/active_genie/league/league.rb +120 -0
- data/lib/active_genie/{leaderboard → league}/player.rb +17 -10
- data/lib/active_genie/league.rb +12 -0
- data/lib/active_genie/logger.rb +45 -0
- data/lib/active_genie/scoring/README.md +4 -8
- data/lib/active_genie/scoring/basic.rb +19 -10
- data/lib/active_genie/scoring/recommended_reviews.rb +7 -9
- data/lib/active_genie/scoring.rb +1 -1
- data/lib/active_genie.rb +9 -17
- data/lib/tasks/install.rake +3 -3
- data/lib/tasks/templates/active_genie.rb +17 -0
- metadata +85 -80
- data/lib/active_genie/clients/openai.rb +0 -61
- data/lib/active_genie/clients/router.rb +0 -41
- data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
- data/lib/active_genie/leaderboard/league.rb +0 -48
- 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
- /data/lib/active_genie/{leaderboard → league}/players_collection.rb +0 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# league
|
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.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The league 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 league.
|
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
|
+
- **league**: 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 league using:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
result = ActiveGenie::League::league.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
|
@@ -1,32 +1,36 @@
|
|
1
1
|
require_relative '../battle/basic'
|
2
|
-
require_relative '../utils/math'
|
3
2
|
|
4
3
|
module ActiveGenie::Leaderboard
|
5
4
|
class EloRanking
|
6
|
-
def self.call(players, criteria,
|
7
|
-
new(players, criteria,
|
5
|
+
def self.call(players, criteria, config: {})
|
6
|
+
new(players, criteria, config:).call
|
8
7
|
end
|
9
8
|
|
10
|
-
def initialize(players, criteria,
|
9
|
+
def initialize(players, criteria, config: {})
|
11
10
|
@players = players
|
12
11
|
@criteria = criteria
|
13
|
-
@
|
12
|
+
@config = config
|
13
|
+
@start_time = Time.now
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
17
|
@players.each(&:generate_elo_by_score)
|
18
18
|
|
19
|
+
round_count = 0
|
19
20
|
while @players.eligible_size > MINIMAL_PLAYERS_TO_BATTLE
|
20
21
|
round = create_round(@players.tier_relegation, @players.tier_defense)
|
21
22
|
|
22
23
|
round.each do |player_a, player_b|
|
23
24
|
winner, loser = battle(player_a, player_b) # This can take a while, can be parallelized
|
24
25
|
update_elo(winner, loser)
|
26
|
+
ActiveGenie::Logger.trace({ **log, step: :elo_battle, winner_id: winner.id, loser_id: loser.id, winner_elo: winner.elo, loser_elo: loser.elo })
|
25
27
|
end
|
26
28
|
|
27
|
-
|
29
|
+
eliminate_all_relegation_players
|
30
|
+
round_count += 1
|
28
31
|
end
|
29
32
|
|
33
|
+
ActiveGenie::Logger.info({ **log, step: :elo_end, round_count:, eligible_size: @players.eligible_size })
|
30
34
|
@players
|
31
35
|
end
|
32
36
|
|
@@ -35,6 +39,7 @@ module ActiveGenie::Leaderboard
|
|
35
39
|
MATCHS_PER_PLAYER = 3
|
36
40
|
LOSE_PENALTY = 15
|
37
41
|
MINIMAL_PLAYERS_TO_BATTLE = 10
|
42
|
+
K = 32
|
38
43
|
|
39
44
|
# Create a round of matches
|
40
45
|
# each round is exactly 1 regation player vs 3 defense players for all regation players
|
@@ -64,14 +69,14 @@ module ActiveGenie::Leaderboard
|
|
64
69
|
player_a,
|
65
70
|
player_b,
|
66
71
|
@criteria,
|
67
|
-
|
72
|
+
config:
|
68
73
|
).values_at('winner', 'loser')
|
69
74
|
end
|
70
75
|
|
71
76
|
def update_elo(winner, loser)
|
72
77
|
return if winner.nil? || loser.nil?
|
73
78
|
|
74
|
-
new_winner_elo, new_loser_elo =
|
79
|
+
new_winner_elo, new_loser_elo = calculate_new_elo(winner.elo, loser.elo)
|
75
80
|
|
76
81
|
winner.elo = [new_winner_elo, max_defense_elo].min
|
77
82
|
loser.elo = [new_loser_elo - LOSE_PENALTY, min_relegation_elo].max
|
@@ -84,5 +89,33 @@ module ActiveGenie::Leaderboard
|
|
84
89
|
def min_relegation_elo
|
85
90
|
@players.tier_relegation.min_by(&:elo).elo
|
86
91
|
end
|
92
|
+
|
93
|
+
# Read more about the formula on https://en.wikipedia.org/wiki/Elo_rating_system
|
94
|
+
def calculate_new_elo(winner_elo, loser_elo)
|
95
|
+
expected_score_a = 1 / (1 + 10**((loser_elo - winner_elo) / 400))
|
96
|
+
expected_score_b = 1 - expected_score_a
|
97
|
+
|
98
|
+
new_elo_winner = winner_elo + K * (1 - expected_score_a)
|
99
|
+
new_elo_loser = loser_elo + K * (1 - expected_score_b)
|
100
|
+
|
101
|
+
[new_elo_winner, new_elo_loser]
|
102
|
+
end
|
103
|
+
|
104
|
+
def eliminate_all_relegation_players
|
105
|
+
eliminations = @players.tier_relegation.size
|
106
|
+
@players.tier_relegation.each { |player| player.eliminated = 'tier_relegation' }
|
107
|
+
ActiveGenie::Logger.trace({ **log, step: :elo_round, eligible_size: @players.eligible_size, eliminations: })
|
108
|
+
end
|
109
|
+
|
110
|
+
def config
|
111
|
+
{ **@config }
|
112
|
+
end
|
113
|
+
|
114
|
+
def log
|
115
|
+
{
|
116
|
+
**(@config.dig(:log) || {}),
|
117
|
+
duration: Time.now - @start_time
|
118
|
+
}
|
119
|
+
end
|
87
120
|
end
|
88
121
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative '../battle/basic'
|
2
|
+
|
3
|
+
module ActiveGenie::Leaderboard
|
4
|
+
class FreeForAll
|
5
|
+
def self.call(players, criteria, config: {})
|
6
|
+
new(players, criteria, config:).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
|
+
end
|
15
|
+
|
16
|
+
def call
|
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.free_for_all[:draw] += 1
|
22
|
+
player_b.free_for_all[:draw] += 1
|
23
|
+
else
|
24
|
+
winner.free_for_all[:win] += 1
|
25
|
+
loser.free_for_all[:lose] += 1
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveGenie::Logger.trace({**log, step: :free_for_all_battle, winner_id: winner&.id, player_a_id: player_a.id, player_a_free_for_all_score: player_a.free_for_all_score, player_b_id: player_b.id, player_b_free_for_all_score: player_b.free_for_all_score })
|
29
|
+
end
|
30
|
+
|
31
|
+
@players
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# TODO: reduce the number of matches based on transitivity.
|
37
|
+
# For example, if A is better than B, and B is better than C, then A should clearly be better than C
|
38
|
+
def matches
|
39
|
+
@players.eligible.combination(2).to_a
|
40
|
+
end
|
41
|
+
|
42
|
+
def battle(player_a, player_b)
|
43
|
+
result = ActiveGenie::Battle.basic(
|
44
|
+
player_a,
|
45
|
+
player_b,
|
46
|
+
@criteria,
|
47
|
+
config:
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
result.values_at('winner', 'loser')
|
52
|
+
end
|
53
|
+
|
54
|
+
def config
|
55
|
+
{ **@config }
|
56
|
+
end
|
57
|
+
|
58
|
+
def log
|
59
|
+
{ **(@config.dig(:log) || {}), duration: Time.now - @start_time }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
require_relative './players_collection'
|
4
|
+
require_relative './free_for_all'
|
5
|
+
require_relative './elo_ranking'
|
6
|
+
require_relative '../scoring/recommended_reviews'
|
7
|
+
|
8
|
+
# This class orchestrates player ranking through multiple evaluation stages
|
9
|
+
# using Elo ranking and free-for-all match simulations.
|
10
|
+
# 1. Sets initial scores
|
11
|
+
# 2. Eliminates low performers
|
12
|
+
# 3. Runs Elo ranking (for large groups)
|
13
|
+
# 4. Conducts free-for-all matches
|
14
|
+
#
|
15
|
+
# @example Basic usage
|
16
|
+
# League.call(players, criteria)
|
17
|
+
#
|
18
|
+
# @param param_players [Array] Collection of player objects to evaluate
|
19
|
+
# Example: ["Circle", "Triangle", "Square"]
|
20
|
+
# or
|
21
|
+
# [
|
22
|
+
# { content: "Circle", score: 10 },
|
23
|
+
# { content: "Triangle", score: 7 },
|
24
|
+
# { content: "Square", score: 5 }
|
25
|
+
# ]
|
26
|
+
# @param criteria [String] Evaluation criteria configuration
|
27
|
+
# Example: "What is more similar to the letter 'O'?"
|
28
|
+
# @param config [Hash] Additional configuration config
|
29
|
+
# Example: { model: "gpt-4o", api_key: ENV['OPENAI_API_KEY'] }
|
30
|
+
# @return [Hash] Final ranked player results
|
31
|
+
module ActiveGenie::League
|
32
|
+
class League
|
33
|
+
def self.call(param_players, criteria, config: {})
|
34
|
+
new(param_players, criteria, config:).call
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(param_players, criteria, config: {})
|
38
|
+
@param_players = param_players
|
39
|
+
@criteria = criteria
|
40
|
+
@config = config
|
41
|
+
@league_id = SecureRandom.uuid
|
42
|
+
@start_time = Time.now
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
set_initial_score_players
|
47
|
+
eliminate_obvious_bad_players
|
48
|
+
run_elo_ranking if players.eligible_size > 10
|
49
|
+
run_free_for_all
|
50
|
+
|
51
|
+
ActiveGenie::Logger.info({ **log, step: :league_end, top5: players.first(5).map(&:id) })
|
52
|
+
players.to_h
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
SCORE_VARIATION_THRESHOLD = 10
|
58
|
+
|
59
|
+
def set_initial_score_players
|
60
|
+
players_without_score = players.reject { |player| player.score }
|
61
|
+
players_without_score.each do |player|
|
62
|
+
player.score = generate_score(player.content) # This can take a while, can be parallelized
|
63
|
+
ActiveGenie::Logger.trace({ **log, step: :player_score, player_id: player.id, score: player.score })
|
64
|
+
end
|
65
|
+
|
66
|
+
ActiveGenie::Logger.info({ **log, step: :initial_score, evaluated_players: players_without_score.size })
|
67
|
+
end
|
68
|
+
|
69
|
+
def generate_score(content)
|
70
|
+
ActiveGenie::Scoring::Basic.call(content, @criteria, reviewers, config:)['final_score']
|
71
|
+
end
|
72
|
+
|
73
|
+
def eliminate_obvious_bad_players
|
74
|
+
eliminated_count = 0
|
75
|
+
while players.coefficient_of_variation >= SCORE_VARIATION_THRESHOLD
|
76
|
+
players.eligible.last.eliminated = 'variation_too_high'
|
77
|
+
eliminated_count += 1
|
78
|
+
end
|
79
|
+
|
80
|
+
ActiveGenie::Logger.info({ **log, step: :eliminate_obvious_bad_players, eliminated_count: })
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_elo_ranking
|
84
|
+
EloRanking.call(players, @criteria, config:)
|
85
|
+
end
|
86
|
+
|
87
|
+
def run_free_for_all
|
88
|
+
FreeForAll.call(players, @criteria, config:)
|
89
|
+
end
|
90
|
+
|
91
|
+
def reviewers
|
92
|
+
[recommended_reviews['reviewer1'], recommended_reviews['reviewer2'], recommended_reviews['reviewer3']]
|
93
|
+
end
|
94
|
+
|
95
|
+
def recommended_reviews
|
96
|
+
@recommended_reviews ||= ActiveGenie::Scoring::RecommendedReviews.call(
|
97
|
+
[players.sample.content, players.sample.content].join("\n\n"),
|
98
|
+
@criteria,
|
99
|
+
config:
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def players
|
104
|
+
@players ||= PlayersCollection.new(@param_players)
|
105
|
+
end
|
106
|
+
|
107
|
+
def config
|
108
|
+
{ log:, **@config }
|
109
|
+
end
|
110
|
+
|
111
|
+
def log
|
112
|
+
{
|
113
|
+
**(@config.dig(:log) || {}),
|
114
|
+
league_id: @league_id,
|
115
|
+
league_start_time: @start_time,
|
116
|
+
duration: Time.now - @start_time
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -9,20 +9,24 @@ module ActiveGenie::Leaderboard
|
|
9
9
|
@content = params.dig(:content) || params
|
10
10
|
@score = params.dig(:score) || nil
|
11
11
|
@elo = params.dig(:elo) || nil
|
12
|
-
@
|
13
|
-
win: params.dig(:
|
14
|
-
lose: params.dig(:
|
15
|
-
draw: params.dig(:
|
12
|
+
@free_for_all = {
|
13
|
+
win: params.dig(:free_for_all, :win) || 0,
|
14
|
+
lose: params.dig(:free_for_all, :lose) || 0,
|
15
|
+
draw: params.dig(:free_for_all, :draw) || 0
|
16
16
|
}
|
17
17
|
@eliminated = params.dig(:eliminated) || nil
|
18
18
|
end
|
19
19
|
|
20
|
-
attr_reader :id, :content, :score, :elo, :
|
20
|
+
attr_reader :id, :content, :score, :elo, :free_for_all, :eliminated
|
21
21
|
|
22
22
|
def generate_elo_by_score
|
23
|
-
return if !@elo.nil?
|
23
|
+
return if !@elo.nil?
|
24
24
|
|
25
|
-
|
25
|
+
if @score.nil?
|
26
|
+
@elo = BASE_ELO
|
27
|
+
else
|
28
|
+
@elo = BASE_ELO + (@score - 50)
|
29
|
+
end
|
26
30
|
end
|
27
31
|
|
28
32
|
def score=(value)
|
@@ -37,12 +41,15 @@ module ActiveGenie::Leaderboard
|
|
37
41
|
@eliminated = value
|
38
42
|
end
|
39
43
|
|
40
|
-
def
|
41
|
-
@
|
44
|
+
def free_for_all_score
|
45
|
+
@free_for_all[:win] * 3 + @free_for_all[:draw]
|
42
46
|
end
|
43
47
|
|
44
48
|
def to_h
|
45
|
-
{
|
49
|
+
{
|
50
|
+
id:, content:, score:, elo:,
|
51
|
+
eliminated:, free_for_all:, free_for_all_score:
|
52
|
+
}
|
46
53
|
end
|
47
54
|
|
48
55
|
private
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module ActiveGenie
|
5
|
+
module Logger
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def info(log)
|
9
|
+
save(log, level: :info)
|
10
|
+
end
|
11
|
+
|
12
|
+
def error(log)
|
13
|
+
save(log, level: :error)
|
14
|
+
end
|
15
|
+
|
16
|
+
def warn(log)
|
17
|
+
save(log, level: :warn)
|
18
|
+
end
|
19
|
+
|
20
|
+
def debug(log)
|
21
|
+
save(log, level: :debug)
|
22
|
+
end
|
23
|
+
|
24
|
+
def trace(log)
|
25
|
+
save(log, level: :trace)
|
26
|
+
end
|
27
|
+
|
28
|
+
LOG_LEVELS = { info: 0, error: 1, warn: 2, debug: 3, trace: 4 }.freeze
|
29
|
+
|
30
|
+
def save(log, level: :info)
|
31
|
+
return if LOG_LEVELS[log.dig(:log, :log_level)] || -1 < LOG_LEVELS[level]
|
32
|
+
|
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
|
37
|
+
|
38
|
+
FileUtils.mkdir_p('logs')
|
39
|
+
File.write('logs/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
|
40
|
+
puts log
|
41
|
+
|
42
|
+
log
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -49,26 +49,22 @@ result = ActiveGenie::Scoring::Basic.call(text, criteria)
|
|
49
49
|
|
50
50
|
## Interface
|
51
51
|
|
52
|
-
### `Basic.call(text, criteria, reviewers = [],
|
52
|
+
### `Basic.call(text, criteria, reviewers = [], config: {})`
|
53
53
|
Main interface for scoring text content.
|
54
54
|
|
55
55
|
#### Parameters
|
56
56
|
- `text` [String] - The text content to be evaluated
|
57
57
|
- `criteria` [String] - The evaluation criteria or rubric to assess against
|
58
58
|
- `reviewers` [Array<String>] - Optional list of specific reviewers
|
59
|
-
- `
|
60
|
-
- `:detailed_feedback` [Boolean] - Request more detailed feedback (WIP)
|
61
|
-
- `:reviewer_weights` [Hash] - Custom weights for different reviewers (WIP)
|
59
|
+
- `config` [Hash] - Additional configuration config
|
62
60
|
|
63
|
-
### `RecommendedReviews.call(text, criteria,
|
61
|
+
### `RecommendedReviews.call(text, criteria, config: {})`
|
64
62
|
Recommends appropriate reviewers based on content and criteria.
|
65
63
|
|
66
64
|
#### Parameters
|
67
65
|
- `text` [String] - The text content to analyze
|
68
66
|
- `criteria` [String] - The evaluation criteria
|
69
|
-
- `
|
70
|
-
- `:prefer_technical` [Boolean] - Favor technical expertise (WIP)
|
71
|
-
- `:prefer_domain` [Boolean] - Favor domain expertise (WIP)
|
67
|
+
- `config` [Hash] - Additional configuration config
|
72
68
|
|
73
69
|
### Usage Notes
|
74
70
|
- Best suited for objective evaluation of text content
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '../clients/
|
3
|
+
require_relative '../clients/unified_client'
|
4
4
|
|
5
5
|
module ActiveGenie::Scoring
|
6
6
|
# The Basic class provides a foundation for scoring text content against specified criteria
|
@@ -21,21 +21,19 @@ module ActiveGenie::Scoring
|
|
21
21
|
# @param criteria [String] The evaluation criteria or rubric to assess against
|
22
22
|
# @param reviewers [Array<String>] Optional list of specific reviewers. If empty,
|
23
23
|
# reviewers will be automatically recommended based on the content and criteria
|
24
|
-
# @param
|
25
|
-
# @option options [Boolean] :detailed_feedback Request more detailed feedback in the reasoning
|
26
|
-
# @option options [Hash] :reviewer_weights Custom weights for different reviewers
|
24
|
+
# @param config [Hash] Additional configuration config that modify the scoring behavior
|
27
25
|
# @return [Hash] The evaluation result containing the scores and reasoning
|
28
26
|
# @return [Number] :final_score The final score of the text based on the criteria and reviewers
|
29
27
|
# @return [String] :final_reasoning Detailed explanation of why the final score was reached
|
30
|
-
def self.call(text, criteria, reviewers = [],
|
31
|
-
new(text, criteria, reviewers,
|
28
|
+
def self.call(text, criteria, reviewers = [], config: {})
|
29
|
+
new(text, criteria, reviewers, config:).call
|
32
30
|
end
|
33
31
|
|
34
|
-
def initialize(text, criteria, reviewers = [],
|
32
|
+
def initialize(text, criteria, reviewers = [], config: {})
|
35
33
|
@text = text
|
36
34
|
@criteria = criteria
|
37
35
|
@reviewers = Array(reviewers).compact.uniq
|
38
|
-
@
|
36
|
+
@config = config
|
39
37
|
end
|
40
38
|
|
41
39
|
def call
|
@@ -78,7 +76,7 @@ module ActiveGenie::Scoring
|
|
78
76
|
}
|
79
77
|
}
|
80
78
|
|
81
|
-
::ActiveGenie::Clients::
|
79
|
+
::ActiveGenie::Clients::UnifiedClient.function_calling(messages, function, config:)
|
82
80
|
end
|
83
81
|
|
84
82
|
private
|
@@ -87,7 +85,7 @@ module ActiveGenie::Scoring
|
|
87
85
|
@get_or_recommend_reviewers ||= if @reviewers.count > 0
|
88
86
|
@reviewers
|
89
87
|
else
|
90
|
-
recommended_reviews = RecommendedReviews.call(@text, @criteria,
|
88
|
+
recommended_reviews = RecommendedReviews.call(@text, @criteria, config:)
|
91
89
|
|
92
90
|
[recommended_reviews[:reviewer1], recommended_reviews[:reviewer2], recommended_reviews[:reviewer3]]
|
93
91
|
end
|
@@ -113,5 +111,16 @@ module ActiveGenie::Scoring
|
|
113
111
|
- Deconstruct each criterion into actionable components for a systematic evaluation.
|
114
112
|
- If the text lacks information, apply reasonable judgment to assign a score while clearly explaining the rationale.
|
115
113
|
PROMPT
|
114
|
+
|
115
|
+
def config
|
116
|
+
{
|
117
|
+
all_providers: { model_tier: 'lower_tier' },
|
118
|
+
log: {
|
119
|
+
**(@config.dig(:log) || {}),
|
120
|
+
trace: self.class.name,
|
121
|
+
},
|
122
|
+
**@config
|
123
|
+
}
|
124
|
+
end
|
116
125
|
end
|
117
126
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '../clients/
|
3
|
+
require_relative '../clients/unified_client'
|
4
4
|
|
5
5
|
module ActiveGenie::Scoring
|
6
6
|
# The RecommendedReviews class intelligently suggests appropriate reviewer roles
|
@@ -17,21 +17,19 @@ module ActiveGenie::Scoring
|
|
17
17
|
# # reviewer3: "Developer Advocate", reasoning: "..." }
|
18
18
|
#
|
19
19
|
class RecommendedReviews
|
20
|
-
def self.call(text, criteria,
|
21
|
-
new(text, criteria,
|
20
|
+
def self.call(text, criteria, config: {})
|
21
|
+
new(text, criteria, config:).call
|
22
22
|
end
|
23
23
|
|
24
24
|
# Initializes a new reviewer recommendation instance
|
25
25
|
#
|
26
26
|
# @param text [String] The text content to analyze for reviewer recommendations
|
27
27
|
# @param criteria [String] The evaluation criteria that will guide reviewer selection
|
28
|
-
# @param
|
29
|
-
|
30
|
-
# @option options [Boolean] :prefer_domain Whether to favor domain expertise
|
31
|
-
def initialize(text, criteria, options: {})
|
28
|
+
# @param config [Hash] Additional configuration config that modify the recommendation process
|
29
|
+
def initialize(text, criteria, config: {})
|
32
30
|
@text = text
|
33
31
|
@criteria = criteria
|
34
|
-
@
|
32
|
+
@config = config
|
35
33
|
end
|
36
34
|
|
37
35
|
def call
|
@@ -55,7 +53,7 @@ module ActiveGenie::Scoring
|
|
55
53
|
}
|
56
54
|
}
|
57
55
|
|
58
|
-
::ActiveGenie::Clients::
|
56
|
+
::ActiveGenie::Clients::UnifiedClient.function_calling(messages, function, config: @config)
|
59
57
|
end
|
60
58
|
|
61
59
|
private
|
data/lib/active_genie/scoring.rb
CHANGED
@@ -2,7 +2,7 @@ require_relative 'scoring/basic'
|
|
2
2
|
require_relative 'scoring/recommended_reviews'
|
3
3
|
|
4
4
|
module ActiveGenie
|
5
|
-
#
|
5
|
+
# See the [Scoring README](lib/active_genie/scoring/README.md) for more information.
|
6
6
|
module Scoring
|
7
7
|
module_function
|
8
8
|
|
data/lib/active_genie.rb
CHANGED
@@ -1,15 +1,19 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require_relative 'active_genie/logger'
|
2
|
+
require_relative 'active_genie/configuration'
|
3
3
|
|
4
|
-
|
4
|
+
module ActiveGenie
|
5
5
|
autoload :DataExtractor, File.join(__dir__, 'active_genie/data_extractor')
|
6
6
|
autoload :Battle, File.join(__dir__, 'active_genie/battle')
|
7
7
|
autoload :Scoring, File.join(__dir__, 'active_genie/scoring')
|
8
8
|
autoload :Leaderboard, File.join(__dir__, 'active_genie/leaderboard')
|
9
9
|
|
10
|
-
class << self
|
10
|
+
class << self
|
11
11
|
def configure
|
12
|
-
yield(
|
12
|
+
yield(configuration) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def configuration
|
16
|
+
@configuration ||= Configuration
|
13
17
|
end
|
14
18
|
|
15
19
|
def load_tasks
|
@@ -18,17 +22,5 @@ module ActiveGenie
|
|
18
22
|
Rake::Task.define_task(:environment)
|
19
23
|
Dir.glob(File.join(__dir__, 'tasks', '*.rake')).each { |r| load r }
|
20
24
|
end
|
21
|
-
|
22
|
-
def config
|
23
|
-
@config ||= Configuration.new
|
24
|
-
end
|
25
|
-
|
26
|
-
def [](key)
|
27
|
-
config.values[key.to_s]
|
28
|
-
end
|
29
|
-
|
30
|
-
def config_by_model(model = nil)
|
31
|
-
config.values[model&.to_s&.downcase&.strip] || config.values.values.first || {}
|
32
|
-
end
|
33
25
|
end
|
34
26
|
end
|
data/lib/tasks/install.rake
CHANGED
@@ -3,10 +3,10 @@ require 'fileutils'
|
|
3
3
|
namespace :active_genie do
|
4
4
|
desc 'Install active_genie configuration file'
|
5
5
|
task :install do
|
6
|
-
source = File.join(__dir__, 'templates', 'active_genie.
|
7
|
-
target = File.join('config', 'active_genie.
|
6
|
+
source = File.join(__dir__, 'templates', 'active_genie.rb')
|
7
|
+
target = File.join('config', 'initializers', 'active_genie.rb')
|
8
8
|
|
9
9
|
FileUtils.cp(source, target)
|
10
|
-
puts "Successfully installed
|
10
|
+
puts "Successfully installed active_genie!"
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
ActiveGenie.configure do |config|
|
3
|
+
# example with openai and the current default for each config
|
4
|
+
# config.providers.openai.api_key = ENV['OPENAI_API_KEY']
|
5
|
+
# config.providers.openai.organization = ENV['OPENAI_ORGANIZATION']
|
6
|
+
# config.providers.openai.api_url = 'https://api.openai.com/v1'
|
7
|
+
# config.providers.openai.lower_tier_model = 'gpt-4o-mini'
|
8
|
+
# config.providers.openai.middle_tier_model = 'gpt-4o'
|
9
|
+
# config.providers.openai.upper_tier_model = 'o1-preview'
|
10
|
+
# config.providers.openai.client = ActiveGenie::Providers::Openai::Client.new(config)
|
11
|
+
|
12
|
+
# example how add a new provider
|
13
|
+
# config.providers.register(:internal_company_api, InternalCompanyApi::Configuration)
|
14
|
+
|
15
|
+
# Logs configuration
|
16
|
+
# config.log_level = :debug # default is :info
|
17
|
+
end
|