active_genie 0.0.24 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -50
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +5 -5
  5. data/lib/active_genie/battle/generalist.rb +132 -0
  6. data/lib/active_genie/battle.rb +6 -5
  7. data/lib/active_genie/clients/providers/anthropic_client.rb +77 -0
  8. data/lib/active_genie/clients/{base_client.rb → providers/base_client.rb} +74 -100
  9. data/lib/active_genie/clients/providers/deepseek_client.rb +91 -0
  10. data/lib/active_genie/clients/providers/google_client.rb +132 -0
  11. data/lib/active_genie/clients/providers/openai_client.rb +96 -0
  12. data/lib/active_genie/clients/unified_client.rb +42 -12
  13. data/lib/active_genie/concerns/loggable.rb +11 -23
  14. data/lib/active_genie/config/battle_config.rb +8 -0
  15. data/lib/active_genie/config/data_extractor_config.rb +23 -0
  16. data/lib/active_genie/config/llm_config.rb +36 -0
  17. data/lib/active_genie/config/log_config.rb +44 -0
  18. data/lib/active_genie/config/providers/anthropic_config.rb +57 -0
  19. data/lib/active_genie/config/providers/deepseek_config.rb +50 -0
  20. data/lib/active_genie/config/providers/google_config.rb +52 -0
  21. data/lib/active_genie/config/providers/openai_config.rb +50 -0
  22. data/lib/active_genie/config/providers/provider_base.rb +89 -0
  23. data/lib/active_genie/config/providers_config.rb +62 -0
  24. data/lib/active_genie/config/ranking_config.rb +21 -0
  25. data/lib/active_genie/config/scoring_config.rb +8 -0
  26. data/lib/active_genie/configuration.rb +51 -28
  27. data/lib/active_genie/data_extractor/README.md +13 -13
  28. data/lib/active_genie/data_extractor/from_informal.rb +54 -48
  29. data/lib/active_genie/data_extractor/generalist.md +12 -0
  30. data/lib/active_genie/data_extractor/generalist.rb +125 -0
  31. data/lib/active_genie/data_extractor.rb +7 -5
  32. data/lib/active_genie/errors/invalid_provider_error.rb +41 -0
  33. data/lib/active_genie/logger.rb +17 -66
  34. data/lib/active_genie/ranking/README.md +31 -1
  35. data/lib/active_genie/ranking/elo_round.rb +107 -104
  36. data/lib/active_genie/ranking/free_for_all.rb +78 -74
  37. data/lib/active_genie/ranking/player.rb +79 -71
  38. data/lib/active_genie/ranking/players_collection.rb +83 -71
  39. data/lib/active_genie/ranking/ranking.rb +71 -94
  40. data/lib/active_genie/ranking/ranking_scoring.rb +71 -50
  41. data/lib/active_genie/ranking.rb +2 -0
  42. data/lib/active_genie/scoring/README.md +4 -4
  43. data/lib/active_genie/scoring/generalist.rb +171 -0
  44. data/lib/active_genie/scoring/recommended_reviewers.rb +70 -71
  45. data/lib/active_genie/scoring.rb +8 -5
  46. data/lib/active_genie.rb +23 -1
  47. data/lib/tasks/benchmark.rake +10 -9
  48. data/lib/tasks/install.rake +3 -1
  49. data/lib/tasks/templates/active_genie.rb +11 -6
  50. metadata +31 -22
  51. data/lib/active_genie/battle/basic.rb +0 -129
  52. data/lib/active_genie/clients/anthropic_client.rb +0 -84
  53. data/lib/active_genie/clients/google_client.rb +0 -135
  54. data/lib/active_genie/clients/helpers/retry.rb +0 -29
  55. data/lib/active_genie/clients/openai_client.rb +0 -98
  56. data/lib/active_genie/configuration/log_config.rb +0 -14
  57. data/lib/active_genie/configuration/providers/anthropic_config.rb +0 -54
  58. data/lib/active_genie/configuration/providers/base_config.rb +0 -85
  59. data/lib/active_genie/configuration/providers/deepseek_config.rb +0 -54
  60. data/lib/active_genie/configuration/providers/google_config.rb +0 -56
  61. data/lib/active_genie/configuration/providers/internal_company_api_config.rb +0 -54
  62. data/lib/active_genie/configuration/providers/openai_config.rb +0 -54
  63. data/lib/active_genie/configuration/providers_config.rb +0 -40
  64. data/lib/active_genie/configuration/runtime_config.rb +0 -35
  65. data/lib/active_genie/data_extractor/basic.rb +0 -101
  66. data/lib/active_genie/scoring/basic.rb +0 -170
