Skip to content

Add AI-powered intelligent player selector using Gemini#211

Open
Copilot wants to merge 3 commits intodevfrom
copilot/add-intelligent-player-selector
Open

Add AI-powered intelligent player selector using Gemini#211
Copilot wants to merge 3 commits intodevfrom
copilot/add-intelligent-player-selector

Conversation

Copy link
Contributor

Copilot AI commented Jan 20, 2026

Implements an AI-powered player import feature that matches user-provided names (nicknames, abbreviations, partial names) against existing players using Gemini's similarity matching.

Changes

Backend

  • New app/utils/ai_player_matcher.py - LangChain/Gemini integration for name matching with confidence levels (high/medium/low)
  • New /api/match-players endpoint in main_routes.py - accepts input names, returns matches and unmatched names

Frontend

  • "Importar Lista" button added to armar_equipos page
  • Import modal with textarea for pasting names (comma or newline separated)
  • Results display with confidence indicators and selection checkboxes
  • Applies selected matches to main player list

Usage

Input: "Nahue, Nico, Martin"
Output: 
  - "Nahue" → "Nahuel Garcia" (high confidence)
  - "Nico" → "Nicolas Perez" (high confidence)
  - "Martin" → "Martin Rodriguez" (high confidence)

Screenshots

Import Button
Import Button

Import Modal
Import Modal

Loading State
Loading State

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.sendinblue.com
    • Triggering command: /home/REDACTED/.local/bin/uvicorn uvicorn app.main:app --host 127.0.0.1 --port 8000 (dns block)
    • Triggering command: /usr/bin/python python -m pytest tests/ -v (dns block)
  • generativelanguage.googleapis.com
    • Triggering command: /usr/bin/python python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Quiero que utilizando inteligencia artificial generativa, en mi caso la api de gemini que ya vengo usando, implementemos un selector de jugadores inteligente. El cual acepte una lista de jugadores y los busque por similitud dentro de la lista de jugadores creados en la app. Para así agilizar la creación de equipos y que el usuario no tenga que elegirlos uno por uno. Este agente ayudador tiene que leer todos los jugadores presentes en la lista actual en la página de armar equipos, e intentar encontrar los que tengan nombres parecidos, Por ejemplo si en la lista de jugadores que pasa el usuario tengo un "Nahue" y en la lista de jugadores creados hay un "Nahuel" debe darse cuenta que se está hablando de la misma persona. En caso de no encontrar a todos debe avisar a quienes no encontró. No hace falta hacerlo tipo interfaz de chatbot, yo me imagino un botón para importar lista y que luego se seleccionen los jugadores automáticamente para que el usuario después toque el botón de armar equipos directamente


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 20, 2026 02:37
- Create ai_player_matcher.py with AI-powered name matching
- Add /api/match-players endpoint for player name similarity search
- Add import list button and modal to armar_equipos page
- Add CSS styles for import modal and results display
- Implement JavaScript logic for importing and selecting players

Co-authored-by: fedecarboni7 <37885214+fedecarboni7@users.noreply.github.com>
Co-authored-by: fedecarboni7 <37885214+fedecarboni7@users.noreply.github.com>
Copilot AI changed the title [WIP] Add intelligent player selector using generative AI Add AI-powered intelligent player selector using Gemini Jan 20, 2026
Copilot AI requested a review from fedecarboni7 January 20, 2026 02:49
@fedecarboni7 fedecarboni7 marked this pull request as ready for review January 20, 2026 03:05
Copilot AI review requested due to automatic review settings January 20, 2026 03:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements an AI-powered player import feature that uses Google's Gemini API to intelligently match user-provided names (including nicknames, abbreviations, and partial names) against existing players in the system. The feature adds a convenient "Importar Lista" button to the team builder page, allowing users to paste a list of names and automatically select matching players.

