Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13f98a0fde | |||
| 487b44fda8 | |||
| 737abe4f90 | |||
| cbef113736 | |||
| dc8ed530f0 | |||
| 62cfcb182e | |||
| 111a2bc6b1 | |||
| 61d705f6e6 | |||
| cafd4a18da | |||
| b67d1b90e0 | |||
| 6a89ae8256 | |||
| 4d36a4c3e2 | |||
| fed6fe1421 | |||
| 3a958944a0 | |||
| 8d18c05350 | |||
| eeefddf6bf | |||
| 35de1f9e15 | |||
| 2c0b2d1fb3 | |||
| 211d0694e3 | |||
| 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,32 @@
|
||||
# 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 [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
|
||||
|
||||
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 +47,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 +106,7 @@ api_key = "sk-..." # Replace with your actual API key
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
One line for run OpenManus:
|
||||
|
||||
```bash
|
||||
@@ -77,19 +122,39 @@ 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!
|
||||
|
||||
Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT) and [OpenHands](https://github.com/All-Hands-AI/OpenHands).
|
||||
|
||||
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}},
|
||||
}
|
||||
```
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
[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) 为本项目提供的基础支持!
|
||||
|
||||
此外,我们感谢 [AAAJ](https://github.com/metauto-ai/agent-as-a-judge),[MetaGPT](https://github.com/geekan/MetaGPT) 和 [OpenHands](https://github.com/All-Hands-AI/OpenHands).
|
||||
|
||||
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
|
||||
+3
-1
@@ -4,7 +4,7 @@ from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from app.llm import LLM
|
||||
from app.llm.inference import LLM
|
||||
from app.logger import logger
|
||||
from app.schema import AgentState, Memory, Message
|
||||
|
||||
@@ -144,6 +144,8 @@ class BaseAgent(BaseModel, ABC):
|
||||
results.append(f"Step {self.current_step}: {step_result}")
|
||||
|
||||
if self.current_step >= self.max_steps:
|
||||
self.current_step = 0 # setting back to 0 when reached max steps
|
||||
self.state = AgentState.IDLE # setting the status
|
||||
results.append(f"Terminated: Reached max steps ({self.max_steps})")
|
||||
|
||||
return "\n".join(results) if results else "No steps executed"
|
||||
|
||||
@@ -32,3 +32,5 @@ class Manus(ToolCallAgent):
|
||||
PythonExecute(), GoogleSearch(), BrowserUseTool(), FileSaver(), Terminate()
|
||||
)
|
||||
)
|
||||
|
||||
max_steps: int = 20
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from pydantic import Field
|
||||
|
||||
from app.agent.base import BaseAgent
|
||||
from app.llm import LLM
|
||||
from app.llm.inference import LLM
|
||||
from app.schema import AgentState, Memory
|
||||
|
||||
|
||||
|
||||
@@ -154,7 +154,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: "[ ]",
|
||||
}
|
||||
|
||||
+26
-31
@@ -5,8 +5,8 @@ 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.llm import LLM
|
||||
from app.flow.base import BaseFlow, PlanStepStatus
|
||||
from app.llm.inference import LLM
|
||||
from app.logger import logger
|
||||
from app.schema import AgentState, Message
|
||||
from app.tool import PlanningTool
|
||||
@@ -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:
|
||||
|
||||
-254
@@ -1,254 +0,0 @@
|
||||
from typing import Dict, List, Literal, Optional, Union
|
||||
|
||||
from openai import (
|
||||
APIError,
|
||||
AsyncOpenAI,
|
||||
AuthenticationError,
|
||||
OpenAIError,
|
||||
RateLimitError,
|
||||
)
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
|
||||
from app.config import LLMSettings, config
|
||||
from app.logger import logger # Assuming a logger is set up in your app
|
||||
from app.schema import Message
|
||||
|
||||
|
||||
class LLM:
|
||||
_instances: Dict[str, "LLM"] = {}
|
||||
|
||||
def __new__(
|
||||
cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None
|
||||
):
|
||||
if config_name not in cls._instances:
|
||||
instance = super().__new__(cls)
|
||||
instance.__init__(config_name, llm_config)
|
||||
cls._instances[config_name] = instance
|
||||
return cls._instances[config_name]
|
||||
|
||||
def __init__(
|
||||
self, config_name: str = "default", llm_config: Optional[LLMSettings] = None
|
||||
):
|
||||
if not hasattr(self, "client"): # Only initialize if not already initialized
|
||||
llm_config = llm_config or config.llm
|
||||
llm_config = llm_config.get(config_name, llm_config["default"])
|
||||
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
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_messages(messages: List[Union[dict, Message]]) -> List[dict]:
|
||||
"""
|
||||
Format messages for LLM by converting them to OpenAI message format.
|
||||
|
||||
Args:
|
||||
messages: List of messages that can be either dict or Message objects
|
||||
|
||||
Returns:
|
||||
List[dict]: List of formatted messages in OpenAI format
|
||||
|
||||
Raises:
|
||||
ValueError: If messages are invalid or missing required fields
|
||||
TypeError: If unsupported message types are provided
|
||||
|
||||
Examples:
|
||||
>>> msgs = [
|
||||
... Message.system_message("You are a helpful assistant"),
|
||||
... {"role": "user", "content": "Hello"},
|
||||
... Message.user_message("How are you?")
|
||||
... ]
|
||||
>>> formatted = LLM.format_messages(msgs)
|
||||
"""
|
||||
formatted_messages = []
|
||||
|
||||
for message in messages:
|
||||
if isinstance(message, dict):
|
||||
# If message is already a dict, ensure it has required fields
|
||||
if "role" not in message:
|
||||
raise ValueError("Message dict must contain 'role' field")
|
||||
formatted_messages.append(message)
|
||||
elif isinstance(message, Message):
|
||||
# If message is a Message object, convert it to dict
|
||||
formatted_messages.append(message.to_dict())
|
||||
else:
|
||||
raise TypeError(f"Unsupported message type: {type(message)}")
|
||||
|
||||
# Validate all messages have required fields
|
||||
for msg in formatted_messages:
|
||||
if msg["role"] not in ["system", "user", "assistant", "tool"]:
|
||||
raise ValueError(f"Invalid role: {msg['role']}")
|
||||
if "content" not in msg and "tool_calls" not in msg:
|
||||
raise ValueError(
|
||||
"Message must contain either 'content' or 'tool_calls'"
|
||||
)
|
||||
|
||||
return formatted_messages
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=60),
|
||||
stop=stop_after_attempt(6),
|
||||
)
|
||||
async def ask(
|
||||
self,
|
||||
messages: List[Union[dict, Message]],
|
||||
system_msgs: Optional[List[Union[dict, Message]]] = None,
|
||||
stream: bool = True,
|
||||
temperature: Optional[float] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Send a prompt to the LLM and get the response.
|
||||
|
||||
Args:
|
||||
messages: List of conversation messages
|
||||
system_msgs: Optional system messages to prepend
|
||||
stream (bool): Whether to stream the response
|
||||
temperature (float): Sampling temperature for the response
|
||||
|
||||
Returns:
|
||||
str: The generated response
|
||||
|
||||
Raises:
|
||||
ValueError: If messages are invalid or response is empty
|
||||
OpenAIError: If API call fails after retries
|
||||
Exception: For unexpected errors
|
||||
"""
|
||||
try:
|
||||
# Format system and user messages
|
||||
if system_msgs:
|
||||
system_msgs = self.format_messages(system_msgs)
|
||||
messages = system_msgs + self.format_messages(messages)
|
||||
else:
|
||||
messages = self.format_messages(messages)
|
||||
|
||||
if not stream:
|
||||
# Non-streaming request
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=temperature or self.temperature,
|
||||
stream=False,
|
||||
)
|
||||
if not response.choices or not response.choices[0].message.content:
|
||||
raise ValueError("Empty or invalid response from LLM")
|
||||
return response.choices[0].message.content
|
||||
|
||||
# Streaming request
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=temperature or self.temperature,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
collected_messages = []
|
||||
async for chunk in response:
|
||||
chunk_message = chunk.choices[0].delta.content or ""
|
||||
collected_messages.append(chunk_message)
|
||||
print(chunk_message, end="", flush=True)
|
||||
|
||||
print() # Newline after streaming
|
||||
full_response = "".join(collected_messages).strip()
|
||||
if not full_response:
|
||||
raise ValueError("Empty response from streaming LLM")
|
||||
return full_response
|
||||
|
||||
except ValueError as ve:
|
||||
logger.error(f"Validation error: {ve}")
|
||||
raise
|
||||
except OpenAIError as oe:
|
||||
logger.error(f"OpenAI API error: {oe}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in ask: {e}")
|
||||
raise
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=60),
|
||||
stop=stop_after_attempt(6),
|
||||
)
|
||||
async def ask_tool(
|
||||
self,
|
||||
messages: List[Union[dict, Message]],
|
||||
system_msgs: Optional[List[Union[dict, Message]]] = None,
|
||||
timeout: int = 60,
|
||||
tools: Optional[List[dict]] = None,
|
||||
tool_choice: Literal["none", "auto", "required"] = "auto",
|
||||
temperature: Optional[float] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Ask LLM using functions/tools and return the response.
|
||||
|
||||
Args:
|
||||
messages: List of conversation messages
|
||||
system_msgs: Optional system messages to prepend
|
||||
timeout: Request timeout in seconds
|
||||
tools: List of tools to use
|
||||
tool_choice: Tool choice strategy
|
||||
temperature: Sampling temperature for the response
|
||||
**kwargs: Additional completion arguments
|
||||
|
||||
Returns:
|
||||
ChatCompletionMessage: The model's response
|
||||
|
||||
Raises:
|
||||
ValueError: If tools, tool_choice, or messages are invalid
|
||||
OpenAIError: If API call fails after retries
|
||||
Exception: For unexpected errors
|
||||
"""
|
||||
try:
|
||||
# Validate tool_choice
|
||||
if tool_choice not in ["none", "auto", "required"]:
|
||||
raise ValueError(f"Invalid tool_choice: {tool_choice}")
|
||||
|
||||
# Format messages
|
||||
if system_msgs:
|
||||
system_msgs = self.format_messages(system_msgs)
|
||||
messages = system_msgs + self.format_messages(messages)
|
||||
else:
|
||||
messages = self.format_messages(messages)
|
||||
|
||||
# Validate tools if provided
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict) or "type" not in tool:
|
||||
raise ValueError("Each tool must be a dict with 'type' field")
|
||||
|
||||
# Set up the completion request
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=temperature or self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Check if response is valid
|
||||
if not response.choices or not response.choices[0].message:
|
||||
print(response)
|
||||
raise ValueError("Invalid or empty response from LLM")
|
||||
|
||||
return response.choices[0].message
|
||||
|
||||
except ValueError as ve:
|
||||
logger.error(f"Validation error in ask_tool: {ve}")
|
||||
raise
|
||||
except OpenAIError as oe:
|
||||
if isinstance(oe, AuthenticationError):
|
||||
logger.error("Authentication failed. Check API key.")
|
||||
elif isinstance(oe, RateLimitError):
|
||||
logger.error("Rate limit exceeded. Consider increasing retry attempts.")
|
||||
elif isinstance(oe, APIError):
|
||||
logger.error(f"API error: {oe}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in ask_tool: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class Cost:
|
||||
"""
|
||||
Cost class can record various costs during running and evaluation.
|
||||
Currently we define the following costs:
|
||||
accumulated_cost: the total cost (USD $) of the current LLM.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._accumulated_cost: float = 0.0
|
||||
self._costs: list[float] = []
|
||||
|
||||
@property
|
||||
def accumulated_cost(self) -> float:
|
||||
return self._accumulated_cost
|
||||
|
||||
@accumulated_cost.setter
|
||||
def accumulated_cost(self, value: float) -> None:
|
||||
if value < 0:
|
||||
raise ValueError("Total cost cannot be negative.")
|
||||
self._accumulated_cost = value
|
||||
|
||||
@property
|
||||
def costs(self) -> list:
|
||||
return self._costs
|
||||
|
||||
def add_cost(self, value: float) -> None:
|
||||
if value < 0:
|
||||
raise ValueError("Added cost cannot be negative.")
|
||||
self._accumulated_cost += value
|
||||
self._costs.append(value)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Return the costs in a dictionary.
|
||||
"""
|
||||
return {"accumulated_cost": self._accumulated_cost, "costs": self._costs}
|
||||
|
||||
def log(self):
|
||||
"""
|
||||
Log the costs.
|
||||
"""
|
||||
cost = self.get()
|
||||
logs = ""
|
||||
for key, value in cost.items():
|
||||
logs += f"{key}: {value}\n"
|
||||
return logs
|
||||
@@ -0,0 +1,525 @@
|
||||
import base64
|
||||
import os
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
import litellm
|
||||
from litellm import completion, completion_cost
|
||||
from litellm.exceptions import (
|
||||
APIConnectionError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError,
|
||||
)
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
from app.config import LLMSettings, config
|
||||
from app.llm.cost import Cost
|
||||
from app.logger import logger
|
||||
from app.schema import Message
|
||||
|
||||
|
||||
class LLM:
|
||||
_instances: Dict[str, "LLM"] = {}
|
||||
|
||||
def __new__(
|
||||
cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None
|
||||
):
|
||||
if config_name not in cls._instances:
|
||||
instance = super().__new__(cls)
|
||||
instance.__init__(config_name, llm_config)
|
||||
cls._instances[config_name] = instance
|
||||
return cls._instances[config_name]
|
||||
|
||||
def __init__(
|
||||
self, config_name: str = "default", llm_config: Optional[LLMSettings] = None
|
||||
):
|
||||
if not hasattr(
|
||||
self, "initialized"
|
||||
): # Only initialize if not already initialized
|
||||
llm_config = llm_config or config.llm
|
||||
llm_config = llm_config.get(config_name, llm_config["default"])
|
||||
|
||||
self.model = getattr(llm_config, "model", "gpt-3.5-turbo")
|
||||
self.max_tokens = getattr(llm_config, "max_tokens", 4096)
|
||||
self.temperature = getattr(llm_config, "temperature", 0.7)
|
||||
self.top_p = getattr(llm_config, "top_p", 0.9)
|
||||
self.api_type = getattr(llm_config, "api_type", "openai")
|
||||
self.api_key = getattr(
|
||||
llm_config, "api_key", os.environ.get("OPENAI_API_KEY", "")
|
||||
)
|
||||
self.api_version = getattr(llm_config, "api_version", "")
|
||||
self.base_url = getattr(llm_config, "base_url", "https://api.openai.com/v1")
|
||||
self.timeout = getattr(llm_config, "timeout", 60)
|
||||
self.num_retries = getattr(llm_config, "num_retries", 3)
|
||||
self.retry_min_wait = getattr(llm_config, "retry_min_wait", 1)
|
||||
self.retry_max_wait = getattr(llm_config, "retry_max_wait", 10)
|
||||
self.custom_llm_provider = getattr(llm_config, "custom_llm_provider", None)
|
||||
|
||||
# Get model info if available
|
||||
self.model_info = None
|
||||
try:
|
||||
self.model_info = litellm.get_model_info(self.model)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get model info for {self.model}: {e}")
|
||||
|
||||
# Configure litellm
|
||||
if self.api_type == "azure":
|
||||
litellm.api_base = self.base_url
|
||||
litellm.api_key = self.api_key
|
||||
litellm.api_version = self.api_version
|
||||
else:
|
||||
litellm.api_key = self.api_key
|
||||
if self.base_url:
|
||||
litellm.api_base = self.base_url
|
||||
|
||||
# Initialize cost tracker
|
||||
self.cost_tracker = Cost()
|
||||
self.initialized = True
|
||||
|
||||
# Initialize completion function
|
||||
self._initialize_completion_function()
|
||||
|
||||
def _initialize_completion_function(self):
|
||||
"""Initialize the completion function with retry logic"""
|
||||
|
||||
def attempt_on_error(retry_state):
|
||||
logger.error(
|
||||
f"{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number}"
|
||||
)
|
||||
return True
|
||||
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_attempt(self.num_retries),
|
||||
wait=wait_random_exponential(
|
||||
min=self.retry_min_wait, max=self.retry_max_wait
|
||||
),
|
||||
retry=retry_if_exception_type(
|
||||
(RateLimitError, APIConnectionError, ServiceUnavailableError)
|
||||
),
|
||||
after=attempt_on_error,
|
||||
)
|
||||
def wrapper(*args, **kwargs):
|
||||
model_name = self.model
|
||||
if self.api_type == "azure":
|
||||
model_name = f"azure/{self.model}"
|
||||
|
||||
# Set default parameters if not provided
|
||||
if "max_tokens" not in kwargs:
|
||||
kwargs["max_tokens"] = self.max_tokens
|
||||
if "temperature" not in kwargs:
|
||||
kwargs["temperature"] = self.temperature
|
||||
if "top_p" not in kwargs:
|
||||
kwargs["top_p"] = self.top_p
|
||||
if "timeout" not in kwargs:
|
||||
kwargs["timeout"] = self.timeout
|
||||
|
||||
kwargs["model"] = model_name
|
||||
|
||||
# Add API credentials if not in kwargs
|
||||
if "api_key" not in kwargs:
|
||||
kwargs["api_key"] = self.api_key
|
||||
if "base_url" not in kwargs and self.base_url:
|
||||
kwargs["base_url"] = self.base_url
|
||||
if "api_version" not in kwargs and self.api_version:
|
||||
kwargs["api_version"] = self.api_version
|
||||
if "custom_llm_provider" not in kwargs and self.custom_llm_provider:
|
||||
kwargs["custom_llm_provider"] = self.custom_llm_provider
|
||||
|
||||
resp = completion(**kwargs)
|
||||
return resp
|
||||
|
||||
self._completion = wrapper
|
||||
|
||||
@staticmethod
|
||||
def format_messages(messages: List[Union[dict, Message]]) -> List[dict]:
|
||||
"""
|
||||
Format messages for LLM by converting them to OpenAI message format.
|
||||
|
||||
Args:
|
||||
messages: List of messages that can be either dict or Message objects
|
||||
|
||||
Returns:
|
||||
List[dict]: List of formatted messages in OpenAI format
|
||||
|
||||
Raises:
|
||||
ValueError: If messages are invalid or missing required fields
|
||||
TypeError: If unsupported message types are provided
|
||||
"""
|
||||
formatted_messages = []
|
||||
|
||||
for message in messages:
|
||||
if isinstance(message, dict):
|
||||
# If message is already a dict, ensure it has required fields
|
||||
if "role" not in message:
|
||||
raise ValueError("Message dict must contain 'role' field")
|
||||
formatted_messages.append(message)
|
||||
elif isinstance(message, Message):
|
||||
# If message is a Message object, convert it to dict
|
||||
formatted_messages.append(message.to_dict())
|
||||
else:
|
||||
raise TypeError(f"Unsupported message type: {type(message)}")
|
||||
|
||||
# Validate all messages have required fields
|
||||
for msg in formatted_messages:
|
||||
if msg["role"] not in ["system", "user", "assistant", "tool"]:
|
||||
raise ValueError(f"Invalid role: {msg['role']}")
|
||||
if "content" not in msg and "tool_calls" not in msg:
|
||||
raise ValueError(
|
||||
"Message must contain either 'content' or 'tool_calls'"
|
||||
)
|
||||
|
||||
return formatted_messages
|
||||
|
||||
def _calculate_and_track_cost(self, response) -> float:
|
||||
"""
|
||||
Calculate and track the cost of an LLM API call.
|
||||
|
||||
Args:
|
||||
response: The response from litellm
|
||||
|
||||
Returns:
|
||||
float: The calculated cost
|
||||
"""
|
||||
try:
|
||||
# Use litellm's completion_cost function
|
||||
cost = completion_cost(completion_response=response)
|
||||
|
||||
# Add the cost to our tracker
|
||||
if cost > 0:
|
||||
self.cost_tracker.add_cost(cost)
|
||||
logger.info(
|
||||
f"Added cost: ${cost:.6f}, Total: ${self.cost_tracker.accumulated_cost:.6f}"
|
||||
)
|
||||
|
||||
return cost
|
||||
except Exception as e:
|
||||
logger.warning(f"Cost calculation failed: {e}")
|
||||
return 0.0
|
||||
|
||||
def is_local(self) -> bool:
|
||||
"""
|
||||
Check if the model is running locally.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is running locally, False otherwise
|
||||
"""
|
||||
if self.base_url:
|
||||
return any(
|
||||
substring in self.base_url
|
||||
for substring in ["localhost", "127.0.0.1", "0.0.0.0"]
|
||||
)
|
||||
if self.model and (
|
||||
self.model.startswith("ollama") or "local" in self.model.lower()
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def do_completion(self, *args, **kwargs) -> Tuple[Any, float, float]:
|
||||
"""
|
||||
Perform a completion request and track cost.
|
||||
|
||||
Returns:
|
||||
Tuple[Any, float, float]: (response, current_cost, accumulated_cost)
|
||||
"""
|
||||
response = self._completion(*args, **kwargs)
|
||||
|
||||
# Calculate and track cost
|
||||
current_cost = self._calculate_and_track_cost(response)
|
||||
|
||||
return response, current_cost, self.cost_tracker.accumulated_cost
|
||||
|
||||
@staticmethod
|
||||
def encode_image(image_path: str) -> str:
|
||||
"""
|
||||
Encode an image to base64.
|
||||
|
||||
Args:
|
||||
image_path: Path to the image file
|
||||
|
||||
Returns:
|
||||
str: Base64-encoded image
|
||||
"""
|
||||
with open(image_path, "rb") as image_file:
|
||||
return base64.b64encode(image_file.read()).decode("utf-8")
|
||||
|
||||
def prepare_messages(
|
||||
self, text: str, image_path: Optional[str] = None
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Prepare messages for completion, including multimodal content if needed.
|
||||
|
||||
Args:
|
||||
text: Text content
|
||||
image_path: Optional path to an image file
|
||||
|
||||
Returns:
|
||||
List[dict]: Formatted messages
|
||||
"""
|
||||
messages = [{"role": "user", "content": text}]
|
||||
if image_path:
|
||||
base64_image = self.encode_image(image_path)
|
||||
messages[0]["content"] = [
|
||||
{"type": "text", "text": text},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
|
||||
},
|
||||
]
|
||||
return messages
|
||||
|
||||
def do_multimodal_completion(
|
||||
self, text: str, image_path: str
|
||||
) -> Tuple[Any, float, float]:
|
||||
"""
|
||||
Perform a multimodal completion with text and image.
|
||||
|
||||
Args:
|
||||
text: Text prompt
|
||||
image_path: Path to the image file
|
||||
|
||||
Returns:
|
||||
Tuple[Any, float, float]: (response, current_cost, accumulated_cost)
|
||||
"""
|
||||
messages = self.prepare_messages(text, image_path=image_path)
|
||||
return self.do_completion(messages=messages)
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=60),
|
||||
stop=stop_after_attempt(6),
|
||||
)
|
||||
async def ask(
|
||||
self,
|
||||
messages: List[Union[dict, Message]],
|
||||
system_msgs: Optional[List[Union[dict, Message]]] = None,
|
||||
stream: bool = True,
|
||||
temperature: Optional[float] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Send a prompt to the LLM and get the response.
|
||||
|
||||
Args:
|
||||
messages: List of conversation messages
|
||||
system_msgs: Optional system messages to prepend
|
||||
stream (bool): Whether to stream the response
|
||||
temperature (float): Sampling temperature for the response
|
||||
|
||||
Returns:
|
||||
str: The generated response
|
||||
|
||||
Raises:
|
||||
ValueError: If messages are invalid or response is empty
|
||||
Exception: For unexpected errors
|
||||
"""
|
||||
try:
|
||||
# Format system and user messages
|
||||
if system_msgs:
|
||||
system_msgs = self.format_messages(system_msgs)
|
||||
messages = system_msgs + self.format_messages(messages)
|
||||
else:
|
||||
messages = self.format_messages(messages)
|
||||
|
||||
model_name = self.model
|
||||
if self.api_type == "azure":
|
||||
# For Azure, litellm expects model name in format: azure/<deployment_name>
|
||||
model_name = f"azure/{self.model}"
|
||||
|
||||
if not stream:
|
||||
# Non-streaming request
|
||||
response = await litellm.acompletion(
|
||||
model=model_name,
|
||||
messages=messages,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=temperature or self.temperature,
|
||||
stream=False,
|
||||
)
|
||||
|
||||
# Calculate and track cost
|
||||
self._calculate_and_track_cost(response)
|
||||
|
||||
if not response.choices or not response.choices[0].message.content:
|
||||
raise ValueError("Empty or invalid response from LLM")
|
||||
return response.choices[0].message.content
|
||||
|
||||
# Streaming request
|
||||
collected_messages = []
|
||||
async for chunk in await litellm.acompletion(
|
||||
model=model_name,
|
||||
messages=messages,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=temperature or self.temperature,
|
||||
stream=True,
|
||||
):
|
||||
chunk_message = chunk.choices[0].delta.content or ""
|
||||
collected_messages.append(chunk_message)
|
||||
print(chunk_message, end="", flush=True)
|
||||
|
||||
# For streaming responses, cost is calculated on the last chunk
|
||||
if hasattr(chunk, "usage") and chunk.usage:
|
||||
self._calculate_and_track_cost(chunk)
|
||||
|
||||
print() # Newline after streaming
|
||||
full_response = "".join(collected_messages).strip()
|
||||
if not full_response:
|
||||
raise ValueError("Empty response from streaming LLM")
|
||||
return full_response
|
||||
|
||||
except ValueError as ve:
|
||||
logger.error(f"Validation error: {ve}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in ask: {e}")
|
||||
raise
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=60),
|
||||
stop=stop_after_attempt(6),
|
||||
)
|
||||
async def ask_tool(
|
||||
self,
|
||||
messages: List[Union[dict, Message]],
|
||||
system_msgs: Optional[List[Union[dict, Message]]] = None,
|
||||
timeout: int = 60,
|
||||
tools: Optional[List[dict]] = None,
|
||||
tool_choice: Literal["none", "auto", "required"] = "auto",
|
||||
temperature: Optional[float] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Ask LLM using functions/tools and return the response.
|
||||
|
||||
Args:
|
||||
messages: List of conversation messages
|
||||
system_msgs: Optional system messages to prepend
|
||||
timeout: Request timeout in seconds
|
||||
tools: List of tools to use
|
||||
tool_choice: Tool choice strategy
|
||||
temperature: Sampling temperature for the response
|
||||
**kwargs: Additional completion arguments
|
||||
|
||||
Returns:
|
||||
The model's response
|
||||
|
||||
Raises:
|
||||
ValueError: If tools, tool_choice, or messages are invalid
|
||||
Exception: For unexpected errors
|
||||
"""
|
||||
try:
|
||||
# Validate tool_choice
|
||||
if tool_choice not in ["none", "auto", "required"]:
|
||||
raise ValueError(f"Invalid tool_choice: {tool_choice}")
|
||||
|
||||
# Format messages
|
||||
if system_msgs:
|
||||
system_msgs = self.format_messages(system_msgs)
|
||||
messages = system_msgs + self.format_messages(messages)
|
||||
else:
|
||||
messages = self.format_messages(messages)
|
||||
|
||||
# Validate tools if provided
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict) or "type" not in tool:
|
||||
raise ValueError("Each tool must be a dict with 'type' field")
|
||||
|
||||
model_name = self.model
|
||||
if self.api_type == "azure":
|
||||
# For Azure, litellm expects model name in format: azure/<deployment_name>
|
||||
model_name = f"azure/{self.model}"
|
||||
|
||||
# Set up the completion request
|
||||
response = await litellm.acompletion(
|
||||
model=model_name,
|
||||
messages=messages,
|
||||
temperature=temperature or self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Calculate and track cost
|
||||
self._calculate_and_track_cost(response)
|
||||
|
||||
# Check if response is valid
|
||||
if not response.choices or not response.choices[0].message:
|
||||
print(response)
|
||||
raise ValueError("Invalid or empty response from LLM")
|
||||
|
||||
return response.choices[0].message
|
||||
|
||||
except ValueError as ve:
|
||||
logger.error(f"Validation error: {ve}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in ask_tool: {e}")
|
||||
raise
|
||||
|
||||
def get_cost(self):
|
||||
"""
|
||||
Get the current cost information.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing accumulated cost and individual costs
|
||||
"""
|
||||
return self.cost_tracker.get()
|
||||
|
||||
def log_cost(self):
|
||||
"""
|
||||
Log the current cost information.
|
||||
|
||||
Returns:
|
||||
str: Formatted string of cost information
|
||||
"""
|
||||
return self.cost_tracker.log()
|
||||
|
||||
def get_token_count(self, messages):
|
||||
"""
|
||||
Get the token count for a list of messages.
|
||||
|
||||
Args:
|
||||
messages: List of messages
|
||||
|
||||
Returns:
|
||||
int: Token count
|
||||
"""
|
||||
return litellm.token_counter(model=self.model, messages=messages)
|
||||
|
||||
def __str__(self):
|
||||
return f"LLM(model={self.model}, base_url={self.base_url})"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Load environment variables if needed
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Create LLM instance
|
||||
llm = LLM()
|
||||
|
||||
# Test text completion
|
||||
messages = llm.prepare_messages("Hello, how are you?")
|
||||
response, cost, total_cost = llm.do_completion(messages=messages)
|
||||
print(f"Response: {response['choices'][0]['message']['content']}")
|
||||
print(f"Cost: ${cost:.6f}, Total cost: ${total_cost:.6f}")
|
||||
|
||||
# Test multimodal if image path is available
|
||||
image_path = os.getenv("TEST_IMAGE_PATH")
|
||||
if image_path and os.path.exists(image_path):
|
||||
multimodal_response, mm_cost, mm_total_cost = llm.do_multimodal_completion(
|
||||
"What's in this image?", image_path
|
||||
)
|
||||
print(
|
||||
f"Multimodal response: {multimodal_response['choices'][0]['message']['content']}"
|
||||
)
|
||||
print(f"Cost: ${mm_cost:.6f}, Total cost: ${mm_total_cost:.6f}")
|
||||
+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
|
||||
|
||||
@@ -10,5 +10,9 @@ BrowserUseTool: Open, browse, and use web browsers.If you open a local HTML file
|
||||
|
||||
GoogleSearch: Perform web information retrieval
|
||||
|
||||
Terminate: End the current interaction when the task is complete or when you need additional information from the user. Use this tool to signal that you've finished addressing the user's request or need clarification before proceeding further.
|
||||
|
||||
Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps.
|
||||
|
||||
Always maintain a helpful, informative tone throughout the interaction. If you encounter any limitations or need more details, clearly communicate this to the user before terminating.
|
||||
"""
|
||||
|
||||
+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.
|
||||
"""
|
||||
|
||||
@@ -12,6 +12,8 @@ from pydantic_core.core_schema import ValidationInfo
|
||||
from app.tool.base import BaseTool, ToolResult
|
||||
|
||||
|
||||
MAX_LENGTH = 2000
|
||||
|
||||
_BROWSER_DESCRIPTION = """
|
||||
Interact with a web browser to perform various actions such as navigation, element interaction,
|
||||
content extraction, and tab management. Supported actions include:
|
||||
@@ -20,6 +22,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 +47,7 @@ class BrowserUseTool(BaseTool):
|
||||
"input_text",
|
||||
"screenshot",
|
||||
"get_html",
|
||||
"get_text",
|
||||
"execute_js",
|
||||
"scroll",
|
||||
"switch_tab",
|
||||
@@ -177,9 +182,21 @@ class BrowserUseTool(BaseTool):
|
||||
|
||||
elif action == "get_html":
|
||||
html = await context.get_page_html()
|
||||
truncated = html[:2000] + "..." if len(html) > 2000 else html
|
||||
truncated = (
|
||||
html[:MAX_LENGTH] + "..." if len(html) > MAX_LENGTH 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)}"
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -1,13 +1,22 @@
|
||||
# Global LLM configuration
|
||||
[llm]
|
||||
model = "claude-3-5-sonnet"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
model = "gpt-4o" #"claude-3-5-sonnet"
|
||||
base_url = "https://api.openai.com/v1" # "https://api.anthropic.com"
|
||||
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"
|
||||
model = "gpt-4o" # "claude-3-5-sonnet"
|
||||
base_url = "https://api.openai.com/v1" # "https://api.anthropic.com"
|
||||
api_key = "sk-..."
|
||||
|
||||
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:
|
||||
|
||||
+4
-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,6 @@ 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
|
||||
litellm~=1.63.6
|
||||
|
||||
+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",
|
||||
],
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user