minion-space / app_with_mcp.py
femtowin's picture
default
13e03a4
import gradio as gr
import asyncio
import os
from typing import Dict, Any, List, Optional
from dotenv import load_dotenv
from minion.main import LocalPythonEnv
from minion.main.rpyc_python_env import RpycPythonEnv
from minion.main.brain import Brain
from minion.providers import create_llm_provider
# Import our MCP integration
from mcp_integration import MCPBrainClient, create_final_answer_tool, BrainTool, add_filesystem_tool
# Load .env file
load_dotenv()
class LLMConfig:
def __init__(self, api_type: str, api_key: str, base_url: str, api_version: str,
model: str, temperature: float = 0.7, max_tokens: int = 4000,
vision_enabled: bool = False):
self.api_type = api_type
self.api_key = api_key
self.base_url = base_url
self.api_version = api_version
self.model = model
self.temperature = temperature
self.max_tokens = max_tokens
self.vision_enabled = vision_enabled
def get_preset_configs():
"""Get preset configurations"""
presets = {
"gpt-4o": LLMConfig(
api_type=os.getenv("GPT_4O_API_TYPE", "azure"),
api_key=os.getenv("GPT_4O_API_KEY", ""),
base_url=os.getenv("GPT_4O_BASE_URL", ""),
api_version=os.getenv("GPT_4O_API_VERSION", "2024-06-01"),
model=os.getenv("GPT_4O_MODEL", "gpt-4o"),
temperature=float(os.getenv("GPT_4O_TEMPERATURE", "0")),
max_tokens=int(os.getenv("GPT_4O_MAX_TOKENS", "4000"))
),
"gpt-4o-mini": LLMConfig(
api_type=os.getenv("GPT_4O_MINI_API_TYPE", "azure"),
api_key=os.getenv("GPT_4O_MINI_API_KEY", ""),
base_url=os.getenv("GPT_4O_MINI_BASE_URL", ""),
api_version=os.getenv("GPT_4O_MINI_API_VERSION", "2024-06-01"),
model=os.getenv("GPT_4O_MINI_MODEL", "gpt-4o-mini"),
temperature=float(os.getenv("GPT_4O_MINI_TEMPERATURE", "0.1")),
max_tokens=int(os.getenv("GPT_4O_MINI_MAX_TOKENS", "4000"))
),
"gpt-4.1": LLMConfig(
api_type=os.getenv("GPT_41_API_TYPE", "azure"),
api_key=os.getenv("GPT_41_API_KEY", ""),
base_url=os.getenv("GPT_41_BASE_URL", ""),
api_version=os.getenv("GPT_41_API_VERSION", "2025-03-01-preview"),
model=os.getenv("GPT_41_MODEL", "gpt-4.1"),
temperature=float(os.getenv("GPT_41_TEMPERATURE", "0.7")),
max_tokens=int(os.getenv("GPT_41_MAX_TOKENS", "4000"))
),
"o4-mini": LLMConfig(
api_type=os.getenv("O4_MINI_API_TYPE", "azure"),
api_key=os.getenv("O4_MINI_API_KEY", ""),
base_url=os.getenv("O4_MINI_BASE_URL", ""),
api_version=os.getenv("O4_MINI_API_VERSION", "2025-03-01-preview"),
model=os.getenv("O4_MINI_MODEL", "o4-mini"),
temperature=float(os.getenv("O4_MINI_TEMPERATURE", "0.7")),
max_tokens=int(os.getenv("O4_MINI_MAX_TOKENS", "4000"))
)
}
return presets
def get_default_config():
"""Get default configuration"""
return LLMConfig(
api_type=os.getenv("DEFAULT_API_TYPE", "azure"),
api_key=os.getenv("DEFAULT_API_KEY", ""),
base_url=os.getenv("DEFAULT_BASE_URL", ""),
api_version=os.getenv("DEFAULT_API_VERSION", "2024-06-01"),
model=os.getenv("DEFAULT_MODEL", "gpt-4o"),
temperature=float(os.getenv("DEFAULT_TEMPERATURE", "0.7")),
max_tokens=int(os.getenv("DEFAULT_MAX_TOKENS", "4000"))
)
def get_available_routes():
"""Get available route options for current minion system"""
return [
"", # Auto route selection (empty for automatic)
"raw", # Raw LLM output without processing
"native", # Native minion processing
"cot", # Chain of Thought reasoning
"dcot", # Dynamic Chain of Thought
"plan", # Planning-based approach
"python" # Python code execution
]
def create_custom_llm_config(api_type: str, api_key: str, base_url: str,
api_version: str, model: str, temperature: float,
max_tokens: int) -> Dict[str, Any]:
"""Create custom LLM configuration"""
return {
'api_type': api_type,
'api_key': api_key,
'base_url': base_url,
'api_version': api_version,
'model': model,
'temperature': temperature,
'max_tokens': max_tokens,
'vision_enabled': False
}
def build_brain_with_config(llm_config_dict: Dict[str, Any]):
"""Build brain with specified configuration"""
# Create a config object similar to LLMConfig
class Config:
def __init__(self, config_dict):
for key, value in config_dict.items():
setattr(self, key, value)
config_obj = Config(llm_config_dict)
llm = create_llm_provider(config_obj)
python_env = LocalPythonEnv(verbose=False)
brain = Brain(
python_env=python_env,
llm=llm,
)
return brain
# Global MCP client instance
mcp_client: Optional[MCPBrainClient] = None
async def setup_mcp_tools():
"""Setup MCP tools and connections"""
global mcp_client
if mcp_client is None:
mcp_client = MCPBrainClient()
await mcp_client.__aenter__()
# Add filesystem tool (always try to add this)
try:
await add_filesystem_tool(mcp_client)
print("✓ Added filesystem tool")
except Exception as e:
print(f"⚠ Failed to add filesystem tool: {e}")
# Add MCP servers from environment variables
# Example: SSE server
sse_url = os.getenv("MCP_SSE_URL")
if sse_url:
try:
await mcp_client.add_mcp_server("sse", url=sse_url)
print(f"✓ Added SSE server: {sse_url}")
except Exception as e:
print(f"⚠ Failed to add SSE server: {e}")
# Example: Stdio server
stdio_command = os.getenv("MCP_STDIO_COMMAND")
if stdio_command:
try:
await mcp_client.add_mcp_server("stdio", command=stdio_command)
print(f"✓ Added stdio server: {stdio_command}")
except Exception as e:
print(f"⚠ Failed to add stdio server: {e}")
return mcp_client
async def get_available_tools() -> List[BrainTool]:
"""Get all available tools including MCP tools and final answer tool"""
try:
mcp_client = await setup_mcp_tools()
mcp_tools = mcp_client.get_tools_for_brain()
except Exception as e:
print(f"Warning: Failed to setup MCP tools: {e}")
mcp_tools = []
# Always add final answer tool
final_answer_tool = create_final_answer_tool()
return mcp_tools #+ [final_answer_tool]
# Get preset configurations and default configuration
preset_configs = get_preset_configs()
default_config = get_default_config()
available_routes = get_available_routes()
async def minion_respond_async(query: str,
preset_model: str = "gpt-4o",
api_type: str = None,
api_key: str = None,
base_url: str = None,
api_version: str = None,
model: str = None,
temperature: float = None,
max_tokens: int = None,
route: str = "",
query_type: str = "calculate",
check_enabled: bool = False,
use_tools: bool = True):
"""Respond to query using specified configuration with optional MCP tools"""
# Get default config for None values
if api_type is None:
api_type = default_config.api_type
if api_key is None:
api_key = default_config.api_key
if base_url is None:
base_url = default_config.base_url
if api_version is None:
api_version = default_config.api_version
if model is None:
model = default_config.model
if temperature is None:
temperature = default_config.temperature
if max_tokens is None:
max_tokens = default_config.max_tokens
# Always use the current UI values, regardless of preset selection
# Preset is only used for initializing UI fields, not for actual execution
llm_config_dict = create_custom_llm_config(
api_type, api_key, base_url, api_version, model, temperature, max_tokens
)
if preset_model != "Custom":
print(f"🔧 Using preset '{preset_model}' base with UI overrides:")
print(f" - API Type: {api_type}")
print(f" - Model: {model}")
print(f" - Base URL: {base_url}")
print(f" - API Version: {api_version}")
print(f" - Temperature: {temperature}")
print(f" - Max tokens: {max_tokens}")
else:
print(f"🔧 Using custom configuration:")
print(f" - API Type: {api_type}")
print(f" - Model: {model}")
print(f" - Base URL: {base_url}")
print(f" - API Version: {api_version}")
print(f" - Temperature: {temperature}")
print(f" - Max tokens: {max_tokens}")
# Always rebuild brain with current UI configuration
print(f"🧠 Building brain with final config:")
print(f" - Final API type: {llm_config_dict['api_type']}")
print(f" - Final model: {llm_config_dict['model']}")
print(f" - Final temperature: {llm_config_dict['temperature']}")
print(f" - Final max_tokens: {llm_config_dict['max_tokens']}")
brain = build_brain_with_config(llm_config_dict)
# Handle empty route selection for auto route
route_param = route if route else None
# Build kwargs for brain.step
kwargs = {'query': query, 'route': route_param, 'check': check_enabled}
# Add query_type to kwargs if route is python
if route == "python" and query_type:
kwargs['query_type'] = query_type
# Add tools if enabled
if use_tools:
try:
tools = await get_available_tools()
kwargs['tools'] = tools
print(f"🔧 Using {len(tools)} tools: {[tool.name for tool in tools]}")
except Exception as e:
print(f"⚠️ Warning: Failed to get tools: {e}")
print(f"🚀 Executing brain.step with route='{route_param}', check={check_enabled}")
obs, score, *_ = await brain.step(**kwargs)
return obs
def minion_respond(query: str, preset_model: str, api_type: str, api_key: str,
base_url: str, api_version: str, model: str, temperature: float,
max_tokens: int, route: str, query_type: str, check_enabled: bool,
use_tools: bool):
"""Gradio sync interface, automatically schedules async"""
return asyncio.run(minion_respond_async(
query, preset_model, api_type, api_key, base_url,
api_version, model, temperature, max_tokens, route, query_type, check_enabled,
use_tools
))
def update_fields(preset_model: str):
"""Update other fields when preset model is selected"""
if preset_model == "Custom":
# Return default values, let user configure themselves
return (
default_config.api_type,
default_config.api_key, # Show real API key instead of empty
default_config.base_url,
default_config.api_version,
default_config.model,
default_config.temperature,
default_config.max_tokens
)
else:
config_obj = preset_configs.get(preset_model, default_config)
# Ensure API type is from valid choices
api_type = config_obj.api_type if config_obj.api_type in ["azure", "openai", "groq", "ollama", "anthropic", "gemini"] else "azure"
return (
api_type,
config_obj.api_key, # Show real API key from preset config
config_obj.base_url,
config_obj.api_version,
config_obj.model,
config_obj.temperature,
config_obj.max_tokens
)
def update_query_type_visibility(route: str):
"""Show query_type dropdown only when route is python"""
return gr.update(visible=(route == "python"))
async def get_tool_status():
"""Get status of available tools"""
try:
tools = await get_available_tools()
return f"Available tools: {', '.join([tool.name for tool in tools])}"
except Exception as e:
return f"Error getting tools: {str(e)}"
def check_tools():
"""Sync wrapper for tool status check"""
return asyncio.run(get_tool_status())
# Create Gradio interface
with gr.Blocks(title="Minion Brain Chat with MCP Tools") as demo:
gr.Markdown("# Minion Brain Chat with MCP Tools\nIntelligent Q&A powered by Minion1 Brain with MCP tool support")
with gr.Row():
with gr.Column(scale=2):
query_input = gr.Textbox(
label="Enter your question",
placeholder="Please enter your question...",
lines=3
)
submit_btn = gr.Button("Submit", variant="primary")
# Tool status
with gr.Row():
tool_status_btn = gr.Button("Check Available Tools", size="sm")
tool_status_output = gr.Textbox(
label="Tool Status",
lines=2,
interactive=False
)
# Move Answer section to left column, closer to question input
output = gr.Textbox(
label="Answer",
lines=10,
show_copy_button=True
)
with gr.Column(scale=1):
# Tool settings
use_tools_checkbox = gr.Checkbox(
label="Enable MCP Tools",
value=True,
info="Use Model Context Protocol tools"
)
# Move route selection to the front
route_dropdown = gr.Dropdown(
label="Route",
choices=available_routes,
value="",
info="empty: auto select, raw: direct LLM, native: standard, cot: chain of thought, dcot: dynamic cot, plan: planning, python: code execution"
)
# Add query_type option, visible only when route="python"
query_type_dropdown = gr.Dropdown(
label="Query Type",
choices=["calculate", "code_solution", "generate"],
value="calculate",
visible=False,
info="Type of query for python route"
)
# Add check option
check_checkbox = gr.Checkbox(
label="Enable Check",
value=False,
info="Enable output verification and validation"
)
preset_dropdown = gr.Dropdown(
label="Preset Model",
choices=["Custom"] + list(preset_configs.keys()),
value="gpt-4o",
info="Select preset configuration or custom"
)
api_type_input = gr.Dropdown(
label="API Type",
choices=["azure", "openai", "groq", "ollama", "anthropic", "gemini"],
value=default_config.api_type,
info="Select API provider type"
)
api_key_input = gr.Textbox(
label="API Key",
value=default_config.api_key, # Show real API key instead of masked value
type="password",
info="Your API key"
)
base_url_input = gr.Textbox(
label="Base URL",
value=default_config.base_url,
info="API base URL"
)
api_version_input = gr.Textbox(
label="API Version",
value=default_config.api_version,
info="API version (required for Azure)"
)
model_input = gr.Textbox(
label="Model",
value=default_config.model,
info="Model name"
)
temperature_input = gr.Slider(
label="Temperature",
minimum=0.0,
maximum=2.0,
value=default_config.temperature,
step=0.1,
info="Control output randomness"
)
max_tokens_input = gr.Slider(
label="Max Tokens",
minimum=100,
maximum=8000,
value=default_config.max_tokens,
step=100,
info="Maximum number of tokens to generate"
)
# Update other fields when preset model changes
preset_dropdown.change(
fn=update_fields,
inputs=[preset_dropdown],
outputs=[api_type_input, api_key_input, base_url_input,
api_version_input, model_input, temperature_input, max_tokens_input]
)
# Update query_type visibility when route changes
route_dropdown.change(
fn=update_query_type_visibility,
inputs=[route_dropdown],
outputs=[query_type_dropdown]
)
# Tool status check
tool_status_btn.click(
fn=check_tools,
outputs=[tool_status_output]
)
# Submit button event
submit_btn.click(
fn=minion_respond_async,
inputs=[query_input, preset_dropdown, api_type_input, api_key_input,
base_url_input, api_version_input, model_input, temperature_input,
max_tokens_input, route_dropdown, query_type_dropdown, check_checkbox,
use_tools_checkbox],
outputs=[output]
)
# Enter key submit
query_input.submit(
fn=minion_respond_async,
inputs=[query_input, preset_dropdown, api_type_input, api_key_input,
base_url_input, api_version_input, model_input, temperature_input,
max_tokens_input, route_dropdown, query_type_dropdown, check_checkbox,
use_tools_checkbox],
outputs=[output]
)
# Cleanup function
async def cleanup_on_exit():
"""Clean up MCP client on exit"""
global mcp_client
if mcp_client:
await mcp_client.cleanup()
if __name__ == "__main__":
try:
demo.launch(mcp_server=True)
finally:
asyncio.run(cleanup_on_exit())