├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── request_new_features.yaml │ └── show_me_the_bug.yaml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build-package.yaml │ ├── environment-corrupt-check.yaml │ ├── pr-autodiff.yaml │ ├── pre-commit.yaml │ ├── stale.yaml │ └── top-issues.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_ja.md ├── README_ko.md ├── README_zh.md ├── app ├── __init__.py ├── agent │ ├── __init__.py │ ├── base.py │ ├── browser.py │ ├── data_analysis.py │ ├── manus.py │ ├── mcp.py │ ├── react.py │ ├── swe.py │ └── toolcall.py ├── bedrock.py ├── config.py ├── exceptions.py ├── flow │ ├── __init__.py │ ├── base.py │ ├── flow_factory.py │ └── planning.py ├── llm.py ├── logger.py ├── mcp │ ├── __init__.py │ └── server.py ├── prompt │ ├── __init__.py │ ├── browser.py │ ├── manus.py │ ├── mcp.py │ ├── planning.py │ ├── swe.py │ ├── toolcall.py │ └── visualization.py ├── sandbox │ ├── __init__.py │ ├── client.py │ └── core │ │ ├── exceptions.py │ │ ├── manager.py │ │ ├── sandbox.py │ │ └── terminal.py ├── schema.py └── tool │ ├── __init__.py │ ├── ask_human.py │ ├── base.py │ ├── bash.py │ ├── browser_use_tool.py │ ├── chart_visualization │ ├── README.md │ ├── README_zh.md │ ├── __init__.py │ ├── chart_prepare.py │ ├── data_visualization.py │ ├── package-lock.json │ ├── package.json │ ├── python_execute.py │ ├── src │ │ └── chartVisualize.ts │ ├── test │ │ ├── chart_demo.py │ │ └── report_demo.py │ └── tsconfig.json │ ├── create_chat_completion.py │ ├── file_operators.py │ ├── mcp.py │ ├── planning.py │ ├── python_execute.py │ ├── search │ ├── __init__.py │ ├── baidu_search.py │ ├── base.py │ ├── bing_search.py │ ├── duckduckgo_search.py │ └── google_search.py │ ├── str_replace_editor.py │ ├── terminate.py │ ├── tool_collection.py │ └── web_search.py ├── assets ├── community_group.jpg └── logo.jpg ├── config ├── .gitignore ├── config.example-model-anthropic.toml ├── config.example-model-azure.toml ├── config.example-model-google.toml ├── config.example-model-ollama.toml ├── config.example-model-ppio.toml ├── config.example.toml └── mcp.example.json ├── examples ├── benchmarks │ └── __init__.py └── use_case │ ├── japan-travel-plan │ ├── japan_travel_guide_instructions.txt │ ├── japan_travel_handbook.html │ ├── japan_travel_handbook_mobile.html │ └── japan_travel_handbook_print.html │ ├── pictures │ ├── japan-travel-plan-1.png │ └── japan-travel-plan-2.png │ └── readme.md ├── main.py ├── requirements.txt ├── run_flow.py ├── run_mcp.py ├── run_mcp_server.py ├── setup.py ├── tests └── sandbox │ ├── test_client.py │ ├── test_docker_terminal.py │ ├── test_sandbox.py │ └── test_sandbox_manager.py └── workspace └── example.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # HTML code is incorrectly calculated into statistics, so ignore them 2 | *.html linguist-detectable=false 3 | # Auto detect text files and perform LF normalization 4 | * text=auto eol=lf 5 | # Ensure shell scripts use LF (Linux style) line endings on Windows 6 | *.sh text eol=lf 7 | # Treat specific binary files as binary and prevent line ending conversion 8 | *.png binary 9 | *.jpg binary 10 | *.gif binary 11 | *.ico binary 12 | *.jpeg binary 13 | *.mp3 binary 14 | *.zip binary 15 | *.bin binary 16 | # Preserve original line endings for specific document files 17 | *.doc text eol=crlf 18 | *.docx text eol=crlf 19 | *.pdf binary 20 | # Ensure source code and script files use LF line endings 21 | *.py text eol=lf 22 | *.js text eol=lf 23 | *.html text eol=lf 24 | *.css text eol=lf 25 | # Specify custom diff driver for specific file types 26 | *.md diff=markdown 27 | *.json diff=json 28 | *.mp4 filter=lfs diff=lfs merge=lfs -text 29 | *.mov filter=lfs diff=lfs merge=lfs -text 30 | *.webm filter=lfs diff=lfs merge=lfs -text 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "Join the Community Group" 4 | about: Join the OpenManus community to discuss and get help from others 5 | url: https://github.com/mannaandpoem/OpenManus?tab=readme-ov-file#community-group 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request_new_features.yaml: -------------------------------------------------------------------------------- 1 | name: "🤔 Request new features" 2 | description: Suggest ideas or features you’d like to see implemented in OpenManus. 3 | labels: enhancement 4 | body: 5 | - type: textarea 6 | id: feature-description 7 | attributes: 8 | label: Feature description 9 | description: | 10 | Provide a clear and concise description of the proposed feature 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: your-feature 15 | attributes: 16 | label: Your Feature 17 | description: | 18 | Explain your idea or implementation process, if any. Optionally, include a Pull Request URL. 19 | Ensure accompanying docs/tests/examples are provided for review. 20 | validations: 21 | required: false 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/show_me_the_bug.yaml: -------------------------------------------------------------------------------- 1 | name: "🪲 Show me the Bug" 2 | description: Report a bug encountered while using OpenManus and seek assistance. 3 | labels: bug 4 | body: 5 | - type: textarea 6 | id: bug-description 7 | attributes: 8 | label: Bug Description 9 | description: | 10 | Clearly describe the bug you encountered 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: solve-method 15 | attributes: 16 | label: Bug solved method 17 | description: | 18 | If resolved, explain the solution. Optionally, include a Pull Request URL. 19 | If unresolved, provide additional details to aid investigation 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: environment-information 24 | attributes: 25 | label: Environment information 26 | description: | 27 | System: e.g., Ubuntu 22.04 28 | Python: e.g., 3.12 29 | OpenManus version: e.g., 0.1.0 30 | value: | 31 | - System version: 32 | - Python version: 33 | - OpenManus version or branch: 34 | - Installation method (e.g., `pip install -r requirements.txt` or `pip install -e .`): 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: extra-information 39 | attributes: 40 | label: Extra information 41 | description: | 42 | For example, attach screenshots or logs to help diagnose the issue 43 | validations: 44 | required: false 45 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Features** 2 | 3 | 4 | - Feature 1 5 | - Feature 2 6 | 7 | **Feature Docs** 8 | 9 | 10 | **Influence** 11 | 12 | 13 | **Result** 14 | 15 | 16 | **Other** 17 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 4 8 | groups: 9 | # Group critical packages that might need careful review 10 | core-dependencies: 11 | patterns: 12 | - "pydantic*" 13 | - "openai" 14 | - "fastapi" 15 | - "tiktoken" 16 | browsergym-related: 17 | patterns: 18 | - "browsergym*" 19 | - "browser-use" 20 | - "playwright" 21 | search-tools: 22 | patterns: 23 | - "googlesearch-python" 24 | - "baidusearch" 25 | - "duckduckgo_search" 26 | pre-commit: 27 | patterns: 28 | - "pre-commit" 29 | security-all: 30 | applies-to: "security-updates" 31 | patterns: 32 | - "*" 33 | version-all: 34 | applies-to: "version-updates" 35 | patterns: 36 | - "*" 37 | exclude-patterns: 38 | - "pydantic*" 39 | - "openai" 40 | - "fastapi" 41 | - "tiktoken" 42 | - "browsergym*" 43 | - "browser-use" 44 | - "playwright" 45 | - "googlesearch-python" 46 | - "baidusearch" 47 | - "duckduckgo_search" 48 | - "pre-commit" 49 | 50 | - package-ecosystem: "github-actions" 51 | directory: "/" 52 | schedule: 53 | interval: "weekly" 54 | open-pull-requests-limit: 4 55 | groups: 56 | actions: 57 | patterns: 58 | - "*" 59 | -------------------------------------------------------------------------------- /.github/workflows/build-package.yaml: -------------------------------------------------------------------------------- 1 | name: Build and upload Python package 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created, published] 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.12' 17 | cache: 'pip' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | pip install setuptools wheel twine 23 | - name: Set package version 24 | run: | 25 | export VERSION="${GITHUB_REF#refs/tags/v}" 26 | sed -i "s/version=.*/version=\"${VERSION}\",/" setup.py 27 | - name: Build and publish 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | run: | 32 | python setup.py bdist_wheel sdist 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /.github/workflows/environment-corrupt-check.yaml: -------------------------------------------------------------------------------- 1 | name: Environment Corruption Check 2 | on: 3 | push: 4 | branches: ["main"] 5 | paths: 6 | - requirements.txt 7 | pull_request: 8 | branches: ["main"] 9 | paths: 10 | - requirements.txt 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | jobs: 15 | test-python-versions: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ["3.11.11", "3.12.8", "3.13.2"] 20 | fail-fast: false 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Upgrade pip 29 | run: | 30 | python -m pip install --upgrade pip 31 | - name: Install dependencies 32 | run: | 33 | pip install -r requirements.txt 34 | -------------------------------------------------------------------------------- /.github/workflows/pr-autodiff.yaml: -------------------------------------------------------------------------------- 1 | name: PR Diff Summarization 2 | on: 3 | # pull_request: 4 | # branches: [main] 5 | # types: [opened, ready_for_review, reopened] 6 | issue_comment: 7 | types: [created] 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | jobs: 12 | pr-diff-summarization: 13 | runs-on: ubuntu-latest 14 | if: | 15 | (github.event_name == 'pull_request') || 16 | (github.event_name == 'issue_comment' && 17 | contains(github.event.comment.body, '!pr-diff') && 18 | (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && 19 | github.event.issue.pull_request) 20 | steps: 21 | - name: Get PR head SHA 22 | id: get-pr-sha 23 | run: | 24 | PR_URL="${{ github.event.issue.pull_request.url || github.event.pull_request.url }}" 25 | # https://api.github.com/repos/OpenManus/pulls/1 26 | RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL) 27 | SHA=$(echo $RESPONSE | jq -r '.head.sha') 28 | TARGET_BRANCH=$(echo $RESPONSE | jq -r '.base.ref') 29 | echo "pr_sha=$SHA" >> $GITHUB_OUTPUT 30 | echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT 31 | echo "Retrieved PR head SHA from API: $SHA, target branch: $TARGET_BRANCH" 32 | - name: Check out code 33 | uses: actions/checkout@v4 34 | with: 35 | ref: ${{ steps.get-pr-sha.outputs.pr_sha }} 36 | fetch-depth: 0 37 | - name: Set up Python 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: '3.11' 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | pip install openai requests 45 | - name: Create and run Python script 46 | env: 47 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 48 | OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} 49 | GH_TOKEN: ${{ github.token }} 50 | PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} 51 | TARGET_BRANCH: ${{ steps.get-pr-sha.outputs.target_branch }} 52 | run: |- 53 | cat << 'EOF' > /tmp/_workflow_core.py 54 | import os 55 | import subprocess 56 | import json 57 | import requests 58 | from openai import OpenAI 59 | 60 | def get_diff(): 61 | result = subprocess.run( 62 | ['git', 'diff', 'origin/' + os.getenv('TARGET_BRANCH') + '...HEAD'], 63 | capture_output=True, text=True, check=True) 64 | return '\n'.join( 65 | line for line in result.stdout.split('\n') 66 | if any(line.startswith(c) for c in ('+', '-')) 67 | and not line.startswith(('---', '+++')) 68 | )[:round(200000 * 0.4)] # Truncate to prevent overflow 69 | 70 | def generate_comment(diff_content): 71 | client = OpenAI( 72 | base_url=os.getenv("OPENAI_BASE_URL"), 73 | api_key=os.getenv("OPENAI_API_KEY") 74 | ) 75 | 76 | guidelines = ''' 77 | 1. English version first, Chinese Simplified version after 78 | 2. Example format: 79 | # Diff Report 80 | ## English 81 | - Added `ABC` class 82 | - Fixed `f()` behavior in `foo` module 83 | 84 | ### Comments Highlight 85 | - `config.toml` needs to be configured properly to make sure new features work as expected. 86 | 87 | ### Spelling/Offensive Content Check 88 | - No spelling mistakes or offensive content found in the code or comments. 89 | 90 | ## 中文(简体) 91 | - 新增了 `ABC` 类 92 | - `foo` 模块中的 `f()` 行为已修复 93 | 94 | ### 评论高亮 95 | - `config.toml` 需要正确配置才能确保新功能正常运行。 96 | 97 | ### 内容检查 98 | - 没有发现代码或注释中的拼写错误或不当措辞。 99 | 100 | 3. Highlight non-English comments 101 | 4. Check for spelling/offensive content''' 102 | 103 | response = client.chat.completions.create( 104 | model="o3-mini", 105 | messages=[{ 106 | "role": "system", 107 | "content": "Generate bilingual code review feedback." 108 | }, { 109 | "role": "user", 110 | "content": f"Review these changes per guidelines:\n{guidelines}\n\nDIFF:\n{diff_content}" 111 | }] 112 | ) 113 | return response.choices[0].message.content 114 | 115 | def post_comment(comment): 116 | repo = os.getenv("GITHUB_REPOSITORY") 117 | pr_number = os.getenv("PR_NUMBER") 118 | 119 | headers = { 120 | "Authorization": f"Bearer {os.getenv('GH_TOKEN')}", 121 | "Accept": "application/vnd.github.v3+json" 122 | } 123 | url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" 124 | 125 | requests.post(url, json={"body": comment}, headers=headers) 126 | 127 | if __name__ == "__main__": 128 | diff_content = get_diff() 129 | if not diff_content.strip(): 130 | print("No meaningful diff detected.") 131 | exit(0) 132 | 133 | comment = generate_comment(diff_content) 134 | post_comment(comment) 135 | print("Comment posted successfully.") 136 | EOF 137 | 138 | python /tmp/_workflow_core.py 139 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Pre-commit checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '**' 7 | push: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | pre-commit-check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Source Code 16 | uses: actions/checkout@v4 17 | - name: Set up Python 3.12 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.12' 21 | - name: Install pre-commit and tools 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install pre-commit black==23.1.0 isort==5.12.0 autoflake==2.0.1 25 | - name: Run pre-commit hooks 26 | run: pre-commit run --all-files 27 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | 3 | on: 4 | schedule: 5 | - cron: "5 0 * * *" 6 | 7 | jobs: 8 | close-issues: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | days-before-issue-stale: 30 17 | days-before-issue-close: 14 18 | stale-issue-label: "inactive" 19 | stale-issue-message: "This issue has been inactive for 30 days. Please comment if you have updates." 20 | close-issue-message: "This issue was closed due to 45 days of inactivity. Reopen if still relevant." 21 | days-before-pr-stale: -1 22 | days-before-pr-close: -1 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/top-issues.yaml: -------------------------------------------------------------------------------- 1 | name: Top issues 2 | on: 3 | schedule: 4 | - cron: '0 0/2 * * *' 5 | workflow_dispatch: 6 | jobs: 7 | ShowAndLabelTopIssues: 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | actions: read 12 | contents: read 13 | name: Display and label top issues 14 | runs-on: ubuntu-latest 15 | if: github.repository == 'mannaandpoem/OpenManus' 16 | steps: 17 | - name: Run top issues action 18 | uses: rickstaa/top-issues-action@7e8dda5d5ae3087670f9094b9724a9a091fc3ba1 # v1.3.101 19 | env: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | label: true 23 | dashboard: true 24 | dashboard_show_total_reactions: true 25 | top_issues: true 26 | top_features: true 27 | top_bugs: true 28 | top_pull_requests: true 29 | top_list_size: 14 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Project-specific ### 2 | # Logs 3 | logs/ 4 | 5 | # Data 6 | data/ 7 | 8 | # Workspace 9 | workspace/ 10 | 11 | ### Python ### 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | cover/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | .pybuilder/ 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | # For a library or package, you might want to ignore these files since the code is 98 | # intended to run in multiple environments; otherwise, check them in: 99 | # .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # UV 109 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 110 | # This is especially recommended for binary packages to ensure reproducibility, and is more 111 | # commonly ignored for libraries. 112 | #uv.lock 113 | 114 | # poetry 115 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 116 | # This is especially recommended for binary packages to ensure reproducibility, and is more 117 | # commonly ignored for libraries. 118 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 119 | #poetry.lock 120 | 121 | # pdm 122 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 123 | #pdm.lock 124 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 125 | # in version control. 126 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 127 | .pdm.toml 128 | .pdm-python 129 | .pdm-build/ 130 | 131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 132 | __pypackages__/ 133 | 134 | # Celery stuff 135 | celerybeat-schedule 136 | celerybeat.pid 137 | 138 | # SageMath parsed files 139 | *.sage.py 140 | 141 | # Environments 142 | .env 143 | .venv 144 | env/ 145 | venv/ 146 | ENV/ 147 | env.bak/ 148 | venv.bak/ 149 | 150 | # Spyder project settings 151 | .spyderproject 152 | .spyproject 153 | 154 | # Rope project settings 155 | .ropeproject 156 | 157 | # mkdocs documentation 158 | /site 159 | 160 | # mypy 161 | .mypy_cache/ 162 | .dmypy.json 163 | dmypy.json 164 | 165 | # Pyre type checker 166 | .pyre/ 167 | 168 | # pytype static type analyzer 169 | .pytype/ 170 | 171 | # Cython debug symbols 172 | cython_debug/ 173 | 174 | # PyCharm 175 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 176 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 177 | # and can be added to the global gitignore or merged into this file. For a more nuclear 178 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 179 | .idea/ 180 | 181 | # PyPI configuration file 182 | .pypirc 183 | 184 | ### Visual Studio Code ### 185 | .vscode/* 186 | !.vscode/settings.json 187 | !.vscode/tasks.json 188 | !.vscode/launch.json 189 | !.vscode/extensions.json 190 | !.vscode/*.code-snippets 191 | 192 | # Local History for Visual Studio Code 193 | .history/ 194 | 195 | # Built Visual Studio Code Extensions 196 | *.vsix 197 | 198 | # OSX 199 | .DS_Store 200 | 201 | # node 202 | node_modules 203 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.1.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.4.0 9 | hooks: 10 | - id: trailing-whitespace 11 | - id: end-of-file-fixer 12 | - id: check-yaml 13 | - id: check-added-large-files 14 | 15 | - repo: https://github.com/PyCQA/autoflake 16 | rev: v2.0.1 17 | hooks: 18 | - id: autoflake 19 | args: [ 20 | --remove-all-unused-imports, 21 | --ignore-init-module-imports, 22 | --expand-star-imports, 23 | --remove-duplicate-keys, 24 | --remove-unused-variables, 25 | --recursive, 26 | --in-place, 27 | --exclude=__init__.py, 28 | ] 29 | files: \.py$ 30 | 31 | - repo: https://github.com/pycqa/isort 32 | rev: 5.12.0 33 | hooks: 34 | - id: isort 35 | args: [ 36 | "--profile", "black", 37 | "--filter-files", 38 | "--lines-after-imports=2", 39 | ] 40 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "tamasfe.even-better-toml", 4 | "ms-python.black-formatter", 5 | "ms-python.isort" 6 | ], 7 | "unwantedRecommendations": [] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter", 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "always" 6 | } 7 | }, 8 | "[toml]": { 9 | "editor.defaultFormatter": "tamasfe.even-better-toml", 10 | }, 11 | "pre-commit-helper.runOnSave": "none", 12 | "pre-commit-helper.config": ".pre-commit-config.yaml", 13 | "evenBetterToml.schema.enabled": true, 14 | "evenBetterToml.schema.associations": { 15 | "^.+config[/\\\\].+\\.toml$": "../config/schema.config.json" 16 | }, 17 | "files.insertFinalNewline": true, 18 | "files.trimTrailingWhitespace": true, 19 | "editor.formatOnSave": true 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | 3 | WORKDIR /app/OpenManus 4 | 5 | RUN apt-get update && apt-get install -y --no-install-recommends git curl \ 6 | && rm -rf /var/lib/apt/lists/* \ 7 | && (command -v uv >/dev/null 2>&1 || pip install --no-cache-dir uv) 8 | 9 | COPY . . 10 | 11 | RUN uv pip install --system -r requirements.txt 12 | 13 | CMD ["bash"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 manna_and_poem 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. 22 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [English](README.md) | [中文](README_zh.md) | [한국어](README_ko.md) | 日本語 6 | 7 | [![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers) 8 |   9 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   10 | [![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) 11 | [![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) 12 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) 13 | 14 | # 👋 OpenManus 15 | 16 | Manusは素晴らしいですが、OpenManusは*招待コード*なしでどんなアイデアも実現できます!🛫 17 | 18 | 私たちのチームメンバー [@Xinbin Liang](https://github.com/mannaandpoem) と [@Jinyu Xiang](https://github.com/XiangJinyu)(主要開発者)、そして [@Zhaoyang Yu](https://github.com/MoshiQAQ)、[@Jiayi Zhang](https://github.com/didiforgithub)、[@Sirui Hong](https://github.com/stellaHSR) は [@MetaGPT](https://github.com/geekan/MetaGPT) から来ました。プロトタイプは3時間以内に立ち上げられ、継続的に開発を進めています! 19 | 20 | これはシンプルな実装ですので、どんな提案、貢献、フィードバックも歓迎します! 21 | 22 | OpenManusで自分だけのエージェントを楽しみましょう! 23 | 24 | また、UIUCとOpenManusの研究者が共同開発した[OpenManus-RL](https://github.com/OpenManus/OpenManus-RL)をご紹介できることを嬉しく思います。これは強化学習(RL)ベース(GRPOなど)のLLMエージェントチューニング手法に特化したオープンソースプロジェクトです。 25 | 26 | ## プロジェクトデモ 27 | 28 | 29 | 30 | ## インストール方法 31 | 32 | インストール方法は2つ提供しています。方法2(uvを使用)は、より高速なインストールと優れた依存関係管理のため推奨されています。 33 | 34 | ### 方法1:condaを使用 35 | 36 | 1. 新しいconda環境を作成します: 37 | 38 | ```bash 39 | conda create -n open_manus python=3.12 40 | conda activate open_manus 41 | ``` 42 | 43 | 2. リポジトリをクローンします: 44 | 45 | ```bash 46 | git clone https://github.com/mannaandpoem/OpenManus.git 47 | cd OpenManus 48 | ``` 49 | 50 | 3. 依存関係をインストールします: 51 | 52 | ```bash 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | ### 方法2:uvを使用(推奨) 57 | 58 | 1. uv(高速なPythonパッケージインストーラーと管理機能)をインストールします: 59 | 60 | ```bash 61 | curl -LsSf https://astral.sh/uv/install.sh | sh 62 | ``` 63 | 64 | 2. リポジトリをクローンします: 65 | 66 | ```bash 67 | git clone https://github.com/mannaandpoem/OpenManus.git 68 | cd OpenManus 69 | ``` 70 | 71 | 3. 新しい仮想環境を作成してアクティベートします: 72 | 73 | ```bash 74 | uv venv --python 3.12 75 | source .venv/bin/activate # Unix/macOSの場合 76 | # Windowsの場合: 77 | # .venv\Scripts\activate 78 | ``` 79 | 80 | 4. 依存関係をインストールします: 81 | 82 | ```bash 83 | uv pip install -r requirements.txt 84 | ``` 85 | 86 | ### ブラウザ自動化ツール(オプション) 87 | ```bash 88 | playwright install 89 | ``` 90 | 91 | ## 設定 92 | 93 | OpenManusを使用するには、LLM APIの設定が必要です。以下の手順に従って設定してください: 94 | 95 | 1. `config`ディレクトリに`config.toml`ファイルを作成します(サンプルからコピーできます): 96 | 97 | ```bash 98 | cp config/config.example.toml config/config.toml 99 | ``` 100 | 101 | 2. `config/config.toml`を編集してAPIキーを追加し、設定をカスタマイズします: 102 | 103 | ```toml 104 | # グローバルLLM設定 105 | [llm] 106 | model = "gpt-4o" 107 | base_url = "https://api.openai.com/v1" 108 | api_key = "sk-..." # 実際のAPIキーに置き換えてください 109 | max_tokens = 4096 110 | temperature = 0.0 111 | 112 | # 特定のLLMモデル用のオプション設定 113 | [llm.vision] 114 | model = "gpt-4o" 115 | base_url = "https://api.openai.com/v1" 116 | api_key = "sk-..." # 実際のAPIキーに置き換えてください 117 | ``` 118 | 119 | ## クイックスタート 120 | 121 | OpenManusを実行する一行コマンド: 122 | 123 | ```bash 124 | python main.py 125 | ``` 126 | 127 | その後、ターミナルからプロンプトを入力してください! 128 | 129 | MCP ツールバージョンを使用する場合は、以下を実行します: 130 | ```bash 131 | python run_mcp.py 132 | ``` 133 | 134 | 開発中のマルチエージェントバージョンを試すには、以下を実行します: 135 | 136 | ```bash 137 | python run_flow.py 138 | ``` 139 | 140 | ## 貢献方法 141 | 142 | 我々は建設的な意見や有益な貢献を歓迎します!issueを作成するか、プルリクエストを提出してください。 143 | 144 | または @mannaandpoem に📧メールでご連絡ください:mannaandpoem@gmail.com 145 | 146 | **注意**: プルリクエストを送信する前に、pre-commitツールを使用して変更を確認してください。`pre-commit run --all-files`を実行してチェックを実行します。 147 | 148 | ## コミュニティグループ 149 | Feishuのネットワーキンググループに参加して、他の開発者と経験を共有しましょう! 150 | 151 |
152 | OpenManus 交流群 153 |
154 | 155 | ## スター履歴 156 | 157 | [![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date) 158 | 159 | ## 謝辞 160 | 161 | このプロジェクトの基本的なサポートを提供してくれた[anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) 162 | と[browser-use](https://github.com/browser-use/browser-use)に感謝します! 163 | 164 | さらに、[AAAJ](https://github.com/metauto-ai/agent-as-a-judge)、[MetaGPT](https://github.com/geekan/MetaGPT)、[OpenHands](https://github.com/All-Hands-AI/OpenHands)、[SWE-agent](https://github.com/SWE-agent/SWE-agent)にも感謝します。 165 | 166 | また、Hugging Face デモスペースをサポートしてくださった阶跃星辰 (stepfun)にも感謝いたします。 167 | 168 | OpenManusはMetaGPTのコントリビューターによって構築されました。このエージェントコミュニティに大きな感謝を! 169 | 170 | ## 引用 171 | ```bibtex 172 | @misc{openmanus2025, 173 | author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, 174 | title = {OpenManus: An open-source framework for building general AI agents}, 175 | year = {2025}, 176 | publisher = {Zenodo}, 177 | doi = {10.5281/zenodo.15186407}, 178 | url = {https://doi.org/10.5281/zenodo.15186407}, 179 | } 180 | ``` 181 | -------------------------------------------------------------------------------- /README_ko.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [English](README.md) | [中文](README_zh.md) | 한국어 | [日本語](README_ja.md) 6 | 7 | [![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers) 8 |   9 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   10 | [![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) 11 | [![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) 12 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) 13 | 14 | # 👋 OpenManus 15 | 16 | Manus는 놀라운 도구지만, OpenManus는 *초대 코드* 없이도 모든 아이디어를 실현할 수 있습니다! 🛫 17 | 18 | 우리 팀의 멤버인 [@Xinbin Liang](https://github.com/mannaandpoem)와 [@Jinyu Xiang](https://github.com/XiangJinyu) (핵심 작성자), 그리고 [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), [@Sirui Hong](https://github.com/stellaHSR)이 함께 했습니다. 우리는 [@MetaGPT](https://github.com/geekan/MetaGPT)로부터 왔습니다. 프로토타입은 단 3시간 만에 출시되었으며, 계속해서 발전하고 있습니다! 19 | 20 | 이 프로젝트는 간단한 구현에서 시작되었으며, 여러분의 제안, 기여 및 피드백을 환영합니다! 21 | 22 | OpenManus를 통해 여러분만의 에이전트를 즐겨보세요! 23 | 24 | 또한 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL)을 소개하게 되어 기쁩니다. OpenManus와 UIUC 연구자들이 공동 개발한 이 오픈소스 프로젝트는 LLM 에이전트에 대해 강화 학습(RL) 기반 (예: GRPO) 튜닝 방법을 제공합니다. 25 | 26 | ## 프로젝트 데모 27 | 28 | 29 | 30 | ## 설치 방법 31 | 32 | 두 가지 설치 방법을 제공합니다. **방법 2 (uv 사용)** 이 더 빠른 설치와 효율적인 종속성 관리를 위해 권장됩니다. 33 | 34 | ### 방법 1: conda 사용 35 | 36 | 1. 새로운 conda 환경을 생성합니다: 37 | 38 | ```bash 39 | conda create -n open_manus python=3.12 40 | conda activate open_manus 41 | ``` 42 | 43 | 2. 저장소를 클론합니다: 44 | 45 | ```bash 46 | git clone https://github.com/mannaandpoem/OpenManus.git 47 | cd OpenManus 48 | ``` 49 | 50 | 3. 종속성을 설치합니다: 51 | 52 | ```bash 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | ### 방법 2: uv 사용 (권장) 57 | 58 | 1. uv를 설치합니다. (빠른 Python 패키지 설치 및 종속성 관리 도구): 59 | 60 | ```bash 61 | curl -LsSf https://astral.sh/uv/install.sh | sh 62 | ``` 63 | 64 | 2. 저장소를 클론합니다: 65 | 66 | ```bash 67 | git clone https://github.com/mannaandpoem/OpenManus.git 68 | cd OpenManus 69 | ``` 70 | 71 | 3. 새로운 가상 환경을 생성하고 활성화합니다: 72 | 73 | ```bash 74 | uv venv --python 3.12 75 | source .venv/bin/activate # Unix/macOS의 경우 76 | # Windows의 경우: 77 | # .venv\Scripts\activate 78 | ``` 79 | 80 | 4. 종속성을 설치합니다: 81 | 82 | ```bash 83 | uv pip install -r requirements.txt 84 | ``` 85 | 86 | ### 브라우저 자동화 도구 (선택사항) 87 | ```bash 88 | playwright install 89 | ``` 90 | 91 | ## 설정 방법 92 | 93 | OpenManus를 사용하려면 사용하는 LLM API에 대한 설정이 필요합니다. 아래 단계를 따라 설정을 완료하세요: 94 | 95 | 1. `config` 디렉토리에 `config.toml` 파일을 생성하세요 (예제 파일을 복사하여 사용할 수 있습니다): 96 | 97 | ```bash 98 | cp config/config.example.toml config/config.toml 99 | ``` 100 | 101 | 2. `config/config.toml` 파일을 편집하여 API 키를 추가하고 설정을 커스터마이징하세요: 102 | 103 | ```toml 104 | # 전역 LLM 설정 105 | [llm] 106 | model = "gpt-4o" 107 | base_url = "https://api.openai.com/v1" 108 | api_key = "sk-..." # 실제 API 키로 변경하세요 109 | max_tokens = 4096 110 | temperature = 0.0 111 | 112 | # 특정 LLM 모델에 대한 선택적 설정 113 | [llm.vision] 114 | model = "gpt-4o" 115 | base_url = "https://api.openai.com/v1" 116 | api_key = "sk-..." # 실제 API 키로 변경하세요 117 | ``` 118 | 119 | ## 빠른 시작 120 | 121 | OpenManus를 실행하는 한 줄 명령어: 122 | 123 | ```bash 124 | python main.py 125 | ``` 126 | 127 | 이후 터미널에서 아이디어를 작성하세요! 128 | 129 | MCP 도구 버전을 사용하려면 다음을 실행하세요: 130 | ```bash 131 | python run_mcp.py 132 | ``` 133 | 134 | 불안정한 멀티 에이전트 버전을 실행하려면 다음을 실행할 수 있습니다: 135 | 136 | ```bash 137 | python run_flow.py 138 | ``` 139 | 140 | ## 기여 방법 141 | 142 | 모든 친절한 제안과 유용한 기여를 환영합니다! 이슈를 생성하거나 풀 리퀘스트를 제출해 주세요. 143 | 144 | 또는 📧 메일로 연락주세요. @mannaandpoem : mannaandpoem@gmail.com 145 | 146 | **참고**: pull request를 제출하기 전에 pre-commit 도구를 사용하여 변경 사항을 확인하십시오. `pre-commit run --all-files`를 실행하여 검사를 실행합니다. 147 | 148 | ## 커뮤니티 그룹 149 | Feishu 네트워킹 그룹에 참여하여 다른 개발자들과 경험을 공유하세요! 150 | 151 |
152 | OpenManus 交流群 153 |
154 | 155 | ## Star History 156 | 157 | [![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date) 158 | 159 | ## 감사의 글 160 | 161 | 이 프로젝트에 기본적인 지원을 제공해 주신 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)와 162 | [browser-use](https://github.com/browser-use/browser-use)에게 감사드립니다! 163 | 164 | 또한, [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands), [SWE-agent](https://github.com/SWE-agent/SWE-agent)에 깊은 감사를 드립니다. 165 | 166 | 또한 Hugging Face 데모 공간을 지원해 주신 阶跃星辰 (stepfun)에게 감사드립니다. 167 | 168 | OpenManus는 MetaGPT 기여자들에 의해 개발되었습니다. 이 에이전트 커뮤니티에 깊은 감사를 전합니다! 169 | 170 | ## 인용 171 | ```bibtex 172 | @misc{openmanus2025, 173 | author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, 174 | title = {OpenManus: An open-source framework for building general AI agents}, 175 | year = {2025}, 176 | publisher = {Zenodo}, 177 | doi = {10.5281/zenodo.15186407}, 178 | url = {https://doi.org/10.5281/zenodo.15186407}, 179 | } 180 | ``` 181 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [English](README.md) | 中文 | [한국어](README_ko.md) | [日本語](README_ja.md) 6 | 7 | [![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers) 8 |   9 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   10 | [![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) 11 | [![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) 12 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) 13 | 14 | # 👋 OpenManus 15 | 16 | Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫! 17 | 18 | 我们的团队成员 [@Xinbin Liang](https://github.com/mannaandpoem) 和 [@Jinyu Xiang](https://github.com/XiangJinyu)(核心作者),以及 [@Zhaoyang Yu](https://github.com/MoshiQAQ)、[@Jiayi Zhang](https://github.com/didiforgithub) 和 [@Sirui Hong](https://github.com/stellaHSR),来自 [@MetaGPT](https://github.com/geekan/MetaGPT)团队。我们在 3 19 | 小时内完成了开发并持续迭代中! 20 | 21 | 这是一个简洁的实现方案,欢迎任何建议、贡献和反馈! 22 | 23 | 用 OpenManus 开启你的智能体之旅吧! 24 | 25 | 我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。 26 | 27 | ## 项目演示 28 | 29 | 30 | 31 | ## 安装指南 32 | 33 | 我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。 34 | 35 | ### 方式一:使用 conda 36 | 37 | 1. 创建新的 conda 环境: 38 | 39 | ```bash 40 | conda create -n open_manus python=3.12 41 | conda activate open_manus 42 | ``` 43 | 44 | 2. 克隆仓库: 45 | 46 | ```bash 47 | git clone https://github.com/mannaandpoem/OpenManus.git 48 | cd OpenManus 49 | ``` 50 | 51 | 3. 安装依赖: 52 | 53 | ```bash 54 | pip install -r requirements.txt 55 | ``` 56 | 57 | ### 方式二:使用 uv(推荐) 58 | 59 | 1. 安装 uv(一个快速的 Python 包管理器): 60 | 61 | ```bash 62 | curl -LsSf https://astral.sh/uv/install.sh | sh 63 | ``` 64 | 65 | 2. 克隆仓库: 66 | 67 | ```bash 68 | git clone https://github.com/mannaandpoem/OpenManus.git 69 | cd OpenManus 70 | ``` 71 | 72 | 3. 创建并激活虚拟环境: 73 | 74 | ```bash 75 | uv venv --python 3.12 76 | source .venv/bin/activate # Unix/macOS 系统 77 | # Windows 系统使用: 78 | # .venv\Scripts\activate 79 | ``` 80 | 81 | 4. 安装依赖: 82 | 83 | ```bash 84 | uv pip install -r requirements.txt 85 | ``` 86 | 87 | ### 浏览器自动化工具(可选) 88 | ```bash 89 | playwright install 90 | ``` 91 | 92 | ## 配置说明 93 | 94 | OpenManus 需要配置使用的 LLM API,请按以下步骤设置: 95 | 96 | 1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制): 97 | 98 | ```bash 99 | cp config/config.example.toml config/config.toml 100 | ``` 101 | 102 | 2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置: 103 | 104 | ```toml 105 | # 全局 LLM 配置 106 | [llm] 107 | model = "gpt-4o" 108 | base_url = "https://api.openai.com/v1" 109 | api_key = "sk-..." # 替换为真实 API 密钥 110 | max_tokens = 4096 111 | temperature = 0.0 112 | 113 | # 可选特定 LLM 模型配置 114 | [llm.vision] 115 | model = "gpt-4o" 116 | base_url = "https://api.openai.com/v1" 117 | api_key = "sk-..." # 替换为真实 API 密钥 118 | ``` 119 | 120 | ## 快速启动 121 | 122 | 一行命令运行 OpenManus: 123 | 124 | ```bash 125 | python main.py 126 | ``` 127 | 128 | 然后通过终端输入你的创意! 129 | 130 | 如需使用 MCP 工具版本,可运行: 131 | ```bash 132 | python run_mcp.py 133 | ``` 134 | 135 | 如需体验不稳定的多智能体版本,可运行: 136 | 137 | ```bash 138 | python run_flow.py 139 | ``` 140 | 141 | ## 贡献指南 142 | 143 | 我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。 144 | 145 | 或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com 146 | 147 | **注意**: 在提交 pull request 之前,请使用 pre-commit 工具检查您的更改。运行 `pre-commit run --all-files` 来执行检查。 148 | 149 | ## 交流群 150 | 151 | 加入我们的飞书交流群,与其他开发者分享经验! 152 | 153 |
154 | OpenManus 交流群 155 |
156 | 157 | ## Star 数量 158 | 159 | [![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date) 160 | 161 | 162 | ## 赞助商 163 | 感谢[PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) 提供的算力支持。 164 | > PPIO派欧云:一键调用高性价比的开源模型API和GPU容器 165 | 166 | ## 致谢 167 | 168 | 特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) 169 | 和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持! 170 | 171 | 此外,我们感谢 [AAAJ](https://github.com/metauto-ai/agent-as-a-judge),[MetaGPT](https://github.com/geekan/MetaGPT),[OpenHands](https://github.com/All-Hands-AI/OpenHands) 和 [SWE-agent](https://github.com/SWE-agent/SWE-agent). 172 | 173 | 我们也感谢阶跃星辰 (stepfun) 提供的 Hugging Face 演示空间支持。 174 | 175 | OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区! 176 | 177 | ## 引用 178 | ```bibtex 179 | @misc{openmanus2025, 180 | author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, 181 | title = {OpenManus: An open-source framework for building general AI agents}, 182 | year = {2025}, 183 | publisher = {Zenodo}, 184 | doi = {10.5281/zenodo.15186407}, 185 | url = {https://doi.org/10.5281/zenodo.15186407}, 186 | } 187 | ``` 188 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # Python version check: 3.11-3.13 2 | import sys 3 | 4 | 5 | if sys.version_info < (3, 11) or sys.version_info > (3, 13): 6 | print( 7 | "Warning: Unsupported Python version {ver}, please use 3.11-3.13".format( 8 | ver=".".join(map(str, sys.version_info)) 9 | ) 10 | ) 11 | -------------------------------------------------------------------------------- /app/agent/__init__.py: -------------------------------------------------------------------------------- 1 | from app.agent.base import BaseAgent 2 | from app.agent.browser import BrowserAgent 3 | from app.agent.mcp import MCPAgent 4 | from app.agent.react import ReActAgent 5 | from app.agent.swe import SWEAgent 6 | from app.agent.toolcall import ToolCallAgent 7 | 8 | 9 | __all__ = [ 10 | "BaseAgent", 11 | "BrowserAgent", 12 | "ReActAgent", 13 | "SWEAgent", 14 | "ToolCallAgent", 15 | "MCPAgent", 16 | ] 17 | -------------------------------------------------------------------------------- /app/agent/browser.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING, Optional 3 | 4 | from pydantic import Field, model_validator 5 | 6 | from app.agent.toolcall import ToolCallAgent 7 | from app.logger import logger 8 | from app.prompt.browser import NEXT_STEP_PROMPT, SYSTEM_PROMPT 9 | from app.schema import Message, ToolChoice 10 | from app.tool import BrowserUseTool, Terminate, ToolCollection 11 | 12 | 13 | # Avoid circular import if BrowserAgent needs BrowserContextHelper 14 | if TYPE_CHECKING: 15 | from app.agent.base import BaseAgent # Or wherever memory is defined 16 | 17 | 18 | class BrowserContextHelper: 19 | def __init__(self, agent: "BaseAgent"): 20 | self.agent = agent 21 | self._current_base64_image: Optional[str] = None 22 | 23 | async def get_browser_state(self) -> Optional[dict]: 24 | browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name) 25 | if not browser_tool or not hasattr(browser_tool, "get_current_state"): 26 | logger.warning("BrowserUseTool not found or doesn't have get_current_state") 27 | return None 28 | try: 29 | result = await browser_tool.get_current_state() 30 | if result.error: 31 | logger.debug(f"Browser state error: {result.error}") 32 | return None 33 | if hasattr(result, "base64_image") and result.base64_image: 34 | self._current_base64_image = result.base64_image 35 | else: 36 | self._current_base64_image = None 37 | return json.loads(result.output) 38 | except Exception as e: 39 | logger.debug(f"Failed to get browser state: {str(e)}") 40 | return None 41 | 42 | async def format_next_step_prompt(self) -> str: 43 | """Gets browser state and formats the browser prompt.""" 44 | browser_state = await self.get_browser_state() 45 | url_info, tabs_info, content_above_info, content_below_info = "", "", "", "" 46 | results_info = "" # Or get from agent if needed elsewhere 47 | 48 | if browser_state and not browser_state.get("error"): 49 | url_info = f"\n URL: {browser_state.get('url', 'N/A')}\n Title: {browser_state.get('title', 'N/A')}" 50 | tabs = browser_state.get("tabs", []) 51 | if tabs: 52 | tabs_info = f"\n {len(tabs)} tab(s) available" 53 | pixels_above = browser_state.get("pixels_above", 0) 54 | pixels_below = browser_state.get("pixels_below", 0) 55 | if pixels_above > 0: 56 | content_above_info = f" ({pixels_above} pixels)" 57 | if pixels_below > 0: 58 | content_below_info = f" ({pixels_below} pixels)" 59 | 60 | if self._current_base64_image: 61 | image_message = Message.user_message( 62 | content="Current browser screenshot:", 63 | base64_image=self._current_base64_image, 64 | ) 65 | self.agent.memory.add_message(image_message) 66 | self._current_base64_image = None # Consume the image after adding 67 | 68 | return NEXT_STEP_PROMPT.format( 69 | url_placeholder=url_info, 70 | tabs_placeholder=tabs_info, 71 | content_above_placeholder=content_above_info, 72 | content_below_placeholder=content_below_info, 73 | results_placeholder=results_info, 74 | ) 75 | 76 | async def cleanup_browser(self): 77 | browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name) 78 | if browser_tool and hasattr(browser_tool, "cleanup"): 79 | await browser_tool.cleanup() 80 | 81 | 82 | class BrowserAgent(ToolCallAgent): 83 | """ 84 | A browser agent that uses the browser_use library to control a browser. 85 | 86 | This agent can navigate web pages, interact with elements, fill forms, 87 | extract content, and perform other browser-based actions to accomplish tasks. 88 | """ 89 | 90 | name: str = "browser" 91 | description: str = "A browser agent that can control a browser to accomplish tasks" 92 | 93 | system_prompt: str = SYSTEM_PROMPT 94 | next_step_prompt: str = NEXT_STEP_PROMPT 95 | 96 | max_observe: int = 10000 97 | max_steps: int = 20 98 | 99 | # Configure the available tools 100 | available_tools: ToolCollection = Field( 101 | default_factory=lambda: ToolCollection(BrowserUseTool(), Terminate()) 102 | ) 103 | 104 | # Use Auto for tool choice to allow both tool usage and free-form responses 105 | tool_choices: ToolChoice = ToolChoice.AUTO 106 | special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name]) 107 | 108 | browser_context_helper: Optional[BrowserContextHelper] = None 109 | 110 | @model_validator(mode="after") 111 | def initialize_helper(self) -> "BrowserAgent": 112 | self.browser_context_helper = BrowserContextHelper(self) 113 | return self 114 | 115 | async def think(self) -> bool: 116 | """Process current state and decide next actions using tools, with browser state info added""" 117 | self.next_step_prompt = ( 118 | await self.browser_context_helper.format_next_step_prompt() 119 | ) 120 | return await super().think() 121 | 122 | async def cleanup(self): 123 | """Clean up browser agent resources by calling parent cleanup.""" 124 | await self.browser_context_helper.cleanup_browser() 125 | -------------------------------------------------------------------------------- /app/agent/data_analysis.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from app.agent.toolcall import ToolCallAgent 4 | from app.config import config 5 | from app.prompt.visualization import NEXT_STEP_PROMPT, SYSTEM_PROMPT 6 | from app.tool import Terminate, ToolCollection 7 | from app.tool.chart_visualization.chart_prepare import VisualizationPrepare 8 | from app.tool.chart_visualization.data_visualization import DataVisualization 9 | from app.tool.chart_visualization.python_execute import NormalPythonExecute 10 | 11 | 12 | class DataAnalysis(ToolCallAgent): 13 | """ 14 | A data analysis agent that uses planning to solve various data analysis tasks. 15 | 16 | This agent extends ToolCallAgent with a comprehensive set of tools and capabilities, 17 | including Data Analysis, Chart Visualization, Data Report. 18 | """ 19 | 20 | name: str = "DataAnalysis" 21 | description: str = "An analytical agent that utilizes multiple tools to solve diverse data analysis tasks" 22 | 23 | system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) 24 | next_step_prompt: str = NEXT_STEP_PROMPT 25 | 26 | max_observe: int = 15000 27 | max_steps: int = 20 28 | 29 | # Add general-purpose tools to the tool collection 30 | available_tools: ToolCollection = Field( 31 | default_factory=lambda: ToolCollection( 32 | NormalPythonExecute(), 33 | VisualizationPrepare(), 34 | DataVisualization(), 35 | Terminate(), 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /app/agent/manus.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional 2 | 3 | from pydantic import Field, model_validator 4 | 5 | from app.agent.browser import BrowserContextHelper 6 | from app.agent.toolcall import ToolCallAgent 7 | from app.config import config 8 | from app.logger import logger 9 | from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT 10 | from app.tool import Terminate, ToolCollection 11 | from app.tool.ask_human import AskHuman 12 | from app.tool.browser_use_tool import BrowserUseTool 13 | from app.tool.mcp import MCPClients, MCPClientTool 14 | from app.tool.python_execute import PythonExecute 15 | from app.tool.str_replace_editor import StrReplaceEditor 16 | 17 | 18 | class Manus(ToolCallAgent): 19 | """A versatile general-purpose agent with support for both local and MCP tools.""" 20 | 21 | name: str = "Manus" 22 | description: str = "A versatile agent that can solve various tasks using multiple tools including MCP-based tools" 23 | 24 | system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) 25 | next_step_prompt: str = NEXT_STEP_PROMPT 26 | 27 | max_observe: int = 10000 28 | max_steps: int = 20 29 | 30 | # MCP clients for remote tool access 31 | mcp_clients: MCPClients = Field(default_factory=MCPClients) 32 | 33 | # Add general-purpose tools to the tool collection 34 | available_tools: ToolCollection = Field( 35 | default_factory=lambda: ToolCollection( 36 | PythonExecute(), 37 | BrowserUseTool(), 38 | StrReplaceEditor(), 39 | AskHuman(), 40 | Terminate(), 41 | ) 42 | ) 43 | 44 | special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name]) 45 | browser_context_helper: Optional[BrowserContextHelper] = None 46 | 47 | # Track connected MCP servers 48 | connected_servers: Dict[str, str] = Field( 49 | default_factory=dict 50 | ) # server_id -> url/command 51 | _initialized: bool = False 52 | 53 | @model_validator(mode="after") 54 | def initialize_helper(self) -> "Manus": 55 | """Initialize basic components synchronously.""" 56 | self.browser_context_helper = BrowserContextHelper(self) 57 | return self 58 | 59 | @classmethod 60 | async def create(cls, **kwargs) -> "Manus": 61 | """Factory method to create and properly initialize a Manus instance.""" 62 | instance = cls(**kwargs) 63 | await instance.initialize_mcp_servers() 64 | instance._initialized = True 65 | return instance 66 | 67 | async def initialize_mcp_servers(self) -> None: 68 | """Initialize connections to configured MCP servers.""" 69 | for server_id, server_config in config.mcp_config.servers.items(): 70 | try: 71 | if server_config.type == "sse": 72 | if server_config.url: 73 | await self.connect_mcp_server(server_config.url, server_id) 74 | logger.info( 75 | f"Connected to MCP server {server_id} at {server_config.url}" 76 | ) 77 | elif server_config.type == "stdio": 78 | if server_config.command: 79 | await self.connect_mcp_server( 80 | server_config.command, 81 | server_id, 82 | use_stdio=True, 83 | stdio_args=server_config.args, 84 | ) 85 | logger.info( 86 | f"Connected to MCP server {server_id} using command {server_config.command}" 87 | ) 88 | except Exception as e: 89 | logger.error(f"Failed to connect to MCP server {server_id}: {e}") 90 | 91 | async def connect_mcp_server( 92 | self, 93 | server_url: str, 94 | server_id: str = "", 95 | use_stdio: bool = False, 96 | stdio_args: List[str] = None, 97 | ) -> None: 98 | """Connect to an MCP server and add its tools.""" 99 | if use_stdio: 100 | await self.mcp_clients.connect_stdio( 101 | server_url, stdio_args or [], server_id 102 | ) 103 | self.connected_servers[server_id or server_url] = server_url 104 | else: 105 | await self.mcp_clients.connect_sse(server_url, server_id) 106 | self.connected_servers[server_id or server_url] = server_url 107 | 108 | # Update available tools with only the new tools from this server 109 | new_tools = [ 110 | tool for tool in self.mcp_clients.tools if tool.server_id == server_id 111 | ] 112 | self.available_tools.add_tools(*new_tools) 113 | 114 | async def disconnect_mcp_server(self, server_id: str = "") -> None: 115 | """Disconnect from an MCP server and remove its tools.""" 116 | await self.mcp_clients.disconnect(server_id) 117 | if server_id: 118 | self.connected_servers.pop(server_id, None) 119 | else: 120 | self.connected_servers.clear() 121 | 122 | # Rebuild available tools without the disconnected server's tools 123 | base_tools = [ 124 | tool 125 | for tool in self.available_tools.tools 126 | if not isinstance(tool, MCPClientTool) 127 | ] 128 | self.available_tools = ToolCollection(*base_tools) 129 | self.available_tools.add_tools(*self.mcp_clients.tools) 130 | 131 | async def cleanup(self): 132 | """Clean up Manus agent resources.""" 133 | if self.browser_context_helper: 134 | await self.browser_context_helper.cleanup_browser() 135 | # Disconnect from all MCP servers only if we were initialized 136 | if self._initialized: 137 | await self.disconnect_mcp_server() 138 | self._initialized = False 139 | 140 | async def think(self) -> bool: 141 | """Process current state and decide next actions with appropriate context.""" 142 | if not self._initialized: 143 | await self.initialize_mcp_servers() 144 | self._initialized = True 145 | 146 | original_prompt = self.next_step_prompt 147 | recent_messages = self.memory.messages[-3:] if self.memory.messages else [] 148 | browser_in_use = any( 149 | tc.function.name == BrowserUseTool().name 150 | for msg in recent_messages 151 | if msg.tool_calls 152 | for tc in msg.tool_calls 153 | ) 154 | 155 | if browser_in_use: 156 | self.next_step_prompt = ( 157 | await self.browser_context_helper.format_next_step_prompt() 158 | ) 159 | 160 | result = await super().think() 161 | 162 | # Restore original prompt 163 | self.next_step_prompt = original_prompt 164 | 165 | return result 166 | -------------------------------------------------------------------------------- /app/agent/react.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from pydantic import Field 5 | 6 | from app.agent.base import BaseAgent 7 | from app.llm import LLM 8 | from app.schema import AgentState, Memory 9 | 10 | 11 | class ReActAgent(BaseAgent, ABC): 12 | name: str 13 | description: Optional[str] = None 14 | 15 | system_prompt: Optional[str] = None 16 | next_step_prompt: Optional[str] = None 17 | 18 | llm: Optional[LLM] = Field(default_factory=LLM) 19 | memory: Memory = Field(default_factory=Memory) 20 | state: AgentState = AgentState.IDLE 21 | 22 | max_steps: int = 10 23 | current_step: int = 0 24 | 25 | @abstractmethod 26 | async def think(self) -> bool: 27 | """Process current state and decide next action""" 28 | 29 | @abstractmethod 30 | async def act(self) -> str: 31 | """Execute decided actions""" 32 | 33 | async def step(self) -> str: 34 | """Execute a single step: think and act.""" 35 | should_act = await self.think() 36 | if not should_act: 37 | return "Thinking complete - no action needed" 38 | return await self.act() 39 | -------------------------------------------------------------------------------- /app/agent/swe.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import Field 4 | 5 | from app.agent.toolcall import ToolCallAgent 6 | from app.prompt.swe import SYSTEM_PROMPT 7 | from app.tool import Bash, StrReplaceEditor, Terminate, ToolCollection 8 | 9 | 10 | class SWEAgent(ToolCallAgent): 11 | """An agent that implements the SWEAgent paradigm for executing code and natural conversations.""" 12 | 13 | name: str = "swe" 14 | description: str = "an autonomous AI programmer that interacts directly with the computer to solve tasks." 15 | 16 | system_prompt: str = SYSTEM_PROMPT 17 | next_step_prompt: str = "" 18 | 19 | available_tools: ToolCollection = ToolCollection( 20 | Bash(), StrReplaceEditor(), Terminate() 21 | ) 22 | special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name]) 23 | 24 | max_steps: int = 20 25 | -------------------------------------------------------------------------------- /app/exceptions.py: -------------------------------------------------------------------------------- 1 | class ToolError(Exception): 2 | """Raised when a tool encounters an error.""" 3 | 4 | def __init__(self, message): 5 | self.message = message 6 | 7 | 8 | class OpenManusError(Exception): 9 | """Base exception for all OpenManus errors""" 10 | 11 | 12 | class TokenLimitExceeded(OpenManusError): 13 | """Exception raised when the token limit is exceeded""" 14 | -------------------------------------------------------------------------------- /app/flow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/app/flow/__init__.py -------------------------------------------------------------------------------- /app/flow/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Dict, List, Optional, Union 3 | 4 | from pydantic import BaseModel 5 | 6 | from app.agent.base import BaseAgent 7 | 8 | 9 | class BaseFlow(BaseModel, ABC): 10 | """Base class for execution flows supporting multiple agents""" 11 | 12 | agents: Dict[str, BaseAgent] 13 | tools: Optional[List] = None 14 | primary_agent_key: Optional[str] = None 15 | 16 | class Config: 17 | arbitrary_types_allowed = True 18 | 19 | def __init__( 20 | self, agents: Union[BaseAgent, List[BaseAgent], Dict[str, BaseAgent]], **data 21 | ): 22 | # Handle different ways of providing agents 23 | if isinstance(agents, BaseAgent): 24 | agents_dict = {"default": agents} 25 | elif isinstance(agents, list): 26 | agents_dict = {f"agent_{i}": agent for i, agent in enumerate(agents)} 27 | else: 28 | agents_dict = agents 29 | 30 | # If primary agent not specified, use first agent 31 | primary_key = data.get("primary_agent_key") 32 | if not primary_key and agents_dict: 33 | primary_key = next(iter(agents_dict)) 34 | data["primary_agent_key"] = primary_key 35 | 36 | # Set the agents dictionary 37 | data["agents"] = agents_dict 38 | 39 | # Initialize using BaseModel's init 40 | super().__init__(**data) 41 | 42 | @property 43 | def primary_agent(self) -> Optional[BaseAgent]: 44 | """Get the primary agent for the flow""" 45 | return self.agents.get(self.primary_agent_key) 46 | 47 | def get_agent(self, key: str) -> Optional[BaseAgent]: 48 | """Get a specific agent by key""" 49 | return self.agents.get(key) 50 | 51 | def add_agent(self, key: str, agent: BaseAgent) -> None: 52 | """Add a new agent to the flow""" 53 | self.agents[key] = agent 54 | 55 | @abstractmethod 56 | async def execute(self, input_text: str) -> str: 57 | """Execute the flow with given input""" 58 | -------------------------------------------------------------------------------- /app/flow/flow_factory.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Dict, List, Union 3 | 4 | from app.agent.base import BaseAgent 5 | from app.flow.base import BaseFlow 6 | from app.flow.planning import PlanningFlow 7 | 8 | 9 | class FlowType(str, Enum): 10 | PLANNING = "planning" 11 | 12 | 13 | class FlowFactory: 14 | """Factory for creating different types of flows with support for multiple agents""" 15 | 16 | @staticmethod 17 | def create_flow( 18 | flow_type: FlowType, 19 | agents: Union[BaseAgent, List[BaseAgent], Dict[str, BaseAgent]], 20 | **kwargs, 21 | ) -> BaseFlow: 22 | flows = { 23 | FlowType.PLANNING: PlanningFlow, 24 | } 25 | 26 | flow_class = flows.get(flow_type) 27 | if not flow_class: 28 | raise ValueError(f"Unknown flow type: {flow_type}") 29 | 30 | return flow_class(agents, **kwargs) 31 | -------------------------------------------------------------------------------- /app/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime 3 | 4 | from loguru import logger as _logger 5 | 6 | from app.config import PROJECT_ROOT 7 | 8 | 9 | _print_level = "INFO" 10 | 11 | 12 | def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None): 13 | """Adjust the log level to above level""" 14 | global _print_level 15 | _print_level = print_level 16 | 17 | current_date = datetime.now() 18 | formatted_date = current_date.strftime("%Y%m%d%H%M%S") 19 | log_name = ( 20 | f"{name}_{formatted_date}" if name else formatted_date 21 | ) # name a log with prefix name 22 | 23 | _logger.remove() 24 | _logger.add(sys.stderr, level=print_level) 25 | _logger.add(PROJECT_ROOT / f"logs/{log_name}.log", level=logfile_level) 26 | return _logger 27 | 28 | 29 | logger = define_log_level() 30 | 31 | 32 | if __name__ == "__main__": 33 | logger.info("Starting application") 34 | logger.debug("Debug message") 35 | logger.warning("Warning message") 36 | logger.error("Error message") 37 | logger.critical("Critical message") 38 | 39 | try: 40 | raise ValueError("Test error") 41 | except Exception as e: 42 | logger.exception(f"An error occurred: {e}") 43 | -------------------------------------------------------------------------------- /app/mcp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/app/mcp/__init__.py -------------------------------------------------------------------------------- /app/prompt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/app/prompt/__init__.py -------------------------------------------------------------------------------- /app/prompt/browser.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = """\ 2 | You are an AI agent designed to automate browser tasks. Your goal is to accomplish the ultimate task following the rules. 3 | 4 | # Input Format 5 | Task 6 | Previous steps 7 | Current URL 8 | Open Tabs 9 | Interactive Elements 10 | [index]text 11 | - index: Numeric identifier for interaction 12 | - type: HTML element type (button, input, etc.) 13 | - text: Element description 14 | Example: 15 | [33] 16 | 17 | - Only elements with numeric indexes in [] are interactive 18 | - elements without [] provide only context 19 | 20 | # Response Rules 21 | 1. RESPONSE FORMAT: You must ALWAYS respond with valid JSON in this exact format: 22 | {{"current_state": {{"evaluation_previous_goal": "Success|Failed|Unknown - Analyze the current elements and the image to check if the previous goals/actions are successful like intended by the task. Mention if something unexpected happened. Shortly state why/why not", 23 | "memory": "Description of what has been done and what you need to remember. Be very specific. Count here ALWAYS how many times you have done something and how many remain. E.g. 0 out of 10 websites analyzed. Continue with abc and xyz", 24 | "next_goal": "What needs to be done with the next immediate action"}}, 25 | "action":[{{"one_action_name": {{// action-specific parameter}}}}, // ... more actions in sequence]}} 26 | 27 | 2. ACTIONS: You can specify multiple actions in the list to be executed in sequence. But always specify only one action name per item. Use maximum {{max_actions}} actions per sequence. 28 | Common action sequences: 29 | - Form filling: [{{"input_text": {{"index": 1, "text": "username"}}}}, {{"input_text": {{"index": 2, "text": "password"}}}}, {{"click_element": {{"index": 3}}}}] 30 | - Navigation and extraction: [{{"go_to_url": {{"url": "https://example.com"}}}}, {{"extract_content": {{"goal": "extract the names"}}}}] 31 | - Actions are executed in the given order 32 | - If the page changes after an action, the sequence is interrupted and you get the new state. 33 | - Only provide the action sequence until an action which changes the page state significantly. 34 | - Try to be efficient, e.g. fill forms at once, or chain actions where nothing changes on the page 35 | - only use multiple actions if it makes sense. 36 | 37 | 3. ELEMENT INTERACTION: 38 | - Only use indexes of the interactive elements 39 | - Elements marked with "[]Non-interactive text" are non-interactive 40 | 41 | 4. NAVIGATION & ERROR HANDLING: 42 | - If no suitable elements exist, use other functions to complete the task 43 | - If stuck, try alternative approaches - like going back to a previous page, new search, new tab etc. 44 | - Handle popups/cookies by accepting or closing them 45 | - Use scroll to find elements you are looking for 46 | - If you want to research something, open a new tab instead of using the current tab 47 | - If captcha pops up, try to solve it - else try a different approach 48 | - If the page is not fully loaded, use wait action 49 | 50 | 5. TASK COMPLETION: 51 | - Use the done action as the last action as soon as the ultimate task is complete 52 | - Dont use "done" before you are done with everything the user asked you, except you reach the last step of max_steps. 53 | - If you reach your last step, use the done action even if the task is not fully finished. Provide all the information you have gathered so far. If the ultimate task is completly finished set success to true. If not everything the user asked for is completed set success in done to false! 54 | - If you have to do something repeatedly for example the task says for "each", or "for all", or "x times", count always inside "memory" how many times you have done it and how many remain. Don't stop until you have completed like the task asked you. Only call done after the last step. 55 | - Don't hallucinate actions 56 | - Make sure you include everything you found out for the ultimate task in the done text parameter. Do not just say you are done, but include the requested information of the task. 57 | 58 | 6. VISUAL CONTEXT: 59 | - When an image is provided, use it to understand the page layout 60 | - Bounding boxes with labels on their top right corner correspond to element indexes 61 | 62 | 7. Form filling: 63 | - If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field. 64 | 65 | 8. Long tasks: 66 | - Keep track of the status and subresults in the memory. 67 | 68 | 9. Extraction: 69 | - If your task is to find information - call extract_content on the specific pages to get and store the information. 70 | Your responses must be always JSON with the specified format. 71 | """ 72 | 73 | NEXT_STEP_PROMPT = """ 74 | What should I do next to achieve my goal? 75 | 76 | When you see [Current state starts here], focus on the following: 77 | - Current URL and page title{url_placeholder} 78 | - Available tabs{tabs_placeholder} 79 | - Interactive elements and their indices 80 | - Content above{content_above_placeholder} or below{content_below_placeholder} the viewport (if indicated) 81 | - Any action results or errors{results_placeholder} 82 | 83 | For browser interactions: 84 | - To navigate: browser_use with action="go_to_url", url="..." 85 | - To click: browser_use with action="click_element", index=N 86 | - To type: browser_use with action="input_text", index=N, text="..." 87 | - To extract: browser_use with action="extract_content", goal="..." 88 | - To scroll: browser_use with action="scroll_down" or "scroll_up" 89 | 90 | Consider both what's visible and what might be beyond the current viewport. 91 | Be methodical - remember your progress and what you've learned so far. 92 | 93 | If you want to stop the interaction at any point, use the `terminate` tool/function call. 94 | """ 95 | -------------------------------------------------------------------------------- /app/prompt/manus.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = ( 2 | "You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, web browsing, or human interaction (only for extreme cases), you can handle it all." 3 | "The initial directory is: {directory}" 4 | ) 5 | 6 | NEXT_STEP_PROMPT = """ 7 | Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. 8 | 9 | If you want to stop the interaction at any point, use the `terminate` tool/function call. 10 | """ 11 | -------------------------------------------------------------------------------- /app/prompt/mcp.py: -------------------------------------------------------------------------------- 1 | """Prompts for the MCP Agent.""" 2 | 3 | SYSTEM_PROMPT = """You are an AI assistant with access to a Model Context Protocol (MCP) server. 4 | You can use the tools provided by the MCP server to complete tasks. 5 | The MCP server will dynamically expose tools that you can use - always check the available tools first. 6 | 7 | When using an MCP tool: 8 | 1. Choose the appropriate tool based on your task requirements 9 | 2. Provide properly formatted arguments as required by the tool 10 | 3. Observe the results and use them to determine next steps 11 | 4. Tools may change during operation - new tools might appear or existing ones might disappear 12 | 13 | Follow these guidelines: 14 | - Call tools with valid parameters as documented in their schemas 15 | - Handle errors gracefully by understanding what went wrong and trying again with corrected parameters 16 | - For multimedia responses (like images), you'll receive a description of the content 17 | - Complete user requests step by step, using the most appropriate tools 18 | - If multiple tools need to be called in sequence, make one call at a time and wait for results 19 | 20 | Remember to clearly explain your reasoning and actions to the user. 21 | """ 22 | 23 | NEXT_STEP_PROMPT = """Based on the current state and available tools, what should be done next? 24 | Think step by step about the problem and identify which MCP tool would be most helpful for the current stage. 25 | If you've already made progress, consider what additional information you need or what actions would move you closer to completing the task. 26 | """ 27 | 28 | # Additional specialized prompts 29 | TOOL_ERROR_PROMPT = """You encountered an error with the tool '{tool_name}'. 30 | Try to understand what went wrong and correct your approach. 31 | Common issues include: 32 | - Missing or incorrect parameters 33 | - Invalid parameter formats 34 | - Using a tool that's no longer available 35 | - Attempting an operation that's not supported 36 | 37 | Please check the tool specifications and try again with corrected parameters. 38 | """ 39 | 40 | MULTIMEDIA_RESPONSE_PROMPT = """You've received a multimedia response (image, audio, etc.) from the tool '{tool_name}'. 41 | This content has been processed and described for you. 42 | Use this information to continue the task or provide insights to the user. 43 | """ 44 | -------------------------------------------------------------------------------- /app/prompt/planning.py: -------------------------------------------------------------------------------- 1 | PLANNING_SYSTEM_PROMPT = """ 2 | You are an expert Planning Agent tasked with solving problems efficiently through structured plans. 3 | Your job is: 4 | 1. Analyze requests to understand the task scope 5 | 2. Create a clear, actionable plan that makes meaningful progress with the `planning` tool 6 | 3. Execute steps using available tools as needed 7 | 4. Track progress and adapt plans when necessary 8 | 5. Use `finish` to conclude immediately when the task is complete 9 | 10 | 11 | Available tools will vary by task but may include: 12 | - `planning`: Create, update, and track plans (commands: create, update, mark_step, etc.) 13 | - `finish`: End the task when complete 14 | Break tasks into logical steps with clear outcomes. Avoid excessive detail or sub-steps. 15 | Think about dependencies and verification methods. 16 | Know when to conclude - don't continue thinking once objectives are met. 17 | """ 18 | 19 | NEXT_STEP_PROMPT = """ 20 | Based on the current state, what's your next action? 21 | Choose the most efficient path forward: 22 | 1. Is the plan sufficient, or does it need refinement? 23 | 2. Can you execute the next step immediately? 24 | 3. Is the task complete? If so, use `finish` right away. 25 | 26 | Be concise in your reasoning, then select the appropriate tool or action. 27 | """ 28 | -------------------------------------------------------------------------------- /app/prompt/swe.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = """SETTING: You are an autonomous programmer, and you're working directly in the command line with a special interface. 2 | 3 | The special interface consists of a file editor that shows you {{WINDOW}} lines of a file at a time. 4 | In addition to typical bash commands, you can also use specific commands to help you navigate and edit files. 5 | To call a command, you need to invoke it with a function call/tool call. 6 | 7 | Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. 8 | If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run. 9 | 10 | RESPONSE FORMAT: 11 | Your shell prompt is formatted as follows: 12 | (Open file: ) 13 | (Current directory: ) 14 | bash-$ 15 | 16 | First, you should _always_ include a general thought about what you're going to do next. 17 | Then, for every response, you must include exactly _ONE_ tool call/function call. 18 | 19 | Remember, you should always include a _SINGLE_ tool call/function call and then wait for a response from the shell before continuing with more discussion and commands. Everything you include in the DISCUSSION section will be saved for future reference. 20 | If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first tool call, and then after receiving a response you'll be able to issue the second tool call. 21 | Note that the environment does NOT support interactive session commands (e.g. python, vim), so please do not invoke them. 22 | """ 23 | -------------------------------------------------------------------------------- /app/prompt/toolcall.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = "You are an agent that can execute tool calls" 2 | 3 | NEXT_STEP_PROMPT = ( 4 | "If you want to stop interaction, use `terminate` tool/function call." 5 | ) 6 | -------------------------------------------------------------------------------- /app/prompt/visualization.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = """You are an AI agent designed to data analysis / visualization task. You have various tools at your disposal that you can call upon to efficiently complete complex requests. 2 | # Note: 3 | 1. The workspace directory is: {directory}; Read / write file in workspace 4 | 2. Generate analysis conclusion report in the end""" 5 | 6 | NEXT_STEP_PROMPT = """Based on user needs, break down the problem and use different tools step by step to solve it. 7 | # Note 8 | 1. Each step select the most appropriate tool proactively (ONLY ONE). 9 | 2. After using each tool, clearly explain the execution results and suggest the next steps. 10 | 3. When observation with Error, review and fix it.""" 11 | -------------------------------------------------------------------------------- /app/sandbox/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Docker Sandbox Module 3 | 4 | Provides secure containerized execution environment with resource limits 5 | and isolation for running untrusted code. 6 | """ 7 | from app.sandbox.client import ( 8 | BaseSandboxClient, 9 | LocalSandboxClient, 10 | create_sandbox_client, 11 | ) 12 | from app.sandbox.core.exceptions import ( 13 | SandboxError, 14 | SandboxResourceError, 15 | SandboxTimeoutError, 16 | ) 17 | from app.sandbox.core.manager import SandboxManager 18 | from app.sandbox.core.sandbox import DockerSandbox 19 | 20 | 21 | __all__ = [ 22 | "DockerSandbox", 23 | "SandboxManager", 24 | "BaseSandboxClient", 25 | "LocalSandboxClient", 26 | "create_sandbox_client", 27 | "SandboxError", 28 | "SandboxTimeoutError", 29 | "SandboxResourceError", 30 | ] 31 | -------------------------------------------------------------------------------- /app/sandbox/client.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Dict, Optional, Protocol 3 | 4 | from app.config import SandboxSettings 5 | from app.sandbox.core.sandbox import DockerSandbox 6 | 7 | 8 | class SandboxFileOperations(Protocol): 9 | """Protocol for sandbox file operations.""" 10 | 11 | async def copy_from(self, container_path: str, local_path: str) -> None: 12 | """Copies file from container to local. 13 | 14 | Args: 15 | container_path: File path in container. 16 | local_path: Local destination path. 17 | """ 18 | ... 19 | 20 | async def copy_to(self, local_path: str, container_path: str) -> None: 21 | """Copies file from local to container. 22 | 23 | Args: 24 | local_path: Local source file path. 25 | container_path: Destination path in container. 26 | """ 27 | ... 28 | 29 | async def read_file(self, path: str) -> str: 30 | """Reads file content from container. 31 | 32 | Args: 33 | path: File path in container. 34 | 35 | Returns: 36 | str: File content. 37 | """ 38 | ... 39 | 40 | async def write_file(self, path: str, content: str) -> None: 41 | """Writes content to file in container. 42 | 43 | Args: 44 | path: File path in container. 45 | content: Content to write. 46 | """ 47 | ... 48 | 49 | 50 | class BaseSandboxClient(ABC): 51 | """Base sandbox client interface.""" 52 | 53 | @abstractmethod 54 | async def create( 55 | self, 56 | config: Optional[SandboxSettings] = None, 57 | volume_bindings: Optional[Dict[str, str]] = None, 58 | ) -> None: 59 | """Creates sandbox.""" 60 | 61 | @abstractmethod 62 | async def run_command(self, command: str, timeout: Optional[int] = None) -> str: 63 | """Executes command.""" 64 | 65 | @abstractmethod 66 | async def copy_from(self, container_path: str, local_path: str) -> None: 67 | """Copies file from container.""" 68 | 69 | @abstractmethod 70 | async def copy_to(self, local_path: str, container_path: str) -> None: 71 | """Copies file to container.""" 72 | 73 | @abstractmethod 74 | async def read_file(self, path: str) -> str: 75 | """Reads file.""" 76 | 77 | @abstractmethod 78 | async def write_file(self, path: str, content: str) -> None: 79 | """Writes file.""" 80 | 81 | @abstractmethod 82 | async def cleanup(self) -> None: 83 | """Cleans up resources.""" 84 | 85 | 86 | class LocalSandboxClient(BaseSandboxClient): 87 | """Local sandbox client implementation.""" 88 | 89 | def __init__(self): 90 | """Initializes local sandbox client.""" 91 | self.sandbox: Optional[DockerSandbox] = None 92 | 93 | async def create( 94 | self, 95 | config: Optional[SandboxSettings] = None, 96 | volume_bindings: Optional[Dict[str, str]] = None, 97 | ) -> None: 98 | """Creates a sandbox. 99 | 100 | Args: 101 | config: Sandbox configuration. 102 | volume_bindings: Volume mappings. 103 | 104 | Raises: 105 | RuntimeError: If sandbox creation fails. 106 | """ 107 | self.sandbox = DockerSandbox(config, volume_bindings) 108 | await self.sandbox.create() 109 | 110 | async def run_command(self, command: str, timeout: Optional[int] = None) -> str: 111 | """Runs command in sandbox. 112 | 113 | Args: 114 | command: Command to execute. 115 | timeout: Execution timeout in seconds. 116 | 117 | Returns: 118 | Command output. 119 | 120 | Raises: 121 | RuntimeError: If sandbox not initialized. 122 | """ 123 | if not self.sandbox: 124 | raise RuntimeError("Sandbox not initialized") 125 | return await self.sandbox.run_command(command, timeout) 126 | 127 | async def copy_from(self, container_path: str, local_path: str) -> None: 128 | """Copies file from container to local. 129 | 130 | Args: 131 | container_path: File path in container. 132 | local_path: Local destination path. 133 | 134 | Raises: 135 | RuntimeError: If sandbox not initialized. 136 | """ 137 | if not self.sandbox: 138 | raise RuntimeError("Sandbox not initialized") 139 | await self.sandbox.copy_from(container_path, local_path) 140 | 141 | async def copy_to(self, local_path: str, container_path: str) -> None: 142 | """Copies file from local to container. 143 | 144 | Args: 145 | local_path: Local source file path. 146 | container_path: Destination path in container. 147 | 148 | Raises: 149 | RuntimeError: If sandbox not initialized. 150 | """ 151 | if not self.sandbox: 152 | raise RuntimeError("Sandbox not initialized") 153 | await self.sandbox.copy_to(local_path, container_path) 154 | 155 | async def read_file(self, path: str) -> str: 156 | """Reads file from container. 157 | 158 | Args: 159 | path: File path in container. 160 | 161 | Returns: 162 | File content. 163 | 164 | Raises: 165 | RuntimeError: If sandbox not initialized. 166 | """ 167 | if not self.sandbox: 168 | raise RuntimeError("Sandbox not initialized") 169 | return await self.sandbox.read_file(path) 170 | 171 | async def write_file(self, path: str, content: str) -> None: 172 | """Writes file to container. 173 | 174 | Args: 175 | path: File path in container. 176 | content: File content. 177 | 178 | Raises: 179 | RuntimeError: If sandbox not initialized. 180 | """ 181 | if not self.sandbox: 182 | raise RuntimeError("Sandbox not initialized") 183 | await self.sandbox.write_file(path, content) 184 | 185 | async def cleanup(self) -> None: 186 | """Cleans up resources.""" 187 | if self.sandbox: 188 | await self.sandbox.cleanup() 189 | self.sandbox = None 190 | 191 | 192 | def create_sandbox_client() -> LocalSandboxClient: 193 | """Creates a sandbox client. 194 | 195 | Returns: 196 | LocalSandboxClient: Sandbox client instance. 197 | """ 198 | return LocalSandboxClient() 199 | 200 | 201 | SANDBOX_CLIENT = create_sandbox_client() 202 | -------------------------------------------------------------------------------- /app/sandbox/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exception classes for the sandbox system. 2 | 3 | This module defines custom exceptions used throughout the sandbox system to 4 | handle various error conditions in a structured way. 5 | """ 6 | 7 | 8 | class SandboxError(Exception): 9 | """Base exception for sandbox-related errors.""" 10 | 11 | 12 | class SandboxTimeoutError(SandboxError): 13 | """Exception raised when a sandbox operation times out.""" 14 | 15 | 16 | class SandboxResourceError(SandboxError): 17 | """Exception raised for resource-related errors.""" 18 | -------------------------------------------------------------------------------- /app/schema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List, Literal, Optional, Union 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class Role(str, Enum): 8 | """Message role options""" 9 | 10 | SYSTEM = "system" 11 | USER = "user" 12 | ASSISTANT = "assistant" 13 | TOOL = "tool" 14 | 15 | 16 | ROLE_VALUES = tuple(role.value for role in Role) 17 | ROLE_TYPE = Literal[ROLE_VALUES] # type: ignore 18 | 19 | 20 | class ToolChoice(str, Enum): 21 | """Tool choice options""" 22 | 23 | NONE = "none" 24 | AUTO = "auto" 25 | REQUIRED = "required" 26 | 27 | 28 | TOOL_CHOICE_VALUES = tuple(choice.value for choice in ToolChoice) 29 | TOOL_CHOICE_TYPE = Literal[TOOL_CHOICE_VALUES] # type: ignore 30 | 31 | 32 | class AgentState(str, Enum): 33 | """Agent execution states""" 34 | 35 | IDLE = "IDLE" 36 | RUNNING = "RUNNING" 37 | FINISHED = "FINISHED" 38 | ERROR = "ERROR" 39 | 40 | 41 | class Function(BaseModel): 42 | name: str 43 | arguments: str 44 | 45 | 46 | class ToolCall(BaseModel): 47 | """Represents a tool/function call in a message""" 48 | 49 | id: str 50 | type: str = "function" 51 | function: Function 52 | 53 | 54 | class Message(BaseModel): 55 | """Represents a chat message in the conversation""" 56 | 57 | role: ROLE_TYPE = Field(...) # type: ignore 58 | content: Optional[str] = Field(default=None) 59 | tool_calls: Optional[List[ToolCall]] = Field(default=None) 60 | name: Optional[str] = Field(default=None) 61 | tool_call_id: Optional[str] = Field(default=None) 62 | base64_image: Optional[str] = Field(default=None) 63 | 64 | def __add__(self, other) -> List["Message"]: 65 | """支持 Message + list 或 Message + Message 的操作""" 66 | if isinstance(other, list): 67 | return [self] + other 68 | elif isinstance(other, Message): 69 | return [self, other] 70 | else: 71 | raise TypeError( 72 | f"unsupported operand type(s) for +: '{type(self).__name__}' and '{type(other).__name__}'" 73 | ) 74 | 75 | def __radd__(self, other) -> List["Message"]: 76 | """支持 list + Message 的操作""" 77 | if isinstance(other, list): 78 | return other + [self] 79 | else: 80 | raise TypeError( 81 | f"unsupported operand type(s) for +: '{type(other).__name__}' and '{type(self).__name__}'" 82 | ) 83 | 84 | def to_dict(self) -> dict: 85 | """Convert message to dictionary format""" 86 | message = {"role": self.role} 87 | if self.content is not None: 88 | message["content"] = self.content 89 | if self.tool_calls is not None: 90 | message["tool_calls"] = [tool_call.dict() for tool_call in self.tool_calls] 91 | if self.name is not None: 92 | message["name"] = self.name 93 | if self.tool_call_id is not None: 94 | message["tool_call_id"] = self.tool_call_id 95 | if self.base64_image is not None: 96 | message["base64_image"] = self.base64_image 97 | return message 98 | 99 | @classmethod 100 | def user_message( 101 | cls, content: str, base64_image: Optional[str] = None 102 | ) -> "Message": 103 | """Create a user message""" 104 | return cls(role=Role.USER, content=content, base64_image=base64_image) 105 | 106 | @classmethod 107 | def system_message(cls, content: str) -> "Message": 108 | """Create a system message""" 109 | return cls(role=Role.SYSTEM, content=content) 110 | 111 | @classmethod 112 | def assistant_message( 113 | cls, content: Optional[str] = None, base64_image: Optional[str] = None 114 | ) -> "Message": 115 | """Create an assistant message""" 116 | return cls(role=Role.ASSISTANT, content=content, base64_image=base64_image) 117 | 118 | @classmethod 119 | def tool_message( 120 | cls, content: str, name, tool_call_id: str, base64_image: Optional[str] = None 121 | ) -> "Message": 122 | """Create a tool message""" 123 | return cls( 124 | role=Role.TOOL, 125 | content=content, 126 | name=name, 127 | tool_call_id=tool_call_id, 128 | base64_image=base64_image, 129 | ) 130 | 131 | @classmethod 132 | def from_tool_calls( 133 | cls, 134 | tool_calls: List[Any], 135 | content: Union[str, List[str]] = "", 136 | base64_image: Optional[str] = None, 137 | **kwargs, 138 | ): 139 | """Create ToolCallsMessage from raw tool calls. 140 | 141 | Args: 142 | tool_calls: Raw tool calls from LLM 143 | content: Optional message content 144 | base64_image: Optional base64 encoded image 145 | """ 146 | formatted_calls = [ 147 | {"id": call.id, "function": call.function.model_dump(), "type": "function"} 148 | for call in tool_calls 149 | ] 150 | return cls( 151 | role=Role.ASSISTANT, 152 | content=content, 153 | tool_calls=formatted_calls, 154 | base64_image=base64_image, 155 | **kwargs, 156 | ) 157 | 158 | 159 | class Memory(BaseModel): 160 | messages: List[Message] = Field(default_factory=list) 161 | max_messages: int = Field(default=100) 162 | 163 | def add_message(self, message: Message) -> None: 164 | """Add a message to memory""" 165 | self.messages.append(message) 166 | # Optional: Implement message limit 167 | if len(self.messages) > self.max_messages: 168 | self.messages = self.messages[-self.max_messages :] 169 | 170 | def add_messages(self, messages: List[Message]) -> None: 171 | """Add multiple messages to memory""" 172 | self.messages.extend(messages) 173 | # Optional: Implement message limit 174 | if len(self.messages) > self.max_messages: 175 | self.messages = self.messages[-self.max_messages :] 176 | 177 | def clear(self) -> None: 178 | """Clear all messages""" 179 | self.messages.clear() 180 | 181 | def get_recent_messages(self, n: int) -> List[Message]: 182 | """Get n most recent messages""" 183 | return self.messages[-n:] 184 | 185 | def to_dict_list(self) -> List[dict]: 186 | """Convert messages to list of dicts""" 187 | return [msg.to_dict() for msg in self.messages] 188 | -------------------------------------------------------------------------------- /app/tool/__init__.py: -------------------------------------------------------------------------------- 1 | from app.tool.base import BaseTool 2 | from app.tool.bash import Bash 3 | from app.tool.browser_use_tool import BrowserUseTool 4 | from app.tool.create_chat_completion import CreateChatCompletion 5 | from app.tool.planning import PlanningTool 6 | from app.tool.str_replace_editor import StrReplaceEditor 7 | from app.tool.terminate import Terminate 8 | from app.tool.tool_collection import ToolCollection 9 | from app.tool.web_search import WebSearch 10 | 11 | 12 | __all__ = [ 13 | "BaseTool", 14 | "Bash", 15 | "BrowserUseTool", 16 | "Terminate", 17 | "StrReplaceEditor", 18 | "WebSearch", 19 | "ToolCollection", 20 | "CreateChatCompletion", 21 | "PlanningTool", 22 | ] 23 | -------------------------------------------------------------------------------- /app/tool/ask_human.py: -------------------------------------------------------------------------------- 1 | from app.tool import BaseTool 2 | 3 | 4 | class AskHuman(BaseTool): 5 | """Add a tool to ask human for help.""" 6 | 7 | name: str = "ask_human" 8 | description: str = "Use this tool to ask human for help." 9 | parameters: str = { 10 | "type": "object", 11 | "properties": { 12 | "inquire": { 13 | "type": "string", 14 | "description": "The question you want to ask human.", 15 | } 16 | }, 17 | "required": ["inquire"], 18 | } 19 | 20 | async def execute(self, inquire: str) -> str: 21 | return input(f"""Bot: {inquire}\n\nYou: """).strip() 22 | -------------------------------------------------------------------------------- /app/tool/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, Optional 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class BaseTool(ABC, BaseModel): 8 | name: str 9 | description: str 10 | parameters: Optional[dict] = None 11 | 12 | class Config: 13 | arbitrary_types_allowed = True 14 | 15 | async def __call__(self, **kwargs) -> Any: 16 | """Execute the tool with given parameters.""" 17 | return await self.execute(**kwargs) 18 | 19 | @abstractmethod 20 | async def execute(self, **kwargs) -> Any: 21 | """Execute the tool with given parameters.""" 22 | 23 | def to_param(self) -> Dict: 24 | """Convert tool to function call format.""" 25 | return { 26 | "type": "function", 27 | "function": { 28 | "name": self.name, 29 | "description": self.description, 30 | "parameters": self.parameters, 31 | }, 32 | } 33 | 34 | 35 | class ToolResult(BaseModel): 36 | """Represents the result of a tool execution.""" 37 | 38 | output: Any = Field(default=None) 39 | error: Optional[str] = Field(default=None) 40 | base64_image: Optional[str] = Field(default=None) 41 | system: Optional[str] = Field(default=None) 42 | 43 | class Config: 44 | arbitrary_types_allowed = True 45 | 46 | def __bool__(self): 47 | return any(getattr(self, field) for field in self.__fields__) 48 | 49 | def __add__(self, other: "ToolResult"): 50 | def combine_fields( 51 | field: Optional[str], other_field: Optional[str], concatenate: bool = True 52 | ): 53 | if field and other_field: 54 | if concatenate: 55 | return field + other_field 56 | raise ValueError("Cannot combine tool results") 57 | return field or other_field 58 | 59 | return ToolResult( 60 | output=combine_fields(self.output, other.output), 61 | error=combine_fields(self.error, other.error), 62 | base64_image=combine_fields(self.base64_image, other.base64_image, False), 63 | system=combine_fields(self.system, other.system), 64 | ) 65 | 66 | def __str__(self): 67 | return f"Error: {self.error}" if self.error else self.output 68 | 69 | def replace(self, **kwargs): 70 | """Returns a new ToolResult with the given fields replaced.""" 71 | # return self.copy(update=kwargs) 72 | return type(self)(**{**self.dict(), **kwargs}) 73 | 74 | 75 | class CLIResult(ToolResult): 76 | """A ToolResult that can be rendered as a CLI output.""" 77 | 78 | 79 | class ToolFailure(ToolResult): 80 | """A ToolResult that represents a failure.""" 81 | -------------------------------------------------------------------------------- /app/tool/bash.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from typing import Optional 4 | 5 | from app.exceptions import ToolError 6 | from app.tool.base import BaseTool, CLIResult 7 | 8 | 9 | _BASH_DESCRIPTION = """Execute a bash command in the terminal. 10 | * Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. 11 | * Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command=`ctrl+c` to interrupt the process. 12 | * Timeout: If a command execution result says "Command timed out. Sending SIGINT to the process", the assistant should retry running the command in the background. 13 | """ 14 | 15 | 16 | class _BashSession: 17 | """A session of a bash shell.""" 18 | 19 | _started: bool 20 | _process: asyncio.subprocess.Process 21 | 22 | command: str = "/bin/bash" 23 | _output_delay: float = 0.2 # seconds 24 | _timeout: float = 120.0 # seconds 25 | _sentinel: str = "<>" 26 | 27 | def __init__(self): 28 | self._started = False 29 | self._timed_out = False 30 | 31 | async def start(self): 32 | if self._started: 33 | return 34 | 35 | self._process = await asyncio.create_subprocess_shell( 36 | self.command, 37 | preexec_fn=os.setsid, 38 | shell=True, 39 | bufsize=0, 40 | stdin=asyncio.subprocess.PIPE, 41 | stdout=asyncio.subprocess.PIPE, 42 | stderr=asyncio.subprocess.PIPE, 43 | ) 44 | 45 | self._started = True 46 | 47 | def stop(self): 48 | """Terminate the bash shell.""" 49 | if not self._started: 50 | raise ToolError("Session has not started.") 51 | if self._process.returncode is not None: 52 | return 53 | self._process.terminate() 54 | 55 | async def run(self, command: str): 56 | """Execute a command in the bash shell.""" 57 | if not self._started: 58 | raise ToolError("Session has not started.") 59 | if self._process.returncode is not None: 60 | return CLIResult( 61 | system="tool must be restarted", 62 | error=f"bash has exited with returncode {self._process.returncode}", 63 | ) 64 | if self._timed_out: 65 | raise ToolError( 66 | f"timed out: bash has not returned in {self._timeout} seconds and must be restarted", 67 | ) 68 | 69 | # we know these are not None because we created the process with PIPEs 70 | assert self._process.stdin 71 | assert self._process.stdout 72 | assert self._process.stderr 73 | 74 | # send command to the process 75 | self._process.stdin.write( 76 | command.encode() + f"; echo '{self._sentinel}'\n".encode() 77 | ) 78 | await self._process.stdin.drain() 79 | 80 | # read output from the process, until the sentinel is found 81 | try: 82 | async with asyncio.timeout(self._timeout): 83 | while True: 84 | await asyncio.sleep(self._output_delay) 85 | # if we read directly from stdout/stderr, it will wait forever for 86 | # EOF. use the StreamReader buffer directly instead. 87 | output = ( 88 | self._process.stdout._buffer.decode() 89 | ) # pyright: ignore[reportAttributeAccessIssue] 90 | if self._sentinel in output: 91 | # strip the sentinel and break 92 | output = output[: output.index(self._sentinel)] 93 | break 94 | except asyncio.TimeoutError: 95 | self._timed_out = True 96 | raise ToolError( 97 | f"timed out: bash has not returned in {self._timeout} seconds and must be restarted", 98 | ) from None 99 | 100 | if output.endswith("\n"): 101 | output = output[:-1] 102 | 103 | error = ( 104 | self._process.stderr._buffer.decode() 105 | ) # pyright: ignore[reportAttributeAccessIssue] 106 | if error.endswith("\n"): 107 | error = error[:-1] 108 | 109 | # clear the buffers so that the next output can be read correctly 110 | self._process.stdout._buffer.clear() # pyright: ignore[reportAttributeAccessIssue] 111 | self._process.stderr._buffer.clear() # pyright: ignore[reportAttributeAccessIssue] 112 | 113 | return CLIResult(output=output, error=error) 114 | 115 | 116 | class Bash(BaseTool): 117 | """A tool for executing bash commands""" 118 | 119 | name: str = "bash" 120 | description: str = _BASH_DESCRIPTION 121 | parameters: dict = { 122 | "type": "object", 123 | "properties": { 124 | "command": { 125 | "type": "string", 126 | "description": "The bash command to execute. Can be empty to view additional logs when previous exit code is `-1`. Can be `ctrl+c` to interrupt the currently running process.", 127 | }, 128 | }, 129 | "required": ["command"], 130 | } 131 | 132 | _session: Optional[_BashSession] = None 133 | 134 | async def execute( 135 | self, command: str | None = None, restart: bool = False, **kwargs 136 | ) -> CLIResult: 137 | if restart: 138 | if self._session: 139 | self._session.stop() 140 | self._session = _BashSession() 141 | await self._session.start() 142 | 143 | return CLIResult(system="tool has been restarted.") 144 | 145 | if self._session is None: 146 | self._session = _BashSession() 147 | await self._session.start() 148 | 149 | if command is not None: 150 | return await self._session.run(command) 151 | 152 | raise ToolError("no command provided.") 153 | 154 | 155 | if __name__ == "__main__": 156 | bash = Bash() 157 | rst = asyncio.run(bash.execute("ls -l")) 158 | print(rst) 159 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Chart Visualization Tool 4 | 5 | The chart visualization tool generates data processing code through Python and ultimately invokes [@visactor/vmind](https://github.com/VisActor/VMind) to obtain chart specifications. Chart rendering is implemented using [@visactor/vchart](https://github.com/VisActor/VChart). 6 | 7 | ## Installation 8 | 9 | 1. Install Node.js >= 18 10 | 11 | ```bash 12 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash 13 | # After installation, restart the terminal and install the latest Node.js LTS version: 14 | nvm install --lts 15 | ``` 16 | 17 | 2. Install dependencies 18 | 19 | ```bash 20 | cd app/tool/chart_visualization 21 | npm install 22 | ``` 23 | 24 | ## Tool 25 | ### python_execute 26 | 27 | Execute the necessary parts of data analysis (excluding data visualization) using Python code, including data processing, data summary, report generation, and some general Python script code. 28 | 29 | #### Input 30 | ```typescript 31 | { 32 | // Code type: data processing/data report/other general tasks 33 | code_type: "process" | "report" | "others" 34 | // Final execution code 35 | code: string; 36 | } 37 | ``` 38 | 39 | #### Output 40 | Python execution results, including the saving of intermediate files and print output results. 41 | 42 | ### visualization_preparation 43 | 44 | A pre-tool for data visualization with two purposes, 45 | 46 | #### Data -> Chart 47 | Used to extract the data needed for analysis (.csv) and the corresponding visualization description from the data, ultimately outputting a JSON configuration file. 48 | 49 | #### Chart + Insight -> Chart 50 | Select existing charts and corresponding data insights, choose data insights to add to the chart in the form of data annotations, and finally generate a JSON configuration file. 51 | 52 | #### Input 53 | ```typescript 54 | { 55 | // Code type: data visualization or data insight addition 56 | code_type: "visualization" | "insight" 57 | // Python code used to produce the final JSON file 58 | code: string; 59 | } 60 | ``` 61 | 62 | #### Output 63 | A configuration file for data visualization, used for the `data_visualization tool`. 64 | 65 | ## data_visualization 66 | 67 | Generate specific data visualizations based on the content of `visualization_preparation`. 68 | 69 | ### Input 70 | ```typescript 71 | { 72 | // Configuration file path 73 | json_path: string; 74 | // Current purpose, data visualization or insight annotation addition 75 | tool_type: "visualization" | "insight"; 76 | // Final product png or html; html supports vchart rendering and interaction 77 | output_type: 'png' | 'html' 78 | // Language, currently supports Chinese and English 79 | language: "zh" | "en" 80 | } 81 | ``` 82 | 83 | ## VMind Configuration 84 | 85 | ### LLM 86 | 87 | VMind requires LLM invocation for intelligent chart generation. By default, it uses the `config.llm["default"]` configuration. 88 | 89 | ### Generation Settings 90 | 91 | Main configurations include chart dimensions, theme, and generation method: 92 | ### Generation Method 93 | Default: png. Currently supports automatic selection of `output_type` by LLM based on context. 94 | 95 | ### Dimensions 96 | Default dimensions are unspecified. For HTML output, charts fill the entire page by default. For PNG output, defaults to `1000*1000`. 97 | 98 | ### Theme 99 | Default theme: `'light'`. VChart supports multiple themes. See [Themes](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension). 100 | 101 | ## Test 102 | 103 | Currently, three tasks of different difficulty levels are set for testing. 104 | 105 | ### Simple Chart Generation Task 106 | 107 | Provide data and specific chart generation requirements, test results, execute the command: 108 | ```bash 109 | python -m app.tool.chart_visualization.test.chart_demo 110 | ``` 111 | The results should be located under `workspace\visualization`, involving 9 different chart results. 112 | 113 | ### Simple Data Report Task 114 | 115 | Provide simple raw data analysis requirements, requiring simple processing of the data, execute the command: 116 | ```bash 117 | python -m app.tool.chart_visualization.test.report_demo 118 | ``` 119 | The results are also located under `workspace\visualization`. 120 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/README_zh.md: -------------------------------------------------------------------------------- 1 | # 图表可视化工具 2 | 3 | 图表可视化工具,通过python生成数据处理代码,最终调用[@visactor/vmind](https://github.com/VisActor/VMind)得到图表的spec结果,图表渲染使用[@visactor/vchart](https://github.com/VisActor/VChart) 4 | 5 | ## 安装 6 | 7 | 1. 安装node >= 18 8 | 9 | ```bash 10 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash 11 | # 安装完成后重启终端,然后安装 Node 最新 LTS 版本: 12 | nvm install --lts 13 | ``` 14 | 15 | 2. 安装依赖 16 | 17 | ```bash 18 | cd app/tool/chart_visualization 19 | npm install 20 | ``` 21 | ## Tool 22 | ### python_execute 23 | 24 | 用python代码执行数据分析(除数据可视化以外)中需要的部分,包括数据处理,数据总结摘要,报告生成以及一些通用python脚本代码 25 | 26 | #### 输入 27 | ```typescript 28 | { 29 | // 代码类型:数据处理/数据报告/其他通用任务 30 | code_type: "process" | "report" | "others" 31 | // 最终执行代码 32 | code: string; 33 | } 34 | ``` 35 | 36 | #### 输出 37 | python执行结果,带有中间文件的保存和print输出结果 38 | 39 | ### visualization_preparation 40 | 41 | 数据可视化前置工具,有两种用途, 42 | 43 | #### Data -〉 Chart 44 | 用于从数据中提取需要分析的数据(.csv)和对应可视化的描述,最终输出一份json配置文件。 45 | 46 | #### Chart + Insight -> Chart 47 | 选取已有的图表和对应的数据洞察,挑选数据洞察以数据标注的形式增加到图表中,最终生成一份json配置文件。 48 | 49 | #### 输入 50 | ```typescript 51 | { 52 | // 代码类型:数据可视化 或者 数据洞察添加 53 | code_type: "visualization" | "insight" 54 | // 用于生产最终json文件的python代码 55 | code: string; 56 | } 57 | ``` 58 | 59 | #### 输出 60 | 数据可视化的配置文件,用于`data_visualization tool` 61 | 62 | 63 | ## data_visualization 64 | 65 | 根据`visualization_preparation`的内容,生成具体的数据可视化 66 | 67 | ### 输入 68 | ```typescript 69 | { 70 | // 配置文件路径 71 | json_path: string; 72 | // 当前用途,数据可视化或者洞察标注添加 73 | tool_type: "visualization" | "insight"; 74 | // 最终产物png或者html;html下支持vchart渲染和交互 75 | output_type: 'png' | 'html' 76 | // 语言,目前支持中文和英文 77 | language: "zh" | "en" 78 | } 79 | ``` 80 | 81 | ## 输出 82 | 最终以'png'或者'html'的形式保存在本地,输出保存的图表路径以及图表中发现的数据洞察 83 | 84 | ## VMind配置 85 | 86 | ### LLM 87 | 88 | VMind本身也需要通过调用大模型得到智能图表生成结果,目前默认会使用`config.llm["default"]`配置 89 | 90 | ### 生成配置 91 | 92 | 主要生成配置包括图表的宽高、主题以及生成方式; 93 | ### 生成方式 94 | 默认为png,目前支持大模型根据上下文自己选择`output_type` 95 | 96 | ### 宽高 97 | 目前默认不指定宽高,`html`下默认占满整个页面,'png'下默认为`1000 * 1000` 98 | 99 | ### 主题 100 | 目前默认主题为`'light'`,VChart图表支持多种主题,详见[主题](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension) 101 | 102 | 103 | ## 测试 104 | 105 | 当前设置了三种不同难度的任务用于测试 106 | 107 | ### 简单图表生成任务 108 | 109 | 给予数据和具体的图表生成需求,测试结果,执行命令: 110 | ```bash 111 | python -m app.tool.chart_visualization.test.simple_chart 112 | ``` 113 | 结果应位于`worksapce\visualization`下,涉及到9种不同的图表结果 114 | 115 | ### 简单数据报表任务 116 | 117 | 给予简单原始数据可分析需求,需要对数据进行简单加工处理,执行命令: 118 | ```bash 119 | python -m app.tool.chart_visualization.test.simple_report 120 | ``` 121 | 结果同样位于`worksapce\visualization`下 122 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/__init__.py: -------------------------------------------------------------------------------- 1 | from app.tool.chart_visualization.chart_prepare import VisualizationPrepare 2 | from app.tool.chart_visualization.data_visualization import DataVisualization 3 | from app.tool.chart_visualization.python_execute import NormalPythonExecute 4 | 5 | 6 | __all__ = ["DataVisualization", "VisualizationPrepare", "NormalPythonExecute"] 7 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/chart_prepare.py: -------------------------------------------------------------------------------- 1 | from app.tool.chart_visualization.python_execute import NormalPythonExecute 2 | 3 | 4 | class VisualizationPrepare(NormalPythonExecute): 5 | """A tool for Chart Generation Preparation""" 6 | 7 | name: str = "visualization_preparation" 8 | description: str = "Using Python code to generates metadata of data_visualization tool. Outputs: 1) JSON Information. 2) Cleaned CSV data files (Optional)." 9 | parameters: dict = { 10 | "type": "object", 11 | "properties": { 12 | "code_type": { 13 | "description": "code type, visualization: csv -> chart; insight: choose insight into chart", 14 | "type": "string", 15 | "default": "visualization", 16 | "enum": ["visualization", "insight"], 17 | }, 18 | "code": { 19 | "type": "string", 20 | "description": """Python code for data_visualization prepare. 21 | ## Visualization Type 22 | 1. Data loading logic 23 | 2. Csv Data and chart description generate 24 | 2.1 Csv data (The data you want to visulazation, cleaning / transform from origin data, saved in .csv) 25 | 2.2 Chart description of csv data (The chart title or description should be concise and clear. Examples: 'Product sales distribution', 'Monthly revenue trend'.) 26 | 3. Save information in json file.( format: {"csvFilePath": string, "chartTitle": string}[]) 27 | ## Insight Type 28 | 1. Select the insights from the data_visualization results that you want to add to the chart. 29 | 2. Save information in json file.( format: {"chartPath": string, "insights_id": number[]}[]) 30 | # Note 31 | 1. You can generate one or multiple csv data with different visualization needs. 32 | 2. Make each chart data esay, clean and different. 33 | 3. Json file saving in utf-8 with path print: print(json_path) 34 | """, 35 | }, 36 | }, 37 | "required": ["code", "code_type"], 38 | } 39 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chart_visualization", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "devDependencies": { 6 | "@types/node": "^22.10.1", 7 | "ts-node": "^10.9.2", 8 | "typescript": "^5.7.2" 9 | }, 10 | "dependencies": { 11 | "@visactor/vchart": "^1.13.7", 12 | "@visactor/vmind": "2.0.5", 13 | "canvas": "^2.11.2", 14 | "get-stdin": "^9.0.0" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "description": "" 22 | } 23 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/python_execute.py: -------------------------------------------------------------------------------- 1 | from app.config import config 2 | from app.tool.python_execute import PythonExecute 3 | 4 | 5 | class NormalPythonExecute(PythonExecute): 6 | """A tool for executing Python code with timeout and safety restrictions.""" 7 | 8 | name: str = "python_execute" 9 | description: str = """Execute Python code for in-depth data analysis / data report(task conclusion) / other normal task without direct visualization.""" 10 | parameters: dict = { 11 | "type": "object", 12 | "properties": { 13 | "code_type": { 14 | "description": "code type, data process / data report / others", 15 | "type": "string", 16 | "default": "process", 17 | "enum": ["process", "report", "others"], 18 | }, 19 | "code": { 20 | "type": "string", 21 | "description": """Python code to execute. 22 | # Note 23 | 1. The code should generate a comprehensive text-based report containing dataset overview, column details, basic statistics, derived metrics, timeseries comparisons, outliers, and key insights. 24 | 2. Use print() for all outputs so the analysis (including sections like 'Dataset Overview' or 'Preprocessing Results') is clearly visible and save it also 25 | 3. Save any report / processed files / each analysis result in worksapce directory: {directory} 26 | 4. Data reports need to be content-rich, including your overall analysis process and corresponding data visualization. 27 | 5. You can invode this tool step-by-step to do data analysis from summary to in-depth with data report saved also""".format( 28 | directory=config.workspace_root 29 | ), 30 | }, 31 | }, 32 | "required": ["code"], 33 | } 34 | 35 | async def execute(self, code: str, code_type: str | None = None, timeout=5): 36 | return await super().execute(code, timeout) 37 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/test/chart_demo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from app.agent.data_analysis import DataAnalysis 4 | from app.logger import logger 5 | 6 | 7 | prefix = "Help me generate charts and save them locally, specifically:" 8 | tasks = [ 9 | { 10 | "prompt": "Help me show the sales of different products in different regions", 11 | "data": """Product Name,Region,Sales 12 | Coke,South,2350 13 | Coke,East,1027 14 | Coke,West,1027 15 | Coke,North,1027 16 | Sprite,South,215 17 | Sprite,East,654 18 | Sprite,West,159 19 | Sprite,North,28 20 | Fanta,South,345 21 | Fanta,East,654 22 | Fanta,West,2100 23 | Fanta,North,1679 24 | Xingmu,South,1476 25 | Xingmu,East,830 26 | Xingmu,West,532 27 | Xingmu,North,498 28 | """, 29 | }, 30 | { 31 | "prompt": "Show market share of each brand", 32 | "data": """Brand Name,Market Share,Average Price,Net Profit 33 | Apple,0.5,7068,314531 34 | Samsung,0.2,6059,362345 35 | Vivo,0.05,3406,234512 36 | Nokia,0.01,1064,-1345 37 | Xiaomi,0.1,4087,131345""", 38 | }, 39 | { 40 | "prompt": "Please help me show the sales trend of each product", 41 | "data": """Date,Type,Value 42 | 2023-01-01,Product A,52.9 43 | 2023-01-01,Product B,63.6 44 | 2023-01-01,Product C,11.2 45 | 2023-01-02,Product A,45.7 46 | 2023-01-02,Product B,89.1 47 | 2023-01-02,Product C,21.4 48 | 2023-01-03,Product A,67.2 49 | 2023-01-03,Product B,82.4 50 | 2023-01-03,Product C,31.7 51 | 2023-01-04,Product A,80.7 52 | 2023-01-04,Product B,55.1 53 | 2023-01-04,Product C,21.1 54 | 2023-01-05,Product A,65.6 55 | 2023-01-05,Product B,78 56 | 2023-01-05,Product C,31.3 57 | 2023-01-06,Product A,75.6 58 | 2023-01-06,Product B,89.1 59 | 2023-01-06,Product C,63.5 60 | 2023-01-07,Product A,67.3 61 | 2023-01-07,Product B,77.2 62 | 2023-01-07,Product C,43.7 63 | 2023-01-08,Product A,96.1 64 | 2023-01-08,Product B,97.6 65 | 2023-01-08,Product C,59.9 66 | 2023-01-09,Product A,96.1 67 | 2023-01-09,Product B,100.6 68 | 2023-01-09,Product C,66.8 69 | 2023-01-10,Product A,101.6 70 | 2023-01-10,Product B,108.3 71 | 2023-01-10,Product C,56.9""", 72 | }, 73 | { 74 | "prompt": "Show the popularity of search keywords", 75 | "data": """Keyword,Popularity 76 | Hot Word,1000 77 | Zao Le Wo Men,800 78 | Rao Jian Huo,400 79 | My Wish is World Peace,400 80 | Xiu Xiu Xiu,400 81 | Shenzhou 11,400 82 | Hundred Birds Facing the Wind,400 83 | China Women's Volleyball Team,400 84 | My Guan Na,400 85 | Leg Dong,400 86 | Hot Pot Hero,400 87 | Baby's Heart is Bitter,400 88 | Olympics,400 89 | Awesome My Brother,400 90 | Poetry and Distance,400 91 | Song Joong-ki,400 92 | PPAP,400 93 | Blue Thin Mushroom,400 94 | Rain Dew Evenly,400 95 | Friendship's Little Boat Says It Flips,400 96 | Beijing Slump,400 97 | Dedication,200 98 | Apple,200 99 | Dog Belt,200 100 | Old Driver,200 101 | Melon-Eating Crowd,200 102 | Zootopia,200 103 | City Will Play,200 104 | Routine,200 105 | Water Reverse,200 106 | Why Don't You Go to Heaven,200 107 | Snake Spirit Man,200 108 | Why Don't You Go to Heaven,200 109 | Samsung Explosion Gate,200 110 | Little Li Oscar,200 111 | Ugly People Need to Read More,200 112 | Boyfriend Power,200 113 | A Face of Confusion,200 114 | Descendants of the Sun,200""", 115 | }, 116 | { 117 | "prompt": "Help me compare the performance of different electric vehicle brands using a scatter plot", 118 | "data": """Range,Charging Time,Brand Name,Average Price 119 | 2904,46,Brand1,2350 120 | 1231,146,Brand2,1027 121 | 5675,324,Brand3,1242 122 | 543,57,Brand4,6754 123 | 326,234,Brand5,215 124 | 1124,67,Brand6,654 125 | 3426,81,Brand7,159 126 | 2134,24,Brand8,28 127 | 1234,52,Brand9,345 128 | 2345,27,Brand10,654 129 | 526,145,Brand11,2100 130 | 234,93,Brand12,1679 131 | 567,94,Brand13,1476 132 | 789,45,Brand14,830 133 | 469,75,Brand15,532 134 | 5689,54,Brand16,498 135 | """, 136 | }, 137 | { 138 | "prompt": "Show conversion rates for each process", 139 | "data": """Process,Conversion Rate,Month 140 | Step1,100,1 141 | Step2,80,1 142 | Step3,60,1 143 | Step4,40,1""", 144 | }, 145 | { 146 | "prompt": "Show the difference in breakfast consumption between men and women", 147 | "data": """Day,Men-Breakfast,Women-Breakfast 148 | Monday,15,22 149 | Tuesday,12,10 150 | Wednesday,15,20 151 | Thursday,10,12 152 | Friday,13,15 153 | Saturday,10,15 154 | Sunday,12,14""", 155 | }, 156 | { 157 | "prompt": "Help me show this person's performance in different aspects, is he a hexagonal warrior", 158 | "data": """dimension,performance 159 | Strength,5 160 | Speed,5 161 | Shooting,3 162 | Endurance,5 163 | Precision,5 164 | Growth,5""", 165 | }, 166 | { 167 | "prompt": "Show data flow", 168 | "data": """Origin,Destination,value 169 | Node A,Node 1,10 170 | Node A,Node 2,5 171 | Node B,Node 2,8 172 | Node B,Node 3,2 173 | Node C,Node 2,4 174 | Node A,Node C,2 175 | Node C,Node 1,2""", 176 | }, 177 | ] 178 | 179 | 180 | async def main(): 181 | for index, item in enumerate(tasks): 182 | logger.info(f"Begin task {index} / {len(tasks)}!") 183 | agent = DataAnalysis() 184 | await agent.run( 185 | f"{prefix},chart_description:{item['prompt']},Data:{item['data']}" 186 | ) 187 | logger.info(f"Finish with {item['prompt']}") 188 | 189 | 190 | if __name__ == "__main__": 191 | asyncio.run(main()) 192 | -------------------------------------------------------------------------------- /app/tool/chart_visualization/test/report_demo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from app.agent.data_analysis import DataAnalysis 4 | 5 | 6 | # from app.agent.manus import Manus 7 | 8 | 9 | async def main(): 10 | agent = DataAnalysis() 11 | # agent = Manus() 12 | await agent.run( 13 | """Requirement: 14 | 1. Analyze the following data and generate a graphical data report in HTML format. The final product should be a data report. 15 | Data: 16 | Month | Team A | Team B | Team C 17 | January | 1200 hours | 1350 hours | 1100 hours 18 | February | 1250 hours | 1400 hours | 1150 hours 19 | March | 1180 hours | 1300 hours | 1300 hours 20 | April | 1220 hours | 1280 hours | 1400 hours 21 | May | 1230 hours | 1320 hours | 1450 hours 22 | June | 1200 hours | 1250 hours | 1500 hours """ 23 | ) 24 | 25 | 26 | if __name__ == "__main__": 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /app/tool/create_chat_completion.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional, Type, Union, get_args, get_origin 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | from app.tool import BaseTool 6 | 7 | 8 | class CreateChatCompletion(BaseTool): 9 | name: str = "create_chat_completion" 10 | description: str = ( 11 | "Creates a structured completion with specified output formatting." 12 | ) 13 | 14 | # Type mapping for JSON schema 15 | type_mapping: dict = { 16 | str: "string", 17 | int: "integer", 18 | float: "number", 19 | bool: "boolean", 20 | dict: "object", 21 | list: "array", 22 | } 23 | response_type: Optional[Type] = None 24 | required: List[str] = Field(default_factory=lambda: ["response"]) 25 | 26 | def __init__(self, response_type: Optional[Type] = str): 27 | """Initialize with a specific response type.""" 28 | super().__init__() 29 | self.response_type = response_type 30 | self.parameters = self._build_parameters() 31 | 32 | def _build_parameters(self) -> dict: 33 | """Build parameters schema based on response type.""" 34 | if self.response_type == str: 35 | return { 36 | "type": "object", 37 | "properties": { 38 | "response": { 39 | "type": "string", 40 | "description": "The response text that should be delivered to the user.", 41 | }, 42 | }, 43 | "required": self.required, 44 | } 45 | 46 | if isinstance(self.response_type, type) and issubclass( 47 | self.response_type, BaseModel 48 | ): 49 | schema = self.response_type.model_json_schema() 50 | return { 51 | "type": "object", 52 | "properties": schema["properties"], 53 | "required": schema.get("required", self.required), 54 | } 55 | 56 | return self._create_type_schema(self.response_type) 57 | 58 | def _create_type_schema(self, type_hint: Type) -> dict: 59 | """Create a JSON schema for the given type.""" 60 | origin = get_origin(type_hint) 61 | args = get_args(type_hint) 62 | 63 | # Handle primitive types 64 | if origin is None: 65 | return { 66 | "type": "object", 67 | "properties": { 68 | "response": { 69 | "type": self.type_mapping.get(type_hint, "string"), 70 | "description": f"Response of type {type_hint.__name__}", 71 | } 72 | }, 73 | "required": self.required, 74 | } 75 | 76 | # Handle List type 77 | if origin is list: 78 | item_type = args[0] if args else Any 79 | return { 80 | "type": "object", 81 | "properties": { 82 | "response": { 83 | "type": "array", 84 | "items": self._get_type_info(item_type), 85 | } 86 | }, 87 | "required": self.required, 88 | } 89 | 90 | # Handle Dict type 91 | if origin is dict: 92 | value_type = args[1] if len(args) > 1 else Any 93 | return { 94 | "type": "object", 95 | "properties": { 96 | "response": { 97 | "type": "object", 98 | "additionalProperties": self._get_type_info(value_type), 99 | } 100 | }, 101 | "required": self.required, 102 | } 103 | 104 | # Handle Union type 105 | if origin is Union: 106 | return self._create_union_schema(args) 107 | 108 | return self._build_parameters() 109 | 110 | def _get_type_info(self, type_hint: Type) -> dict: 111 | """Get type information for a single type.""" 112 | if isinstance(type_hint, type) and issubclass(type_hint, BaseModel): 113 | return type_hint.model_json_schema() 114 | 115 | return { 116 | "type": self.type_mapping.get(type_hint, "string"), 117 | "description": f"Value of type {getattr(type_hint, '__name__', 'any')}", 118 | } 119 | 120 | def _create_union_schema(self, types: tuple) -> dict: 121 | """Create schema for Union types.""" 122 | return { 123 | "type": "object", 124 | "properties": { 125 | "response": {"anyOf": [self._get_type_info(t) for t in types]} 126 | }, 127 | "required": self.required, 128 | } 129 | 130 | async def execute(self, required: list | None = None, **kwargs) -> Any: 131 | """Execute the chat completion with type conversion. 132 | 133 | Args: 134 | required: List of required field names or None 135 | **kwargs: Response data 136 | 137 | Returns: 138 | Converted response based on response_type 139 | """ 140 | required = required or self.required 141 | 142 | # Handle case when required is a list 143 | if isinstance(required, list) and len(required) > 0: 144 | if len(required) == 1: 145 | required_field = required[0] 146 | result = kwargs.get(required_field, "") 147 | else: 148 | # Return multiple fields as a dictionary 149 | return {field: kwargs.get(field, "") for field in required} 150 | else: 151 | required_field = "response" 152 | result = kwargs.get(required_field, "") 153 | 154 | # Type conversion logic 155 | if self.response_type == str: 156 | return result 157 | 158 | if isinstance(self.response_type, type) and issubclass( 159 | self.response_type, BaseModel 160 | ): 161 | return self.response_type(**kwargs) 162 | 163 | if get_origin(self.response_type) in (list, dict): 164 | return result # Assuming result is already in correct format 165 | 166 | try: 167 | return self.response_type(result) 168 | except (ValueError, TypeError): 169 | return result 170 | -------------------------------------------------------------------------------- /app/tool/file_operators.py: -------------------------------------------------------------------------------- 1 | """File operation interfaces and implementations for local and sandbox environments.""" 2 | 3 | import asyncio 4 | from pathlib import Path 5 | from typing import Optional, Protocol, Tuple, Union, runtime_checkable 6 | 7 | from app.config import SandboxSettings 8 | from app.exceptions import ToolError 9 | from app.sandbox.client import SANDBOX_CLIENT 10 | 11 | 12 | PathLike = Union[str, Path] 13 | 14 | 15 | @runtime_checkable 16 | class FileOperator(Protocol): 17 | """Interface for file operations in different environments.""" 18 | 19 | async def read_file(self, path: PathLike) -> str: 20 | """Read content from a file.""" 21 | ... 22 | 23 | async def write_file(self, path: PathLike, content: str) -> None: 24 | """Write content to a file.""" 25 | ... 26 | 27 | async def is_directory(self, path: PathLike) -> bool: 28 | """Check if path points to a directory.""" 29 | ... 30 | 31 | async def exists(self, path: PathLike) -> bool: 32 | """Check if path exists.""" 33 | ... 34 | 35 | async def run_command( 36 | self, cmd: str, timeout: Optional[float] = 120.0 37 | ) -> Tuple[int, str, str]: 38 | """Run a shell command and return (return_code, stdout, stderr).""" 39 | ... 40 | 41 | 42 | class LocalFileOperator(FileOperator): 43 | """File operations implementation for local filesystem.""" 44 | 45 | encoding: str = "utf-8" 46 | 47 | async def read_file(self, path: PathLike) -> str: 48 | """Read content from a local file.""" 49 | try: 50 | return Path(path).read_text(encoding=self.encoding) 51 | except Exception as e: 52 | raise ToolError(f"Failed to read {path}: {str(e)}") from None 53 | 54 | async def write_file(self, path: PathLike, content: str) -> None: 55 | """Write content to a local file.""" 56 | try: 57 | Path(path).write_text(content, encoding=self.encoding) 58 | except Exception as e: 59 | raise ToolError(f"Failed to write to {path}: {str(e)}") from None 60 | 61 | async def is_directory(self, path: PathLike) -> bool: 62 | """Check if path points to a directory.""" 63 | return Path(path).is_dir() 64 | 65 | async def exists(self, path: PathLike) -> bool: 66 | """Check if path exists.""" 67 | return Path(path).exists() 68 | 69 | async def run_command( 70 | self, cmd: str, timeout: Optional[float] = 120.0 71 | ) -> Tuple[int, str, str]: 72 | """Run a shell command locally.""" 73 | process = await asyncio.create_subprocess_shell( 74 | cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 75 | ) 76 | 77 | try: 78 | stdout, stderr = await asyncio.wait_for( 79 | process.communicate(), timeout=timeout 80 | ) 81 | return ( 82 | process.returncode or 0, 83 | stdout.decode(), 84 | stderr.decode(), 85 | ) 86 | except asyncio.TimeoutError as exc: 87 | try: 88 | process.kill() 89 | except ProcessLookupError: 90 | pass 91 | raise TimeoutError( 92 | f"Command '{cmd}' timed out after {timeout} seconds" 93 | ) from exc 94 | 95 | 96 | class SandboxFileOperator(FileOperator): 97 | """File operations implementation for sandbox environment.""" 98 | 99 | def __init__(self): 100 | self.sandbox_client = SANDBOX_CLIENT 101 | 102 | async def _ensure_sandbox_initialized(self): 103 | """Ensure sandbox is initialized.""" 104 | if not self.sandbox_client.sandbox: 105 | await self.sandbox_client.create(config=SandboxSettings()) 106 | 107 | async def read_file(self, path: PathLike) -> str: 108 | """Read content from a file in sandbox.""" 109 | await self._ensure_sandbox_initialized() 110 | try: 111 | return await self.sandbox_client.read_file(str(path)) 112 | except Exception as e: 113 | raise ToolError(f"Failed to read {path} in sandbox: {str(e)}") from None 114 | 115 | async def write_file(self, path: PathLike, content: str) -> None: 116 | """Write content to a file in sandbox.""" 117 | await self._ensure_sandbox_initialized() 118 | try: 119 | await self.sandbox_client.write_file(str(path), content) 120 | except Exception as e: 121 | raise ToolError(f"Failed to write to {path} in sandbox: {str(e)}") from None 122 | 123 | async def is_directory(self, path: PathLike) -> bool: 124 | """Check if path points to a directory in sandbox.""" 125 | await self._ensure_sandbox_initialized() 126 | result = await self.sandbox_client.run_command( 127 | f"test -d {path} && echo 'true' || echo 'false'" 128 | ) 129 | return result.strip() == "true" 130 | 131 | async def exists(self, path: PathLike) -> bool: 132 | """Check if path exists in sandbox.""" 133 | await self._ensure_sandbox_initialized() 134 | result = await self.sandbox_client.run_command( 135 | f"test -e {path} && echo 'true' || echo 'false'" 136 | ) 137 | return result.strip() == "true" 138 | 139 | async def run_command( 140 | self, cmd: str, timeout: Optional[float] = 120.0 141 | ) -> Tuple[int, str, str]: 142 | """Run a command in sandbox environment.""" 143 | await self._ensure_sandbox_initialized() 144 | try: 145 | stdout = await self.sandbox_client.run_command( 146 | cmd, timeout=int(timeout) if timeout else None 147 | ) 148 | return ( 149 | 0, # Always return 0 since we don't have explicit return code from sandbox 150 | stdout, 151 | "", # No stderr capture in the current sandbox implementation 152 | ) 153 | except TimeoutError as exc: 154 | raise TimeoutError( 155 | f"Command '{cmd}' timed out after {timeout} seconds in sandbox" 156 | ) from exc 157 | except Exception as exc: 158 | return 1, "", f"Error executing command in sandbox: {str(exc)}" 159 | -------------------------------------------------------------------------------- /app/tool/mcp.py: -------------------------------------------------------------------------------- 1 | from contextlib import AsyncExitStack 2 | from typing import Dict, List, Optional 3 | 4 | from mcp import ClientSession, StdioServerParameters 5 | from mcp.client.sse import sse_client 6 | from mcp.client.stdio import stdio_client 7 | from mcp.types import TextContent 8 | 9 | from app.logger import logger 10 | from app.tool.base import BaseTool, ToolResult 11 | from app.tool.tool_collection import ToolCollection 12 | 13 | 14 | class MCPClientTool(BaseTool): 15 | """Represents a tool proxy that can be called on the MCP server from the client side.""" 16 | 17 | session: Optional[ClientSession] = None 18 | server_id: str = "" # Add server identifier 19 | original_name: str = "" 20 | 21 | async def execute(self, **kwargs) -> ToolResult: 22 | """Execute the tool by making a remote call to the MCP server.""" 23 | if not self.session: 24 | return ToolResult(error="Not connected to MCP server") 25 | 26 | try: 27 | logger.info(f"Executing tool: {self.original_name}") 28 | result = await self.session.call_tool(self.original_name, kwargs) 29 | content_str = ", ".join( 30 | item.text for item in result.content if isinstance(item, TextContent) 31 | ) 32 | return ToolResult(output=content_str or "No output returned.") 33 | except Exception as e: 34 | return ToolResult(error=f"Error executing tool: {str(e)}") 35 | 36 | 37 | class MCPClients(ToolCollection): 38 | """ 39 | A collection of tools that connects to multiple MCP servers and manages available tools through the Model Context Protocol. 40 | """ 41 | 42 | sessions: Dict[str, ClientSession] = {} 43 | exit_stacks: Dict[str, AsyncExitStack] = {} 44 | description: str = "MCP client tools for server interaction" 45 | 46 | def __init__(self): 47 | super().__init__() # Initialize with empty tools list 48 | self.name = "mcp" # Keep name for backward compatibility 49 | 50 | async def connect_sse(self, server_url: str, server_id: str = "") -> None: 51 | """Connect to an MCP server using SSE transport.""" 52 | if not server_url: 53 | raise ValueError("Server URL is required.") 54 | 55 | server_id = server_id or server_url 56 | 57 | # Always ensure clean disconnection before new connection 58 | if server_id in self.sessions: 59 | await self.disconnect(server_id) 60 | 61 | exit_stack = AsyncExitStack() 62 | self.exit_stacks[server_id] = exit_stack 63 | 64 | streams_context = sse_client(url=server_url) 65 | streams = await exit_stack.enter_async_context(streams_context) 66 | session = await exit_stack.enter_async_context(ClientSession(*streams)) 67 | self.sessions[server_id] = session 68 | 69 | await self._initialize_and_list_tools(server_id) 70 | 71 | async def connect_stdio( 72 | self, command: str, args: List[str], server_id: str = "" 73 | ) -> None: 74 | """Connect to an MCP server using stdio transport.""" 75 | if not command: 76 | raise ValueError("Server command is required.") 77 | 78 | server_id = server_id or command 79 | 80 | # Always ensure clean disconnection before new connection 81 | if server_id in self.sessions: 82 | await self.disconnect(server_id) 83 | 84 | exit_stack = AsyncExitStack() 85 | self.exit_stacks[server_id] = exit_stack 86 | 87 | server_params = StdioServerParameters(command=command, args=args) 88 | stdio_transport = await exit_stack.enter_async_context( 89 | stdio_client(server_params) 90 | ) 91 | read, write = stdio_transport 92 | session = await exit_stack.enter_async_context(ClientSession(read, write)) 93 | self.sessions[server_id] = session 94 | 95 | await self._initialize_and_list_tools(server_id) 96 | 97 | async def _initialize_and_list_tools(self, server_id: str) -> None: 98 | """Initialize session and populate tool map.""" 99 | session = self.sessions.get(server_id) 100 | if not session: 101 | raise RuntimeError(f"Session not initialized for server {server_id}") 102 | 103 | await session.initialize() 104 | response = await session.list_tools() 105 | 106 | # Create proper tool objects for each server tool 107 | for tool in response.tools: 108 | original_name = tool.name 109 | # Always prefix with server_id to ensure uniqueness 110 | tool_name = f"mcp_{server_id}_{original_name}" 111 | 112 | server_tool = MCPClientTool( 113 | name=tool_name, 114 | description=tool.description, 115 | parameters=tool.inputSchema, 116 | session=session, 117 | server_id=server_id, 118 | original_name=original_name, 119 | ) 120 | self.tool_map[tool_name] = server_tool 121 | 122 | # Update tools tuple 123 | self.tools = tuple(self.tool_map.values()) 124 | logger.info( 125 | f"Connected to server {server_id} with tools: {[tool.name for tool in response.tools]}" 126 | ) 127 | 128 | async def disconnect(self, server_id: str = "") -> None: 129 | """Disconnect from a specific MCP server or all servers if no server_id provided.""" 130 | if server_id: 131 | if server_id in self.sessions: 132 | try: 133 | exit_stack = self.exit_stacks.get(server_id) 134 | 135 | # Close the exit stack which will handle session cleanup 136 | if exit_stack: 137 | try: 138 | await exit_stack.aclose() 139 | except RuntimeError as e: 140 | if "cancel scope" in str(e).lower(): 141 | logger.warning( 142 | f"Cancel scope error during disconnect from {server_id}, continuing with cleanup: {e}" 143 | ) 144 | else: 145 | raise 146 | 147 | # Clean up references 148 | self.sessions.pop(server_id, None) 149 | self.exit_stacks.pop(server_id, None) 150 | 151 | # Remove tools associated with this server 152 | self.tool_map = { 153 | k: v 154 | for k, v in self.tool_map.items() 155 | if v.server_id != server_id 156 | } 157 | self.tools = tuple(self.tool_map.values()) 158 | logger.info(f"Disconnected from MCP server {server_id}") 159 | except Exception as e: 160 | logger.error(f"Error disconnecting from server {server_id}: {e}") 161 | else: 162 | # Disconnect from all servers in a deterministic order 163 | for sid in sorted(list(self.sessions.keys())): 164 | await self.disconnect(sid) 165 | self.tool_map = {} 166 | self.tools = tuple() 167 | logger.info("Disconnected from all MCP servers") 168 | -------------------------------------------------------------------------------- /app/tool/python_execute.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import sys 3 | from io import StringIO 4 | from typing import Dict 5 | 6 | from app.tool.base import BaseTool 7 | 8 | 9 | class PythonExecute(BaseTool): 10 | """A tool for executing Python code with timeout and safety restrictions.""" 11 | 12 | name: str = "python_execute" 13 | description: str = "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results." 14 | parameters: dict = { 15 | "type": "object", 16 | "properties": { 17 | "code": { 18 | "type": "string", 19 | "description": "The Python code to execute.", 20 | }, 21 | }, 22 | "required": ["code"], 23 | } 24 | 25 | def _run_code(self, code: str, result_dict: dict, safe_globals: dict) -> None: 26 | original_stdout = sys.stdout 27 | try: 28 | output_buffer = StringIO() 29 | sys.stdout = output_buffer 30 | exec(code, safe_globals, safe_globals) 31 | result_dict["observation"] = output_buffer.getvalue() 32 | result_dict["success"] = True 33 | except Exception as e: 34 | result_dict["observation"] = str(e) 35 | result_dict["success"] = False 36 | finally: 37 | sys.stdout = original_stdout 38 | 39 | async def execute( 40 | self, 41 | code: str, 42 | timeout: int = 5, 43 | ) -> Dict: 44 | """ 45 | Executes the provided Python code with a timeout. 46 | 47 | Args: 48 | code (str): The Python code to execute. 49 | timeout (int): Execution timeout in seconds. 50 | 51 | Returns: 52 | Dict: Contains 'output' with execution output or error message and 'success' status. 53 | """ 54 | 55 | with multiprocessing.Manager() as manager: 56 | result = manager.dict({"observation": "", "success": False}) 57 | if isinstance(__builtins__, dict): 58 | safe_globals = {"__builtins__": __builtins__} 59 | else: 60 | safe_globals = {"__builtins__": __builtins__.__dict__.copy()} 61 | proc = multiprocessing.Process( 62 | target=self._run_code, args=(code, result, safe_globals) 63 | ) 64 | proc.start() 65 | proc.join(timeout) 66 | 67 | # timeout process 68 | if proc.is_alive(): 69 | proc.terminate() 70 | proc.join(1) 71 | return { 72 | "observation": f"Execution timeout after {timeout} seconds", 73 | "success": False, 74 | } 75 | return dict(result) 76 | -------------------------------------------------------------------------------- /app/tool/search/__init__.py: -------------------------------------------------------------------------------- 1 | from app.tool.search.baidu_search import BaiduSearchEngine 2 | from app.tool.search.base import WebSearchEngine 3 | from app.tool.search.bing_search import BingSearchEngine 4 | from app.tool.search.duckduckgo_search import DuckDuckGoSearchEngine 5 | from app.tool.search.google_search import GoogleSearchEngine 6 | 7 | 8 | __all__ = [ 9 | "WebSearchEngine", 10 | "BaiduSearchEngine", 11 | "DuckDuckGoSearchEngine", 12 | "GoogleSearchEngine", 13 | "BingSearchEngine", 14 | ] 15 | -------------------------------------------------------------------------------- /app/tool/search/baidu_search.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from baidusearch.baidusearch import search 4 | 5 | from app.tool.search.base import SearchItem, WebSearchEngine 6 | 7 | 8 | class BaiduSearchEngine(WebSearchEngine): 9 | def perform_search( 10 | self, query: str, num_results: int = 10, *args, **kwargs 11 | ) -> List[SearchItem]: 12 | """ 13 | Baidu search engine. 14 | 15 | Returns results formatted according to SearchItem model. 16 | """ 17 | raw_results = search(query, num_results=num_results) 18 | 19 | # Convert raw results to SearchItem format 20 | results = [] 21 | for i, item in enumerate(raw_results): 22 | if isinstance(item, str): 23 | # If it's just a URL 24 | results.append( 25 | SearchItem(title=f"Baidu Result {i+1}", url=item, description=None) 26 | ) 27 | elif isinstance(item, dict): 28 | # If it's a dictionary with details 29 | results.append( 30 | SearchItem( 31 | title=item.get("title", f"Baidu Result {i+1}"), 32 | url=item.get("url", ""), 33 | description=item.get("abstract", None), 34 | ) 35 | ) 36 | else: 37 | # Try to get attributes directly 38 | try: 39 | results.append( 40 | SearchItem( 41 | title=getattr(item, "title", f"Baidu Result {i+1}"), 42 | url=getattr(item, "url", ""), 43 | description=getattr(item, "abstract", None), 44 | ) 45 | ) 46 | except Exception: 47 | # Fallback to a basic result 48 | results.append( 49 | SearchItem( 50 | title=f"Baidu Result {i+1}", url=str(item), description=None 51 | ) 52 | ) 53 | 54 | return results 55 | -------------------------------------------------------------------------------- /app/tool/search/base.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class SearchItem(BaseModel): 7 | """Represents a single search result item""" 8 | 9 | title: str = Field(description="The title of the search result") 10 | url: str = Field(description="The URL of the search result") 11 | description: Optional[str] = Field( 12 | default=None, description="A description or snippet of the search result" 13 | ) 14 | 15 | def __str__(self) -> str: 16 | """String representation of a search result item.""" 17 | return f"{self.title} - {self.url}" 18 | 19 | 20 | class WebSearchEngine(BaseModel): 21 | """Base class for web search engines.""" 22 | 23 | model_config = {"arbitrary_types_allowed": True} 24 | 25 | def perform_search( 26 | self, query: str, num_results: int = 10, *args, **kwargs 27 | ) -> List[SearchItem]: 28 | """ 29 | Perform a web search and return a list of search items. 30 | 31 | Args: 32 | query (str): The search query to submit to the search engine. 33 | num_results (int, optional): The number of search results to return. Default is 10. 34 | args: Additional arguments. 35 | kwargs: Additional keyword arguments. 36 | 37 | Returns: 38 | List[SearchItem]: A list of SearchItem objects matching the search query. 39 | """ 40 | raise NotImplementedError 41 | -------------------------------------------------------------------------------- /app/tool/search/bing_search.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | import requests 4 | from bs4 import BeautifulSoup 5 | 6 | from app.logger import logger 7 | from app.tool.search.base import SearchItem, WebSearchEngine 8 | 9 | 10 | ABSTRACT_MAX_LENGTH = 300 11 | 12 | USER_AGENTS = [ 13 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", 14 | "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", 15 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36", 16 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR) AppleWebKit/533.3 (KHTML, like Gecko) QtWeb Internet Browser/3.7 http://www.QtWeb.net", 17 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", 18 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) ChromePlus/4.0.222.3 Chrome/4.0.222.3 Safari/532.2", 19 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4pre) Gecko/20070404 K-Ninja/2.1.3", 20 | "Mozilla/5.0 (Future Star Technologies Corp.; Star-Blade OS; x86_64; U; en-US) iNet Browser 4.7", 21 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201", 22 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080414 Firefox/2.0.0.13 Pogo/2.0.0.13.6866", 23 | ] 24 | 25 | HEADERS = { 26 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 27 | "Content-Type": "application/x-www-form-urlencoded", 28 | "User-Agent": USER_AGENTS[0], 29 | "Referer": "https://www.bing.com/", 30 | "Accept-Encoding": "gzip, deflate", 31 | "Accept-Language": "zh-CN,zh;q=0.9", 32 | } 33 | 34 | BING_HOST_URL = "https://www.bing.com" 35 | BING_SEARCH_URL = "https://www.bing.com/search?q=" 36 | 37 | 38 | class BingSearchEngine(WebSearchEngine): 39 | session: Optional[requests.Session] = None 40 | 41 | def __init__(self, **data): 42 | """Initialize the BingSearch tool with a requests session.""" 43 | super().__init__(**data) 44 | self.session = requests.Session() 45 | self.session.headers.update(HEADERS) 46 | 47 | def _search_sync(self, query: str, num_results: int = 10) -> List[SearchItem]: 48 | """ 49 | Synchronous Bing search implementation to retrieve search results. 50 | 51 | Args: 52 | query (str): The search query to submit to Bing. 53 | num_results (int, optional): Maximum number of results to return. Defaults to 10. 54 | 55 | Returns: 56 | List[SearchItem]: A list of search items with title, URL, and description. 57 | """ 58 | if not query: 59 | return [] 60 | 61 | list_result = [] 62 | first = 1 63 | next_url = BING_SEARCH_URL + query 64 | 65 | while len(list_result) < num_results: 66 | data, next_url = self._parse_html( 67 | next_url, rank_start=len(list_result), first=first 68 | ) 69 | if data: 70 | list_result.extend(data) 71 | if not next_url: 72 | break 73 | first += 10 74 | 75 | return list_result[:num_results] 76 | 77 | def _parse_html( 78 | self, url: str, rank_start: int = 0, first: int = 1 79 | ) -> Tuple[List[SearchItem], str]: 80 | """ 81 | Parse Bing search result HTML to extract search results and the next page URL. 82 | 83 | Returns: 84 | tuple: (List of SearchItem objects, next page URL or None) 85 | """ 86 | try: 87 | res = self.session.get(url=url) 88 | res.encoding = "utf-8" 89 | root = BeautifulSoup(res.text, "lxml") 90 | 91 | list_data = [] 92 | ol_results = root.find("ol", id="b_results") 93 | if not ol_results: 94 | return [], None 95 | 96 | for li in ol_results.find_all("li", class_="b_algo"): 97 | title = "" 98 | url = "" 99 | abstract = "" 100 | try: 101 | h2 = li.find("h2") 102 | if h2: 103 | title = h2.text.strip() 104 | url = h2.a["href"].strip() 105 | 106 | p = li.find("p") 107 | if p: 108 | abstract = p.text.strip() 109 | 110 | if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: 111 | abstract = abstract[:ABSTRACT_MAX_LENGTH] 112 | 113 | rank_start += 1 114 | 115 | # Create a SearchItem object 116 | list_data.append( 117 | SearchItem( 118 | title=title or f"Bing Result {rank_start}", 119 | url=url, 120 | description=abstract, 121 | ) 122 | ) 123 | except Exception: 124 | continue 125 | 126 | next_btn = root.find("a", title="Next page") 127 | if not next_btn: 128 | return list_data, None 129 | 130 | next_url = BING_HOST_URL + next_btn["href"] 131 | return list_data, next_url 132 | except Exception as e: 133 | logger.warning(f"Error parsing HTML: {e}") 134 | return [], None 135 | 136 | def perform_search( 137 | self, query: str, num_results: int = 10, *args, **kwargs 138 | ) -> List[SearchItem]: 139 | """ 140 | Bing search engine. 141 | 142 | Returns results formatted according to SearchItem model. 143 | """ 144 | return self._search_sync(query, num_results=num_results) 145 | -------------------------------------------------------------------------------- /app/tool/search/duckduckgo_search.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from duckduckgo_search import DDGS 4 | 5 | from app.tool.search.base import SearchItem, WebSearchEngine 6 | 7 | 8 | class DuckDuckGoSearchEngine(WebSearchEngine): 9 | def perform_search( 10 | self, query: str, num_results: int = 10, *args, **kwargs 11 | ) -> List[SearchItem]: 12 | """ 13 | DuckDuckGo search engine. 14 | 15 | Returns results formatted according to SearchItem model. 16 | """ 17 | raw_results = DDGS().text(query, max_results=num_results) 18 | 19 | results = [] 20 | for i, item in enumerate(raw_results): 21 | if isinstance(item, str): 22 | # If it's just a URL 23 | results.append( 24 | SearchItem( 25 | title=f"DuckDuckGo Result {i + 1}", url=item, description=None 26 | ) 27 | ) 28 | elif isinstance(item, dict): 29 | # Extract data from the dictionary 30 | results.append( 31 | SearchItem( 32 | title=item.get("title", f"DuckDuckGo Result {i + 1}"), 33 | url=item.get("href", ""), 34 | description=item.get("body", None), 35 | ) 36 | ) 37 | else: 38 | # Try to extract attributes directly 39 | try: 40 | results.append( 41 | SearchItem( 42 | title=getattr(item, "title", f"DuckDuckGo Result {i + 1}"), 43 | url=getattr(item, "href", ""), 44 | description=getattr(item, "body", None), 45 | ) 46 | ) 47 | except Exception: 48 | # Fallback 49 | results.append( 50 | SearchItem( 51 | title=f"DuckDuckGo Result {i + 1}", 52 | url=str(item), 53 | description=None, 54 | ) 55 | ) 56 | 57 | return results 58 | -------------------------------------------------------------------------------- /app/tool/search/google_search.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from googlesearch import search 4 | 5 | from app.tool.search.base import SearchItem, WebSearchEngine 6 | 7 | 8 | class GoogleSearchEngine(WebSearchEngine): 9 | def perform_search( 10 | self, query: str, num_results: int = 10, *args, **kwargs 11 | ) -> List[SearchItem]: 12 | """ 13 | Google search engine. 14 | 15 | Returns results formatted according to SearchItem model. 16 | """ 17 | raw_results = search(query, num_results=num_results, advanced=True) 18 | 19 | results = [] 20 | for i, item in enumerate(raw_results): 21 | if isinstance(item, str): 22 | # If it's just a URL 23 | results.append( 24 | {"title": f"Google Result {i+1}", "url": item, "description": ""} 25 | ) 26 | else: 27 | results.append( 28 | SearchItem( 29 | title=item.title, url=item.url, description=item.description 30 | ) 31 | ) 32 | 33 | return results 34 | -------------------------------------------------------------------------------- /app/tool/terminate.py: -------------------------------------------------------------------------------- 1 | from app.tool.base import BaseTool 2 | 3 | 4 | _TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task. 5 | When you have finished all the tasks, call this tool to end the work.""" 6 | 7 | 8 | class Terminate(BaseTool): 9 | name: str = "terminate" 10 | description: str = _TERMINATE_DESCRIPTION 11 | parameters: dict = { 12 | "type": "object", 13 | "properties": { 14 | "status": { 15 | "type": "string", 16 | "description": "The finish status of the interaction.", 17 | "enum": ["success", "failure"], 18 | } 19 | }, 20 | "required": ["status"], 21 | } 22 | 23 | async def execute(self, status: str) -> str: 24 | """Finish the current execution""" 25 | return f"The interaction has been completed with status: {status}" 26 | -------------------------------------------------------------------------------- /app/tool/tool_collection.py: -------------------------------------------------------------------------------- 1 | """Collection classes for managing multiple tools.""" 2 | from typing import Any, Dict, List 3 | 4 | from app.exceptions import ToolError 5 | from app.logger import logger 6 | from app.tool.base import BaseTool, ToolFailure, ToolResult 7 | 8 | 9 | class ToolCollection: 10 | """A collection of defined tools.""" 11 | 12 | class Config: 13 | arbitrary_types_allowed = True 14 | 15 | def __init__(self, *tools: BaseTool): 16 | self.tools = tools 17 | self.tool_map = {tool.name: tool for tool in tools} 18 | 19 | def __iter__(self): 20 | return iter(self.tools) 21 | 22 | def to_params(self) -> List[Dict[str, Any]]: 23 | return [tool.to_param() for tool in self.tools] 24 | 25 | async def execute( 26 | self, *, name: str, tool_input: Dict[str, Any] = None 27 | ) -> ToolResult: 28 | tool = self.tool_map.get(name) 29 | if not tool: 30 | return ToolFailure(error=f"Tool {name} is invalid") 31 | try: 32 | result = await tool(**tool_input) 33 | return result 34 | except ToolError as e: 35 | return ToolFailure(error=e.message) 36 | 37 | async def execute_all(self) -> List[ToolResult]: 38 | """Execute all tools in the collection sequentially.""" 39 | results = [] 40 | for tool in self.tools: 41 | try: 42 | result = await tool() 43 | results.append(result) 44 | except ToolError as e: 45 | results.append(ToolFailure(error=e.message)) 46 | return results 47 | 48 | def get_tool(self, name: str) -> BaseTool: 49 | return self.tool_map.get(name) 50 | 51 | def add_tool(self, tool: BaseTool): 52 | """Add a single tool to the collection. 53 | 54 | If a tool with the same name already exists, it will be skipped and a warning will be logged. 55 | """ 56 | if tool.name in self.tool_map: 57 | logger.warning(f"Tool {tool.name} already exists in collection, skipping") 58 | return self 59 | 60 | self.tools += (tool,) 61 | self.tool_map[tool.name] = tool 62 | return self 63 | 64 | def add_tools(self, *tools: BaseTool): 65 | """Add multiple tools to the collection. 66 | 67 | If any tool has a name conflict with an existing tool, it will be skipped and a warning will be logged. 68 | """ 69 | for tool in tools: 70 | self.add_tool(tool) 71 | return self 72 | -------------------------------------------------------------------------------- /assets/community_group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/assets/community_group.jpg -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/assets/logo.jpg -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | # prevent the local config file from being uploaded to the remote repository 2 | config.toml 3 | -------------------------------------------------------------------------------- /config/config.example-model-anthropic.toml: -------------------------------------------------------------------------------- 1 | # Global LLM configuration 2 | [llm] 3 | model = "claude-3-7-sonnet-latest" # The LLM model to use 4 | base_url = "https://api.anthropic.com/v1/" # API endpoint URL 5 | api_key = "YOUR_API_KEY" # Your API key 6 | max_tokens = 8192 # Maximum number of tokens in the response 7 | temperature = 0.0 # Controls randomness 8 | 9 | 10 | # Optional configuration for specific LLM models 11 | [llm.vision] 12 | model = "claude-3-7-sonnet-20250219" # The vision model to use 13 | base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model 14 | api_key = "YOUR_API_KEY" # Your API key for vision model 15 | max_tokens = 8192 # Maximum number of tokens in the response 16 | temperature = 0.0 # Controls randomness for vision model 17 | -------------------------------------------------------------------------------- /config/config.example-model-azure.toml: -------------------------------------------------------------------------------- 1 | # Global LLM configuration 2 | [llm] #AZURE OPENAI: 3 | api_type= 'azure' 4 | model = "gpt-4o-mini" # The LLM model to use 5 | base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" # API endpoint URL 6 | api_key = "YOUR_API_KEY" # Your API key 7 | max_tokens = 8096 # Maximum number of tokens in the response 8 | temperature = 0.0 # Controls randomness 9 | api_version="AZURE API VERSION" #"2024-08-01-preview" # Azure Openai version if AzureOpenai 10 | 11 | 12 | # Optional configuration for specific LLM models 13 | [llm.vision] 14 | model = "gpt-4o" # The vision model to use 15 | base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" 16 | api_key = "YOUR_API_KEY" # Your API key for vision model 17 | max_tokens = 8192 # Maximum number of tokens in the response 18 | temperature = 0.0 # Controls randomness for vision model 19 | -------------------------------------------------------------------------------- /config/config.example-model-google.toml: -------------------------------------------------------------------------------- 1 | # Global LLM configuration 2 | [llm] 3 | model = "gemini-2.0-flash" # The LLM model to use 4 | base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # API endpoint URL 5 | api_key = "YOUR_API_KEY" # Your API key 6 | temperature = 0.0 # Controls randomness 7 | max_tokens = 8096 # Maximum number of tokens in the response 8 | 9 | 10 | # Optional configuration for specific LLM models for Google 11 | [llm.vision] 12 | model = "gemini-2.0-flash-exp" # The vision model to use 13 | base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # API endpoint URL for vision model 14 | api_key = "YOUR_API_KEY" # Your API key for vision model 15 | max_tokens = 8192 # Maximum number of tokens in the response 16 | temperature = 0.0 # Controls randomness for vision model 17 | -------------------------------------------------------------------------------- /config/config.example-model-ollama.toml: -------------------------------------------------------------------------------- 1 | # Global LLM configuration 2 | [llm] #OLLAMA: 3 | api_type = 'ollama' 4 | model = "llama3.2" # The LLM model to use 5 | base_url = "http://localhost:11434/v1" # API endpoint URL 6 | api_key = "ollama" # Your API key 7 | max_tokens = 4096 # Maximum number of tokens in the response 8 | temperature = 0.0 # Controls randomness 9 | 10 | 11 | [llm.vision] #OLLAMA VISION: 12 | api_type = 'ollama' 13 | model = "llama3.2-vision" # The vision model to use 14 | base_url = "http://localhost:11434/v1" # API endpoint URL for vision model 15 | api_key = "ollama" # Your API key for vision model 16 | max_tokens = 4096 # Maximum number of tokens in the response 17 | temperature = 0.0 # Controls randomness for vision model 18 | -------------------------------------------------------------------------------- /config/config.example-model-ppio.toml: -------------------------------------------------------------------------------- 1 | # Global LLM configuration 2 | [llm] #PPIO: 3 | api_type = 'ppio' 4 | model = "deepseek/deepseek-v3-0324" # The LLM model to use 5 | base_url = "https://api.ppinfra.com/v3/openai" # API endpoint URL 6 | api_key = "your ppio api key" # Your API key 7 | max_tokens = 16000 # Maximum number of tokens in the response 8 | temperature = 0.0 # Controls randomness 9 | 10 | 11 | [llm.vision] #PPIO VISION: 12 | api_type = 'ppio' 13 | model = "qwen/qwen2.5-vl-72b-instruct" # The vision model to use 14 | base_url = "https://api.ppinfra.com/v3/openai" # API endpoint URL for vision model 15 | api_key = "your ppio api key" # Your API key for vision model 16 | max_tokens = 96000 # Maximum number of tokens in the response 17 | temperature = 0.0 # Controls randomness for vision model 18 | -------------------------------------------------------------------------------- /config/config.example.toml: -------------------------------------------------------------------------------- 1 | # Global LLM configuration 2 | [llm] 3 | model = "claude-3-7-sonnet-20250219" # The LLM model to use 4 | base_url = "https://api.anthropic.com/v1/" # API endpoint URL 5 | api_key = "YOUR_API_KEY" # Your API key 6 | max_tokens = 8192 # Maximum number of tokens in the response 7 | temperature = 0.0 # Controls randomness 8 | 9 | # [llm] # Amazon Bedrock 10 | # api_type = "aws" # Required 11 | # model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID 12 | # base_url = "bedrock-runtime.us-west-2.amazonaws.com" # Not used now 13 | # max_tokens = 8192 14 | # temperature = 1.0 15 | # api_key = "bear" # Required but not used for Bedrock 16 | 17 | # [llm] #AZURE OPENAI: 18 | # api_type= 'azure' 19 | # model = "YOUR_MODEL_NAME" #"gpt-4o-mini" 20 | # base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" 21 | # api_key = "AZURE API KEY" 22 | # max_tokens = 8096 23 | # temperature = 0.0 24 | # api_version="AZURE API VERSION" #"2024-08-01-preview" 25 | 26 | # [llm] #OLLAMA: 27 | # api_type = 'ollama' 28 | # model = "llama3.2" 29 | # base_url = "http://localhost:11434/v1" 30 | # api_key = "ollama" 31 | # max_tokens = 4096 32 | # temperature = 0.0 33 | 34 | # Optional configuration for specific LLM models 35 | [llm.vision] 36 | model = "claude-3-7-sonnet-20250219" # The vision model to use 37 | base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model 38 | api_key = "YOUR_API_KEY" # Your API key for vision model 39 | max_tokens = 8192 # Maximum number of tokens in the response 40 | temperature = 0.0 # Controls randomness for vision model 41 | 42 | # [llm.vision] #OLLAMA VISION: 43 | # api_type = 'ollama' 44 | # model = "llama3.2-vision" 45 | # base_url = "http://localhost:11434/v1" 46 | # api_key = "ollama" 47 | # max_tokens = 4096 48 | # temperature = 0.0 49 | 50 | # Optional configuration for specific browser configuration 51 | # [browser] 52 | # Whether to run browser in headless mode (default: false) 53 | #headless = false 54 | # Disable browser security features (default: true) 55 | #disable_security = true 56 | # Extra arguments to pass to the browser 57 | #extra_chromium_args = [] 58 | # Path to a Chrome instance to use to connect to your normal browser 59 | # e.g. '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' 60 | #chrome_instance_path = "" 61 | # Connect to a browser instance via WebSocket 62 | #wss_url = "" 63 | # Connect to a browser instance via CDP 64 | #cdp_url = "" 65 | 66 | # Optional configuration, Proxy settings for the browser 67 | # [browser.proxy] 68 | # server = "http://proxy-server:port" 69 | # username = "proxy-username" 70 | # password = "proxy-password" 71 | 72 | # Optional configuration, Search settings. 73 | # [search] 74 | # Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo" or "Bing". 75 | #engine = "Google" 76 | # Fallback engine order. Default is ["DuckDuckGo", "Baidu", "Bing"] - will try in this order after primary engine fails. 77 | #fallback_engines = ["DuckDuckGo", "Baidu", "Bing"] 78 | # Seconds to wait before retrying all engines again when they all fail due to rate limits. Default is 60. 79 | #retry_delay = 60 80 | # Maximum number of times to retry all engines when all fail. Default is 3. 81 | #max_retries = 3 82 | # Language code for search results. Options: "en" (English), "zh" (Chinese), etc. 83 | #lang = "en" 84 | # Country code for search results. Options: "us" (United States), "cn" (China), etc. 85 | #country = "us" 86 | 87 | 88 | ## Sandbox configuration 89 | #[sandbox] 90 | #use_sandbox = false 91 | #image = "python:3.12-slim" 92 | #work_dir = "/workspace" 93 | #memory_limit = "1g" # 512m 94 | #cpu_limit = 2.0 95 | #timeout = 300 96 | #network_enabled = true 97 | 98 | # MCP (Model Context Protocol) configuration 99 | [mcp] 100 | server_reference = "app.mcp.server" # default server module reference 101 | -------------------------------------------------------------------------------- /config/mcp.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "server1": { 4 | "type": "sse", 5 | "url": "http://localhost:8000/sse" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenManus benchmark system for standardized agent evaluation. 3 | """ 4 | -------------------------------------------------------------------------------- /examples/use_case/japan-travel-plan/japan_travel_guide_instructions.txt: -------------------------------------------------------------------------------- 1 | JAPAN TRAVEL HANDBOOK - GUIDE TO VERSIONS 2 | 3 | Location: D:/OpenManus/ 4 | 5 | 1. DETAILED DIGITAL VERSION 6 | File: japan_travel_handbook.html 7 | Best for: Desktop/laptop viewing 8 | Features: 9 | - Complete comprehensive guide 10 | - Detailed itinerary 11 | - Full proposal planning section 12 | - All hotel recommendations 13 | - Comprehensive budget breakdown 14 | Usage: Open in web browser for trip planning and detailed reference 15 | 16 | 2. PRINT-FRIENDLY VERSION 17 | File: japan_travel_handbook_print.html 18 | Best for: Physical reference during travel 19 | Features: 20 | - Condensed essential information 21 | - Optimized for paper printing 22 | - Clear, printer-friendly formatting 23 | - Quick reference tables 24 | Usage: Print and keep in travel documents folder 25 | 26 | 3. MOBILE-OPTIMIZED VERSION 27 | File: japan_travel_handbook_mobile.html 28 | Best for: On-the-go reference during trip 29 | Features: 30 | - Touch-friendly interface 31 | - Collapsible sections 32 | - Quick access emergency buttons 33 | - Dark mode support 34 | - Responsive design 35 | Usage: Save to phone's browser bookmarks for quick access 36 | 37 | RECOMMENDED SETUP: 38 | 1. Before Trip: 39 | - Use detailed version for planning 40 | - Print the print-friendly version 41 | - Save mobile version to phone 42 | 43 | 2. During Trip: 44 | - Keep printed version with travel documents 45 | - Use mobile version for daily reference 46 | - Access detailed version when needed for specific information 47 | 48 | 3. Emergency Access: 49 | - Mobile version has quick-access emergency information 50 | - Keep printed version as backup 51 | - All emergency numbers and contacts in both versions 52 | 53 | Note: All versions contain the same core information but are formatted differently for optimal use in different situations. 54 | 55 | IMPORTANT DATES: 56 | - Trip Duration: April 15-23, 2024 57 | - Proposal Day: April 19, 2024 58 | - Key Reservation Deadlines: 59 | * Flights: Book by January 2024 60 | * Hotels: Book by February 2024 61 | * Restaurant Reservations: Book by January 2024 62 | * JR Pass: Purchase by March 2024 63 | -------------------------------------------------------------------------------- /examples/use_case/japan-travel-plan/japan_travel_handbook.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Japan Travel Handbook - April 15-23, 2024 7 | 24 | 25 | 26 |
27 | [Previous content remains the same...] 28 | 29 |
30 |

