Skip to content

Commit 39ad326

Browse files
committed
fix: solve Zeitwerk module nesting
1 parent 2fda2cb commit 39ad326

6 files changed

Lines changed: 116 additions & 122 deletions

File tree

app/modules/ai_intelligence/channels/draft_channel.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def picks_updated(data)
3939

4040
# 2. Sinergia via embeddings: SynergyMatrixService quando team_a tem >= 2 picks
4141
synergy_data = if team_a.size >= 2
42-
AiIntelligence::SynergyMatrixService.call(champions: team_a)
42+
SynergyMatrixService.call(champions: team_a)
4343
else
4444
{ champions: team_a, matrix: [], top_pairs: [], weakest_pairs: [] }
4545
end
@@ -64,7 +64,7 @@ def picks_updated(data)
6464

6565
# 5. Patch win rates para todos os campeões envolvidos
6666
all_champions = (team_a + team_b).uniq
67-
patch_win_rates = patch.present? ? AiIntelligence::Services::ChampionWinrateService.bulk_lookup(all_champions, patch) : {}
67+
patch_win_rates = patch.present? ? ChampionWinrateService.bulk_lookup(all_champions, patch) : {}
6868

6969
ActionCable.server.broadcast(
7070
"draft_#{current_org_id}_#{draft_id}",

app/modules/ai_intelligence/controllers/champion_analytics_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ def index
2323
return render json: { error: 'team_champions required' }, status: :bad_request if champions.empty?
2424

2525
data = champions.filter_map do |champ|
26-
wr = Services::ChampionWinrateService.win_rate_for(champion: champ, patch: patch)
26+
wr = ChampionWinrateService.win_rate_for(champion: champ, patch: patch)
2727
next if wr.nil?
2828

2929
prev_wr = if patch.present?
3030
prev_patch = patch.to_s.split('.').first.to_i - 1
31-
Services::ChampionWinrateService.win_rate_for(champion: champ, patch: prev_patch.to_s)
31+
ChampionWinrateService.win_rate_for(champion: champ, patch: prev_patch.to_s)
3232
end
3333

3434
trend = if prev_wr.nil? then 'stable'

app/modules/ai_intelligence/controllers/draft_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def analyze
2929
blueprint = DraftAnalysisBlueprint.render_as_hash(result)
3030

3131
all_champs = (Array(team_a) + Array(team_b)).uniq
32-
champion_win_rates = Services::ChampionWinrateService.bulk_lookup(all_champs, patch)
32+
champion_win_rates = ChampionWinrateService.bulk_lookup(all_champs, patch)
3333
blueprint[:champion_win_rates] = champion_win_rates
3434

3535
render_success(blueprint)

app/modules/ai_intelligence/controllers/recommend_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def recommend_pick
3636
patch = params[:patch]
3737
if patch.present? && result[:recommendations].is_a?(Array)
3838
result[:recommendations].each do |rec|
39-
rec[:patch_win_rate] = Services::ChampionWinrateService.win_rate_for(
39+
rec[:patch_win_rate] = ChampionWinrateService.win_rate_for(
4040
champion: rec[:champion],
4141
patch: patch
4242
)
Lines changed: 54 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,63 @@
11
# frozen_string_literal: true
22

3-
module AiIntelligence
4-
module Services
5-
# Loads champion patch win-rate data from champion_patch_winrate.json and
6-
# exposes fast lookups cached in Rails.cache for 24 hours.
7-
#
8-
# Key format in JSON: "Azir_16" => 0.582
9-
# where the suffix is the major integer of the patch (e.g. "16.08" -> "16").
10-
class ChampionWinrateService
11-
PRIMARY_FILE = Rails.root.join('data', 'champion_patch_winrate.json').freeze
12-
FALLBACK_FILE = Pathname.new('/home/bullet/PROJETOS/prostaff-ml/data/champion_patch_winrate.json').freeze
13-
CACHE_KEY = 'champion_winrates'
14-
CACHE_TTL = 24.hours
15-
16-
# Returns the win rate (Float) for a given champion on a given patch,
17-
# or nil if no data is available.
18-
#
19-
# @param champion [String] e.g. "Azir"
20-
# @param patch [String] e.g. "16.08" or Integer 16
21-
# @return [Float, nil]
22-
def self.win_rate_for(champion:, patch:)
23-
return nil if champion.blank? || patch.nil?
24-
25-
key = "#{champion}_#{patch.to_s.split('.').first}"
26-
data[key]
27-
end
3+
# Loads champion patch win-rate data from champion_patch_winrate.json and
4+
# exposes fast lookups cached in Rails.cache for 24 hours.
5+
#
6+
# Key format in JSON: "Azir_16" => 0.582
7+
# where the suffix is the major integer of the patch (e.g. "16.08" -> "16").
8+
class ChampionWinrateService
9+
PRIMARY_FILE = Rails.root.join('data', 'champion_patch_winrate.json').freeze
10+
FALLBACK_FILE = Pathname.new('/home/bullet/PROJETOS/prostaff-ml/data/champion_patch_winrate.json').freeze
11+
CACHE_KEY = 'champion_winrates'
12+
CACHE_TTL = 24.hours
13+
14+
# Returns the win rate (Float) for a given champion on a given patch,
15+
# or nil if no data is available.
16+
#
17+
# @param champion [String] e.g. "Azir"
18+
# @param patch [String] e.g. "16.08" or Integer 16
19+
# @return [Float, nil]
20+
def self.win_rate_for(champion:, patch:)
21+
return nil if champion.blank? || patch.nil?
22+
23+
key = "#{champion}_#{patch.to_s.split('.').first}"
24+
data[key]
25+
end
2826

29-
# Returns a hash mapping each champion name to its win rate (or nil).
30-
#
31-
# @param champions [Array<String>]
32-
# @param patch [String]
33-
# @return [Hash{String => Float, nil}]
34-
def self.bulk_lookup(champions, patch)
35-
Array(champions).map { |c| [c, win_rate_for(champion: c, patch: patch)] }.to_h
36-
end
27+
# Returns a hash mapping each champion name to its win rate (or nil).
28+
#
29+
# @param champions [Array<String>]
30+
# @param patch [String]
31+
# @return [Hash{String => Float, nil}]
32+
def self.bulk_lookup(champions, patch)
33+
Array(champions).map { |c| [c, win_rate_for(champion: c, patch: patch)] }.to_h
34+
end
3735

38-
# Loads (and caches) the win-rate JSON. Returns {} on any error.
39-
#
40-
# @return [Hash{String => Float}]
41-
def self.data
42-
Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_TTL) do
43-
file_path = resolve_file_path
44-
if file_path
45-
JSON.parse(File.read(file_path))
46-
else
47-
Rails.logger.warn 'ChampionWinrateService: champion_patch_winrate.json not found in any known path'
48-
{}
49-
end
50-
rescue => e
51-
Rails.logger.warn "ChampionWinrateService: failed to load win-rate data — #{e.message}"
52-
{}
53-
end
36+
# Loads (and caches) the win-rate JSON. Returns {} on any error.
37+
#
38+
# @return [Hash{String => Float}]
39+
def self.data
40+
Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_TTL) do
41+
file_path = resolve_file_path
42+
if file_path
43+
JSON.parse(File.read(file_path))
44+
else
45+
Rails.logger.warn 'ChampionWinrateService: champion_patch_winrate.json not found in any known path'
46+
{}
5447
end
48+
rescue => e
49+
Rails.logger.warn "ChampionWinrateService: failed to load win-rate data — #{e.message}"
50+
{}
51+
end
52+
end
5553

