fredcaixeta commited on
Commit
53ce4cd
·
1 Parent(s): 167b153
Files changed (11) hide show
  1. .gitignore +11 -0
  2. .python-version +1 -0
  3. agent.py +83 -0
  4. app.py +29 -62
  5. client_agent.py +187 -0
  6. main.py +6 -0
  7. mcp_server.py +356 -0
  8. pyproject.toml +10 -0
  9. tools/init.py +0 -0
  10. tools/searching.py +44 -0
  11. 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
- from huggingface_hub import InferenceClient
 
 
3
 
 
 
4
 
5
- def respond(
6
- message,
7
- history: list[dict[str, str]],
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
- messages = [{"role": "system", "content": system_message}]
20
 
21
- messages.extend(history)
22
-
23
- messages.append({"role": "user", "content": message})
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
- with gr.Blocks() as demo:
64
- with gr.Sidebar():
65
- gr.LoginButton()
66
- chatbot.render()
67
-
 
 
 
 
 
 
 
 
68
 
69
  if __name__ == "__main__":
70
- demo.launch()
 
 
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