Spaces:
Running
Running
| """ | |
| Gradio Interface for LangGraph ReAct Agent | |
| A production-ready web interface with real-time streaming output | |
| Styled similar to gradio_ui.py with sidebar layout | |
| """ | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from typing import Generator, Optional | |
| from dataclasses import dataclass | |
| import gradio as gr | |
| from gradio.themes.utils import fonts | |
| from dotenv import load_dotenv | |
| from langchain_openai import ChatOpenAI | |
| from langchain_core.messages import HumanMessage, AIMessage | |
| from langchain_anthropic import ChatAnthropic | |
| # Import the agent from agent_v3 | |
| sys.path.append(os.path.dirname(__file__)) | |
| from agent_v3 import CodeAgent | |
| from core.types import AgentConfig | |
| # Import support modules | |
| from managers import PythonExecutor | |
| # Load environment variables | |
| load_dotenv("./.env") | |
| class GradioAgentUI: | |
| """ | |
| Gradio interface for interacting with the LangGraph ReAct Agent. | |
| Styled similar to the smolagents GradioUI with sidebar layout. | |
| """ | |
| def __init__(self, model=None, config=None): | |
| """Initialize the Gradio Agent UI.""" | |
| # Default model | |
| if model is None: | |
| api_key = os.environ["OPENROUTER_API_KEY"] | |
| if not api_key: | |
| raise ValueError( | |
| "OPENROUTER_API_KEY environment variable is not set. " | |
| "Please set it or provide a model instance." | |
| ) | |
| # model = ChatOpenAI( | |
| # model="google/gemini-2.5-flash", | |
| # base_url="https://openrouter.ai/api/v1", | |
| # temperature=0.7, | |
| # api_key=api_key, | |
| # ) | |
| api_key_anthropic = os.environ["Anthropic_API_KEY"] | |
| model = ChatAnthropic( | |
| model='claude-sonnet-4-5-20250929', | |
| temperature=0.7, | |
| api_key=api_key_anthropic | |
| ) | |
| # Default config | |
| if config is None: | |
| config = AgentConfig( | |
| max_steps=15, | |
| max_conversation_length=30, | |
| retry_attempts=3, | |
| timeout_seconds=1200, | |
| verbose=True | |
| ) | |
| self.model = model | |
| self.config = config | |
| self.name = "Chat with AlphaGenome Agent" | |
| self.description = "AlphaGenome Agent is automatically created by [Paper2Agent](https://github.com/jmiao24/Paper2Agent) to use [AlphaGenome](https://github.com/google-deepmind/alphagenome) and interpret DNA variants." | |
| def get_step_footnote(self, step_num: int, duration: float) -> str: | |
| """Create a footnote for a step with timing information.""" | |
| return f'<span style="color: #888; font-size: 0.9em;">Step {step_num} | Duration: {duration:.2f}s</span>' | |
| def format_code_block(self, code: str) -> str: | |
| """Format code as a Python code block.""" | |
| code = code.strip() | |
| if not code.startswith("```"): | |
| code = f"```python\n{code}\n```" | |
| return code | |
| def extract_content_parts(self, message_content: str) -> dict: | |
| """Extract different parts from the message content.""" | |
| parts = { | |
| 'thinking': '', | |
| 'plan': None, | |
| 'code': None, | |
| 'solution': None, | |
| 'error': None, | |
| 'observation': None | |
| } | |
| # Extract thinking (content before any tags) | |
| thinking_pattern = r'^(.*?)(?=<(?:execute|solution|error|observation)|$)' | |
| thinking_match = re.match(thinking_pattern, message_content, re.DOTALL) | |
| if thinking_match: | |
| thinking = thinking_match.group(1).strip() | |
| # Remove plan from thinking | |
| plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*' | |
| thinking = re.sub(plan_pattern, '', thinking).strip() | |
| # Remove any existing "Thinking:" or "Plan:" labels (including duplicates) | |
| thinking = re.sub(r'(^|\n)(Thinking:|Plan:)\s*', '\n', thinking).strip() | |
| # Remove any duplicate Thinking: that might remain | |
| thinking = re.sub(r'Thinking:\s*', '', thinking).strip() | |
| if thinking: | |
| parts['thinking'] = thinking | |
| # Extract plan | |
| plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*' | |
| plan_match = re.search(plan_pattern, message_content) | |
| if plan_match: | |
| parts['plan'] = plan_match.group(0) | |
| # Extract code | |
| code_match = re.search(r'<execute>(.*?)</execute>', message_content, re.DOTALL) | |
| if code_match: | |
| parts['code'] = code_match.group(1).strip() | |
| # Extract solution | |
| solution_match = re.search(r'<solution>(.*?)</solution>', message_content, re.DOTALL) | |
| if solution_match: | |
| parts['solution'] = solution_match.group(1).strip() | |
| # Extract error | |
| error_match = re.search(r'<error>(.*?)</error>', message_content, re.DOTALL) | |
| if error_match: | |
| parts['error'] = error_match.group(1).strip() | |
| # Extract observation | |
| obs_match = re.search(r'<observation>(.*?)</observation>', message_content, re.DOTALL) | |
| if obs_match: | |
| parts['observation'] = obs_match.group(1).strip() | |
| return parts | |
| def truncate_output(self, text: str, max_lines: int = 20) -> str: | |
| """Truncate output to specified number of lines.""" | |
| lines = text.split('\n') | |
| if len(lines) > max_lines: | |
| truncated = '\n'.join(lines[:max_lines]) | |
| truncated += f"\n... (truncated {len(lines) - max_lines} lines)" | |
| return truncated | |
| return text | |
| def stream_agent_response(self, agent: CodeAgent, query: str) -> Generator: | |
| """Stream agent responses as Gradio ChatMessages with structured blocks.""" | |
| # Get all tool functions using the agent's method | |
| tools_dict = agent.get_all_tool_functions() | |
| agent.python_executor.send_functions(tools_dict) | |
| agent.python_executor.send_variables({}) | |
| # Create initial state | |
| input_state = { | |
| "messages": [HumanMessage(content=query)], | |
| "step_count": 0, | |
| "error_count": 0, | |
| "start_time": time.time(), | |
| "current_plan": None | |
| } | |
| # Track state | |
| displayed_reasoning = set() | |
| previous_plan = None | |
| step_timings = {} | |
| try: | |
| # Stream through the workflow execution | |
| for state in agent.workflow_engine.graph.stream(input_state, stream_mode="values"): | |
| step_count = state.get("step_count", 0) | |
| error_count = state.get("error_count", 0) | |
| current_plan = state.get("current_plan") | |
| # Track timing | |
| if step_count not in step_timings: | |
| step_timings[step_count] = time.time() | |
| message = state["messages"][-1] | |
| if isinstance(message, AIMessage): | |
| content = message.content | |
| parts = self.extract_content_parts(content) | |
| # First yield step header only | |
| if step_count > 0: | |
| step_header = f"## Step {step_count}\n" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=step_header, | |
| metadata={"status": "done"} | |
| ) | |
| # Display Thinking block (styled like Current Plan) | |
| if parts['thinking'] and len(parts['thinking']) > 20: | |
| thinking_text = parts['thinking'] | |
| # Clean up any remaining labels | |
| thinking_text = re.sub(r'(Thinking:|Plan:)\s*', '', thinking_text).strip() | |
| # Clean up excessive whitespace - replace multiple newlines with max 2 | |
| thinking_text = re.sub(r'\n\s*\n\s*\n+', '\n\n', thinking_text).strip() | |
| # Also clean up any leading/trailing whitespace on each line | |
| thinking_text = '\n'.join(line.strip() for line in thinking_text.split('\n') if line.strip()) | |
| content_hash = hash(thinking_text) | |
| if content_hash not in displayed_reasoning and thinking_text: | |
| thinking_block = f"""<div style="background-color: #f8f9fa; border-left: 4px solid #6b7280; padding: 12px; margin: 10px 0; border-radius: 4px;"> | |
| <div style="font-weight: bold; color: #374151; margin-bottom: 8px;">🤔 Thinking</div> | |
| <div style="margin: 0; white-space: pre-line; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #000000; background-color: #ffffff; padding: 10px; border-radius: 4px; font-size: 14px; line-height: 1.6;">{thinking_text}</div> | |
| </div>""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=thinking_block, | |
| metadata={"status": "done"} | |
| ) | |
| displayed_reasoning.add(content_hash) | |
| # Then display Current Plan after thinking | |
| if current_plan and current_plan != previous_plan: | |
| formatted_plan = current_plan.replace('[ ]', '☐').replace('[✓]', '✅').replace('[✗]', '❌') | |
| plan_block = f"""<div style="background-color: #f8f9fa; border-left: 4px solid #000000; padding: 12px; margin: 10px 0; border-radius: 4px;"> | |
| <div style="font-weight: bold; color: #000000; margin-bottom: 8px;">📋 Current Plan</div> | |
| <pre style="margin: 0; white-space: pre-wrap; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; color: #000000; background-color: #ffffff; padding: 10px; border-radius: 4px; font-size: 14px; line-height: 1.6;">{formatted_plan}</pre> | |
| </div>""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=plan_block, | |
| metadata={"status": "done"} | |
| ) | |
| previous_plan = current_plan | |
| # Display Executing Code as independent block with proper syntax highlighting | |
| if parts['code']: | |
| # Format as markdown code block for proper rendering | |
| code_block = f"""<div style="background-color: #f7fafc; border-left: 4px solid #48bb78; padding: 12px; margin: 10px 0; border-radius: 4px;"> | |
| <div style="font-weight: bold; color: #22543d; margin-bottom: 8px;">⚡ Executing Code</div> | |
| </div> | |
| ```python | |
| {parts['code']} | |
| ```""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=code_block, | |
| metadata={"status": "done"} | |
| ) | |
| # Display Execution Result as independent block | |
| if parts['observation']: | |
| truncated = self.truncate_output(parts['observation']) | |
| result_block = f"""<div style="background-color: #fef5e7; border-left: 4px solid #f6ad55; padding: 12px; margin: 10px 0; border-radius: 4px;"> | |
| <div style="font-weight: bold; color: #744210; margin-bottom: 8px;">📊 Execution Result</div> | |
| </div> | |
| ``` | |
| {truncated} | |
| ```""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=result_block, | |
| metadata={"status": "done"} | |
| ) | |
| # Display solution with special formatting as independent block | |
| if parts['solution']: | |
| solution_block = f"""<div style="background-color: #d1fae5; border-left: 4px solid #10b981; padding: 16px; margin: 10px 0; border-radius: 6px;"> | |
| <div style="font-weight: bold; color: #065f46; margin-bottom: 12px; font-size: 16px;">✅ Final Solution</div> | |
| <div class="solution-content" style="color: #1f2937 !important; background-color: #ffffff; padding: 16px; border-radius: 4px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 15px; line-height: 1.7; border: 1px solid #e5e7eb;"> | |
| {parts['solution']} | |
| </div> | |
| <style> | |
| .solution-content, .solution-content * {{ | |
| color: #1f2937 !important; | |
| background-color: transparent !important; | |
| }} | |
| .solution-content code, .solution-content pre {{ | |
| background-color: #f3f4f6 !important; | |
| color: #1f2937 !important; | |
| padding: 2px 6px; | |
| border-radius: 3px; | |
| font-family: 'Monaco', 'Menlo', 'Courier New', monospace; | |
| }} | |
| .solution-content pre {{ | |
| padding: 12px; | |
| overflow-x: auto; | |
| }} | |
| .solution-content table {{ | |
| border-collapse: collapse; | |
| width: 100%; | |
| margin: 10px 0; | |
| background-color: #ffffff !important; | |
| }} | |
| .solution-content td, .solution-content th {{ | |
| border: 1px solid #d1d5db; | |
| padding: 8px; | |
| color: #1f2937 !important; | |
| background-color: #ffffff !important; | |
| }} | |
| .solution-content th {{ | |
| background-color: #f3f4f6 !important; | |
| font-weight: 600; | |
| }} | |
| .solution-content p, .solution-content div, .solution-content span {{ | |
| background-color: transparent !important; | |
| }} | |
| </style> | |
| </div>""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=solution_block, | |
| metadata={"status": "done"} | |
| ) | |
| # Display error with special formatting | |
| if parts['error']: | |
| error_block = f"""<div style="background-color: #fed7d7; border-left: 4px solid #fc8181; padding: 12px; margin: 10px 0; border-radius: 4px;"> | |
| <div style="font-weight: bold; color: #742a2a; margin-bottom: 8px;">⚠️ Error</div> | |
| <div style="color: #742a2a;">{parts['error']}</div> | |
| </div>""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=error_block, | |
| metadata={"status": "done"} | |
| ) | |
| # Add step footnote with timing ONLY after Execution Result | |
| if parts['observation'] and step_count in step_timings: | |
| duration = time.time() - step_timings[step_count] | |
| footnote = f'<div style="color: #718096; font-size: 0.875em; margin-top: 8px;">Step {step_count} | Duration: {duration:.2f}s</div>' | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=footnote, | |
| metadata={"status": "done"} | |
| ) | |
| # Add separator between steps | |
| if step_count > 0: | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content='<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 20px 0;">', | |
| metadata={"status": "done"} | |
| ) | |
| except Exception as e: | |
| error_block = f"""<div style="background-color: #fed7d7; border-left: 4px solid #fc8181; padding: 12px; margin: 10px 0; border-radius: 4px;"> | |
| <div style="font-weight: bold; color: #742a2a; margin-bottom: 8px;">💥 Critical Error</div> | |
| <div style="color: #742a2a;">Error during agent execution: {str(e)}</div> | |
| </div>""" | |
| yield gr.ChatMessage( | |
| role="assistant", | |
| content=error_block, | |
| metadata={"status": "done"} | |
| ) | |
| def interact_with_agent(self, prompt: str, api_key: str, messages: list, session_state: dict) -> Generator: | |
| """Handle interaction with the agent.""" | |
| # Store original user input for display | |
| original_prompt = prompt | |
| # Add API key to the prompt if provided (for agent processing) | |
| if api_key and api_key.strip(): | |
| prompt = f"{prompt} My API key is: {api_key.strip()}." | |
| # Initialize agent in session if needed | |
| if "agent" not in session_state: | |
| session_state["agent"] = CodeAgent( | |
| model=self.model, | |
| config=self.config, | |
| use_tool_manager=True, | |
| use_tool_selection=False # Disable for Gradio UI by default | |
| ) | |
| # Try to load MCP config if available | |
| mcp_config_path = "./mcp_config.yaml" | |
| if os.path.exists(mcp_config_path): | |
| try: | |
| session_state["agent"].add_mcp(mcp_config_path) | |
| print(f"✅ Loaded MCP tools from {mcp_config_path}") | |
| except Exception as e: | |
| print(f"⚠️ Could not load MCP tools: {e}") | |
| agent = session_state["agent"] | |
| try: | |
| # Add user message immediately (use original prompt for display) | |
| messages.append(gr.ChatMessage(role="user", content=original_prompt, metadata={"status": "done"})) | |
| yield messages | |
| # Add a "thinking" indicator immediately | |
| messages.append(gr.ChatMessage(role="assistant", content="🤔 Processing...", metadata={"status": "pending"})) | |
| yield messages | |
| # Remove the thinking indicator before adding real content | |
| messages.pop() | |
| # Stream agent responses with real-time updates | |
| for msg in self.stream_agent_response(agent, prompt): | |
| messages.append(msg) | |
| yield messages | |
| except Exception as e: | |
| messages.append( | |
| gr.ChatMessage( | |
| role="assistant", | |
| content=f"Error: {str(e)}", | |
| metadata={"title": "💥 Error", "status": "done"} | |
| ) | |
| ) | |
| yield messages | |
| def create_app(self): | |
| """Create the Gradio app with sidebar layout.""" | |
| # Create custom theme with modern fonts | |
| modern_theme = gr.themes.Monochrome( | |
| font=fonts.GoogleFont("Inter"), | |
| font_mono=fonts.GoogleFont("JetBrains Mono") | |
| ) | |
| with gr.Blocks(theme=modern_theme, fill_height=True, title=self.name, css=""" | |
| /* Hide Gradio footer */ | |
| .footer {display: none !important;} | |
| footer {display: none !important;} | |
| .gradio-footer {display: none !important;} | |
| #footer {display: none !important;} | |
| [class*="footer"] {display: none !important;} | |
| [id*="footer"] {display: none !important;} | |
| .block.svelte-1scc9gv {display: none !important;} | |
| .built-with-gradio {display: none !important;} | |
| .gradio-container footer {display: none !important;} | |
| """) as demo: | |
| # Force light theme and hide footer | |
| demo.load(js=""" | |
| () => { | |
| document.body.classList.remove('dark'); | |
| document.querySelector('gradio-app').classList.remove('dark'); | |
| const url = new URL(window.location); | |
| url.searchParams.set('__theme', 'light'); | |
| window.history.replaceState({}, '', url); | |
| // Hide footer elements | |
| setTimeout(() => { | |
| const footers = document.querySelectorAll('footer, .footer, .gradio-footer, #footer, [class*="footer"], [id*="footer"], .built-with-gradio'); | |
| footers.forEach(footer => footer.style.display = 'none'); | |
| // Also hide any Gradio logo/branding | |
| const brandingElements = document.querySelectorAll('a[href*="gradio"], .gradio-logo, [alt*="gradio"]'); | |
| brandingElements.forEach(el => el.style.display = 'none'); | |
| }, 100); | |
| } | |
| """) | |
| # Session state | |
| session_state = gr.State({}) | |
| stored_messages = gr.State([]) | |
| with gr.Row(): | |
| # Sidebar | |
| with gr.Column(scale=1): | |
| gr.Markdown(f"# {self.name}") | |
| gr.Markdown(f"> {self.description}") | |
| gr.Markdown("---") | |
| # API Key section | |
| with gr.Group(): | |
| gr.Markdown("**AlphaGenome API Key**") | |
| api_key_input = gr.Textbox( | |
| label="API Key", | |
| type="password", | |
| placeholder="Enter your AlphaGenome API key", | |
| value="AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U" | |
| ) | |
| # Input section | |
| with gr.Group(): | |
| gr.Markdown("**Your Request**") | |
| text_input = gr.Textbox( | |
| lines=4, | |
| label="Query", | |
| placeholder="Enter your query here and press Enter or click Submit", | |
| value="""Use AlphaGenome MCP to analyze heart gene expression data to identify the causal gene | |
| for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia.""" | |
| ) | |
| submit_btn = gr.Button("🚀 Submit", variant="primary", size="lg") | |
| # Configuration section | |
| with gr.Accordion("⚙️ Configuration", open=False): | |
| max_steps_input = gr.Slider( | |
| label="Max Steps", | |
| minimum=5, | |
| maximum=50, | |
| value=self.config.max_steps, | |
| step=1 | |
| ) | |
| temperature_input = gr.Slider( | |
| label="Temperature", | |
| minimum=0.0, | |
| maximum=1.0, | |
| value=0.7, | |
| step=0.1 | |
| ) | |
| apply_config_btn = gr.Button("Apply Configuration", size="sm") | |
| # Example queries | |
| with gr.Accordion("📚 Example Queries", open=False): | |
| gr.Examples( | |
| examples=[ | |
| ["What's the quantile score of chr3:197081044:TACTC>T on splice junction in Artery (tibial)?"], | |
| ["What are the raw and quantile scores of the variant chr3:120280774:G>T for chromatin accessibility in the GM12878 cell line?"], | |
| ], | |
| inputs=text_input, | |
| ) | |
| gr.Markdown("---") | |
| # gr.HTML( | |
| # "<center><small>Powered by LangGraph & OpenRouter</small></center>" | |
| # ) | |
| # Main chat area | |
| with gr.Column(scale=3): | |
| chatbot = gr.Chatbot( | |
| label="Agent Conversation", | |
| type="messages", | |
| height=700, | |
| show_copy_button=True, | |
| avatar_images=( | |
| None, # Default user avatar | |
| "images.png" # Assistant avatar | |
| ), | |
| latex_delimiters=[ | |
| {"left": r"$$", "right": r"$$", "display": True}, | |
| {"left": r"$", "right": r"$", "display": False}, | |
| ], | |
| render_markdown=True, | |
| sanitize_html=False, # Allow custom HTML for better formatting | |
| ) | |
| with gr.Row(): | |
| clear_btn = gr.Button("🗑️ Clear", size="sm") | |
| # Note: stop_btn is created but not wired up yet | |
| # This could be used for future cancellation functionality | |
| stop_btn = gr.Button("⏹️ Stop", size="sm", variant="stop") | |
| # Event handlers | |
| def update_config(max_steps, temperature, session_state): | |
| """Update agent configuration.""" | |
| if "agent" in session_state: | |
| agent = session_state["agent"] | |
| agent.config.max_steps = max_steps | |
| if hasattr(agent.model, 'temperature'): | |
| agent.model.temperature = temperature | |
| return "Configuration updated!" | |
| def clear_chat(): | |
| """Clear the chat.""" | |
| return [], [] | |
| # Wire up events | |
| text_input.submit( | |
| lambda x: ("", gr.Button(interactive=False)), | |
| [text_input], | |
| [text_input, submit_btn] | |
| ).then( | |
| self.interact_with_agent, | |
| [stored_messages, api_key_input, chatbot, session_state], | |
| [chatbot] | |
| ).then( | |
| lambda: gr.Button(interactive=True), | |
| None, | |
| [submit_btn] | |
| ) | |
| submit_btn.click( | |
| lambda x: (x, "", gr.Button(interactive=False)), | |
| [text_input], | |
| [stored_messages, text_input, submit_btn] | |
| ).then( | |
| self.interact_with_agent, | |
| [stored_messages, api_key_input, chatbot, session_state], | |
| [chatbot] | |
| ).then( | |
| lambda: gr.Button(interactive=True), | |
| None, | |
| [submit_btn] | |
| ) | |
| apply_config_btn.click( | |
| update_config, | |
| [max_steps_input, temperature_input, session_state], | |
| None | |
| ) | |
| clear_btn.click( | |
| clear_chat, | |
| None, | |
| [chatbot, stored_messages] | |
| ) | |
| return demo | |
| def launch(self, share: bool = False, **kwargs): | |
| """Launch the Gradio app.""" | |
| app = self.create_app() | |
| # Set defaults if not provided in kwargs | |
| kwargs.setdefault('server_name', '0.0.0.0') | |
| kwargs.setdefault('server_port', 7860) | |
| kwargs.setdefault('show_error', True) | |
| app.queue(max_size=10).launch( | |
| share=share, | |
| show_api=False, | |
| favicon_path=None, | |
| **kwargs | |
| ) | |
| if __name__ == "__main__": | |
| # Check for API key before starting | |
| if not os.environ.get("OPENROUTER_API_KEY"): | |
| print("\n⚠️ Error: OPENROUTER_API_KEY environment variable is not set!") | |
| print("\nPlease set it using one of these methods:") | |
| print("1. Export it in your shell:") | |
| print(" export OPENROUTER_API_KEY='your-api-key-here'") | |
| print("\n2. Create a .env file in the current directory with:") | |
| print(" OPENROUTER_API_KEY=your-api-key-here") | |
| print("\n3. Or provide your own model instance when initializing GradioAgentUI") | |
| sys.exit(1) | |
| try: | |
| # Create and launch the interface with optional MCP config | |
| ui = GradioAgentUI() | |
| # Optional: Load MCP tools if config exists | |
| mcp_config_path = "./mcp_config.yaml" | |
| if os.path.exists(mcp_config_path): | |
| print(f"Found MCP config at {mcp_config_path}") | |
| ui.launch(share=False, quiet=False) | |
| except Exception as e: | |
| print(f"\n❌ Error starting Gradio interface: {e}") | |
| sys.exit(1) |