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.
- checksums.yaml +4 -4
- data/README.md +36 -72
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +2 -2
- data/lib/active_genie/battle/basic.rb +52 -56
- data/lib/active_genie/battle.rb +1 -1
- data/lib/active_genie/clients/openai_client.rb +119 -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 -5
- data/lib/active_genie/data_extractor/basic.rb +13 -13
- data/lib/active_genie/data_extractor/from_informal.rb +7 -7
- data/lib/active_genie/data_extractor.rb +1 -1
- data/lib/active_genie/logger.rb +72 -0
- data/lib/active_genie/ranking/README.md +43 -0
- data/lib/active_genie/ranking/elo_round.rb +113 -0
- data/lib/active_genie/ranking/free_for_all.rb +76 -0
- data/lib/active_genie/ranking/player.rb +97 -0
- data/lib/active_genie/{leaderboard → ranking}/players_collection.rb +18 -11
- data/lib/active_genie/ranking/ranking.rb +98 -0
- data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
- data/lib/active_genie/ranking.rb +12 -0
- data/lib/active_genie/scoring/README.md +4 -8
- data/lib/active_genie/scoring/basic.rb +58 -24
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +21 -12
- data/lib/active_genie/scoring.rb +4 -4
- data/lib/active_genie.rb +10 -18
- data/lib/tasks/install.rake +3 -3
- data/lib/tasks/templates/active_genie.rb +17 -0
- metadata +74 -90
- data/lib/active_genie/clients/openai.rb +0 -61
- data/lib/active_genie/clients/router.rb +0 -41
- data/lib/active_genie/leaderboard/elo_ranking.rb +0 -88
- data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
- data/lib/active_genie/leaderboard/league.rb +0 -48
- data/lib/active_genie/leaderboard/player.rb +0 -52
- 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
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.
|
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-
|
11
|
+
date: 2025-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: "# ActiveGenie \U0001F9DE♂️\n>
|
14
|
-
|
13
|
+
description: "# ActiveGenie \U0001F9DE♂️\n> The lodash for GenAI, stop reinventing
|
14
|
+
the wheel\n\n[](https://badge.fury.io/rb/active_genie)\n[](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
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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.
|
27
|
-
in `config/active_genie.
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
text
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
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
|
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###
|
72
|
-
|
73
|
-
|
74
|
-
to
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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/
|
125
|
-
- lib/active_genie/clients/
|
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/
|
132
|
-
- lib/active_genie/
|
133
|
-
- lib/active_genie/
|
134
|
-
- lib/active_genie/
|
135
|
-
- lib/active_genie/
|
136
|
-
- lib/active_genie/
|
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/
|
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.
|
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,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
|