@@ -1,75 +1,87 @@
1
- require_relative './player'
2
-
3
- module ActiveGenie::Ranking
4
- class PlayersCollection
5
- def initialize(param_players)
6
- @players = build(param_players)
7
- end
8
- attr_reader :players
9
-
10
- def coefficient_of_variation
11
- score_list = eligible.map(&:score).compact
12
- return nil if score_list.empty?
13
-
14
- mean = score_list.sum.to_f / score_list.size
15
- return nil if mean == 0
16
-
17
- variance = score_list.map { |num| (num - mean) ** 2 }.sum / score_list.size
18
- standard_deviation = Math.sqrt(variance)
19
-
20
- (standard_deviation / mean) * 100
21
- end
22
-
23
- def calc_relegation_tier
24
- eligible[(tier_size*-1)..-1]
25
- end
26
-
27
- def calc_defender_tier
28
- eligible[(tier_size*-2)...(tier_size*-1)]
29
- end
30
-
31
- def eligible
32
- sorted.reject(&:eliminated)
33
- end
1
+ # frozen_string_literal: true
34
2
 
35
- def eligible_size
36
- @players.reject(&:eliminated).size
37
- end
38
-
39
- def elo_eligible?
40
- eligible.size > 15
41
- end
42
-
43
- def sorted
44
- sorted_players = @players.sort_by { |p| [-p.ffa_score, -(p.elo || 0), -(p.score || 0)] }
45
- sorted_players.each_with_index { |p, i| p.rank = i + 1 }
46
- sorted_players
47
- end
48
-
49
- def to_h
50
- sorted.map { |p| p.to_h }
51
- end
52
-
53
- def method_missing(...)
54
- @players.send(...)
55
- end
56
-
57
- private
58
-
59
- def build(param_players)
60
- param_players.map { |p| Player.new(p) }
61
- end
3
+ require_relative './player'
62
4
 