🌸 Proposal Planning Guide 🌸

31 | 32 |

Ring Security & Transport

33 |
    34 |
  • Carrying the Ring: 35 |
      36 |
    • Always keep the ring in your carry-on luggage, never in checked bags
    • 37 |
    • Use a discrete, non-branded box or case
    • 38 |
    • Consider travel insurance that covers jewelry
    • 39 |
    • Keep receipt/appraisal documentation separate from the ring
    • 40 |
    41 |
  • 42 |
  • Airport Security Tips: 43 |
      44 |
    • No need to declare the ring unless value exceeds ¥1,000,000 (~$6,700)
    • 45 |
    • If asked, simply state it's "personal jewelry"
    • 46 |
    • Consider requesting private screening to maintain surprise
    • 47 |
    • Keep ring in original box until through security, then transfer to more discrete case
    • 48 |
    49 |
  • 50 |
51 | 52 |

Proposal Location Details - Maruyama Park

53 |
    54 |
  • Best Timing: 55 |
      56 |
    • Date: April 19 (Day 5)
    • 57 |
    • Time: 5:30 PM (30 minutes before sunset)
    • 58 |
    • Park closes at 8:00 PM in April
    • 59 |
    60 |
  • 61 |
  • Specific Spot Recommendations: 62 |
      63 |
    • Primary Location: Near the famous weeping cherry tree 64 |
      - Less crowded in early evening 65 |
      - Beautiful illumination starts at dusk 66 |
      - Iconic Kyoto backdrop 67 |
    • 68 |
    • Backup Location: Gion Shirakawa area 69 |
      - Atmospheric stone-paved street 70 |
      - Traditional buildings and cherry trees 71 |
      - Beautiful in light rain 72 |
    • 73 |
    74 |
  • 75 |
