fredcaixeta commited on
Commit ·
53ce4cd
1
Parent(s): 167b153
go
Browse files- .gitignore +11 -0
- .python-version +1 -0
- agent.py +83 -0
- app.py +29 -62
- client_agent.py +187 -0
- main.py +6 -0
- mcp_server.py +356 -0
- pyproject.toml +10 -0
- tools/init.py +0 -0
- tools/searching.py +44 -0
- uv.lock +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
.venv
|
| 11 |
+
.env
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
agent.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
from pydantic_ai import Agent, RunContext
|
| 5 |
+
# from pydantic_ai.messages import (ModelMessage, ModelRequest, ModelResponse, UserPromptPart,
|
| 6 |
+
# SystemPromptPart, TextPart, ModelMessagesTypeAdapter,
|
| 7 |
+
# ToolReturnPart)
|
| 8 |
+
from pydantic_ai.usage import UsageLimits
|
| 9 |
+
from pydantic_core import to_jsonable_python
|
| 10 |
+
from pydantic_ai.models.groq import GroqModel
|
| 11 |
+
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
from tools.searching import SearchingTools
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Defina um prompt de sistema padrão
|
| 22 |
+
DEFAULT_SYSTEM_PROMPT = f"""
|
| 23 |
+
Você é um agente assistente em um programa de detecção de conteúdos falsos (fake news).
|
| 24 |
+
Intereja com o usuário sugerindo as ferramentas que você tem disponível. Se o usuário perguntar do que se trata,
|
| 25 |
+
responda 'Sou uma IA disponível para te ajudar a detectar se um conteúdo que circula na internet é verdadeiro ou não,
|
| 26 |
+
sou capaz de pesquisar na web se o conteúdo é verídico ou não.' Use as ferramenta sempre que possível.
|
| 27 |
+
A data de hoje é {data_formatada}.
|
| 28 |
+
Anexe sempre a fonte da pesquisa na resposta (G1, BBC, New York Times, etc), se houver fonte na pesquisa utilizada.
|
| 29 |
+
Não invente fontes, não invente dados. Se não houver nada relacionado à pergunta do usuário nas pesquisas, diga que nada foi encontrado.
|
| 30 |
+
Responda sempre em português do Brasil.
|
| 31 |
+
Limite-se a 600 caracteres.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class SearchAgentDeps:
|
| 36 |
+
tools: SearchingTools
|
| 37 |
+
|
| 38 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 39 |
+
model = GroqModel(
|
| 40 |
+
model_name="openai/gpt-oss-120b"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def start_convo(user_input, messages_history):
|
| 45 |
+
search_agent = Agent(
|
| 46 |
+
model,
|
| 47 |
+
deps_type=SearchAgentDeps,
|
| 48 |
+
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
| 49 |
+
history_processors=[context_aware_processor]
|
| 50 |
+
)
|
| 51 |
+
@search_agent.tool(name="procura_web_noticias_detectadas_como_fake_news",retries=3)
|
| 52 |
+
def procura_web_noticias_detectadas_como_fake_news(ctx: RunContext[SearchAgentDeps], search_query: str) -> str:
|
| 53 |
+
"""Pesquisa em um canal de coleta de Fake News Brasileira"""
|
| 54 |
+
return ctx.deps.tools.search_web(
|
| 55 |
+
search_query=search_query,
|
| 56 |
+
include_domains=["https://g1.globo.com/fato-ou-fake/"],
|
| 57 |
+
max_results=15
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
@search_agent.tool(name="pesquisa_web_global", retries=3)
|
| 61 |
+
def pesquisa_web_global(ctx: RunContext[SearchAgentDeps], search_query: str) -> str:
|
| 62 |
+
"""Pesquisa na web de maneira geral"""
|
| 63 |
+
return ctx.deps.tools.search_web(
|
| 64 |
+
search_query=search_query,
|
| 65 |
+
exclude_domains=["https://g1.globo.com/fato-ou-fake/"],
|
| 66 |
+
max_results=7
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
@search_agent.tool(name="pesquisa_social_media_verificados", retries=3)
|
| 70 |
+
def pesquisa_social_media_verificados(ctx: RunContext[SearchAgentDeps], search_query: str) -> str:
|
| 71 |
+
"""Pesquisa na web de maneira geral"""
|
| 72 |
+
return ctx.deps.tools.search_web(
|
| 73 |
+
search_query=search_query,
|
| 74 |
+
include_domains=[
|
| 75 |
+
"https://x.com/OGloboPolitica",
|
| 76 |
+
"https://x.com/CNNBrasil",
|
| 77 |
+
"https://x.com/recordnews",
|
| 78 |
+
"https://x.com/JovemPanNews"
|
| 79 |
+
],
|
| 80 |
+
max_results=15
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
return search_agent
|
app.py
CHANGED
|
@@ -1,70 +1,37 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
system_message,
|
| 9 |
-
max_tokens,
|
| 10 |
-
temperature,
|
| 11 |
-
top_p,
|
| 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 |
-
response = ""
|
| 26 |
-
|
| 27 |
-
for message in client.chat_completion(
|
| 28 |
-
messages,
|
| 29 |
-
max_tokens=max_tokens,
|
| 30 |
-
stream=True,
|
| 31 |
-
temperature=temperature,
|
| 32 |
-
top_p=top_p,
|
| 33 |
-
):
|
| 34 |
-
choices = message.choices
|
| 35 |
-
token = ""
|
| 36 |
-
if len(choices) and choices[0].delta.content:
|
| 37 |
-
token = choices[0].delta.content
|
| 38 |
-
|
| 39 |
-
response += token
|
| 40 |
-
yield response
|
| 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 |
-
|
| 64 |
-
with
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
if __name__ == "__main__":
|
| 70 |
-
demo.
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import uuid
|
| 3 |
+
from agent import start_convo
|
| 4 |
+
from utils.history import convert_to_pydantic_history, context_aware_processor
|
| 5 |
|
| 6 |
+
# Generate a unique user ID for the session
|
| 7 |
+
USER_ID = str(uuid.uuid4())
|
| 8 |
|
| 9 |
+
from tools.searching import SearchingTools # Importe aqui
|
| 10 |
+
from agent import SearchAgentDeps # Importe se necessário (baseado no seu código anterior)
|
| 11 |
+
from pydantic_ai import UsageLimits
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
import asyncio
|
| 14 |
|
| 15 |
+
from client_agent import agent_startup
|
| 16 |
+
usage_limits=UsageLimits(
|
| 17 |
+
output_tokens_limit=10000,
|
| 18 |
+
tool_calls_limit=10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
)
|
| 20 |
+
def respond(message, history):
|
| 21 |
+
#pydantic_history_messages = convert_to_pydantic_history(history)
|
| 22 |
+
# Append the user's message with a placeholder for the bot's response
|
| 23 |
+
message = str(message)
|
| 24 |
+
print(f"Message received: {message}")
|
| 25 |
+
tools_instance = SearchingTools()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
result = asyncio.run(agent_startup(message))
|
| 29 |
+
|
| 30 |
+
print(f"Result: {result}")
|
| 31 |
+
# Adicione a nova resposta ao histórico original (se precisar manter)
|
| 32 |
+
|
| 33 |
+
return result
|
| 34 |
|
| 35 |
if __name__ == "__main__":
|
| 36 |
+
demo = gr.ChatInterface(fn=respond, type="messages", title="Fake News Detector Bot", save_history=True)
|
| 37 |
+
demo.launch(ssr_mode=False)
|
client_agent.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from pydantic_ai import Agent, RunContext
|
| 3 |
+
from pydantic_ai.mcp import MCPServerStreamableHTTP
|
| 4 |
+
from pydantic_ai.models.openai import OpenAIChatModel
|
| 5 |
+
from pydantic_ai.providers.openrouter import OpenRouterProvider
|
| 6 |
+
from pydantic_ai.providers.groq import GroqProvider
|
| 7 |
+
from pydantic_ai.models.groq import GroqModel
|
| 8 |
+
from tools.searching import SearchingTools
|
| 9 |
+
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
load_dotenv()
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
# Criar servidor MCP via stdio
|
| 15 |
+
neo4j_server = MCPServerStreamableHTTP(
|
| 16 |
+
url = "http://127.0.0.1:8000/mcp"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
SYSTEM_PROMPT = """
|
| 20 |
+
You are an Expert Football Analyst with access to Barcelona graphdatabase from 2016 to 2020.
|
| 21 |
+
Use the tools available to respond the user query.
|
| 22 |
+
Base your analysis **ONLY** by the query results. If the database can't provide what the user is
|
| 23 |
+
asking for, report that in a professional way.
|
| 24 |
+
Below you can find some cypher queries as example, so you can understand which artifacts and metadatas are available in the database:
|
| 25 |
+
|
| 26 |
+
// General Overview: players, connections, goals per temporada
|
| 27 |
+
MATCH (p:Player {team: "Barcelona"})
|
| 28 |
+
OPTIONAL MATCH (p)-[r:PASSED_TO]->()
|
| 29 |
+
OPTIONAL MATCH (g:GoalSequence {team: "Barcelona"})
|
| 30 |
+
RETURN
|
| 31 |
+
count(DISTINCT p) as TotalPlayers,
|
| 32 |
+
count(DISTINCT r) as TotalPassConnections,
|
| 33 |
+
sum(r.weight) as TotalPasses,
|
| 34 |
+
count(DISTINCT g) as TotalGoalSequences,
|
| 35 |
+
collect(DISTINCT p.season_date) as Seasons
|
| 36 |
+
// List all seasons available in neo4j
|
| 37 |
+
MATCH (p:Player)
|
| 38 |
+
RETURN DISTINCT p.season_date as Season, p.season_id as SeasonID
|
| 39 |
+
ORDER BY p.season_date
|
| 40 |
+
// Top 5 connections per season
|
| 41 |
+
MATCH (p1:Player)-[r:PASSED_TO]->(p2:Player)
|
| 42 |
+
WITH p1.season_date as Season, p1.name as P1, p2.name as P2, r.weight as Weight
|
| 43 |
+
ORDER BY Weight DESC
|
| 44 |
+
WITH Season, collect({passer: P1, receiver: P2, passes: Weight})[0..5] as TopConnections
|
| 45 |
+
RETURN Season, TopConnections
|
| 46 |
+
ORDER BY Season
|
| 47 |
+
// Connections between different zones in field
|
| 48 |
+
MATCH (p1:Player)-[r:PASSED_TO]->(p2:Player)
|
| 49 |
+
WHERE p1.season_id = 90
|
| 50 |
+
WITH p1, p2, r,
|
| 51 |
+
CASE WHEN p1.avg_x < 40 THEN 'Def' WHEN p1.avg_x < 80 THEN 'Mid' ELSE 'Att' END as Zone1,
|
| 52 |
+
CASE WHEN p2.avg_x < 40 THEN 'Def' WHEN p2.avg_x < 80 THEN 'Mid' ELSE 'Att' END as Zone2
|
| 53 |
+
WHERE Zone1 <> Zone2
|
| 54 |
+
RETURN Zone1 + ' -> ' + Zone2 as Transition, sum(r.weight) as TotalPasses
|
| 55 |
+
ORDER BY TotalPasses DESC
|
| 56 |
+
// Total number of sequences of goals that Rakitić (a player) was involved
|
| 57 |
+
MATCH (p:Player {name: "Ivan Rakitić"})-[:INVOLVED_IN]->(g:GoalSequence)
|
| 58 |
+
RETURN count(g) as TotalGoalSequences
|
| 59 |
+
|
| 60 |
+
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.
|
| 61 |
+
The Nodes are: Player, GoalSequence.
|
| 62 |
+
The Relationships are: INVOLVED_IN, PASSED_IN_SEQUENCE, PASSED_TO.
|
| 63 |
+
|
| 64 |
+
All players played in all seasons are:
|
| 65 |
+
Abel Ruiz Ortega
|
| 66 |
+
Aleix Vidal Parreu
|
| 67 |
+
André Filipe Tavares Gomes
|
| 68 |
+
Andrés Iniesta Luján
|
| 69 |
+
Anssumane Fati
|
| 70 |
+
Antoine Griezmann
|
| 71 |
+
Arda Turan
|
| 72 |
+
Arthur Henrique Ramos de Oliveira Melo
|
| 73 |
+
Arturo Erasmo Vidal Pardo
|
| 74 |
+
Carles Aleña Castillo
|
| 75 |
+
Carles Pérez Sayol
|
| 76 |
+
Claudio Andrés Bravo Muñoz
|
| 77 |
+
Clément Lenglet
|
| 78 |
+
Denis Suárez Fernández
|
| 79 |
+
Francisco Alcácer García
|
| 80 |
+
Francisco António Machado Mota de Castro Trincão
|
| 81 |
+
Frenkie de Jong
|
| 82 |
+
Gerard Deulofeu Lázaro
|
| 83 |
+
Gerard Piqué Bernabéu
|
| 84 |
+
Héctor Junior Firpo Adames
|
| 85 |
+
Ivan Rakitić
|
| 86 |
+
Jasper Cillessen
|
| 87 |
+
Javier Alejandro Mascherano
|
| 88 |
+
Jean-Clair Todibo
|
| 89 |
+
Jordi Alba Ramos
|
| 90 |
+
José Manuel Arnáiz Díaz
|
| 91 |
+
José Paulo Bezzera Maciel Júnior
|
| 92 |
+
Jérémy Mathieu
|
| 93 |
+
Kevin-Prince Boateng
|
| 94 |
+
Lionel Andrés Messi Cuccittini
|
| 95 |
+
Lucas Digne
|
| 96 |
+
Luis Alberto Suárez Díaz
|
| 97 |
+
Malcom Filipe Silva de Oliveira
|
| 98 |
+
Marc-André ter Stegen
|
| 99 |
+
Marlon Santos da Silva Barbosa
|
| 100 |
+
Martin Braithwaite Christensen
|
| 101 |
+
Miralem Pjanić
|
| 102 |
+
Moriba Kourouma Kourouma
|
| 103 |
+
Moussa Wagué
|
| 104 |
+
Munir El Haddadi Mohamed
|
| 105 |
+
Neymar da Silva Santos Junior
|
| 106 |
+
Norberto Murara Neto
|
| 107 |
+
Nélson Cabral Semedo
|
| 108 |
+
Ousmane Dembélé
|
| 109 |
+
Pedro González López
|
| 110 |
+
Philippe Coutinho Correia
|
| 111 |
+
Rafael Alcântara do Nascimento
|
| 112 |
+
Ricard Puig Martí
|
| 113 |
+
Ronald Federico Araújo da Silva
|
| 114 |
+
Samuel Yves Umtiti
|
| 115 |
+
Sergi Roberto Carnicer
|
| 116 |
+
Sergino Dest
|
| 117 |
+
Sergio Busquets i Burgos
|
| 118 |
+
Thomas Vermaelen
|
| 119 |
+
Yerry Fernando Mina González
|
| 120 |
+
Álex Collado Gutiérrez
|
| 121 |
+
Óscar Mingueza García
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
api_key = os.getenv("GROQ_API_KEY")
|
| 128 |
+
groq_model = GroqModel(
|
| 129 |
+
"openai/gpt-oss-20b",
|
| 130 |
+
provider=GroqProvider(api_key=api_key)
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
@dataclass
|
| 134 |
+
class SearchAgentDeps:
|
| 135 |
+
tools: SearchingTools
|
| 136 |
+
|
| 137 |
+
# Criar agent com o servidor MCP
|
| 138 |
+
agent = Agent(
|
| 139 |
+
model=groq_model,
|
| 140 |
+
toolsets=[neo4j_server],
|
| 141 |
+
system_prompt=SYSTEM_PROMPT,
|
| 142 |
+
deps_type=SearchAgentDeps,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
tools_instance = SearchingTools()
|
| 146 |
+
deps = SearchAgentDeps(tools=tools_instance)
|
| 147 |
+
|
| 148 |
+
@search_agent.tool(name="web_search",retries=3)
|
| 149 |
+
def procura_web(ctx: RunContext[SearchAgentDeps], search_query: str) -> str:
|
| 150 |
+
"""Pesquisa na web"""
|
| 151 |
+
return ctx.deps.tools.search_web(
|
| 152 |
+
search_query=search_query,
|
| 153 |
+
max_results=15
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
async def agent_startup(user_query):
|
| 157 |
+
async with agent: # Gerencia a conexão MCP automaticamente
|
| 158 |
+
# Exemplo 1: Análise do Rakitić
|
| 159 |
+
print("=" * 60)
|
| 160 |
+
result1 = await agent.run(
|
| 161 |
+
user_prompt=user_query
|
| 162 |
+
)
|
| 163 |
+
print(result1.output)
|
| 164 |
+
return result1.output
|
| 165 |
+
# # Exemplo 2: Cadeias de passes
|
| 166 |
+
# print("\n" + "=" * 60)
|
| 167 |
+
# result2 = await agent.run(
|
| 168 |
+
# "Mostre as 5 cadeias de 2 passes mais frequentes do Rakitić"
|
| 169 |
+
# )
|
| 170 |
+
# print(result2.output)
|
| 171 |
+
|
| 172 |
+
# # Exemplo 3: Eficiência
|
| 173 |
+
# print("\n" + "=" * 60)
|
| 174 |
+
# result3 = await agent.run(
|
| 175 |
+
# "Qual a eficiência do Rakitić? Compare passes totais com passes que resultam em gol. Compare com outros jogadores."
|
| 176 |
+
# )
|
| 177 |
+
# print(result3.output)
|
| 178 |
+
|
| 179 |
+
# # Exemplo 4: Query customizada
|
| 180 |
+
# print("\n" + "=" * 60)
|
| 181 |
+
# result4 = await agent.run(
|
| 182 |
+
# "List the 5 top players that participated in goals, each season"
|
| 183 |
+
# )
|
| 184 |
+
# print(result4.output)
|
| 185 |
+
|
| 186 |
+
if __name__ == '__main__':
|
| 187 |
+
asyncio.run(main())
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from hf-barca-neo4j-agent!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
mcp_server.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional, List, Dict, Any
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from neo4j import GraphDatabase
|
| 5 |
+
from mcp.server.fastmcp import FastMCP, Context
|
| 6 |
+
|
| 7 |
+
# Carregar variáveis de ambiente
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
# Configuração do Neo4j
|
| 11 |
+
NEO4J_URI = os.getenv("NEO4J_URI")
|
| 12 |
+
NEO4J_USER = os.getenv("NEO4J_USER")
|
| 13 |
+
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
|
| 14 |
+
|
| 15 |
+
# Criar conexão global Neo4j
|
| 16 |
+
class Neo4jConnection:
|
| 17 |
+
def __init__(self):
|
| 18 |
+
self.driver = GraphDatabase.driver(
|
| 19 |
+
NEO4J_URI,
|
| 20 |
+
auth=(NEO4J_USER, NEO4J_PASSWORD)
|
| 21 |
+
)
|
| 22 |
+
print(f"✓ Conectado ao Neo4j: {NEO4J_URI}")
|
| 23 |
+
|
| 24 |
+
def execute_query(self, query: str, parameters: Optional[Dict[str, Any]] = None):
|
| 25 |
+
with self.driver.session() as session:
|
| 26 |
+
result = session.run(query, parameters or {})
|
| 27 |
+
return [record.data() for record in result]
|
| 28 |
+
|
| 29 |
+
def close(self):
|
| 30 |
+
self.driver.close()
|
| 31 |
+
|
| 32 |
+
# Instância global
|
| 33 |
+
neo4j = Neo4jConnection()
|
| 34 |
+
|
| 35 |
+
# Criar servidor MCP
|
| 36 |
+
mcp = FastMCP(name="Neo4j Football Analytics")
|
| 37 |
+
|
| 38 |
+
# Tool 1: Query Cypher customizada
|
| 39 |
+
@mcp.tool()
|
| 40 |
+
async def execute_cypher_query(
|
| 41 |
+
query: str,
|
| 42 |
+
parameters: Optional[Dict[str, Any]] = None,
|
| 43 |
+
limit: int = 100
|
| 44 |
+
) -> str:
|
| 45 |
+
"""
|
| 46 |
+
Executa uma query Cypher READ-ONLY no banco de dados Neo4j.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
query: Query Cypher a ser executada (apenas MATCH/RETURN)
|
| 50 |
+
parameters: Dicionário de parâmetros opcionais
|
| 51 |
+
limit: Número máximo de resultados (padrão: 100)
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
String formatada com os resultados da query
|
| 55 |
+
"""
|
| 56 |
+
# Validação de segurança
|
| 57 |
+
query_upper = query.upper().strip()
|
| 58 |
+
dangerous = ['DELETE', 'DETACH', 'REMOVE', 'SET', 'CREATE', 'MERGE', 'DROP']
|
| 59 |
+
|
| 60 |
+
if any(keyword in query_upper for keyword in dangerous):
|
| 61 |
+
return "❌ ERRO: Apenas queries de leitura (MATCH/RETURN) são permitidas."
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
# Adicionar LIMIT se não existir
|
| 65 |
+
if 'LIMIT' not in query_upper:
|
| 66 |
+
query += f" LIMIT {limit}"
|
| 67 |
+
|
| 68 |
+
results = neo4j.execute_query(query, parameters)
|
| 69 |
+
|
| 70 |
+
if not results:
|
| 71 |
+
return "✓ Query executada, mas nenhum resultado encontrado."
|
| 72 |
+
|
| 73 |
+
# Formatar resultados
|
| 74 |
+
output = [f"📊 Resultados ({len(results)} encontrados):\n"]
|
| 75 |
+
|
| 76 |
+
for i, record in enumerate(results[:10], 1):
|
| 77 |
+
items = [f"{k}={v}" for k, v in record.items()]
|
| 78 |
+
output.append(f"{i}. {', '.join(items)}")
|
| 79 |
+
|
| 80 |
+
if len(results) > 10:
|
| 81 |
+
output.append(f"\n... e mais {len(results) - 10} resultados.")
|
| 82 |
+
|
| 83 |
+
return "\n".join(output)
|
| 84 |
+
|
| 85 |
+
except Exception as e:
|
| 86 |
+
return f"❌ Erro ao executar query: {str(e)}"
|
| 87 |
+
|
| 88 |
+
@mcp.tool()
|
| 89 |
+
def search_goal_sequences_by_player(
|
| 90 |
+
player_name: str,
|
| 91 |
+
limit: int = 10
|
| 92 |
+
) -> str:
|
| 93 |
+
"""
|
| 94 |
+
Busca sequências de gol que começam com passes de um jogador específico.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
player_name: Nome do jogador (ex: "Ivan Rakitić")
|
| 98 |
+
limit: Número máximo de resultados a retornar
|
| 99 |
+
"""
|
| 100 |
+
query = """
|
| 101 |
+
MATCH (p:Player {name: $player_name})-[:INVOLVED_IN]->(g:GoalSequence)
|
| 102 |
+
RETURN count(g) as TotalGoalSequences
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
results = neo4j.execute_query(query, {"player_name": player_name})
|
| 107 |
+
|
| 108 |
+
if not results:
|
| 109 |
+
return f"Nenhuma sequência de gol encontrada para {player_name}"
|
| 110 |
+
|
| 111 |
+
total = results[0].get('TotalGoalSequences', 0)
|
| 112 |
+
return f"## Análise de {player_name}:\n{player_name} participou de {total} sequências que resultaram em gol."
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
return f"Erro ao executar query: {str(e)}"
|
| 116 |
+
|
| 117 |
+
# Tool 2: Análise de sequências de gol por jogador
|
| 118 |
+
@mcp.tool()
|
| 119 |
+
def analyze_pass_chains(
|
| 120 |
+
player_name: str,
|
| 121 |
+
chain_length: int = 2,
|
| 122 |
+
limit: int = 10
|
| 123 |
+
) -> str:
|
| 124 |
+
"""
|
| 125 |
+
Analisa caminhos de passes de comprimento específico começando por um jogador.
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
player_name: Nome do jogador inicial
|
| 129 |
+
chain_length: Comprimento da cadeia de passes (número de passes)
|
| 130 |
+
limit: Número máximo de cadeias a retornar
|
| 131 |
+
"""
|
| 132 |
+
query = f"""
|
| 133 |
+
MATCH path = (p1:Player {{name: $player_name}})-[:PASSED_TO*{chain_length}]->(p2:Player)
|
| 134 |
+
RETURN p1.name as Start,
|
| 135 |
+
p2.name as End,
|
| 136 |
+
[node IN nodes(path) | node.name] as PassChain,
|
| 137 |
+
count(path) as Frequency
|
| 138 |
+
ORDER BY Frequency DESC
|
| 139 |
+
LIMIT $limit
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
results = neo4j.execute_query(
|
| 144 |
+
query,
|
| 145 |
+
{"player_name": player_name, "limit": limit}
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
if not results:
|
| 149 |
+
return f"Nenhuma cadeia de {chain_length} passes encontrada para {player_name}"
|
| 150 |
+
|
| 151 |
+
# Formatar resultados
|
| 152 |
+
return_data = [f"## Cadeias de Passes de {player_name}:\n"]
|
| 153 |
+
|
| 154 |
+
for i, record in enumerate(results[:5], 1): # Top 5 cadeias
|
| 155 |
+
chain = " → ".join(record['PassChain'])
|
| 156 |
+
freq = record['Frequency']
|
| 157 |
+
return_data.append(f"{i}. {chain} (Frequência: {freq})")
|
| 158 |
+
|
| 159 |
+
return "\n".join(return_data)
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
return f"Erro ao executar query: {str(e)}"
|
| 163 |
+
|
| 164 |
+
@mcp.tool()
|
| 165 |
+
async def count_goal_initiations(
|
| 166 |
+
player_name: str
|
| 167 |
+
) -> str:
|
| 168 |
+
"""
|
| 169 |
+
Conta quantos gols/chutes começam com passes de um jogador específico.
|
| 170 |
+
|
| 171 |
+
Args:
|
| 172 |
+
player_name: Nome do jogador a analisar
|
| 173 |
+
"""
|
| 174 |
+
query = """
|
| 175 |
+
MATCH (rak:Player {name: $player_name})-[r:PASSED_IN_SEQUENCE]->(receiver:Player)
|
| 176 |
+
WITH rak, receiver, r, r.sequence_id as seqId
|
| 177 |
+
MATCH (g:GoalSequence {sequence_id: seqId})
|
| 178 |
+
RETURN receiver.name as PassedTo,
|
| 179 |
+
count(DISTINCT seqId) as NumberOfGoalSequences,
|
| 180 |
+
avg(r.order) as AvgPassPosition,
|
| 181 |
+
collect(DISTINCT g.match_id)[0..3] as SampleMatches
|
| 182 |
+
ORDER BY NumberOfGoalSequences DESC
|
| 183 |
+
LIMIT 5
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
results = neo4j.execute_query(query, {"player_name": player_name})
|
| 188 |
+
|
| 189 |
+
if not results:
|
| 190 |
+
return f"Nenhum dado encontrado para {player_name}"
|
| 191 |
+
|
| 192 |
+
return_data = [f"## Estatísticas de Gol - {player_name}:\n"]
|
| 193 |
+
|
| 194 |
+
for record in results:
|
| 195 |
+
receiver = record['PassedTo']
|
| 196 |
+
goals = record['NumberOfGoalSequences']
|
| 197 |
+
avg_pos = round(record['AvgPassPosition'], 2)
|
| 198 |
+
return_data.append(
|
| 199 |
+
f"→ Para {receiver}: {goals} sequências de gol (posição média no passe: {avg_pos})"
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
return "\n".join(return_data)
|
| 203 |
+
|
| 204 |
+
except Exception as e:
|
| 205 |
+
return f"Erro ao executar query: {str(e)}"
|
| 206 |
+
|
| 207 |
+
# Tool 3: Cadeias de passes
|
| 208 |
+
@mcp.tool()
|
| 209 |
+
async def analyze_pass_chains(
|
| 210 |
+
player_name: str,
|
| 211 |
+
chain_length: int = 2,
|
| 212 |
+
top_n: int = 5
|
| 213 |
+
) -> str:
|
| 214 |
+
"""
|
| 215 |
+
Analisa cadeias de passes começando por um jogador.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
player_name: Nome do jogador inicial
|
| 219 |
+
chain_length: Comprimento da cadeia (número de passes)
|
| 220 |
+
top_n: Número de cadeias mais frequentes a retornar
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
Análise das cadeias de passes mais frequentes
|
| 224 |
+
"""
|
| 225 |
+
query = f"""
|
| 226 |
+
MATCH path = (p1:Player {{name: $player_name}})-[:PASSED_TO*{chain_length}]->(p2:Player)
|
| 227 |
+
WITH [node IN nodes(path) | node.name] as PassChain, count(path) as Frequency
|
| 228 |
+
RETURN PassChain, Frequency
|
| 229 |
+
ORDER BY Frequency DESC
|
| 230 |
+
LIMIT $top_n
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
try:
|
| 234 |
+
results = neo4j.execute_query(
|
| 235 |
+
query,
|
| 236 |
+
{"player_name": player_name, "top_n": top_n}
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
if not results:
|
| 240 |
+
return f"❌ Nenhuma cadeia de {chain_length} passes encontrada para {player_name}"
|
| 241 |
+
|
| 242 |
+
output = [f"🔗 Top {len(results)} cadeias de {chain_length} passes de {player_name}:\n"]
|
| 243 |
+
|
| 244 |
+
for i, record in enumerate(results, 1):
|
| 245 |
+
chain = " → ".join(record['PassChain'])
|
| 246 |
+
freq = record['Frequency']
|
| 247 |
+
output.append(f"{i}. {chain} (frequência: {freq})")
|
| 248 |
+
|
| 249 |
+
return "\n".join(output)
|
| 250 |
+
|
| 251 |
+
except Exception as e:
|
| 252 |
+
return f"❌ Erro: {str(e)}"
|
| 253 |
+
|
| 254 |
+
# Tool 4: Eficiência de passes em gols
|
| 255 |
+
@mcp.tool()
|
| 256 |
+
async def player_efficiency(player_name: str) -> str:
|
| 257 |
+
"""
|
| 258 |
+
Calcula a eficiência de um jogador: passes totais vs passes que resultam em gol.
|
| 259 |
+
|
| 260 |
+
Args:
|
| 261 |
+
player_name: Nome do jogador a analisar
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
Estatísticas de eficiência formatadas
|
| 265 |
+
"""
|
| 266 |
+
query = """
|
| 267 |
+
MATCH (p:Player {name: $player_name})
|
| 268 |
+
OPTIONAL MATCH (p)-[total:PASSED_TO]->()
|
| 269 |
+
OPTIONAL MATCH (p)-[goals:PASSED_IN_SEQUENCE]->()
|
| 270 |
+
WITH p,
|
| 271 |
+
sum(total.weight) as TotalPasses,
|
| 272 |
+
count(DISTINCT goals) as PassesInGoals
|
| 273 |
+
RETURN p.name as Player,
|
| 274 |
+
TotalPasses,
|
| 275 |
+
PassesInGoals,
|
| 276 |
+
CASE
|
| 277 |
+
WHEN TotalPasses > 0
|
| 278 |
+
THEN round(PassesInGoals * 100.0 / TotalPasses, 2)
|
| 279 |
+
ELSE 0
|
| 280 |
+
END as EfficiencyPercent
|
| 281 |
+
"""
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
results = neo4j.execute_query(query, {"player_name": player_name})
|
| 285 |
+
|
| 286 |
+
if not results or not results[0]['TotalPasses']:
|
| 287 |
+
return f"❌ Dados insuficientes para {player_name}"
|
| 288 |
+
|
| 289 |
+
r = results[0]
|
| 290 |
+
output = [
|
| 291 |
+
f"📈 Eficiência de {r['Player']}:",
|
| 292 |
+
f"Total de passes: {r['TotalPasses']}",
|
| 293 |
+
f"Passes em sequências de gol: {r['PassesInGoals']}",
|
| 294 |
+
f"Taxa de eficiência: {r['EfficiencyPercent']}%"
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
return "\n".join(output)
|
| 298 |
+
|
| 299 |
+
except Exception as e:
|
| 300 |
+
return f"❌ Erro: {str(e)}"
|
| 301 |
+
|
| 302 |
+
# Tool 5: Estatísticas específicas do Rakitić
|
| 303 |
+
@mcp.tool()
|
| 304 |
+
async def rakitic_goal_statistics() -> str:
|
| 305 |
+
"""
|
| 306 |
+
Retorna estatísticas detalhadas de Ivan Rakitić em sequências de gol.
|
| 307 |
+
|
| 308 |
+
Returns:
|
| 309 |
+
Análise completa do desempenho de Rakitić
|
| 310 |
+
"""
|
| 311 |
+
query = """
|
| 312 |
+
MATCH (rak:Player {name: "Ivan Rakitić"})-[r:PASSED_IN_SEQUENCE]->(receiver:Player)
|
| 313 |
+
WITH rak, receiver, r, r.sequence_id as seqId
|
| 314 |
+
MATCH (g:GoalSequence {sequence_id: seqId})
|
| 315 |
+
RETURN receiver.name as PassedTo,
|
| 316 |
+
count(DISTINCT seqId) as NumberOfGoalSequences,
|
| 317 |
+
round(avg(r.order), 2) as AvgPassPosition,
|
| 318 |
+
collect(DISTINCT g.match_id)[0..3] as SampleMatches
|
| 319 |
+
ORDER BY NumberOfGoalSequences DESC
|
| 320 |
+
LIMIT 10
|
| 321 |
+
"""
|
| 322 |
+
|
| 323 |
+
try:
|
| 324 |
+
results = neo4j.execute_query(query)
|
| 325 |
+
|
| 326 |
+
if not results:
|
| 327 |
+
return "❌ Nenhum dado encontrado para Ivan Rakitić"
|
| 328 |
+
|
| 329 |
+
output = ["⭐ Estatísticas de Ivan Rakitić em Sequências de Gol:\n"]
|
| 330 |
+
|
| 331 |
+
total_sequences = sum(r['NumberOfGoalSequences'] for r in results)
|
| 332 |
+
output.append(f"Total de sequências envolvidas: {total_sequences}\n")
|
| 333 |
+
output.append("🎯 Principais destinatários de passes:")
|
| 334 |
+
|
| 335 |
+
for i, record in enumerate(results, 1):
|
| 336 |
+
output.append(
|
| 337 |
+
f"{i}. {record['PassedTo']}: {record['NumberOfGoalSequences']} sequências "
|
| 338 |
+
f"(posição média: {record['AvgPassPosition']})"
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
return "\n".join(output)
|
| 342 |
+
|
| 343 |
+
except Exception as e:
|
| 344 |
+
return f"❌ Erro: {str(e)}"
|
| 345 |
+
|
| 346 |
+
# Executar servidor
|
| 347 |
+
if __name__ == '__main__':
|
| 348 |
+
async def run():
|
| 349 |
+
try:
|
| 350 |
+
# Escolher transporte: 'stdio', 'sse', ou 'streamable-http'
|
| 351 |
+
print("🚀 Iniciando servidor MCP Neo4j...")
|
| 352 |
+
await mcp.run_streamable_http_async() # Usar 'streamable-http' para HTTP
|
| 353 |
+
finally:
|
| 354 |
+
neo4j.close()
|
| 355 |
+
import asyncio
|
| 356 |
+
asyncio.run(run())
|
pyproject.toml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "hf-barca-neo4j-agent"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"pydantic-ai>=1.1.0",
|
| 9 |
+
"tavily-python>=0.7.12",
|
| 10 |
+
]
|
tools/init.py
ADDED
|
File without changes
|
tools/searching.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from tavily import TavilyClient
|
| 3 |
+
|
| 4 |
+
tavily_api_key = os.getenv("TAVILY_API_KEY")
|
| 5 |
+
tavily_api_key_2 = os.getenv("TAVILY_API_KEY_2")
|
| 6 |
+
|
| 7 |
+
class SearchingTools:
|
| 8 |
+
def search_web(
|
| 9 |
+
self,
|
| 10 |
+
search_query: str,
|
| 11 |
+
max_results: int = 3,
|
| 12 |
+
include_domains: list = None,
|
| 13 |
+
exclude_domains: list = None
|
| 14 |
+
) -> str:
|
| 15 |
+
try:
|
| 16 |
+
# Tente a primeira chave
|
| 17 |
+
tavily = TavilyClient(api_key=tavily_api_key)
|
| 18 |
+
search_results = tavily.search(
|
| 19 |
+
query=search_query,
|
| 20 |
+
max_results=max_results,
|
| 21 |
+
include_domains=include_domains,
|
| 22 |
+
exclude_domains=exclude_domains
|
| 23 |
+
)
|
| 24 |
+
except Exception as e:
|
| 25 |
+
# Fallback para a segunda chave se a primeira falhar
|
| 26 |
+
if tavily_api_key_2:
|
| 27 |
+
try:
|
| 28 |
+
tavily = TavilyClient(api_key=tavily_api_key_2)
|
| 29 |
+
search_results = tavily.search(
|
| 30 |
+
query=search_query,
|
| 31 |
+
max_results=max_results,
|
| 32 |
+
include_domains=include_domains,
|
| 33 |
+
exclude_domains=exclude_domains
|
| 34 |
+
)
|
| 35 |
+
except Exception as fallback_e:
|
| 36 |
+
return f"Erro na busca: {str(fallback_e)}"
|
| 37 |
+
else:
|
| 38 |
+
return f"Erro na busca: {str(e)}"
|
| 39 |
+
|
| 40 |
+
raw_data = []
|
| 41 |
+
for result in search_results.get('results', []):
|
| 42 |
+
raw_data.append(f"Url: {result['url']} {result['title']}: {result['content'][:1500]}")
|
| 43 |
+
|
| 44 |
+
return " ".join(raw_data) if raw_data else "Nenhum resultado encontrado."
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|