Skip to content

Commit 6a0447b

Browse files
committed
feat: implement ProStaff ML
Atualmente na ProStaff tenho um modelo próprio treinado de AI para recomendações do tipo "X é melhor que Y nesse contexto: - XGBoost para classificação binária (win/loss dado o draft + contexto) com dados tabulares, treina rápido em CPU - Matrix Factorization para sinergias implícitas (similar ao que o Netflix usa para recomendação) e descobre relações latentes entre campeões que o win rate bruto não captura - Embeddings de campeão treinados no seu próprio histórico (Word2Vec sobre sequências de picks), representação vetorial própria, mais rica que os vetores atuais baseados só em stats médios
1 parent 8885c10 commit 6a0447b

3 files changed

Lines changed: 161 additions & 1 deletion

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module AiIntelligence
4+
module Controllers
5+
# Champion pick recommendations powered by the ProStaff ML AI Service.
6+
#
7+
# Calls the FastAPI ML service (ai-service container) and falls back to the
8+
# Ruby DraftSuggester when the ML service is unavailable.
9+
#
10+
# The X-AI-Source response header indicates which engine answered:
11+
# X-AI-Source: ml_v2 — ML service (XGBoost + Champion2Vec, 327 features)
12+
# X-AI-Source: legacy — DraftSuggester (cosine similarity, AiChampionVector table)
13+
class RecommendController < Api::V1::BaseController
14+
before_action :require_predictive_analytics_access!
15+
16+
# POST /api/v1/ai/recommend-pick
17+
#
18+
# @param our_picks [Array<String>] champions already picked by our team (0-4)
19+
# @param opponent_picks [Array<String>] champions picked by the opponent (0-5)
20+
# @param our_bans [Array<String>] champions banned by our team (optional)
21+
# @param opponent_bans [Array<String>] champions banned by opponent (optional)
22+
# @param patch [String] patch version, e.g. "16.08" (optional)
23+
# @param league [String] league identifier, e.g. "LCK" (optional)
24+
#
25+
# @return [JSON] { recommendations: [...], source: "ml_v2"|"legacy", model_version: "v2"|nil }
26+
def recommend_pick
27+
result = AiRecommendationService.call(
28+
our_picks: Array(params[:our_picks]),
29+
opponent_picks: Array(params[:opponent_picks]),
30+
our_bans: Array(params[:our_bans]),
31+
opponent_bans: Array(params[:opponent_bans]),
32+
patch: params[:patch],
33+
league: params[:league]
34+
)
35+
36+
response.set_header('X-AI-Source', result[:source])
37+
render_success(result)
38+
end
39+
40+
private
41+
42+
def require_predictive_analytics_access!
43+
return if current_organization.can_access?('predictive_analytics')
44+
45+
render_error(
46+
message: 'AI recommendations require Tier 1 (Professional) subscription',
47+
code: 'UPGRADE_REQUIRED',
48+
status: :forbidden
49+
)
50+
end
51+
end
52+
end
53+
end
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
# HTTP client for the ProStaff ML AI Service (FastAPI).
4+
#
5+
# Calls POST /recommend on the ML service and returns top-N champion picks
6+
# with composite scores. Falls back to DraftSuggester (Ruby cosine-similarity
7+
# implementation) when the ML service is unreachable or returns an error.
8+
#
9+
# Configuration:
10+
# AI_SERVICE_URL — base URL of the FastAPI service, e.g. http://ai-service:8001
11+
# Defaults to http://localhost:8001 for local development.
12+
#
13+
# Source tagging:
14+
# Returns { source: "ml_v2" } when ML responded successfully.
15+
# Returns { source: "legacy" } when falling back to DraftSuggester.
16+
#
17+
# @example
18+
# result = AiRecommendationService.call(
19+
# our_picks: %w[Jinx Thresh Azir Gnar],
20+
# opponent_picks: %w[Caitlyn Nautilus Syndra Renekton Graves],
21+
# our_bans: [],
22+
# opponent_bans: [],
23+
# patch: "16.08",
24+
# league: "LCK"
25+
# )
26+
# result[:source] # => "ml_v2"
27+
# result[:recommendations] # => [{ champion: "Lissandra", score: 0.52, ... }]
28+
class AiRecommendationService
29+
class MlServiceError < StandardError; end
30+
31+
REQUEST_TIMEOUT = 5
32+
33+
def self.call(**)
34+
new(**).call
35+
end
36+
37+
def initialize(our_picks:, opponent_picks:, our_bans: [], opponent_bans: [], patch: nil, league: nil)
38+
@our_picks = our_picks
39+
@opponent_picks = opponent_picks
40+
@our_bans = our_bans
41+
@opponent_bans = opponent_bans
42+
@patch = patch
43+
@league = league
44+
@base_url = ENV.fetch('AI_SERVICE_URL', 'http://localhost:8001')
45+
end
46+
47+
def call
48+
call_ml_service
49+
rescue MlServiceError => e
50+
Rails.logger.warn("[AiRecommendationService] ML service unavailable, using legacy fallback: #{e.message}")
51+
legacy_fallback
52+
end
53+
54+
private
55+
56+
def call_ml_service
57+
response = connection.post('/recommend') do |req|
58+
req.headers['Content-Type'] = 'application/json'
59+
req.body = build_payload.to_json
60+
req.options.timeout = REQUEST_TIMEOUT
61+
end
62+
63+
raise MlServiceError, "ML service returned #{response.status}" unless response.success?
64+
65+
body = JSON.parse(response.body, symbolize_names: true)
66+
{
67+
source: body[:source] || 'ml_v2',
68+
model_version: body[:model_version],
69+
recommendations: body[:recommendations] || []
70+
}
71+
rescue Faraday::TimeoutError => e
72+
raise MlServiceError, "timeout: #{e.message}"
73+
rescue Faraday::ConnectionFailed => e
74+
raise MlServiceError, "connection failed: #{e.message}"
75+
rescue Faraday::Error => e
76+
raise MlServiceError, "network error: #{e.message}"
77+
rescue JSON::ParserError => e
78+
raise MlServiceError, "invalid JSON response: #{e.message}"
79+
end
80+
81+
def legacy_fallback
82+
suggestions = DraftSuggester.call(team_a: @our_picks, team_b: @opponent_picks)
83+
{
84+
source: 'legacy',
85+
model_version: nil,
86+
recommendations: suggestions.map { |champ| { champion: champ, score: nil } }
87+
}
88+
end
89+
90+
def build_payload
91+
{
92+
our_picks: @our_picks,
93+
opponent_picks: @opponent_picks,
94+
our_bans: @our_bans,
95+
opponent_bans: @opponent_bans,
96+
patch: @patch,
97+
league: @league
98+
}
99+
end
100+
101+
def connection
102+
@connection ||= Faraday.new(url: @base_url) do |f|
103+
f.adapter Faraday.default_adapter
104+
end
105+
end
106+
end

config/routes.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,8 @@
459459
# AI Intelligence Module — draft analysis and win probability
460460
# Requires Tier 1 (Professional) subscription.
461461
namespace :ai do
462-
post 'draft/analyze', to: '/ai_intelligence/controllers/draft#analyze'
462+
post 'draft/analyze', to: '/ai_intelligence/controllers/draft#analyze'
463+
post 'recommend-pick', to: '/ai_intelligence/controllers/recommend#recommend_pick'
463464
end
464465

465466
# Tournaments Module — ArenaBR double elimination

0 commit comments

Comments
 (0)