56-
# @return [Pathname, nil]
57-
def self.resolve_file_path
58-
return PRIMARY_FILE if PRIMARY_FILE.exist?
59-
return FALLBACK_FILE if FALLBACK_FILE.exist?
60-
61-
nil
62-
end
54+
# @return [Pathname, nil]
55+
def self.resolve_file_path
56+
return PRIMARY_FILE if PRIMARY_FILE.exist?
57+
return FALLBACK_FILE if FALLBACK_FILE.exist?
6358

64-
private_class_method :resolve_file_path
65-
end
59+
nil
6660
end
61+
62+
private_class_method :resolve_file_path
6763
end
Lines changed: 56 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,72 @@
11
# frozen_string_literal: true
22

3-
module AiIntelligence
4-
# Calculates an N×N cosine-similarity matrix from 64-dimensional champion embeddings.
5-
#
6-
# Embeddings are loaded once per 24h from champion_embeddings_64d.json via Rails.cache.
7-
# Primary path: ai_service/data/champion_embeddings_64d.json
8-
# Fallback path: models/champion_embeddings_64d.json (prostaff-ml artefact)
9-
class SynergyMatrixService
10-
EMBEDDINGS_FILE = Rails.root.join('ai_service', 'data', 'champion_embeddings_64d.json').freeze
11-
FALLBACK_FILE = Rails.root.join('models', 'champion_embeddings_64d.json').freeze
12-
CACHE_KEY = 'ai_intelligence/champion_embeddings_64d'
13-
CACHE_TTL = 24.hours
3+
# Calculates an N×N cosine-similarity matrix from 64-dimensional champion embeddings.
4+
#
5+
# Embeddings are loaded once per 24h from champion_embeddings_64d.json via Rails.cache.
6+
# Primary path: ai_service/data/champion_embeddings_64d.json
7+
# Fallback path: models/champion_embeddings_64d.json (prostaff-ml artefact)
8+
class SynergyMatrixService
9+
EMBEDDINGS_FILE = Rails.root.join('ai_service', 'data', 'champion_embeddings_64d.json').freeze
10+
FALLBACK_FILE = Rails.root.join('models', 'champion_embeddings_64d.json').freeze
11+
CACHE_KEY = 'ai_intelligence/champion_embeddings_64d'
12+
CACHE_TTL = 24.hours
1413

