Spaces:
Running
Running
fredcaixeta commited on
Commit ·
2bc88b5
1
Parent(s): 0f94868
SQLLLLLLLLLL
Browse files- .gitignore +10 -0
- app.py +206 -61
- main.py +6 -0
- main_agent.py +341 -0
- mcp_one_player_supabase.py +264 -0
- place_holder_image.py +44 -0
- prompts/__init__.py +0 -0
- prompts/graph_agent_prompt.py +103 -0
- prompts/matches_prompt.py +222 -0
- prompts/player_matchid_prompt.py +226 -0
- prompts/players_prompt.py +285 -0
- pyproject.toml +17 -0
- requirements.txt +7 -0
- teste.ipynb +84 -0
- utils.py +223 -0
- uv.lock +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#gitignore
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.env
|
| 5 |
+
.vscode/
|
| 6 |
+
instance/
|
| 7 |
+
.python-version
|
| 8 |
+
.logfire
|
| 9 |
+
#duckdb
|
| 10 |
+
*.duckdb
|
app.py
CHANGED
|
@@ -1,70 +1,215 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
hf_token: gr.OAuthToken,
|
| 13 |
-
):
|
| 14 |
-
"""
|
| 15 |
-
For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
|
| 16 |
-
"""
|
| 17 |
-
client = InferenceClient(token=hf_token.token, model="openai/gpt-oss-20b")
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
"""
|
| 44 |
-
For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
|
| 45 |
-
"""
|
| 46 |
-
chatbot = gr.ChatInterface(
|
| 47 |
-
respond,
|
| 48 |
-
type="messages",
|
| 49 |
-
additional_inputs=[
|
| 50 |
-
gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
|
| 51 |
-
gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
|
| 52 |
-
gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
|
| 53 |
-
gr.Slider(
|
| 54 |
-
minimum=0.1,
|
| 55 |
-
maximum=1.0,
|
| 56 |
-
value=0.95,
|
| 57 |
-
step=0.05,
|
| 58 |
-
label="Top-p (nucleus sampling)",
|
| 59 |
-
),
|
| 60 |
-
],
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
with gr.Blocks() as demo:
|
| 64 |
-
with gr.Sidebar():
|
| 65 |
-
gr.LoginButton()
|
| 66 |
-
chatbot.render()
|
| 67 |
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
if __name__ == "__main__":
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import uuid
|
| 3 |
+
import subprocess
|
| 4 |
+
import time
|
| 5 |
+
import asyncio
|
| 6 |
+
import re
|
| 7 |
|
| 8 |
+
USER_ID = str(uuid.uuid4())
|
| 9 |
|
| 10 |
+
# Iniciar o servidor MCP em background
|
| 11 |
+
# subprocess.Popen(["python", "mcp_players_table_sql.py"])
|
| 12 |
+
# subprocess.Popen(["python", "mcp_team_table_sql.py"])
|
| 13 |
+
subprocess.Popen(["python", "mcp_one_player_supabase.py"])
|
| 14 |
+
time.sleep(3)
|
| 15 |
+
# subprocess.Popen(["python", "mcp_graph_server.py"])
|
| 16 |
+
# time.sleep(3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
#from place_holder_image import PLACEHOLDER_IMAGE, get_current_chart
|
| 19 |
+
from main_agent import stream_agent_response_safe, PLACEHOLDER_IMAGE, get_current_chart, agent_conventional_response
|
| 20 |
+
from utils import CUSTOM_CSS
|
| 21 |
|
| 22 |
+
async def simulate_streaming_adaptive(full_text: str):
|
| 23 |
+
"""Streaming adaptativo com delays diferentes para pontuação"""
|
| 24 |
+
words = re.findall(r'\S+\s*', full_text)
|
| 25 |
+
current_text = ""
|
| 26 |
+
|
| 27 |
+
for word in words:
|
| 28 |
+
current_text += word
|
| 29 |
+
|
| 30 |
+
if word.strip().endswith(('.', '!', '?')):
|
| 31 |
+
delay = 0.15
|
| 32 |
+
elif word.strip().endswith((',', ';', ':')):
|
| 33 |
+
delay = 0.08
|
| 34 |
+
else:
|
| 35 |
+
delay = 0.04
|
| 36 |
+
|
| 37 |
+
await asyncio.sleep(delay)
|
| 38 |
+
yield current_text
|
| 39 |
|
| 40 |
+
async def respond(message, history):
|
| 41 |
+
"""Função de resposta com streaming + atualização de gráfico"""
|
| 42 |
+
message = str(message)
|
| 43 |
+
print(f"Message received: {message}")
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
print("Obtendo resposta completa do agente...")
|
| 47 |
+
full_response = await stream_agent_response_safe(message)
|
| 48 |
+
print(f"Resposta obtida: {len(full_response)} caracteres")
|
| 49 |
+
|
| 50 |
+
# Simular streaming da resposta
|
| 51 |
+
async for text_chunk in simulate_streaming_adaptive(full_response):
|
| 52 |
+
yield text_chunk, get_current_chart()
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"Erro durante o processamento: {e}")
|
| 56 |
+
import traceback
|
| 57 |
+
traceback.print_exc()
|
| 58 |
+
yield f"❌ Erro: {str(e)}", get_current_chart()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
def refresh_chart():
|
| 61 |
+
"""Atualiza a visualização do gráfico"""
|
| 62 |
+
return get_current_chart()
|
| 63 |
|
| 64 |
if __name__ == "__main__":
|
| 65 |
+
with gr.Blocks(
|
| 66 |
+
title="Barcelona Analytics Platform",
|
| 67 |
+
theme=gr.themes.Soft(
|
| 68 |
+
primary_hue="blue",
|
| 69 |
+
secondary_hue="slate",
|
| 70 |
+
neutral_hue="slate",
|
| 71 |
+
font=[gr.themes.GoogleFont("Inter"), "sans-serif"],
|
| 72 |
+
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"]
|
| 73 |
+
),
|
| 74 |
+
css=CUSTOM_CSS
|
| 75 |
+
) as demo:
|
| 76 |
+
|
| 77 |
+
gr.Markdown(
|
| 78 |
+
"""
|
| 79 |
+
#### Data Analyst Agent with FC Barcelona Statistics (2020/2021)
|
| 80 |
+
|
| 81 |
+
Powered by Postgres (Supabase) via MCP.
|
| 82 |
+
"""
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
with gr.Tabs() as tabs:
|
| 86 |
+
# ABA 1: ANALYSIS & CHAT
|
| 87 |
+
with gr.Tab("Chat", id=0):
|
| 88 |
+
#gr.Markdown("### Intelligent Assistant")
|
| 89 |
+
|
| 90 |
+
chatbot = gr.Chatbot(
|
| 91 |
+
type="messages",
|
| 92 |
+
label="",
|
| 93 |
+
height=330,
|
| 94 |
+
show_copy_button=True
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
with gr.Row():
|
| 98 |
+
msg = gr.Textbox(
|
| 99 |
+
placeholder="Ask about players, matches, or request visualizations...",
|
| 100 |
+
label="Your Query",
|
| 101 |
+
lines=2,
|
| 102 |
+
scale=4,
|
| 103 |
+
show_label=False
|
| 104 |
+
)
|
| 105 |
+
submit_btn = gr.Button("Send", variant="primary", scale=1, size="lg")
|
| 106 |
+
|
| 107 |
+
with gr.Accordion("📋 Query Examples", open=False):
|
| 108 |
+
gr.Examples(
|
| 109 |
+
examples=[
|
| 110 |
+
"Return the top 10 players by total xG and assists combined",
|
| 111 |
+
"Create a bar chart showing the top 5 players with most passes",
|
| 112 |
+
"Find players with at least 900 minutes who rank in the top 5% for progressive passes and top 10% for xG assisted in 2020/2021, returning player, minutes, prog_passes, xA, and z-scores"
|
| 113 |
+
],
|
| 114 |
+
inputs=msg,
|
| 115 |
+
label=""
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# ABA 2: DATA VISUALIZATION
|
| 119 |
+
with gr.Tab("Data Visualization", id=1):
|
| 120 |
+
gr.Markdown(
|
| 121 |
+
"""
|
| 122 |
+
### Interactive Visualizations
|
| 123 |
+
|
| 124 |
+
Charts and graphs generated by the AI assistant will appear here.
|
| 125 |
+
Request visualizations in the Analysis tab to see them rendered.
|
| 126 |
+
"""
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
chart_display = gr.Image(
|
| 130 |
+
value=PLACEHOLDER_IMAGE,
|
| 131 |
+
label="",
|
| 132 |
+
type="pil",
|
| 133 |
+
height=330,
|
| 134 |
+
show_label=False,
|
| 135 |
+
show_download_button=True,
|
| 136 |
+
show_share_button=False
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
with gr.Row():
|
| 140 |
+
refresh_btn = gr.Button("🔄 Refresh Visualization", variant="secondary", size="lg")
|
| 141 |
+
gr.Markdown("_Last updated: Live_", elem_classes="status-badge")
|
| 142 |
+
|
| 143 |
+
# Footer informativo
|
| 144 |
+
gr.Markdown(
|
| 145 |
+
"""
|
| 146 |
+
---
|
| 147 |
+
**Data Source:** SQL Database • **AI Model:** Groq-powered Analysis • **Coverage:** 1 Seasons (2020-2021)
|
| 148 |
+
"""
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
async def respond_and_update(message, history):
|
| 152 |
+
"""Responde e retorna tanto o chat quanto o gráfico atualizado"""
|
| 153 |
+
if not message.strip():
|
| 154 |
+
yield history, get_current_chart()
|
| 155 |
+
return
|
| 156 |
+
|
| 157 |
+
# Adicionar mensagem do usuário
|
| 158 |
+
history.append(gr.ChatMessage(role="user", content=message))
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
# Obter resposta
|
| 162 |
+
# full_response = await asyncio.wait_for(
|
| 163 |
+
# stream_agent_response_safe(message),
|
| 164 |
+
# timeout=120.0
|
| 165 |
+
# )
|
| 166 |
+
|
| 167 |
+
full_response = await agent_conventional_response(message)
|
| 168 |
+
|
| 169 |
+
# Adicionar mensagem inicial do assistente
|
| 170 |
+
history.append(gr.ChatMessage(role="assistant", content=""))
|
| 171 |
+
|
| 172 |
+
# Streaming adaptativo
|
| 173 |
+
async for text_chunk in simulate_streaming_adaptive(full_response):
|
| 174 |
+
history[-1] = gr.ChatMessage(role="assistant", content=text_chunk)
|
| 175 |
+
yield history, get_current_chart()
|
| 176 |
+
|
| 177 |
+
except asyncio.TimeoutError:
|
| 178 |
+
history.append([message, "⏱️ Timeout: consulta demorou muito"])
|
| 179 |
+
yield history, get_current_chart()
|
| 180 |
+
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f"Erro: {e}")
|
| 183 |
+
import traceback
|
| 184 |
+
traceback.print_exc()
|
| 185 |
+
history.append(gr.ChatMessage(role="assistant", content=f"⚠️ Error: {str(e)}"))
|
| 186 |
+
yield history, get_current_chart()
|
| 187 |
+
|
| 188 |
+
# Eventos
|
| 189 |
+
submit_btn.click(
|
| 190 |
+
fn=respond_and_update,
|
| 191 |
+
inputs=[msg, chatbot],
|
| 192 |
+
outputs=[chatbot, chart_display]
|
| 193 |
+
).then(
|
| 194 |
+
lambda: "",
|
| 195 |
+
None,
|
| 196 |
+
[msg]
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
msg.submit(
|
| 200 |
+
fn=respond_and_update,
|
| 201 |
+
inputs=[msg, chatbot],
|
| 202 |
+
outputs=[chatbot, chart_display]
|
| 203 |
+
).then(
|
| 204 |
+
lambda: "",
|
| 205 |
+
None,
|
| 206 |
+
[msg]
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
refresh_btn.click(
|
| 210 |
+
fn=refresh_chart,
|
| 211 |
+
inputs=[],
|
| 212 |
+
outputs=[chart_display]
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
demo.launch(ssr_mode=False, share=False)
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from barcelona-ai-agent-stats!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
main_agent.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from pydantic_ai import Agent, RunContext
|
| 3 |
+
from pydantic_ai.mcp import MCPServerStreamableHTTP
|
| 4 |
+
from pydantic_ai.providers.groq import GroqProvider
|
| 5 |
+
from pydantic_ai.models.groq import GroqModel
|
| 6 |
+
from pydantic_ai.messages import PartDeltaEvent, TextPartDelta
|
| 7 |
+
from pydantic_graph import End
|
| 8 |
+
|
| 9 |
+
# import logfire
|
| 10 |
+
# logfire.configure()
|
| 11 |
+
# logfire.instrument_pydantic_ai()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
#PROMPTS
|
| 15 |
+
from prompts import matches_prompt, players_prompt, player_matchid_prompt, graph_agent_prompt
|
| 16 |
+
MATCHES_SYSTEM_PROMPT = matches_prompt.MATCHES_SYSTEM_PROMPT
|
| 17 |
+
PLAYERS_SYSTEM_PROMPT = players_prompt.PLAYERS_SYSTEM_PROMPT
|
| 18 |
+
PLAYER_MATCHID_SYSTEM_PROMPT = player_matchid_prompt.SYSTEM_PROMPT
|
| 19 |
+
GRAPH_AGENT_PROMPT = graph_agent_prompt.GRAPH_AGENT_SYSTEM_PROMPT
|
| 20 |
+
|
| 21 |
+
import pandas as pd
|
| 22 |
+
from typing import List, Dict, Any, Optional
|
| 23 |
+
|
| 24 |
+
import matplotlib
|
| 25 |
+
matplotlib.use('Agg')
|
| 26 |
+
import matplotlib.pyplot as plt
|
| 27 |
+
import io
|
| 28 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
from dataclasses import dataclass
|
| 32 |
+
from dotenv import load_dotenv
|
| 33 |
+
load_dotenv()
|
| 34 |
+
import os
|
| 35 |
+
|
| 36 |
+
# Criar servidor MCP via stdio
|
| 37 |
+
mcp_players_server = MCPServerStreamableHTTP(
|
| 38 |
+
url = "http://127.0.0.1:8001/mcp"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
mcp_one_player = MCPServerStreamableHTTP(
|
| 42 |
+
url = "http://127.0.0.1:8003/mcp"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
mcp_matches_server = MCPServerStreamableHTTP(
|
| 46 |
+
url = "http://127.0.0.1:8002/mcp"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
mcp_graph_server = MCPServerStreamableHTTP(
|
| 50 |
+
url = "http://127.0.0.1:8004/mcp"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
ROUTER_SYSTEM_PROMPT = """
|
| 54 |
+
# You are an Expert Football Analyst with access to Barcelona match statistics from season 2020/2021.
|
| 55 |
+
You are connected to 2 agents connected to tools - one that can retrieve match statistics, one that can provide player match-level statistics based on match IDs.
|
| 56 |
+
Use both agents to answer user queries as needed.
|
| 57 |
+
Return to the user properly in a professional manner. Limit your answers to 1500 characters.
|
| 58 |
+
"""
|
| 59 |
+
from pydantic import BaseModel
|
| 60 |
+
from pydantic_ai import Agent, RunContext
|
| 61 |
+
class Deps(BaseModel):
|
| 62 |
+
ai_query: str
|
| 63 |
+
|
| 64 |
+
api_key = os.getenv("GROQ_DEV_API_KEY")
|
| 65 |
+
groq_model = GroqModel(
|
| 66 |
+
"moonshotai/kimi-k2-instruct-0905",
|
| 67 |
+
provider=GroqProvider(api_key=api_key)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
api_key_2 = os.getenv("GROQ_DEV_API_KEY_2")
|
| 71 |
+
groq_model_2 = GroqModel(
|
| 72 |
+
"openai/gpt-oss-20b",
|
| 73 |
+
provider=GroqProvider(api_key=api_key_2)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
main_agent = Agent(
|
| 77 |
+
model=groq_model_2,
|
| 78 |
+
system_prompt=ROUTER_SYSTEM_PROMPT,
|
| 79 |
+
deps_type=Deps,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
matches_agent = Agent(
|
| 83 |
+
model=groq_model,
|
| 84 |
+
system_prompt=ROUTER_SYSTEM_PROMPT,
|
| 85 |
+
toolsets=[mcp_matches_server]
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
players_agent = Agent(
|
| 89 |
+
model=groq_model_2,
|
| 90 |
+
system_prompt=ROUTER_SYSTEM_PROMPT,
|
| 91 |
+
toolsets=[mcp_players_server]
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
one_player_agent = Agent(
|
| 95 |
+
model=groq_model,
|
| 96 |
+
system_prompt=PLAYER_MATCHID_SYSTEM_PROMPT,
|
| 97 |
+
toolsets=[mcp_one_player]
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
graph_agent = Agent(
|
| 101 |
+
model=groq_model_2,
|
| 102 |
+
system_prompt=GRAPH_AGENT_PROMPT,
|
| 103 |
+
toolsets=[mcp_graph_server]
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# ============= NOVA FUNCIONALIDADE: VISUALIZAÇÕES =============
|
| 107 |
+
|
| 108 |
+
# Variável global para armazenar o último gráfico
|
| 109 |
+
last_chart_image = None
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def create_placeholder_image():
|
| 113 |
+
"""Cria uma imagem placeholder simples com texto"""
|
| 114 |
+
# Cores profissionais
|
| 115 |
+
bg_color = (248, 250, 252) # Slate-50
|
| 116 |
+
text_color = (100, 116, 139) # Slate-500
|
| 117 |
+
|
| 118 |
+
img = Image.new('RGB', (800, 600), color=bg_color)
|
| 119 |
+
draw = ImageDraw.Draw(img)
|
| 120 |
+
|
| 121 |
+
# Carregar fonte
|
| 122 |
+
try:
|
| 123 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 48)
|
| 124 |
+
except:
|
| 125 |
+
font = ImageFont.load_default()
|
| 126 |
+
|
| 127 |
+
# Texto centralizado
|
| 128 |
+
text = "Waiting for plot..."
|
| 129 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
| 130 |
+
text_width = bbox[2] - bbox[0]
|
| 131 |
+
text_height = bbox[3] - bbox[1]
|
| 132 |
+
|
| 133 |
+
x = (800 - text_width) // 2
|
| 134 |
+
y = (600 - text_height) // 2
|
| 135 |
+
|
| 136 |
+
draw.text((x, y), text, fill=text_color, font=font)
|
| 137 |
+
|
| 138 |
+
return img
|
| 139 |
+
|
| 140 |
+
last_chart_image = None
|
| 141 |
+
PLACEHOLDER_IMAGE = create_placeholder_image()
|
| 142 |
+
|
| 143 |
+
def get_current_chart():
|
| 144 |
+
"""Retorna o gráfico atual ou placeholder"""
|
| 145 |
+
global last_chart_image
|
| 146 |
+
return last_chart_image if last_chart_image is not None else PLACEHOLDER_IMAGE
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@main_agent.tool(name="get_matches_stats", description="Use this tool to get match-level statistics for FC Barcelona matches.")
|
| 150 |
+
async def get_matches_stats(ctx: RunContext[Deps], request_for_agent: str, retries=0) -> str:
|
| 151 |
+
print("Getting matches stats...")
|
| 152 |
+
async with asyncio.timeout(60):
|
| 153 |
+
resp = await matches_agent.run(request_for_agent)
|
| 154 |
+
return resp.output
|
| 155 |
+
|
| 156 |
+
@main_agent.tool(name="get_players_stats", description="Get overall statistics of players.", retries=0)
|
| 157 |
+
async def get_players_stats(ctx: RunContext[Deps], request_for_agent: str) -> str:
|
| 158 |
+
print("Getting players stats...")
|
| 159 |
+
async with asyncio.timeout(60):
|
| 160 |
+
resp = await players_agent.run(request_for_agent)
|
| 161 |
+
return resp.output
|
| 162 |
+
|
| 163 |
+
@main_agent.tool(name="get_one_player_stats", description="Use this tool to get match-level statistics for a specific Barcelona player based on match IDs.")
|
| 164 |
+
async def get_one_player_stats(ctx: RunContext[Deps], request_for_agent: str, retries=0) -> str:
|
| 165 |
+
print("Getting one player stats...")
|
| 166 |
+
async with asyncio.timeout(60):
|
| 167 |
+
resp = await one_player_agent.run(request_for_agent)
|
| 168 |
+
return resp.output
|
| 169 |
+
|
| 170 |
+
# @one_player_agent.tool(name="get_graph_passes_infomation", description="Get Deep Graph Passes information from specialized Agent.")
|
| 171 |
+
# async def get_graph_passes_infomation(ctx: RunContext[Deps], query: str) -> str:
|
| 172 |
+
# print("Getting passes information...")
|
| 173 |
+
# resp = await graph_agent.run(query)
|
| 174 |
+
# return resp.output
|
| 175 |
+
|
| 176 |
+
# @one_player_agent.tool(
|
| 177 |
+
# name="get_graph_passes_infomation",
|
| 178 |
+
# description="Get Deep Graph Passes information from specialized Agent."
|
| 179 |
+
# )
|
| 180 |
+
# async def get_graph_passes_infomation(ctx: RunContext[Deps], query: str) -> str:
|
| 181 |
+
# """
|
| 182 |
+
# ⚠️ CRÍTICO: Não use agent.run() dentro de agent.iter()!
|
| 183 |
+
# Isso causa nested cancel scopes.
|
| 184 |
+
|
| 185 |
+
# Solução: Use run_sync() ou conecte diretamente ao MCP.
|
| 186 |
+
# """
|
| 187 |
+
# try:
|
| 188 |
+
# # ✅ Opção 1: Use run_sync para evitar nesting
|
| 189 |
+
# import asyncio
|
| 190 |
+
# result = await asyncio.to_thread(
|
| 191 |
+
# lambda: asyncio.run(graph_agent.run(query))
|
| 192 |
+
# )
|
| 193 |
+
# return result.output
|
| 194 |
+
|
| 195 |
+
# except Exception as e:
|
| 196 |
+
# return f"❌ Erro ao acessar graph agent: {str(e)}"
|
| 197 |
+
|
| 198 |
+
@one_player_agent.tool(name="create_chart", retries=2)
|
| 199 |
+
async def create_chart(
|
| 200 |
+
ctx: RunContext[Deps],
|
| 201 |
+
data: List[Dict[str, Any]],
|
| 202 |
+
x_column: str,
|
| 203 |
+
y_column: str,
|
| 204 |
+
chart_type: str = "bar",
|
| 205 |
+
title: Optional[str] = None,
|
| 206 |
+
x_title: Optional[str] = None,
|
| 207 |
+
y_title: Optional[str] = None,
|
| 208 |
+
) -> str:
|
| 209 |
+
"""
|
| 210 |
+
Cria uma visualização dos dados.
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
data: Lista de dicionários com os dados
|
| 214 |
+
x_column: Coluna do eixo X
|
| 215 |
+
y_column: Coluna do eixo Y
|
| 216 |
+
chart_type: Tipo ('bar', 'line', 'scatter', 'horizontal_bar')
|
| 217 |
+
title: Título do gráfico
|
| 218 |
+
x_title: Título do eixo X
|
| 219 |
+
y_title: Título do eixo Y
|
| 220 |
+
"""
|
| 221 |
+
global last_chart_image
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
df = pd.DataFrame(data)
|
| 225 |
+
|
| 226 |
+
if x_column not in df.columns or y_column not in df.columns:
|
| 227 |
+
return f"❌ Erro: Colunas não encontradas. Disponíveis: {list(df.columns)}"
|
| 228 |
+
|
| 229 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 230 |
+
|
| 231 |
+
if chart_type == "bar":
|
| 232 |
+
bars = ax.bar(df[x_column], df[y_column], color='#4682B4',
|
| 233 |
+
edgecolor='#2F4F7F', linewidth=1.5)
|
| 234 |
+
for bar in bars:
|
| 235 |
+
height = bar.get_height()
|
| 236 |
+
ax.text(bar.get_x() + bar.get_width()/2., height,
|
| 237 |
+
f'{height:.0f}', ha='center', va='bottom', fontsize=9)
|
| 238 |
+
|
| 239 |
+
elif chart_type == "horizontal_bar":
|
| 240 |
+
ax.barh(df[x_column], df[y_column], color='#4682B4',
|
| 241 |
+
edgecolor='#2F4F7F', linewidth=1.5)
|
| 242 |
+
|
| 243 |
+
elif chart_type == "line":
|
| 244 |
+
ax.plot(df[x_column], df[y_column], marker='o', linewidth=2.5,
|
| 245 |
+
markersize=8, color='#4682B4', markerfacecolor='#FF6B6B')
|
| 246 |
+
ax.grid(True, alpha=0.3, linestyle='--')
|
| 247 |
+
|
| 248 |
+
elif chart_type == "scatter":
|
| 249 |
+
ax.scatter(df[x_column], df[y_column], s=100, alpha=0.6,
|
| 250 |
+
color='#4682B4', edgecolors='#2F4F7F', linewidth=1.5)
|
| 251 |
+
ax.grid(True, alpha=0.3, linestyle='--')
|
| 252 |
+
else:
|
| 253 |
+
return f"❌ Tipo '{chart_type}' não suportado"
|
| 254 |
+
|
| 255 |
+
ax.set_title(title or f"{y_column} por {x_column}",
|
| 256 |
+
fontsize=16, fontweight='bold', pad=20)
|
| 257 |
+
ax.set_xlabel(x_title or x_column, fontsize=12, fontweight='bold')
|
| 258 |
+
ax.set_ylabel(y_title or y_column, fontsize=12, fontweight='bold')
|
| 259 |
+
|
| 260 |
+
if chart_type != "horizontal_bar":
|
| 261 |
+
plt.xticks(rotation=45, ha='right')
|
| 262 |
+
|
| 263 |
+
ax.set_axisbelow(True)
|
| 264 |
+
ax.yaxis.grid(True, alpha=0.3, linestyle='--')
|
| 265 |
+
plt.tight_layout()
|
| 266 |
+
|
| 267 |
+
# Salvar imagem
|
| 268 |
+
buf = io.BytesIO()
|
| 269 |
+
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight',
|
| 270 |
+
facecolor='white')
|
| 271 |
+
buf.seek(0)
|
| 272 |
+
last_chart_image = Image.open(buf).copy()
|
| 273 |
+
plt.close()
|
| 274 |
+
|
| 275 |
+
return f"✅ Gráfico de **{chart_type}** criado! {len(df)} registros. Confira na aba **Visualização** 📊"
|
| 276 |
+
|
| 277 |
+
except Exception as e:
|
| 278 |
+
return f"❌ Erro ao criar gráfico: {str(e)}"
|
| 279 |
+
|
| 280 |
+
# async def stream_agent_response_safe(user_query: str) -> str:
|
| 281 |
+
# """
|
| 282 |
+
# Versão mais segura que sempre retorna a resposta completa ao final.
|
| 283 |
+
# Ideal para garantir que APENAS a resposta final seja exibida.
|
| 284 |
+
|
| 285 |
+
# Args:
|
| 286 |
+
# user_query: A query do usuário
|
| 287 |
+
|
| 288 |
+
# Yields:
|
| 289 |
+
# str: Resposta final completa (após processamento de todas as tools)
|
| 290 |
+
# """
|
| 291 |
+
|
| 292 |
+
# async with one_player_agent.iter(user_query) as agent_run:
|
| 293 |
+
# async for node in agent_run:
|
| 294 |
+
# if isinstance(node, End):
|
| 295 |
+
# if agent_run.result:
|
| 296 |
+
# return str(agent_run.result.output)
|
| 297 |
+
|
| 298 |
+
async def agent_conventional_response(user_query: str) -> str:
|
| 299 |
+
final_response = None
|
| 300 |
+
res = await one_player_agent.run(user_prompt=user_query)
|
| 301 |
+
final_response = res.output
|
| 302 |
+
return final_response
|
| 303 |
+
|
| 304 |
+
async def stream_agent_response_safe(user_query: str) -> str:
|
| 305 |
+
"""
|
| 306 |
+
Executa o agente com proteção contra cancelamento
|
| 307 |
+
"""
|
| 308 |
+
final_response = None
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
async with one_player_agent.iter(user_query) as agent_run:
|
| 312 |
+
async for node in agent_run:
|
| 313 |
+
if isinstance(node, End):
|
| 314 |
+
if agent_run.result:
|
| 315 |
+
final_response = str(agent_run.result.output)
|
| 316 |
+
break
|
| 317 |
+
|
| 318 |
+
except asyncio.CancelledError:
|
| 319 |
+
# Permite que o MCP client feche gracefully
|
| 320 |
+
print("⚠️ Agente cancelado - aguardando cleanup do MCP...")
|
| 321 |
+
await asyncio.sleep(0.5) # Dá tempo para cleanup
|
| 322 |
+
raise
|
| 323 |
+
|
| 324 |
+
except Exception as e:
|
| 325 |
+
print(f"❌ Erro no agente: {str(e)}")
|
| 326 |
+
import traceback
|
| 327 |
+
traceback.print_exc()
|
| 328 |
+
return f"Erro ao processar: {str(e)}"
|
| 329 |
+
|
| 330 |
+
return final_response if final_response else "Nenhuma resposta gerada"
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
# Teste
|
| 334 |
+
async def test_safe():
|
| 335 |
+
async with one_player_agent:
|
| 336 |
+
result = await one_player_agent.run("Which player has most yellow cards per time played - cards/min? Answer in cards per minute.")
|
| 337 |
+
print(result.output)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
if __name__ == "__main__":
|
| 341 |
+
asyncio.run(test_safe())
|
mcp_one_player_supabase.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import asyncio
|
| 4 |
+
import signal
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from functools import partial
|
| 7 |
+
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from psycopg2 import pool
|
| 10 |
+
from psycopg2.extras import RealDictCursor
|
| 11 |
+
|
| 12 |
+
from mcp.server.fastmcp import FastMCP
|
| 13 |
+
|
| 14 |
+
# =========================
|
| 15 |
+
# .env
|
| 16 |
+
# =========================
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
SUPABASE_DB_URI = os.getenv("SUPABASE_DB_URI")
|
| 21 |
+
|
| 22 |
+
DEFAULT_PORT = int(os.getenv("DEFAULT_PORT", "8003"))
|
| 23 |
+
|
| 24 |
+
if "YOUR_PASSWORD" in SUPABASE_DB_URI:
|
| 25 |
+
print("❌ ERRO: Defina SUPABASE_DB_URI com sua senha real.")
|
| 26 |
+
sys.exit(1)
|
| 27 |
+
|
| 28 |
+
# =========================
|
| 29 |
+
# Conexão Supabase (URI + SSL)
|
| 30 |
+
# =========================
|
| 31 |
+
class SupabaseConnection:
|
| 32 |
+
def __init__(self, dsn: str):
|
| 33 |
+
try:
|
| 34 |
+
self.pool = pool.ThreadedConnectionPool(
|
| 35 |
+
minconn=1,
|
| 36 |
+
maxconn=10,
|
| 37 |
+
dsn=dsn
|
| 38 |
+
)
|
| 39 |
+
print("✓ Pool criado")
|
| 40 |
+
|
| 41 |
+
conn = self.pool.getconn()
|
| 42 |
+
try:
|
| 43 |
+
with conn.cursor() as cur:
|
| 44 |
+
cur.execute("SELECT current_database(), inet_server_addr(), inet_server_port();")
|
| 45 |
+
db, ip, port = cur.fetchone()
|
| 46 |
+
print(f"✓ Conexão OK: db={db}, ip={ip}, port={port}")
|
| 47 |
+
finally:
|
| 48 |
+
self.pool.putconn(conn)
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print("❌ Falha na conexão:", e)
|
| 52 |
+
sys.exit(1)
|
| 53 |
+
|
| 54 |
+
async def execute_query_async(self, query: str, parameters: Optional[tuple] = None):
|
| 55 |
+
loop = asyncio.get_event_loop()
|
| 56 |
+
return await loop.run_in_executor(None, partial(self._execute_query_sync, query, parameters))
|
| 57 |
+
|
| 58 |
+
def _execute_query_sync(self, query: str, parameters: Optional[tuple] = None):
|
| 59 |
+
conn = None
|
| 60 |
+
try:
|
| 61 |
+
conn = self.pool.getconn()
|
| 62 |
+
conn.autocommit = True
|
| 63 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 64 |
+
if parameters:
|
| 65 |
+
cur.execute(query, parameters)
|
| 66 |
+
else:
|
| 67 |
+
cur.execute(query)
|
| 68 |
+
if cur.description:
|
| 69 |
+
return [dict(r) for r in cur.fetchall()]
|
| 70 |
+
return []
|
| 71 |
+
except Exception as e:
|
| 72 |
+
raise Exception(f"Erro ao executar query: {str(e)}")
|
| 73 |
+
finally:
|
| 74 |
+
if conn:
|
| 75 |
+
self.pool.putconn(conn)
|
| 76 |
+
|
| 77 |
+
def close(self):
|
| 78 |
+
if hasattr(self, 'pool') and self.pool:
|
| 79 |
+
self.pool.closeall()
|
| 80 |
+
print("✓ Pool fechado")
|
| 81 |
+
|
| 82 |
+
db = SupabaseConnection(SUPABASE_DB_URI)
|
| 83 |
+
|
| 84 |
+
# =========================
|
| 85 |
+
# MCP Server
|
| 86 |
+
# =========================
|
| 87 |
+
mcp = FastMCP(name="Football Player Match Analytics - Supabase", port=str(DEFAULT_PORT))
|
| 88 |
+
|
| 89 |
+
@mcp.tool()
|
| 90 |
+
async def execute_sql_query(query: str, limit: int = 100) -> str:
|
| 91 |
+
"""
|
| 92 |
+
Executa query SQL READ-ONLY.
|
| 93 |
+
"""
|
| 94 |
+
query_upper = query.upper().strip()
|
| 95 |
+
dangerous = ['DELETE', 'DROP', 'INSERT', 'UPDATE', 'ALTER', 'CREATE', 'TRUNCATE']
|
| 96 |
+
if any(k in query_upper for k in dangerous):
|
| 97 |
+
return "❌ ERRO: Apenas queries de leitura (SELECT) são permitidas."
|
| 98 |
+
try:
|
| 99 |
+
if 'LIMIT' not in query_upper:
|
| 100 |
+
query += f" LIMIT {limit}"
|
| 101 |
+
results = await db.execute_query_async(query)
|
| 102 |
+
if not results:
|
| 103 |
+
return "✓ Query executada, mas nenhum resultado encontrado."
|
| 104 |
+
lines = [f"📊 Resultados ({len(results)} registros):\n"]
|
| 105 |
+
for i, record in enumerate(results[:10], 1):
|
| 106 |
+
items = [f"{k}={v}" for k, v in record.items()]
|
| 107 |
+
lines.append(f"{i}. {', '.join(items)}")
|
| 108 |
+
if len(results) > 10:
|
| 109 |
+
lines.append(f"\n... e mais {len(results) - 10} resultados.")
|
| 110 |
+
return "\n".join(lines)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
return f"❌ Erro: {str(e)}"
|
| 113 |
+
|
| 114 |
+
@mcp.tool()
|
| 115 |
+
async def get_player_match_history(player_name: str, limit: int = 10) -> str:
|
| 116 |
+
query = """
|
| 117 |
+
SELECT
|
| 118 |
+
match_date, opponent, home_away, minutes_played,
|
| 119 |
+
goals, assists, shots, xg, pass_completion_pct, player_nickname
|
| 120 |
+
FROM player_match_stats
|
| 121 |
+
WHERE player_nickname ILIKE %s
|
| 122 |
+
ORDER BY match_date DESC
|
| 123 |
+
LIMIT %s
|
| 124 |
+
"""
|
| 125 |
+
try:
|
| 126 |
+
results = await db.execute_query_async(query, (f'%{player_name}%', limit))
|
| 127 |
+
if not results:
|
| 128 |
+
return f"❌ Nenhum dado encontrado para '{player_name}'"
|
| 129 |
+
out = [f"📊 HISTÓRICO DE PARTIDAS - {player_name.upper()}\n"]
|
| 130 |
+
for r in results:
|
| 131 |
+
xg = f"{(r.get('xg') or 0):.2f}"
|
| 132 |
+
pcp = f"{(r.get('pass_completion_pct') or 0):.1f}"
|
| 133 |
+
out.append(
|
| 134 |
+
f"📅 {r['match_date']} vs {r['opponent']} ({r['home_away']}) - "
|
| 135 |
+
f"{r['minutes_played']}min | "
|
| 136 |
+
f"⚽ {r['goals']} gols, 🤝 {r['assists']} assists | "
|
| 137 |
+
f"🎯 {r['shots']} chutes (xG: {xg}) | "
|
| 138 |
+
f"📈 {pcp}% passes"
|
| 139 |
+
)
|
| 140 |
+
return "\n".join(out)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
return f"❌ Erro: {str(e)}"
|
| 143 |
+
|
| 144 |
+
@mcp.tool()
|
| 145 |
+
async def get_match_performances(match_date: Optional[str] = None, opponent: Optional[str] = None, limit: int = 15) -> str:
|
| 146 |
+
if not match_date and not opponent:
|
| 147 |
+
return "❌ Forneça match_date OU opponent"
|
| 148 |
+
where_clauses, params = [], []
|
| 149 |
+
if match_date:
|
| 150 |
+
where_clauses.append("match_date = %s")
|
| 151 |
+
params.append(match_date)
|
| 152 |
+
if opponent:
|
| 153 |
+
where_clauses.append("opponent ILIKE %s")
|
| 154 |
+
params.append(f'%{opponent}%')
|
| 155 |
+
where_sql = " AND ".join(where_clauses)
|
| 156 |
+
params.append(limit)
|
| 157 |
+
query = f"""
|
| 158 |
+
SELECT
|
| 159 |
+
player_nickname, minutes_played, goals, assists,
|
| 160 |
+
(goals + assists) as contributions, shots, xg,
|
| 161 |
+
pass_completion_pct, touches
|
| 162 |
+
FROM player_match_stats
|
| 163 |
+
WHERE {where_sql}
|
| 164 |
+
ORDER BY (goals + assists) DESC, xg DESC
|
| 165 |
+
LIMIT %s
|
| 166 |
+
"""
|
| 167 |
+
try:
|
| 168 |
+
results = await db.execute_query_async(query, tuple(params))
|
| 169 |
+
if not results:
|
| 170 |
+
return "❌ Partida não encontrada"
|
| 171 |
+
info = f"{match_date or 'Data'} vs {opponent or 'Adversário'}"
|
| 172 |
+
out = [f"🏟️ PERFORMANCES - {info}\n"]
|
| 173 |
+
for i, r in enumerate(results, 1):
|
| 174 |
+
xg = f"{(r.get('xg') or 0):.2f}"
|
| 175 |
+
pcp = f"{(r.get('pass_completion_pct') or 0):.1f}"
|
| 176 |
+
touches = r.get('touches', 0)
|
| 177 |
+
out.append(
|
| 178 |
+
f"{i}. {r['player_nickname']} ({r['minutes_played']}min): "
|
| 179 |
+
f"⚽ {r['goals']}G + 🤝 {r['assists']}A = {r['contributions']} contrib. | "
|
| 180 |
+
f"🎯 {r['shots']} chutes (xG: {xg}) | "
|
| 181 |
+
f"📈 {pcp}% passes | "
|
| 182 |
+
f"👟 {touches} toques"
|
| 183 |
+
)
|
| 184 |
+
return "\n".join(out)
|
| 185 |
+
except Exception as e:
|
| 186 |
+
return f"❌ Erro: {str(e)}"
|
| 187 |
+
|
| 188 |
+
@mcp.tool()
|
| 189 |
+
async def get_top_performances(metric: str = "goals", limit: int = 10) -> str:
|
| 190 |
+
valid = {
|
| 191 |
+
"goals": ("goals", "Gols"),
|
| 192 |
+
"assists": ("assists", "Assistências"),
|
| 193 |
+
"contributions": ("goals + assists", "Contribuições"),
|
| 194 |
+
"xg": ("xg", "xG"),
|
| 195 |
+
"shots": ("shots", "Finalizações")
|
| 196 |
+
}
|
| 197 |
+
if metric not in valid:
|
| 198 |
+
return f"❌ Métrica inválida. Opções: {', '.join(valid.keys())}"
|
| 199 |
+
metric_sql, metric_name = valid[metric]
|
| 200 |
+
query = f"""
|
| 201 |
+
SELECT
|
| 202 |
+
player_nickname, match_date, opponent, home_away,
|
| 203 |
+
goals, assists, xg, shots, pass_completion_pct
|
| 204 |
+
FROM player_match_stats
|
| 205 |
+
ORDER BY {metric_sql} DESC
|
| 206 |
+
LIMIT %s
|
| 207 |
+
"""
|
| 208 |
+
try:
|
| 209 |
+
results = await db.execute_query_async(query, (limit,))
|
| 210 |
+
if not results:
|
| 211 |
+
return "❌ Nenhum dado encontrado"
|
| 212 |
+
out = [f"🏆 TOP {len(results)} PERFORMANCES - {metric_name.upper()}\n"]
|
| 213 |
+
for i, r in enumerate(results, 1):
|
| 214 |
+
xg = f"{(r.get('xg') or 0):.2f}"
|
| 215 |
+
out.append(
|
| 216 |
+
f"{i}. {r['player_nickname']} - {r['match_date']} vs {r['opponent']} ({r['home_away']}): "
|
| 217 |
+
f"⚽ {r['goals']}G, 🤝 {r['assists']}A, xG: {xg}"
|
| 218 |
+
)
|
| 219 |
+
return "\n".join(out)
|
| 220 |
+
except Exception as e:
|
| 221 |
+
return f"❌ Erro: {str(e)}"
|
| 222 |
+
|
| 223 |
+
# =========================
|
| 224 |
+
# Lifecycle robusto (evita CancelScope em task errada)
|
| 225 |
+
# =========================
|
| 226 |
+
shutdown_event = asyncio.Event()
|
| 227 |
+
|
| 228 |
+
def _handle_signal():
|
| 229 |
+
if not shutdown_event.is_set():
|
| 230 |
+
shutdown_event.set()
|
| 231 |
+
|
| 232 |
+
async def main():
|
| 233 |
+
loop = asyncio.get_running_loop()
|
| 234 |
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
| 235 |
+
try:
|
| 236 |
+
loop.add_signal_handler(sig, _handle_signal)
|
| 237 |
+
except NotImplementedError:
|
| 238 |
+
pass
|
| 239 |
+
|
| 240 |
+
try:
|
| 241 |
+
# print("=" * 70)
|
| 242 |
+
# print("🚀 SERVIDOR MCP - SUPABASE (URI)")
|
| 243 |
+
# print("=" * 70)
|
| 244 |
+
# print(f"🔗 DSN: {SUPABASE_DB_URI.split('@')[1] if '@' in SUPABASE_DB_URI else 'URI configurada'}")
|
| 245 |
+
# print(f"🌐 Porta HTTP MCP: {DEFAULT_PORT}")
|
| 246 |
+
# print("=" * 70)
|
| 247 |
+
|
| 248 |
+
server_task = asyncio.create_task(
|
| 249 |
+
mcp.run_streamable_http_async()
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
await shutdown_event.wait()
|
| 253 |
+
|
| 254 |
+
server_task.cancel()
|
| 255 |
+
try:
|
| 256 |
+
await server_task
|
| 257 |
+
except asyncio.CancelledError:
|
| 258 |
+
pass
|
| 259 |
+
finally:
|
| 260 |
+
db.close()
|
| 261 |
+
print("Servidor MCP finalizado.")
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
asyncio.run(main())
|
place_holder_image.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib
|
| 2 |
+
matplotlib.use('Agg')
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import io
|
| 5 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 6 |
+
|
| 7 |
+
def create_placeholder_image():
|
| 8 |
+
"""Cria uma imagem placeholder simples com texto"""
|
| 9 |
+
# Cores profissionais
|
| 10 |
+
bg_color = (248, 250, 252) # Slate-50
|
| 11 |
+
text_color = (100, 116, 139) # Slate-500
|
| 12 |
+
|
| 13 |
+
img = Image.new('RGB', (800, 600), color=bg_color)
|
| 14 |
+
draw = ImageDraw.Draw(img)
|
| 15 |
+
|
| 16 |
+
# Carregar fonte
|
| 17 |
+
try:
|
| 18 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 48)
|
| 19 |
+
except:
|
| 20 |
+
font = ImageFont.load_default()
|
| 21 |
+
|
| 22 |
+
# Texto centralizado
|
| 23 |
+
text = "Waiting for plot..."
|
| 24 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
| 25 |
+
text_width = bbox[2] - bbox[0]
|
| 26 |
+
text_height = bbox[3] - bbox[1]
|
| 27 |
+
|
| 28 |
+
x = (800 - text_width) // 2
|
| 29 |
+
y = (600 - text_height) // 2
|
| 30 |
+
|
| 31 |
+
draw.text((x, y), text, fill=text_color, font=font)
|
| 32 |
+
|
| 33 |
+
return img
|
| 34 |
+
|
| 35 |
+
last_chart_image = None
|
| 36 |
+
PLACEHOLDER_IMAGE = create_placeholder_image()
|
| 37 |
+
|
| 38 |
+
def get_current_chart():
|
| 39 |
+
"""Retorna o gráfico atual ou placeholder"""
|
| 40 |
+
global last_chart_image
|
| 41 |
+
return last_chart_image if last_chart_image is not None else PLACEHOLDER_IMAGE
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
|
prompts/__init__.py
ADDED
|
File without changes
|
prompts/graph_agent_prompt.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GRAPH_AGENT_SYSTEM_PROMPT = """
|
| 2 |
+
# You are an Expert Football Analyst with access to Barcelona graphdatabase from five seasons - from 2016/2017 to 2020/2021.
|
| 3 |
+
Respond only about season 2020/2021. You respond back to a Agent, so be concise.
|
| 4 |
+
Use the tools available to respond the user query. You are connected to neo4j by a MCP server created by Frederico Caixeta.
|
| 5 |
+
Base your analysis **ONLY** by the query results. If the database can't provide what the user is
|
| 6 |
+
asking for, report that in a professional way.
|
| 7 |
+
# Below you can find some cypher queries as example, so you can understand which artifacts and metadatas are available in the database:
|
| 8 |
+
// General Overview: players, connections, goals per temporada
|
| 9 |
+
MATCH (p:Player {team: "Barcelona"})
|
| 10 |
+
OPTIONAL MATCH (p)-[r:PASSED_TO]->()
|
| 11 |
+
OPTIONAL MATCH (g:GoalSequence {team: "Barcelona"})
|
| 12 |
+
RETURN
|
| 13 |
+
count(DISTINCT p) as TotalPlayers,
|
| 14 |
+
count(DISTINCT r) as TotalPassConnections,
|
| 15 |
+
sum(r.weight) as TotalPasses,
|
| 16 |
+
count(DISTINCT g) as TotalGoalSequences,
|
| 17 |
+
collect(DISTINCT p.season_date) as Seasons
|
| 18 |
+
// List all seasons available in neo4j
|
| 19 |
+
MATCH (p:Player)
|
| 20 |
+
RETURN DISTINCT p.season_date as Season, p.season_id as SeasonID
|
| 21 |
+
ORDER BY p.season_date
|
| 22 |
+
// Top 5 connections per season
|
| 23 |
+
MATCH (p1:Player)-[r:PASSED_TO]->(p2:Player)
|
| 24 |
+
WITH p1.season_date as Season, p1.name as P1, p2.name as P2, r.weight as Weight
|
| 25 |
+
ORDER BY Weight DESC
|
| 26 |
+
WITH Season, collect({passer: P1, receiver: P2, passes: Weight})[0..5] as TopConnections
|
| 27 |
+
RETURN Season, TopConnections
|
| 28 |
+
ORDER BY Season
|
| 29 |
+
// Connections between different zones in field
|
| 30 |
+
MATCH (p1:Player)-[r:PASSED_TO]->(p2:Player)
|
| 31 |
+
WHERE p1.season_id = 90
|
| 32 |
+
WITH p1, p2, r,
|
| 33 |
+
CASE WHEN p1.avg_x < 40 THEN 'Def' WHEN p1.avg_x < 80 THEN 'Mid' ELSE 'Att' END as Zone1,
|
| 34 |
+
CASE WHEN p2.avg_x < 40 THEN 'Def' WHEN p2.avg_x < 80 THEN 'Mid' ELSE 'Att' END as Zone2
|
| 35 |
+
WHERE Zone1 <> Zone2
|
| 36 |
+
RETURN Zone1 + ' -> ' + Zone2 as Transition, sum(r.weight) as TotalPasses
|
| 37 |
+
ORDER BY TotalPasses DESC
|
| 38 |
+
// Total number of sequences of goals that Rakitić (a player) was involved
|
| 39 |
+
MATCH (p:Player {name: "Ivan Rakitić"})-[:INVOLVED_IN]->(g:GoalSequence)
|
| 40 |
+
RETURN count(g) as TotalGoalSequences
|
| 41 |
+
# The Property Keys available are: avg_x, avg_y, data, end_x, end_y, id, match_id, name, nodes, num_passes, order, possession, relationships, season_date, season_id, sequence_id, style, team, visualisation, weight, x, y.
|
| 42 |
+
The Nodes are: Player, GoalSequence.
|
| 43 |
+
The Relationships are: INVOLVED_IN, PASSED_IN_SEQUENCE, PASSED_TO.
|
| 44 |
+
The seasons ids and their dates: [{90: '2020/2021'}, {42: '2019/2020'}, {4: '2018/2019'}, {1: '2017/2018'}, {2: '2016/2017'}]
|
| 45 |
+
# All players played in all seasons are:
|
| 46 |
+
Abel Ruiz Ortega
|
| 47 |
+
Aleix Vidal Parreu
|
| 48 |
+
André Filipe Tavares Gomes
|
| 49 |
+
Andrés Iniesta Luján
|
| 50 |
+
Anssumane Fati
|
| 51 |
+
Antoine Griezmann
|
| 52 |
+
Arda Turan
|
| 53 |
+
Arthur Henrique Ramos de Oliveira Melo
|
| 54 |
+
Arturo Erasmo Vidal Pardo
|
| 55 |
+
Carles Aleña Castillo
|
| 56 |
+
Carles Pérez Sayol
|
| 57 |
+
Claudio Andrés Bravo Muñoz
|
| 58 |
+
Clément Lenglet
|
| 59 |
+
Denis Suárez Fernández
|
| 60 |
+
Francisco Alcácer García
|
| 61 |
+
Francisco António Machado Mota de Castro Trincão
|
| 62 |
+
Frenkie de Jong
|
| 63 |
+
Gerard Deulofeu Lázaro
|
| 64 |
+
Gerard Piqué Bernabéu
|
| 65 |
+
Héctor Junior Firpo Adames
|
| 66 |
+
Ivan Rakitić
|
| 67 |
+
Jasper Cillessen
|
| 68 |
+
Javier Alejandro Mascherano
|
| 69 |
+
Jean-Clair Todibo
|
| 70 |
+
Jordi Alba Ramos
|
| 71 |
+
José Manuel Arnáiz Díaz
|
| 72 |
+
José Paulo Bezzera Maciel Júnior
|
| 73 |
+
Jérémy Mathieu
|
| 74 |
+
Kevin-Prince Boateng
|
| 75 |
+
Lionel Andrés Messi Cuccittini
|
| 76 |
+
Lucas Digne
|
| 77 |
+
Luis Alberto Suárez Díaz
|
| 78 |
+
Malcom Filipe Silva de Oliveira
|
| 79 |
+
Marc-André ter Stegen
|
| 80 |
+
Marlon Santos da Silva Barbosa
|
| 81 |
+
Martin Braithwaite Christensen
|
| 82 |
+
Miralem Pjanić
|
| 83 |
+
Moriba Kourouma Kourouma
|
| 84 |
+
Moussa Wagué
|
| 85 |
+
Munir El Haddadi Mohamed
|
| 86 |
+
Neymar da Silva Santos Junior
|
| 87 |
+
Norberto Murara Neto
|
| 88 |
+
Nélson Cabral Semedo
|
| 89 |
+
Ousmane Dembélé
|
| 90 |
+
Pedro González López
|
| 91 |
+
Philippe Coutinho Correia
|
| 92 |
+
Rafael Alcântara do Nascimento
|
| 93 |
+
Ricard Puig Martí
|
| 94 |
+
Ronald Federico Araújo da Silva
|
| 95 |
+
Samuel Yves Umtiti
|
| 96 |
+
Sergi Roberto Carnicer
|
| 97 |
+
Sergino Dest
|
| 98 |
+
Sergio Busquets i Burgos
|
| 99 |
+
Thomas Vermaelen
|
| 100 |
+
Yerry Fernando Mina González
|
| 101 |
+
Álex Collado Gutiérrez
|
| 102 |
+
Óscar Mingueza García
|
| 103 |
+
"""
|
prompts/matches_prompt.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MATCHES_SYSTEM_PROMPT = """
|
| 2 |
+
# You are an Expert Football Analyst with access to Barcelona match statistics from season 2020/2021.
|
| 3 |
+
You respond back to an AI agent that will after respond back to the user. That meaans you should be concise and to the point - respond **only** the statistics needed to answer the query.
|
| 4 |
+
You are connected to a **DuckDB database** via MCP server created by Frederico Caixeta with a single table called 'barcelona' containing aggregated match statistics.
|
| 5 |
+
## Instructions:
|
| 6 |
+
- Use the available SQL tools to respond to user queries
|
| 7 |
+
- Use a maximum of 3 SQL queries to gather the necessary data to answer the user's question
|
| 8 |
+
- Base your analysis **ONLY** on query results from the database
|
| 9 |
+
- If the database cannot provide what the user is asking for, report that professionally
|
| 10 |
+
- Limit your answer to **1500 characters**
|
| 11 |
+
- When users ask for visualizations, graphs, or charts, you MUST use the create_chart tool
|
| 12 |
+
- Always cite your sources from the database queries
|
| 13 |
+
## Database Structure:
|
| 14 |
+
### Table: barcelona
|
| 15 |
+
Contains one row per match with 35+ metrics:
|
| 16 |
+
**Match Information:**
|
| 17 |
+
- match_date (DATE)
|
| 18 |
+
- opponent (VARCHAR)
|
| 19 |
+
- home_away (VARCHAR) - "Home" or "Away"
|
| 20 |
+
**Possession & Control:**
|
| 21 |
+
- possession (DOUBLE) - Proportion 0-1 (multiply by 100 for %)
|
| 22 |
+
- touches (INTEGER)
|
| 23 |
+
- final_third_entries (INTEGER)
|
| 24 |
+
- passes_into_box (INTEGER)
|
| 25 |
+
**Passing:**
|
| 26 |
+
- passes_attempted (INTEGER)
|
| 27 |
+
- passes_completed (INTEGER)
|
| 28 |
+
- pass_completion_pct (DOUBLE) - Already as percentage
|
| 29 |
+
- progressive_passes (INTEGER) - Passes advancing ≥10m
|
| 30 |
+
- through_balls (INTEGER)
|
| 31 |
+
- long_passes_attempted (INTEGER)
|
| 32 |
+
**Shooting:**
|
| 33 |
+
- shots (INTEGER)
|
| 34 |
+
- shots_on_target (INTEGER)
|
| 35 |
+
- shot_on_target_pct (DOUBLE)
|
| 36 |
+
- goals (INTEGER)
|
| 37 |
+
- xg_total (DOUBLE) - Expected Goals
|
| 38 |
+
- xg_per_shot (DOUBLE)
|
| 39 |
+
**Defense:**
|
| 40 |
+
- pressures (INTEGER)
|
| 41 |
+
- pressure_regains (INTEGER)
|
| 42 |
+
- tackles (INTEGER)
|
| 43 |
+
- interceptions (INTEGER)
|
| 44 |
+
- blocks (INTEGER)
|
| 45 |
+
**Offensive Actions:**
|
| 46 |
+
- carries (INTEGER)
|
| 47 |
+
- progressive_carries (INTEGER)
|
| 48 |
+
- dribbles_attempted (INTEGER)
|
| 49 |
+
- dribble_success_pct (DOUBLE)
|
| 50 |
+
**Set Pieces & Discipline:**
|
| 51 |
+
- corners (INTEGER)
|
| 52 |
+
- free_kicks (INTEGER)
|
| 53 |
+
- penalties (INTEGER)
|
| 54 |
+
- fouls_committed (INTEGER)
|
| 55 |
+
- fouls_won (INTEGER)
|
| 56 |
+
- yellow_cards (INTEGER)
|
| 57 |
+
## Example SQL Queries:
|
| 58 |
+
### Season Summary
|
| 59 |
+
```sql
|
| 60 |
+
SELECT
|
| 61 |
+
COUNT(*) as total_matches,
|
| 62 |
+
ROUND(AVG(possession) * 100, 1) as avg_possession_pct,
|
| 63 |
+
SUM(goals) as total_goals,
|
| 64 |
+
ROUND(AVG(xg_total), 2) as avg_xg
|
| 65 |
+
FROM barcelona
|
| 66 |
+
```
|
| 67 |
+
### Top 5 Matches by xG
|
| 68 |
+
```sql
|
| 69 |
+
SELECT
|
| 70 |
+
match_date,
|
| 71 |
+
opponent,
|
| 72 |
+
home_away,
|
| 73 |
+
xg_total,
|
| 74 |
+
goals,
|
| 75 |
+
shots
|
| 76 |
+
FROM barcelona
|
| 77 |
+
ORDER BY xg_total DESC
|
| 78 |
+
LIMIT 5
|
| 79 |
+
```
|
| 80 |
+
### Home vs Away Performance
|
| 81 |
+
```sql
|
| 82 |
+
SELECT
|
| 83 |
+
home_away,
|
| 84 |
+
COUNT(*) as matches,
|
| 85 |
+
ROUND(AVG(possession) * 100, 1) as avg_possession,
|
| 86 |
+
SUM(goals) as total_goals,
|
| 87 |
+
ROUND(AVG(xg_total), 2) as avg_xg
|
| 88 |
+
FROM barcelona
|
| 89 |
+
GROUP BY home_away
|
| 90 |
+
```
|
| 91 |
+
### Search Specific Matches
|
| 92 |
+
```sql
|
| 93 |
+
SELECT
|
| 94 |
+
match_date,
|
| 95 |
+
opponent,
|
| 96 |
+
goals,
|
| 97 |
+
xg_total,
|
| 98 |
+
possession * 100 as possession_pct
|
| 99 |
+
FROM barcelona
|
| 100 |
+
WHERE opponent LIKE '%Real Madrid%'
|
| 101 |
+
OR goals >= 4
|
| 102 |
+
OR xg_total > 3.0
|
| 103 |
+
ORDER BY match_date
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### Efficiency Analysis: Goals vs xG
|
| 107 |
+
```sql
|
| 108 |
+
SELECT
|
| 109 |
+
match_date,
|
| 110 |
+
opponent,
|
| 111 |
+
goals,
|
| 112 |
+
xg_total,
|
| 113 |
+
ROUND(goals - xg_total, 2) as xg_difference,
|
| 114 |
+
CASE
|
| 115 |
+
WHEN goals > xg_total THEN 'Overperformance'
|
| 116 |
+
WHEN goals < xg_total THEN 'Underperformance'
|
| 117 |
+
ELSE 'As Expected'
|
| 118 |
+
END as performance
|
| 119 |
+
FROM barcelona
|
| 120 |
+
ORDER BY (goals - xg_total) DESC
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### Possession Impact Analysis
|
| 124 |
+
```sql
|
| 125 |
+
SELECT
|
| 126 |
+
CASE
|
| 127 |
+
WHEN possession >= 0.70 THEN 'Very High (≥70%)'
|
| 128 |
+
WHEN possession >= 0.60 THEN 'High (60-70%)'
|
| 129 |
+
WHEN possession >= 0.50 THEN 'Medium (50-60%)'
|
| 130 |
+
ELSE 'Low (<50%)'
|
| 131 |
+
END as possession_range,
|
| 132 |
+
COUNT(*) as matches,
|
| 133 |
+
ROUND(AVG(xg_total), 2) as avg_xg,
|
| 134 |
+
ROUND(AVG(goals), 2) as avg_goals
|
| 135 |
+
FROM barcelona
|
| 136 |
+
GROUP BY possession_range
|
| 137 |
+
ORDER BY AVG(possession) DESC
|
| 138 |
+
```
|
| 139 |
+
### Shooting Efficiency
|
| 140 |
+
```sql
|
| 141 |
+
SELECT
|
| 142 |
+
match_date,
|
| 143 |
+
opponent,
|
| 144 |
+
shots,
|
| 145 |
+
shots_on_target,
|
| 146 |
+
goals,
|
| 147 |
+
ROUND((CAST(goals AS DOUBLE) / shots) * 100, 1) as conversion_pct,
|
| 148 |
+
xg_total
|
| 149 |
+
FROM barcelona
|
| 150 |
+
WHERE shots > 0
|
| 151 |
+
ORDER BY conversion_pct DESC
|
| 152 |
+
LIMIT 10
|
| 153 |
+
```
|
| 154 |
+
### Progressive Play Analysis
|
| 155 |
+
```sql
|
| 156 |
+
SELECT
|
| 157 |
+
match_date,
|
| 158 |
+
opponent,
|
| 159 |
+
progressive_passes,
|
| 160 |
+
progressive_carries,
|
| 161 |
+
passes_into_box,
|
| 162 |
+
final_third_entries,
|
| 163 |
+
xg_total,
|
| 164 |
+
goals
|
| 165 |
+
FROM barcelona
|
| 166 |
+
ORDER BY (progressive_passes + progressive_carries) DESC
|
| 167 |
+
LIMIT 10
|
| 168 |
+
```
|
| 169 |
+
### Defensive Intensity
|
| 170 |
+
```sql
|
| 171 |
+
SELECT
|
| 172 |
+
match_date,
|
| 173 |
+
opponent,
|
| 174 |
+
pressures,
|
| 175 |
+
pressure_regains,
|
| 176 |
+
tackles + interceptions + blocks as defensive_actions,
|
| 177 |
+
ROUND((CAST(pressure_regains AS DOUBLE) / pressures) * 100, 1) as pressure_success_pct
|
| 178 |
+
FROM barcelona
|
| 179 |
+
WHERE pressures > 0
|
| 180 |
+
ORDER BY pressures DESC
|
| 181 |
+
LIMIT 10
|
| 182 |
+
```
|
| 183 |
+
## Key Metrics Explained:
|
| 184 |
+
- **xG (Expected Goals)**: Statistical model predicting goal probability (0-1 per shot)
|
| 185 |
+
- **Progressive Pass**: Pass advancing ≥10m toward opponent's goal
|
| 186 |
+
- **Progressive Carry**: Ball carry advancing ≥5m toward opponent's goal
|
| 187 |
+
- **Possession**: Calculated as team possessions / total possessions
|
| 188 |
+
- **Pressure Success**: Proportion of pressures resulting in ball recovery
|
| 189 |
+
|
| 190 |
+
## Analysis Tips:
|
| 191 |
+
1. Use aggregation (AVG, SUM, COUNT) for season-level insights
|
| 192 |
+
2. Use WHERE clauses to filter specific opponents or date ranges
|
| 193 |
+
3. Use ORDER BY to find top/bottom performances
|
| 194 |
+
4. Use CASE statements for categorical analysis
|
| 195 |
+
5. Compare metrics across home_away to understand venue impact
|
| 196 |
+
6. Analyze correlation between possession, xG, and goals
|
| 197 |
+
7. Identify overperformance (goals > xG) and underperformance (goals < xG)
|
| 198 |
+
## Available Tools:
|
| 199 |
+
- execute_sql_query(query, limit)
|
| 200 |
+
- get_season_summary()
|
| 201 |
+
- get_top_matches(metric, limit)
|
| 202 |
+
- analyze_match(opponent, date)
|
| 203 |
+
- compare_home_away()
|
| 204 |
+
- search_matches(opponent, min_xg, min_goals, min_possession, home_away, limit)
|
| 205 |
+
- analyze_efficiency(metric)
|
| 206 |
+
- show_available_columns()
|
| 207 |
+
**Remember:** All analyses must be based on SQL queries against the 'barcelona' table. Use the tools provided to access the data.
|
| 208 |
+
|
| 209 |
+
# All players played in the seasons are:
|
| 210 |
+
Lionel Messi
|
| 211 |
+
Antoine Griezmann
|
| 212 |
+
Frenkie de Jong
|
| 213 |
+
Gerard Piqué
|
| 214 |
+
Jordi Alba
|
| 215 |
+
Marc-André ter Stegen
|
| 216 |
+
Clément Lenglet
|
| 217 |
+
Ansu Fati
|
| 218 |
+
Trincão
|
| 219 |
+
Junior Firpo
|
| 220 |
+
Martin Braithwaite, Pedri, Riqui Puig, Sergi Roberto, Sergio Busquets, Sergiño Dest, Ronald Araújo, Óscar Mingueza, Ilaix Moriba, Neto, Samuel Umtiti, Miralem Pjanić
|
| 221 |
+
Ousmane Dembélé.
|
| 222 |
+
"""
|
prompts/player_matchid_prompt.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SYSTEM_PROMPT = """
|
| 2 |
+
# You are an Expert Football Analyst with access to Barcelona player match-level statistics from season 2020/2021.
|
| 3 |
+
You respond back to an AI agent that will after respond back to the user. That meaans you should be concise and to the point - respond **only** the statistics needed to answer the query.
|
| 4 |
+
You are connected to a **DuckDB database** via MCP server created by Frederico Caixeta with a table called 'player_match_stats' containing individual player performance in each match.
|
| 5 |
+
|
| 6 |
+
## Instructions:
|
| 7 |
+
- Use a maximum of 3 SQL queries to gather the necessary data to answer the user's question
|
| 8 |
+
- Use the available SQL tools to respond to user queries
|
| 9 |
+
- You have two math tools - multiply and divide - to help with calculations
|
| 10 |
+
- Base your analysis **ONLY** on query results from the database
|
| 11 |
+
- If the database cannot provide what the user is asking for, report that professionally
|
| 12 |
+
- You have a tool - get_graph_passes_infomation - to get deep-graph-detailed passing information from an Agent
|
| 13 |
+
- Always cite your sources from the database queries
|
| 14 |
+
- If it is suitable for visualizations, graphs, or charts, (or if the user asks for it) you can use the create_chart tool
|
| 15 |
+
|
| 16 |
+
## Database Structure:
|
| 17 |
+
### Table: player_match_stats
|
| 18 |
+
Contains one row per player per match with 40 metrics:
|
| 19 |
+
**Identification (8 columns):**
|
| 20 |
+
- player_name (VARCHAR) - Full name
|
| 21 |
+
- player_nickname (VARCHAR) - Common nickname
|
| 22 |
+
- match_id (INTEGER) - Unique match identifier
|
| 23 |
+
- match_date (DATE) - Match date
|
| 24 |
+
- opponent (VARCHAR) - Opponent team
|
| 25 |
+
- home_away (VARCHAR) - "Home" or "Away"
|
| 26 |
+
- season_id (INTEGER)
|
| 27 |
+
- season_name (VARCHAR) - "2020/2021"
|
| 28 |
+
|
| 29 |
+
**Participation (2 columns):**
|
| 30 |
+
- minutes_played (INTEGER) - Minutes played (approximation)
|
| 31 |
+
- touches (INTEGER) - Ball touches
|
| 32 |
+
|
| 33 |
+
**Passing (7 columns):**
|
| 34 |
+
- passes_attempted (INTEGER)
|
| 35 |
+
- passes_completed (INTEGER)
|
| 36 |
+
- pass_completion_pct (DOUBLE) - Percentage
|
| 37 |
+
- progressive_passes (INTEGER) - Passes advancing ≥10m
|
| 38 |
+
- key_passes (INTEGER) - Passes leading to shot
|
| 39 |
+
- assists (INTEGER)
|
| 40 |
+
- passes_received (INTEGER)
|
| 41 |
+
**Shooting (6 columns):**
|
| 42 |
+
- shots (INTEGER)
|
| 43 |
+
- shots_on_target (INTEGER)
|
| 44 |
+
- shot_on_target_pct (DOUBLE) - Percentage
|
| 45 |
+
- goals (INTEGER)
|
| 46 |
+
- xg (DOUBLE) - Expected Goals
|
| 47 |
+
- npxg (DOUBLE) - Non-penalty xG
|
| 48 |
+
**Defense (6 columns):**
|
| 49 |
+
- tackles (INTEGER)
|
| 50 |
+
- interceptions (INTEGER)
|
| 51 |
+
- blocks (INTEGER)
|
| 52 |
+
- clearances (INTEGER)
|
| 53 |
+
- pressures (INTEGER)
|
| 54 |
+
- pressure_regains (INTEGER)
|
| 55 |
+
**Possession (4 columns):**
|
| 56 |
+
- dribbles_attempted (INTEGER) - Dribble attempts
|
| 57 |
+
- dribbles_completed (INTEGER) - Successful dribbles
|
| 58 |
+
- carries (INTEGER) - Ball carries
|
| 59 |
+
- progressive_carries (INTEGER) - Carries advancing ≥5m
|
| 60 |
+
**Position (2 columns):**
|
| 61 |
+
- avg_position_x (DOUBLE) - Average X position (0-120)
|
| 62 |
+
- avg_position_y (DOUBLE) - Average Y position (0-80)
|
| 63 |
+
**Additional Metrics (3+ columns):**
|
| 64 |
+
- `fouls_committed` (INTEGER)
|
| 65 |
+
- `fouls_won` (INTEGER)
|
| 66 |
+
- `yellow_cards` (INTEGER)
|
| 67 |
+
- `red_cards` (INTEGER)
|
| 68 |
+
|
| 69 |
+
## Example SQL Queries:
|
| 70 |
+
### All Matches for a Player
|
| 71 |
+
```sql
|
| 72 |
+
SELECT
|
| 73 |
+
match_date,
|
| 74 |
+
opponent,
|
| 75 |
+
home_away,
|
| 76 |
+
goals,
|
| 77 |
+
assists,
|
| 78 |
+
shots,
|
| 79 |
+
pass_completion_pct
|
| 80 |
+
FROM player_match_stats
|
| 81 |
+
WHERE player_nickname LIKE '%Messi%'
|
| 82 |
+
ORDER BY match_date
|
| 83 |
+
```
|
| 84 |
+
### Top Individual Performances
|
| 85 |
+
```sql
|
| 86 |
+
SELECT
|
| 87 |
+
player_nickname,
|
| 88 |
+
match_date,
|
| 89 |
+
opponent,
|
| 90 |
+
goals,
|
| 91 |
+
assists,
|
| 92 |
+
goals + assists as contributions,
|
| 93 |
+
xg
|
| 94 |
+
FROM player_match_stats
|
| 95 |
+
WHERE goals + assists >= 2
|
| 96 |
+
ORDER BY (goals + assists) DESC, xg DESC
|
| 97 |
+
LIMIT 10
|
| 98 |
+
```
|
| 99 |
+
### Player Evolution (Moving Average)
|
| 100 |
+
```sql
|
| 101 |
+
SELECT
|
| 102 |
+
match_date,
|
| 103 |
+
opponent,
|
| 104 |
+
goals,
|
| 105 |
+
AVG(goals) OVER (
|
| 106 |
+
PARTITION BY player_nickname
|
| 107 |
+
ORDER BY match_date
|
| 108 |
+
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
|
| 109 |
+
) as avg_goals_last_3_games
|
| 110 |
+
FROM player_match_stats
|
| 111 |
+
WHERE player_nickname LIKE '%Messi%'
|
| 112 |
+
ORDER BY match_date
|
| 113 |
+
```
|
| 114 |
+
### Performance vs Specific Opponent
|
| 115 |
+
```sql
|
| 116 |
+
SELECT
|
| 117 |
+
player_nickname,
|
| 118 |
+
COUNT(*) as matches,
|
| 119 |
+
SUM(goals) as total_goals,
|
| 120 |
+
SUM(assists) as total_assists,
|
| 121 |
+
ROUND(AVG(pass_completion_pct), 1) as avg_pass_acc
|
| 122 |
+
FROM player_match_stats
|
| 123 |
+
WHERE opponent LIKE '%Real Madrid%'
|
| 124 |
+
GROUP BY player_nickname
|
| 125 |
+
ORDER BY (SUM(goals) + SUM(assists)) DESC
|
| 126 |
+
```
|
| 127 |
+
### Player Consistency Analysis
|
| 128 |
+
```sql
|
| 129 |
+
SELECT
|
| 130 |
+
player_nickname,
|
| 131 |
+
COUNT(*) as matches,
|
| 132 |
+
ROUND(AVG(goals), 2) as avg_goals_per_match,
|
| 133 |
+
ROUND(STDDEV(goals), 2) as goals_std_dev,
|
| 134 |
+
MAX(goals) as best_performance,
|
| 135 |
+
COUNT(CASE WHEN goals > 0 THEN 1 END) as matches_scored
|
| 136 |
+
FROM player_match_stats
|
| 137 |
+
GROUP BY player_nickname
|
| 138 |
+
HAVING COUNT(*) >= 10
|
| 139 |
+
ORDER BY avg_goals_per_match DESC
|
| 140 |
+
```
|
| 141 |
+
### Recent Form (Last 5 Matches)
|
| 142 |
+
```sql
|
| 143 |
+
SELECT
|
| 144 |
+
player_nickname,
|
| 145 |
+
match_date,
|
| 146 |
+
opponent,
|
| 147 |
+
goals,
|
| 148 |
+
assists,
|
| 149 |
+
pass_completion_pct
|
| 150 |
+
FROM (
|
| 151 |
+
SELECT *,
|
| 152 |
+
ROW_NUMBER() OVER (
|
| 153 |
+
PARTITION BY player_nickname
|
| 154 |
+
ORDER BY match_date DESC
|
| 155 |
+
) as rn
|
| 156 |
+
FROM player_match_stats
|
| 157 |
+
) recent
|
| 158 |
+
WHERE rn <= 5 AND player_nickname LIKE '%Messi%'
|
| 159 |
+
ORDER BY match_date DESC
|
| 160 |
+
```
|
| 161 |
+
### Hat-tricks and Multi-goal Games
|
| 162 |
+
```sql
|
| 163 |
+
SELECT
|
| 164 |
+
player_nickname,
|
| 165 |
+
match_date,
|
| 166 |
+
opponent,
|
| 167 |
+
goals,
|
| 168 |
+
assists,
|
| 169 |
+
shots,
|
| 170 |
+
xg
|
| 171 |
+
FROM player_match_stats
|
| 172 |
+
WHERE goals >= 2
|
| 173 |
+
ORDER BY goals DESC, match_date
|
| 174 |
+
```
|
| 175 |
+
### Players in Specific Match
|
| 176 |
+
```sql
|
| 177 |
+
SELECT
|
| 178 |
+
player_nickname,
|
| 179 |
+
minutes_played,
|
| 180 |
+
goals,
|
| 181 |
+
assists,
|
| 182 |
+
shots,
|
| 183 |
+
pass_completion_pct,
|
| 184 |
+
touches
|
| 185 |
+
FROM player_match_stats
|
| 186 |
+
WHERE match_date = '2020-10-24'
|
| 187 |
+
AND opponent LIKE '%Real Madrid%'
|
| 188 |
+
ORDER BY (goals + assists) DESC
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
## Key Metrics Explained:
|
| 192 |
+
- **xG (Expected Goals)**: Statistical model predicting goal probability per shot
|
| 193 |
+
- **npxG (Non-Penalty xG)**: Expected Goals excluding penalties
|
| 194 |
+
- **Progressive Pass**: Pass advancing ≥10m toward opponent's goal
|
| 195 |
+
- **Progressive Carry**: Ball carry advancing ≥5m toward opponent's goal
|
| 196 |
+
- **Key Pass**: Pass that leads directly to a shot
|
| 197 |
+
- **Pressure Regain**: Successful pressure resulting in ball recovery
|
| 198 |
+
- **Position (X, Y)**: Average position on field (X: 0-120 length, Y: 0-80 width)
|
| 199 |
+
|
| 200 |
+
## Analysis Tips:
|
| 201 |
+
1. Use **ORDER BY match_date** to see chronological progression
|
| 202 |
+
2. Use **WINDOW FUNCTIONS** for moving averages and trends
|
| 203 |
+
3. Use **ROW_NUMBER()** to identify top performances per match
|
| 204 |
+
4. Use **PARTITION BY player_nickname** for player-specific analysis
|
| 205 |
+
5. Use **HAVING** to filter aggregated results
|
| 206 |
+
6. Compare **goals vs xG** to identify overperformance/underperformance
|
| 207 |
+
7. Use **STDDEV()** to measure consistency
|
| 208 |
+
8. Filter by **minutes_played** to focus on substantial appearances
|
| 209 |
+
|
| 210 |
+
## Available Tools:
|
| 211 |
+
- get_graph_passes_infomation(query: str)
|
| 212 |
+
- execute_sql_query(query, limit)
|
| 213 |
+
- get_player_match_history(player_name, limit)
|
| 214 |
+
- get_match_performances(match_date, opponent)
|
| 215 |
+
- get_top_performances(metric, limit)
|
| 216 |
+
- get_player_form(player_name, num_matches)
|
| 217 |
+
- compare_player_matches(player1, player2, opponent)
|
| 218 |
+
- get_consistency_stats(player_name)
|
| 219 |
+
- get_home_away_comparison(player_name)
|
| 220 |
+
- show_available_columns()
|
| 221 |
+
**Remember:** All analyses must be based on SQL queries against the 'player_match_stats' table. This table provides MATCH-LEVEL granularity, allowing you to track individual performances game by game.
|
| 222 |
+
Players of the season: Lionel Messi, Antoine Griezmann, Ousmane Dembélé, Ansu Fati, Martin Braithwaite
|
| 223 |
+
Sergio Busquets, Frenkie de Jong, Pedri, Riqui Puig, Miralem Pjanić, Philippe Coutinho
|
| 224 |
+
Gerard Piqué, Jordi Alba, Clément Lenglet, Sergi Roberto, Sergiño Dest, Ronald Araújo, Óscar Mingueza, Samuel Umtiti, Junior Firpo
|
| 225 |
+
Marc-André ter Stegen, Neto
|
| 226 |
+
"""
|
prompts/players_prompt.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PLAYERS_SYSTEM_PROMPT = """
|
| 2 |
+
# You are an Expert Football Analyst with access to Barcelona player statistics from season 2020/2021.
|
| 3 |
+
You respond back to an AI agent that will after respond back to the user. That meaans you should be concise and to the point - respond **only** the statistics needed to answer the query.
|
| 4 |
+
# You are connected to a **DuckDB database** via MCP server created by Frederico Caixeta with a single table called 'players' containing individual player statistics per season.
|
| 5 |
+
|
| 6 |
+
## Instructions:
|
| 7 |
+
- Use the available SQL tools to respond to user queries
|
| 8 |
+
- Use a maximum of 3 SQL queries to gather the necessary data to answer the user's question
|
| 9 |
+
- Base your analysis **ONLY** on query results from the database
|
| 10 |
+
- If the database cannot provide what the user is asking for, report that professionally
|
| 11 |
+
- Limit your answer to **1500 characters**
|
| 12 |
+
- When users ask for visualizations, graphs, or charts, you MUST use the create_chart tool
|
| 13 |
+
- Always cite your sources from the database queries
|
| 14 |
+
|
| 15 |
+
## Database Structure:
|
| 16 |
+
|
| 17 |
+
### Table: players
|
| 18 |
+
Contains one row per player per season with 39 metrics:
|
| 19 |
+
|
| 20 |
+
**Player Information:**
|
| 21 |
+
- player_name (VARCHAR) - Full name
|
| 22 |
+
- player_nickname (VARCHAR) - Common nickname/short name
|
| 23 |
+
- season_id (INTEGER)
|
| 24 |
+
- season_name (VARCHAR) - Ex: "2020/2021"
|
| 25 |
+
- team (VARCHAR)
|
| 26 |
+
|
| 27 |
+
**Match Participation:**
|
| 28 |
+
- matches_played (INTEGER)
|
| 29 |
+
- minutes_played (INTEGER)
|
| 30 |
+
|
| 31 |
+
**Passing (8 columns):**
|
| 32 |
+
- passes_attempted (INTEGER)
|
| 33 |
+
- passes_completed (INTEGER)
|
| 34 |
+
- pass_completion_pct (DOUBLE) - Percentage
|
| 35 |
+
- progressive_passes (INTEGER) - Passes advancing ≥10m
|
| 36 |
+
- key_passes (INTEGER) - Passes leading to shot
|
| 37 |
+
- assists (INTEGER)
|
| 38 |
+
- passes_received (INTEGER)
|
| 39 |
+
- progressive_passes_received (INTEGER)
|
| 40 |
+
|
| 41 |
+
**Shooting (6 columns):**
|
| 42 |
+
- shots (INTEGER)
|
| 43 |
+
- shots_on_target (INTEGER)
|
| 44 |
+
- shot_on_target_pct (DOUBLE) - Percentage
|
| 45 |
+
- goals (INTEGER)
|
| 46 |
+
- xg (DOUBLE) - Expected Goals
|
| 47 |
+
- npxg (DOUBLE) - Non-penalty xG
|
| 48 |
+
|
| 49 |
+
**Defense (6 columns):**
|
| 50 |
+
- tackles (INTEGER)
|
| 51 |
+
- interceptions (INTEGER)
|
| 52 |
+
- blocks (INTEGER)
|
| 53 |
+
- clearances (INTEGER)
|
| 54 |
+
- pressures (INTEGER)
|
| 55 |
+
- pressure_regains (INTEGER)
|
| 56 |
+
|
| 57 |
+
**Offensive Actions (5 columns):**
|
| 58 |
+
- dribbles_attempted (INTEGER)
|
| 59 |
+
- dribbles_completed (INTEGER)
|
| 60 |
+
- dribble_success_pct (DOUBLE) - Percentage
|
| 61 |
+
- carries (INTEGER)
|
| 62 |
+
- progressive_carries (INTEGER) - Carries advancing ≥5m
|
| 63 |
+
|
| 64 |
+
**Discipline (4 columns):**
|
| 65 |
+
- fouls_committed (INTEGER)
|
| 66 |
+
- fouls_won (INTEGER)
|
| 67 |
+
- yellow_cards (INTEGER)
|
| 68 |
+
- red_cards (INTEGER)
|
| 69 |
+
|
| 70 |
+
**Other (3 columns):**
|
| 71 |
+
- touches (INTEGER)
|
| 72 |
+
- avg_position_x (DOUBLE) - Average X position on field (0-120)
|
| 73 |
+
- avg_position_y (DOUBLE) - Average Y position on field (0-80)
|
| 74 |
+
|
| 75 |
+
## Example SQL Queries:
|
| 76 |
+
|
| 77 |
+
### Top 10 Scorers (All Seasons)
|
| 78 |
+
```sql
|
| 79 |
+
SELECT
|
| 80 |
+
player_nickname,
|
| 81 |
+
SUM(goals) as total_goals,
|
| 82 |
+
SUM(xg) as total_xg,
|
| 83 |
+
ROUND(SUM(goals) - SUM(xg), 2) as goals_vs_xg,
|
| 84 |
+
SUM(shots) as total_shots,
|
| 85 |
+
COUNT(DISTINCT season_name) as seasons
|
| 86 |
+
FROM players
|
| 87 |
+
GROUP BY player_nickname
|
| 88 |
+
ORDER BY total_goals DESC
|
| 89 |
+
LIMIT 10
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### Player Stats in Specific Season
|
| 93 |
+
```sql
|
| 94 |
+
SELECT
|
| 95 |
+
player_nickname,
|
| 96 |
+
matches_played,
|
| 97 |
+
goals,
|
| 98 |
+
assists,
|
| 99 |
+
xg,
|
| 100 |
+
pass_completion_pct,
|
| 101 |
+
dribble_success_pct
|
| 102 |
+
FROM players
|
| 103 |
+
WHERE player_nickname LIKE '%Messi%'
|
| 104 |
+
AND season_name = '2020/2021'
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Compare Two Players (All Seasons)
|
| 108 |
+
```sql
|
| 109 |
+
SELECT
|
| 110 |
+
player_nickname,
|
| 111 |
+
SUM(goals) as goals,
|
| 112 |
+
SUM(assists) as assists,
|
| 113 |
+
SUM(passes_attempted) as passes,
|
| 114 |
+
ROUND(AVG(pass_completion_pct), 1) as pass_acc,
|
| 115 |
+
SUM(dribbles_attempted) as dribbles,
|
| 116 |
+
ROUND(AVG(dribble_success_pct), 1) as dribble_acc
|
| 117 |
+
FROM players
|
| 118 |
+
WHERE player_nickname IN ('Messi', 'Suárez')
|
| 119 |
+
GROUP BY player_nickname
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### Top Assisters by Season
|
| 123 |
+
```sql
|
| 124 |
+
SELECT
|
| 125 |
+
player_nickname,
|
| 126 |
+
season_name,
|
| 127 |
+
assists,
|
| 128 |
+
key_passes,
|
| 129 |
+
progressive_passes
|
| 130 |
+
FROM players
|
| 131 |
+
WHERE season_name = '2019/2020'
|
| 132 |
+
ORDER BY assists DESC
|
| 133 |
+
LIMIT 10
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### Players by Field Position
|
| 137 |
+
```sql
|
| 138 |
+
SELECT
|
| 139 |
+
player_nickname,
|
| 140 |
+
ROUND(AVG(avg_position_x), 1) as avg_x,
|
| 141 |
+
ROUND(AVG(avg_position_y), 1) as avg_y,
|
| 142 |
+
CASE
|
| 143 |
+
WHEN AVG(avg_position_x) < 40 THEN 'Defense'
|
| 144 |
+
WHEN AVG(avg_position_x) < 80 THEN 'Midfield'
|
| 145 |
+
ELSE 'Attack'
|
| 146 |
+
END as position_zone,
|
| 147 |
+
SUM(matches_played) as total_matches
|
| 148 |
+
FROM players
|
| 149 |
+
WHERE avg_position_x IS NOT NULL
|
| 150 |
+
GROUP BY player_nickname
|
| 151 |
+
ORDER BY AVG(avg_position_x)
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Shooting Efficiency Analysis
|
| 155 |
+
```sql
|
| 156 |
+
SELECT
|
| 157 |
+
player_nickname,
|
| 158 |
+
SUM(goals) as goals,
|
| 159 |
+
SUM(xg) as xg,
|
| 160 |
+
ROUND(SUM(goals) - SUM(xg), 2) as efficiency,
|
| 161 |
+
SUM(shots) as shots,
|
| 162 |
+
ROUND(100.0 * SUM(goals) / SUM(shots), 1) as conversion_pct
|
| 163 |
+
FROM players
|
| 164 |
+
WHERE shots >= 50
|
| 165 |
+
GROUP BY player_nickname
|
| 166 |
+
ORDER BY efficiency DESC
|
| 167 |
+
LIMIT 10
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
### Top Passers (Volume + Quality)
|
| 171 |
+
```sql
|
| 172 |
+
SELECT
|
| 173 |
+
player_nickname,
|
| 174 |
+
SUM(passes_attempted) as total_passes,
|
| 175 |
+
ROUND(AVG(pass_completion_pct), 1) as avg_accuracy,
|
| 176 |
+
SUM(progressive_passes) as progressive,
|
| 177 |
+
SUM(key_passes) as key_passes,
|
| 178 |
+
SUM(assists) as assists
|
| 179 |
+
FROM players
|
| 180 |
+
WHERE passes_attempted >= 100
|
| 181 |
+
GROUP BY player_nickname
|
| 182 |
+
ORDER BY progressive DESC
|
| 183 |
+
LIMIT 10
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
### Best Dribblers
|
| 187 |
+
```sql
|
| 188 |
+
SELECT
|
| 189 |
+
player_nickname,
|
| 190 |
+
SUM(dribbles_attempted) as attempted,
|
| 191 |
+
SUM(dribbles_completed) as completed,
|
| 192 |
+
ROUND(AVG(dribble_success_pct), 1) as success_rate,
|
| 193 |
+
SUM(progressive_carries) as prog_carries
|
| 194 |
+
FROM players
|
| 195 |
+
WHERE dribbles_attempted >= 50
|
| 196 |
+
GROUP BY player_nickname
|
| 197 |
+
ORDER BY completed DESC
|
| 198 |
+
LIMIT 10
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
### Defensive Leaders
|
| 202 |
+
```sql
|
| 203 |
+
SELECT
|
| 204 |
+
player_nickname,
|
| 205 |
+
SUM(pressures) as total_pressures,
|
| 206 |
+
SUM(pressure_regains) as regains,
|
| 207 |
+
ROUND(100.0 * SUM(pressure_regains) / SUM(pressures), 1) as success_pct,
|
| 208 |
+
SUM(tackles) as tackles,
|
| 209 |
+
SUM(interceptions) as interceptions
|
| 210 |
+
FROM players
|
| 211 |
+
WHERE pressures >= 100
|
| 212 |
+
GROUP BY player_nickname
|
| 213 |
+
ORDER BY total_pressures DESC
|
| 214 |
+
LIMIT 10
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
### Player Evolution Across Seasons
|
| 218 |
+
```sql
|
| 219 |
+
SELECT
|
| 220 |
+
season_name,
|
| 221 |
+
goals,
|
| 222 |
+
assists,
|
| 223 |
+
xg,
|
| 224 |
+
pass_completion_pct,
|
| 225 |
+
dribble_success_pct,
|
| 226 |
+
matches_played
|
| 227 |
+
FROM players
|
| 228 |
+
WHERE player_nickname LIKE '%Messi%'
|
| 229 |
+
ORDER BY season_name
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
## Key Metrics Explained:
|
| 233 |
+
|
| 234 |
+
- **xG (Expected Goals)**: Statistical model predicting goal probability (0-1 per shot)
|
| 235 |
+
- **npxG (Non-Penalty xG)**: Expected Goals excluding penalties
|
| 236 |
+
- **Progressive Pass**: Pass advancing ≥10m toward opponent's goal
|
| 237 |
+
- **Progressive Carry**: Ball carry advancing ≥5m toward opponent's goal
|
| 238 |
+
- **Key Pass**: Pass that leads directly to a shot
|
| 239 |
+
- **Pressure Regain**: Successful pressure resulting in ball recovery
|
| 240 |
+
- **Position (X, Y)**: Average position on field (X: 0-120 length, Y: 0-80 width)
|
| 241 |
+
|
| 242 |
+
## Analysis Tips:
|
| 243 |
+
|
| 244 |
+
1. Use **SUM()** to aggregate stats across seasons (goals, assists, passes)
|
| 245 |
+
2. Use **AVG()** for percentages and averages (pass accuracy, position)
|
| 246 |
+
3. Use **COUNT(DISTINCT season_name)** to count seasons played
|
| 247 |
+
4. Use **WHERE player_nickname LIKE '%Name%'** for flexible player search
|
| 248 |
+
5. Use **HAVING** after GROUP BY to filter aggregated results
|
| 249 |
+
6. Compare **goals vs xG** to identify clinical finishers or underperformers
|
| 250 |
+
7. Analyze **position (avg_x, avg_y)** to understand player roles
|
| 251 |
+
8. Use **ORDER BY** with multiple columns for nuanced rankings
|
| 252 |
+
|
| 253 |
+
## Position Zones Reference:
|
| 254 |
+
|
| 255 |
+
- **Defense**: avg_position_x < 40
|
| 256 |
+
- **Midfield**: 40 ≤ avg_position_x < 80
|
| 257 |
+
- **Attack**: avg_position_x ≥ 80
|
| 258 |
+
|
| 259 |
+
## Available Tools:
|
| 260 |
+
|
| 261 |
+
- execute_sql_query(query, limit)
|
| 262 |
+
- get_player_stats(player_name, season)
|
| 263 |
+
- get_top_scorers(limit, season)
|
| 264 |
+
- get_top_assisters(limit, season)
|
| 265 |
+
- compare_players(player1, player2, season)
|
| 266 |
+
- get_players_by_position(position_zone, limit)
|
| 267 |
+
- get_top_passers(limit, season)
|
| 268 |
+
- get_top_dribblers(limit, season)
|
| 269 |
+
- get_defensive_leaders(limit, season)
|
| 270 |
+
- show_available_columns()
|
| 271 |
+
|
| 272 |
+
**Remember:** All analyses must be based on SQL queries against the 'players' table. Use the tools provided to access the data.
|
| 273 |
+
|
| 274 |
+
## Key Players in Database (5 Seasons):
|
| 275 |
+
|
| 276 |
+
**Forwards:** Lionel Messi, Luis Suárez, Neymar, Antoine Griezmann, Ousmane Dembélé, Ansu Fati, Martin Braithwaite
|
| 277 |
+
|
| 278 |
+
**Midfielders:** Sergio Busquets, Ivan Rakitić, Andrés Iniesta, Frenkie de Jong, Arthur, Arturo Vidal, Philippe Coutinho, Pedri, Riqui Puig, Miralem Pjanić
|
| 279 |
+
|
| 280 |
+
**Defenders:** Gerard Piqué, Jordi Alba, Sergi Roberto, Samuel Umtiti, Clément Lenglet, Junior Firpo, Sergiño Dest, Ronald Araújo, Óscar Mingueza, Nélson Semedo
|
| 281 |
+
|
| 282 |
+
**Goalkeepers:** Marc-André ter Stegen, Claudio Bravo, Jasper Cillessen, Neto
|
| 283 |
+
|
| 284 |
+
*Note: Player availability varies by season (2016/2017 to 2020/2021)*
|
| 285 |
+
"""
|
pyproject.toml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "barcelona-ai-agent-stats"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"duckdb>=1.4.1",
|
| 9 |
+
"matplotlib>=3.10.7",
|
| 10 |
+
"neo4j>=6.0.2",
|
| 11 |
+
"pandas>=2.3.3",
|
| 12 |
+
"psycopg2>=2.9.11",
|
| 13 |
+
"pydantic-ai>=1.2.1",
|
| 14 |
+
"pydantic-ai-slim[logfire]>=1.2.1",
|
| 15 |
+
"python-dotenv>=1.1.1",
|
| 16 |
+
"tavily-python>=0.7.12",
|
| 17 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pydantic-ai>=1.1.0
|
| 2 |
+
tavily-python>=0.7.12
|
| 3 |
+
neo4j>=6.0.2
|
| 4 |
+
matplotlib>=3.10.7
|
| 5 |
+
pandas>=2.3.3
|
| 6 |
+
duckdb>=1.4.1
|
| 7 |
+
psycopg2>=2.9.11
|
teste.ipynb
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "2338caab",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [
|
| 9 |
+
{
|
| 10 |
+
"name": "stdout",
|
| 11 |
+
"output_type": "stream",
|
| 12 |
+
"text": [
|
| 13 |
+
"✓ Conectado ao Supabase: aws-1-us-east-1.pooler.supabase.com\n",
|
| 14 |
+
"✓ Database 'postgres' possui 2 tabela(s)\n",
|
| 15 |
+
"✓ Tabela 'player_match_stats' encontrada com 40 colunas\n"
|
| 16 |
+
]
|
| 17 |
+
}
|
| 18 |
+
],
|
| 19 |
+
"source": [
|
| 20 |
+
"import mcp_one_player_supabase as mcp"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"cell_type": "code",
|
| 25 |
+
"execution_count": 2,
|
| 26 |
+
"id": "eda8d380",
|
| 27 |
+
"metadata": {},
|
| 28 |
+
"outputs": [],
|
| 29 |
+
"source": [
|
| 30 |
+
"result = await mcp.get_player_match_history(player_name=\"Messi\")"
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"cell_type": "code",
|
| 35 |
+
"execution_count": 3,
|
| 36 |
+
"id": "80558806",
|
| 37 |
+
"metadata": {},
|
| 38 |
+
"outputs": [
|
| 39 |
+
{
|
| 40 |
+
"name": "stdout",
|
| 41 |
+
"output_type": "stream",
|
| 42 |
+
"text": [
|
| 43 |
+
"📊 HISTÓRICO DE PARTIDAS - MESSI\n",
|
| 44 |
+
"\n",
|
| 45 |
+
"📅 2021-05-16 vs Celta Vigo (Home) - 92min | ⚽ 1 gols, 🤝 0 assists | 🎯 8 chutes (xG: 0.69) | 📈 83.1% passes\n",
|
| 46 |
+
"📅 2021-05-11 vs Levante UD (Away) - 93min | ⚽ 1 gols, 🤝 0 assists | 🎯 6 chutes (xG: 0.60) | 📈 80.8% passes\n",
|
| 47 |
+
"📅 2021-05-08 vs Atlético Madrid (Home) - 91min | ⚽ 0 gols, 🤝 0 assists | 🎯 3 chutes (xG: 0.18) | 📈 83.0% passes\n",
|
| 48 |
+
"📅 2021-05-02 vs Valencia (Away) - 92min | ⚽ 2 gols, 🤝 0 assists | 🎯 5 chutes (xG: 1.41) | 📈 77.9% passes\n",
|
| 49 |
+
"📅 2021-04-29 vs Granada (Home) - 94min | ⚽ 1 gols, 🤝 0 assists | 🎯 6 chutes (xG: 0.64) | 📈 81.6% passes\n",
|
| 50 |
+
"📅 2021-04-25 vs Villarreal (Away) - 95min | ⚽ 0 gols, 🤝 0 assists | 🎯 3 chutes (xG: 0.15) | 📈 89.8% passes\n",
|
| 51 |
+
"📅 2021-04-22 vs Getafe (Home) - 91min | ⚽ 2 gols, 🤝 1 assists | 🎯 7 chutes (xG: 0.62) | 📈 79.7% passes\n",
|
| 52 |
+
"📅 2021-04-10 vs Real Madrid (Away) - 92min | ⚽ 0 gols, 🤝 0 assists | 🎯 7 chutes (xG: 0.46) | 📈 85.7% passes\n",
|
| 53 |
+
"📅 2021-04-05 vs Real Valladolid (Home) - 91min | ⚽ 0 gols, 🤝 0 assists | 🎯 7 chutes (xG: 0.46) | 📈 83.6% passes\n",
|
| 54 |
+
"📅 2021-03-21 vs Real Sociedad (Away) - 88min | ⚽ 2 gols, 🤝 1 assists | 🎯 5 chutes (xG: 0.71) | 📈 85.1% passes\n"
|
| 55 |
+
]
|
| 56 |
+
}
|
| 57 |
+
],
|
| 58 |
+
"source": [
|
| 59 |
+
"print(result)"
|
| 60 |
+
]
|
| 61 |
+
}
|
| 62 |
+
],
|
| 63 |
+
"metadata": {
|
| 64 |
+
"kernelspec": {
|
| 65 |
+
"display_name": "barcelona-ai-agent-stats (3.12.10)",
|
| 66 |
+
"language": "python",
|
| 67 |
+
"name": "python3"
|
| 68 |
+
},
|
| 69 |
+
"language_info": {
|
| 70 |
+
"codemirror_mode": {
|
| 71 |
+
"name": "ipython",
|
| 72 |
+
"version": 3
|
| 73 |
+
},
|
| 74 |
+
"file_extension": ".py",
|
| 75 |
+
"mimetype": "text/x-python",
|
| 76 |
+
"name": "python",
|
| 77 |
+
"nbconvert_exporter": "python",
|
| 78 |
+
"pygments_lexer": "ipython3",
|
| 79 |
+
"version": "3.12.10"
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
"nbformat": 4,
|
| 83 |
+
"nbformat_minor": 5
|
| 84 |
+
}
|
utils.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# CSS Profissional
|
| 3 |
+
CUSTOM_CSS = """
|
| 4 |
+
/* Importar fontes profissionais do Google Fonts */
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
| 6 |
+
|
| 7 |
+
/* Aplicar fonte Inter em todo o app */
|
| 8 |
+
* {
|
| 9 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* Fonte monospace para código */
|
| 13 |
+
code, pre, .message code {
|
| 14 |
+
font-family: 'JetBrains Mono', 'Courier New', monospace !important;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* Header principal */
|
| 18 |
+
.gradio-container h1 {
|
| 19 |
+
font-size: 2.5rem !important;
|
| 20 |
+
font-weight: 700 !important;
|
| 21 |
+
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%);
|
| 22 |
+
-webkit-background-clip: text;
|
| 23 |
+
-webkit-text-fill-color: transparent;
|
| 24 |
+
background-clip: text;
|
| 25 |
+
margin-bottom: 1rem !important;
|
| 26 |
+
letter-spacing: -0.02em;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Subtítulos */
|
| 30 |
+
.gradio-container h3 {
|
| 31 |
+
font-size: 1.25rem !important;
|
| 32 |
+
font-weight: 600 !important;
|
| 33 |
+
color: #1e293b;
|
| 34 |
+
margin-bottom: 1rem !important;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Descrições */
|
| 38 |
+
.gradio-container p {
|
| 39 |
+
font-size: 1rem !important;
|
| 40 |
+
color: #475569;
|
| 41 |
+
line-height: 1.6;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* Tabs - Estilo profissional */
|
| 45 |
+
.tabs button {
|
| 46 |
+
font-size: 1rem !important;
|
| 47 |
+
font-weight: 600 !important;
|
| 48 |
+
padding: 0.75rem 1.5rem !important;
|
| 49 |
+
border-radius: 0.5rem 0.5rem 0 0 !important;
|
| 50 |
+
transition: all 0.2s ease !important;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.tabs button[aria-selected="true"] {
|
| 54 |
+
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%) !important;
|
| 55 |
+
color: white !important;
|
| 56 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.tabs button:not([aria-selected="true"]) {
|
| 60 |
+
background: #f1f5f9 !important;
|
| 61 |
+
color: #64748b !important;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.tabs button:hover:not([aria-selected="true"]) {
|
| 65 |
+
background: #e2e8f0 !important;
|
| 66 |
+
color: #334155 !important;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* Botões principais */
|
| 70 |
+
button[variant="primary"] {
|
| 71 |
+
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%) !important;
|
| 72 |
+
border: none !important;
|
| 73 |
+
font-weight: 600 !important;
|
| 74 |
+
font-size: 1rem !important;
|
| 75 |
+
padding: 0.75rem 1.5rem !important;
|
| 76 |
+
border-radius: 0.5rem !important;
|
| 77 |
+
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3) !important;
|
| 78 |
+
transition: all 0.3s ease !important;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
button[variant="primary"]:hover {
|
| 82 |
+
transform: translateY(-2px);
|
| 83 |
+
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.4) !important;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* Botões secundários */
|
| 87 |
+
button[variant="secondary"] {
|
| 88 |
+
background: #f8fafc !important;
|
| 89 |
+
border: 2px solid #cbd5e1 !important;
|
| 90 |
+
color: #475569 !important;
|
| 91 |
+
font-weight: 500 !important;
|
| 92 |
+
border-radius: 0.5rem !important;
|
| 93 |
+
transition: all 0.2s ease !important;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
button[variant="secondary"]:hover {
|
| 97 |
+
background: #e2e8f0 !important;
|
| 98 |
+
border-color: #94a3b8 !important;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* Chatbot - Design limpo e profissional */
|
| 102 |
+
.message-wrap {
|
| 103 |
+
padding: 1rem !important;
|
| 104 |
+
margin: 0.5rem 0 !important;
|
| 105 |
+
border-radius: 0.75rem !important;
|
| 106 |
+
font-size: 1rem !important;
|
| 107 |
+
line-height: 1.6 !important;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.message-wrap.user {
|
| 111 |
+
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%) !important;
|
| 112 |
+
border-left: 4px solid #3b82f6 !important;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.message-wrap.bot {
|
| 116 |
+
background: #f8fafc !important;
|
| 117 |
+
border-left: 4px solid #10b981 !important;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* Input de texto */
|
| 121 |
+
.gr-textbox textarea {
|
| 122 |
+
font-size: 1rem !important;
|
| 123 |
+
padding: 0.875rem !important;
|
| 124 |
+
border-radius: 0.5rem !important;
|
| 125 |
+
border: 2px solid #e2e8f0 !important;
|
| 126 |
+
transition: all 0.2s ease !important;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.gr-textbox textarea:focus {
|
| 130 |
+
border-color: #3b82f6 !important;
|
| 131 |
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
| 132 |
+
outline: none !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Exemplos */
|
| 136 |
+
.examples {
|
| 137 |
+
border-radius: 0.75rem !important;
|
| 138 |
+
padding: 1rem !important;
|
| 139 |
+
background: #f8fafc !important;
|
| 140 |
+
border: 1px solid #e2e8f0 !important;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.examples button {
|
| 144 |
+
background: white !important;
|
| 145 |
+
border: 1px solid #e2e8f0 !important;
|
| 146 |
+
border-radius: 0.5rem !important;
|
| 147 |
+
padding: 0.75rem 1rem !important;
|
| 148 |
+
font-size: 0.95rem !important;
|
| 149 |
+
color: #475569 !important;
|
| 150 |
+
transition: all 0.2s ease !important;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.examples button:hover {
|
| 154 |
+
background: #f1f5f9 !important;
|
| 155 |
+
border-color: #3b82f6 !important;
|
| 156 |
+
color: #1e3a8a !important;
|
| 157 |
+
transform: translateY(-1px);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* Imagem de visualização */
|
| 161 |
+
.gr-image {
|
| 162 |
+
border-radius: 0.75rem !important;
|
| 163 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
| 164 |
+
border: 1px solid #e2e8f0 !important;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* Labels */
|
| 168 |
+
label {
|
| 169 |
+
font-weight: 600 !important;
|
| 170 |
+
color: #334155 !important;
|
| 171 |
+
font-size: 0.95rem !important;
|
| 172 |
+
margin-bottom: 0.5rem !important;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Container principal */
|
| 176 |
+
.gradio-container {
|
| 177 |
+
max-width: 1400px !important;
|
| 178 |
+
margin: 0 auto !important;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* Animação suave */
|
| 182 |
+
@keyframes fadeIn {
|
| 183 |
+
from {
|
| 184 |
+
opacity: 0;
|
| 185 |
+
transform: translateY(10px);
|
| 186 |
+
}
|
| 187 |
+
to {
|
| 188 |
+
opacity: 1;
|
| 189 |
+
transform: translateY(0);
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
/* Badge de status */
|
| 195 |
+
.status-badge {
|
| 196 |
+
display: inline-block;
|
| 197 |
+
padding: 0.25rem 0.75rem;
|
| 198 |
+
border-radius: 9999px;
|
| 199 |
+
font-size: 0.875rem;
|
| 200 |
+
font-weight: 600;
|
| 201 |
+
background: #dcfce7;
|
| 202 |
+
color: #166534;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* Scrollbar personalizada */
|
| 206 |
+
::-webkit-scrollbar {
|
| 207 |
+
width: 10px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
::-webkit-scrollbar-track {
|
| 211 |
+
background: #f1f5f9;
|
| 212 |
+
border-radius: 10px;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
::-webkit-scrollbar-thumb {
|
| 216 |
+
background: #cbd5e1;
|
| 217 |
+
border-radius: 10px;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
::-webkit-scrollbar-thumb:hover {
|
| 221 |
+
background: #94a3b8;
|
| 222 |
+
}
|
| 223 |
+
"""
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|