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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -72
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +2 -2
  5. data/lib/active_genie/battle/basic.rb +52 -56
  6. data/lib/active_genie/battle.rb +1 -1
  7. data/lib/active_genie/clients/openai_client.rb +119 -0
  8. data/lib/active_genie/clients/unified_client.rb +19 -0
  9. data/lib/active_genie/configuration/log_config.rb +14 -0
  10. data/lib/active_genie/configuration/openai_config.rb +56 -0
  11. data/lib/active_genie/configuration/providers_config.rb +37 -0
  12. data/lib/active_genie/configuration.rb +18 -23
  13. data/lib/active_genie/data_extractor/README.md +4 -5
  14. data/lib/active_genie/data_extractor/basic.rb +13 -13
  15. data/lib/active_genie/data_extractor/from_informal.rb +7 -7
  16. data/lib/active_genie/data_extractor.rb +1 -1
  17. data/lib/active_genie/logger.rb +72 -0
  18. data/lib/active_genie/ranking/README.md +43 -0
  19. data/lib/active_genie/ranking/elo_round.rb +113 -0
  20. data/lib/active_genie/ranking/free_for_all.rb +76 -0
  21. data/lib/active_genie/ranking/player.rb +97 -0
  22. data/lib/active_genie/{leaderboard → ranking}/players_collection.rb +18 -11
  23. data/lib/active_genie/ranking/ranking.rb +98 -0
  24. data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
  25. data/lib/active_genie/ranking.rb +12 -0
  26. data/lib/active_genie/scoring/README.md +4 -8
  27. data/lib/active_genie/scoring/basic.rb +58 -24
  28. data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +21 -12
  29. data/lib/active_genie/scoring.rb +4 -4
  30. data/lib/active_genie.rb +10 -18
  31. data/lib/tasks/install.rake +3 -3
  32. data/lib/tasks/templates/active_genie.rb +17 -0
  33. metadata +74 -90
  34. data/lib/active_genie/clients/openai.rb +0 -61
  35. data/lib/active_genie/clients/router.rb +0 -41
  36. data/lib/active_genie/leaderboard/elo_ranking.rb +0 -88
  37. data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
  38. data/lib/active_genie/leaderboard/league.rb +0 -48
  39. data/lib/active_genie/leaderboard/player.rb +0 -52
  40. data/lib/active_genie/leaderboard.rb +0 -11
  41. data/lib/active_genie/utils/math.rb +0 -15
  42. data/lib/tasks/templates/active_genie.yml +0 -7
@@ -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
@@ -0,0 +1,12 @@
1
+ require_relative 'ranking/ranking'
2
+
3
+ module ActiveGenie
4
+ # See the [ranking README](lib/active_genie/ranking/README.md) for more information.
5
+ module Ranking
6
+ module_function
7
+
8
+ def call(...)
9
+ Ranking.call(...)
10
+ end
11
+ end
12
+ 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 = [], options: {})`
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
- - `options` [Hash] - Additional configuration options
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, options: {})`
61
+ ### `RecommendedReviewers.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
- - `options` [Hash] - Additional configuration options
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/router'
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 options [Hash] Additional configuration options that modify the scoring behavior
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 = [], options: {})
31
- new(text, criteria, reviewers, options:).call
28
+ def self.call(...)
29
+ new(...).call
32
30
  end
33
31
 
34
- def initialize(text, criteria, reviewers = [], options: {})
32
+ def initialize(text, criteria, reviewers = [], config: {})
35
33
  @text = text
36
34
  @criteria = criteria
37
35
  @reviewers = Array(reviewers).compact.uniq
38
- @options = options
36
+ @config = ActiveGenie::Configuration.to_h(config)
39
37
  end
40
38
 
41
39
  def call
@@ -78,7 +76,12 @@ module ActiveGenie::Scoring
78
76
  }
79
77
  }
80
78
 
81
- ::ActiveGenie::Clients::Router.function_calling(messages, function, options: @options)
79
+ ::ActiveGenie::Clients::UnifiedClient.function_calling(
80
+ messages,
81
+ function,
82
+ model_tier: 'lower_tier',
83
+ config: @config
84
+ )
82
85
  end