63
- # Returns the number of players to battle in each round
64
- # based on the eligible size, start fast and go slow until top 10
65
- # Example:
66
- # - 50 eligible, tier_size: 15
67
- # - 35 eligible, tier_size: 11
68
- # - 24 eligible, tier_size: 10
69
- # - 14 eligible, tier_size: 4
70
- # 4 rounds to reach top 10 with 50 players
71
- def tier_size
72
- [[(eligible_size / 3).ceil, 10].max, eligible_size - 10].min
5
+ module ActiveGenie
6
+ module Ranking
7
+ class PlayersCollection
8
+ def initialize(param_players)
9
+ @players = build(param_players)
10
+ end
11
+ attr_reader :players
12
+
13
+ def coefficient_of_variation
14
+ score_list = eligible.map(&:score).compact
15
+ return nil if score_list.empty?
16
+
17
+ mean = score_list.sum.to_f / score_list.size
18
+ return nil if mean.zero?
19
+
20
+ variance = score_list.map { |num| (num - mean)**2 }.sum / score_list.size
21
+ standard_deviation = Math.sqrt(variance)
22
+
23
+ (standard_deviation / mean) * 100
24
+ end
25
+
26
+ def calc_relegation_tier
27
+ eligible[(tier_size * -1)..]
28
+ end
29
+
30
+ def calc_defender_tier
31
+ eligible[(tier_size * -2)...(tier_size * -1)]
32
+ end
33
+
34
+ def eligible
35
+ sorted.reject(&:eliminated)
36
+ end
37
+
38
+ def eligible_size
39
+ @players.reject(&:eliminated).size
40
+ end
41
+
42
+ def elo_eligible?
43
+ eligible.size > 15
44
+ end
45
+
46
+ def sorted
47
+ sorted_players = @players.sort_by { |p| -p.sort_value }
48
+ sorted_players.each_with_index { |p, i| p.rank = i + 1 }
49
+ sorted_players
50
+ end
51
+
52
+ def to_json(*_args)
53
+ to_h.to_json
54
+ end
55
+
56
+ def to_h
57
+ sorted.map(&:to_h)
58
+ end
59
+
60
+ def method_missing(...)
61
+ @players.send(...)
62
+ end
63
+
64
+ def respond_to_missing?(method_name, include_private = false)
65
+ @players.respond_to?(method_name, include_private)
66
+ end
67
+
68
+ private
69
+
70
+ def build(param_players)
71
+ param_players.map { |p| Player.new(p) }
72
+ end
73
+
74
+ # Returns the number of players to battle in each round
75
+ # based on the eligible size, start fast and go slow until top 10
76
+ # Example:
77
+ # - 50 eligible, tier_size: 15
78
+ # - 35 eligible, tier_size: 11
79
+ # - 24 eligible, tier_size: 10
80
+ # - 14 eligible, tier_size: 4
81
+ # 4 rounds to reach top 10 with 50 players
82
+ def tier_size
83
+ [[(eligible_size / 3).ceil, 10].max, eligible_size - 10].min
84
+ end
73
85
  end
74
86
  end
75
- end
87
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../concerns/loggable'
2
4
  require_relative './players_collection'
3
5
  require_relative './free_for_all'
@@ -27,127 +29,102 @@ require_relative './ranking_scoring'
27
29
  # @param config [Hash] Additional configuration config
28
30
  # Example: { model: "gpt-4o", api_key: ENV['OPENAI_API_KEY'] }
29
31
  # @return [Hash] Final ranked player results
30
- module ActiveGenie::Ranking
31
- class Ranking
32
- include ActiveGenie::Concerns::Loggable
32
+ module ActiveGenie
33
+ module Ranking
34
+ class Ranking
35
+ include ActiveGenie::Concerns::Loggable
33
36
 
34
- def self.call(...)
35
- new(...).call
36
- end
37
+ def self.call(...)
38
+ new(...).call
39
+ end
37
40
 
38
- def initialize(param_players, criteria, reviewers: [], config: {})
39
- @criteria = criteria
40
- @reviewers = Array(reviewers).compact.uniq
41
- @config = ActiveGenie::Configuration.to_h(config)
42
- @players = PlayersCollection.new(param_players)
43
- @elo_rounds_played = 0
44
- @elo_round_battle_count = 0
45
- @free_for_all_battle_count = 0
46
- @total_tokens = 0
47
- @start_time = Time.now
48
- end
41
+ def initialize(_param_players, criteria, reviewers: [], config: {})
42
+ @criteria = criteria
43
+ @reviewers = Array(reviewers).compact.uniq
44
+ @config = ActiveGenie.configuration.merge(config)
45
+ @players = nil
46
+ end
49
47
 
50
- def call
51
- initial_log
48
+ def call
49
+ @players = create_players
52
50
 
53
- set_initial_player_scores!
54
- eliminate_obvious_bad_players!
51
+ set_initial_player_scores!
52
+ eliminate_obvious_bad_players!
55
53
 
56
- while @players.elo_eligible?
57
- elo_report = run_elo_round!
58
- eliminate_relegation_players!
59
- rebalance_players!(elo_report)
60
- end
54
+ while @players.elo_eligible?
55
+ elo_report = run_elo_round!
56
+ eliminate_relegation_players!
57
+ rebalance_players!(elo_report)
58
+ end
61
59
 
62
- run_free_for_all!
63
- final_logs
60
+ run_free_for_all!
64
61
 