15-
# @param champions [Array<String>] 2–10 champion names
16-
# @return [Hash] { champions:, matrix:, top_pairs:, weakest_pairs: }
17-
def self.call(champions:)
18-
embs = embeddings
19-
resolved = champions.filter_map do |c|
20-
vec = embs[c] || embs[c.downcase]
21-
[c, vec] if vec
22-
end.to_h
14+
# @param champions [Array<String>] 2–10 champion names
15+
# @return [Hash] { champions:, matrix:, top_pairs:, weakest_pairs: }
16+
def self.call(champions:)
17+
embs = embeddings
18+
resolved = champions.filter_map do |c|
19+
vec = embs[c] || embs[c.downcase]
20+
[c, vec] if vec
21+
end.to_h
2322

24-
present = resolved.keys
25-
return { champions: present, matrix: [], top_pairs: [], weakest_pairs: [] } if present.size < 2
23+
present = resolved.keys
24+
return { champions: present, matrix: [], top_pairs: [], weakest_pairs: [] } if present.size < 2
2625

27-
matrix = present.map.with_index do |a, i|
28-
present.map.with_index do |b, j|
29-
i == j ? 1.0 : cosine_similarity(resolved[a], resolved[b])
30-
end
26+
matrix = present.map.with_index do |a, i|
27+
present.map.with_index do |b, j|
28+
i == j ? 1.0 : cosine_similarity(resolved[a], resolved[b])
3129
end
30+
end
3231

33-
pairs = []
34-
present.combination(2).each do |a, b|
35-
ia = present.index(a)
36-
ib = present.index(b)
37-
pairs << { pair: [a, b], score: matrix[ia][ib].round(4) }
38-
end
39-
pairs.sort_by! { |p| -p[:score] }
40-
41-
{
42-
champions: present,
43-
matrix: matrix.map { |row| row.map { |v| v.round(4) } },
44-
top_pairs: pairs.first(5),
45-
weakest_pairs: pairs.last(3)
46-
}
32+
pairs = []
33+
present.combination(2).each do |a, b|
34+
ia = present.index(a)
35+
ib = present.index(b)
36+
pairs << { pair: [a, b], score: matrix[ia][ib].round(4) }
4737
end
38+
pairs.sort_by! { |p| -p[:score] }
4839

49-
# ── private ──────────────────────────────────────────────────────────
40+
{
41+
champions: present,
42+
matrix: matrix.map { |row| row.map { |v| v.round(4) } },
43+
top_pairs: pairs.first(5),
44+
weakest_pairs: pairs.last(3)
45+
}
46+
end
5047

51-
def self.embeddings
52-
Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_TTL) { load_embeddings }
53-
end
54-
private_class_method :embeddings
48+
# ── private ──────────────────────────────────────────────────────────
5549

56-
def self.load_embeddings
57-
path = EMBEDDINGS_FILE.exist? ? EMBEDDINGS_FILE : FALLBACK_FILE
58-
raise "Champion embeddings file not found (tried #{EMBEDDINGS_FILE} and #{FALLBACK_FILE})" unless path.exist?
50+
def self.embeddings
51+
Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_TTL) { load_embeddings }
52+
end
53+
private_class_method :embeddings
5954

60-
JSON.parse(File.read(path))
61-
end
62-
private_class_method :load_embeddings
55+
def self.load_embeddings
56+
path = EMBEDDINGS_FILE.exist? ? EMBEDDINGS_FILE : FALLBACK_FILE
57+
raise "Champion embeddings file not found (tried #{EMBEDDINGS_FILE} and #{FALLBACK_FILE})" unless path.exist?
6358

64-
def self.cosine_similarity(a, b)
65-
dot = a.zip(b).sum { |x, y| x * y }
66-
na = Math.sqrt(a.sum { |x| x**2 })
67-
nb = Math.sqrt(b.sum { |x| x**2 })
68-
return 0.0 if na < 1e-9 || nb < 1e-9
59+
JSON.parse(File.read(path))
60+
end
61+
private_class_method :load_embeddings
6962

70-
(dot / (na * nb)).clamp(-1.0, 1.0)
71-
end
72-
private_class_method :cosine_similarity
63+
def self.cosine_similarity(a, b)
64+
dot = a.zip(b).sum { |x, y| x * y }
65+
na = Math.sqrt(a.sum { |x| x**2 })
66+
nb = Math.sqrt(b.sum { |x| x**2 })
67+
return 0.0 if na < 1e-9 || nb < 1e-9
68+
69+
(dot / (na * nb)).clamp(-1.0, 1.0)
7370
end
71+
private_class_method :cosine_similarity
7472
end

0 commit comments

Comments
 (0)