fredcaixeta commited on
Commit
0111f03
·
1 Parent(s): fe5ca24
Files changed (2) hide show
  1. app.py +2 -2
  2. main_agent.py +159 -291
app.py CHANGED
@@ -10,8 +10,8 @@ USER_ID = str(uuid.uuid4())
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
 
 
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
 
main_agent.py CHANGED
@@ -1,341 +1,209 @@
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())
 
1
  import asyncio
2
+ import os
3
+ from typing import List, Dict, Any, Optional
4
+ from pathlib import Path
5
+ from functools import partial
6
+ from dotenv import load_dotenv
7
+ from psycopg2 import pool
8
+ from psycopg2.extras import RealDictCursor
9
+ load_dotenv()
10
+
11
  from pydantic_ai import Agent, RunContext
 
12
  from pydantic_ai.providers.groq import GroqProvider
13
  from pydantic_ai.models.groq import GroqModel
14
  from pydantic_ai.messages import PartDeltaEvent, TextPartDelta
15
  from pydantic_graph import End
16
+ from pydantic import BaseModel
17
 
18
+ # PROMPTS
 
 
 
 
 
19
  from prompts import matches_prompt, players_prompt, player_matchid_prompt, graph_agent_prompt
20
  MATCHES_SYSTEM_PROMPT = matches_prompt.MATCHES_SYSTEM_PROMPT
21
  PLAYERS_SYSTEM_PROMPT = players_prompt.PLAYERS_SYSTEM_PROMPT
22
  PLAYER_MATCHID_SYSTEM_PROMPT = player_matchid_prompt.SYSTEM_PROMPT
23
+ GRAPH_AGENT_SYSTEM_PROMPT = graph_agent_prompt.GRAPH_AGENT_SYSTEM_PROMPT # Ajustado
24
 
25
  import pandas as pd
 
 
26
  import matplotlib
27
  matplotlib.use('Agg')
28
  import matplotlib.pyplot as plt
29
  import io
30
  from PIL import Image, ImageDraw, ImageFont
 
 
31
  from dataclasses import dataclass
 
 
 
32
 
33
+ SUPABASE_DB_URI = os.getenv("SUPABASE_DB_URI")
34
+
35
+ # ========================= SupabaseConnection (migrada do MCP) =========================
36
+ class SupabaseConnection:
37
+ def __init__(self, dsn: str):
38
+ self.pool = pool.ThreadedConnectionPool(minconn=1, maxconn=10, dsn=dsn)
39
+ print("✓ Pool Supabase criado")
40
+ # Teste conexão
41
+ conn = self.pool.getconn()
42
+ try:
43
+ with conn.cursor() as cur:
44
+ cur.execute("SELECT 1")
45
+ finally:
46
+ self.pool.putconn(conn)
47
+
48
+ async def execute_query_async(self, query: str, parameters: Optional[tuple] = None):
49
+ loop = asyncio.get_event_loop()
50
+ return await loop.run_in_executor(None, partial(self._execute_query_sync, query, parameters))
51
+
52
+ def _execute_query_sync(self, query: str, parameters: Optional[tuple] = None):
53
+ conn = None
54
+ try:
55
+ conn = self.pool.getconn()
56
+ conn.autocommit = True
57
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
58
+ if parameters:
59
+ cur.execute(query, parameters)
60
+ else:
61
+ cur.execute(query)
62
+ if cur.description:
63
+ return [dict(r) for r in cur.fetchall()]
64
+ return []
65
+ except Exception as e:
66
+ raise Exception(f"Erro query: {str(e)}")
67
+ finally:
68
+ if conn:
69
+ self.pool.putconn(conn)
70
+
71
+ def close(self):
72
+ if hasattr(self, 'pool') and self.pool:
73
+ self.pool.closeall()
74
+ print("✓ Pool fechado")
75
+
76
+ db = SupabaseConnection(SUPABASE_DB_URI)
77
+
78
+ # ========================= Agents (sem MCP) =========================
79
  class Deps(BaseModel):