83
86
 
84
87
  private
@@ -87,31 +90,62 @@ module ActiveGenie::Scoring
87
90
  @get_or_recommend_reviewers ||= if @reviewers.count > 0
88
91
  @reviewers
89
92
  else
90
- recommended_reviews = RecommendedReviews.call(@text, @criteria, options: @options)
93
+ result = RecommendedReviewers.call(@text, @criteria, config: @config)
91
94
 
92
- [recommended_reviews[:reviewer1], recommended_reviews[:reviewer2], recommended_reviews[:reviewer3]]
95
+ [result['reviewer1'], result['reviewer2'], result['reviewer3']]
93
96
  end
94
97
  end
95
98
 
96
99
  PROMPT = <<~PROMPT
97
- Evaluate and score the provided text based on predefined criteria, which may include rules, keywords, or patterns. Use a scoring range of 0 to 100, with 100 representing the highest possible score. Follow the instructions below to ensure an accurate and objective assessment.
100
+ Evaluate and score the provided text based on predefined criteria, using a scoring range of 0 to 100 with 100 representing the highest possible score.
101
+
102
+ Follow the instructions below to ensure a comprehensive and objective assessment.
98
103
 
99
104
  # Evaluation Process
100
- 1. **Analysis**: Thoroughly compare the text against each criterion to ensure comprehensive evaluation.
101
- 2. **Document Deviations**: Clearly identify and document any areas where the content does not align with the specified criteria.
102
- 3. **Highlight Strengths**: Emphasize notable features or elements that enhance the overall quality or effectiveness of the content.
103
- 4. **Identify Weaknesses**: Specify areas where the content fails to meet the criteria or where improvements could be made.
104
105
 
105
- # Output Requirements
106
- Provide a detailed review, including:
107
- - A final score (0-100)
108
- - Specific reasoning for the assigned score, covering all evaluated criteria.
109
- - Ensure the reasoning includes both positive aspects and suggested improvements.
106
+ 1. **Analysis**:
107
+ - Thoroughly compare the text against each criterion for a comprehensive evaluation.
108
+
109
+ 2. **Document Deviations**:
110
+ - Identify and document areas where the content does not align with the specified criteria.
111
+
112
+ 3. **Highlight Strengths**:
113
+ - Note notable features or elements that enhance the quality or effectiveness of the content.
114
+
115
+ 4. **Identify Weaknesses**:
116
+ - Specify areas where the content fails to meet the criteria or where improvements could be made.
117
+
118
+ # Scoring Fairness
119
+
120
+ - Ensure the assigned score reflects both the alignment with the criteria and the content's effectiveness.
121
+ - Consider if the fulfillment of other criteria compensates for areas lacking extreme details.
122
+
123
+ # Scoring Range
124
+
125
+ Segment scores into five parts before assigning a final score:
126
+ - **Terrible**: 0-20 - Content does not meet the criteria.
127
+ - **Bad**: 21-40 - Content is substandard but meets some criteria.
128
+ - **Average**: 41-60 - Content meets criteria with room for improvement.
129
+ - **Good**: 61-80 - Content exceeds criteria and is above average.
130
+ - **Great**: 81-100 - Content exceeds all expectations.
110
131
 
111
132
  # Guidelines
112
- - Maintain objectivity, avoiding biases or preconceived notions.
113
- - Deconstruct each criterion into actionable components for a systematic evaluation.
114
- - If the text lacks information, apply reasonable judgment to assign a score while clearly explaining the rationale.
133
+
134
+ - Maintain objectivity and avoid biases.
135
+ - Deconstruct each criterion into actionable components for systematic evaluation.
136
+ - Apply reasonable judgment in assigning a score, justifying your rationale clearly.
137
+
138
+ # Output Format
139
+
140
+ - Provide a detailed review including:
141
+ - A final score (0-100)
142
+ - Specific reasoning for the assigned score, detailing all evaluated criteria
143
+ - Include both positive aspects and suggested improvements
144
+
145
+ # Notes
146
+
147
+ - Consider edge cases where the text may partially align with criteria.
148
+ - If lacking information, reasonably judge and explain your scoring approach.
115
149
  PROMPT