76 | 77 |

Proposal Day Planning

78 |
    79 |
  • Morning Preparation: 80 |
      81 |
    • Confirm weather forecast
    • 82 |
    • Transfer ring to secure pocket/bag
    • 83 |
    • Have backup indoor location details ready
    • 84 |
    85 |
  • 86 |
  • Suggested Timeline: 87 |
      88 |
    • 4:00 PM: Start heading to Maruyama Park area
    • 89 |
    • 4:30 PM: Light refreshments at nearby tea house
    • 90 |
    • 5:15 PM: Begin walk through park
    • 91 |
    • 5:30 PM: Arrive at proposal spot
    • 92 |
    • 6:00 PM: Sunset and illumination begins
    • 93 |
    • 7:00 PM: Celebratory dinner reservation
    • 94 |
    95 |
  • 96 |
97 | 98 |

Celebration Dinner Options

99 |
    100 |
  • Traditional Japanese: Kikunoi Roan 101 |
    - Intimate 2-star Michelin restaurant 102 |
    - Advance reservation required (3 months) 103 |
    - Price: ¥15,000-20,000 per person 104 |
  • 105 |
  • Modern Fusion: The Sodoh 106 |
    - Beautiful garden views 107 |
    - Western-style seating available 108 |
    - Price: ¥12,000-15,000 per person 109 |
  • 110 |