80
  ai_query: str
81
 
82
  api_key = os.getenv("GROQ_DEV_API_KEY")
83
+ groq_model = GroqModel("moonshotai/kimi-k2-instruct-0905", provider=GroqProvider(api_key=api_key))
 
 
 
84
 
85
  api_key_2 = os.getenv("GROQ_DEV_API_KEY_2")
86
+ groq_model_2 = GroqModel("openai/gpt-oss-20b", provider=GroqProvider(api_key=api_key_2))
 
 
 
87
 
88
+ one_player_agent = Agent(
89
+ model=groq_model,
90
+ system_prompt=PLAYER_MATCHID_SYSTEM_PROMPT,
91
  deps_type=Deps,
92
  )
93
 
94
+ # ========================= TOOLS DIRETAS no one_player_agent =========================
95
+ @one_player_agent.tool()
96
+ async def execute_sql_query(query: str, limit: int = 100) -> str:
97
+ """Executa query SQL READ-ONLY."""
98
+ query_upper = query.upper().strip()
99
+ dangerous = ['DELETE', 'DROP', 'INSERT', 'UPDATE', 'ALTER', 'CREATE', 'TRUNCATE']
100
+ if any(k in query_upper for k in dangerous):
101
+ return "❌ Apenas SELECT permitidas."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
+ if 'LIMIT' not in query_upper:
104
+ query += f" LIMIT {limit}"
105
+ results = await db.execute_query_async(query)
106
+ if not results:
107
+ return "✓ Query OK, sem resultados."
108
+ lines = [f"📊 {len(results)} registros:\n"]
109
+ for i, record in enumerate(results[:10], 1):
110
+ items = [f"{k}={v}" for k, v in record.items()]
111
+ lines.append(f"{i}. {', '.join(items)}")
112
+ if len(results) > 10:
113
+ lines.append(f"\n... +{len(results)-10}")
114
+ return "\n".join(lines)
115
+ except Exception as e:
116
+ return f"❌ Erro: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ @one_player_agent.tool()
119
+ async def get_player_match_history(player_name: str, limit: int = 10) -> str:
120
+ query = """
121
+ SELECT match_date, opponent, home_away, minutes_played, goals, assists, shots, xg, pass_completion_pct, player_nickname
122
+ FROM player_match_stats WHERE player_nickname ILIKE %s ORDER BY match_date DESC LIMIT %s
 
 
 
 
 
 
123
  """
124
+ try:
125
+ results = await db.execute_query_async(query, (f'%{player_name}%', limit))
126
+ if not results: return f"❌ Nenhum dado para '{player_name}'"
127
+ out = [f"📊 HISTÓRICO - {player_name.upper()}\n"]
128
+ for r in results:
129
+ xg = f"{(r.get('xg') or 0):.2f}"
130
+ pcp = f"{(r.get('pass_completion_pct') or 0):.1f}"
131
+ out.append(f"📅 {r['match_date']} vs {r['opponent']} ({r['home_away']}) - {r['minutes_played']}min | ⚽{r['goals']}G 🤝{r['assists']}A | 🎯{r['shots']} (xG:{xg}) | 📈{pcp}%")
132
+ return "\n".join(out)
133
+ except Exception as e:
134
+ return f"❌ Erro: {str(e)}"
135
+
136
+ @one_player_agent.tool()
137
+ async def get_match_performances(match_date: Optional[str] = None, opponent: Optional[str] = None, limit: int = 15) -> str:
138
+ if not match_date and not opponent: return "❌ Forneça match_date OU opponent"
139
+ where_clauses, params = [], []
140
+ if match_date:
141
+ where_clauses.append("match_date = %s"); params.append(match_date)
142
+ if opponent:
143
+ where_clauses.append("opponent ILIKE %s"); params.append(f'%{opponent}%')
144
+ where_sql = " AND ".join(where_clauses)
145
+ params.append(limit)
146
+ query = f"""
147
+ SELECT player_nickname, minutes_played, goals, assists, (goals + assists) as contributions, shots, xg, pass_completion_pct, touches
148
+ FROM player_match_stats WHERE {where_sql} ORDER BY (goals + assists) DESC, xg DESC LIMIT %s
149
  """
 
 