65
- @players.sorted
66
- end
62
+ sorted_players
63
+ end
67
64
 
68
- private
65
+ private
69
66
 
70
- SCORE_VARIATION_THRESHOLD = 15
71
- ELIMINATION_VARIATION = 'variation_too_high'
72
- ELIMINATION_RELEGATION = 'relegation_tier'
73
-
74
- with_logging_context :log_context, ->(log) {
75
- @total_tokens += log[:total_tokens] || 0 if log[:code] == :llm_usage
76
- }
67
+ ELIMINATION_VARIATION = 'variation_too_high'
68
+ ELIMINATION_RELEGATION = 'relegation_tier'
77
69
 
78
- def initial_log
79
- @players.each { |p| ActiveGenie::Logger.debug({ code: :new_player, player: p.to_h }) }
80
- end
70
+ call_with_log_context :log_context
81
71
 
82
- def set_initial_player_scores!
83
- RankingScoring.call(@players, @criteria, reviewers: @reviewers, config: @config)
84
- end
72
+ def create_players
73
+ players = PlayersCollection.new(param_players)
74
+ players.each { |p| logger({ code: :new_player, player: p.to_h }) }
85
75
 
86
- def eliminate_obvious_bad_players!
87
- while @players.coefficient_of_variation >= SCORE_VARIATION_THRESHOLD
88
- @players.eligible.last.eliminated = ELIMINATION_VARIATION
76
+ players
89
77
  end
90
- end
91
78
 
92
- def run_elo_round!
93
- @elo_rounds_played += 1
94
-
95
- elo_report = EloRound.call(@players, @criteria, config: @config)
79
+ def set_initial_player_scores!
80
+ RankingScoring.call(@players, @criteria, reviewers: @reviewers, config: @config)
81
+ end
96
82
 
97
- @elo_round_battle_count += elo_report[:battles_count]
83
+ def eliminate_obvious_bad_players!
84
+ while @players.coefficient_of_variation >= @config.ranking.score_variation_threshold
85
+ @players.eligible.last.eliminated = ELIMINATION_VARIATION
86
+ end
87
+ end
98
88
 
99
- elo_report
100
- end
89
+ def run_elo_round!
90
+ EloRound.call(@players, @criteria, config: @config)
91
+ end
101
92
 
102
- def eliminate_relegation_players!
103
- @players.calc_relegation_tier.each { |player| player.eliminated = ELIMINATION_RELEGATION }
104
- end
93
+ def eliminate_relegation_players!
94
+ @players.calc_relegation_tier.each { |player| player.eliminated = ELIMINATION_RELEGATION }
95
+ end
105
96
 
106
- def rebalance_players!(elo_report)
107
- return if elo_report[:highest_elo_diff].negative?
97
+ def rebalance_players!(elo_report)
98
+ return if elo_report[:highest_elo_diff].negative?
108
99
 
109
- @players.eligible.each do |player|
110
- next if elo_report[:players_in_round].include?(player.id)
100
+ @players.eligible.each do |player|
101
+ next if elo_report[:players_in_round].include?(player.id)
111
102
 
112
- player.elo += elo_report[:highest_elo_diff]
103
+ player.elo += elo_report[:highest_elo_diff]
104
+ end
113
105
  end
114
- end
115
106
 
116
- def run_free_for_all!
117
- ffa_report = FreeForAll.call(@players, @criteria, config: @config)
107
+ def run_free_for_all!
108
+ FreeForAll.call(@players, @criteria, config: @config)
109
+ end
118
110
 
119
- @free_for_all_battle_count += ffa_report[:battles_count]
120
- end
111
+ def sorted_players
112
+ players = @players.sorted
113
+ logger({ code: :ranking_final, players: players.map(&:to_h) })
121
114
 
