active_genie 0.29.1 → 0.30.1
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 +1 -1
- data/VERSION +1 -1
- data/lib/active_genie/{battle/generalist.json → comparator/debate.json} +2 -2
- data/lib/active_genie/{battle/generalist.prompt.md → comparator/debate.prompt.md} +1 -1
- data/lib/active_genie/{battle/generalist.rb → comparator/debate.rb} +20 -21
- data/lib/active_genie/{battle → comparator}/fight.rb +7 -7
- data/lib/active_genie/comparator.rb +24 -0
- data/lib/active_genie/{config/scoring_config.rb → configs/comparator_config.rb} +1 -1
- data/lib/active_genie/{config/data_extractor_config.rb → configs/extractor_config.rb} +1 -1
- data/lib/active_genie/{config/factory_config.rb → configs/lister_config.rb} +1 -1
- data/lib/active_genie/{config → configs}/llm_config.rb +7 -6
- data/lib/active_genie/{config → configs}/providers_config.rb +1 -1
- data/lib/active_genie/{config/ranking_config.rb → configs/ranker_config.rb} +1 -1
- data/lib/active_genie/{config/battle_config.rb → configs/scorer_config.rb} +1 -1
- data/lib/active_genie/configuration.rb +19 -19
- data/lib/active_genie/errors/invalid_provider_error.rb +1 -1
- data/lib/active_genie/{data_extractor/generalist.rb → extractor/explanation.rb} +11 -11
- data/lib/active_genie/{data_extractor/from_informal.rb → extractor/litote.rb} +8 -8
- data/lib/active_genie/extractor.rb +22 -0
- data/lib/active_genie/{factory → lister}/feud.rb +8 -8
- data/lib/active_genie/lister/juries.prompt.md +20 -0
- data/lib/active_genie/lister/juries.rb +82 -0
- data/lib/active_genie/{factory.rb → lister.rb} +7 -6
- data/lib/active_genie/{clients/providers/anthropic_client.rb → providers/anthropic_provider.rb} +4 -4
- data/lib/active_genie/{clients/providers/base_client.rb → providers/base_provider.rb} +14 -7
- data/lib/active_genie/{clients/providers/deepseek_client.rb → providers/deepseek_provider.rb} +3 -3
- data/lib/active_genie/{clients/providers/google_client.rb → providers/google_provider.rb} +6 -6
- data/lib/active_genie/{clients/providers/openai_client.rb → providers/openai_provider.rb} +3 -3
- data/lib/active_genie/providers/unified_provider.rb +44 -0
- data/lib/active_genie/{ranking/elo_round.rb → ranker/elo.rb} +37 -36
- data/lib/active_genie/ranker/entities/player.rb +124 -0
- data/lib/active_genie/ranker/entities/players.rb +102 -0
- data/lib/active_genie/{ranking → ranker}/free_for_all.rb +9 -9
- data/lib/active_genie/ranker/scoring.rb +68 -0
- data/lib/active_genie/{ranking/ranking.rb → ranker/tournament.rb} +22 -31
- data/lib/active_genie/ranker.rb +32 -0
- data/lib/active_genie/scorer/jury_bench.rb +121 -0
- data/lib/active_genie/scorer.rb +17 -0
- data/lib/active_genie/utils/fiber_by_batch.rb +21 -0
- data/lib/active_genie.rb +9 -9
- data/lib/tasks/test.rake +4 -0
- metadata +67 -52
- data/lib/active_genie/battle.rb +0 -31
- data/lib/active_genie/clients/unified_client.rb +0 -50
- data/lib/active_genie/data_extractor.rb +0 -23
- data/lib/active_genie/ranking/player.rb +0 -122
- data/lib/active_genie/ranking/players_collection.rb +0 -95
- data/lib/active_genie/ranking/ranking_scoring.rb +0 -69
- data/lib/active_genie/ranking.rb +0 -14
- data/lib/active_genie/scoring/generalist.json +0 -9
- data/lib/active_genie/scoring/generalist.rb +0 -119
- data/lib/active_genie/scoring/recommended_reviewers.rb +0 -87
- data/lib/active_genie/scoring.rb +0 -23
- /data/lib/active_genie/{battle → comparator}/fight.json +0 -0
- /data/lib/active_genie/{battle → comparator}/fight.prompt.md +0 -0
- /data/lib/active_genie/{config → configs}/log_config.rb +0 -0
- /data/lib/active_genie/{config → configs}/providers/anthropic_config.rb +0 -0
- /data/lib/active_genie/{config → configs}/providers/deepseek_config.rb +0 -0
- /data/lib/active_genie/{config → configs}/providers/google_config.rb +0 -0
- /data/lib/active_genie/{config → configs}/providers/openai_config.rb +0 -0
- /data/lib/active_genie/{config → configs}/providers/provider_base.rb +0 -0
- /data/lib/active_genie/{data_extractor/generalist.json → extractor/explanation.json} +0 -0
- /data/lib/active_genie/{data_extractor/generalist.prompt.md → extractor/explanation.prompt.md} +0 -0
- /data/lib/active_genie/{data_extractor/from_informal.json → extractor/litote.json} +0 -0
- /data/lib/active_genie/{factory → lister}/feud.json +0 -0
- /data/lib/active_genie/{factory → lister}/feud.prompt.md +0 -0
- /data/lib/active_genie/{scoring/generalist.prompt.md → scorer/jury_bench.prompt.md} +0 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../providers/unified_provider'
|
4
|
+
|
5
|
+
module ActiveGenie
|
6
|
+
module Lister
|
7
|
+
# The Juries class intelligently suggests appropriate jury roles
|
8
|
+
# for evaluating text content based on specific criteria. It uses AI to analyze
|
9
|
+
# the content and criteria to identify the most suitable subject matter experts.
|
10
|
+
#
|
11
|
+
# The class ensures a balanced and comprehensive review process by recommending
|
12
|
+
# three distinct jury roles with complementary expertise and perspectives.
|
13
|
+
#
|
14
|
+
# @example Getting jury for technical content
|
15
|
+
# Juries.call("Technical documentation about API design",
|
16
|
+
# "Evaluate technical accuracy and clarity")
|
17
|
+
# # => { jury1: "API Architect", jury2: "Technical Writer",
|
18
|
+
# # jury3: "Developer Advocate", reasoning: "..." }
|
19
|
+
#
|
20
|
+
class Juries
|
21
|
+
def self.call(...)
|
22
|
+
new(...).call
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initializes a new jury recommendation instance
|
26
|
+
#
|
27
|
+
# @param text [String] The text content to analyze for jury recommendations
|
28
|
+
# @param criteria [String] The evaluation criteria that will guide jury selection
|
29
|
+
# @param config [Hash] Additional configuration config that modify the recommendation process
|
30
|
+
def initialize(text, criteria, config: {})
|
31
|
+
@text = text
|
32
|
+
@criteria = criteria
|
33
|
+
@config = ActiveGenie.configuration.merge(config)
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
messages = [
|
38
|
+
{ role: 'system', content: prompt },
|
39
|
+
{ role: 'user', content: "<criteria> #{@criteria}</criteria>" },
|
40
|
+
{ role: 'user', content: "<text-to-score>#{@text}</text-to-score>" }
|
41
|
+
]
|
42
|
+
|
43
|
+
function = {
|
44
|
+
name: 'identify_jury',
|
45
|
+
description: 'Discover a list of juries based on the text and given criteria.',
|
46
|
+
parameters: {
|
47
|
+
type: 'object',
|
48
|
+
properties: {
|
49
|
+
reasoning: { type: 'string' },
|
50
|
+
juries: {
|
51
|
+
type: 'array',
|
52
|
+
description: 'The list of best juries',
|
53
|
+
items: {
|
54
|
+
type: 'string'
|
55
|
+
}
|
56
|
+
}
|
57
|
+
},
|
58
|
+
required: %w[reasoning juries]
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
result = client.function_calling(
|
63
|
+
messages,
|
64
|
+
function,
|
65
|
+
config: @config
|
66
|
+
)
|
67
|
+
|
68
|
+
result['juries'] || []
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def client
|
74
|
+
::ActiveGenie::Providers::UnifiedProvider
|
75
|
+
end
|
76
|
+
|
77
|
+
def prompt
|
78
|
+
@prompt ||= File.read(File.join(__dir__, 'juries.prompt.md'))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -1,21 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'lister/feud'
|
4
|
+
require_relative 'lister/juries'
|
4
5
|
|
5
6
|
module ActiveGenie
|
6
|
-
module
|
7
|
+
module Lister
|
7
8
|
module_function
|
8
9
|
|
9
|
-
def
|
10
|
+
def call(...)
|
10
11
|
Feud.call(...)
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
+
def with_feud(...)
|
14
15
|
Feud.call(...)
|
15
16
|
end
|
16
17
|
|
17
|
-
def
|
18
|
-
|
18
|
+
def with_juries(...)
|
19
|
+
Juries.call(...)
|
19
20
|
end
|
20
21
|
end
|
21
22
|
end
|
data/lib/active_genie/{clients/providers/anthropic_client.rb → providers/anthropic_provider.rb}
RENAMED
@@ -3,12 +3,12 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'net/http'
|
5
5
|
require 'uri'
|
6
|
-
require_relative '
|
6
|
+
require_relative 'base_provider'
|
7
7
|
|
8
8
|
module ActiveGenie
|
9
|
-
module
|
10
|
-
#
|
11
|
-
class
|
9
|
+
module Providers
|
10
|
+
# Provider for interacting with the Anthropic (Claude) API with json response
|
11
|
+
class AnthropicProvider < BaseProvider
|
12
12
|
# Requests structured JSON output from the Anthropic Claude model based on a schema.
|
13
13
|
#
|
14
14
|
# @param messages [Array<Hash>] A list of messages representing the conversation history.
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'net/http'
|
4
|
+
|
3
5
|
module ActiveGenie
|
4
|
-
module
|
5
|
-
class
|
6
|
-
class
|
6
|
+
module Providers
|
7
|
+
class BaseProvider
|
8
|
+
class ProviderUnknownError < StandardError; end
|
9
|
+
class ProviderServerError < StandardError; end
|
7
10
|
|
8
11
|
DEFAULT_HEADERS = {
|
9
12
|
'Content-Type': 'application/json',
|
@@ -81,7 +84,7 @@ module ActiveGenie
|
|
81
84
|
|
82
85
|
response = http_request(request, uri)
|
83
86
|
|
84
|
-
raise
|
87
|
+
raise ProviderUnknownError, "Unexpected response: #{response.code} - #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
85
88
|
|
86
89
|
parsed_response = parse_response(response)
|
87
90
|
|
@@ -143,7 +146,7 @@ module ActiveGenie
|
|
143
146
|
begin
|
144
147
|
JSON.parse(response.body)
|
145
148
|
rescue JSON::ParserError => e
|
146
|
-
raise
|
149
|
+
raise ProviderUnknownError, "Failed to parse JSON response: #{e.message}"
|
147
150
|
end
|
148
151
|
end
|
149
152
|
|
@@ -169,8 +172,12 @@ module ActiveGenie
|
|
169
172
|
retries = 0
|
170
173
|
|
171
174
|
begin
|
172
|
-
yield
|
173
|
-
|
175
|
+
response = yield
|
176
|
+
|
177
|
+
raise ProviderServerError, "Provider server error: #{response.code} - #{response.body}" if !response.is_a?(Net::HTTPSuccess) && response.code.to_i >= 500
|
178
|
+
|
179
|
+
response
|
180
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, ProviderServerError => e
|
174
181
|
raise if retries > max_retries
|
175
182
|
|
176
183
|
sleep_time = retry_delay * (2**retries)
|
data/lib/active_genie/{clients/providers/deepseek_client.rb → providers/deepseek_provider.rb}
RENAMED
@@ -3,11 +3,11 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'net/http'
|
5
5
|
|
6
|
-
require_relative '
|
6
|
+
require_relative 'base_provider'
|
7
7
|
|
8
8
|
module ActiveGenie
|
9
|
-
module
|
10
|
-
class
|
9
|
+
module Providers
|
10
|
+
class DeepseekProvider < BaseProvider
|
11
11
|
class InvalidResponseError < StandardError; end
|
12
12
|
|
13
13
|
# Requests structured JSON output from the Deepseek model based on a schema.
|
@@ -3,12 +3,12 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'net/http'
|
5
5
|
require 'uri'
|
6
|
-
require_relative '
|
6
|
+
require_relative 'base_provider'
|
7
7
|
|
8
8
|
module ActiveGenie
|
9
|
-
module
|
10
|
-
#
|
11
|
-
class
|
9
|
+
module Providers
|
10
|
+
# Provider for interacting with the Google Generative Language API.
|
11
|
+
class GoogleProvider < BaseProvider
|
12
12
|
# Requests structured JSON output from the Google Generative Language model based on a schema.
|
13
13
|
#
|
14
14
|
# @param messages [Array<Hash>] A list of messages representing the conversation history.
|
@@ -94,9 +94,9 @@ module ActiveGenie
|
|
94
94
|
json_instruction = <<~PROMPT
|
95
95
|
Generate a JSON object that strictly adheres to the following JSON schema:
|
96
96
|
|
97
|
-
|
97
|
+
<json_schema>
|
98
98
|
#{JSON.pretty_generate(function_schema[:parameters])}
|
99
|
-
|
99
|
+
</json_schema>
|
100
100
|
|
101
101
|
IMPORTANT: Only output the raw JSON object. Do not include any other text, explanations, or markdown formatting like ```json ... ``` wrappers around the final output.
|
102
102
|
PROMPT
|
@@ -3,11 +3,11 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'net/http'
|
5
5
|
|
6
|
-
require_relative '
|
6
|
+
require_relative 'base_provider'
|
7
7
|
|
8
8
|
module ActiveGenie
|
9
|
-
module
|
10
|
-
class
|
9
|
+
module Providers
|
10
|
+
class OpenaiProvider < BaseProvider
|
11
11
|
class InvalidResponseError < StandardError; end
|
12
12
|
|
13
13
|
# Requests structured JSON output from the OpenAI model based on a schema.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'openai_provider'
|
4
|
+
require_relative 'anthropic_provider'
|
5
|
+
require_relative 'google_provider'
|
6
|
+
require_relative 'deepseek_provider'
|
7
|
+
require_relative '../errors/invalid_provider_error'
|
8
|
+
|
9
|
+
module ActiveGenie
|
10
|
+
module Providers
|
11
|
+
class UnifiedProvider
|
12
|
+
class << self
|
13
|
+
PROVIDER_NAME_TO_PROVIDER = {
|
14
|
+
openai: OpenaiProvider,
|
15
|
+
anthropic: AnthropicProvider,
|
16
|
+
google: GoogleProvider,
|
17
|
+
deepseek: DeepseekProvider
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def function_calling(messages, function, config: {})
|
21
|
+
provider_name = config.llm.provider_name || config.providers.default
|
22
|
+
provider = PROVIDER_NAME_TO_PROVIDER[provider_name.to_sym]
|
23
|
+
|
24
|
+
raise ActiveGenie::InvalidProviderError, provider_name if provider.nil?
|
25
|
+
|
26
|
+
response = provider.new(config).function_calling(messages, function)
|
27
|
+
|
28
|
+
normalize_response(response)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def normalize_response(response)
|
34
|
+
response.each do |key, value|
|
35
|
+
response[key] = nil if ['null', 'none', 'undefined', '', 'unknown',
|
36
|
+
'<unknown>'].include?(value.to_s.strip.downcase)
|
37
|
+
end
|
38
|
+
|
39
|
+
response
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,38 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveGenie
|
4
|
-
module
|
5
|
-
class
|
4
|
+
module Ranker
|
5
|
+
class Elo
|
6
6
|
def self.call(...)
|
7
7
|
new(...).call
|
8
8
|
end
|
9
9
|
|
10
10
|
def initialize(players, criteria, config: nil)
|
11
11
|
@players = players
|
12
|
-
@
|
13
|
-
@
|
12
|
+
@higher_tier = players.calc_higher_tier
|
13
|
+
@lower_tier = players.calc_lower_tier
|
14
14
|
@criteria = criteria
|
15
15
|
@config = ActiveGenie.configuration.merge(config)
|
16
|
-
@
|
16
|
+
@tmp_highers = []
|
17
17
|
@total_tokens = 0
|
18
18
|
@previous_elo = @players.to_h { |player| [player.id, player.elo] }
|
19
|
-
@previous_highest_elo = @
|
19
|
+
@previous_highest_elo = @higher_tier.max_by(&:elo).elo
|
20
20
|
end
|
21
21
|
|
22
22
|
def call
|
23
23
|
@config.log.add_observer(observers: ->(log) { log_observer(log) })
|
24
|
-
@config.log.additional_context = {
|
24
|
+
@config.log.additional_context = { elo_id: }
|
25
|
+
|
26
|
+
ActiveGenie::FiberByBatch.call(matches, config: @config) do |player_a, player_b|
|
27
|
+
winner, loser = debate(player_a, player_b)
|
25
28
|
|
26
|
-
matches.each do |player_a, player_b|
|
27
|
-
# TODO: battle can take a while, can be parallelized
|
28
|
-
winner, loser = battle(player_a, player_b)
|
29
29
|
update_players_elo(winner, loser)
|
30
30
|
end
|
31
31
|
|
32
32
|
build_report
|
33
33
|
end
|
34
34
|
|
35
|
-
|
35
|
+
DEBATE_PER_PLAYER = 3
|
36
36
|
K = 32
|
37
37
|
|
38
38
|
private
|
@@ -40,26 +40,26 @@ module ActiveGenie
|
|
40
40
|
def matches
|
41
41
|
match_keys = {}
|
42
42
|
|
43
|
-
@
|
44
|
-
|
45
|
-
|
43
|
+
@higher_tier.each_with_object([]) do |attack_player, matches|
|
44
|
+
DEBATE_PER_PLAYER.times do
|
45
|
+
higher_player = next_higher_player
|
46
46
|
|
47
|
-
next if match_keys["#{attack_player.id}_#{
|
47
|
+
next if match_keys["#{attack_player.id}_#{higher_player.id}"]
|
48
48
|
|
49
|
-
match_keys["#{attack_player.id}_#{
|
50
|
-
matches << [attack_player,
|
49
|
+
match_keys["#{attack_player.id}_#{higher_player.id}"] = true
|
50
|
+
matches << [attack_player, higher_player]
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
-
def
|
56
|
-
@
|
55
|
+
def next_higher_player
|
56
|
+
@tmp_highers = @higher_tier.shuffle if @tmp_highers.empty?
|
57
57
|
|
58
|
-
@
|
58
|
+
@tmp_highers.pop
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
62
|
-
result = ActiveGenie::
|
61
|
+
def debate(player_a, player_b)
|
62
|
+
result = ActiveGenie::Comparator.by_debate(
|
63
63
|
player_a.content,
|
64
64
|
player_b.content,
|
65
65
|
@criteria,
|
@@ -89,43 +89,44 @@ module ActiveGenie
|
|
89
89
|
player_rating + (K * (score - expected_score)).round
|
90
90
|
end
|
91
91
|
|
92
|
-
def
|
93
|
-
@
|
94
|
-
|
95
|
-
|
92
|
+
def elo_id
|
93
|
+
@elo_id ||= begin
|
94
|
+
higher_tier_ids = @higher_tier.map(&:id).join(',')
|
95
|
+
lower_tier_ids = @lower_tier.map(&:id).join(',')
|
96
96
|
|
97
|
-
|
98
|
-
Digest::MD5.hexdigest(
|
97
|
+
ranker_unique_key = [higher_tier_ids, lower_tier_ids, @criteria, @config.to_json].join('-')
|
98
|
+
Digest::MD5.hexdigest(ranker_unique_key)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def build_report
|
103
103
|
report = {
|
104
|
-
|
105
|
-
|
106
|
-
|
104
|
+
elo_id:,
|
105
|
+
players_in: players_in.map(&:id),
|
106
|
+
debates_count: matches.size,
|
107
107
|
total_tokens: @total_tokens,
|
108
|
+
players_in_round: players_in.map(&:id),
|
108
109
|
previous_highest_elo: @previous_highest_elo,
|
109
110
|
highest_elo:,
|
110
111
|
highest_elo_diff: highest_elo - @previous_highest_elo,
|
111
112
|
players_elo_diff:
|
112
113
|
}
|
113
114
|
|
114
|
-
@config.logger.call({
|
115
|
+
@config.logger.call({ elo_id:, code: :elo_report, **report })
|
115
116
|
|
116
117
|
report
|
117
118
|
end
|
118
119
|
|
119
|
-
def
|
120
|
-
@
|
120
|
+
def players_in
|
121
|
+
@lower_tier + @higher_tier
|
121
122
|
end
|
122
123
|
|
123
124
|
def highest_elo
|
124
|
-
|
125
|
+
players_in.max_by(&:elo).elo
|
125
126
|
end
|
126
127
|
|
127
128
|
def players_elo_diff
|
128
|
-
elo_diffs =
|
129
|
+
elo_diffs = players_in.map do |player|
|
129
130
|
[player.id, player.elo - @previous_elo[player.id]]
|
130
131
|
end
|
131
132
|
elo_diffs.sort_by { |_, diff| -(diff || 0) }.to_h
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module ActiveGenie
|
6
|
+
module Ranker
|
7
|
+
module Entities
|
8
|
+
class Player
|
9
|
+
def initialize(params)
|
10
|
+
@params = params.is_a?(String) ? { content: params } : params.dup
|
11
|
+
@params[:content] ||= @params
|
12
|
+
end
|
13
|
+
|
14
|
+
def content
|
15
|
+
@content ||= @params[:content]
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
@name ||= @params[:name] || content[0..10]
|
20
|
+
end
|
21
|
+
|
22
|
+
def id
|
23
|
+
@id ||= @params[:id] || Digest::MD5.hexdigest(content.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def score
|
27
|
+
@score ||= @params[:score]
|
28
|
+
end
|
29
|
+
|
30
|
+
def elo
|
31
|
+
@elo = @elo || @params[:elo] || generate_elo_by_score
|
32
|
+
end
|
33
|
+
|
34
|
+
def ffa_win_count
|
35
|
+
@ffa_win_count ||= @params[:ffa_win_count] || 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def ffa_lose_count
|
39
|
+
@ffa_lose_count ||= @params[:ffa_lose_count] || 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def ffa_draw_count
|
43
|
+
@ffa_draw_count ||= @params[:ffa_draw_count] || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def eliminated
|
47
|
+
@eliminated ||= @params[:eliminated]
|
48
|
+
end
|
49
|
+
|
50
|
+
def score=(value)
|
51
|
+
@score = value
|
52
|
+
@elo = generate_elo_by_score
|
53
|
+
end
|
54
|
+
|
55
|
+
def elo=(value)
|
56
|
+
@elo = value || BASE_ELO
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_writer :eliminated
|
60
|
+
|
61
|
+
def draw!
|
62
|
+
@ffa_draw_count = ffa_draw_count + 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def win!
|
66
|
+
@ffa_win_count = ffa_win_count + 1
|
67
|
+
end
|
68
|
+
|
69
|
+
def lose!
|
70
|
+
@ffa_lose_count = ffa_lose_count + 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def ffa_score
|
74
|
+
(ffa_win_count * 3) + ffa_draw_count
|
75
|
+
end
|
76
|
+
|
77
|
+
def sort_value
|
78
|
+
(ffa_score * 1_000_000) + ((elo || 0) * 100) + (score || 0)
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_json(*_args)
|
82
|
+
to_h.to_json
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_h
|
86
|
+
{
|
87
|
+
id:, name:, content:,
|
88
|
+
|
89
|
+
score:, elo:,
|
90
|
+
ffa_win_count:, ffa_lose_count:, ffa_draw_count:,
|
91
|
+
eliminated:, ffa_score:, sort_value:
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_missing(method_name, *args, &)
|
96
|
+
if method_name == :[] && args.size == 1
|
97
|
+
attr_name = args.first.to_sym
|
98
|
+
|
99
|
+
return send(attr_name) if respond_to?(attr_name)
|
100
|
+
|
101
|
+
return nil
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
super
|
106
|
+
end
|
107
|
+
|
108
|
+
def respond_to_missing?(method_name, include_private = false)
|
109
|
+
method_name == :[] || super
|
110
|
+
end
|
111
|
+
|
112
|
+
BASE_ELO = 1000
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def generate_elo_by_score
|
117
|
+
return BASE_ELO if @score.nil?
|
118
|
+
|
119
|
+
BASE_ELO + (@score - 50)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'player'
|
4
|
+
|
5
|
+
module ActiveGenie
|
6
|
+
module Ranker
|
7
|
+
module Entities
|
8
|
+
class Players
|
9
|
+
def initialize(players)
|
10
|
+
@players = if players.is_a?(Players)
|
11
|
+
players.players
|
12
|
+
else
|
13
|
+
build(players)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :players
|
18
|
+
|
19
|
+
def coefficient_of_variation
|
20
|
+
mean = score_mean
|
21
|
+
|
22
|
+
return 0 if mean.zero?
|
23
|
+
|
24
|
+
variance = all_scores.map { |num| (num - mean)**2 }.sum / all_scores.size
|
25
|
+
standard_deviation = Math.sqrt(variance)
|
26
|
+
|
27
|
+
(standard_deviation / mean) * 100
|
28
|
+
end
|
29
|
+
|
30
|
+
def all_scores
|
31
|
+
eligible.map(&:score).compact
|
32
|
+
end
|
33
|
+
|
34
|
+
def score_mean
|
35
|
+
return 0 if all_scores.empty?
|
36
|
+
|
37
|
+
all_scores.sum.to_f / all_scores.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def calc_higher_tier
|
41
|
+
eligible[(tier_size * -1)..]
|
42
|
+
end
|
43
|
+
|
44
|
+
def calc_lower_tier
|
45
|
+
eligible[(tier_size * -2)...(tier_size * -1)]
|
46
|
+
end
|
47
|
+
|
48
|
+
def eligible
|
49
|
+
sorted.reject(&:eliminated)
|
50
|
+
end
|
51
|
+
|
52
|
+
def eligible_size
|
53
|
+
@players.reject(&:eliminated).size
|
54
|
+
end
|
55
|
+
|
56
|
+
def elo_eligible?
|
57
|
+
eligible.size > 15
|
58
|
+
end
|
59
|
+
|
60
|
+
def sorted
|
61
|
+
@players.sort_by { |p| -p.sort_value }
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_json(*_args)
|
65
|
+
@players.map(&:to_h).to_json
|
66
|
+
end
|
67
|
+
|
68
|
+
def method_missing(...)
|
69
|
+
@players.send(...)
|
70
|
+
end
|
71
|
+
|
72
|
+
def respond_to_missing?(method_name, include_private = false)
|
73
|
+
@players.respond_to?(method_name, include_private)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def build(param_players)
|
79
|
+
param_players.map { |p| Player.new(p) }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the number of players to debate in each round
|
83
|
+
# based on the eligible size, start fast and go slow until top 10
|
84
|
+
# Example:
|
85
|
+
# - 50 eligible, tier_size: 15
|
86
|
+
# - 35 eligible, tier_size: 11
|
87
|
+
# - 24 eligible, tier_size: 10
|
88
|
+
# - 14 eligible, tier_size: 4
|
89
|
+
# 4 rounds to reach top 10 with 50 players
|
90
|
+
def tier_size
|
91
|
+
size = (eligible_size / 3).ceil
|
92
|
+
|
93
|
+
if eligible_size < 10
|
94
|
+
(eligible_size / 2).ceil
|
95
|
+
else
|
96
|
+
size.clamp(10, eligible_size - 10)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|