111 | 112 |
113 |

Important Notes:

114 |
    115 |
  • Keep proposal plans in separate notes from shared itinerary
  • 116 |
  • Have a backup plan in case of rain (indoor locations listed above)
  • 117 |
  • Consider hiring a local photographer to capture the moment
  • 118 |
  • Save restaurant staff contact info in case of timing changes
  • 119 |
120 |
121 |
122 |
123 | 124 | 125 | -------------------------------------------------------------------------------- /examples/use_case/japan-travel-plan/japan_travel_handbook_print.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Japan Travel Handbook (Print Version) - April 15-23, 2024 6 | 81 | 82 | 83 |

Japan Travel Handbook (Print Version)

84 |

Trip Dates: April 15-23, 2024

85 | 86 |
87 |

Emergency Contacts & Important Information

88 |
    89 |
  • Emergency in Japan: 119 (Ambulance/Fire) / 110 (Police)
  • 90 |
  • US Embassy Tokyo: +81-3-3224-5000
  • 91 |
  • Tourist Information Hotline: 03-3201-3331
  • 92 |
  • Your Travel Insurance: [Write number here]
  • 93 |
94 |
95 | 96 |
97 |

Daily Itinerary Summary

98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
DateLocationKey Activities
Apr 15TokyoArrival, Shinjuku area exploration
Apr 16TokyoMeiji Shrine, Harajuku, Senso-ji, Skytree
Apr 17TokyoTea Ceremony, Budokan, Yanaka Ginza
Apr 18KyotoTravel to Kyoto, Kinkaku-ji, Gion
Apr 19KyotoFushimi Inari, Arashiyama, Evening Proposal
Apr 20Nara/KyotoNara Park day trip, deer feeding
Apr 21TokyoReturn to Tokyo, bay cruise
108 |
109 | 110 |
111 | 112 |
113 |

