Tsitsi19 commited on
Commit
22df7bd
·
verified ·
1 Parent(s): c5440f3

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +32 -35
  2. .github/ISSUE_TEMPLATE/config.yml +5 -0
  3. .github/ISSUE_TEMPLATE/request_new_features.yaml +21 -0
  4. .github/ISSUE_TEMPLATE/show_me_the_bug.yaml +44 -0
  5. .github/PULL_REQUEST_TEMPLATE.md +17 -0
  6. .github/dependabot.yml +58 -0
  7. .github/workflows/build-package.yaml +33 -0
  8. .github/workflows/environment-corrupt-check.yaml +33 -0
  9. .github/workflows/pr-autodiff.yaml +138 -0
  10. .github/workflows/pre-commit.yaml +26 -0
  11. .github/workflows/stale.yaml +23 -0
  12. .github/workflows/top-issues.yaml +29 -0
  13. .gitignore +202 -0
  14. .pre-commit-config.yaml +37 -0
  15. .vscode/extensions.json +8 -0
  16. .vscode/settings.json +20 -0
  17. CODE_OF_CONDUCT.md +162 -0
  18. Dockerfile +67 -0
  19. LICENSE +21 -0
  20. README.md +195 -10
  21. README_ja.md +193 -0
  22. README_ko.md +192 -0
  23. README_zh.md +198 -0
  24. app/__init__.py +10 -0
  25. app/agent/__init__.py +16 -0
  26. app/agent/base.py +196 -0
  27. app/agent/browser.py +129 -0
  28. app/agent/data_analysis.py +37 -0
  29. app/agent/manus.py +165 -0
  30. app/agent/mcp.py +185 -0
  31. app/agent/react.py +38 -0
  32. app/agent/sandbox_agent.py +223 -0
  33. app/agent/swe.py +24 -0
  34. app/agent/toolcall.py +250 -0
  35. app/bedrock.py +334 -0
  36. app/config.py +384 -0
  37. app/daytona/README.md +57 -0
  38. app/daytona/sandbox.py +165 -0
  39. app/daytona/tool_base.py +138 -0
  40. app/exceptions.py +13 -0
  41. app/flow/__init__.py +0 -0
  42. app/flow/base.py +57 -0
  43. app/flow/flow_factory.py +30 -0
  44. app/flow/planning.py +442 -0
  45. app/llm.py +766 -0
  46. app/logger.py +42 -0
  47. app/mcp/__init__.py +0 -0
  48. app/mcp/server.py +180 -0
  49. app/prompt/__init__.py +0 -0
  50. app/prompt/browser.py +94 -0
