Spaces:
Sleeping
Sleeping
fredcaixeta
commited on
Commit
·
0111f03
1
Parent(s):
fe5ca24
toollls
Browse files- app.py +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 |
-
#
|
| 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 |
-
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 77 |
-
model=
|
| 78 |
-
system_prompt=
|
| 79 |
deps_type=Deps,
|
| 80 |
)
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 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 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 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(
|
| 199 |
-
async def
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 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 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
"""
|
| 221 |
-
global last_chart_image
|
| 222 |
-
|
| 223 |
try:
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 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
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
-
#
|
| 293 |
-
|
| 294 |
-
#
|
| 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 |
-
|
| 302 |
-
|
| 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 |
-
|
| 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
|
| 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(
|
|
|
|
| 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 (já 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"))
|