150
  try:
151
+ results = await db.execute_query_async(query, tuple(params))
152
+ if not results: return "❌ Partida não encontrada"
153
+ info = f"{match_date or ''} vs {opponent or ''}".strip()
154
+ out = [f"🏟️ PERFORMANCES - {info}\n"]
155
+ for i, r in enumerate(results, 1):
156
+ xg = f"{(r.get('xg') or 0):.2f}"; pcp = f"{(r.get('pass_completion_pct') or 0):.1f}"
157
+ touches = r.get('touches', 0)
158
+ out.append(f"{i}. {r['player_nickname']} ({r['minutes_played']}min): ⚽{r['goals']}G + 🤝{r['assists']}A = {r['contributions']} | 🎯{r['shots']} (xG:{xg}) | 📈{pcp}% | 👟{touches}")
159
+ return "\n".join(out)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  except Exception as e:
161
+ return f"❌ Erro: {str(e)}"
162
+
163
+ @one_player_agent.tool()
164
+ async def get_top_performances(metric: str = "goals", limit: int = 10) -> str:
165
+ valid = {"goals": ("goals", "Gols"), "assists": ("assists", "Assistências"), "contributions": ("goals + assists", "Contribuições"), "xg": ("xg", "xG"), "shots": ("shots", "Finalizações")}
166
+ if metric not in valid: return f"❌ Métricas: {', '.join(valid)}"
167
+ metric_sql, metric_name = valid[metric]
168
+ query = f"""
169
+ SELECT player_nickname, match_date, opponent, home_away, goals, assists, xg, shots, pass_completion_pct
170
+ FROM player_match_stats ORDER BY {metric_sql} DESC LIMIT %s
171
+ """
172
+ try:
173
+ results = await db.execute_query_async(query, (limit,))
174
+ if not results: return "❌ Sem dados"
175
+ out = [f"🏆 TOP {metric_name.upper()}\n"]
176
+ for i, r in enumerate(results, 1):
177
+ xg = f"{(r.get('xg') or 0):.2f}"
178
+ out.append(f"{i}. {r['player_nickname']} - {r['match_date']} vs {r['opponent']} ({r['home_away']}): ⚽{r['goals']}G 🤝{r['assists']}A xG:{xg}")
179
+ return "\n".join(out)
180
+ except Exception as e:
181
+ return f"❌ Erro: {str(e)}"
182
 
183
+ # Manter create_chart ( era tool local)
184
+ last_chart_image = None
185
+ # ... (código do create_chart igual, global last_chart_image)
 
 
186
 
187
+ # Funções de response (já sem MCP)
188
  async def agent_conventional_response(user_query: str) -> str:
 
189
  res = await one_player_agent.run(user_prompt=user_query)
190
+ return res.output
191
+
 
192
  async def stream_agent_response_safe(user_query: str) -> str:
 
 
 
 
 
193
  try:
194
  async with one_player_agent.iter(user_query) as agent_run:
195
  async for node in agent_run:
196
+ if isinstance(node, End) and agent_run.result:
197
+ return str(agent_run.result.output)
 
 
 
 
 
 
 
 
 
198
  except Exception as e:
 
199
  import traceback
200
  traceback.print_exc()
201
+ return f"Erro: {str(e)}"
202
+ return "Nenhuma resposta."
 
 
 
 
 
 
 
 
203
 
204
+ # Cleanup global
205
+ async def shutdown():
206
+ db.close()
207
 
208
  if __name__ == "__main__":
209
+ asyncio.run(one_player_agent.run("Teste: top performances goals limit 5"))