116
150
  end
117
151
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../clients/router.rb'
3
+ require_relative '../clients/unified_client'
4
4
 
5
5
  module ActiveGenie::Scoring
6
- # The RecommendedReviews class intelligently suggests appropriate reviewer roles
6
+ # The RecommendedReviewers class intelligently suggests appropriate reviewer roles
7
7
  # for evaluating text content based on specific criteria. It uses AI to analyze
8
8
  # the content and criteria to identify the most suitable subject matter experts.
9
9
  #
@@ -11,27 +11,25 @@ module ActiveGenie::Scoring
11
11
  # three distinct reviewer roles with complementary expertise and perspectives.
12
12
  #
13
13
  # @example Getting recommended reviewers for technical content
14
- # RecommendedReviews.call("Technical documentation about API design",
14
+ # RecommendedReviewers.call("Technical documentation about API design",
15
15
  # "Evaluate technical accuracy and clarity")
16
16
  # # => { reviewer1: "API Architect", reviewer2: "Technical Writer",
17
17
  # # reviewer3: "Developer Advocate", reasoning: "..." }
18
18
  #
19
- class RecommendedReviews
20
- def self.call(text, criteria, options: {})
21
- new(text, criteria, options:).call
19
+ class RecommendedReviewers
20
+ def self.call(...)
21
+ new(...).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 options [Hash] Additional configuration options that modify the recommendation process
29
- # @option options [Boolean] :prefer_technical Whether to favor technical expertise
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
- @options = options
32
+ @config = ActiveGenie::Configuration.to_h(config)
35
33
  end
36
34
 
37
35
  def call
@@ -55,7 +53,14 @@ module ActiveGenie::Scoring
55
53
  }
56
54
  }
57
55
 
58
- ::ActiveGenie::Clients::Router.function_calling(messages, function, options: @options)
56
+ result = client.function_calling(
57
+ messages,
58
+ function,
59
+ model_tier: 'lower_tier',
60
+ config: @config
61
+ )
62
+
63
+ result
59
64
  end
60
65
 
61
66
  private
@@ -74,5 +79,9 @@ module ActiveGenie::Scoring
74
79
  - Include reasoning for how each choice supports a thorough and insightful review.
75
80
  - Avoid redundant or overly similar titles/roles to maintain diversity.
76
81
  PROMPT
82
+
83
+ def client
84
+ ::ActiveGenie::Clients::UnifiedClient
85
+ end
77
86
  end
78
87
  end
@@ -1,8 +1,8 @@
1
1
  require_relative 'scoring/basic'
2
- require_relative 'scoring/recommended_reviews'
2
+ require_relative 'scoring/recommended_reviewers'
3
3
 
4
4
  module ActiveGenie
5
- # Text evaluation system that provides detailed scoring and feedback using multiple expert reviewers
5
+ # See the [Scoring README](lib/active_genie/scoring/README.md) for more information.
6
6
  module Scoring
7
7
  module_function
8
8
 
@@ -10,8 +10,8 @@ module ActiveGenie
10
10
  Basic.call(...)
11
11
  end
12
12
 
13
- def recommended_reviews(...)
14
- RecommendedReviews.call(...)
13
+ def recommended_reviewers(...)
14
+ RecommendedReviewers.call(...)
15
15
  end
16
16
  end
17
17
  end
data/lib/active_genie.rb CHANGED
@@ -1,15 +1,19 @@
1
- module ActiveGenie
2
- autoload :Configuration, File.join(__dir__, 'active_genie/configuration')
1
+ require_relative 'active_genie/logger'
2
+ require_relative 'active_genie/configuration'
3
3
 
4
- # Modules
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
- autoload :Leaderboard, File.join(__dir__, 'active_genie/leaderboard')
8
+ autoload :Ranking, File.join(__dir__, 'active_genie/ranking')
9
9
 
10
- class << self
10
+ class << self
11
11
  def configure
12
- yield(config) if block_given?
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
@@ -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.yml')
7
- target = File.join('config', 'active_genie.yml')
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 config/active_genie.yml to #{target}"
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