Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a1065f8cc | |||
| f9b36d8406 | |||
| d75a59aeb8 | |||
| 456868c01c | |||
| 594b38f905 | |||
| 7bd742eafc | |||
| 5ea799d939 | |||
| 6160a5d05b | |||
| b63a0301dc | |||
| d0a62e65c7 | |||
| 6243591e7f | |||
| 8439fdc7ab | |||
| 38e34219d3 | |||
| 693f6a90ad | |||
| d847b550d6 | |||
| 5bba31db3e | |||
| 548c402316 | |||
| c96c016feb | |||
| 728fdaffd6 | |||
| fa2b05b658 | |||
| 7b96e12858 | |||
| 62f937c1ca | |||
| 6e8de4e35e | |||
| 8280b68462 | |||
| 8018124af2 | |||
| 229d6706a4 | |||
| d9418c1668 | |||
| c37c6c19b5 | |||
| a256d5589a | |||
| 2fcad5f097 | |||
| 41308f3aca | |||
| 77df9772fe | |||
| 69325c701b | |||
| f67af3580a | |||
| 34ede1c082 | |||
| b3c4f5ce22 | |||
| bfa5a672c2 | |||
| 576090439b | |||
| 479b47a7d6 | |||
| 9fc1ea3ee4 | |||
| 444fc91230 | |||
| b23ab9a42a | |||
| e38e35d2ab | |||
| 110213a29f | |||
| 81b9bb3660 | |||
| f2ddc63a6f | |||
| d661578044 | |||
| 8b955c7336 | |||
| 1a3ae5d06b | |||
| c3322ee72e | |||
| a8ee4d1617 | |||
| d19e7d5acd | |||
| 186ce2d1c5 | |||
| 8ae3a6c77b | |||
| 825855d13a | |||
| ecdbb3b26a | |||
| c10600d36e | |||
| 60bb2ce00f | |||
| 7501b74030 | |||
| 0345aa0e95 | |||
| 5703502fab | |||
| 915aed17d1 | |||
| 38bb658031 | |||
| 2fe2f75c21 | |||
| 1dfc2c41ec | |||
| 06e2239b70 | |||
| b7c11a5db0 | |||
| abb2516bc3 | |||
| 52b67090a0 | |||
| 85bd75d236 | |||
| e6ed98e53c | |||
| 7a9e22d093 | |||
| c71ee143e2 | |||
| 0027af217c | |||
| 0d0f8ab233 | |||
| cd86cda61e | |||
| 5694a5e89c | |||
| 6164ac7bb8 | |||
| 7090490f75 | |||
| 7bcc104aff | |||
| e47d2eeb02 | |||
| ee3938dae5 | |||
| 3024753e35 | |||
| cf3603dfcc | |||
| f04beec486 | |||
| 56d138220a | |||
| 41063bcd2b | |||
| 6b77a99448 | |||
| e76055b436 | |||
| 6716a47823 | |||
| dc28e9187b | |||
| 35e3b5d94b | |||
| a4340aa3b9 | |||
| 0abdd6f9da | |||
| e70332bfa2 | |||
| d921fc4248 | |||
| d9e6e9a8e5 | |||
| ecac3382ec | |||
| 5650d3170b | |||
| 612fb4964b | |||
| ab0c243850 | |||
| 29415b7393 | |||
| af65f1f754 | |||
| 5226628c3e | |||
| 5eed101f78 | |||
| 5eb5ea1f09 | |||
| d3fb8c440c | |||
| 20a08b6e26 | |||
| 1e2559a0d1 | |||
| a18cf3855d | |||
| a41e37fd1f | |||
| 295da18c5d | |||
| ddbc58e67a | |||
| fa89ba33ce | |||
| 247034d90f | |||
| 77efc59021 | |||
| e55206b7ee | |||
| 4df621c5a1 | |||
| 240a4a0f0e | |||
| 90624ecaa2 | |||
| e370ae6f93 |
@@ -0,0 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "📑 Read online docs"
|
||||
about: Find tutorials, use cases, and guides in the OpenManus documentation.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: "🤔 Request new features"
|
||||
about: Suggest ideas or features you’d like to see implemented in OpenManus.
|
||||
title: ''
|
||||
labels: kind/features
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Feature description**
|
||||
<!-- Provide a clear and concise description of the proposed feature -->
|
||||
|
||||
**Your Feature**
|
||||
<!-- Explain your idea or implementation process. Optionally, include a Pull Request URL. -->
|
||||
<!-- Ensure accompanying docs/tests/examples are provided for review. -->
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "🪲 Show me the Bug"
|
||||
about: Report a bug encountered while using OpenManus and seek assistance.
|
||||
title: ''
|
||||
labels: kind/bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Bug description**
|
||||
<!-- Clearly describe the bug you encountered -->
|
||||
|
||||
**Bug solved method**
|
||||
<!-- If resolved, explain the solution. Optionally, include a Pull Request URL. -->
|
||||
<!-- If unresolved, provide additional details to aid investigation -->
|
||||
|
||||
**Environment information**
|
||||
<!-- System: e.g., Ubuntu 22.04, Python: e.g., 3.12, OpenManus version: e.g., 0.1.0 -->
|
||||
|
||||
- System version:
|
||||
- Python version:
|
||||
- OpenManus version or branch:
|
||||
- Installation method (e.g., `pip install -r requirements.txt` or `pip install -e .`):
|
||||
|
||||
**Screenshots or logs**
|
||||
<!-- Attach screenshots or logs to help diagnose the issue -->
|
||||
@@ -0,0 +1,17 @@
|
||||
**Features**
|
||||
<!-- Describe the features or bug fixes in this PR. For bug fixes, link to the issue. -->
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
|
||||
**Feature Docs**
|
||||
<!-- Provide RFC, tutorial, or use case links for significant updates. Optional for minor changes. -->
|
||||
|
||||
**Influence**
|
||||
<!-- Explain the impact of these changes for reviewer focus. -->
|
||||
|
||||
**Result**
|
||||
<!-- Include screenshots or logs of unit tests or running results. -->
|
||||
|
||||
**Other**
|
||||
<!-- Additional notes about this PR. -->
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Build and upload Python package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created, published]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install setuptools wheel twine
|
||||
- name: Set package version
|
||||
run: |
|
||||
export VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
sed -i "s/version=.*/version=\"${VERSION}\",/" setup.py
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
python setup.py bdist_wheel sdist
|
||||
twine upload dist/*
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Pre-commit checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
pre-commit-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install pre-commit and tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pre-commit black==23.1.0 isort==5.12.0 autoflake==2.0.1
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --all-files
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Close inactive issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "5 0 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "inactive"
|
||||
stale-issue-message: "This issue has been inactive for 30 days. Please comment if you have updates."
|
||||
close-issue-message: "This issue was closed due to 45 days of inactivity. Reopen if still relevant."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -18,22 +18,22 @@ repos:
|
||||
- id: autoflake
|
||||
args: [
|
||||
--remove-all-unused-imports,
|
||||
--ignore-init-module-imports, # 忽略 __init__.py 中的导入
|
||||
--ignore-init-module-imports,
|
||||
--expand-star-imports,
|
||||
--remove-duplicate-keys,
|
||||
--remove-unused-variables,
|
||||
--recursive,
|
||||
--in-place,
|
||||
--exclude=__init__.py, # 排除 __init__.py 文件
|
||||
--exclude=__init__.py,
|
||||
]
|
||||
files: \.py$ # 只处理 Python 文件
|
||||
files: \.py$
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: [
|
||||
"--profile", "black", # 使用 black 兼容的配置
|
||||
"--profile", "black",
|
||||
"--filter-files",
|
||||
"--lines-after-imports=2",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people.
|
||||
* Being respectful of differing opinions, viewpoints, and experiences.
|
||||
* Giving and gracefully accepting constructive feedback.
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience.
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community.
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind.
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks.
|
||||
* Public or private harassment.
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission.
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting.
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
mannaandpoem@gmail.com
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
### Slack and Discord Etiquettes
|
||||
|
||||
These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment
|
||||
for all community members. By following these best practices, we ensure effective communication and collaboration while
|
||||
minimizing disruptions. Let’s work together to build a supportive and welcoming community!
|
||||
|
||||
- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be
|
||||
difficult to interpret in text.
|
||||
- Use threads for specific discussions to keep channels organized and easier to follow.
|
||||
- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize
|
||||
disruptions.
|
||||
- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
|
||||
- Post questions or discussions in the most relevant
|
||||
channel ([discord - #general](https://discord.com/channels/1125308739348594758/1138430348557025341)).
|
||||
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to
|
||||
provide context.
|
||||
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the
|
||||
matter is sensitive or private.
|
||||
- Always adhere to [our standards](https://github.com/mannaandpoem/OpenManus/blob/main/CODE_OF_CONDUCT.md#our-standards)
|
||||
to ensure a welcoming and collaborative environment.
|
||||
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For
|
||||
Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For
|
||||
example, if you're here for discussions about LLMs, mute the channel if it’s too busy, but set notifications to alert
|
||||
you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that
|
||||
best describes your need.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -1,18 +1,33 @@
|
||||
# OpenManus 🙋
|
||||
Manus is incredible, but OpenManus can achieve any ideas without an Invite Code 🛫!
|
||||
English | [中文](README_zh.md)
|
||||
|
||||
Our team members @mannaandpoem @XiangJinyu @MoshiQAQ @didiforgithub from @MetaGPT built it within 3 hours!
|
||||
[](https://github.com/mannaandpoem/OpenManus/stargazers)
|
||||
 
|
||||
[](https://opensource.org/licenses/MIT)  
|
||||
[](https://discord.gg/DYn29wFk9z)
|
||||
|
||||
# 👋 OpenManus
|
||||
|
||||
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
|
||||
|
||||
Our team
|
||||
members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](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!
|
||||
|
||||
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
|
||||
|
||||
Enjoy your own agent with OpenManus!
|
||||
|
||||
## Project Demo
|
||||
[Demo Video](https://github.com/mannaandpoem/OpenManus/blob/main/demo/seo_website.mp4)
|
||||
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.
|
||||
|
||||
## Project Demo
|
||||
|
||||
<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>
|
||||
|
||||
## Installation
|
||||
|
||||
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
|
||||
|
||||
### Method 1: Using conda
|
||||
|
||||
1. Create a new conda environment:
|
||||
|
||||
```bash
|
||||
@@ -33,6 +48,36 @@ cd OpenManus
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Method 2: Using uv (Recommended)
|
||||
|
||||
1. Install uv (A fast Python package installer and resolver):
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
2. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. Create a new virtual environment and activate it:
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate # On Unix/macOS
|
||||
# Or on Windows:
|
||||
# .venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. Install dependencies:
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
|
||||
@@ -62,6 +107,7 @@ api_key = "sk-..." # Replace with your actual API key
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
One line for run OpenManus:
|
||||
|
||||
```bash
|
||||
@@ -77,19 +123,37 @@ python run_flow.py
|
||||
```
|
||||
|
||||
## How to contribute
|
||||
|
||||
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
|
||||
|
||||
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
|
||||
|
||||
## Roadmap
|
||||
- [ ] Better Planning
|
||||
- [ ] Live Demos
|
||||
- [ ] Replay
|
||||
- [ ] RL Fine-tuned Models
|
||||
- [ ] Comprehensive Benchmarks
|
||||
## Community Group
|
||||
Join our networking group on Feishu and share your experience with other developers!
|
||||
|
||||
<div align="center" style="display: flex; gap: 20px;">
|
||||
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
|
||||
</div>
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#mannaandpoem/OpenManus&Date)
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) and [broswer-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
|
||||
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
|
||||
and [browser-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
|
||||
|
||||
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
|
||||
|
||||
## Cite
|
||||
```bibtex
|
||||
@misc{openmanus2025,
|
||||
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
|
||||
title = {OpenManus: An open-source framework for building general AI agents},
|
||||
year = {2025},
|
||||
publisher = {GitHub},
|
||||
journal = {GitHub repository},
|
||||
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
|
||||
}
|
||||
```
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
[English](README.md) | 中文
|
||||
|
||||
[](https://github.com/mannaandpoem/OpenManus/stargazers)
|
||||
 
|
||||
[](https://opensource.org/licenses/MIT)  
|
||||
[](https://discord.gg/DYn29wFk9z)
|
||||
|
||||
# 👋 OpenManus
|
||||
|
||||
Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫!
|
||||
|
||||
我们的团队成员 [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) https://github.com/stellaHSR 来自 [@MetaGPT](https://github.com/geekan/MetaGPT) 组织,我们在 3
|
||||
小时内完成了原型开发并持续迭代中!
|
||||
|
||||
这是一个简洁的实现方案,欢迎任何建议、贡献和反馈!
|
||||
|
||||
用 OpenManus 开启你的智能体之旅吧!
|
||||
|
||||
我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。
|
||||
|
||||
## 项目演示
|
||||
|
||||
<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>
|
||||
|
||||
## 安装指南
|
||||
|
||||
我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。
|
||||
|
||||
### 方式一:使用 conda
|
||||
|
||||
1. 创建新的 conda 环境:
|
||||
|
||||
```bash
|
||||
conda create -n open_manus python=3.12
|
||||
conda activate open_manus
|
||||
```
|
||||
|
||||
2. 克隆仓库:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. 安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 方式二:使用 uv(推荐)
|
||||
|
||||
1. 安装 uv(一个快速的 Python 包管理器):
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
2. 克隆仓库:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mannaandpoem/OpenManus.git
|
||||
cd OpenManus
|
||||
```
|
||||
|
||||
3. 创建并激活虚拟环境:
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate # Unix/macOS 系统
|
||||
# Windows 系统使用:
|
||||
# .venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. 安装依赖:
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
OpenManus 需要配置使用的 LLM API,请按以下步骤设置:
|
||||
|
||||
1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制):
|
||||
|
||||
```bash
|
||||
cp config/config.example.toml config/config.toml
|
||||
```
|
||||
|
||||
2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置:
|
||||
|
||||
```toml
|
||||
# 全局 LLM 配置
|
||||
[llm]
|
||||
model = "gpt-4o"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..." # 替换为真实 API 密钥
|
||||
max_tokens = 4096
|
||||
temperature = 0.0
|
||||
|
||||
# 可选特定 LLM 模型配置
|
||||
[llm.vision]
|
||||
model = "gpt-4o"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..." # 替换为真实 API 密钥
|
||||
```
|
||||
|
||||
## 快速启动
|
||||
|
||||
一行命令运行 OpenManus:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
然后通过终端输入你的创意!
|
||||
|
||||
如需体验开发中版本,可运行:
|
||||
|
||||
```bash
|
||||
python run_flow.py
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
|
||||
我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。
|
||||
|
||||
或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com
|
||||
|
||||
## 交流群
|
||||
|
||||
加入我们的飞书交流群,与其他开发者分享经验!
|
||||
|
||||
<div align="center" style="display: flex; gap: 20px;">
|
||||
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
|
||||
</div>
|
||||
|
||||
## Star 数量
|
||||
|
||||
[](https://star-history.com/#mannaandpoem/OpenManus&Date)
|
||||
|
||||
## 致谢
|
||||
|
||||
特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
|
||||
和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持!
|
||||
|
||||
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
|
||||
@@ -0,0 +1,353 @@
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
import tomllib
|
||||
import uuid
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Body, FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import (
|
||||
FileResponse,
|
||||
HTMLResponse,
|
||||
JSONResponse,
|
||||
StreamingResponse,
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class Task(BaseModel):
|
||||
id: str
|
||||
prompt: str
|
||||
created_at: datetime
|
||||
status: str
|
||||
steps: list = []
|
||||
|
||||
def model_dump(self, *args, **kwargs):
|
||||
data = super().model_dump(*args, **kwargs)
|
||||
data["created_at"] = self.created_at.isoformat()
|
||||
return data
|
||||
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self):
|
||||
self.tasks = {}
|
||||
self.queues = {}
|
||||
|
||||
def create_task(self, prompt: str) -> Task:
|
||||
task_id = str(uuid.uuid4())
|
||||
task = Task(
|
||||
id=task_id, prompt=prompt, created_at=datetime.now(), status="pending"
|
||||
)
|
||||
self.tasks[task_id] = task
|
||||
self.queues[task_id] = asyncio.Queue()
|
||||
return task
|
||||
|
||||
async def update_task_step(
|
||||
self, task_id: str, step: int, result: str, step_type: str = "step"
|
||||
):
|
||||
if task_id in self.tasks:
|
||||
task = self.tasks[task_id]
|
||||
task.steps.append({"step": step, "result": result, "type": step_type})
|
||||
await self.queues[task_id].put(
|
||||
{"type": step_type, "step": step, "result": result}
|
||||
)
|
||||
await self.queues[task_id].put(
|
||||
{"type": "status", "status": task.status, "steps": task.steps}
|
||||
)
|
||||
|
||||
async def complete_task(self, task_id: str):
|
||||
if task_id in self.tasks:
|
||||
task = self.tasks[task_id]
|
||||
task.status = "completed"
|
||||
await self.queues[task_id].put(
|
||||
{"type": "status", "status": task.status, "steps": task.steps}
|
||||
)
|
||||
await self.queues[task_id].put({"type": "complete"})
|
||||
|
||||
async def fail_task(self, task_id: str, error: str):
|
||||
if task_id in self.tasks:
|
||||
self.tasks[task_id].status = f"failed: {error}"
|
||||
await self.queues[task_id].put({"type": "error", "message": error})
|
||||
|
||||
|
||||
task_manager = TaskManager()
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/download")
|
||||
async def download_file(file_path: str):
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
return FileResponse(file_path, filename=os.path.basename(file_path))
|
||||
|
||||
|
||||
@app.post("/tasks")
|
||||
async def create_task(prompt: str = Body(..., embed=True)):
|
||||
task = task_manager.create_task(prompt)
|
||||
asyncio.create_task(run_task(task.id, prompt))
|
||||
return {"task_id": task.id}
|
||||
|
||||
|
||||
from app.agent.manus import Manus
|
||||
|
||||
|
||||
async def run_task(task_id: str, prompt: str):
|
||||
try:
|
||||
task_manager.tasks[task_id].status = "running"
|
||||
|
||||
agent = Manus(
|
||||
name="Manus",
|
||||
description="A versatile agent that can solve various tasks using multiple tools",
|
||||
)
|
||||
|
||||
async def on_think(thought):
|
||||
await task_manager.update_task_step(task_id, 0, thought, "think")
|
||||
|
||||
async def on_tool_execute(tool, input):
|
||||
await task_manager.update_task_step(
|
||||
task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool"
|
||||
)
|
||||
|
||||
async def on_action(action):
|
||||
await task_manager.update_task_step(
|
||||
task_id, 0, f"Executing action: {action}", "act"
|
||||
)
|
||||
|
||||
async def on_run(step, result):
|
||||
await task_manager.update_task_step(task_id, step, result, "run")
|
||||
|
||||
from app.logger import logger
|
||||
|
||||
class SSELogHandler:
|
||||
def __init__(self, task_id):
|
||||
self.task_id = task_id
|
||||
|
||||
async def __call__(self, message):
|
||||
import re
|
||||
|
||||
# Extract - Subsequent Content
|
||||
cleaned_message = re.sub(r"^.*? - ", "", message)
|
||||
|
||||
event_type = "log"
|
||||
if "✨ Manus's thoughts:" in cleaned_message:
|
||||
event_type = "think"
|
||||
elif "🛠️ Manus selected" in cleaned_message:
|
||||
event_type = "tool"
|
||||
elif "🎯 Tool" in cleaned_message:
|
||||
event_type = "act"
|
||||
elif "📝 Oops!" in cleaned_message:
|
||||
event_type = "error"
|
||||
elif "🏁 Special tool" in cleaned_message:
|
||||
event_type = "complete"
|
||||
|
||||
await task_manager.update_task_step(
|
||||
self.task_id, 0, cleaned_message, event_type
|
||||
)
|
||||
|
||||
sse_handler = SSELogHandler(task_id)
|
||||
logger.add(sse_handler)
|
||||
|
||||
result = await agent.run(prompt)
|
||||
await task_manager.update_task_step(task_id, 1, result, "result")
|
||||
await task_manager.complete_task(task_id)
|
||||
except Exception as e:
|
||||
await task_manager.fail_task(task_id, str(e))
|
||||
|
||||
|
||||
@app.get("/tasks/{task_id}/events")
|
||||
async def task_events(task_id: str):
|
||||
async def event_generator():
|
||||
if task_id not in task_manager.queues:
|
||||
yield f"event: error\ndata: {dumps({'message': 'Task not found'})}\n\n"
|
||||
return
|
||||
|
||||
queue = task_manager.queues[task_id]
|
||||
|
||||
task = task_manager.tasks.get(task_id)
|
||||
if task:
|
||||
yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
|
||||
|
||||
while True:
|
||||
try:
|
||||
event = await queue.get()
|
||||
formatted_event = dumps(event)
|
||||
|
||||
yield ": heartbeat\n\n"
|
||||
|
||||
if event["type"] == "complete":
|
||||
yield f"event: complete\ndata: {formatted_event}\n\n"
|
||||
break
|
||||
elif event["type"] == "error":
|
||||
yield f"event: error\ndata: {formatted_event}\n\n"
|
||||
break
|
||||
elif event["type"] == "step":
|
||||
task = task_manager.tasks.get(task_id)
|
||||
if task:
|
||||
yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
|
||||
yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
|
||||
elif event["type"] in ["think", "tool", "act", "run"]:
|
||||
yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
|
||||
else:
|
||||
yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
|
||||
|
||||
except asyncio.CancelledError:
|
||||
print(f"Client disconnected for task {task_id}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in event stream: {str(e)}")
|
||||
yield f"event: error\ndata: {dumps({'message': str(e)})}\n\n"
|
||||
break
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/tasks")
|
||||
async def get_tasks():
|
||||
sorted_tasks = sorted(
|
||||
task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True
|
||||
)
|
||||
return JSONResponse(
|
||||
content=[task.model_dump() for task in sorted_tasks],
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/tasks/{task_id}")
|
||||
async def get_task(task_id: str):
|
||||
if task_id not in task_manager.tasks:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return task_manager.tasks[task_id]
|
||||
|
||||
|
||||
@app.get("/config/status")
|
||||
async def check_config_status():
|
||||
config_path = Path(__file__).parent / "config" / "config.toml"
|
||||
example_config_path = Path(__file__).parent / "config" / "config.example.toml"
|
||||
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
current_config = tomllib.load(f)
|
||||
return {"status": "exists", "config": current_config}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
elif example_config_path.exists():
|
||||
try:
|
||||
with open(example_config_path, "rb") as f:
|
||||
example_config = tomllib.load(f)
|
||||
return {"status": "missing", "example_config": example_config}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
else:
|
||||
return {"status": "no_example"}
|
||||
|
||||
|
||||
@app.post("/config/save")
|
||||
async def save_config(config_data: dict = Body(...)):
|
||||
try:
|
||||
config_dir = Path(__file__).parent / "config"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
config_path = config_dir / "config.toml"
|
||||
|
||||
toml_content = ""
|
||||
|
||||
if "llm" in config_data:
|
||||
toml_content += "# Global LLM configuration\n[llm]\n"
|
||||
llm_config = config_data["llm"]
|
||||
for key, value in llm_config.items():
|
||||
if key != "vision":
|
||||
if isinstance(value, str):
|
||||
toml_content += f'{key} = "{value}"\n'
|
||||
else:
|
||||
toml_content += f"{key} = {value}\n"
|
||||
|
||||
if "server" in config_data:
|
||||
toml_content += "\n# Server configuration\n[server]\n"
|
||||
server_config = config_data["server"]
|
||||
for key, value in server_config.items():
|
||||
if isinstance(value, str):
|
||||
toml_content += f'{key} = "{value}"\n'
|
||||
else:
|
||||
toml_content += f"{key} = {value}\n"
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(toml_content)
|
||||
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def generic_exception_handler(request: Request, exc: Exception):
|
||||
return JSONResponse(
|
||||
status_code=500, content={"message": f"Server error: {str(exc)}"}
|
||||
)
|
||||
|
||||
|
||||
def open_local_browser(config):
|
||||
webbrowser.open_new_tab(f"http://{config['host']}:{config['port']}")
|
||||
|
||||
|
||||
def load_config():
|
||||
try:
|
||||
config_path = Path(__file__).parent / "config" / "config.toml"
|
||||
|
||||
if not config_path.exists():
|
||||
return {"host": "localhost", "port": 5172}
|
||||
|
||||
with open(config_path, "rb") as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
return {"host": config["server"]["host"], "port": config["server"]["port"]}
|
||||
except FileNotFoundError:
|
||||
return {"host": "localhost", "port": 5172}
|
||||
except KeyError as e:
|
||||
print(
|
||||
f"The configuration file is missing necessary fields: {str(e)}, use default configuration"
|
||||
)
|
||||
return {"host": "localhost", "port": 5172}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
config = load_config()
|
||||
open_with_config = partial(open_local_browser, config)
|
||||
threading.Timer(3, open_with_config).start()
|
||||
uvicorn.run(app, host=config["host"], port=config["port"])
|
||||
@@ -26,6 +26,9 @@ class Manus(ToolCallAgent):
|
||||
system_prompt: str = SYSTEM_PROMPT
|
||||
next_step_prompt: str = NEXT_STEP_PROMPT
|
||||
|
||||
max_observe: int = 2000
|
||||
max_steps: int = 20
|
||||
|
||||
# Add general-purpose tools to the tool collection
|
||||
available_tools: ToolCollection = Field(
|
||||
default_factory=lambda: ToolCollection(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import Any, List, Literal
|
||||
from typing import Any, List, Literal, Optional, Union
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -31,6 +31,7 @@ class ToolCallAgent(ReActAgent):
|
||||
tool_calls: List[ToolCall] = Field(default_factory=list)
|
||||
|
||||
max_steps: int = 30
|
||||
max_observe: Optional[Union[int, bool]] = None
|
||||
|
||||
async def think(self) -> bool:
|
||||
"""Process current state and decide next actions using tools"""
|
||||
@@ -114,6 +115,9 @@ class ToolCallAgent(ReActAgent):
|
||||
f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}"
|
||||
)
|
||||
|
||||
if self.max_observe:
|
||||
result = result[: self.max_observe]
|
||||
|
||||
# Add tool response to memory
|
||||
tool_msg = Message.tool_message(
|
||||
content=result, tool_call_id=command.id, name=command.function.name
|
||||
@@ -154,7 +158,7 @@ class ToolCallAgent(ReActAgent):
|
||||
except json.JSONDecodeError:
|
||||
error_msg = f"Error parsing arguments for {name}: Invalid JSON format"
|
||||
logger.error(
|
||||
f"📝 Oops! The arguments for '{name}' don't make sense - invalid JSON"
|
||||
f"📝 Oops! The arguments for '{name}' don't make sense - invalid JSON, arguments:{command.function.arguments}"
|
||||
)
|
||||
return f"Error: {error_msg}"
|
||||
except Exception as e:
|
||||
|
||||
@@ -21,6 +21,8 @@ class LLMSettings(BaseModel):
|
||||
api_key: str = Field(..., description="API key")
|
||||
max_tokens: int = Field(4096, description="Maximum number of tokens per request")
|
||||
temperature: float = Field(1.0, description="Sampling temperature")
|
||||
api_type: str = Field(..., description="AzureOpenai or Openai")
|
||||
api_version: str = Field(..., description="Azure Openai version if AzureOpenai")
|
||||
|
||||
|
||||
class AppConfig(BaseModel):
|
||||
@@ -76,6 +78,8 @@ class Config:
|
||||
"api_key": base_llm.get("api_key"),
|
||||
"max_tokens": base_llm.get("max_tokens", 4096),
|
||||
"temperature": base_llm.get("temperature", 1.0),
|
||||
"api_type": base_llm.get("api_type", ""),
|
||||
"api_version": base_llm.get("api_version", ""),
|
||||
}
|
||||
|
||||
config_dict = {
|
||||
|
||||
@@ -60,3 +60,32 @@ class BaseFlow(BaseModel, ABC):
|
||||
@abstractmethod
|
||||
async def execute(self, input_text: str) -> str:
|
||||
"""Execute the flow with given input"""
|
||||
|
||||
|
||||
class PlanStepStatus(str, Enum):
|
||||
"""Enum class defining possible statuses of a plan step"""
|
||||
|
||||
NOT_STARTED = "not_started"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED = "completed"
|
||||
BLOCKED = "blocked"
|
||||
|
||||
@classmethod
|
||||
def get_all_statuses(cls) -> list[str]:
|
||||
"""Return a list of all possible step status values"""
|
||||
return [status.value for status in cls]
|
||||
|
||||
@classmethod
|
||||
def get_active_statuses(cls) -> list[str]:
|
||||
"""Return a list of values representing active statuses (not started or in progress)"""
|
||||
return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value]
|
||||
|
||||
@classmethod
|
||||
def get_status_marks(cls) -> Dict[str, str]:
|
||||
"""Return a mapping of statuses to their marker symbols"""
|
||||
return {
|
||||
cls.COMPLETED.value: "[✓]",
|
||||
cls.IN_PROGRESS.value: "[→]",
|
||||
cls.BLOCKED.value: "[!]",
|
||||
cls.NOT_STARTED.value: "[ ]",
|
||||
}
|
||||
|
||||
+25
-30
@@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union
|
||||
from pydantic import Field
|
||||
|
||||
from app.agent.base import BaseAgent
|
||||
from app.flow.base import BaseFlow
|
||||
from app.flow.base import BaseFlow, PlanStepStatus
|
||||
from app.llm import LLM
|
||||
from app.logger import logger
|
||||
from app.schema import AgentState, Message
|
||||
@@ -109,12 +109,14 @@ class PlanningFlow(BaseFlow):
|
||||
|
||||
# Create a system message for plan creation
|
||||
system_message = Message.system_message(
|
||||
"You are a planning assistant. Your task is to create a detailed plan with clear steps."
|
||||
"You are a planning assistant. Create a concise, actionable plan with clear steps. "
|
||||
"Focus on key milestones rather than detailed sub-steps. "
|
||||
"Optimize for clarity and efficiency."
|
||||
)
|
||||
|
||||
# Create a user message with the request
|
||||
user_message = Message.user_message(
|
||||
f"Create a detailed plan to accomplish this task: {request}"
|
||||
f"Create a reasonable plan with clear steps to accomplish the task: {request}"
|
||||
)
|
||||
|
||||
# Call LLM with PlanningTool
|
||||
@@ -181,11 +183,11 @@ class PlanningFlow(BaseFlow):
|
||||
# Find first non-completed step
|
||||
for i, step in enumerate(steps):
|
||||
if i >= len(step_statuses):
|
||||
status = "not_started"
|
||||
status = PlanStepStatus.NOT_STARTED.value
|
||||
else:
|
||||
status = step_statuses[i]
|
||||
|
||||
if status in ["not_started", "in_progress"]:
|
||||
if status in PlanStepStatus.get_active_statuses():
|
||||
# Extract step type/category if available
|
||||
step_info = {"text": step}
|
||||
|
||||
@@ -202,17 +204,17 @@ class PlanningFlow(BaseFlow):
|
||||
command="mark_step",
|
||||
plan_id=self.active_plan_id,
|
||||
step_index=i,
|
||||
step_status="in_progress",
|
||||
step_status=PlanStepStatus.IN_PROGRESS.value,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error marking step as in_progress: {e}")
|
||||
# Update step status directly if needed
|
||||
if i < len(step_statuses):
|
||||
step_statuses[i] = "in_progress"
|
||||
step_statuses[i] = PlanStepStatus.IN_PROGRESS.value
|
||||
else:
|
||||
while len(step_statuses) < i:
|
||||
step_statuses.append("not_started")
|
||||
step_statuses.append("in_progress")
|
||||
step_statuses.append(PlanStepStatus.NOT_STARTED.value)
|
||||
step_statuses.append(PlanStepStatus.IN_PROGRESS.value)
|
||||
|
||||
plan_data["step_statuses"] = step_statuses
|
||||
|
||||
@@ -264,7 +266,7 @@ class PlanningFlow(BaseFlow):
|
||||
command="mark_step",
|
||||
plan_id=self.active_plan_id,
|
||||
step_index=self.current_step_index,
|
||||
step_status="completed",
|
||||
step_status=PlanStepStatus.COMPLETED.value,
|
||||
)
|
||||
logger.info(
|
||||
f"Marked step {self.current_step_index} as completed in plan {self.active_plan_id}"
|
||||
@@ -278,10 +280,10 @@ class PlanningFlow(BaseFlow):
|
||||
|
||||
# Ensure the step_statuses list is long enough
|
||||
while len(step_statuses) <= self.current_step_index:
|
||||
step_statuses.append("not_started")
|
||||
step_statuses.append(PlanStepStatus.NOT_STARTED.value)
|
||||
|
||||
# Update the status
|
||||
step_statuses[self.current_step_index] = "completed"
|
||||
step_statuses[self.current_step_index] = PlanStepStatus.COMPLETED.value
|
||||
plan_data["step_statuses"] = step_statuses
|
||||
|
||||
async def _get_plan_text(self) -> str:
|
||||
@@ -309,23 +311,18 @@ class PlanningFlow(BaseFlow):
|
||||
|
||||
# Ensure step_statuses and step_notes match the number of steps
|
||||
while len(step_statuses) < len(steps):
|
||||
step_statuses.append("not_started")
|
||||
step_statuses.append(PlanStepStatus.NOT_STARTED.value)
|
||||
while len(step_notes) < len(steps):
|
||||
step_notes.append("")
|
||||
|
||||
# Count steps by status
|
||||
status_counts = {
|
||||
"completed": 0,
|
||||
"in_progress": 0,
|
||||
"blocked": 0,
|
||||
"not_started": 0,
|
||||
}
|
||||
status_counts = {status: 0 for status in PlanStepStatus.get_all_statuses()}
|
||||
|
||||
for status in step_statuses:
|
||||
if status in status_counts:
|
||||
status_counts[status] += 1
|
||||
|
||||
completed = status_counts["completed"]
|
||||
completed = status_counts[PlanStepStatus.COMPLETED.value]
|
||||
total = len(steps)
|
||||
progress = (completed / total) * 100 if total > 0 else 0
|
||||
|
||||
@@ -335,21 +332,19 @@ class PlanningFlow(BaseFlow):
|
||||
plan_text += (
|
||||
f"Progress: {completed}/{total} steps completed ({progress:.1f}%)\n"
|
||||
)
|
||||
plan_text += f"Status: {status_counts['completed']} completed, {status_counts['in_progress']} in progress, "
|
||||
plan_text += f"{status_counts['blocked']} blocked, {status_counts['not_started']} not started\n\n"
|
||||
plan_text += f"Status: {status_counts[PlanStepStatus.COMPLETED.value]} completed, {status_counts[PlanStepStatus.IN_PROGRESS.value]} in progress, "
|
||||
plan_text += f"{status_counts[PlanStepStatus.BLOCKED.value]} blocked, {status_counts[PlanStepStatus.NOT_STARTED.value]} not started\n\n"
|
||||
plan_text += "Steps:\n"
|
||||
|
||||
status_marks = PlanStepStatus.get_status_marks()
|
||||
|
||||
for i, (step, status, notes) in enumerate(
|
||||
zip(steps, step_statuses, step_notes)
|
||||
):
|
||||
if status == "completed":
|
||||
status_mark = "[✓]"
|
||||
elif status == "in_progress":
|
||||
status_mark = "[→]"
|
||||
elif status == "blocked":
|
||||
status_mark = "[!]"
|
||||
else: # not_started
|
||||
status_mark = "[ ]"
|
||||
# Use status marks to indicate step status
|
||||
status_mark = status_marks.get(
|
||||
status, status_marks[PlanStepStatus.NOT_STARTED.value]
|
||||
)
|
||||
|
||||
plan_text += f"{i}. {status_mark} {step}\n"
|
||||
if notes:
|
||||
|
||||
+13
-3
@@ -2,6 +2,7 @@ from typing import Dict, List, Literal, Optional, Union
|
||||
|
||||
from openai import (
|
||||
APIError,
|
||||
AsyncAzureOpenAI,
|
||||
AsyncOpenAI,
|
||||
AuthenticationError,
|
||||
OpenAIError,
|
||||
@@ -35,9 +36,18 @@ class LLM:
|
||||
self.model = llm_config.model
|
||||
self.max_tokens = llm_config.max_tokens
|
||||
self.temperature = llm_config.temperature
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=llm_config.api_key, base_url=llm_config.base_url
|
||||
)
|
||||
self.api_type = llm_config.api_type
|
||||
self.api_key = llm_config.api_key
|
||||
self.api_version = llm_config.api_version
|
||||
self.base_url = llm_config.base_url
|
||||
if self.api_type == "azure":
|
||||
self.client = AsyncAzureOpenAI(
|
||||
base_url=self.base_url,
|
||||
api_key=self.api_key,
|
||||
api_version=self.api_version,
|
||||
)
|
||||
else:
|
||||
self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
|
||||
@staticmethod
|
||||
def format_messages(messages: List[Union[dict, Message]]) -> List[dict]:
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ def define_log_level(print_level="INFO", logfile_level="DEBUG", name: str = None
|
||||
_print_level = print_level
|
||||
|
||||
current_date = datetime.now()
|
||||
formatted_date = current_date.strftime("%Y%m%d")
|
||||
formatted_date = current_date.strftime("%Y%m%d%H%M%S")
|
||||
log_name = (
|
||||
f"{name}_{formatted_date}" if name else formatted_date
|
||||
) # name a log with prefix name
|
||||
|
||||
+14
-12
@@ -1,25 +1,27 @@
|
||||
PLANNING_SYSTEM_PROMPT = """
|
||||
You are an expert Planning Agent tasked with solving complex problems by creating and managing structured plans.
|
||||
You are an expert Planning Agent tasked with solving problems efficiently through structured plans.
|
||||
Your job is:
|
||||
1. Analyze requests to understand the task scope
|
||||
2. Create clear, actionable plans with the `planning` tool
|
||||
2. Create a clear, actionable plan that makes meaningful progress with the `planning` tool
|
||||
3. Execute steps using available tools as needed
|
||||
4. Track progress and adapt plans dynamically
|
||||
5. Use `finish` to conclude when the task is complete
|
||||
4. Track progress and adapt plans when necessary
|
||||
5. Use `finish` to conclude immediately when the task is complete
|
||||
|
||||
|
||||
Available tools will vary by task but may include:
|
||||
- `planning`: Create, update, and track plans (commands: create, update, mark_step, etc.)
|
||||
- `finish`: End the task when complete
|
||||
|
||||
Break tasks into logical, sequential steps. Think about dependencies and verification methods.
|
||||
Break tasks into logical steps with clear outcomes. Avoid excessive detail or sub-steps.
|
||||
Think about dependencies and verification methods.
|
||||
Know when to conclude - don't continue thinking once objectives are met.
|
||||
"""
|
||||
|
||||
NEXT_STEP_PROMPT = """
|
||||
Based on the current state, what's your next step?
|
||||
Consider:
|
||||
1. Do you need to create or refine a plan?
|
||||
2. Are you ready to execute a specific step?
|
||||
3. Have you completed the task?
|
||||
Based on the current state, what's your next action?
|
||||
Choose the most efficient path forward:
|
||||
1. Is the plan sufficient, or does it need refinement?
|
||||
2. Can you execute the next step immediately?
|
||||
3. Is the task complete? If so, use `finish` right away.
|
||||
|
||||
Provide reasoning, then select the appropriate tool or action.
|
||||
Be concise in your reasoning, then select the appropriate tool or action.
|
||||
"""
|
||||
|
||||
@@ -20,6 +20,8 @@ content extraction, and tab management. Supported actions include:
|
||||
- 'input_text': Input text into an element
|
||||
- 'screenshot': Capture a screenshot
|
||||
- 'get_html': Get page HTML content
|
||||
- 'get_text': Get text content of the page
|
||||
- 'read_links': Get all links on the page
|
||||
- 'execute_js': Execute JavaScript code
|
||||
- 'scroll': Scroll the page
|
||||
- 'switch_tab': Switch to a specific tab
|
||||
@@ -43,6 +45,7 @@ class BrowserUseTool(BaseTool):
|
||||
"input_text",
|
||||
"screenshot",
|
||||
"get_html",
|
||||
"get_text",
|
||||
"execute_js",
|
||||
"scroll",
|
||||
"switch_tab",
|
||||
@@ -100,7 +103,12 @@ class BrowserUseTool(BaseTool):
|
||||
async def _ensure_browser_initialized(self) -> BrowserContext:
|
||||
"""Ensure browser and context are initialized."""
|
||||
if self.browser is None:
|
||||
self.browser = BrowserUseBrowser(BrowserConfig(headless=False))
|
||||
# 使用Chrome命令行参数设置窗口大小和位置
|
||||
browser_config = BrowserConfig(
|
||||
headless=False,
|
||||
disable_security=True,
|
||||
)
|
||||
self.browser = BrowserUseBrowser(browser_config)
|
||||
if self.context is None:
|
||||
self.context = await self.browser.new_context()
|
||||
self.dom_service = DomService(await self.context.get_current_page())
|
||||
@@ -180,6 +188,16 @@ class BrowserUseTool(BaseTool):
|
||||
truncated = html[:2000] + "..." if len(html) > 2000 else html
|
||||
return ToolResult(output=truncated)
|
||||
|
||||
elif action == "get_text":
|
||||
text = await context.execute_javascript("document.body.innerText")
|
||||
return ToolResult(output=text)
|
||||
|
||||
elif action == "read_links":
|
||||
links = await context.execute_javascript(
|
||||
"document.querySelectorAll('a[href]').forEach((elem) => {if (elem.innerText) {console.log(elem.innerText, elem.href)}})"
|
||||
)
|
||||
return ToolResult(output=links)
|
||||
|
||||
elif action == "execute_js":
|
||||
if not script:
|
||||
return ToolResult(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import aiofiles
|
||||
@@ -58,4 +57,3 @@ The tool accepts content and a file path, and saves the content to that location
|
||||
return f"Content successfully saved to {file_path}"
|
||||
except Exception as e:
|
||||
return f"Error saving file: {str(e)}"
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from app.tool.base import BaseTool
|
||||
|
||||
|
||||
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task."""
|
||||
_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task.
|
||||
When you have finished all the tasks, call this tool to end the work."""
|
||||
|
||||
|
||||
class Terminate(BaseTool):
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
@@ -6,8 +6,22 @@ api_key = "sk-..."
|
||||
max_tokens = 4096
|
||||
temperature = 0.0
|
||||
|
||||
# [llm] #AZURE OPENAI:
|
||||
# api_type= 'azure'
|
||||
# model = "YOUR_MODEL_NAME" #"gpt-4o-mini"
|
||||
# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPOLYMENT_ID}"
|
||||
# api_key = "AZURE API KEY"
|
||||
# max_tokens = 8096
|
||||
# temperature = 0.0
|
||||
# api_version="AZURE API VERSION" #"2024-08-01-preview"
|
||||
|
||||
# Optional configuration for specific LLM models
|
||||
[llm.vision]
|
||||
model = "claude-3-5-sonnet"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
api_key = "sk-..."
|
||||
|
||||
# Server configuration
|
||||
[server]
|
||||
host = "localhost"
|
||||
port = 5172
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,62 @@
|
||||
JAPAN TRAVEL HANDBOOK - GUIDE TO VERSIONS
|
||||
|
||||
Location: D:/OpenManus/
|
||||
|
||||
1. DETAILED DIGITAL VERSION
|
||||
File: japan_travel_handbook.html
|
||||
Best for: Desktop/laptop viewing
|
||||
Features:
|
||||
- Complete comprehensive guide
|
||||
- Detailed itinerary
|
||||
- Full proposal planning section
|
||||
- All hotel recommendations
|
||||
- Comprehensive budget breakdown
|
||||
Usage: Open in web browser for trip planning and detailed reference
|
||||
|
||||
2. PRINT-FRIENDLY VERSION
|
||||
File: japan_travel_handbook_print.html
|
||||
Best for: Physical reference during travel
|
||||
Features:
|
||||
- Condensed essential information
|
||||
- Optimized for paper printing
|
||||
- Clear, printer-friendly formatting
|
||||
- Quick reference tables
|
||||
Usage: Print and keep in travel documents folder
|
||||
|
||||
3. MOBILE-OPTIMIZED VERSION
|
||||
File: japan_travel_handbook_mobile.html
|
||||
Best for: On-the-go reference during trip
|
||||
Features:
|
||||
- Touch-friendly interface
|
||||
- Collapsible sections
|
||||
- Quick access emergency buttons
|
||||
- Dark mode support
|
||||
- Responsive design
|
||||
Usage: Save to phone's browser bookmarks for quick access
|
||||
|
||||
RECOMMENDED SETUP:
|
||||
1. Before Trip:
|
||||
- Use detailed version for planning
|
||||
- Print the print-friendly version
|
||||
- Save mobile version to phone
|
||||
|
||||
2. During Trip:
|
||||
- Keep printed version with travel documents
|
||||
- Use mobile version for daily reference
|
||||
- Access detailed version when needed for specific information
|
||||
|
||||
3. Emergency Access:
|
||||
- Mobile version has quick-access emergency information
|
||||
- Keep printed version as backup
|
||||
- All emergency numbers and contacts in both versions
|
||||
|
||||
Note: All versions contain the same core information but are formatted differently for optimal use in different situations.
|
||||
|
||||
IMPORTANT DATES:
|
||||
- Trip Duration: April 15-23, 2024
|
||||
- Proposal Day: April 19, 2024
|
||||
- Key Reservation Deadlines:
|
||||
* Flights: Book by January 2024
|
||||
* Hotels: Book by February 2024
|
||||
* Restaurant Reservations: Book by January 2024
|
||||
* JR Pass: Purchase by March 2024
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Japan Travel Handbook - April 15-23, 2024</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; }
|
||||
.container { max-width: 1000px; margin: 0 auto; }
|
||||
h1, h2, h3 { color: #333; }
|
||||
.day-item { background: #f9f9f9; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.important-note { background: #ffe6e6; padding: 10px; border-radius: 5px; }
|
||||
.phrase-table { width: 100%; border-collapse: collapse; }
|
||||
.phrase-table td, .phrase-table th { border: 1px solid #ddd; padding: 8px; }
|
||||
.proposal-spot { background: #e6ffe6; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.flight-info { background: #e6f3ff; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.checklist { background: #fff3e6; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.hotels { background: #e6e6ff; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.proposal-plan { background: #ffe6ff; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.checkbox-list li { list-style-type: none; margin-bottom: 8px; }
|
||||
.checkbox-list li:before { content: "☐ "; }
|
||||
.warning { color: #ff4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
[Previous content remains the same...]
|
||||
|
||||
<div class="proposal-plan">
|
||||
<h2>🌸 Proposal Planning Guide 🌸</h2>
|
||||
|
||||
<h3>Ring Security & Transport</h3>
|
||||
<ul>
|
||||
<li><strong>Carrying the Ring:</strong>
|
||||
<ul>
|
||||
<li>Always keep the ring in your carry-on luggage, never in checked bags</li>
|
||||
<li>Use a discrete, non-branded box or case</li>
|
||||
<li>Consider travel insurance that covers jewelry</li>
|
||||
<li>Keep receipt/appraisal documentation separate from the ring</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Airport Security Tips:</strong>
|
||||
<ul>
|
||||
<li>No need to declare the ring unless value exceeds ¥1,000,000 (~$6,700)</li>
|
||||
<li>If asked, simply state it's "personal jewelry"</li>
|
||||
<li>Consider requesting private screening to maintain surprise</li>
|
||||
<li>Keep ring in original box until through security, then transfer to more discrete case</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Proposal Location Details - Maruyama Park</h3>
|
||||
<ul>
|
||||
<li><strong>Best Timing:</strong>
|
||||
<ul>
|
||||
<li>Date: April 19 (Day 5)</li>
|
||||
<li>Time: 5:30 PM (30 minutes before sunset)</li>
|
||||
<li>Park closes at 8:00 PM in April</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Specific Spot Recommendations:</strong>
|
||||
<ul>
|
||||
<li>Primary Location: Near the famous weeping cherry tree
|
||||
<br>- Less crowded in early evening
|
||||
<br>- Beautiful illumination starts at dusk
|
||||
<br>- Iconic Kyoto backdrop
|
||||
</li>
|
||||
<li>Backup Location: Gion Shirakawa area
|
||||
<br>- Atmospheric stone-paved street
|
||||
<br>- Traditional buildings and cherry trees
|
||||
<br>- Beautiful in light rain
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Proposal Day Planning</h3>
|
||||
<ul>
|
||||
<li><strong>Morning Preparation:</strong>
|
||||
<ul>
|
||||
<li>Confirm weather forecast</li>
|
||||
<li>Transfer ring to secure pocket/bag</li>
|
||||
<li>Have backup indoor location details ready</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Suggested Timeline:</strong>
|
||||
<ul>
|
||||
<li>4:00 PM: Start heading to Maruyama Park area</li>
|
||||
<li>4:30 PM: Light refreshments at nearby tea house</li>
|
||||
<li>5:15 PM: Begin walk through park</li>
|
||||
<li>5:30 PM: Arrive at proposal spot</li>
|
||||
<li>6:00 PM: Sunset and illumination begins</li>
|
||||
<li>7:00 PM: Celebratory dinner reservation</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Celebration Dinner Options</h3>
|
||||
<ul>
|
||||
<li><strong>Traditional Japanese:</strong> Kikunoi Roan
|
||||
<br>- Intimate 2-star Michelin restaurant
|
||||
<br>- Advance reservation required (3 months)
|
||||
<br>- Price: ¥15,000-20,000 per person
|
||||
</li>
|
||||
<li><strong>Modern Fusion:</strong> The Sodoh
|
||||
<br>- Beautiful garden views
|
||||
<br>- Western-style seating available
|
||||
<br>- Price: ¥12,000-15,000 per person
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="warning">
|
||||
<h3>Important Notes:</h3>
|
||||
<ul>
|
||||
<li>Keep proposal plans in separate notes from shared itinerary</li>
|
||||
<li>Have a backup plan in case of rain (indoor locations listed above)</li>
|
||||
<li>Consider hiring a local photographer to capture the moment</li>
|
||||
<li>Save restaurant staff contact info in case of timing changes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Japan Travel Guide (Mobile)</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
.container {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 1.5em; margin: 10px 0; }
|
||||
h2 { font-size: 1.3em; margin: 8px 0; }
|
||||
h3 { font-size: 1.1em; margin: 6px 0; }
|
||||
|
||||
/* Mobile-friendly cards */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Collapsible sections */
|
||||
.collapsible {
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
/* Mobile-friendly tables */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin: 10px 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 300px;
|
||||
}
|
||||
th, td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Touch-friendly lists */
|
||||
ul, ol {
|
||||
padding-left: 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
li {
|
||||
margin: 8px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
/* Emergency info styling */
|
||||
.emergency {
|
||||
background: #ffe6e6;
|
||||
border-left: 4px solid #ff4444;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Quick access buttons */
|
||||
.quick-access {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.quick-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
.card {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
.collapsible {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
.active {
|
||||
background: #404040;
|
||||
}
|
||||
th {
|
||||
background: #333;
|
||||
}
|
||||
td, th {
|
||||
border-color: #404040;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Japan Travel Guide</h1>
|
||||
<p><strong>April 15-23, 2024</strong></p>
|
||||
|
||||
<div class="quick-access">
|
||||
<button class="quick-btn" onclick="showSection('emergency')">Emergency</button>
|
||||
<button class="quick-btn" onclick="showSection('phrases')">Phrases</button>
|
||||
<button class="quick-btn" onclick="showSection('transport')">Transport</button>
|
||||
<button class="quick-btn" onclick="showSection('proposal')">Proposal</button>
|
||||
</div>
|
||||
|
||||
<div class="emergency card" id="emergency">
|
||||
<h2>Emergency Contacts</h2>
|
||||
<ul>
|
||||
<li>🚑 Emergency: 119</li>
|
||||
<li>👮 Police: 110</li>
|
||||
<li>🏢 US Embassy: +81-3-3224-5000</li>
|
||||
<li>ℹ️ Tourist Info: 03-3201-3331</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button class="collapsible">📅 Daily Itinerary</button>
|
||||
<div class="content">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<tr><th>Date</th><th>Location</th><th>Activities</th></tr>
|
||||
<tr><td>Apr 15</td><td>Tokyo</td><td>Arrival, Shinjuku</td></tr>
|
||||
<tr><td>Apr 16</td><td>Tokyo</td><td>Meiji, Harajuku, Senso-ji</td></tr>
|
||||
<tr><td>Apr 17</td><td>Tokyo</td><td>Tea Ceremony, Budokan</td></tr>
|
||||
<tr><td>Apr 18</td><td>Kyoto</td><td>Travel, Kinkaku-ji</td></tr>
|
||||
<tr><td>Apr 19</td><td>Kyoto</td><td>Fushimi Inari, Proposal</td></tr>
|
||||
<tr><td>Apr 20</td><td>Nara</td><td>Deer Park, Temples</td></tr>
|
||||
<tr><td>Apr 21</td><td>Tokyo</td><td>Return, Bay Cruise</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="collapsible">🗣️ Essential Phrases</button>
|
||||
<div class="content">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<tr><th>English</th><th>Japanese</th></tr>
|
||||
<tr><td>Thank you</td><td>ありがとう</td></tr>
|
||||
<tr><td>Excuse me</td><td>すみません</td></tr>
|
||||
<tr><td>Please</td><td>お願いします</td></tr>
|
||||
<tr><td>Where is...</td><td>...はどこですか</td></tr>
|
||||
<tr><td>Help!</td><td>助けて!</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="collapsible">🚅 Transportation</button>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
<h3>Key Routes</h3>
|
||||
<ul>
|
||||
<li>Tokyo-Kyoto: 2h15m</li>
|
||||
<li>Kyoto-Nara: 45m</li>
|
||||
<li>Last trains: ~midnight</li>
|
||||
</ul>
|
||||
<p><strong>JR Pass:</strong> Activate April 15</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="collapsible">💍 Proposal Plan</button>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
<h3>April 19 Timeline</h3>
|
||||
<ul>
|
||||
<li>4:00 PM: Head to Maruyama Park</li>
|
||||
<li>5:30 PM: Arrive at spot</li>
|
||||
<li>7:00 PM: Dinner at Kikunoi Roan</li>
|
||||
</ul>
|
||||
<p><strong>Backup:</strong> Gion Shirakawa area</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="collapsible">💰 Budget Tracker</button>
|
||||
<div class="content">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<tr><th>Item</th><th>Budget</th></tr>
|
||||
<tr><td>Hotels</td><td>$1500-2000</td></tr>
|
||||
<tr><td>Transport</td><td>$600-800</td></tr>
|
||||
<tr><td>Food</td><td>$800-1000</td></tr>
|
||||
<tr><td>Activities</td><td>$600-800</td></tr>
|
||||
<tr><td>Shopping</td><td>$500-400</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add click handlers for collapsible sections
|
||||
var coll = document.getElementsByClassName("collapsible");
|
||||
for (var i = 0; i < coll.length; i++) {
|
||||
coll[i].addEventListener("click", function() {
|
||||
this.classList.toggle("active");
|
||||
var content = this.nextElementSibling;
|
||||
if (content.style.display === "block") {
|
||||
content.style.display = "none";
|
||||
} else {
|
||||
content.style.display = "block";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to show specific section
|
||||
function showSection(id) {
|
||||
document.getElementById(id).scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Japan Travel Handbook (Print Version) - April 15-23, 2024</title>
|
||||
<style>
|
||||
@media print {
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.4;
|
||||
margin: 0.5in;
|
||||
}
|
||||
h1 { font-size: 16pt; }
|
||||
h2 { font-size: 14pt; }
|
||||
h3 { font-size: 12pt; }
|
||||
|
||||
.section {
|
||||
margin: 10px 0;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.no-break {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
td, th {
|
||||
border: 1px solid #000;
|
||||
padding: 4px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 5px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
li {
|
||||
margin: 3px 0;
|
||||
}
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
}
|
||||
/* Screen styles */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.4;
|
||||
margin: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
td, th {
|
||||
border: 1px solid #000;
|
||||
padding: 8px;
|
||||
}
|
||||
@media screen {
|
||||
.page-break {
|
||||
margin: 30px 0;
|
||||
border-top: 2px dashed #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Japan Travel Handbook (Print Version)</h1>
|
||||
<p><strong>Trip Dates:</strong> April 15-23, 2024</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>Emergency Contacts & Important Information</h2>
|
||||
<ul>
|
||||
<li>Emergency in Japan: 119 (Ambulance/Fire) / 110 (Police)</li>
|
||||
<li>US Embassy Tokyo: +81-3-3224-5000</li>
|
||||
<li>Tourist Information Hotline: 03-3201-3331</li>
|
||||
<li>Your Travel Insurance: [Write number here]</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Daily Itinerary Summary</h2>
|
||||
<table>
|
||||
<tr><th>Date</th><th>Location</th><th>Key Activities</th></tr>
|
||||
<tr><td>Apr 15</td><td>Tokyo</td><td>Arrival, Shinjuku area exploration</td></tr>
|
||||
<tr><td>Apr 16</td><td>Tokyo</td><td>Meiji Shrine, Harajuku, Senso-ji, Skytree</td></tr>
|
||||
<tr><td>Apr 17</td><td>Tokyo</td><td>Tea Ceremony, Budokan, Yanaka Ginza</td></tr>
|
||||
<tr><td>Apr 18</td><td>Kyoto</td><td>Travel to Kyoto, Kinkaku-ji, Gion</td></tr>
|
||||
<tr><td>Apr 19</td><td>Kyoto</td><td>Fushimi Inari, Arashiyama, Evening Proposal</td></tr>
|
||||
<tr><td>Apr 20</td><td>Nara/Kyoto</td><td>Nara Park day trip, deer feeding</td></tr>
|
||||
<tr><td>Apr 21</td><td>Tokyo</td><td>Return to Tokyo, bay cruise</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Essential Japanese Phrases</h2>
|
||||
<table>
|
||||
<tr><th>English</th><th>Japanese</th><th>When to Use</th></tr>
|
||||
<tr><td>Arigatou gozaimasu</td><td>ありがとうございます</td><td>Thank you (formal)</td></tr>
|
||||
<tr><td>Sumimasen</td><td>すみません</td><td>Excuse me/Sorry</td></tr>
|
||||
<tr><td>Onegaishimasu</td><td>お願いします</td><td>Please</td></tr>
|
||||
<tr><td>Toire wa doko desu ka?</td><td>トイレはどこですか?</td><td>Where is the bathroom?</td></tr>
|
||||
<tr><td>Eigo ga hanasemasu ka?</td><td>英語が話せますか?</td><td>Do you speak English?</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Transportation Notes</h2>
|
||||
<ul>
|
||||
<li>JR Pass: Activate on April 15</li>
|
||||
<li>Tokyo-Kyoto Shinkansen: ~2h15m</li>
|
||||
<li>Kyoto-Nara Local Train: ~45m</li>
|
||||
<li>Last trains: Usually around midnight</li>
|
||||
<li>Keep ¥3000 for unexpected taxi rides</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="page-break"></div>
|
||||
|
||||
<div class="section no-break">
|
||||
<h2>Proposal Day Timeline (April 19)</h2>
|
||||
<table>
|
||||
<tr><th>Time</th><th>Activity</th><th>Notes</th></tr>
|
||||
<tr><td>4:00 PM</td><td>Head to Maruyama Park</td><td>Check weather first</td></tr>
|
||||
<tr><td>4:30 PM</td><td>Tea house visit</td><td>Light refreshments</td></tr>
|
||||
<tr><td>5:15 PM</td><td>Park walk begins</td><td>Head to weeping cherry tree</td></tr>
|
||||
<tr><td>5:30 PM</td><td>Arrive at spot</td><td>Find quiet area</td></tr>
|
||||
<tr><td>7:00 PM</td><td>Dinner reservation</td><td>Kikunoi Roan</td></tr>
|
||||
</table>
|
||||
<p><strong>Backup Location:</strong> Gion Shirakawa area (in case of rain)</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Quick Reference Budget</h2>
|
||||
<table>
|
||||
<tr><th>Item</th><th>Budget (USD)</th><th>Notes</th></tr>
|
||||
<tr><td>Hotels</td><td>1500-2000</td><td>Pre-booked</td></tr>
|
||||
<tr><td>Transport</td><td>600-800</td><td>Including JR Pass</td></tr>
|
||||
<tr><td>Food</td><td>800-1000</td><td>~$60/person/day</td></tr>
|
||||
<tr><td>Activities</td><td>600-800</td><td>Including tea ceremony</td></tr>
|
||||
<tr><td>Shopping</td><td>500-400</td><td>Souvenirs/gifts</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,16 @@
|
||||
# Examples
|
||||
|
||||
We put some examples in the `examples` directory. All the examples use the same prompt
|
||||
as [Manus](https://manus.im/?utm_source=ai-bot.cn).
|
||||
|
||||
The Model we use is `claude3.5`.
|
||||
|
||||
## Japan Travel Plan
|
||||
**Prompt**:
|
||||
```
|
||||
I need a 7-day Japan itinerary for April 15-23 from Seattle, with a $2500-5000 budget for my fiancée and me. We love historical sites, hidden gems, and Japanese culture (kendo, tea ceremonies, Zen meditation). We want to see Nara's deer and explore cities on foot. I plan to propose during this trip and need a special location recommendation. Please provide a detailed itinerary and a simple HTML travel handbook with maps, attraction descriptions, essential Japanese phrases, and travel tips we can reference throughout our journey.
|
||||
```
|
||||
**preview**:
|
||||

|
||||
|
||||

|
||||
@@ -8,10 +8,14 @@ async def main():
|
||||
agent = Manus()
|
||||
while True:
|
||||
try:
|
||||
prompt = input("Enter your prompt (or 'exit' to quit): ")
|
||||
if prompt.lower() == "exit":
|
||||
prompt = input("Enter your prompt (or 'exit'/'quit' to quit): ")
|
||||
prompt_lower = prompt.lower()
|
||||
if prompt_lower in ["exit", "quit"]:
|
||||
logger.info("Goodbye!")
|
||||
break
|
||||
if not prompt.strip():
|
||||
logger.warning("Skipping empty prompt.")
|
||||
continue
|
||||
logger.warning("Processing your request...")
|
||||
await agent.run(prompt)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
|
||||
# Log settings
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
|
||||
log_cli_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
# Make sure asyncio works properly
|
||||
asyncio_mode = auto
|
||||
+3
-1
@@ -5,6 +5,7 @@ pyyaml~=6.0.2
|
||||
loguru~=0.7.3
|
||||
numpy
|
||||
datasets~=3.2.0
|
||||
fastapi~=0.115.11
|
||||
|
||||
html2text~=2024.2.26
|
||||
gymnasium~=1.0.0
|
||||
@@ -17,4 +18,5 @@ googlesearch-python~=1.3.0
|
||||
|
||||
aiofiles~=24.1.0
|
||||
pydantic_core~=2.27.2
|
||||
colorama~=0.4.6
|
||||
colorama~=0.4.6
|
||||
playwright~=1.49.1
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d %~dp0
|
||||
|
||||
set "VENV_DIR=%~dp0venv"
|
||||
set "PYTHON_PATH=%VENV_DIR%\python.exe"
|
||||
|
||||
where git >nul 2>&1
|
||||
if %errorlevel% == 0 (
|
||||
echo Trying to sync with GitHub repository...
|
||||
git pull origin front-end 2>&1 || echo Failed to sync with GitHub, skipping update...
|
||||
) else (
|
||||
echo Git not detected, skipping code synchronization
|
||||
)
|
||||
|
||||
if not exist "%VENV_DIR%\" (
|
||||
echo Virtual environment not found, initializing installation...
|
||||
python -m venv "%VENV_DIR%" || (
|
||||
echo Failed to create virtual environment, please install Python 3.12 first
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
call "%VENV_DIR%\Scripts\activate.bat"
|
||||
pip install -r requirements.txt || (
|
||||
echo Dependency installation failed, please check requirements. txt
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo Starting Python application...
|
||||
if not exist "%PYTHON_PATH%" (
|
||||
echo Error: Python executable file does not exist in %PYTHON_PATH%
|
||||
echo Please try deleting the venv folder and running the script again
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
"%PYTHON_PATH%" "%~dp0app.py"
|
||||
|
||||
pause
|
||||
endlocal
|
||||
+28
-8
@@ -1,32 +1,52 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from app.agent.manus import Manus
|
||||
from app.flow.base import FlowType
|
||||
from app.flow.flow_factory import FlowFactory
|
||||
from app.logger import logger
|
||||
|
||||
|
||||
async def run_flow():
|
||||
agent = Manus()
|
||||
agents = {
|
||||
"manus": Manus(),
|
||||
}
|
||||
|
||||
while True:
|
||||
try:
|
||||
prompt = input("Enter your prompt (or 'exit' to quit): ")
|
||||
if prompt.lower() == "exit":
|
||||
print("Goodbye!")
|
||||
logger.info("Goodbye!")
|
||||
break
|
||||
|
||||
flow = FlowFactory.create_flow(
|
||||
flow_type=FlowType.PLANNING,
|
||||
agents=agent,
|
||||
agents=agents,
|
||||
)
|
||||
if prompt.strip().isspace():
|
||||
logger.warning("Skipping empty prompt.")
|
||||
continue
|
||||
logger.warning("Processing your request...")
|
||||
|
||||
print("Processing your request...")
|
||||
result = await flow.execute(prompt)
|
||||
print(result)
|
||||
try:
|
||||
start_time = time.time()
|
||||
result = await asyncio.wait_for(
|
||||
flow.execute(prompt),
|
||||
timeout=3600, # 60 minute timeout for the entire execution
|
||||
)
|
||||
elapsed_time = time.time() - start_time
|
||||
logger.info(f"Request processed in {elapsed_time:.2f} seconds")
|
||||
logger.info(result)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Request processing timed out after 1 hour")
|
||||
logger.info(
|
||||
"Operation terminated due to timeout. Please try a simpler request."
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("Goodbye!")
|
||||
break
|
||||
logger.info("Operation cancelled by user.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name="openmanus",
|
||||
version="0.1.0",
|
||||
author="mannaandpoem and OpenManus Team",
|
||||
author_email="mannaandpoem@gmail.com",
|
||||
description="A versatile agent that can solve various tasks using multiple tools",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/mannaandpoem/OpenManus",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"pydantic~=2.10.4",
|
||||
"openai~=1.58.1",
|
||||
"tenacity~=9.0.0",
|
||||
"pyyaml~=6.0.2",
|
||||
"loguru~=0.7.3",
|
||||
"numpy",
|
||||
"datasets~=3.2.0",
|
||||
"html2text~=2024.2.26",
|
||||
"gymnasium~=1.0.0",
|
||||
"pillow~=10.4.0",
|
||||
"browsergym~=0.13.3",
|
||||
"uvicorn~=0.34.0",
|
||||
"unidiff~=0.7.5",
|
||||
"browser-use~=0.1.40",
|
||||
"googlesearch-python~=1.3.0",
|
||||
"aiofiles~=24.1.0",
|
||||
"pydantic_core~=2.27.2",
|
||||
"colorama~=0.4.6",
|
||||
],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires=">=3.12",
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"openmanus=main:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
const i18n = {
|
||||
en: {
|
||||
welcome: {
|
||||
title: "Welcome to OpenManus Local Version",
|
||||
subtitle: "Please enter a task prompt to start a new task"
|
||||
},
|
||||
config: {
|
||||
title: "System Configuration",
|
||||
subtitle: "Please fill in the necessary configuration information to continue using the system",
|
||||
note: "Please ensure that the following configuration information is correct.",
|
||||
apiKeyNote: "If you do not have an API key, please obtain one from the corresponding AI service provider.",
|
||||
llmConfig: "LLM Configuration",
|
||||
serverConfig: "Server Configuration",
|
||||
save: "Save Configuration",
|
||||
cancel: "Cancel",
|
||||
required: "Required"
|
||||
},
|
||||
input: {
|
||||
placeholder: "Enter task prompt...",
|
||||
button: "Create Task"
|
||||
},
|
||||
history: {
|
||||
title: "History Tasks"
|
||||
},
|
||||
status: {
|
||||
running: "Task running",
|
||||
completed: "Task completed",
|
||||
failed: "Task failed",
|
||||
error: "Error"
|
||||
},
|
||||
buttons: {
|
||||
configSetting: "Configuration"
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
welcome: {
|
||||
title: "欢迎使用 OpenManus 本地版",
|
||||
subtitle: "请输入任务提示以开始新任务"
|
||||
},
|
||||
config: {
|
||||
title: "系统配置",
|
||||
subtitle: "请填写必要的配置信息以继续使用系统",
|
||||
note: "请确保以下配置信息正确无误。",
|
||||
apiKeyNote: "如果您没有API密钥,请从相应的AI服务提供商获取。",
|
||||
llmConfig: "LLM 配置",
|
||||
serverConfig: "服务器配置",
|
||||
save: "保存配置",
|
||||
cancel: "取消",
|
||||
required: "必填"
|
||||
},
|
||||
input: {
|
||||
placeholder: "输入任务提示...",
|
||||
button: "创建任务"
|
||||
},
|
||||
history: {
|
||||
title: "历史任务"
|
||||
},
|
||||
status: {
|
||||
running: "任务运行中",
|
||||
completed: "任务完成",
|
||||
failed: "任务失败",
|
||||
error: "错误"
|
||||
},
|
||||
buttons: {
|
||||
configSetting: "系统配置"
|
||||
}
|
||||
},
|
||||
ja: {
|
||||
welcome: {
|
||||
title: "OpenManus ローカル版へようこそ",
|
||||
subtitle: "タスクを開始するにはプロンプトを入力してください"
|
||||
},
|
||||
config: {
|
||||
title: "システム設定",
|
||||
subtitle: "システムを使用するために必要な設定情報を入力してください",
|
||||
note: "以下の設定情報が正しいことを確認してください。",
|
||||
apiKeyNote: "APIキーをお持ちでない場合は、対応するAIサービスプロバイダーから取得してください。",
|
||||
llmConfig: "LLM 設定",
|
||||
serverConfig: "サーバー設定",
|
||||
save: "設定を保存",
|
||||
cancel: "キャンセル",
|
||||
required: "必須"
|
||||
},
|
||||
input: {
|
||||
placeholder: "タスクプロンプトを入力...",
|
||||
button: "タスク作成"
|
||||
},
|
||||
history: {
|
||||
title: "タスク履歴"
|
||||
},
|
||||
status: {
|
||||
running: "タスク実行中",
|
||||
completed: "タスク完了",
|
||||
failed: "タスク失敗",
|
||||
error: "エラー"
|
||||
},
|
||||
buttons: {
|
||||
configSetting: "システム設定"
|
||||
}
|
||||
},
|
||||
ko: {
|
||||
welcome: {
|
||||
title: "OpenManus 로컬 버전에 오신 것을 환영합니다",
|
||||
subtitle: "새 작업을 시작하려면 프롬프트를 입력하세요"
|
||||
},
|
||||
config: {
|
||||
title: "시스템 설정",
|
||||
subtitle: "시스템을 계속 사용하려면 필요한 구성 정보를 입력하세요",
|
||||
note: "다음 구성 정보가 올바른지 확인하세요.",
|
||||
apiKeyNote: "API 키가 없는 경우 해당 AI 서비스 제공업체에서 얻으세요.",
|
||||
llmConfig: "LLM 설정",
|
||||
serverConfig: "서버 설정",
|
||||
save: "설정 저장",
|
||||
cancel: "취소",
|
||||
required: "필수"
|
||||
},
|
||||
input: {
|
||||
placeholder: "작업 프롬프트 입력...",
|
||||
button: "작업 생성"
|
||||
},
|
||||
history: {
|
||||
title: "작업 기록"
|
||||
},
|
||||
status: {
|
||||
running: "작업 실행 중",
|
||||
completed: "작업 완료",
|
||||
failed: "작업 실패",
|
||||
error: "오류"
|
||||
},
|
||||
buttons: {
|
||||
configSetting: "시스템 설정"
|
||||
}
|
||||
},
|
||||
de: {
|
||||
welcome: {
|
||||
title: "Willkommen bei OpenManus Lokale Version",
|
||||
subtitle: "Bitte geben Sie eine Aufgabenanweisung ein, um eine neue Aufgabe zu starten"
|
||||
},
|
||||
config: {
|
||||
title: "Systemkonfiguration",
|
||||
subtitle: "Bitte füllen Sie die erforderlichen Konfigurationsinformationen aus",
|
||||
note: "Bitte stellen Sie sicher, dass die folgenden Konfigurationsinformationen korrekt sind.",
|
||||
apiKeyNote: "Wenn Sie keinen API-Schlüssel haben, erhalten Sie diesen vom entsprechenden KI-Dienstanbieter.",
|
||||
llmConfig: "LLM Konfiguration",
|
||||
serverConfig: "Server Konfiguration",
|
||||
save: "Konfiguration speichern",
|
||||
cancel: "Abbrechen",
|
||||
required: "Erforderlich"
|
||||
},
|
||||
input: {
|
||||
placeholder: "Aufgabenanweisung eingeben...",
|
||||
button: "Aufgabe erstellen"
|
||||
},
|
||||
history: {
|
||||
title: "Aufgabenverlauf"
|
||||
},
|
||||
status: {
|
||||
running: "Aufgabe wird ausgeführt",
|
||||
completed: "Aufgabe abgeschlossen",
|
||||
failed: "Aufgabe fehlgeschlagen",
|
||||
error: "Fehler"
|
||||
},
|
||||
buttons: {
|
||||
configSetting: "Systemeinstellungen"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function getCurrentLanguage() {
|
||||
return localStorage.getItem('language') || 'en';
|
||||
}
|
||||
|
||||
function setLanguage(lang) {
|
||||
localStorage.setItem('language', lang);
|
||||
updatePageContent(lang);
|
||||
}
|
||||
|
||||
function updatePageContent(lang) {
|
||||
const texts = i18n[lang];
|
||||
|
||||
document.querySelector('.welcome-message h1').textContent = texts.welcome.title;
|
||||
document.querySelector('.welcome-message p').textContent = texts.welcome.subtitle;
|
||||
|
||||
document.getElementById('config-button').value = texts.buttons.configSetting;
|
||||
document.getElementById('config-button').title = texts.buttons.configSetting;
|
||||
|
||||
document.querySelector('.config-modal-header h2').textContent = texts.config.title;
|
||||
document.querySelector('.config-modal-header p').textContent = texts.config.subtitle;
|
||||
document.querySelector('.note-box p:first-child').textContent = texts.config.note;
|
||||
document.querySelector('.note-box p:last-child').textContent = texts.config.apiKeyNote;
|
||||
|
||||
document.querySelectorAll('.config-section h3').forEach(h3 => {
|
||||
if (h3.textContent.includes('LLM')) {
|
||||
h3.textContent = texts.config.llmConfig;
|
||||
} else if (h3.textContent.includes('Server')) {
|
||||
h3.textContent = texts.config.serverConfig;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('save-config-btn').textContent = texts.config.save;
|
||||
document.getElementById('cancel-config-btn').textContent = texts.config.cancel;
|
||||
|
||||
document.getElementById('prompt-input').placeholder = texts.input.placeholder;
|
||||
document.querySelector('#input-container button').textContent = texts.input.button;
|
||||
|
||||
document.querySelector('.history-panel h2').textContent = texts.history.title;
|
||||
|
||||
document.querySelectorAll('.required-mark').forEach(mark => {
|
||||
mark.textContent = texts.config.required;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentLang = getCurrentLanguage();
|
||||
document.getElementById('language-select').value = currentLang;
|
||||
updatePageContent(currentLang);
|
||||
|
||||
document.getElementById('language-select').addEventListener('change', (e) => {
|
||||
setLanguage(e.target.value);
|
||||
});
|
||||
});
|
||||
+752
@@ -0,0 +1,752 @@
|
||||
let currentEventSource = null;
|
||||
|
||||
let exampleApiKey = '';
|
||||
|
||||
function checkConfigStatus() {
|
||||
fetch('/config/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const inputContainer = document.getElementById('input-container');
|
||||
if (data.status === 'missing') {
|
||||
showConfigModal(data.example_config);
|
||||
inputContainer.classList.add('disabled');
|
||||
} else if (data.status === 'no_example') {
|
||||
alert('Error: Missing configuration example file! Please ensure that the config/config.example.toml file exists.');
|
||||
inputContainer.classList.add('disabled');
|
||||
} else {
|
||||
inputContainer.classList.remove('disabled');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Configuration check failed:', error);
|
||||
document.getElementById('input-container').classList.add('disabled');
|
||||
});
|
||||
}
|
||||
|
||||
// Display configuration pop-up and fill in sample configurations
|
||||
function showConfigModal(config) {
|
||||
const configModal = document.getElementById('config-modal');
|
||||
if (!configModal) return;
|
||||
|
||||
configModal.classList.add('active');
|
||||
|
||||
if (config) {
|
||||
fillConfigForm(config);
|
||||
}
|
||||
|
||||
const closeBtn = configModal.querySelector('.close-modal');
|
||||
const cancelBtn = document.getElementById('cancel-config-btn');
|
||||
|
||||
function closeConfigModal() {
|
||||
configModal.classList.remove('active');
|
||||
document.getElementById('config-error').textContent = '';
|
||||
document.querySelectorAll('.form-group.error').forEach(group => {
|
||||
group.classList.remove('error');
|
||||
});
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = closeConfigModal;
|
||||
}
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.onclick = closeConfigModal;
|
||||
}
|
||||
|
||||
const saveButton = document.getElementById('save-config-btn');
|
||||
if (saveButton) {
|
||||
saveButton.onclick = saveConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Use example configuration to fill in the form
|
||||
function fillConfigForm(exampleConfig) {
|
||||
if (exampleConfig.llm) {
|
||||
const llm = exampleConfig.llm;
|
||||
|
||||
setInputValue('llm-model', llm.model);
|
||||
setInputValue('llm-base-url', llm.base_url);
|
||||
setInputValue('llm-api-key', llm.api_key);
|
||||
|
||||
exampleApiKey = llm.api_key || '';
|
||||
|
||||
setInputValue('llm-max-tokens', llm.max_tokens);
|
||||
setInputValue('llm-temperature', llm.temperature);
|
||||
}
|
||||
|
||||
if (exampleConfig.server) {
|
||||
setInputValue('server-host', exampleConfig.server.host);
|
||||
setInputValue('server-port', exampleConfig.server.port);
|
||||
}
|
||||
}
|
||||
|
||||
function setInputValue(id, value) {
|
||||
const input = document.getElementById(id);
|
||||
if (input && value !== undefined) {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
const configData = collectFormData();
|
||||
|
||||
const requiredFields = [
|
||||
{ id: 'llm-model', name: 'Model Name' },
|
||||
{ id: 'llm-base-url', name: 'API Base URL' },
|
||||
{ id: 'llm-api-key', name: 'API Key' },
|
||||
{ id: 'server-host', name: 'Server Host' },
|
||||
{ id: 'server-port', name: 'Server Port' }
|
||||
];
|
||||
|
||||
let missingFields = [];
|
||||
requiredFields.forEach(field => {
|
||||
if (!document.getElementById(field.id).value.trim()) {
|
||||
missingFields.push(field.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
document.getElementById('config-error').textContent =
|
||||
`Please fill in the necessary configuration information: ${missingFields.join(', ')}`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the API key is the same as the example configuration
|
||||
const apiKey = document.getElementById('llm-api-key').value.trim();
|
||||
if (apiKey === exampleApiKey && exampleApiKey.includes('sk-')) {
|
||||
document.getElementById('config-error').textContent =
|
||||
`Please enter your own API key`;
|
||||
document.getElementById('llm-api-key').parentElement.classList.add('error');
|
||||
return;
|
||||
} else {
|
||||
document.getElementById('llm-api-key').parentElement.classList.remove('error');
|
||||
}
|
||||
|
||||
fetch('/config/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(configData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('config-modal').classList.remove('active');
|
||||
document.getElementById('input-container').classList.remove('disabled');
|
||||
alert('Configuration saved successfully! The application will use the new configuration on next startup.');
|
||||
window.location.reload();
|
||||
} else {
|
||||
document.getElementById('config-error').textContent =
|
||||
`Save failed: ${data.message}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('config-error').textContent =
|
||||
`Request error: ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Collect form data
|
||||
function collectFormData() {
|
||||
const configData = {
|
||||
llm: {
|
||||
model: document.getElementById('llm-model').value,
|
||||
base_url: document.getElementById('llm-base-url').value,
|
||||
api_key: document.getElementById('llm-api-key').value
|
||||
},
|
||||
server: {
|
||||
host: document.getElementById('server-host').value,
|
||||
port: parseInt(document.getElementById('server-port').value || '5172')
|
||||
}
|
||||
};
|
||||
|
||||
const maxTokens = document.getElementById('llm-max-tokens').value;
|
||||
if (maxTokens) {
|
||||
configData.llm.max_tokens = parseInt(maxTokens);
|
||||
}
|
||||
|
||||
const temperature = document.getElementById('llm-temperature').value;
|
||||
if (temperature) {
|
||||
configData.llm.temperature = parseFloat(temperature);
|
||||
}
|
||||
|
||||
return configData;
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
const promptInput = document.getElementById('prompt-input');
|
||||
const prompt = promptInput.value.trim();
|
||||
|
||||
if (!prompt) {
|
||||
alert("Please enter a valid prompt");
|
||||
promptInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
container.innerHTML = '<div class="loading">Initializing task...</div>';
|
||||
document.getElementById('input-container').classList.add('bottom');
|
||||
|
||||
fetch('/tasks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ prompt })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => { throw new Error(err.detail || 'Request failed') });
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID');
|
||||
}
|
||||
setupSSE(data.task_id);
|
||||
loadHistory();
|
||||
promptInput.value = '';
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
console.error('Failed to create task:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function setupSSE(taskId) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 2000;
|
||||
let lastResultContent = '';
|
||||
|
||||
const container = document.getElementById('task-container');
|
||||
|
||||
function connect() {
|
||||
const eventSource = new EventSource(`/tasks/${taskId}/events`);
|
||||
currentEventSource = eventSource;
|
||||
|
||||
let heartbeatTimer = setInterval(() => {
|
||||
container.innerHTML += '<div class="ping">·</div>';
|
||||
}, 5000);
|
||||
|
||||
// Initial polling
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Initial status fetch failed:', error);
|
||||
});
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
container.querySelector('.loading')?.remove();
|
||||
container.classList.add('active');
|
||||
|
||||
const stepContainer = ensureStepContainer(container);
|
||||
const { formattedContent, timestamp } = formatStepContent(data, type);
|
||||
const step = createStepElement(type, formattedContent, timestamp);
|
||||
|
||||
stepContainer.appendChild(step);
|
||||
autoScroll(stepContainer);
|
||||
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Status update failed:', error);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Error handling ${type} event:`, e);
|
||||
}
|
||||
};
|
||||
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message'];
|
||||
eventTypes.forEach(type => {
|
||||
eventSource.addEventListener(type, (event) => handleEvent(event, type));
|
||||
});
|
||||
|
||||
eventSource.addEventListener('complete', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
lastResultContent = data.result || '';
|
||||
|
||||
container.innerHTML += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
<pre>${lastResultContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
updateTaskStatus(task);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Final status update failed:', error);
|
||||
});
|
||||
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
} catch (e) {
|
||||
console.error('Error handling complete event:', e);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
❌ Error: ${data.message}
|
||||
</div>
|
||||
`;
|
||||
eventSource.close();
|
||||
currentEventSource = null;
|
||||
} catch (e) {
|
||||
console.error('Error handling failed:', e);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
if (eventSource.readyState === EventSource.CLOSED) return;
|
||||
|
||||
console.error('SSE connection error:', err);
|
||||
clearInterval(heartbeatTimer);
|
||||
eventSource.close();
|
||||
|
||||
fetch(`/tasks/${taskId}`)
|
||||
.then(response => response.json())
|
||||
.then(task => {
|
||||
if (task.status === 'completed' || task.status === 'failed') {
|
||||
updateTaskStatus(task);
|
||||
if (task.status === 'completed') {
|
||||
container.innerHTML += `
|
||||
<div class="complete">
|
||||
<div>✅ Task completed</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
❌ Error: ${task.error || 'Task failed'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
container.innerHTML += `
|
||||
<div class="warning">
|
||||
⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})...
|
||||
</div>
|
||||
`;
|
||||
setTimeout(connect, retryDelay);
|
||||
} else {
|
||||
container.innerHTML += `
|
||||
<div class="error">
|
||||
⚠ Connection lost, please try refreshing the page
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Task status check failed:', error);
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
setTimeout(connect, retryDelay);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
function loadHistory() {
|
||||
fetch('/tasks')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`request failure: ${response.status} - ${text.substring(0, 100)}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(tasks => {
|
||||
const listContainer = document.getElementById('task-list');
|
||||
listContainer.innerHTML = tasks.map(task => `
|
||||
<div class="task-card" data-task-id="${task.id}">
|
||||
<div>${task.prompt}</div>
|
||||
<div class="task-meta">
|
||||
${new Date(task.created_at).toLocaleString()} -
|
||||
<span class="status status-${task.status ? task.status.toLowerCase() : 'unknown'}">
|
||||
${task.status || 'Unknown state'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load history records:', error);
|
||||
const listContainer = document.getElementById('task-list');
|
||||
listContainer.innerHTML = `<div class="error">Load Fail: ${error.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function ensureStepContainer(container) {
|
||||
let stepContainer = container.querySelector('.step-container');
|
||||
if (!stepContainer) {
|
||||
container.innerHTML = '<div class="step-container"></div>';
|
||||
stepContainer = container.querySelector('.step-container');
|
||||
}
|
||||
return stepContainer;
|
||||
}
|
||||
|
||||
function formatStepContent(data, eventType) {
|
||||
return {
|
||||
formattedContent: data.result,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
};
|
||||
}
|
||||
|
||||
function createStepElement(type, content, timestamp) {
|
||||
const step = document.createElement('div');
|
||||
|
||||
// Executing step
|
||||
const stepRegex = /Executing step (\d+)\/(\d+)/;
|
||||
if (type === 'log' && stepRegex.test(content)) {
|
||||
const match = content.match(stepRegex);
|
||||
const currentStep = parseInt(match[1]);
|
||||
const totalSteps = parseInt(match[2]);
|
||||
|
||||
step.className = 'step-divider';
|
||||
step.innerHTML = `
|
||||
<div class="step-circle">${currentStep}</div>
|
||||
<div class="step-line"></div>
|
||||
<div class="step-info">${currentStep}/${totalSteps}</div>
|
||||
`;
|
||||
} else if (type === 'act') {
|
||||
// Check if it contains information about file saving
|
||||
const saveRegex = /Content successfully saved to (.+)/;
|
||||
const match = content.match(saveRegex);
|
||||
|
||||
step.className = `step-item ${type}`;
|
||||
|
||||
if (match && match[1]) {
|
||||
const filePath = match[1].trim();
|
||||
const fileName = filePath.split('/').pop();
|
||||
const fileExtension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
// Handling different types of files
|
||||
let fileInteractionHtml = '';
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(fileExtension)) {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction image-preview">
|
||||
<img src="${filePath}" alt="${fileName}" class="preview-image" onclick="showFullImage('${filePath}')">
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载图片</a>
|
||||
</div>
|
||||
`;
|
||||
} else if (['mp3', 'wav', 'ogg'].includes(fileExtension)) {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction audio-player">
|
||||
<audio controls src="${filePath}"></audio>
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载音频</a>
|
||||
</div>
|
||||
`;
|
||||
} else if (['html', 'js', 'py'].includes(fileExtension)) {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction code-file">
|
||||
<button onclick="simulateRunPython('${filePath}')" class="run-button">▶️ 模拟运行</button>
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载文件</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
fileInteractionHtml = `
|
||||
<div class="file-interaction">
|
||||
<a href="/download?file_path=${filePath}" download="${fileName}" class="download-link">⬇️ 下载文件: ${fileName}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
${fileInteractionHtml}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
step.className = `step-item ${type}`;
|
||||
step.innerHTML = `
|
||||
<div class="log-line">
|
||||
<span class="log-prefix">${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}:</span>
|
||||
<pre>${content}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
function autoScroll(element) {
|
||||
requestAnimationFrame(() => {
|
||||
element.scrollTo({
|
||||
top: element.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
setTimeout(() => {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
function getEventIcon(eventType) {
|
||||
const icons = {
|
||||
'think': '🤔',
|
||||
'tool': '🛠️',
|
||||
'act': '🚀',
|
||||
'result': '🏁',
|
||||
'error': '❌',
|
||||
'complete': '✅',
|
||||
'log': '📝',
|
||||
'run': '⚙️'
|
||||
};
|
||||
return icons[eventType] || 'ℹ️';
|
||||
}
|
||||
|
||||
function getEventLabel(eventType) {
|
||||
const labels = {
|
||||
'think': 'Thinking',
|
||||
'tool': 'Using Tool',
|
||||
'act': 'Action',
|
||||
'result': 'Result',
|
||||
'error': 'Error',
|
||||
'complete': 'Complete',
|
||||
'log': 'Log',
|
||||
'run': 'Running'
|
||||
};
|
||||
return labels[eventType] || 'Info';
|
||||
}
|
||||
|
||||
function updateTaskStatus(task) {
|
||||
const statusBar = document.getElementById('status-bar');
|
||||
if (!statusBar) return;
|
||||
|
||||
if (task.status === 'completed') {
|
||||
statusBar.innerHTML = `<span class="status-complete">✅ Task completed</span>`;
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
} else if (task.status === 'failed') {
|
||||
statusBar.innerHTML = `<span class="status-error">❌ Task failed: ${task.error || 'Unknown error'}</span>`;
|
||||
|
||||
if (currentEventSource) {
|
||||
currentEventSource.close();
|
||||
currentEventSource = null;
|
||||
}
|
||||
} else {
|
||||
statusBar.innerHTML = `<span class="status-running">⚙️ Task running: ${task.status}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Display full screen image
|
||||
function showFullImage(imageSrc) {
|
||||
const modal = document.getElementById('image-modal');
|
||||
if (!modal) {
|
||||
const modalDiv = document.createElement('div');
|
||||
modalDiv.id = 'image-modal';
|
||||
modalDiv.className = 'image-modal';
|
||||
modalDiv.innerHTML = `
|
||||
<span class="close-modal">×</span>
|
||||
<img src="${imageSrc}" class="modal-content" id="full-image">
|
||||
`;
|
||||
document.body.appendChild(modalDiv);
|
||||
|
||||
const closeBtn = modalDiv.querySelector('.close-modal');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
modalDiv.classList.remove('active');
|
||||
});
|
||||
|
||||
modalDiv.addEventListener('click', (e) => {
|
||||
if (e.target === modalDiv) {
|
||||
modalDiv.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => modalDiv.classList.add('active'), 10);
|
||||
} else {
|
||||
document.getElementById('full-image').src = imageSrc;
|
||||
modal.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate running Python files
|
||||
function simulateRunPython(filePath) {
|
||||
let modal = document.getElementById('python-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'python-modal';
|
||||
modal.className = 'python-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="python-console">
|
||||
<div class="close-modal">×</div>
|
||||
<div class="python-output">Loading Python file contents...</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const closeBtn = modal.querySelector('.close-modal');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
modal.classList.add('active');
|
||||
|
||||
// Load Python file content
|
||||
fetch(filePath)
|
||||
.then(response => response.text())
|
||||
.then(code => {
|
||||
const outputDiv = modal.querySelector('.python-output');
|
||||
outputDiv.innerHTML = '';
|
||||
|
||||
const codeElement = document.createElement('pre');
|
||||
codeElement.textContent = code;
|
||||
codeElement.style.marginBottom = '20px';
|
||||
codeElement.style.padding = '10px';
|
||||
codeElement.style.borderBottom = '1px solid #444';
|
||||
outputDiv.appendChild(codeElement);
|
||||
|
||||
// Add simulation run results
|
||||
const resultElement = document.createElement('div');
|
||||
resultElement.innerHTML = `
|
||||
<div style="color: #4CAF50; margin-top: 10px; margin-bottom: 10px;">
|
||||
> Simulated operation output:</div>
|
||||
<pre style="color: #f8f8f8;">
|
||||
#This is the result of Python code simulation run
|
||||
#The actual operational results may vary
|
||||
|
||||
# Running ${filePath.split('/').pop()}...
|
||||
print("Hello from Python Simulated environment!")
|
||||
|
||||
# Code execution completed
|
||||
</pre>
|
||||
`;
|
||||
outputDiv.appendChild(resultElement);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading Python file:', error);
|
||||
const outputDiv = modal.querySelector('.python-output');
|
||||
outputDiv.innerHTML = `Error loading file: ${error.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check configuration status on startup
|
||||
checkConfigStatus();
|
||||
|
||||
loadHistory();
|
||||
|
||||
const configButton = document.getElementById('config-button');
|
||||
if (configButton) {
|
||||
configButton.addEventListener('click', () => {
|
||||
fetch('/config/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
if (data.status === 'exists' && data.config) {
|
||||
showConfigModal(data.config);
|
||||
} else if (data.status === 'missing' && data.example_config) {
|
||||
showConfigModal(data.example_config);
|
||||
} else if (data.status === 'no_example') {
|
||||
alert('Error: Missing configuration example file! Please ensure that the config/config.example.toml file exists.');
|
||||
} else if (data.status === 'error') {
|
||||
alert('Configuration error: ' + data.message);
|
||||
} else {
|
||||
alert('Unable to load configuration. Please check if config.example.toml exists.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to fetch configuration:', error);
|
||||
alert('Failed to load configuration. Please check if the server is running and try again.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
createTask();
|
||||
}
|
||||
});
|
||||
|
||||
const historyToggle = document.getElementById('history-toggle');
|
||||
if (historyToggle) {
|
||||
historyToggle.addEventListener('click', () => {
|
||||
const historyPanel = document.getElementById('history-panel');
|
||||
if (historyPanel) {
|
||||
historyPanel.classList.toggle('open');
|
||||
historyToggle.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clearButton = document.getElementById('clear-btn');
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener('click', () => {
|
||||
document.getElementById('prompt-input').value = '';
|
||||
document.getElementById('prompt-input').focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Add keyboard event listener to close modal boxes
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const imageModal = document.getElementById('image-modal');
|
||||
if (imageModal && imageModal.classList.contains('active')) {
|
||||
imageModal.classList.remove('active');
|
||||
}
|
||||
|
||||
const pythonModal = document.getElementById('python-modal');
|
||||
if (pythonModal && pythonModal.classList.contains('active')) {
|
||||
pythonModal.classList.remove('active');
|
||||
}
|
||||
|
||||
const configModal = document.getElementById('config-modal');
|
||||
if (configModal && configModal.classList.contains('active') && !isConfigRequired()) {
|
||||
configModal.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function isConfigRequired() {
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--primary-hover: #0056b3;
|
||||
--success-color: #28a745;
|
||||
--error-color: #dc3545;
|
||||
--warning-color: #ff9800;
|
||||
--info-color: #2196f3;
|
||||
--text-color: #333;
|
||||
--text-light: #666;
|
||||
--bg-color: #f8f9fa;
|
||||
--border-color: #ddd;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
min-width: 0;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.history-panel {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.set-config-panel {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#language-select {
|
||||
flex: 0 0 120px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
background-color: var(--bg-color);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
#language-select:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#language-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
#config-button {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
padding: 12px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#config-button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
margin-top: 10px;
|
||||
max-height: calc(100vh - 160px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.task-container {
|
||||
@extend .card;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
background: white;
|
||||
z-index: 1;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.welcome-message h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
@extend .card;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-container.disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-container.disabled::after {
|
||||
content: "Please configure the system first";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#prompt-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.task-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.task-item.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#input-container.bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.status-running {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.step-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
margin-bottom: 10px;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.step-item .log-line:not(.result) {
|
||||
opacity: 0.7;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.step-item .log-line.result {
|
||||
opacity: 1;
|
||||
color: #333;
|
||||
font-size: 1em;
|
||||
background: #e8f5e9;
|
||||
border-left: 4px solid #4caf50;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.step-item.show {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.log-line.think,
|
||||
.step-item pre.think {
|
||||
background: var(--info-color-light);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
.log-line.tool,
|
||||
.step-item pre.tool {
|
||||
background: var(--warning-color-light);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.log-line.result,
|
||||
.step-item pre.result {
|
||||
background: var(--success-color-light);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
.log-line.error,
|
||||
.step-item pre.error {
|
||||
background: var(--error-color-light);
|
||||
border-left: 4px solid var(--error-color);
|
||||
}
|
||||
|
||||
.log-line.info,
|
||||
.step-item pre.info {
|
||||
background: var(--bg-color);
|
||||
border-left: 4px solid var(--text-light);
|
||||
}
|
||||
|
||||
.log-prefix {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.step-item pre {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-left: 20px;
|
||||
overflow-x: hidden;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
|
||||
&.log {
|
||||
background: var(--bg-color);
|
||||
border-left: 4px solid var(--text-light);
|
||||
}
|
||||
&.think {
|
||||
background: var(--info-color-light);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
&.tool {
|
||||
background: var(--warning-color-light);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
&.result {
|
||||
background: var(--success-color-light);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Step division line style */
|
||||
.step-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* File interaction style */
|
||||
.file-interaction {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.download-link {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.download-link:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.audio-player audio {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.run-button {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
margin-right: 10px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.run-button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* Full screen image modal box */
|
||||
.image-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Python runs simulation modal boxes */
|
||||
.python-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.python-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.python-console {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
background-color: #1e1e1e;
|
||||
color: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.python-output {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
flex-grow: 1;
|
||||
height: 2px;
|
||||
background-color: var(--border-color);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
margin-left: 15px;
|
||||
font-weight: bold;
|
||||
color: var(--text-light);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.step-item strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #007bff;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.step-item div {
|
||||
color: #444;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 15px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ping {
|
||||
color: #ccc;
|
||||
text-align: center;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
padding: 10px;
|
||||
background: #ffe6e6;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.complete {
|
||||
color: #28a745;
|
||||
padding: 10px;
|
||||
background: #e6ffe6;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.complete pre {
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.config-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.config-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.config-modal-content {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--info-color);
|
||||
color: white;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.config-modal-header h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.config-modal-header .close-modal {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.config-modal-header .close-modal:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.config-modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(90vh - 140px);
|
||||
}
|
||||
|
||||
.config-modal-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.config-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.config-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--info-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background-color: var(--info-color);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background-color: var(--text-light);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.config-error {
|
||||
color: var(--error-color);
|
||||
margin-right: 15px;
|
||||
font-weight: bold;
|
||||
padding: 8px 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group.error input {
|
||||
border-color: var(--error-color);
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
|
||||
background-color: rgba(220, 53, 69, 0.05);
|
||||
}
|
||||
|
||||
.form-group.error label {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.form-group .error-message {
|
||||
color: var(--error-color);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
animation: errorAppear 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes errorAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.note-box {
|
||||
background-color: #fff8e1;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.note-box p {
|
||||
margin: 0 0 8px 0;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.note-box p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.required-mark {
|
||||
color: var(--error-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenManus Local Version</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="side-panel">
|
||||
<div class="history-panel">
|
||||
<h2>History Tasks</h2>
|
||||
<div id="task-list" class="task-list"></div>
|
||||
</div>
|
||||
<div class="set-config-panel">
|
||||
<select id="language-select" aria-label="Select language">
|
||||
<option value="en">English</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="ko">한국어</option>
|
||||
<option value="de">Deutsch</option>
|
||||
</select>
|
||||
<input type="button" id="config-button" value="Configuration Setting" title="Configuration Settings">
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-panel">
|
||||
<div id="task-container" class="task-container">
|
||||
<div class="welcome-message">
|
||||
<h1>Welcome to OpenManus Local Version</h1>
|
||||
<p>Please enter a task prompt to start a new task</p>
|
||||
</div>
|
||||
<div id="log-container" class="step-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="input-container" class="input-container">
|
||||
<input
|
||||
type="text"
|
||||
id="prompt-input"
|
||||
placeholder="Enter task prompt..."
|
||||
onkeypress="if(event.keyCode === 13) createTask()"
|
||||
>
|
||||
<button onclick="createTask()">Create Task</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="config-modal" class="config-modal">
|
||||
<div class="config-modal-content">
|
||||
<div class="config-modal-header">
|
||||
<h2>System Configuration</h2>
|
||||
<p>Please fill in the necessary configuration information to continue using the system</p>
|
||||
<span class="close-modal">×</span>
|
||||
</div>
|
||||
|
||||
<div class="config-modal-body">
|
||||
<div class="note-box">
|
||||
<p>⚠️ Please ensure that the following configuration information is correct.</p>
|
||||
<p>If you do not have an API key, please obtain one from the corresponding AI service provider.</p>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h3>LLM Configuration</h3>
|
||||
<div class="form-group">
|
||||
<label for="llm-model">Model Name <span class="required-mark">*</span></label>
|
||||
<input type="text" id="llm-model" name="llm.model" placeholder="For example: claude-3-5-sonnet">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llm-base-url">API Base URL <span class="required-mark">*</span></label>
|
||||
<input type="text" id="llm-base-url" name="llm.base_url" placeholder="For example: https://api.openai.com/v1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llm-api-key">API Key <span class="required-mark">*</span></label>
|
||||
<input type="password" id="llm-api-key" name="llm.api_key" placeholder="Your API key, for example: sk-...">
|
||||
<span class="field-help">Must be your own valid API key, not the placeholder in the example</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llm-max-tokens">Max Tokens</label>
|
||||
<input type="number" id="llm-max-tokens" name="llm.max_tokens" placeholder="For example: 4096">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llm-temperature">Temperature</label>
|
||||
<input type="number" id="llm-temperature" name="llm.temperature" step="0.1" placeholder="For example: 0.0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<h3>Server Configuration</h3>
|
||||
<div class="form-group">
|
||||
<label for="server-host">Host <span class="required-mark">*</span></label>
|
||||
<input type="text" id="server-host" name="server.host" placeholder="For example: localhost">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="server-port">Port <span class="required-mark">*</span></label>
|
||||
<input type="number" id="server-port" name="server.port" placeholder="For example: 5172">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-modal-footer">
|
||||
<p id="config-error" class="config-error"></p>
|
||||
<div class="config-actions">
|
||||
<button id="cancel-config-btn" class="secondary-btn">Cancel</button>
|
||||
<button id="save-config-btn" class="primary-btn">Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/main.js"></script>
|
||||
<script src="/static/i18n.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user