122
- def report
123
- {
124
- ranking_id: ranking_id,
125
- players_count: @players.size,
126
- variation_too_high: @players.select { |player| player.eliminated == ELIMINATION_VARIATION }.size,
127
- elo_rounds_played: @elo_rounds_played,
128
- elo_round_battle_count: @elo_round_battle_count,
129
- relegation_tier: @players.select { |player| player.eliminated == ELIMINATION_RELEGATION }.size,
130
- ffa_round_battle_count: @free_for_all_battle_count,
131
- top3: @players.eligible[0..2].map(&:id),
132
- total_tokens: @total_tokens,
133
- duration_seconds: Time.now - @start_time,
134
- }
135
- end
136
-
137
- def final_logs
138
- ActiveGenie::Logger.debug({ code: :ranking_final, players: @players.sorted.map(&:to_h) })
139
- ActiveGenie::Logger.info({ code: :ranking, **report })
140
- end
115
+ players.map(&:to_h)
116
+ end
141
117
 
142
- def log_context
143
- { config: @config[:log], ranking_id: }
144
- end
118
+ def log_context
119
+ { ranking_id: }
120
+ end
145
121
 
146
- def ranking_id
147
- player_ids = @players.map(&:id).join(',')
148
- ranking_unique_key = [player_ids, @criteria, @config.to_json].join('-')
122
+ def ranking_id
123
+ player_ids = @players.map(&:id).join(',')
124
+ ranking_unique_key = [player_ids, @criteria, @config.to_json].join('-')
149
125
 
150
- Digest::MD5.hexdigest(ranking_unique_key)
126
+ Digest::MD5.hexdigest(ranking_unique_key)
127
+ end
151
128
  end
152
129
  end
153
130
  end
@@ -1,71 +1,92 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../scoring/recommended_reviewers'
2
4
 
3
- module ActiveGenie::Ranking
4
- class RankingScoring
5
- def self.call(...)
6
- new(...).call
7
- end
5
+ module ActiveGenie
6
+ module Ranking
7
+ class RankingScoring
8
+ def self.call(...)
9
+ new(...).call
10
+ end
8
11
 
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
12
+ def initialize(players, criteria, reviewers: [], config: {})
13
+ @players = players
14
+ @criteria = criteria
15
+ @config = ActiveGenie.configuration.merge(config)
16
+ @reviewers = Array(reviewers).compact.uniq
17
+ end
15
18
 
16
- def call
17
- ActiveGenie::Logger.with_context(log_context) do
18
- @reviewers = generate_reviewers
19
+ def call
20
+ ActiveGenie::Logger.with_context(log_context) do
21
+ @reviewers = generate_reviewers
19
22
 
20
- players_without_score.each do |player|
21
- # TODO: This can take a while, can be parallelized
22
- player.score = generate_score(player)
23
+ players_to_score = players_without_score
24
+ until players_to_score.empty?
25
+ threads = []
26
+ mutex = Mutex.new
27
+
28
+ # Take up to 3 players for parallel processing
29
+ current_batch = players_to_score.shift(3)
30
+
31
+ current_batch.each do |player|
32
+ threads << Thread.new(player) do |p|
33
+ score = generate_score(p)
34
+
35
+ mutex.synchronize do
36
+ p.score = score
37
+ end
38
+ end
39
+ end
40
+
41
+ # Wait for all threads in this batch to complete
42
+ threads.each(&:join)
43
+ end
23
44
  end
24
45
  end
25
- end
26
46
 
27
- private
47
+ private
28
48
 
29
- def players_without_score
30
- @players_without_score ||= @players.select { |player| player.score.nil? }
31
- end
49
+ def players_without_score
50
+ @players_without_score ||= @players.select { |player| player.score.nil? }
51
+ end
32
52
 
33
- def generate_score(player)
34
- score, reasoning = ActiveGenie::Scoring.call(
35
- player.content,
36
- @criteria,
37
- @reviewers,
38
- config: @config
39
- ).values_at('final_score', 'final_reasoning')
53
+ def generate_score(player)
54
+ score, reasoning = ActiveGenie::Scoring.call(
55
+ player.content,
56
+ @criteria,
57
+ @reviewers,
58
+ config: @config
59
+ ).values_at('final_score', 'final_reasoning')
40
60
 