Changes:

  • Added AI-powered name matching using LangChain and Gemini 2.0 Flash Lite model
  • Created new /api/match-players endpoint to handle matching requests
  • Implemented import modal UI with textarea input, confidence indicators, and selection checkboxes

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
app/utils/ai_player_matcher.py New utility module implementing LangChain/Gemini integration for intelligent name matching with confidence levels
app/routes/main_routes.py Added /api/match-players endpoint that accepts input names and returns matched players with confidence indicators
templates/armar_equipos.html Added import modal HTML structure with textarea, results display, and action buttons
static/js/armar_equipos.js Implemented modal controls, API integration, results rendering, and player selection application logic
static/css/armar_equipos.css Added comprehensive styling for import button, modal, loading states, confidence indicators, and responsive design

Comment on lines +169 to +175
<textarea id="import-players-textarea" placeholder="Escribí o pegá los nombres de los jugadores, uno por línea o separados por comas.

Ejemplo:
Nahue
Nico
Martin
Pedro"></textarea>
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The textarea element lacks a label element associated with it. While it has a placeholder, this is not sufficient for accessibility as placeholders disappear when users start typing and aren't always announced by screen readers. Add a label element with a for attribute pointing to the textarea's id, or use an aria-label attribute to provide an accessible name for the textarea.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +68
# Formatear la lista de jugadores disponibles para el prompt
players_formatted = "\n".join([f"- ID: {p['id']}, Nombre: {p['name']}" for p in available_players])
names_formatted = "\n".join([f"- {name}" for name in input_names])

result = await player_matching_chain.ainvoke({
"input_names": names_formatted,
"available_players": players_formatted
})
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there's a large number of players in the system, formatting all players as a string and sending them in the prompt could exceed token limits or result in expensive API calls. The prompt includes all available players in the system, which could be hundreds or thousands of entries. Consider implementing pagination or limiting the number of players sent to the AI, or using a more efficient matching approach for large player lists (e.g., pre-filtering by first letter or using embeddings).

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +70
async def match_players(input_names: list[str], available_players: list[dict]) -> dict:
"""
Usa IA para encontrar coincidencias entre nombres ingresados y jugadores disponibles.

Args:
input_names: Lista de nombres ingresados por el usuario
available_players: Lista de diccionarios con {id, name} de jugadores disponibles

Returns:
Dict con matches encontrados y nombres no encontrados
"""
# Formatear la lista de jugadores disponibles para el prompt
players_formatted = "\n".join([f"- ID: {p['id']}, Nombre: {p['name']}" for p in available_players])
names_formatted = "\n".join([f"- {name}" for name in input_names])

result = await player_matching_chain.ainvoke({
"input_names": names_formatted,
"available_players": players_formatted
})

return result
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks validation of the AI response structure. If the LLM returns a malformed JSON or unexpected structure (e.g., missing 'matches' or 'not_found' keys, or incorrect data types for matched_player_id), this will cause runtime errors when the response is processed in the frontend or API endpoint. Consider adding validation to ensure the response contains the expected keys and data types, and handle cases where the AI returns an unexpected format.

Copilot uses AI. Check for mistakes.
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash-lite")
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LLM instance is initialized globally, which means it's created once when the module is imported. This approach doesn't allow for configuration changes (like API key rotation) without restarting the application, and it may cause issues if initialization fails during module import. Consider moving the LLM initialization inside the function or using a factory pattern that allows for lazy initialization and better error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash-lite")

Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model name "gemini-2.0-flash-lite" is hardcoded without configuration option. This makes it difficult to switch models or test with different models without code changes. Consider making this configurable through environment variables or a configuration file, following the pattern used elsewhere in the application.

Suggested change
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash-lite")
import os
PLAYER_MATCHER_MODEL_NAME = os.getenv("AI_PLAYER_MATCHER_MODEL", "gemini-2.0-flash-lite")
llm = ChatGoogleGenerativeAI(model=PLAYER_MATCHER_MODEL_NAME)

