├── .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 | **[](https://github.com/femto/minion-agent)
2 | [](https://github.com/femto/minion-agent)
3 | [](https://discord.gg/HUC6xEK9aT)
4 | [](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 | 
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 | 
56 |
57 | 
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")
--------------------------------------------------------------------------------