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
metadata CHANGED
@@ -1,51 +1,47 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_genie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Radamés Roriz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-06 00:00:00.000000000 Z
11
+ date: 2025-03-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: "# ActiveGenie \U0001F9DE‍♂️\n> Transform your Ruby application with
14
- powerful, production-ready GenAI features\n\n[![Gem Version](https://badge.fury.io/rb/active_genie.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/active_genie)\n[![Ruby](https://github.com/roriz/active_genie/actions/workflows/ruby.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/ruby.yml)\n\nActiveGenie
13
+ description: "# ActiveGenie \U0001F9DE‍♂️\n> The lodash for GenAI, stop reinventing
14
+ the wheel\n\n[![Gem Version](https://badge.fury.io/rb/active_genie.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/active_genie)\n[![Ruby](https://github.com/roriz/active_genie/actions/workflows/ruby.yml/badge.svg)](https://github.com/roriz/active_genie/actions/workflows/ruby.yml)\n\nActiveGenie
15
15
  is a Ruby gem that provides a polished, production-ready interface for working with
16
- Generative AI (GenAI) models. Just like ActiveStorage simplifies file handling in
17
- Rails, ActiveGenie makes it effortless to integrate GenAI capabilities into your
18
- Ruby applications.\n\n## Features\n\n- \U0001F3AF **Data Extraction**: Extract structured
19
- data from unstructured text with type validation\n- \U0001F4CA **Smart Scoring**:
20
- Multi-reviewer evaluation system with automatic expert selection\n- \U0001F4AD **Sentiment
21
- Analysis**: Advanced sentiment analysis with customizable rules\n- \U0001F512 **Safe
22
- & Secure**: Built-in validation and sanitization\n- \U0001F6E0️ **Configurable**:
23
- Supports multiple GenAI providers and models\n\n## Installation\n\n1. Add to your
24
- Gemfile:\n```ruby\ngem 'active_genie'\n```\n\n2. Install the gem:\n```shell\nbundle
16
+ Generative AI (GenAI) models. Just like Lodash or ActiveStorage, ActiveGenie simplifies
17
+ GenAI integration in your Ruby applications.\n\n## Features\n\n- \U0001F3AF **Data
18
+ Extraction**: Extract structured data from unstructured text with type validation\n-
19
+ \U0001F4CA **Data Scoring**: Multi-reviewer evaluation system\n- ⚔️ **Data Battle**:
20
+ Battle between two data like a political debate\n- \U0001F4AD **Data Ranking**:
21
+ Consistent rank data using scoring + elo ranking + battles\n\n## Installation\n\n1.
22
+ Add to your Gemfile:\n```ruby\ngem 'active_genie'\n```\n\n2. Install the gem:\n```shell\nbundle
25
23
  install\n```\n\n3. Generate the configuration:\n```shell\necho \"ActiveGenie.load_tasks\"
26
- >> Rakefile\nrails g active_genie:install\n```\n\n4. [Optional] Configure your credentials
27
- in `config/active_genie.yml`:\n```yaml\nGPT-4o-mini:\n api_key: <%= ENV['OPENAI_API_KEY']
28
- %>\n provider: \"openai\"\n\nclaude-3-5-sonnet:\n api_key: <%= ENV['ANTHROPIC_API_KEY']
29
- %>\n provider: \"anthropic\"\n```\n\n> The first key will be used as default in
30
- all modules, in this example `GPT-4o-mini`\n\n## Quick Start\n\n### Data Extractor\nExtract
31
- structured data from text using AI-powered analysis, handling informal language
32
- and complex expressions.\n\n```ruby\ntext = \"Nike Air Max 90 - Size 42 - $199.99\"\nschema
33
- = {\n brand: { \n type: 'string',\n enum: [\"Nike\", \"Adidas\", \"Puma\"]\n
34
- \ },\n price: { \n type: 'number',\n minimum: 0\n },\n size: {\n type:
35
- 'integer',\n minimum: 35,\n maximum: 46\n }\n}\n\nresult = ActiveGenie::DataExtractor.call(text,
36
- schema)\n# => { \n# brand: \"Nike\", \n# brand_explanation: \"Brand name
37
- found at start of text\",\n# price: 199.99,\n# price_explanation: \"Price
38
- found in USD format at end\",\n# size: 42,\n# size_explanation: \"Size
39
- explicitly stated in the middle\"\n# }\n```\n\nFeatures:\n- Structured data extraction
40
- with type validation\n- Schema-based extraction with custom constraints\n- Informal
41
- text analysis (litotes, hedging)\n- Detailed explanations for extracted values\n\nSee
42
- the [Data Extractor README](lib/active_genie/data_extractor/README.md) for informal
43
- text processing, advanced schemas, and detailed interface documentation.\n\n###
44
- Scoring\nText evaluation system that provides detailed scoring and feedback using
45
- multiple expert reviewers. Get balanced scoring through AI-powered expert reviewers
46
- that automatically adapt to your content.\n\n```ruby\ntext = \"The code implements
47
- a binary search algorithm with O(log n) complexity\"\ncriteria = \"Evaluate technical
48
- accuracy and clarity\"\n\nresult = ActiveGenie::Scoring::Basic.call(text, criteria)\n#
24
+ >> Rakefile\nrails g active_genie:install\n```\n\n4. Configure your credentials
25
+ in `config/initializers/active_genie.rb`:\n```ruby\nActiveGenie.configure do |config|\n
26
+ \ config.openai.api_key = ENV['OPENAI_API_KEY']\nend\n```\n\n## Quick Start\n\n###
27
+ Data Extractor\nExtract structured data from text using AI-powered analysis, handling
28
+ informal language and complex expressions.\n\n```ruby\ntext = \"Nike Air Max 90
29
+ - Size 42 - $199.99\"\nschema = {\n brand: { \n type: 'string',\n enum: [\"Nike\",
30
+ \"Adidas\", \"Puma\"]\n },\n price: { \n type: 'number',\n minimum: 0\n
31
+ \ },\n size: {\n type: 'integer',\n minimum: 35,\n maximum: 46\n }\n}\n\nresult
32
+ = ActiveGenie::DataExtractor.call(text, schema)\n# => { \n# brand: \"Nike\",
33
+ \n# brand_explanation: \"Brand name found at start of text\",\n# price:
34
+ 199.99,\n# price_explanation: \"Price found in USD format at end\",\n# size:
35
+ 42,\n# size_explanation: \"Size explicitly stated in the middle\"\n# }\n```\n\nFeatures:\n-
36
+ Structured data extraction with type validation\n- Schema-based extraction with
37
+ custom constraints\n- Informal text analysis (litotes, hedging)\n- Detailed explanations
38
+ for extracted values\n\nSee the [Data Extractor README](lib/active_genie/data_extractor/README.md)
39
+ for informal text processing, advanced schemas, and detailed interface documentation.\n\n###
40
+ Data Scoring\nText evaluation system that provides detailed scoring and feedback
41
+ using multiple expert reviewers. Get balanced scoring through AI-powered expert
42
+ reviewers that automatically adapt to your content.\n\n```ruby\ntext = \"The code
43
+ implements a binary search algorithm with O(log n) complexity\"\ncriteria = \"Evaluate
44
+ technical accuracy and clarity\"\n\nresult = ActiveGenie::Scoring.basic(text, criteria)\n#
49
45
  => {\n# algorithm_expert_score: 95,\n# algorithm_expert_reasoning: \"Accurately
50
46
  describes binary search and its complexity\",\n# technical_writer_score: 90,\n#
51
47
  \ technical_writer_reasoning: \"Clear and concise explanation of the algorithm\",\n#
@@ -53,11 +49,11 @@ description: "# ActiveGenie \U0001F9DE‍♂️\n> Transform your Ruby applicati
53
49
  automatic expert selection\n- Detailed feedback with scoring reasoning\n- Customizable
54
50
  reviewer weights\n- Flexible evaluation criteria\n\nSee the [Scoring README](lib/active_genie/scoring/README.md)
55
51
  for advanced usage, custom reviewers, and detailed interface documentation.\n\n###
56
- Battle\nAI-powered battle evaluation system that determines winners between two
57
- players based on specified criteria.\n\n```ruby\nrequire 'active_genie'\n\nplayer_a
52
+ Data Battle\nAI-powered battle evaluation system that determines winners between
53
+ two players based on specified criteria.\n\n```ruby\nrequire 'active_genie'\n\nplayer_a
58
54
  = \"Implementation uses dependency injection for better testability\"\nplayer_b
59
55
  = \"Code has high test coverage but tightly coupled components\"\ncriteria = \"Evaluate
60
- code quality and maintainability\"\n\nresult = ActiveGenie::Battle::Basic.call(player_a,
56
+ code quality and maintainability\"\n\nresult = ActiveGenie::Battle.call(player_a,
61
57
  player_b, criteria)\n# => {\n# winner_player: \"Implementation uses dependency
62
58
  injection for better testability\",\n# reasoning: \"Player A's implementation
63
59
  demonstrates better maintainability through dependency injection, \n# which
@@ -68,46 +64,29 @@ description: "# ActiveGenie \U0001F9DE‍♂️\n> Transform your Ruby applicati
68
64
  evaluation with automatic expert selection\n- Detailed feedback with scoring reasoning\n-
69
65
  Customizable reviewer weights\n- Flexible evaluation criteria\n\nSee the [Battle
70
66
  README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers,
71
- and detailed interface documentation.\n\n### Summarizer (WIP)\nThe summarizer is
72
- a tool that can be used to summarize a given text. It uses a set of rules to summarize
73
- the text out of the box. Uses the best practices of prompt engineering and engineering
74
- to make the summarization as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
75
- = \"Example text to be summarized. The fox jumps over the dog\"\nsummarized_text
76
- = ActiveGenie::Summarizer.call(text)\nputs summarized_text # => \"The fox jumps
77
- over the dog\"\n```\n\n### Language detector (WIP)\nThe language detector is a tool
78
- that can be used to detect the language of a given text. It uses a set of rules
79
- to detect the language of the text out of the box. Uses the best practices of prompt
80
- engineering and engineering to make the language detection as accurate as possible.\n\n```ruby\nrequire
81
- 'active_genie'\n\ntext = \"Example text to be detected\"\nlanguage = ActiveGenie::LanguageDetector.call(text)\nputs
82
- language # => \"en\"\n```\n\n### Translator (WIP)\nThe translator is a tool that
83
- can be used to translate a given text. It uses a set of rules to translate the text
84
- out of the box. Uses the best practices of prompt engineering and engineering to
85
- make the translation as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
86
- = \"Example text to be translated\"\ntranslated_text = ActiveGenie::Translator.call(text,
87
- from: 'en', to: 'pt')\nputs translated_text # => \"Exemplo de texto a ser traduzido\"\n```\n\n###
88
- Sentiment analyzer (WIP)\nThe sentiment analyzer is a tool that can be used to analyze
89
- the sentiment of a given text. It uses a set of rules to analyze the sentiment of
90
- the text out of the box. Uses the best practices of prompt engineering and engineering
91
- to make the sentiment analysis as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
92
- = \"Example text to be analyzed\"\nsentiment = ActiveGenie::SentimentAnalyzer.call(text)\nputs
93
- sentiment # => \"positive\"\n```\n\n### Elo ranking (WIP)\nThe Elo ranking is a
94
- tool that can be used to rank a set of items. It uses a set of rules to rank the
95
- items out of the box. Uses the best practices of prompt engineering and engineering
96
- to make the ranking as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\nitems
97
- = ['Square', 'Circle', 'Triangle']\ncriterias = 'items that look rounded'\nranked_items
98
- = ActiveGenie::EloRanking.call(items, criterias, rounds: 10)\nputs ranked_items
99
- # => [{ name: \"Circle\", score: 1500 }, { name: \"Square\", score: 800 }, { name:
100
- \"Triangle\", score: 800 }]\n```\n\n\n## Configuration Options\n\n| Option | Description
101
- | Default |\n|--------|-------------|---------|\n| `provider` | LLM provider (openai,
102
- anthropic, etc) | `nil` |\n| `model` | Model to use | `nil` |\n| `api_key` | Provider
103
- API key | `nil` |\n| `timeout` | Request timeout in seconds | `5` |\n| `max_retries`
104
- | Maximum retry attempts | `3` |\n\n> **Note:** Each module can append its own set
105
- of configuration options, see the individual module documentation for details.\n\n##
106
- Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout
107
- -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing
108
- feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5.
109
- Open a Pull Request\n## License\n\nThis project is licensed under the MIT License
110
- - see the [LICENSE](LICENSE) file for details.\n\n"
67
+ and detailed interface documentation.\n\n### Data Ranking\nThe Ranking module provides
68
+ competitive ranking through multi-stage evaluation:\n\n\n```ruby\nrequire 'active_genie'\n\nplayers
69
+ = ['REST API', 'GraphQL API', 'SOAP API', 'gRPC API', 'Websocket API']\ncriteria
70
+ = \"Best one to be used into a high changing environment\"\n\nresult = ActiveGenie::Ranking.call(players,
71
+ criteria)\n# => {\n# winner_player: \"gRPC API\",\n# reasoning: \"gRPC
72
+ API is the best one to be used into a high changing environment\",\n# }\n```\n\n-
73
+ **Multi-phase ranking system** combining expert scoring and ELO algorithms\n- **Automatic
74
+ elimination** of inconsistent performers using statistical analysis\n- **Dynamic
75
+ ranking adjustments** based on simulated pairwise battles, from bottom to top\n\nSee
76
+ the [Ranking README](lib/active_genie/ranking/README.md) for implementation details,
77
+ configuration, and advanced ranking strategies.\n\n### Text Summarizer (Future)\n###
78
+ Language detector (Future)\n### Translator (Future)\n### Sentiment analyzer (Future)\n\n##
79
+ Configuration\n\n| Config | Description | Default |\n|--------|-------------|---------|\n|
80
+ `provider` | LLM provider (openai, anthropic, etc) | `nil` |\n| `model` | Model
81
+ to use | `nil` |\n| `api_key` | Provider API key | `nil` |\n| `timeout` | Request
82
+ timeout in seconds | `5` |\n| `max_retries` | Maximum retry attempts | `3` |\n\n>
83
+ **Note:** Each module can append its own set of configuration, see the individual
84
+ module documentation for details.\n\n## Contributing\n\n1. Fork the repository\n2.
85
+ Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit
86
+ your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch (`git
87
+ push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## License\n\nThis
88
+ project is licensed under the Apache License 2.0 License - see the [LICENSE](LICENSE)
89
+ file for details.\n"
111
90
  email:
112
91
  - radames@roriz.dev
113
92
  executables: []
@@ -121,26 +100,31 @@ files:
121
100
  - lib/active_genie/battle.rb
122
101
  - lib/active_genie/battle/README.md
123
102
  - lib/active_genie/battle/basic.rb
124
- - lib/active_genie/clients/openai.rb
125
- - lib/active_genie/clients/router.rb
103
+ - lib/active_genie/clients/openai_client.rb
104
+ - lib/active_genie/clients/unified_client.rb
126
105
  - lib/active_genie/configuration.rb
106
+ - lib/active_genie/configuration/log_config.rb
107
+ - lib/active_genie/configuration/openai_config.rb
108
+ - lib/active_genie/configuration/providers_config.rb
127
109
  - lib/active_genie/data_extractor.rb
128
110
  - lib/active_genie/data_extractor/README.md
129
111
  - lib/active_genie/data_extractor/basic.rb
130
112
  - lib/active_genie/data_extractor/from_informal.rb
131
- - lib/active_genie/leaderboard.rb
132
- - lib/active_genie/leaderboard/elo_ranking.rb
133
- - lib/active_genie/leaderboard/leaderboard.rb
134
- - lib/active_genie/leaderboard/league.rb
135
- - lib/active_genie/leaderboard/player.rb
136
- - lib/active_genie/leaderboard/players_collection.rb
113
+ - lib/active_genie/logger.rb
114
+ - lib/active_genie/ranking.rb
115
+ - lib/active_genie/ranking/README.md
116
+ - lib/active_genie/ranking/elo_round.rb
117
+ - lib/active_genie/ranking/free_for_all.rb
118
+ - lib/active_genie/ranking/player.rb
119
+ - lib/active_genie/ranking/players_collection.rb
120
+ - lib/active_genie/ranking/ranking.rb
121
+ - lib/active_genie/ranking/ranking_scoring.rb
137
122
  - lib/active_genie/scoring.rb
138
123
  - lib/active_genie/scoring/README.md
139
124
  - lib/active_genie/scoring/basic.rb
140
- - lib/active_genie/scoring/recommended_reviews.rb
141
- - lib/active_genie/utils/math.rb
125
+ - lib/active_genie/scoring/recommended_reviewers.rb
142
126
  - lib/tasks/install.rake
143
- - lib/tasks/templates/active_genie.yml
127
+ - lib/tasks/templates/active_genie.rb
144
128
  homepage: https://github.com/Roriz/active_genie
145
129
  licenses:
146
130
  - Apache-2.0
@@ -1,61 +0,0 @@
1
- require 'json'
2
- require 'net/http'
3
-
4
- module ActiveGenie::Clients
5
- class Openai
6
- class << self
7
- def function_calling(messages, function, options: {})
8
- app_config = ActiveGenie.config_by_model(options[:model])
9
-
10
- model = options[:model] || app_config[:model]
11
-
12
- raise "Model can't be blank" if model.nil?
13
-
14
- payload = {
15
- messages:,
16
- response_format: {
17
- type: 'json_schema',
18
- json_schema: function
19
- },
20
- model:,
21
- }
22
-
23
- api_key = options[:api_key] || app_config[:api_key]
24
-
25
- headers = DEFAULT_HEADERS.merge(
26
- 'Authorization': "Bearer #{api_key}"
27
- ).compact
28
-
29
- response = request(payload, headers)
30
-
31
- parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'content'))
32
-
33
- parsed_response.dig('properties') || parsed_response
34
- rescue JSON::ParserError
35
- nil
36
- end
37
-
38
- def request(payload, headers)
39
- response = Net::HTTP.post(
40
- URI(API_URL),
41
- payload.to_json,
42
- headers
43
- )
44
-
45
- raise OpenaiError, response.body unless response.is_a?(Net::HTTPSuccess)
46
- return nil if response.body.empty?
47
-
48
- JSON.parse(response.body)
49
- end
50
-
51
- end
52
-
53
- API_URL = 'https://api.openai.com/v1/chat/completions'.freeze
54
- DEFAULT_HEADERS = {
55
- 'Content-Type': 'application/json',
56
- }
57
-
58
- # TODO: add some more rich error handling
59
- class OpenaiError < StandardError; end
60
- end
61
- end
@@ -1,41 +0,0 @@
1
- require_relative './openai'
2
-
3
- module ActiveGenie::Clients
4
- class Router
5
- class << self
6
- def function_calling(messages, function, options: {})
7
- app_config = ActiveGenie.config_by_model(options[:model])
8
-
9
- provider = options[:provider] || app_config[:provider]
10
- client = PROVIDER_TO_CLIENT[provider&.downcase&.strip&.to_sym]
11
- raise "Provider \"#{provider}\" not supported" unless client
12
-
13
- response = client.function_calling(messages, function, options:)
14
-
15
- clear_invalid_values(response)
16
- end
17
-
18
- private
19
-
20
- PROVIDER_TO_CLIENT = {
21
- openai: Openai,
22
- }
23
-
24
- INVALID_VALUES = [
25
- 'not sure',
26
- 'not clear',
27
- 'not specified',
28
- 'none',
29
- 'null',
30
- 'undefined',
31
- ].freeze
32
-
33
- def clear_invalid_values(data)
34
- data.reduce({}) do |acc, (field, value)|
35
- acc[field] = value unless INVALID_VALUES.include?(value)
36
- acc
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,88 +0,0 @@
1
- require_relative '../battle/basic'
2
- require_relative '../utils/math'
3
-
4
- module ActiveGenie::Leaderboard
5
- class EloRanking
6
- def self.call(players, criteria, options: {})
7
- new(players, criteria, options:).call
8
- end
9
-
10
- def initialize(players, criteria, options: {})
11
- @players = players
12
- @criteria = criteria
13
- @options = options
14
- end
15
-
16
- def call
17
- @players.each(&:generate_elo_by_score)
18
-
19
- while @players.eligible_size > MINIMAL_PLAYERS_TO_BATTLE
20
- round = create_round(@players.tier_relegation, @players.tier_defense)
21
-
22
- round.each do |player_a, player_b|
23
- winner, loser = battle(player_a, player_b) # This can take a while, can be parallelized
24
- update_elo(winner, loser)
25
- end
26
-
27
- @players.tier_relegation.each { |player| player.eliminated = "relegation/#{@players.eligible_size}" }
28
- end
29
-
30
- @players
31
- end
32
-
33
- private
34
-
35
- MATCHS_PER_PLAYER = 3
36
- LOSE_PENALTY = 15
37
- MINIMAL_PLAYERS_TO_BATTLE = 10
38
-
39
- # Create a round of matches
40
- # each round is exactly 1 regation player vs 3 defense players for all regation players
41
- # each match is unique (player vs player)
42
- # each defense player is battle exactly 3 times
43
- def create_round(relegation_players, defense_players)
44
- matches = []
45
-
46
- relegation_players.each do |player_a|
47
- player_enemies = []
48
- MATCHS_PER_PLAYER.times do
49
- defender = nil
50
- while defender.nil? || player_enemies.include?(defender.id)
51
- defender = defense_players.sample
52
- end
53
-
54
- matches << [player_a, defender].shuffle
55
- player_enemies << defender.id
56
- end
57
- end
58
-
59
- matches
60
- end
61
-
62
- def battle(player_a, player_b)
63
- ActiveGenie::Battle.basic(
64
- player_a,
65
- player_b,
66
- @criteria,
67
- options: @options
68
- ).values_at('winner', 'loser')
69
- end
70
-
71
- def update_elo(winner, loser)
72
- return if winner.nil? || loser.nil?
73
-
74
- new_winner_elo, new_loser_elo = ActiveGenie::Utils::Math.calculate_new_elo(winner.elo, loser.elo)
75
-
76
- winner.elo = [new_winner_elo, max_defense_elo].min
77
- loser.elo = [new_loser_elo - LOSE_PENALTY, min_relegation_elo].max
78
- end
79
-
80
- def max_defense_elo
81
- @players.tier_defense.max_by(&:elo).elo
82
- end
83
-
84
- def min_relegation_elo
85
- @players.tier_relegation.min_by(&:elo).elo
86
- end
87
- end
88
- end
@@ -1,72 +0,0 @@
1
- require_relative './players_collection'
2
- require_relative './league'
3
- require_relative './elo_ranking'
4
- require_relative '../scoring/recommended_reviews'
5
-
6
- module ActiveGenie::Leaderboard
7
- class Leaderboard
8
- def self.call(param_players, criteria, options: {})
9
- new(param_players, criteria, options:).call
10
- end
11
-
12
- def initialize(param_players, criteria, options: {})
13
- @param_players = param_players
14
- @criteria = criteria
15
- @options = options
16
- end
17
-
18
- def call
19
- set_initial_score_players
20
- eliminate_obvious_bad_players
21
- run_elo_ranking if players.eligible_size > 10
22
- run_league
23
-
24
- players.to_h
25
- end
26
-
27
- private
28
-
29
- SCORE_VARIATION_THRESHOLD = 10
30
- MATCHS_PER_PLAYER = 3
31
-
32
- def set_initial_score_players
33
- players.each do |player|
34
- player.score = generate_score(player.content) # This can take a while, can be parallelized
35
- end
36
- end
37
-
38
- def generate_score(content)
39
- ActiveGenie::Scoring::Basic.call(content, @criteria, reviewers, options: @options)['final_score']
40
- end
41
-
42
- def eliminate_obvious_bad_players
43
- while players.coefficient_of_variation >= SCORE_VARIATION_THRESHOLD
44
- players.eligible.last.eliminated = 'too_low_score'
45
- end
46
- end
47
-
48
- def run_elo_ranking
49
- EloRanking.call(players, @criteria, options: @options)
50
- end
51
-
52
- def run_league
53
- League.call(players, @criteria, options: @options)
54
- end
55
-
56
- def reviewers
57
- [recommended_reviews['reviewer1'], recommended_reviews['reviewer2'], recommended_reviews['reviewer3']]
58
- end
59
-
60
- def recommended_reviews
61
- @recommended_reviews ||= ActiveGenie::Scoring::RecommendedReviews.call(
62
- players.sample,
63
- @criteria,
64
- options: @options
65
- )
66
- end
67
-
68
- def players
69
- @players ||= PlayersCollection.new(@param_players)
70
- end
71
- end
72
- end
@@ -1,48 +0,0 @@
1
- require_relative '../battle/basic'
2
-
3
- module ActiveGenie::Leaderboard
4
- class League
5
- def self.call(players, criteria, options: {})
6
- new(players, criteria, options:).call
7
- end
8
-
9
- def initialize(players, criteria, options: {})
10
- @players = players
11
- @criteria = criteria
12
- @options = options
13
- end
14
-
15
- def call
16
- matches.each do |player_a, player_b|
17
- winner, loser = battle(player_a, player_b)
18
-
19
- if winner.nil? || loser.nil?
20
- player_a.league[:draw] += 1
21
- player_b.league[:draw] += 1
22
- else
23
- winner.league[:win] += 1
24
- loser.league[:lose] += 1
25
- end
26
- end
27
-
28
- @players
29
- end
30
-
31
- private
32
-
33
- # TODO: reduce the number of matches based on transitivity.
34
- # For example, if A is better than B, and B is better than C, then A should clearly be better than C
35
- def matches
36
- @players.eligible.combination(2).to_a
37
- end
38
-
39
- def battle(player_a, player_b)
40
- ActiveGenie::Battle.basic(
41
- player_a,
42
- player_b,
43
- @criteria,
44
- options: @options
45
- ).values_at('winner', 'loser')
46
- end
47
- end
48
- end
@@ -1,52 +0,0 @@
1
- require 'securerandom'
2
-
3
- module ActiveGenie::Leaderboard
4
- class Player
5
- def initialize(params)
6
- params = { content: params } if params.is_a?(String)
7
-
8
- @id = params.dig(:id) || SecureRandom.uuid
9
- @content = params.dig(:content) || params
10
- @score = params.dig(:score) || nil
11
- @elo = params.dig(:elo) || nil
12
- @league = {
13
- win: params.dig(:league, :win) || 0,
14
- lose: params.dig(:league, :lose) || 0,
15
- draw: params.dig(:league, :draw) || 0
16
- }
17
- @eliminated = params.dig(:eliminated) || nil
18
- end
19
-
20
- attr_reader :id, :content, :score, :elo, :league, :eliminated
21
-
22
- def generate_elo_by_score
23
- return if !@elo.nil? || @score.nil?
24
-
25
- @elo = BASE_ELO + (@score - 50)
26
- end
27
-
28
- def score=(value)
29
- @score = value
30
- end
31
-
32
- def elo=(value)
33
- @elo = value
34
- end
35
-
36
- def eliminated=(value)
37
- @eliminated = value
38
- end
39
-
40
- def league_score
41
- @league[:win] * 3 + @league[:draw]
42
- end
43
-
44
- def to_h
45
- { id:, content:, score:, elo:, eliminated:, league: }
46
- end
47
-
48
- private
49
-
50
- BASE_ELO = 1000
51
- end
52
- end
@@ -1,11 +0,0 @@
1
- require_relative 'leaderboard/leaderboard'
2
-
3
- module ActiveGenie
4
- module Leaderboard
5
- module_function
6
-
7
- def call(...)
8
- Leaderboard.call(...)
9
- end
10
- end
11
- end
@@ -1,15 +0,0 @@
1
- module ActiveGenie::Utils
2
- module Math
3
- module_function
4
-
5
- def self.calculate_new_elo(winner, loser, k: 32)
6
- expected_score_a = 1 / (1 + 10**((loser - winner) / 400))
7
- expected_score_b = 1 - expected_score_a
8
-
9
- new_elo_winner = winner + k * (1 - expected_score_a)
10
- new_elo_loser = loser + k * (1 - expected_score_b)
11
-
12
- [new_elo_winner, new_elo_loser]
13
- end
14
- end
15
- end
@@ -1,7 +0,0 @@
1
- # gpt-4o-mini:
2
- # api_key: <%= ENV['OPENAI_API_KEY'] %>
3
- # provider: 'openai'
4
- #
5
- # claude-3-5-sonnet:
6
- # api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
7
- # provider: 'anthropic'