""" Master Agent - coordinates ExcelAnalysisAgent and WebSearchAgent as tools """ import logging import os import asyncio import json from typing import Dict, Any, Optional from agents import Agent, Runner from agents.mcp import MCPServerStdio, create_static_tool_filter from .excel_agent import create_excel_agent from .web_agent import create_web_search_agent logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) MASTER_AGENT_PROMPT = """ You are the orchestrator of a multi-agent system. Your task is to take the user's query and the file path and pass it to the appropriate agent tool. Available agent tools: - excel_analysis_agent: Executes Python code for data analysis and visualization using pandas and matplotlib. When calling this tool, you MUST pass the complete user query and the file path so it can execute the correct analysis. - web_search_agent: Searches the web for documentation, examples, and solutions. Your strategy: 1. First, try to use the excel_analysis_agent to directly answer the user's query using the file path. IMPORTANT: When calling excel_analysis_agent, include the FULL user query in your message to the tool. 2. If the analysis fails or needs additional context, use the web_search_agent to find relevant information. 3. Use the web search results to guide a retry with the excel_analysis_agent. Always provide clear, actionable results to the user. """ class MasterAgent: """ Master agent that coordinates ExcelAnalysisAgent and WebSearchAgent as tools. """ def __init__(self, api_key: str, model: str = "gpt-4o-mini"): if api_key: os.environ["OPENAI_API_KEY"] = api_key self.model = model def analyze(self, user_query: str, file_path: str) -> Dict[str, Any]: """ Coordinate the two agents to get the best possible result """ async def _arun(): # Create MCP servers python_server = MCPServerStdio( name="excel-tools-python", params={"command": "python", "args": ["-m", "app_agents.mcp_server"]}, cache_tools_list=True, use_structured_content=True, tool_filter=create_static_tool_filter(allowed_tool_names=["execute_python_code"]), ) web_server = MCPServerStdio( name="excel-tools-web", params={"command": "python", "args": ["-m", "app_agents.mcp_server"]}, cache_tools_list=True, tool_filter=create_static_tool_filter(allowed_tool_names=["search_web"]), ) # Connect servers await python_server.connect() await web_server.connect() try: # Create specialized agents using functions from their respective modules excel_agent = create_excel_agent(mcp_server=python_server, model=self.model) web_agent = create_web_search_agent(mcp_server=web_server, model=self.model) # Create orchestrator agent with other agents as tools orchestrator = Agent( name="MasterAgent", model=self.model, instructions=MASTER_AGENT_PROMPT, tools=[ excel_agent.as_tool( tool_name="excel_analysis_agent", tool_description="Execute Python code to analyze Excel/CSV files and create visualizations. The agent receives the user query and file path and must execute the exact analysis requested." ), web_agent.as_tool( tool_name="web_search_agent", tool_description="Search the web for up-to-date information, documentation, and code examples" ), ], ) # Prepare user message with file path user_msg = ( f"User query: {user_query}\n" f"File path: {file_path}\n\n" f"Call the excel_analysis_agent tool with this exact message:\n" f"'Analyze this request: {user_query}\\n\\nThe file is located at: {file_path}\\n\\n" f"Write Python code and call execute_python_code with that code and the same file_path.'\n\n" f"Make sure to pass the complete user query to the excel_analysis_agent so it can perform the correct analysis." ) # Run orchestrator result = await Runner.run(orchestrator, user_msg, max_turns=20) return result finally: # Clean up servers for server in [python_server, web_server]: close_fn = getattr(server, "close", None) or getattr(server, "aclose", None) if close_fn: res = close_fn() if hasattr(res, "__await__"): await res try: loop = asyncio.new_event_loop() try: asyncio.set_event_loop(loop) result = loop.run_until_complete(_arun()) finally: loop.close() asyncio.set_event_loop(None) raw_output = result.final_output or "" # Extract dataframe and images from tool output extracted_df = None extracted_images = [] final_text = raw_output # Extract from result.new_items - Item 1 (ToolCallOutputItem) contains the JSON for item in result.new_items: if hasattr(item, 'output') and isinstance(item.output, str): # Extract JSON from markdown code blocks if present json_str = item.output if "```json" in item.output: parts = item.output.split("```json") if len(parts) > 1: json_str = parts[1].split("```")[0].strip() try: tool_result = json.loads(json_str) if isinstance(tool_result, dict) and "success" in tool_result: # Extract dataframe and images from tool result if isinstance(tool_result.get("dataframe"), list) and tool_result.get("dataframe"): extracted_df = tool_result.get("dataframe") if isinstance(tool_result.get("images"), list) and tool_result.get("images"): extracted_images = tool_result.get("images") break # Found the JSON, no need to continue except (json.JSONDecodeError, ValueError): continue return { 'success': True, 'output': final_text, 'dataframe': extracted_df, 'images': extracted_images, 'code': None, 'error': None } except Exception as e: err = f"MasterAgent error: {e}" logger.error(err) return { "success": False, "output": None, "dataframe": None, "images": [], "code": None, "error": err, }