From d413984c5b4fcf64f9cd85446d22a4de23e739a8 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 25 Apr 2025 17:20:55 -0300 Subject: [PATCH] structure saas with tools and mcps --- src/api/__pycache__/routes.cpython-310.pyc | Bin 6091 -> 6091 bytes .../__pycache__/agent_builder.cpython-310.pyc | Bin 3499 -> 4338 bytes .../__pycache__/agent_runner.cpython-310.pyc | Bin 2748 -> 2891 bytes src/services/agent_builder.py | 51 ++++++--- src/services/agent_runner.py | 24 ++-- src/services/mcp_service.py | 104 ++++++++++++++++++ 6 files changed, 152 insertions(+), 27 deletions(-) create mode 100644 src/services/mcp_service.py diff --git a/src/api/__pycache__/routes.cpython-310.pyc b/src/api/__pycache__/routes.cpython-310.pyc index 1b2bc794f0c180de42317c75ff954db422a1d564..162c691b2e4b174412a09f573c6517b2eb4aefbc 100644 GIT binary patch delta 19 ZcmX@De_Ed_pO=@50SG$ZZR9#A4gfcg1)Kl? delta 19 ZcmX@De_Ed_pO=@50SMIBZR9#A4gfR*1o{8~ diff --git a/src/services/__pycache__/agent_builder.cpython-310.pyc b/src/services/__pycache__/agent_builder.cpython-310.pyc index f8b5a958c7b5600b5ab48b2c79b01ad2617a4be6..60ef6fead0c6d933a4fa338cfcf06c77673abf03 100644 GIT binary patch literal 4338 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 E2?T6=VE_OC diff --git a/src/services/__pycache__/agent_runner.cpython-310.pyc b/src/services/__pycache__/agent_runner.cpython-310.pyc index d4f7c9a608be695da2d4725e365d7bd9eb75ca5e..c84a348c327b0d535adbcc61ea5527e66fccb97c 100644 GIT binary patch delta 815 zcmY*W&1(};5TAMZ+H5wP&DSPv8rxVa1`oC%6r{8k6!hRhYApmM#Jsg;O*go^r60Vt zpBE2-mIq!$Y4Ja>^i%~w1i^#%c=P1RgEtRa-=?j__n6->zxSJ&H_Y4YXI*W@<1&b| z@+0=>Aik~k;m_@h2V)dyqB$^Es+156-8j2}l|iZ*=V($`N?6)R(TtIyS)>10zVVT5XJoJE>!6EEJal=5p(@YLmDCYy1;3X9WpJ8%u|_jw7H*im=Ee!~)G z;s{Oc#M4X+4oZ2^TkyM;^PV|~g6GfngV^aVnDXm6V! zxcU*ffhWm)I`|Ruybv14BCmwj!vkU!c8Pxry~iAX9o|e1`NW`rPPkTYwj6^GM~Zz( zJL~hh4>=$5KJ@s|%NvnPW!Vs$t~&mzJ4$WVu-vLWZ&`+ImxQPjaSX?4v@Cms??;Ak zf`5(N4QIrQv&l!LB4+rsG%`Ujh@^Yv-+D+z4aq9`6_i!sgNGtIlmlT@F(Y)+s)l(~ zgeQOONl~eMes5pe!dr5^Wx2+p+iX0r{SFS7a|-B40yH8#C2 zq|8x-_T)u9n4^MA%op!n$?BP=hQw%o)9=4G z_N^*>-@o{F#rOQ$2-cT~?bnGTA}_-AT~hFir06f+Gf0Ved-D2O0&A#Qp434D-p02| zjxb9dKZyvx0zyw=XTo3L=t8VU=m1FW5CBCgQ<)M-Oky(85;@K>AbJ@wR7SDJZ%XsF zM$ANtb*2;4M|7JHr1>YOT%;2_G8iUK0-Hec3671ax0`ws6q_s;m<*W43{ps>>*#JZ zwwQHb9U}UYS%H0mfiqi4v4VnpY))ND%zTEPMgs5TRqZ$=(ok6LE#l{>5F_>ul@zzgPa2Z diff --git a/src/services/agent_builder.py b/src/services/agent_builder.py index 30d91cdd..5032f3c2 100644 --- a/src/services/agent_builder.py +++ b/src/services/agent_builder.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Tuple from google.adk.agents.llm_agent import LlmAgent from google.adk.agents import SequentialAgent, ParallelAgent, LoopAgent from google.adk.models.lite_llm import LiteLlm @@ -6,7 +6,9 @@ from src.utils.logger import setup_logger from src.core.exceptions import AgentNotFoundError from src.services.agent_service import get_agent from src.services.custom_tools import CustomToolBuilder +from src.services.mcp_service import MCPService from sqlalchemy.orm import Session +from contextlib import AsyncExitStack logger = setup_logger(__name__) @@ -14,23 +16,33 @@ class AgentBuilder: def __init__(self, db: Session): self.db = db self.custom_tool_builder = CustomToolBuilder() + self.mcp_service = MCPService() - def _create_llm_agent(self, agent) -> LlmAgent: + async def _create_llm_agent(self, agent) -> Tuple[LlmAgent, Optional[AsyncExitStack]]: """Cria um agente LLM a partir dos dados do agente.""" # Obtém ferramentas personalizadas da configuração custom_tools = [] if agent.config.get("tools"): custom_tools = self.custom_tool_builder.build_tools(agent.config["tools"]) + # Obtém ferramentas MCP da configuração + mcp_tools = [] + mcp_exit_stack = None + if agent.config.get("mcpServers"): + mcp_tools, mcp_exit_stack = await self.mcp_service.build_tools(agent.config) + + # Combina todas as ferramentas + all_tools = custom_tools + mcp_tools + return LlmAgent( name=agent.name, model=LiteLlm(model=agent.model, api_key=agent.api_key), instruction=agent.instruction, description=agent.config.get("description", ""), - tools=custom_tools, - ) + tools=all_tools, + ), mcp_exit_stack - def _get_sub_agents(self, sub_agent_ids: List[str]) -> List[LlmAgent]: + async def _get_sub_agents(self, sub_agent_ids: List[str]) -> List[Tuple[LlmAgent, Optional[AsyncExitStack]]]: """Obtém e cria os sub-agentes LLM.""" sub_agents = [] for sub_agent_id in sub_agent_ids: @@ -42,29 +54,32 @@ class AgentBuilder: if agent.type != "llm": raise ValueError(f"Agente {agent.name} (ID: {agent.id}) não é um agente LLM") - sub_agents.append(self._create_llm_agent(agent)) + sub_agent, exit_stack = await self._create_llm_agent(agent) + sub_agents.append((sub_agent, exit_stack)) return sub_agents - def build_llm_agent(self, root_agent) -> LlmAgent: + async def build_llm_agent(self, root_agent) -> Tuple[LlmAgent, Optional[AsyncExitStack]]: """Constrói um agente LLM com seus sub-agentes.""" logger.info("Criando agente LLM") sub_agents = [] if root_agent.config.get("sub_agents"): - sub_agents = self._get_sub_agents(root_agent.config.get("sub_agents")) + sub_agents_with_stacks = await self._get_sub_agents(root_agent.config.get("sub_agents")) + sub_agents = [agent for agent, _ in sub_agents_with_stacks] - root_llm_agent = self._create_llm_agent(root_agent) + root_llm_agent, exit_stack = await self._create_llm_agent(root_agent) if sub_agents: root_llm_agent.sub_agents = sub_agents - return root_llm_agent + return root_llm_agent, exit_stack - def build_composite_agent(self, root_agent) -> SequentialAgent | ParallelAgent | LoopAgent: + async def build_composite_agent(self, root_agent) -> Tuple[SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack]]: """Constrói um agente composto (Sequential, Parallel ou Loop) com seus sub-agentes.""" logger.info(f"Processando sub-agentes para agente {root_agent.type}") - sub_agents = self._get_sub_agents(root_agent.config.get("sub_agents", [])) + sub_agents_with_stacks = await self._get_sub_agents(root_agent.config.get("sub_agents", [])) + sub_agents = [agent for agent, _ in sub_agents_with_stacks] if root_agent.type == "sequential": logger.info("Criando SequentialAgent") @@ -72,14 +87,14 @@ class AgentBuilder: name=root_agent.name, sub_agents=sub_agents, description=root_agent.config.get("description", ""), - ) + ), None elif root_agent.type == "parallel": logger.info("Criando ParallelAgent") return ParallelAgent( name=root_agent.name, sub_agents=sub_agents, description=root_agent.config.get("description", ""), - ) + ), None elif root_agent.type == "loop": logger.info("Criando LoopAgent") return LoopAgent( @@ -87,13 +102,13 @@ class AgentBuilder: sub_agents=sub_agents, description=root_agent.config.get("description", ""), max_iterations=root_agent.config.get("max_iterations", 5), - ) + ), None else: raise ValueError(f"Tipo de agente inválido: {root_agent.type}") - def build_agent(self, root_agent) -> LlmAgent | SequentialAgent | ParallelAgent | LoopAgent: + async def build_agent(self, root_agent) -> Tuple[LlmAgent | SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack]]: """Constrói o agente apropriado baseado no tipo do agente root.""" if root_agent.type == "llm": - return self.build_llm_agent(root_agent) + return await self.build_llm_agent(root_agent) else: - return self.build_composite_agent(root_agent) \ No newline at end of file + return await self.build_composite_agent(root_agent) \ No newline at end of file diff --git a/src/services/agent_runner.py b/src/services/agent_runner.py index 7dd08374..e3e2af8c 100644 --- a/src/services/agent_runner.py +++ b/src/services/agent_runner.py @@ -12,6 +12,7 @@ from src.core.exceptions import AgentNotFoundError, InternalServerError from src.services.agent_service import get_agent from src.services.agent_builder import AgentBuilder from sqlalchemy.orm import Session +from contextlib import AsyncExitStack logger = setup_logger(__name__) @@ -40,7 +41,7 @@ async def run_agent( # Usando o AgentBuilder para criar o agente agent_builder = AgentBuilder(db) - root_agent = agent_builder.build_agent(get_root_agent) + root_agent, exit_stack = await agent_builder.build_agent(get_root_agent) logger.info("Configurando Runner") agent_runner = Runner( @@ -70,14 +71,19 @@ async def run_agent( logger.info("Iniciando execução do agente") final_response_text = None - for event in agent_runner.run( - user_id=contact_id, - session_id=session_id, - new_message=content, - ): - if event.is_final_response() and event.content and event.content.parts: - final_response_text = event.content.parts[0].text - logger.info(f"Resposta final recebida: {final_response_text}") + try: + for event in agent_runner.run( + user_id=contact_id, + session_id=session_id, + new_message=content, + ): + if event.is_final_response() and event.content and event.content.parts: + final_response_text = event.content.parts[0].text + logger.info(f"Resposta final recebida: {final_response_text}") + finally: + # Garante que o exit_stack seja fechado corretamente + if exit_stack: + await exit_stack.aclose() logger.info("Execução do agente concluída com sucesso") return final_response_text diff --git a/src/services/mcp_service.py b/src/services/mcp_service.py new file mode 100644 index 00000000..d6f6b756 --- /dev/null +++ b/src/services/mcp_service.py @@ -0,0 +1,104 @@ +from typing import Any, Dict, List, Optional, Tuple +from google.adk.tools.mcp_tool.mcp_toolset import ( + MCPToolset, + StdioServerParameters, + SseServerParams, +) +from contextlib import AsyncExitStack +import os +import logging +from src.utils.logger import setup_logger + +logger = setup_logger(__name__) + +class MCPService: + def __init__(self): + self.tools = [] + self.exit_stack = AsyncExitStack() + + async def _connect_to_mcp_server(self, server_config: Dict[str, Any]) -> Tuple[List[Any], Optional[AsyncExitStack]]: + """Conecta a um servidor MCP específico e retorna suas ferramentas.""" + try: + # Determina o tipo de servidor (local ou remoto) + if "url" in server_config: + # Servidor remoto (SSE) + connection_params = SseServerParams( + url=server_config["url"], + headers=server_config.get("headers", {}) + ) + else: + # Servidor local (Stdio) + command = server_config.get("command", "npx") + args = server_config.get("args", []) + + # Adiciona variáveis de ambiente se especificadas + env = server_config.get("env", {}) + if env: + for key, value in env.items(): + os.environ[key] = value + + connection_params = StdioServerParameters( + command=command, + args=args, + env=env + ) + + tools, exit_stack = await MCPToolset.from_server( + connection_params=connection_params + ) + return tools, exit_stack + + except Exception as e: + logger.error(f"Erro ao conectar ao servidor MCP: {e}") + return [], None + + def _filter_incompatible_tools(self, tools: List[Any]) -> List[Any]: + """Filtra ferramentas incompatíveis com o modelo.""" + problematic_tools = [ + "create_pull_request_review", # Esta ferramenta causa o erro 400 INVALID_ARGUMENT + ] + + filtered_tools = [] + removed_count = 0 + + for tool in tools: + if tool.name in problematic_tools: + logger.warning(f"Removendo ferramenta incompatível: {tool.name}") + removed_count += 1 + else: + filtered_tools.append(tool) + + if removed_count > 0: + logger.warning(f"Removidas {removed_count} ferramentas incompatíveis.") + + return filtered_tools + + async def build_tools(self, mcp_config: Dict[str, Any]) -> Tuple[List[Any], AsyncExitStack]: + """Constrói uma lista de ferramentas a partir de múltiplos servidores MCP.""" + self.tools = [] + self.exit_stack = AsyncExitStack() + + # Processa cada servidor MCP da configuração + for server_name, server_config in mcp_config.get("mcpServers", {}).items(): + logger.info(f"Conectando ao servidor MCP: {server_name}") + try: + tools, exit_stack = await self._connect_to_mcp_server(server_config) + + if tools and exit_stack: + # Filtra ferramentas incompatíveis + filtered_tools = self._filter_incompatible_tools(tools) + self.tools.extend(filtered_tools) + + # Registra o exit_stack com o AsyncExitStack + await self.exit_stack.enter_async_context(exit_stack) + logger.info(f"Conectado com sucesso. Adicionadas {len(filtered_tools)} ferramentas.") + else: + logger.warning(f"Falha na conexão ou nenhuma ferramenta disponível para {server_name}") + + except Exception as e: + logger.error(f"Erro ao conectar ao servidor MCP {server_name}: {e}") + continue + + logger.info(f"MCP Toolset criado com sucesso. Total de {len(self.tools)} ferramentas.") + + return self.tools, self.exit_stack \ No newline at end of file