Essential Japanese Phrases

114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
EnglishJapaneseWhen to Use
Arigatou gozaimasuありがとうございますThank you (formal)
SumimasenすみませんExcuse me/Sorry
Onegaishimasuお願いしますPlease
Toire wa doko desu ka?トイレはどこですか?Where is the bathroom?
Eigo ga hanasemasu ka?英語が話せますか?Do you speak English?
122 |
123 | 124 |
125 |

Transportation Notes

126 |
    127 |
  • JR Pass: Activate on April 15
  • 128 |
  • Tokyo-Kyoto Shinkansen: ~2h15m
  • 129 |
  • Kyoto-Nara Local Train: ~45m
  • 130 |
  • Last trains: Usually around midnight
  • 131 |
  • Keep ¥3000 for unexpected taxi rides
  • 132 |
133 |
134 | 135 |
136 | 137 |
138 |

Proposal Day Timeline (April 19)

139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
TimeActivityNotes
4:00 PMHead to Maruyama ParkCheck weather first
4:30 PMTea house visitLight refreshments
5:15 PMPark walk beginsHead to weeping cherry tree
5:30 PMArrive at spotFind quiet area
7:00 PMDinner reservationKikunoi Roan
147 |

Backup Location: Gion Shirakawa area (in case of rain)