41
- ActiveGenie::Logger.debug({ code: :new_score, player_id: player.id, score:, reasoning: })
61
+ ActiveGenie::Logger.call({ code: :new_score, player_id: player.id, score:, reasoning: })
42
62
 
43
- score
44
- end
63
+ score
64
+ end
45
65
 
46
- def generate_reviewers
47
- return @reviewers if @reviewers.size > 0
66
+ def generate_reviewers
67
+ return @reviewers if @reviewers.size.positive?
48
68
 
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({ code: :new_reviewers, reviewers: [reviewer1, reviewer2, reviewer3] })
69
+ reviewer1, reviewer2, reviewer3 = ActiveGenie::Scoring::RecommendedReviewers.call(
70
+ [@players.sample.content, @players.sample.content].join("\n\n"),
71
+ @criteria,
72
+ config: @config
73
+ ).values_at('reviewer1', 'reviewer2', 'reviewer3')
56
74
 
57
- [reviewer1, reviewer2, reviewer3]
58
- end
75
+ ActiveGenie::Logger.call({ code: :new_reviewers, reviewers: [reviewer1, reviewer2, reviewer3] })
59
76
 
60
- def log_context
61
- { ranking_scoring_id: }
62
- end
77
+ [reviewer1, reviewer2, reviewer3]
78
+ end
63
79
 
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('-')
80
+ def log_context
81
+ { ranking_scoring_id: }
82
+ end
67
83
 
68
- Digest::MD5.hexdigest(ranking_unique_key)
84
+ def ranking_scoring_id
85
+ player_ids = players_without_score.map(&:id).join(',')
86
+ ranking_unique_key = [player_ids, @criteria, @config.to_json].join('-')
87
+
88
+ "#{Digest::MD5.hexdigest(ranking_unique_key)}-scoring"
89
+ end
69
90
  end
70
91
  end
71
92
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'ranking/ranking'
2
4
 
3
5
  module ActiveGenie
@@ -17,7 +17,7 @@ text = "The code implements a binary search algorithm with O(log n) complexity"
17
17
  criteria = "Evaluate technical accuracy and clarity"
18
18
  reviewers = ["Algorithm Expert", "Technical Writer"]
19
19
 
20
- result = ActiveGenie::Scoring::Basic.call(text, criteria, reviewers)
20
+ result = ActiveGenie::Scoring.call(text, criteria, reviewers)
21
21
  # => {
22
22
  # algorithm_expert_score: 95,
23
23
  # algorithm_expert_reasoning: "Accurately describes binary search and its complexity",
@@ -35,7 +35,7 @@ When no reviewers are specified, the system automatically recommends appropriate
35
35
  text = "The patient shows signs of improved cardiac function"
36
36
  criteria = "Evaluate medical accuracy and clarity"
37
37
 
38
- result = ActiveGenie::Scoring::Basic.call(text, criteria)
38
+ result = ActiveGenie::Scoring.call(text, criteria)
39
39
  # => {
40
40
  # cardiologist_score: 88,
41
41
  # cardiologist_reasoning: "Accurate assessment of cardiac improvement",
@@ -49,7 +49,7 @@ result = ActiveGenie::Scoring::Basic.call(text, criteria)
49
49
 
50
50
  ## Interface
51
51
 
52
- ### `Basic.call(text, criteria, reviewers = [], config: {})`
52
+ ### `.call(text, criteria, reviewers = [], config: {})`
53
53
  Main interface for scoring text content.
54
54
 
55
55
  #### Parameters
@@ -73,4 +73,4 @@ Recommends appropriate reviewers based on content and criteria.
73
73
  - Supports custom weighting of reviewer scores
74
74
  - Returns detailed reasoning for each score
75
75
 
76
- Performance Impact: Using multiple reviewers or requesting detailed feedback may increase processing time.
76
+ Performance Impact: Using multiple reviewers or requesting detailed feedback may increase processing time.