Copilot uses AI. Check for mistakes.
alert('No hay jugadores seleccionados para agregar');
return;
}

Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When applying imported players, the current selection is completely cleared and replaced with the imported players. This behavior might be unexpected for users who have already selected some players manually. Consider either appending to the existing selection, or providing a clear warning/confirmation dialog explaining that the current selection will be replaced before proceeding.

Suggested change
// If there is an existing selection, confirm replacement
if (selectedPlayers.size > 0) {
const shouldReplace = window.confirm(
'Ya tenés jugadores seleccionados manualmente.\n' +
'Al aplicar la importación se va a reemplazar la selección actual por los jugadores importados.\n\n' +
'¿Querés continuar y reemplazar la selección actual?'
);
if (!shouldReplace) {
return;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +201 to +241
@router.post("/api/match-players", response_class=JSONResponse)
async def match_players_api(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Endpoint para buscar coincidencias entre nombres de jugadores ingresados
y jugadores disponibles en el sistema usando IA.
"""
if not current_user:
return JSONResponse(content={"error": "No autenticado"}, status_code=401)

try:
data = await request.json()
input_names = data.get('input_names', [])
club_id = data.get('club_id')
scale = data.get('scale', '1-5')

if not input_names:
return JSONResponse(content={"error": "No se proporcionaron nombres"}, status_code=400)

# Obtener jugadores disponibles según el contexto
current_user_id = current_user.id

all_players = execute_with_retries(query_players, db, current_user_id, club_id, scale)

if not all_players:
return JSONResponse(content={"error": "No hay jugadores disponibles"}, status_code=400)

# Formatear jugadores para el matcher
available_players = [{"id": p.id, "name": p.name} for p in all_players]

# Usar IA para hacer el matching
result = await match_players(input_names, available_players)

return JSONResponse(content=result)

except Exception as e:
logging.exception("Error matching players: %s", str(e))
return JSONResponse(content={"error": "Error al buscar coincidencias de jugadores"}, status_code=500) No newline at end of file
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new AI-powered player matching endpoint and functionality lacks test coverage. The repository has comprehensive tests for other endpoints (as seen in test_main_routes.py, test_player_routes.py), but no tests exist for the /api/match-players endpoint or the match_players function. Add tests to verify correct behavior with valid inputs, malformed AI responses, empty player lists, and error scenarios.

Copilot uses AI. Check for mistakes.
Comment on lines +943 to +1104
alert('Por favor, ingresá una lista de nombres de jugadores');
return;
}

// Parse names from text (handle both commas and newlines)
const inputNames = text
.split(/[\n,]+/)
.map(name => name.trim())
.filter(name => name.length > 0);

if (inputNames.length === 0) {
alert('No se encontraron nombres válidos en la lista');
return;
}

// Show loading state
processBtn.disabled = true;
processBtn.innerHTML = '<span class="spinner"></span> Buscando...';
resultsDiv.style.display = 'block';
matchesDiv.innerHTML = '<div class="import-loading"><div class="spinner"></div>Buscando coincidencias con IA...</div>';
notFoundDiv.style.display = 'none';

try {
// Build request data
const requestData = {
input_names: inputNames,
scale: currentScale === 5 ? '1-5' : '1-10'
};

// Add club_id if using a club context
const clubId = getCurrentClubId();
if (clubId !== 'my-players') {
requestData.club_id = parseInt(clubId);
}

// Call API
const response = await fetch('/api/match-players', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(requestData)
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Error al buscar coincidencias');
}

const data = await response.json();
importMatchResults = data;

// Render results
renderImportResults(data);

// Show apply button if there are matches
if (data.matches && data.matches.length > 0) {
processBtn.style.display = 'none';
applyBtn.style.display = 'flex';
}

} catch (error) {
console.error('Error processing import:', error);
matchesDiv.innerHTML = `<div style="color: #ff6b6b; padding: 20px; text-align: center;">
❌ ${error.message}
</div>`;
} finally {
processBtn.disabled = false;
processBtn.innerHTML = '<i class="fa-solid fa-magnifying-glass"></i> Buscar coincidencias';
}
}

function renderImportResults(data) {
const matchesDiv = document.getElementById('import-matches');
const notFoundDiv = document.getElementById('import-not-found');
const notFoundList = document.getElementById('not-found-list');

// Clear previous results
matchesDiv.innerHTML = '';

// Render matches
if (data.matches && data.matches.length > 0) {
data.matches.forEach((match, index) => {
const confidenceClass = match.confidence === 'low' ? 'low-confidence' : '';
const matchItem = document.createElement('div');
matchItem.className = `import-match-item ${confidenceClass}`;
matchItem.innerHTML = `
<div class="import-match-info">
<span class="import-match-input">"${escapeHTML(match.input_name)}" →</span>
<span class="import-match-result">${escapeHTML(match.matched_player_name)}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span class="import-match-confidence ${match.confidence}">${getConfidenceLabel(match.confidence)}</span>
<input type="checkbox" class="import-match-checkbox" data-player-id="${match.matched_player_id}" data-player-name="${escapeHTML(match.matched_player_name)}" checked>
</div>
`;
matchesDiv.appendChild(matchItem);
});
} else {
matchesDiv.innerHTML = '<div style="color: #999; padding: 20px; text-align: center;">No se encontraron coincidencias</div>';
}

// Render not found
if (data.not_found && data.not_found.length > 0) {
notFoundDiv.style.display = 'block';
notFoundList.innerHTML = '';
data.not_found.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
notFoundList.appendChild(li);
});
} else {
notFoundDiv.style.display = 'none';
}
}

function getConfidenceLabel(confidence) {
switch (confidence) {
case 'high': return '✓ Seguro';
case 'medium': return '~ Probable';
case 'low': return '? Posible';
default: return confidence;
}
}

function applyImportedPlayers() {
// Get all checked matches
const checkboxes = document.querySelectorAll('.import-match-checkbox:checked');

if (checkboxes.length === 0) {
alert('No hay jugadores seleccionados para agregar');
return;
}

// Clear current selection
selectedPlayers.clear();

// Add matched players to selection
checkboxes.forEach(checkbox => {
const playerName = checkbox.dataset.playerName;
if (playerName) {
selectedPlayers.add(playerName);
}
});

// Render updated player list
renderPlayers();

// Close modal
closeImportModal();

// Show success message
const count = selectedPlayers.size;
const notFoundCount = importMatchResults?.not_found?.length || 0;

let message = `✅ Se seleccionaron ${count} jugador${count !== 1 ? 'es' : ''}`;
if (notFoundCount > 0) {
message += `\n⚠️ ${notFoundCount} nombre${notFoundCount !== 1 ? 's' : ''} no ${notFoundCount !== 1 ? 'fueron encontrados' : 'fue encontrado'}`;
}

alert(message);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alert() function is used for user feedback, which is considered poor UX in modern web applications. Consider replacing alert() calls with a more user-friendly notification system (e.g., toast notifications or inline messages) that doesn't block the UI and provides a better user experience.

Copilot uses AI. Check for mistakes.
<div class="import-modal-content">
<div class="import-modal-header">
<h2><i class="fa-solid fa-wand-magic-sparkles"></i> Importar Lista con IA</h2>
<button class="close-btn" id="close-import-modal">&times;</button>
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close button uses the HTML entity × without an accessible label or aria-label. Screen reader users won't know what this button does. Add an aria-label attribute (e.g., aria-label="Cerrar modal") to make the button accessible to users with screen readers.

Suggested change
<button class="close-btn" id="close-import-modal">&times;</button>
<button class="close-btn" id="close-import-modal" aria-label="Cerrar modal">&times;</button>

Copilot uses AI. Check for mistakes.
const resultsDiv = document.getElementById('import-results');
const matchesDiv = document.getElementById('import-matches');
const notFoundDiv = document.getElementById('import-not-found');
const notFoundList = document.getElementById('not-found-list');
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable notFoundList.

Suggested change
const notFoundList = document.getElementById('not-found-list');

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants