active_genie 0.0.10 → 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 +17 -54
- data/VERSION +1 -1
- data/lib/active_genie/battle/basic.rb +49 -58
- data/lib/active_genie/clients/openai_client.rb +60 -18
- data/lib/active_genie/clients/unified_client.rb +2 -2
- data/lib/active_genie/configuration/openai_config.rb +9 -9
- data/lib/active_genie/data_extractor/README.md +0 -1
- data/lib/active_genie/data_extractor/basic.rb +8 -18
- data/lib/active_genie/data_extractor/from_informal.rb +4 -15
- data/lib/active_genie/logger.rb +34 -7
- data/lib/active_genie/{league → ranking}/README.md +7 -7
- 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/{league → 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 +1 -1
- data/lib/active_genie/scoring/basic.rb +55 -30
- data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +18 -7
- data/lib/active_genie/scoring.rb +3 -3
- data/lib/active_genie.rb +1 -1
- metadata +66 -87
- data/lib/active_genie/league/elo_ranking.rb +0 -121
- data/lib/active_genie/league/free_for_all.rb +0 -62
- data/lib/active_genie/league/league.rb +0 -120
- data/lib/active_genie/league/player.rb +0 -59
- data/lib/active_genie/league.rb +0 -12
@@ -25,15 +25,15 @@ module ActiveGenie::Scoring
|
|
25
25
|
# @return [Hash] The evaluation result containing the scores and reasoning
|
26
26
|
# @return [Number] :final_score The final score of the text based on the criteria and reviewers
|
27
27
|
# @return [String] :final_reasoning Detailed explanation of why the final score was reached
|
28
|
-
def self.call(
|
29
|
-
new(
|
28
|
+
def self.call(...)
|
29
|
+
new(...).call
|
30
30
|
end
|
31
31
|
|
32
32
|
def initialize(text, criteria, reviewers = [], config: {})
|
33
33
|
@text = text
|
34
34
|
@criteria = criteria
|
35
35
|
@reviewers = Array(reviewers).compact.uniq
|
36
|
-
@config = config
|
36
|
+
@config = ActiveGenie::Configuration.to_h(config)
|
37
37
|
end
|
38
38
|
|
39
39
|
def call
|
@@ -76,7 +76,12 @@ module ActiveGenie::Scoring
|
|
76
76
|
}
|
77
77
|
}
|
78
78
|
|
79
|
-
::ActiveGenie::Clients::UnifiedClient.function_calling(
|
79
|
+
::ActiveGenie::Clients::UnifiedClient.function_calling(
|
80
|
+
messages,
|
81
|
+
function,
|
82
|
+
model_tier: 'lower_tier',
|
83
|
+
config: @config
|
84
|
+
)
|
80
85
|
end
|
81
86
|
|
82
87
|
private
|
@@ -85,42 +90,62 @@ module ActiveGenie::Scoring
|
|
85
90
|
@get_or_recommend_reviewers ||= if @reviewers.count > 0
|
86
91
|
@reviewers
|
87
92
|
else
|
88
|
-
|
93
|
+
result = RecommendedReviewers.call(@text, @criteria, config: @config)
|
89
94
|
|
90
|
-
[
|
95
|
+
[result['reviewer1'], result['reviewer2'], result['reviewer3']]
|
91
96
|
end
|
92
97
|
end
|
93
98
|
|
94
99
|
PROMPT = <<~PROMPT
|
95
|
-
Evaluate and score the provided text based on predefined criteria,
|
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.
|
96
103
|
|
97
104
|
# Evaluation Process
|
98
|
-
1. **Analysis**: Thoroughly compare the text against each criterion to ensure comprehensive evaluation.
|
99
|
-
2. **Document Deviations**: Clearly identify and document any areas where the content does not align with the specified criteria.
|
100
|
-
3. **Highlight Strengths**: Emphasize notable features or elements that enhance the overall quality or effectiveness of the content.
|
101
|
-
4. **Identify Weaknesses**: Specify areas where the content fails to meet the criteria or where improvements could be made.
|
102
105
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
-
|
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.
|
108
131
|
|
109
132
|
# Guidelines
|
110
|
-
- Maintain objectivity, avoiding biases or preconceived notions.
|
111
|
-
- Deconstruct each criterion into actionable components for a systematic evaluation.
|
112
|
-
- If the text lacks information, apply reasonable judgment to assign a score while clearly explaining the rationale.
|
113
|
-
PROMPT
|
114
133
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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.
|
149
|
+
PROMPT
|
125
150
|
end
|
126
151
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
require_relative '../clients/unified_client'
|
4
4
|
|
5
5
|
module ActiveGenie::Scoring
|
6
|
-
# The
|
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,14 +11,14 @@ 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
|
-
#
|
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
|
20
|
-
def self.call(
|
21
|
-
new(
|
19
|
+
class RecommendedReviewers
|
20
|
+
def self.call(...)
|
21
|
+
new(...).call
|
22
22
|
end
|
23
23
|
|
24
24
|
# Initializes a new reviewer recommendation instance
|
@@ -29,7 +29,7 @@ module ActiveGenie::Scoring
|
|
29
29
|
def initialize(text, criteria, config: {})
|
30
30
|
@text = text
|
31
31
|
@criteria = criteria
|
32
|
-
@config = config
|
32
|
+
@config = ActiveGenie::Configuration.to_h(config)
|
33
33
|
end
|
34
34
|
|
35
35
|
def call
|
@@ -53,7 +53,14 @@ module ActiveGenie::Scoring
|
|
53
53
|
}
|
54
54
|
}
|
55
55
|
|
56
|
-
|
56
|
+
result = client.function_calling(
|
57
|
+
messages,
|
58
|
+
function,
|
59
|
+
model_tier: 'lower_tier',
|
60
|
+
config: @config
|
61
|
+
)
|
62
|
+
|
63
|
+
result
|
57
64
|
end
|
58
65
|
|
59
66
|
private
|
@@ -72,5 +79,9 @@ module ActiveGenie::Scoring
|
|
72
79
|
- Include reasoning for how each choice supports a thorough and insightful review.
|
73
80
|
- Avoid redundant or overly similar titles/roles to maintain diversity.
|
74
81
|
PROMPT
|
82
|
+
|
83
|
+
def client
|
84
|
+
::ActiveGenie::Clients::UnifiedClient
|
85
|
+
end
|
75
86
|
end
|
76
87
|
end
|
data/lib/active_genie/scoring.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require_relative 'scoring/basic'
|
2
|
-
require_relative 'scoring/
|
2
|
+
require_relative 'scoring/recommended_reviewers'
|
3
3
|
|
4
4
|
module ActiveGenie
|
5
5
|
# See the [Scoring README](lib/active_genie/scoring/README.md) for more information.
|
@@ -10,8 +10,8 @@ module ActiveGenie
|
|
10
10
|
Basic.call(...)
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
14
|
-
|
13
|
+
def recommended_reviewers(...)
|
14
|
+
RecommendedReviewers.call(...)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
data/lib/active_genie.rb
CHANGED
@@ -5,7 +5,7 @@ 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 :
|
8
|
+
autoload :Ranking, File.join(__dir__, 'active_genie/ranking')
|
9
9
|
|
10
10
|
class << self
|
11
11
|
def configure
|
metadata
CHANGED
@@ -1,57 +1,56 @@
|
|
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
|
-
Consistent rank
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
\
|
31
|
-
\
|
32
|
-
|
33
|
-
\
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
\
|
49
|
-
\
|
50
|
-
|
51
|
-
evaluation criteria\n\nSee the [Scoring README](lib/active_genie/scoring/README.md)
|
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
|
23
|
+
install\n```\n\n3. Generate the configuration:\n```shell\necho \"ActiveGenie.load_tasks\"
|
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#
|
45
|
+
=> {\n# algorithm_expert_score: 95,\n# algorithm_expert_reasoning: \"Accurately
|
46
|
+
describes binary search and its complexity\",\n# technical_writer_score: 90,\n#
|
47
|
+
\ technical_writer_reasoning: \"Clear and concise explanation of the algorithm\",\n#
|
48
|
+
\ final_score: 92.5\n# }\n```\n\nFeatures:\n- Multi-reviewer evaluation with
|
49
|
+
automatic expert selection\n- Detailed feedback with scoring reasoning\n- Customizable
|
50
|
+
reviewer weights\n- Flexible evaluation criteria\n\nSee the [Scoring README](lib/active_genie/scoring/README.md)
|
52
51
|
for advanced usage, custom reviewers, and detailed interface documentation.\n\n###
|
53
|
-
Battle\nAI-powered battle evaluation system that determines winners between
|
54
|
-
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
|
55
54
|
= \"Implementation uses dependency injection for better testability\"\nplayer_b
|
56
55
|
= \"Code has high test coverage but tightly coupled components\"\ncriteria = \"Evaluate
|
57
56
|
code quality and maintainability\"\n\nresult = ActiveGenie::Battle.call(player_a,
|
@@ -65,50 +64,29 @@ description: "# ActiveGenie \U0001F9DE♂️\n> Transform your Ruby applicati
|
|
65
64
|
evaluation with automatic expert selection\n- Detailed feedback with scoring reasoning\n-
|
66
65
|
Customizable reviewer weights\n- Flexible evaluation criteria\n\nSee the [Battle
|
67
66
|
README](lib/active_genie/battle/README.md) for advanced usage, custom reviewers,
|
68
|
-
and detailed interface documentation.\n\n###
|
67
|
+
and detailed interface documentation.\n\n### Data Ranking\nThe Ranking module provides
|
69
68
|
competitive ranking through multi-stage evaluation:\n\n\n```ruby\nrequire 'active_genie'\n\nplayers
|
70
69
|
= ['REST API', 'GraphQL API', 'SOAP API', 'gRPC API', 'Websocket API']\ncriteria
|
71
|
-
= \"Best one to be used into a high changing environment\"\n\nresult = ActiveGenie::
|
70
|
+
= \"Best one to be used into a high changing environment\"\n\nresult = ActiveGenie::Ranking.call(players,
|
72
71
|
criteria)\n# => {\n# winner_player: \"gRPC API\",\n# reasoning: \"gRPC
|
73
72
|
API is the best one to be used into a high changing environment\",\n# }\n```\n\n-
|
74
73
|
**Multi-phase ranking system** combining expert scoring and ELO algorithms\n- **Automatic
|
75
74
|
elimination** of inconsistent performers using statistical analysis\n- **Dynamic
|
76
75
|
ranking adjustments** based on simulated pairwise battles, from bottom to top\n\nSee
|
77
|
-
the [
|
78
|
-
configuration, and advanced ranking strategies.\n\n### Summarizer (
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
to translate a given text. It uses a set of rules to translate the text out of the
|
92
|
-
box. Uses the best practices of prompt engineering and engineering to make the translation
|
93
|
-
as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext = \"Example
|
94
|
-
text to be translated\"\ntranslated_text = ActiveGenie::Translator.call(text, from:
|
95
|
-
'en', to: 'pt')\nputs translated_text # => \"Exemplo de texto a ser traduzido\"\n```\n\n###
|
96
|
-
Sentiment analyzer (WIP)\nThe sentiment analyzer is a tool that can be used to analyze
|
97
|
-
the sentiment of a given text. It uses a set of rules to analyze the sentiment of
|
98
|
-
the text out of the box. Uses the best practices of prompt engineering and engineering
|
99
|
-
to make the sentiment analysis as accurate as possible.\n\n```ruby\nrequire 'active_genie'\n\ntext
|
100
|
-
= \"Example text to be analyzed\"\nsentiment = ActiveGenie::SentimentAnalyzer.call(text)\nputs
|
101
|
-
sentiment # => \"positive\"\n```\n\n## Configuration\n\n| Config | Description |
|
102
|
-
Default |\n|--------|-------------|---------|\n| `provider` | LLM provider (openai,
|
103
|
-
anthropic, etc) | `nil` |\n| `model` | Model to use | `nil` |\n| `api_key` | Provider
|
104
|
-
API key | `nil` |\n| `timeout` | Request timeout in seconds | `5` |\n| `max_retries`
|
105
|
-
| Maximum retry attempts | `3` |\n\n> **Note:** Each module can append its own set
|
106
|
-
of configuration, see the individual module documentation for details.\n\n## Contributing\n\n1.
|
107
|
-
Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3.
|
108
|
-
Commit your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch
|
109
|
-
(`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n## License\n\nThis
|
110
|
-
project is licensed under the MIT License - see the [LICENSE](LICENSE) file for
|
111
|
-
details.\n"
|
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"
|
112
90
|
email:
|
113
91
|
- radames@roriz.dev
|
114
92
|
executables: []
|
@@ -132,18 +110,19 @@ files:
|
|
132
110
|
- lib/active_genie/data_extractor/README.md
|
133
111
|
- lib/active_genie/data_extractor/basic.rb
|
134
112
|
- lib/active_genie/data_extractor/from_informal.rb
|
135
|
-
- lib/active_genie/league.rb
|
136
|
-
- lib/active_genie/league/README.md
|
137
|
-
- lib/active_genie/league/elo_ranking.rb
|
138
|
-
- lib/active_genie/league/free_for_all.rb
|
139
|
-
- lib/active_genie/league/league.rb
|
140
|
-
- lib/active_genie/league/player.rb
|
141
|
-
- lib/active_genie/league/players_collection.rb
|
142
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
|
143
122
|
- lib/active_genie/scoring.rb
|
144
123
|
- lib/active_genie/scoring/README.md
|
145
124
|
- lib/active_genie/scoring/basic.rb
|
146
|
-
- lib/active_genie/scoring/
|
125
|
+
- lib/active_genie/scoring/recommended_reviewers.rb
|
147
126
|
- lib/tasks/install.rake
|
148
127
|
- lib/tasks/templates/active_genie.rb
|
149
128
|
homepage: https://github.com/Roriz/active_genie
|
@@ -1,121 +0,0 @@
|
|
1
|
-
require_relative '../battle/basic'
|
2
|
-
|
3
|
-
module ActiveGenie::Leaderboard
|
4
|
-
class EloRanking
|
5
|
-
def self.call(players, criteria, config: {})
|
6
|
-
new(players, criteria, config:).call
|
7
|
-
end
|
8
|
-
|
9
|
-
def initialize(players, criteria, config: {})
|
10
|
-
@players = players
|
11
|
-
@criteria = criteria
|
12
|
-
@config = config
|
13
|
-
@start_time = Time.now
|
14
|
-
end
|
15
|
-
|
16
|
-
def call
|
17
|
-
@players.each(&:generate_elo_by_score)
|
18
|
-
|
19
|
-
round_count = 0
|
20
|
-
while @players.eligible_size > MINIMAL_PLAYERS_TO_BATTLE
|
21
|
-
round = create_round(@players.tier_relegation, @players.tier_defense)
|
22
|
-
|
23
|
-
round.each do |player_a, player_b|
|
24
|
-
winner, loser = battle(player_a, player_b) # This can take a while, can be parallelized
|
25
|
-
update_elo(winner, loser)
|
26
|
-
ActiveGenie::Logger.trace({ **log, step: :elo_battle, winner_id: winner.id, loser_id: loser.id, winner_elo: winner.elo, loser_elo: loser.elo })
|
27
|
-
end
|
28
|
-
|
29
|
-
eliminate_all_relegation_players
|
30
|
-
round_count += 1
|
31
|
-
end
|
32
|
-
|
33
|
-
ActiveGenie::Logger.info({ **log, step: :elo_end, round_count:, eligible_size: @players.eligible_size })
|
34
|
-
@players
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
MATCHS_PER_PLAYER = 3
|
40
|
-
LOSE_PENALTY = 15
|
41
|
-
MINIMAL_PLAYERS_TO_BATTLE = 10
|
42
|
-
K = 32
|
43
|
-
|
44
|
-
# Create a round of matches
|
45
|
-
# each round is exactly 1 regation player vs 3 defense players for all regation players
|
46
|
-
# each match is unique (player vs player)
|
47
|
-
# each defense player is battle exactly 3 times
|
48
|
-
def create_round(relegation_players, defense_players)
|
49
|
-
matches = []
|
50
|
-
|
51
|
-
relegation_players.each do |player_a|
|
52
|
-
player_enemies = []
|
53
|
-
MATCHS_PER_PLAYER.times do
|
54
|
-
defender = nil
|
55
|
-
while defender.nil? || player_enemies.include?(defender.id)
|
56
|
-
defender = defense_players.sample
|
57
|
-
end
|
58
|
-
|
59
|
-
matches << [player_a, defender].shuffle
|
60
|
-
player_enemies << defender.id
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
matches
|
65
|
-
end
|
66
|
-
|
67
|
-
def battle(player_a, player_b)
|
68
|
-
ActiveGenie::Battle.basic(
|
69
|
-
player_a,
|
70
|
-
player_b,
|
71
|
-
@criteria,
|
72
|
-
config:
|
73
|
-
).values_at('winner', 'loser')
|
74
|
-
end
|
75
|
-
|
76
|
-
def update_elo(winner, loser)
|
77
|
-
return if winner.nil? || loser.nil?
|
78
|
-
|
79
|
-
new_winner_elo, new_loser_elo = calculate_new_elo(winner.elo, loser.elo)
|
80
|
-
|
81
|
-
winner.elo = [new_winner_elo, max_defense_elo].min
|
82
|
-
loser.elo = [new_loser_elo - LOSE_PENALTY, min_relegation_elo].max
|
83
|
-
end
|
84
|
-
|
85
|
-
def max_defense_elo
|
86
|
-
@players.tier_defense.max_by(&:elo).elo
|
87
|
-
end
|
88
|
-
|
89
|
-
def min_relegation_elo
|
90
|
-
@players.tier_relegation.min_by(&:elo).elo
|
91
|
-
end
|
92
|
-
|
93
|
-
# Read more about the formula on https://en.wikipedia.org/wiki/Elo_rating_system
|
94
|
-
def calculate_new_elo(winner_elo, loser_elo)
|
95
|
-
expected_score_a = 1 / (1 + 10**((loser_elo - winner_elo) / 400))
|
96
|
-
expected_score_b = 1 - expected_score_a
|
97
|
-
|
98
|
-
new_elo_winner = winner_elo + K * (1 - expected_score_a)
|
99
|
-
new_elo_loser = loser_elo + K * (1 - expected_score_b)
|
100
|
-
|
101
|
-
[new_elo_winner, new_elo_loser]
|
102
|
-
end
|
103
|
-
|
104
|
-
def eliminate_all_relegation_players
|
105
|
-
eliminations = @players.tier_relegation.size
|
106
|
-
@players.tier_relegation.each { |player| player.eliminated = 'tier_relegation' }
|
107
|
-
ActiveGenie::Logger.trace({ **log, step: :elo_round, eligible_size: @players.eligible_size, eliminations: })
|
108
|
-
end
|
109
|
-
|
110
|
-
def config
|
111
|
-
{ **@config }
|
112
|
-
end
|
113
|
-
|
114
|
-
def log
|
115
|
-
{
|
116
|
-
**(@config.dig(:log) || {}),
|
117
|
-
duration: Time.now - @start_time
|
118
|
-
}
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
require_relative '../battle/basic'
|
2
|
-
|
3
|
-
module ActiveGenie::Leaderboard
|
4
|
-
class FreeForAll
|
5
|
-
def self.call(players, criteria, config: {})
|
6
|
-
new(players, criteria, config:).call
|
7
|
-
end
|
8
|
-
|
9
|
-
def initialize(players, criteria, config: {})
|
10
|
-
@players = players
|
11
|
-
@criteria = criteria
|
12
|
-
@config = config
|
13
|
-
@start_time = Time.now
|
14
|
-
end
|
15
|
-
|
16
|
-
def call
|
17
|
-
matches.each do |player_a, player_b|
|
18
|
-
winner, loser = battle(player_a, player_b)
|
19
|
-
|
20
|
-
if winner.nil? || loser.nil?
|
21
|
-
player_a.free_for_all[:draw] += 1
|
22
|
-
player_b.free_for_all[:draw] += 1
|
23
|
-
else
|
24
|
-
winner.free_for_all[:win] += 1
|
25
|
-
loser.free_for_all[:lose] += 1
|
26
|
-
end
|
27
|
-
|
28
|
-
ActiveGenie::Logger.trace({**log, step: :free_for_all_battle, winner_id: winner&.id, player_a_id: player_a.id, player_a_free_for_all_score: player_a.free_for_all_score, player_b_id: player_b.id, player_b_free_for_all_score: player_b.free_for_all_score })
|
29
|
-
end
|
30
|
-
|
31
|
-
@players
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
# TODO: reduce the number of matches based on transitivity.
|
37
|
-
# For example, if A is better than B, and B is better than C, then A should clearly be better than C
|
38
|
-
def matches
|
39
|
-
@players.eligible.combination(2).to_a
|
40
|
-
end
|
41
|
-
|
42
|
-
def battle(player_a, player_b)
|
43
|
-
result = ActiveGenie::Battle.basic(
|
44
|
-
player_a,
|
45
|
-
player_b,
|
46
|
-
@criteria,
|
47
|
-
config:
|
48
|
-
)
|
49
|
-
|
50
|
-
|
51
|
-
result.values_at('winner', 'loser')
|
52
|
-
end
|
53
|
-
|
54
|
-
def config
|
55
|
-
{ **@config }
|
56
|
-
end
|
57
|
-
|
58
|
-
def log
|
59
|
-
{ **(@config.dig(:log) || {}), duration: Time.now - @start_time }
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|