.gitattributes CHANGED
@@ -1,35 +1,32 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
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
+ assets/community_group.png filter=lfs diff=lfs merge=lfs -text
32
+ examples/use_case/pictures/japan-travel-plan-1.png filter=lfs diff=lfs merge=lfs -text
 
 
 
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
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/FoundationAgents/OpenManus?tab=readme-ov-file#community-group
.github/ISSUE_TEMPLATE/request_new_features.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.github/ISSUE_TEMPLATE/show_me_the_bug.yaml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.github/PULL_REQUEST_TEMPLATE.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **Features**
2
+ <!-- Describe the features or bug fixes in this PR. For bug fixes, link to the issue. -->
3
+
4
+ - Feature 1
5
+ - Feature 2
6
+
7
+ **Feature Docs**
8
+ <!-- Provide RFC, tutorial, or use case links for significant updates. Optional for minor changes. -->
9
+
10
+ **Influence**
11
+ <!-- Explain the impact of these changes for reviewer focus. -->
12
+
13
+ **Result**
14
+ <!-- Include screenshots or logs of unit tests or running results. -->
15
+
16
+ **Other**
17
+ <!-- Additional notes about this PR. -->
.github/dependabot.yml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ - "*"
.github/workflows/build-package.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/*
.github/workflows/environment-corrupt-check.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.github/workflows/pr-autodiff.yaml ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.github/workflows/pre-commit.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.github/workflows/stale.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 }}
.github/workflows/top-issues.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 == 'FoundationAgents/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
.gitignore ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.pre-commit-config.yaml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ [
21
+ --remove-all-unused-imports,
22
+ --ignore-init-module-imports,
23
+ --expand-star-imports,
24
+ --remove-duplicate-keys,
25
+ --remove-unused-variables,
26
+ --recursive,
27
+ --in-place,
28
+ --exclude=__init__.py,
29
+ ]
30
+ files: \.py$
31
+
32
+ - repo: https://github.com/pycqa/isort
33
+ rev: 5.12.0
34
+ hooks:
35
+ - id: isort
36
+ args:
37
+ ["--profile", "black", "--filter-files", "--lines-after-imports=2"]
.vscode/extensions.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "recommendations": [
3
+ "tamasfe.even-better-toml",
4
+ "ms-python.black-formatter",
5
+ "ms-python.isort"
6
+ ],
7
+ "unwantedRecommendations": []
8
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people.
21
+ * Being respectful of differing opinions, viewpoints, and experiences.
22
+ * Giving and gracefully accepting constructive feedback.
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience.
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community.
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind.
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks.
33
+ * Public or private harassment.
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission.
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting.
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ mannaandpoem@gmail.com
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ### Slack and Discord Etiquettes
116
+
117
+ These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment
118
+ for all community members. By following these best practices, we ensure effective communication and collaboration while
119
+ minimizing disruptions. Let’s work together to build a supportive and welcoming community!
120
+
121
+ - Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be
122
+ difficult to interpret in text.
123
+ - Use threads for specific discussions to keep channels organized and easier to follow.
124
+ - Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize
125
+ disruptions.
126
+ - Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
127
+ - Post questions or discussions in the most relevant
128
+ channel ([discord - #general](https://discord.com/channels/1125308739348594758/1138430348557025341)).
129
+ - When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to
130
+ provide context.
131
+ - Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the
132
+ matter is sensitive or private.
133
+ - Always adhere to [our standards](https://github.com/FoundationAgents/OpenManus/blob/main/CODE_OF_CONDUCT.md#our-standards)
134
+ to ensure a welcoming and collaborative environment.
135
+ - If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For
136
+ Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For
137
+ example, if you're here for discussions about LLMs, mute the channel if it’s too busy, but set notifications to alert
138
+ you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that
139
+ best describes your need.
140
+
141
+ ## Attribution
142
+
143
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
144
+ version 2.1, available at
145
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
146
+
147
+ Community Impact Guidelines were inspired by
148
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
149
+
150
+ For answers to common questions about this code of conduct, see the FAQ at
151
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
152
+ [https://www.contributor-covenant.org/translations][translations].
153
+
154
+ [homepage]: https://www.contributor-covenant.org
155
+
156
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
157
+
158
+ [Mozilla CoC]: https://github.com/mozilla/diversity
159
+
160
+ [FAQ]: https://www.contributor-covenant.org/faq
161
+
162
+ [translations]: https://www.contributor-covenant.org/translations
Dockerfile ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ # Éviter les questions lors de l'installation des paquets
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ # Créer un utilisateur non-root pour Hugging Face Spaces
8
+ RUN useradd -m -u 1000 user
9
+ WORKDIR /home/user/app
10
+
11
+ # Installer les dépendances système nécessaires
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ git \
14
+ curl \
15
+ wget \
16
+ gnupg \
17
+ ca-certificates \
18
+ libglib2.0-0 \
19
+ libnss3 \
20
+ libnspr4 \
21
+ libatk1.0-0 \
22
+ libatk-bridge2.0-0 \
23
+ libcups2 \
24
+ libdrm2 \
25
+ libdbus-1-3 \
26
+ libxcb1 \
27
+ libxkbcommon0 \
28
+ libx11-6 \
29
+ libxcomposite1 \
30
+ libxdamage1 \
31
+ libxext6 \
32
+ libxfixes3 \
33
+ librandr2 \
34
+ libgbm1 \
35
+ libpango-1.0-0 \
36
+ libcairo2 \
37
+ libasound2 \
38
+ && rm -rf /var/lib/apt/lists/*
39
+
40
+ # Copier les fichiers du projet
41
+ COPY --chown=user:user . .
42
+
43
+ # Installer les dépendances Python
44
+ RUN pip install --no-cache-dir -r requirements.txt
45
+
46
+ # Installer Playwright et ses navigateurs
47
+ RUN pip install playwright && playwright install --with-deps chromium
48
+
49
+ # S'assurer que le répertoire de travail appartient à l'utilisateur
50
+ RUN chown -R user:user /home/user/app
51
+
52
+ # Passer à l'utilisateur non-root
53
+ USER user
54
+
55
+ # Exposer le port (Hugging Face utilise souvent 7860 par défaut pour Gradio/Streamlit,
56
+ # mais ici c'est un agent CLI. On peut ajouter une interface simple si besoin,
57
+ # mais pour l'instant on suit les instructions de déploiement Docker standard.)
58
+ EXPOSE 7860
59
+
60
+ # Commande par défaut (on peut lancer main.py ou un script d'attente)
61
+ # Pour Hugging Face Spaces, il faut souvent un service qui écoute sur un port.
62
+ # Si OpenManus est purement CLI, on pourrait avoir besoin d'un wrapper web.
63
+ # Cependant, l'utilisateur demande de vérifier que l'agent répond via l'URL du Space.
64
+ # Je vais ajouter un petit script serveur web minimal pour maintenir le Space actif
65
+ # et éventuellement fournir une interface de chat basique si OpenManus n'en a pas.
66
+
67
+ CMD ["python", "app_hf.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
README.md CHANGED
@@ -1,10 +1,195 @@
1
- ---
2
- title: OpenManus Gemini
3
- emoji: 🏢
4
- colorFrom: green
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <img src="assets/logo.jpg" width="200"/>
3
+ </p>
4
+
5
+ English | [中文](README_zh.md) | [한국어](README_ko.md) | [日本語](README_ja.md)
6
+
7
+ [![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
8
+ &ensp;
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
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 is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
17
+
18
+ Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
19
+
20
+ It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
21
+
22
+ Enjoy your own agent with OpenManus!
23
+
24
+ We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
25
+
26
+ ## Project Demo
27
+
28
+ <video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
29
+
30
+ ## Installation
31
+
32
+ We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
33
+
34
+ ### Method 1: Using conda
35
+
36
+ 1. Create a new conda environment:
37
+
38
+ ```bash
39
+ conda create -n open_manus python=3.12
40
+ conda activate open_manus
41
+ ```
42
+
43
+ 2. Clone the repository:
44
+
45
+ ```bash
46
+ git clone https://github.com/FoundationAgents/OpenManus.git
47
+ cd OpenManus
48
+ ```
49
+
50
+ 3. Install dependencies:
51
+
52
+ ```bash
53
+ pip install -r requirements.txt
54
+ ```
55
+
56
+ ### Method 2: Using uv (Recommended)
57
+
58
+ 1. Install uv (A fast Python package installer and resolver):
59
+
60
+ ```bash
61
+ curl -LsSf https://astral.sh/uv/install.sh | sh
62
+ ```
63
+
64
+ 2. Clone the repository:
65
+
66
+ ```bash
67
+ git clone https://github.com/FoundationAgents/OpenManus.git
68
+ cd OpenManus
69
+ ```
70
+
71
+ 3. Create a new virtual environment and activate it:
72
+
73
+ ```bash
74
+ uv venv --python 3.12
75
+ source .venv/bin/activate # On Unix/macOS
76
+ # Or on Windows:
77
+ # .venv\Scripts\activate
78
+ ```
79
+
80
+ 4. Install dependencies:
81
+
82
+ ```bash
83
+ uv pip install -r requirements.txt
84
+ ```
85
+
86
+ ### Browser Automation Tool (Optional)
87
+ ```bash
88
+ playwright install
89
+ ```
90
+
91
+ ## Configuration
92
+
93
+ OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
94
+
95
+ 1. Create a `config.toml` file in the `config` directory (you can copy from the example):
96
+
97
+ ```bash
98
+ cp config/config.example.toml config/config.toml
99
+ ```
100
+
101
+ 2. Edit `config/config.toml` to add your API keys and customize settings:
102
+
103
+ ```toml
104
+ # Global LLM configuration
105
+ [llm]
106
+ model = "gpt-4o"
107
+ base_url = "https://api.openai.com/v1"
108
+ api_key = "sk-..." # Replace with your actual API key
109
+ max_tokens = 4096
110
+ temperature = 0.0
111
+
112
+ # Optional configuration for specific LLM models
113
+ [llm.vision]
114
+ model = "gpt-4o"
115
+ base_url = "https://api.openai.com/v1"
116
+ api_key = "sk-..." # Replace with your actual API key
117
+ ```
118
+
119
+ ## Quick Start
120
+
121
+ One line for run OpenManus:
122
+
123
+ ```bash
124
+ python main.py
125
+ ```
126
+
127
+ Then input your idea via terminal!
128
+
129
+ For MCP tool version, you can run:
130
+ ```bash
131
+ python run_mcp.py
132
+ ```
133
+
134
+ For unstable multi-agent version, you also can run:
135
+
136
+ ```bash
137
+ python run_flow.py
138
+ ```
139
+
140
+ ### Custom Adding Multiple Agents
141
+
142
+ Currently, besides the general OpenManus Agent, we have also integrated the DataAnalysis Agent, which is suitable for data analysis and data visualization tasks. You can add this agent to `run_flow` in `config.toml`.
143
+
144
+ ```toml
145
+ # Optional configuration for run-flow
146
+ [runflow]
147
+ use_data_analysis_agent = true # Disabled by default, change to true to activate
148
+ ```
149
+ In addition, you need to install the relevant dependencies to ensure the agent runs properly: [Detailed Installation Guide](app/tool/chart_visualization/README.md##Installation)
150
+
151
+ ## How to contribute
152
+
153
+ We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
154
+
155
+ Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
156
+
157
+ **Note**: Before submitting a pull request, please use the pre-commit tool to check your changes. Run `pre-commit run --all-files` to execute the checks.
158
+
159
+ ## Community Group
160
+ Join our networking group on Feishu and share your experience with other developers!
161
+
162
+ <div align="center" style="display: flex; gap: 20px;">
163
+ <img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
164
+ </div>
165
+
166
+ ## Star History
167
+
168
+ [![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
169
+
170
+ ## Sponsors
171
+ Thanks to [PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) for computing source support.
172
+ > PPIO: The most affordable and easily-integrated MaaS and GPU cloud solution.
173
+
174
+
175
+ ## Acknowledgement
176
+
177
+ Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), [browser-use](https://github.com/browser-use/browser-use) and [crawl4ai](https://github.com/unclecode/crawl4ai) for providing basic support for this project!
178
+
179
+ Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands) and [SWE-agent](https://github.com/SWE-agent/SWE-agent).
180
+
181
+ We also thank stepfun(阶跃星辰) for supporting our Hugging Face demo space.
182
+
183
+ OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
184
+
185
+ ## Cite
186
+ ```bibtex
187
+ @misc{openmanus2025,
188
+ author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang and Bang Liu and Yuyu Luo and Chenglin Wu},
189
+ title = {OpenManus: An open-source framework for building general AI agents},
190
+ year = {2025},
191
+ publisher = {Zenodo},
192
+ doi = {10.5281/zenodo.15186407},
193
+ url = {https://doi.org/10.5281/zenodo.15186407},
194
+ }
195
+ ```
README_ja.md ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <img src="assets/logo.jpg" width="200"/>
3
+ </p>
4
+
5
+ [English](README.md) | [中文](README_zh.md) | [한국어](README_ko.md) | 日本語
6
+
7
+ [![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
8
+ &ensp;
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
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
+ <video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
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/FoundationAgents/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/FoundationAgents/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
+ 現在、一般的なOpenManusエージェントに加えて、データ分析とデータ可視化タスクに適したDataAnalysisエージェントが組み込まれています。このエージェントを`config.toml`の`run_flow`に追加することができます。
143
+
144
+ ```toml
145
+ # run-flowのオプション設定
146
+ [runflow]
147
+ use_data_analysis_agent = true # デフォルトでは無効、trueに変更すると有効化されます
148
+ ```
149
+
150
+ これに加えて、エージェントが正常に動作するために必要な依存関係をインストールする必要があります:[具体的なインストールガイド](app/tool/chart_visualization/README_ja.md##インストール)
151
+
152
+
153
+ ## 貢献方法
154
+
155
+ 我々は建設的な意見や有益な貢献を歓迎します!issueを作成するか、プルリクエストを提出してください。
156
+
157
+ または @mannaandpoem に📧メールでご連絡ください:mannaandpoem@gmail.com
158
+
159
+ **注意**: プルリクエストを送信する前に、pre-commitツールを使用して変更を確認してください。`pre-commit run --all-files`を実行してチェックを実行します。
160
+
161
+ ## コミュニティグループ
162
+ Feishuのネットワーキンググループに参加して、他の開発者と経験を共有しましょう!
163
+
164
+ <div align="center" style="display: flex; gap: 20px;">
165
+ <img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
166
+ </div>
167
+
168
+ ## スター履歴
169
+
170
+ [![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
171
+
172
+ ## 謝辞
173
+
174
+ このプロジェクトの基本的なサポートを提供してくれた[anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
175
+ と[browser-use](https://github.com/browser-use/browser-use)に感謝します!
176
+
177
+ さらに、[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)にも感謝します。
178
+
179
+ また、Hugging Face デモスペースをサポートしてくださった阶跃星辰 (stepfun)にも感謝いたします。
180
+
181
+ OpenManusはMetaGPTのコントリビューターによって構築されました。このエージェントコミュニティに大きな感謝を!
182
+
183
+ ## 引用
184
+ ```bibtex
185
+ @misc{openmanus2025,
186
+ author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
187
+ title = {OpenManus: An open-source framework for building general AI agents},
188
+ year = {2025},
189
+ publisher = {Zenodo},
190
+ doi = {10.5281/zenodo.15186407},
191
+ url = {https://doi.org/10.5281/zenodo.15186407},
192
+ }
193
+ ```
README_ko.md ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <img src="assets/logo.jpg" width="200"/>
3
+ </p>
4
+
5
+ [English](README.md) | [中文](README_zh.md) | 한국어 | [日本語](README_ja.md)
6
+
7
+ [![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
8
+ &ensp;
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
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
+ <video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
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/FoundationAgents/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/FoundationAgents/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
+ 현재 일반 OpenManus 에이전트 외에도 데이터 분석 및 데이터 시각화 작업에 적합한 DataAnalysis 에이전트를 통합했습니다. 이 에이전트를 `config.toml`의 `run_flow`에 추가할 수 있습니다.
143
+
144
+ ```toml
145
+ # run-flow에 대한 선택적 구성
146
+ [runflow]
147
+ use_data_analysis_agent = true # 기본적으로 비활성화되어 있으며, 활성화하려면 true로 변경
148
+ ```
149
+
150
+ 또한, 에이전트가 제대로 작동하도록 관련 종속성을 설치해야 합니다: [상세 설치 가이드](app/tool/chart_visualization/README.md##Installation)
151
+
152
+ ## 기여 방법
153
+
154
+ 모든 친절한 제안과 유용한 기여를 환영합니다! 이슈를 생성하거나 풀 리퀘스트를 제출해 주세요.
155
+
156
+ 또는 📧 메일로 연락주세요. @mannaandpoem : mannaandpoem@gmail.com
157
+
158
+ **참고**: pull request를 제출하기 전에 pre-commit 도구를 사용하여 변경 사항을 확인하십시오. `pre-commit run --all-files`를 실행하여 검사를 실행합니다.
159
+
160
+ ## 커뮤니티 그룹
161
+ Feishu 네트워킹 그룹에 참여하여 다른 개발자들과 경험을 공유하세요!
162
+
163
+ <div align="center" style="display: flex; gap: 20px;">
164
+ <img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
165
+ </div>
166
+
167
+ ## Star History
168
+
169
+ [![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
170
+
171
+ ## 감사의 글
172
+
173
+ 이 프로젝트에 기본적인 지원을 제공해 주신 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)와
174
+ [browser-use](https://github.com/browser-use/browser-use)에게 감사드립니다!
175
+
176
+ 또한, [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)에 깊은 감사를 드립니다.
177
+
178
+ 또한 Hugging Face 데모 공간을 지원해 주신 阶跃星辰 (stepfun)에게 감사드립니다.
179
+
180
+ OpenManus는 MetaGPT 기여자들에 의해 개발되었습니다. 이 에이전트 커뮤니티에 깊은 감사를 전합니다!
181
+
182
+ ## 인용
183
+ ```bibtex
184
+ @misc{openmanus2025,
185
+ author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
186
+ title = {OpenManus: An open-source framework for building general AI agents},
187
+ year = {2025},
188
+ publisher = {Zenodo},
189
+ doi = {10.5281/zenodo.15186407},
190
+ url = {https://doi.org/10.5281/zenodo.15186407},
191
+ }
192
+ ```
README_zh.md ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <img src="assets/logo.jpg" width="200"/>
3
+ </p>
4
+
5
+ [English](README.md) | 中文 | [한국어](README_ko.md) | [日本語](README_ja.md)
6
+
7
+ [![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
8
+ &ensp;
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
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
+ <video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
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/FoundationAgents/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/FoundationAgents/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
+ 目前除了通用的 OpenManus Agent, 我们还内置了DataAnalysis Agent,适用于数据分析和数据可视化任务,你可以在`config.toml`中将这个智能体加入到`run_flow`中
144
+ ```toml
145
+ # run-flow可选配置
146
+ [runflow]
147
+ use_data_analysis_agent = true # 默认关闭,将其改为true则为激活
148
+ ```
149
+ 除此之外,你还需要安装相关的依赖来确保智能体正常运行:[具体安装指南](app/tool/chart_visualization/README_zh.md##安装)
150
+
151
+
152
+ ## 贡献指南
153
+
154
+ 我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。
155
+
156
+ 或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com
157
+
158
+ **注意**: 在提交 pull request 之前,请使用 pre-commit 工具检查您的更改。运行 `pre-commit run --all-files` 来执行检查。
159
+
160
+ ## 交流群
161
+
162
+ 加入我们的飞书交流群,与其他开发者分享经验!
163
+
164
+ <div align="center" style="display: flex; gap: 20px;">
165
+ <img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
166
+ </div>
167
+
168
+ ## Star 数量
169
+
170
+ [![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
171
+
172
+
173
+ ## 赞助商
174
+ 感谢[PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) 提供的算力支持。
175
+ > PPIO派欧云:一键调用高性价比的开源模型API和GPU容器
176
+
177
+ ## 致谢
178
+
179
+ 特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
180
+ 和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持!
181
+
182
+ 此外,我们感谢 [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).
183
+
184
+ 我们也感谢阶跃星辰 (stepfun) 提供的 Hugging Face 演示空间支持。
185
+
186
+ OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
187
+
188
+ ## 引用
189
+ ```bibtex
190
+ @misc{openmanus2025,
191
+ author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
192
+ title = {OpenManus: An open-source framework for building general AI agents},
193
+ year = {2025},
194
+ publisher = {Zenodo},
195
+ doi = {10.5281/zenodo.15186407},
196
+ url = {https://doi.org/10.5281/zenodo.15186407},
197
+ }
198
+ ```
app/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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
+ )
app/agent/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ]
app/agent/base.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from contextlib import asynccontextmanager
3
+ from typing import List, Optional
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+ from app.llm import LLM
8
+ from app.logger import logger
9
+ from app.sandbox.client import SANDBOX_CLIENT
10
+ from app.schema import ROLE_TYPE, AgentState, Memory, Message
11
+
12
+
13
+ class BaseAgent(BaseModel, ABC):
14
+ """Abstract base class for managing agent state and execution.
15
+
16
+ Provides foundational functionality for state transitions, memory management,
17
+ and a step-based execution loop. Subclasses must implement the `step` method.
18
+ """
19
+
20
+ # Core attributes
21
+ name: str = Field(..., description="Unique name of the agent")
22
+ description: Optional[str] = Field(None, description="Optional agent description")
23
+
24
+ # Prompts
25
+ system_prompt: Optional[str] = Field(
26
+ None, description="System-level instruction prompt"
27
+ )
28
+ next_step_prompt: Optional[str] = Field(
29
+ None, description="Prompt for determining next action"
30
+ )
31
+
32
+ # Dependencies
33
+ llm: LLM = Field(default_factory=LLM, description="Language model instance")
34
+ memory: Memory = Field(default_factory=Memory, description="Agent's memory store")
35
+ state: AgentState = Field(
36
+ default=AgentState.IDLE, description="Current agent state"
37
+ )
38
+
39
+ # Execution control
40
+ max_steps: int = Field(default=10, description="Maximum steps before termination")
41
+ current_step: int = Field(default=0, description="Current step in execution")
42
+
43
+ duplicate_threshold: int = 2
44
+
45
+ class Config:
46
+ arbitrary_types_allowed = True
47
+ extra = "allow" # Allow extra fields for flexibility in subclasses
48
+
49
+ @model_validator(mode="after")
50
+ def initialize_agent(self) -> "BaseAgent":
51
+ """Initialize agent with default settings if not provided."""
52
+ if self.llm is None or not isinstance(self.llm, LLM):
53
+ self.llm = LLM(config_name=self.name.lower())
54
+ if not isinstance(self.memory, Memory):
55
+ self.memory = Memory()
56
+ return self
57
+
58
+ @asynccontextmanager
59
+ async def state_context(self, new_state: AgentState):
60
+ """Context manager for safe agent state transitions.
61
+
62
+ Args:
63
+ new_state: The state to transition to during the context.
64
+
65
+ Yields:
66
+ None: Allows execution within the new state.
67
+
68
+ Raises:
69
+ ValueError: If the new_state is invalid.
70
+ """
71
+ if not isinstance(new_state, AgentState):
72
+ raise ValueError(f"Invalid state: {new_state}")
73
+
74
+ previous_state = self.state
75
+ self.state = new_state
76
+ try:
77
+ yield
78
+ except Exception as e:
79
+ self.state = AgentState.ERROR # Transition to ERROR on failure
80
+ raise e
81
+ finally:
82
+ self.state = previous_state # Revert to previous state
83
+
84
+ def update_memory(
85
+ self,
86
+ role: ROLE_TYPE, # type: ignore
87
+ content: str,
88
+ base64_image: Optional[str] = None,
89
+ **kwargs,
90
+ ) -> None:
91
+ """Add a message to the agent's memory.
92
+
93
+ Args:
94
+ role: The role of the message sender (user, system, assistant, tool).
95
+ content: The message content.
96
+ base64_image: Optional base64 encoded image.
97
+ **kwargs: Additional arguments (e.g., tool_call_id for tool messages).
98
+
99
+ Raises:
100
+ ValueError: If the role is unsupported.
101
+ """
102
+ message_map = {
103
+ "user": Message.user_message,
104
+ "system": Message.system_message,
105
+ "assistant": Message.assistant_message,
106
+ "tool": lambda content, **kw: Message.tool_message(content, **kw),
107
+ }
108
+
109
+ if role not in message_map:
110
+ raise ValueError(f"Unsupported message role: {role}")
111
+
112
+ # Create message with appropriate parameters based on role
113
+ kwargs = {"base64_image": base64_image, **(kwargs if role == "tool" else {})}
114
+ self.memory.add_message(message_map[role](content, **kwargs))
115
+
116
+ async def run(self, request: Optional[str] = None) -> str:
117
+ """Execute the agent's main loop asynchronously.
118
+
119
+ Args:
120
+ request: Optional initial user request to process.
121
+
122
+ Returns:
123
+ A string summarizing the execution results.
124
+
125
+ Raises:
126
+ RuntimeError: If the agent is not in IDLE state at start.
127
+ """
128
+ if self.state != AgentState.IDLE:
129
+ raise RuntimeError(f"Cannot run agent from state: {self.state}")
130
+
131
+ if request:
132
+ self.update_memory("user", request)
133
+
134
+ results: List[str] = []
135
+ async with self.state_context(AgentState.RUNNING):
136
+ while (
137
+ self.current_step < self.max_steps and self.state != AgentState.FINISHED
138
+ ):
139
+ self.current_step += 1
140
+ logger.info(f"Executing step {self.current_step}/{self.max_steps}")
141
+ step_result = await self.step()
142
+
143
+ # Check for stuck state
144
+ if self.is_stuck():
145
+ self.handle_stuck_state()
146
+
147
+ results.append(f"Step {self.current_step}: {step_result}")
148
+
149
+ if self.current_step >= self.max_steps:
150
+ self.current_step = 0
151
+ self.state = AgentState.IDLE
152
+ results.append(f"Terminated: Reached max steps ({self.max_steps})")
153
+ await SANDBOX_CLIENT.cleanup()
154
+ return "\n".join(results) if results else "No steps executed"
155
+
156
+ @abstractmethod
157
+ async def step(self) -> str:
158
+ """Execute a single step in the agent's workflow.
159
+
160
+ Must be implemented by subclasses to define specific behavior.
161
+ """
162
+
163
+ def handle_stuck_state(self):
164
+ """Handle stuck state by adding a prompt to change strategy"""
165
+ stuck_prompt = "\
166
+ Observed duplicate responses. Consider new strategies and avoid repeating ineffective paths already attempted."
167
+ self.next_step_prompt = f"{stuck_prompt}\n{self.next_step_prompt}"
168
+ logger.warning(f"Agent detected stuck state. Added prompt: {stuck_prompt}")
169
+
170
+ def is_stuck(self) -> bool:
171
+ """Check if the agent is stuck in a loop by detecting duplicate content"""
172
+ if len(self.memory.messages) < 2:
173
+ return False
174
+
175
+ last_message = self.memory.messages[-1]
176
+ if not last_message.content:
177
+ return False
178
+
179
+ # Count identical content occurrences
180
+ duplicate_count = sum(
181
+ 1
182
+ for msg in reversed(self.memory.messages[:-1])
183
+ if msg.role == "assistant" and msg.content == last_message.content
184
+ )
185
+
186
+ return duplicate_count >= self.duplicate_threshold
187
+
188
+ @property
189
+ def messages(self) -> List[Message]:
190
+ """Retrieve a list of messages from the agent's memory."""
191
+ return self.memory.messages
192
+
193
+ @messages.setter
194
+ def messages(self, value: List[Message]):
195
+ """Set the list of messages in the agent's memory."""
196
+ self.memory.messages = value
app/agent/browser.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool
12
+
13
+
14
+ # Avoid circular import if BrowserAgent needs BrowserContextHelper
15
+ if TYPE_CHECKING:
16
+ from app.agent.base import BaseAgent # Or wherever memory is defined
17
+
18
+
19
+ class BrowserContextHelper:
20
+ def __init__(self, agent: "BaseAgent"):
21
+ self.agent = agent
22
+ self._current_base64_image: Optional[str] = None
23
+
24
+ async def get_browser_state(self) -> Optional[dict]:
25
+ browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name)
26
+ if not browser_tool:
27
+ browser_tool = self.agent.available_tools.get_tool(
28
+ SandboxBrowserTool().name
29
+ )
30
+ if not browser_tool or not hasattr(browser_tool, "get_current_state"):
31
+ logger.warning("BrowserUseTool not found or doesn't have get_current_state")
32
+ return None
33
+ try:
34
+ result = await browser_tool.get_current_state()
35
+ if result.error:
36
+ logger.debug(f"Browser state error: {result.error}")
37
+ return None
38
+ if hasattr(result, "base64_image") and result.base64_image:
39
+ self._current_base64_image = result.base64_image
40
+ else:
41
+ self._current_base64_image = None
42
+ return json.loads(result.output)
43
+ except Exception as e:
44
+ logger.debug(f"Failed to get browser state: {str(e)}")
45
+ return None
46
+
47
+ async def format_next_step_prompt(self) -> str:
48
+ """Gets browser state and formats the browser prompt."""
49
+ browser_state = await self.get_browser_state()
50
+ url_info, tabs_info, content_above_info, content_below_info = "", "", "", ""
51
+ results_info = "" # Or get from agent if needed elsewhere
52
+
53
+ if browser_state and not browser_state.get("error"):
54
+ url_info = f"\n URL: {browser_state.get('url', 'N/A')}\n Title: {browser_state.get('title', 'N/A')}"
55
+ tabs = browser_state.get("tabs", [])
56
+ if tabs:
57
+ tabs_info = f"\n {len(tabs)} tab(s) available"
58
+ pixels_above = browser_state.get("pixels_above", 0)
59
+ pixels_below = browser_state.get("pixels_below", 0)
60
+ if pixels_above > 0:
61
+ content_above_info = f" ({pixels_above} pixels)"
62
+ if pixels_below > 0:
63
+ content_below_info = f" ({pixels_below} pixels)"
64
+
65
+ if self._current_base64_image:
66
+ image_message = Message.user_message(
67
+ content="Current browser screenshot:",
68
+ base64_image=self._current_base64_image,
69
+ )
70
+ self.agent.memory.add_message(image_message)
71
+ self._current_base64_image = None # Consume the image after adding
72
+
73
+ return NEXT_STEP_PROMPT.format(
74
+ url_placeholder=url_info,
75
+ tabs_placeholder=tabs_info,
76
+ content_above_placeholder=content_above_info,
77
+ content_below_placeholder=content_below_info,
78
+ results_placeholder=results_info,
79
+ )
80
+
81
+ async def cleanup_browser(self):
82
+ browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name)
83
+ if browser_tool and hasattr(browser_tool, "cleanup"):
84
+ await browser_tool.cleanup()
85
+
86
+
87
+ class BrowserAgent(ToolCallAgent):
88
+ """
89
+ A browser agent that uses the browser_use library to control a browser.
90
+
91
+ This agent can navigate web pages, interact with elements, fill forms,
92
+ extract content, and perform other browser-based actions to accomplish tasks.
93
+ """
94
+
95
+ name: str = "browser"
96
+ description: str = "A browser agent that can control a browser to accomplish tasks"
97
+
98
+ system_prompt: str = SYSTEM_PROMPT
99
+ next_step_prompt: str = NEXT_STEP_PROMPT
100
+
101
+ max_observe: int = 10000
102
+ max_steps: int = 20
103
+
104
+ # Configure the available tools
105
+ available_tools: ToolCollection = Field(
106
+ default_factory=lambda: ToolCollection(BrowserUseTool(), Terminate())
107
+ )
108
+
109
+ # Use Auto for tool choice to allow both tool usage and free-form responses
110
+ tool_choices: ToolChoice = ToolChoice.AUTO
111
+ special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
112
+
113
+ browser_context_helper: Optional[BrowserContextHelper] = None
114
+
115
+ @model_validator(mode="after")
116
+ def initialize_helper(self) -> "BrowserAgent":
117
+ self.browser_context_helper = BrowserContextHelper(self)
118
+ return self
119
+
120
+ async def think(self) -> bool:
121
+ """Process current state and decide next actions using tools, with browser state info added"""
122
+ self.next_step_prompt = (
123
+ await self.browser_context_helper.format_next_step_prompt()
124
+ )
125
+ return await super().think()
126
+
127
+ async def cleanup(self):
128
+ """Clean up browser agent resources by calling parent cleanup."""
129
+ await self.browser_context_helper.cleanup_browser()
app/agent/data_analysis.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = "Data_Analysis"
21
+ description: str = "An analytical agent that utilizes python and data visualization 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
+ )
app/agent/manus.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
app/agent/mcp.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Optional, Tuple
2
+
3
+ from pydantic import Field
4
+
5
+ from app.agent.toolcall import ToolCallAgent
6
+ from app.logger import logger
7
+ from app.prompt.mcp import MULTIMEDIA_RESPONSE_PROMPT, NEXT_STEP_PROMPT, SYSTEM_PROMPT
8
+ from app.schema import AgentState, Message
9
+ from app.tool.base import ToolResult
10
+ from app.tool.mcp import MCPClients
11
+
12
+
13
+ class MCPAgent(ToolCallAgent):
14
+ """Agent for interacting with MCP (Model Context Protocol) servers.
15
+
16
+ This agent connects to an MCP server using either SSE or stdio transport
17
+ and makes the server's tools available through the agent's tool interface.
18
+ """
19
+
20
+ name: str = "mcp_agent"
21
+ description: str = "An agent that connects to an MCP server and uses its tools."
22
+
23
+ system_prompt: str = SYSTEM_PROMPT
24
+ next_step_prompt: str = NEXT_STEP_PROMPT
25
+
26
+ # Initialize MCP tool collection
27
+ mcp_clients: MCPClients = Field(default_factory=MCPClients)
28
+ available_tools: MCPClients = None # Will be set in initialize()
29
+
30
+ max_steps: int = 20
31
+ connection_type: str = "stdio" # "stdio" or "sse"
32
+
33
+ # Track tool schemas to detect changes
34
+ tool_schemas: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
35
+ _refresh_tools_interval: int = 5 # Refresh tools every N steps
36
+
37
+ # Special tool names that should trigger termination
38
+ special_tool_names: List[str] = Field(default_factory=lambda: ["terminate"])
39
+
40
+ async def initialize(
41
+ self,
42
+ connection_type: Optional[str] = None,
43
+ server_url: Optional[str] = None,
44
+ command: Optional[str] = None,
45
+ args: Optional[List[str]] = None,
46
+ ) -> None:
47
+ """Initialize the MCP connection.
48
+
49
+ Args:
50
+ connection_type: Type of connection to use ("stdio" or "sse")
51
+ server_url: URL of the MCP server (for SSE connection)
52
+ command: Command to run (for stdio connection)
53
+ args: Arguments for the command (for stdio connection)
54
+ """
55
+ if connection_type:
56
+ self.connection_type = connection_type
57
+
58
+ # Connect to the MCP server based on connection type
59
+ if self.connection_type == "sse":
60
+ if not server_url:
61
+ raise ValueError("Server URL is required for SSE connection")
62
+ await self.mcp_clients.connect_sse(server_url=server_url)
63
+ elif self.connection_type == "stdio":
64
+ if not command:
65
+ raise ValueError("Command is required for stdio connection")
66
+ await self.mcp_clients.connect_stdio(command=command, args=args or [])
67
+ else:
68
+ raise ValueError(f"Unsupported connection type: {self.connection_type}")
69
+
70
+ # Set available_tools to our MCP instance
71
+ self.available_tools = self.mcp_clients
72
+
73
+ # Store initial tool schemas
74
+ await self._refresh_tools()
75
+
76
+ # Add system message about available tools
77
+ tool_names = list(self.mcp_clients.tool_map.keys())
78
+ tools_info = ", ".join(tool_names)
79
+
80
+ # Add system prompt and available tools information
81
+ self.memory.add_message(
82
+ Message.system_message(
83
+ f"{self.system_prompt}\n\nAvailable MCP tools: {tools_info}"
84
+ )
85
+ )
86
+
87
+ async def _refresh_tools(self) -> Tuple[List[str], List[str]]:
88
+ """Refresh the list of available tools from the MCP server.
89
+
90
+ Returns:
91
+ A tuple of (added_tools, removed_tools)
92
+ """
93
+ if not self.mcp_clients.sessions:
94
+ return [], []
95
+
96
+ # Get current tool schemas directly from the server
97
+ response = await self.mcp_clients.list_tools()
98
+ current_tools = {tool.name: tool.inputSchema for tool in response.tools}
99
+
100
+ # Determine added, removed, and changed tools
101
+ current_names = set(current_tools.keys())
102
+ previous_names = set(self.tool_schemas.keys())
103
+
104
+ added_tools = list(current_names - previous_names)
105
+ removed_tools = list(previous_names - current_names)
106
+
107
+ # Check for schema changes in existing tools
108
+ changed_tools = []
109
+ for name in current_names.intersection(previous_names):
110
+ if current_tools[name] != self.tool_schemas.get(name):
111
+ changed_tools.append(name)
112
+
113
+ # Update stored schemas
114
+ self.tool_schemas = current_tools
115
+
116
+ # Log and notify about changes
117
+ if added_tools:
118
+ logger.info(f"Added MCP tools: {added_tools}")
119
+ self.memory.add_message(
120
+ Message.system_message(f"New tools available: {', '.join(added_tools)}")
121
+ )
122
+ if removed_tools:
123
+ logger.info(f"Removed MCP tools: {removed_tools}")
124
+ self.memory.add_message(
125
+ Message.system_message(
126
+ f"Tools no longer available: {', '.join(removed_tools)}"
127
+ )
128
+ )
129
+ if changed_tools:
130
+ logger.info(f"Changed MCP tools: {changed_tools}")
131
+
132
+ return added_tools, removed_tools
133
+
134
+ async def think(self) -> bool:
135
+ """Process current state and decide next action."""
136
+ # Check MCP session and tools availability
137
+ if not self.mcp_clients.sessions or not self.mcp_clients.tool_map:
138
+ logger.info("MCP service is no longer available, ending interaction")
139
+ self.state = AgentState.FINISHED
140
+ return False
141
+
142
+ # Refresh tools periodically
143
+ if self.current_step % self._refresh_tools_interval == 0:
144
+ await self._refresh_tools()
145
+ # All tools removed indicates shutdown
146
+ if not self.mcp_clients.tool_map:
147
+ logger.info("MCP service has shut down, ending interaction")
148
+ self.state = AgentState.FINISHED
149
+ return False
150
+
151
+ # Use the parent class's think method
152
+ return await super().think()
153
+
154
+ async def _handle_special_tool(self, name: str, result: Any, **kwargs) -> None:
155
+ """Handle special tool execution and state changes"""
156
+ # First process with parent handler
157
+ await super()._handle_special_tool(name, result, **kwargs)
158
+
159
+ # Handle multimedia responses
160
+ if isinstance(result, ToolResult) and result.base64_image:
161
+ self.memory.add_message(
162
+ Message.system_message(
163
+ MULTIMEDIA_RESPONSE_PROMPT.format(tool_name=name)
164
+ )
165
+ )
166
+
167
+ def _should_finish_execution(self, name: str, **kwargs) -> bool:
168
+ """Determine if tool execution should finish the agent"""
169
+ # Terminate if the tool name is 'terminate'
170
+ return name.lower() == "terminate"
171
+
172
+ async def cleanup(self) -> None:
173
+ """Clean up MCP connection when done."""
174
+ if self.mcp_clients.sessions:
175
+ await self.mcp_clients.disconnect()
176
+ logger.info("MCP connection closed")
177
+
178
+ async def run(self, request: Optional[str] = None) -> str:
179
+ """Run the agent with cleanup when done."""
180
+ try:
181
+ result = await super().run(request)
182
+ return result
183
+ finally:
184
+ # Ensure cleanup happens even if there's an error
185
+ await self.cleanup()
app/agent/react.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
app/agent/sandbox_agent.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.daytona.sandbox import create_sandbox, delete_sandbox
9
+ from app.daytona.tool_base import SandboxToolsBase
10
+ from app.logger import logger
11
+ from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
12
+ from app.tool import Terminate, ToolCollection
13
+ from app.tool.ask_human import AskHuman
14
+ from app.tool.mcp import MCPClients, MCPClientTool
15
+ from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool
16
+ from app.tool.sandbox.sb_files_tool import SandboxFilesTool
17
+ from app.tool.sandbox.sb_shell_tool import SandboxShellTool
18
+ from app.tool.sandbox.sb_vision_tool import SandboxVisionTool
19
+
20
+
21
+ class SandboxManus(ToolCallAgent):
22
+ """A versatile general-purpose agent with support for both local and MCP tools."""
23
+
24
+ name: str = "SandboxManus"
25
+ description: str = "A versatile agent that can solve various tasks using multiple sandbox-tools including MCP-based tools"
26
+
27
+ system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root)
28
+ next_step_prompt: str = NEXT_STEP_PROMPT
29
+
30
+ max_observe: int = 10000
31
+ max_steps: int = 20
32
+
33
+ # MCP clients for remote tool access
34
+ mcp_clients: MCPClients = Field(default_factory=MCPClients)
35
+
36
+ # Add general-purpose tools to the tool collection
37
+ available_tools: ToolCollection = Field(
38
+ default_factory=lambda: ToolCollection(
39
+ # PythonExecute(),
40
+ # BrowserUseTool(),
41
+ # StrReplaceEditor(),
42
+ AskHuman(),
43
+ Terminate(),
44
+ )
45
+ )
46
+
47
+ special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
48
+ browser_context_helper: Optional[BrowserContextHelper] = None
49
+
50
+ # Track connected MCP servers
51
+ connected_servers: Dict[str, str] = Field(
52
+ default_factory=dict
53
+ ) # server_id -> url/command
54
+ _initialized: bool = False
55
+ sandbox_link: Optional[dict[str, dict[str, str]]] = Field(default_factory=dict)
56
+
57
+ @model_validator(mode="after")
58
+ def initialize_helper(self) -> "SandboxManus":
59
+ """Initialize basic components synchronously."""
60
+ self.browser_context_helper = BrowserContextHelper(self)
61
+ return self
62
+
63
+ @classmethod
64
+ async def create(cls, **kwargs) -> "SandboxManus":
65
+ """Factory method to create and properly initialize a Manus instance."""
66
+ instance = cls(**kwargs)
67
+ await instance.initialize_mcp_servers()
68
+ await instance.initialize_sandbox_tools()
69
+ instance._initialized = True
70
+ return instance
71
+
72
+ async def initialize_sandbox_tools(
73
+ self,
74
+ password: str = config.daytona.VNC_password,
75
+ ) -> None:
76
+ try:
77
+ # 创建新沙箱
78
+ if password:
79
+ sandbox = create_sandbox(password=password)
80
+ self.sandbox = sandbox
81
+ else:
82
+ raise ValueError("password must be provided")
83
+ vnc_link = sandbox.get_preview_link(6080)
84
+ website_link = sandbox.get_preview_link(8080)
85
+ vnc_url = vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link)
86
+ website_url = (
87
+ website_link.url if hasattr(website_link, "url") else str(website_link)
88
+ )
89
+
90
+ # Get the actual sandbox_id from the created sandbox
91
+ actual_sandbox_id = sandbox.id if hasattr(sandbox, "id") else "new_sandbox"
92
+ if not self.sandbox_link:
93
+ self.sandbox_link = {}
94
+ self.sandbox_link[actual_sandbox_id] = {
95
+ "vnc": vnc_url,
96
+ "website": website_url,
97
+ }
98
+ logger.info(f"VNC URL: {vnc_url}")
99
+ logger.info(f"Website URL: {website_url}")
100
+ SandboxToolsBase._urls_printed = True
101
+ sb_tools = [
102
+ SandboxBrowserTool(sandbox),
103
+ SandboxFilesTool(sandbox),
104
+ SandboxShellTool(sandbox),
105
+ SandboxVisionTool(sandbox),
106
+ ]
107
+ self.available_tools.add_tools(*sb_tools)
108
+
109
+ except Exception as e:
110
+ logger.error(f"Error initializing sandbox tools: {e}")
111
+ raise
112
+
113
+ async def initialize_mcp_servers(self) -> None:
114
+ """Initialize connections to configured MCP servers."""
115
+ for server_id, server_config in config.mcp_config.servers.items():
116
+ try:
117
+ if server_config.type == "sse":
118
+ if server_config.url:
119
+ await self.connect_mcp_server(server_config.url, server_id)
120
+ logger.info(
121
+ f"Connected to MCP server {server_id} at {server_config.url}"
122
+ )
123
+ elif server_config.type == "stdio":
124
+ if server_config.command:
125
+ await self.connect_mcp_server(
126
+ server_config.command,
127
+ server_id,
128
+ use_stdio=True,
129
+ stdio_args=server_config.args,
130
+ )
131
+ logger.info(
132
+ f"Connected to MCP server {server_id} using command {server_config.command}"
133
+ )
134
+ except Exception as e:
135
+ logger.error(f"Failed to connect to MCP server {server_id}: {e}")
136
+
137
+ async def connect_mcp_server(
138
+ self,
139
+ server_url: str,
140
+ server_id: str = "",
141
+ use_stdio: bool = False,
142
+ stdio_args: List[str] = None,
143
+ ) -> None:
144
+ """Connect to an MCP server and add its tools."""
145
+ if use_stdio:
146
+ await self.mcp_clients.connect_stdio(
147
+ server_url, stdio_args or [], server_id
148
+ )
149
+ self.connected_servers[server_id or server_url] = server_url
150
+ else:
151
+ await self.mcp_clients.connect_sse(server_url, server_id)
152
+ self.connected_servers[server_id or server_url] = server_url
153
+
154
+ # Update available tools with only the new tools from this server
155
+ new_tools = [
156
+ tool for tool in self.mcp_clients.tools if tool.server_id == server_id
157
+ ]
158
+ self.available_tools.add_tools(*new_tools)
159
+
160
+ async def disconnect_mcp_server(self, server_id: str = "") -> None:
161
+ """Disconnect from an MCP server and remove its tools."""
162
+ await self.mcp_clients.disconnect(server_id)
163
+ if server_id:
164
+ self.connected_servers.pop(server_id, None)
165
+ else:
166
+ self.connected_servers.clear()
167
+
168
+ # Rebuild available tools without the disconnected server's tools
169
+ base_tools = [
170
+ tool
171
+ for tool in self.available_tools.tools
172
+ if not isinstance(tool, MCPClientTool)
173
+ ]
174
+ self.available_tools = ToolCollection(*base_tools)
175
+ self.available_tools.add_tools(*self.mcp_clients.tools)
176
+
177
+ async def delete_sandbox(self, sandbox_id: str) -> None:
178
+ """Delete a sandbox by ID."""
179
+ try:
180
+ await delete_sandbox(sandbox_id)
181
+ logger.info(f"Sandbox {sandbox_id} deleted successfully")
182
+ if sandbox_id in self.sandbox_link:
183
+ del self.sandbox_link[sandbox_id]
184
+ except Exception as e:
185
+ logger.error(f"Error deleting sandbox {sandbox_id}: {e}")
186
+ raise e
187
+
188
+ async def cleanup(self):
189
+ """Clean up Manus agent resources."""
190
+ if self.browser_context_helper:
191
+ await self.browser_context_helper.cleanup_browser()
192
+ # Disconnect from all MCP servers only if we were initialized
193
+ if self._initialized:
194
+ await self.disconnect_mcp_server()
195
+ await self.delete_sandbox(self.sandbox.id if self.sandbox else "unknown")
196
+ self._initialized = False
197
+
198
+ async def think(self) -> bool:
199
+ """Process current state and decide next actions with appropriate context."""
200
+ if not self._initialized:
201
+ await self.initialize_mcp_servers()
202
+ self._initialized = True
203
+
204
+ original_prompt = self.next_step_prompt
205
+ recent_messages = self.memory.messages[-3:] if self.memory.messages else []
206
+ browser_in_use = any(
207
+ tc.function.name == SandboxBrowserTool().name
208
+ for msg in recent_messages
209
+ if msg.tool_calls
210
+ for tc in msg.tool_calls
211
+ )
212
+
213
+ if browser_in_use:
214
+ self.next_step_prompt = (
215
+ await self.browser_context_helper.format_next_step_prompt()
216
+ )
217
+
218
+ result = await super().think()
219
+
220
+ # Restore original prompt
221
+ self.next_step_prompt = original_prompt
222
+
223
+ return result
app/agent/swe.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
app/agent/toolcall.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ from typing import Any, List, Optional, Union
4
+
5
+ from pydantic import Field
6
+
7
+ from app.agent.react import ReActAgent
8
+ from app.exceptions import TokenLimitExceeded
9
+ from app.logger import logger
10
+ from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT
11
+ from app.schema import TOOL_CHOICE_TYPE, AgentState, Message, ToolCall, ToolChoice
12
+ from app.tool import CreateChatCompletion, Terminate, ToolCollection
13
+
14
+
15
+ TOOL_CALL_REQUIRED = "Tool calls required but none provided"
16
+
17
+
18
+ class ToolCallAgent(ReActAgent):
19
+ """Base agent class for handling tool/function calls with enhanced abstraction"""
20
+
21
+ name: str = "toolcall"
22
+ description: str = "an agent that can execute tool calls."
23
+
24
+ system_prompt: str = SYSTEM_PROMPT
25
+ next_step_prompt: str = NEXT_STEP_PROMPT
26
+
27
+ available_tools: ToolCollection = ToolCollection(
28
+ CreateChatCompletion(), Terminate()
29
+ )
30
+ tool_choices: TOOL_CHOICE_TYPE = ToolChoice.AUTO # type: ignore
31
+ special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
32
+
33
+ tool_calls: List[ToolCall] = Field(default_factory=list)
34
+ _current_base64_image: Optional[str] = None
35
+
36
+ max_steps: int = 30
37
+ max_observe: Optional[Union[int, bool]] = None
38
+
39
+ async def think(self) -> bool:
40
+ """Process current state and decide next actions using tools"""
41
+ if self.next_step_prompt:
42
+ user_msg = Message.user_message(self.next_step_prompt)
43
+ self.messages += [user_msg]
44
+
45
+ try:
46
+ # Get response with tool options
47
+ response = await self.llm.ask_tool(
48
+ messages=self.messages,
49
+ system_msgs=(
50
+ [Message.system_message(self.system_prompt)]
51
+ if self.system_prompt
52
+ else None
53
+ ),
54
+ tools=self.available_tools.to_params(),
55
+ tool_choice=self.tool_choices,
56
+ )
57
+ except ValueError:
58
+ raise
59
+ except Exception as e:
60
+ # Check if this is a RetryError containing TokenLimitExceeded
61
+ if hasattr(e, "__cause__") and isinstance(e.__cause__, TokenLimitExceeded):
62
+ token_limit_error = e.__cause__
63
+ logger.error(
64
+ f"🚨 Token limit error (from RetryError): {token_limit_error}"
65
+ )
66
+ self.memory.add_message(
67
+ Message.assistant_message(
68
+ f"Maximum token limit reached, cannot continue execution: {str(token_limit_error)}"
69
+ )
70
+ )
71
+ self.state = AgentState.FINISHED
72
+ return False
73
+ raise
74
+
75
+ self.tool_calls = tool_calls = (
76
+ response.tool_calls if response and response.tool_calls else []
77
+ )
78
+ content = response.content if response and response.content else ""
79
+
80
+ # Log response info
81
+ logger.info(f"✨ {self.name}'s thoughts: {content}")
82
+ logger.info(
83
+ f"🛠️ {self.name} selected {len(tool_calls) if tool_calls else 0} tools to use"
84
+ )
85
+ if tool_calls:
86
+ logger.info(
87
+ f"🧰 Tools being prepared: {[call.function.name for call in tool_calls]}"
88
+ )
89
+ logger.info(f"🔧 Tool arguments: {tool_calls[0].function.arguments}")
90
+
91
+ try:
92
+ if response is None:
93
+ raise RuntimeError("No response received from the LLM")
94
+
95
+ # Handle different tool_choices modes
96
+ if self.tool_choices == ToolChoice.NONE:
97
+ if tool_calls:
98
+ logger.warning(
99
+ f"🤔 Hmm, {self.name} tried to use tools when they weren't available!"
100
+ )
101
+ if content:
102
+ self.memory.add_message(Message.assistant_message(content))
103
+ return True
104
+ return False
105
+
106
+ # Create and add assistant message
107
+ assistant_msg = (
108
+ Message.from_tool_calls(content=content, tool_calls=self.tool_calls)
109
+ if self.tool_calls
110
+ else Message.assistant_message(content)
111
+ )
112
+ self.memory.add_message(assistant_msg)
113
+
114
+ if self.tool_choices == ToolChoice.REQUIRED and not self.tool_calls:
115
+ return True # Will be handled in act()
116
+
117
+ # For 'auto' mode, continue with content if no commands but content exists
118
+ if self.tool_choices == ToolChoice.AUTO and not self.tool_calls:
119
+ return bool(content)
120
+
121
+ return bool(self.tool_calls)
122
+ except Exception as e:
123
+ logger.error(f"🚨 Oops! The {self.name}'s thinking process hit a snag: {e}")
124
+ self.memory.add_message(
125
+ Message.assistant_message(
126
+ f"Error encountered while processing: {str(e)}"
127
+ )
128
+ )
129
+ return False
130
+
131
+ async def act(self) -> str:
132
+ """Execute tool calls and handle their results"""
133
+ if not self.tool_calls:
134
+ if self.tool_choices == ToolChoice.REQUIRED:
135
+ raise ValueError(TOOL_CALL_REQUIRED)
136
+
137
+ # Return last message content if no tool calls
138
+ return self.messages[-1].content or "No content or commands to execute"
139
+
140
+ results = []
141
+ for command in self.tool_calls:
142
+ # Reset base64_image for each tool call
143
+ self._current_base64_image = None
144
+
145
+ result = await self.execute_tool(command)
146
+
147
+ if self.max_observe:
148
+ result = result[: self.max_observe]
149
+
150
+ logger.info(
151
+ f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}"
152
+ )
153
+
154
+ # Add tool response to memory
155
+ tool_msg = Message.tool_message(
156
+ content=result,
157
+ tool_call_id=command.id,
158
+ name=command.function.name,
159
+ base64_image=self._current_base64_image,
160
+ )
161
+ self.memory.add_message(tool_msg)
162
+ results.append(result)
163
+
164
+ return "\n\n".join(results)
165
+
166
+ async def execute_tool(self, command: ToolCall) -> str:
167
+ """Execute a single tool call with robust error handling"""
168
+ if not command or not command.function or not command.function.name:
169
+ return "Error: Invalid command format"
170
+
171
+ name = command.function.name
172
+ if name not in self.available_tools.tool_map:
173
+ return f"Error: Unknown tool '{name}'"
174
+
175
+ try:
176
+ # Parse arguments
177
+ args = json.loads(command.function.arguments or "{}")
178
+
179
+ # Execute the tool
180
+ logger.info(f"🔧 Activating tool: '{name}'...")
181
+ result = await self.available_tools.execute(name=name, tool_input=args)
182
+
183
+ # Handle special tools
184
+ await self._handle_special_tool(name=name, result=result)
185
+
186
+ # Check if result is a ToolResult with base64_image
187
+ if hasattr(result, "base64_image") and result.base64_image:
188
+ # Store the base64_image for later use in tool_message
189
+ self._current_base64_image = result.base64_image
190
+
191
+ # Format result for display (standard case)
192
+ observation = (
193
+ f"Observed output of cmd `{name}` executed:\n{str(result)}"
194
+ if result
195
+ else f"Cmd `{name}` completed with no output"
196
+ )
197
+
198
+ return observation
199
+ except json.JSONDecodeError:
200
+ error_msg = f"Error parsing arguments for {name}: Invalid JSON format"
201
+ logger.error(
202
+ f"📝 Oops! The arguments for '{name}' don't make sense - invalid JSON, arguments:{command.function.arguments}"
203
+ )
204
+ return f"Error: {error_msg}"
205
+ except Exception as e:
206
+ error_msg = f"⚠️ Tool '{name}' encountered a problem: {str(e)}"
207
+ logger.exception(error_msg)
208
+ return f"Error: {error_msg}"
209
+
210
+ async def _handle_special_tool(self, name: str, result: Any, **kwargs):
211
+ """Handle special tool execution and state changes"""
212
+ if not self._is_special_tool(name):
213
+ return
214
+
215
+ if self._should_finish_execution(name=name, result=result, **kwargs):
216
+ # Set agent state to finished
217
+ logger.info(f"🏁 Special tool '{name}' has completed the task!")
218
+ self.state = AgentState.FINISHED
219
+
220
+ @staticmethod
221
+ def _should_finish_execution(**kwargs) -> bool:
222
+ """Determine if tool execution should finish the agent"""
223
+ return True
224
+
225
+ def _is_special_tool(self, name: str) -> bool:
226
+ """Check if tool name is in special tools list"""
227
+ return name.lower() in [n.lower() for n in self.special_tool_names]
228
+
229
+ async def cleanup(self):
230
+ """Clean up resources used by the agent's tools."""
231
+ logger.info(f"🧹 Cleaning up resources for agent '{self.name}'...")
232
+ for tool_name, tool_instance in self.available_tools.tool_map.items():
233
+ if hasattr(tool_instance, "cleanup") and asyncio.iscoroutinefunction(
234
+ tool_instance.cleanup
235
+ ):
236
+ try:
237
+ logger.debug(f"🧼 Cleaning up tool: {tool_name}")
238
+ await tool_instance.cleanup()
239
+ except Exception as e:
240
+ logger.error(
241
+ f"🚨 Error cleaning up tool '{tool_name}': {e}", exc_info=True
242
+ )
243
+ logger.info(f"✨ Cleanup complete for agent '{self.name}'.")
244
+
245
+ async def run(self, request: Optional[str] = None) -> str:
246
+ """Run the agent with cleanup when done."""
247
+ try:
248
+ return await super().run(request)
249
+ finally:
250
+ await self.cleanup()
app/bedrock.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+ import time
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Dict, List, Literal, Optional
7
+
8
+ import boto3
9
+
10
+
11
+ # Global variables to track the current tool use ID across function calls
12
+ # Tmp solution
13
+ CURRENT_TOOLUSE_ID = None
14
+
15
+
16
+ # Class to handle OpenAI-style response formatting
17
+ class OpenAIResponse:
18
+ def __init__(self, data):
19
+ # Recursively convert nested dicts and lists to OpenAIResponse objects
20
+ for key, value in data.items():
21
+ if isinstance(value, dict):
22
+ value = OpenAIResponse(value)
23
+ elif isinstance(value, list):
24
+ value = [
25
+ OpenAIResponse(item) if isinstance(item, dict) else item
26
+ for item in value
27
+ ]
28
+ setattr(self, key, value)
29
+
30
+ def model_dump(self, *args, **kwargs):
31
+ # Convert object to dict and add timestamp
32
+ data = self.__dict__
33
+ data["created_at"] = datetime.now().isoformat()
34
+ return data
35
+
36
+
37
+ # Main client class for interacting with Amazon Bedrock
38
+ class BedrockClient:
39
+ def __init__(self):
40
+ # Initialize Bedrock client, you need to configure AWS env first
41
+ try:
42
+ self.client = boto3.client("bedrock-runtime")
43
+ self.chat = Chat(self.client)
44
+ except Exception as e:
45
+ print(f"Error initializing Bedrock client: {e}")
46
+ sys.exit(1)
47
+
48
+
49
+ # Chat interface class
50
+ class Chat:
51
+ def __init__(self, client):
52
+ self.completions = ChatCompletions(client)
53
+
54
+
55
+ # Core class handling chat completions functionality
56
+ class ChatCompletions:
57
+ def __init__(self, client):
58
+ self.client = client
59
+
60
+ def _convert_openai_tools_to_bedrock_format(self, tools):
61
+ # Convert OpenAI function calling format to Bedrock tool format
62
+ bedrock_tools = []
63
+ for tool in tools:
64
+ if tool.get("type") == "function":
65
+ function = tool.get("function", {})
66
+ bedrock_tool = {
67
+ "toolSpec": {
68
+ "name": function.get("name", ""),
69
+ "description": function.get("description", ""),
70
+ "inputSchema": {
71
+ "json": {
72
+ "type": "object",
73
+ "properties": function.get("parameters", {}).get(
74
+ "properties", {}
75
+ ),
76
+ "required": function.get("parameters", {}).get(
77
+ "required", []
78
+ ),
79
+ }
80
+ },
81
+ }
82
+ }
83
+ bedrock_tools.append(bedrock_tool)
84
+ return bedrock_tools
85
+
86
+ def _convert_openai_messages_to_bedrock_format(self, messages):
87
+ # Convert OpenAI message format to Bedrock message format
88
+ bedrock_messages = []
89
+ system_prompt = []
90
+ for message in messages:
91
+ if message.get("role") == "system":
92
+ system_prompt = [{"text": message.get("content")}]
93
+ elif message.get("role") == "user":
94
+ bedrock_message = {
95
+ "role": message.get("role", "user"),
96
+ "content": [{"text": message.get("content")}],
97
+ }
98
+ bedrock_messages.append(bedrock_message)
99
+ elif message.get("role") == "assistant":
100
+ bedrock_message = {
101
+ "role": "assistant",
102
+ "content": [{"text": message.get("content")}],
103
+ }
104
+ openai_tool_calls = message.get("tool_calls", [])
105
+ if openai_tool_calls:
106
+ bedrock_tool_use = {
107
+ "toolUseId": openai_tool_calls[0]["id"],
108
+ "name": openai_tool_calls[0]["function"]["name"],
109
+ "input": json.loads(
110
+ openai_tool_calls[0]["function"]["arguments"]
111
+ ),
112
+ }
113
+ bedrock_message["content"].append({"toolUse": bedrock_tool_use})
114
+ global CURRENT_TOOLUSE_ID
115
+ CURRENT_TOOLUSE_ID = openai_tool_calls[0]["id"]
116
+ bedrock_messages.append(bedrock_message)
117
+ elif message.get("role") == "tool":
118
+ bedrock_message = {
119
+ "role": "user",
120
+ "content": [
121
+ {
122
+ "toolResult": {
123
+ "toolUseId": CURRENT_TOOLUSE_ID,
124
+ "content": [{"text": message.get("content")}],
125
+ }
126
+ }
127
+ ],
128
+ }
129
+ bedrock_messages.append(bedrock_message)
130
+ else:
131
+ raise ValueError(f"Invalid role: {message.get('role')}")
132
+ return system_prompt, bedrock_messages
133
+
134
+ def _convert_bedrock_response_to_openai_format(self, bedrock_response):
135
+ # Convert Bedrock response format to OpenAI format
136
+ content = ""
137
+ if bedrock_response.get("output", {}).get("message", {}).get("content"):
138
+ content_array = bedrock_response["output"]["message"]["content"]
139
+ content = "".join(item.get("text", "") for item in content_array)
140
+ if content == "":
141
+ content = "."
142
+
143
+ # Handle tool calls in response
144
+ openai_tool_calls = []
145
+ if bedrock_response.get("output", {}).get("message", {}).get("content"):
146
+ for content_item in bedrock_response["output"]["message"]["content"]:
147
+ if content_item.get("toolUse"):
148
+ bedrock_tool_use = content_item["toolUse"]
149
+ global CURRENT_TOOLUSE_ID
150
+ CURRENT_TOOLUSE_ID = bedrock_tool_use["toolUseId"]
151
+ openai_tool_call = {
152
+ "id": CURRENT_TOOLUSE_ID,
153
+ "type": "function",
154
+ "function": {
155
+ "name": bedrock_tool_use["name"],
156
+ "arguments": json.dumps(bedrock_tool_use["input"]),
157
+ },
158
+ }
159
+ openai_tool_calls.append(openai_tool_call)
160
+
161
+ # Construct final OpenAI format response
162
+ openai_format = {
163
+ "id": f"chatcmpl-{uuid.uuid4()}",
164
+ "created": int(time.time()),
165
+ "object": "chat.completion",
166
+ "system_fingerprint": None,
167
+ "choices": [
168
+ {
169
+ "finish_reason": bedrock_response.get("stopReason", "end_turn"),
170
+ "index": 0,
171
+ "message": {
172
+ "content": content,
173
+ "role": bedrock_response.get("output", {})
174
+ .get("message", {})
175
+ .get("role", "assistant"),
176
+ "tool_calls": openai_tool_calls
177
+ if openai_tool_calls != []
178
+ else None,
179
+ "function_call": None,
180
+ },
181
+ }
182
+ ],
183
+ "usage": {
184
+ "completion_tokens": bedrock_response.get("usage", {}).get(
185
+ "outputTokens", 0
186
+ ),
187
+ "prompt_tokens": bedrock_response.get("usage", {}).get(
188
+ "inputTokens", 0
189
+ ),
190
+ "total_tokens": bedrock_response.get("usage", {}).get("totalTokens", 0),
191
+ },
192
+ }
193
+ return OpenAIResponse(openai_format)
194
+
195
+ async def _invoke_bedrock(
196
+ self,
197
+ model: str,
198
+ messages: List[Dict[str, str]],
199
+ max_tokens: int,
200
+ temperature: float,
201
+ tools: Optional[List[dict]] = None,
202
+ tool_choice: Literal["none", "auto", "required"] = "auto",
203
+ **kwargs,
204
+ ) -> OpenAIResponse:
205
+ # Non-streaming invocation of Bedrock model
206
+ (
207
+ system_prompt,
208
+ bedrock_messages,
209
+ ) = self._convert_openai_messages_to_bedrock_format(messages)
210
+ response = self.client.converse(
211
+ modelId=model,
212
+ system=system_prompt,
213
+ messages=bedrock_messages,
214
+ inferenceConfig={"temperature": temperature, "maxTokens": max_tokens},
215
+ toolConfig={"tools": tools} if tools else None,
216
+ )
217
+ openai_response = self._convert_bedrock_response_to_openai_format(response)
218
+ return openai_response
219
+
220
+ async def _invoke_bedrock_stream(
221
+ self,
222
+ model: str,
223
+ messages: List[Dict[str, str]],
224
+ max_tokens: int,
225
+ temperature: float,
226
+ tools: Optional[List[dict]] = None,
227
+ tool_choice: Literal["none", "auto", "required"] = "auto",
228
+ **kwargs,
229
+ ) -> OpenAIResponse:
230
+ # Streaming invocation of Bedrock model
231
+ (
232
+ system_prompt,
233
+ bedrock_messages,
234
+ ) = self._convert_openai_messages_to_bedrock_format(messages)
235
+ response = self.client.converse_stream(
236
+ modelId=model,
237
+ system=system_prompt,
238
+ messages=bedrock_messages,
239
+ inferenceConfig={"temperature": temperature, "maxTokens": max_tokens},
240
+ toolConfig={"tools": tools} if tools else None,
241
+ )
242
+
243
+ # Initialize response structure
244
+ bedrock_response = {
245
+ "output": {"message": {"role": "", "content": []}},
246
+ "stopReason": "",
247
+ "usage": {},
248
+ "metrics": {},
249
+ }
250
+ bedrock_response_text = ""
251
+ bedrock_response_tool_input = ""
252
+
253
+ # Process streaming response
254
+ stream = response.get("stream")
255
+ if stream:
256
+ for event in stream:
257
+ if event.get("messageStart", {}).get("role"):
258
+ bedrock_response["output"]["message"]["role"] = event[
259
+ "messageStart"
260
+ ]["role"]
261
+ if event.get("contentBlockDelta", {}).get("delta", {}).get("text"):
262
+ bedrock_response_text += event["contentBlockDelta"]["delta"]["text"]
263
+ print(
264
+ event["contentBlockDelta"]["delta"]["text"], end="", flush=True
265
+ )
266
+ if event.get("contentBlockStop", {}).get("contentBlockIndex") == 0:
267
+ bedrock_response["output"]["message"]["content"].append(
268
+ {"text": bedrock_response_text}
269
+ )
270
+ if event.get("contentBlockStart", {}).get("start", {}).get("toolUse"):
271
+ bedrock_tool_use = event["contentBlockStart"]["start"]["toolUse"]
272
+ tool_use = {
273
+ "toolUseId": bedrock_tool_use["toolUseId"],
274
+ "name": bedrock_tool_use["name"],
275
+ }
276
+ bedrock_response["output"]["message"]["content"].append(
277
+ {"toolUse": tool_use}
278
+ )
279
+ global CURRENT_TOOLUSE_ID
280
+ CURRENT_TOOLUSE_ID = bedrock_tool_use["toolUseId"]
281
+ if event.get("contentBlockDelta", {}).get("delta", {}).get("toolUse"):
282
+ bedrock_response_tool_input += event["contentBlockDelta"]["delta"][
283
+ "toolUse"
284
+ ]["input"]
285
+ print(
286
+ event["contentBlockDelta"]["delta"]["toolUse"]["input"],
287
+ end="",
288
+ flush=True,
289
+ )
290
+ if event.get("contentBlockStop", {}).get("contentBlockIndex") == 1:
291
+ bedrock_response["output"]["message"]["content"][1]["toolUse"][
292
+ "input"
293
+ ] = json.loads(bedrock_response_tool_input)
294
+ print()
295
+ openai_response = self._convert_bedrock_response_to_openai_format(
296
+ bedrock_response
297
+ )
298
+ return openai_response
299
+
300
+ def create(
301
+ self,
302
+ model: str,
303
+ messages: List[Dict[str, str]],
304
+ max_tokens: int,
305
+ temperature: float,
306
+ stream: Optional[bool] = True,
307
+ tools: Optional[List[dict]] = None,
308
+ tool_choice: Literal["none", "auto", "required"] = "auto",
309
+ **kwargs,
310
+ ) -> OpenAIResponse:
311
+ # Main entry point for chat completion
312
+ bedrock_tools = []
313
+ if tools is not None:
314
+ bedrock_tools = self._convert_openai_tools_to_bedrock_format(tools)
315
+ if stream:
316
+ return self._invoke_bedrock_stream(
317
+ model,
318
+ messages,
319
+ max_tokens,
320
+ temperature,
321
+ bedrock_tools,
322
+ tool_choice,
323
+ **kwargs,
324
+ )
325
+ else:
326
+ return self._invoke_bedrock(
327
+ model,
328
+ messages,
329
+ max_tokens,
330
+ temperature,
331
+ bedrock_tools,
332
+ tool_choice,
333
+ **kwargs,
334
+ )
app/config.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import threading
3
+ import tomllib
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ def get_project_root() -> Path:
12
+ """Get the project root directory"""
13
+ return Path(__file__).resolve().parent.parent
14
+
15
+
16
+ PROJECT_ROOT = get_project_root()
17
+ WORKSPACE_ROOT = PROJECT_ROOT / "workspace"
18
+
19
+
20
+ class LLMSettings(BaseModel):
21
+ model: str = Field(..., description="Model name")
22
+ base_url: str = Field(..., description="API base URL")
23
+ api_key: str = Field(..., description="API key")
24
+ max_tokens: int = Field(4096, description="Maximum number of tokens per request")
25
+ max_input_tokens: Optional[int] = Field(
26
+ None,
27
+ description="Maximum input tokens to use across all requests (None for unlimited)",
28
+ )
29
+ temperature: float = Field(1.0, description="Sampling temperature")
30
+ api_type: str = Field(..., description="Azure, Openai, or Ollama")
31
+ api_version: str = Field(..., description="Azure Openai version if AzureOpenai")
32
+
33
+
34
+ class ProxySettings(BaseModel):
35
+ server: str = Field(None, description="Proxy server address")
36
+ username: Optional[str] = Field(None, description="Proxy username")
37
+ password: Optional[str] = Field(None, description="Proxy password")
38
+
39
+
40
+ class SearchSettings(BaseModel):
41
+ engine: str = Field(default="Google", description="Search engine the llm to use")
42
+ fallback_engines: List[str] = Field(
43
+ default_factory=lambda: ["DuckDuckGo", "Baidu", "Bing"],
44
+ description="Fallback search engines to try if the primary engine fails",
45
+ )
46
+ retry_delay: int = Field(
47
+ default=60,
48
+ description="Seconds to wait before retrying all engines again after they all fail",
49
+ )
50
+ max_retries: int = Field(
51
+ default=3,
52
+ description="Maximum number of times to retry all engines when all fail",
53
+ )
54
+ lang: str = Field(
55
+ default="en",
56
+ description="Language code for search results (e.g., en, zh, fr)",
57
+ )
58
+ country: str = Field(
59
+ default="us",
60
+ description="Country code for search results (e.g., us, cn, uk)",
61
+ )
62
+
63
+
64
+ class RunflowSettings(BaseModel):
65
+ use_data_analysis_agent: bool = Field(
66
+ default=False, description="Enable data analysis agent in run flow"
67
+ )
68
+
69
+
70
+ class BrowserSettings(BaseModel):
71
+ headless: bool = Field(False, description="Whether to run browser in headless mode")
72
+ disable_security: bool = Field(
73
+ True, description="Disable browser security features"
74
+ )
75
+ extra_chromium_args: List[str] = Field(
76
+ default_factory=list, description="Extra arguments to pass to the browser"
77
+ )
78
+ chrome_instance_path: Optional[str] = Field(
79
+ None, description="Path to a Chrome instance to use"
80
+ )
81
+ wss_url: Optional[str] = Field(
82
+ None, description="Connect to a browser instance via WebSocket"
83
+ )
84
+ cdp_url: Optional[str] = Field(
85
+ None, description="Connect to a browser instance via CDP"
86
+ )
87
+ proxy: Optional[ProxySettings] = Field(
88
+ None, description="Proxy settings for the browser"
89
+ )
90
+ max_content_length: int = Field(
91
+ 2000, description="Maximum length for content retrieval operations"
92
+ )
93
+
94
+
95
+ class SandboxSettings(BaseModel):
96
+ """Configuration for the execution sandbox"""
97
+
98
+ use_sandbox: bool = Field(False, description="Whether to use the sandbox")
99
+ image: str = Field("python:3.12-slim", description="Base image")
100
+ work_dir: str = Field("/workspace", description="Container working directory")
101
+ memory_limit: str = Field("512m", description="Memory limit")
102
+ cpu_limit: float = Field(1.0, description="CPU limit")
103
+ timeout: int = Field(300, description="Default command timeout (seconds)")
104
+ network_enabled: bool = Field(
105
+ False, description="Whether network access is allowed"
106
+ )
107
+
108
+
109
+ class DaytonaSettings(BaseModel):
110
+ daytona_api_key: str
111
+ daytona_server_url: Optional[str] = Field(
112
+ "https://app.daytona.io/api", description=""
113
+ )
114
+ daytona_target: Optional[str] = Field("us", description="enum ['eu', 'us']")
115
+ sandbox_image_name: Optional[str] = Field("whitezxj/sandbox:0.1.0", description="")
116
+ sandbox_entrypoint: Optional[str] = Field(
117
+ "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
118
+ description="",
119
+ )
120
+ # sandbox_id: Optional[str] = Field(
121
+ # None, description="ID of the daytona sandbox to use, if any"
122
+ # )
123
+ VNC_password: Optional[str] = Field(
124
+ "123456", description="VNC password for the vnc service in sandbox"
125
+ )
126
+
127
+
128
+ class MCPServerConfig(BaseModel):
129
+ """Configuration for a single MCP server"""
130
+
131
+ type: str = Field(..., description="Server connection type (sse or stdio)")
132
+ url: Optional[str] = Field(None, description="Server URL for SSE connections")
133
+ command: Optional[str] = Field(None, description="Command for stdio connections")
134
+ args: List[str] = Field(
135
+ default_factory=list, description="Arguments for stdio command"
136
+ )
137
+
138
+
139
+ class MCPSettings(BaseModel):
140
+ """Configuration for MCP (Model Context Protocol)"""
141
+
142
+ server_reference: str = Field(
143
+ "app.mcp.server", description="Module reference for the MCP server"
144
+ )
145
+ servers: Dict[str, MCPServerConfig] = Field(
146
+ default_factory=dict, description="MCP server configurations"
147
+ )
148
+
149
+ @classmethod
150
+ def load_server_config(cls) -> Dict[str, MCPServerConfig]:
151
+ """Load MCP server configuration from JSON file"""
152
+ config_path = PROJECT_ROOT / "config" / "mcp.json"
153
+
154
+ try:
155
+ config_file = config_path if config_path.exists() else None
156
+ if not config_file:
157
+ return {}
158
+
159
+ with config_file.open() as f:
160
+ data = json.load(f)
161
+ servers = {}
162
+
163
+ for server_id, server_config in data.get("mcpServers", {}).items():
164
+ servers[server_id] = MCPServerConfig(
165
+ type=server_config["type"],
166
+ url=server_config.get("url"),
167
+ command=server_config.get("command"),
168
+ args=server_config.get("args", []),
169
+ )
170
+ return servers
171
+ except Exception as e:
172
+ raise ValueError(f"Failed to load MCP server config: {e}")
173
+
174
+
175
+ class AppConfig(BaseModel):
176
+ llm: Dict[str, LLMSettings]
177
+ sandbox: Optional[SandboxSettings] = Field(
178
+ None, description="Sandbox configuration"
179
+ )
180
+ browser_config: Optional[BrowserSettings] = Field(
181
+ None, description="Browser configuration"
182
+ )
183
+ search_config: Optional[SearchSettings] = Field(
184
+ None, description="Search configuration"
185
+ )
186
+ mcp_config: Optional[MCPSettings] = Field(None, description="MCP configuration")
187
+ run_flow_config: Optional[RunflowSettings] = Field(
188
+ None, description="Run flow configuration"
189
+ )
190
+ daytona_config: Optional[DaytonaSettings] = Field(
191
+ None, description="Daytona configuration"
192
+ )
193
+
194
+ class Config:
195
+ arbitrary_types_allowed = True
196
+
197
+
198
+ class Config:
199
+ _instance = None
200
+ _lock = threading.Lock()
201
+ _initialized = False
202
+
203
+ def __new__(cls):
204
+ if cls._instance is None:
205
+ with cls._lock:
206
+ if cls._instance is None:
207
+ cls._instance = super().__new__(cls)
208
+ return cls._instance
209
+
210
+ def __init__(self):
211
+ if not self._initialized:
212
+ with self._lock:
213
+ if not self._initialized:
214
+ self._config = None
215
+ self._load_initial_config()
216
+ self._initialized = True
217
+
218
+ @staticmethod
219
+ def _get_config_path() -> Path:
220
+ root = PROJECT_ROOT
221
+ config_path = root / "config" / "config.toml"
222
+ if config_path.exists():
223
+ return config_path
224
+ example_path = root / "config" / "config.example.toml"
225
+ if example_path.exists():
226
+ return example_path
227
+ raise FileNotFoundError("No configuration file found in config directory")
228
+
229
+ def _load_config(self) -> dict:
230
+ config_path = self._get_config_path()
231
+ with config_path.open("rb") as f:
232
+ config_data = tomllib.load(f)
233
+
234
+ # Override with environment variables if present
235
+ if "llm" not in config_data:
236
+ config_data["llm"] = {}
237
+
238
+ if os.environ.get("GEMINI_API_KEY"):
239
+ config_data["llm"]["api_key"] = os.environ.get("GEMINI_API_KEY")
240
+ config_data["llm"]["base_url"] = "https://generativelanguage.googleapis.com/v1beta/openai/"
241
+ config_data["llm"]["model"] = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro")
242
+
243
+ return config_data
244
+
245
+ def _load_initial_config(self):
246
+ raw_config = self._load_config()
247
+ base_llm = raw_config.get("llm", {})
248
+ llm_overrides = {
249
+ k: v for k, v in raw_config.get("llm", {}).items() if isinstance(v, dict)
250
+ }
251
+
252
+ default_settings = {
253
+ "model": base_llm.get("model"),
254
+ "base_url": base_llm.get("base_url"),
255
+ "api_key": base_llm.get("api_key"),
256
+ "max_tokens": base_llm.get("max_tokens", 4096),
257
+ "max_input_tokens": base_llm.get("max_input_tokens"),
258
+ "temperature": base_llm.get("temperature", 1.0),
259
+ "api_type": base_llm.get("api_type", ""),
260
+ "api_version": base_llm.get("api_version", ""),
261
+ }
262
+
263
+ # handle browser config.
264
+ browser_config = raw_config.get("browser", {})
265
+ browser_settings = None
266
+
267
+ if browser_config:
268
+ # handle proxy settings.
269
+ proxy_config = browser_config.get("proxy", {})
270
+ proxy_settings = None
271
+
272
+ if proxy_config and proxy_config.get("server"):
273
+ proxy_settings = ProxySettings(
274
+ **{
275
+ k: v
276
+ for k, v in proxy_config.items()
277
+ if k in ["server", "username", "password"] and v
278
+ }
279
+ )
280
+
281
+ # filter valid browser config parameters.
282
+ valid_browser_params = {
283
+ k: v
284
+ for k, v in browser_config.items()
285
+ if k in BrowserSettings.__annotations__ and v is not None
286
+ }
287
+
288
+ # if there is proxy settings, add it to the parameters.
289
+ if proxy_settings:
290
+ valid_browser_params["proxy"] = proxy_settings
291
+
292
+ # only create BrowserSettings when there are valid parameters.
293
+ if valid_browser_params:
294
+ browser_settings = BrowserSettings(**valid_browser_params)
295
+
296
+ search_config = raw_config.get("search", {})
297
+ search_settings = None
298
+ if search_config:
299
+ search_settings = SearchSettings(**search_config)
300
+ sandbox_config = raw_config.get("sandbox", {})
301
+ if sandbox_config:
302
+ sandbox_settings = SandboxSettings(**sandbox_config)
303
+ else:
304
+ sandbox_settings = SandboxSettings()
305
+ daytona_config = raw_config.get("daytona", {})
306
+ if daytona_config:
307
+ daytona_settings = DaytonaSettings(**daytona_config)
308
+ else:
309
+ daytona_settings = DaytonaSettings()
310
+
311
+ mcp_config = raw_config.get("mcp", {})
312
+ mcp_settings = None
313
+ if mcp_config:
314
+ # Load server configurations from JSON
315
+ mcp_config["servers"] = MCPSettings.load_server_config()
316
+ mcp_settings = MCPSettings(**mcp_config)
317
+ else:
318
+ mcp_settings = MCPSettings(servers=MCPSettings.load_server_config())
319
+
320
+ run_flow_config = raw_config.get("runflow")
321
+ if run_flow_config:
322
+ run_flow_settings = RunflowSettings(**run_flow_config)
323
+ else:
324
+ run_flow_settings = RunflowSettings()
325
+ config_dict = {
326
+ "llm": {
327
+ "default": default_settings,
328
+ **{
329
+ name: {**default_settings, **override_config}
330
+ for name, override_config in llm_overrides.items()
331
+ },
332
+ },
333
+ "sandbox": sandbox_settings,
334
+ "browser_config": browser_settings,
335
+ "search_config": search_settings,
336
+ "mcp_config": mcp_settings,
337
+ "run_flow_config": run_flow_settings,
338
+ "daytona_config": daytona_settings,
339
+ }
340
+
341
+ self._config = AppConfig(**config_dict)
342
+
343
+ @property
344
+ def llm(self) -> Dict[str, LLMSettings]:
345
+ return self._config.llm
346
+
347
+ @property
348
+ def sandbox(self) -> SandboxSettings:
349
+ return self._config.sandbox
350
+
351
+ @property
352
+ def daytona(self) -> DaytonaSettings:
353
+ return self._config.daytona_config
354
+
355
+ @property
356
+ def browser_config(self) -> Optional[BrowserSettings]:
357
+ return self._config.browser_config
358
+
359
+ @property
360
+ def search_config(self) -> Optional[SearchSettings]:
361
+ return self._config.search_config
362
+
363
+ @property
364
+ def mcp_config(self) -> MCPSettings:
365
+ """Get the MCP configuration"""
366
+ return self._config.mcp_config
367
+
368
+ @property
369
+ def run_flow_config(self) -> RunflowSettings:
370
+ """Get the Run Flow configuration"""
371
+ return self._config.run_flow_config
372
+
373
+ @property
374
+ def workspace_root(self) -> Path:
375
+ """Get the workspace root directory"""
376
+ return WORKSPACE_ROOT
377
+
378
+ @property
379
+ def root_path(self) -> Path:
380
+ """Get the root path of the application"""
381
+ return PROJECT_ROOT
382
+
383
+
384
+ config = Config()
app/daytona/README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent with Daytona sandbox
2
+
3
+
4
+
5
+
6
+ ## Prerequisites
7
+ - conda activate 'Your OpenManus python env'
8
+ - pip install daytona==0.21.8 structlog==25.4.0
9
+
10
+
11
+
12
+ ## Setup & Running
13
+
14
+ 1. daytona config :
15
+ ```bash
16
+ cd OpenManus
17
+ cp config/config.example-daytona.toml config/config.toml
18
+ ```
19
+ 2. get daytona apikey :
20
+ goto https://app.daytona.io/dashboard/keys and create your apikey
21
+
22
+ 3. set your apikey in config.toml
23
+ ```toml
24
+ # daytona config
25
+ [daytona]
26
+ daytona_api_key = ""
27
+ #daytona_server_url = "https://app.daytona.io/api"
28
+ #daytona_target = "us" #Daytona is currently available in the following regions:United States (us)、Europe (eu)
29
+ #sandbox_image_name = "whitezxj/sandbox:0.1.0" #If you don't use this default image,sandbox tools may be useless
30
+ #sandbox_entrypoint = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf" #If you change this entrypoint,server in sandbox may be useless
31
+ #VNC_password = #The password you set to log in sandbox by VNC,it will be 123456 if you don't set
32
+ ```
33
+ 2. Run :
34
+
35
+ ```bash
36
+ cd OpenManus
37
+ python sandbox_main.py
38
+ ```
39
+
40
+ 3. Send tasks to Agent
41
+ You can sent tasks to Agent by terminate,agent will use sandbox tools to handle your tasks.
42
+
43
+ 4. See results
44
+ If agent use sb_browser_use tool, you can see the operations by VNC link, The VNC link will print in the termination,e.g.:https://6080-sandbox-123456.h7890.daytona.work.
45
+ If agent use sb_shell tool, you can see the results by terminate of sandbox in https://app.daytona.io/dashboard/sandboxes.
46
+ Agent can use sb_files tool to operate files to sandbox.
47
+
48
+
49
+ ## Example
50
+
51
+ You can send task e.g.:"帮我在https://hk.trip.com/travel-guide/guidebook/nanjing-9696/?ishideheader=true&isHideNavBar=YES&disableFontScaling=1&catalogId=514634&locale=zh-HK查询相关信息上制定一份南京旅游攻略,并在工作区保存为index.html"
52
+
53
+ Then you can see the agent's browser action in VNC link(https://6080-sandbox-123456.h7890.proxy.daytona.work) and you can see the html made by agent in Website URL(https://8080-sandbox-123456.h7890.proxy.daytona.work).
54
+
55
+ ## Learn More
56
+
57
+ - [Daytona Documentation](https://www.daytona.io/docs/)
app/daytona/sandbox.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+
3
+ from daytona import (
4
+ CreateSandboxFromImageParams,
5
+ Daytona,
6
+ DaytonaConfig,
7
+ Resources,
8
+ Sandbox,
9
+ SandboxState,
10
+ SessionExecuteRequest,
11
+ )
12
+
13
+ from app.config import config
14
+ from app.utils.logger import logger
15
+
16
+
17
+ # load_dotenv()
18
+ daytona_settings = config.daytona
19
+ logger.info("Initializing Daytona sandbox configuration")
20
+ daytona_config = DaytonaConfig(
21
+ api_key=daytona_settings.daytona_api_key,
22
+ server_url=daytona_settings.daytona_server_url,
23
+ target=daytona_settings.daytona_target,
24
+ )
25
+
26
+ if daytona_config.api_key:
27
+ logger.info("Daytona API key configured successfully")
28
+ else:
29
+ logger.warning("No Daytona API key found in environment variables")
30
+
31
+ if daytona_config.server_url:
32
+ logger.info(f"Daytona server URL set to: {daytona_config.server_url}")
33
+ else:
34
+ logger.warning("No Daytona server URL found in environment variables")
35
+
36
+ if daytona_config.target:
37
+ logger.info(f"Daytona target set to: {daytona_config.target}")
38
+ else:
39
+ logger.warning("No Daytona target found in environment variables")
40
+
41
+ daytona = Daytona(daytona_config)
42
+ logger.info("Daytona client initialized")
43
+
44
+
45
+ async def get_or_start_sandbox(sandbox_id: str):
46
+ """Retrieve a sandbox by ID, check its state, and start it if needed."""
47
+
48
+ logger.info(f"Getting or starting sandbox with ID: {sandbox_id}")
49
+
50
+ try:
51
+ sandbox = daytona.get(sandbox_id)
52
+
53
+ # Check if sandbox needs to be started
54
+ if (
55
+ sandbox.state == SandboxState.ARCHIVED
56
+ or sandbox.state == SandboxState.STOPPED
57
+ ):
58
+ logger.info(f"Sandbox is in {sandbox.state} state. Starting...")
59
+ try:
60
+ daytona.start(sandbox)
61
+ # Wait a moment for the sandbox to initialize
62
+ # sleep(5)
63
+ # Refresh sandbox state after starting
64
+ sandbox = daytona.get(sandbox_id)
65
+
66
+ # Start supervisord in a session when restarting
67
+ start_supervisord_session(sandbox)
68
+ except Exception as e:
69
+ logger.error(f"Error starting sandbox: {e}")
70
+ raise e
71
+
72
+ logger.info(f"Sandbox {sandbox_id} is ready")
73
+ return sandbox
74
+
75
+ except Exception as e:
76
+ logger.error(f"Error retrieving or starting sandbox: {str(e)}")
77
+ raise e
78
+
79
+
80
+ def start_supervisord_session(sandbox: Sandbox):
81
+ """Start supervisord in a session."""
82
+ session_id = "supervisord-session"
83
+ try:
84
+ logger.info(f"Creating session {session_id} for supervisord")
85
+ sandbox.process.create_session(session_id)
86
+
87
+ # Execute supervisord command
88
+ sandbox.process.execute_session_command(
89
+ session_id,
90
+ SessionExecuteRequest(
91
+ command="exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
92
+ var_async=True,
93
+ ),
94
+ )
95
+ time.sleep(25) # Wait a bit to ensure supervisord starts properly
96
+ logger.info(f"Supervisord started in session {session_id}")
97
+ except Exception as e:
98
+ logger.error(f"Error starting supervisord session: {str(e)}")
99
+ raise e
100
+
101
+
102
+ def create_sandbox(password: str, project_id: str = None):
103
+ """Create a new sandbox with all required services configured and running."""
104
+
105
+ logger.info("Creating new Daytona sandbox environment")
106
+ logger.info("Configuring sandbox with browser-use image and environment variables")
107
+
108
+ labels = None
109
+ if project_id:
110
+ logger.info(f"Using sandbox_id as label: {project_id}")
111
+ labels = {"id": project_id}
112
+
113
+ params = CreateSandboxFromImageParams(
114
+ image=daytona_settings.sandbox_image_name,
115
+ public=True,
116
+ labels=labels,
117
+ env_vars={
118
+ "CHROME_PERSISTENT_SESSION": "true",
119
+ "RESOLUTION": "1024x768x24",
120
+ "RESOLUTION_WIDTH": "1024",
121
+ "RESOLUTION_HEIGHT": "768",
122
+ "VNC_PASSWORD": password,
123
+ "ANONYMIZED_TELEMETRY": "false",
124
+ "CHROME_PATH": "",
125
+ "CHROME_USER_DATA": "",
126
+ "CHROME_DEBUGGING_PORT": "9222",
127
+ "CHROME_DEBUGGING_HOST": "localhost",
128
+ "CHROME_CDP": "",
129
+ },
130
+ resources=Resources(
131
+ cpu=2,
132
+ memory=4,
133
+ disk=5,
134
+ ),
135
+ auto_stop_interval=15,
136
+ auto_archive_interval=24 * 60,
137
+ )
138
+
139
+ # Create the sandbox
140
+ sandbox = daytona.create(params)
141
+ logger.info(f"Sandbox created with ID: {sandbox.id}")
142
+
143
+ # Start supervisord in a session for new sandbox
144
+ start_supervisord_session(sandbox)
145
+
146
+ logger.info(f"Sandbox environment successfully initialized")
147
+ return sandbox
148
+
149
+
150
+ async def delete_sandbox(sandbox_id: str):
151
+ """Delete a sandbox by its ID."""
152
+ logger.info(f"Deleting sandbox with ID: {sandbox_id}")
153
+
154
+ try:
155
+ # Get the sandbox
156
+ sandbox = daytona.get(sandbox_id)
157
+
158
+ # Delete the sandbox
159
+ daytona.delete(sandbox)
160
+
161
+ logger.info(f"Successfully deleted sandbox {sandbox_id}")
162
+ return True
163
+ except Exception as e:
164
+ logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
165
+ raise e
app/daytona/tool_base.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from typing import Any, ClassVar, Dict, Optional
4
+
5
+ from daytona import Daytona, DaytonaConfig, Sandbox, SandboxState
6
+ from pydantic import Field
7
+
8
+ from app.config import config
9
+ from app.daytona.sandbox import create_sandbox, start_supervisord_session
10
+ from app.tool.base import BaseTool
11
+ from app.utils.files_utils import clean_path
12
+ from app.utils.logger import logger
13
+
14
+
15
+ # load_dotenv()
16
+ daytona_settings = config.daytona
17
+ daytona_config = DaytonaConfig(
18
+ api_key=daytona_settings.daytona_api_key,
19
+ server_url=daytona_settings.daytona_server_url,
20
+ target=daytona_settings.daytona_target,
21
+ )
22
+ daytona = Daytona(daytona_config)
23
+
24
+
25
+ @dataclass
26
+ class ThreadMessage:
27
+ """
28
+ Represents a message to be added to a thread.
29
+ """
30
+
31
+ type: str
32
+ content: Dict[str, Any]
33
+ is_llm_message: bool = False
34
+ metadata: Optional[Dict[str, Any]] = None
35
+ timestamp: Optional[float] = field(
36
+ default_factory=lambda: datetime.now().timestamp()
37
+ )
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ """Convert the message to a dictionary for API calls"""
41
+ return {
42
+ "type": self.type,
43
+ "content": self.content,
44
+ "is_llm_message": self.is_llm_message,
45
+ "metadata": self.metadata or {},
46
+ "timestamp": self.timestamp,
47
+ }
48
+
49
+
50
+ class SandboxToolsBase(BaseTool):
51
+ """Base class for all sandbox tools that provides project-based sandbox access."""
52
+
53
+ # Class variable to track if sandbox URLs have been printed
54
+ _urls_printed: ClassVar[bool] = False
55
+
56
+ # Required fields
57
+ project_id: Optional[str] = None
58
+ # thread_manager: Optional[ThreadManager] = None
59
+
60
+ # Private fields (not part of the model schema)
61
+ _sandbox: Optional[Sandbox] = None
62
+ _sandbox_id: Optional[str] = None
63
+ _sandbox_pass: Optional[str] = None
64
+ workspace_path: str = Field(default="/workspace", exclude=True)
65
+ _sessions: dict[str, str] = {}
66
+
67
+ class Config:
68
+ arbitrary_types_allowed = True # Allow non-pydantic types like ThreadManager
69
+ underscore_attrs_are_private = True
70
+
71
+ async def _ensure_sandbox(self) -> Sandbox:
72
+ """Ensure we have a valid sandbox instance, retrieving it from the project if needed."""
73
+ if self._sandbox is None:
74
+ # Get or start the sandbox
75
+ try:
76
+ self._sandbox = create_sandbox(password=config.daytona.VNC_password)
77
+ # Log URLs if not already printed
78
+ if not SandboxToolsBase._urls_printed:
79
+ vnc_link = self._sandbox.get_preview_link(6080)
80
+ website_link = self._sandbox.get_preview_link(8080)
81
+
82
+ vnc_url = (
83
+ vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link)
84
+ )
85
+ website_url = (
86
+ website_link.url
87
+ if hasattr(website_link, "url")
88
+ else str(website_link)
89
+ )
90
+
91
+ print("\033[95m***")
92
+ print(f"VNC URL: {vnc_url}")
93
+ print(f"Website URL: {website_url}")
94
+ print("***\033[0m")
95
+ SandboxToolsBase._urls_printed = True
96
+ except Exception as e:
97
+ logger.error(f"Error retrieving or starting sandbox: {str(e)}")
98
+ raise e
99
+ else:
100
+ if (
101
+ self._sandbox.state == SandboxState.ARCHIVED
102
+ or self._sandbox.state == SandboxState.STOPPED
103
+ ):
104
+ logger.info(f"Sandbox is in {self._sandbox.state} state. Starting...")
105
+ try:
106
+ daytona.start(self._sandbox)
107
+ # Wait a moment for the sandbox to initialize
108
+ # sleep(5)
109
+ # Refresh sandbox state after starting
110
+
111
+ # Start supervisord in a session when restarting
112
+ start_supervisord_session(self._sandbox)
113
+ except Exception as e:
114
+ logger.error(f"Error starting sandbox: {e}")
115
+ raise e
116
+ return self._sandbox
117
+
118
+ @property
119
+ def sandbox(self) -> Sandbox:
120
+ """Get the sandbox instance, ensuring it exists."""
121
+ if self._sandbox is None:
122
+ raise RuntimeError("Sandbox not initialized. Call _ensure_sandbox() first.")
123
+ return self._sandbox
124
+
125
+ @property
126
+ def sandbox_id(self) -> str:
127
+ """Get the sandbox ID, ensuring it exists."""
128
+ if self._sandbox_id is None:
129
+ raise RuntimeError(
130
+ "Sandbox ID not initialized. Call _ensure_sandbox() first."
131
+ )
132
+ return self._sandbox_id
133
+
134
+ def clean_path(self, path: str) -> str:
135
+ """Clean and normalize a path to be relative to /workspace."""
136
+ cleaned_path = clean_path(path, self.workspace_path)
137
+ logger.debug(f"Cleaned path: {path} -> {cleaned_path}")
138
+ return cleaned_path
app/exceptions.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"""
app/flow/__init__.py ADDED
File without changes
app/flow/base.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"""
app/flow/flow_factory.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
app/flow/planning.py ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ from enum import Enum
4
+ from typing import Dict, List, Optional, Union
5
+
6
+ from pydantic import Field
7
+
8
+ from app.agent.base import BaseAgent
9
+ from app.flow.base import BaseFlow
10
+ from app.llm import LLM
11
+ from app.logger import logger
12
+ from app.schema import AgentState, Message, ToolChoice
13
+ from app.tool import PlanningTool
14
+
15
+
16
+ class PlanStepStatus(str, Enum):
17
+ """Enum class defining possible statuses of a plan step"""
18
+
19
+ NOT_STARTED = "not_started"
20
+ IN_PROGRESS = "in_progress"
21
+ COMPLETED = "completed"
22
+ BLOCKED = "blocked"
23
+
24
+ @classmethod
25
+ def get_all_statuses(cls) -> list[str]:
26
+ """Return a list of all possible step status values"""
27
+ return [status.value for status in cls]
28
+
29
+ @classmethod
30
+ def get_active_statuses(cls) -> list[str]:
31
+ """Return a list of values representing active statuses (not started or in progress)"""
32
+ return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value]
33
+
34
+ @classmethod
35
+ def get_status_marks(cls) -> Dict[str, str]:
36
+ """Return a mapping of statuses to their marker symbols"""
37
+ return {
38
+ cls.COMPLETED.value: "[✓]",
39
+ cls.IN_PROGRESS.value: "[→]",
40
+ cls.BLOCKED.value: "[!]",
41
+ cls.NOT_STARTED.value: "[ ]",
42
+ }
43
+
44
+
45
+ class PlanningFlow(BaseFlow):
46
+ """A flow that manages planning and execution of tasks using agents."""
47
+
48
+ llm: LLM = Field(default_factory=lambda: LLM())
49
+ planning_tool: PlanningTool = Field(default_factory=PlanningTool)
50
+ executor_keys: List[str] = Field(default_factory=list)
51
+ active_plan_id: str = Field(default_factory=lambda: f"plan_{int(time.time())}")
52
+ current_step_index: Optional[int] = None
53
+
54
+ def __init__(
55
+ self, agents: Union[BaseAgent, List[BaseAgent], Dict[str, BaseAgent]], **data
56
+ ):
57
+ # Set executor keys before super().__init__
58
+ if "executors" in data:
59
+ data["executor_keys"] = data.pop("executors")
60
+
61
+ # Set plan ID if provided
62
+ if "plan_id" in data:
63
+ data["active_plan_id"] = data.pop("plan_id")
64
+
65
+ # Initialize the planning tool if not provided
66
+ if "planning_tool" not in data:
67
+ planning_tool = PlanningTool()
68
+ data["planning_tool"] = planning_tool
69
+
70
+ # Call parent's init with the processed data
71
+ super().__init__(agents, **data)
72
+
73
+ # Set executor_keys to all agent keys if not specified
74
+ if not self.executor_keys:
75
+ self.executor_keys = list(self.agents.keys())
76
+
77
+ def get_executor(self, step_type: Optional[str] = None) -> BaseAgent:
78
+ """
79
+ Get an appropriate executor agent for the current step.
80
+ Can be extended to select agents based on step type/requirements.
81
+ """
82
+ # If step type is provided and matches an agent key, use that agent
83
+ if step_type and step_type in self.agents:
84
+ return self.agents[step_type]
85
+
86
+ # Otherwise use the first available executor or fall back to primary agent
87
+ for key in self.executor_keys:
88
+ if key in self.agents:
89
+ return self.agents[key]
90
+
91
+ # Fallback to primary agent
92
+ return self.primary_agent
93
+
94
+ async def execute(self, input_text: str) -> str:
95
+ """Execute the planning flow with agents."""
96
+ try:
97
+ if not self.primary_agent:
98
+ raise ValueError("No primary agent available")
99
+
100
+ # Create initial plan if input provided
101
+ if input_text:
102
+ await self._create_initial_plan(input_text)
103
+
104
+ # Verify plan was created successfully
105
+ if self.active_plan_id not in self.planning_tool.plans:
106
+ logger.error(
107
+ f"Plan creation failed. Plan ID {self.active_plan_id} not found in planning tool."
108
+ )
109
+ return f"Failed to create plan for: {input_text}"
110
+
111
+ result = ""
112
+ while True:
113
+ # Get current step to execute
114
+ self.current_step_index, step_info = await self._get_current_step_info()
115
+
116
+ # Exit if no more steps or plan completed
117
+ if self.current_step_index is None:
118
+ result += await self._finalize_plan()
119
+ break
120
+
121
+ # Execute current step with appropriate agent
122
+ step_type = step_info.get("type") if step_info else None
123
+ executor = self.get_executor(step_type)
124
+ step_result = await self._execute_step(executor, step_info)
125
+ result += step_result + "\n"
126
+
127
+ # Check if agent wants to terminate
128
+ if hasattr(executor, "state") and executor.state == AgentState.FINISHED:
129
+ break
130
+
131
+ return result
132
+ except Exception as e:
133
+ logger.error(f"Error in PlanningFlow: {str(e)}")
134
+ return f"Execution failed: {str(e)}"
135
+
136
+ async def _create_initial_plan(self, request: str) -> None:
137
+ """Create an initial plan based on the request using the flow's LLM and PlanningTool."""
138
+ logger.info(f"Creating initial plan with ID: {self.active_plan_id}")
139
+
140
+ system_message_content = (
141
+ "You are a planning assistant. Create a concise, actionable plan with clear steps. "
142
+ "Focus on key milestones rather than detailed sub-steps. "
143
+ "Optimize for clarity and efficiency."
144
+ )
145
+ agents_description = []
146
+ for key in self.executor_keys:
147
+ if key in self.agents:
148
+ agents_description.append(
149
+ {
150
+ "name": key.upper(),
151
+ "description": self.agents[key].description,
152
+ }
153
+ )
154
+ if len(agents_description) > 1:
155
+ # Add description of agents to select
156
+ system_message_content += (
157
+ f"\nNow we have {agents_description} agents. "
158
+ f"The infomation of them are below: {json.dumps(agents_description)}\n"
159
+ "When creating steps in the planning tool, please specify the agent names using the format '[agent_name]'."
160
+ )
161
+
162
+ # Create a system message for plan creation
163
+ system_message = Message.system_message(system_message_content)
164
+
165
+ # Create a user message with the request
166
+ user_message = Message.user_message(
167
+ f"Create a reasonable plan with clear steps to accomplish the task: {request}"
168
+ )
169
+
170
+ # Call LLM with PlanningTool
171
+ response = await self.llm.ask_tool(
172
+ messages=[user_message],
173
+ system_msgs=[system_message],
174
+ tools=[self.planning_tool.to_param()],
175
+ tool_choice=ToolChoice.AUTO,
176
+ )
177
+
178
+ # Process tool calls if present
179
+ if response.tool_calls:
180
+ for tool_call in response.tool_calls:
181
+ if tool_call.function.name == "planning":
182
+ # Parse the arguments
183
+ args = tool_call.function.arguments
184
+ if isinstance(args, str):
185
+ try:
186
+ args = json.loads(args)
187
+ except json.JSONDecodeError:
188
+ logger.error(f"Failed to parse tool arguments: {args}")
189
+ continue
190
+
191
+ # Ensure plan_id is set correctly and execute the tool
192
+ args["plan_id"] = self.active_plan_id
193
+
194
+ # Execute the tool via ToolCollection instead of directly
195
+ result = await self.planning_tool.execute(**args)
196
+
197
+ logger.info(f"Plan creation result: {str(result)}")
198
+ return
199
+
200
+ # If execution reached here, create a default plan
201
+ logger.warning("Creating default plan")
202
+
203
+ # Create default plan using the ToolCollection
204
+ await self.planning_tool.execute(
205
+ **{
206
+ "command": "create",
207
+ "plan_id": self.active_plan_id,
208
+ "title": f"Plan for: {request[:50]}{'...' if len(request) > 50 else ''}",
209
+ "steps": ["Analyze request", "Execute task", "Verify results"],
210
+ }
211
+ )
212
+
213
+ async def _get_current_step_info(self) -> tuple[Optional[int], Optional[dict]]:
214
+ """
215
+ Parse the current plan to identify the first non-completed step's index and info.
216
+ Returns (None, None) if no active step is found.
217
+ """
218
+ if (
219
+ not self.active_plan_id
220
+ or self.active_plan_id not in self.planning_tool.plans
221
+ ):
222
+ logger.error(f"Plan with ID {self.active_plan_id} not found")
223
+ return None, None
224
+
225
+ try:
226
+ # Direct access to plan data from planning tool storage
227
+ plan_data = self.planning_tool.plans[self.active_plan_id]
228
+ steps = plan_data.get("steps", [])
229
+ step_statuses = plan_data.get("step_statuses", [])
230
+
231
+ # Find first non-completed step
232
+ for i, step in enumerate(steps):
233
+ if i >= len(step_statuses):
234
+ status = PlanStepStatus.NOT_STARTED.value
235
+ else:
236
+ status = step_statuses[i]
237
+
238
+ if status in PlanStepStatus.get_active_statuses():
239
+ # Extract step type/category if available
240
+ step_info = {"text": step}
241
+
242
+ # Try to extract step type from the text (e.g., [SEARCH] or [CODE])
243
+ import re
244
+
245
+ type_match = re.search(r"\[([A-Z_]+)\]", step)
246
+ if type_match:
247
+ step_info["type"] = type_match.group(1).lower()
248
+
249
+ # Mark current step as in_progress
250
+ try:
251
+ await self.planning_tool.execute(
252
+ command="mark_step",
253
+ plan_id=self.active_plan_id,
254
+ step_index=i,
255
+ step_status=PlanStepStatus.IN_PROGRESS.value,
256
+ )
257
+ except Exception as e:
258
+ logger.warning(f"Error marking step as in_progress: {e}")
259
+ # Update step status directly if needed
260
+ if i < len(step_statuses):
261
+ step_statuses[i] = PlanStepStatus.IN_PROGRESS.value
262
+ else:
263
+ while len(step_statuses) < i:
264
+ step_statuses.append(PlanStepStatus.NOT_STARTED.value)
265
+ step_statuses.append(PlanStepStatus.IN_PROGRESS.value)
266
+
267
+ plan_data["step_statuses"] = step_statuses
268
+
269
+ return i, step_info
270
+
271
+ return None, None # No active step found
272
+
273
+ except Exception as e:
274
+ logger.warning(f"Error finding current step index: {e}")
275
+ return None, None
276
+
277
+ async def _execute_step(self, executor: BaseAgent, step_info: dict) -> str:
278
+ """Execute the current step with the specified agent using agent.run()."""
279
+ # Prepare context for the agent with current plan status
280
+ plan_status = await self._get_plan_text()
281
+ step_text = step_info.get("text", f"Step {self.current_step_index}")
282
+
283
+ # Create a prompt for the agent to execute the current step
284
+ step_prompt = f"""
285
+ CURRENT PLAN STATUS:
286
+ {plan_status}
287
+
288
+ YOUR CURRENT TASK:
289
+ You are now working on step {self.current_step_index}: "{step_text}"
290
+
291
+ Please only execute this current step using the appropriate tools. When you're done, provide a summary of what you accomplished.
292
+ """
293
+
294
+ # Use agent.run() to execute the step
295
+ try:
296
+ step_result = await executor.run(step_prompt)
297
+
298
+ # Mark the step as completed after successful execution
299
+ await self._mark_step_completed()
300
+
301
+ return step_result
302
+ except Exception as e:
303
+ logger.error(f"Error executing step {self.current_step_index}: {e}")
304
+ return f"Error executing step {self.current_step_index}: {str(e)}"
305
+
306
+ async def _mark_step_completed(self) -> None:
307
+ """Mark the current step as completed."""
308
+ if self.current_step_index is None:
309
+ return
310
+
311
+ try:
312
+ # Mark the step as completed
313
+ await self.planning_tool.execute(
314
+ command="mark_step",
315
+ plan_id=self.active_plan_id,
316
+ step_index=self.current_step_index,
317
+ step_status=PlanStepStatus.COMPLETED.value,
318
+ )
319
+ logger.info(
320
+ f"Marked step {self.current_step_index} as completed in plan {self.active_plan_id}"
321
+ )
322
+ except Exception as e:
323
+ logger.warning(f"Failed to update plan status: {e}")
324
+ # Update step status directly in planning tool storage
325
+ if self.active_plan_id in self.planning_tool.plans:
326
+ plan_data = self.planning_tool.plans[self.active_plan_id]
327
+ step_statuses = plan_data.get("step_statuses", [])
328
+
329
+ # Ensure the step_statuses list is long enough
330
+ while len(step_statuses) <= self.current_step_index:
331
+ step_statuses.append(PlanStepStatus.NOT_STARTED.value)
332
+
333
+ # Update the status
334
+ step_statuses[self.current_step_index] = PlanStepStatus.COMPLETED.value
335
+ plan_data["step_statuses"] = step_statuses
336
+
337
+ async def _get_plan_text(self) -> str:
338
+ """Get the current plan as formatted text."""
339
+ try:
340
+ result = await self.planning_tool.execute(
341
+ command="get", plan_id=self.active_plan_id
342
+ )
343
+ return result.output if hasattr(result, "output") else str(result)
344
+ except Exception as e:
345
+ logger.error(f"Error getting plan: {e}")
346
+ return self._generate_plan_text_from_storage()
347
+
348
+ def _generate_plan_text_from_storage(self) -> str:
349
+ """Generate plan text directly from storage if the planning tool fails."""
350
+ try:
351
+ if self.active_plan_id not in self.planning_tool.plans:
352
+ return f"Error: Plan with ID {self.active_plan_id} not found"
353
+
354
+ plan_data = self.planning_tool.plans[self.active_plan_id]
355
+ title = plan_data.get("title", "Untitled Plan")
356
+ steps = plan_data.get("steps", [])
357
+ step_statuses = plan_data.get("step_statuses", [])
358
+ step_notes = plan_data.get("step_notes", [])
359
+
360
+ # Ensure step_statuses and step_notes match the number of steps
361
+ while len(step_statuses) < len(steps):
362
+ step_statuses.append(PlanStepStatus.NOT_STARTED.value)
363
+ while len(step_notes) < len(steps):
364
+ step_notes.append("")
365
+
366
+ # Count steps by status
367
+ status_counts = {status: 0 for status in PlanStepStatus.get_all_statuses()}
368
+
369
+ for status in step_statuses:
370
+ if status in status_counts:
371
+ status_counts[status] += 1
372
+
373
+ completed = status_counts[PlanStepStatus.COMPLETED.value]
374
+ total = len(steps)
375
+ progress = (completed / total) * 100 if total > 0 else 0
376
+
377
+ plan_text = f"Plan: {title} (ID: {self.active_plan_id})\n"
378
+ plan_text += "=" * len(plan_text) + "\n\n"
379
+
380
+ plan_text += (
381
+ f"Progress: {completed}/{total} steps completed ({progress:.1f}%)\n"
382
+ )
383
+ plan_text += f"Status: {status_counts[PlanStepStatus.COMPLETED.value]} completed, {status_counts[PlanStepStatus.IN_PROGRESS.value]} in progress, "
384
+ plan_text += f"{status_counts[PlanStepStatus.BLOCKED.value]} blocked, {status_counts[PlanStepStatus.NOT_STARTED.value]} not started\n\n"
385
+ plan_text += "Steps:\n"
386
+
387
+ status_marks = PlanStepStatus.get_status_marks()
388
+
389
+ for i, (step, status, notes) in enumerate(
390
+ zip(steps, step_statuses, step_notes)
391
+ ):
392
+ # Use status marks to indicate step status
393
+ status_mark = status_marks.get(
394
+ status, status_marks[PlanStepStatus.NOT_STARTED.value]
395
+ )
396
+
397
+ plan_text += f"{i}. {status_mark} {step}\n"
398
+ if notes:
399
+ plan_text += f" Notes: {notes}\n"
400
+
401
+ return plan_text
402
+ except Exception as e:
403
+ logger.error(f"Error generating plan text from storage: {e}")
404
+ return f"Error: Unable to retrieve plan with ID {self.active_plan_id}"
405
+
406
+ async def _finalize_plan(self) -> str:
407
+ """Finalize the plan and provide a summary using the flow's LLM directly."""
408
+ plan_text = await self._get_plan_text()
409
+
410
+ # Create a summary using the flow's LLM directly
411
+ try:
412
+ system_message = Message.system_message(
413
+ "You are a planning assistant. Your task is to summarize the completed plan."
414
+ )
415
+
416
+ user_message = Message.user_message(
417
+ f"The plan has been completed. Here is the final plan status:\n\n{plan_text}\n\nPlease provide a summary of what was accomplished and any final thoughts."
418
+ )
419
+
420
+ response = await self.llm.ask(
421
+ messages=[user_message], system_msgs=[system_message]
422
+ )
423
+
424
+ return f"Plan completed:\n\n{response}"
425
+ except Exception as e:
426
+ logger.error(f"Error finalizing plan with LLM: {e}")
427
+
428
+ # Fallback to using an agent for the summary
429
+ try:
430
+ agent = self.primary_agent
431
+ summary_prompt = f"""
432
+ The plan has been completed. Here is the final plan status:
433
+
434
+ {plan_text}
435
+
436
+ Please provide a summary of what was accomplished and any final thoughts.
437
+ """
438
+ summary = await agent.run(summary_prompt)
439
+ return f"Plan completed:\n\n{summary}"
440
+ except Exception as e2:
441
+ logger.error(f"Error finalizing plan with agent: {e2}")
442
+ return "Plan completed. Error generating summary."
app/llm.py ADDED
@@ -0,0 +1,766 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Dict, List, Optional, Union
3
+
4
+ import tiktoken
5
+ from openai import (
6
+ APIError,
7
+ AsyncAzureOpenAI,
8
+ AsyncOpenAI,
9
+ AuthenticationError,
10
+ OpenAIError,
11
+ RateLimitError,
12
+ )
13
+ from openai.types.chat import ChatCompletion, ChatCompletionMessage
14
+ from tenacity import (
15
+ retry,
16
+ retry_if_exception_type,
17
+ stop_after_attempt,
18
+ wait_random_exponential,
19
+ )
20
+
21
+ from app.bedrock import BedrockClient
22
+ from app.config import LLMSettings, config
23
+ from app.exceptions import TokenLimitExceeded
24
+ from app.logger import logger # Assuming a logger is set up in your app
25
+ from app.schema import (
26
+ ROLE_VALUES,
27
+ TOOL_CHOICE_TYPE,
28
+ TOOL_CHOICE_VALUES,
29
+ Message,
30
+ ToolChoice,
31
+ )
32
+
33
+
34
+ REASONING_MODELS = ["o1", "o3-mini"]
35
+ MULTIMODAL_MODELS = [
36
+ "gpt-4-vision-preview",
37
+ "gpt-4o",
38
+ "gpt-4o-mini",
39
+ "claude-3-opus-20240229",
40
+ "claude-3-sonnet-20240229",
41
+ "claude-3-haiku-20240307",
42
+ ]
43
+
44
+
45
+ class TokenCounter:
46
+ # Token constants
47
+ BASE_MESSAGE_TOKENS = 4
48
+ FORMAT_TOKENS = 2
49
+ LOW_DETAIL_IMAGE_TOKENS = 85
50
+ HIGH_DETAIL_TILE_TOKENS = 170
51
+
52
+ # Image processing constants
53
+ MAX_SIZE = 2048
54
+ HIGH_DETAIL_TARGET_SHORT_SIDE = 768
55
+ TILE_SIZE = 512
56
+
57
+ def __init__(self, tokenizer):
58
+ self.tokenizer = tokenizer
59
+
60
+ def count_text(self, text: str) -> int:
61
+ """Calculate tokens for a text string"""
62
+ return 0 if not text else len(self.tokenizer.encode(text))
63
+
64
+ def count_image(self, image_item: dict) -> int:
65
+ """
66
+ Calculate tokens for an image based on detail level and dimensions
67
+
68
+ For "low" detail: fixed 85 tokens
69
+ For "high" detail:
70
+ 1. Scale to fit in 2048x2048 square
71
+ 2. Scale shortest side to 768px
72
+ 3. Count 512px tiles (170 tokens each)
73
+ 4. Add 85 tokens
74
+ """
75
+ detail = image_item.get("detail", "medium")
76
+
77
+ # For low detail, always return fixed token count
78
+ if detail == "low":
79
+ return self.LOW_DETAIL_IMAGE_TOKENS
80
+
81
+ # For medium detail (default in OpenAI), use high detail calculation
82
+ # OpenAI doesn't specify a separate calculation for medium
83
+
84
+ # For high detail, calculate based on dimensions if available
85
+ if detail == "high" or detail == "medium":
86
+ # If dimensions are provided in the image_item
87
+ if "dimensions" in image_item:
88
+ width, height = image_item["dimensions"]
89
+ return self._calculate_high_detail_tokens(width, height)
90
+
91
+ return (
92
+ self._calculate_high_detail_tokens(1024, 1024) if detail == "high" else 1024
93
+ )
94
+
95
+ def _calculate_high_detail_tokens(self, width: int, height: int) -> int:
96
+ """Calculate tokens for high detail images based on dimensions"""
97
+ # Step 1: Scale to fit in MAX_SIZE x MAX_SIZE square
98
+ if width > self.MAX_SIZE or height > self.MAX_SIZE:
99
+ scale = self.MAX_SIZE / max(width, height)
100
+ width = int(width * scale)
101
+ height = int(height * scale)
102
+
103
+ # Step 2: Scale so shortest side is HIGH_DETAIL_TARGET_SHORT_SIDE
104
+ scale = self.HIGH_DETAIL_TARGET_SHORT_SIDE / min(width, height)
105
+ scaled_width = int(width * scale)
106
+ scaled_height = int(height * scale)
107
+
108
+ # Step 3: Count number of 512px tiles
109
+ tiles_x = math.ceil(scaled_width / self.TILE_SIZE)
110
+ tiles_y = math.ceil(scaled_height / self.TILE_SIZE)
111
+ total_tiles = tiles_x * tiles_y
112
+
113
+ # Step 4: Calculate final token count
114
+ return (
115
+ total_tiles * self.HIGH_DETAIL_TILE_TOKENS
116
+ ) + self.LOW_DETAIL_IMAGE_TOKENS
117
+
118
+ def count_content(self, content: Union[str, List[Union[str, dict]]]) -> int:
119
+ """Calculate tokens for message content"""
120
+ if not content:
121
+ return 0
122
+
123
+ if isinstance(content, str):
124
+ return self.count_text(content)
125
+
126
+ token_count = 0
127
+ for item in content:
128
+ if isinstance(item, str):
129
+ token_count += self.count_text(item)
130
+ elif isinstance(item, dict):
131
+ if "text" in item:
132
+ token_count += self.count_text(item["text"])
133
+ elif "image_url" in item:
134
+ token_count += self.count_image(item)
135
+ return token_count
136
+
137
+ def count_tool_calls(self, tool_calls: List[dict]) -> int:
138
+ """Calculate tokens for tool calls"""
139
+ token_count = 0
140
+ for tool_call in tool_calls:
141
+ if "function" in tool_call:
142
+ function = tool_call["function"]
143
+ token_count += self.count_text(function.get("name", ""))
144
+ token_count += self.count_text(function.get("arguments", ""))
145
+ return token_count
146
+
147
+ def count_message_tokens(self, messages: List[dict]) -> int:
148
+ """Calculate the total number of tokens in a message list"""
149
+ total_tokens = self.FORMAT_TOKENS # Base format tokens
150
+
151
+ for message in messages:
152
+ tokens = self.BASE_MESSAGE_TOKENS # Base tokens per message
153
+
154
+ # Add role tokens
155
+ tokens += self.count_text(message.get("role", ""))
156
+
157
+ # Add content tokens
158
+ if "content" in message:
159
+ tokens += self.count_content(message["content"])
160
+
161
+ # Add tool calls tokens
162
+ if "tool_calls" in message:
163
+ tokens += self.count_tool_calls(message["tool_calls"])
164
+
165
+ # Add name and tool_call_id tokens
166
+ tokens += self.count_text(message.get("name", ""))
167
+ tokens += self.count_text(message.get("tool_call_id", ""))
168
+
169
+ total_tokens += tokens
170
+
171
+ return total_tokens
172
+
173
+
174
+ class LLM:
175
+ _instances: Dict[str, "LLM"] = {}
176
+
177
+ def __new__(
178
+ cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None
179
+ ):
180
+ if config_name not in cls._instances:
181
+ instance = super().__new__(cls)
182
+ instance.__init__(config_name, llm_config)
183
+ cls._instances[config_name] = instance
184
+ return cls._instances[config_name]
185
+
186
+ def __init__(
187
+ self, config_name: str = "default", llm_config: Optional[LLMSettings] = None
188
+ ):
189
+ if not hasattr(self, "client"): # Only initialize if not already initialized
190
+ llm_config = llm_config or config.llm
191
+ llm_config = llm_config.get(config_name, llm_config["default"])
192
+ self.model = llm_config.model
193
+ self.max_tokens = llm_config.max_tokens
194
+ self.temperature = llm_config.temperature
195
+ self.api_type = llm_config.api_type
196
+ self.api_key = llm_config.api_key
197
+ self.api_version = llm_config.api_version
198
+ self.base_url = llm_config.base_url
199
+
200
+ # Add token counting related attributes
201
+ self.total_input_tokens = 0
202
+ self.total_completion_tokens = 0
203
+ self.max_input_tokens = (
204
+ llm_config.max_input_tokens
205
+ if hasattr(llm_config, "max_input_tokens")
206
+ else None
207
+ )
208
+
209
+ # Initialize tokenizer
210
+ try:
211
+ self.tokenizer = tiktoken.encoding_for_model(self.model)
212
+ except KeyError:
213
+ # If the model is not in tiktoken's presets, use cl100k_base as default
214
+ self.tokenizer = tiktoken.get_encoding("cl100k_base")
215
+
216
+ if self.api_type == "azure":
217
+ self.client = AsyncAzureOpenAI(
218
+ base_url=self.base_url,
219
+ api_key=self.api_key,
220
+ api_version=self.api_version,
221
+ )
222
+ elif self.api_type == "aws":
223
+ self.client = BedrockClient()
224
+ else:
225
+ self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
226
+
227
+ self.token_counter = TokenCounter(self.tokenizer)
228
+
229
+ def count_tokens(self, text: str) -> int:
230
+ """Calculate the number of tokens in a text"""
231
+ if not text:
232
+ return 0
233
+ return len(self.tokenizer.encode(text))
234
+
235
+ def count_message_tokens(self, messages: List[dict]) -> int:
236
+ return self.token_counter.count_message_tokens(messages)
237
+
238
+ def update_token_count(self, input_tokens: int, completion_tokens: int = 0) -> None:
239
+ """Update token counts"""
240
+ # Only track tokens if max_input_tokens is set
241
+ self.total_input_tokens += input_tokens
242
+ self.total_completion_tokens += completion_tokens
243
+ logger.info(
244
+ f"Token usage: Input={input_tokens}, Completion={completion_tokens}, "
245
+ f"Cumulative Input={self.total_input_tokens}, Cumulative Completion={self.total_completion_tokens}, "
246
+ f"Total={input_tokens + completion_tokens}, Cumulative Total={self.total_input_tokens + self.total_completion_tokens}"
247
+ )
248
+
249
+ def check_token_limit(self, input_tokens: int) -> bool:
250
+ """Check if token limits are exceeded"""
251
+ if self.max_input_tokens is not None:
252
+ return (self.total_input_tokens + input_tokens) <= self.max_input_tokens
253
+ # If max_input_tokens is not set, always return True
254
+ return True
255
+
256
+ def get_limit_error_message(self, input_tokens: int) -> str:
257
+ """Generate error message for token limit exceeded"""
258
+ if (
259
+ self.max_input_tokens is not None
260
+ and (self.total_input_tokens + input_tokens) > self.max_input_tokens
261
+ ):
262
+ return f"Request may exceed input token limit (Current: {self.total_input_tokens}, Needed: {input_tokens}, Max: {self.max_input_tokens})"
263
+
264
+ return "Token limit exceeded"
265
+
266
+ @staticmethod
267
+ def format_messages(
268
+ messages: List[Union[dict, Message]], supports_images: bool = False
269
+ ) -> List[dict]:
270
+ """
271
+ Format messages for LLM by converting them to OpenAI message format.
272
+
273
+ Args:
274
+ messages: List of messages that can be either dict or Message objects
275
+ supports_images: Flag indicating if the target model supports image inputs
276
+
277
+ Returns:
278
+ List[dict]: List of formatted messages in OpenAI format
279
+
280
+ Raises:
281
+ ValueError: If messages are invalid or missing required fields
282
+ TypeError: If unsupported message types are provided
283
+
284
+ Examples:
285
+ >>> msgs = [
286
+ ... Message.system_message("You are a helpful assistant"),
287
+ ... {"role": "user", "content": "Hello"},
288
+ ... Message.user_message("How are you?")
289
+ ... ]
290
+ >>> formatted = LLM.format_messages(msgs)
291
+ """
292
+ formatted_messages = []
293
+
294
+ for message in messages:
295
+ # Convert Message objects to dictionaries
296
+ if isinstance(message, Message):
297
+ message = message.to_dict()
298
+
299
+ if isinstance(message, dict):
300
+ # If message is a dict, ensure it has required fields
301
+ if "role" not in message:
302
+ raise ValueError("Message dict must contain 'role' field")
303
+
304
+ # Process base64 images if present and model supports images
305
+ if supports_images and message.get("base64_image"):
306
+ # Initialize or convert content to appropriate format
307
+ if not message.get("content"):
308
+ message["content"] = []
309
+ elif isinstance(message["content"], str):
310
+ message["content"] = [
311
+ {"type": "text", "text": message["content"]}
312
+ ]
313
+ elif isinstance(message["content"], list):
314
+ # Convert string items to proper text objects
315
+ message["content"] = [
316
+ (
317
+ {"type": "text", "text": item}
318
+ if isinstance(item, str)
319
+ else item
320
+ )
321
+ for item in message["content"]
322
+ ]
323
+
324
+ # Add the image to content
325
+ message["content"].append(
326
+ {
327
+ "type": "image_url",
328
+ "image_url": {
329
+ "url": f"data:image/jpeg;base64,{message['base64_image']}"
330
+ },
331
+ }
332
+ )
333
+
334
+ # Remove the base64_image field
335
+ del message["base64_image"]
336
+ # If model doesn't support images but message has base64_image, handle gracefully
337
+ elif not supports_images and message.get("base64_image"):
338
+ # Just remove the base64_image field and keep the text content
339
+ del message["base64_image"]
340
+
341
+ if "content" in message or "tool_calls" in message:
342
+ formatted_messages.append(message)
343
+ # else: do not include the message
344
+ else:
345
+ raise TypeError(f"Unsupported message type: {type(message)}")
346
+
347
+ # Validate all messages have required fields
348
+ for msg in formatted_messages:
349
+ if msg["role"] not in ROLE_VALUES:
350
+ raise ValueError(f"Invalid role: {msg['role']}")
351
+
352
+ return formatted_messages
353
+
354
+ @retry(
355
+ wait=wait_random_exponential(min=1, max=60),
356
+ stop=stop_after_attempt(6),
357
+ retry=retry_if_exception_type(
358
+ (OpenAIError, Exception, ValueError)
359
+ ), # Don't retry TokenLimitExceeded
360
+ )
361
+ async def ask(
362
+ self,
363
+ messages: List[Union[dict, Message]],
364
+ system_msgs: Optional[List[Union[dict, Message]]] = None,
365
+ stream: bool = True,
366
+ temperature: Optional[float] = None,
367
+ ) -> str:
368
+ """
369
+ Send a prompt to the LLM and get the response.
370
+
371
+ Args:
372
+ messages: List of conversation messages
373
+ system_msgs: Optional system messages to prepend
374
+ stream (bool): Whether to stream the response
375
+ temperature (float): Sampling temperature for the response
376
+
377
+ Returns:
378
+ str: The generated response
379
+
380
+ Raises:
381
+ TokenLimitExceeded: If token limits are exceeded
382
+ ValueError: If messages are invalid or response is empty
383
+ OpenAIError: If API call fails after retries
384
+ Exception: For unexpected errors
385
+ """
386
+ try:
387
+ # Check if the model supports images
388
+ supports_images = self.model in MULTIMODAL_MODELS
389
+
390
+ # Format system and user messages with image support check
391
+ if system_msgs:
392
+ system_msgs = self.format_messages(system_msgs, supports_images)
393
+ messages = system_msgs + self.format_messages(messages, supports_images)
394
+ else:
395
+ messages = self.format_messages(messages, supports_images)
396
+
397
+ # Calculate input token count
398
+ input_tokens = self.count_message_tokens(messages)
399
+
400
+ # Check if token limits are exceeded
401
+ if not self.check_token_limit(input_tokens):
402
+ error_message = self.get_limit_error_message(input_tokens)
403
+ # Raise a special exception that won't be retried
404
+ raise TokenLimitExceeded(error_message)
405
+
406
+ params = {
407
+ "model": self.model,
408
+ "messages": messages,
409
+ }
410
+
411
+ if self.model in REASONING_MODELS:
412
+ params["max_completion_tokens"] = self.max_tokens
413
+ else:
414
+ params["max_tokens"] = self.max_tokens
415
+ params["temperature"] = (
416
+ temperature if temperature is not None else self.temperature
417
+ )
418
+
419
+ if not stream:
420
+ # Non-streaming request
421
+ response = await self.client.chat.completions.create(
422
+ **params, stream=False
423
+ )
424
+
425
+ if not response.choices or not response.choices[0].message.content:
426
+ raise ValueError("Empty or invalid response from LLM")
427
+
428
+ # Update token counts
429
+ self.update_token_count(
430
+ response.usage.prompt_tokens, response.usage.completion_tokens
431
+ )
432
+
433
+ return response.choices[0].message.content
434
+
435
+ # Streaming request, For streaming, update estimated token count before making the request
436
+ self.update_token_count(input_tokens)
437
+
438
+ response = await self.client.chat.completions.create(**params, stream=True)
439
+
440
+ collected_messages = []
441
+ completion_text = ""
442
+ async for chunk in response:
443
+ chunk_message = chunk.choices[0].delta.content or ""
444
+ collected_messages.append(chunk_message)
445
+ completion_text += chunk_message
446
+ print(chunk_message, end="", flush=True)
447
+
448
+ print() # Newline after streaming
449
+ full_response = "".join(collected_messages).strip()
450
+ if not full_response:
451
+ raise ValueError("Empty response from streaming LLM")
452
+
453
+ # estimate completion tokens for streaming response
454
+ completion_tokens = self.count_tokens(completion_text)
455
+ logger.info(
456
+ f"Estimated completion tokens for streaming response: {completion_tokens}"
457
+ )
458
+ self.total_completion_tokens += completion_tokens
459
+
460
+ return full_response
461
+
462
+ except TokenLimitExceeded:
463
+ # Re-raise token limit errors without logging
464
+ raise
465
+ except ValueError:
466
+ logger.exception(f"Validation error")
467
+ raise
468
+ except OpenAIError as oe:
469
+ logger.exception(f"OpenAI API error")
470
+ if isinstance(oe, AuthenticationError):
471
+ logger.error("Authentication failed. Check API key.")
472
+ elif isinstance(oe, RateLimitError):
473
+ logger.error("Rate limit exceeded. Consider increasing retry attempts.")
474
+ elif isinstance(oe, APIError):
475
+ logger.error(f"API error: {oe}")
476
+ raise
477
+ except Exception:
478
+ logger.exception(f"Unexpected error in ask")
479
+ raise
480
+
481
+ @retry(
482
+ wait=wait_random_exponential(min=1, max=60),
483
+ stop=stop_after_attempt(6),
484
+ retry=retry_if_exception_type(
485
+ (OpenAIError, Exception, ValueError)
486
+ ), # Don't retry TokenLimitExceeded
487
+ )
488
+ async def ask_with_images(
489
+ self,
490
+ messages: List[Union[dict, Message]],
491
+ images: List[Union[str, dict]],
492
+ system_msgs: Optional[List[Union[dict, Message]]] = None,
493
+ stream: bool = False,
494
+ temperature: Optional[float] = None,
495
+ ) -> str:
496
+ """
497
+ Send a prompt with images to the LLM and get the response.
498
+
499
+ Args:
500
+ messages: List of conversation messages
501
+ images: List of image URLs or image data dictionaries
502
+ system_msgs: Optional system messages to prepend
503
+ stream (bool): Whether to stream the response
504
+ temperature (float): Sampling temperature for the response
505
+
506
+ Returns:
507
+ str: The generated response
508
+
509
+ Raises:
510
+ TokenLimitExceeded: If token limits are exceeded
511
+ ValueError: If messages are invalid or response is empty
512
+ OpenAIError: If API call fails after retries
513
+ Exception: For unexpected errors
514
+ """
515
+ try:
516
+ # For ask_with_images, we always set supports_images to True because
517
+ # this method should only be called with models that support images
518
+ if self.model not in MULTIMODAL_MODELS:
519
+ raise ValueError(
520
+ f"Model {self.model} does not support images. Use a model from {MULTIMODAL_MODELS}"
521
+ )
522
+
523
+ # Format messages with image support
524
+ formatted_messages = self.format_messages(messages, supports_images=True)
525
+
526
+ # Ensure the last message is from the user to attach images
527
+ if not formatted_messages or formatted_messages[-1]["role"] != "user":
528
+ raise ValueError(
529
+ "The last message must be from the user to attach images"
530
+ )
531
+
532
+ # Process the last user message to include images
533
+ last_message = formatted_messages[-1]
534
+
535
+ # Convert content to multimodal format if needed
536
+ content = last_message["content"]
537
+ multimodal_content = (
538
+ [{"type": "text", "text": content}]
539
+ if isinstance(content, str)
540
+ else content
541
+ if isinstance(content, list)
542
+ else []
543
+ )
544
+
545
+ # Add images to content
546
+ for image in images:
547
+ if isinstance(image, str):
548
+ multimodal_content.append(
549
+ {"type": "image_url", "image_url": {"url": image}}
550
+ )
551
+ elif isinstance(image, dict) and "url" in image:
552
+ multimodal_content.append({"type": "image_url", "image_url": image})
553
+ elif isinstance(image, dict) and "image_url" in image:
554
+ multimodal_content.append(image)
555
+ else:
556
+ raise ValueError(f"Unsupported image format: {image}")
557
+
558
+ # Update the message with multimodal content
559
+ last_message["content"] = multimodal_content
560
+
561
+ # Add system messages if provided
562
+ if system_msgs:
563
+ all_messages = (
564
+ self.format_messages(system_msgs, supports_images=True)
565
+ + formatted_messages
566
+ )
567
+ else:
568
+ all_messages = formatted_messages
569
+
570
+ # Calculate tokens and check limits
571
+ input_tokens = self.count_message_tokens(all_messages)
572
+ if not self.check_token_limit(input_tokens):
573
+ raise TokenLimitExceeded(self.get_limit_error_message(input_tokens))
574
+
575
+ # Set up API parameters
576
+ params = {
577
+ "model": self.model,
578
+ "messages": all_messages,
579
+ "stream": stream,
580
+ }
581
+
582
+ # Add model-specific parameters
583
+ if self.model in REASONING_MODELS:
584
+ params["max_completion_tokens"] = self.max_tokens
585
+ else:
586
+ params["max_tokens"] = self.max_tokens
587
+ params["temperature"] = (
588
+ temperature if temperature is not None else self.temperature
589
+ )
590
+
591
+ # Handle non-streaming request
592
+ if not stream:
593
+ response = await self.client.chat.completions.create(**params)
594
+
595
+ if not response.choices or not response.choices[0].message.content:
596
+ raise ValueError("Empty or invalid response from LLM")
597
+
598
+ self.update_token_count(response.usage.prompt_tokens)
599
+ return response.choices[0].message.content
600
+
601
+ # Handle streaming request
602
+ self.update_token_count(input_tokens)
603
+ response = await self.client.chat.completions.create(**params)
604
+
605
+ collected_messages = []
606
+ async for chunk in response:
607
+ chunk_message = chunk.choices[0].delta.content or ""
608
+ collected_messages.append(chunk_message)
609
+ print(chunk_message, end="", flush=True)
610
+
611
+ print() # Newline after streaming
612
+ full_response = "".join(collected_messages).strip()
613
+
614
+ if not full_response:
615
+ raise ValueError("Empty response from streaming LLM")
616
+
617
+ return full_response
618
+
619
+ except TokenLimitExceeded:
620
+ raise
621
+ except ValueError as ve:
622
+ logger.error(f"Validation error in ask_with_images: {ve}")
623
+ raise
624
+ except OpenAIError as oe:
625
+ logger.error(f"OpenAI API error: {oe}")
626
+ if isinstance(oe, AuthenticationError):
627
+ logger.error("Authentication failed. Check API key.")
628
+ elif isinstance(oe, RateLimitError):
629
+ logger.error("Rate limit exceeded. Consider increasing retry attempts.")
630
+ elif isinstance(oe, APIError):
631
+ logger.error(f"API error: {oe}")
632
+ raise
633
+ except Exception as e:
634
+ logger.error(f"Unexpected error in ask_with_images: {e}")
635
+ raise
636
+
637
+ @retry(
638
+ wait=wait_random_exponential(min=1, max=60),
639
+ stop=stop_after_attempt(6),
640
+ retry=retry_if_exception_type(
641
+ (OpenAIError, Exception, ValueError)
642
+ ), # Don't retry TokenLimitExceeded
643
+ )
644
+ async def ask_tool(
645
+ self,
646
+ messages: List[Union[dict, Message]],
647
+ system_msgs: Optional[List[Union[dict, Message]]] = None,
648
+ timeout: int = 300,
649
+ tools: Optional[List[dict]] = None,
650
+ tool_choice: TOOL_CHOICE_TYPE = ToolChoice.AUTO, # type: ignore
651
+ temperature: Optional[float] = None,
652
+ **kwargs,
653
+ ) -> ChatCompletionMessage | None:
654
+ """
655
+ Ask LLM using functions/tools and return the response.
656
+
657
+ Args:
658
+ messages: List of conversation messages
659
+ system_msgs: Optional system messages to prepend
660
+ timeout: Request timeout in seconds
661
+ tools: List of tools to use
662
+ tool_choice: Tool choice strategy
663
+ temperature: Sampling temperature for the response
664
+ **kwargs: Additional completion arguments
665
+
666
+ Returns:
667
+ ChatCompletionMessage: The model's response
668
+
669
+ Raises:
670
+ TokenLimitExceeded: If token limits are exceeded
671
+ ValueError: If tools, tool_choice, or messages are invalid
672
+ OpenAIError: If API call fails after retries
673
+ Exception: For unexpected errors
674
+ """
675
+ try:
676
+ # Validate tool_choice
677
+ if tool_choice not in TOOL_CHOICE_VALUES:
678
+ raise ValueError(f"Invalid tool_choice: {tool_choice}")
679
+
680
+ # Check if the model supports images
681
+ supports_images = self.model in MULTIMODAL_MODELS
682
+
683
+ # Format messages
684
+ if system_msgs:
685
+ system_msgs = self.format_messages(system_msgs, supports_images)
686
+ messages = system_msgs + self.format_messages(messages, supports_images)
687
+ else:
688
+ messages = self.format_messages(messages, supports_images)
689
+
690
+ # Calculate input token count
691
+ input_tokens = self.count_message_tokens(messages)
692
+
693
+ # If there are tools, calculate token count for tool descriptions
694
+ tools_tokens = 0
695
+ if tools:
696
+ for tool in tools:
697
+ tools_tokens += self.count_tokens(str(tool))
698
+
699
+ input_tokens += tools_tokens
700
+
701
+ # Check if token limits are exceeded
702
+ if not self.check_token_limit(input_tokens):
703
+ error_message = self.get_limit_error_message(input_tokens)
704
+ # Raise a special exception that won't be retried
705
+ raise TokenLimitExceeded(error_message)
706
+
707
+ # Validate tools if provided
708
+ if tools:
709
+ for tool in tools:
710
+ if not isinstance(tool, dict) or "type" not in tool:
711
+ raise ValueError("Each tool must be a dict with 'type' field")
712
+
713
+ # Set up the completion request
714
+ params = {
715
+ "model": self.model,
716
+ "messages": messages,
717
+ "tools": tools,
718
+ "tool_choice": tool_choice,
719
+ "timeout": timeout,
720
+ **kwargs,
721
+ }
722
+
723
+ if self.model in REASONING_MODELS:
724
+ params["max_completion_tokens"] = self.max_tokens
725
+ else:
726
+ params["max_tokens"] = self.max_tokens
727
+ params["temperature"] = (
728
+ temperature if temperature is not None else self.temperature
729
+ )
730
+
731
+ params["stream"] = False # Always use non-streaming for tool requests
732
+ response: ChatCompletion = await self.client.chat.completions.create(
733
+ **params
734
+ )
735
+
736
+ # Check if response is valid
737
+ if not response.choices or not response.choices[0].message:
738
+ print(response)
739
+ # raise ValueError("Invalid or empty response from LLM")
740
+ return None
741
+
742
+ # Update token counts
743
+ self.update_token_count(
744
+ response.usage.prompt_tokens, response.usage.completion_tokens
745
+ )
746
+
747
+ return response.choices[0].message
748
+
749
+ except TokenLimitExceeded:
750
+ # Re-raise token limit errors without logging
751
+ raise
752
+ except ValueError as ve:
753
+ logger.error(f"Validation error in ask_tool: {ve}")
754
+ raise
755
+ except OpenAIError as oe:
756
+ logger.error(f"OpenAI API error: {oe}")
757
+ if isinstance(oe, AuthenticationError):
758
+ logger.error("Authentication failed. Check API key.")
759
+ elif isinstance(oe, RateLimitError):
760
+ logger.error("Rate limit exceeded. Consider increasing retry attempts.")
761
+ elif isinstance(oe, APIError):
762
+ logger.error(f"API error: {oe}")
763
+ raise
764
+ except Exception as e:
765
+ logger.error(f"Unexpected error in ask_tool: {e}")
766
+ raise
app/logger.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")
app/mcp/__init__.py ADDED
File without changes
app/mcp/server.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+
4
+
5
+ logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler(sys.stderr)])
6
+
7
+ import argparse
8
+ import asyncio
9
+ import atexit
10
+ import json
11
+ from inspect import Parameter, Signature
12
+ from typing import Any, Dict, Optional
13
+
14
+ from mcp.server.fastmcp import FastMCP
15
+
16
+ from app.logger import logger
17
+ from app.tool.base import BaseTool
18
+ from app.tool.bash import Bash
19
+ from app.tool.browser_use_tool import BrowserUseTool
20
+ from app.tool.str_replace_editor import StrReplaceEditor
21
+ from app.tool.terminate import Terminate
22
+
23
+
24
+ class MCPServer:
25
+ """MCP Server implementation with tool registration and management."""
26
+
27
+ def __init__(self, name: str = "openmanus"):
28
+ self.server = FastMCP(name)
29
+ self.tools: Dict[str, BaseTool] = {}
30
+
31
+ # Initialize standard tools
32
+ self.tools["bash"] = Bash()
33
+ self.tools["browser"] = BrowserUseTool()
34
+ self.tools["editor"] = StrReplaceEditor()
35
+ self.tools["terminate"] = Terminate()
36
+
37
+ def register_tool(self, tool: BaseTool, method_name: Optional[str] = None) -> None:
38
+ """Register a tool with parameter validation and documentation."""
39
+ tool_name = method_name or tool.name
40
+ tool_param = tool.to_param()
41
+ tool_function = tool_param["function"]
42
+
43
+ # Define the async function to be registered
44
+ async def tool_method(**kwargs):
45
+ logger.info(f"Executing {tool_name}: {kwargs}")
46
+ result = await tool.execute(**kwargs)
47
+
48
+ logger.info(f"Result of {tool_name}: {result}")
49
+
50
+ # Handle different types of results (match original logic)
51
+ if hasattr(result, "model_dump"):
52
+ return json.dumps(result.model_dump())
53
+ elif isinstance(result, dict):
54
+ return json.dumps(result)
55
+ return result
56
+
57
+ # Set method metadata
58
+ tool_method.__name__ = tool_name
59
+ tool_method.__doc__ = self._build_docstring(tool_function)
60
+ tool_method.__signature__ = self._build_signature(tool_function)
61
+
62
+ # Store parameter schema (important for tools that access it programmatically)
63
+ param_props = tool_function.get("parameters", {}).get("properties", {})
64
+ required_params = tool_function.get("parameters", {}).get("required", [])
65
+ tool_method._parameter_schema = {
66
+ param_name: {
67
+ "description": param_details.get("description", ""),
68
+ "type": param_details.get("type", "any"),
69
+ "required": param_name in required_params,
70
+ }
71
+ for param_name, param_details in param_props.items()
72
+ }
73
+
74
+ # Register with server
75
+ self.server.tool()(tool_method)
76
+ logger.info(f"Registered tool: {tool_name}")
77
+
78
+ def _build_docstring(self, tool_function: dict) -> str:
79
+ """Build a formatted docstring from tool function metadata."""
80
+ description = tool_function.get("description", "")
81
+ param_props = tool_function.get("parameters", {}).get("properties", {})
82
+ required_params = tool_function.get("parameters", {}).get("required", [])
83
+
84
+ # Build docstring (match original format)
85
+ docstring = description
86
+ if param_props:
87
+ docstring += "\n\nParameters:\n"
88
+ for param_name, param_details in param_props.items():
89
+ required_str = (
90
+ "(required)" if param_name in required_params else "(optional)"
91
+ )
92
+ param_type = param_details.get("type", "any")
93
+ param_desc = param_details.get("description", "")
94
+ docstring += (
95
+ f" {param_name} ({param_type}) {required_str}: {param_desc}\n"
96
+ )
97
+
98
+ return docstring
99
+
100
+ def _build_signature(self, tool_function: dict) -> Signature:
101
+ """Build a function signature from tool function metadata."""
102
+ param_props = tool_function.get("parameters", {}).get("properties", {})
103
+ required_params = tool_function.get("parameters", {}).get("required", [])
104
+
105
+ parameters = []
106
+
107
+ # Follow original type mapping
108
+ for param_name, param_details in param_props.items():
109
+ param_type = param_details.get("type", "")
110
+ default = Parameter.empty if param_name in required_params else None
111
+
112
+ # Map JSON Schema types to Python types (same as original)
113
+ annotation = Any
114
+ if param_type == "string":
115
+ annotation = str
116
+ elif param_type == "integer":
117
+ annotation = int
118
+ elif param_type == "number":
119
+ annotation = float
120
+ elif param_type == "boolean":
121
+ annotation = bool
122
+ elif param_type == "object":
123
+ annotation = dict
124
+ elif param_type == "array":
125
+ annotation = list
126
+
127
+ # Create parameter with same structure as original
128
+ param = Parameter(
129
+ name=param_name,
130
+ kind=Parameter.KEYWORD_ONLY,
131
+ default=default,
132
+ annotation=annotation,
133
+ )
134
+ parameters.append(param)
135
+
136
+ return Signature(parameters=parameters)
137
+
138
+ async def cleanup(self) -> None:
139
+ """Clean up server resources."""
140
+ logger.info("Cleaning up resources")
141
+ # Follow original cleanup logic - only clean browser tool
142
+ if "browser" in self.tools and hasattr(self.tools["browser"], "cleanup"):
143
+ await self.tools["browser"].cleanup()
144
+
145
+ def register_all_tools(self) -> None:
146
+ """Register all tools with the server."""
147
+ for tool in self.tools.values():
148
+ self.register_tool(tool)
149
+
150
+ def run(self, transport: str = "stdio") -> None:
151
+ """Run the MCP server."""
152
+ # Register all tools
153
+ self.register_all_tools()
154
+
155
+ # Register cleanup function (match original behavior)
156
+ atexit.register(lambda: asyncio.run(self.cleanup()))
157
+
158
+ # Start server (with same logging as original)
159
+ logger.info(f"Starting OpenManus server ({transport} mode)")
160
+ self.server.run(transport=transport)
161
+
162
+
163
+ def parse_args() -> argparse.Namespace:
164
+ """Parse command line arguments."""
165
+ parser = argparse.ArgumentParser(description="OpenManus MCP Server")
166
+ parser.add_argument(
167
+ "--transport",
168
+ choices=["stdio"],
169
+ default="stdio",
170
+ help="Communication method: stdio or http (default: stdio)",
171
+ )
172
+ return parser.parse_args()
173
+
174
+
175
+ if __name__ == "__main__":
176
+ args = parse_args()
177
+
178
+ # Create and run server (maintaining original flow)
179
+ server = MCPServer()
180
+ server.run(transport=args.transport)
app/prompt/__init__.py ADDED
File without changes
app/prompt/browser.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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]<type>text</type>
11
+ - index: Numeric identifier for interaction
12
+ - type: HTML element type (button, input, etc.)
13
+ - text: Element description
14
+ Example:
15
+ [33]<button>Submit Form</button>
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
+ """