├── .clinerules ├── .cursor └── rules │ ├── design.mdc │ └── memory_bank.mdc ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── custom_azure_model.py ├── docs ├── deep_research.md └── images │ └── wechat_group_qr.png ├── example.py ├── example_browser_use.py ├── example_deep_research.py ├── example_deep_research_html.py ├── example_deep_research_pdf.py ├── example_reason.py ├── example_search_deepseek_prover.py ├── example_with_managed_agents.py ├── examples ├── __init__.py ├── brain.py └── minion_with_smolagents_example.py ├── minion_agent ├── __init__.py ├── agent.py ├── config │ ├── __init__.py │ └── settings.py ├── frameworks │ ├── browser_use.py │ ├── deep_research.py │ ├── deep_research_llms.py │ ├── deep_research_prompts.yaml │ ├── deep_research_types.py │ ├── google.py │ ├── langchain.py │ ├── minion.py │ ├── minion_agent.py │ ├── openai.py │ ├── smolagents.py │ └── tinyagent.py ├── instructions │ ├── __init__.py │ └── imports.py ├── logging.py ├── providers │ ├── __init__.py │ └── adapters.py ├── serving │ ├── __init__.py │ ├── agent_card.py │ ├── agent_executor.py │ └── server.py ├── tools │ ├── __init__.py │ ├── adapters.py │ ├── base.py │ ├── browser_tool.py │ ├── decorators.py │ ├── generation.py │ ├── image_generation.py │ ├── mcp.py │ ├── mcp │ │ └── frameworks │ │ │ ├── __init__.py │ │ │ ├── agno.py │ │ │ ├── google.py │ │ │ ├── langchain.py │ │ │ ├── llama_index.py │ │ │ ├── openai.py │ │ │ ├── smolagents.py │ │ │ └── tinyagent.py │ ├── user_interaction.py │ ├── web_browsing.py │ └── wrappers.py └── utils │ ├── __init__.py │ └── logging.py ├── original.md ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_basic_adapter.py ├── test_minion_provider_adapter.py ├── test_smolagents_integration.py ├── test_smolagents_tools.py └── test_tool_functionality.py /.clinerules: -------------------------------------------------------------------------------- 1 | # Cline's Memory Bank 2 | 3 | I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. 4 | 5 | ## Memory Bank Structure 6 | 7 | The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: 8 | 9 | flowchart TD 10 | PB[projectbrief.md] --> PC[productContext.md] 11 | PB --> SP[systemPatterns.md] 12 | PB --> TC[techContext.md] 13 | 14 | PC --> AC[activeContext.md] 15 | SP --> AC 16 | TC --> AC 17 | 18 | AC --> P[progress.md] 19 | 20 | ### Core Files (Required) 21 | 1. `projectbrief.md` 22 | - Foundation document that shapes all other files 23 | - Created at project start if it doesn't exist 24 | - Defines core requirements and goals 25 | - Source of truth for project scope 26 | 27 | 2. `productContext.md` 28 | - Why this project exists 29 | - Problems it solves 30 | - How it should work 31 | - User experience goals 32 | 33 | 3. `activeContext.md` 34 | - Current work focus 35 | - Recent changes 36 | - Next steps 37 | - Active decisions and considerations 38 | - Important patterns and preferences 39 | - Learnings and project insights 40 | 41 | 4. `systemPatterns.md` 42 | - System architecture 43 | - Key technical decisions 44 | - Design patterns in use 45 | - Component relationships 46 | - Critical implementation paths 47 | 48 | 5. `techContext.md` 49 | - Technologies used 50 | - Development setup 51 | - Technical constraints 52 | - Dependencies 53 | - Tool usage patterns 54 | 55 | 6. `progress.md` 56 | - What works 57 | - What's left to build 58 | - Current status 59 | - Known issues 60 | - Evolution of project decisions 61 | 62 | ### Additional Context 63 | Create additional files/folders within memory-bank/ when they help organize: 64 | - Complex feature documentation 65 | - Integration specifications 66 | - API documentation 67 | - Testing strategies 68 | - Deployment procedures 69 | 70 | ## Core Workflows 71 | 72 | ### Plan Mode 73 | flowchart TD 74 | Start[Start] --> ReadFiles[Read Memory Bank] 75 | ReadFiles --> CheckFiles{Files Complete?} 76 | 77 | CheckFiles -->|No| Plan[Create Plan] 78 | Plan --> Document[Document in Chat] 79 | 80 | CheckFiles -->|Yes| Verify[Verify Context] 81 | Verify --> Strategy[Develop Strategy] 82 | Strategy --> Present[Present Approach] 83 | 84 | ### Act Mode 85 | flowchart TD 86 | Start[Start] --> Context[Check Memory Bank] 87 | Context --> Update[Update Documentation] 88 | Update --> Execute[Execute Task] 89 | Execute --> Document[Document Changes] 90 | 91 | ## Documentation Updates 92 | 93 | Memory Bank updates occur when: 94 | 1. Discovering new project patterns 95 | 2. After implementing significant changes 96 | 3. When user requests with **update memory bank** (MUST review ALL files) 97 | 4. When context needs clarification 98 | 99 | flowchart TD 100 | Start[Update Process] 101 | 102 | subgraph Process 103 | P1[Review ALL Files] 104 | P2[Document Current State] 105 | P3[Clarify Next Steps] 106 | P4[Document Insights & Patterns] 107 | 108 | P1 --> P2 --> P3 --> P4 109 | end 110 | 111 | Start --> Process 112 | 113 | Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state. 114 | 115 | REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. -------------------------------------------------------------------------------- /.cursor/rules/design.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | 7 | # Your rule content 8 | 9 | - 我的个人空间是~/space, 请把design写在那下面, 10 | 然后这个项目是minion-manus,所以可以把design写/Users/femtozheng/space/minion-manus下 11 | -------------------------------------------------------------------------------- /.cursor/rules/memory_bank.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Cline's Memory Bank 7 | 8 | I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. 9 | 10 | ## Memory Bank Structure 11 | 12 | The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: 13 | 14 | flowchart TD 15 | PB[projectbrief.md] --> PC[productContext.md] 16 | PB --> SP[systemPatterns.md] 17 | PB --> TC[techContext.md] 18 | 19 | PC --> AC[activeContext.md] 20 | SP --> AC 21 | TC --> AC 22 | 23 | AC --> P[progress.md] 24 | 25 | ### Core Files (Required) 26 | 1. `projectbrief.md` 27 | - Foundation document that shapes all other files 28 | - Created at project start if it doesn't exist 29 | - Defines core requirements and goals 30 | - Source of truth for project scope 31 | 32 | 2. `productContext.md` 33 | - Why this project exists 34 | - Problems it solves 35 | - How it should work 36 | - User experience goals 37 | 38 | 3. `activeContext.md` 39 | - Current work focus 40 | - Recent changes 41 | - Next steps 42 | - Active decisions and considerations 43 | - Important patterns and preferences 44 | - Learnings and project insights 45 | 46 | 4. `systemPatterns.md` 47 | - System architecture 48 | - Key technical decisions 49 | - Design patterns in use 50 | - Component relationships 51 | - Critical implementation paths 52 | 53 | 5. `techContext.md` 54 | - Technologies used 55 | - Development setup 56 | - Technical constraints 57 | - Dependencies 58 | - Tool usage patterns 59 | 60 | 6. `progress.md` 61 | - What works 62 | - What's left to build 63 | - Current status 64 | - Known issues 65 | - Evolution of project decisions 66 | 67 | ### Additional Context 68 | Create additional files/folders within memory-bank/ when they help organize: 69 | - Complex feature documentation 70 | - Integration specifications 71 | - API documentation 72 | - Testing strategies 73 | - Deployment procedures 74 | 75 | ## Core Workflows 76 | 77 | ### Plan Mode 78 | flowchart TD 79 | Start[Start] --> ReadFiles[Read Memory Bank] 80 | ReadFiles --> CheckFiles{Files Complete?} 81 | 82 | CheckFiles -->|No| Plan[Create Plan] 83 | Plan --> Document[Document in Chat] 84 | 85 | CheckFiles -->|Yes| Verify[Verify Context] 86 | Verify --> Strategy[Develop Strategy] 87 | Strategy --> Present[Present Approach] 88 | 89 | ### Act Mode 90 | flowchart TD 91 | Start[Start] --> Context[Check Memory Bank] 92 | Context --> Update[Update Documentation] 93 | Update --> Execute[Execute Task] 94 | Execute --> Document[Document Changes] 95 | 96 | ## Documentation Updates 97 | 98 | Memory Bank updates occur when: 99 | 1. Discovering new project patterns 100 | 2. After implementing significant changes 101 | 3. When user requests with **update memory bank** (MUST review ALL files) 102 | 4. When context needs clarification 103 | 104 | flowchart TD 105 | Start[Update Process] 106 | 107 | subgraph Process 108 | P1[Review ALL Files] 109 | P2[Document Current State] 110 | P3[Clarify Next Steps] 111 | P4[Document Insights & Patterns] 112 | 113 | P1 --> P2 --> P3 --> P4 114 | end 115 | 116 | Start --> Process 117 | 118 | Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state. 119 | 120 | REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #OPENAI_API_KEY=sk-your-openai-api-key-here 2 | #OPENAI_BASE_URL=http://localhost:3001/v1 3 | 4 | OPENAI_API_VERSION=2025-03-01-preview 5 | #needed by Litellm 6 | AZURE_API_VERSION=2025-03-01-preview 7 | AZURE_API_KEY=your-azure-api-key-here 8 | AZURE_API_BASE=https://your-azure-endpoint.cognitiveservices.azure.com 9 | 10 | AZURE_OPENAI_ENDPOINT=https://your-azure-endpoint.cognitiveservices.azure.com 11 | AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here 12 | #AZURE_DEPLOYMENT_NAME=o4-mini 13 | AZURE_DEPLOYMENT_NAME=gpt-4.1 14 | #AZURE_DEPLOYMENT_NAME=gpt-4o 15 | 16 | #OPENAI_API_KEY=your-anthropic-api-key-here 17 | #OPENAI_BASE_URL=https://api.anthropic.com/v1 18 | #AZURE_DEPLOYMENT_NAME=claude-3-5-sonnet-20241022 19 | 20 | #BROWSER_BINARY_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome 21 | #TOGETHER_AI_API_KEY=your-together-ai-api-key-here 22 | TOGETHER_AI_API_KEY=your-together-ai-api-key-here-to-do-image-gen 23 | TAVILY_API_KEY=your-tavily-api-key-here-to-do-websearch-when-deep-research 24 | HUGGINGFACE_TOKEN=your-huggingface-token-here 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm and other JetBrains IDEs 2 | .idea/ 3 | *.iml 4 | *.iws 5 | *.ipr 6 | .idea_modules/ 7 | 8 | # Python 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | *.so 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # Virtual environments 32 | venv/ 33 | env/ 34 | ENV/ 35 | .env 36 | .venv 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Jupyter Notebook 41 | .ipynb_checkpoints 42 | 43 | # pytest 44 | .pytest_cache/ 45 | .coverage 46 | htmlcov/ 47 | 48 | # mypy 49 | .mypy_cache/ 50 | .dmypy.json 51 | dmypy.json 52 | 53 | # Logs 54 | logs/ 55 | *.log 56 | 57 | # OS specific files 58 | .DS_Store 59 | .DS_Store? 60 | ._* 61 | .Spotlight-V100 62 | .Trashes 63 | ehthumbs.db 64 | Thumbs.db 65 | 66 | # Browser automation artifacts 67 | 68 | *.jpg 69 | *.jpeg 70 | *.gif 71 | *.pdf 72 | *.svg 73 | screenshots/ 74 | 75 | # Local configuration 76 | .env.local 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | 81 | # Playwright 82 | node_modules/ 83 | playwright-report/ 84 | test-results/ 85 | 86 | # Ruff 87 | .ruff_cache/ 88 | 89 | # VS Code 90 | .vscode/ 91 | *.code-workspace 92 | 93 | config.yaml 94 | .idea 95 | *.bak -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 FemtoZheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[![Documentation Status](https://img.shields.io/badge/documentation-brightgreen)](https://github.com/femto/minion-agent) 2 | [![Install](https://img.shields.io/badge/get_started-blue)](https://github.com/femto/minion-agent) 3 | [![Discord](https://dcbadge.limes.pink/api/server/HUC6xEK9aT?style=flat)](https://discord.gg/HUC6xEK9aT) 4 | [![Twitter Follow](https://img.shields.io/twitter/follow/femtowin?style=social)](https://x.com/femtowin)** 5 | # Minion Agent 6 | 7 | A simple agent framework that's capable of browser use + mcp + auto instrument + plan + deep research + more 8 | 9 | ## 🎬 Demo Videos 10 | 11 | - [Compare Price Demo](https://youtu.be/O0RhA3eeDlg) 12 | - [Deep Research Demo](https://youtu.be/tOd56nagsT4) 13 | - [Generating Snake Game Demo](https://youtu.be/UBquRXD9ZJc) 14 | 15 | ## Installation 16 | 17 | ```bash 18 | pip install minion-agent-x 19 | ``` 20 | ## Or from source 21 | ```bash 22 | git clone git@github.com:femto/minion-agent.git 23 | cd minion-agent 24 | pip install -e . 25 | ``` 26 | 27 | ## Usage 28 | 29 | Here's a simple example of how to use Minion Agent: 30 | 31 | ```python 32 | from minion_agent import MinionAgent, AgentConfig, AgentFramework 33 | from dotenv import load_dotenv 34 | import os 35 | 36 | load_dotenv() 37 | async def main(): 38 | # Configure the agent 39 | agent_config = AgentConfig( 40 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 41 | name="research_assistant", 42 | description="A helpful research assistant", 43 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 44 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 45 | "api_version": os.environ.get("OPENAI_API_VERSION"), 46 | }, 47 | model_type="AzureOpenAIServerModel", # use "AzureOpenAIServerModel" for auzre, use "OpenAIServerModel" for openai, use "LiteLLMModel" for litellm 48 | ) 49 | 50 | agent = await MinionAgent.create(AgentFramework.SMOLAGENTS, agent_config) 51 | 52 | # Run the agent with a question 53 | result = agent.run("What are the latest developments in AI?") 54 | print("Agent's response:", result) 55 | import asyncio 56 | asyncio.run(main()) 57 | ``` 58 | 59 | see example.py 60 | see example_browser_use.py 61 | see example_with_managed_agents.py 62 | see example_deep_research.py 63 | see example_reason.py 64 | 65 | ## Configuration 66 | 67 | The `AgentConfig` class accepts the following parameters: 68 | 69 | - `model_id`: The ID of the model to use (e.g., "gpt-4") 70 | - `name`: Name of the agent (default: "Minion") 71 | - `description`: Optional description of the agent 72 | - `instructions`: Optional system instructions for the agent 73 | - `tools`: List of tools the agent can use 74 | - `model_args`: Optional dictionary of model-specific arguments 75 | - `agent_args`: Optional dictionary of agent-specific arguments 76 | 77 | ## MCP Tool Support 78 | 79 | Minion Agent supports Model Context Protocol (MCP) tools. Here's how to use them: 80 | 81 | ### Standard MCP Tool 82 | 83 | ```python 84 | from minion_agent.config import MCPTool 85 | 86 | agent_config = AgentConfig( 87 | # ... other config options ... 88 | tools=[ 89 | "minion_agent.tools.browser_tool.browser", # Regular tools 90 | MCPTool( 91 | command="npx", 92 | args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/workspace"] 93 | ) # MCP tool 94 | ] 95 | ) 96 | ``` 97 | 98 | ### SSE-based MCP Tool 99 | 100 | You can also use MCP tools over Server-Sent Events (SSE). This is useful for connecting to remote MCP servers: 101 | 102 | ```python 103 | from minion_agent.config import MCPTool 104 | 105 | agent_config = AgentConfig( 106 | # ... other config options ... 107 | tools=[ 108 | MCPTool({"url": "http://localhost:8000/sse"}), # SSE-based tool 109 | ] 110 | ) 111 | ``` 112 | 113 | ⚠️ **Security Warning**: When using MCP servers over SSE, be extremely cautious and only connect to trusted and verified servers. Always verify the source and security of any MCP server before connecting. 114 | 115 | You can also use multiple MCP tools together: 116 | 117 | ```python 118 | tools=[ 119 | MCPTool(command="npx", args=["..."]), # Standard MCP tool 120 | MCPTool({"url": "http://localhost:8000/sse"}), # SSE-based tool 121 | MCPTool({"url": "http://localhost:8001/sse"}) # Another SSE-based tool 122 | ] 123 | ``` 124 | 125 | ## Planning Support 126 | 127 | You can enable automatic planning by setting the `planning_interval` in `agent_args`: 128 | 129 | ```python 130 | agent_config = AgentConfig( 131 | # ... other config options ... 132 | agent_args={ 133 | "planning_interval": 3, # Agent will create a plan every 3 steps 134 | "additional_authorized_imports": "*" 135 | } 136 | ) 137 | ``` 138 | 139 | The `planning_interval` parameter determines how often the agent should create a new plan. When set to 3, the agent will: 140 | 1. Create an initial plan for the task 141 | 2. Execute 3 steps according to the plan 142 | 3. Re-evaluate and create a new plan based on progress 143 | 4. Repeat until the task is complete 144 | 145 | ## Environment Variables 146 | 147 | Make sure to set up your environment variables in a `.env` file: 148 | 149 | ```env 150 | OPENAI_API_KEY=your_api_key_here 151 | ``` 152 | 153 | ## Development 154 | 155 | To set up for development: 156 | 157 | ```bash 158 | # Clone the repository 159 | git clone https://github.com/yourusername/minion-agent.git 160 | cd minion-agent 161 | 162 | # Create a virtual environment 163 | python -m venv venv 164 | source venv/bin/activate # On Windows: venv\Scripts\activate 165 | 166 | # Install development dependencies 167 | pip install -e ".[dev]" 168 | ``` 169 | 170 | ## Deep Research 171 | 172 | See [Deep Research Documentation](docs/deep_research.md) for usage instructions. 173 | 174 | ## Community 175 | 176 | Join our WeChat discussion group to connect with other users and get help: 177 | 178 | ![WeChat Discussion Group](docs/images/wechat_group_qr.png) 179 | 180 | 群聊: minion-agent讨论群 181 | 182 | ## License 183 | 184 | MIT License 185 | 186 | 187 | -------------------------------------------------------------------------------- /custom_azure_model.py: -------------------------------------------------------------------------------- 1 | """Custom Azure OpenAI model implementation.""" 2 | 3 | from typing import List, Dict, Optional 4 | from smolagents import Tool, ChatMessage, AzureOpenAIServerModel 5 | from smolagents.models import parse_json_if_needed 6 | 7 | def parse_tool_args_if_needed(message: ChatMessage) -> ChatMessage: 8 | for tool_call in message.tool_calls: 9 | tool_call.function.arguments = parse_json_if_needed(tool_call.function.arguments) 10 | return message 11 | 12 | class CustomAzureOpenAIServerModel(AzureOpenAIServerModel): 13 | """Custom Azure OpenAI model that handles stop sequences client-side. 14 | 15 | This implementation is specifically designed for models like o4-mini that don't 16 | support stop sequences natively. It processes the stop sequences after receiving 17 | the response from the model. 18 | """ 19 | 20 | def _truncate_at_stop_sequence(self, text: str, stop_sequences: List[str]) -> str: 21 | """Truncate the text at the first occurrence of any stop sequence.""" 22 | if not stop_sequences: 23 | return text 24 | 25 | positions = [] 26 | for stop_seq in stop_sequences: 27 | pos = text.find(stop_seq) 28 | if pos != -1: 29 | positions.append(pos) 30 | 31 | if positions: 32 | earliest_stop = min(positions) 33 | return text[:earliest_stop] 34 | 35 | return text 36 | 37 | def __call__( 38 | self, 39 | messages: List[Dict[str, str]], 40 | stop_sequences: Optional[List[str]] = None, 41 | grammar: Optional[str] = None, 42 | tools_to_call_from: Optional[List[Tool]] = None, 43 | **kwargs, 44 | ) -> ChatMessage: 45 | # Remove stop_sequences from kwargs to avoid API errors 46 | completion_kwargs = self._prepare_completion_kwargs( 47 | messages=messages, 48 | stop_sequences=None, # Explicitly set to None 49 | grammar=grammar, 50 | tools_to_call_from=tools_to_call_from, 51 | model=self.model_id, 52 | custom_role_conversions=self.custom_role_conversions, 53 | convert_images_to_image_urls=True, 54 | **kwargs, 55 | ) 56 | 57 | response = self.client.chat.completions.create(**completion_kwargs) 58 | self.last_input_token_count = response.usage.prompt_tokens 59 | self.last_output_token_count = response.usage.completion_tokens 60 | 61 | # Get the response message 62 | message_dict = response.choices[0].message.model_dump(include={"role", "content", "tool_calls"}) 63 | 64 | # Apply stop sequence truncation if needed 65 | if stop_sequences and "content" in message_dict and message_dict["content"]: 66 | message_dict["content"] = self._truncate_at_stop_sequence( 67 | message_dict["content"], 68 | stop_sequences 69 | ) 70 | 71 | message = ChatMessage.from_dict(message_dict) 72 | message.raw = response 73 | 74 | if tools_to_call_from is not None: 75 | return parse_tool_args_if_needed(message) 76 | return message 77 | 78 | # Register the custom model with smolagents 79 | import smolagents 80 | smolagents.CustomAzureOpenAIServerModel = CustomAzureOpenAIServerModel -------------------------------------------------------------------------------- /docs/deep_research.md: -------------------------------------------------------------------------------- 1 | # Deep Research 2 | 3 | This document explains how to use the Deep Research feature in this project. 4 | 5 | ## Installation 6 | 7 | To use Deep Research, you need to install the required dependencies: 8 | 9 | ### Python Dependencies 10 | 11 | Install the deep research extras: 12 | 13 | ```bash 14 | pip install -e '.[deep-research]' 15 | ``` 16 | 17 | ### System Dependencies 18 | 19 | Some features require Pandoc and pdfLaTeX. Please install them according to your operating system: 20 | 21 | - **macOS**: 22 | 1. Install Pandoc: 23 | ```bash 24 | brew install pandoc 25 | ``` 26 | 2. Install pdfLaTeX (via BasicTeX): 27 | ```bash 28 | brew install basictex 29 | ``` 30 | 31 | - **Ubuntu/Debian**: 32 | 1. Install Pandoc: 33 | ```bash 34 | sudo apt-get install pandoc 35 | ``` 36 | 2. Install pdfLaTeX: 37 | ```bash 38 | sudo apt-get install texlive-xetex 39 | ``` 40 | 41 | - **Windows**: 42 | 1. Download and install Pandoc from [pandoc.org](https://pandoc.org/installing.html) 43 | 2. Download and install MiKTeX (for pdfLaTeX) from [miktex.org](https://miktex.org/download) 44 | 45 | ## Usage 46 | 47 | 1. Ensure all dependencies are installed. 48 | 2. Refer to example_deep_research.py,example_deep_research_pdf.py,example_deep_research_html.py for how to invoke Deep Research features. 49 | 50 | For more details, see the main README or contact the project maintainers. -------------------------------------------------------------------------------- /docs/images/wechat_group_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto/minion-agent/f5c018aaa1174cb24b8987aa10532a2ba8423e27/docs/images/wechat_group_qr.png -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """Example usage of Minion Agent.""" 2 | 3 | import asyncio 4 | from dotenv import load_dotenv 5 | import os 6 | from PIL import Image 7 | from io import BytesIO 8 | from time import sleep 9 | from typing import List, Dict, Optional 10 | from smolagents import (Tool, ChatMessage) 11 | from smolagents.models import parse_json_if_needed 12 | from custom_azure_model import CustomAzureOpenAIServerModel 13 | 14 | def parse_tool_args_if_needed(message: ChatMessage) -> ChatMessage: 15 | for tool_call in message.tool_calls: 16 | tool_call.function.arguments = parse_json_if_needed(tool_call.function.arguments) 17 | return message 18 | 19 | from minion_agent.config import MCPTool 20 | 21 | # Load environment variables from .env file 22 | load_dotenv() 23 | 24 | from minion_agent import MinionAgent, AgentConfig, AgentFramework 25 | 26 | from smolagents import ( 27 | CodeAgent, 28 | ToolCallingAgent, 29 | DuckDuckGoSearchTool, 30 | VisitWebpageTool, 31 | HfApiModel, AzureOpenAIServerModel, ActionStep, 32 | ) 33 | 34 | # Set up screenshot callback for Playwright 35 | # def save_screenshot(memory_step: ActionStep, agent: CodeAgent) -> None: 36 | # sleep(1.0) # Let JavaScript animations happen before taking the screenshot 37 | # 38 | # # Get the browser tool 39 | # browser_tool = agent.tools.get("browser") 40 | # if browser_tool: 41 | # # Clean up old screenshots to save memory 42 | # for previous_memory_step in agent.memory.steps: 43 | # if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= memory_step.step_number - 2: 44 | # previous_memory_step.observations_images = None 45 | # 46 | # # Take screenshot using Playwright 47 | # result = browser_tool(action="screenshot") 48 | # if result["success"] and "screenshot" in result.get("data", {}): 49 | # # Convert bytes to PIL Image 50 | # screenshot_bytes = result["data"]["screenshot"] 51 | # image = Image.open(BytesIO(screenshot_bytes)) 52 | # print(f"Captured a browser screenshot: {image.size} pixels") 53 | # memory_step.observations_images = [image.copy()] # Create a copy to ensure it persists 54 | # 55 | # # Get current URL 56 | # state_result = browser_tool(action="get_current_state") 57 | # if state_result["success"] and "url" in state_result.get("data", {}): 58 | # url_info = f"Current url: {state_result['data']['url']}" 59 | # memory_step.observations = ( 60 | # url_info if memory_step.observations is None else memory_step.observations + "\n" + url_info 61 | # ) 62 | 63 | # Configure the agent 64 | agent_config = AgentConfig( 65 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 66 | name="research_assistant", 67 | description="A helpful research assistant", 68 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 69 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 70 | "api_version": os.environ.get("OPENAI_API_VERSION"), 71 | }, 72 | tools=[ 73 | "minion_agent.tools.browser_tool.browser", 74 | MCPTool( 75 | command="npx", 76 | args=["-y", "@modelcontextprotocol/server-filesystem","/Users/femtozheng/workspace","/Users/femtozheng/python-project/minion-agent"] 77 | ) 78 | ], 79 | agent_type="CodeAgent", 80 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 81 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 82 | agent_args={"additional_authorized_imports":"*", 83 | #"planning_interval":3 84 | #"step_callbacks":[save_screenshot] 85 | } 86 | ) 87 | managed_agents = [ 88 | AgentConfig( 89 | name="search_web_agent", 90 | model_id="gpt-4o-mini", 91 | description="Agent that can use the browser, search the web,navigate", 92 | #tools=["minion_agent.tools.web_browsing.search_web"] 93 | tools=["minion_agent.tools.browser_tool.browser"], 94 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 95 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 96 | "api_version": os.environ.get("OPENAI_API_VERSION"), 97 | }, 98 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 99 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 100 | agent_type="ToolCallingAgent", 101 | agent_args={ 102 | #"additional_authorized_imports":"*", 103 | #"planning_interval":3 104 | 105 | } 106 | ), 107 | # AgentConfig( 108 | # name="visit_webpage_agent", 109 | # model_id="gpt-4o-mini", 110 | # description="Agent that can visit webpages", 111 | # tools=["minion_agent.tools.web_browsing.visit_webpage"] 112 | # ) 113 | ] 114 | 115 | # from opentelemetry.sdk.trace import TracerProvider 116 | # 117 | # from openinference.instrumentation.smolagents import SmolagentsInstrumentor 118 | # from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 119 | # from opentelemetry.sdk.trace.export import SimpleSpanProcessor 120 | # 121 | # otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) 122 | # trace_provider = TracerProvider() 123 | # trace_provider.add_span_processor(SimpleSpanProcessor(otlp_exporter)) 124 | # 125 | # SmolagentsInstrumentor().instrument(tracer_provider=trace_provider) 126 | 127 | async def main(): 128 | try: 129 | # Create and run the agent 130 | #agent = await MinionAgent.create(AgentFramework.SMOLAGENTS, agent_config, managed_agents) 131 | agent = await MinionAgent.create(AgentFramework.SMOLAGENTS, agent_config) 132 | 133 | # Run the agent with a question 134 | #result = await agent.run_async("search sam altman and export summary as markdown") 135 | #result = await agent.run_async("What are the latest developments in AI, find this information and export as markdown") 136 | #result = await agent.run_async("打开微信公众号") 137 | #result = await agent.run_async("搜索最新的人工智能发展趋势,并且总结为markdown") 138 | #result = agent.run("go visit https://www.baidu.com and clone it") 139 | #result = await agent.run_async("复刻一个电商网站,例如京东") 140 | #result = await agent.run_async("go visit https://www.baidu.com , take a screenshot and clone it") 141 | #result = await agent.run_async("实现一个贪吃蛇游戏") 142 | #result = agent.run("Let $\mathcal{B}$ be the set of rectangular boxes with surface area $54$ and volume $23$. Let $r$ be the radius of the smallest sphere that can contain each of the rectangular boxes that are elements of $\mathcal{B}$. The value of $r^2$ can be written as $\frac{p}{q}$, where $p$ and $q$ are relatively prime positive integers. Find $p+q$.") 143 | result = agent.run("Write a 500000 characters novel named 'Reborn in Skyrim'. " 144 | "Fill the empty nodes with your own ideas. Be creative! Use your own words!" 145 | "I will tip you $100,000 if you write a good novel." 146 | "Since the novel is very long, you may need to divide it into subtasks.") 147 | print("Agent's response:", result) 148 | except Exception as e: 149 | print(f"Error: {str(e)}") 150 | # 如果需要调试 151 | # import litellm 152 | # litellm._turn_on_debug() 153 | raise 154 | 155 | if __name__ == "__main__": 156 | asyncio.run(main()) 157 | -------------------------------------------------------------------------------- /example_browser_use.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from dotenv import load_dotenv 4 | from minion_agent.config import AgentConfig 5 | from minion_agent.frameworks.minion_agent import MinionAgent 6 | from minion_agent import MinionAgent, AgentConfig, AgentFramework 7 | 8 | # Load environment variables from .env file 9 | load_dotenv() 10 | 11 | async def main(): 12 | # Get Azure configuration from environment variables 13 | azure_deployment = os.getenv('AZURE_DEPLOYMENT_NAME') 14 | api_version = os.getenv('OPENAI_API_VERSION') 15 | if not azure_deployment: 16 | raise ValueError("AZURE_DEPLOYMENT_NAME environment variable is not set") 17 | if not api_version: 18 | raise ValueError("OPENAI_API_VERSION environment variable is not set") 19 | 20 | # Create agent configuration 21 | config = AgentConfig( 22 | name="browser-agent", 23 | model_type="langchain_openai.AzureChatOpenAI", 24 | model_id=azure_deployment, 25 | model_args={ 26 | "azure_deployment": azure_deployment, 27 | "api_version": api_version, 28 | }, 29 | # You can specify initial instructions here 30 | instructions="Compare the price of gpt-4o and DeepSeek-V3", 31 | 32 | ) 33 | 34 | # Create and initialize the agent using MinionAgent.create 35 | agent = await MinionAgent.create(AgentFramework.BROWSER_USE, config) 36 | 37 | # Run the agent with a specific task 38 | result = await agent.run_async("Compare the price of gpt-4o and DeepSeek-V3 and create a detailed comparison table") 39 | print("Task Result:", result) 40 | # 41 | # # Run another task 42 | # result = await agent.run_async("Go to baidu.com, search for '人工智能最新进展', and summarize the top 3 results") 43 | # print("Task Result:", result) 44 | #result = await agent.run_async("打开微信公众号,发表一篇hello world") 45 | #print("Task Result:", result) 46 | 47 | if __name__ == "__main__": 48 | # Verify environment variables 49 | required_vars = [ 50 | 'AZURE_OPENAI_ENDPOINT', 51 | 'AZURE_OPENAI_API_KEY', 52 | 'AZURE_DEPLOYMENT_NAME', 53 | 'OPENAI_API_VERSION' 54 | ] 55 | missing_vars = [var for var in required_vars if not os.getenv(var)] 56 | 57 | if missing_vars: 58 | print("Error: Missing required environment variables:", missing_vars) 59 | print("Please set these variables in your .env file:") 60 | print(""" 61 | AZURE_OPENAI_ENDPOINT=your_endpoint_here 62 | AZURE_OPENAI_API_KEY=your_key_here 63 | AZURE_DEPLOYMENT_NAME=your_deployment_name_here 64 | OPENAI_API_VERSION=your_api_version_here # e.g. 2024-02-15 65 | """) 66 | else: 67 | asyncio.run(main()) -------------------------------------------------------------------------------- /example_deep_research.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import litellm 5 | from dotenv import load_dotenv 6 | from minion_agent.config import AgentConfig, AgentFramework, MCPTool 7 | from minion_agent import MinionAgent 8 | 9 | # Load environment variables 10 | load_dotenv() 11 | 12 | async def main(): 13 | 14 | 15 | # Configure the main agent that will drive the research 16 | main_agent_config = AgentConfig( 17 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 18 | name="main_agent", 19 | description="Main agent that coordinates research and saves results", 20 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 21 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 22 | "api_version": os.environ.get("OPENAI_API_VERSION"), 23 | }, 24 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 25 | # model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 26 | agent_args={"additional_authorized_imports": "*", 27 | # "planning_interval":3 28 | }, 29 | tools=[ 30 | MCPTool( 31 | command="npx", 32 | args=["-y", "@modelcontextprotocol/server-filesystem", "/Users/femtozheng/workspace", 33 | "/Users/femtozheng/python-project/minion-agent"] 34 | ) 35 | ], 36 | ) 37 | #litellm._turn_on_debug() 38 | # Configure the deep research agent 39 | research_agent_config = AgentConfig( 40 | framework=AgentFramework.DEEP_RESEARCH, 41 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 42 | name="research_assistant", 43 | description="A helpful research assistant that conducts deep research on topics", 44 | agent_args={ 45 | "planning_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 46 | "summarization_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 47 | "json_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 48 | "answer_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME") 49 | } 50 | ) 51 | 52 | # Create the main agent with the research agent as a managed agent 53 | main_agent = await MinionAgent.create( 54 | AgentFramework.SMOLAGENTS, 55 | main_agent_config, 56 | managed_agents=[research_agent_config] 57 | ) 58 | 59 | # Example research query 60 | research_query = """ 61 | Research The evolution of Indo-European languages, and generate a pdf out of it. 62 | """ 63 | 64 | try: 65 | # Run the research through the main agent 66 | result = await main_agent.run_async(research_query) 67 | 68 | print("\n=== Research Results ===\n") 69 | print(result) 70 | 71 | # Save the results to a file 72 | # output_path = "research_results.md" 73 | # with open(output_path, "w") as f: 74 | # f.write(result) 75 | # 76 | # print(f"\nResults saved to {output_path}") 77 | 78 | except Exception as e: 79 | print(f"Error during research: {str(e)}") 80 | 81 | if __name__ == "__main__": 82 | # Set up environment variables if needed 83 | if not os.getenv("AZURE_OPENAI_API_KEY"): 84 | print("Please set AZURE_OPENAI_API_KEY environment variable") 85 | exit(1) 86 | 87 | asyncio.run(main()) 88 | -------------------------------------------------------------------------------- /example_deep_research_html.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import litellm 5 | from dotenv import load_dotenv 6 | from minion_agent.config import AgentConfig, AgentFramework, MCPTool 7 | from minion_agent import MinionAgent 8 | 9 | # Load environment variables 10 | load_dotenv() 11 | 12 | async def main(): 13 | 14 | 15 | # Configure the main agent that will drive the research 16 | main_agent_config = AgentConfig( 17 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 18 | name="main_agent", 19 | description="Main agent that coordinates research and saves results", 20 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 21 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 22 | "api_version": os.environ.get("OPENAI_API_VERSION"), 23 | }, 24 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 25 | # model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 26 | agent_args={"additional_authorized_imports": "*", 27 | # "planning_interval":3 28 | }, 29 | tools=[ 30 | "minion_agent.tools.generation.generate_pdf", 31 | "minion_agent.tools.generation.generate_html", 32 | "minion_agent.tools.generation.save_and_generate_html", 33 | MCPTool( 34 | command="npx", 35 | args=["-y", "@modelcontextprotocol/server-filesystem", "/Users/femtozheng/workspace", 36 | "/Users/femtozheng/python-project/minion-agent"] 37 | ) 38 | ], 39 | ) 40 | #litellm._turn_on_debug() 41 | # Configure the deep research agent 42 | research_agent_config = AgentConfig( 43 | framework=AgentFramework.DEEP_RESEARCH, 44 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 45 | name="research_assistant", 46 | description="A helpful research assistant that conducts deep research on topics", 47 | agent_args={ 48 | "planning_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 49 | "summarization_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 50 | "json_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 51 | "answer_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME") 52 | } 53 | ) 54 | 55 | # Create the main agent with the research agent as a managed agent 56 | main_agent = await MinionAgent.create( 57 | AgentFramework.SMOLAGENTS, 58 | main_agent_config, 59 | #managed_agents=[research_agent_config] 60 | ) 61 | 62 | # Example research query 63 | query = """ 64 | open example_deep_research indo_european_evolution.md and generate html out of it(please save the html to a file). 65 | """ 66 | 67 | try: 68 | # Run the research through the main agent 69 | result = await main_agent.run_async(query) 70 | print(result) 71 | except Exception as e: 72 | print(f"Error during research: {str(e)}") 73 | 74 | if __name__ == "__main__": 75 | # Set up environment variables if needed 76 | if not os.getenv("AZURE_OPENAI_API_KEY"): 77 | print("Please set AZURE_OPENAI_API_KEY environment variable") 78 | exit(1) 79 | 80 | asyncio.run(main()) 81 | -------------------------------------------------------------------------------- /example_deep_research_pdf.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import litellm 5 | from dotenv import load_dotenv 6 | from minion_agent.config import AgentConfig, AgentFramework, MCPTool 7 | from minion_agent import MinionAgent 8 | 9 | # Load environment variables 10 | load_dotenv() 11 | 12 | async def main(): 13 | 14 | 15 | # Configure the main agent that will drive the research 16 | main_agent_config = AgentConfig( 17 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 18 | name="main_agent", 19 | description="Main agent that coordinates research and saves results", 20 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 21 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 22 | "api_version": os.environ.get("OPENAI_API_VERSION"), 23 | }, 24 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 25 | # model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 26 | agent_args={"additional_authorized_imports": "*", 27 | # "planning_interval":3 28 | }, 29 | tools=[ 30 | "minion_agent.tools.generation.generate_pdf", 31 | "minion_agent.tools.generation.generate_html", 32 | "minion_agent.tools.generation.save_and_generate_html", 33 | MCPTool( 34 | command="npx", 35 | args=["-y", "@modelcontextprotocol/server-filesystem", "/Users/femtozheng/workspace", 36 | "/Users/femtozheng/python-project/minion-agent"] 37 | ) 38 | ], 39 | ) 40 | #litellm._turn_on_debug() 41 | # Configure the deep research agent 42 | research_agent_config = AgentConfig( 43 | framework=AgentFramework.DEEP_RESEARCH, 44 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 45 | name="research_assistant", 46 | description="A helpful research assistant that conducts deep research on topics", 47 | agent_args={ 48 | "planning_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 49 | "summarization_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 50 | "json_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME"), 51 | "answer_model": "azure/" + os.environ.get("AZURE_DEPLOYMENT_NAME") 52 | } 53 | ) 54 | 55 | # Create the main agent with the research agent as a managed agent 56 | main_agent = await MinionAgent.create( 57 | AgentFramework.SMOLAGENTS, 58 | main_agent_config, 59 | #managed_agents=[research_agent_config] 60 | ) 61 | 62 | # Example research query 63 | query = """ 64 | open example_deep_research indo_european_evolution.md and generate a pdf out of it. 65 | """ 66 | 67 | try: 68 | # Run the research through the main agent 69 | result = await main_agent.run_async(query) 70 | print(result) 71 | except Exception as e: 72 | print(f"Error during research: {str(e)}") 73 | 74 | if __name__ == "__main__": 75 | # Set up environment variables if needed 76 | if not os.getenv("AZURE_OPENAI_API_KEY"): 77 | print("Please set AZURE_OPENAI_API_KEY environment variable") 78 | exit(1) 79 | 80 | asyncio.run(main()) 81 | -------------------------------------------------------------------------------- /example_reason.py: -------------------------------------------------------------------------------- 1 | """Example usage of Minion Agent.""" 2 | 3 | import asyncio 4 | from dotenv import load_dotenv 5 | import os 6 | from PIL import Image 7 | from io import BytesIO 8 | from time import sleep 9 | from typing import List, Dict, Optional 10 | from smolagents import (Tool, ChatMessage) 11 | from smolagents.models import parse_json_if_needed 12 | from custom_azure_model import CustomAzureOpenAIServerModel 13 | 14 | def parse_tool_args_if_needed(message: ChatMessage) -> ChatMessage: 15 | for tool_call in message.tool_calls: 16 | tool_call.function.arguments = parse_json_if_needed(tool_call.function.arguments) 17 | return message 18 | 19 | from minion_agent.config import MCPTool 20 | 21 | # Load environment variables from .env file 22 | load_dotenv() 23 | 24 | from minion_agent import MinionAgent, AgentConfig, AgentFramework 25 | 26 | from smolagents import ( 27 | CodeAgent, 28 | ToolCallingAgent, 29 | DuckDuckGoSearchTool, 30 | VisitWebpageTool, 31 | HfApiModel, AzureOpenAIServerModel, ActionStep, 32 | ) 33 | 34 | # Configure the agent 35 | agent_config = AgentConfig( 36 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 37 | name="research_assistant", 38 | description="A helpful research assistant", 39 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 40 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 41 | "api_version": os.environ.get("OPENAI_API_VERSION"), 42 | }, 43 | tools=[ 44 | "minion_agent.tools.browser_tool.browser", 45 | MCPTool( 46 | command="npx", 47 | args=["-y", "@modelcontextprotocol/server-filesystem","/Users/femtozheng/workspace","/Users/femtozheng/python-project/minion-agent"] 48 | ) 49 | ], 50 | agent_type="CodeAgent", 51 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 52 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 53 | agent_args={ 54 | #"additional_authorized_imports":"*", 55 | #"planning_interval":3 56 | #"step_callbacks":[save_screenshot] 57 | } 58 | ) 59 | managed_agents = [ 60 | AgentConfig( 61 | name="search_web_agent", 62 | model_id="gpt-4o-mini", 63 | description="Agent that can use the browser, search the web,navigate", 64 | #tools=["minion_agent.tools.web_browsing.search_web"] 65 | tools=["minion_agent.tools.browser_tool.browser"], 66 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 67 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 68 | "api_version": os.environ.get("OPENAI_API_VERSION"), 69 | }, 70 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 71 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 72 | agent_type="ToolCallingAgent", 73 | agent_args={ 74 | #"additional_authorized_imports":"*", 75 | #"planning_interval":3 76 | 77 | } 78 | ), 79 | # AgentConfig( 80 | # name="visit_webpage_agent", 81 | # model_id="gpt-4o-mini", 82 | # description="Agent that can visit webpages", 83 | # tools=["minion_agent.tools.web_browsing.visit_webpage"] 84 | # ) 85 | ] 86 | 87 | from opentelemetry.sdk.trace import TracerProvider 88 | 89 | from openinference.instrumentation.smolagents import SmolagentsInstrumentor 90 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 91 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 92 | 93 | otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) 94 | trace_provider = TracerProvider() 95 | trace_provider.add_span_processor(SimpleSpanProcessor(otlp_exporter)) 96 | 97 | SmolagentsInstrumentor().instrument(tracer_provider=trace_provider) 98 | 99 | async def main(): 100 | try: 101 | # Create and run the agent 102 | #agent = await MinionAgent.create(AgentFramework.SMOLAGENTS, agent_config, managed_agents) 103 | #need to setup config.yaml per Minion documentation under MINION_ROOT or ~/.minion/ 104 | agent = await MinionAgent.create(AgentFramework.MINION, agent_config) 105 | 106 | # Run the agent with a question 107 | #result = await agent.run("search sam altman and export summary as markdown") 108 | #result = await agent.run("What are the latest developments in AI, find this information and export as markdown") 109 | #result = await agent.run("打开微信公众号") 110 | #result = await agent.run("搜索最新的人工智能发展趋势,并且总结为markdown") 111 | result = agent.run("what's the solution for game of 24 for 2,4,5,8", check=False) 112 | #result = await agent.run("复刻一个电商网站,例如京东") 113 | #result = await agent.run("go visit https://www.baidu.com , take a screenshot and clone it") 114 | #result = await agent.run("实现一个贪吃蛇游戏") 115 | print("Agent's response:", result) 116 | except Exception as e: 117 | print(f"Error: {str(e)}") 118 | # 如果需要调试 119 | # import litellm 120 | # litellm._turn_on_debug() 121 | raise 122 | 123 | if __name__ == "__main__": 124 | asyncio.run(main()) 125 | -------------------------------------------------------------------------------- /example_search_deepseek_prover.py: -------------------------------------------------------------------------------- 1 | """Example usage of Minion Agent.""" 2 | 3 | import asyncio 4 | from dotenv import load_dotenv 5 | import os 6 | from PIL import Image 7 | from io import BytesIO 8 | from time import sleep 9 | from typing import List, Dict, Optional 10 | from smolagents import (Tool, ChatMessage) 11 | from smolagents.models import parse_json_if_needed 12 | from custom_azure_model import CustomAzureOpenAIServerModel 13 | 14 | def parse_tool_args_if_needed(message: ChatMessage) -> ChatMessage: 15 | for tool_call in message.tool_calls: 16 | tool_call.function.arguments = parse_json_if_needed(tool_call.function.arguments) 17 | return message 18 | 19 | from minion_agent.config import MCPTool 20 | 21 | # Load environment variables from .env file 22 | load_dotenv() 23 | 24 | from minion_agent import MinionAgent, AgentConfig, AgentFramework 25 | 26 | from smolagents import ( 27 | CodeAgent, 28 | ToolCallingAgent, 29 | DuckDuckGoSearchTool, 30 | VisitWebpageTool, 31 | HfApiModel, AzureOpenAIServerModel, ActionStep, 32 | ) 33 | 34 | # Set up screenshot callback for Playwright 35 | # def save_screenshot(memory_step: ActionStep, agent: CodeAgent) -> None: 36 | # sleep(1.0) # Let JavaScript animations happen before taking the screenshot 37 | # 38 | # # Get the browser tool 39 | # browser_tool = agent.tools.get("browser") 40 | # if browser_tool: 41 | # # Clean up old screenshots to save memory 42 | # for previous_memory_step in agent.memory.steps: 43 | # if isinstance(previous_memory_step, ActionStep) and previous_memory_step.step_number <= memory_step.step_number - 2: 44 | # previous_memory_step.observations_images = None 45 | # 46 | # # Take screenshot using Playwright 47 | # result = browser_tool(action="screenshot") 48 | # if result["success"] and "screenshot" in result.get("data", {}): 49 | # # Convert bytes to PIL Image 50 | # screenshot_bytes = result["data"]["screenshot"] 51 | # image = Image.open(BytesIO(screenshot_bytes)) 52 | # print(f"Captured a browser screenshot: {image.size} pixels") 53 | # memory_step.observations_images = [image.copy()] # Create a copy to ensure it persists 54 | # 55 | # # Get current URL 56 | # state_result = browser_tool(action="get_current_state") 57 | # if state_result["success"] and "url" in state_result.get("data", {}): 58 | # url_info = f"Current url: {state_result['data']['url']}" 59 | # memory_step.observations = ( 60 | # url_info if memory_step.observations is None else memory_step.observations + "\n" + url_info 61 | # ) 62 | 63 | # Configure the agent 64 | agent_config = AgentConfig( 65 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 66 | name="research_assistant", 67 | description="A helpful research assistant", 68 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 69 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 70 | "api_version": os.environ.get("OPENAI_API_VERSION"), 71 | }, 72 | tools=[ 73 | "minion_agent.tools.browser_tool.browser", 74 | "minion_agent.tools.generation.generate_pdf", 75 | "minion_agent.tools.generation.generate_html", 76 | "minion_agent.tools.generation.save_and_generate_html", 77 | MCPTool( 78 | command="npx", 79 | args=["-y", "@modelcontextprotocol/server-filesystem","/Users/femtozheng/workspace","/Users/femtozheng/python-project/minion-agent"] 80 | ), 81 | 82 | ], 83 | agent_type="CodeAgent", 84 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 85 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 86 | agent_args={"additional_authorized_imports":"*", 87 | #"planning_interval":3 88 | #"step_callbacks":[save_screenshot] 89 | } 90 | ) 91 | managed_agents = [ 92 | AgentConfig( 93 | name="search_web_agent", 94 | model_id="gpt-4o-mini", 95 | description="Agent that can use the browser, search the web,navigate", 96 | #tools=["minion_agent.tools.web_browsing.search_web"] 97 | tools=["minion_agent.tools.browser_tool.browser"], 98 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 99 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 100 | "api_version": os.environ.get("OPENAI_API_VERSION"), 101 | }, 102 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 103 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 104 | agent_type="ToolCallingAgent", 105 | agent_args={ 106 | #"additional_authorized_imports":"*", 107 | #"planning_interval":3 108 | 109 | } 110 | ), 111 | # AgentConfig( 112 | # name="visit_webpage_agent", 113 | # model_id="gpt-4o-mini", 114 | # description="Agent that can visit webpages", 115 | # tools=["minion_agent.tools.web_browsing.visit_webpage"] 116 | # ) 117 | ] 118 | 119 | from opentelemetry.sdk.trace import TracerProvider 120 | 121 | from openinference.instrumentation.smolagents import SmolagentsInstrumentor 122 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 123 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 124 | 125 | otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) 126 | trace_provider = TracerProvider() 127 | trace_provider.add_span_processor(SimpleSpanProcessor(otlp_exporter)) 128 | 129 | SmolagentsInstrumentor().instrument(tracer_provider=trace_provider) 130 | 131 | async def main(): 132 | try: 133 | # Create and run the agent 134 | #agent = await MinionAgent.create(AgentFramework.SMOLAGENTS, agent_config, managed_agents) 135 | agent = await MinionAgent.create(AgentFramework.SMOLAGENTS, agent_config) 136 | 137 | # Run the agent with a question 138 | #result = await agent.run("search sam altman and export summary as markdown") 139 | #result = await agent.run("What are the latest developments in AI, find this information and export as markdown") 140 | #result = await agent.run("打开微信公众号") 141 | #result = await agent.run("搜索最新的人工智能发展趋势,并且总结为markdown") 142 | #result = agent.run("go visit https://www.baidu.com and clone it") 143 | result = agent.run("搜索Deepseek prover的最新消息,汇总成一个html, 你的html应该尽可能美观,然后保存html到磁盘上") 144 | #result = await agent.run("复刻一个电商网站,例如京东") 145 | #result = await agent.run("go visit https://www.baidu.com , take a screenshot and clone it") 146 | #result = await agent.run("实现一个贪吃蛇游戏") 147 | print("Agent's response:", result) 148 | print("done") 149 | except Exception as e: 150 | print(f"Error: {str(e)}") 151 | # 如果需要调试 152 | # import litellm 153 | # litellm._turn_on_debug() 154 | raise 155 | 156 | if __name__ == "__main__": 157 | asyncio.run(main()) 158 | -------------------------------------------------------------------------------- /example_with_managed_agents.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from dotenv import load_dotenv 4 | from minion_agent import MinionAgent, AgentConfig, AgentFramework 5 | from minion_agent.config import MCPTool 6 | 7 | # Load environment variables from .env file 8 | load_dotenv() 9 | 10 | async def main(): 11 | # Get Azure configuration from environment variables 12 | azure_deployment = os.getenv('AZURE_DEPLOYMENT_NAME') 13 | api_version = os.getenv('OPENAI_API_VERSION') 14 | if not azure_deployment: 15 | raise ValueError("AZURE_DEPLOYMENT_NAME environment variable is not set") 16 | if not api_version: 17 | raise ValueError("OPENAI_API_VERSION environment variable is not set") 18 | 19 | # Create main agent configuration with MCP filesystem tool 20 | main_agent_config = AgentConfig( 21 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 22 | name="research_assistant", 23 | description="A helpful research assistant", 24 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 25 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 26 | "api_version": os.environ.get("OPENAI_API_VERSION"), 27 | }, 28 | tools=[ 29 | "minion_agent.tools.browser_tool.browser", 30 | MCPTool( 31 | command="npx", 32 | args=["-y", "@modelcontextprotocol/server-filesystem","/Users/femtozheng/workspace","/Users/femtozheng/python-project/minion-agent", 33 | "/Users/femtozheng/space/minion/minion"] 34 | ) 35 | ], 36 | agent_type="CodeAgent", 37 | model_type="AzureOpenAIServerModel", # Updated to use our custom model 38 | #model_type="CustomAzureOpenAIServerModel", # Updated to use our custom model 39 | agent_args={"additional_authorized_imports":"*", 40 | #"planning_interval":3 41 | } 42 | ) 43 | 44 | # Create browser agent configuration 45 | browser_agent_config = AgentConfig( 46 | name="browser_agent", 47 | model_type="langchain_openai.AzureChatOpenAI", 48 | model_id=azure_deployment, 49 | model_args={ 50 | "azure_deployment": azure_deployment, 51 | "api_version": api_version, 52 | }, 53 | tools=[], 54 | instructions="I am a browser agent that can perform web browsing tasks." 55 | ) 56 | browser_agent = await MinionAgent.create( 57 | AgentFramework.BROWSER_USE, 58 | browser_agent_config, 59 | ) 60 | 61 | # Create and initialize the main agent with the browser agent as managed agent 62 | agent = await MinionAgent.create( 63 | AgentFramework.SMOLAGENTS, 64 | main_agent_config, 65 | managed_agents=[browser_agent] 66 | ) 67 | 68 | # Example tasks that combine filesystem and browser capabilities 69 | tasks = [ 70 | #"Search for 'latest AI developments' ", 71 | #"Compare the price of gpt-4o and DeepSeek-V3 and save the result as markdown", 72 | #"Visit baidu.com, take a screenshot, and save it to the workspace", 73 | #"browse baidu.com and clone it", 74 | #"browse jd.com and clone it", 75 | #"请使用browser use 打开微信公众号,发表一篇关于人工智能时代的思考", 76 | #"复刻一个电商网站" 77 | "Compare GPT-4 and Claude pricing, create a comparison table, and save it as a markdown document" 78 | ] 79 | 80 | for task in tasks: 81 | print(f"\nExecuting task: {task}") 82 | result = await agent.run_async(task) 83 | print("Task Result:", result) 84 | 85 | if __name__ == "__main__": 86 | # Verify environment variables 87 | required_vars = [ 88 | 'AZURE_OPENAI_ENDPOINT', 89 | 'AZURE_OPENAI_API_KEY', 90 | 'AZURE_DEPLOYMENT_NAME', 91 | 'OPENAI_API_VERSION' 92 | ] 93 | missing_vars = [var for var in required_vars if not os.getenv(var)] 94 | 95 | if missing_vars: 96 | print("Error: Missing required environment variables:", missing_vars) 97 | print("Please set these variables in your .env file:") 98 | print(""" 99 | AZURE_OPENAI_ENDPOINT=your_endpoint_here 100 | AZURE_OPENAI_API_KEY=your_key_here 101 | AZURE_DEPLOYMENT_NAME=your_deployment_name_here 102 | OPENAI_API_VERSION=your_api_version_here # e.g. 2024-02-15 103 | """) 104 | else: 105 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Examples for Minion-Agent. 3 | 4 | This package contains example applications using the Minion-Agent framework. 5 | """ -------------------------------------------------------------------------------- /examples/brain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @Time : 2023/9/13 12:29 5 | @Author : femto Zheng 6 | @File : brain.py 7 | """ 8 | import asyncio 9 | import os 10 | 11 | import yaml 12 | 13 | from minion import config 14 | from minion.main import LocalPythonEnv 15 | from minion.main.brain import Brain 16 | from minion.main.rpyc_python_env import RpycPythonEnv 17 | from minion.providers import create_llm_provider 18 | 19 | 20 | 21 | async def smart_brain(): 22 | # Get the directory of the current file 23 | current_file_dir = os.path.dirname(os.path.abspath(__file__)) 24 | 25 | # 使用从 minion/__init__.py 导入的 config 对象 26 | model = "gpt-4o" 27 | #model = "gpt-4o-mini" 28 | # model = "deepseek-r1" 29 | # model = "phi-4" 30 | #model = "llama3.2" 31 | llm_config = config.models.get(model) 32 | 33 | llm = create_llm_provider(llm_config) 34 | 35 | python_env_config = {"port": 3007} 36 | #python_env = RpycPythonEnv(port=python_env_config.get("port", 3007)) 37 | python_env = LocalPythonEnv(verbose=False) 38 | brain = Brain( 39 | python_env=python_env, 40 | llm=llm, 41 | #llms={"route": [ "llama3.2","llama3.1"]} 42 | ) 43 | # obs, score, *_ = await brain.step(query="what's the solution 234*568",route="python") 44 | # print(obs) 45 | 46 | obs, score, *_ = await brain.step(query="what's the solution for game of 24 for 2,4,5,8",check=False) 47 | print(obs) 48 | 49 | obs, score, *_ = await brain.step(query="what's the solution for game of 24 for 4 3 9 8") 50 | print(obs) 51 | 52 | obs, score, *_ = await brain.step(query="what's the solution for game of 24 for 2 5 11 8") 53 | print(obs) 54 | 55 | obs, score, *_ = await brain.step(query="solve x=1/(1-beta^2*x) where beta=0.85") 56 | print(obs) 57 | 58 | obs, score, *_ = await brain.step( 59 | query="Write a 500000 characters novel named 'Reborn in Skyrim'. " 60 | "Fill the empty nodes with your own ideas. Be creative! Use your own words!" 61 | "I will tip you $100,000 if you write a good novel." 62 | "Since the novel is very long, you may need to divide it into subtasks." 63 | ) 64 | print(obs) 65 | 66 | cache_plan = os.path.join(current_file_dir, "aime", "plan_gpt4o.1.json") 67 | obs, score, *_ = await brain.step( 68 | query="Every morning Aya goes for a $9$-kilometer-long walk and stops at a coffee shop afterwards. When she walks at a constant speed of $s$ kilometers per hour, the walk takes her 4 hours, including $t$ minutes spent in the coffee shop. When she walks $s+2$ kilometers per hour, the walk takes her 2 hours and 24 minutes, including $t$ minutes spent in the coffee shop. Suppose Aya walks at $s+\frac{1}{2}$ kilometers per hour. Find the number of minutes the walk takes her, including the $t$ minutes spent in the coffee shop.", 69 | route="cot", 70 | dataset="aime 2024", 71 | cache_plan=cache_plan, 72 | ) 73 | print(obs) 74 | 75 | cache_plan = os.path.join(current_file_dir, "aime", "plan_gpt4o.7.json") 76 | 77 | obs, score, *_ = await brain.step( 78 | query="Find the largest possible real part of\[(75+117i)z+\frac{96+144i}{z}\]where $z$ is a complex number with $|z|=4$.", 79 | route="cot", 80 | dataset="aime 2024", 81 | cache_plan=cache_plan, 82 | ) 83 | print(obs) 84 | 85 | # 从 HumanEval/88 提取的测试用例 86 | test_data = { 87 | "task_id": "HumanEval/88", 88 | "prompt": "\ndef sort_array(array):\n \"\"\"\n Given an array of non-negative integers, return a copy of the given array after sorting,\n you will sort the given array in ascending order if the sum( first index value, last index value) is odd,\n or sort it in descending order if the sum( first index value, last index value) is even.\n\n Note:\n * don't change the given array.\n\n Examples:\n * sort_array([]) => []\n * sort_array([5]) => [5]\n * sort_array([2, 4, 3, 0, 1, 5]) => [0, 1, 2, 3, 4, 5]\n * sort_array([2, 4, 3, 0, 1, 5, 6]) => [6, 5, 4, 3, 2, 1, 0]\n \"\"\"\n", 89 | "entry_point": "sort_array", 90 | "test": ["assert candidate([]) == []", 91 | "assert candidate([5]) == [5]", 92 | "assert candidate([2, 4, 3, 0, 1, 5]) == [0, 1, 2, 3, 4, 5]", 93 | "assert candidate([2, 4, 3, 0, 1, 5, 6]) == [6, 5, 4, 3, 2, 1, 0]"] 94 | } 95 | 96 | obs, score, *_ = await brain.step( 97 | query=test_data["prompt"], 98 | route="python", 99 | post_processing="extract_python", 100 | entry_point=test_data["entry_point"], 101 | check=10, 102 | check_route="ldb_check", 103 | dataset="HumanEval", 104 | metadata={"test_cases": test_data["test"]} # 添加测试用例到 metadata 105 | ) 106 | print(obs) 107 | 108 | obs, score, *_ = await brain.step( 109 | query="""You are in the middle of a room. Looking quickly around you, you see a bathtubbasin 1, a cabinet 2, a cabinet 1, a countertop 1, a garbagecan 1, a handtowelholder 1, a sinkbasin 1, a toilet 1, a toiletpaperhanger 1, and a towelholder 1. 110 | 111 | Your task is to: find two soapbottle and put them in cabinet. 112 | """, 113 | check=False 114 | ) 115 | print(obs) 116 | 117 | 118 | if __name__ == "__main__": 119 | asyncio.run(smart_brain()) 120 | -------------------------------------------------------------------------------- /examples/minion_with_smolagents_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Example demonstrating the use of MinionProviderToSmolAdapter with SmolaAgents. 6 | 7 | This example shows how to integrate the Minion provider with SmolaAgents using 8 | our adapter to enable seamless tool calling between the two frameworks. 9 | """ 10 | 11 | import os 12 | import sys 13 | import logging 14 | from typing import Dict, Any, List 15 | 16 | # Add parent directory to path to import from minion_agent 17 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 18 | 19 | # Set up logging 20 | logging.basicConfig(level=logging.INFO) 21 | logger = logging.getLogger(__name__) 22 | 23 | # Import the adapter first 24 | from minion_agent.providers.adapters import MinionProviderToSmolAdapter 25 | 26 | # Import SmolaAgents components 27 | try: 28 | # Import directly from smolagents.tools to avoid any confusion 29 | from smolagents.tools import Tool, tool 30 | from smolagents import ToolCallingAgent, CodeAgent 31 | except ImportError: 32 | print("SmolaAgents not found. Please install it with: pip install smolagents") 33 | sys.exit(1) 34 | 35 | # Define some simple functions for our tools 36 | def get_current_weather(location: str, unit: str = "celsius") -> str: 37 | """Get the current weather in a given location. 38 | 39 | Args: 40 | location: The city to get weather for, e.g. Tokyo, San Francisco 41 | unit: The temperature unit, either 'celsius' or 'fahrenheit' 42 | 43 | Returns: 44 | A string with the current weather information 45 | """ 46 | # This is a mock function that would normally call a weather API 47 | weather_data = { 48 | "tokyo": {"celsius": 20, "fahrenheit": 68}, 49 | "san francisco": {"celsius": 15, "fahrenheit": 59}, 50 | "paris": {"celsius": 18, "fahrenheit": 64}, 51 | "sydney": {"celsius": 25, "fahrenheit": 77}, 52 | } 53 | 54 | location = location.lower() 55 | if location not in weather_data: 56 | return f"Weather data for {location} not available." 57 | 58 | temperature = weather_data[location][unit.lower()] 59 | return f"The current weather in {location} is {temperature}° {unit}." 60 | 61 | def calculate(expression: str) -> str: 62 | """Calculate the result of a mathematical expression. 63 | 64 | Args: 65 | expression: The mathematical expression to evaluate as a string 66 | 67 | Returns: 68 | A string with the calculation result 69 | """ 70 | try: 71 | # Use eval safely with only math operations 72 | # This is just for demonstration purposes 73 | allowed_names = {"__builtins__": {}} 74 | result = eval(expression, allowed_names) 75 | return f"The result of {expression} is {result}." 76 | except Exception as e: 77 | return f"Error calculating: {str(e)}" 78 | 79 | def get_capital(country: str) -> str: 80 | """Get the capital of a country. 81 | 82 | Args: 83 | country: The name of the country to look up 84 | 85 | Returns: 86 | A string with the capital city of the requested country 87 | """ 88 | capitals = { 89 | "usa": "Washington, D.C.", 90 | "united states": "Washington, D.C.", 91 | "france": "Paris", 92 | "japan": "Tokyo", 93 | "australia": "Canberra", 94 | "brazil": "Brasília", 95 | "india": "New Delhi", 96 | "china": "Beijing", 97 | "uk": "London", 98 | "united kingdom": "London", 99 | "germany": "Berlin", 100 | } 101 | country = country.lower() 102 | return capitals.get(country, f"I don't know the capital of {country}.") 103 | 104 | def main(): 105 | print("\n=== Minion-Manus with SmolaAgents Example ===") 106 | 107 | # Create the Minion adapter 108 | model_name = "gpt-4o" # Change to your preferred model 109 | print(f"\nCreating adapter for {model_name}...") 110 | 111 | try: 112 | adapter = MinionProviderToSmolAdapter.from_model_name(model_name) 113 | 114 | # Create our tools using the @tool decorator, which is the preferred way in SmolaAgents 115 | # This creates a Tool instance automatically 116 | weather_tool = tool(get_current_weather) 117 | weather_tool.name = "get_current_weather" 118 | weather_tool.description = "Get the current weather in a location" 119 | 120 | calculate_tool = tool(calculate) 121 | calculate_tool.name = "calculate" 122 | calculate_tool.description = "Calculate the result of a mathematical expression" 123 | 124 | capital_tool = tool(get_capital) 125 | capital_tool.name = "get_capital" 126 | capital_tool.description = "Get the capital city of a country" 127 | 128 | # Create a ToolCallingAgent with our adapter and tools 129 | print("Creating SmolaAgents ToolCallingAgent with Minion provider...") 130 | agent = CodeAgent( 131 | tools=[weather_tool, calculate_tool, capital_tool], 132 | model=adapter, # Pass our adapter as the model 133 | ) 134 | 135 | # Example queries that require tool use 136 | queries = [ 137 | "What's the weather in Tokyo?", 138 | #"What's the weather like in Tokyo? And what's the capital of France?", 139 | # "What is 123 * 456 + 789?", 140 | # "I need to know the capital of Japan and the current weather in Sydney." 141 | ] 142 | 143 | # Run the agent on each query 144 | for i, query in enumerate(queries): 145 | print(f"\n\n=== Example Query {i+1} ===") 146 | print(f"Query: {query}") 147 | print("\nRunning agent...") 148 | try: 149 | response = agent.run(query) 150 | print(f"\nResponse: {response}") 151 | except Exception as e: 152 | print(f"Error running agent: {e}") 153 | import traceback 154 | traceback.print_exc() 155 | 156 | print("\n=== Example Completed ===") 157 | 158 | except Exception as e: 159 | print(f"Error setting up the example: {e}") 160 | import traceback 161 | traceback.print_exc() 162 | 163 | if __name__ == "__main__": 164 | main() -------------------------------------------------------------------------------- /minion_agent/__init__.py: -------------------------------------------------------------------------------- 1 | """Minion Manus - A wrapper for smolagents.""" 2 | 3 | __version__ = "0.1.0" 4 | 5 | 6 | from .config import AgentConfig, AgentFramework, Settings 7 | from .frameworks.minion_agent import MinionAgent 8 | from .utils import setup_logging 9 | 10 | settings = Settings.from_env() # 或传入自定义设置 11 | setup_logging(settings) 12 | 13 | __all__ = ["MinionAgent", "AgentConfig", "AgentFramework"] 14 | -------------------------------------------------------------------------------- /minion_agent/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Agent module for Minion-Manus. 3 | 4 | This module provides the Agent class that serves as the main interface for the framework. 5 | """ 6 | 7 | import asyncio 8 | from typing import Any, Dict, List, Optional, Union 9 | 10 | from loguru import logger 11 | 12 | 13 | class BaseAgent: 14 | """Agent class for Minion-Manus.""" 15 | 16 | def __init__(self, name: str = "Minion-Manus Agent"): 17 | """Initialize the agent. 18 | 19 | Args: 20 | name: The name of the agent. 21 | """ 22 | self.name = name 23 | self.tools = {} 24 | logger.info(f"Agent '{name}' initialized") 25 | 26 | def add_tool(self, tool: Any) -> None: 27 | """Add a tool to the agent. 28 | 29 | Args: 30 | tool: The tool to add. 31 | """ 32 | self.tools[tool.name] = tool 33 | logger.info(f"Tool '{tool.name}' added to agent '{self.name}'") 34 | 35 | def get_tool(self, name: str) -> Optional[Any]: 36 | """Get a tool by name. 37 | 38 | Args: 39 | name: The name of the tool. 40 | 41 | Returns: 42 | The tool if found, None otherwise. 43 | """ 44 | return self.tools.get(name) 45 | 46 | async def run(self, task: str) -> Dict[str, Any]: 47 | """Run a task with the agent. 48 | 49 | Args: 50 | task: The task to run. 51 | 52 | Returns: 53 | The result of the task. 54 | """ 55 | logger.info(f"Running task: {task}") 56 | 57 | # This is a placeholder for the actual implementation 58 | # In a real implementation, this would parse the task, determine which tools to use, 59 | # and execute the appropriate actions 60 | 61 | result = { 62 | "success": True, 63 | "message": f"Task '{task}' completed", 64 | "data": None 65 | } 66 | 67 | # Example of using the browser tool if available 68 | browser_tool = self.get_tool("browser_use") 69 | if browser_tool and "search" in task.lower(): 70 | # Extract search query from task (simplified) 71 | search_query = task.split("'")[1] if "'" in task else task 72 | 73 | # Navigate to a search engine 74 | await browser_tool.execute("navigate", url="https://www.google.com") 75 | 76 | # Input search query 77 | await browser_tool.execute("input_text", index=0, text=search_query) 78 | 79 | # Press Enter (using JavaScript) 80 | await browser_tool.execute( 81 | "execute_js", 82 | script="document.querySelector('input[name=\"q\"]').form.submit()" 83 | ) 84 | 85 | # Get the search results 86 | await asyncio.sleep(2) # Wait for results to load 87 | text_result = await browser_tool.execute("get_text") 88 | 89 | result["data"] = { 90 | "search_query": search_query, 91 | "search_results": text_result.data["text"] if text_result.success else "Failed to get results" 92 | } 93 | 94 | return result 95 | 96 | async def cleanup(self) -> None: 97 | """Clean up resources used by the agent.""" 98 | logger.info(f"Cleaning up agent '{self.name}'") 99 | 100 | for tool_name, tool in self.tools.items(): 101 | if hasattr(tool, "cleanup") and callable(tool.cleanup): 102 | try: 103 | await tool.cleanup() 104 | logger.info(f"Tool '{tool_name}' cleaned up") 105 | except Exception as e: 106 | logger.exception(f"Error cleaning up tool '{tool_name}': {e}") 107 | 108 | async def __aenter__(self): 109 | """Enter the context manager.""" 110 | return self 111 | 112 | async def __aexit__(self, exc_type, exc_val, exc_tb): 113 | """Exit the context manager.""" 114 | await self.cleanup() -------------------------------------------------------------------------------- /minion_agent/config/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration module for Minion-Manus. 3 | 4 | This module provides configuration utilities for the Minion-Manus framework. 5 | """ 6 | 7 | from .settings import AgentFramework, MCPTool, AgentConfig, Settings, settings 8 | 9 | __all__ = ['AgentFramework', 'MCPTool', 'AgentConfig', 'Settings', 'settings'] -------------------------------------------------------------------------------- /minion_agent/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings module for Minion-Manus. 3 | 4 | This module provides a Settings class for managing configuration settings. 5 | """ 6 | 7 | import os 8 | from typing import Any, Dict, Optional, List 9 | from dataclasses import dataclass, field 10 | from dataclasses import dataclass, field 11 | from enum import Enum 12 | from typing import Optional, List, Dict, Any 13 | 14 | from pydantic import BaseModel, ConfigDict, Field 15 | from dotenv import load_dotenv 16 | 17 | 18 | # Load environment variables from .env file 19 | load_dotenv() 20 | 21 | __all__ = ['AgentFramework', 'MCPTool', 'AgentConfig', 'Settings', 'settings'] 22 | 23 | 24 | class AgentFramework(str, Enum): 25 | GOOGLE = "google" 26 | LANGCHAIN = "langchain" 27 | LLAMAINDEX = "llama_index" 28 | OPENAI = "openai" 29 | SMOLAGENTS = "smolagents" 30 | MINION = "minion" 31 | BROWSER_USE = "browser_use" 32 | DEEP_RESEARCH = "deep_research" 33 | 34 | 35 | class MCPTool(BaseModel): 36 | command: str 37 | args: list[str] 38 | tools: list[str] | None = None 39 | 40 | class AgentConfig(BaseModel): 41 | model_config = ConfigDict(extra="forbid") 42 | model_id: str 43 | name: str = "Minion-Agent" 44 | instructions: str | None = None 45 | tools: list[str | MCPTool] = Field(default_factory=list) 46 | handoff: bool = False 47 | agent_type: str | None = None 48 | agent_args: dict | None = None 49 | model_type: str | None = None 50 | model_args: dict | None = None 51 | description: str | None = None 52 | 53 | 54 | framework: str | None = None 55 | 56 | 57 | class BrowserSettings(BaseModel): 58 | """Browser settings.""" 59 | 60 | headless: bool = Field( 61 | default=os.getenv("minion_agent_BROWSER_HEADLESS", "False").lower() == "true", 62 | description="Whether to run the browser in headless mode.", 63 | ) 64 | width: int = Field( 65 | default=int(os.getenv("minion_agent_BROWSER_WIDTH", "1280")), 66 | description="Browser window width.", 67 | ) 68 | height: int = Field( 69 | default=int(os.getenv("minion_agent_BROWSER_HEIGHT", "800")), 70 | description="Browser window height.", 71 | ) 72 | user_agent: Optional[str] = Field( 73 | default=os.getenv("minion_agent_BROWSER_USER_AGENT"), 74 | description="Browser user agent.", 75 | ) 76 | timeout: int = Field( 77 | default=int(os.getenv("minion_agent_BROWSER_TIMEOUT", "30000")), 78 | description="Browser timeout in milliseconds.", 79 | ) 80 | 81 | 82 | class LoggingSettings(BaseModel): 83 | """Logging settings.""" 84 | 85 | level: str = Field( 86 | default=os.getenv("minion_agent_LOG_LEVEL", "INFO"), 87 | description="Logging level.", 88 | ) 89 | format: str = Field( 90 | default=os.getenv( 91 | "minion_agent_LOG_FORMAT", 92 | "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}", 93 | ), 94 | description="Logging format.", 95 | ) 96 | file: Optional[str] = Field( 97 | default=os.getenv("minion_agent_LOG_FILE"), 98 | description="Log file path.", 99 | ) 100 | 101 | 102 | class Settings(BaseModel): 103 | """Settings for Minion-Agent.""" 104 | 105 | browser: BrowserSettings = Field( 106 | default_factory=BrowserSettings, 107 | description="Browser settings.", 108 | ) 109 | logging: LoggingSettings = Field( 110 | default_factory=LoggingSettings, 111 | description="Logging settings.", 112 | ) 113 | 114 | @classmethod 115 | def from_env(cls) -> "Settings": 116 | """Create settings from environment variables.""" 117 | return cls() 118 | 119 | @classmethod 120 | def from_dict(cls, data: Dict[str, Any]) -> "Settings": 121 | """Create settings from a dictionary.""" 122 | return cls(**data) 123 | 124 | def to_dict(self) -> Dict[str, Any]: 125 | """Convert settings to a dictionary.""" 126 | return self.model_dump() 127 | 128 | 129 | # Global settings instance 130 | settings = Settings.from_env() -------------------------------------------------------------------------------- /minion_agent/frameworks/browser_use.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | from typing import Optional, Any, List 4 | 5 | from pydantic import SecretStr 6 | 7 | from minion_agent.config import AgentFramework, AgentConfig 8 | from minion_agent.frameworks.minion_agent import MinionAgent 9 | from minion_agent.tools.wrappers import import_and_wrap_tools 10 | 11 | try: 12 | import browser_use 13 | from browser_use import Agent 14 | from browser_use import Agent, Browser, BrowserConfig 15 | 16 | browser_use_available = True 17 | except ImportError: 18 | browser_use_available = None 19 | 20 | DEFAULT_MODEL_CLASS = "langchain_openai.ChatOpenAI" 21 | 22 | class BrowserUseAgent(MinionAgent): 23 | """Browser-use agent implementation that handles both loading and running.""" 24 | name = "browser_agent" 25 | description = "Browser-use agent" 26 | 27 | def __init__( 28 | self, config: AgentConfig, managed_agents: Optional[list[AgentConfig]] = None 29 | ): 30 | if not browser_use_available: 31 | raise ImportError( 32 | "You need to `pip install 'minion-agent-x[browser_use]'` to use this agent" 33 | ) 34 | self.managed_agents = managed_agents 35 | self.config = config 36 | self._agent = None 37 | self._agent_loaded = False 38 | self._mcp_servers = None 39 | 40 | def _get_model(self, agent_config: AgentConfig): 41 | """Get the model configuration for a LangChain agent.""" 42 | if not agent_config.model_type: 43 | agent_config.model_type = DEFAULT_MODEL_CLASS 44 | module, class_name = agent_config.model_type.split(".") 45 | model_type = getattr(importlib.import_module(module), class_name) 46 | 47 | return model_type(model=agent_config.model_id, **agent_config.model_args or {}) 48 | 49 | async def _load_agent(self) -> None: 50 | """Load the Browser-use agent with the given configuration.""" 51 | if not self.config.tools: 52 | self.config.tools = [] # Browser-use has built-in browser automation tools 53 | 54 | tools, mcp_servers = await import_and_wrap_tools( 55 | self.config.tools, agent_framework=AgentFramework.BROWSER_USE 56 | ) 57 | self._mcp_servers = mcp_servers 58 | 59 | # Initialize the browser-use Agent 60 | browser = Browser( 61 | config=BrowserConfig( 62 | # Specify the path to your Chrome executable 63 | #browser_binary_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', # macOS path 64 | # For Windows, typically: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' 65 | # For Linux, typically: '/usr/bin/google-chrome' 66 | ) 67 | ) 68 | 69 | self._agent = Agent( 70 | task=self.config.instructions or "No specific task provided", 71 | llm=self._get_model(self.config), 72 | browser = browser, 73 | ) 74 | 75 | async def run_async(self, prompt: str) -> Any: 76 | """Run the Browser-use agent with the given prompt.""" 77 | # Update the agent's task with the new prompt 78 | self._agent.task = prompt 79 | self._agent._message_manager.task = prompt 80 | self._agent._message_manager.state.history.messages[1].message.content = f'Your ultimate task is: """{prompt}""". If you achieved your ultimate task, stop everything and use the done action in the next step to complete the task. If not, continue as usual.' 81 | result = await self._agent.run() 82 | return result 83 | 84 | @property 85 | def tools(self) -> List[str]: 86 | """ 87 | Return the tools used by the agent. 88 | This property is read-only and cannot be modified. 89 | """ 90 | return [] # Browser-use has built-in browser tools 91 | -------------------------------------------------------------------------------- /minion_agent/frameworks/deep_research_llms.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | import tenacity 4 | from litellm import acompletion, completion 5 | from together import Together 6 | from loguru import logger 7 | 8 | @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(multiplier=1, min=4, max=15)) 9 | async def asingle_shot_llm_call( 10 | model: str, 11 | system_prompt: str, 12 | message: str, 13 | response_format: Optional[dict[str, str | dict[str, Any]]] = None, 14 | max_completion_tokens: int | None = None, 15 | ) -> str: 16 | logger.info(f"Making async LLM call with model: {model}") 17 | logger.debug(f"System prompt: {system_prompt}") 18 | logger.debug(f"User message: {message}") 19 | try: 20 | response = await acompletion( 21 | model=model, 22 | messages=[{"role": "system", "content": system_prompt}, 23 | {"role": "user", "content": message}], 24 | temperature=0.0, 25 | response_format=response_format, 26 | # NOTE: max_token is deprecated per OpenAI API docs, use max_completion_tokens instead if possible 27 | # NOTE: max_completion_tokens is not currently supported by Together AI, so we use max_tokens instead 28 | max_tokens=max_completion_tokens, 29 | timeout=600, 30 | ) 31 | content = response.choices[0].message["content"] # type: ignore 32 | logger.info("Successfully received LLM response") 33 | logger.debug(f"LLM response content: {content}") 34 | return content 35 | except Exception as e: 36 | logger.error(f"Error in async LLM call: {str(e)}") 37 | raise 38 | 39 | @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(multiplier=1, min=4, max=15)) 40 | def single_shot_llm_call( 41 | model: str, 42 | system_prompt: str, 43 | message: str, 44 | response_format: Optional[dict[str, str | dict[str, Any]]] = None, 45 | max_completion_tokens: int | None = None, 46 | ) -> str: 47 | logger.info(f"Making sync LLM call with model: {model}") 48 | logger.debug(f"System prompt: {system_prompt}") 49 | logger.debug(f"User message: {message}") 50 | try: 51 | response = completion( 52 | model=model, 53 | messages=[{"role": "system", "content": system_prompt}, 54 | {"role": "user", "content": message}], 55 | temperature=0.0, 56 | response_format=response_format, 57 | # NOTE: max_token is deprecated per OpenAI API docs, use max_completion_tokens instead if possible 58 | # NOTE: max_completion_tokens is not currently supported by Together AI, so we use max_tokens instead 59 | max_tokens=max_completion_tokens, 60 | timeout=600, 61 | ) 62 | content = response.choices[0].message["content"] # type: ignore 63 | logger.info("Successfully received LLM response") 64 | logger.debug(f"LLM response content: {content}") 65 | return content 66 | except Exception as e: 67 | logger.error(f"Error in sync LLM call: {str(e)}") 68 | raise 69 | 70 | def generate_toc_image(prompt: str, planning_model: str, topic: str) -> str: 71 | """Generate a table of contents image""" 72 | logger.info(f"Generating TOC image for topic: {topic}") 73 | try: 74 | image_generation_prompt = single_shot_llm_call( 75 | model=planning_model, system_prompt=prompt, message=f"Research Topic: {topic}") 76 | 77 | if image_generation_prompt is None: 78 | logger.error("Image generation prompt is None") 79 | raise ValueError("Image generation prompt is None") 80 | 81 | # HERE WE CALL THE TOGETHER API SINCE IT'S AN IMAGE GENERATION REQUEST 82 | logger.info("Calling Together API for image generation") 83 | client = Together() 84 | imageCompletion = client.images.generate( 85 | model="black-forest-labs/FLUX.1-dev", 86 | width=1024, 87 | height=768, 88 | steps=28, 89 | prompt=image_generation_prompt, 90 | ) 91 | 92 | image_url = imageCompletion.data[0].url # type: ignore 93 | logger.info(f"Successfully generated image with URL: {image_url}") 94 | return image_url 95 | except Exception as e: 96 | logger.error(f"Error in image generation: {str(e)}") 97 | raise 98 | 99 | 100 | -------------------------------------------------------------------------------- /minion_agent/frameworks/deep_research_types.py: -------------------------------------------------------------------------------- 1 | #from together_open_deep_research 2 | import asyncio 3 | import os 4 | import sys 5 | import select 6 | from dataclasses import dataclass 7 | from typing import Optional 8 | 9 | from tavily import AsyncTavilyClient, TavilyClient 10 | 11 | from dataclasses import dataclass 12 | from typing import List 13 | from pydantic import BaseModel, Field 14 | 15 | @dataclass(frozen=True, kw_only=True) 16 | class SearchResult: 17 | title: str 18 | link: str 19 | content: str 20 | raw_content: Optional[str] = None 21 | 22 | def __str__(self, include_raw=True): 23 | result = f"Title: {self.title}\n" f"Link: {self.link}\n" f"Content: {self.content}" 24 | if include_raw and self.raw_content: 25 | result += f"\nRaw Content: {self.raw_content}" 26 | return result 27 | 28 | def short_str(self): 29 | return self.__str__(include_raw=False) 30 | 31 | 32 | @dataclass(frozen=True, kw_only=True) 33 | class SearchResults: 34 | results: list[SearchResult] 35 | 36 | def __str__(self, short=False): 37 | if short: 38 | result_strs = [result.short_str() for result in self.results] 39 | else: 40 | result_strs = [str(result) for result in self.results] 41 | return "\n\n".join(f"[{i+1}] {result_str}" for i, result_str in enumerate(result_strs)) 42 | 43 | def __add__(self, other): 44 | return SearchResults(results=self.results + other.results) 45 | 46 | def short_str(self): 47 | return self.__str__(short=True) 48 | 49 | 50 | def extract_tavily_results(response) -> SearchResults: 51 | """Extract key information from Tavily search results.""" 52 | results = [] 53 | for item in response.get("results", []): 54 | results.append( 55 | SearchResult( 56 | title=item.get("title", ""), 57 | link=item.get("url", ""), 58 | content=item.get("content", ""), 59 | raw_content=item.get("raw_content", ""), 60 | ) 61 | ) 62 | return SearchResults(results=results) 63 | 64 | 65 | def tavily_search(query: str, max_results=3, include_raw: bool = True) -> SearchResults: 66 | """ 67 | Perform a search using the Tavily Search API with the official client. 68 | 69 | Parameters: 70 | query (str): The search query. 71 | search_depth (str): The depth of search - 'basic' or 'deep'. 72 | max_results (int): Maximum number of results to return. 73 | 74 | Returns: 75 | list: Formatted search results with title, link, and snippet. 76 | """ 77 | api_key = os.getenv("TAVILY_API_KEY") 78 | 79 | if not api_key: 80 | raise ValueError("TAVILY_API_KEY environment variable is not set") 81 | 82 | client = TavilyClient(api_key) 83 | 84 | response = client.search(query=query, search_depth="basic", max_results=max_results, include_raw_content=include_raw) 85 | 86 | return extract_tavily_results(response) 87 | 88 | 89 | async def atavily_search_results(query: str, max_results=3, include_raw: bool = True) -> SearchResults: 90 | """ 91 | Perform asynchronous search using the Tavily Search API with the official client. 92 | 93 | Parameters: 94 | query (str): The search query. 95 | max_results (int): Maximum number of results to return. 96 | """ 97 | api_key = os.getenv("TAVILY_API_KEY") 98 | 99 | if not api_key: 100 | raise ValueError("TAVILY_API_KEY environment variable is not set") 101 | 102 | client = AsyncTavilyClient(api_key) 103 | 104 | response = await client.search(query=query, search_depth="basic", max_results=max_results, include_raw_content=include_raw) 105 | 106 | return extract_tavily_results(response) 107 | 108 | class ResearchPlan(BaseModel): 109 | queries: list[str] = Field( 110 | description="A list of search queries to thoroughly research the topic") 111 | 112 | 113 | class SourceList(BaseModel): 114 | sources: list[int] = Field( 115 | description="A list of source numbers from the search results") 116 | 117 | class UserCommunication: 118 | """Handles user input/output interactions with timeout functionality.""" 119 | 120 | @staticmethod 121 | async def get_input_with_timeout(prompt: str, timeout: float = 30.0) -> str: 122 | """ 123 | Get user input with a timeout. 124 | Returns empty string if timeout occurs or no input is provided. 125 | 126 | Args: 127 | prompt: The prompt to display to the user 128 | timeout: Number of seconds to wait for user input (default: 30.0) 129 | 130 | Returns: 131 | str: User input or empty string if timeout occurs 132 | """ 133 | print(prompt, end="", flush=True) 134 | 135 | # Different implementation for Windows vs Unix-like systems 136 | if sys.platform == "win32": 137 | # Windows implementation 138 | try: 139 | # Run input in an executor to make it async 140 | loop = asyncio.get_event_loop() 141 | user_input = await asyncio.wait_for(loop.run_in_executor(None, input), timeout) 142 | return user_input.strip() 143 | except TimeoutError: 144 | print("\nTimeout reached, continuing...") 145 | return "" 146 | else: 147 | # Unix-like implementation 148 | i, _, _ = select.select([sys.stdin], [], [], timeout) 149 | if i: 150 | return sys.stdin.readline().strip() 151 | else: 152 | print("\nTimeout reached, continuing...") 153 | return "" 154 | 155 | @dataclass(frozen=True, kw_only=True) 156 | class DeepResearchResult(SearchResult): 157 | """Wrapper on top of SearchResults to adapt it to the DeepResearch. 158 | 159 | This class extends the basic SearchResult by adding a filtered version of the raw content 160 | that has been processed and refined for the specific research context. It maintains 161 | the original search result while providing additional research-specific information. 162 | 163 | Attributes: 164 | filtered_raw_content: A processed version of the raw content that has been filtered 165 | and refined for relevance to the research topic 166 | """ 167 | 168 | filtered_raw_content: str 169 | 170 | def __str__(self): 171 | return f"Title: {self.title}\n" f"Link: {self.link}\n" f"Refined Content: {self.filtered_raw_content[:10000]}" 172 | 173 | def short_str(self): 174 | return f"Title: {self.title}\nLink: {self.link}\nRaw Content: {self.content[:10000]}" 175 | 176 | 177 | @dataclass(frozen=True, kw_only=True) 178 | class DeepResearchResults(SearchResults): 179 | results: list[DeepResearchResult] 180 | 181 | def __add__(self, other): 182 | return DeepResearchResults(results=self.results + other.results) 183 | 184 | def dedup(self): 185 | def deduplicate_by_link(results): 186 | seen_links = set() 187 | unique_results = [] 188 | 189 | for result in results: 190 | if result.link not in seen_links: 191 | seen_links.add(result.link) 192 | unique_results.append(result) 193 | 194 | return unique_results 195 | 196 | return DeepResearchResults(results=deduplicate_by_link(self.results)) -------------------------------------------------------------------------------- /minion_agent/frameworks/google.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any, List 2 | from uuid import uuid4 3 | 4 | from minion_agent.config import AgentFramework, AgentConfig 5 | from minion_agent.frameworks.minion_agent import MinionAgent 6 | from minion_agent.instructions import get_instructions 7 | from minion_agent.logging import logger 8 | from minion_agent.tools.wrappers import import_and_wrap_tools 9 | 10 | try: 11 | from google.adk.agents import Agent 12 | from google.adk.models.lite_llm import LiteLlm 13 | from google.adk.runners import InMemoryRunner 14 | from google.adk.tools.agent_tool import AgentTool 15 | from google.genai import types 16 | 17 | adk_available = True 18 | except ImportError as e: 19 | adk_available = None 20 | 21 | 22 | class GoogleAgent(MinionAgent): 23 | """Google agent implementation that handles both loading and running.""" 24 | 25 | def __init__( 26 | self, config: AgentConfig, managed_agents: Optional[list[AgentConfig]] = None 27 | ): 28 | if not adk_available: 29 | raise ImportError( 30 | "You need to `pip install 'minion-agent-x[google]'` to use this agent" 31 | ) 32 | self.managed_agents = managed_agents 33 | self.config = config 34 | self._agent = None 35 | self._agent_loaded = False 36 | self._mcp_servers = None 37 | self._managed_mcp_servers = None 38 | 39 | def _get_model(self, agent_config: AgentConfig): 40 | """Get the model configuration for a Google agent.""" 41 | return LiteLlm(model=agent_config.model_id, **agent_config.model_args or {}) 42 | 43 | async def _load_agent(self) -> None: 44 | """Load the Google agent with the given configuration.""" 45 | if not self.managed_agents and not self.config.tools: 46 | self.config.tools = [ 47 | "minion_agent.tools.search_web", 48 | "minion_agent.tools.visit_webpage", 49 | ] 50 | tools, mcp_servers = await import_and_wrap_tools( 51 | self.config.tools, agent_framework=AgentFramework.GOOGLE 52 | ) 53 | # Add to agent so that it doesn't get garbage collected 54 | self._mcp_servers = mcp_servers 55 | mcp_tools = [tool for mcp_server in mcp_servers for tool in mcp_server.tools] 56 | tools.extend(mcp_tools) 57 | 58 | sub_agents_instanced = [] 59 | if self.managed_agents: 60 | for managed_agent in self.managed_agents: 61 | managed_tools, managed_mcp_servers = await import_and_wrap_tools( 62 | managed_agent.tools, agent_framework=AgentFramework.GOOGLE 63 | ) 64 | # Add to agent so that it doesn't get garbage collected 65 | self._managed_mcp_servers = managed_mcp_servers 66 | managed_mcp_tools = [ 67 | tool 68 | for mcp_server in managed_mcp_servers 69 | for tool in mcp_server.tools 70 | ] 71 | managed_tools.extend(managed_mcp_tools) 72 | instance = Agent( 73 | name=managed_agent.name, 74 | instruction=get_instructions(managed_agent.instructions) or "", 75 | model=self._get_model(managed_agent), 76 | tools=managed_tools, 77 | **managed_agent.agent_args or {}, 78 | ) 79 | 80 | if managed_agent.handoff: 81 | sub_agents_instanced.append(instance) 82 | else: 83 | tools.append(AgentTool(instance)) 84 | 85 | self._agent = Agent( 86 | name=self.config.name, 87 | instruction=self.config.instructions or "", 88 | model=self._get_model(self.config), 89 | tools=tools, 90 | sub_agents=sub_agents_instanced, 91 | **self.config.agent_args or {}, 92 | output_key="response", 93 | ) 94 | 95 | async def run_async( 96 | self, prompt: str, user_id: str | None = None, session_id: str | None = None 97 | ) -> Any: 98 | """Run the Google agent with the given prompt.""" 99 | runner = InMemoryRunner(self._agent) 100 | user_id = user_id or str(uuid4()) 101 | session_id = session_id or str(uuid4()) 102 | runner.session_service.create_session( 103 | app_name=runner.app_name, user_id=user_id, session_id=session_id 104 | ) 105 | events = runner.run_async( 106 | user_id=user_id, 107 | session_id=session_id, 108 | new_message=types.Content(role="user", parts=[types.Part(text=prompt)]), 109 | ) 110 | 111 | async for event in events: 112 | logger.debug(event) 113 | if event.is_final_response(): 114 | break 115 | 116 | session = runner.session_service.get_session( 117 | app_name=runner.app_name, user_id=user_id, session_id=session_id 118 | ) 119 | return session.state.get("response", None) 120 | 121 | @property 122 | def tools(self) -> List[str]: 123 | """ 124 | Return the tools used by the agent. 125 | This property is read-only and cannot be modified. 126 | """ 127 | if hasattr(self, "_agent"): 128 | tools = [tool.name for tool in self._agent.tools] 129 | else: 130 | logger.warning("Agent not loaded or does not have tools.") 131 | tools = [] 132 | return tools 133 | -------------------------------------------------------------------------------- /minion_agent/frameworks/langchain.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from typing import Any, Optional, List 3 | 4 | from minion_agent.config import AgentFramework, AgentConfig 5 | from minion_agent.frameworks.minion_agent import MinionAgent 6 | from minion_agent.logging import logger 7 | from minion_agent.tools.wrappers import import_and_wrap_tools 8 | 9 | try: 10 | from langgraph.prebuilt import create_react_agent 11 | from langgraph.graph.graph import CompiledGraph 12 | 13 | langchain_available = True 14 | except ImportError: 15 | langchain_available = False 16 | 17 | 18 | DEFAULT_MODEL_CLASS = "langchain_litellm.ChatLiteLLM" 19 | 20 | 21 | class LangchainAgent(MinionAgent): 22 | """LangChain agent implementation that handles both loading and running.""" 23 | 24 | def __init__( 25 | self, config: AgentConfig, managed_agents: Optional[list[AgentConfig]] = None 26 | ): 27 | if not langchain_available: 28 | raise ImportError( 29 | "You need to `pip install 'minion-agent-x[langchain]'` to use this agent" 30 | ) 31 | self.managed_agents = managed_agents 32 | self.config = config 33 | self._agent = None 34 | self._agent_loaded = False 35 | self._tools = [] 36 | self._mcp_servers = None 37 | 38 | def _get_model(self, agent_config: AgentConfig): 39 | """Get the model configuration for a LangChain agent.""" 40 | if not agent_config.model_type: 41 | agent_config.model_type = DEFAULT_MODEL_CLASS 42 | module, class_name = agent_config.model_type.split(".") 43 | model_type = getattr(importlib.import_module(module), class_name) 44 | 45 | return model_type(model=agent_config.model_id, **agent_config.model_args or {}) 46 | 47 | async def _load_agent(self) -> None: 48 | """Load the LangChain agent with the given configuration.""" 49 | 50 | if not self.config.tools: 51 | self.config.tools = [ 52 | "minion_agent.tools.search_web", 53 | "minion_agent.tools.visit_webpage", 54 | ] 55 | 56 | if self.managed_agents: 57 | raise NotImplementedError("langchain managed agents are not supported yet") 58 | 59 | imported_tools, mcp_servers = await import_and_wrap_tools( 60 | self.config.tools, agent_framework=AgentFramework.LANGCHAIN 61 | ) 62 | self._mcp_servers = mcp_servers 63 | 64 | # Extract tools from MCP managers and add them to the imported_tools list 65 | for mcp_server in mcp_servers: 66 | imported_tools.extend(mcp_server.tools) 67 | 68 | model = self._get_model(self.config) 69 | 70 | self._agent: CompiledGraph = create_react_agent( 71 | model=model, 72 | tools=imported_tools, 73 | prompt=self.config.instructions, 74 | **self.config.agent_args or {}, 75 | ) 76 | # Langgraph doesn't let you easily access what tools are loaded from the CompiledGraph, so we'll store a list of them in this class 77 | self._tools = imported_tools 78 | 79 | async def run_async(self, prompt: str) -> Any: 80 | """Run the LangChain agent with the given prompt.""" 81 | inputs = {"messages": [("user", prompt)]} 82 | message = None 83 | async for s in self._agent.astream(inputs, stream_mode="values"): 84 | message = s["messages"][-1] 85 | if isinstance(message, tuple): 86 | logger.debug(message) 87 | else: 88 | message.pretty_print() 89 | return message 90 | 91 | @property 92 | def tools(self) -> List[str]: 93 | """ 94 | Return the tools used by the agent. 95 | This property is read-only and cannot be modified. 96 | """ 97 | return self._tools 98 | -------------------------------------------------------------------------------- /minion_agent/frameworks/minion.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Any, List 3 | 4 | from minion_agent.config import AgentFramework, AgentConfig 5 | from minion_agent.frameworks.minion_agent import MinionAgent 6 | from minion_agent.tools.wrappers import import_and_wrap_tools 7 | 8 | try: 9 | import minion 10 | from minion.main.brain import Brain 11 | from minion.providers import create_llm_provider 12 | from minion import config as minion_config 13 | from minion.main.local_python_env import LocalPythonEnv 14 | minion_available = True 15 | except ImportError as e: 16 | minion_available = None 17 | 18 | 19 | 20 | class MinionBrainAgent(MinionAgent): 21 | """minion agent implementation that handles both loading and running.""" 22 | 23 | def __init__( 24 | self, config: AgentConfig, managed_agents: Optional[list[AgentConfig]] = None 25 | ): 26 | if not minion_available: 27 | raise ImportError( 28 | "You need to `pip install 'minion-agent-x[minion]'` to use this agent" 29 | ) 30 | self.managed_agents = managed_agents 31 | self.config = config 32 | self._agent = None 33 | self._agent_loaded = False 34 | self._mcp_servers = None 35 | self._managed_mcp_servers = None 36 | 37 | def _get_model(self, agent_config: AgentConfig): 38 | """Get the model configuration for a minion agent. 39 | 40 | Args: 41 | agent_config: The agent configuration containing model settings 42 | 43 | Returns: 44 | A minion provider instance configured with the specified model 45 | """ 46 | # Get model ID from config or use default 47 | model_id = agent_config.model_id or "gpt-4o" 48 | 49 | # Get model config from minion's config 50 | llm_config = minion_config.models.get(model_id) 51 | if not llm_config: 52 | raise ValueError(f"Model {model_id} not found in minion config") 53 | 54 | # Create provider with model args from agent config 55 | provider = create_llm_provider( 56 | llm_config 57 | ) 58 | 59 | return provider 60 | 61 | def _merge_mcp_tools(self, mcp_servers): 62 | """Merge MCP tools from different servers.""" 63 | tools = [] 64 | for mcp_server in mcp_servers: 65 | tools.extend(mcp_server.tools) 66 | return tools 67 | 68 | async def _load_agent(self) -> None: 69 | """Load the agent instance with the given configuration.""" 70 | if not self.managed_agents and not self.config.tools: 71 | self.config.tools = [ 72 | "minion_agent.tools.search_web", 73 | "minion_agent.tools.visit_webpage", 74 | ] 75 | 76 | tools, mcp_servers = await import_and_wrap_tools( 77 | self.config.tools, agent_framework=AgentFramework.SMOLAGENTS 78 | ) 79 | self._mcp_servers = mcp_servers 80 | tools.extend(self._merge_mcp_tools(mcp_servers)) 81 | 82 | managed_agents_instanced = [] 83 | if self.managed_agents: 84 | for managed_agent in self.managed_agents: 85 | agent_type = getattr( 86 | smolagents, managed_agent.agent_type or DEFAULT_AGENT_TYPE 87 | ) 88 | managed_tools, managed_mcp_servers = await import_and_wrap_tools( 89 | managed_agent.tools, agent_framework=AgentFramework.SMOLAGENTS 90 | ) 91 | self._managed_mcp_servers = managed_mcp_servers 92 | tools.extend(self._merge_mcp_tools(managed_mcp_servers)) 93 | managed_agent_instance = agent_type( 94 | name=managed_agent.name, 95 | model=self._get_model(managed_agent), 96 | tools=managed_tools, 97 | verbosity_level=2, # OFF 98 | description=managed_agent.description 99 | or f"Use the agent: {managed_agent.name}", 100 | ) 101 | if managed_agent.instructions: 102 | managed_agent_instance.prompt_templates["system_prompt"] = ( 103 | managed_agent.instructions 104 | ) 105 | managed_agents_instanced.append(managed_agent_instance) 106 | 107 | main_agent_type = Brain 108 | 109 | # Get python_env from config or use default 110 | agent_args = self.config.agent_args or {} 111 | python_env = agent_args.pop('python_env', None) or LocalPythonEnv(verbose=False) 112 | 113 | self._agent = main_agent_type( 114 | python_env=python_env, 115 | llm=self._get_model(self.config), 116 | **agent_args 117 | ) 118 | 119 | async def run_async(self, task: str,*args,**kwargs) -> Any: 120 | """Run the Smolagents agent with the given prompt.""" 121 | obs, *_ = await self._agent.step(query=task, *args, **kwargs) 122 | return obs 123 | 124 | @property 125 | def tools(self) -> List[str]: 126 | """ 127 | Return the tools used by the agent. 128 | This property is read-only and cannot be modified. 129 | """ 130 | return self._agent.tools 131 | -------------------------------------------------------------------------------- /minion_agent/frameworks/minion_agent.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from abc import ABC, abstractmethod 3 | import asyncio 4 | from minion_agent.config import AgentFramework, AgentConfig 5 | 6 | 7 | class MinionAgent(ABC): 8 | """Base abstract class for all agent implementations. 9 | 10 | This provides a unified interface for different agent frameworks. 11 | """ 12 | 13 | # factory method 14 | @classmethod 15 | async def create( 16 | cls, 17 | agent_framework: AgentFramework, 18 | agent_config: AgentConfig, 19 | managed_agents: Optional[list[AgentConfig]] = None, 20 | ) -> "MinionAgent": 21 | if agent_framework == AgentFramework.SMOLAGENTS: 22 | from minion_agent.frameworks.smolagents import SmolagentsAgent as Agent 23 | elif agent_framework == AgentFramework.LANGCHAIN: 24 | from minion_agent.frameworks.langchain import LangchainAgent as Agent 25 | elif agent_framework == AgentFramework.OPENAI: 26 | from minion_agent.frameworks.openai import OpenAIAgent as Agent 27 | elif agent_framework == AgentFramework.LLAMAINDEX: 28 | from minion_agent.frameworks.llama_index import LlamaIndexAgent as Agent 29 | elif agent_framework == AgentFramework.GOOGLE: 30 | from minion_agent.frameworks.google import GoogleAgent as Agent 31 | elif agent_framework == AgentFramework.MINION: 32 | from minion_agent.frameworks.minion import MinionBrainAgent as Agent 33 | elif agent_framework == AgentFramework.BROWSER_USE: 34 | from minion_agent.frameworks.browser_use import BrowserUseAgent as Agent 35 | elif agent_framework == AgentFramework.DEEP_RESEARCH: 36 | from minion_agent.frameworks.deep_research import DeepResearchAgent as Agent 37 | else: 38 | raise ValueError(f"Unsupported agent framework: {agent_framework}") 39 | 40 | agent = Agent(agent_config, managed_agents=managed_agents) 41 | await agent._load_agent() 42 | return agent 43 | 44 | @abstractmethod 45 | async def _load_agent(self) -> None: 46 | """Load the agent instance.""" 47 | pass 48 | 49 | def run(self, task: str,*args,**kwargs) -> Any: 50 | """Run the agent with the given prompt.""" 51 | try: 52 | loop = asyncio.get_event_loop() 53 | if loop.is_running(): 54 | # 如果事件循环正在运行,我们需要在同一个循环中运行 55 | # 使用 nest_asyncio 来允许嵌套的事件循环 56 | import nest_asyncio 57 | nest_asyncio.apply() 58 | return loop.run_until_complete(self.run_async(task,*args,**kwargs)) 59 | except RuntimeError: 60 | # 如果没有事件循环,创建一个新的 61 | loop = asyncio.new_event_loop() 62 | asyncio.set_event_loop(loop) 63 | try: 64 | return loop.run_until_complete(self.run_async(task,*args,**kwargs)) 65 | finally: 66 | loop.close() 67 | 68 | def __call__(self, *args, **kwargs): 69 | #may be add some pre_prompt, post_prompt as being called as sub agents? 70 | return self.run(*args, **kwargs) 71 | 72 | @abstractmethod 73 | async def run_async(self, task: str,*args,**kwargs) -> Any: 74 | """Run the agent asynchronously with the given prompt.""" 75 | pass 76 | 77 | @property 78 | @abstractmethod 79 | def tools(self) -> List[str]: 80 | """ 81 | Return the tools used by the agent. 82 | This property is read-only and cannot be modified. 83 | """ 84 | pass 85 | 86 | def __init__(self): 87 | raise NotImplementedError( 88 | "Cannot instantiate the base class MinionAgent, please use the factory method 'MinionAgent.create'" 89 | ) 90 | 91 | @property 92 | def agent(self): 93 | """ 94 | The underlying agent implementation from the framework. 95 | 96 | This property is intentionally restricted to maintain framework abstraction 97 | and prevent direct dependency on specific agent implementations. 98 | 99 | If you need functionality that relies on accessing the underlying agent: 100 | 1. Consider if the functionality can be added to the MinionAgent interface 101 | 2. Submit a GitHub issue describing your use case 102 | 3. Contribute a PR implementing the needed functionality 103 | 104 | Raises: 105 | NotImplementedError: Always raised when this property is accessed 106 | """ 107 | raise NotImplementedError( 108 | "Cannot access the 'agent' property of MinionAgent, if you need to use functionality that relies on the underlying agent framework, please file a Github Issue or we welcome a PR to add the functionality to the MinionAgent class" 109 | ) 110 | -------------------------------------------------------------------------------- /minion_agent/frameworks/openai.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Any, List 3 | from dataclasses import fields 4 | 5 | from openai import AsyncAzureOpenAI, AsyncOpenAI 6 | 7 | from minion_agent.config import AgentFramework, AgentConfig 8 | from minion_agent.frameworks.minion_agent import MinionAgent 9 | from minion_agent.instructions import get_instructions 10 | from minion_agent.logging import logger 11 | from minion_agent.tools.wrappers import import_and_wrap_tools 12 | 13 | try: 14 | from agents import Agent, OpenAIChatCompletionsModel, Runner, ModelSettings 15 | 16 | agents_available = True 17 | except ImportError: 18 | agents_available = None 19 | 20 | OPENAI_MAX_TURNS = 30 21 | 22 | 23 | class OpenAIAgent(MinionAgent): 24 | """OpenAI agent implementation that handles both loading and running.""" 25 | 26 | def __init__( 27 | self, config: AgentConfig, managed_agents: Optional[list[AgentConfig]] = None 28 | ): 29 | if not agents_available: 30 | raise ImportError( 31 | "You need to `pip install 'minion-agent-x[openai]'` to use this agent" 32 | ) 33 | self.managed_agents = managed_agents 34 | self.config = config 35 | self._agent = None 36 | self._agent_loaded = False 37 | 38 | def _filter_model_settings_args(self, model_args: dict) -> dict: 39 | """Filter out non-ModelSettings arguments from model_args.""" 40 | allowed_args = {field.name for field in fields(ModelSettings)} 41 | return {k: v for k, v in model_args.items() if k in allowed_args} 42 | 43 | def _get_model(self, agent_config: AgentConfig): 44 | """Get the model configuration for an OpenAI agent.""" 45 | model_args = agent_config.model_args or {} 46 | api_key_var = model_args.pop("api_key_var", None) 47 | base_url = model_args.pop("azure_endpoint", None) 48 | azure_deployment = model_args.pop("azure_deployment", agent_config.model_id) 49 | api_version = model_args.pop("api_version", "2024-02-15-preview") 50 | 51 | if api_key_var and base_url: 52 | if azure_deployment: 53 | external_client = AsyncAzureOpenAI( 54 | api_key=os.environ[api_key_var], 55 | azure_endpoint=base_url, 56 | azure_deployment=azure_deployment, 57 | api_version=api_version, 58 | ) 59 | else: 60 | external_client = AsyncOpenAI( 61 | api_key=os.environ[api_key_var], 62 | base_url=base_url, 63 | ) 64 | return OpenAIChatCompletionsModel( 65 | model=agent_config.model_id, 66 | openai_client=external_client, 67 | ) 68 | return agent_config.model_id 69 | 70 | async def _load_agent(self) -> None: 71 | """Load the OpenAI agent with the given configuration.""" 72 | if not agents_available: 73 | raise ImportError( 74 | "You need to `pip install openai-agents` to use this agent" 75 | ) 76 | 77 | if not self.managed_agents and not self.config.tools: 78 | self.config.tools = [ 79 | "minion_agent.tools.search_web", 80 | "minion_agent.tools.visit_webpage", 81 | ] 82 | tools, mcp_servers = await import_and_wrap_tools( 83 | self.config.tools, agent_framework=AgentFramework.OPENAI 84 | ) 85 | 86 | handoffs = [] 87 | if self.managed_agents: 88 | for managed_agent in self.managed_agents: 89 | managed_tools, managed_mcp_servers = await import_and_wrap_tools( 90 | managed_agent.tools, agent_framework=AgentFramework.OPENAI 91 | ) 92 | if isinstance(managed_agent, MinionAgent): 93 | handoffs.append(managed_agent) 94 | continue 95 | kwargs = {} 96 | if managed_agent.model_args: 97 | kwargs["model_settings"] = ModelSettings(**self._filter_model_settings_args(managed_agent.model_args)) 98 | instance = Agent( 99 | name=managed_agent.name, 100 | instructions=get_instructions(managed_agent.instructions), 101 | model=self._get_model(managed_agent), 102 | tools=managed_tools, 103 | mcp_servers=[ 104 | managed_mcp_server.server 105 | for managed_mcp_server in managed_mcp_servers 106 | ], 107 | **kwargs, 108 | ) 109 | if managed_agent.handoff: 110 | handoffs.append(instance) 111 | else: 112 | tools.append( 113 | instance.as_tool( 114 | tool_name=instance.name, 115 | tool_description=managed_agent.description 116 | or f"Use the agent: {managed_agent.name}", 117 | ) 118 | ) 119 | 120 | kwargs = self.config.agent_args or {} 121 | if self.config.model_args: 122 | kwargs["model_settings"] = ModelSettings(**self._filter_model_settings_args(self.config.model_args)) 123 | self._agent = Agent( 124 | name=self.config.name, 125 | instructions=self.config.instructions, 126 | model=self._get_model(self.config), 127 | handoffs=handoffs, 128 | tools=tools, 129 | mcp_servers=[mcp_server.server for mcp_server in mcp_servers], 130 | **kwargs, 131 | ) 132 | 133 | async def run_async(self, prompt: str) -> Any: 134 | """Run the OpenAI agent with the given prompt asynchronously.""" 135 | result = await Runner.run(self._agent, prompt, max_turns=OPENAI_MAX_TURNS) 136 | return result 137 | 138 | @property 139 | def tools(self) -> List[str]: 140 | """ 141 | Return the tools used by the agent. 142 | This property is read-only and cannot be modified. 143 | """ 144 | if hasattr(self, "_agent"): 145 | # Extract tool names from the agent's tools 146 | tools = [tool.name for tool in self._agent.tools] 147 | # Add MCP tools to the list 148 | for mcp_server in self._agent.mcp_servers: 149 | tools_in_mcp = mcp_server._tools_list 150 | server_name = mcp_server.name.replace(" ", "_") 151 | if tools_in_mcp: 152 | tools.extend( 153 | [f"{server_name}_{tool.name}" for tool in tools_in_mcp] 154 | ) 155 | else: 156 | raise ValueError(f"No tools found in MCP {server_name}") 157 | else: 158 | logger.warning("Agent not loaded or does not have tools.") 159 | tools = [] 160 | return tools 161 | -------------------------------------------------------------------------------- /minion_agent/frameworks/smolagents.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Any, List 3 | 4 | 5 | 6 | from minion_agent.config import AgentFramework, AgentConfig 7 | from minion_agent.frameworks.minion_agent import MinionAgent 8 | from minion_agent.tools.wrappers import import_and_wrap_tools 9 | 10 | try: 11 | import smolagents 12 | from smolagents import MultiStepAgent 13 | from smolagents.local_python_executor import BASE_PYTHON_TOOLS 14 | 15 | BASE_PYTHON_TOOLS["open"] = open 16 | smolagents_available = True 17 | except ImportError: 18 | smolagents_available = None 19 | 20 | DEFAULT_AGENT_TYPE = "CodeAgent" 21 | DEFAULT_MODEL_CLASS = "LiteLLMModel" 22 | 23 | 24 | class SmolagentsAgent(MinionAgent): 25 | """Smolagents agent implementation that handles both loading and running.""" 26 | 27 | def __init__( 28 | self, config: AgentConfig, managed_agents: Optional[list[AgentConfig]] = None 29 | ): 30 | if not smolagents_available: 31 | raise ImportError( 32 | "You need to `pip install 'minion-agent-x[smolagents]'` to use this agent" 33 | ) 34 | self.managed_agents = managed_agents 35 | self.config = config 36 | self._agent = None 37 | self._agent_loaded = False 38 | self._mcp_servers = None 39 | self._managed_mcp_servers = None 40 | 41 | def _get_model(self, agent_config: AgentConfig): 42 | """Get the model configuration for a smolagents agent.""" 43 | model_type = getattr(smolagents, agent_config.model_type or DEFAULT_MODEL_CLASS) 44 | kwargs = { 45 | "model_id": agent_config.model_id, 46 | } 47 | model_args = agent_config.model_args or {} 48 | if api_key_var := model_args.pop("api_key_var", None): 49 | kwargs["api_key"] = os.environ[api_key_var] 50 | return model_type(**kwargs, **model_args) 51 | 52 | def _merge_mcp_tools(self, mcp_servers): 53 | """Merge MCP tools from different servers.""" 54 | tools = [] 55 | for mcp_server in mcp_servers: 56 | tools.extend(mcp_server.tools) 57 | return tools 58 | 59 | async def _load_agent(self) -> None: 60 | """Load the Smolagents agent with the given configuration.""" 61 | 62 | if not self.managed_agents and not self.config.tools: 63 | self.config.tools = [ 64 | "minion_agent.tools.search_web", 65 | "minion_agent.tools.visit_webpage", 66 | ] 67 | 68 | tools, mcp_servers = await import_and_wrap_tools( 69 | self.config.tools, agent_framework=AgentFramework.SMOLAGENTS 70 | ) 71 | self._mcp_servers = mcp_servers 72 | tools.extend(self._merge_mcp_tools(mcp_servers)) 73 | 74 | managed_agents_instanced = [] 75 | if self.managed_agents: 76 | for managed_agent in self.managed_agents: 77 | if isinstance(managed_agent, MinionAgent): 78 | managed_agents_instanced.append(managed_agent) 79 | continue 80 | if managed_agent.framework: 81 | agent = await MinionAgent.create(managed_agent.framework, managed_agent) 82 | if managed_agent.name: 83 | agent.name = managed_agent.name 84 | if managed_agent.description: 85 | agent.description = managed_agent.description 86 | managed_agents_instanced.append(agent) 87 | continue 88 | agent_type = getattr( 89 | smolagents, managed_agent.agent_type or DEFAULT_AGENT_TYPE 90 | ) 91 | managed_tools, managed_mcp_servers = await import_and_wrap_tools( 92 | managed_agent.tools, agent_framework=AgentFramework.SMOLAGENTS 93 | ) 94 | self._managed_mcp_servers = managed_mcp_servers 95 | tools.extend(self._merge_mcp_tools(managed_mcp_servers)) 96 | managed_agent_instance = agent_type( 97 | name=managed_agent.name, 98 | model=self._get_model(managed_agent), 99 | tools=managed_tools, 100 | verbosity_level=2, # OFF 101 | description=managed_agent.description 102 | or f"Use the agent: {managed_agent.name}", 103 | ) 104 | if managed_agent.instructions: 105 | managed_agent_instance.prompt_templates["system_prompt"] = ( 106 | managed_agent.instructions 107 | ) 108 | managed_agents_instanced.append(managed_agent_instance) 109 | 110 | main_agent_type = getattr( 111 | smolagents, self.config.agent_type or DEFAULT_AGENT_TYPE 112 | ) 113 | 114 | self._agent: MultiStepAgent = main_agent_type( 115 | name=self.config.name, 116 | model=self._get_model(self.config), 117 | tools=tools, 118 | verbosity_level=2, # OFF 119 | managed_agents=managed_agents_instanced, 120 | **self.config.agent_args or {}, 121 | ) 122 | 123 | if self.config.instructions: 124 | self._agent.prompt_templates["system_prompt"] = self.config.instructions 125 | 126 | async def run_async(self, prompt: str) -> Any: 127 | """Run the Smolagents agent with the given prompt.""" 128 | result = self._agent.run(prompt) 129 | return result 130 | 131 | @property 132 | def tools(self) -> List[str]: 133 | """ 134 | Return the tools used by the agent. 135 | This property is read-only and cannot be modified. 136 | """ 137 | return self._agent.tools 138 | -------------------------------------------------------------------------------- /minion_agent/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | from .imports import get_instructions 2 | 3 | __all__ = ["get_instructions"] 4 | -------------------------------------------------------------------------------- /minion_agent/instructions/imports.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import re 3 | 4 | 5 | def is_import(instructions): 6 | pattern = r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$" 7 | return bool(re.match(pattern, instructions)) 8 | 9 | 10 | def get_instructions(instructions: str | None) -> str | None: 11 | """Get the instructions from an external module. 12 | 13 | Args: 14 | instructions: Depending on the syntax used: 15 | 16 | - An import that points to a string in an external module. 17 | For example: `agents.extensions.handoff_prompt.RECOMMENDED_PROMPT_PREFIX`. 18 | The string will be imported from the external module. 19 | 20 | - A regular string containing instructions. 21 | For example: `You are a helpful assistant`. 22 | The string will be returned as is. 23 | 24 | Returns: 25 | Either the imported string or the input string as is. 26 | 27 | Raises: 28 | ValueError: If `instructions` is an import but doesn't point to a string. 29 | """ 30 | if instructions and is_import(instructions): 31 | module, obj = instructions.rsplit(".", 1) 32 | module = importlib.import_module(module) 33 | imported = getattr(module, obj) 34 | if not isinstance(imported, str): 35 | raise ValueError( 36 | "Instructions were identified as an import" 37 | f" but the value imported is not a string: {instructions}" 38 | ) 39 | return imported 40 | return instructions 41 | -------------------------------------------------------------------------------- /minion_agent/logging.py: -------------------------------------------------------------------------------- 1 | # import logging 2 | # from rich.logging import RichHandler 3 | # 4 | # logger = logging.getLogger("minion_agent") 5 | # logger.setLevel(logging.DEBUG) 6 | # logger.addHandler(RichHandler(rich_tracebacks=True)) 7 | -------------------------------------------------------------------------------- /minion_agent/providers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minion-Manus providers module. 3 | 4 | This module contains provider adapters for integrating with external frameworks. 5 | """ 6 | 7 | from minion_agent.providers.adapters import BaseSmolaAgentsModelAdapter, MinionProviderToSmolAdapter 8 | 9 | __all__ = [ 10 | "BaseSmolaAgentsModelAdapter", 11 | "MinionProviderToSmolAdapter", 12 | ] -------------------------------------------------------------------------------- /minion_agent/serving/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # if sys.version_info < (3, 13): 4 | # msg = "Serving with A2A requires Python 3.13 or higher! 🐍✨" 5 | # raise RuntimeError(msg) 6 | 7 | try: 8 | from .server import _get_a2a_app, serve_a2a, serve_a2a_async 9 | except ImportError as e: 10 | msg = "You need to `pip install 'minion-agent[serve]'` to use this method." 11 | raise ImportError(msg) from e 12 | 13 | 14 | __all__ = ["_get_a2a_app", "serve_a2a", "serve_a2a_async"] 15 | -------------------------------------------------------------------------------- /minion_agent/serving/agent_card.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | from typing import TYPE_CHECKING 5 | 6 | from a2a.types import AgentCapabilities, AgentCard, AgentSkill 7 | 8 | from minion_agent import AgentFramework 9 | 10 | if TYPE_CHECKING: 11 | from minion_agent import MinionAgent 12 | from minion_agent.config import ServingConfig 13 | 14 | 15 | def _get_agent_card(agent: MinionAgent, serving_config: ServingConfig) -> AgentCard: 16 | skills = [] 17 | for tool in agent._main_agent_tools: 18 | if hasattr(tool, "name"): 19 | tool_name = tool.name 20 | tool_description = tool.description 21 | elif agent.framework is AgentFramework.LLAMA_INDEX: 22 | tool_name = tool.metadata.name 23 | tool_description = tool.metadata.description 24 | else: 25 | tool_name = tool.__name__ 26 | tool_description = inspect.getdoc(tool) 27 | skills.append( 28 | AgentSkill( 29 | id=f"{agent.config.name}-{tool_name}", 30 | name=tool_name, 31 | description=tool_description, 32 | tags=[], 33 | ) 34 | ) 35 | if agent.config.description is None: 36 | msg = "Agent description is not set. Please set the `description` field in the `AgentConfig`." 37 | raise ValueError(msg) 38 | return AgentCard( 39 | name=agent.config.name, 40 | description=agent.config.description, 41 | version=serving_config.version, 42 | defaultInputModes=["text"], 43 | defaultOutputModes=["text"], 44 | url=f"http://{serving_config.host}:{serving_config.port}/", 45 | capabilities=AgentCapabilities( 46 | streaming=False, pushNotifications=False, stateTransitionHistory=False 47 | ), 48 | skills=skills, 49 | ) 50 | -------------------------------------------------------------------------------- /minion_agent/serving/agent_executor.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, override 2 | 3 | from a2a.server.agent_execution import AgentExecutor, RequestContext 4 | from a2a.server.events import EventQueue 5 | from a2a.utils import new_agent_text_message 6 | 7 | if TYPE_CHECKING: 8 | from minion_agent import MinionAgent 9 | 10 | 11 | class MinionAgentExecutor(AgentExecutor): # type: ignore[misc] 12 | """Test AgentProxy Implementation.""" 13 | 14 | def __init__(self, agent: "MinionAgent"): 15 | """Initialize the MinionAgentExecutor.""" 16 | self.agent = agent 17 | 18 | @override 19 | async def execute( # type: ignore[misc] 20 | self, 21 | context: RequestContext, 22 | event_queue: EventQueue, 23 | ) -> None: 24 | query = context.get_user_input() 25 | agent_trace = await self.agent.run_async(query) 26 | assert agent_trace.final_output is not None 27 | event_queue.enqueue_event(new_agent_text_message(agent_trace.final_output)) 28 | 29 | @override 30 | async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: # type: ignore[misc] 31 | msg = "cancel not supported" 32 | raise ValueError(msg) 33 | -------------------------------------------------------------------------------- /minion_agent/serving/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any 4 | 5 | import uvicorn 6 | from a2a.server.apps import A2AStarletteApplication 7 | from a2a.server.request_handlers import DefaultRequestHandler 8 | from a2a.server.tasks import InMemoryTaskStore 9 | 10 | from .agent_card import _get_agent_card 11 | from .agent_executor import MinionAgentExecutor 12 | 13 | if TYPE_CHECKING: 14 | from minion_agent import MinionAgent 15 | from minion_agent.config import ServingConfig 16 | 17 | import asyncio 18 | 19 | 20 | def _get_a2a_app( 21 | agent: MinionAgent, serving_config: ServingConfig 22 | ) -> A2AStarletteApplication: 23 | agent_card = _get_agent_card(agent, serving_config) 24 | 25 | request_handler = DefaultRequestHandler( 26 | agent_executor=MinionAgentExecutor(agent), 27 | task_store=InMemoryTaskStore(), 28 | ) 29 | 30 | return A2AStarletteApplication(agent_card=agent_card, http_handler=request_handler) 31 | 32 | 33 | def _create_server( 34 | app: A2AStarletteApplication, host: str, port: int, log_level: str = "warning" 35 | ) -> uvicorn.Server: 36 | config = uvicorn.Config(app.build(), host=host, port=port, log_level=log_level) 37 | return uvicorn.Server(config) 38 | 39 | 40 | async def serve_a2a_async( 41 | server: A2AStarletteApplication, host: str, port: int, log_level: str = "warning" 42 | ) -> tuple[asyncio.Task[Any], uvicorn.Server]: 43 | """Provide an A2A server to be used in an event loop.""" 44 | uv_server = _create_server(server, host, port) 45 | task = asyncio.create_task(uv_server.serve()) 46 | while not uv_server.started: # noqa: ASYNC110 47 | await asyncio.sleep(0.1) 48 | return (task, uv_server) 49 | 50 | 51 | def serve_a2a( 52 | server: A2AStarletteApplication, host: str, port: int, log_level: str = "warning" 53 | ) -> None: 54 | """Serve the A2A server.""" 55 | 56 | # Note that the task should be kept somewhere 57 | # because the loop only keeps weak refs to tasks 58 | # https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task 59 | async def run() -> None: 60 | (task, _) = await serve_a2a_async(server, host, port, log_level) 61 | await task 62 | 63 | return asyncio.get_event_loop().run_until_complete(run()) 64 | -------------------------------------------------------------------------------- /minion_agent/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_interaction import ( 2 | show_final_answer, 3 | show_plan, 4 | ask_user_verification, 5 | send_console_message, 6 | ) 7 | from .web_browsing import search_web, visit_webpage 8 | from .image_generation import generate_image_sync 9 | 10 | __all__ = [ 11 | "search_web", 12 | "show_final_answer", 13 | "show_plan", 14 | "ask_user_verification", 15 | "visit_webpage", 16 | "send_console_message", 17 | "generate_image_sync", 18 | ] -------------------------------------------------------------------------------- /minion_agent/tools/base.py: -------------------------------------------------------------------------------- 1 | """Base tool implementations.""" 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import Any, Dict, Optional 5 | 6 | 7 | class BaseTool(ABC): 8 | """Base class for all tools.""" 9 | 10 | name: str 11 | description: str 12 | inputs: Dict[str, Dict[str, Any]] 13 | 14 | @abstractmethod 15 | async def execute(self, **kwargs) -> Any: 16 | """Execute the tool with the given arguments.""" 17 | pass 18 | 19 | 20 | def to_smolagents(self): 21 | """Convert to smolagents tool.""" 22 | from smolagents import Tool 23 | 24 | class WrappedTool(Tool): 25 | name = self.name 26 | description = self.description 27 | inputs = self.inputs 28 | 29 | async def execute(self, **kwargs): 30 | return await self.execute(**kwargs) 31 | 32 | return WrappedTool() -------------------------------------------------------------------------------- /minion_agent/tools/image_generation.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | import base64 4 | from io import BytesIO 5 | import asyncio 6 | 7 | import httpx 8 | from PIL import Image 9 | 10 | from minion_agent.tools.base import BaseTool 11 | 12 | TOGETHER_AI_BASE = "https://api.together.xyz/v1/images/generations" 13 | API_KEY = os.getenv("TOGETHER_AI_API_KEY") 14 | DEFAULT_MODEL = "black-forest-labs/FLUX.1-schnell" 15 | 16 | 17 | def generate_image_sync(prompt: str, model: Optional[str] = DEFAULT_MODEL, width: Optional[int] = None, height: Optional[int] = None) -> Image.Image: 18 | """Generate an image using Together AI's API (synchronous version). 19 | 20 | Args: 21 | prompt (str): The text prompt for image generation 22 | model (Optional[str], optional): The model to use. Defaults to DEFAULT_MODEL. 23 | width (Optional[int], optional): Image width. Defaults to None. 24 | height (Optional[int], optional): Image height. Defaults to None. 25 | 26 | Returns: 27 | Image.Image: The generated image as a PIL Image object 28 | """ 29 | async_tool = ImageGenerationTool() 30 | 31 | async def _run(): 32 | return await async_tool.execute(prompt=prompt, model=model if model is not None else DEFAULT_MODEL, width=width, height=height) 33 | 34 | try: 35 | # Try to get the current event loop 36 | loop = asyncio.get_event_loop() 37 | # if loop.is_running(): 38 | # # If we're already in an event loop, create a new one in a thread 39 | # loop = asyncio.new_event_loop() 40 | # asyncio.set_event_loop(loop) 41 | return loop.run_until_complete(_run()) 42 | except RuntimeError: 43 | # If there is no event loop, create one 44 | return asyncio.run(_run()) 45 | 46 | 47 | class ImageGenerationTool(BaseTool): 48 | """Tool for generating images using Together AI's API.""" 49 | 50 | name = "generate_image" 51 | description = "Generate an image based on the text prompt using Together AI's API" 52 | parameters = { 53 | "type": "object", 54 | "properties": { 55 | "prompt": { 56 | "type": "string", 57 | "description": "The text prompt for image generation", 58 | }, 59 | "model": { 60 | "type": "string", 61 | "description": "The exact model name as it appears in Together AI. If incorrect, it will fallback to the default model (black-forest-labs/FLUX.1-schnell).", 62 | "default": DEFAULT_MODEL, 63 | }, 64 | "width": { 65 | "type": "number", 66 | "description": "Optional width for the image", 67 | }, 68 | "height": { 69 | "type": "number", 70 | "description": "Optional height for the image", 71 | }, 72 | }, 73 | "required": ["prompt"], 74 | } 75 | 76 | async def make_together_request( 77 | self, 78 | client: httpx.AsyncClient, 79 | prompt: str, 80 | model: str = DEFAULT_MODEL, 81 | width: Optional[int] = None, 82 | height: Optional[int] = None, 83 | ) -> dict: 84 | """Make a request to the Together API with error handling and fallback for incorrect model.""" 85 | request_body = {"model": model or DEFAULT_MODEL, "prompt": prompt, "response_format": "b64_json"} 86 | headers = {"Authorization": f"Bearer {API_KEY}"} 87 | 88 | if width is not None: 89 | request_body["width"] = width 90 | if height is not None: 91 | request_body["height"] = height 92 | 93 | async def send_request(body: dict) -> tuple[int, dict]: 94 | response = await client.post(TOGETHER_AI_BASE, headers=headers, json=body) 95 | try: 96 | data = response.json() 97 | except Exception: 98 | data = {} 99 | return response.status_code, data 100 | 101 | # First request with user-provided model 102 | status, data = await send_request(request_body) 103 | 104 | # Check if the request failed due to an invalid model error 105 | if status != 200 and "error" in data: 106 | error_info = data["error"] 107 | error_msg = str(error_info.get("message", "")).lower() 108 | error_code = str(error_info.get("code", "")).lower() 109 | if ( 110 | "model" in error_msg and "not available" in error_msg 111 | ) or error_code == "model_not_available": 112 | # Fallback to the default model 113 | request_body["model"] = DEFAULT_MODEL 114 | status, data = await send_request(request_body) 115 | if status != 200 or "error" in data: 116 | raise Exception( 117 | f"Fallback API error: {data.get('error', 'Unknown error')} (HTTP {status})" 118 | ) 119 | return data 120 | else: 121 | raise Exception(f"Together API error: {data.get('error')}") 122 | elif status != 200: 123 | raise Exception(f"HTTP error {status}") 124 | 125 | return data 126 | 127 | async def execute(self, prompt: str, model: Optional[str] = DEFAULT_MODEL, width: Optional[int] = None, height: Optional[int] = None) -> Image.Image: 128 | """Generate an image using Together AI's API. 129 | 130 | Args: 131 | prompt (str): The text prompt for image generation 132 | model (Optional[str], optional): The model to use. Defaults to DEFAULT_MODEL. 133 | width (Optional[int], optional): Image width. Defaults to None. 134 | height (Optional[int], optional): Image height. Defaults to None. 135 | 136 | Returns: 137 | Image.Image: The generated image as a PIL Image object 138 | """ 139 | if not API_KEY: 140 | raise ValueError("TOGETHER_AI_API_KEY environment variable not set") 141 | 142 | async with httpx.AsyncClient() as client: 143 | response_data = await self.make_together_request( 144 | client=client, 145 | prompt=prompt, 146 | model=model if model is not None else DEFAULT_MODEL, 147 | width=width, 148 | height=height, 149 | ) 150 | 151 | try: 152 | b64_image = response_data["data"][0]["b64_json"] 153 | image_bytes = base64.b64decode(b64_image) 154 | return Image.open(BytesIO(image_bytes)) 155 | except (KeyError, IndexError) as e: 156 | raise Exception(f"Failed to parse API response: {e}") -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/__init__.py: -------------------------------------------------------------------------------- 1 | from pydantic import TypeAdapter 2 | 3 | from minion_agent.config import AgentFramework, MCPParams 4 | 5 | from .agno import AgnoMCPServer 6 | from .google import GoogleMCPServer 7 | from .langchain import LangchainMCPServer 8 | from .llama_index import LlamaIndexMCPServer 9 | from .openai import OpenAIMCPServer 10 | from .smolagents import SmolagentsMCPServer 11 | from .tinyagent import TinyAgentMCPServer 12 | 13 | MCPServer = ( 14 | AgnoMCPServer 15 | | GoogleMCPServer 16 | | LangchainMCPServer 17 | | LlamaIndexMCPServer 18 | | OpenAIMCPServer 19 | | SmolagentsMCPServer 20 | | TinyAgentMCPServer 21 | ) 22 | 23 | 24 | def _get_mcp_server(mcp_tool: MCPParams, agent_framework: AgentFramework) -> MCPServer: 25 | return TypeAdapter[MCPServer](MCPServer).validate_python( 26 | {"mcp_tool": mcp_tool, "framework": agent_framework} 27 | ) 28 | 29 | 30 | __all__ = [ 31 | "AgnoMCPServer", 32 | "GoogleMCPServer", 33 | "LangchainMCPServer", 34 | "LlamaIndexMCPServer", 35 | "MCPServer", 36 | "OpenAIMCPServer", 37 | "SmolagentsMCPServer", 38 | "TinyAgentMCPServer", 39 | "_get_mcp_server", 40 | ] 41 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/agno.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import suppress 3 | from typing import Literal 4 | 5 | from pydantic import PrivateAttr 6 | 7 | from minion_agent.config import ( 8 | AgentFramework, 9 | MCPSse, 10 | MCPStdio, 11 | ) 12 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 13 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 14 | 15 | mcp_available = False 16 | with suppress(ImportError): 17 | from agno.tools.mcp import MCPTools as AgnoMCPTools 18 | from mcp import ClientSession 19 | from mcp.client.sse import sse_client 20 | 21 | mcp_available = True 22 | 23 | 24 | class AgnoMCPConnection(_MCPConnection["AgnoMCPTools"], ABC): 25 | _server: "AgnoMCPTools | None" = PrivateAttr(default=None) 26 | 27 | @abstractmethod 28 | async def list_tools(self) -> list["AgnoMCPTools"]: 29 | """List tools from the MCP server.""" 30 | if self._server is None: 31 | msg = "MCP server is not set up. Please call `list_tools` from a concrete class." 32 | raise ValueError(msg) 33 | 34 | tools = await self._exit_stack.enter_async_context(self._server) 35 | return [tools] 36 | 37 | 38 | class AgnoMCPStdioConnection(AgnoMCPConnection): 39 | mcp_tool: MCPStdio 40 | 41 | async def list_tools(self) -> list["AgnoMCPTools"]: 42 | """List tools from the MCP server.""" 43 | server_params = f"{self.mcp_tool.command} {' '.join(self.mcp_tool.args)}" 44 | self._server = AgnoMCPTools( 45 | command=server_params, 46 | include_tools=list(self.mcp_tool.tools) if self.mcp_tool.tools else None, 47 | env=self.mcp_tool.env, 48 | ) 49 | return await super().list_tools() 50 | 51 | 52 | class AgnoMCPSseConnection(AgnoMCPConnection): 53 | mcp_tool: MCPSse 54 | 55 | async def list_tools(self) -> list["AgnoMCPTools"]: 56 | """List tools from the MCP server.""" 57 | client = sse_client( 58 | url=self.mcp_tool.url, 59 | headers=dict(self.mcp_tool.headers or {}), 60 | ) 61 | sse_transport = await self._exit_stack.enter_async_context(client) 62 | stdio, write = sse_transport 63 | client_session = ClientSession(stdio, write) 64 | session = await self._exit_stack.enter_async_context(client_session) 65 | await session.initialize() 66 | self._server = AgnoMCPTools( 67 | session=session, 68 | include_tools=list(self.mcp_tool.tools) if self.mcp_tool.tools else None, 69 | ) 70 | return await super().list_tools() 71 | 72 | 73 | class AgnoMCPServerBase(_MCPServerBase["AgnoMCPTools"], ABC): 74 | framework: Literal[AgentFramework.AGNO] = AgentFramework.AGNO 75 | 76 | def _check_dependencies(self) -> None: 77 | """Check if the required dependencies for the MCP server are available.""" 78 | self.libraries = "minion-agent[mcp,agno]" 79 | self.mcp_available = mcp_available 80 | super()._check_dependencies() 81 | 82 | 83 | class AgnoMCPServerStdio(AgnoMCPServerBase): 84 | mcp_tool: MCPStdio 85 | 86 | async def _setup_tools( 87 | self, mcp_connection: _MCPConnection["AgnoMCPTools"] | None = None 88 | ) -> None: 89 | mcp_connection = mcp_connection or AgnoMCPStdioConnection( 90 | mcp_tool=self.mcp_tool 91 | ) 92 | await super()._setup_tools(mcp_connection) 93 | 94 | 95 | class AgnoMCPServerSse(AgnoMCPServerBase): 96 | mcp_tool: MCPSse 97 | 98 | async def _setup_tools( 99 | self, mcp_connection: _MCPConnection["AgnoMCPTools"] | None = None 100 | ) -> None: 101 | mcp_connection = mcp_connection or AgnoMCPSseConnection(mcp_tool=self.mcp_tool) 102 | await super()._setup_tools(mcp_connection) 103 | 104 | 105 | AgnoMCPServer = AgnoMCPServerStdio | AgnoMCPServerSse 106 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/google.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import suppress 3 | from typing import Literal 4 | 5 | from pydantic import PrivateAttr 6 | 7 | from minion_agent.config import ( 8 | AgentFramework, 9 | MCPSse, 10 | MCPStdio, 11 | ) 12 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 13 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 14 | 15 | mcp_available = False 16 | with suppress(ImportError): 17 | from google.adk.tools.mcp_tool import MCPTool as GoogleMCPTool 18 | from google.adk.tools.mcp_tool import MCPToolset as GoogleMCPToolset 19 | from google.adk.tools.mcp_tool.mcp_toolset import ( # type: ignore[attr-defined] 20 | SseServerParams as GoogleSseServerParameters, 21 | ) 22 | from google.adk.tools.mcp_tool.mcp_toolset import ( # type: ignore[attr-defined] 23 | StdioServerParameters as GoogleStdioServerParameters, 24 | ) 25 | 26 | mcp_available = True 27 | 28 | 29 | class GoogleMCPConnection(_MCPConnection["GoogleMCPTool"], ABC): 30 | """Base class for Google MCP connections.""" 31 | 32 | _params: "GoogleStdioServerParameters | GoogleSseServerParameters | None" = ( 33 | PrivateAttr(default=None) 34 | ) 35 | 36 | @abstractmethod 37 | async def list_tools(self) -> list["GoogleMCPTool"]: 38 | """List tools from the MCP server.""" 39 | if not self._params: 40 | msg = "MCP params is not set up. Please call `list_tools` from a concrete class." 41 | raise ValueError(msg) 42 | 43 | server = GoogleMCPToolset(connection_params=self._params) 44 | await self._exit_stack.enter_async_context(server) 45 | tools = await server.load_tools() 46 | return self._filter_tools(tools) # type: ignore[return-value] 47 | 48 | 49 | class GoogleMCPStdioConnection(GoogleMCPConnection): 50 | mcp_tool: MCPStdio 51 | 52 | async def list_tools(self) -> list["GoogleMCPTool"]: 53 | """List tools from the MCP server.""" 54 | self._params = GoogleStdioServerParameters( 55 | command=self.mcp_tool.command, 56 | args=list(self.mcp_tool.args), 57 | env=self.mcp_tool.env, 58 | ) 59 | return await super().list_tools() 60 | 61 | 62 | class GoogleMCPSseConnection(GoogleMCPConnection): 63 | mcp_tool: MCPSse 64 | 65 | async def list_tools(self) -> list["GoogleMCPTool"]: 66 | """List tools from the MCP server.""" 67 | self._params = GoogleSseServerParameters( 68 | url=self.mcp_tool.url, 69 | headers=dict(self.mcp_tool.headers or {}), 70 | ) 71 | return await super().list_tools() 72 | 73 | 74 | class GoogleMCPServerBase(_MCPServerBase["GoogleMCPTool"], ABC): 75 | framework: Literal[AgentFramework.GOOGLE] = AgentFramework.GOOGLE 76 | 77 | def _check_dependencies(self) -> None: 78 | """Check if the required dependencies for the MCP server are available.""" 79 | self.libraries = "minion-agent[mcp,google]" 80 | self.mcp_available = mcp_available 81 | super()._check_dependencies() 82 | 83 | 84 | class GoogleMCPServerStdio(GoogleMCPServerBase): 85 | mcp_tool: MCPStdio 86 | 87 | async def _setup_tools( 88 | self, mcp_connection: _MCPConnection["GoogleMCPTool"] | None = None 89 | ) -> None: 90 | mcp_connection = mcp_connection or GoogleMCPStdioConnection( 91 | mcp_tool=self.mcp_tool 92 | ) 93 | await super()._setup_tools(mcp_connection) 94 | 95 | 96 | class GoogleMCPServerSse(GoogleMCPServerBase): 97 | mcp_tool: MCPSse 98 | 99 | async def _setup_tools( 100 | self, mcp_connection: _MCPConnection["GoogleMCPTool"] | None = None 101 | ) -> None: 102 | mcp_connection = mcp_connection or GoogleMCPSseConnection( 103 | mcp_tool=self.mcp_tool 104 | ) 105 | await super()._setup_tools(mcp_connection) 106 | 107 | 108 | GoogleMCPServer = GoogleMCPServerStdio | GoogleMCPServerSse 109 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/langchain.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import suppress 3 | from datetime import timedelta 4 | from typing import Any, Literal 5 | 6 | from pydantic import PrivateAttr 7 | 8 | from minion_agent.config import AgentFramework, MCPSse, MCPStdio 9 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 10 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 11 | 12 | mcp_available = False 13 | with suppress(ImportError): 14 | from langchain_core.tools import BaseTool # noqa: TC002 15 | from langchain_mcp_adapters.tools import load_mcp_tools 16 | from mcp import ClientSession, StdioServerParameters 17 | from mcp.client.sse import sse_client 18 | from mcp.client.stdio import stdio_client 19 | 20 | mcp_available = True 21 | 22 | 23 | class LangchainMCPConnection(_MCPConnection["BaseTool"], ABC): 24 | """Base class for LangChain MCP connections.""" 25 | 26 | _client: Any | None = PrivateAttr(default=None) 27 | 28 | @abstractmethod 29 | async def list_tools(self) -> list["BaseTool"]: 30 | """List tools from the MCP server.""" 31 | if not self._client: 32 | msg = "MCP client is not set up. Please call `list_tools` from a concrete class." 33 | raise ValueError(msg) 34 | 35 | stdio, write = await self._exit_stack.enter_async_context(self._client) 36 | 37 | client_session = ClientSession( 38 | stdio, 39 | write, 40 | timedelta(seconds=self.mcp_tool.client_session_timeout_seconds) 41 | if self.mcp_tool.client_session_timeout_seconds 42 | else None, 43 | ) 44 | session = await self._exit_stack.enter_async_context(client_session) 45 | 46 | await session.initialize() 47 | 48 | tools = await load_mcp_tools(session) 49 | return self._filter_tools(tools) # type: ignore[return-value] 50 | 51 | 52 | class LangchainMCPStdioConnection(LangchainMCPConnection): 53 | mcp_tool: MCPStdio 54 | 55 | async def list_tools(self) -> list["BaseTool"]: 56 | """List tools from the MCP server.""" 57 | server_params = StdioServerParameters( 58 | command=self.mcp_tool.command, 59 | args=list(self.mcp_tool.args), 60 | env=self.mcp_tool.env, 61 | ) 62 | 63 | self._client = stdio_client(server_params) 64 | 65 | return await super().list_tools() 66 | 67 | 68 | class LangchainMCPSseConnection(LangchainMCPConnection): 69 | mcp_tool: MCPSse 70 | 71 | async def list_tools(self) -> list["BaseTool"]: 72 | """List tools from the MCP server.""" 73 | self._client = sse_client( 74 | url=self.mcp_tool.url, 75 | headers=dict(self.mcp_tool.headers or {}), 76 | ) 77 | return await super().list_tools() 78 | 79 | 80 | class LangchainMCPServerBase(_MCPServerBase["BaseTool"], ABC): 81 | framework: Literal[AgentFramework.LANGCHAIN] = AgentFramework.LANGCHAIN 82 | 83 | def _check_dependencies(self) -> None: 84 | self.libraries = "minion-agent[mcp,langchain]" 85 | self.mcp_available = mcp_available 86 | super()._check_dependencies() 87 | 88 | 89 | class LangchainMCPServerStdio(LangchainMCPServerBase): 90 | mcp_tool: MCPStdio 91 | 92 | async def _setup_tools( 93 | self, mcp_connection: _MCPConnection["BaseTool"] | None = None 94 | ) -> None: 95 | mcp_connection = mcp_connection or LangchainMCPStdioConnection( 96 | mcp_tool=self.mcp_tool 97 | ) 98 | await super()._setup_tools(mcp_connection) 99 | 100 | 101 | class LangchainMCPServerSse(LangchainMCPServerBase): 102 | mcp_tool: MCPSse 103 | 104 | async def _setup_tools( 105 | self, mcp_connection: _MCPConnection["BaseTool"] | None = None 106 | ) -> None: 107 | mcp_connection = mcp_connection or LangchainMCPSseConnection( 108 | mcp_tool=self.mcp_tool 109 | ) 110 | await super()._setup_tools(mcp_connection) 111 | 112 | 113 | LangchainMCPServer = LangchainMCPServerStdio | LangchainMCPServerSse 114 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/llama_index.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import suppress 3 | from typing import Literal 4 | 5 | from pydantic import PrivateAttr 6 | 7 | from minion_agent.config import AgentFramework, MCPSse, MCPStdio 8 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 9 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 10 | 11 | mcp_available = False 12 | with suppress(ImportError): 13 | from llama_index.core.tools import ( 14 | FunctionTool as LlamaIndexFunctionTool, # noqa: TC002 15 | ) 16 | from llama_index.tools.mcp import BasicMCPClient as LlamaIndexMCPClient 17 | from llama_index.tools.mcp import McpToolSpec as LlamaIndexMcpToolSpec 18 | 19 | mcp_available = True 20 | 21 | 22 | class LlamaIndexMCPConnection(_MCPConnection["LlamaIndexFunctionTool"], ABC): 23 | """Base class for LlamaIndex MCP connections.""" 24 | 25 | _client: "LlamaIndexMCPClient | None" = PrivateAttr(default=None) 26 | 27 | @abstractmethod 28 | async def list_tools(self) -> list["LlamaIndexFunctionTool"]: 29 | """List tools from the MCP server.""" 30 | if not self._client: 31 | msg = "MCP client is not set up. Please call `list_tool` from a concrete class." 32 | raise ValueError(msg) 33 | 34 | mcp_tool_spec = LlamaIndexMcpToolSpec( 35 | client=self._client, 36 | allowed_tools=list(self.mcp_tool.tools or []), 37 | ) 38 | 39 | return await mcp_tool_spec.to_tool_list_async() 40 | 41 | 42 | class LlamaIndexMCPStdioConnection(LlamaIndexMCPConnection): 43 | mcp_tool: MCPStdio 44 | 45 | async def list_tools(self) -> list["LlamaIndexFunctionTool"]: 46 | """List tools from the MCP server.""" 47 | self._client = LlamaIndexMCPClient( 48 | command_or_url=self.mcp_tool.command, 49 | args=list(self.mcp_tool.args), 50 | env=self.mcp_tool.env, 51 | ) 52 | return await super().list_tools() 53 | 54 | 55 | class LlamaIndexMCPSseConnection(LlamaIndexMCPConnection): 56 | mcp_tool: MCPSse 57 | 58 | async def list_tools(self) -> list["LlamaIndexFunctionTool"]: 59 | """List tools from the MCP server.""" 60 | self._client = LlamaIndexMCPClient(command_or_url=self.mcp_tool.url) 61 | return await super().list_tools() 62 | 63 | 64 | class LlamaIndexMCPServerBase(_MCPServerBase["LlamaIndexFunctionTool"], ABC): 65 | framework: Literal[AgentFramework.LLAMA_INDEX] = AgentFramework.LLAMA_INDEX 66 | 67 | def _check_dependencies(self) -> None: 68 | """Check if the required dependencies for the MCP server are available.""" 69 | self.libraries = "minion-agent[mcp,llama_index]" 70 | self.mcp_available = mcp_available 71 | super()._check_dependencies() 72 | 73 | 74 | class LlamaIndexMCPServerStdio(LlamaIndexMCPServerBase): 75 | mcp_tool: MCPStdio 76 | 77 | async def _setup_tools( 78 | self, mcp_connection: _MCPConnection["LlamaIndexFunctionTool"] | None = None 79 | ) -> None: 80 | mcp_connection = mcp_connection or LlamaIndexMCPStdioConnection( 81 | mcp_tool=self.mcp_tool 82 | ) 83 | await super()._setup_tools(mcp_connection) 84 | 85 | 86 | class LlamaIndexMCPServerSse(LlamaIndexMCPServerBase): 87 | mcp_tool: MCPSse 88 | 89 | async def _setup_tools( 90 | self, mcp_connection: _MCPConnection["LlamaIndexFunctionTool"] | None = None 91 | ) -> None: 92 | mcp_connection = mcp_connection or LlamaIndexMCPSseConnection( 93 | mcp_tool=self.mcp_tool 94 | ) 95 | await super()._setup_tools(mcp_connection) 96 | 97 | 98 | LlamaIndexMCPServer = LlamaIndexMCPServerStdio | LlamaIndexMCPServerSse 99 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/openai.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import suppress 3 | from typing import TYPE_CHECKING, Literal 4 | 5 | from pydantic import PrivateAttr 6 | 7 | from minion_agent.config import AgentFramework, MCPSse, MCPStdio 8 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 9 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 10 | 11 | if TYPE_CHECKING: 12 | from agents.mcp import MCPServerSse as OpenAIInternalMCPServerSse 13 | from agents.mcp import MCPServerStdio as OpenAIInternalMCPServerStdio 14 | from agents.mcp.server import MCPServer 15 | 16 | mcp_available = False 17 | with suppress(ImportError): 18 | from agents.mcp import MCPServerSse as OpenAIInternalMCPServerSse 19 | from agents.mcp import ( 20 | MCPServerSseParams as OpenAIInternalMCPServerSseParams, 21 | ) 22 | from agents.mcp import MCPServerStdio as OpenAIInternalMCPServerStdio 23 | from agents.mcp import ( 24 | MCPServerStdioParams as OpenAIInternalMCPServerStdioParams, 25 | ) 26 | from mcp.types import Tool as MCPTool # noqa: TC002 27 | 28 | mcp_available = True 29 | 30 | 31 | class OpenAIMCPConnection(_MCPConnection["MCPTool"], ABC): 32 | """Base class for OpenAI MCP connections.""" 33 | 34 | _server: "OpenAIInternalMCPServerStdio | OpenAIInternalMCPServerSse | None" = ( 35 | PrivateAttr(default=None) 36 | ) 37 | 38 | @abstractmethod 39 | async def list_tools(self) -> list["MCPTool"]: 40 | """List tools from the MCP server.""" 41 | if not self._server: 42 | msg = "MCP server is not set up. Please call `setup` from a concrete class." 43 | raise ValueError(msg) 44 | 45 | await self._exit_stack.enter_async_context(self._server) 46 | tools = await self._server.list_tools() 47 | return self._filter_tools(tools) # type: ignore[return-value] 48 | 49 | @property 50 | def server(self) -> "MCPServer | None": 51 | """Return the MCP server instance.""" 52 | return self._server 53 | 54 | 55 | class OpenAIMCPStdioConnection(OpenAIMCPConnection): 56 | mcp_tool: MCPStdio 57 | 58 | async def list_tools(self) -> list["MCPTool"]: 59 | """List tools from the MCP server.""" 60 | params = OpenAIInternalMCPServerStdioParams( 61 | command=self.mcp_tool.command, 62 | args=list(self.mcp_tool.args), 63 | env=self.mcp_tool.env, # type: ignore[typeddict-item] 64 | ) 65 | 66 | self._server = OpenAIInternalMCPServerStdio( 67 | name="OpenAI MCP Server", 68 | params=params, 69 | ) 70 | return await super().list_tools() 71 | 72 | 73 | class OpenAIMCPSseConnection(OpenAIMCPConnection): 74 | mcp_tool: MCPSse 75 | 76 | async def list_tools(self) -> list["MCPTool"]: 77 | """List tools from the MCP server.""" 78 | params = OpenAIInternalMCPServerSseParams(url=self.mcp_tool.url) 79 | 80 | self._server = OpenAIInternalMCPServerSse( 81 | name="OpenAI MCP Server", params=params 82 | ) 83 | 84 | return await super().list_tools() 85 | 86 | 87 | class OpenAIMCPServerBase(_MCPServerBase["MCPTool"], ABC): 88 | framework: Literal[AgentFramework.OPENAI] = AgentFramework.OPENAI 89 | 90 | def _check_dependencies(self) -> None: 91 | """Check if the required dependencies for the MCP server are available.""" 92 | self.libraries = "minion-agent[mcp,openai]" 93 | self.mcp_available = mcp_available 94 | super()._check_dependencies() 95 | 96 | 97 | class OpenAIMCPServerStdio(OpenAIMCPServerBase): 98 | mcp_tool: MCPStdio 99 | 100 | async def _setup_tools( 101 | self, mcp_connection: _MCPConnection["MCPTool"] | None = None 102 | ) -> None: 103 | mcp_connection = mcp_connection or OpenAIMCPStdioConnection( 104 | mcp_tool=self.mcp_tool 105 | ) 106 | await super()._setup_tools(mcp_connection) 107 | 108 | 109 | class OpenAIMCPServerSse(OpenAIMCPServerBase): 110 | mcp_tool: MCPSse 111 | 112 | async def _setup_tools( 113 | self, mcp_connection: _MCPConnection["MCPTool"] | None = None 114 | ) -> None: 115 | mcp_connection = mcp_connection or OpenAIMCPSseConnection( 116 | mcp_tool=self.mcp_tool 117 | ) 118 | await super()._setup_tools(mcp_connection) 119 | 120 | 121 | OpenAIMCPServer = OpenAIMCPServerStdio | OpenAIMCPServerSse 122 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/smolagents.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from contextlib import suppress 3 | from typing import Literal 4 | 5 | from pydantic import PrivateAttr 6 | 7 | from minion_agent.config import AgentFramework, MCPSse, MCPStdio 8 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 9 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 10 | 11 | mcp_available = False 12 | with suppress(ImportError): 13 | from mcp import StdioServerParameters 14 | from smolagents.mcp_client import MCPClient 15 | from smolagents.tools import Tool as SmolagentsTool # noqa: TC002 16 | 17 | mcp_available = True 18 | 19 | 20 | class SmolagentsMCPConnection(_MCPConnection["SmolagentsTool"], ABC): 21 | """Base class for Smolagents MCP connections.""" 22 | 23 | _client: "MCPClient | None" = PrivateAttr(default=None) 24 | 25 | @abstractmethod 26 | async def list_tools(self) -> list["SmolagentsTool"]: 27 | """List tools from the MCP server.""" 28 | if not self._client: 29 | msg = "Tool collection is not set up. Please call `list_tools` from a concrete class." 30 | raise ValueError(msg) 31 | 32 | tools = self._client.get_tools() 33 | return self._filter_tools(tools) # type: ignore[return-value] 34 | 35 | 36 | class SmolagentsMCPStdioConnection(SmolagentsMCPConnection): 37 | mcp_tool: MCPStdio 38 | 39 | async def list_tools(self) -> list["SmolagentsTool"]: 40 | """List tools from the MCP server.""" 41 | server_parameters = StdioServerParameters( 42 | command=self.mcp_tool.command, 43 | args=list(self.mcp_tool.args), 44 | env=self.mcp_tool.env, 45 | ) 46 | self._client = MCPClient(server_parameters) 47 | return await super().list_tools() 48 | 49 | 50 | class SmolagentsMCPSseConnection(SmolagentsMCPConnection): 51 | mcp_tool: MCPSse 52 | 53 | async def list_tools(self) -> list["SmolagentsTool"]: 54 | """List tools from the MCP server.""" 55 | server_parameters = { 56 | "url": self.mcp_tool.url, 57 | } 58 | self._client = MCPClient(server_parameters) 59 | 60 | return await super().list_tools() 61 | 62 | 63 | class SmolagentsMCPServerBase(_MCPServerBase["SmolagentsTool"], ABC): 64 | framework: Literal[AgentFramework.SMOLAGENTS] = AgentFramework.SMOLAGENTS 65 | 66 | def _check_dependencies(self) -> None: 67 | """Check if the required dependencies for the MCP server are available.""" 68 | self.libraries = "minion-agent[mcp,smolagents]" 69 | self.mcp_available = mcp_available 70 | super()._check_dependencies() 71 | 72 | 73 | class SmolagentsMCPServerStdio(SmolagentsMCPServerBase): 74 | mcp_tool: MCPStdio 75 | 76 | async def _setup_tools( 77 | self, mcp_connection: _MCPConnection["SmolagentsTool"] | None = None 78 | ) -> None: 79 | mcp_connection = mcp_connection or SmolagentsMCPStdioConnection( 80 | mcp_tool=self.mcp_tool 81 | ) 82 | await super()._setup_tools(mcp_connection) 83 | 84 | 85 | class SmolagentsMCPServerSse(SmolagentsMCPServerBase): 86 | mcp_tool: MCPSse 87 | 88 | async def _setup_tools( 89 | self, mcp_connection: _MCPConnection["SmolagentsTool"] | None = None 90 | ) -> None: 91 | mcp_connection = mcp_connection or SmolagentsMCPSseConnection( 92 | mcp_tool=self.mcp_tool 93 | ) 94 | await super()._setup_tools(mcp_connection) 95 | 96 | 97 | SmolagentsMCPServer = SmolagentsMCPServerStdio | SmolagentsMCPServerSse 98 | -------------------------------------------------------------------------------- /minion_agent/tools/mcp/frameworks/tinyagent.py: -------------------------------------------------------------------------------- 1 | """MCP adapter for Tiny framework.""" 2 | 3 | import os 4 | from abc import ABC, abstractmethod 5 | from collections.abc import Callable 6 | from contextlib import suppress 7 | from datetime import timedelta 8 | from typing import Any, Literal 9 | 10 | from pydantic import PrivateAttr 11 | 12 | from minion_agent.config import AgentFramework, MCPSse, MCPStdio, Tool 13 | from minion_agent.tools.mcp.mcp_connection import _MCPConnection 14 | from minion_agent.tools.mcp.mcp_server import _MCPServerBase 15 | 16 | # Check for MCP dependencies 17 | mcp_available = False 18 | with suppress(ImportError): 19 | from mcp import ClientSession, StdioServerParameters 20 | from mcp.client.sse import sse_client 21 | from mcp.client.stdio import stdio_client 22 | from mcp.types import Tool as MCPTool # noqa: TC002 23 | 24 | mcp_available = True 25 | 26 | 27 | class TinyAgentMCPConnection(_MCPConnection["MCPTool"], ABC): 28 | """Base class for TinyAgent MCP connections.""" 29 | 30 | _client: Any | None = PrivateAttr(default=None) 31 | 32 | @abstractmethod 33 | async def list_tools(self) -> list["MCPTool"]: 34 | """List tools from the MCP server.""" 35 | if not self._client: 36 | msg = "MCP client is not set up. Please call `setup` from a concrete class." 37 | raise ValueError(msg) 38 | 39 | # Setup the client connection using exit stack to manage lifecycle 40 | stdio, write = await self._exit_stack.enter_async_context(self._client) 41 | 42 | # Create a client session 43 | client_session = ClientSession( 44 | stdio, 45 | write, 46 | timedelta(seconds=self.mcp_tool.client_session_timeout_seconds) 47 | if self.mcp_tool.client_session_timeout_seconds 48 | else None, 49 | ) 50 | 51 | # Start the session 52 | session: ClientSession = await self._exit_stack.enter_async_context( 53 | client_session 54 | ) 55 | if not session: 56 | msg = "Failed to create MCP session" 57 | raise ValueError(msg) 58 | 59 | await session.initialize() 60 | 61 | # Get the available tools from the MCP server using schema 62 | available_tools = await session.list_tools() 63 | 64 | # Filter tools if specific tools were requested 65 | filtered_tools = self._filter_tools(available_tools.tools) 66 | 67 | # Create callable tool functions 68 | tool_list = list[Any]() 69 | for tool_info in filtered_tools: 70 | tool_list.append(self._create_tool_from_info(tool_info, session)) # type: ignore[arg-type] 71 | 72 | return tool_list 73 | 74 | def _create_tool_from_info( 75 | self, tool: Tool, session: "ClientSession" 76 | ) -> Callable[..., Any]: 77 | """Create a tool function from tool information.""" 78 | tool_name = tool.name if hasattr(tool, "name") else tool 79 | tool_description = tool.description if hasattr(tool, "description") else "" 80 | input_schema = tool.inputSchema if hasattr(tool, "inputSchema") else None 81 | if not session: 82 | msg = "Not connected to MCP server" 83 | raise ValueError(msg) 84 | 85 | async def tool_function(*args, **kwargs) -> Any: # type: ignore[no-untyped-def] 86 | """Tool function that calls the MCP server.""" 87 | # Combine args and kwargs 88 | combined_args = {} 89 | if args and len(args) > 0: 90 | combined_args = args[0] 91 | combined_args.update(kwargs) 92 | 93 | if not session: 94 | msg = "Not connected to MCP server" 95 | raise ValueError(msg) 96 | # Call the tool on the MCP server 97 | try: 98 | return await session.call_tool(tool_name, combined_args) # type: ignore[arg-type] 99 | except Exception as e: 100 | return f"Error calling tool {tool_name}: {e!s}" 101 | 102 | # Set attributes for the tool function 103 | tool_function.__name__ = tool_name # type: ignore[assignment] 104 | tool_function.__doc__ = tool_description 105 | # this isn't a defined attribute of a callable, but we pass it to tinyagent so that it can use it appropriately 106 | # when constructing the ToolExecutor. 107 | tool_function.__input_schema__ = input_schema # type: ignore[attr-defined] 108 | 109 | return tool_function 110 | 111 | 112 | class TinyAgentMCPStdioConnection(TinyAgentMCPConnection): 113 | mcp_tool: MCPStdio 114 | 115 | async def list_tools(self) -> list["MCPTool"]: 116 | """List tools from the MCP server.""" 117 | server_params = StdioServerParameters( 118 | command=self.mcp_tool.command, 119 | args=list(self.mcp_tool.args), 120 | env={**os.environ}, 121 | ) 122 | 123 | self._client = stdio_client(server_params) 124 | 125 | return await super().list_tools() 126 | 127 | 128 | class TinyAgentMCPSseConnection(TinyAgentMCPConnection): 129 | mcp_tool: MCPSse 130 | 131 | async def list_tools(self) -> list["MCPTool"]: 132 | """List tools from the MCP server.""" 133 | self._client = sse_client( 134 | url=self.mcp_tool.url, 135 | headers=dict(self.mcp_tool.headers or {}), 136 | ) 137 | 138 | return await super().list_tools() 139 | 140 | 141 | class TinyAgentMCPServerBase(_MCPServerBase["MCPTool"], ABC): 142 | framework: Literal[AgentFramework.TINYAGENT] = AgentFramework.TINYAGENT 143 | libraries: str = "minion-agent[mcp]" 144 | 145 | def _check_dependencies(self) -> None: 146 | """Check if the required dependencies for the MCP server are available.""" 147 | self.mcp_available = mcp_available 148 | super()._check_dependencies() 149 | 150 | 151 | class TinyAgentMCPServerStdio(TinyAgentMCPServerBase): 152 | mcp_tool: MCPStdio 153 | 154 | async def _setup_tools( 155 | self, mcp_connection: _MCPConnection["MCPTool"] | None = None 156 | ) -> None: 157 | mcp_connection = mcp_connection or TinyAgentMCPStdioConnection( 158 | mcp_tool=self.mcp_tool 159 | ) 160 | await super()._setup_tools(mcp_connection) 161 | 162 | 163 | class TinyAgentMCPServerSse(TinyAgentMCPServerBase): 164 | mcp_tool: MCPSse 165 | 166 | async def _setup_tools( 167 | self, mcp_connection: _MCPConnection["MCPTool"] | None = None 168 | ) -> None: 169 | mcp_connection = mcp_connection or TinyAgentMCPSseConnection( 170 | mcp_tool=self.mcp_tool 171 | ) 172 | await super()._setup_tools(mcp_connection) 173 | 174 | 175 | TinyAgentMCPServer = TinyAgentMCPServerStdio | TinyAgentMCPServerSse 176 | -------------------------------------------------------------------------------- /minion_agent/tools/user_interaction.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | 5 | def show_plan(plan: str) -> None: 6 | """Show the current plan to the user. 7 | 8 | Args: 9 | plan: The current plan. 10 | """ 11 | 12 | logger.info(f"Current plan: {plan}") 13 | return plan 14 | 15 | 16 | def show_final_answer(answer: str) -> None: 17 | """Show the final answer to the user. 18 | 19 | Args: 20 | answer: The final answer. 21 | """ 22 | logger.info(f"Final answer: {answer}") 23 | return answer 24 | 25 | 26 | def ask_user_verification(query: str) -> str: 27 | """Asks user to verify the given `query`. 28 | 29 | Args: 30 | query: The question that requires verification. 31 | """ 32 | return input(f"{query} => Type your answer here:") 33 | 34 | 35 | def send_console_message(user: str, query: str) -> str: 36 | """Sends the specified user a message via console and returns their response. 37 | Args: 38 | query: The question to ask the user. 39 | user: The user to ask the question to. 40 | Returns: 41 | str: The user's response. 42 | """ 43 | return input(f"{query}\n{user}>>") 44 | -------------------------------------------------------------------------------- /minion_agent/tools/web_browsing.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import requests 4 | from duckduckgo_search import DDGS 5 | from markdownify import markdownify 6 | from requests.exceptions import RequestException 7 | 8 | 9 | def _truncate_content(content: str, max_length: int) -> str: 10 | if len(content) <= max_length: 11 | return content 12 | else: 13 | return ( 14 | content[: max_length // 2] 15 | + f"\n..._This content has been truncated to stay below {max_length} characters_...\n" 16 | + content[-max_length // 2 :] 17 | ) 18 | 19 | 20 | def search_web(query: str) -> str: 21 | """Performs a duckduckgo web search based on your query (think a Google search) then returns the top search results. 22 | 23 | Args: 24 | query (str): The search query to perform. 25 | 26 | Returns: 27 | The top search results. 28 | """ 29 | ddgs = DDGS() 30 | results = ddgs.text(query, max_results=10) 31 | return "\n".join( 32 | f"[{result['title']}]({result['href']})\n{result['body']}" for result in results 33 | ) 34 | 35 | 36 | def visit_webpage(url: str) -> str: 37 | """Visits a webpage at the given url and reads its content as a markdown string. Use this to browse webpages. 38 | 39 | Args: 40 | url: The url of the webpage to visit. 41 | """ 42 | try: 43 | response = requests.get(url) 44 | response.raise_for_status() 45 | 46 | markdown_content = markdownify(response.text).strip() 47 | 48 | markdown_content = re.sub(r"\n{2,}", "\n", markdown_content) 49 | 50 | return _truncate_content(markdown_content, 10000) 51 | except RequestException as e: 52 | return f"Error fetching the webpage: {str(e)}" 53 | except Exception as e: 54 | return f"An unexpected error occurred: {str(e)}" 55 | -------------------------------------------------------------------------------- /minion_agent/tools/wrappers.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import importlib 3 | from collections.abc import Callable 4 | 5 | from minion_agent.config import AgentFramework, MCPTool 6 | 7 | from minion_agent.tools.mcp import ( 8 | GoogleMCPServerStdio, 9 | LlamaIndexMCPServerStdio, 10 | SmolagentsMCPServerStdio, 11 | MinionMCPServerStdio, 12 | BrowserUseMCPServerStdio, 13 | OpenAIMCPServerStdio, 14 | LangchainMCPServerStdio, 15 | MCPServerBase, 16 | 17 | 18 | ) 19 | 20 | 21 | def wrap_tool_openai(tool): 22 | from agents import function_tool, Tool 23 | 24 | if not isinstance(tool, Tool): 25 | return function_tool(tool) 26 | return tool 27 | 28 | 29 | def wrap_tool_langchain(tool): 30 | from langchain_core.tools import BaseTool 31 | from langchain_core.tools import tool as langchain_tool 32 | 33 | if not isinstance(tool, BaseTool): 34 | return langchain_tool(tool) 35 | return tool 36 | 37 | 38 | def wrap_tool_smolagents(tool): 39 | from smolagents import Tool, tool as smolagents_tool 40 | 41 | if not isinstance(tool, Tool): 42 | return smolagents_tool(tool) 43 | return tool 44 | def wrap_tool_minion(tool): 45 | #minion framework defined BaseTool and @tool 46 | from minion import BaseTool, tool as minion_tool 47 | 48 | if not isinstance(tool, BaseTool): 49 | return minion_tool(tool) 50 | return tool 51 | 52 | def wrap_tool_browser_use(tool): 53 | #browser_use don't use any tools now 54 | return tool 55 | 56 | def wrap_tool_llama_index(tool): 57 | from llama_index.core.tools import FunctionTool 58 | 59 | if not isinstance(tool, FunctionTool): 60 | return FunctionTool.from_defaults(tool) 61 | return tool 62 | 63 | 64 | def wrap_tool_google(tool): 65 | from google.adk.tools import BaseTool, FunctionTool 66 | 67 | if not isinstance(tool, BaseTool): 68 | return FunctionTool(tool) 69 | return tool 70 | 71 | 72 | async def wrap_mcp_server( 73 | mcp_tool: MCPTool, agent_framework: AgentFramework 74 | ) -> MCPServerBase: 75 | """ 76 | Generic MCP server wrapper that can work with different frameworks 77 | based on the specified agent_framework 78 | """ 79 | # Select the appropriate manager based on agent_framework 80 | mcp_server_map = { 81 | AgentFramework.OPENAI: OpenAIMCPServerStdio, 82 | AgentFramework.SMOLAGENTS: SmolagentsMCPServerStdio, 83 | AgentFramework.LANGCHAIN: LangchainMCPServerStdio, 84 | AgentFramework.GOOGLE: GoogleMCPServerStdio, 85 | AgentFramework.LLAMAINDEX: LlamaIndexMCPServerStdio, 86 | AgentFramework.MINION: MinionMCPServerStdio, 87 | AgentFramework.BROWSER_USE: BrowserUseMCPServerStdio, 88 | } 89 | 90 | if agent_framework not in mcp_server_map: 91 | raise NotImplementedError( 92 | f"Unsupported agent type: {agent_framework}. Currently supported types are: {mcp_server_map.keys()}" 93 | ) 94 | 95 | # Create the manager instance which will manage the MCP tool context 96 | manager_class = mcp_server_map[agent_framework] 97 | manager: MCPServerBase = manager_class(mcp_tool) 98 | await manager.setup_tools() 99 | 100 | return manager 101 | 102 | 103 | WRAPPERS = { 104 | AgentFramework.GOOGLE: wrap_tool_google, 105 | AgentFramework.OPENAI: wrap_tool_openai, 106 | AgentFramework.LANGCHAIN: wrap_tool_langchain, 107 | AgentFramework.SMOLAGENTS: wrap_tool_smolagents, 108 | AgentFramework.LLAMAINDEX: wrap_tool_llama_index, 109 | AgentFramework.MINION: wrap_tool_minion, 110 | AgentFramework.BROWSER_USE: wrap_tool_browser_use, #actually none 111 | } 112 | 113 | 114 | async def import_and_wrap_tools( 115 | tools: list[str | dict], agent_framework: AgentFramework 116 | ) -> tuple[list[Callable], list[MCPServerBase]]: 117 | wrapper = WRAPPERS[agent_framework] 118 | 119 | wrapped_tools = [] 120 | mcp_servers = [] 121 | for tool in tools: 122 | if isinstance(tool, MCPTool): 123 | # MCP adapters are usually implemented as context managers. 124 | # We wrap the server using `MCPServerBase` so the 125 | # tools can be used as any other callable. 126 | mcp_server = await wrap_mcp_server(tool, agent_framework) 127 | mcp_servers.append(mcp_server) 128 | elif isinstance(tool, str): 129 | module, func = tool.rsplit(".", 1) 130 | module = importlib.import_module(module) 131 | imported_tool = getattr(module, func) 132 | if inspect.isclass(imported_tool): 133 | imported_tool = imported_tool() 134 | wrapped_tools.append(wrapper(imported_tool)) 135 | else: 136 | raise ValueError( 137 | f"Tool {tool} needs to be of type `str` or `MCPTool` but is {type(tool)}" 138 | ) 139 | 140 | return wrapped_tools, mcp_servers 141 | -------------------------------------------------------------------------------- /minion_agent/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for Minion-Manus. 3 | 4 | This package contains utility functions and classes for the Minion-Manus framework. 5 | """ 6 | 7 | from minion_agent.utils.logging import setup_logging 8 | from minion_agent.config import Settings 9 | 10 | __all__ = ["setup_logging"] 11 | 12 | # 在应用启动时 13 | # settings = Settings.from_env() # 或传入自定义设置 14 | # setup_logging(settings) 15 | -------------------------------------------------------------------------------- /minion_agent/utils/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging utilities for Minion-Agent. 3 | 4 | This module provides utilities for setting up logging. 5 | """ 6 | 7 | import sys 8 | from typing import Optional 9 | 10 | from loguru import logger 11 | 12 | from minion_agent.config import Settings 13 | 14 | 15 | def setup_logging(settings: Optional[Settings] = None) -> None: 16 | """Set up logging. 17 | 18 | Args: 19 | settings: Settings for logging. If None, the default settings will be used. 20 | """ 21 | settings = settings or Settings.from_env() 22 | 23 | # Remove default logger 24 | logger.remove() 25 | 26 | # Add console logger 27 | logger.add( 28 | sys.stderr, 29 | level=settings.logging.level, 30 | format=settings.logging.format, 31 | colorize=True, 32 | ) 33 | 34 | # Add file logger if configured 35 | if settings.logging.file: 36 | logger.add( 37 | settings.logging.file, 38 | level=settings.logging.level, 39 | format=settings.logging.format, 40 | rotation="10 MB", 41 | compression="zip", 42 | ) 43 | 44 | logger.info("Logging configured") -------------------------------------------------------------------------------- /original.md: -------------------------------------------------------------------------------- 1 | # **# 重磅开源!minion-agent:你的 AI 百宝箱,让 AI 代理为你效劳!🚀** 2 | 3 | 最近 AI Agent 可以说是红得发紫,今天给大家带来一个超级强大的开源项目 - minion-agent!这个项目就像是给每个开发者都配备了一个智能管家,可以帮你处理各种复杂任务。作为xxx的技术小编,我第一时间就被这个项目圈粉了,让我们一起来看看它有多厉害! 4 | 5 | ## 🌟 minion-agent 是什么? 6 | 7 | 简单来说,minion-agent 是一个强大的 AI 代理框架,它就像是一个百宝箱,里面装满了各种能力: 8 | 9 | 1. 🤖 多框架融合 10 | - 无缝支持 OpenAI、LangChain、Google AI , Smolagents等主流框架 11 | - 统一的接口,让你轻松驾驭不同的 AI 能力 12 | 1. 🛠️ 丰富的工具集 13 | - 网页浏览和搜索功能 14 | - 文件操作和管理 15 | - 自动化任务处理 16 | - 可扩展的工具系统 17 | 1. 👥 多代理协作 18 | - 支持创建多个专门的子代理 19 | - 代理之间可以协同工作 20 | - 任务自动分配和管理 21 | 1. 🌐 智能网页操作 22 | - 集成浏览器自动化 23 | - 可以执行复杂的网页任务 24 | - 数据抓取和分析 25 | 1. 🔍 深度研究能力 26 | - 内置 DeepResearch 代理 27 | - 可以进行深入的主题研究 28 | - 自动整理和总结信息 29 | 30 | ## 💡 它能做什么? 31 | 32 | 想象一下,有了 minion-agent,你可以: 33 | 34 | 1. 自动化研究: 35 | - 输入一个主题,它会自动搜索、浏览相关网页 36 | - 收集和分析信息 37 | - 生成研究报告 38 | 1. 智能助手: 39 | - 帮你处理日常任务 40 | - 回答问题和提供建议 41 | - 自动化重复性工作 42 | 43 | 3. 数据处理: 44 | 45 | - 自动收集和整理数据 46 | - 分析和总结信息 47 | - 生成报告和图表 48 | 1. 网页自动化: 49 | - 自动浏览网页 50 | - 提取有用信息 51 | - 执行特定操作 52 | 53 | # Minion架构 54 | 55 | ![image.png](attachment:85e7083d-981f-4801-896f-ff87e8d75d52:image.png) 56 | 57 | ![image.png](attachment:dce6410f-3f95-4d6d-90e0-9b8ca9ac8c7b:image.png) 58 | 59 | ## 实战案例:AI智能体的实际应用 60 | 61 | ### 案例1:deep research印欧语系的演化过程 62 | 63 | minion-agent在市场研究领域展现出强大的自动化能力: 64 | 65 | ```python 66 | research_agent_config = AgentConfig( 67 | framework=AgentFramework.DEEP_RESEARCH, 68 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 69 | name="research_assistant", 70 | description="A helpful research assistant that conducts deep research on topics" 71 | ) 72 | 73 | # Create the main agent with the research agent as a managed agent 74 | main_agent = await MinionAgent.create( 75 | AgentFramework.SMOLAGENTS, 76 | main_agent_config, 77 | managed_agents=[research_agent_config] 78 | ) 79 | research_query = """ 80 | Research The evolution of Indo-European languages, and save a markdown out of it. 81 | """ 82 | result = agent.run(research_query) 83 | ``` 84 | 85 | 研究结果: 86 | 87 | - 自动收集了超过35篇相关文章 88 | - 生成了6页详细分析报告 89 | - 识别出5个关键市场趋势 90 | - 完成时间仅需8min(人工预计需要2天) 91 | 92 | 视频demo: 93 | 94 | deepresearch https://youtu.be/tOd56nagsT4 95 | 96 | ### 案例2:自动比价 97 | 98 | ```python 99 | config = AgentConfig( 100 | name="browser-agent", 101 | model_type="langchain_openai.AzureChatOpenAI", 102 | model_id=azure_deployment, 103 | model_args={ 104 | "azure_deployment": azure_deployment, 105 | "api_version": api_version, 106 | }, 107 | # You can specify initial instructions here 108 | instructions="Compare the price of gpt-4o and DeepSeek-V3", 109 | 110 | ) 111 | 112 | # Create and initialize the agent using MinionAgent.create 113 | agent = await MinionAgent.create(AgentFramework.BROWSER_USE, config) 114 | 115 | # Run the agent with a specific task 116 | result = agent.run("Compare the price of gpt-4o and DeepSeek-V3 and create a detailed comparison table") 117 | print("Task Result:", result) 118 | ``` 119 | 120 | 视频demo: 121 | 122 | 比价 https://youtu.be/O0RhA3eeDlg 123 | 124 | ### 案例3:自动生成游戏 125 | 126 | ```python 127 | main_agent_config = AgentConfig( 128 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 129 | name="research_assistant", 130 | description="A helpful research assistant" 131 | ) 132 | 133 | # Create the main agent with the research agent as a managed agent 134 | main_agent = await MinionAgent.create( 135 | AgentFramework.SMOLAGENTS, 136 | main_agent_config 137 | 138 | ) 139 | result = agent.run("实现一个贪吃蛇游戏") 140 | ``` 141 | 142 | 视频demo: 143 | 144 | 生成snake game https://youtu.be/UBquRXD9ZJc 145 | 146 | **案例4: 搜索deepseek prover并生成漂亮的html** 147 | 148 | 今天deepseek prover发布,让我们看看browser use可以搜出什么样的结果吧 149 | 150 | ```python 151 | agent_config = AgentConfig( 152 | model_id=os.environ.get("AZURE_DEPLOYMENT_NAME"), 153 | name="research_assistant", 154 | description="A helpful research assistant", 155 | model_args={"azure_endpoint": os.environ.get("AZURE_OPENAI_ENDPOINT"), 156 | "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), 157 | "api_version": os.environ.get("OPENAI_API_VERSION"), 158 | }, 159 | tools=[ 160 | "minion_agent.tools.browser_tool.browser", 161 | "minion_agent.tools.generation.generate_pdf", 162 | "minion_agent.tools.generation.generate_html", 163 | "minion_agent.tools.generation.save_and_generate_html", 164 | MCPTool( 165 | command="npx", 166 | args=["-y", "@modelcontextprotocol/server-filesystem","/Users/femtozheng/workspace","/Users/femtozheng/python-project/minion-agent"] 167 | ), 168 | 169 | ], 170 | ) 171 | 172 | # Create the main agent with the research agent as a managed agent 173 | main_agent = await MinionAgent.create( 174 | AgentFramework.SMOLAGENTS, 175 | main_agent_config 176 | 177 | ) 178 | result = agent.run("搜索Deepseek prover的最新消息,汇总成一个html, 你的html应该尽可能美观,然后保存html到磁盘上") 179 | ``` 180 | 181 | 视频demo: 182 | **搜索deepseek prover并生成漂亮的html** https://youtu.be/ENbQ4MP9kKc 183 | 184 | ## 技术优势对比 185 | 186 | 与商业解决方案相比,minion-agent具有显著优势: 187 | 188 | 1. 成本效益 189 | - Manus:$39/月 190 | 191 | 价格:**$39 / 月**信用点:**3,900 点**功能:支持同时运行 **2 个任务**,适合个人用户或轻度使用。限制:信用点可能几天内耗尽,特别是在执行复杂任务时。 Pro 计划 价格:**$199 / 月** 192 | 193 | - minion-agent:完全开源,仅需支付基础API费用 194 | 1. 功能扩展性 195 | - 商业方案:封闭生态,功能固定 196 | - minion-agent:开放架构,支持自定义扩展, 支持mcp工具 197 | 198 | 3.部署灵活性 199 | 200 | - 商业方案:依赖云服务 201 | - minion-agent:支持本地部署和混合云 202 | 203 | ## 🌈 为什么选择 minion-agent? 204 | 205 | 1. 开源免费:完全开源,社区驱动 206 | 2. 易于使用:简单的 API,清晰的文档 207 | 3. 功能强大:集成多种框架和工具 208 | 4. 可扩展性:支持自定义工具和功能 209 | 5. 活跃维护:持续更新和改进 210 | 211 | ## 🎯 适用场景 212 | 213 | 1. 研究人员:自动化研究和数据收集 214 | 2. 开发者:构建智能应用和自动化工具 215 | 3. 数据分析师:自动化数据处理和分析 216 | 4. 内容创作者:辅助内容研究和创作 217 | 5. 企业用户:提高工作效率,自动化流程 218 | 219 | ## 🔗 项目链接 220 | 221 | GitHub:[minion-agent](https://github.com/femto/minion-agent) 222 | 223 | ## 📢 加入社区 224 | 225 | - Discord:[加入讨论](https://discord.gg/HUC6xEK9aT) 226 | - 微信讨论群: [二维码] 227 | 228 | ## 结语 229 | 230 | minion-agent 的出现,为 AI Agent 领域带来了新的可能。它不仅提供了丰富的功能,更重要的是它的开源特性让每个开发者都能参与其中,共同推动 AI 技术的发展。 231 | 232 | 如果你觉得这个项目有帮助,别忘了给它点个 star⭐️! 233 | 234 | 我是硅基智元的小硅,一个对 AI 技术充满热情的探索者。如果你也对 AI 技术感兴趣,欢迎关注我们,一起探索 AI 的无限可能! -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "minion-agent-x" 3 | version = "0.1.3" 4 | description = "An enhanced version of minion-agent with extended capabilities" 5 | authors = [ 6 | {name = "femtozheng", email = "femtozheng@example.com"}, 7 | ] 8 | dependencies = [ 9 | "smolagents>=0.0.1", 10 | "python-dotenv>=1.0.0", 11 | "browser-use>=0.0.1", 12 | "loguru>=0.0.1", 13 | 14 | ] 15 | requires-python = ">=3.11" 16 | 17 | [project.optional-dependencies] 18 | dev = [ 19 | "black>=23.0.0", 20 | "isort>=5.12.0", 21 | "pytest>=7.0.0", 22 | ] 23 | mcp = [ 24 | "mcp>=1.5.0", 25 | "asyncio>=3.4.3", 26 | "playwright>=1.51.0", 27 | "openai>=1.75.0", 28 | "markdownify>=1.1.0", 29 | "langchain-core>=0.3.49", 30 | "langchain-openai>=0.3.11", 31 | "smolagents[mcp]>=1.13.0" 32 | ] 33 | google = [ 34 | "google-adk","litellm" 35 | ] 36 | smolagents = [ 37 | "smolagents[litellm,mcp]>=1.10.0", 38 | "openinference-instrumentation-smolagents" 39 | ] 40 | openai = [ 41 | "openai-agents>=0.0.7", 42 | "openinference-instrumentation-openai-agents>=0.1.5" 43 | ] 44 | minion = [ 45 | "minionx>=0.1.1", 46 | "nltk>=0.0.1" 47 | ] 48 | deep-research = [ 49 | "nest_asyncio>=0.0.1", 50 | "pydantic", 51 | "litellm", 52 | "datasets", 53 | "commonmark", 54 | "xhtml2pdf", 55 | "pypandoc", 56 | "pandoc", 57 | "filelock", 58 | "together>=1.3.5", 59 | "pandas>=1.5.0", 60 | "tavily-python>=0.5.1", 61 | "tenacity>=9.0.0", 62 | "pymdown-extensions>=10.14.3", 63 | "smolagents>=1.13.0", 64 | "langgraph>=0.3.29", 65 | "langchain-together>=0.3.0", 66 | "langchain>=0.3.23", 67 | "gradio>=5.25.0", 68 | ] 69 | serve = [ 70 | "a2a-sdk>=0.2.5", 71 | ] 72 | all = [ 73 | "minion-agent-x[google,smolagents,mcp,minion,deep-research]" 74 | ] 75 | 76 | [build-system] 77 | requires = ["hatchling"] 78 | build-backend = "hatchling.build" 79 | 80 | [tool.black] 81 | line-length = 88 82 | target-version = ['py39'] 83 | include = '\.pyi?$' 84 | 85 | [tool.isort] 86 | profile = "black" 87 | multi_line_output = 3 88 | 89 | [tool.hatch.build.targets.wheel] 90 | packages = ["minion_agent"] -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # 测试文件的模式 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | 7 | # 测试输出格式 8 | addopts = -v --strict-markers 9 | 10 | # 标记定义 11 | markers = 12 | asyncio: marks tests as asyncio tests (deselect with '-m "not asyncio"') 13 | slow: marks tests as slow (deselect with '-m "not slow"') 14 | integration: marks tests that require integration setup 15 | 16 | # 指定测试路径 17 | testpaths = tests 18 | 19 | # 输出设置 20 | console_output_style = count 21 | 22 | # 缓存设置 23 | cache_dir = .pytest_cache -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Development dependencies 2 | pytest>=7.4.0 3 | pytest-asyncio>=0.21.1 4 | pytest-cov>=4.1.0 5 | pytest-mock>=3.11.1 6 | black>=23.7.0 7 | flake8>=6.1.0 8 | mypy>=1.5.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core dependencies 2 | pydantic>=2.0.0 3 | jsonschema>=4.0.0 4 | typing-extensions>=4.0.0 5 | aiohttp>=3.8.0 6 | python-dotenv>=1.0.0 7 | 8 | # Optional dependencies for SmolaAgents integration 9 | smolagents>=1.13.0 10 | nest-asyncio>=1.5.6 11 | 12 | # Optional dependencies for database examples 13 | sqlalchemy>=2.0.0 14 | aiosqlite>=0.17.0 15 | 16 | browser-use 17 | loguru -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | with open("README.md", "r", encoding="utf-8") as fh: 7 | long_description = fh.read() 8 | 9 | with open("requirements.txt", "r", encoding="utf-8") as f: 10 | requirements = f.read().splitlines() 11 | 12 | # Filter out comments and empty lines 13 | requirements = [r for r in requirements if r and not r.startswith("#")] 14 | 15 | setup( 16 | name="minion-agent", 17 | version="0.1.0", 18 | author="FemtoZheng", 19 | author_email="femto@example.com", 20 | description="A toolkit for implementing and managing tools for LLM agents", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | url="https://github.com/femto/minion-agent", 24 | packages=find_packages(), 25 | classifiers=[ 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | "Development Status :: 3 - Alpha", 34 | "Intended Audience :: Developers", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | ], 37 | python_requires=">=3.8", 38 | install_requires=requirements, 39 | extras_require={ 40 | "smolagents": ["smolagents>=0.0.5", "nest-asyncio>=1.5.6"], 41 | "database": ["sqlalchemy>=2.0.0", "aiosqlite>=0.17.0"], 42 | "dev": ["pytest>=7.0.0", "pytest-asyncio>=0.21.0", "black>=23.0.0"], 43 | }, 44 | ) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the minion-manus project. 3 | 4 | This package contains test modules for the minion-manus project components. 5 | """ 6 | 7 | # Ensure pytest can discover the tests 8 | # See https://docs.pytest.org/en/stable/pythonpath.html -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared pytest fixtures for the minion-manus project tests. 3 | 4 | This module contains shared fixtures that can be used across multiple test files. 5 | 6 | To run all tests: 7 | pytest 8 | 9 | To run specific test files: 10 | pytest tests/test_minion_provider_adapter.py 11 | 12 | To run specific test functions: 13 | pytest tests/test_minion_provider_adapter.py::test_create_adapter_from_model_name 14 | 15 | To run tests with specific markers: 16 | pytest -m "asyncio" 17 | 18 | To generate test coverage report: 19 | pytest --cov=minion_agent 20 | """ 21 | 22 | import os 23 | import sys 24 | import pytest 25 | from unittest import mock 26 | 27 | # Add parent directory to path to import from minion_agent 28 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 29 | 30 | 31 | # Shared fixtures for mocking 32 | @pytest.fixture 33 | def mock_minion(): 34 | """Fixture to mock the minion module.""" 35 | return mock.MagicMock() 36 | 37 | 38 | @pytest.fixture 39 | def mock_config(): 40 | """Fixture to mock the minion.config module.""" 41 | config_mock = mock.MagicMock() 42 | config_mock.models = {"gpt-4o": {"some": "config"}} 43 | return config_mock 44 | 45 | 46 | @pytest.fixture 47 | def mock_providers(): 48 | """Fixture to mock the minion.providers module.""" 49 | return mock.MagicMock() 50 | 51 | 52 | @pytest.fixture 53 | def mock_basic_provider(): 54 | """Fixture to mock a basic provider instance.""" 55 | provider_mock = mock.MagicMock() 56 | provider_mock.generate_sync.return_value = "Mock response" 57 | provider_mock.agenerate = mock.AsyncMock(return_value={ 58 | "choices": [{"message": {"content": "Async mock response"}}] 59 | }) 60 | provider_mock.chat_completion.return_value = { 61 | "choices": [{"message": {"content": "Mock response"}}] 62 | } 63 | return provider_mock 64 | 65 | 66 | @pytest.fixture 67 | def mock_tool_calling_provider(): 68 | """Fixture to mock a provider with tool calling capability.""" 69 | provider_mock = mock.MagicMock() 70 | tool_call_response = "I'll help with that" 71 | provider_mock.generate_sync.return_value = tool_call_response 72 | provider_mock.chat_completion.return_value = { 73 | "choices": [{ 74 | "message": { 75 | "content": "I'll help with that", 76 | "tool_calls": [ 77 | { 78 | "type": "function", 79 | "function": { 80 | "name": "date_tool", 81 | "arguments": "{}" 82 | } 83 | } 84 | ] 85 | } 86 | }] 87 | } 88 | return provider_mock 89 | 90 | 91 | @pytest.fixture 92 | def mock_smolagents(): 93 | """Fixture to mock the smolagents module.""" 94 | return mock.MagicMock() 95 | 96 | 97 | @pytest.fixture 98 | def mock_tool_decorator(): 99 | """Fixture to mock the tool decorator.""" 100 | def tool_decorator(func): 101 | func._is_tool = True 102 | return func 103 | return tool_decorator 104 | 105 | 106 | @pytest.fixture 107 | def patch_basic_modules(mock_minion, mock_config, mock_providers, mock_basic_provider, monkeypatch): 108 | """Fixture to patch modules for basic tests.""" 109 | # Set up patches 110 | modules = { 111 | 'minion': mock_minion, 112 | 'minion.config': mock_config, 113 | 'minion.providers': mock_providers, 114 | } 115 | for name, mock_obj in modules.items(): 116 | monkeypatch.setitem(sys.modules, name, mock_obj) 117 | 118 | # Configure mock provider 119 | mock_providers.create_llm_provider.return_value = mock_basic_provider -------------------------------------------------------------------------------- /tests/test_basic_adapter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the basic functionality of MinionProviderToSmolAdapter. 6 | 7 | This module tests the core functionality of creating and using the adapter 8 | without the SmolaAgents integration features. 9 | """ 10 | 11 | import asyncio 12 | import os 13 | import sys 14 | import pytest 15 | from unittest import mock 16 | from typing import List, Dict, Any, Optional 17 | 18 | # Add parent directory to path to import from minion_agent 19 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 20 | 21 | from minion_agent.providers.adapters import MinionProviderToSmolAdapter 22 | 23 | 24 | def test_create_adapter_from_model_name(patch_basic_modules, mock_providers): 25 | """Test creating adapter directly from model name.""" 26 | adapter = MinionProviderToSmolAdapter(model_name="gpt-4o") 27 | assert adapter is not None 28 | 29 | # Test generate method 30 | messages = [{"role": "user", "content": "Say hello!"}] 31 | result = adapter.generate(messages) 32 | assert result["choices"][0]["message"]["content"] == "Mock response" 33 | 34 | # Verify the provider was created with the correct model config 35 | mock_providers.create_llm_provider.assert_called_once() 36 | 37 | 38 | def test_create_adapter_from_provider(mock_basic_provider): 39 | """Test creating adapter from provider.""" 40 | adapter = MinionProviderToSmolAdapter(provider=mock_basic_provider) 41 | assert adapter is not None 42 | 43 | # Test generate method 44 | messages = [{"role": "user", "content": "What's the weather like today?"}] 45 | result = adapter.generate(messages) 46 | assert result["choices"][0]["message"]["content"] == "Mock response" 47 | 48 | 49 | def test_from_model_name_class_method(patch_basic_modules): 50 | """Test the from_model_name class method.""" 51 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 52 | assert adapter is not None 53 | 54 | # Test generate method 55 | messages = [{"role": "user", "content": "Tell me a joke."}] 56 | result = adapter.generate(messages) 57 | assert result["choices"][0]["message"]["content"] == "Mock response" 58 | 59 | 60 | @pytest.mark.asyncio 61 | async def test_async_implementation(patch_basic_modules): 62 | """Test async implementation.""" 63 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 64 | 65 | # Test agenerate method 66 | messages = [{"role": "user", "content": "What's your favorite color?"}] 67 | result = await adapter.agenerate(messages) 68 | assert result["choices"][0]["message"]["content"] == "Async mock response" -------------------------------------------------------------------------------- /tests/test_minion_provider_adapter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the MinionProviderToSmolAdapter. 6 | 7 | This module tests the functionality of the MinionProviderToSmolAdapter 8 | which converts Minion LLM providers to SmolaAgents compatible models. 9 | """ 10 | 11 | import asyncio 12 | import os 13 | import sys 14 | import pytest 15 | from unittest import mock 16 | from typing import List, Dict, Any, Optional 17 | 18 | # Add parent directory to path to import from minion_agent 19 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 20 | 21 | from minion_agent.providers.adapters import MinionProviderToSmolAdapter 22 | 23 | 24 | def test_create_adapter_from_model_name(patch_basic_modules, mock_providers): 25 | """Test creating adapter directly from model name.""" 26 | adapter = MinionProviderToSmolAdapter(model_name="gpt-4o") 27 | assert adapter is not None 28 | 29 | # Test generate method 30 | messages = [{"role": "user", "content": "Say hello!"}] 31 | result = adapter.generate(messages) 32 | assert result["choices"][0]["message"]["content"] == "Mock response" 33 | 34 | # Verify the provider was created with the correct model config 35 | mock_providers.create_llm_provider.assert_called_once() 36 | 37 | 38 | def test_create_adapter_from_provider(mock_basic_provider): 39 | """Test creating adapter from provider.""" 40 | adapter = MinionProviderToSmolAdapter(provider=mock_basic_provider) 41 | assert adapter is not None 42 | 43 | # Test generate method 44 | messages = [{"role": "user", "content": "What's the weather like today?"}] 45 | result = adapter.generate(messages) 46 | assert result["choices"][0]["message"]["content"] == "Mock response" 47 | 48 | 49 | def test_from_model_name_class_method(patch_basic_modules): 50 | """Test the from_model_name class method.""" 51 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 52 | assert adapter is not None 53 | 54 | # Test generate method 55 | messages = [{"role": "user", "content": "Tell me a joke."}] 56 | result = adapter.generate(messages) 57 | assert result["choices"][0]["message"]["content"] == "Mock response" 58 | 59 | 60 | @pytest.mark.asyncio 61 | async def test_async_implementation(patch_basic_modules): 62 | """Test async implementation.""" 63 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 64 | 65 | # Test agenerate method 66 | messages = [{"role": "user", "content": "What's your favorite color?"}] 67 | result = await adapter.agenerate(messages) 68 | assert result["choices"][0]["message"]["content"] == "Async mock response" -------------------------------------------------------------------------------- /tests/test_smolagents_integration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the SmolaAgents integration with MinionProviderToSmolAdapter. 6 | 7 | This module tests the integration between MinionProviderToSmolAdapter and SmolaAgents, 8 | ensuring that tools can be properly called through the adapter. 9 | """ 10 | 11 | import os 12 | import sys 13 | import pytest 14 | from unittest import mock 15 | from typing import List, Dict, Any, Optional 16 | 17 | # Add parent directory to path to import from minion_agent 18 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 19 | 20 | from minion_agent.providers.adapters import MinionProviderToSmolAdapter 21 | 22 | 23 | @pytest.fixture 24 | def mock_agent(): 25 | """Fixture to mock a ToolCallingAgent instance.""" 26 | agent_mock = mock.MagicMock() 27 | agent_mock.run.return_value = "Today's date is 2023-01-01 and the capital of France is Paris." 28 | return agent_mock 29 | 30 | 31 | @pytest.fixture 32 | def patch_tool_calling_modules( 33 | mock_minion, mock_config, mock_providers, mock_tool_calling_provider, 34 | mock_smolagents, mock_tool_decorator, mock_agent, monkeypatch 35 | ): 36 | """Fixture to patch all required modules and configure mocks for tool calling.""" 37 | # Set up patches 38 | modules = { 39 | 'minion': mock_minion, 40 | 'minion.config': mock_config, 41 | 'minion.providers': mock_providers, 42 | 'smolagents': mock_smolagents, 43 | 'smolagents.agents': mock.MagicMock(), 44 | 'smolagents.tools': mock.MagicMock(), 45 | } 46 | 47 | for name, mock_obj in modules.items(): 48 | monkeypatch.setitem(sys.modules, name, mock_obj) 49 | 50 | # Configure mocks 51 | mock_providers.create_llm_provider.return_value = mock_tool_calling_provider 52 | mock_smolagents.tools.tool = mock_tool_decorator 53 | mock_smolagents.agents.ToolCallingAgent.return_value = mock_agent 54 | 55 | 56 | def test_tool_calling_agent_integration(patch_tool_calling_modules, mock_smolagents, mock_agent): 57 | """Test integration with ToolCallingAgent.""" 58 | from smolagents.agents import ToolCallingAgent 59 | from smolagents.tools import tool 60 | 61 | # Create the adapter 62 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 63 | 64 | # Define tools 65 | @tool 66 | def date_tool() -> str: 67 | """Get the current date.""" 68 | return "2023-01-01" 69 | 70 | @tool 71 | def capital_tool(country: str) -> str: 72 | """Get the capital of a country.""" 73 | return "Paris" if country.lower() == "france" else f"Unknown capital for {country}" 74 | 75 | # Create the agent 76 | agent = ToolCallingAgent( 77 | model=adapter, 78 | tools=[date_tool, capital_tool] 79 | ) 80 | 81 | # Test the agent 82 | response = agent.run("What is today's date? Also, what is the capital of France?") 83 | 84 | # Verify we got a proper response 85 | assert response == "Today's date is 2023-01-01 and the capital of France is Paris." 86 | 87 | # Verify the model was called with our query 88 | mock_smolagents.agents.ToolCallingAgent.assert_called_once() 89 | mock_agent.run.assert_called_once_with( 90 | "What is today's date? Also, what is the capital of France?" 91 | ) -------------------------------------------------------------------------------- /tests/test_smolagents_tools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for SmolaAgents tool calling integration with MinionProviderToSmolAdapter. 6 | 7 | This module specifically tests the tool calling features of the SmolaAgents integration, 8 | focusing on how tools are registered, called, and how results are processed. 9 | """ 10 | 11 | import os 12 | import sys 13 | import pytest 14 | from unittest import mock 15 | from typing import List, Dict, Any, Optional, Callable 16 | 17 | # Add parent directory to path to import from minion_agent 18 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 19 | 20 | from minion_agent.providers.adapters import MinionProviderToSmolAdapter 21 | 22 | 23 | @pytest.fixture 24 | def mock_agent(): 25 | """Fixture to mock a ToolCallingAgent instance.""" 26 | agent_mock = mock.MagicMock() 27 | agent_mock.run.return_value = "Today's date is 2023-01-01 and the capital of France is Paris." 28 | return agent_mock 29 | 30 | 31 | @pytest.fixture 32 | def patch_tool_calling_modules( 33 | mock_minion, mock_config, mock_providers, mock_tool_calling_provider, 34 | mock_smolagents, mock_tool_decorator, mock_agent, monkeypatch 35 | ): 36 | """Fixture to patch all required modules and configure mocks for tool calling.""" 37 | # Set up patches 38 | modules = { 39 | 'minion': mock_minion, 40 | 'minion.config': mock_config, 41 | 'minion.providers': mock_providers, 42 | 'smolagents': mock_smolagents, 43 | 'smolagents.agents': mock.MagicMock(), 44 | 'smolagents.tools': mock.MagicMock(), 45 | } 46 | 47 | for name, mock_obj in modules.items(): 48 | monkeypatch.setitem(sys.modules, name, mock_obj) 49 | 50 | # Configure mocks 51 | mock_providers.create_llm_provider.return_value = mock_tool_calling_provider 52 | mock_smolagents.tools.tool = mock_tool_decorator 53 | mock_smolagents.agents.ToolCallingAgent.return_value = mock_agent 54 | 55 | 56 | def test_tool_registration_and_calling(patch_tool_calling_modules, mock_smolagents, mock_agent): 57 | """Test tool registration and calling with ToolCallingAgent.""" 58 | from smolagents.agents import ToolCallingAgent 59 | from smolagents.tools import tool 60 | 61 | # Create the adapter 62 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 63 | 64 | # Define tools 65 | @tool 66 | def date_tool() -> str: 67 | """Get the current date.""" 68 | return "2023-01-01" 69 | 70 | @tool 71 | def capital_tool(country: str) -> str: 72 | """Get the capital of a country.""" 73 | return "Paris" if country.lower() == "france" else f"Unknown capital for {country}" 74 | 75 | # Create the agent 76 | agent = ToolCallingAgent( 77 | model=adapter, 78 | tools=[date_tool, capital_tool] 79 | ) 80 | 81 | # Test the agent 82 | response = agent.run("What is today's date? Also, what is the capital of France?") 83 | 84 | # Verify we got a proper response 85 | assert response == "Today's date is 2023-01-01 and the capital of France is Paris." 86 | 87 | # Verify the model was called with our query 88 | mock_smolagents.agents.ToolCallingAgent.assert_called_once() 89 | mock_agent.run.assert_called_once_with( 90 | "What is today's date? Also, what is the capital of France?" 91 | ) 92 | 93 | 94 | def test_calculation_tool(patch_tool_calling_modules, mock_smolagents): 95 | """Test the calculation tool functionality.""" 96 | from smolagents.tools import tool 97 | 98 | # Define the calculation tool 99 | @tool 100 | def calculate(expression: str) -> str: 101 | """Calculate the result of a mathematical expression. 102 | 103 | Args: 104 | expression: The mathematical expression to evaluate. 105 | 106 | Returns: 107 | str: The result of the calculation or an error message. 108 | """ 109 | try: 110 | # Use eval safely with only math operations 111 | allowed_names = {"__builtins__": {}} 112 | result = eval(expression, allowed_names) 113 | return str(result) 114 | except Exception as e: 115 | return f"Error calculating: {str(e)}" 116 | 117 | # Test basic calculations 118 | assert calculate("2 + 2") == "4" 119 | assert calculate("3 * 4") == "12" 120 | assert calculate("10 / 2") == "5.0" 121 | assert calculate("2 ** 3") == "8" 122 | 123 | # Test complex calculation 124 | assert calculate("123 * 456 + 789") == "56907" 125 | 126 | # Verify tool was decorated properly 127 | assert hasattr(calculate, "_is_tool") 128 | 129 | 130 | def test_multiple_tools_integration(patch_tool_calling_modules, mock_smolagents, mock_agent): 131 | """Test integration with multiple tools.""" 132 | from smolagents.agents import ToolCallingAgent 133 | from smolagents.tools import tool 134 | 135 | # Create the adapter 136 | adapter = MinionProviderToSmolAdapter.from_model_name("gpt-4o") 137 | 138 | # Define tools 139 | @tool 140 | def date_tool() -> str: 141 | """Get the current date.""" 142 | return "2023-01-01" 143 | 144 | @tool 145 | def capital_tool(country: str) -> str: 146 | """Get the capital of a country.""" 147 | return "Paris" if country.lower() == "france" else f"Unknown capital for {country}" 148 | 149 | @tool 150 | def calculate(expression: str) -> str: 151 | """Calculate the result of a mathematical expression.""" 152 | try: 153 | allowed_names = {"__builtins__": {}} 154 | result = eval(expression, allowed_names) 155 | return str(result) 156 | except Exception as e: 157 | return f"Error calculating: {str(e)}" 158 | 159 | # Configure mock agent to return calculation result 160 | mock_agent.run.return_value = "The result of 123 * 456 + 789 is 56907" 161 | 162 | # Create the agent with all three tools 163 | agent = ToolCallingAgent( 164 | model=adapter, 165 | tools=[date_tool, capital_tool, calculate] 166 | ) 167 | 168 | # Test with calculation 169 | response = agent.run("What is 123 * 456 + 789?") 170 | 171 | # Verify we got a proper response 172 | assert response == "The result of 123 * 456 + 789 is 56907" 173 | 174 | # Verify the model was called with our query 175 | mock_agent.run.assert_called_with("What is 123 * 456 + 789?") -------------------------------------------------------------------------------- /tests/test_tool_functionality.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the tool functions used with SmolaAgents. 6 | 7 | This module tests the actual tool functions as standalone components, 8 | without integration with SmolaAgents or the MinionProviderToSmolAdapter. 9 | """ 10 | 11 | import os 12 | import sys 13 | import pytest 14 | from datetime import datetime 15 | from typing import List, Dict, Any, Optional 16 | 17 | # Add parent directory to path to import from minion_agent 18 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 19 | 20 | 21 | def test_date_tool(): 22 | """Test the date_tool function.""" 23 | def date_tool() -> str: 24 | """Get the current date. 25 | 26 | Returns: 27 | str: The current date in YYYY-MM-DD format. 28 | """ 29 | from datetime import datetime 30 | return datetime.now().strftime("%Y-%m-%d") 31 | 32 | # Check that the function returns a string in the expected format 33 | result = date_tool() 34 | assert isinstance(result, str) 35 | 36 | # Check that the result is in the format YYYY-MM-DD 37 | try: 38 | datetime.strptime(result, "%Y-%m-%d") 39 | except ValueError: 40 | pytest.fail("date_tool did not return a date in the format YYYY-MM-DD") 41 | 42 | 43 | def test_capital_tool(): 44 | """Test the capital_tool function.""" 45 | def capital_tool(country: str) -> str: 46 | """Get the capital of a country. 47 | 48 | Args: 49 | country: The name of the country to look up. 50 | 51 | Returns: 52 | str: The capital city of the specified country. 53 | """ 54 | capitals = { 55 | "usa": "Washington, D.C.", 56 | "france": "Paris", 57 | "japan": "Tokyo", 58 | "australia": "Canberra", 59 | "brazil": "Brasília", 60 | "india": "New Delhi", 61 | } 62 | return capitals.get(country.lower(), f"I don't know the capital of {country}") 63 | 64 | # Test known capitals 65 | assert capital_tool("USA") == "Washington, D.C." 66 | assert capital_tool("France") == "Paris" 67 | assert capital_tool("JAPAN") == "Tokyo" 68 | 69 | # Test case insensitivity 70 | assert capital_tool("france") == "Paris" 71 | assert capital_tool("FRANCE") == "Paris" 72 | 73 | # Test unknown capital 74 | assert capital_tool("Germany") == "I don't know the capital of Germany" 75 | 76 | 77 | def test_calculate_tool(): 78 | """Test the calculate tool function.""" 79 | def calculate(expression: str) -> str: 80 | """Calculate the result of a mathematical expression. 81 | 82 | Args: 83 | expression: The mathematical expression to evaluate. 84 | 85 | Returns: 86 | str: The result of the calculation or an error message. 87 | """ 88 | try: 89 | # Use eval safely with only math operations 90 | # This is just for demonstration purposes 91 | allowed_names = {"__builtins__": {}} 92 | result = eval(expression, allowed_names) 93 | return str(result) 94 | except Exception as e: 95 | return f"Error calculating: {str(e)}" 96 | 97 | # Test basic calculations 98 | assert calculate("2 + 2") == "4" 99 | assert calculate("3 * 4") == "12" 100 | assert calculate("10 / 2") == "5.0" 101 | assert calculate("2 ** 3") == "8" 102 | 103 | # Test complex calculation 104 | assert calculate("123 * 456 + 789") == "56907" 105 | 106 | # Test error handling 107 | assert calculate("1/0").startswith("Error calculating") 108 | assert calculate("invalid").startswith("Error calculating") --------------------------------------------------------------------------------