148 |
149 | 150 |
151 |

Quick Reference Budget

152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
ItemBudget (USD)Notes
Hotels1500-2000Pre-booked
Transport600-800Including JR Pass
Food800-1000~$60/person/day
Activities600-800Including tea ceremony
Shopping500-400Souvenirs/gifts
160 |
161 | 162 | 163 | -------------------------------------------------------------------------------- /examples/use_case/pictures/japan-travel-plan-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/examples/use_case/pictures/japan-travel-plan-1.png -------------------------------------------------------------------------------- /examples/use_case/pictures/japan-travel-plan-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannaandpoem/OpenManus/8593116982c21d0e98569df2fdf171b659d60449/examples/use_case/pictures/japan-travel-plan-2.png -------------------------------------------------------------------------------- /examples/use_case/readme.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | We put some examples in the `examples` directory. All the examples use the same prompt 4 | as [Manus](https://manus.im/?utm_source=ai-bot.cn). 5 | 6 | The Model we use is `claude3.5`. 7 | 8 | ## Japan Travel Plan 9 | **Prompt**: 10 | ``` 11 | I need a 7-day Japan itinerary for April 15-23 from Seattle, with a $2500-5000 budget for my fiancée and me. We love historical sites, hidden gems, and Japanese culture (kendo, tea ceremonies, Zen meditation). We want to see Nara's deer and explore cities on foot. I plan to propose during this trip and need a special location recommendation. Please provide a detailed itinerary and a simple HTML travel handbook with maps, attraction descriptions, essential Japanese phrases, and travel tips we can reference throughout our journey. 12 | ``` 13 | **preview**: 14 | ![alt text](picturesapan-travel-plan-1.png) 15 | 16 | ![alt text](picturesapan-travel-plan-2.png) 17 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from app.agent.manus import Manus 4 | from app.logger import logger 5 | 6 | 7 | async def main(): 8 | # Create and initialize Manus agent 9 | agent = await Manus.create() 10 | try: 11 | prompt = input("Enter your prompt: ") 12 | if not prompt.strip(): 13 | logger.warning("Empty prompt provided.") 14 | return 15 | 16 | logger.warning("Processing your request...") 17 | await agent.run(prompt) 18 | logger.info("Request processing completed.") 19 | except KeyboardInterrupt: 20 | logger.warning("Operation interrupted.") 21 | finally: 22 | # Ensure agent resources are cleaned up before exiting 23 | await agent.cleanup() 24 | 25 | 26 | if __name__ == "__main__": 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic~=2.10.6 2 | openai~=1.66.3 3 | tenacity~=9.0.0 4 | pyyaml~=6.0.2 5 | loguru~=0.7.3 6 | numpy 7 | datasets~=3.4.1 8 | fastapi~=0.115.11 9 | tiktoken~=0.9.0 10 | 11 | html2text~=2024.2.26 12 | gymnasium~=1.1.1 13 | pillow~=11.1.0 14 | browsergym~=0.13.3 15 | uvicorn~=0.34.0 16 | unidiff~=0.7.5 17 | browser-use~=0.1.40 18 | googlesearch-python~=1.3.0 19 | baidusearch~=1.0.3 20 | duckduckgo_search~=7.5.3 21 | 22 | aiofiles~=24.1.0 23 | pydantic_core~=2.27.2 24 | colorama~=0.4.6 25 | playwright~=1.51.0 26 | 27 | docker~=7.1.0 28 | pytest~=8.3.5 29 | pytest-asyncio~=0.25.3 30 | 31 | mcp~=1.5.0 32 | httpx>=0.27.0 33 | tomli>=2.0.0 34 | 35 | boto3~=1.37.18 36 | 37 | requests~=2.32.3 38 | beautifulsoup4~=4.13.3 39 | 40 | huggingface-hub~=0.29.2 41 | setuptools~=75.8.0 42 | -------------------------------------------------------------------------------- /run_flow.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from app.agent.manus import Manus 5 | from app.flow.flow_factory import FlowFactory, FlowType 6 | from app.logger import logger 7 | 8 | 9 | async def run_flow(): 10 | agents = { 11 | "manus": Manus(), 12 | } 13 | 14 | try: 15 | prompt = input("Enter your prompt: ") 16 | 17 | if prompt.strip().isspace() or not prompt: 18 | logger.warning("Empty prompt provided.") 19 | return 20 | 21 | flow = FlowFactory.create_flow( 22 | flow_type=FlowType.PLANNING, 23 | agents=agents, 24 | ) 25 | logger.warning("Processing your request...") 26 | 27 | try: 28 | start_time = time.time() 29 | result = await asyncio.wait_for( 30 | flow.execute(prompt), 31 | timeout=3600, # 60 minute timeout for the entire execution 32 | ) 33 | elapsed_time = time.time() - start_time 34 | logger.info(f"Request processed in {elapsed_time:.2f} seconds") 35 | logger.info(result) 36 | except asyncio.TimeoutError: 37 | logger.error("Request processing timed out after 1 hour") 38 | logger.info( 39 | "Operation terminated due to timeout. Please try a simpler request." 40 | ) 41 | 42 | except KeyboardInterrupt: 43 | logger.info("Operation cancelled by user.") 44 | except Exception as e: 45 | logger.error(f"Error: {str(e)}") 46 | 47 | 48 | if __name__ == "__main__": 49 | asyncio.run(run_flow()) 50 | -------------------------------------------------------------------------------- /run_mcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import asyncio 4 | import sys 5 | 6 | from app.agent.mcp import MCPAgent 7 | from app.config import config 8 | from app.logger import logger 9 | 10 | 11 | class MCPRunner: 12 | """Runner class for MCP Agent with proper path handling and configuration.""" 13 | 14 | def __init__(self): 15 | self.root_path = config.root_path 16 | self.server_reference = config.mcp_config.server_reference 17 | self.agent = MCPAgent() 18 | 19 | async def initialize( 20 | self, 21 | connection_type: str, 22 | server_url: str | None = None, 23 | ) -> None: 24 | """Initialize the MCP agent with the appropriate connection.""" 25 | logger.info(f"Initializing MCPAgent with {connection_type} connection...") 26 | 27 | if connection_type == "stdio": 28 | await self.agent.initialize( 29 | connection_type="stdio", 30 | command=sys.executable, 31 | args=["-m", self.server_reference], 32 | ) 33 | else: # sse 34 | await self.agent.initialize(connection_type="sse", server_url=server_url) 35 | 36 | logger.info(f"Connected to MCP server via {connection_type}") 37 | 38 | async def run_interactive(self) -> None: 39 | """Run the agent in interactive mode.""" 40 | print("\nMCP Agent Interactive Mode (type 'exit' to quit)\n") 41 | while True: 42 | user_input = input("\nEnter your request: ") 43 | if user_input.lower() in ["exit", "quit", "q"]: 44 | break 45 | response = await self.agent.run(user_input) 46 | print(f"\nAgent: {response}") 47 | 48 | async def run_single_prompt(self, prompt: str) -> None: 49 | """Run the agent with a single prompt.""" 50 | await self.agent.run(prompt) 51 | 52 | async def run_default(self) -> None: 53 | """Run the agent in default mode.""" 54 | prompt = input("Enter your prompt: ") 55 | if not prompt.strip(): 56 | logger.warning("Empty prompt provided.") 57 | return 58 | 59 | logger.warning("Processing your request...") 60 | await self.agent.run(prompt) 61 | logger.info("Request processing completed.") 62 | 63 | async def cleanup(self) -> None: 64 | """Clean up agent resources.""" 65 | await self.agent.cleanup() 66 | logger.info("Session ended") 67 | 68 | 69 | def parse_args() -> argparse.Namespace: 70 | """Parse command line arguments.""" 71 | parser = argparse.ArgumentParser(description="Run the MCP Agent") 72 | parser.add_argument( 73 | "--connection", 74 | "-c", 75 | choices=["stdio", "sse"], 76 | default="stdio", 77 | help="Connection type: stdio or sse", 78 | ) 79 | parser.add_argument( 80 | "--server-url", 81 | default="http://127.0.0.1:8000/sse", 82 | help="URL for SSE connection", 83 | ) 84 | parser.add_argument( 85 | "--interactive", "-i", action="store_true", help="Run in interactive mode" 86 | ) 87 | parser.add_argument("--prompt", "-p", help="Single prompt to execute and exit") 88 | return parser.parse_args() 89 | 90 | 91 | async def run_mcp() -> None: 92 | """Main entry point for the MCP runner.""" 93 | args = parse_args() 94 | runner = MCPRunner() 95 | 96 | try: 97 | await runner.initialize(args.connection, args.server_url) 98 | 99 | if args.prompt: 100 | await runner.run_single_prompt(args.prompt) 101 | elif args.interactive: 102 | await runner.run_interactive() 103 | else: 104 | await runner.run_default() 105 | 106 | except KeyboardInterrupt: 107 | logger.info("Program interrupted by user") 108 | except Exception as e: 109 | logger.error(f"Error running MCPAgent: {str(e)}", exc_info=True) 110 | sys.exit(1) 111 | finally: 112 | await runner.cleanup() 113 | 114 | 115 | if __name__ == "__main__": 116 | asyncio.run(run_mcp()) 117 | -------------------------------------------------------------------------------- /run_mcp_server.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # A shortcut to launch OpenManus MCP server, where its introduction also solves other import issues. 3 | from app.mcp.server import MCPServer, parse_args 4 | 5 | 6 | if __name__ == "__main__": 7 | args = parse_args() 8 | 9 | # Create and run server (maintaining original flow) 10 | server = MCPServer() 11 | server.run(transport=args.transport) 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | with open("README.md", "r", encoding="utf-8") as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name="openmanus", 9 | version="0.1.0", 10 | author="mannaandpoem and OpenManus Team", 11 | author_email="mannaandpoem@gmail.com", 12 | description="A versatile agent that can solve various tasks using multiple tools", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/mannaandpoem/OpenManus", 16 | packages=find_packages(), 17 | install_requires=[ 18 | "pydantic~=2.10.4", 19 | "openai>=1.58.1,<1.67.0", 20 | "tenacity~=9.0.0", 21 | "pyyaml~=6.0.2", 22 | "loguru~=0.7.3", 23 | "numpy", 24 | "datasets>=3.2,<3.5", 25 | "html2text~=2024.2.26", 26 | "gymnasium>=1.0,<1.2", 27 | "pillow>=10.4,<11.2", 28 | "browsergym~=0.13.3", 29 | "uvicorn~=0.34.0", 30 | "unidiff~=0.7.5", 31 | "browser-use~=0.1.40", 32 | "googlesearch-python~=1.3.0", 33 | "aiofiles~=24.1.0", 34 | "pydantic_core>=2.27.2,<2.28.0", 35 | "colorama~=0.4.6", 36 | ], 37 | classifiers=[ 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.12", 40 | "License :: OSI Approved :: MIT License", 41 | "Operating System :: OS Independent", 42 | ], 43 | python_requires=">=3.12", 44 | entry_points={ 45 | "console_scripts": [ 46 | "openmanus=main:main", 47 | ], 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /tests/sandbox/test_client.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from typing import AsyncGenerator 4 | 5 | import pytest 6 | import pytest_asyncio 7 | 8 | from app.config import SandboxSettings 9 | from app.sandbox.client import LocalSandboxClient, create_sandbox_client 10 | 11 | 12 | @pytest_asyncio.fixture(scope="function") 13 | async def local_client() -> AsyncGenerator[LocalSandboxClient, None]: 14 | """Creates a local sandbox client for testing.""" 15 | client = create_sandbox_client() 16 | try: 17 | yield client 18 | finally: 19 | await client.cleanup() 20 | 21 | 22 | @pytest.fixture(scope="function") 23 | def temp_dir() -> Path: 24 | """Creates a temporary directory for testing.""" 25 | with tempfile.TemporaryDirectory() as tmp_dir: 26 | yield Path(tmp_dir) 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_sandbox_creation(local_client: LocalSandboxClient): 31 | """Tests sandbox creation with specific configuration.""" 32 | config = SandboxSettings( 33 | image="python:3.12-slim", 34 | work_dir="/workspace", 35 | memory_limit="512m", 36 | cpu_limit=0.5, 37 | ) 38 | 39 | await local_client.create(config) 40 | result = await local_client.run_command("python3 --version") 41 | assert "Python 3.10" in result 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_local_command_execution(local_client: LocalSandboxClient): 46 | """Tests command execution in local sandbox.""" 47 | await local_client.create() 48 | 49 | result = await local_client.run_command("echo 'test'") 50 | assert result.strip() == "test" 51 | 52 | with pytest.raises(Exception): 53 | await local_client.run_command("sleep 10", timeout=1) 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_local_file_operations(local_client: LocalSandboxClient, temp_dir: Path): 58 | """Tests file operations in local sandbox.""" 59 | await local_client.create() 60 | 61 | # Test write and read operations 62 | test_content = "Hello, World!" 63 | await local_client.write_file("/workspace/test.txt", test_content) 64 | content = await local_client.read_file("/workspace/test.txt") 65 | assert content.strip() == test_content 66 | 67 | # Test copying file to container 68 | src_file = temp_dir / "src.txt" 69 | src_file.write_text("Copy to container") 70 | await local_client.copy_to(str(src_file), "/workspace/copied.txt") 71 | content = await local_client.read_file("/workspace/copied.txt") 72 | assert content.strip() == "Copy to container" 73 | 74 | # Test copying file from container 75 | dst_file = temp_dir / "dst.txt" 76 | await local_client.copy_from("/workspace/test.txt", str(dst_file)) 77 | assert dst_file.read_text().strip() == test_content 78 | 79 | 80 | @pytest.mark.asyncio 81 | async def test_local_volume_binding(local_client: LocalSandboxClient, temp_dir: Path): 82 | """Tests volume binding in local sandbox.""" 83 | bind_path = str(temp_dir) 84 | volume_bindings = {bind_path: "/data"} 85 | 86 | await local_client.create(volume_bindings=volume_bindings) 87 | 88 | test_file = temp_dir / "test.txt" 89 | test_file.write_text("Volume test") 90 | 91 | content = await local_client.read_file("/data/test.txt") 92 | assert "Volume test" in content 93 | 94 | 95 | @pytest.mark.asyncio 96 | async def test_local_error_handling(local_client: LocalSandboxClient): 97 | """Tests error handling in local sandbox.""" 98 | await local_client.create() 99 | 100 | with pytest.raises(Exception) as exc: 101 | await local_client.read_file("/nonexistent.txt") 102 | assert "not found" in str(exc.value).lower() 103 | 104 | with pytest.raises(Exception) as exc: 105 | await local_client.copy_from("/nonexistent.txt", "local.txt") 106 | assert "not found" in str(exc.value).lower() 107 | 108 | 109 | if __name__ == "__main__": 110 | pytest.main(["-v", __file__]) 111 | -------------------------------------------------------------------------------- /tests/sandbox/test_docker_terminal.py: -------------------------------------------------------------------------------- 1 | """Tests for the AsyncDockerizedTerminal implementation.""" 2 | 3 | import docker 4 | import pytest 5 | import pytest_asyncio 6 | 7 | from app.sandbox.core.terminal import AsyncDockerizedTerminal 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def docker_client(): 12 | """Fixture providing a Docker client.""" 13 | return docker.from_env() 14 | 15 | 16 | @pytest_asyncio.fixture(scope="module") 17 | async def docker_container(docker_client): 18 | """Fixture providing a test Docker container.""" 19 | container = docker_client.containers.run( 20 | "python:3.12-slim", 21 | "tail -f /dev/null", 22 | name="test_container", 23 | detach=True, 24 | remove=True, 25 | ) 26 | yield container 27 | container.stop() 28 | 29 | 30 | @pytest_asyncio.fixture 31 | async def terminal(docker_container): 32 | """Fixture providing an initialized AsyncDockerizedTerminal instance.""" 33 | terminal = AsyncDockerizedTerminal( 34 | docker_container, 35 | working_dir="/workspace", 36 | env_vars={"TEST_VAR": "test_value"}, 37 | default_timeout=30, 38 | ) 39 | await terminal.init() 40 | yield terminal 41 | await terminal.close() 42 | 43 | 44 | class TestAsyncDockerizedTerminal: 45 | """Test cases for AsyncDockerizedTerminal.""" 46 | 47 | @pytest.mark.asyncio 48 | async def test_basic_command_execution(self, terminal): 49 | """Test basic command execution functionality.""" 50 | result = await terminal.run_command("echo 'Hello World'") 51 | assert "Hello World" in result 52 | 53 | @pytest.mark.asyncio 54 | async def test_environment_variables(self, terminal): 55 | """Test environment variable setting and access.""" 56 | result = await terminal.run_command("echo $TEST_VAR") 57 | assert "test_value" in result 58 | 59 | @pytest.mark.asyncio 60 | async def test_working_directory(self, terminal): 61 | """Test working directory setup.""" 62 | result = await terminal.run_command("pwd") 63 | assert "/workspace" == result 64 | 65 | @pytest.mark.asyncio 66 | async def test_command_timeout(self, docker_container): 67 | """Test command timeout functionality.""" 68 | terminal = AsyncDockerizedTerminal(docker_container, default_timeout=1) 69 | await terminal.init() 70 | try: 71 | with pytest.raises(TimeoutError): 72 | await terminal.run_command("sleep 5") 73 | finally: 74 | await terminal.close() 75 | 76 | @pytest.mark.asyncio 77 | async def test_multiple_commands(self, terminal): 78 | """Test execution of multiple commands in sequence.""" 79 | cmd1 = await terminal.run_command("echo 'First'") 80 | cmd2 = await terminal.run_command("echo 'Second'") 81 | assert "First" in cmd1 82 | assert "Second" in cmd2 83 | 84 | @pytest.mark.asyncio 85 | async def test_session_cleanup(self, docker_container): 86 | """Test proper cleanup of resources.""" 87 | terminal = AsyncDockerizedTerminal(docker_container) 88 | await terminal.init() 89 | assert terminal.session is not None 90 | await terminal.close() 91 | # Verify session is properly cleaned up 92 | # Note: session object still exists, but internal connection is closed 93 | assert terminal.session is not None 94 | 95 | 96 | # Configure pytest-asyncio 97 | def pytest_configure(config): 98 | """Configure pytest-asyncio.""" 99 | config.addinivalue_line("asyncio_mode", "strict") 100 | config.addinivalue_line("asyncio_default_fixture_loop_scope", "function") 101 | 102 | 103 | if __name__ == "__main__": 104 | pytest.main(["-v", __file__]) 105 | -------------------------------------------------------------------------------- /tests/sandbox/test_sandbox.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytest_asyncio 3 | 4 | from app.sandbox.core.sandbox import DockerSandbox, SandboxSettings 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def sandbox_config(): 9 | """Creates sandbox configuration for testing.""" 10 | return SandboxSettings( 11 | image="python:3.12-slim", 12 | work_dir="/workspace", 13 | memory_limit="1g", 14 | cpu_limit=0.5, 15 | network_enabled=True, 16 | ) 17 | 18 | 19 | @pytest_asyncio.fixture(scope="module") 20 | async def sandbox(sandbox_config): 21 | """Creates and manages a test sandbox instance.""" 22 | sandbox = DockerSandbox(sandbox_config) 23 | await sandbox.create() 24 | try: 25 | yield sandbox 26 | finally: 27 | await sandbox.cleanup() 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_sandbox_working_directory(sandbox): 32 | """Tests sandbox working directory configuration.""" 33 | result = await sandbox.terminal.run_command("pwd") 34 | assert result.strip() == "/workspace" 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_sandbox_file_operations(sandbox): 39 | """Tests sandbox file read/write operations.""" 40 | # Test file writing 41 | test_content = "Hello from sandbox!" 42 | await sandbox.write_file("/workspace/test.txt", test_content) 43 | 44 | # Test file reading 45 | content = await sandbox.read_file("/workspace/test.txt") 46 | assert content.strip() == test_content 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_sandbox_python_execution(sandbox): 51 | """Tests Python code execution in sandbox.""" 52 | # Write test file 53 | await sandbox.write_file("/workspace/test.txt", "Hello from file!") 54 | 55 | # Write Python script 56 | python_code = """ 57 | print("Hello from Python!") 58 | with open('/workspace/test.txt') as f: 59 | print(f.read()) 60 | """ 61 | await sandbox.write_file("/workspace/test.py", python_code) 62 | 63 | # Execute script and verify output 64 | result = await sandbox.terminal.run_command("python3 /workspace/test.py") 65 | assert "Hello from Python!" in result 66 | assert "Hello from file!" in result 67 | 68 | 69 | @pytest.mark.asyncio 70 | async def test_sandbox_file_persistence(sandbox): 71 | """Tests file persistence in sandbox.""" 72 | # Create multiple files 73 | files = { 74 | "file1.txt": "Content 1", 75 | "file2.txt": "Content 2", 76 | "nested/file3.txt": "Content 3", 77 | } 78 | 79 | # Write files 80 | for path, content in files.items(): 81 | await sandbox.write_file(f"/workspace/{path}", content) 82 | 83 | # Verify file contents 84 | for path, expected_content in files.items(): 85 | content = await sandbox.read_file(f"/workspace/{path}") 86 | assert content.strip() == expected_content 87 | 88 | 89 | @pytest.mark.asyncio 90 | async def test_sandbox_python_environment(sandbox): 91 | """Tests Python environment configuration.""" 92 | # Test Python version 93 | result = await sandbox.terminal.run_command("python3 --version") 94 | assert "Python 3.10" in result 95 | 96 | # Test basic module imports 97 | python_code = """ 98 | import sys 99 | import os 100 | import json 101 | print("Python is working!") 102 | """ 103 | await sandbox.write_file("/workspace/env_test.py", python_code) 104 | result = await sandbox.terminal.run_command("python3 /workspace/env_test.py") 105 | assert "Python is working!" in result 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_sandbox_network_access(sandbox): 110 | """Tests sandbox network access.""" 111 | if not sandbox.config.network_enabled: 112 | pytest.skip("Network access is disabled") 113 | 114 | # Test network connectivity 115 | await sandbox.terminal.run_command("apt update && apt install curl -y") 116 | result = await sandbox.terminal.run_command("curl -I https://www.example.com") 117 | assert "HTTP/2 200" in result 118 | 119 | 120 | @pytest.mark.asyncio 121 | async def test_sandbox_cleanup(sandbox_config): 122 | """Tests sandbox cleanup process.""" 123 | sandbox = DockerSandbox(sandbox_config) 124 | await sandbox.create() 125 | 126 | # Create test files 127 | await sandbox.write_file("/workspace/test.txt", "test") 128 | container_id = sandbox.terminal.container.id 129 | # Perform cleanup 130 | await sandbox.cleanup() 131 | 132 | # Verify container has been removed 133 | import docker 134 | 135 | client = docker.from_env() 136 | containers = client.containers.list(all=True) 137 | assert not any(c.id == container_id for c in containers) 138 | 139 | 140 | @pytest.mark.asyncio 141 | async def test_sandbox_error_handling(): 142 | """Tests error handling with invalid configuration.""" 143 | # Test invalid configuration 144 | invalid_config = SandboxSettings(image="nonexistent:latest", work_dir="/invalid") 145 | 146 | sandbox = DockerSandbox(invalid_config) 147 | with pytest.raises(Exception): 148 | await sandbox.create() 149 | 150 | 151 | if __name__ == "__main__": 152 | pytest.main(["-v", __file__]) 153 | -------------------------------------------------------------------------------- /tests/sandbox/test_sandbox_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import tempfile 4 | from typing import AsyncGenerator 5 | 6 | import pytest 7 | import pytest_asyncio 8 | 9 | from app.sandbox.core.manager import SandboxManager 10 | 11 | 12 | @pytest_asyncio.fixture(scope="function") 13 | async def manager() -> AsyncGenerator[SandboxManager, None]: 14 | """Creates a sandbox manager instance. 15 | 16 | Uses function scope to ensure each test case has its own manager instance. 17 | """ 18 | manager = SandboxManager(max_sandboxes=2, idle_timeout=60, cleanup_interval=30) 19 | try: 20 | yield manager 21 | finally: 22 | # Ensure all resources are cleaned up 23 | await manager.cleanup() 24 | 25 | 26 | @pytest.fixture 27 | def temp_file(): 28 | """Creates a temporary test file.""" 29 | with tempfile.NamedTemporaryFile(mode="w+", delete=False) as f: 30 | f.write("test content") 31 | path = f.name 32 | try: 33 | yield path 34 | finally: 35 | if os.path.exists(path): 36 | os.unlink(path) 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_create_sandbox(manager): 41 | """Tests sandbox creation.""" 42 | # Create default sandbox 43 | sandbox_id = await manager.create_sandbox() 44 | assert sandbox_id in manager._sandboxes 45 | assert sandbox_id in manager._last_used 46 | 47 | # Verify sandbox functionality 48 | sandbox = await manager.get_sandbox(sandbox_id) 49 | result = await sandbox.run_command("echo 'test'") 50 | assert result.strip() == "test" 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_max_sandboxes_limit(manager): 55 | """Tests maximum sandbox limit enforcement.""" 56 | created_sandboxes = [] 57 | try: 58 | # Create maximum number of sandboxes 59 | for _ in range(manager.max_sandboxes): 60 | sandbox_id = await manager.create_sandbox() 61 | created_sandboxes.append(sandbox_id) 62 | 63 | # Verify created sandbox count 64 | assert len(manager._sandboxes) == manager.max_sandboxes 65 | 66 | # Attempting to create additional sandbox should fail 67 | with pytest.raises(RuntimeError) as exc_info: 68 | await manager.create_sandbox() 69 | 70 | # Verify error message 71 | expected_message = ( 72 | f"Maximum number of sandboxes ({manager.max_sandboxes}) reached" 73 | ) 74 | assert str(exc_info.value) == expected_message 75 | 76 | finally: 77 | # Clean up all created sandboxes 78 | for sandbox_id in created_sandboxes: 79 | try: 80 | await manager.delete_sandbox(sandbox_id) 81 | except Exception as e: 82 | print(f"Failed to cleanup sandbox {sandbox_id}: {e}") 83 | 84 | 85 | @pytest.mark.asyncio 86 | async def test_get_nonexistent_sandbox(manager): 87 | """Tests retrieving a non-existent sandbox.""" 88 | with pytest.raises(KeyError, match="Sandbox .* not found"): 89 | await manager.get_sandbox("nonexistent-id") 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_sandbox_cleanup(manager): 94 | """Tests sandbox cleanup functionality.""" 95 | sandbox_id = await manager.create_sandbox() 96 | assert sandbox_id in manager._sandboxes 97 | 98 | await manager.delete_sandbox(sandbox_id) 99 | assert sandbox_id not in manager._sandboxes 100 | assert sandbox_id not in manager._last_used 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_idle_sandbox_cleanup(manager): 105 | """Tests automatic cleanup of idle sandboxes.""" 106 | # Set short idle timeout 107 | manager.idle_timeout = 0.1 108 | 109 | sandbox_id = await manager.create_sandbox() 110 | assert sandbox_id in manager._sandboxes 111 | 112 | # Wait longer than idle timeout 113 | await asyncio.sleep(0.2) 114 | 115 | # Trigger cleanup 116 | await manager._cleanup_idle_sandboxes() 117 | assert sandbox_id not in manager._sandboxes 118 | 119 | 120 | @pytest.mark.asyncio 121 | async def test_manager_cleanup(manager): 122 | """Tests manager cleanup functionality.""" 123 | # Create multiple sandboxes 124 | sandbox_ids = [] 125 | for _ in range(2): 126 | sandbox_id = await manager.create_sandbox() 127 | sandbox_ids.append(sandbox_id) 128 | 129 | # Clean up all resources 130 | await manager.cleanup() 131 | 132 | # Verify all sandboxes have been cleaned up 133 | assert not manager._sandboxes 134 | assert not manager._last_used 135 | 136 | 137 | if __name__ == "__main__": 138 | pytest.main(["-v", __file__]) 139 | -------------------------------------------------------------------------------- /workspace/example.txt: -------------------------------------------------------------------------------- 1 | This is a sample file. Files generated by OpenManus are stored in the current folder by default. 2 | --------------------------------------------------------------------------------